From 486d745d47c5ce5c10afc18758436af48055c4cc Mon Sep 17 00:00:00 2001 From: ospab Date: Sun, 14 Jun 2026 00:02:08 +0300 Subject: [PATCH] feat(tun): implement process bypass for TCP/UDP and IP bypass for UDP using existing Extended tables --- ostp-client/src/bin/test_route.rs | 21 ++++- ostp-client/src/tunnel/native_handler.rs | 102 +++++++++++++---------- ostp-client/src/tunnel/udp_nat.rs | 90 +++++++++++++++++++- ostp-flutter/lib/ui/settings_screen.dart | 8 +- ostp-gui/src/index.html | 2 +- ostp-jni/src/lib.rs | 4 +- ostp-tun/src/lib.rs | 1 + ostp/src/main.rs | 6 +- 8 files changed, 171 insertions(+), 63 deletions(-) diff --git a/ostp-client/src/bin/test_route.rs b/ostp-client/src/bin/test_route.rs index 8dcc5c1..f966db8 100644 --- a/ostp-client/src/bin/test_route.rs +++ b/ostp-client/src/bin/test_route.rs @@ -1,4 +1,21 @@ fn main() { - let route = ostp_tun::windows::windows_route::sys::get_default_ipv4_route(); - println!("Default IPv4 route: {:?}", route); + let socket = std::net::UdpSocket::bind("0.0.0.0:0").unwrap(); + let port = socket.local_addr().unwrap().port(); + println!("Bound UDP to port {}", port); + + if let Some(name) = ostp_client::tunnel::process_lookup::get_process_name_from_port_udp(port) { + println!("Found process for UDP port {}: {}", port, name); + } else { + println!("Process not found for UDP port {}", port); + } + + let tcp_socket = std::net::TcpListener::bind("0.0.0.0:0").unwrap(); + let tcp_port = tcp_socket.local_addr().unwrap().port(); + println!("Bound TCP to port {}", tcp_port); + + if let Some(name) = ostp_client::tunnel::process_lookup::get_process_name_from_port(tcp_port) { + println!("Found process for TCP port {}: {}", tcp_port, name); + } else { + println!("Process not found for TCP port {}", tcp_port); + } } diff --git a/ostp-client/src/tunnel/native_handler.rs b/ostp-client/src/tunnel/native_handler.rs index 572e4e8..d5844d2 100644 --- a/ostp-client/src/tunnel/native_handler.rs +++ b/ostp-client/src/tunnel/native_handler.rs @@ -84,13 +84,6 @@ pub async fn run_native_tunnel( } } - if !config.exclusions.processes.is_empty() { - tracing::warn!( - "Process-based split tunneling is not fully supported in TUN mode on all platforms \ - without WFP/eBPF. Processes in the exclusion list will still be tunneled. \ - Use IP or domain exclusions instead." - ); - } // ── 3. Create TUN device via ostp-tun crate ─────────────────────────────── let opts = ostp_tun::OstpTunOptions { @@ -167,10 +160,41 @@ pub async fn run_native_tunnel( } a }; + // Build exclusion matcher for dynamic bypass + let current_exclusions = exclusions_rx.borrow().clone(); + let matcher = crate::tunnel::exclusion::ExclusionMatcher::new(¤t_exclusions, None, None); + let matcher_arc = std::sync::Arc::new(tokio::sync::RwLock::new(matcher)); + + let matcher_clone = matcher_arc.clone(); + tokio::spawn(async move { + while let Ok(_) = exclusions_rx.changed().await { + let current = exclusions_rx.borrow().clone(); + let new_matcher = crate::tunnel::exclusion::ExclusionMatcher::new(¤t, None, None); + *matcher_clone.write().await = new_matcher; + if true { + tracing::debug!("Desktop TUN exclusions hot-reloaded"); + } + } + }); + + // Linux: physical interface name for SO_BINDTODEVICE + #[cfg(target_os = "linux")] + let linux_phys_name = crate::tunnel::proxy::get_linux_physical_if_name(); + #[cfg(not(target_os = "linux"))] + let linux_phys_name: Option = None; + let _ = &linux_phys_name; // suppress unused warning on Windows + let debug_udp = debug; + let udp_matcher = matcher_arc.clone(); + #[cfg(target_os = "linux")] + let udp_lin_name = linux_phys_name.clone(); + let mut udp_proxy_task = tokio::spawn(async move { if let Some(udp_sock) = udp_socket { - super::udp_nat::run_udp_nat(udp_sock, udp_proxy_addr, debug_udp).await; + #[cfg(target_os = "linux")] + super::udp_nat::run_udp_nat(udp_sock, udp_proxy_addr, debug_udp, udp_matcher, phys_if_for_bypass, udp_lin_name).await; + #[cfg(not(target_os = "linux"))] + super::udp_nat::run_udp_nat(udp_sock, udp_proxy_addr, debug_udp, udp_matcher, phys_if_for_bypass, None).await; } }); @@ -193,32 +217,8 @@ pub async fn run_native_tunnel( a }; - // Build exclusion matcher for SNI-based domain bypass (fallback / CDN handling) - let current_exclusions = exclusions_rx.borrow().clone(); - let matcher = crate::tunnel::exclusion::ExclusionMatcher::new(¤t_exclusions, None, None); - let matcher_arc = std::sync::Arc::new(tokio::sync::RwLock::new(matcher)); - - let matcher_clone = matcher_arc.clone(); - tokio::spawn(async move { - while let Ok(_) = exclusions_rx.changed().await { - let current = exclusions_rx.borrow().clone(); - let new_matcher = crate::tunnel::exclusion::ExclusionMatcher::new(¤t, None, None); - *matcher_clone.write().await = new_matcher; - if true { - tracing::debug!("Desktop TUN exclusions hot-reloaded"); - } - } - }); - // Physical interface index was captured at the start of the function. - // Linux: physical interface name for SO_BINDTODEVICE - #[cfg(target_os = "linux")] - let linux_phys_name = crate::tunnel::proxy::get_linux_physical_if_name(); - #[cfg(not(target_os = "linux"))] - let linux_phys_name: Option = None; - let _ = &linux_phys_name; // suppress unused warning on Windows - let mut tcp_accept_task = tokio::spawn(async move { let Some(mut listener) = tcp_listener else { return; }; @@ -250,8 +250,21 @@ pub async fn run_native_tunnel( // ── Decide: bypass or tunnel? ───────────────────────────────── let mut should_bypass = false; - // 1. SNI domain check (belt-and-suspenders for CDNs / late-resolved IPs) - if sniff_len > 0 { + // 1. Process match via OS Extended TCP Table (Windows) + #[cfg(target_os = "windows")] + if !should_bypass { + if let Some(proc_name) = crate::tunnel::process_lookup::get_process_name_from_port(local.port()) { + if matcher.match_process(&proc_name) { + if true { + tracing::debug!("TUN BYPASS (Process match): {} → {remote}", proc_name); + } + should_bypass = true; + } + } + } + + // 2. SNI domain check (belt-and-suspenders for CDNs / late-resolved IPs) + if !should_bypass && sniff_len > 0 { if let Some(sni) = crate::tunnel::sni_sniff::extract_sni(&sniff_buf[..sniff_len]) { @@ -267,7 +280,7 @@ pub async fn run_native_tunnel( } } - // 2. Destination IP CIDR check (for IPs not in routing table / IPv6) + // 3. Destination IP CIDR check (for IPs not in routing table / IPv6) if !should_bypass && matcher.match_ip(&remote.ip()) { if true { tracing::debug!("TUN BYPASS (IP match): {remote}"); @@ -540,14 +553,6 @@ pub async fn run_native_tunnel_from_fd( proxy_addr = proxy_addr.replace("0.0.0.0:", "127.0.0.1:"); } - let udp_proxy_addr = proxy_addr.clone(); - let debug_udp = debug; - let mut udp_proxy_task = tokio::spawn(async move { - if let Some(udp_sock) = udp_socket { - super::udp_nat::run_udp_nat(udp_sock, udp_proxy_addr, debug_udp).await; - } - }); - let current_exclusions = exclusions_rx.borrow().clone(); let matcher = crate::tunnel::exclusion::ExclusionMatcher::new(¤t_exclusions, None, None); let matcher_arc = std::sync::Arc::new(tokio::sync::RwLock::new(matcher)); @@ -564,6 +569,17 @@ pub async fn run_native_tunnel_from_fd( } }); + let udp_proxy_addr = proxy_addr.clone(); + let debug_udp = debug; + let udp_matcher = matcher_arc.clone(); + let mut udp_proxy_task = tokio::spawn(async move { + if let Some(udp_sock) = udp_socket { + super::udp_nat::run_udp_nat(udp_sock, udp_proxy_addr, debug_udp, udp_matcher, None, None).await; + } + }); + + + let mut tcp_accept_task = tokio::spawn(async move { let Some(mut listener) = tcp_listener else { return; }; diff --git a/ostp-client/src/tunnel/udp_nat.rs b/ostp-client/src/tunnel/udp_nat.rs index 04c70e6..bc24953 100644 --- a/ostp-client/src/tunnel/udp_nat.rs +++ b/ostp-client/src/tunnel/udp_nat.rs @@ -10,6 +10,9 @@ pub async fn run_udp_nat( udp_socket: netstack_smoltcp::UdpSocket, proxy_addr: String, _debug: bool, + matcher: std::sync::Arc>, + phys_if_index: Option, + phys_if_name: Option, ) { let (mut rx, tx) = udp_socket.split(); let tx = Arc::new(Mutex::new(tx)); @@ -33,11 +36,41 @@ pub async fn run_udp_nat( let proxy_addr_clone = proxy_addr.clone(); let tx_clone = tx.clone(); + let mut should_bypass = false; + { + let matcher_guard = matcher.read().await; + if matcher_guard.match_ip(&dst.ip()) { + should_bypass = true; + tracing::debug!("TUN UDP BYPASS (IP match): {} → {}", src, dst); + } + + #[cfg(target_os = "windows")] + if !should_bypass { + if let Some(proc_name) = crate::tunnel::process_lookup::get_process_name_from_port_udp(src.port()) { + if matcher_guard.match_process(&proc_name) { + should_bypass = true; + tracing::debug!("TUN UDP BYPASS (Process match): {} ({} → {})", proc_name, src, dst); + } + } + } + } + + let p_if_idx = phys_if_index; + let p_if_name = phys_if_name.clone(); + tokio::spawn(async move { - tracing::debug!("Starting UDP NAT session for {}", src); - let res = start_udp_session(src, proxy_addr_clone, &mut session_rx, tx_clone).await; - if res.is_err() { - tracing::debug!("UDP NAT session for {} ended: {:?}", src, res.err()); + if should_bypass { + tracing::debug!("Starting UDP BYPASS session for {}", src); + let res = start_udp_bypass_session(src, p_if_idx, p_if_name, &mut session_rx, tx_clone).await; + if res.is_err() { + tracing::debug!("UDP BYPASS session for {} ended: {:?}", src, res.err()); + } + } else { + tracing::debug!("Starting UDP NAT session for {}", src); + let res = start_udp_session(src, proxy_addr_clone, &mut session_rx, tx_clone).await; + if res.is_err() { + tracing::debug!("UDP NAT session for {} ended: {:?}", src, res.err()); + } } }); } @@ -58,6 +91,55 @@ pub async fn run_udp_nat( } } +async fn start_udp_bypass_session( + client_src: SocketAddr, + phys_if_index: Option, + phys_if_name: Option, + session_rx: &mut mpsc::Receiver<(Vec, SocketAddr)>, + smoltcp_tx: Arc>, +) -> anyhow::Result<()> { + let socket = match client_src { + SocketAddr::V4(_) => UdpSocket::bind("0.0.0.0:0").await?, + SocketAddr::V6(_) => UdpSocket::bind("[::]:0").await?, + }; + + #[cfg(target_os = "windows")] + if let Some(idx) = phys_if_index { + let _ = crate::tunnel::proxy::bind_socket_to_interface(&socket, client_src.is_ipv6(), idx); + } + + #[cfg(target_os = "linux")] + if let Some(ref name) = phys_if_name { + let _ = crate::tunnel::proxy::bind_socket_to_interface(&socket, name); + } + + let socket = Arc::new(socket); + let socket_rx = socket.clone(); + + // Spawn a task to read from physical socket and send back to smoltcp + let tx_clone = smoltcp_tx.clone(); + tokio::spawn(async move { + use futures::SinkExt; + let mut buf = [0u8; 65536]; + loop { + match socket_rx.recv_from(&mut buf).await { + Ok((n, peer)) => { + let mut lock = tx_clone.lock().await; + let _ = lock.send((buf[..n].to_vec(), peer, client_src)).await; + } + Err(_) => break, + } + } + }); + + while let Some((payload, dst)) = session_rx.recv().await { + socket.send_to(&payload, dst).await?; + } + + Ok(()) +} + + async fn start_udp_session( client_src: SocketAddr, proxy_addr: String, diff --git a/ostp-flutter/lib/ui/settings_screen.dart b/ostp-flutter/lib/ui/settings_screen.dart index 4393f1f..5b5c296 100644 --- a/ostp-flutter/lib/ui/settings_screen.dart +++ b/ostp-flutter/lib/ui/settings_screen.dart @@ -389,13 +389,7 @@ class _SettingsScreenState extends State { }, ); }), - const SizedBox(height: 16), - _buildToggle('XTLS-Reality', 'Подделка TLS-сессии (Stealth-домен должен быть TLS 1.3)', _realityEnabled, (val) { - setState(() { - _realityEnabled = val; - }); - }), - const SizedBox(height: 16), + ], ), ), diff --git a/ostp-gui/src/index.html b/ostp-gui/src/index.html index 7cce7d1..bfece76 100644 --- a/ostp-gui/src/index.html +++ b/ostp-gui/src/index.html @@ -347,7 +347,7 @@ - Only works in TUN mode. Type process name and press Enter. + Type process name and press Enter. diff --git a/ostp-jni/src/lib.rs b/ostp-jni/src/lib.rs index 8c2538b..e549aa6 100644 --- a/ostp-jni/src/lib.rs +++ b/ostp-jni/src/lib.rs @@ -333,8 +333,10 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_nativeStartClient( } let shutdown_rx_clone = shutdown_tx.subscribe(); let config_clone = config.clone(); + let (exclusions_tx, exclusions_rx) = tokio::sync::watch::channel(config.exclusions.clone()); rt.spawn(async move { - if let Err(e) = tunnel::native_handler::run_native_tunnel_from_fd(config_clone, shutdown_rx_clone, fd).await { + let _tx = exclusions_tx; // keep tx alive + if let Err(e) = tunnel::native_handler::run_native_tunnel_from_fd(config_clone, shutdown_rx_clone, exclusions_rx, fd).await { add_log(format!("Native TUN exited with error: {}", e)); } }); diff --git a/ostp-tun/src/lib.rs b/ostp-tun/src/lib.rs index 4f94c2c..fcbb37e 100644 --- a/ostp-tun/src/lib.rs +++ b/ostp-tun/src/lib.rs @@ -24,6 +24,7 @@ pub mod linux; pub mod macos; impl OstpTunInterface { + #[allow(unused_variables)] pub async fn create(opts: OstpTunOptions) -> Result { #[cfg(target_os = "windows")] return windows::create(opts).await; diff --git a/ostp/src/main.rs b/ostp/src/main.rs index c49e6e2..f5e268d 100644 --- a/ostp/src/main.rs +++ b/ostp/src/main.rs @@ -1300,8 +1300,6 @@ async fn run_app() -> Result<()> { "target": "127.0.0.1:8080" }}, - // Reality (XTLS) / UoT Masquerade parameters - "debug": false }}"#, key) @@ -1347,9 +1345,7 @@ async fn run_app() -> Result<()> { "processes": [] }}, - // Reality (XTLS) / WebRTC Masquerade parameters - - // Transport Mode: "udp" (default WebRTC masquerade) or "uot" (TCP XTLS-Reality) + // Transport Mode: "udp" (default WebRTC masquerade) or "uot" (TCP UoT) "transport": {{ "mode": "udp", "stealth_sni": "www.microsoft.com",