diff --git a/ostp-client/src/tunnel/mod.rs b/ostp-client/src/tunnel/mod.rs index 15a0456..cd87812 100644 --- a/ostp-client/src/tunnel/mod.rs +++ b/ostp-client/src/tunnel/mod.rs @@ -2,6 +2,7 @@ mod proxy; mod wintun_handler; mod linux_handler; pub mod native_handler; +mod udp_nat; pub async fn run_tun_tunnel( config: crate::config::ClientConfig, diff --git a/ostp-client/src/tunnel/native_handler.rs b/ostp-client/src/tunnel/native_handler.rs index a00cae1..05fcb8b 100644 --- a/ostp-client/src/tunnel/native_handler.rs +++ b/ostp-client/src/tunnel/native_handler.rs @@ -162,50 +162,7 @@ pub async fn run_native_tunnel( let debug_udp = config.debug; let mut udp_proxy_task = tokio::spawn(async move { if let Some(udp_sock) = udp_socket { - let (mut rx, tx) = udp_sock.split(); - let tx = std::sync::Arc::new(tokio::sync::Mutex::new(tx)); - while let Some((payload, src, dst)) = rx.next().await { - if payload.is_empty() { continue; } - if dst.port() == 53 { - let tx_clone = tx.clone(); - let proxy_addr = udp_proxy_addr.clone(); - tokio::spawn(async move { - if debug_udp { tracing::info!("Native TUN intercepted UDP DNS to {}", dst); } - if let Ok(mut socks) = tokio::net::TcpStream::connect(&proxy_addr).await { - if socks.write_all(&[5, 1, 0]).await.is_err() { return; } - let mut buf = [0u8; 2]; - if socks.read_exact(&mut buf).await.is_err() || buf[0] != 5 || buf[1] != 0 { return; } - - let mut req = vec![5, 1, 0]; - match dst.ip() { - std::net::IpAddr::V4(v4) => { req.push(1); req.extend_from_slice(&v4.octets()); } - std::net::IpAddr::V6(v6) => { req.push(4); req.extend_from_slice(&v6.octets()); } - } - req.extend_from_slice(&dst.port().to_be_bytes()); - if socks.write_all(&req).await.is_err() { return; } - - let mut rep = [0u8; 10]; - if socks.read_exact(&mut rep).await.is_err() || rep[1] != 0 { return; } - - let len = payload.len() as u16; - let mut dns_req = Vec::with_capacity(2 + payload.len()); - dns_req.extend_from_slice(&len.to_be_bytes()); - dns_req.extend_from_slice(&payload); - - if socks.write_all(&dns_req).await.is_ok() { - let mut len_buf = [0u8; 2]; - if socks.read_exact(&mut len_buf).await.is_ok() { - let resp_len = u16::from_be_bytes(len_buf) as usize; - let mut response_buf = vec![0u8; resp_len]; - if socks.read_exact(&mut response_buf).await.is_ok() { - let _ = tx_clone.lock().await.send((response_buf, dst, src)).await; - } - } - } - } - }); - } - } + super::udp_nat::run_udp_nat(udp_sock, udp_proxy_addr, debug_udp).await; } }); @@ -413,54 +370,13 @@ pub async fn run_native_tunnel_from_fd( } }); + + let udp_proxy_addr = config.local_proxy.bind_addr.clone(); let debug_udp = config.debug; let mut udp_proxy_task = tokio::spawn(async move { if let Some(udp_sock) = udp_socket { - let (mut rx, tx) = udp_sock.split(); - let tx = std::sync::Arc::new(tokio::sync::Mutex::new(tx)); - while let Some((payload, src, dst)) = rx.next().await { - if payload.is_empty() { continue; } - if dst.port() == 53 { - let tx_clone = tx.clone(); - let proxy_addr = udp_proxy_addr.clone(); - tokio::spawn(async move { - if debug_udp { tracing::info!("Native TUN intercepted UDP DNS to {}", dst); } - if let Ok(mut socks) = tokio::net::TcpStream::connect(&proxy_addr).await { - if socks.write_all(&[5, 1, 0]).await.is_err() { return; } - let mut buf = [0u8; 2]; - if socks.read_exact(&mut buf).await.is_err() || buf[0] != 5 || buf[1] != 0 { return; } - - let mut req = vec![5, 1, 0]; - match dst.ip() { - std::net::IpAddr::V4(v4) => { req.push(1); req.extend_from_slice(&v4.octets()); } - std::net::IpAddr::V6(v6) => { req.push(4); req.extend_from_slice(&v6.octets()); } - } - req.extend_from_slice(&dst.port().to_be_bytes()); - if socks.write_all(&req).await.is_err() { return; } - - let mut rep = [0u8; 10]; - if socks.read_exact(&mut rep).await.is_err() || rep[1] != 0 { return; } - - let len = payload.len() as u16; - let mut dns_req = Vec::with_capacity(2 + payload.len()); - dns_req.extend_from_slice(&len.to_be_bytes()); - dns_req.extend_from_slice(&payload); - - if socks.write_all(&dns_req).await.is_ok() { - let mut len_buf = [0u8; 2]; - if socks.read_exact(&mut len_buf).await.is_ok() { - let resp_len = u16::from_be_bytes(len_buf) as usize; - let mut response_buf = vec![0u8; resp_len]; - if socks.read_exact(&mut response_buf).await.is_ok() { - let _ = tx_clone.lock().await.send((response_buf, dst, src)).await; - } - } - } - } - }); - } - } + super::udp_nat::run_udp_nat(udp_sock, udp_proxy_addr, debug_udp).await; } }); diff --git a/ostp-client/src/tunnel/proxy.rs b/ostp-client/src/tunnel/proxy.rs index 24bc2ff..207c45a 100644 --- a/ostp-client/src/tunnel/proxy.rs +++ b/ostp-client/src/tunnel/proxy.rs @@ -9,6 +9,199 @@ use tokio::time::{timeout, Duration}; use crate::config::{ExclusionConfig, LocalProxyConfig, OstpConfig}; use crate::tunnel::{ProxyEvent, ProxyToClientMsg}; +#[cfg(target_os = "windows")] +use std::os::windows::io::AsRawSocket; + +#[cfg(target_os = "linux")] +use std::os::fd::AsRawFd; + +#[cfg(target_os = "windows")] +#[link(name = "ws2_32")] +extern "system" { + fn setsockopt( + s: usize, + level: i32, + optname: i32, + optval: *const u8, + optlen: i32, + ) -> i32; +} + +#[cfg(target_os = "windows")] +fn bind_socket_to_interface(socket: &impl AsRawSocket, is_ipv6: bool, if_index: u32) -> std::io::Result<()> { + let s = socket.as_raw_socket() as usize; + if is_ipv6 { + let optval = if_index; + let ret = unsafe { + setsockopt( + s, + 41, // IPPROTO_IPV6 + 31, // IPV6_UNICAST_IF + &optval as *const u32 as *const u8, + 4, + ) + }; + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } + } else { + let optval = if_index.to_be(); + let ret = unsafe { + setsockopt( + s, + 0, // IPPROTO_IP + 31, // IP_UNICAST_IF + &optval as *const u32 as *const u8, + 4, + ) + }; + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } + } + Ok(()) +} + +#[cfg(target_os = "linux")] +fn bind_socket_to_interface(socket: &impl AsRawFd, if_name: &str) -> std::io::Result<()> { + let fd = socket.as_raw_fd(); + let mut if_name_bytes = if_name.as_bytes().to_vec(); + if_name_bytes.push(0); + let ret = unsafe { + libc::setsockopt( + fd, + libc::SOL_SOCKET, + libc::SO_BINDTODEVICE, + if_name_bytes.as_ptr() as *const std::ffi::c_void, + if_name_bytes.len() as libc::socklen_t, + ) + }; + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) +} + +fn get_windows_physical_if_index() -> Option { + #[cfg(target_os = "windows")] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + let output = std::process::Command::new("powershell") + .creation_flags(CREATE_NO_WINDOW) + .args([ + "-NoProfile", + "-Command", + "Get-NetRoute -DestinationPrefix '0.0.0.0/0' | Where-Object { $_.InterfaceAlias -notmatch 'ostp' -and $_.InterfaceAlias -notmatch 'tun' -and $_.InterfaceAlias -notmatch 'wintun' } | Sort-Object RouteMetric | Select-Object -ExpandProperty InterfaceIndex -First 1" + ]) + .output() + .ok()?; + if output.status.success() { + let s = String::from_utf8_lossy(&output.stdout); + if let Ok(index) = s.trim().parse::() { + return Some(index); + } + } + } + None +} + +fn get_linux_physical_if_name() -> Option { + #[cfg(target_os = "linux")] + { + let output = std::process::Command::new("ip") + .args(["route", "show", "default"]) + .output() + .ok()?; + if output.status.success() { + let s = String::from_utf8_lossy(&output.stdout); + if let Some(dev_part) = s.split_whitespace().skip_while(|w| *w != "dev").nth(1) { + return Some(dev_part.to_string()); + } + } + } + None +} + +async fn connect_bypassing_tun( + target: &str, + physical_if_index: Option, + _physical_if_name: &Option, +) -> Result { + let resolved = tokio::net::lookup_host(target).await + .with_context(|| format!("failed to resolve host for bypass connect: {target}"))?; + + let mut last_err = None; + for addr in resolved { + let socket = if addr.is_ipv6() { + let s = tokio::net::TcpSocket::new_v6()?; + let _ = s.bind("[::]:0".parse().unwrap()); + s + } else { + let s = tokio::net::TcpSocket::new_v4()?; + let _ = s.bind("0.0.0.0:0".parse().unwrap()); + s + }; + + #[cfg(target_os = "windows")] + if let Some(if_index) = physical_if_index { + if let Err(e) = bind_socket_to_interface(&socket, addr.is_ipv6(), if_index) { + tracing::warn!("Failed to bind TCP socket to interface {}: {}", if_index, e); + } + } + + #[cfg(target_os = "linux")] + if let Some(ref if_name) = _physical_if_name { + if let Err(e) = bind_socket_to_interface(&socket, if_name) { + tracing::warn!("Failed to bind TCP socket to interface {}: {}", if_name, e); + } + } + + match socket.connect(addr).await { + Ok(stream) => return Ok(stream), + Err(e) => { + last_err = Some(e); + } + } + } + + Err(anyhow!( + "direct connect failed: {:?}", + last_err.map(|e| e.to_string()).unwrap_or_else(|| "no addresses resolved".to_string()) + )) +} + +async fn create_udp_socket_bypassing_tun( + is_ipv6: bool, + physical_if_index: Option, + _physical_if_name: &Option, +) -> Result { + let addr: std::net::SocketAddr = if is_ipv6 { + "[::]:0".parse().unwrap() + } else { + "0.0.0.0:0".parse().unwrap() + }; + + let socket = UdpSocket::bind(addr).await + .with_context(|| format!("failed to bind direct UdpSocket to wildcard {}", addr))?; + + #[cfg(target_os = "windows")] + if let Some(if_index) = physical_if_index { + if let Err(e) = bind_socket_to_interface(&socket, is_ipv6, if_index) { + tracing::warn!("Failed to bind UDP socket to interface index {}: {}", if_index, e); + } + } + + #[cfg(target_os = "linux")] + if let Some(ref if_name) = _physical_if_name { + if let Err(e) = bind_socket_to_interface(&socket, if_name) { + tracing::warn!("Failed to bind UDP socket to interface {}: {}", if_name, e); + } + } + + Ok(socket) +} + pub async fn run_local_socks5_proxy( cfg: LocalProxyConfig, ostp: OstpConfig, @@ -28,7 +221,17 @@ pub async fn run_local_socks5_proxy( tracing::info!("Windows system proxy: set HTTP proxy to {}. tun2socks: SOCKS5 on same address.", cfg.bind_addr); } - let matcher = ExclusionMatcher::new(&exclusions); + let physical_if_index = tokio::task::spawn_blocking(get_windows_physical_if_index).await.unwrap_or(None); + let physical_if_name = tokio::task::spawn_blocking(get_linux_physical_if_name).await.unwrap_or(None); + + if physical_if_index.is_some() { + tracing::info!("Local proxy physical interface index: {:?}", physical_if_index); + } + if physical_if_name.is_some() { + tracing::info!("Local proxy physical interface name: {:?}", physical_if_name); + } + + let matcher = ExclusionMatcher::new(&exclusions, physical_if_index, physical_if_name.clone()); let (connect_tx, mut connect_rx) = mpsc::channel(128); let max_chunk = ostp.mtu.saturating_sub(150).max(512); @@ -148,15 +351,20 @@ async fn handle_udp_associate( event_tx: mpsc::Sender, mut rx: mpsc::UnboundedReceiver, close_tx: mpsc::Sender, - _debug: bool, + debug: bool, + matcher: ExclusionMatcher, + connect_timeout: Duration, ) -> Result<()> { - let mut client_udp_addr = None; + let client_udp_addr = Arc::new(std::sync::Mutex::new(None)); let mut buf = vec![0u8; 65536]; let udp_socket = Arc::new(udp_socket); let sock_rx = udp_socket.clone(); let sock_tx = udp_socket; + let mut direct_udp_v4: Option> = None; + let mut direct_udp_v6: Option> = None; + let mut tcp_buf = [0u8; 1]; loop { tokio::select! { @@ -166,8 +374,11 @@ async fn handle_udp_associate( } res = sock_rx.recv_from(&mut buf) => { let (len, addr) = res?; - if client_udp_addr.is_none() { - client_udp_addr = Some(addr); + { + let mut guard = client_udp_addr.lock().unwrap(); + if guard.is_none() { + *guard = Some(addr); + } } if len < 4 { continue; } let frag = buf[2]; @@ -199,12 +410,66 @@ async fn handle_udp_associate( _ => continue, }; let payload = bytes::Bytes::copy_from_slice(&buf[header_len..len]); - let _ = event_tx.send(ProxyEvent::UdpData { stream_id, target, payload }).await; + + // Check if target should bypass the tunnel + if matcher.should_bypass(&target, connect_timeout).await { + if debug { + tracing::info!("proxy UDP BYPASS target={}", target); + } + // Resolve target to find if it is IPv4 or IPv6 + if let Ok(resolved_addrs) = tokio::net::lookup_host(&target).await { + if let Some(target_addr) = resolved_addrs.into_iter().next() { + let is_ipv6 = target_addr.is_ipv6(); + let direct_socket = if is_ipv6 { + if direct_udp_v6.is_none() { + match create_udp_socket_bypassing_tun(true, matcher.physical_if_index, &matcher.physical_if_name).await { + Ok(s) => { + let s_arc = Arc::new(s); + spawn_direct_udp_reader(s_arc.clone(), sock_tx.clone(), client_udp_addr.clone(), debug); + direct_udp_v6 = Some(s_arc); + } + Err(e) => { + tracing::error!("Failed to create bypass UDP v6 socket: {}", e); + } + } + } + &direct_udp_v6 + } else { + if direct_udp_v4.is_none() { + match create_udp_socket_bypassing_tun(false, matcher.physical_if_index, &matcher.physical_if_name).await { + Ok(s) => { + let s_arc = Arc::new(s); + spawn_direct_udp_reader(s_arc.clone(), sock_tx.clone(), client_udp_addr.clone(), debug); + direct_udp_v4 = Some(s_arc); + } + Err(e) => { + tracing::error!("Failed to create bypass UDP v4 socket: {}", e); + } + } + } + &direct_udp_v4 + }; + + if let Some(s) = direct_socket { + if let Err(e) = s.send_to(&payload, target_addr).await { + if debug { + tracing::warn!("failed to send bypass UDP packet to {}: {}", target_addr, e); + } + } + } + } + } + } else { + let _ = event_tx.send(ProxyEvent::UdpData { stream_id, target, payload }).await; + } } msg = rx.recv() => { match msg { Some(ProxyToClientMsg::UdpData(target, data)) => { - if let Some(client_addr) = client_udp_addr { + if let Some(client_addr) = { + let guard = client_udp_addr.lock().unwrap(); + *guard + } { let mut packet = vec![0x00, 0x00, 0x00]; let mut parts = target.rsplitn(2, ':'); let port_str = parts.next().unwrap_or("0"); @@ -239,6 +504,52 @@ async fn handle_udp_associate( Ok(()) } +fn spawn_direct_udp_reader( + direct_socket: Arc, + sock_tx: Arc, + client_udp_addr: Arc>>, + debug: bool, +) { + tokio::spawn(async move { + let mut buf = vec![0u8; 65536]; + loop { + match direct_socket.recv_from(&mut buf).await { + Ok((len, target_addr)) => { + let client_addr = { + let guard = client_udp_addr.lock().unwrap(); + *guard + }; + if let Some(client_addr) = client_addr { + let mut packet = vec![0x00, 0x00, 0x00]; + if let Ok(ipv4) = target_addr.ip().to_string().parse::() { + packet.push(0x01); + packet.extend_from_slice(&ipv4.octets()); + } else if let Ok(ipv6) = target_addr.ip().to_string().parse::() { + packet.push(0x04); + packet.extend_from_slice(&ipv6.octets()); + } else { + continue; + } + packet.extend_from_slice(&target_addr.port().to_be_bytes()); + packet.extend_from_slice(&buf[..len]); + if let Err(e) = sock_tx.send_to(&packet, client_addr).await { + if debug { + tracing::warn!("failed to send direct UDP response to client: {e}"); + } + } + } + } + Err(e) => { + if debug { + tracing::debug!("direct UDP socket read loop exiting: {e}"); + } + break; + } + } + } + }); +} + async fn handle_proxy_client( mut client: TcpStream, stream_id: u16, @@ -327,14 +638,32 @@ async fn handle_proxy_client( client.write_all(&reply).await?; event_tx.send(ProxyEvent::UdpAssociate { stream_id }).await?; - return handle_udp_associate(client, udp_socket, stream_id, event_tx, rx, close_tx, debug).await; + return handle_udp_associate( + client, + udp_socket, + stream_id, + event_tx, + rx, + close_tx, + debug, + matcher, + connect_timeout, + ).await; } if debug { tracing::info!("proxy CONNECT stream_id={stream_id} target={target}"); } if matcher.should_bypass(&target, connect_timeout).await { - return direct_connect_socks5(client, stream_id, &target, close_tx, debug).await; + return direct_connect_socks5( + client, + stream_id, + &target, + matcher.physical_if_index, + &matcher.physical_if_name, + close_tx, + debug, + ).await; } event_tx.send(ProxyEvent::NewStream { stream_id, target: target.clone() }).await?; @@ -417,6 +746,8 @@ async fn handle_proxy_client( &target, method.as_str(), header_bytes, + matcher.physical_if_index, + &matcher.physical_if_name, close_tx, debug, ).await; @@ -513,10 +844,16 @@ async fn handle_proxy_client( struct ExclusionMatcher { domain_suffix: Vec, cidrs: Vec, + physical_if_index: Option, + physical_if_name: Option, } impl ExclusionMatcher { - fn new(exclusions: &ExclusionConfig) -> Self { + fn new( + exclusions: &ExclusionConfig, + physical_if_index: Option, + physical_if_name: Option, + ) -> Self { let mut cidrs = Vec::new(); for ip in &exclusions.ips { if let Some(cidr) = parse_cidr(ip) { @@ -532,6 +869,8 @@ impl ExclusionMatcher { .filter(|d| !d.is_empty()) .collect(), cidrs, + physical_if_index, + physical_if_name, } } @@ -645,14 +984,15 @@ async fn direct_connect_socks5( mut client: TcpStream, stream_id: u16, target: &str, + physical_if_index: Option, + physical_if_name: &Option, close_tx: mpsc::Sender, debug: bool, ) -> Result<()> { if debug { tracing::info!("proxy BYPASS stream_id={stream_id} target={target}"); } - let mut remote = TcpStream::connect(target).await - .with_context(|| format!("direct connect failed: {target}"))?; + let mut remote = connect_bypassing_tun(target, physical_if_index, physical_if_name).await?; client.write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?; let _ = tokio::io::copy_bidirectional(&mut client, &mut remote).await; @@ -666,14 +1006,15 @@ async fn direct_connect_http( target: &str, method: &str, header_bytes: Vec, + physical_if_index: Option, + physical_if_name: &Option, close_tx: mpsc::Sender, debug: bool, ) -> Result<()> { if debug { tracing::info!("proxy BYPASS stream_id={stream_id} target={target}"); } - let mut remote = TcpStream::connect(target).await - .with_context(|| format!("direct connect failed: {target}"))?; + let mut remote = connect_bypassing_tun(target, physical_if_index, physical_if_name).await?; if method == "CONNECT" { client.write_all(b"HTTP/1.1 200 Connection Established\r\nProxy-Agent: ostp/1.0\r\n\r\n").await?; diff --git a/ostp-client/src/tunnel/udp_nat.rs b/ostp-client/src/tunnel/udp_nat.rs new file mode 100644 index 0000000..51d4ecf --- /dev/null +++ b/ostp-client/src/tunnel/udp_nat.rs @@ -0,0 +1,145 @@ +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::sync::{mpsc, Mutex}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpStream, UdpSocket}; +use futures::StreamExt; + +pub async fn run_udp_nat( + udp_socket: netstack_smoltcp::UdpSocket, + proxy_addr: String, + debug: bool, +) { + let (mut rx, tx) = udp_socket.split(); + let tx = Arc::new(Mutex::new(tx)); + + // map from internal client src to a channel that sends (payload, external_dst) + let mut sessions: HashMap, SocketAddr)>> = HashMap::new(); + + while let Some((payload, src, dst)) = rx.next().await { + if payload.is_empty() { continue; } + + if !sessions.contains_key(&src) { + let (session_tx, mut session_rx) = mpsc::channel::<(Vec, SocketAddr)>(128); + sessions.insert(src, session_tx); + + let proxy_addr_clone = proxy_addr.clone(); + let tx_clone = tx.clone(); + + tokio::spawn(async move { + if debug { tracing::info!("Starting UDP NAT session for {}", src); } + let res = start_udp_session(src, proxy_addr_clone, &mut session_rx, tx_clone).await; + if debug && res.is_err() { + tracing::info!("UDP NAT session for {} ended: {:?}", src, res.err()); + } + }); + } + + if let Some(sender) = sessions.get(&src) { + if sender.send((payload, dst)).await.is_err() { + sessions.remove(&src); + } + } + } +} + +async fn start_udp_session( + client_src: SocketAddr, + proxy_addr: String, + session_rx: &mut mpsc::Receiver<(Vec, SocketAddr)>, + smoltcp_tx: Arc>, +) -> anyhow::Result<()> { + // 1. TCP Connect to SOCKS5 proxy + let mut tcp = TcpStream::connect(&proxy_addr).await?; + + // Auth + tcp.write_all(&[5, 1, 0]).await?; + let mut buf = [0u8; 2]; + tcp.read_exact(&mut buf).await?; + if buf[0] != 5 || buf[1] != 0 { + return Err(anyhow::anyhow!("socks5 auth rejected")); + } + + // UDP ASSOCIATE to 0.0.0.0:0 + tcp.write_all(&[5, 3, 0, 1, 0, 0, 0, 0, 0, 0]).await?; + let mut rep = [0u8; 10]; + tcp.read_exact(&mut rep).await?; + if rep[1] != 0 { + return Err(anyhow::anyhow!("socks5 udp associate rejected")); + } + + // Parse BND.ADDR and BND.PORT + let relay_ip = std::net::Ipv4Addr::new(rep[4], rep[5], rep[6], rep[7]); + let relay_port = u16::from_be_bytes([rep[8], rep[9]]); + let mut relay_addr = SocketAddr::new(std::net::IpAddr::V4(relay_ip), relay_port); + + // If proxy returned 0.0.0.0, use the proxy's IP + if relay_ip.is_unspecified() { + if let Ok(proxy_sock) = proxy_addr.parse::() { + relay_addr.set_ip(proxy_sock.ip()); + } + } + + let udp = UdpSocket::bind("127.0.0.1:0").await?; + + let mut buf = vec![0u8; 65536]; + + let timeout = std::time::Duration::from_secs(300); // 5 min idle timeout + let mut tcp_buf = [0u8; 1]; + + loop { + tokio::select! { + res = tokio::time::timeout(timeout, session_rx.recv()) => { + match res { + Ok(Some((payload, dst))) => { + let mut packet = vec![0u8; 3]; // RSV, FRAG + match dst.ip() { + std::net::IpAddr::V4(v4) => { packet.push(1); packet.extend_from_slice(&v4.octets()); } + std::net::IpAddr::V6(v6) => { packet.push(4); packet.extend_from_slice(&v6.octets()); } + } + packet.extend_from_slice(&dst.port().to_be_bytes()); + packet.extend_from_slice(&payload); + udp.send_to(&packet, relay_addr).await?; + } + Ok(None) => break, + Err(_) => break, // timeout + } + } + res = udp.recv_from(&mut buf) => { + let (len, _peer) = res?; + if len < 10 { continue; } // At least 10 bytes for SOCKS5 header + let frag = buf[2]; + if frag != 0 { continue; } // fragment not supported + let atyp = buf[3]; + let (header_len, remote_dst) = match atyp { + 1 => { + if len < 10 { continue; } + let ip = std::net::Ipv4Addr::new(buf[4], buf[5], buf[6], buf[7]); + let port = u16::from_be_bytes([buf[8], buf[9]]); + (10, SocketAddr::new(std::net::IpAddr::V4(ip), port)) + } + 4 => { + if len < 22 { continue; } + let mut octets = [0u8; 16]; + octets.copy_from_slice(&buf[4..20]); + let ip = std::net::Ipv6Addr::from(octets); + let port = u16::from_be_bytes([buf[20], buf[21]]); + (22, SocketAddr::new(std::net::IpAddr::V6(ip), port)) + } + _ => continue, // Domain name not supported for incoming packets in typical UDP associate + }; + let payload = buf[header_len..len].to_vec(); + use futures::SinkExt; + let _ = smoltcp_tx.lock().await.send((payload, remote_dst, client_src)).await; + } + // If TCP drops, UDP association is over + res = tcp.read(&mut tcp_buf) => { + let n = res?; + if n == 0 { break; } + } + } + } + + Ok(()) +} diff --git a/ostp-client/src/tunnel/wintun_handler.rs b/ostp-client/src/tunnel/wintun_handler.rs index c7b9d14..2f0b400 100644 --- a/ostp-client/src/tunnel/wintun_handler.rs +++ b/ostp-client/src/tunnel/wintun_handler.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Result}; use tokio::sync::watch; + #[cfg(target_os = "windows")] pub async fn run_wintun_tunnel( config: crate::config::ClientConfig, @@ -115,7 +116,7 @@ pub async fn run_wintun_tunnel( ); // 4. Launch tun2socks + route setup IN PARALLEL to save ~3 seconds - let proxy_url = format!("http://{}", config.local_proxy.bind_addr); + let proxy_url = format!("socks5://{}", config.local_proxy.bind_addr); tracing::info!("Starting tun2socks (proxy={})", proxy_url); // Spawn tun2socks immediately — it creates the adapter on its own @@ -191,6 +192,7 @@ pub async fn run_wintun_tunnel( config.ostp.mtu ); + if let Some(ref dns) = config.dns_server { if !dns.is_empty() { tracing::info!("DNS server: {}", dns); @@ -199,7 +201,7 @@ pub async fn run_wintun_tunnel( )); } } - + let _ = tokio::task::spawn_blocking(move || { Command::new("powershell") .creation_flags(CREATE_NO_WINDOW) @@ -238,10 +240,12 @@ pub async fn run_wintun_tunnel( // 8. Wait for shutdown signal let _ = shutdown.changed().await; + + tracing::info!("Deactivating TUN tunnel..."); drop(_guard); tracing::info!("TUN tunnel stopped."); - + Ok(()) } diff --git a/ostp-core/src/protocol.rs b/ostp-core/src/protocol.rs index d74c751..b95e204 100644 --- a/ostp-core/src/protocol.rs +++ b/ostp-core/src/protocol.rs @@ -500,12 +500,12 @@ impl ProtocolMachine { let mut actions = Vec::new(); // ── Gap Recovery ────────────────────────────────────────────── - // If expected_recv_nonce hasn't advanced for 5+ seconds and there + // If expected_recv_nonce hasn't advanced for 500ms+ and there // are buffered frames waiting, the sender likely evicted the lost // frame from sent_history. Skip the gap to restore data flow. // This trades a small amount of data loss for connection liveness. if !self.reorder_buffer.is_empty() - && self.last_recv_advance.elapsed() > Duration::from_secs(5) + && self.last_recv_advance.elapsed() > Duration::from_millis(500) { if let Some(&first_buffered) = self.reorder_buffer.keys().next() { let skipped = first_buffered.saturating_sub(self.expected_recv_nonce); diff --git a/ostp-flutter/ostp-client-release.apk b/ostp-flutter/ostp-client-release.apk index 370c45e..3e81ca6 100644 Binary files a/ostp-flutter/ostp-client-release.apk and b/ostp-flutter/ostp-client-release.apk differ diff --git a/ostp-gui/src-tauri/Cargo.lock b/ostp-gui/src-tauri/Cargo.lock index 0e38548..b6b5f21 100644 --- a/ostp-gui/src-tauri/Cargo.lock +++ b/ostp-gui/src-tauri/Cargo.lock @@ -2632,7 +2632,7 @@ dependencies = [ [[package]] name = "ostp-client" -version = "0.2.61" +version = "0.2.66" dependencies = [ "anyhow", "base64 0.22.1", @@ -2662,7 +2662,7 @@ dependencies = [ [[package]] name = "ostp-core" -version = "0.2.61" +version = "0.2.66" dependencies = [ "anyhow", "bytes", diff --git a/ostp-gui/src/i18n.js b/ostp-gui/src/i18n.js index 5a744a7..a94422c 100644 --- a/ostp-gui/src/i18n.js +++ b/ostp-gui/src/i18n.js @@ -21,6 +21,8 @@ const translations = { ph_key: 'Enter secure access key', label_socks: 'Local Proxy Address', label_dns: 'Custom DNS Server', + label_owndns: 'Built-in Server DNS', + owndns_hint: 'Route DNS queries through the VPN server (10.1.0.1)', label_tun: 'TUN Tunnel Mode', tun_hint: 'Route all system traffic (Admin req.)', label_transport: 'Transport Protocol', @@ -66,6 +68,8 @@ const translations = { ph_key: 'Введите ключ доступа', label_socks: 'Адрес локального прокси', label_dns: 'DNS сервер', + label_owndns: 'Встроенный DNS сервера', + owndns_hint: 'Направлять DNS-запросы через VPN сервер (10.1.0.1)', label_tun: 'Режим TUN-туннеля', tun_hint: 'Направить весь трафик (нужны права администратора)', label_transport: 'Транспортный протокол', diff --git a/ostp-gui/src/index.html b/ostp-gui/src/index.html index addc692..f153aec 100644 --- a/ostp-gui/src/index.html +++ b/ostp-gui/src/index.html @@ -172,12 +172,24 @@ -
- -
- - + +
+
+ Built-in Server DNS + Route DNS through the VPN server (10.1.0.1)
+ +
+ + +
+ +
diff --git a/ostp-gui/src/main.js b/ostp-gui/src/main.js index ea4f002..5d55d91 100644 --- a/ostp-gui/src/main.js +++ b/ostp-gui/src/main.js @@ -41,6 +41,8 @@ const inServer = $('in-server'); const inKey = $('in-key'); const inSocks = $('in-socks'); const inDns = $('in-dns'); +const inOwndns = $('in-owndns'); +const groupCustomDns = $('group-custom-dns'); const inTransport = $('in-transport'); const inSni = $('in-stealth-sni'); const inPbk = $('in-pbk'); @@ -89,6 +91,13 @@ function showToast(msg, variant = '') { }, 2400); } +// ── DNS visibility ──────────────────────────────────────────────────────────── +function updateDnsVisibility() { + if (!groupCustomDns || !inOwndns) return; + groupCustomDns.style.display = inOwndns.checked ? 'none' : 'block'; +} + + // ── State machine ──────────────────────────────────────────────────────────── function setState(next) { if (appState === next) return; @@ -239,7 +248,14 @@ async function loadConfigIntoForm() { inMuxSessions.value = c.mux?.sessions || ''; groupTunStack.style.display = inTun.checked ? 'block' : 'none'; - inDns.value = c.tun?.dns || ''; + + // owndns: detect if saved dns is 10.1.0.1 + const savedDns = c.tun?.dns || ''; + const isOwndns = savedDns === '10.1.0.1'; + inOwndns.checked = isOwndns; + inDns.value = isOwndns ? '' : savedDns; + updateDnsVisibility(); + inDebug.checked = !!c.debug; const ex = c.exclude || {}; @@ -306,7 +322,8 @@ async function handleSave(silent = false) { rawConfig.tun = { wintun_path: './wintun.dll', ipv4_address: '10.1.0.2/24' }; } rawConfig.tun.enable = inTun.checked; - rawConfig.tun.dns = inDns.value.trim() || null; + // owndns: if toggle is on, always write 10.1.0.1; otherwise use the custom field + rawConfig.tun.dns = inOwndns.checked ? '10.1.0.1' : (inDns.value.trim() || null); rawConfig.tun.stack = inTunStack.value; rawConfig.exclude = { @@ -367,6 +384,7 @@ function togglePeek() { window.addEventListener('DOMContentLoaded', async () => { applyTranslations(); setState('disconnected'); + updateDnsVisibility(); // initialise field visibility from current checkbox state // Event wiring if (window.__TAURI__ && window.__TAURI__.event) { @@ -381,15 +399,11 @@ window.addEventListener('DOMContentLoaded', async () => { btnBack.addEventListener('click', () => showScreen('home')); btnImport.addEventListener('click', handleImport); btnPeekKey.addEventListener('click', togglePeek); - const btnUseBuiltinDns = $('btn-use-builtin-dns'); - if (btnUseBuiltinDns) { - btnUseBuiltinDns.addEventListener('click', () => { - inDns.value = '10.1.0.1'; - saveConfig(); - showToast('DNS set to built-in server (10.1.0.1)', 'success'); - }); - } - inTun.addEventListener('change', () => { groupTunStack.style.display = inTun.checked ? 'block' : 'none'; }); + inOwndns.addEventListener('change', () => { + updateDnsVisibility(); + scheduleAutoSave(); + }); + inTun.addEventListener('change', () => { groupTunStack.style.display = inTun.checked ? 'block' : 'none'; }); importInput.addEventListener('keydown', e => { if (e.key === 'Enter') handleImport(); }); // Auto-save wiring