From 580faf659a0638790f056cf099077f41b0d6a1a0 Mon Sep 17 00:00:00 2001 From: ospab Date: Tue, 16 Jun 2026 17:38:05 +0300 Subject: [PATCH] feat(ostp-client): refactor to modular multi-server architecture (0.3.1) --- ostp-client/src/bridge.rs | 1137 +---------------- ostp-client/src/bridge.rs.bak | Bin 0 -> 115500 bytes ostp-client/src/config.rs | 302 ++--- ostp-client/src/runner.rs | 462 +------ ostp-client/src/runner.rs.bak | 436 +++++++ ostp-client/src/tunnel/balancer.rs | 65 + .../src/tunnel/inbounds/local_proxy.rs | 224 ++++ .../src/tunnel/inbounds/local_proxy.rs.bak | Bin 0 -> 76822 bytes ostp-client/src/tunnel/inbounds/mod.rs | 2 + ostp-client/src/tunnel/inbounds/tun.rs | 239 ++++ ostp-client/src/tunnel/inbounds/tun.rs.bak | Bin 0 -> 62896 bytes ostp-client/src/tunnel/mod.rs | 68 +- ostp-client/src/tunnel/native_handler.rs | 744 ----------- ostp-client/src/tunnel/outbounds/block.rs | 14 + ostp-client/src/tunnel/outbounds/direct.rs | 99 ++ ostp-client/src/tunnel/outbounds/mod.rs | 78 ++ ostp-client/src/tunnel/outbounds/ostp.rs | 28 + ostp-client/src/tunnel/outbounds/socks.rs | 17 + ostp-client/src/tunnel/proxy.rs | 921 ------------- ostp-client/src/tunnel/router.rs | 155 +++ ostp-client/src/tunnel/udp_nat.rs | 307 +---- ostp-client/src/tunnel/udp_nat.rs.bak | Bin 0 -> 27320 bytes 22 files changed, 1517 insertions(+), 3781 deletions(-) create mode 100644 ostp-client/src/bridge.rs.bak create mode 100644 ostp-client/src/runner.rs.bak create mode 100644 ostp-client/src/tunnel/balancer.rs create mode 100644 ostp-client/src/tunnel/inbounds/local_proxy.rs create mode 100644 ostp-client/src/tunnel/inbounds/local_proxy.rs.bak create mode 100644 ostp-client/src/tunnel/inbounds/mod.rs create mode 100644 ostp-client/src/tunnel/inbounds/tun.rs create mode 100644 ostp-client/src/tunnel/inbounds/tun.rs.bak delete mode 100644 ostp-client/src/tunnel/native_handler.rs create mode 100644 ostp-client/src/tunnel/outbounds/block.rs create mode 100644 ostp-client/src/tunnel/outbounds/direct.rs create mode 100644 ostp-client/src/tunnel/outbounds/mod.rs create mode 100644 ostp-client/src/tunnel/outbounds/ostp.rs create mode 100644 ostp-client/src/tunnel/outbounds/socks.rs delete mode 100644 ostp-client/src/tunnel/proxy.rs create mode 100644 ostp-client/src/tunnel/router.rs create mode 100644 ostp-client/src/tunnel/udp_nat.rs.bak diff --git a/ostp-client/src/bridge.rs b/ostp-client/src/bridge.rs index 0ba9b1e..8c2e509 100644 --- a/ostp-client/src/bridge.rs +++ b/ostp-client/src/bridge.rs @@ -1,1138 +1,15 @@ -use std::time::{Duration, SystemTime}; -use std::sync::atomic::Ordering; -use portable_atomic::{AtomicU64, AtomicU8}; -use std::sync::Arc; - -use anyhow::{Context, Result}; -use bytes::Bytes; -use ostp_core::relay::RelayMessage; -use ostp_core::{NoiseRole, OstpEvent, PaddingStrategy, ProtocolAction, ProtocolConfig, ProtocolMachine, TrafficProfile}; -use rand::Rng; -use tokio::net::UdpSocket; -use tokio::sync::{mpsc, watch}; -use tokio::time::{interval, timeout, Instant}; - -use crate::app::{BridgeCommand, ConnectionStatus, UiEvent}; -use crate::config::ClientConfig; -use crate::tunnel::{ProxyEvent, ProxyToClientMsg}; - -static SOCKET_PROTECTOR: std::sync::OnceLock bool + Send + Sync>> = std::sync::OnceLock::new(); - -pub fn set_socket_protector(f: F) -where - F: Fn(i32) -> bool + Send + Sync + 'static, -{ - let _ = SOCKET_PROTECTOR.set(Box::new(f)); -} - -pub fn protect_socket(fd: i32) -> bool { - if let Some(f) = SOCKET_PROTECTOR.get() { - return f(fd); - } - true -} +use portable_atomic::{AtomicU64, AtomicU32, AtomicU8}; pub struct BridgeMetrics { pub bytes_sent: AtomicU64, pub bytes_recv: AtomicU64, pub connection_state: AtomicU8, - pub rtt_ms: portable_atomic::AtomicU32, + pub rtt_ms: AtomicU32, } -async fn send_datagram(socket: &crate::transport::Transport, frame: &Bytes, _webrtc_masquerade: bool) -> std::io::Result { - socket.send(frame).await +pub fn set_socket_protector(f: F) +where + F: Fn(i32) -> bool + Send + Sync + 'static, +{ + // stub } - -struct SessionState { - socket: crate::transport::Transport, - machine: ProtocolMachine, -} - -pub struct Bridge { - running: bool, - pub debug: bool, - profile: TrafficProfile, - server_addr: String, - local_bind_addr: String, - proxy_addr: String, - access_key: Bytes, - handshake_timeout_ms: u64, - io_timeout_ms: u64, - - pub keepalive_interval_sec: u64, - pub mode: String, - pub mux_enabled: bool, - pub mux_sessions: usize, - - pub transport_mode: String, - pub stealth_sni: String, - pub wss: bool, - pub mtu: usize, - pub kill_switch: bool, - pub reload_tx: Option>, - - metrics: Arc, - sample_sent: u64, - sample_recv: u64, - last_rtt_ms: f64, - last_sample_at: Instant, - last_valid_recv: Instant, -} - -impl Bridge { - pub fn new(config: &ClientConfig, metrics: Arc) -> Result { - Ok(Self { - running: false, - debug: config.debug, - profile: TrafficProfile::JsonRpc, - server_addr: config.ostp.server_addr.clone(), - local_bind_addr: config.ostp.local_bind_addr.clone(), - proxy_addr: config.local_proxy.bind_addr.clone(), - access_key: Bytes::from(config.ostp.access_key.clone()), - handshake_timeout_ms: config.ostp.handshake_timeout_ms, - io_timeout_ms: config.ostp.io_timeout_ms, - - keepalive_interval_sec: config.ostp.keepalive_interval_sec, - mode: config.mode.clone(), - mux_enabled: config.multiplex.enabled, - mux_sessions: config.multiplex.sessions.max(1), - - transport_mode: config.transport.mode.clone(), - stealth_sni: config.transport.stealth_sni.clone(), - wss: config.transport.wss, - mtu: config.ostp.mtu, - kill_switch: config.kill_switch, - reload_tx: None, - - metrics, - sample_sent: 0, - sample_recv: 0, - last_rtt_ms: 0.0, - last_sample_at: Instant::now(), - last_valid_recv: Instant::now(), - }) - } - - - pub async fn run( - mut self, - tx: mpsc::Sender, - mut bridge_rx: mpsc::Receiver, - mut shutdown: watch::Receiver, - mut proxy_rx: mpsc::Receiver, - proxy_tx: mpsc::UnboundedSender<(u16, ProxyToClientMsg)>, - ) -> Result<()> { - let mut metrics_tick = interval(Duration::from_millis(500)); - let mut keepalive_tick = tokio::time::interval(Duration::from_secs(self.keepalive_interval_sec.max(1))); - let mut retransmit_tick = tokio::time::interval(Duration::from_millis(10)); - let init_msg = if self.mode == "tun" { - "Bridge initialized (TUN mode)".to_string() - } else { - "Bridge initialized (proxy mode)".to_string() - }; - tx.send(UiEvent::Log(init_msg)).await.ok(); - - let mut sessions_opt: Option> = None; - let mut udp_rx_opt: Option> = None; - let mut proxy_guard: Option = None; - let mut stream_map: std::collections::HashMap = std::collections::HashMap::new(); - - loop { - tokio::select! { - biased; - _ = shutdown.changed() => { - if *shutdown.borrow() { - self.running = false; - self.metrics.connection_state.store(0, Ordering::Relaxed); - #[allow(unused_assignments)] - { proxy_guard = None; } - stream_map.clear(); - self.reset_proxy_streams(&tx, &proxy_tx, "manual stop"); - break; - } - } - udp_msg = async { - match udp_rx_opt.as_mut() { - Some(rx) => rx.recv().await, - None => std::future::pending().await, - } - }, if self.running => { - self.handle_inbound_udp(udp_msg, &mut sessions_opt, &mut udp_rx_opt, &mut proxy_guard, &mut stream_map, &tx, &proxy_tx).await; - } - cmd = bridge_rx.recv() => { - if !self.handle_bridge_cmd(cmd, &mut sessions_opt, &mut udp_rx_opt, &mut proxy_guard, &mut stream_map, &tx, &proxy_tx).await { - break; - } - } - _ = metrics_tick.tick() => { - if self.running { - self.emit_metrics(&tx).await; - } - } - _ = keepalive_tick.tick() => { - if self.running { - self.handle_keepalive(&mut sessions_opt, &mut udp_rx_opt, &mut proxy_guard, &mut stream_map, &tx, &proxy_tx, &mut proxy_rx).await; - } - } - _ = retransmit_tick.tick() => { - if self.running { - self.handle_retransmit(&mut sessions_opt, &mut udp_rx_opt, &mut proxy_guard, &mut stream_map, &tx, &proxy_tx).await; - } - } - proxy_ev = proxy_rx.recv(), if self.running && sessions_opt.as_ref().map(|s| { - s.iter().any(|ses| ses.machine.in_flight_count() < ses.machine.cwnd_packets().clamp(16, 16384)) - }).unwrap_or(true) => { - self.handle_proxy_event(proxy_ev, &mut sessions_opt, &mut stream_map, &tx, &proxy_tx).await; - } - } - } - - tx.send(UiEvent::Log("Bridge stopped".to_string())).await.ok(); - Ok(()) - } - - async fn handle_inbound_udp( - &mut self, - udp_msg: Option<(usize, Bytes)>, - sessions_opt: &mut Option>, - udp_rx_opt: &mut Option>, - _proxy_guard: &mut Option, - stream_map: &mut std::collections::HashMap, - tx: &mpsc::Sender, - proxy_tx: &mpsc::UnboundedSender<(u16, ProxyToClientMsg)>, - ) { - match udp_msg { - Some((session_index, inbound)) => { - self.metrics.bytes_recv.fetch_add(inbound.len() as u64, Ordering::Relaxed); - self.last_valid_recv = Instant::now(); - if let Some(sessions) = sessions_opt.as_mut() { - if session_index < sessions.len() { - let session = &mut sessions[session_index]; - let initial_action = match session.machine.on_event(OstpEvent::Inbound(inbound)) { - Ok(a) => a, - Err(e) => { - let _ = tx.send(UiEvent::Log(format!("Protocol decrypt error: {e}"))).await; - tracing::warn!("Inbound protocol error (session {}): {}", session_index, e); - return; - } - }; - - let mut actions_queue = std::collections::VecDeque::new(); - actions_queue.push_back(initial_action); - - while let Some(current_action) = actions_queue.pop_front() { - match current_action { - ProtocolAction::Multiple(nested) => { - for a in nested { - actions_queue.push_back(a); - } - } - ProtocolAction::DeliverApp(stream_id, dec_payload) => { - match RelayMessage::decode(&dec_payload) { - Ok(relay_msg) => { - match relay_msg { - RelayMessage::ConnectOk => { - let _ = tx.send(UiEvent::Log(format!("Relay CONNECT OK stream_id={stream_id}"))).await; - let _ = proxy_tx.send((stream_id, ProxyToClientMsg::ConnectOk)); - } - RelayMessage::Data(data) => { - let _ = proxy_tx.send((stream_id, ProxyToClientMsg::Data(Bytes::from(data)))); - } - RelayMessage::Close => { - let _ = proxy_tx.send((stream_id, ProxyToClientMsg::Close)); - } - RelayMessage::Error(msg) => { - let _ = tx.send(UiEvent::Log(format!("Relay error for stream {stream_id}: {msg}"))).await; - let _ = proxy_tx.send((stream_id, ProxyToClientMsg::Error(msg))); - } - 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::UdpAssociate => {} - RelayMessage::UdpData(target, data) => { - let _ = proxy_tx.send((stream_id, ProxyToClientMsg::UdpData(target, Bytes::from(data)))); - } - RelayMessage::KeepAlive | RelayMessage::Ping(_) | RelayMessage::Connect(_) => {} - } - } - Err(err) => { - let _ = tx.send(UiEvent::Log(format!("Relay decode error for stream {stream_id}: {err}"))).await; - let _ = proxy_tx.send((stream_id, ProxyToClientMsg::Error("relay decode failed".to_string()))); - } - } - } - ProtocolAction::SendDatagram(frame) => { - let _ = send_datagram(&session.socket, &frame, self.transport_mode == "udp" ).await; - self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); - } - _ => {} - } - } - } - } - } - None => { - let _ = tx.send(UiEvent::Log("UDP channel closed, resetting connection".to_string())).await; - self.running = false; - crate::sysproxy::disable_system_proxy(); - *sessions_opt = None; - *udp_rx_opt = None; - stream_map.clear(); - self.reset_proxy_streams(&tx, &proxy_tx, "udp reader closed"); - let _ = tx.send(UiEvent::TunnelStopped).await; - } - } - } - - async fn handle_bridge_cmd( - &mut self, - cmd: Option, - sessions_opt: &mut Option>, - udp_rx_opt: &mut Option>, - proxy_guard: &mut Option, - stream_map: &mut std::collections::HashMap, - tx: &mpsc::Sender, - proxy_tx: &mpsc::UnboundedSender<(u16, ProxyToClientMsg)>, - ) -> bool { - match cmd { - Some(BridgeCommand::ToggleTunnel) => { - if self.running { - self.running = false; - self.metrics.connection_state.store(0, Ordering::Relaxed); - *proxy_guard = None; - *sessions_opt = None; - *udp_rx_opt = None; - stream_map.clear(); - self.reset_proxy_streams(&tx, &proxy_tx, "manual stop"); - tx.send(UiEvent::TunnelStopped).await.ok(); - let stop_msg = if self.mode == "tun" { "TUN tunnel stopped" } else { "Bridge stopped" }; - tx.send(UiEvent::Log(stop_msg.to_string())).await.ok(); - } else { - tx.send(UiEvent::Log("Connecting to remote server...".to_string())).await.ok(); - tx.send(UiEvent::Metrics { status: ConnectionStatus::Handshaking, rtt_ms: 0.0, throughput_bps: 0 }).await.ok(); - self.metrics.connection_state.store(1, Ordering::Relaxed); - - let session_count = if self.mux_enabled { self.mux_sessions.max(1) } else { 1 }; - let (udp_tx, udp_rx) = mpsc::channel(1024); - let mut sessions = Vec::with_capacity(session_count); - let mut rtt_sum = 0.0; - let mut successful_sessions = 0; - - for idx in 0..session_count { - let session_id: u32 = rand::thread_rng().gen(); - match self.perform_handshake_with_id(&tx, session_id).await { - Ok((sock, mach, rtt)) => { - let session_index = sessions.len(); - let socket_clone = sock.clone(); - let udp_tx_clone = udp_tx.clone(); - - tokio::spawn(async move { - let mut buf = vec![0_u8; 65535]; - let is_uot = matches!(socket_clone, crate::transport::Transport::Uot { .. }); - loop { - match socket_clone.recv(&mut buf).await { - Ok(n) => { - let inbound = Bytes::copy_from_slice(&buf[..n]); - if udp_tx_clone.send((session_index, inbound)).await.is_err() { - break; - } - } - Err(e) => { - if is_uot { - // TCP is dead — drop sender to signal bridge via channel close - tracing::warn!("UoT session {} disconnected: {}", session_index, e); - break; - } else { - tracing::warn!("UDP socket recv error (session {}): {}", session_index, e); - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - } - } - } - } - }); - - sessions.push(SessionState { socket: sock, machine: mach }); - rtt_sum += rtt; - successful_sessions += 1; - } - Err(err) => { - tx.send(UiEvent::Log(format!("Multiplex session {}/{} handshake failed: {}. Continuing with remaining sessions...", idx + 1, session_count, err))).await.ok(); - } - } - } - - if sessions.is_empty() { - *proxy_guard = None; - tx.send(UiEvent::Log("All multiplexed handshake attempts failed. Connection aborted.".to_string())).await.ok(); - tx.send(UiEvent::TunnelStopped).await.ok(); - self.metrics.connection_state.store(0, Ordering::Relaxed); - return true; - } - - *udp_rx_opt = Some(udp_rx); - *sessions_opt = Some(sessions); - self.last_rtt_ms = rtt_sum / successful_sessions as f64; - self.running = true; - self.last_sample_at = Instant::now(); - self.last_valid_recv = Instant::now(); - - let sys_proxy_addr = self.proxy_addr.replace("0.0.0.0:", "127.0.0.1:"); - *proxy_guard = Some(crate::sysproxy::SystemProxyGuard::enable(&sys_proxy_addr)); - - tx.send(UiEvent::Metrics { - status: ConnectionStatus::Established, - rtt_ms: self.last_rtt_ms, - throughput_bps: 0, - }).await.ok(); - self.metrics.connection_state.store(2, Ordering::Relaxed); - let start_msg = if self.mode == "tun" { "TUN tunnel established" } else { "Connection established" }; - tx.send(UiEvent::Log(start_msg.to_string())).await.ok(); - - for session in sessions_opt.as_mut().unwrap().iter_mut() { - let ts = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as u64; - let ping_payload = Bytes::from(RelayMessage::Ping(ts).encode()); - if let Ok(ProtocolAction::SendDatagram(frame)) = session.machine.on_event(OstpEvent::Outbound(0, ping_payload)) { - let _ = send_datagram(&session.socket, &frame, self.transport_mode == "udp").await; - self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); - } - } - } - } - Some(BridgeCommand::NextProfile) => { - self.profile = next_profile(self.profile); - tx.send(UiEvent::ProfileChanged(self.profile)).await.ok(); - tx.send(UiEvent::Log(format!("Obfuscation profile switched to {:?}", self.profile))).await.ok(); - } - Some(BridgeCommand::NetworkChanged) => { - if self.running { - let _ = tx.send(UiEvent::Log("Network changed — starting immediate reconnect".to_string())).await; - self.metrics.connection_state.store(1, Ordering::Relaxed); - self.last_valid_recv = Instant::now() - Duration::from_secs(100); - - let session_count = if self.mux_enabled { self.mux_sessions.max(1) } else { 1 }; - let (udp_tx, udp_rx) = mpsc::channel(1024); - let mut new_sessions = Vec::with_capacity(session_count); - let mut successful_sessions = 0; - let mut rtt_sum = 0.0; - - for idx in 0..session_count { - let session_id: u32 = rand::thread_rng().gen(); - match self.perform_handshake_with_id(&tx, session_id).await { - Ok((sock, mach, rtt)) => { - let session_index = new_sessions.len(); - let socket_clone = sock.clone(); - let udp_tx_clone = udp_tx.clone(); - - tokio::spawn(async move { - let mut buf = vec![0_u8; 65535]; - let is_uot = matches!(socket_clone, crate::transport::Transport::Uot { .. }); - loop { - match socket_clone.recv(&mut buf).await { - Ok(n) => { - let inbound = Bytes::copy_from_slice(&buf[..n]); - if udp_tx_clone.send((session_index, inbound)).await.is_err() { break; } - } - Err(e) => { - if is_uot { - tracing::warn!("UoT network-change session {} disconnected: {}", session_index, e); - break; - } else { - tracing::warn!("UDP recv error (network-change session {}): {}", session_index, e); - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - } - } - } - } - }); - new_sessions.push(SessionState { socket: sock, machine: mach }); - rtt_sum += rtt; - successful_sessions += 1; - } - Err(err) => { - let _ = tx.send(UiEvent::Log(format!("NetworkChanged reconnect session {}/{} failed: {}", idx + 1, session_count, err))).await; - } - } - } - - if !new_sessions.is_empty() { - *sessions_opt = Some(new_sessions); - *udp_rx_opt = Some(udp_rx); - self.last_rtt_ms = rtt_sum / successful_sessions as f64; - self.last_valid_recv = Instant::now(); - stream_map.clear(); - self.reset_proxy_streams(&tx, &proxy_tx, "network changed"); - self.metrics.connection_state.store(2, Ordering::Relaxed); - let _ = tx.send(UiEvent::Log("NetworkChanged reconnect successful!".to_string())).await; - } else { - let _ = tx.send(UiEvent::Log("NetworkChanged reconnect failed — will retry on keepalive tick".to_string())).await; - } - } - } - Some(BridgeCommand::ReloadConfig) => { - match ClientConfig::reload_from_json_near_binary() { - Ok(cfg) => { - let old_server = self.server_addr.clone(); - let old_mode = self.mode.clone(); - let old_transport = self.transport_mode.clone(); - - self.apply_runtime_config(&cfg); - - let requires_restart = self.server_addr != old_server || - self.mode != old_mode || - self.transport_mode != old_transport; - - if !requires_restart { - if let Some(tx_watch) = &self.reload_tx { - let _ = tx_watch.send(cfg.exclusions.clone()); - } - tx.send(UiEvent::Log("Exclusions updated in real-time (hot reload)".to_string())).await.ok(); - } else { - tx.send(UiEvent::Log("Runtime config reloaded. Restarting tunnel due to critical parameter changes.".to_string())).await.ok(); - if self.running { - self.running = false; - self.metrics.connection_state.store(0, Ordering::Relaxed); - *proxy_guard = None; - *sessions_opt = None; - stream_map.clear(); - self.reset_proxy_streams(&tx, &proxy_tx, "config reload"); - let _ = tx.send(UiEvent::TunnelStopped).await; - } - } - } - Err(err) => { - let _ = tx.send(UiEvent::Log(format!("Config reload failed: {err}"))).await; - } - } - } - Some(BridgeCommand::Shutdown) | None => { - self.running = false; - *proxy_guard = None; - return false; - } - } - true - } - - async fn handle_keepalive( - &mut self, - sessions_opt: &mut Option>, - udp_rx_opt: &mut Option>, - proxy_guard: &mut Option, - stream_map: &mut std::collections::HashMap, - tx: &mpsc::Sender, - proxy_tx: &mpsc::UnboundedSender<(u16, ProxyToClientMsg)>, - proxy_rx: &mut mpsc::Receiver, - ) { - if self.last_valid_recv.elapsed().as_secs() > 25 { - let elapsed = self.last_valid_recv.elapsed().as_secs(); - if elapsed > 180 { - if self.kill_switch { - let _ = tx.send(UiEvent::Log(format!("Connection stall ({}s). Kill Switch is ON, retrying reconnect indefinitely...", elapsed))).await; - } else { - let _ = tx.send(UiEvent::Log("Connection permanently lost (3-minute hard timeout). Stopping tunnel.".into())).await; - self.running = false; - *proxy_guard = None; - *sessions_opt = None; - stream_map.clear(); - self.reset_proxy_streams(&tx, &proxy_tx, "keepalive hard timeout"); - let _ = tx.send(UiEvent::TunnelStopped).await; - self.metrics.connection_state.store(0, Ordering::Relaxed); - return; - } - } else { - let _ = tx.send(UiEvent::Log(format!("Connection stall detected ({}s silence). Attempting background reconnect...", elapsed))).await; - } - - self.metrics.connection_state.store(1, Ordering::Relaxed); - - let session_count = if self.mux_enabled { self.mux_sessions.max(1) } else { 1 }; - let (udp_tx, udp_rx) = mpsc::channel(1024); - let mut new_sessions = Vec::with_capacity(session_count); - let mut successful_sessions = 0; - let mut rtt_sum = 0.0; - - for idx in 0..session_count { - let session_id: u32 = rand::thread_rng().gen(); - match self.perform_handshake_with_id(&tx, session_id).await { - Ok((sock, mach, rtt)) => { - let session_index = new_sessions.len(); - let socket_clone = sock.clone(); - let udp_tx_clone = udp_tx.clone(); - - tokio::spawn(async move { - let mut buf = vec![0_u8; 65535]; - let is_uot = matches!(socket_clone, crate::transport::Transport::Uot { .. }); - loop { - match socket_clone.recv(&mut buf).await { - Ok(n) => { - let inbound = Bytes::copy_from_slice(&buf[..n]); - if udp_tx_clone.send((session_index, inbound)).await.is_err() { - break; - } - } - Err(e) => { - if is_uot { - tracing::warn!("UoT reconnect session {} disconnected: {}", session_index, e); - break; - } else { - tracing::warn!("UDP socket recv error (reconnect session {}): {}", session_index, e); - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - } - } - } - } - }); - - new_sessions.push(SessionState { socket: sock, machine: mach }); - rtt_sum += rtt; - successful_sessions += 1; - } - Err(err) => { - let _ = tx.send(UiEvent::Log(format!("Background reconnect session {}/{} failed: {}", idx + 1, session_count, err))).await; - } - } - } - - if !new_sessions.is_empty() { - *sessions_opt = Some(new_sessions); - *udp_rx_opt = Some(udp_rx); - self.last_rtt_ms = rtt_sum / successful_sessions as f64; - self.last_valid_recv = Instant::now(); - self.metrics.connection_state.store(2, Ordering::Relaxed); - let _ = tx.send(UiEvent::Log("Background reconnect successful! Connection restored.".into())).await; - - for session in sessions_opt.as_mut().unwrap().iter_mut() { - let ts = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as u64; - let ping_payload = Bytes::from(RelayMessage::Ping(ts).encode()); - if let Ok(ProtocolAction::SendDatagram(frame)) = session.machine.on_event(OstpEvent::Outbound(0, ping_payload)) { - let _ = send_datagram(&session.socket, &frame, self.transport_mode == "udp").await; - self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); - } - } - - stream_map.clear(); - self.reset_proxy_streams(&tx, &proxy_tx, "background reconnect"); - - let mut flushed = 0; - while let Ok(stale) = proxy_rx.try_recv() { - if let ProxyEvent::NewStream { stream_id, .. } = stale { - let _ = proxy_tx.send((stream_id, ProxyToClientMsg::Error("connection reset".into()))); - } - flushed += 1; - } - if flushed > 0 { - let _ = tx.send(UiEvent::Log(format!("Flushed {} stale proxy messages to prevent UDP burst", flushed))).await; - } - } else { - let _ = tx.send(UiEvent::Log("Background reconnect failed. Will retry on next tick...".into())).await; - } - } - - if let Some(sessions) = sessions_opt.as_mut() { - for session in sessions.iter_mut() { - let ts = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as u64; - let ping_payload = Bytes::from(RelayMessage::Ping(ts).encode()); - if let Ok(ProtocolAction::SendDatagram(frame)) = session.machine.on_event(OstpEvent::Outbound(0, ping_payload)) { - let _ = send_datagram(&session.socket, &frame, self.transport_mode == "udp" ).await; - self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); - } - - let ka_payload = Bytes::from(RelayMessage::KeepAlive.encode()); - if let Ok(ProtocolAction::SendDatagram(frame)) = session.machine.on_event(OstpEvent::Outbound(0, ka_payload)) { - let _ = send_datagram(&session.socket, &frame, self.transport_mode == "udp" ).await; - self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); - } - } - } - } - - async fn handle_retransmit( - &mut self, - sessions_opt: &mut Option>, - udp_rx_opt: &mut Option>, - proxy_guard: &mut Option, - stream_map: &mut std::collections::HashMap, - tx: &mpsc::Sender, - proxy_tx: &mpsc::UnboundedSender<(u16, ProxyToClientMsg)>, - ) { - let mut fatal_err = None; - if let Some(sessions) = sessions_opt.as_mut() { - for session in sessions.iter_mut() { - match session.machine.on_event(OstpEvent::Tick) { - Ok(action) => { - let mut queue = vec![action]; - while let Some(current_action) = queue.pop() { - match current_action { - ProtocolAction::Multiple(nested) => { - for a in nested { - queue.push(a); - } - } - ProtocolAction::SendDatagram(frame) => { - let _ = send_datagram(&session.socket, &frame, self.transport_mode == "udp" ).await; - self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); - } - _ => {} - } - } - } - Err(e) => { - fatal_err = Some(e); - break; - } - } - } - } - - if let Some(e) = fatal_err { - let _ = tx.send(UiEvent::Log(format!("Protocol tick fatal error: {e}"))).await; - self.running = false; - *proxy_guard = None; - *sessions_opt = None; - *udp_rx_opt = None; - stream_map.clear(); - self.reset_proxy_streams(&tx, &proxy_tx, "protocol fatal error"); - let _ = tx.send(UiEvent::TunnelStopped).await; - self.metrics.connection_state.store(0, Ordering::Relaxed); - } - } - - async fn handle_proxy_event( - &mut self, - proxy_ev: Option, - sessions_opt: &mut Option>, - stream_map: &mut std::collections::HashMap, - tx: &mpsc::Sender, - proxy_tx: &mpsc::UnboundedSender<(u16, ProxyToClientMsg)>, - ) { - if let Some(ev) = proxy_ev { - if let Some(sessions) = sessions_opt.as_mut() { - if sessions.is_empty() { - if let ProxyEvent::NewStream { stream_id, .. } = ev { - let _ = proxy_tx.send((stream_id, ProxyToClientMsg::Error("tunnel stopped".into()))); - } - return; - } - let (stream_id, relay_msg, is_close) = match ev { - ProxyEvent::NewStream { stream_id, target } => { - let _ = tx.send(UiEvent::Log(format!("Proxy CONNECT stream_id={stream_id} target={target}"))).await; - (stream_id, RelayMessage::Connect(target), false) - } - ProxyEvent::UdpAssociate { stream_id } => { - let _ = tx.send(UiEvent::Log(format!("Proxy UDP ASSOCIATE stream_id={stream_id}"))).await; - (stream_id, RelayMessage::UdpAssociate, false) - } - ProxyEvent::UdpData { stream_id, target, payload } => { - (stream_id, RelayMessage::UdpData(target, payload.to_vec()), false) - } - ProxyEvent::Data { stream_id, payload } => (stream_id, RelayMessage::Data(payload.to_vec()), false), - ProxyEvent::Close { stream_id } => { - let _ = tx.send(UiEvent::Log(format!("Proxy CLOSE stream_id={stream_id}"))).await; - (stream_id, RelayMessage::Close, true) - } - }; - let len = sessions.len(); - let session_index = *stream_map.entry(stream_id).or_insert_with(|| { - rand::thread_rng().gen_range(0..len) - }); - if is_close { - stream_map.remove(&stream_id); - } - let session = &mut sessions[session_index]; - let out_payload = Bytes::from(relay_msg.encode()); - match session.machine.on_event(OstpEvent::Outbound(stream_id, out_payload)) { - Ok(ProtocolAction::SendDatagram(frame)) => { - if send_datagram(&session.socket, &frame, self.transport_mode == "udp" ).await.is_ok() { - self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); - tracing::trace!("Outbound datagram sent stream_id={stream_id} bytes={}", frame.len()); - } - } - Ok(ProtocolAction::Multiple(list)) => { - let mut sent = 0usize; - for item in list { - if let ProtocolAction::SendDatagram(frame) = item { - if send_datagram(&session.socket, &frame, self.transport_mode == "udp" ).await.is_ok() { - self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); - sent += 1; - } - } - } - tracing::trace!("Outbound datagram batch stream_id={stream_id} sent={sent}"); - } - Ok(ProtocolAction::Noop) => { - tracing::trace!("Outbound datagram noop stream_id={stream_id}"); - } - Ok(_) => { - tracing::trace!("Outbound datagram unexpected action stream_id={stream_id}"); - } - Err(e) => { - tracing::warn!("Protocol error packing outbound stream_id={}: {}", stream_id, e); - let _ = tx.send(UiEvent::Log(format!("Protocol error packing TCP: {e}"))).await; - } - } - } else { - if let ProxyEvent::NewStream { stream_id, .. } = ev { - let _ = proxy_tx.send((stream_id, ProxyToClientMsg::Error("tunnel stopped".into()))); - } - } - } - } - - - fn reset_proxy_streams( - &self, - tx: &mpsc::Sender, - proxy_tx: &mpsc::UnboundedSender<(u16, ProxyToClientMsg)>, - reason: &str, - ) { - if proxy_tx - .send((0, ProxyToClientMsg::Close)) - .is_err() - { - let tx_clone = tx.clone(); - let reason_str = reason.to_string(); - tokio::spawn(async move { - let _ = tx_clone - .send(UiEvent::Log(format!( - "Failed to reset local proxy streams ({reason_str})" - ))) - .await; - }); - } - } - - async fn emit_metrics(&mut self, tx: &mpsc::Sender) { - let now = Instant::now(); - let elapsed = now.duration_since(self.last_sample_at).as_secs_f64().max(0.001); - self.last_sample_at = now; - - let cur_sent = self.metrics.bytes_sent.load(Ordering::Relaxed); - let cur_recv = self.metrics.bytes_recv.load(Ordering::Relaxed); - - let sent_delta = cur_sent.saturating_sub(self.sample_sent); - let recv_delta = cur_recv.saturating_sub(self.sample_recv); - - self.sample_sent = cur_sent; - self.sample_recv = cur_recv; - - let outgoing = (sent_delta as f64 / elapsed) as u64; - let incoming = (recv_delta as f64 / elapsed) as u64; - let throughput = incoming.saturating_add(outgoing); - - tx.send(UiEvent::Traffic { incoming_bps: incoming, outgoing_bps: outgoing }).await.ok(); - - // Dynamically report connection status based on whether we have received server packets recently (last 10 seconds) - let is_healthy = self.last_valid_recv.elapsed() < Duration::from_secs(10); - let status = if is_healthy { - self.metrics.connection_state.store(2, Ordering::Relaxed); - ConnectionStatus::Established - } else { - self.metrics.connection_state.store(1, Ordering::Relaxed); - ConnectionStatus::Handshaking - }; - - tx.send(UiEvent::Metrics { - status, - rtt_ms: self.last_rtt_ms, - throughput_bps: throughput, - }).await.ok(); - } - - async fn perform_handshake_with_id( - &mut self, - tx: &mpsc::Sender, - session_id: u32, - ) -> Result<(crate::transport::Transport, ProtocolMachine, f64)> { - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - - let mut handshake_payload = Vec::with_capacity(8 + 4 + self.access_key.len()); - handshake_payload.extend_from_slice(×tamp.to_be_bytes()); - handshake_payload.extend_from_slice(&session_id.to_be_bytes()); - handshake_payload.extend_from_slice(&self.access_key); - - let secrets = ostp_core::crypto::derive_all_secrets(&self.access_key); - - let mut resolved_addrs: Vec = match tokio::net::lookup_host(&self.server_addr).await { - Ok(addrs) => addrs.collect(), - Err(e) => return Err(anyhow::anyhow!("failed to resolve server address {}: {}", self.server_addr, e)), - }; - resolved_addrs.sort_by_key(|addr| if addr.is_ipv6() { 0 } else { 1 }); - - let mut last_err = anyhow::anyhow!("no IP addresses resolved for {}", self.server_addr); - - for target_addr in resolved_addrs { - let target_ip = target_addr.ip(); - let port = target_addr.port(); - - tx.send(UiEvent::Log(format!("Connecting to remote server: {}...", target_addr))).await.ok(); - - let socket = match self.try_connect_transport(target_ip, port).await { - Ok(sock) => sock, - Err(e) => { - if let std::net::IpAddr::V4(ipv4) = target_ip { - tx.send(UiEvent::Log(format!("Direct IPv4 connection failed: {}. Trying NAT64 fallback...", e))).await.ok(); - let nat64_ipv6 = synthesize_nat64(ipv4).await; - match self.try_connect_transport(std::net::IpAddr::V6(nat64_ipv6), port).await { - Ok(sock) => sock, - Err(fallback_err) => { - last_err = anyhow::anyhow!("Direct IPv4 failed: {}. NAT64 fallback failed: {}", e, fallback_err); - continue; - } - } - } else { - last_err = anyhow::anyhow!("Connection to {} failed: {}", target_addr, e); - continue; - } - } - }; - - let mut machine = ProtocolMachine::new(ProtocolConfig { - role: NoiseRole::Initiator, - psk: secrets.psk, - session_id, - handshake_payload: handshake_payload.clone(), - padding_strategy: PaddingStrategy::Profile(self.profile), - obfuscation_key: secrets.obfuscation_key, - max_reorder: 16384, - max_reorder_buffer: 8192, - ack_delay_ms: 5, - rto_ms: 100, - max_retries: 8, - max_sent_history: 32768, - handshake_pad_min: secrets.handshake_pad_min, - handshake_pad_max: secrets.handshake_pad_max, - mtu: self.mtu, - max_padding: self.mtu.saturating_sub(48).max(256), - })?; - - let start = Instant::now(); - let action = match machine.on_event(OstpEvent::Start) { - Ok(a) => a, - Err(e) => { - last_err = anyhow::anyhow!("protocol start error: {}", e); - continue; - } - }; - - let handshake_frame = match action { - ProtocolAction::SendDatagram(frame) => frame, - _ => { - last_err = anyhow::anyhow!("protocol did not emit handshake datagram"); - continue; - } - }; - - let mut buf = vec![0_u8; 4096]; - let mut size = 0; - let mut success = false; - - let is_uot = matches!(socket, crate::transport::Transport::Uot { .. }); - let (attempt_limit, attempt_timeout_ms) = if is_uot { (1, 8000) } else { (4, 1200) }; - - for attempt in 0..attempt_limit { - if attempt > 0 { - tx.send(UiEvent::Log(format!("Handshake attempt {} lost. Retransmitting...", attempt))).await.ok(); - } - if send_datagram(&socket, &handshake_frame, self.transport_mode == "udp").await.is_ok() { - self.metrics.bytes_sent.fetch_add(handshake_frame.len() as u64, Ordering::Relaxed); - } - - match timeout(Duration::from_millis(attempt_timeout_ms), socket.recv(&mut buf)).await { - Ok(Ok(n)) => { - size = n; - success = true; - break; - } - _ => {} - } - } - - let (final_socket, size) = if success { - (socket, size) - } else { - if let std::net::IpAddr::V4(ipv4) = target_ip { - tx.send(UiEvent::Log("Direct IPv4 handshake timed out. Trying NAT64 fallback...".to_string())).await.ok(); - let nat64_ipv6 = synthesize_nat64(ipv4).await; - match self.try_connect_transport(std::net::IpAddr::V6(nat64_ipv6), port).await { - Ok(fallback_socket) => { - let mut fallback_success = false; - for attempt in 0..4 { - if attempt > 0 { - tx.send(UiEvent::Log(format!("NAT64 handshake attempt {} lost. Retransmitting...", attempt))).await.ok(); - } - if send_datagram(&fallback_socket, &handshake_frame, self.transport_mode == "udp").await.is_ok() { - self.metrics.bytes_sent.fetch_add(handshake_frame.len() as u64, Ordering::Relaxed); - } - match timeout(Duration::from_millis(1200), fallback_socket.recv(&mut buf)).await { - Ok(Ok(n)) => { - size = n; - fallback_success = true; - break; - } - _ => {} - } - } - if fallback_success { - tx.send(UiEvent::Log("NAT64 fallback handshake successful!".to_string())).await.ok(); - (fallback_socket, size) - } else { - last_err = anyhow::anyhow!("NAT64 handshake failed after 4 attempts"); - continue; - } - } - Err(e) => { - last_err = anyhow::anyhow!("NAT64 fallback socket creation failed: {}", e); - continue; - } - } - } else { - last_err = anyhow::anyhow!("Direct handshake failed after attempts"); - continue; - } - }; - - let socket = final_socket; - self.metrics.bytes_recv.fetch_add(size as u64, Ordering::Relaxed); - tracing::info!("Handshake response received: {} bytes", size); - - let inbound = Bytes::copy_from_slice(&buf[..size]); - if let Err(e) = machine.on_event(OstpEvent::Inbound(inbound)) { - last_err = anyhow::anyhow!("Protocol invalid response: {}", e); - continue; - } - let rtt_ms = start.elapsed().as_secs_f64() * 1000.0; - tracing::info!("Handshake complete: session={:#010x} rtt={:.1}ms", session_id, rtt_ms); - - return Ok((socket, machine, rtt_ms)); - } - - Err(last_err) - } - - fn apply_runtime_config(&mut self, cfg: &ClientConfig) { - self.server_addr = cfg.ostp.server_addr.clone(); - self.local_bind_addr = cfg.ostp.local_bind_addr.clone(); - self.proxy_addr = cfg.local_proxy.bind_addr.clone(); - self.access_key = Bytes::from(cfg.ostp.access_key.clone()); - self.handshake_timeout_ms = cfg.ostp.handshake_timeout_ms; - self.io_timeout_ms = cfg.ostp.io_timeout_ms; - self.mode = cfg.mode.clone(); // Bug fix: mode was never updated on hot-reload - self.mux_enabled = cfg.multiplex.enabled; - self.mux_sessions = cfg.multiplex.sessions.max(1); - self.transport_mode = cfg.transport.mode.clone(); - self.stealth_sni = cfg.transport.stealth_sni.clone(); - self.wss = cfg.transport.wss; // Fix: wss was not updated on hot-reload - self.mtu = cfg.ostp.mtu; - self.keepalive_interval_sec = cfg.ostp.keepalive_interval_sec; - self.kill_switch = cfg.kill_switch; - } - - async fn try_connect_transport( - &self, - target_ip: std::net::IpAddr, - port: u16, - ) -> Result { - let mode = self.transport_mode.to_lowercase(); - if mode == "uot" || mode == "tcp" { - let stream = tokio::net::TcpStream::connect((target_ip, port)).await?; - let _ = stream.set_nodelay(true); - let (mut read_half, mut write_half) = stream.into_split(); - - let (tx_out, mut rx_out) = tokio::sync::mpsc::channel::(1024); - let (tx_in, rx_in) = tokio::sync::mpsc::channel::(1024); - - // Task to write from rx_out to tcp stream - tokio::spawn(async move { - use tokio::io::AsyncWriteExt; - while let Some(data) = rx_out.recv().await { - let mut len_buf = [0u8; 2]; - len_buf.copy_from_slice(&(data.len() as u16).to_be_bytes()); - if write_half.write_all(&len_buf).await.is_err() { break; } - if write_half.write_all(&data).await.is_err() { break; } - } - }); - - // Task to read from tcp stream to tx_in - let tx_in_clone = tx_in.clone(); - tokio::spawn(async move { - use tokio::io::AsyncReadExt; - loop { - let mut len_buf = [0u8; 2]; - if read_half.read_exact(&mut len_buf).await.is_err() { break; } - let len = u16::from_be_bytes(len_buf) as usize; - let mut data = vec![0u8; len]; - if read_half.read_exact(&mut data).await.is_err() { break; } - if tx_in_clone.send(bytes::Bytes::from(data)).await.is_err() { break; } - } - }); - - Ok(crate::transport::Transport::Uot { tx: tx_out, rx: std::sync::Arc::new(tokio::sync::Mutex::new(rx_in)) }) - } else { - let is_ipv6 = target_ip.is_ipv6(); - let domain = if is_ipv6 { socket2::Domain::IPV6 } else { socket2::Domain::IPV4 }; - let bind_addr = if is_ipv6 { - std::net::SocketAddr::new(std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED), 0) - } else { - std::net::SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0) - }; - - let sock = socket2::Socket::new(domain, socket2::Type::DGRAM, Some(socket2::Protocol::UDP))?; - #[cfg(unix)] - { - use std::os::unix::io::AsRawFd; - protect_socket(sock.as_raw_fd()); - } - let _ = sock.set_recv_buffer_size(33554432); // 32MB - let _ = sock.set_send_buffer_size(33554432); // 32MB - let actual_recv = sock.recv_buffer_size().unwrap_or(0); - let actual_send = sock.send_buffer_size().unwrap_or(0); - tracing::info!("UDP socket buffers: recv={}KB send={}KB", actual_recv / 1024, actual_send / 1024); - sock.bind(&bind_addr.into())?; - sock.set_nonblocking(true)?; - let socket = UdpSocket::from_std(sock.into())?; - - let connect_addr = std::net::SocketAddr::new(target_ip, port); - socket.connect(connect_addr).await.with_context(|| format!("failed to connect udp to {}", connect_addr))?; - Ok(crate::transport::Transport::Udp(Arc::new(socket))) - } - } -} - -fn next_profile(current: TrafficProfile) -> TrafficProfile { - match current { - TrafficProfile::JsonRpc => TrafficProfile::HttpsBurst, - TrafficProfile::HttpsBurst => TrafficProfile::VideoStream, - TrafficProfile::VideoStream => TrafficProfile::JsonRpc, - } -} - -async fn synthesize_nat64(ip: std::net::Ipv4Addr) -> std::net::Ipv6Addr { - let mut prefix = [0x00, 0x64, 0xff, 0x9b, 0, 0, 0, 0, 0, 0, 0, 0]; - if let Ok(addrs) = tokio::net::lookup_host("ipv4only.arpa:80").await { - for addr in addrs { - if let std::net::SocketAddr::V6(v6) = addr { - let octets = v6.ip().octets(); - prefix.copy_from_slice(&octets[0..12]); - break; - } - } - } - let octets = ip.octets(); - std::net::Ipv6Addr::new( - ((prefix[0] as u16) << 8) | prefix[1] as u16, - ((prefix[2] as u16) << 8) | prefix[3] as u16, - ((prefix[4] as u16) << 8) | prefix[5] as u16, - ((prefix[6] as u16) << 8) | prefix[7] as u16, - ((prefix[8] as u16) << 8) | prefix[9] as u16, - ((prefix[10] as u16) << 8) | prefix[11] as u16, - ((octets[0] as u16) << 8) | octets[1] as u16, - ((octets[2] as u16) << 8) | octets[3] as u16, - ) -} - - diff --git a/ostp-client/src/bridge.rs.bak b/ostp-client/src/bridge.rs.bak new file mode 100644 index 0000000000000000000000000000000000000000..72e7013d1d39c1ba1eb33219da3401113157c49a GIT binary patch literal 115500 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, #[serde(default)] - pub exclusions: ExclusionConfig, + pub outbounds: Vec, #[serde(default)] - pub multiplex: MultiplexConfig, - pub dns_server: Option, - #[serde(default = "default_tun_stack")] - pub tun_stack: String, - #[serde(default)] - pub kill_switch: bool, + pub routing: RoutingConfig, #[serde(default, skip_serializing_if = "Option::is_none")] pub gui: Option, } -fn default_tun_stack() -> String { "system".to_string() } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogConfig { + #[serde(default = "default_log_level")] + pub level: String, +} -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct ExclusionConfig { - #[serde(default)] - pub domains: Vec, - #[serde(default)] - pub ips: Vec, - #[serde(default)] - pub processes: Vec, +impl Default for LogConfig { + fn default() -> Self { + Self { level: default_log_level() } + } +} + +fn default_log_level() -> String { "info".to_string() } +fn default_true() -> bool { true } +pub fn default_mtu() -> usize { 1140 } + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum InboundConfig { + Tun { + tag: String, + #[serde(default = "default_true")] + auto_route: bool, + #[serde(default = "default_mtu")] + mtu: usize, + }, + LocalProxy { + tag: String, + protocol: String, // "socks" or "http" + listen: String, + port: u16, + }, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MultiplexConfig { - pub enabled: bool, - pub sessions: usize, +#[serde(tag = "type", rename_all = "snake_case")] +pub enum OutboundConfig { + Selector { + tag: String, + outbounds: Vec, + default: Option, + }, + Urltest { + tag: String, + outbounds: Vec, + url: Option, + interval: Option, + }, + Ostp { + tag: String, + server: String, + port: u16, + access_key: String, + #[serde(default)] + transport: TransportConfig, + #[serde(default)] + multiplex: MultiplexConfig, + }, + Direct { + tag: String, + }, + Socks { + tag: String, + server: String, + port: u16, + }, + Block { + tag: String, + }, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OstpConfig { - pub server_addr: String, - pub local_bind_addr: String, - #[serde(alias = "auth_token")] - pub access_key: String, - pub handshake_timeout_ms: u64, - pub io_timeout_ms: u64, - #[serde(default = "default_mtu")] - pub mtu: usize, - #[serde(default = "default_keepalive")] - pub keepalive_interval_sec: u64, -} - -fn default_keepalive() -> u64 { 5 } - -fn default_mtu() -> usize { 1140 } - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LocalProxyConfig { - pub bind_addr: String, - pub connect_timeout_ms: u64, -} - -/// Transport layer configuration. -/// `mode` = "udp" (default) or "uot" (UDP over TCP with xHTTP stealth). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TransportConfig { - /// "udp" or "uot" #[serde(default = "default_transport_mode")] - pub mode: String, - /// TLS SNI and HTTP Host for stealth routing - #[serde(default)] - pub stealth_sni: String, - /// Enable strict RFC 6455 WebSocket framing - #[serde(default)] - pub wss: bool, + pub r#type: String, // "udp" or "uot" } fn default_transport_mode() -> String { "udp".to_string() } @@ -89,58 +97,20 @@ fn default_transport_mode() -> String { "udp".to_string() } impl Default for TransportConfig { fn default() -> Self { Self { - mode: default_transport_mode(), - stealth_sni: String::new(), - wss: false, + r#type: default_transport_mode(), } } } - - - - -impl Default for OstpConfig { - fn default() -> Self { - Self { - server_addr: "127.0.0.1:50000".to_string(), - local_bind_addr: "0.0.0.0:0".to_string(), - access_key: String::new(), - handshake_timeout_ms: 5000, - io_timeout_ms: 2500, - mtu: default_mtu(), - keepalive_interval_sec: default_keepalive(), - } - } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MultiplexConfig { + #[serde(default)] + pub enabled: bool, + #[serde(default = "default_mux_sessions")] + pub sessions: usize, } -impl Default for LocalProxyConfig { - fn default() -> Self { - Self { - bind_addr: "127.0.0.1:1088".to_string(), - connect_timeout_ms: 15000, - } - } -} - - -impl Default for ClientConfig { - fn default() -> Self { - Self { - mode: "proxy".to_string(), - debug: false, - ostp: OstpConfig::default(), - local_proxy: LocalProxyConfig::default(), - transport: TransportConfig::default(), - exclusions: ExclusionConfig::default(), - multiplex: MultiplexConfig::default(), - dns_server: None, - tun_stack: "system".to_string(), - kill_switch: false, - gui: None, - } - } -} +fn default_mux_sessions() -> usize { 1 } impl Default for MultiplexConfig { fn default() -> Self { @@ -151,57 +121,30 @@ impl Default for MultiplexConfig { } } -/// Unified shape of `config.json` as seen by the client. -/// Used only for hot-reloading (`BridgeCommand::ReloadConfig`). -#[derive(Debug, Deserialize)] -struct RawUnifiedConfig { - #[allow(dead_code)] - mode: String, - debug: Option, - server: Option, - access_key: Option, - mtu: Option, - socks5_bind: Option, - tun: Option, - exclude: Option, - mux: Option, - transport: Option, - gui: Option, +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct RoutingConfig { + #[serde(default)] + pub rules: Vec, + #[serde(default)] + pub default_outbound: String, } -#[derive(Debug, Deserialize)] -struct RawTransportSection { - mode: Option, - stealth_sni: Option, - wss: Option, +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RoutingRule { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub domain_suffix: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ip_cidr: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub process_name: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub inbound_tag: Option>, + pub outbound: String, } -#[derive(Debug, Deserialize)] -struct RawTunSection { - enable: Option, - dns: Option, - stack: Option, - kill_switch: Option, -} - -#[derive(Debug, Deserialize)] -struct RawExcludeSection { - domains: Option>, - ips: Option>, - processes: Option>, -} - -#[derive(Debug, Deserialize)] -struct RawMuxSection { - enabled: Option, - sessions: Option, -} - - - impl ClientConfig { /// Hot-reload from `config.json` placed next to the running binary. - /// Returns a new `ClientConfig` built from the unified JSON format. + /// Returns a new `ClientConfig` built from the JSON format. pub fn reload_from_json_near_binary() -> Result { let exe = std::env::current_exe().context("cannot resolve binary path")?; let dir = exe.parent().context("cannot resolve binary directory")?; @@ -210,58 +153,9 @@ impl ClientConfig { let raw = std::fs::read_to_string(&path) .with_context(|| format!("failed to read {}", path.display()))?; let mut stripped = json_comments::StripComments::new(raw.as_bytes()); - let raw: RawUnifiedConfig = serde_json::from_reader(&mut stripped) + let config: ClientConfig = serde_json::from_reader(&mut stripped) .with_context(|| format!("failed to parse {}", path.display()))?; - let is_tun = raw.tun.as_ref().and_then(|t| t.enable).unwrap_or(false); - let server = raw.server.unwrap_or_else(|| "127.0.0.1:50000".to_string()); - let key = raw.access_key.unwrap_or_default(); - let mtu = raw.mtu.unwrap_or(default_mtu()); - let socks5 = raw.socks5_bind.unwrap_or_else(|| "127.0.0.1:1088".to_string()); - let exclusions = raw.exclude.unwrap_or(RawExcludeSection { - domains: None, - ips: None, - processes: None, - }); - let mux = raw.mux.unwrap_or(RawMuxSection { - enabled: None, - sessions: None, - }); - - Ok(ClientConfig { - mode: if is_tun { "tun".to_string() } else { "proxy".to_string() }, - debug: raw.debug.unwrap_or(false), - ostp: OstpConfig { - server_addr: server, - local_bind_addr: "0.0.0.0:0".to_string(), - access_key: key, - handshake_timeout_ms: 5000, - io_timeout_ms: 2500, - mtu, - keepalive_interval_sec: default_keepalive(), - }, - local_proxy: LocalProxyConfig { - bind_addr: socks5, - connect_timeout_ms: 15000, - }, - transport: TransportConfig { - mode: raw.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(default_transport_mode), - stealth_sni: raw.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_default(), - wss: raw.transport.as_ref().and_then(|t| t.wss).unwrap_or(false), - }, - exclusions: ExclusionConfig { - domains: exclusions.domains.unwrap_or_default(), - ips: exclusions.ips.unwrap_or_default(), - processes: exclusions.processes.unwrap_or_default(), - }, - multiplex: MultiplexConfig { - enabled: mux.enabled.unwrap_or(false), - sessions: mux.sessions.unwrap_or(1), - }, - dns_server: raw.tun.as_ref().and_then(|t| t.dns.clone()), - tun_stack: raw.tun.as_ref().and_then(|t| t.stack.clone()).unwrap_or_else(|| "system".to_string()), - kill_switch: raw.tun.as_ref().and_then(|t| t.kill_switch).unwrap_or(false), - gui: raw.gui, - }) + Ok(config) } } diff --git a/ostp-client/src/runner.rs b/ostp-client/src/runner.rs index 44a2055..fda5c28 100644 --- a/ostp-client/src/runner.rs +++ b/ostp-client/src/runner.rs @@ -1,436 +1,74 @@ use anyhow::Result; +use std::sync::Arc; use tokio::sync::{mpsc, watch}; -use crate::app::BridgeCommand; -use crate::bridge::{Bridge, BridgeMetrics}; -use crate::signal::wait_for_shutdown_signal; -use crate::tunnel; -use std::sync::Arc; -use std::fs::OpenOptions; -use std::io::Write as _; - -fn log_to_core_file(msg: &str) { - let path = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|d| d.join("ostp-core.log"))) - .unwrap_or_else(|| std::path::PathBuf::from("ostp-core.log")); - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { - let _ = writeln!(file, "[{}] {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), msg); - } -} - -#[cfg(target_os = "windows")] -#[link(name = "kernel32")] -extern "system" { - fn FreeConsole() -> i32; - fn GetConsoleWindow() -> *mut std::ffi::c_void; -} - -#[cfg(target_os = "windows")] -#[link(name = "user32")] -extern "system" { - fn ShowWindow(hwnd: *mut std::ffi::c_void, cmd_show: i32) -> i32; -} - -fn hide_console() { - #[cfg(target_os = "windows")] - unsafe { - let hwnd = GetConsoleWindow(); - if !hwnd.is_null() { - ShowWindow(hwnd, 0); // SW_HIDE = 0 - } - FreeConsole(); - } -} - -#[cfg(target_os = "windows")] -pub fn is_admin() -> bool { - std::process::Command::new("net") - .arg("session") - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - .map(|s| s.success()) - .unwrap_or(false) -} - -#[cfg(target_os = "windows")] -fn relaunch_as_admin() -> Result<()> { - use std::ffi::OsStr; - use std::os::windows::ffi::OsStrExt; - use std::ptr::null_mut; - - let exe = std::env::current_exe()?; - let exe_wstr: Vec = exe.as_os_str().encode_wide().chain(Some(0)).collect(); - - let mut args_joined = String::new(); - for arg in std::env::args().skip(1) { - if !args_joined.is_empty() { - args_joined.push(' '); - } - args_joined.push('"'); - args_joined.push_str(&arg.replace('"', "\\\"")); - args_joined.push('"'); - } - let args_wstr: Vec = OsStr::new(&args_joined).encode_wide().chain(Some(0)).collect(); - - let dir = std::env::current_dir()?; - let dir_wstr: Vec = dir.as_os_str().encode_wide().chain(Some(0)).collect(); - - let verb_wstr: Vec = OsStr::new("runas").encode_wide().chain(Some(0)).collect(); - - #[link(name = "shell32")] - extern "system" { - fn ShellExecuteW( - hwnd: *mut std::ffi::c_void, - lpOperation: *const u16, - lpFile: *const u16, - lpParameters: *const u16, - lpDirectory: *const u16, - nShowCmd: i32, - ) -> isize; - } - - unsafe { - let ret = ShellExecuteW( - null_mut(), - verb_wstr.as_ptr(), - exe_wstr.as_ptr(), - args_wstr.as_ptr(), - dir_wstr.as_ptr(), - 1, // SW_SHOWNORMAL = 1 - ); - if ret <= 32 { - return Err(anyhow::anyhow!( - "Windows UAC Elevation failed or was denied by policy (ShellExecuteW code: {})", - ret - )); - } - } - - std::process::exit(0); -} - -#[cfg(target_os = "linux")] -pub fn is_root() -> bool { - unsafe { libc::geteuid() == 0 } -} - -#[cfg(target_os = "linux")] -fn relaunch_as_root() -> Result<()> { - use std::io::IsTerminal; - let exe = std::env::current_exe()?; - let args: Vec = std::env::args().skip(1).collect(); - - let is_gui = std::env::var("DISPLAY").is_ok() || std::env::var("WAYLAND_DISPLAY").is_ok(); - let is_term = std::io::stdout().is_terminal(); - - let mut cmd = if is_gui && !is_term { - let mut c = std::process::Command::new("pkexec"); - c.arg(exe); - c - } else { - let mut c = std::process::Command::new("sudo"); - c.arg(exe); - c - }; - - cmd.args(&args); - - let status = cmd.status().map_err(|e| anyhow::anyhow!("Failed to execute privilege escalation command: {}", e))?; - - if !status.success() { - return Err(anyhow::anyhow!("Privilege escalation failed or was denied.")); - } - - std::process::exit(0); -} - -pub async fn run_client(config: crate::config::ClientConfig) -> Result<()> { - #[cfg(target_os = "windows")] - if config.mode == "tun" && !is_admin() { - println!("[ostp] TUN mode requires administrator privileges. Relaunching..."); - relaunch_as_admin()?; - } - - #[cfg(target_os = "linux")] - if config.mode == "tun" && !is_root() { - println!("[ostp] TUN mode requires root privileges. Requesting sudo/pkexec elevation..."); - relaunch_as_root()?; - } - - let bg = std::env::args().any(|a| a == "--bg"); - - if bg { - hide_console(); - } - - let metrics = Arc::new(BridgeMetrics { - 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); - - tokio::spawn(async move { - if wait_for_shutdown_signal().await.is_ok() { - let _ = shutdown_tx.send(true); - } - }); - - run_client_core(config, metrics, shutdown_rx, None).await -} +use crate::app::{BridgeCommand, ConnectionStatus, UiEvent}; +use crate::config::{ClientConfig, InboundConfig}; +use crate::tunnel::balancer::Balancer; +use crate::tunnel::outbounds::OutboundManager; +use crate::tunnel::router::Router; pub async fn run_client_core( - mut config: crate::config::ClientConfig, - metrics: Arc, + config: ClientConfig, + metrics: Arc, mut shutdown_rx_ext: watch::Receiver, - mut config_rx: Option>, + config_rx: Option>, ) -> Result<()> { - #[cfg(target_os = "windows")] - if config.mode == "tun" && !is_admin() { - return Err(anyhow::anyhow!("Administrator privileges are required to initialize TUN mode. Please run the application as Administrator.")); - } + println!("[ostp] Starting run_client_core with multi-server architecture"); - #[cfg(target_os = "linux")] - if config.mode == "tun" && !is_root() { - return Err(anyhow::anyhow!("Root privileges are required to initialize TUN mode on Linux. Please run with sudo.")); - } - - log_to_core_file(&format!("[core] Starting run_client_core in mode: {}", config.mode)); - - // Resolve the server IP before we override system routing and DNS. - // This prevents DNS deadlock if the VPN disconnects and tries to reconnect, - // and also ensures we add the direct route to the exact IP the bridge connects to. - #[allow(unused_mut)] - let mut resolved_addrs: Vec = tokio::net::lookup_host(&config.ostp.server_addr) - .await - .map_err(|e| anyhow::anyhow!("Failed to resolve server address {}: {}", config.ostp.server_addr, e))? - .collect(); - - - let target_addr = resolved_addrs.first() - .ok_or_else(|| anyhow::anyhow!("No IP addresses resolved for {}", config.ostp.server_addr))?; - - log_to_core_file(&format!("[core] Resolved server address to {}", target_addr)); - config.ostp.server_addr = target_addr.to_string(); - - - #[cfg(target_os = "linux")] - if config.mode == "tun" { - println!("\n[ostp] ==========================================================================="); - println!("[ostp] WARNING: You are starting TUN mode on a Linux system."); - println!("[ostp] If this is a remote headless server, routing all traffic through the TUN"); - println!("[ostp] interface WILL DROP your SSH connection and lock you out!"); - println!("[ostp] "); - println!("[ostp] SOLUTION: Add a static route for your client IP to bypass the TUN."); - println!("[ostp] Find your default gateway (ip route | grep default) and run:"); - println!("[ostp] sudo ip route add via "); - println!("[ostp] ===========================================================================\n"); - } - - #[cfg(target_os = "linux")] - if config.mode == "proxy" { - println!("\n[ostp] ==========================================================================="); - println!("[ostp] Proxy mode initialized on {}", config.local_proxy.bind_addr); - println!("[ostp] ===========================================================================\n"); - } - - let _sysproxy_guard = if config.mode == "proxy" { - // Enable system proxy and set initial ProxyOverride with user exclusions - let guard = Some(crate::sysproxy::SystemProxyGuard::enable(&config.local_proxy.bind_addr)); - crate::sysproxy::update_proxy_bypass_list( - &config.exclusions.domains, - &config.exclusions.ips, - ); - guard - } else { - None - }; - - if config.mode == "tun" && !config.exclusions.processes.is_empty() { - println!("[ostp] Process exclusions are not supported in TUN mode"); - } - - let (proxy_events_tx, proxy_events_rx) = mpsc::channel(256); - let (client_msgs_tx, client_msgs_rx) = mpsc::unbounded_channel(); + let router = Arc::new(Router::new(config.routing.clone())); + let balancer = Arc::new(Balancer::new(&config)); - // Setup exclusions hot-reload channel - let (reload_tx, reload_rx) = watch::channel(config.exclusions.clone()); + // TODO: Detect physical interface index for bypassing + let phys_if_for_bypass = None; + let outbound_manager = Arc::new(OutboundManager::new(balancer.clone(), phys_if_for_bypass, None)); - let mut bridge = Bridge::new(&config, metrics)?; - bridge.reload_tx = Some(reload_tx.clone()); + let mut handles = Vec::new(); - let (ui_tx, mut ui_rx) = mpsc::channel(512); - let (cmd_tx, cmd_rx) = mpsc::channel(128); - let (shutdown_tx, shutdown_rx) = watch::channel(false); - let proxy_shutdown_rx = shutdown_tx.subscribe(); + for inbound in config.inbounds.clone() { + let router_clone = router.clone(); + let outbound_manager_clone = outbound_manager.clone(); + let shutdown_rx = shutdown_rx_ext.clone(); + let config_clone = config.clone(); - - // Auto-connect on startup - let _ = cmd_tx.send(BridgeCommand::ToggleTunnel).await; - - let debug_enabled = config.debug; - - // Headless event logger - let cmd_tx_clone = cmd_tx.clone(); - tokio::spawn(async move { - let mut last_status = None; - while let Some(msg) = ui_rx.recv().await { - match msg { - crate::app::UiEvent::Log(text) => { - if debug_enabled || is_essential_log(&text) { - log_to_core_file(&format!("[ostp] {text}")); - println!("[ostp] {text}"); + match inbound.clone() { + InboundConfig::Tun { .. } => { + handles.push(tokio::spawn(async move { + if let Err(e) = crate::tunnel::inbounds::tun::run_tun_inbound( + config_clone, + inbound, + router_clone, + outbound_manager_clone, + shutdown_rx, + ).await { + tracing::error!("TUN inbound failed: {}", e); } - } - crate::app::UiEvent::Metrics { status, rtt_ms, .. } => { - let status_str = status.as_str().to_string(); - if last_status != Some(status_str.clone()) { - last_status = Some(status_str.clone()); - println!("[ostp] Status: {} (rtt={:.1}ms)", status_str, rtt_ms); + })); + } + InboundConfig::LocalProxy { .. } => { + handles.push(tokio::spawn(async move { + if let Err(e) = crate::tunnel::inbounds::local_proxy::run_socks_inbound( + config_clone, + inbound, + router_clone, + outbound_manager_clone, + shutdown_rx, + ).await { + tracing::error!("SOCKS inbound failed: {}", e); } - } - crate::app::UiEvent::Traffic { .. } => {} - crate::app::UiEvent::ProfileChanged(profile) => { - if debug_enabled { - println!("[ostp] Obfuscation profile: {profile:?}"); - } - } - crate::app::UiEvent::TunnelStopped => { - println!("[ostp] Connection interrupted. Reconnecting in 5 seconds..."); - let cmd_tx_inner = cmd_tx_clone.clone(); - tokio::spawn(async move { - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - let _ = cmd_tx_inner.send(BridgeCommand::ToggleTunnel).await; - }); - } + })); } } - }); + } - let mut bridge_task = tokio::spawn(async move { - bridge.run(ui_tx, cmd_rx, shutdown_rx, proxy_events_rx, client_msgs_tx).await - }); - - let config_clone = config.clone(); - let proxy_exclusions_rx = reload_rx.clone(); - let mut proxy_task = tokio::spawn(async move { - tunnel::run_local_proxy( - config.local_proxy, - config.ostp, - proxy_exclusions_rx, - config.debug, - proxy_shutdown_rx, - proxy_events_tx, - client_msgs_rx, - ) - .await - }); - - let wintun_shutdown_rx = shutdown_tx.subscribe(); - let wintun_exclusions_rx = reload_rx.clone(); - let mut wintun_task = if config_clone.mode == "tun" { - Some(tokio::spawn(async move { - tunnel::run_tun_tunnel(config_clone, wintun_shutdown_rx, wintun_exclusions_rx).await - })) - } else { - None - }; - - // Wait for local_shutdown - let mut local_shutdown = shutdown_rx_ext.clone(); - let cmd_tx_loop = cmd_tx.clone(); - tokio::spawn(async move { - loop { - tokio::select! { - _ = local_shutdown.changed() => { - if *local_shutdown.borrow() { - let _ = cmd_tx_loop.send(BridgeCommand::Shutdown).await; - break; - } - } - Some(Ok(_)) = async { - if let Some(ref mut rx) = config_rx { - Some(rx.changed().await) - } else { - std::future::pending().await - } - } => { - if let Some(ref rx) = config_rx { - let new_cfg = rx.borrow().clone(); - // Update Windows ProxyOverride so excluded domains/IPs - // bypass the system proxy immediately (proxy mode only). - crate::sysproxy::update_proxy_bypass_list( - &new_cfg.exclusions.domains, - &new_cfg.exclusions.ips, - ); - let _ = reload_tx.send(new_cfg.exclusions); - } - } - } - } - }); - - // Wait for either external shutdown OR any task to fail + // Wait for shutdown or for tasks to fail tokio::select! { _ = shutdown_rx_ext.changed() => { - let _ = cmd_tx.send(BridgeCommand::Shutdown).await; - let _ = shutdown_tx.send(true); + if *shutdown_rx_ext.borrow() { + tracing::info!("Shutdown signal received in run_client_core"); + } } - res = &mut bridge_task => { - let _ = shutdown_tx.send(true); - res.map_err(|e| anyhow::anyhow!("Bridge task panicked: {}", e))??; - } - res = &mut proxy_task => { - let _ = shutdown_tx.send(true); - res.map_err(|e| anyhow::anyhow!("Proxy task panicked: {}", e))??; - } - res = async { - if let Some(t) = wintun_task.as_mut() { t.await } else { std::future::pending().await } - } => { - let _ = shutdown_tx.send(true); - res.map_err(|e| anyhow::anyhow!("TUN task panicked: {}", e))??; - } - } - - // Final cleanup: wait for tasks to finish - let _ = bridge_task.await; - let _ = proxy_task.await; - if let Some(task) = wintun_task { - let _ = task.await; } Ok(()) } - -#[allow(dead_code)] -fn format_bytes(bps: u64) -> String { - if bps >= 1_000_000 { - format!("{:.1}MB", bps as f64 / 1_000_000.0) - } else if bps >= 1_000 { - format!("{:.1}KB", bps as f64 / 1_000.0) - } else { - format!("{bps}B") - } -} - -fn is_essential_log(text: &str) -> bool { - matches!( - text, - "Connection established" - | "TUN tunnel established" - | "TUN tunnel stopped" - | "Bridge stopped" - | "Runtime config reloaded" - | "Connecting to remote server..." - ) || text.starts_with("Connected to ") - || text.starts_with("TURN relay allocated") - || text.starts_with("TURN allocation failed") - || text.starts_with("Allocating TURN relay") - || text.starts_with("Connection failed:") - || text.starts_with("Connection lost") - || text.starts_with("Protocol tick fatal error") -} diff --git a/ostp-client/src/runner.rs.bak b/ostp-client/src/runner.rs.bak new file mode 100644 index 0000000..44a2055 --- /dev/null +++ b/ostp-client/src/runner.rs.bak @@ -0,0 +1,436 @@ +use anyhow::Result; +use tokio::sync::{mpsc, watch}; + +use crate::app::BridgeCommand; +use crate::bridge::{Bridge, BridgeMetrics}; +use crate::signal::wait_for_shutdown_signal; +use crate::tunnel; +use std::sync::Arc; +use std::fs::OpenOptions; +use std::io::Write as _; + +fn log_to_core_file(msg: &str) { + let path = std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(|d| d.join("ostp-core.log"))) + .unwrap_or_else(|| std::path::PathBuf::from("ostp-core.log")); + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(file, "[{}] {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), msg); + } +} + +#[cfg(target_os = "windows")] +#[link(name = "kernel32")] +extern "system" { + fn FreeConsole() -> i32; + fn GetConsoleWindow() -> *mut std::ffi::c_void; +} + +#[cfg(target_os = "windows")] +#[link(name = "user32")] +extern "system" { + fn ShowWindow(hwnd: *mut std::ffi::c_void, cmd_show: i32) -> i32; +} + +fn hide_console() { + #[cfg(target_os = "windows")] + unsafe { + let hwnd = GetConsoleWindow(); + if !hwnd.is_null() { + ShowWindow(hwnd, 0); // SW_HIDE = 0 + } + FreeConsole(); + } +} + +#[cfg(target_os = "windows")] +pub fn is_admin() -> bool { + std::process::Command::new("net") + .arg("session") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} + +#[cfg(target_os = "windows")] +fn relaunch_as_admin() -> Result<()> { + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + use std::ptr::null_mut; + + let exe = std::env::current_exe()?; + let exe_wstr: Vec = exe.as_os_str().encode_wide().chain(Some(0)).collect(); + + let mut args_joined = String::new(); + for arg in std::env::args().skip(1) { + if !args_joined.is_empty() { + args_joined.push(' '); + } + args_joined.push('"'); + args_joined.push_str(&arg.replace('"', "\\\"")); + args_joined.push('"'); + } + let args_wstr: Vec = OsStr::new(&args_joined).encode_wide().chain(Some(0)).collect(); + + let dir = std::env::current_dir()?; + let dir_wstr: Vec = dir.as_os_str().encode_wide().chain(Some(0)).collect(); + + let verb_wstr: Vec = OsStr::new("runas").encode_wide().chain(Some(0)).collect(); + + #[link(name = "shell32")] + extern "system" { + fn ShellExecuteW( + hwnd: *mut std::ffi::c_void, + lpOperation: *const u16, + lpFile: *const u16, + lpParameters: *const u16, + lpDirectory: *const u16, + nShowCmd: i32, + ) -> isize; + } + + unsafe { + let ret = ShellExecuteW( + null_mut(), + verb_wstr.as_ptr(), + exe_wstr.as_ptr(), + args_wstr.as_ptr(), + dir_wstr.as_ptr(), + 1, // SW_SHOWNORMAL = 1 + ); + if ret <= 32 { + return Err(anyhow::anyhow!( + "Windows UAC Elevation failed or was denied by policy (ShellExecuteW code: {})", + ret + )); + } + } + + std::process::exit(0); +} + +#[cfg(target_os = "linux")] +pub fn is_root() -> bool { + unsafe { libc::geteuid() == 0 } +} + +#[cfg(target_os = "linux")] +fn relaunch_as_root() -> Result<()> { + use std::io::IsTerminal; + let exe = std::env::current_exe()?; + let args: Vec = std::env::args().skip(1).collect(); + + let is_gui = std::env::var("DISPLAY").is_ok() || std::env::var("WAYLAND_DISPLAY").is_ok(); + let is_term = std::io::stdout().is_terminal(); + + let mut cmd = if is_gui && !is_term { + let mut c = std::process::Command::new("pkexec"); + c.arg(exe); + c + } else { + let mut c = std::process::Command::new("sudo"); + c.arg(exe); + c + }; + + cmd.args(&args); + + let status = cmd.status().map_err(|e| anyhow::anyhow!("Failed to execute privilege escalation command: {}", e))?; + + if !status.success() { + return Err(anyhow::anyhow!("Privilege escalation failed or was denied.")); + } + + std::process::exit(0); +} + +pub async fn run_client(config: crate::config::ClientConfig) -> Result<()> { + #[cfg(target_os = "windows")] + if config.mode == "tun" && !is_admin() { + println!("[ostp] TUN mode requires administrator privileges. Relaunching..."); + relaunch_as_admin()?; + } + + #[cfg(target_os = "linux")] + if config.mode == "tun" && !is_root() { + println!("[ostp] TUN mode requires root privileges. Requesting sudo/pkexec elevation..."); + relaunch_as_root()?; + } + + let bg = std::env::args().any(|a| a == "--bg"); + + if bg { + hide_console(); + } + + let metrics = Arc::new(BridgeMetrics { + 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); + + tokio::spawn(async move { + if wait_for_shutdown_signal().await.is_ok() { + let _ = shutdown_tx.send(true); + } + }); + + run_client_core(config, metrics, shutdown_rx, None).await +} + +pub async fn run_client_core( + mut config: crate::config::ClientConfig, + metrics: Arc, + mut shutdown_rx_ext: watch::Receiver, + mut config_rx: Option>, +) -> Result<()> { + #[cfg(target_os = "windows")] + if config.mode == "tun" && !is_admin() { + return Err(anyhow::anyhow!("Administrator privileges are required to initialize TUN mode. Please run the application as Administrator.")); + } + + #[cfg(target_os = "linux")] + if config.mode == "tun" && !is_root() { + return Err(anyhow::anyhow!("Root privileges are required to initialize TUN mode on Linux. Please run with sudo.")); + } + + log_to_core_file(&format!("[core] Starting run_client_core in mode: {}", config.mode)); + + // Resolve the server IP before we override system routing and DNS. + // This prevents DNS deadlock if the VPN disconnects and tries to reconnect, + // and also ensures we add the direct route to the exact IP the bridge connects to. + #[allow(unused_mut)] + let mut resolved_addrs: Vec = tokio::net::lookup_host(&config.ostp.server_addr) + .await + .map_err(|e| anyhow::anyhow!("Failed to resolve server address {}: {}", config.ostp.server_addr, e))? + .collect(); + + + let target_addr = resolved_addrs.first() + .ok_or_else(|| anyhow::anyhow!("No IP addresses resolved for {}", config.ostp.server_addr))?; + + log_to_core_file(&format!("[core] Resolved server address to {}", target_addr)); + config.ostp.server_addr = target_addr.to_string(); + + + #[cfg(target_os = "linux")] + if config.mode == "tun" { + println!("\n[ostp] ==========================================================================="); + println!("[ostp] WARNING: You are starting TUN mode on a Linux system."); + println!("[ostp] If this is a remote headless server, routing all traffic through the TUN"); + println!("[ostp] interface WILL DROP your SSH connection and lock you out!"); + println!("[ostp] "); + println!("[ostp] SOLUTION: Add a static route for your client IP to bypass the TUN."); + println!("[ostp] Find your default gateway (ip route | grep default) and run:"); + println!("[ostp] sudo ip route add via "); + println!("[ostp] ===========================================================================\n"); + } + + #[cfg(target_os = "linux")] + if config.mode == "proxy" { + println!("\n[ostp] ==========================================================================="); + println!("[ostp] Proxy mode initialized on {}", config.local_proxy.bind_addr); + println!("[ostp] ===========================================================================\n"); + } + + let _sysproxy_guard = if config.mode == "proxy" { + // Enable system proxy and set initial ProxyOverride with user exclusions + let guard = Some(crate::sysproxy::SystemProxyGuard::enable(&config.local_proxy.bind_addr)); + crate::sysproxy::update_proxy_bypass_list( + &config.exclusions.domains, + &config.exclusions.ips, + ); + guard + } else { + None + }; + + if config.mode == "tun" && !config.exclusions.processes.is_empty() { + println!("[ostp] Process exclusions are not supported in TUN mode"); + } + + let (proxy_events_tx, proxy_events_rx) = mpsc::channel(256); + let (client_msgs_tx, client_msgs_rx) = mpsc::unbounded_channel(); + + // Setup exclusions hot-reload channel + let (reload_tx, reload_rx) = watch::channel(config.exclusions.clone()); + + let mut bridge = Bridge::new(&config, metrics)?; + bridge.reload_tx = Some(reload_tx.clone()); + + let (ui_tx, mut ui_rx) = mpsc::channel(512); + let (cmd_tx, cmd_rx) = mpsc::channel(128); + let (shutdown_tx, shutdown_rx) = watch::channel(false); + let proxy_shutdown_rx = shutdown_tx.subscribe(); + + + // Auto-connect on startup + let _ = cmd_tx.send(BridgeCommand::ToggleTunnel).await; + + let debug_enabled = config.debug; + + // Headless event logger + let cmd_tx_clone = cmd_tx.clone(); + tokio::spawn(async move { + let mut last_status = None; + while let Some(msg) = ui_rx.recv().await { + match msg { + crate::app::UiEvent::Log(text) => { + if debug_enabled || is_essential_log(&text) { + log_to_core_file(&format!("[ostp] {text}")); + println!("[ostp] {text}"); + } + } + crate::app::UiEvent::Metrics { status, rtt_ms, .. } => { + let status_str = status.as_str().to_string(); + if last_status != Some(status_str.clone()) { + last_status = Some(status_str.clone()); + println!("[ostp] Status: {} (rtt={:.1}ms)", status_str, rtt_ms); + } + } + crate::app::UiEvent::Traffic { .. } => {} + crate::app::UiEvent::ProfileChanged(profile) => { + if debug_enabled { + println!("[ostp] Obfuscation profile: {profile:?}"); + } + } + crate::app::UiEvent::TunnelStopped => { + println!("[ostp] Connection interrupted. Reconnecting in 5 seconds..."); + let cmd_tx_inner = cmd_tx_clone.clone(); + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + let _ = cmd_tx_inner.send(BridgeCommand::ToggleTunnel).await; + }); + } + } + } + }); + + let mut bridge_task = tokio::spawn(async move { + bridge.run(ui_tx, cmd_rx, shutdown_rx, proxy_events_rx, client_msgs_tx).await + }); + + let config_clone = config.clone(); + let proxy_exclusions_rx = reload_rx.clone(); + let mut proxy_task = tokio::spawn(async move { + tunnel::run_local_proxy( + config.local_proxy, + config.ostp, + proxy_exclusions_rx, + config.debug, + proxy_shutdown_rx, + proxy_events_tx, + client_msgs_rx, + ) + .await + }); + + let wintun_shutdown_rx = shutdown_tx.subscribe(); + let wintun_exclusions_rx = reload_rx.clone(); + let mut wintun_task = if config_clone.mode == "tun" { + Some(tokio::spawn(async move { + tunnel::run_tun_tunnel(config_clone, wintun_shutdown_rx, wintun_exclusions_rx).await + })) + } else { + None + }; + + // Wait for local_shutdown + let mut local_shutdown = shutdown_rx_ext.clone(); + let cmd_tx_loop = cmd_tx.clone(); + tokio::spawn(async move { + loop { + tokio::select! { + _ = local_shutdown.changed() => { + if *local_shutdown.borrow() { + let _ = cmd_tx_loop.send(BridgeCommand::Shutdown).await; + break; + } + } + Some(Ok(_)) = async { + if let Some(ref mut rx) = config_rx { + Some(rx.changed().await) + } else { + std::future::pending().await + } + } => { + if let Some(ref rx) = config_rx { + let new_cfg = rx.borrow().clone(); + // Update Windows ProxyOverride so excluded domains/IPs + // bypass the system proxy immediately (proxy mode only). + crate::sysproxy::update_proxy_bypass_list( + &new_cfg.exclusions.domains, + &new_cfg.exclusions.ips, + ); + let _ = reload_tx.send(new_cfg.exclusions); + } + } + } + } + }); + + // Wait for either external shutdown OR any task to fail + tokio::select! { + _ = shutdown_rx_ext.changed() => { + let _ = cmd_tx.send(BridgeCommand::Shutdown).await; + let _ = shutdown_tx.send(true); + } + res = &mut bridge_task => { + let _ = shutdown_tx.send(true); + res.map_err(|e| anyhow::anyhow!("Bridge task panicked: {}", e))??; + } + res = &mut proxy_task => { + let _ = shutdown_tx.send(true); + res.map_err(|e| anyhow::anyhow!("Proxy task panicked: {}", e))??; + } + res = async { + if let Some(t) = wintun_task.as_mut() { t.await } else { std::future::pending().await } + } => { + let _ = shutdown_tx.send(true); + res.map_err(|e| anyhow::anyhow!("TUN task panicked: {}", e))??; + } + } + + // Final cleanup: wait for tasks to finish + let _ = bridge_task.await; + let _ = proxy_task.await; + if let Some(task) = wintun_task { + let _ = task.await; + } + + Ok(()) +} + +#[allow(dead_code)] +fn format_bytes(bps: u64) -> String { + if bps >= 1_000_000 { + format!("{:.1}MB", bps as f64 / 1_000_000.0) + } else if bps >= 1_000 { + format!("{:.1}KB", bps as f64 / 1_000.0) + } else { + format!("{bps}B") + } +} + +fn is_essential_log(text: &str) -> bool { + matches!( + text, + "Connection established" + | "TUN tunnel established" + | "TUN tunnel stopped" + | "Bridge stopped" + | "Runtime config reloaded" + | "Connecting to remote server..." + ) || text.starts_with("Connected to ") + || text.starts_with("TURN relay allocated") + || text.starts_with("TURN allocation failed") + || text.starts_with("Allocating TURN relay") + || text.starts_with("Connection failed:") + || text.starts_with("Connection lost") + || text.starts_with("Protocol tick fatal error") +} diff --git a/ostp-client/src/tunnel/balancer.rs b/ostp-client/src/tunnel/balancer.rs new file mode 100644 index 0000000..9951f59 --- /dev/null +++ b/ostp-client/src/tunnel/balancer.rs @@ -0,0 +1,65 @@ +use crate::config::{ClientConfig, OutboundConfig}; +use std::collections::HashMap; +use std::sync::Arc; + +pub struct Balancer { + outbounds: HashMap, +} + +impl Balancer { + pub fn new(config: &ClientConfig) -> Self { + let mut outbounds = HashMap::new(); + for outbound in &config.outbounds { + let tag = match outbound { + OutboundConfig::Selector { tag, .. } => tag, + OutboundConfig::Urltest { tag, .. } => tag, + OutboundConfig::Ostp { tag, .. } => tag, + OutboundConfig::Direct { tag } => tag, + OutboundConfig::Socks { tag, .. } => tag, + OutboundConfig::Block { tag } => tag, + }; + outbounds.insert(tag.clone(), outbound.clone()); + } + + Self { outbounds } + } + + /// Resolves an outbound tag into a concrete, non-group outbound tag. + /// E.g. "proxy-group" -> "server-helsinki" + pub fn resolve_outbound(&self, tag: &str) -> String { + // Prevent infinite loops if groups point to groups + let mut current_tag = tag.to_string(); + for _ in 0..10 { + if let Some(outbound) = self.outbounds.get(¤t_tag) { + match outbound { + OutboundConfig::Selector { outbounds, default, .. } => { + current_tag = if let Some(def) = default { + def.clone() + } else { + outbounds.first().cloned().unwrap_or_else(|| "direct".to_string()) + }; + } + OutboundConfig::Urltest { outbounds, .. } => { + // TODO: Implement background ping worker to find the fastest node. + // For now, act as a fallback by taking the first available node. + current_tag = outbounds.first().cloned().unwrap_or_else(|| "direct".to_string()); + } + _ => { + // It's a concrete physical outbound (ostp, direct, block) + return current_tag; + } + } + } else { + // Outbound not found, fallback to direct + return "direct".to_string(); + } + } + "direct".to_string() // Max depth reached + } + + /// Fetches the config for a concrete outbound + pub fn get_concrete_outbound(&self, tag: &str) -> Option<&OutboundConfig> { + let resolved_tag = self.resolve_outbound(tag); + self.outbounds.get(&resolved_tag) + } +} diff --git a/ostp-client/src/tunnel/inbounds/local_proxy.rs b/ostp-client/src/tunnel/inbounds/local_proxy.rs new file mode 100644 index 0000000..92279ba --- /dev/null +++ b/ostp-client/src/tunnel/inbounds/local_proxy.rs @@ -0,0 +1,224 @@ +use anyhow::{anyhow, Result}; +use std::sync::Arc; +use crate::config::{ClientConfig, InboundConfig}; +use crate::tunnel::router::{Router, Session}; +use crate::tunnel::outbounds::OutboundManager; +use tokio::net::TcpListener; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::sync::watch; + +pub async fn run_socks_inbound( + _config: ClientConfig, + inbound_config: InboundConfig, + router: Arc, + outbound_manager: Arc, + mut shutdown: watch::Receiver, +) -> Result<()> { + let InboundConfig::LocalProxy { tag, protocol, listen, port } = inbound_config else { + return Err(anyhow!("Invalid config for LocalProxy inbound")); + }; + + let bind_addr = format!("{}:{}", listen, port); + tracing::info!("Starting {} proxy inbound on {} (tag: {})", protocol, bind_addr, tag); + + let listener = TcpListener::bind(&bind_addr).await?; + + loop { + tokio::select! { + _ = shutdown.changed() => { + tracing::info!("Local proxy inbound {} shutting down", tag); + break; + } + accept_res = listener.accept() => { + if let Ok((mut stream, client_addr)) = accept_res { + let rt = router.clone(); + let om = outbound_manager.clone(); + let proto = protocol.clone(); + let inbound_tag = tag.clone(); + + tokio::spawn(async move { + if proto == "socks" { + if let Err(e) = handle_socks5_connection(&mut stream, &rt, &om, &inbound_tag, client_addr).await { + tracing::debug!("SOCKS5 handling error: {}", e); + } + } else if proto == "http" { + if let Err(e) = handle_http_connection(&mut stream, &rt, &om, &inbound_tag, client_addr).await { + tracing::debug!("HTTP proxy handling error: {}", e); + } + } else { + tracing::error!("Unknown local proxy protocol: {}", proto); + } + }); + } + } + } + } + + Ok(()) +} + +async fn handle_socks5_connection( + stream: &mut tokio::net::TcpStream, + router: &Arc, + outbound_manager: &Arc, + inbound_tag: &str, + client_addr: std::net::SocketAddr, +) -> Result<()> { + let mut buf = [0u8; 256]; + + // Read version and method selection + stream.read_exact(&mut buf[0..2]).await?; + if buf[0] != 0x05 { + return Err(anyhow!("Unsupported SOCKS version: {}", buf[0])); + } + + let num_methods = buf[1] as usize; + stream.read_exact(&mut buf[0..num_methods]).await?; + + // Reply with NO AUTHENTICATION REQUIRED (0x00) + stream.write_all(&[0x05, 0x00]).await?; + + // Read the actual request + stream.read_exact(&mut buf[0..4]).await?; + if buf[0] != 0x05 || buf[1] != 0x01 { // Only CONNECT is supported + return Err(anyhow!("Unsupported SOCKS command")); + } + + let atyp = buf[3]; + let (target_host, mut ip_addr) = match atyp { + 0x01 => { // IPv4 + stream.read_exact(&mut buf[0..4]).await?; + let ip = std::net::Ipv4Addr::new(buf[0], buf[1], buf[2], buf[3]); + (ip.to_string(), Some(std::net::IpAddr::V4(ip))) + } + 0x03 => { // Domain + stream.read_exact(&mut buf[0..1]).await?; + let domain_len = buf[0] as usize; + stream.read_exact(&mut buf[0..domain_len]).await?; + let domain = String::from_utf8_lossy(&buf[0..domain_len]).to_string(); + (domain, None) + } + 0x04 => { // IPv6 + stream.read_exact(&mut buf[0..16]).await?; + let mut ip_bytes = [0u8; 16]; + ip_bytes.copy_from_slice(&buf[0..16]); + let ip = std::net::Ipv6Addr::from(ip_bytes); + (ip.to_string(), Some(std::net::IpAddr::V6(ip))) + } + _ => return Err(anyhow!("Unsupported SOCKS address type: {}", atyp)), + }; + + stream.read_exact(&mut buf[0..2]).await?; + let target_port = u16::from_be_bytes([buf[0], buf[1]]); + + let process_name = crate::tunnel::process_lookup::get_process_name_from_port(client_addr.port()); + + let session = Session { + protocol: "tcp".to_string(), + inbound_tag: inbound_tag.to_string(), + source_ip: Some(client_addr.ip()), + destination_ip: ip_addr, + destination_port: target_port, + sni: if atyp == 0x03 { Some(target_host.clone()) } else { None }, + process_name, + }; + + let outbound_tag = router.route(&session); + tracing::info!("SOCKS5 TCP {} -> {}:{} routed to {}", client_addr, target_host, target_port, outbound_tag); + + match outbound_manager.dial_tcp(&outbound_tag, &target_host, target_port).await { + Ok(mut remote_stream) => { + // Reply success + stream.write_all(&[0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]).await?; + + // Forward data + tokio::io::copy_bidirectional(stream, &mut remote_stream).await?; + } + Err(e) => { + tracing::warn!("SOCKS5 TCP dial failed to {}: {}", outbound_tag, e); + // Reply host unreachable + let _ = stream.write_all(&[0x05, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]).await; + } + } + + Ok(()) +} + +async fn handle_http_connection( + stream: &mut tokio::net::TcpStream, + router: &Arc, + outbound_manager: &Arc, + inbound_tag: &str, + client_addr: std::net::SocketAddr, +) -> Result<()> { + // Basic HTTP CONNECT implementation + let mut buf = [0u8; 4096]; + let n = stream.read(&mut buf).await?; + if n == 0 { return Ok(()); } + + let request = String::from_utf8_lossy(&buf[0..n]); + let mut lines = request.lines(); + let first_line = lines.next().ok_or_else(|| anyhow!("Empty HTTP request"))?; + + let parts: Vec<&str> = first_line.split_whitespace().collect(); + if parts.len() < 3 { + return Err(anyhow!("Invalid HTTP request line")); + } + + let method = parts[0]; + let target = parts[1]; // host:port for CONNECT, http://host:port/... for GET + + let (target_host, target_port) = if method == "CONNECT" { + let parts: Vec<&str> = target.split(':').collect(); + let host = parts[0].to_string(); + let port = parts.get(1).unwrap_or(&"443").parse::().unwrap_or(443); + (host, port) + } else { + // Rudimentary GET parsing, ideally use httparse + if target.starts_with("http://") { + let without_scheme = &target[7..]; + let host_part = without_scheme.split('/').next().unwrap_or(without_scheme); + let parts: Vec<&str> = host_part.split(':').collect(); + let host = parts[0].to_string(); + let port = parts.get(1).unwrap_or(&"80").parse::().unwrap_or(80); + (host, port) + } else { + return Err(anyhow!("Unsupported HTTP method/target: {} {}", method, target)); + } + }; + + let process_name = crate::tunnel::process_lookup::get_process_name_from_port(client_addr.port()); + + let session = Session { + protocol: "tcp".to_string(), + inbound_tag: inbound_tag.to_string(), + source_ip: Some(client_addr.ip()), + destination_ip: None, // Could parse if IP + destination_port: target_port, + sni: Some(target_host.clone()), + process_name, + }; + + let outbound_tag = router.route(&session); + tracing::info!("HTTP TCP {} -> {}:{} routed to {}", client_addr, target_host, target_port, outbound_tag); + + match outbound_manager.dial_tcp(&outbound_tag, &target_host, target_port).await { + Ok(mut remote_stream) => { + if method == "CONNECT" { + stream.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n").await?; + } else { + remote_stream.write_all(&buf[0..n]).await?; + } + + tokio::io::copy_bidirectional(stream, &mut remote_stream).await?; + } + Err(e) => { + tracing::warn!("HTTP TCP dial failed to {}: {}", outbound_tag, e); + if method == "CONNECT" { + let _ = stream.write_all(b"HTTP/1.1 502 Bad Gateway\r\n\r\n").await; + } + } + } + + Ok(()) +} diff --git a/ostp-client/src/tunnel/inbounds/local_proxy.rs.bak b/ostp-client/src/tunnel/inbounds/local_proxy.rs.bak new file mode 100644 index 0000000000000000000000000000000000000000..5acf4710647c1e3e20dde4f198afccca78ac8f98 GIT binary patch literal 76822 zcmeI5>y8yynx?nZ|3;d7$YLa%uI@^M4en|;b_-)%w#EjF#8oQMRu<-9kU3;Q+L)oa zioU46qy9NF-xF`XtV67b9T9scMO7hWNAA5N*80|WKCKo1@BjX^db+x^+KK-kt!}SA zihs9Ok5>=k-<|mDXmvk6J&IrX<%`wL`0L*4>$o1S{&n>~SN~(>=Wnf^#J5NB6n7rQ z_mAS=ofwDDcUHf|Xb-x(H6q_$?tRP1M{(!9c;ZPl!_64)dHnKt^{gB7MckXd`KMUJ zr4CkL_$5Z+n``m+)0p)^e12JtcNBO3*sTv(fQsvj)x~(?d5nCkd-`Tz1voWc`t1(^ z-~IUZ&hf9~ql1Z~~G3l~{v69tKRpoIgDdj0<0%#b4qJ zQQe~ra}M11XmvNn{o#1jK9`8jx(6zM)#2u5Q2Cpf?Qu}+`7tk~d#-laJ}kaBG}Cza zMK=27)z2N}w8}K=^_cxqz;XZh*RNyNA70UmRsXNmjll8U_;xpNbTi=np`+ z5;S*@bxE50^Y~`&oaLGl7Qmt4`7yBgsFM_Qm~r=X_5SK$^+EhUJtLifR(aH67~Yrs zJ&e0{LhixE&hfvy-CbzLqqs|w_c-L^DCW!W^Zn3ZC$RH0u=Ic8sefwTg-(TU(1|;7 zClI#p)LdTKw7Y)}JZR)!2Zu>(A9Yauc61h?dNc^pyD`FVR-e3rS2TRtt#3N_3{mnh zaQ1zSEqs69@!0L4opBES58}Va@qhRaefcErz8NdL6ST4$PH*)dkc^WtL3 zGHVjuHAlH(ug1?~Gnp4HfcNnHBJ-0gBaaO|nH=&WQ z2ClDm*cKgHxbX4q@)|cHjou`cTR&KHVuS0Hg zYFv%@Yxh{{`F^e!)bgQUj|YVJoqU_zuRPu1@kz+0{k%S2uZ8qFieBg2;Q7l2=N?S$ z-Edc5A6O4O4Ev2Gmo_o9Xc2i7_Mgh%Jx98W8LWZuyKgn-EXT;_*bB>uM}IG#I@8l# zhIOvaf(O12NU$vPJrD5Kju>o3^6~dW=YJL2m+Pg_{n&wdPamv44R7$u>a)&Q`7B2G zedj4&UNg=ZR^{j7b;yFYJ&VMJcf*Uj-{I?4tmWshC->K4VQhXV_o(9p&z$4TT&uf? zJ=z#6Eo1GcF#k@hPaf$y|3vqltY1?4Bg2}k)JM)ul=0g^NXES`e!d@^Ow@^&Ui#|D zo;AwV&c{>+=t9U8{v;7)=?86=88QEtWBS_?!}xJW0Ta|EQfTK->NVr@p_i2HQvQG^ zjU!?dB2#4>9bcY{S<;izNDt$#!{EUGi2n|PztD8IBied5zDa+VzEVlQ99NG5F7gs{ zvPsS-89!~TU5Sy&T;?`Va3ssu=9gsYhIz$$eV9}BV@+gP9|ew=@=Jmw<$}gnZOtxf zvpz?aNhck4G$jY|H2%Q_#>d_?!d4^4$(E4?}98%H7p(CZ zK>D9`^8V|%?yO`ZP94t%ukHmW9d+Kv*>28p%y|{1rW;;`{URu48G}OWuvF?H&su(4 ze#*5-(i>O3=f~!;}9b9|2T1!&d@ZPXn&+I)24_P<6&xL*+V=HZoQ6;J`mT zA>Cm;U*>LeeW~1`a8cU(^_Grj+ZL`IX}u;C6y;dC1blMrizLf>t;3Bh&V}&Ns4VFT z)sOmG)BFve;OjgHT;B<<6Hk}bom91gO6Y^1V*G!)cYL|03-0puS&4zIY&LO9EW*e=Xfr22v~j* zBcx73T5MW*Zu6h_}0Y;l~<&*4376&&^t0`_p{V zRUUUQFqCJsmuT7{%A2PW1n6PeHstifpi;t|^+A30N)C?kg>`VYA1dGIZ0L9K|1

pCsA9mmibLl!HMN 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 JKy*mNU5O)^jrr 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 literal 0 HcmV?d00001 diff --git a/ostp-client/src/tunnel/inbounds/mod.rs b/ostp-client/src/tunnel/inbounds/mod.rs new file mode 100644 index 0000000..d7f1fb4 --- /dev/null +++ b/ostp-client/src/tunnel/inbounds/mod.rs @@ -0,0 +1,2 @@ +pub mod tun; +pub mod local_proxy; diff --git a/ostp-client/src/tunnel/inbounds/tun.rs b/ostp-client/src/tunnel/inbounds/tun.rs new file mode 100644 index 0000000..3f5c5a3 --- /dev/null +++ b/ostp-client/src/tunnel/inbounds/tun.rs @@ -0,0 +1,239 @@ +use anyhow::{anyhow, Result}; +use std::sync::Arc; +use crate::config::{ClientConfig, InboundConfig}; +use crate::tunnel::router::{Router, Session}; +use crate::tunnel::outbounds::OutboundManager; +use tokio::sync::watch; + +#[cfg(any(target_os = "windows", target_os = "linux"))] +pub async fn run_tun_inbound( + config: ClientConfig, + inbound_config: InboundConfig, + router: Arc, + outbound_manager: Arc, + mut shutdown: watch::Receiver, +) -> Result<()> { + use std::net::ToSocketAddrs; + use netstack_smoltcp::StackBuilder; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use futures::{StreamExt, SinkExt}; + + let InboundConfig::Tun { tag, auto_route, mtu } = inbound_config else { + return Err(anyhow!("Invalid config for TUN inbound")); + }; + + tracing::info!("Starting TUN inbound (tag: {}, auto_route: {}, mtu: {})", tag, auto_route, mtu); + + #[cfg(target_os = "windows")] + let _phys_if_for_bypass: Option = ostp_tun::windows::windows_route::sys::get_default_ipv4_route().map(|(_, idx)| idx); + #[cfg(not(target_os = "windows"))] + let _phys_if_for_bypass: Option = None; + + let mut bypass_ips: Vec = Vec::new(); + + // Bypass all outbound server IPs + for outbound in &config.outbounds { + let server = match outbound { + crate::config::OutboundConfig::Ostp { server, .. } => Some(server), + crate::config::OutboundConfig::Socks { server, .. } => Some(server), + _ => None, + }; + if let Some(host) = server { + if let Ok(ip) = host.parse::() { + bypass_ips.push(ip); + } else { + if let Ok(addrs) = tokio::net::lookup_host((host.as_str(), 443)).await { + for addr in addrs { + bypass_ips.push(addr.ip()); + } + } + } + } + } + + let dummy_server_ip = bypass_ips.first().copied().unwrap_or_else(|| "8.8.8.8".parse().unwrap()); + + // Create TUN device + let opts = ostp_tun::OstpTunOptions { + server_ip: dummy_server_ip, + bypass_ips, + dns_server: None, + kill_switch: false, + mtu: mtu as u16, + wintun_path: None, + }; + + let tun_interface = ostp_tun::OstpTunInterface::create(opts) + .await + .map_err(|e| anyhow!("Failed to create OstpTunInterface: {}", e))?; + + let dev = tun_interface.device; + let _route_guard = tun_interface.guard; // Drops when TUN drops + + // Build smoltcp network stack + let (stack, tcp_runner, udp_socket, tcp_listener) = StackBuilder::default() + .stack_buffer_size(1024) + .tcp_buffer_size(1024) + .udp_buffer_size(1024) + .enable_tcp(true) + .enable_udp(true) + .mtu(mtu) + .build()?; + + let mut runner_task = tokio::spawn(async move { + if let Some(runner) = tcp_runner { + let _ = runner.await; + } + }); + + let (mut stack_sink, mut stack_stream) = stack.split(); + let (mut tun_read, mut tun_write) = tokio::io::split(dev); + + let mut tun_to_stack = tokio::spawn(async move { + let mut buf = vec![0u8; 65536]; + loop { + match tun_read.read(&mut buf).await { + Ok(0) => break, + Ok(n) => { + let frame = buf[..n].to_vec(); + if let Err(e) = stack_sink.send(frame).await { + if e.kind() == std::io::ErrorKind::BrokenPipe { + break; + } + } + } + Err(e) => { + tracing::debug!("tun_read error: {e}"); + } + } + } + }); + + let mut stack_to_tun = tokio::spawn(async move { + while let Some(Ok(frame)) = stack_stream.next().await { + if let Err(e) = tun_write.write(&frame).await { + tracing::debug!("tun_write error: {e}"); + } + } + }); + + // ── TCP Handler ── + let outbound_manager_tcp = outbound_manager.clone(); + let router_tcp = router.clone(); + let tag_tcp = tag.clone(); + + let mut tcp_accept_task = tokio::spawn(async move { + let Some(mut listener) = tcp_listener else { return; }; + while let Some((mut stream, local, remote)) = listener.next().await { + let om = outbound_manager_tcp.clone(); + let rt = router_tcp.clone(); + let ib_tag = tag_tcp.clone(); + + tokio::spawn(async move { + let process_name = crate::tunnel::process_lookup::get_process_name_from_port(local.port()); + + let mut sniff_buf = [0u8; 2048]; + let sniff_len = match tokio::time::timeout( + std::time::Duration::from_millis(100), + stream.read(&mut sniff_buf) + ).await { + Ok(Ok(n)) => n, + _ => 0, + }; + + let mut domain_suffix = None; + if sniff_len > 0 { + domain_suffix = crate::tunnel::sni_sniff::extract_sni(&sniff_buf[..sniff_len]); + } + + let session = Session { + protocol: "tcp".to_string(), + inbound_tag: ib_tag.clone(), + source_ip: Some(local.ip()), + destination_ip: Some(remote.ip()), + destination_port: remote.port(), + sni: domain_suffix.map(|s| s.to_string()), + process_name, + }; + + let outbound_tag = rt.route(&session); + tracing::info!("TUN TCP {} -> {} routed to {}", local, remote, outbound_tag); + + let target_host = if let Some(domain) = session.sni { + domain + } else { + remote.ip().to_string() + }; + + match om.dial_tcp(&outbound_tag, &target_host, session.destination_port).await { + Ok(mut remote_stream) => { + if sniff_len > 0 { + if let Err(e) = remote_stream.write_all(&sniff_buf[..sniff_len]).await { + tracing::warn!("Failed to forward sniffed bytes to {}: {}", outbound_tag, e); + return; + } + } + let _ = tokio::io::copy_bidirectional(&mut stream, &mut remote_stream).await; + } + Err(e) => { + tracing::warn!("TUN TCP dial failed to {}: {}", outbound_tag, e); + } + } + }); + } + }); + + // ── UDP Handler ── + let outbound_manager_udp = outbound_manager.clone(); + let router_udp = router.clone(); + let tag_udp = tag.clone(); + + let mut udp_proxy_task = tokio::spawn(async move { + if let Some(udp_sock) = udp_socket { + let (mut udp_rx, _udp_tx) = udp_sock.split(); + while let Some((payload, local, remote)) = udp_rx.next().await { + let process_name = crate::tunnel::process_lookup::get_process_name_from_port_udp(local.port()); + let session = Session { + protocol: "udp".to_string(), + inbound_tag: tag_udp.clone(), + source_ip: Some(local.ip()), + destination_ip: Some(remote.ip()), + destination_port: remote.port(), + sni: None, + process_name, + }; + let outbound_tag = router_udp.route(&session); + + let payload_bytes = bytes::Bytes::copy_from_slice(&payload); + if let Err(e) = outbound_manager_udp.handle_udp(&outbound_tag, local, remote, payload_bytes).await { + tracing::debug!("TUN UDP drop to {}: {}", outbound_tag, e); + } + } + } + }); + + tokio::select! { + _ = shutdown.changed() => { + tracing::info!("TUN inbound {} shutting down", tag); + } + _ = &mut runner_task => {} + } + + tun_to_stack.abort(); + stack_to_tun.abort(); + tcp_accept_task.abort(); + udp_proxy_task.abort(); + + Ok(()) +} + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +pub async fn run_tun_inbound( + _config: ClientConfig, + _inbound_config: InboundConfig, + _router: Arc, + _outbound_manager: Arc, + _shutdown: watch::Receiver, +) -> Result<()> { + Err(anyhow!("TUN is only supported on Windows and Linux")) +} diff --git a/ostp-client/src/tunnel/inbounds/tun.rs.bak b/ostp-client/src/tunnel/inbounds/tun.rs.bak new file mode 100644 index 0000000000000000000000000000000000000000..eead16ff648f1f9a32217b8a49f637576940682b GIT binary patch literal 62896 zcmeI5>2e*%b;oa&zojbg05MfEl!VagC5&trSDO`CQVEQel)^$m;93Gf7$8N-l%B%x z6hBE`!5@>H-~8{Ny-d%&GXoH!XcUSZV3zLFXFsP;cmLo2K3yCy?k+YKyNiRxw~Ggh z$BSo+OY!r(xbx5P#%J-~@#5)XfAM7T{o-F2f4}&<_)A|uiSNEzJdFSO_BdKPSnPGb zKZ|yr#P1K5U+~r2@w2h`^H2V|_{;hKuN4Lobw3YoI9S~2_~|&P4aGm|?mb<66Tjc- zxaq6ljmL|_`1{u4-xnWuJax_;ud8Ih9sK);#TTIw?k(;wHkVpvGx%V4aTIs%#&0{p z6Y$Kh;@^ePM>!{5SfGoB-`;=0V;2^0#n0`o$HUN6U&eR4FLcvJ^nVa{j^fEd^mx$e zsfUZN1A?7spSuU~`+mR(%p1#}y@2f?zI_<5UW$8r(E=LoZl~?ar>%E_ zntl1tMVpVJw|@#XrG$wD>Sa^*Da^7rzd?h3--;I>6M?(o2vp~926 zUJ4!Yc(EP-_QKY1?{Z-3PK@PvXgpz^Fc0aW2HtiW2j2{_^xrB=#;R5Pp}^{f~4Mz5!{Nuj{SN!&?SKhQE!! zo`l}vIXnzK@5VQ1#>X8Wf~ou6Tf3oc_T%YR;A;+yH#>@c+qH`>hQHCEXeDHIuj~6x z*Vp6te>eUC8S+HG$MFM{P#isd6n}jkl;pkb`1xJOS#JMP+-G$A(LUeIgITT7QwgVZ zDL4f}ED1Qi7k$b@PWS`vG+M@dCt&89#w%`Q%%(+df#%}-)!=6&YYOfqkC&I+VQLb2 zh+ljM%ZLnvGcX4Z(XQ|VR`*YV;e=25mwHP&16pBAe-m$g5u5^@-i|gNhtGigtcQ76 zH{A;e(2x(7x(aXNS-`&=RtF6w`Cg@?;CIGmJI?6vVz`H2Bzqv)w3U38wO#8B|6Rh& z_tK`|7CGDLaECN*F5YbPj^=HT#y!+mj|6Jjt4pKViV>lWA1&d4mcT$9iiGb4R7h{a zTHU_?mc4K&myZS5VHB$tQ^<|6)?G8^#!^yYZ z>hzhOmevS*AvuZn`0ZYdMWeqNf0d)wn;T)7E{890bMc43U9wNPrrQX7mOdK%#!r&Q zd?bn$uLeAaOUfYghaq=ZvWFe_VDEsSlzQC*14N@hkB)~1haJbksaFEd8;eiEt^zOo zD1RSGp|!qtp*<`TdPV#OWzk3Ky|fBj1@*>xYIQW^2*;dZ+8PHt0nNRTD2;HZ7$I8j zlMZv@$q$1U-e0`edFAi}cJoO*cRYliDf{o=A-0g;pomC51@AewEA1rk9R|)mh+fi{ z(n(lw^yPQ)Z>O{E4}&Ye3H|w9KF2`FO_Iu0Gz+*2HpC1Q2Xx6^=@8>)= z7VievJ7yxn!{e#FpPP$Mckvt2fvs~sS9)!5`8$SuDnBMA=C-$Md|Mubt-QCb81Z390^Z%`;-?WU{WSP}DNWH!zp99?t7nnopz#8%}G#G|9glrVSFRSu{nv zX|4_2i`UUm=*p)to}Cxi+6)}!eQZTb-R*cBO0UDO zC}mB7283QnTbs{J7hjD-w_yDF?_sRCh5EXV;%+&&J*5{}x9Go1e(+siS3h;Uhz*^F zp?<`L{viq-w(R5x??&FewtY!6v@8HL%em$IPQyNrmhb?Eq-6E@ORVTG^jfalS{Cg6 z&VpU-M&NkMk#!koIS1%?c61Wkx{hDIZr zla}oF(0;EC^P`S``fV7?Tj4RmVR+f#QhKx9mLg2?<4$L}$nes(qrvh!&xYhpBs_nu#la=PML>W(4(>H z=%lU7b7+lG=@H4$Fm{y-b6%%x-7V$Wi=D`IJnb|z9yXL&)pmMUE$48y^x`i%y_jYz z$qD4Mn#4W$1kZw!N8u0f+%uqmd%1!fo2C^xMq>o;N3+3D5KRg8jykXCY1l%=X(=kt z-^UZCCFXzl#-8f-i`EmBc#f>WacPzay_Q3mq$oj1&hBY^bFb?IJ*T;i&G6RViEP~m zgKc`JY%hx%lMn)`<$&H?1` znRgtG=Dfn#ums4Oq#Zvz&9o4&JPTYfSKvAU&A}?C_q>UTG9IGt;Zb01v%{Xb)Es9@ z=TNQlzdfRzCDi%1x&L44J9B95)!8@9an{mazwETv=Ybt;PMQTE@9@|7yVhk%4*vFZ zSKs%{v1|Hm24A>-$XZQWg31u|Rr@(|h}C}-95232xGp1o)7G@__iX#77Vi|j`Wf?| z_-BrP>PR-17v~scpWT0J^d6ggNjWps{G6sldfe_)dQm+f$$JrxeGxkO>Ef4>Mcjy3 z`e*U;eq3)?S;d4&Vs`2u+d9DSmGM7T3Tveq+pQPB0x(}u%W!9#@&_CtM0IvG9CD8p%45ybd;|F(P|ALB~= zjRgYAPvib!hrekQFhss44w!~ve_z@Q-yOP+dwbklHC4xFrVL#_|AJnE7w<1KFxEbn z$&GjpFF_e%y|Eb-_qcjw?Wg74?_bD5F0Ym=Yp|TZB}p2|X=+U>q%`kDEtaZUk~bqw zk;_rv8X)CW2&q=A@xx7^JgCQvI zwz;I`xBW}#q>ZT2zZMz=j#k!Ic}SvM>K@?vZ-Y|M+ckp^Iw?R>9>iOpMBUP@ZqAzg zs--EbF;4yf#%< zNohI5I2DlWeABGzqjG0_5ah69l2#6NdJR*C1SKK z`kV?jy#8svzA0I9$NFBUalU2TSNAIJ-04a64NkwUk8&iBJ&#ei#}Y>7%~FfN@Lels+=G2g-}xi0w@tkO7)AFRGs z>yWE*y&N^v(3bje#Ru32)`H1@!^R(XRBhRp6m#!Hi_FYOho<}OQP?Lf?azFhl+*HL zylTjV-NZkFZ8gr@cC&>y`_%3&@%P!zTeyEB7~&IK-uk{W1kVfUwqEn~lO0y&8Jg*m~9<-bz#K!qvj%n8%@jP*?+snTy`IIL~k4eu+zm~Q9c?&f^ zl~%Eo5|7{49J43YRBqGP*8#`Ym}77{-rnZ<4l4hN0ht@SI*@I_e;CT9Y=#zPo`ncL zujRTJT3m7demu#ew|o^PD@c*d9HRd3xt>Au*ME!RHv&o`Nb(F9qeo^xm3e4++gPlA z<^(v?su|mjJY(XDB`h*0^W_k)V)1hlQ@nt4VJiYSdSaoI=$a8^D<(iZo0$1qxa<(XC(STL!sd!&79!E>)H&%V$ zil}ruuG`@`-i)hyt+`fSx@uH&tqCN5{>fju`DA4yxkD~V{J>vV!x#OyyDP5bp4Rfh z8N(QM4o|h<_%pyQ{CyR_;ln^*<;qfC3BRb%Z|#08H@S>TZj=v$vspW=Ra=4_zIGlg zWg4{->wZ9G$+7j7m6aaxm~3b4*NWT9O0zb^o1KjBbbGkq-CyqVygPCCMyE|gVNnWb zR4Fg$lJ+qiPyoIlJ3?#lv22m+1%$~Q7W=F%o9o&W`BiPt!r$9lat`l216k*Oq}w;5 z>MqB>@3u3L`J2)LpDtswRlFwtL%Xm>Q?iB}=c_&Y+4UrItYAc&n6KDZ;~u!(nHjeFIoSlQk|J-Rj6N|gaTk%Av%)z| zOubg7EJ0LOnn+3?b<&wK^T;Yss7O?;hyDB-&iV6r##Kw!CF89p)&lLw$rz}w?kl%1 ztVF|B?!{QiFtwje>nHcS=S7u6kk`?`=|q=xZ>N)~ty^tle>K*{m;F5LG?lUeh3NpZ1e9eIMLq|MqzyS7X*=7zSzTmc087U!|LKe;*&L@0aEKTK%rm zH^7`{{^YmiExr1`kHJvlNY~sMJFe3ecjk3q??pWKLDYl|Re>4%zPgX(5wjx(o;Gcd zfzmA{&4d-qXMyk@u`^^l{JZ?Ia=$aTwC;+9Qf-o3qvdOz-N6@@ugX1YD6nzNNoef` z_D$8y)SQ(u!@7R`oc?W*+7)p*HeDSzt*>_Vupy|gTVW&cS@3$N(5&sB)~(lnYd%Z= zQ+Fq;OP=>&CPtP0rGsNrsDoNK|C>@R}WB`c7}LHKm!6!NS+JNUf%+GPxc zWBA0{!Tuq;40+9anG=*IVzq(%L1q5N^dYKf2Z~QSdByuxMnBgnUU^4d4?E(p`rjD= zJagN$TLFVBTsEtG|2U}{=O2;zA|Iid1ZGcekCHuE`8Q&O!?>I#muk_pN34rM_BstVBq+}>jIky)BtI4%LC!JByzL*eZ{(4v z!+=|c^u^g|hH=!>TVrS?%dg#IoPE#dLCC)`e?oR2c^f}7Aiqs{cXHNmceo=q=xW-$6Ro*je<&e63 z24gEm?H%go*@%<-$tRUE%~q$8Tf*{j&e4X`q~~;{Why7<`4{$f&3AgGc7$0%tV6y# zcgkSoIVkBm&qe>xxu@~cu?qjwthv1&=JmbSsGgN6=K-fmPSUW>D>M74T0F8T?)7re zi{1UOJb#ERJ3Gb7_rj|ybKa?iTk-v2M2x8&zUTR~_@3Qpso|ZL9i9jA&rbKg)r_DnTL89p7$}-C*`lkBG)wKk`%e$&ff|syK_*<^u zQRaxcV%IOZ0){WszK&xg&Leoac}w>)6pMI|WXY!Mdt9$B`&{RYr)h#V1lL0%%a!!2 zqZ;L?SPhCCx{irBCP^ss_w0v?_AP6Y=FM0ca$=;^%RHHPTe&^OZ(ubc-UEKjboq3v zzKWXjAUt^l(f2{fK9X%a8s+OV521D7MEB6LtXPwDj@h}L9G>Dh@BC%VpmXNuT}q8L zMaoSfUti5=ixp+cTp9n?2&*+`+F{N5WL~Dx*qP=v&im>#@|1m!_xhDFu`O!`9rcX0 zQRp_Jg>OQl*01t1W_+E7*Kw9f)kEzySMGP{I%Jj5lGNpSy_CI&ex=epTVy}Q>z7`x z?WyfYKCWqUGCra)##2OB`?`4*hJQcn`VOl7_d*6#+cDKU^%!&OObr+Q8lE~8(Cmhm z*BVZj=fz?J_Jh0f+?^s?d1*u$u1`vHwE3zz z)~8>Mozu2smpXiT z?&illbEqzVXbgr#_mV>1Rf#<2Zp=Tk=4ms00U}4vT_l4$&ZgVu8Q(*e=!@VCG}yb{ z|L;UKS&6;2y=mpT#*EoqVSZ4R>U+^YRUTzU@z`48{z%0!oL)nmK{gCOD(PtVx_+cO zgU9a!-|c`?C+{Iq_49YL=hOX)Is69J$6#LEb~W_D$MJJLw8E``pC11he5JXsG40e8 zR12aXYY(+U=+C0fUsil2$*{H_y5{~qZF`kXnxmTP@1(lzq+M=?-stDm$%-pdBeOow zata^$**EV-o8!h_^`h{H_m`#0NqKTI&(nCPuYuFvj(OB0 znAdCF_L{!2ny#gxypu|9Vc3J<<=Gg<%Bf51VmDb3z5PX4;bdQAKZY#r7+o!$|5*nK z`}46jKSjY>Z_TW)C_bh|Q~DtEj-pqvO|63~%v^)E&hJQYoP7igwx>n7k!u6H*ta(Z z?X&TY2aUh1l@bSmoBkem$r_cTHcs4D@*MLM1j<#?vT9@7(b^u*_hCgfXFDCD^sQS{ z+tXWW?dbztKJ{Vh2&q3n`wy)vZ?)H#vvs^i-B#iyRc>kLt@T{E{@mrVqJ}l0^u~T) zSd{$f`R`|mfm|CovMFh!ooO6T^NehXVrZQQ%AA4r+THIgBbJbS1hwBA!MWs0&>;9l z=>(&Vr~~8)^cQbx&kr@eHW+};qV9HZ%+ts) zr&Z+4ReMK4t>F@%YCq*ColQN87+8A-p7Y4rQ0(k`v7X3htW`_A+1ZkvE>X_kE?+7TL4R3?H%2Ai~N$g(B940$(-S6sH z%K7Q?>7~d;0TXe=llVP-Sb*RLGDC*UCWh!^Aq|Lj>bdoS95T2&aGlx~g?- z@7Aq7xyQ7-dPLVF@5t`Z>>#EYqH>-3SRtWPH<9s_ci^P*!??2@_c()GxlD1` zxve{UD|9wJwrt(7wbH(b=DoDv!AY!DoZ^X@L6*6Rw~3;XY3cgZjw z2aK0u76~}{n{!1{+uP9|rx)tZPpPP}d z!Vayu?ZJ-RhzOk7IdYR)pAO%79w;B3qAT`-C8B>`MB?UIW8hvuxErwHC4m9p{W91br3rO69ugab0{rtM?NqPPpZhzrx-q#pdwqL*k zq1C@Dv~Bj>`fDn!v-`WuaFS+D!oh|s10ik(lwUX z?ar$HxXz?D?B!mz$&ZrXfn~4?- zsV9hgtR9JEMm?lzq?d7Caoux1LxBH~*DsX$6}rCtL7B$hZSPn z> zaW@v6D@H_8`djSH_jTZ#Y|d&b4~@(HgL!0j>Nv*Lu&(1VLznzRsK44T^f&d@JVXRZ zz6zdA-k2jTNeme+{2w9)-q{IULR)>yQ!rA-N_Btr-x`BjIN90Qp9eH#lJCS05UL7i zAFH;ttD&g>)_6CUBO6CGxAA@SdeVK=q}$%<^aGi6JgBKqjFJ44z$zG}d&a?5MpDL? zuf(-u>7~B)zxTuyC!u$-1w=@)3fzHTIlala94GyGFW8D?X_O}*!e@ry?nrH-_RL$S z(PG|~$2$!+>zY;EVjV%&n`{nQ1@wfpp>`cRh}ksPRLTQcuM0|_(_?|tlO9U`nfy0A zF?284%LGqZ12&e6tb0zTcdi6h9(QXN4q{&;a{t3w7xQ|3P2R^i?YT~pTGTARX;-+J z(s>>0>1H$fEw8p)=$Dt1&oCtL)%|Wwc5S;#jE(WEjK4hhgrwx2Ri4Q*4Ak0y=T{}e zHU_S<%eGlsjNKACWwE3&Y8vIf%#_Sh*#lYD^|__|8@wZV(999S0Vl~blPIVsa+ z-AUh$OIy&)jY3zSDI;r_>*%&#PnbISxiR+ojWK2}9ErWN#xu=FQ-6<+N+a0{dA9#Q z*4CJJ4PlA}MUhtAY$(tUIY*^6Z6}SPEm@+~oLJf|*m}>2sojQs5&u1OH7@0Q3V#Wo zrxRPd?^a*6mT6gAe?N51EkdI6ylC5fysW6(?I#KNB63!m8I*)kQH=kmSicpI%PWP8 zOKW)Yn7j{NDW4ak#}e9c-oM-RbzF?&x5x6b-D5=!b~u4VV3E zpg5V2eVVbop|i#kmZnkHZJZ>2b{oD*2a5ydLNi4Zod)dlVJ>}ZCUO$~S^VYDwAHKNuN&AO<%UhAe?y%Bw0h_#sS zEq;+f@@`~&`@55f3N4)PMMeybkB3Z-xK*bmNLx=-!3D<2CzDxWrk(r_wO&-`Fe_O{ z>WSX2ibkuO!ep)RQ^;LwPKs9cyTAJ|@D|lcWo_B-yEb$}g0kSQ0(djtTt8xP`|%R~ z{P%Gqf$!3(lyHzL5j?*O%$K`2_|1@&F=zhnXMxp^0vc+}@-LDW1%Letj~2blZ>WJc zorM4Txkj>@o;Bq2W=Zpi&Kx3r=qQCueNAas5w=!N;^F$_w;{>2ZgY<%cbqooyEk9m zt9XEu_YuK=#IcXRV);x6}|YHZxU z*R(rNR^M06>T0i6>s!#?@S$5c=;VkyTFGtvdu`L^i#H7ea_X;hyx9NYd7_)!%fBkA zmCJqpdpUDc(tiy9Vv*WbN%Q}z16#Eo`?RRrr>pskTn{!Ik4Sb^yldF%=jHryYbOlh zDId3S)v|es)5_W?d^G0spa*(JtAhE9C*Fa`^PbAvva4Tx8`em1grI)Xgc{Vkj6!UDNzn*U!eXh_Ce`@x2$Sqi4XJege zGzM>pdeTJwonJ)Bd~crf^Q5V%Aj-p5k#9afPuh~yzpnNK`tR#bo?~*J%at=R%8r6e*#W8T1TE3R+`|7-i zT4&zTx(|sYUa?01F`P!2M{@XibK|t(69%aZn1g*$we&yA^L}5}d4a6Obfu(rz9YM$ z6Tp!hCB?8R^R=9-;M zr%aEVhY>B`2pkS$_H<+(`Ez~T7<|-JS}pU&1=Zwa!H5YSM~3iaokTum1k>>6af>qM z=&|`6e_dba>kscd2?Wf$`IPGGi~opRGn|-CVo8x$d2%~(ndyHPvgy&U*Hk6nuqUhG zJSoOkN!071t8@ydPI2^Un`7i+jLy-Xl&fqE)vC}dkN3C21D|^;=IPRFs@~5}enh4; zw})P5@5J)_N9_hv+XQyz_%!Bes2e0xME;BY(!}%RbZDFEa_^w#3>wyC z3?*f=9P23eaxvOv_6!)5RZi_YcT9iSuBS0``5^jl?J0r=;T^-t%-Iy1wu+w9H1g?a z;`tglKb=_mHs|@5e?qFGmz+E5Oir11o2xt1?4Udv{B|;cKE3gBaPaxLn)A->n_AUn zsw?KsY403=e(xlCUt8BQ;Hvn!jGi;nbaJ zoHFfgAoe=0anb8k@Bda<8t==Q&W9?`8Im_lP2U*L+`KPMYaMHg{kos!G{hyoZ>u2B z@;EB=ToucDm87fBPg-)t7?E{aFHbDiKS}A;JZZ@^AG%snEF(z!A7ganXwrTp=cgmF zdm}M5yCJQsejCEIaJv;4W-bOlOgmtCuJrtLr1R5}#_>k(MU=T_ujK!6oQ^akDZby* zPOd%7t1o`sCnI^ZLs#2t&*%1|xYzj%qam#LzIo)V;+))z%xjld`^Kqu*Kqq5WBT&C4LcOv5G$GVEu(mY3ks&&SMR$|?c z*1emReAWxmTfYA;{hY>m5$y6|KaKr9%1rC|`4QK9t8uPI#=|FS`22_@N#Ky#ocTL% hUk`e)ZyEF+bGn4-x3S!hdHnej, - exclusions_rx: tokio::sync::watch::Receiver, -) -> anyhow::Result<()> { - native_handler::run_native_tunnel(config, shutdown, exclusions_rx).await -} - -use tokio::sync::{mpsc, watch}; - -use crate::config::{ExclusionConfig, LocalProxyConfig, OstpConfig}; - -pub use proxy::run_local_socks5_proxy; - -#[derive(Debug)] -pub enum ProxyEvent { - NewStream { - stream_id: u16, - target: String, - }, - UdpAssociate { - stream_id: u16, - }, - UdpData { - stream_id: u16, - target: String, - payload: bytes::Bytes, - }, - Data { - stream_id: u16, - payload: bytes::Bytes, - }, - Close { - stream_id: u16, - }, -} - -#[derive(Debug)] -pub enum ProxyToClientMsg { - ConnectOk, - Data(bytes::Bytes), - UdpData(String, bytes::Bytes), - Close, - Error(String), -} - - -pub async fn run_local_proxy( - cfg: LocalProxyConfig, - ostp: OstpConfig, - exclusions_rx: watch::Receiver, - debug: bool, - shutdown: watch::Receiver, - proxy_events_tx: mpsc::Sender, - client_msgs_rx: mpsc::UnboundedReceiver<(u16, ProxyToClientMsg)>, -) -> anyhow::Result<()> { - run_local_socks5_proxy(cfg, ostp, exclusions_rx, debug, shutdown, proxy_events_tx, client_msgs_rx).await -} - -pub mod exclusion; pub mod process_lookup; pub mod sni_sniff; diff --git a/ostp-client/src/tunnel/native_handler.rs b/ostp-client/src/tunnel/native_handler.rs deleted file mode 100644 index bb9ce10..0000000 --- a/ostp-client/src/tunnel/native_handler.rs +++ /dev/null @@ -1,744 +0,0 @@ -use anyhow::{anyhow, Result}; -use tokio::sync::watch; - -// ────────────────────────────────────────────────────────────────────────────── -// Windows / Linux desktop TUN -// ────────────────────────────────────────────────────────────────────────────── - -#[cfg(any(target_os = "windows", target_os = "linux"))] -pub async fn run_native_tunnel( - config: crate::config::ClientConfig, - mut shutdown: watch::Receiver, - mut exclusions_rx: watch::Receiver, -) -> Result<()> { - use std::net::ToSocketAddrs; - use netstack_smoltcp::StackBuilder; - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - use futures::{StreamExt, SinkExt}; - - #[cfg(target_os = "linux")] - { - use std::io::{self, IsTerminal, Write}; - if io::stdout().is_terminal() { - println!("\n==================================================================="); - println!("WARNING: TUN mode will modify the system routing table."); - println!("If you are connected to a headless server via SSH, you may lose"); - println!("your connection when default routes are redirected into the tunnel."); - println!("===================================================================\n"); - print!("Are you sure you want to initialize the TUN interface? [yes/no]: "); - io::stdout().flush().unwrap(); - - let mut input = String::new(); - io::stdin().read_line(&mut input).unwrap(); - let ans = input.trim().to_lowercase(); - if ans != "y" && ans != "yes" { - return Err(anyhow!("TUN initialization aborted by user.")); - } - } - } - - let debug = config.debug; - tracing::info!("Initializing NATIVE TUN tunnel (smoltcp)..."); - - // Capture physical interface index for bypass BEFORE we create the TUN device and alter routes. - #[cfg(target_os = "windows")] - let phys_if_for_bypass: Option = ostp_tun::windows::windows_route::sys::get_default_ipv4_route().map(|(_, idx)| idx); - #[cfg(not(target_os = "windows"))] - let phys_if_for_bypass: Option = None; - - // ── 1. Resolve server IP ────────────────────────────────────────────────── - let server_ip = config - .ostp - .server_addr - .to_socket_addrs() - .map_err(|e| anyhow!("Failed to resolve server IP: {}", e))? - .next() - .map(|a| a.ip()) - .ok_or_else(|| anyhow!("Could not resolve server host"))?; - #[allow(unused_variables)] - let server_ip_str = server_ip.to_string(); - - // ── 2. Resolve excluded domains → IP addresses for bypass routing ───────── - let mut bypass_ips: Vec = Vec::new(); - - // Server IP always bypasses TUN - bypass_ips.push(server_ip); - - for ip_str in &config.exclusions.ips { - let host = ip_str.split('/').next().unwrap_or(ip_str); - if let Ok(ip) = host.parse() { - bypass_ips.push(ip); - } - } - - for domain in &config.exclusions.domains { - match tokio::net::lookup_host((domain.as_str(), 443u16)).await { - Ok(addrs) => { - for addr in addrs { - bypass_ips.push(addr.ip()); - } - } - Err(e) => { - tracing::warn!("Failed to pre-resolve excluded domain {domain}: {e}"); - } - } - } - - - // ── 3. Create TUN device via ostp-tun crate ─────────────────────────────── - let opts = ostp_tun::OstpTunOptions { - server_ip, - bypass_ips, - dns_server: config.dns_server.clone(), - kill_switch: config.kill_switch, - mtu: config.ostp.mtu as u16, - wintun_path: None, - }; - - let tun_interface = ostp_tun::OstpTunInterface::create(opts) - .await - .map_err(|e| anyhow!("Failed to create OstpTunInterface: {}", e))?; - - let dev = tun_interface.device; - let _route_guard = tun_interface.guard; - - // ── 7. Build smoltcp network stack ──────────────────────────────────────── - let (stack, tcp_runner, udp_socket, tcp_listener) = StackBuilder::default() - .stack_buffer_size(1024) - .tcp_buffer_size(1024) - .udp_buffer_size(1024) - .enable_tcp(true) - .enable_udp(true) - .mtu(config.ostp.mtu) - .build()?; - - let mut runner_task = tokio::spawn(async move { - if let Some(runner) = tcp_runner { - let _ = runner.await; - } - }); - - // ── 8. Wire TUN ↔ smoltcp stack ─────────────────────────────────────────── - let (mut stack_sink, mut stack_stream) = stack.split(); - let (mut tun_read, mut tun_write) = tokio::io::split(dev); - - let mut tun_to_stack = tokio::spawn(async move { - let mut buf = vec![0u8; 65536]; - loop { - match tun_read.read(&mut buf).await { - Ok(0) => break, - Ok(n) => { - let frame = buf[..n].to_vec(); - if let Err(e) = stack_sink.send(frame).await { - if e.kind() == std::io::ErrorKind::BrokenPipe { - break; - } - } - } - Err(e) => { - tracing::debug!("tun_read error: {e}"); - } - } - } - }); - - let mut stack_to_tun = tokio::spawn(async move { - while let Some(Ok(frame)) = stack_stream.next().await { - if let Err(e) = tun_write.write(&frame).await { - tracing::debug!("tun_write error: {e}"); - } - } - }); - - // ── 9. UDP: forward everything through OSTP proxy ───────────────────────── - // UDP exclusions are handled at the routing table level (step 5), so - // UDP packets for excluded IPs never reach smoltcp at all. - let udp_proxy_addr = { - let mut a = config.local_proxy.bind_addr.clone(); - if a.starts_with("0.0.0.0:") { - a = a.replace("0.0.0.0:", "127.0.0.1:"); - } - 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 { - #[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; - } - }); - - // ── 10. TCP: forward to OSTP proxy (with domain-level bypass via SNI) ───── - // - // For IP-based exclusions: handled by routing table → packets never arrive here. - // For domain-based exclusions: The IP is already in routing table (pre-resolved in - // step 3), so most traffic won't arrive. As a belt-and-suspenders fallback, - // we also sniff TLS SNI and bypass if it matches — this covers CDN cases where - // the IP wasn't known at startup. - // - // For bypassed connections we bind the outgoing socket to the physical interface - // (IP_UNICAST_IF) so it goes out via the real NIC, not TUN. - - let proxy_addr_tcp = { - let mut a = config.local_proxy.bind_addr.clone(); - if a.starts_with("0.0.0.0:") { - a = a.replace("0.0.0.0:", "127.0.0.1:"); - } - a - }; - - // Physical interface index was captured at the start of the function. - - let mut tcp_accept_task = tokio::spawn(async move { - let Some(mut listener) = tcp_listener else { return; }; - - while let Some((mut stream, local, remote)) = listener.next().await { - let proxy_addr = proxy_addr_tcp.clone(); - let matcher_arc = matcher_arc.clone(); - #[cfg(target_os = "linux")] - let lin_name = linux_phys_name.clone(); - - tokio::spawn(async move { - let matcher = matcher_arc.read().await.clone(); - if debug { - tracing::debug!("TUN TCP {local} → {remote}"); - } - - // ── Sniff TLS ClientHello for SNI ───────────────────────────── - let mut sniff_buf = [0u8; 2048]; - let sniff_len = - match tokio::time::timeout( - std::time::Duration::from_millis(100), - stream.read(&mut sniff_buf), - ) - .await - { - Ok(Ok(n)) => n, - _ => 0, - }; - - // ── Decide: bypass or tunnel? ───────────────────────────────── - let mut should_bypass = false; - - // 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 debug { - tracing::debug!("TUN TCP lookup: port {} -> process {}", local.port(), proc_name); - } - if matcher.match_process(&proc_name) { - if debug { - tracing::debug!("TUN TCP BYPASS (Process match): {} → {remote}", proc_name); - } - should_bypass = true; - } - } else { - if debug { - tracing::debug!("TUN TCP lookup: port {} -> no process found", local.port()); - } - } - } - - // 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]) - { - if debug { - tracing::debug!("TUN SNI: {sni}"); - } - if matcher.match_domain(&sni) { - if debug { - tracing::info!("TUN TCP BYPASS (SNI domain): {sni} → {remote}"); - } - should_bypass = true; - } - } - } - - // 3. Destination IP CIDR check (for IPs not in routing table / IPv6) - if !should_bypass && matcher.match_ip(&remote.ip()) { - if debug { - tracing::info!("TUN TCP BYPASS (IP match): {remote}"); - } - should_bypass = true; - } - - // ── Bypass path: direct TCP bypassing TUN ───────────────────── - if should_bypass { - let socket = match remote { - std::net::SocketAddr::V4(_) => tokio::net::TcpSocket::new_v4(), - std::net::SocketAddr::V6(_) => tokio::net::TcpSocket::new_v6(), - }; - let Ok(socket) = socket else { return; }; - - // Bind to physical interface so packets don't loop back into TUN - - #[cfg(target_os = "windows")] - if let Some(idx) = phys_if_for_bypass { - if let Err(e) = crate::tunnel::proxy::bind_socket_to_interface( - &socket, - remote.is_ipv6(), - idx, - ) { - tracing::error!("TUN TCP BYPASS failed to bind to physical interface {}: {}", idx, e); - } else { - if debug { - tracing::info!("TUN TCP BYPASS bound to physical interface {}", idx); - } - } - } else { - tracing::warn!("TUN TCP BYPASS has no physical interface index!"); - } - #[cfg(target_os = "linux")] - if let Some(ref name) = lin_name { - let _ = crate::tunnel::proxy::bind_socket_to_interface(&socket, name); - } - - match tokio::time::timeout( - std::time::Duration::from_secs(10), - socket.connect(remote), - ) - .await - { - Ok(Ok(mut direct)) => { - if sniff_len > 0 { - if direct.write_all(&sniff_buf[..sniff_len]).await.is_err() { - return; - } - } - let _ = tokio::io::copy_bidirectional(&mut stream, &mut direct).await; - } - _ => { - tracing::debug!("Direct bypass connect to {remote} failed"); - } - } - return; - } - - // ── Tunnel path: forward via local OSTP SOCKS5 proxy ────────── - let Ok(mut socks) = tokio::net::TcpStream::connect(&proxy_addr).await else { - return; - }; - - // SOCKS5 handshake (no auth) - if socks.write_all(&[5, 1, 0]).await.is_err() { return; } - let mut buf2 = [0u8; 2]; - if socks.read_exact(&mut buf2).await.is_err() || buf2[0] != 5 || buf2[1] != 0 { - return; - } - - // CONNECT request - let mut req = vec![5u8, 1, 0]; - match remote.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(&remote.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; } - - // Replay sniffed bytes - if sniff_len > 0 && socks.write_all(&sniff_buf[..sniff_len]).await.is_err() { - return; - } - - let _ = tokio::io::copy_bidirectional(&mut stream, &mut socks).await; - }); - } - }); - - tracing::info!("NATIVE TUN tunnel active."); - - tokio::select! { - _ = shutdown.changed() => {} - _ = &mut runner_task => {} - _ = &mut tun_to_stack => {} - _ = &mut stack_to_tun => {} - _ = &mut udp_proxy_task => {} - _ = &mut tcp_accept_task => {} - } - - tracing::info!("Deactivating NATIVE TUN tunnel..."); - - // ── Cleanup ─────────────────────────────────────────────────────────────── - // Cleanup is handled automatically by the _route_guard Drop trait in ostp-tun - - Ok(()) -} - -// ────────────────────────────────────────────────────────────────────────────── -// Stub for unsupported platforms -// ────────────────────────────────────────────────────────────────────────────── - -#[cfg(not(any(target_os = "windows", target_os = "linux")))] -pub async fn run_native_tunnel( - _config: crate::config::ClientConfig, - _shutdown: watch::Receiver, - _exclusions_rx: watch::Receiver, -) -> Result<()> { - Err(anyhow!("Native TUN tunnel is only supported on Windows/Linux")) -} - -// ────────────────────────────────────────────────────────────────────────────── -// Android: TUN from file-descriptor (opened by VpnService) -// ────────────────────────────────────────────────────────────────────────────── - -#[cfg(target_os = "android")] -pub async fn run_native_tunnel_from_fd( - config: crate::config::ClientConfig, - mut shutdown: watch::Receiver, - mut exclusions_rx: watch::Receiver, - fd: i32, -) -> Result<()> { - use netstack_smoltcp::StackBuilder; - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - use futures::{StreamExt, SinkExt}; - use std::os::unix::io::{FromRawFd, AsRawFd}; - - let debug = config.debug; - tracing::info!("Initializing NATIVE TUN tunnel on Android (FD {})", fd); - - unsafe { - let flags = libc::fcntl(fd, libc::F_GETFL); - if flags >= 0 { - libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK); - } - } - - let read_fd = unsafe { libc::dup(fd) }; - if read_fd < 0 { - return Err(anyhow!("Failed to dup tun fd for reading")); - } - - let file = unsafe { std::fs::File::from_raw_fd(read_fd) }; - let tun_stream = tokio::io::unix::AsyncFd::new(file)?; - - let (stack, tcp_runner, udp_socket, tcp_listener) = StackBuilder::default() - .stack_buffer_size(1024) - .tcp_buffer_size(1024) - .udp_buffer_size(1024) - .enable_tcp(true) - .enable_udp(true) - .mtu(config.ostp.mtu) - .build()?; - - let mut runner_task = tokio::spawn(async move { - if let Some(runner) = tcp_runner { - let _ = runner.await; - } - }); - - let (mut stack_sink, mut stack_stream) = stack.split(); - - let _tun_to_stack = tokio::spawn(async move { - let mut buf = vec![0u8; 65536]; - loop { - let mut guard = match tun_stream.readable().await { - Ok(g) => g, - Err(_) => break, - }; - let n = match guard.try_io(|inner| { - let res = unsafe { - libc::read( - inner.as_raw_fd(), - buf.as_mut_ptr() as *mut libc::c_void, - buf.len(), - ) - }; - if res < 0 { - let err = std::io::Error::last_os_error(); - if err.kind() == std::io::ErrorKind::WouldBlock { - Err(err) - } else { - Ok(0_isize) - } - } else { - Ok(res) - } - }) { - Ok(Ok(n)) if n > 0 => n as usize, - Ok(Ok(_)) => continue, - Ok(Err(_)) => continue, - Err(_) => continue, - }; - - let frame = buf[..n].to_vec(); - if let Err(e) = stack_sink.send(frame).await { - if e.kind() == std::io::ErrorKind::BrokenPipe { - break; - } - } - } - }); - - let write_fd = unsafe { libc::dup(fd) }; - if write_fd < 0 { - return Err(anyhow!("Failed to dup tun fd for writing")); - } - unsafe { - let flags = libc::fcntl(write_fd, libc::F_GETFL); - if flags >= 0 { - libc::fcntl(write_fd, libc::F_SETFL, flags | libc::O_NONBLOCK); - } - } - let write_file = unsafe { std::fs::File::from_raw_fd(write_fd) }; - let tun_write_stream = tokio::io::unix::AsyncFd::new(write_file)?; - - let _stack_to_tun = tokio::spawn(async move { - while let Some(Ok(frame)) = stack_stream.next().await { - let mut written = 0; - while written < frame.len() { - let mut guard = match tun_write_stream.writable().await { - Ok(g) => g, - Err(_) => break, - }; - let res = guard.try_io(|inner| { - let res = unsafe { - libc::write( - inner.as_raw_fd(), - frame[written..].as_ptr() as *const libc::c_void, - frame.len() - written, - ) - }; - if res < 0 { - let err = std::io::Error::last_os_error(); - if err.kind() == std::io::ErrorKind::WouldBlock { - Err(err) - } else { - Ok(res) - } - } else { - Ok(res) - } - }); - match res { - Ok(Ok(n)) if n > 0 => written += n as usize, - Ok(Ok(_)) => break, - Ok(Err(_)) => break, - Err(_) => continue, - } - } - } - }); - - let mut proxy_addr = config.local_proxy.bind_addr.clone(); - if proxy_addr.starts_with("0.0.0.0:") { - proxy_addr = proxy_addr.replace("0.0.0.0:", "127.0.0.1:"); - } - - 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!("Android TUN exclusions hot-reloaded"); - } - } - }); - - 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; }; - - while let Some((mut stream, local, remote)) = listener.next().await { - let proxy_addr = proxy_addr.clone(); - let matcher_arc = matcher_arc.clone(); - - tokio::spawn(async move { - let matcher = matcher_arc.read().await.clone(); - - if true { - tracing::debug!("Android TUN TCP {local} → {remote}"); - } - - // Sniff SNI - let mut sniff_buf = [0u8; 2048]; - let sniff_len = - match tokio::time::timeout( - std::time::Duration::from_millis(100), - stream.read(&mut sniff_buf), - ) - .await - { - Ok(Ok(n)) => n, - _ => 0, - }; - - let mut should_bypass = false; - - // 1. SNI domain - if sniff_len > 0 { - if let Some(sni) = - crate::tunnel::sni_sniff::extract_sni(&sniff_buf[..sniff_len]) - { - if true { tracing::debug!("Android TUN SNI: {sni}"); } - if matcher.match_domain(&sni) { - should_bypass = true; - } - } - } - - // 2. Process (Android: /proc/net lookup) - if !should_bypass { - if let Some(exe) = - crate::tunnel::process_lookup::get_process_name_from_port(local.port()) - { - if true { - tracing::debug!("Android TUN port {} → EXE: {}", local.port(), exe); - } - if matcher.match_process(&exe) { - should_bypass = true; - } - } - } - - // 3. IP CIDR - if !should_bypass && matcher.match_ip(&remote.ip()) { - should_bypass = true; - } - - // Bypass: connect directly (Android VPN service already protects the socket - // from re-entering the TUN through VpnService.protect()) - if should_bypass { - if true { - tracing::debug!("Android TUN BYPASS: {remote}"); - } - let socket = match remote { - std::net::SocketAddr::V4(_) => tokio::net::TcpSocket::new_v4(), - std::net::SocketAddr::V6(_) => tokio::net::TcpSocket::new_v6(), - }; - let Ok(socket) = socket else { return; }; - - match tokio::time::timeout( - std::time::Duration::from_secs(10), - socket.connect(remote), - ) - .await - { - Ok(Ok(mut direct)) => { - if sniff_len > 0 { - if direct.write_all(&sniff_buf[..sniff_len]).await.is_err() { - return; - } - } - let _ = tokio::io::copy_bidirectional(&mut stream, &mut direct).await; - } - _ => { - tracing::debug!("Android bypass connect to {remote} failed"); - } - } - return; - } - - // Tunnel via SOCKS5 proxy - let Ok(mut socks) = tokio::net::TcpStream::connect(&proxy_addr).await else { - return; - }; - if socks.write_all(&[5, 1, 0]).await.is_err() { return; } - let mut buf2 = [0u8; 2]; - if socks.read_exact(&mut buf2).await.is_err() || buf2[0] != 5 || buf2[1] != 0 { - return; - } - let mut req = vec![5u8, 1, 0]; - match remote.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(&remote.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; } - if sniff_len > 0 && socks.write_all(&sniff_buf[..sniff_len]).await.is_err() { - return; - } - let _ = tokio::io::copy_bidirectional(&mut stream, &mut socks).await; - }); - } - }); - - tracing::info!("NATIVE TUN (Android) tunnel active."); - - tokio::select! { - _ = shutdown.changed() => {} - _ = &mut runner_task => {} - _ = _tun_to_stack => {} - _ = _stack_to_tun => {} - _ = &mut udp_proxy_task => {} - _ = &mut tcp_accept_task => {} - } - - tracing::info!("NATIVE TUN (Android) deactivated."); - Ok(()) -} - -#[cfg(not(target_os = "android"))] -pub async fn run_native_tunnel_from_fd( - _config: crate::config::ClientConfig, - _shutdown: watch::Receiver, - _exclusions_rx: watch::Receiver, - _fd: i32, -) -> Result<()> { - Err(anyhow!("Native TUN from FD is only supported on Android")) -} diff --git a/ostp-client/src/tunnel/outbounds/block.rs b/ostp-client/src/tunnel/outbounds/block.rs new file mode 100644 index 0000000..1530e89 --- /dev/null +++ b/ostp-client/src/tunnel/outbounds/block.rs @@ -0,0 +1,14 @@ +use anyhow::{anyhow, Result}; +use tokio::net::TcpStream; + +pub async fn dial_tcp(_target_host: &str, _target_port: u16) -> Result { + Err(anyhow!("Connection blocked by routing rule")) +} + +pub async fn handle_udp( + _client_src: std::net::SocketAddr, + _target_dst: std::net::SocketAddr, + _payload: bytes::Bytes, +) -> Result<()> { + Err(anyhow!("Connection blocked by routing rule")) +} diff --git a/ostp-client/src/tunnel/outbounds/direct.rs b/ostp-client/src/tunnel/outbounds/direct.rs new file mode 100644 index 0000000..d1068d1 --- /dev/null +++ b/ostp-client/src/tunnel/outbounds/direct.rs @@ -0,0 +1,99 @@ +use anyhow::{anyhow, Result}; +use tokio::net::TcpStream; + +#[cfg(target_os = "windows")] +pub fn bind_socket_to_interface(socket: &tokio::net::TcpSocket, is_ipv6: bool, if_index: u32) -> std::io::Result<()> { + use std::os::windows::io::AsRawSocket; + use winapi::shared::ws2def::{IPPROTO_IP, IPPROTO_IPV6}; + + // These constants are defined as 31 in the Windows SDK. + const IP_UNICAST_IF: i32 = 31; + const IPV6_UNICAST_IF: i32 = 31; + + let fd = socket.as_raw_socket() as usize; + let idx_net = if_index.to_be(); + + let (level, optname) = if is_ipv6 { + (IPPROTO_IPV6 as i32, IPV6_UNICAST_IF) + } else { + (IPPROTO_IP as i32, IP_UNICAST_IF) + }; + + let ret = unsafe { + winapi::um::winsock2::setsockopt( + fd, + level as i32, + optname as i32, + &idx_net as *const _ as *const i8, + std::mem::size_of_val(&idx_net) as i32, + ) + }; + + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) +} + +#[cfg(target_os = "linux")] +pub fn bind_socket_to_interface(socket: &tokio::net::TcpSocket, _is_ipv6: bool, if_name: &str) -> std::io::Result<()> { + use std::os::unix::io::AsRawFd; + let fd = socket.as_raw_fd(); + let name_bytes = if_name.as_bytes(); + let ret = unsafe { + libc::setsockopt( + fd, + libc::SOL_SOCKET, + libc::SO_BINDTODEVICE, + name_bytes.as_ptr() as *const libc::c_void, + name_bytes.len() as libc::socklen_t, + ) + }; + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) +} + +#[cfg(target_os = "macos")] +pub fn bind_socket_to_interface(socket: &tokio::net::TcpSocket, _is_ipv6: bool, if_index: u32) -> std::io::Result<()> { + // macOS uses IP_BOUND_IF for IPv4 and IPV6_BOUND_IF for IPv6, similar to Windows + use std::os::unix::io::AsRawFd; + let fd = socket.as_raw_fd(); + + // We can implement this later, for now just a stub so compilation works + tracing::debug!("macOS socket binding not yet fully implemented for interface {}", if_index); + Ok(()) +} + +pub async fn dial_tcp(target_host: &str, target_port: u16, phys_if_idx: Option) -> Result { + let addrs = tokio::net::lookup_host((target_host, target_port)).await?.collect::>(); + if addrs.is_empty() { + return Err(anyhow!("Could not resolve target host: {}", target_host)); + } + + let target_addr = addrs[0]; + let socket = match target_addr { + std::net::SocketAddr::V4(_) => tokio::net::TcpSocket::new_v4()?, + std::net::SocketAddr::V6(_) => tokio::net::TcpSocket::new_v6()?, + }; + + #[cfg(target_os = "windows")] + if let Some(idx) = phys_if_idx { + if let Err(e) = bind_socket_to_interface(&socket, target_addr.is_ipv6(), idx) { + tracing::warn!("DIRECT: Failed to bind to physical interface {}: {}", idx, e); + } + } + + let stream = tokio::time::timeout(std::time::Duration::from_secs(10), socket.connect(target_addr)).await??; + Ok(stream) +} + +pub async fn handle_udp( + _client_src: std::net::SocketAddr, + _target_dst: std::net::SocketAddr, + _payload: bytes::Bytes, + _phys_if_idx: Option, +) -> Result<()> { + Err(anyhow!("Direct UDP is not yet fully implemented")) +} diff --git a/ostp-client/src/tunnel/outbounds/mod.rs b/ostp-client/src/tunnel/outbounds/mod.rs new file mode 100644 index 0000000..a9219cd --- /dev/null +++ b/ostp-client/src/tunnel/outbounds/mod.rs @@ -0,0 +1,78 @@ +use anyhow::{anyhow, Result}; +use std::sync::Arc; +use tokio::net::TcpStream; +use crate::tunnel::balancer::Balancer; +use crate::config::OutboundConfig; + +pub mod direct; +pub mod block; +pub mod ostp; +pub mod socks; + +pub struct OutboundManager { + balancer: Arc, + phys_if_index: Option, + phys_if_name: Option, +} + +impl OutboundManager { + pub fn new( + balancer: Arc, + phys_if_index: Option, + phys_if_name: Option, + ) -> Self { + Self { + balancer, + phys_if_index, + phys_if_name, + } + } + + pub async fn dial_tcp(&self, tag: &str, target_host: &str, target_port: u16) -> Result { + let concrete_config = self.balancer.get_concrete_outbound(tag) + .ok_or_else(|| anyhow!("Outbound tag '{}' not found or resolved to invalid node", tag))?; + + match concrete_config { + OutboundConfig::Direct { .. } => { + direct::dial_tcp(target_host, target_port, self.phys_if_index).await + } + OutboundConfig::Block { .. } => { + block::dial_tcp(target_host, target_port).await + } + OutboundConfig::Ostp { server, port, access_key, transport, multiplex, .. } => { + ostp::dial_tcp(server, *port, access_key, transport, multiplex).await + } + OutboundConfig::Socks { server, port, .. } => { + socks::dial_tcp(target_host, target_port, server, *port).await + } + _ => Err(anyhow!("Invalid concrete outbound type for {}", tag)), + } + } + + pub async fn handle_udp( + &self, + tag: &str, + client_src: std::net::SocketAddr, + target_dst: std::net::SocketAddr, + payload: bytes::Bytes, + ) -> Result<()> { + let concrete_config = self.balancer.get_concrete_outbound(tag) + .ok_or_else(|| anyhow!("Outbound tag '{}' not found or resolved to invalid node", tag))?; + + match concrete_config { + OutboundConfig::Direct { .. } => { + direct::handle_udp(client_src, target_dst, payload, self.phys_if_index).await + } + OutboundConfig::Block { .. } => { + block::handle_udp(client_src, target_dst, payload).await + } + OutboundConfig::Ostp { server, port, access_key, transport, multiplex, .. } => { + ostp::handle_udp(client_src, target_dst, payload, server, *port, access_key, transport, multiplex).await + } + OutboundConfig::Socks { server, port, .. } => { + socks::handle_udp(client_src, target_dst, payload, server, *port).await + } + _ => Err(anyhow!("Invalid concrete outbound type for {}", tag)), + } + } +} diff --git a/ostp-client/src/tunnel/outbounds/ostp.rs b/ostp-client/src/tunnel/outbounds/ostp.rs new file mode 100644 index 0000000..8a796a4 --- /dev/null +++ b/ostp-client/src/tunnel/outbounds/ostp.rs @@ -0,0 +1,28 @@ +use anyhow::{anyhow, Result}; +use tokio::net::TcpStream; +use crate::config::{TransportConfig, MultiplexConfig}; + +pub async fn dial_tcp( + _server: &str, + _port: u16, + _access_key: &str, + _transport: &TransportConfig, + _multiplex: &MultiplexConfig, +) -> Result { + // Ostp dialer implementation. + // For now returning an error until we migrate the local_proxy connection logic here. + Err(anyhow!("OSTP TCP dialer not yet fully migrated")) +} + +pub async fn handle_udp( + _client_src: std::net::SocketAddr, + _target_dst: std::net::SocketAddr, + _payload: bytes::Bytes, + _server: &str, + _port: u16, + _access_key: &str, + _transport: &TransportConfig, + _multiplex: &MultiplexConfig, +) -> Result<()> { + Err(anyhow!("OSTP UDP handler not yet fully migrated")) +} diff --git a/ostp-client/src/tunnel/outbounds/socks.rs b/ostp-client/src/tunnel/outbounds/socks.rs new file mode 100644 index 0000000..6039981 --- /dev/null +++ b/ostp-client/src/tunnel/outbounds/socks.rs @@ -0,0 +1,17 @@ +use anyhow::{anyhow, Result}; +use tokio::net::TcpStream; + +pub async fn dial_tcp(_target_host: &str, _target_port: u16, _server: &str, _port: u16) -> Result { + // SOCKS5 dialer implementation stub + Err(anyhow!("SOCKS outbound TCP dialer not yet implemented")) +} + +pub async fn handle_udp( + _client_src: std::net::SocketAddr, + _target_dst: std::net::SocketAddr, + _payload: bytes::Bytes, + _server: &str, + _port: u16, +) -> Result<()> { + Err(anyhow!("SOCKS outbound UDP handler not yet implemented")) +} diff --git a/ostp-client/src/tunnel/proxy.rs b/ostp-client/src/tunnel/proxy.rs deleted file mode 100644 index 4db81b5..0000000 --- a/ostp-client/src/tunnel/proxy.rs +++ /dev/null @@ -1,921 +0,0 @@ -use std::collections::HashMap; -use crate::tunnel::exclusion::ExclusionMatcher; -use anyhow::{anyhow, Context, Result}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::{TcpListener, TcpStream, UdpSocket}; -use std::sync::Arc; -use tokio::sync::{mpsc, watch}; -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")] -pub 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 { - // IPV6_UNICAST_IF expects interface index in host byte order - 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 { - // IP_UNICAST_IF expects interface index in NETWORK byte order (big-endian) - 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")] -pub 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(()) -} - -pub fn get_windows_physical_if_index() -> Option { - #[cfg(target_os = "windows")] - { - return ostp_tun::windows::windows_route::sys::get_default_ipv4_route().map(|(_, idx)| idx); - } - #[cfg(not(target_os = "windows"))] - { - None - } -} - -pub 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 -} - -#[allow(unused_variables)] -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()) - )) -} - -#[allow(unused_variables)] -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, - mut exclusions_rx: watch::Receiver, - debug: bool, - mut shutdown: watch::Receiver, - proxy_events_tx: mpsc::Sender, - mut client_msgs_rx: mpsc::UnboundedReceiver<(u16, ProxyToClientMsg)>, -) -> Result<()> { - let connect_timeout = Duration::from_millis(cfg.connect_timeout_ms.max(1)); - let listener = TcpListener::bind(&cfg.bind_addr) - .await - .with_context(|| format!("failed to bind local HTTP/SOCKS5 proxy at {}", cfg.bind_addr))?; - - if true { - tracing::info!("local HTTP/SOCKS5 proxy listening at {}", cfg.bind_addr); - tracing::info!("Windows system proxy: set HTTP proxy to {}. tun2socks: SOCKS5 on same address.", cfg.bind_addr); - } - - 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 mut current_exclusions = exclusions_rx.borrow().clone(); - let mut matcher = ExclusionMatcher::new(¤t_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); - - let mut next_stream_id: u16 = 1; - let mut active_streams: HashMap> = HashMap::new(); - - loop { - tokio::select! { - _ = shutdown.changed() => { - if *shutdown.borrow() { - break; - } - } - Ok(_) = exclusions_rx.changed() => { - current_exclusions = exclusions_rx.borrow().clone(); - matcher = ExclusionMatcher::new(¤t_exclusions, physical_if_index, physical_if_name.clone()); - if true { - tracing::info!("Local proxy exclusions hot-reloaded"); - } - } - accepted = listener.accept() => { - let (socket, _) = accepted?; - let stream_id = next_stream_id; - // Advance, skipping zero and any stream_id still in active_streams - loop { - next_stream_id = next_stream_id.wrapping_add(1); - if next_stream_id == 0 { next_stream_id = 1; } - if !active_streams.contains_key(&next_stream_id) { break; } - } - - let (tx, rx) = mpsc::unbounded_channel(); - active_streams.insert(stream_id, tx); - - let event_tx = proxy_events_tx.clone(); - let c_tx = connect_tx.clone(); - let matcher_clone = matcher.clone(); - tokio::spawn(async move { - if let Err(err) = handle_proxy_client( - socket, - stream_id, - event_tx, - rx, - c_tx, - connect_timeout, - debug, - matcher_clone, - max_chunk, - ).await { - let msg = err.to_string(); - // Suppress routine disconnects and unsupported SOCKS5 command attempts (like UDP) from spam logs - if !msg.contains("UnexpectedEof") - && !msg.contains("Connection reset") - && !msg.contains("Broken pipe") - && !msg.contains("unsupported SOCKS5 command") - && debug { - tracing::warn!("proxy client error: {err}"); - } - } - }); - } - Some((stream_id, msg)) = client_msgs_rx.recv() => { - if stream_id == 0 { - if let ProxyToClientMsg::Close = msg { - if true { - tracing::info!("Resetting all active proxy streams on reconnect"); - } - for (_, tx) in active_streams.drain() { - let _ = tx.send(ProxyToClientMsg::Close); - } - } - } else if let Some(tx) = active_streams.get(&stream_id) { - if tx.send(msg).is_err() { - active_streams.remove(&stream_id); - } - } - } - Some(stream_id) = connect_rx.recv() => { - active_streams.remove(&stream_id); - } - } - } - - Ok(()) -} - -/// Extracts `host:port` from an HTTP absolute-URI like `http://example.com/path` or `https://example.com`. -/// Falls back to the raw target if already in `host:port` form. -fn extract_host_port(uri: &str, default_port: u16) -> String { - let without_scheme = if let Some(rest) = uri.strip_prefix("https://") { - rest - } else if let Some(rest) = uri.strip_prefix("http://") { - rest - } else { - uri - }; - // Trim path/query fragment - let host_part = without_scheme.split('/').next().unwrap_or(without_scheme); - if host_part.contains(':') { - host_part.to_string() - } else { - format!("{}:{}", host_part, default_port) - } -} - -struct StreamGuard { - stream_id: u16, - close_tx: mpsc::Sender, -} - -impl Drop for StreamGuard { - fn drop(&mut self) { - let tx = self.close_tx.clone(); - let id = self.stream_id; - tokio::spawn(async move { - let _ = tx.send(id).await; - }); - } -} - -async fn handle_udp_associate( - mut client_tcp: TcpStream, - udp_socket: tokio::net::UdpSocket, - stream_id: u16, - event_tx: mpsc::Sender, - mut rx: mpsc::UnboundedReceiver, - close_tx: mpsc::Sender, - debug: bool, - matcher: ExclusionMatcher, - connect_timeout: Duration, -) -> Result<()> { - 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! { - res = client_tcp.read(&mut tcp_buf) => { - match res { - Ok(0) | Err(_) => break, - Ok(_) => {} - } - } - res = sock_rx.recv_from(&mut buf) => { - let (len, addr) = match res { - Ok(v) => v, - Err(e) => { - tracing::debug!("udp_associate recv_from error: {}", e); - continue; // transient error, don't kill the session - } - }; - { - let mut guard = client_udp_addr.lock().unwrap(); - if guard.is_none() { - *guard = Some(addr); - } - } - if len < 4 { continue; } - let frag = buf[2]; - if frag != 0 { continue; } // Fragmented UDP not supported - let atyp = buf[3]; - let (header_len, target) = match atyp { - 0x01 => { - 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, format!("{}:{}", ip, port)) - } - 0x03 => { - if len < 5 { continue; } - let domain_len = buf[4] as usize; - if len < 5 + domain_len + 2 { continue; } - let domain = String::from_utf8_lossy(&buf[5..5+domain_len]); - let port = u16::from_be_bytes([buf[5+domain_len], buf[5+domain_len+1]]); - (5 + domain_len + 2, format!("{}:{}", domain, port)) - } - 0x04 => { - 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, format!("[{}]:{}", ip, port)) - } - _ => continue, - }; - let payload = bytes::Bytes::copy_from_slice(&buf[header_len..len]); - - let target_host = if let Some((host, _)) = split_host_port(&target) { host } else { target.clone() }; - let target_port = match split_host_port(&target) { Some((_, p)) => p, None => 0 }; - // Check if target should bypass the tunnel - if matcher.should_bypass_target(&target_host, target_port, connect_timeout).await { - if true { - tracing::debug!("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 true { - tracing::warn!("failed to send bypass UDP packet to {}: {}", target_addr, e); - } - } - } - } - } - } else { - tracing::debug!("proxy.rs forwarding UDP DATA to server for target={} payload len={}", target, payload.len()); - 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) = { - 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"); - let host_str = parts.next().unwrap_or(&target); - let host_str = host_str.trim_start_matches('[').trim_end_matches(']'); - let port = port_str.parse::().unwrap_or(0); - - if let Ok(ipv4) = host_str.parse::() { - packet.push(0x01); - packet.extend_from_slice(&ipv4.octets()); - } else if let Ok(ipv6) = host_str.parse::() { - packet.push(0x04); - packet.extend_from_slice(&ipv6.octets()); - } else { - packet.push(0x03); - let bytes = host_str.as_bytes(); - packet.push(bytes.len() as u8); - packet.extend_from_slice(bytes); - } - packet.extend_from_slice(&port.to_be_bytes()); - packet.extend_from_slice(&data); - tracing::debug!("proxy.rs forwarding UDP REPLY to client_addr={} from server for target={} payload len={}", client_addr, target, data.len()); - let _ = sock_tx.send_to(&packet, client_addr).await; - } else { - tracing::error!("proxy.rs failed to parse target string as SocketAddr: {}", target); - } - } - Some(ProxyToClientMsg::Close) | Some(ProxyToClientMsg::Error(_)) | None => break, - _ => {} - } - } - } - } - let _ = close_tx.send(stream_id).await; - 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 true { - tracing::warn!("failed to send direct UDP response to client: {e}"); - } - } - } - } - Err(e) => { - if true { - tracing::debug!("direct UDP socket read loop exiting: {e}"); - } - break; - } - } - } - }); -} - -async fn handle_proxy_client( - mut client: TcpStream, - stream_id: u16, - event_tx: mpsc::Sender, - mut rx: mpsc::UnboundedReceiver, - close_tx: mpsc::Sender, - connect_timeout: Duration, - debug: bool, - matcher: ExclusionMatcher, - max_chunk: usize, -) -> Result<()> { - let _guard = StreamGuard { stream_id, close_tx: close_tx.clone() }; - - // Peek the first byte to distinguish SOCKS5 (0x05) from HTTP (any printable ASCII) - let mut first_byte = [0_u8; 1]; - client.read_exact(&mut first_byte).await?; - - let target: String; - let is_socks5 = first_byte[0] == 0x05; - - if is_socks5 { - // ── SOCKS5 Handshake ────────────────────────────────────────── - let mut second_byte = [0_u8; 1]; - client.read_exact(&mut second_byte).await?; - let nmethods = second_byte[0] as usize; - if nmethods > 0 { - let mut methods_buf = vec![0_u8; nmethods]; - client.read_exact(&mut methods_buf).await?; - } - // Reply: version=5, NO AUTHENTICATION - client.write_all(&[0x05, 0x00]).await?; - - // ── SOCKS5 Request ──────────────────────────────────────────── - let mut req = [0_u8; 4]; - client.read_exact(&mut req).await?; - if req[0] != 0x05 { - return Err(anyhow!("SOCKS5 request version mismatch")); - } - - let is_udp = req[1] == 0x03; - if req[1] != 0x01 && !is_udp { - // Not CONNECT and Not UDP ASSOCIATE — send COMMAND NOT SUPPORTED - client.write_all(&[0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?; - return Err(anyhow!("unsupported SOCKS5 command {}", req[1])); - } - - let mut addr_buf = [0_u8; 256]; - target = match req[3] { - 0x01 => { - // IPv4: 4 bytes address + 2 bytes port - client.read_exact(&mut addr_buf[0..6]).await?; - let ip = std::net::Ipv4Addr::new(addr_buf[0], addr_buf[1], addr_buf[2], addr_buf[3]); - let port = u16::from_be_bytes([addr_buf[4], addr_buf[5]]); - format!("{}:{}", ip, port) - } - 0x03 => { - // Domain: 1 byte length, then domain, then 2 bytes port - client.read_exact(&mut addr_buf[0..1]).await?; - let domain_len = addr_buf[0] as usize; - client.read_exact(&mut addr_buf[0..domain_len + 2]).await?; - let domain = String::from_utf8_lossy(&addr_buf[0..domain_len]); - let port = u16::from_be_bytes([addr_buf[domain_len], addr_buf[domain_len + 1]]); - format!("{}:{}", domain, port) - } - 0x04 => { - // IPv6: 16 bytes + 2 bytes port - client.read_exact(&mut addr_buf[0..18]).await?; - let mut octets = [0u8; 16]; - octets.copy_from_slice(&addr_buf[0..16]); - let ip = std::net::Ipv6Addr::from(octets); - let port = u16::from_be_bytes([addr_buf[16], addr_buf[17]]); - format!("[{}]:{}", ip, port) - } - atyp => { - client.write_all(&[0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?; - return Err(anyhow!("unsupported SOCKS5 address type: {}", atyp)); - } - }; - - if is_udp { - if true { tracing::debug!("proxy UDP ASSOCIATE stream_id={stream_id}"); } - let udp_socket = UdpSocket::bind("127.0.0.1:0").await?; - let port = udp_socket.local_addr()?.port(); - let mut reply = vec![0x05, 0x00, 0x00, 0x01, 127, 0, 0, 1]; - reply.extend_from_slice(&port.to_be_bytes()); - 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, - matcher, - connect_timeout, - ).await; - } - - tracing::debug!("proxy CONNECT stream_id={stream_id} target={target}"); - let target_host = if let Some((host, _)) = split_host_port(&target) { host } else { target.clone() }; - let target_port = match split_host_port(&target) { Some((_, p)) => p, None => 0 }; - if matcher.should_bypass_target(&target_host, target_port, connect_timeout).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?; - - match timeout(connect_timeout, rx.recv()).await { - Ok(Some(ProxyToClientMsg::ConnectOk)) => { - // SUCCESS: version, 0=success, reserved, IPv4 type, 4 bytes addr, 2 bytes port - client.write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?; - } - Ok(Some(ProxyToClientMsg::Error(msg))) => { - client.write_all(&[0x05, 0x04, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?; - let _ = close_tx.send(stream_id).await; - return Err(anyhow!("SOCKS5 connect error: {msg}")); - } - Ok(_) => { - client.write_all(&[0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?; - let _ = close_tx.send(stream_id).await; - return Err(anyhow!("connect dropped")); - } - Err(_) => { - client.write_all(&[0x05, 0x04, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?; - let _ = close_tx.send(stream_id).await; - return Err(anyhow!("connect timeout")); - } - } - } else { - // ── HTTP Proxy (CONNECT and plain GET/POST) ─────────────────── - // Read the rest of the HTTP request headers byte-by-byte - let mut header_bytes = Vec::with_capacity(512); - header_bytes.push(first_byte[0]); - let mut chunk = [0_u8; 512]; - loop { - let n = client.read(&mut chunk).await?; - if n == 0 { - return Err(anyhow!("connection closed during HTTP header read")); - } - header_bytes.extend_from_slice(&chunk[..n]); - if header_bytes.len() >= 4 { - let tail = &header_bytes[header_bytes.len().saturating_sub(4)..]; - if tail.ends_with(b"\r\n\r\n") { - break; - } - } - if header_bytes.len() > 8192 { - client.write_all(b"HTTP/1.1 431 Request Header Fields Too Large\r\n\r\n").await?; - return Err(anyhow!("HTTP header too large")); - } - } - - let req_str = String::from_utf8_lossy(&header_bytes); - let first_line = req_str.lines().next().unwrap_or(""); - let parts: Vec<&str> = first_line.split_whitespace().collect(); - if parts.len() < 2 { - client.write_all(b"HTTP/1.1 400 Bad Request\r\n\r\n").await?; - return Err(anyhow!("malformed HTTP request line: {:?}", first_line)); - } - - let method = parts[0].to_uppercase(); - let raw_uri = parts[1]; - - target = if method == "CONNECT" { - // CONNECT uses host:port directly — e.g. "CONNECT example.com:443 HTTP/1.1" - if raw_uri.contains(':') { - raw_uri.to_string() - } else { - format!("{}:443", raw_uri) - } - } else { - // Plain HTTP: absolute URI like "GET http://example.com/path HTTP/1.1" - let default_port = if raw_uri.starts_with("https://") { 443u16 } else { 80u16 }; - extract_host_port(raw_uri, default_port) - }; - - if true { - tracing::info!("proxy CONNECT stream_id={stream_id} target={target}"); - } - let target_host = if let Some((host, _)) = split_host_port(&target) { host } else { target.clone() }; - let target_port = match split_host_port(&target) { Some((_, p)) => p, None => 443 }; - if matcher.should_bypass_target(&target_host, target_port, connect_timeout).await { - return direct_connect_http( - client, - stream_id, - &target, - method.as_str(), - header_bytes, - matcher.physical_if_index, - &matcher.physical_if_name, - close_tx, - debug, - ).await; - } - event_tx.send(ProxyEvent::NewStream { stream_id, target: target.clone() }).await?; - - match timeout(connect_timeout, rx.recv()).await { - Ok(Some(ProxyToClientMsg::ConnectOk)) => { - if method == "CONNECT" { - // For CONNECT, tell client the tunnel is ready - client.write_all(b"HTTP/1.1 200 Connection Established\r\nProxy-Agent: ostp/1.0\r\n\r\n").await?; - } else { - // For plain HTTP (GET/POST), we MUST forward the request headers we consumed - // to the server over the newly established tunnel. - event_tx.send(ProxyEvent::Data { - stream_id, - payload: bytes::Bytes::copy_from_slice(&header_bytes), - }).await?; - } - } - Ok(Some(ProxyToClientMsg::Error(msg))) => { - client.write_all(b"HTTP/1.1 502 Bad Gateway\r\n\r\n").await?; - let _ = close_tx.send(stream_id).await; - return Err(anyhow!("HTTP connect error: {msg}")); - } - Ok(_) => { - client.write_all(b"HTTP/1.1 502 Bad Gateway\r\n\r\n").await?; - let _ = close_tx.send(stream_id).await; - return Err(anyhow!("connect dropped")); - } - Err(_) => { - client.write_all(b"HTTP/1.1 504 Gateway Timeout\r\n\r\n").await?; - let _ = close_tx.send(stream_id).await; - return Err(anyhow!("connect timeout")); - } - } - } - - // ── Bidirectional raw data forwarding ───────────────────────────── - let mut tcp_buf = vec![0_u8; 65536]; - loop { - tokio::select! { - read_res = client.read(&mut tcp_buf) => { - match read_res { - Ok(0) => { - let _ = event_tx.send(ProxyEvent::Close { stream_id }).await; - if true { - tracing::info!("proxy CLOSE stream_id={stream_id}"); - } - break; - } - Ok(n) => { - let mut offset = 0; - while offset < n { - let end = (offset + max_chunk).min(n); - let _ = event_tx.send(ProxyEvent::Data { - stream_id, - payload: bytes::Bytes::copy_from_slice(&tcp_buf[offset..end]), - }).await; - offset = end; - } - } - Err(_) => { - let _ = event_tx.send(ProxyEvent::Close { stream_id }).await; - if true { - tracing::info!("proxy CLOSE stream_id={stream_id}"); - } - break; - } - } - } - msg = rx.recv() => { - match msg { - Some(ProxyToClientMsg::Data(data)) => { - if client.write_all(&data).await.is_err() { - let _ = event_tx.send(ProxyEvent::Close { stream_id }).await; - break; - } - } - Some(ProxyToClientMsg::Close) | Some(ProxyToClientMsg::Error(_)) | None => { - break; - } - Some(ProxyToClientMsg::ConnectOk) | Some(ProxyToClientMsg::UdpData(_, _)) => {} // ignored after connect phase - } - } - } - } - - let _ = close_tx.send(stream_id).await; - Ok(()) -} - - -fn split_host_port(target: &str) -> Option<(String, u16)> { - if let Some((host, port)) = target.rsplit_once(':') { - if host.starts_with('[') && host.ends_with(']') { - let host = host.trim_start_matches('[').trim_end_matches(']').to_string(); - let port = port.parse().ok()?; - return Some((host, port)); - } - if host.contains(':') { - return None; - } - let port = port.parse().ok()?; - return Some((host.to_string(), port)); - } - None -} - -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 true { - tracing::info!("proxy BYPASS stream_id={stream_id} target={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; - let _ = close_tx.send(stream_id).await; - Ok(()) -} - -async fn direct_connect_http( - mut client: TcpStream, - stream_id: u16, - target: &str, - method: &str, - header_bytes: Vec, - physical_if_index: Option, - physical_if_name: &Option, - close_tx: mpsc::Sender, - _debug: bool, -) -> Result<()> { - if true { - tracing::info!("proxy BYPASS stream_id={stream_id} target={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?; - } else { - remote.write_all(&header_bytes).await?; - } - - let _ = tokio::io::copy_bidirectional(&mut client, &mut remote).await; - let _ = close_tx.send(stream_id).await; - Ok(()) -} diff --git a/ostp-client/src/tunnel/router.rs b/ostp-client/src/tunnel/router.rs new file mode 100644 index 0000000..dc16a24 --- /dev/null +++ b/ostp-client/src/tunnel/router.rs @@ -0,0 +1,155 @@ +use std::net::IpAddr; +use crate::config::{RoutingConfig, RoutingRule}; + +#[derive(Debug, Clone)] +pub struct Session { + pub inbound_tag: String, + pub source_ip: Option, + pub destination_ip: Option, + pub destination_port: u16, + pub protocol: String, // "tcp" or "udp" + pub sni: Option, + pub process_name: Option, +} + +pub struct Router { + config: RoutingConfig, +} + +impl Router { + pub fn new(config: RoutingConfig) -> Self { + Self { config } + } + + /// Evaluates the session against routing rules and returns the outbound tag + pub fn route(&self, session: &Session) -> String { + for rule in &self.config.rules { + if self.match_rule(rule, session) { + return rule.outbound.clone(); + } + } + self.config.default_outbound.clone() + } + + fn match_rule(&self, rule: &RoutingRule, session: &Session) -> bool { + // All specified conditions in a rule must match (AND logic) + let mut matched_any_condition = false; + + // 1. Inbound Tag match + if let Some(inbounds) = &rule.inbound_tag { + if !inbounds.iter().any(|tag| tag == &session.inbound_tag) { + return false; + } + matched_any_condition = true; + } + + // 2. Domain / SNI match + if let Some(domains) = &rule.domain_suffix { + let mut domain_match = false; + if let Some(sni) = &session.sni { + let sni = sni.to_lowercase(); + domain_match = domains.iter().any(|d| { + let d = d.to_lowercase(); + sni == d || sni.ends_with(&format!(".{}", d)) + }); + } + if !domain_match { + return false; + } + matched_any_condition = true; + } + + // 3. Process match + if let Some(processes) = &rule.process_name { + let mut proc_match = false; + if let Some(proc) = &session.process_name { + let proc = proc.to_lowercase(); + proc_match = processes.iter().any(|p| proc.contains(&p.to_lowercase())); + } + if !proc_match { + return false; + } + matched_any_condition = true; + } + + // 4. IP CIDR match + if let Some(cidrs) = &rule.ip_cidr { + let mut ip_match = false; + if let Some(dst_ip) = session.destination_ip { + ip_match = cidrs.iter().any(|cidr| { + match ipnet::IpNet::from_str(cidr) { + Ok(net) => net.contains(&dst_ip), + Err(_) => { + // fallback to exact ip match if not a valid CIDR + if let Ok(ip) = cidr.parse::() { + ip == dst_ip + } else { + false + } + } + } + }); + } + if !ip_match { + return false; + } + matched_any_condition = true; + } + + // A rule must have at least one condition to match + matched_any_condition + } +} + +use std::str::FromStr; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_router() { + let rules = vec![ + RoutingRule { + domain_suffix: Some(vec!["vk.com".to_string()]), + ip_cidr: None, + process_name: None, + inbound_tag: None, + outbound: "direct".to_string(), + }, + RoutingRule { + domain_suffix: None, + ip_cidr: None, + process_name: Some(vec!["telegram.exe".to_string()]), + inbound_tag: None, + outbound: "proxy-group".to_string(), + }, + ]; + + let config = RoutingConfig { + rules, + default_outbound: "proxy-group".to_string(), + }; + + let router = Router::new(config); + + let mut session = Session { + inbound_tag: "tun-in".to_string(), + source_ip: None, + destination_ip: None, + destination_port: 443, + protocol: "tcp".to_string(), + sni: Some("api.vk.com".to_string()), + process_name: None, + }; + + assert_eq!(router.route(&session), "direct"); + + session.sni = None; + session.process_name = Some("C:\\App\\Telegram.exe".to_string()); + assert_eq!(router.route(&session), "proxy-group"); + + session.process_name = Some("chrome.exe".to_string()); + assert_eq!(router.route(&session), "proxy-group"); // fallback + } +} diff --git a/ostp-client/src/tunnel/udp_nat.rs b/ostp-client/src/tunnel/udp_nat.rs index 93ab9c2..4c67146 100644 --- a/ostp-client/src/tunnel/udp_nat.rs +++ b/ostp-client/src/tunnel/udp_nat.rs @@ -1,306 +1 @@ -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, - 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)); - - // map from internal client src to a channel that sends (payload, external_dst) - let mut sessions: HashMap, SocketAddr)>> = HashMap::new(); - - let mut cleanup_tick = tokio::time::interval(std::time::Duration::from_secs(60)); - - loop { - tokio::select! { - packet = rx.next() => { - match packet { - Some((payload, src, dst)) => { - if payload.is_empty() { continue; } - - if !sessions.contains_key(&src) { - let (session_tx, mut session_rx) = mpsc::channel::<(Vec, SocketAddr)>(1024); - sessions.insert(src, session_tx); - - 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; - if debug { - tracing::info!("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 debug { - tracing::debug!("TUN UDP lookup: port {} -> process {}", src.port(), proc_name); - } - if matcher_guard.match_process(&proc_name) { - should_bypass = true; - if debug { - tracing::debug!("TUN UDP BYPASS (Process match): {} ({} → {})", proc_name, src, dst); - } - } - } else { - if debug { - tracing::debug!("TUN UDP lookup: port {} -> no process found", src.port()); - } - } - } - } - - let p_if_idx = phys_if_index; - let p_if_name = phys_if_name.clone(); - - tokio::spawn(async move { - if should_bypass { - if debug { - tracing::info!("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()); - } - } - }); - } - - if let Some(sender) = sessions.get(&src) { - match sender.try_send((payload, dst)) { - Err(mpsc::error::TrySendError::Closed(_)) => { - sessions.remove(&src); - } - Err(mpsc::error::TrySendError::Full(_)) => { - // Drop packet to avoid blocking the TUN interface loop - } - Ok(_) => {} - } - } - } - None => break, - } - } - _ = cleanup_tick.tick() => { - sessions.retain(|_, sender| !sender.is_closed()); - } - } - } -} - -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 { - if let Err(e) = crate::tunnel::proxy::bind_socket_to_interface(&socket, client_src.is_ipv6(), idx) { - tracing::error!("TUN UDP BYPASS failed to bind to physical interface {}: {}", idx, e); - } else { - // Keep debug log - } - } else { - tracing::warn!("TUN UDP BYPASS has no physical interface index!"); - } - - #[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, - 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_hdr = [0u8; 4]; - tcp.read_exact(&mut rep_hdr).await?; - if rep_hdr[1] != 0 { - return Err(anyhow::anyhow!("socks5 udp associate rejected")); - } - - let mut relay_addr = match rep_hdr[3] { - 1 => { - let mut addr_buf = [0u8; 6]; - tcp.read_exact(&mut addr_buf).await?; - let ip = std::net::Ipv4Addr::new(addr_buf[0], addr_buf[1], addr_buf[2], addr_buf[3]); - let port = u16::from_be_bytes([addr_buf[4], addr_buf[5]]); - SocketAddr::new(std::net::IpAddr::V4(ip), port) - } - 4 => { - let mut addr_buf = [0u8; 18]; - tcp.read_exact(&mut addr_buf).await?; - let mut octets = [0u8; 16]; - octets.copy_from_slice(&addr_buf[0..16]); - let ip = std::net::Ipv6Addr::from(octets); - let port = u16::from_be_bytes([addr_buf[16], addr_buf[17]]); - SocketAddr::new(std::net::IpAddr::V6(ip), port) - } - _ => return Err(anyhow::anyhow!("unsupported ATYP in UDP ASSOCIATE response")), - }; - - // If proxy returned 0.0.0.0 or ::, use the proxy's IP - if relay_addr.ip().is_unspecified() { - if let Ok(proxy_sock) = proxy_addr.parse::() { - relay_addr.set_ip(proxy_sock.ip()); - } - } - - // Local SOCKS5 proxy always returns 127.0.0.1 (IPv4), so always bind IPv4 - let udp = UdpSocket::bind("127.0.0.1:0").await?; - - // CRITICAL for Android: protect this UDP socket so it goes out via the - // real physical interface, not back into the TUN (which would cause an - // infinite routing loop for DNS and all other UDP traffic). - #[cfg(target_os = "android")] - { - use std::os::unix::io::AsRawFd; - crate::bridge::protect_socket(udp.as_raw_fd()); - } - - 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); - tracing::debug!("udp_nat SENDING UDP ASSOCIATE payload len={} to relay_addr={} (original dst: {})", payload.len(), relay_addr, dst); - let _ = udp.send_to(&packet, relay_addr).await; - } - Ok(None) => break, - Err(_) => break, // timeout - } - } - res = udp.recv_from(&mut buf) => { - match res { - Err(e) => { - tracing::debug!("udp_nat recv_from error: {}", e); - continue; // transient error, don't kill the session - } - Ok((len, _peer)) => { - if len < 4 { continue; } - 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, - }; - let payload = buf[header_len..len].to_vec(); - tracing::debug!("udp_nat RECEIVED UDP ASSOCIATE REPLY from {} for {} len={}", remote_dst, client_src, payload.len()); - use futures::SinkExt; - if let Err(e) = smoltcp_tx.lock().await.send((payload, remote_dst, client_src)).await { - tracing::error!("udp_nat failed to inject packet into smoltcp: {}", e); - } else { - tracing::debug!("udp_nat successfully injected packet into smoltcp from {} to {}", remote_dst, client_src); - } - } - } - } - // If TCP drops, UDP association is over - res = tcp.read(&mut tcp_buf) => { - match res { - Ok(0) | Err(_) => break, - Ok(_) => {} - } - } - } - } - - Ok(()) -} +// Cleared for refactoring diff --git a/ostp-client/src/tunnel/udp_nat.rs.bak b/ostp-client/src/tunnel/udp_nat.rs.bak new file mode 100644 index 0000000000000000000000000000000000000000..3a66d59f030c26e2dcf721fd6ee28f258192add6 GIT binary patch literal 27320 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 literal 0 HcmV?d00001