diff --git a/ostp-client/src/bridge.rs b/ostp-client/src/bridge.rs index 44ab0ff..d8374f3 100644 --- a/ostp-client/src/bridge.rs +++ b/ostp-client/src/bridge.rs @@ -36,6 +36,7 @@ pub struct BridgeMetrics { pub bytes_sent: AtomicU64, pub bytes_recv: AtomicU64, pub connection_state: AtomicU8, + pub rtt_ms: portable_atomic::AtomicU32, } async fn send_datagram(socket: &crate::transport::Transport, frame: &Bytes, webrtc_masquerade: bool) -> std::io::Result { @@ -210,6 +211,7 @@ impl Bridge { RelayMessage::Pong(ts) => { let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as u64; self.last_rtt_ms = now.saturating_sub(ts) as f64; + self.metrics.rtt_ms.store(self.last_rtt_ms as u32, Ordering::Relaxed); } RelayMessage::KeepAlive | RelayMessage::Ping(_) | RelayMessage::Connect(_) => {} } diff --git a/ostp-client/src/runner.rs b/ostp-client/src/runner.rs index 93c0dd1..bf48eda 100644 --- a/ostp-client/src/runner.rs +++ b/ostp-client/src/runner.rs @@ -128,6 +128,7 @@ pub async fn run_client(config: crate::config::ClientConfig) -> Result<()> { bytes_sent: portable_atomic::AtomicU64::new(0), bytes_recv: portable_atomic::AtomicU64::new(0), connection_state: portable_atomic::AtomicU8::new(0), + rtt_ms: portable_atomic::AtomicU32::new(0), }); let (shutdown_tx, shutdown_rx) = watch::channel(false); diff --git a/ostp-gui/src-tauri/src/lib.rs b/ostp-gui/src-tauri/src/lib.rs index 80e0c8b..6423240 100644 --- a/ostp-gui/src-tauri/src/lib.rs +++ b/ostp-gui/src-tauri/src/lib.rs @@ -78,6 +78,7 @@ struct MuxConfig { struct UIMetrics { bytes_sent: u64, bytes_recv: u64, + rtt_ms: u32, } // ── Messages exchanged with the privileged helper ──────────────────────────── @@ -87,7 +88,7 @@ struct UIMetrics { enum HelperMsg { Status { value: u8 }, Log { message: String }, - Metrics { bytes_sent: u64, bytes_recv: u64 }, + Metrics { bytes_sent: u64, bytes_recv: u64, rtt_ms: u32 }, Error { message: String }, } @@ -262,12 +263,14 @@ async fn get_metrics(state: tauri::State<'_, AppState>) -> Result Ok(Some(UIMetrics { bytes_sent: s.metrics.bytes_sent.load(Ordering::Relaxed), bytes_recv: s.metrics.bytes_recv.load(Ordering::Relaxed), + rtt_ms: s.metrics.rtt_ms.load(Ordering::Relaxed), })), Some(TunnelHandle::Helper(h)) => { let ps = h.pipe_state.lock().await; Ok(Some(UIMetrics { bytes_sent: ps.bytes_sent, bytes_recv: ps.bytes_recv, + rtt_ms: ps.rtt_ms, })) } } @@ -338,6 +341,7 @@ async fn start_proxy_in_process( // Start at 1 (connecting) so UI polling doesn't see 0 and flip back to disconnected // before the handshake task has had a chance to begin. connection_state: portable_atomic::AtomicU8::new(1), + rtt_ms: portable_atomic::AtomicU32::new(0), }); let (shutdown_tx, shutdown_rx) = watch::channel(false); @@ -383,7 +387,7 @@ async fn start_tun_via_helper( }).to_string(); let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::channel::(16); - let pipe_state = Arc::new(Mutex::new(HelperPipeState { connection_state: 1, bytes_sent: 0, bytes_recv: 0 })); + let pipe_state = Arc::new(Mutex::new(HelperPipeState { connection_state: 1, bytes_sent: 0, bytes_recv: 0, rtt_ms: 0 })); let state_for_task = pipe_state.clone(); tokio::spawn(async move { @@ -403,7 +407,7 @@ async fn start_tun_via_helper( let mut s = state_for_task.lock().await; match msg { HelperMsg::Status { value } => s.connection_state = value, - HelperMsg::Metrics { bytes_sent, bytes_recv } => { s.bytes_sent = bytes_sent; s.bytes_recv = bytes_recv; } + HelperMsg::Metrics { bytes_sent, bytes_recv, rtt_ms } => { s.bytes_sent = bytes_sent; s.bytes_recv = bytes_recv; s.rtt_ms = rtt_ms; } HelperMsg::Error { message } => { s.connection_state = 0; eprintln!("Helper error: {}", message); } _ => {} } @@ -425,6 +429,7 @@ struct HelperPipeState { connection_state: u8, bytes_sent: u64, bytes_recv: u64, + rtt_ms: u32, } fn find_helper_exe() -> Option { diff --git a/ostp-gui/src/main.js b/ostp-gui/src/main.js index c26ad34..b0182b5 100644 --- a/ostp-gui/src/main.js +++ b/ostp-gui/src/main.js @@ -159,6 +159,11 @@ async function poll() { if (metrics) { metricDown.textContent = fmtBytes(metrics.bytes_recv); metricUp.textContent = fmtBytes(metrics.bytes_sent); + + const isTun = rawConfig?.tun?.enable; + const modeStr = isTun ? 'TUN' : 'SOCKS5'; + const pingStr = metrics.rtt_ms > 0 ? ` (${metrics.rtt_ms} ms)` : ''; + metricMode.textContent = modeStr + pingStr; } } catch { setState('disconnected'); diff --git a/ostp-jni/src/lib.rs b/ostp-jni/src/lib.rs index 0bedd2b..70f94a3 100644 --- a/ostp-jni/src/lib.rs +++ b/ostp-jni/src/lib.rs @@ -142,6 +142,7 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_startClient( bytes_sent: portable_atomic::AtomicU64::new(0), bytes_recv: portable_atomic::AtomicU64::new(0), connection_state: portable_atomic::AtomicU8::new(0), + rtt_ms: portable_atomic::AtomicU32::new(0), }); let bridge = match Bridge::new(&config, Arc::clone(&metrics)) { @@ -310,16 +311,17 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_getMetrics( let sent = m.bytes_sent.load(Ordering::Relaxed); let recv = m.bytes_recv.load(Ordering::Relaxed); let conn_state = m.connection_state.load(Ordering::Relaxed); + let rtt = m.rtt_ms.load(Ordering::Relaxed); let json = format!( - r#"{{"bytes_sent": {}, "bytes_recv": {}, "connection_state": {}}}"#, - sent, recv, conn_state + r#"{{"bytes_sent": {}, "bytes_recv": {}, "connection_state": {}, "rtt_ms": {}}}"#, + sent, recv, conn_state, rtt ); match env.new_string(json) { Ok(s) => s.into_raw(), Err(_) => std::ptr::null_mut(), } } else { - match env.new_string(r#"{"bytes_sent": 0, "bytes_recv": 0, "connection_state": 0}"#) { + match env.new_string(r#"{"bytes_sent": 0, "bytes_recv": 0, "connection_state": 0, "rtt_ms": 0}"#) { Ok(s) => s.into_raw(), Err(_) => std::ptr::null_mut(), } diff --git a/ostp-tun-helper/src/main.rs b/ostp-tun-helper/src/main.rs index 0cbb7e6..8b34a17 100644 --- a/ostp-tun-helper/src/main.rs +++ b/ostp-tun-helper/src/main.rs @@ -37,7 +37,7 @@ enum GuiCmd { enum HelperMsg { Status { value: u8 }, Log { message: String }, - Metrics { bytes_sent: u64, bytes_recv: u64 }, + Metrics { bytes_sent: u64, bytes_recv: u64, rtt_ms: u32 }, Error { message: String }, } @@ -144,6 +144,7 @@ async fn run_server() -> Result<()> { bytes_sent: portable_atomic::AtomicU64::new(0), bytes_recv: portable_atomic::AtomicU64::new(0), connection_state: portable_atomic::AtomicU8::new(0), + rtt_ms: portable_atomic::AtomicU32::new(0), }); let (shutdown_tx, shutdown_rx) = watch::channel(false); @@ -179,13 +180,15 @@ async fn run_server() -> Result<()> { let sent = metrics_tick.bytes_sent.load(Ordering::Relaxed); let recv = metrics_tick.bytes_recv.load(Ordering::Relaxed); + let rtt = metrics_tick.rtt_ms.load(Ordering::Relaxed); + let mut w = writer_tick.lock().await; if cs != last_state { last_state = cs; let json = serde_json::to_string(&HelperMsg::Status { value: cs }).unwrap_or_default(); if w.write_all(format!("{}\n", json).as_bytes()).await.is_err() { break; } } - let json = serde_json::to_string(&HelperMsg::Metrics { bytes_sent: sent, bytes_recv: recv }).unwrap_or_default(); + let json = serde_json::to_string(&HelperMsg::Metrics { bytes_sent: sent, bytes_recv: recv, rtt_ms: rtt }).unwrap_or_default(); if w.write_all(format!("{}\n", json).as_bytes()).await.is_err() { break; } drop(w); }