Skip to content

Commit 358795b

Browse files
AYastrebovclaude
andcommitted
Add traffic stats callback to FFI (iOS + Android)
Wire cumulative upload/download byte counters through the TUN server. TCP bytes are counted after copy_bidirectional completes; UDP bytes are counted per-message in destination tasks. A 1-second timer in the TUN event loop invokes the platform callback with current totals. iOS: shoes_start now takes a ShoesTrafficCallback parameter. Android: ShoesNative.start now takes a TrafficListener parameter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dba023c commit 358795b

7 files changed

Lines changed: 256 additions & 11 deletions

File tree

android/src/main/java/com/shoesproxy/ShoesNative.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ package com.shoesproxy
2525
* device_fd: $tunFd
2626
* """.trimIndent()
2727
*
28-
* shoesHandle = ShoesNative.start(config) { fd -> protect(fd) }
28+
* shoesHandle = ShoesNative.start(config, { fd -> protect(fd) }) { up, down ->
29+
* Log.d("VPN", "Traffic: up=$up down=$down")
30+
* }
2931
* return START_STICKY
3032
* }
3133
*
@@ -59,6 +61,22 @@ object ShoesNative {
5961
fun protect(fd: Int): Boolean
6062
}
6163

64+
/**
65+
* Functional interface for receiving traffic statistics.
66+
*
67+
* Called periodically (~1 second) from the native engine with cumulative
68+
* byte counts since the last [start] call.
69+
*/
70+
fun interface TrafficListener {
71+
/**
72+
* Called with updated traffic statistics.
73+
*
74+
* @param uploadBytes Total bytes sent from device to proxy since start.
75+
* @param downloadBytes Total bytes received from proxy to device since start.
76+
*/
77+
fun onTrafficUpdate(uploadBytes: Long, downloadBytes: Long)
78+
}
79+
6280
/**
6381
* Initialize the shoes library.
6482
*
@@ -95,9 +113,14 @@ object ShoesNative {
95113
* @param configYaml YAML configuration string.
96114
* @param protectCallback Called by the engine to exempt outbound sockets
97115
* from VPN routing (pass `this::protect` from your VpnService).
116+
* @param trafficCallback Called periodically with cumulative traffic byte counts.
98117
* @return A positive handle on success, -1 on error.
99118
*/
100-
external fun start(configYaml: String, protectCallback: SocketProtector): Long
119+
external fun start(
120+
configYaml: String,
121+
protectCallback: SocketProtector,
122+
trafficCallback: TrafficListener,
123+
): Long
101124

102125
/**
103126
* Stop the VPN service.

include/shoes.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@
1919
*/
2020
typedef bool (*ProtectSocketCallback)(int fd);
2121

22+
/**
23+
* Traffic statistics callback type.
24+
* Called periodically (every ~1 second) with cumulative byte counts.
25+
*
26+
* @param upload_bytes Total bytes sent from device to proxy since start.
27+
* @param download_bytes Total bytes received from proxy to device since start.
28+
*/
29+
typedef void (*ShoesTrafficCallback)(uint64_t upload_bytes, uint64_t download_bytes);
30+
2231
/**
2332
* Initialize the shoes library.
2433
*
@@ -40,6 +49,7 @@ int shoes_init(const char *log_level);
4049
* # Arguments
4150
* * `config_yaml` - YAML configuration string (must include device_fd in TUN config)
4251
* * `protect_callback` - Callback function to protect sockets from VPN routing
52+
* * `traffic_callback` - Callback function for periodic traffic statistics
4353
*
4454
* # Returns
4555
* * Handle (> 0) on success
@@ -48,7 +58,9 @@ int shoes_init(const char *log_level);
4858
* # Safety
4959
* `config_yaml` must be a valid null-terminated C string.
5060
*/
51-
long shoes_start(const char *config_yaml, ProtectSocketCallback protect_callback);
61+
long shoes_start(const char *config_yaml,
62+
ProtectSocketCallback protect_callback,
63+
ShoesTrafficCallback traffic_callback);
5264

5365
/**
5466
* Stop the shoes VPN service.

src/ffi/android.rs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,21 +179,28 @@ pub extern "system" fn Java_com_shoesproxy_ShoesNative_start<'local>(
179179
_class: JClass<'local>,
180180
config_yaml: JString<'local>,
181181
protect_callback: JObject<'local>,
182+
traffic_callback: JObject<'local>,
182183
) -> jlong {
183184
info!("Starting shoes service");
184185

185186
let result = unowned
186187
.with_env(
187-
|env| -> jni::errors::Result<(String, Global<JObject<'static>>, jni::JavaVM)> {
188+
|env| -> jni::errors::Result<(
189+
String,
190+
Global<JObject<'static>>,
191+
Global<JObject<'static>>,
192+
jni::JavaVM,
193+
)> {
188194
let config_str: String = env.get_string(&config_yaml).map(|s| s.to_string())?;
189-
let callback_ref = env.new_global_ref(protect_callback)?;
195+
let protect_ref = env.new_global_ref(protect_callback)?;
196+
let traffic_ref = env.new_global_ref(traffic_callback)?;
190197
let jvm = env.get_java_vm()?;
191-
Ok((config_str, callback_ref, jvm))
198+
Ok((config_str, protect_ref, traffic_ref, jvm))
192199
},
193200
)
194201
.into_outcome();
195202

196-
let (config_str, callback_ref, jvm) = match result {
203+
let (config_str, protect_ref, traffic_ref, jvm) = match result {
197204
Outcome::Ok(v) => v,
198205
Outcome::Err(e) => {
199206
error!("Failed to extract JNI values for start: {}", e);
@@ -204,15 +211,15 @@ pub extern "system" fn Java_com_shoesproxy_ShoesNative_start<'local>(
204211
let jvm: Arc<jni::JavaVM> = Arc::new(jvm);
205212

206213
// Socket protector calls VpnService.protect() to exempt sockets from VPN routing
207-
let callback_ref: Arc<Global<JObject<'static>>> = Arc::new(callback_ref);
214+
let protect_ref: Arc<Global<JObject<'static>>> = Arc::new(protect_ref);
208215
let jvm_clone = jvm.clone();
209-
let callback_clone = callback_ref.clone();
216+
let protect_clone = protect_ref.clone();
210217

211218
let protector = FnSocketProtector::new(move |fd: i32| {
212219
let protect_ok = jvm_clone
213220
.attach_current_thread(|env: &mut jni::Env| -> jni::errors::Result<bool> {
214221
let v = env.call_method(
215-
&*callback_clone,
222+
&*protect_clone,
216223
jni::jni_str!("protect"),
217224
jni::jni_sig!("(I)Z"),
218225
&[JValue::Int(fd)],
@@ -233,6 +240,23 @@ pub extern "system" fn Java_com_shoesproxy_ShoesNative_start<'local>(
233240

234241
set_global_socket_protector(Arc::new(protector));
235242

243+
// Traffic callback calls TrafficListener.onTrafficUpdate(long, long)
244+
let traffic_ref: Arc<Global<JObject<'static>>> = Arc::new(traffic_ref);
245+
let jvm_traffic = jvm.clone();
246+
crate::tun::traffic::reset_traffic_counters();
247+
crate::tun::traffic::set_traffic_callback(Arc::new(move |upload: u64, download: u64| {
248+
let _ =
249+
jvm_traffic.attach_current_thread(|env: &mut jni::Env| -> jni::errors::Result<()> {
250+
env.call_method(
251+
&*traffic_ref,
252+
jni::jni_str!("onTrafficUpdate"),
253+
jni::jni_sig!("(JJ)V"),
254+
&[JValue::Long(upload as i64), JValue::Long(download as i64)],
255+
)?;
256+
Ok(())
257+
});
258+
}));
259+
236260
let runtime = match Runtime::new() {
237261
Ok(rt) => rt,
238262
Err(e) => {
@@ -282,6 +306,7 @@ pub extern "system" fn Java_com_shoesproxy_ShoesNative_stop(
282306
_handle: jlong,
283307
) {
284308
common::stop_service();
309+
crate::tun::traffic::clear_traffic_callback();
285310
}
286311

287312
/// Check if the TUN service is running.

src/ffi/ios.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ use super::common::{
3939
/// The callback receives a file descriptor and should return true if protected successfully.
4040
pub type ProtectSocketCallback = extern "C" fn(fd: c_int) -> bool;
4141

42+
/// Traffic statistics callback type.
43+
/// Called periodically with cumulative upload and download byte counts.
44+
pub type ShoesTrafficCallback = extern "C" fn(upload_bytes: u64, download_bytes: u64);
45+
4246
/// Global socket protector callback.
4347
static PROTECT_CALLBACK: OnceLock<Mutex<Option<ProtectSocketCallback>>> = OnceLock::new();
4448

@@ -127,6 +131,7 @@ pub unsafe extern "C" fn shoes_init(log_level: *const c_char) -> c_int {
127131
pub unsafe extern "C" fn shoes_start(
128132
config_yaml: *const c_char,
129133
protect_callback: ProtectSocketCallback,
134+
traffic_callback: ShoesTrafficCallback,
130135
) -> c_long {
131136
if config_yaml.is_null() {
132137
error!("shoes_start: config_yaml is null");
@@ -148,6 +153,12 @@ pub unsafe extern "C" fn shoes_start(
148153
*callback_guard = Some(protect_callback);
149154
}
150155

156+
// Store traffic callback and reset counters
157+
crate::tun::traffic::reset_traffic_counters();
158+
crate::tun::traffic::set_traffic_callback(Arc::new(move |upload, download| {
159+
traffic_callback(upload, download);
160+
}));
161+
151162
crate::tun::set_global_socket_protector(Arc::new(IosSocketProtector));
152163

153164
let runtime = match tokio::runtime::Builder::new_multi_thread()
@@ -193,6 +204,7 @@ pub unsafe extern "C" fn shoes_start(
193204
#[unsafe(no_mangle)]
194205
pub extern "C" fn shoes_stop(_handle: c_long) {
195206
common::stop_service();
207+
crate::tun::traffic::clear_traffic_callback();
196208

197209
if let Some(callback) = PROTECT_CALLBACK.get() {
198210
let mut guard = callback.lock();

src/tun/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
3131
mod tcp_conn;
3232
mod tcp_stack_direct;
33+
pub mod traffic;
3334
mod tun_server;
3435
mod udp_handler;
3536
mod udp_manager;
@@ -155,6 +156,16 @@ pub async fn run_tun_server(
155156

156157
info!("TUN server started successfully");
157158

159+
// Periodic traffic stats reporting (every 1 second)
160+
let traffic_task = tokio::spawn(async {
161+
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1));
162+
interval.tick().await; // skip immediate first tick
163+
loop {
164+
interval.tick().await;
165+
traffic::report_traffic();
166+
}
167+
});
168+
158169
// Wait for shutdown signal or stack thread exit
159170
tokio::select! {
160171
_ = &mut shutdown_rx => {
@@ -170,6 +181,8 @@ pub async fn run_tun_server(
170181
}
171182
}
172183

184+
traffic_task.abort();
185+
173186
if let Some(t) = tcp_task {
174187
t.abort();
175188
}
@@ -226,6 +239,8 @@ async fn handle_tcp_connection(
226239

227240
match result {
228241
Ok((client_to_remote, remote_to_client)) => {
242+
traffic::add_upload_bytes(client_to_remote);
243+
traffic::add_download_bytes(remote_to_client);
229244
debug!(
230245
"TCP connection to {} completed: {} bytes sent, {} bytes received",
231246
remote_location.location(),

0 commit comments

Comments
 (0)