diff --git a/Cargo.lock b/Cargo.lock index 2ea9a52..dfaf891 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,6 +123,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.8.9" @@ -224,6 +246,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -327,6 +351,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -404,6 +437,15 @@ dependencies = [ "syn", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -426,6 +468,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "errno" version = "0.3.14" @@ -457,6 +505,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" version = "0.3.32" @@ -523,6 +577,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "ghash" version = "0.5.1" @@ -820,6 +886,16 @@ dependencies = [ "syn", ] +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.98" @@ -909,6 +985,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-traits" version = "0.2.19" @@ -970,6 +1052,7 @@ dependencies = [ "portable-atomic", "rand", "rustls", + "rustls-pki-types", "serde", "serde_json", "sha2", @@ -977,6 +1060,7 @@ dependencies = [ "tokio", "tokio-rustls", "tracing", + "webpki-roots 0.26.11", ] [[package]] @@ -1022,11 +1106,14 @@ dependencies = [ "ostp-core", "portable-atomic", "rand", + "rcgen", + "rustls", "serde", "serde_json", "sha2", "socket2", "tokio", + "tokio-rustls", "tower-http", "tracing", ] @@ -1045,6 +1132,16 @@ dependencies = [ "winres", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1095,6 +1192,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1122,6 +1225,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -1149,7 +1258,20 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", +] + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", ] [[package]] @@ -1177,7 +1299,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -1198,6 +1320,8 @@ version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ + "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -1221,6 +1345,7 @@ version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1468,6 +1593,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + [[package]] name = "tinystr" version = "0.8.3" @@ -1708,6 +1852,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.121" @@ -1753,6 +1906,24 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -1978,12 +2149,27 @@ dependencies = [ "toml", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.2" diff --git a/ostp-client/Cargo.toml b/ostp-client/Cargo.toml index 5d123ea..ef029f3 100644 --- a/ostp-client/Cargo.toml +++ b/ostp-client/Cargo.toml @@ -17,9 +17,11 @@ json_comments = "0.2" portable-atomic.workspace = true chrono = "0.4" socket2 = "0.6.3" -rustls = { version = "0.23.40", default-features = false, features = ["ring", "std"] } -tokio-rustls = { version = "0.26.0", default-features = false, features = ["ring"] } +rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } +tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } futures-util = "0.3.32" hmac = "0.12.1" sha2 = "0.10.8" base64 = "0.22.1" +webpki-roots = "0.26" +rustls-pki-types = "1.7" diff --git a/ostp-client/src/bridge.rs b/ostp-client/src/bridge.rs index 5b6a0a7..579728d 100644 --- a/ostp-client/src/bridge.rs +++ b/ostp-client/src/bridge.rs @@ -38,11 +38,16 @@ pub struct BridgeMetrics { pub connection_state: AtomicU8, } -async fn send_datagram(socket: &crate::transport::Transport, frame: &Bytes, turn_enabled: bool) -> std::io::Result { - if turn_enabled { - let mut out = bytes::BytesMut::with_capacity(4 + frame.len()); - bytes::BufMut::put_u16(&mut out, 0x4000); - bytes::BufMut::put_u16(&mut out, frame.len() as u16); +async fn send_datagram(socket: &crate::transport::Transport, frame: &Bytes, webrtc_masquerade: bool) -> std::io::Result { + if webrtc_masquerade { + let mut out = bytes::BytesMut::with_capacity(12 + frame.len()); + // Fake SRTP Header: + // [0] 0x80 (Version 2) + // [1] 0x60 (Payload Type 96 - dynamic video) + // [2..3] Sequence number (dummy 0x1234) + // [4..7] Timestamp (dummy) + // [8..11] SSRC (dummy) + out.extend_from_slice(&[0x80, 0x60, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44]); out.extend_from_slice(frame); socket.send(&out.freeze()).await } else { @@ -66,10 +71,7 @@ pub struct Bridge { handshake_timeout_ms: u64, io_timeout_ms: u64, - pub turn_enabled: bool, - pub turn_server: String, - pub turn_username: String, - pub turn_password: String, + pub keepalive_interval_sec: u64, pub mode: String, pub mux_enabled: bool, pub mux_sessions: usize, @@ -78,6 +80,7 @@ pub struct Bridge { pub stealth_sni: String, pub stealth_port: u16, pub mtu: usize, + pub reality_enabled: bool, metrics: Arc, sample_sent: u64, @@ -100,10 +103,7 @@ impl Bridge { handshake_timeout_ms: config.ostp.handshake_timeout_ms, io_timeout_ms: config.ostp.io_timeout_ms, - turn_enabled: config.turn.enabled, - turn_server: config.turn.server_addr.clone(), - turn_username: config.turn.username.clone(), - turn_password: config.turn.access_key.clone(), + keepalive_interval_sec: config.ostp.keepalive_interval_sec, mode: config.mode.clone(), mux_enabled: config.multiplex.enabled, mux_sessions: config.multiplex.sessions.max(1), @@ -112,6 +112,7 @@ impl Bridge { stealth_sni: config.transport.stealth_sni.clone(), stealth_port: config.transport.stealth_port, mtu: config.ostp.mtu, + reality_enabled: !config.reality.pbk.is_empty(), metrics, sample_sent: 0, @@ -131,7 +132,7 @@ impl Bridge { 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(5)); + 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(50)); let init_msg = if self.mode == "tun" { "Bridge initialized (TUN mode)".to_string() @@ -220,7 +221,7 @@ impl Bridge { } } ProtocolAction::SendDatagram(frame) => { - let _ = send_datagram(&session.socket, &frame, self.turn_enabled).await; + let _ = send_datagram(&session.socket, &frame, (self.transport_mode == "udp")).await; self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); } _ => {} @@ -273,19 +274,14 @@ impl Bridge { let session_index = sessions.len(); let socket_clone = sock.clone(); let udp_tx_clone = udp_tx.clone(); - let is_turn = self.turn_enabled; + let is_webrtc = (self.transport_mode == "udp"); tokio::spawn(async move { let mut buf = vec![0_u8; 65535]; loop { match socket_clone.recv(&mut buf).await { Ok(n) => { - let inbound = if is_turn && n >= 4 && buf[0] == 0x40 && buf[1] == 0x00 { - let len = u16::from_be_bytes([buf[2], buf[3]]) as usize; - if 4 + len <= n { - Bytes::copy_from_slice(&buf[4..4+len]) - } else { - Bytes::copy_from_slice(&buf[..n]) - } + let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 { + Bytes::copy_from_slice(&buf[12..n]) } else { Bytes::copy_from_slice(&buf[..n]) }; @@ -368,15 +364,14 @@ impl Bridge { let session_index = new_sessions.len(); let socket_clone = sock.clone(); let udp_tx_clone = udp_tx.clone(); - let is_turn = self.turn_enabled; + let is_webrtc = (self.transport_mode == "udp"); tokio::spawn(async move { let mut buf = vec![0_u8; 65535]; loop { match socket_clone.recv(&mut buf).await { Ok(n) => { - let inbound = if is_turn && n >= 4 && buf[0] == 0x40 && buf[1] == 0x00 { - let len = u16::from_be_bytes([buf[2], buf[3]]) as usize; - if 4 + len <= n { Bytes::copy_from_slice(&buf[4..4+len]) } else { Bytes::copy_from_slice(&buf[..n]) } + let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 { + Bytes::copy_from_slice(&buf[12..n]) } else { Bytes::copy_from_slice(&buf[..n]) }; @@ -480,19 +475,14 @@ impl Bridge { let session_index = new_sessions.len(); let socket_clone = sock.clone(); let udp_tx_clone = udp_tx.clone(); - let is_turn = self.turn_enabled; + let is_webrtc = (self.transport_mode == "udp"); tokio::spawn(async move { let mut buf = vec![0_u8; 65535]; loop { match socket_clone.recv(&mut buf).await { Ok(n) => { - let inbound = if is_turn && n >= 4 && buf[0] == 0x40 && buf[1] == 0x00 { - let len = u16::from_be_bytes([buf[2], buf[3]]) as usize; - if 4 + len <= n { - Bytes::copy_from_slice(&buf[4..4+len]) - } else { - Bytes::copy_from_slice(&buf[..n]) - } + let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 { + Bytes::copy_from_slice(&buf[12..n]) } else { Bytes::copy_from_slice(&buf[..n]) }; @@ -539,14 +529,14 @@ impl Bridge { if let Ok(ProtocolAction::SendDatagram(frame)) = session.machine.on_event(OstpEvent::Outbound(0, ping_payload)) { // Must go through send_datagram() for TURN-mode wrapping; // raw socket.send() bypasses the ChannelData header and breaks RTT in TURN. - let _ = send_datagram(&session.socket, &frame, self.turn_enabled).await; + let _ = send_datagram(&session.socket, &frame, (self.transport_mode == "udp")).await; self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); } // Send Relay KeepAlive (Force NAT/Server Persistence) 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.turn_enabled).await; + let _ = send_datagram(&session.socket, &frame, (self.transport_mode == "udp")).await; self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); } } @@ -569,7 +559,7 @@ impl Bridge { } } ProtocolAction::SendDatagram(frame) => { - let _ = send_datagram(&session.socket, &frame, self.turn_enabled).await; + let _ = send_datagram(&session.socket, &frame, (self.transport_mode == "udp")).await; self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); } _ => {} @@ -632,7 +622,7 @@ impl Bridge { 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.turn_enabled).await.is_ok() { + 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); if self.debug { let _ = tx.send(UiEvent::Log(format!( @@ -646,7 +636,7 @@ impl Bridge { let mut sent = 0usize; for item in list { if let ProtocolAction::SendDatagram(frame) = item { - if send_datagram(&session.socket, &frame, self.turn_enabled).await.is_ok() { + 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; } @@ -817,64 +807,7 @@ impl Bridge { } }; - if self.turn_enabled && self.transport_mode != "wss" { - let udp_socket = match &socket { - crate::transport::Transport::Udp(sock) => sock, - _ => return Err(anyhow::anyhow!("TURN requires UDP transport")), - }; - let turn_addr = if self.turn_server.contains(':') { - self.turn_server.clone() - } else { - format!("{}:3478", self.turn_server) - }; - tx.send(UiEvent::Log(format!("Allocating TURN relay via {}", turn_addr))).await.ok(); - - match crate::turn::perform_turn_allocation(udp_socket, &turn_addr, &self.turn_username, &self.turn_password, &self.server_addr).await { - Ok(relay_addr) => { - tx.send(UiEvent::Log(format!("TURN relay allocated ({})", relay_addr))).await.ok(); - - let resolved_turn: Vec = tokio::net::lookup_host(&turn_addr).await - .map_err(|e| anyhow::anyhow!("failed to resolve TURN {}: {}", turn_addr, e))? - .collect(); - let turn_target = resolved_turn.first().ok_or_else(|| anyhow::anyhow!("no IP resolved for TURN {}", turn_addr))?; - - let connect_ip = if udp_socket.local_addr().map(|a| a.is_ipv6()).unwrap_or(false) && turn_target.is_ipv4() { - if let std::net::IpAddr::V4(ipv4) = turn_target.ip() { - std::net::IpAddr::V6(synthesize_nat64(ipv4)) - } else { - turn_target.ip() - } - } else { - turn_target.ip() - }; - - let connect_addr = std::net::SocketAddr::new(connect_ip, turn_target.port()); - udp_socket - .connect(connect_addr) - .await - .with_context(|| format!("failed to re-connect to TURN {}", connect_addr))?; - } - Err(e) => { - tx.send(UiEvent::Log(format!("TURN allocation failed: {}. Using direct UDP.", e))).await.ok(); - let connect_ip = if udp_socket.local_addr().map(|a| a.is_ipv6()).unwrap_or(false) && target_ip.is_ipv4() { - if let std::net::IpAddr::V4(ipv4) = target_ip { - std::net::IpAddr::V6(synthesize_nat64(ipv4)) - } else { - target_ip - } - } else { - target_ip - }; - let connect_addr = std::net::SocketAddr::new(connect_ip, port); - udp_socket - .connect(connect_addr) - .await - .with_context(|| format!("failed to connect udp to {}", connect_addr))?; - } - } - } - - // Connection to remote is handled inside the TURN/direct branches above + // Connection to remote is handled inside try_connect_transport let start = Instant::now(); let action = machine.on_event(OstpEvent::Start)?; @@ -897,7 +830,7 @@ impl Bridge { if attempt > 0 { tx.send(UiEvent::Log(format!("Handshake attempt {} lost. Retransmitting...", attempt))).await.ok(); } - send_datagram(&socket, &handshake_frame, self.turn_enabled).await?; + send_datagram(&socket, &handshake_frame, (self.transport_mode == "udp")).await?; 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 { @@ -923,7 +856,7 @@ impl Bridge { if attempt > 0 { tx.send(UiEvent::Log(format!("NAT64 handshake attempt {} lost. Retransmitting...", attempt))).await.ok(); } - send_datagram(&fallback_socket, &handshake_frame, self.turn_enabled).await?; + send_datagram(&fallback_socket, &handshake_frame, (self.transport_mode == "udp")).await?; match timeout(Duration::from_millis(1200), fallback_socket.recv(&mut buf)).await { Ok(Ok(n)) => { size = n; @@ -950,13 +883,8 @@ impl Bridge { self.metrics.bytes_recv.fetch_add(size as u64, Ordering::Relaxed); tracing::info!("Handshake response received: {} bytes", size); - let inbound = if self.turn_enabled && size >= 4 && buf[0] == 0x40 && buf[1] == 0x00 { - let len = u16::from_be_bytes([buf[2], buf[3]]) as usize; - if 4 + len <= size { - Bytes::copy_from_slice(&buf[4..4+len]) - } else { - Bytes::copy_from_slice(&buf[..size]) - } + let inbound = if (self.transport_mode == "udp") && size >= 12 && buf[0] == 0x80 { + Bytes::copy_from_slice(&buf[12..size]) } else { Bytes::copy_from_slice(&buf[..size]) }; @@ -975,15 +903,12 @@ impl Bridge { 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.turn_enabled = cfg.turn.enabled; - self.turn_server = cfg.turn.server_addr.clone(); - self.turn_username = cfg.turn.username.clone(); - self.turn_password = cfg.turn.access_key.clone(); 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.stealth_port = cfg.transport.stealth_port; + self.reality_enabled = !cfg.reality.pbk.is_empty(); } async fn try_connect_transport( @@ -1002,7 +927,7 @@ impl Bridge { port }; let (tx, rx) = crate::transport::xhttp::connect_xhttp( - target_ip, uot_port, &self.stealth_sni, &self.access_key + target_ip, uot_port, &self.stealth_sni, &self.access_key, self.reality_enabled ).await?; Ok(crate::transport::Transport::Uot { tx, rx }) } else { @@ -1053,3 +978,4 @@ fn synthesize_nat64(ip: std::net::Ipv4Addr) -> std::net::Ipv6Addr { ) } + diff --git a/ostp-client/src/config.rs b/ostp-client/src/config.rs index f4e558a..4583986 100644 --- a/ostp-client/src/config.rs +++ b/ostp-client/src/config.rs @@ -12,7 +12,7 @@ pub struct ClientConfig { pub debug: bool, pub ostp: OstpConfig, pub local_proxy: LocalProxyConfig, - pub turn: TurnConfig, + pub reality: RealityConfig, #[serde(default)] pub transport: TransportConfig, #[serde(default)] @@ -48,8 +48,12 @@ pub struct OstpConfig { 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 { 1350 } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -88,11 +92,17 @@ impl Default for TransportConfig { #[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct TurnConfig { - pub enabled: bool, - pub server_addr: String, - pub username: String, - pub access_key: String, +pub struct RealityConfig { + #[serde(default)] + pub sni: String, + #[serde(default)] + pub fp: String, + #[serde(default)] + pub pbk: String, + #[serde(default)] + pub sid: String, + #[serde(default)] + pub spx: String, } @@ -105,6 +115,7 @@ impl Default for OstpConfig { handshake_timeout_ms: 5000, io_timeout_ms: 2500, mtu: default_mtu(), + keepalive_interval_sec: default_keepalive(), } } } @@ -126,7 +137,7 @@ impl Default for ClientConfig { debug: false, ostp: OstpConfig::default(), local_proxy: LocalProxyConfig::default(), - turn: TurnConfig::default(), + reality: RealityConfig::default(), transport: TransportConfig::default(), exclusions: ExclusionConfig::default(), multiplex: MultiplexConfig::default(), @@ -158,7 +169,7 @@ struct RawUnifiedConfig { tun: Option, exclude: Option, mux: Option, - turn: Option, + reality: Option, transport: Option, } @@ -189,11 +200,12 @@ struct RawMuxSection { } #[derive(Debug, Deserialize)] -struct RawTurnSection { - enabled: Option, - server_addr: Option, - username: Option, - access_key: Option, +struct RawRealitySection { + sni: Option, + fp: Option, + pbk: Option, + sid: Option, + spx: Option, } impl ClientConfig { @@ -235,16 +247,18 @@ impl ClientConfig { handshake_timeout_ms: 5000, io_timeout_ms: 2500, mtu, + keepalive_interval_sec: default_keepalive(), }, local_proxy: LocalProxyConfig { bind_addr: socks5, connect_timeout_ms: 15000, }, - turn: TurnConfig { - enabled: raw.turn.as_ref().and_then(|t| t.enabled).unwrap_or(false), - server_addr: raw.turn.as_ref().and_then(|t| t.server_addr.clone()).unwrap_or_default(), - username: raw.turn.as_ref().and_then(|t| t.username.clone()).unwrap_or_default(), - access_key: raw.turn.as_ref().and_then(|t| t.access_key.clone()).unwrap_or_default(), + reality: RealityConfig { + sni: raw.reality.as_ref().and_then(|t| t.sni.clone()).unwrap_or_default(), + fp: raw.reality.as_ref().and_then(|t| t.fp.clone()).unwrap_or_default(), + pbk: raw.reality.as_ref().and_then(|t| t.pbk.clone()).unwrap_or_default(), + sid: raw.reality.as_ref().and_then(|t| t.sid.clone()).unwrap_or_default(), + spx: raw.reality.as_ref().and_then(|t| t.spx.clone()).unwrap_or_default(), }, transport: TransportConfig { mode: raw.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(|| "udp".to_string()), diff --git a/ostp-client/src/lib.rs b/ostp-client/src/lib.rs index 5c6fcf0..efd5eb5 100644 --- a/ostp-client/src/lib.rs +++ b/ostp-client/src/lib.rs @@ -6,5 +6,5 @@ pub mod sysproxy; pub mod transport; pub mod tunnel; -pub mod turn; + pub mod runner; diff --git a/ostp-client/src/transport/xhttp.rs b/ostp-client/src/transport/xhttp.rs index 5ecf48d..4532101 100644 --- a/ostp-client/src/transport/xhttp.rs +++ b/ostp-client/src/transport/xhttp.rs @@ -8,6 +8,62 @@ use tokio::sync::mpsc; use hmac::{Hmac, Mac}; use sha2::Sha256; use base64::Engine; +use rustls::ClientConfig; +use rustls::pki_types::ServerName; +use std::sync::Arc as StdArc; +use tokio_rustls::TlsConnector; + +mod danger { + use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; + use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; + use rustls::DigitallySignedStruct; + use rustls::crypto::CryptoProvider; + + #[derive(Debug)] + pub struct NoCertificateVerification; + + impl ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + rustls::SignatureScheme::RSA_PKCS1_SHA256, + rustls::SignatureScheme::RSA_PKCS1_SHA384, + rustls::SignatureScheme::RSA_PKCS1_SHA512, + rustls::SignatureScheme::ECDSA_NISTP256_SHA256, + rustls::SignatureScheme::ECDSA_NISTP384_SHA384, + rustls::SignatureScheme::ED25519, + ] + } + } +} type HmacSha256 = Hmac; @@ -16,12 +72,45 @@ pub async fn connect_xhttp( port: u16, sni: &str, access_key: &[u8], + tls_enabled: bool, ) -> Result<(mpsc::Sender, Arc>>)> { let addr = std::net::SocketAddr::new(target_ip, port); - let mut tcp_stream = TcpStream::connect(addr).await + let tcp_stream = TcpStream::connect(addr).await .with_context(|| format!("failed to connect to {}", addr))?; tcp_stream.set_nodelay(true)?; + if tls_enabled { + // Setup rustls client skipping cert validation (Reality self-signed certs) + let mut config = ClientConfig::builder_with_provider(rustls::crypto::ring::default_provider().into()) + .with_safe_default_protocol_versions() + .unwrap() + .dangerous() + .with_custom_certificate_verifier(StdArc::new(danger::NoCertificateVerification)) + .with_no_client_auth(); + config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + + let connector = TlsConnector::from(StdArc::new(config)); + let server_name = ServerName::try_from(sni.to_string()) + .unwrap_or_else(|_| ServerName::try_from("www.microsoft.com").unwrap()) + .to_owned(); + + let tls_stream = connector.connect(server_name, tcp_stream).await + .with_context(|| "TLS handshake failed")?; + xhttp_handshake_and_loop(tls_stream, target_ip, sni, access_key).await + } else { + xhttp_handshake_and_loop(tcp_stream, target_ip, sni, access_key).await + } +} + +async fn xhttp_handshake_and_loop( + mut stream: S, + target_ip: IpAddr, + sni: &str, + access_key: &[u8], +) -> Result<(mpsc::Sender, Arc>>)> +where + S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static, +{ // 1. Generate auth token: [8-byte timestamp BE] ++ [HMAC-SHA256] let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); let ts_bytes = timestamp.to_be_bytes(); @@ -38,8 +127,6 @@ pub async fn connect_xhttp( let http_host = if sni.is_empty() { target_ip.to_string() } else { sni.to_string() }; // 2. Send fake WebSocket upgrade — looks like a legit browser request to bypass DPI/proxies. - // The server responds with 101 Switching Protocols and we stream raw UoT frames after that. - // NOTE: always plain TCP — TLS is NOT used. Obfuscation comes from the fake WS headers. let req = format!( "GET /stream HTTP/1.1\r\n\ Host: {}\r\n\ @@ -52,14 +139,14 @@ pub async fn connect_xhttp( http_host, auth_token ); - tcp_stream.write_all(req.as_bytes()).await?; - tcp_stream.flush().await?; + stream.write_all(req.as_bytes()).await?; + stream.flush().await?; // 3. Read server response headers let mut buf = vec![0u8; 4096]; let mut header_len = 0; loop { - let n = tcp_stream.read(&mut buf[header_len..]).await?; + let n = stream.read(&mut buf[header_len..]).await?; if n == 0 { anyhow::bail!("connection closed before handshake complete"); } header_len += n; if buf[..header_len].windows(4).any(|w| w == b"\r\n\r\n") { break; } @@ -80,7 +167,7 @@ pub async fn connect_xhttp( let leftover = buf[headers_end..header_len].to_vec(); // 5. Split into read/write halves and start UoT loops - let (rx, tx) = tcp_stream.into_split(); + let (rx, tx) = tokio::io::split(stream); start_uot_loops(rx, tx, leftover) } diff --git a/ostp-client/src/tunnel/linux_handler.rs b/ostp-client/src/tunnel/linux_handler.rs index 40d6947..2103848 100644 --- a/ostp-client/src/tunnel/linux_handler.rs +++ b/ostp-client/src/tunnel/linux_handler.rs @@ -138,7 +138,7 @@ pub async fn run_linux_tunnel( // 4. Setup commands (Using standard /1 routing trick for fail-proof overriding) let setup_script = format!( "ip tuntap add name ostp_tun mode tun || true; \ - ip link set dev ostp_tun mtu 1300; \ + ip link set dev ostp_tun mtu {}; \ ip addr add 10.1.0.2/24 dev ostp_tun || true; \ ip link set dev ostp_tun up; \ ip route add {} via {} dev {}; \ diff --git a/ostp-client/src/tunnel/wintun_handler.rs b/ostp-client/src/tunnel/wintun_handler.rs index 7c5d5d9..a7fae85 100644 --- a/ostp-client/src/tunnel/wintun_handler.rs +++ b/ostp-client/src/tunnel/wintun_handler.rs @@ -174,8 +174,9 @@ pub async fn run_wintun_tunnel( tracing::info!("Applying network configuration..."); let mut net_setup = format!( "netsh interface ipv4 set address name=\"{TUN_NAME}\" static 10.1.0.2 255.255.255.0 10.1.0.1\n\ - netsh interface ipv4 set subinterface \"{TUN_NAME}\" mtu=1300 store=persistent\n\ - netsh interface ipv4 set interface name=\"{TUN_NAME}\" metric=5\n" + netsh interface ipv4 set subinterface \"{TUN_NAME}\" mtu={} store=persistent\n\ + netsh interface ipv4 set interface name=\"{TUN_NAME}\" metric=5\n", + config.ostp.mtu ); if let Some(ref dns) = config.dns_server { diff --git a/ostp-client/src/turn.rs b/ostp-client/src/turn.rs deleted file mode 100644 index e6acb2b..0000000 --- a/ostp-client/src/turn.rs +++ /dev/null @@ -1,397 +0,0 @@ -//! TURN (RFC 5766) allocation and channel binding for NAT traversal. -//! -//! Implements the minimal STUN/TURN message flow needed to allocate a relay -//! address and bind a channel to the OSTP server. All crypto (MD5, SHA-1, -//! HMAC-SHA1) is implemented inline to avoid external dependencies. - -use std::time::Duration; - -use anyhow::Result; -use tokio::net::UdpSocket; -use tokio::time::timeout; - -/// Real RFC-5766 TURN allocation with HMAC-SHA1 long-term credentials. -/// -/// Flow: -/// 1. Send Allocate (unauthenticated) -> get 401 with realm + nonce -/// 2. Compute HMAC-SHA1 key = MD5(username:realm:password) -/// 3. Re-send Allocate with MESSAGE-INTEGRITY -/// 4. Extract XOR-RELAYED-ADDRESS from success response -/// 5. Send ChannelBind to bind channel 0x4000 to the OSTP server addr -/// -/// Returns the relay address string like "1.2.3.4:12345". -pub async fn perform_turn_allocation( - socket: &UdpSocket, - turn_addr: &str, - username: &str, - password: &str, - ostp_server_addr: &str, -) -> Result { - use std::net::ToSocketAddrs; - - let turn_sock: std::net::SocketAddr = turn_addr - .to_socket_addrs() - .map_err(|e| anyhow::anyhow!("TURN DNS resolution failed: {e}"))? - .next() - .ok_or_else(|| anyhow::anyhow!("TURN addr resolved to nothing"))?; - - let transaction_id = { - use rand::Rng; - let mut id = [0u8; 12]; - rand::thread_rng().fill(&mut id); - id - }; - - // ── Step 1: unauthenticated Allocate ───────────────────────────── - // REQUESTED-TRANSPORT attr: 0x0019, value = 17 (UDP) + 3 reserved bytes - let req_transport = stun_attr(0x0019, &[17u8, 0, 0, 0]); - let alloc_req = build_stun_msg(0x0003, &transaction_id, &req_transport); - - socket.send_to(&alloc_req, turn_sock).await - .map_err(|e| anyhow::anyhow!("TURN send Allocate failed: {e}"))?; - - let mut buf = [0u8; 2048]; - let (n, _) = timeout(Duration::from_millis(3000), socket.recv_from(&mut buf)) - .await - .map_err(|_| anyhow::anyhow!("TURN Allocate response timed out"))? - .map_err(|e| anyhow::anyhow!("TURN recv failed: {e}"))?; - - let resp = &buf[..n]; - if resp.len() < 20 { - anyhow::bail!("TURN response too short"); - } - - let msg_type = u16::from_be_bytes([resp[0], resp[1]]); - - // 0x0113 = Allocate Error Response - if msg_type != 0x0113 { - anyhow::bail!("Expected TURN 401 error response, got type 0x{:04x}", msg_type); - } - - // Parse realm and nonce from the error response attributes - let mut realm: Option = None; - let mut nonce: Option = None; - { - let mut idx = 20usize; - while idx + 4 <= n { - let atype = u16::from_be_bytes([resp[idx], resp[idx + 1]]); - let alen = u16::from_be_bytes([resp[idx + 2], resp[idx + 3]]) as usize; - idx += 4; - if idx + alen > n { break; } - let val = &resp[idx..idx + alen]; - match atype { - 0x0014 => realm = Some(String::from_utf8_lossy(val).to_string()), // REALM - 0x0015 => nonce = Some(String::from_utf8_lossy(val).to_string()), // NONCE - _ => {} - } - idx += alen; - let pad = (4 - (alen % 4)) % 4; - idx += pad; - } - } - - let realm = realm.ok_or_else(|| anyhow::anyhow!("TURN 401: no REALM in response"))?; - let nonce = nonce.ok_or_else(|| anyhow::anyhow!("TURN 401: no NONCE in response"))?; - - // ── Step 2: Compute long-term credential key per RFC 5389 §15.4 ── - // key = MD5(username ":" realm ":" password) - let key_input = format!("{}:{}:{}", username, realm, password); - let key = md5_hash(key_input.as_bytes()); - - // HMAC-SHA1 of the message (MESSAGE-INTEGRITY attribute, RFC 5389 §15.4) - let mut attrs2 = Vec::new(); - attrs2.extend_from_slice(&stun_attr(0x0006, username.as_bytes())); // USERNAME - attrs2.extend_from_slice(&stun_attr(0x0014, realm.as_bytes())); // REALM - attrs2.extend_from_slice(&stun_attr(0x0015, nonce.as_bytes())); // NONCE - attrs2.extend_from_slice(&req_transport); // REQUESTED-TRANSPORT - - // For MESSAGE-INTEGRITY we need the full message length including the MI attr (24 bytes) - let mi_placeholder_len = attrs2.len() + 4 + 20; // +4 header, +20 HMAC-SHA1 - let mut msg_for_hmac = build_stun_msg(0x0003, &transaction_id, &attrs2); - // Set length field to include the upcoming MI attr - let new_len = (mi_placeholder_len - 20) as u16; // total attrs length including MI - msg_for_hmac[2..4].copy_from_slice(&new_len.to_be_bytes()); - // Append MI header (without value) - msg_for_hmac.extend_from_slice(&0x0008_u16.to_be_bytes()); // attr type - msg_for_hmac.extend_from_slice(&20_u16.to_be_bytes()); // attr len - - let hmac = hmac_sha1(&key, &msg_for_hmac); - let mut final_attrs = attrs2.clone(); - final_attrs.extend_from_slice(&stun_attr(0x0008, &hmac)); // MESSAGE-INTEGRITY - - let alloc_req2 = build_stun_msg(0x0003, &transaction_id, &final_attrs); - - socket.send_to(&alloc_req2, turn_sock).await - .map_err(|e| anyhow::anyhow!("TURN authenticated Allocate send failed: {e}"))?; - - let (n2, _) = timeout(Duration::from_millis(5000), socket.recv_from(&mut buf)) - .await - .map_err(|_| anyhow::anyhow!("TURN authenticated Allocate timed out"))? - .map_err(|e| anyhow::anyhow!("TURN recv2 failed: {e}"))?; - - let resp2 = &buf[..n2]; - if resp2.len() < 20 { - anyhow::bail!("TURN auth response too short"); - } - let msg_type2 = u16::from_be_bytes([resp2[0], resp2[1]]); - // 0x0103 = Allocate Success Response - if msg_type2 != 0x0103 { - anyhow::bail!("TURN Allocate auth failed, response type 0x{:04x}", msg_type2); - } - - // ── Step 3: Parse XOR-RELAYED-ADDRESS ──────────────────────────── - let relay_addr_str = { - let mut relayed: Option = None; - let mut idx = 20usize; - while idx + 4 <= n2 { - let atype = u16::from_be_bytes([resp2[idx], resp2[idx + 1]]); - let alen = u16::from_be_bytes([resp2[idx + 2], resp2[idx + 3]]) as usize; - idx += 4; - if idx + alen > n2 { break; } - let val = &resp2[idx..idx + alen]; - if atype == 0x0016 && alen >= 8 { // XOR-RELAYED-ADDRESS - let x_port = u16::from_be_bytes([val[2], val[3]]) ^ 0x2112; - let x_ip = [val[4], val[5], val[6], val[7]]; - let ip = std::net::Ipv4Addr::new( - x_ip[0] ^ 0x21, x_ip[1] ^ 0x12, x_ip[2] ^ 0xA4, x_ip[3] ^ 0x42, - ); - relayed = Some(format!("{}:{}", ip, x_port)); - } - idx += alen; - let pad = (4 - (alen % 4)) % 4; - idx += pad; - } - relayed.ok_or_else(|| anyhow::anyhow!("TURN: no XOR-RELAYED-ADDRESS in response"))? - }; - - // ── Step 4: ChannelBind to the OSTP server ──────────────────────── - let ostp_sock: std::net::SocketAddr = ostp_server_addr - .to_socket_addrs() - .map_err(|e| anyhow::anyhow!("OSTP server DNS resolution failed: {e}"))? - .next() - .ok_or_else(|| anyhow::anyhow!("OSTP server addr resolved to nothing"))?; - - let channel_number: u16 = 0x4000; - let mut peer_addr_attr = Vec::new(); - peer_addr_attr.push(0u8); // reserved - peer_addr_attr.push(0x01u8); // family IPv4 - peer_addr_attr.extend_from_slice(&(ostp_sock.port() ^ 0x2112).to_be_bytes()); // XOR port - if let std::net::IpAddr::V4(ipv4) = ostp_sock.ip() { - let octets = ipv4.octets(); - peer_addr_attr.push(octets[0] ^ 0x21); - peer_addr_attr.push(octets[1] ^ 0x12); - peer_addr_attr.push(octets[2] ^ 0xA4); - peer_addr_attr.push(octets[3] ^ 0x42); - } else { - anyhow::bail!("TURN ChannelBind: IPv6 OSTP server not yet supported"); - } - - let mut cb_attrs = Vec::new(); - // CHANNEL-NUMBER attr: 0x000C - cb_attrs.extend_from_slice(&stun_attr(0x000C, &[ - (channel_number >> 8) as u8, channel_number as u8, 0, 0 - ])); - // XOR-PEER-ADDRESS attr: 0x0012 - cb_attrs.extend_from_slice(&stun_attr(0x0012, &peer_addr_attr)); - cb_attrs.extend_from_slice(&stun_attr(0x0006, username.as_bytes())); - cb_attrs.extend_from_slice(&stun_attr(0x0014, realm.as_bytes())); - cb_attrs.extend_from_slice(&stun_attr(0x0015, nonce.as_bytes())); - - // Compute MESSAGE-INTEGRITY for ChannelBind too - let mi_len2 = cb_attrs.len() + 4 + 20; - let mut cb_for_hmac = build_stun_msg(0x0009, &transaction_id, &cb_attrs); - cb_for_hmac[2..4].copy_from_slice(&((mi_len2 - 20) as u16).to_be_bytes()); - cb_for_hmac.extend_from_slice(&0x0008_u16.to_be_bytes()); - cb_for_hmac.extend_from_slice(&20_u16.to_be_bytes()); - let cb_hmac = hmac_sha1(&key, &cb_for_hmac); - cb_attrs.extend_from_slice(&stun_attr(0x0008, &cb_hmac)); - - let cb_req = build_stun_msg(0x0009, &transaction_id, &cb_attrs); - socket.send_to(&cb_req, turn_sock).await - .map_err(|e| anyhow::anyhow!("TURN ChannelBind send failed: {e}"))?; - - let (n3, _) = timeout(Duration::from_millis(3000), socket.recv_from(&mut buf)) - .await - .map_err(|_| anyhow::anyhow!("TURN ChannelBind response timed out"))? - .map_err(|e| anyhow::anyhow!("TURN ChannelBind recv failed: {e}"))?; - - let resp3 = &buf[..n3]; - if resp3.len() < 4 { - anyhow::bail!("TURN ChannelBind response too short"); - } - let cb_resp_type = u16::from_be_bytes([resp3[0], resp3[1]]); - // 0x0109 = ChannelBind Success Response - if cb_resp_type != 0x0109 { - anyhow::bail!("TURN ChannelBind failed, response type 0x{:04x}", cb_resp_type); - } - - Ok(relay_addr_str) -} - -// ── STUN message helpers ───────────────────────────────────────────────────── - -fn build_stun_msg(msg_type: u16, tx_id: &[u8; 12], attrs: &[u8]) -> Vec { - let mut msg = Vec::with_capacity(20 + attrs.len()); - msg.extend_from_slice(&msg_type.to_be_bytes()); - msg.extend_from_slice(&(attrs.len() as u16).to_be_bytes()); - msg.extend_from_slice(&0x2112A442_u32.to_be_bytes()); // Magic Cookie - msg.extend_from_slice(tx_id); - msg.extend_from_slice(attrs); - msg -} - -fn stun_attr(attr_type: u16, value: &[u8]) -> Vec { - let mut out = Vec::new(); - out.extend_from_slice(&attr_type.to_be_bytes()); - out.extend_from_slice(&(value.len() as u16).to_be_bytes()); - out.extend_from_slice(value); - // Pad to 4-byte boundary - let pad = (4 - (value.len() % 4)) % 4; - out.extend(std::iter::repeat_n(0u8, pad)); - out -} - -// ── Cryptographic primitives (inline, zero external deps) ──────────────────── - -/// Pure-Rust MD5 hash (16 bytes). Used for TURN long-term credential key derivation. -fn md5_hash(input: &[u8]) -> [u8; 16] { - // RFC 1321 MD5 constants - const S: [u32; 64] = [ - 7,12,17,22, 7,12,17,22, 7,12,17,22, 7,12,17,22, - 5, 9,14,20, 5, 9,14,20, 5, 9,14,20, 5, 9,14,20, - 4,11,16,23, 4,11,16,23, 4,11,16,23, 4,11,16,23, - 6,10,15,21, 6,10,15,21, 6,10,15,21, 6,10,15,21, - ]; - const K: [u32; 64] = [ - 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, - 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, - 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, - 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, - 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, - 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, - 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, - 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, - 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, - 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, - 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, - ]; - - let msg_len = input.len(); - let bit_len = (msg_len as u64) * 8; - - let mut padded = input.to_vec(); - padded.push(0x80); - while padded.len() % 64 != 56 { - padded.push(0); - } - padded.extend_from_slice(&bit_len.to_le_bytes()); - - let mut a0: u32 = 0x67452301; - let mut b0: u32 = 0xefcdab89; - let mut c0: u32 = 0x98badcfe; - let mut d0: u32 = 0x10325476; - - for chunk in padded.chunks(64) { - let mut m = [0u32; 16]; - for (i, item) in m.iter_mut().enumerate() { - *item = u32::from_le_bytes([chunk[i*4], chunk[i*4+1], chunk[i*4+2], chunk[i*4+3]]); - } - let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0); - for i in 0..64usize { - let (f, g) = match i { - 0..=15 => ((b & c) | (!b & d), i), - 16..=31 => ((d & b) | (!d & c), (5*i + 1) % 16), - 32..=47 => (b ^ c ^ d, (3*i + 5) % 16), - _ => (c ^ (b | !d), (7*i) % 16), - }; - let temp = d; - d = c; - c = b; - b = b.wrapping_add((a.wrapping_add(f).wrapping_add(K[i]).wrapping_add(m[g])).rotate_left(S[i])); - a = temp; - } - a0 = a0.wrapping_add(a); - b0 = b0.wrapping_add(b); - c0 = c0.wrapping_add(c); - d0 = d0.wrapping_add(d); - } - - let mut result = [0u8; 16]; - result[0..4].copy_from_slice(&a0.to_le_bytes()); - result[4..8].copy_from_slice(&b0.to_le_bytes()); - result[8..12].copy_from_slice(&c0.to_le_bytes()); - result[12..16].copy_from_slice(&d0.to_le_bytes()); - result -} - -/// HMAC-SHA1 for TURN MESSAGE-INTEGRITY (RFC 2104 + RFC 5389 §15.4). -fn hmac_sha1(key: &[u8], message: &[u8]) -> [u8; 20] { - const BLOCK_SIZE: usize = 64; - - let mut k = [0u8; BLOCK_SIZE]; - if key.len() > BLOCK_SIZE { - let h = sha1_hash(key); - k[..20].copy_from_slice(&h); - } else { - k[..key.len()].copy_from_slice(key); - } - - let mut ipad = [0u8; BLOCK_SIZE]; - let mut opad = [0u8; BLOCK_SIZE]; - for i in 0..BLOCK_SIZE { - ipad[i] = k[i] ^ 0x36; - opad[i] = k[i] ^ 0x5C; - } - - let mut inner = ipad.to_vec(); - inner.extend_from_slice(message); - let inner_hash = sha1_hash(&inner); - - let mut outer = opad.to_vec(); - outer.extend_from_slice(&inner_hash); - sha1_hash(&outer) -} - -/// Pure-Rust SHA-1 (RFC 3174). -fn sha1_hash(input: &[u8]) -> [u8; 20] { - let msg_len = input.len(); - let bit_len = (msg_len as u64) * 8; - let mut padded = input.to_vec(); - padded.push(0x80); - while padded.len() % 64 != 56 { - padded.push(0); - } - padded.extend_from_slice(&bit_len.to_be_bytes()); - - let mut h: [u32; 5] = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]; - - for chunk in padded.chunks(64) { - let mut w = [0u32; 80]; - for i in 0..16 { - w[i] = u32::from_be_bytes([chunk[i*4], chunk[i*4+1], chunk[i*4+2], chunk[i*4+3]]); - } - for i in 16..80 { - w[i] = (w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16]).rotate_left(1); - } - let (mut a, mut b, mut c, mut d, mut e) = (h[0], h[1], h[2], h[3], h[4]); - for i in 0..80usize { - let (f, k) = match i { - 0..=19 => ((b & c) | (!b & d), 0x5A827999u32), - 20..=39 => (b ^ c ^ d, 0x6ED9EBA1), - 40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1BBCDC), - _ => (b ^ c ^ d, 0xCA62C1D6), - }; - let temp = a.rotate_left(5).wrapping_add(f).wrapping_add(e).wrapping_add(k).wrapping_add(w[i]); - e = d; d = c; c = b.rotate_left(30); b = a; a = temp; - } - h[0] = h[0].wrapping_add(a); h[1] = h[1].wrapping_add(b); - h[2] = h[2].wrapping_add(c); h[3] = h[3].wrapping_add(d); - h[4] = h[4].wrapping_add(e); - } - - let mut out = [0u8; 20]; - for (i, &v) in h.iter().enumerate() { - out[i*4..(i+1)*4].copy_from_slice(&v.to_be_bytes()); - } - out -} diff --git a/ostp-server/Cargo.toml b/ostp-server/Cargo.toml index 8261a34..a79f2ab 100644 --- a/ostp-server/Cargo.toml +++ b/ostp-server/Cargo.toml @@ -20,3 +20,6 @@ portable-atomic.workspace = true hmac.workspace = true sha2.workspace = true base64 = "0.22" +rustls = "0.23" +tokio-rustls = "0.26" +rcgen = "0.13" diff --git a/ostp-server/src/api.rs b/ostp-server/src/api.rs index 42e39cc..37b983d 100644 --- a/ostp-server/src/api.rs +++ b/ostp-server/src/api.rs @@ -42,8 +42,8 @@ pub struct ApiState { pub api_token: String, /// Server address for subscription links (e.g. "example.com") pub server_host: String, - /// Server listen port pub server_port: u16, + pub reality_query: String, } // ── API configuration ──────────────────────────────────────────────────────── @@ -137,6 +137,7 @@ pub async fn start_api_server( user_stats: Arc>>>, server_host: String, server_port: u16, + reality_query: String, ) { let state = ApiState { access_keys, @@ -145,6 +146,7 @@ pub async fn start_api_server( api_token: config.token.clone(), server_host, server_port, + reality_query, }; let app = create_api_router(state); @@ -416,7 +418,7 @@ async fn handle_subscribe( // If client requests plain text, return ostp:// share link if accept.contains("text/plain") { - let link = format!("ostp://{}@{}:{}", key, state.server_host, state.server_port); + let link = format!("ostp://{}@{}:{}{}", key, state.server_host, state.server_port, state.reality_query); return (StatusCode::OK, Json(serde_json::json!({ "ok": true, "data": link diff --git a/ostp-server/src/lib.rs b/ostp-server/src/lib.rs index 2b9a3be..622b183 100644 --- a/ostp-server/src/lib.rs +++ b/ostp-server/src/lib.rs @@ -22,6 +22,15 @@ pub use outbound::{OutboundAction, OutboundConfig, OutboundRule}; pub use api::ApiConfig; pub use fallback::FallbackConfig; +#[derive(Debug, Clone)] +pub struct RealityServerConfig { + pub dest: String, + pub private_key: String, + pub pbk: String, + pub sid: String, + pub sni_list: Vec, +} + // ── Internal event types ───────────────────────────────────────────────────── #[derive(Debug, Clone)] @@ -59,6 +68,8 @@ pub async fn run_server( api_config: Option, fallback_config: Option, debug: bool, + reality_query: Option, + reality_config: Option, ) -> Result<()> { let mut keys_map = HashMap::new(); for key in access_keys { @@ -161,8 +172,9 @@ pub async fn run_server( let parts: Vec<&str> = primary.rsplitn(2, ':').collect(); let server_port: u16 = parts.first().and_then(|p| p.parse().ok()).unwrap_or(50000); let server_host = parts.get(1).unwrap_or(&"0.0.0.0").to_string(); + let rq = reality_query.clone().unwrap_or_default(); tokio::spawn(async move { - api::start_api_server(api_cfg, api_keys, api_stats, server_host, server_port).await; + api::start_api_server(api_cfg, api_keys, api_stats, server_host, server_port, rq).await; }); } } @@ -213,8 +225,25 @@ pub async fn run_server( let key_count = shared_keys.read().unwrap().len(); tracing::info!(listeners = bind_addrs.len(), keys = key_count, "server started"); tracing::info!("ARQ config: max_reorder=16384, reorder_buf=8192, sent_history=32768, rto=100ms"); + let tls_config = if let Some(rc) = reality_config { + let subject_alt_names = rc.sni_list.clone(); + let cert = rcgen::generate_simple_self_signed(subject_alt_names)?; + let cert_der = cert.cert.der().to_vec(); + let priv_key = cert.key_pair.serialize_der(); + + let server_config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert( + vec![rustls::pki_types::CertificateDer::from(cert_der)], + rustls::pki_types::PrivatePkcs8KeyDer::from(priv_key).into(), + )?; + Some(std::sync::Arc::new(server_config)) + } else { + None + }; + tokio::select! { - res = run_server_loop(bind_addrs.clone(), primary_socket, sockets, dispatcher, ui_cmd_rx, ui_event_tx, shared_keys, outbound, debug) => { + res = run_server_loop(bind_addrs.clone(), primary_socket, sockets, dispatcher, ui_cmd_rx, ui_event_tx, shared_keys, outbound, debug, tls_config) => { if let Err(e) = res { tracing::error!("Server error: {e}"); } @@ -239,6 +268,7 @@ async fn run_server_loop( shared_keys: std::sync::Arc>>, outbound: Option, debug: bool, + tls_config: Option>, ) -> Result<()> { let mut remotes: HashMap<(u32, u16), RemoteState> = HashMap::new(); let (stream_tx, mut stream_rx) = mpsc::unbounded_channel::<(u32, u16, Vec)>(); @@ -257,7 +287,11 @@ async fn run_server_loop( loop { match sock_clone.recv_from(&mut buf).await { Ok((size, peer)) => { - let packet = Bytes::copy_from_slice(&buf[..size]); + let packet = if size >= 12 && buf[0] == 0x80 { + Bytes::copy_from_slice(&buf[12..size]) + } else { + Bytes::copy_from_slice(&buf[..size]) + }; if tx.send((packet, peer)).await.is_err() { break; } @@ -275,6 +309,7 @@ async fn run_server_loop( let shared_keys_clone = shared_keys.clone(); let udp_tx_clone = udp_tx.clone(); + let tls_cfg = tls_config.clone(); tokio::spawn(async move { if let Ok(listener) = tokio::net::TcpListener::bind(&addr).await { tracing::info!("TCP (UoT) listener bound to {}", addr); @@ -312,9 +347,24 @@ async fn run_server_loop( let tm = tcp_map_clone.clone(); let keys = shared_keys_clone.clone(); let tx = udp_tx_clone.clone(); + let tls = tls_cfg.clone(); tokio::spawn(async move { - if let Err(e) = crate::transport::uot::handle_tcp_connection(stream, peer_addr, keys, tx, tm).await { - tracing::warn!("UoT connection from {} closed: {}", peer_addr, e); + if let Some(cfg) = tls { + let acceptor = tokio_rustls::TlsAcceptor::from(cfg); + match acceptor.accept(stream).await { + Ok(tls_stream) => { + if let Err(e) = crate::transport::uot::handle_tcp_connection(tls_stream, peer_addr, keys, tx, tm).await { + tracing::warn!("UoT TLS connection from {} closed: {}", peer_addr, e); + } + } + Err(e) => { + tracing::warn!("UoT TLS handshake from {} failed: {}", peer_addr, e); + } + } + } else { + if let Err(e) = crate::transport::uot::handle_tcp_connection(stream, peer_addr, keys, tx, tm).await { + tracing::warn!("UoT connection from {} closed: {}", peer_addr, e); + } } }); } @@ -391,7 +441,10 @@ async fn run_server_loop( } } if !sent_tcp { - let _ = socket.send_to(&resp, peer_addr).await?; + let mut out = bytes::BytesMut::with_capacity(12 + resp.len()); + out.extend_from_slice(&[0x80, 0x60, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44]); + out.extend_from_slice(&resp); + let _ = socket.send_to(&out.freeze(), peer_addr).await?; } let _ = ui_event_tx.send(UiEvent::Tx { peer: peer_ip, bytes: resp_len }); } @@ -475,7 +528,10 @@ async fn run_server_loop( } } if !sent_tcp { - let _ = socket.send_to(&frame, peer_addr).await?; + let mut out = bytes::BytesMut::with_capacity(12 + frame.len()); + out.extend_from_slice(&[0x80, 0x60, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44]); + out.extend_from_slice(&frame); + let _ = socket.send_to(&out.freeze(), peer_addr).await?; } } for sid in dropped_sessions { diff --git a/ostp-server/src/transport/uot.rs b/ostp-server/src/transport/uot.rs index 219dc62..6e0f5f3 100644 --- a/ostp-server/src/transport/uot.rs +++ b/ostp-server/src/transport/uot.rs @@ -11,13 +11,16 @@ use tokio::net::TcpStream; use tokio::sync::{mpsc, RwLock}; use tracing::info; -pub async fn handle_tcp_connection( - mut stream: TcpStream, +pub async fn handle_tcp_connection( + mut stream: S, peer_addr: SocketAddr, shared_keys: Arc>>, udp_tx: mpsc::Sender<(Bytes, SocketAddr)>, tcp_map: Arc>>>, -) -> Result<()> { +) -> Result<()> +where + S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static, +{ // 1. Read HTTP Handshake let mut buf = [0u8; 4096]; let mut header_len = 0; @@ -123,7 +126,7 @@ pub async fn handle_tcp_connection( let leftover = &buf[headers_end..header_len]; // Process streams - let (mut read_half, mut write_half) = stream.into_split(); + let (mut read_half, mut write_half) = tokio::io::split(stream); // Spawn writer task let peer_clone = peer_addr; @@ -181,7 +184,10 @@ pub async fn handle_tcp_connection( Ok(()) } -async fn send_404(stream: &mut TcpStream) -> Result<()> { +async fn send_404(stream: &mut S) -> Result<()> +where + S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static, +{ let body = "Not Found"; let resp = format!( "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", diff --git a/ostp/src/main.rs b/ostp/src/main.rs index 1d6f1ae..d67dca4 100644 --- a/ostp/src/main.rs +++ b/ostp/src/main.rs @@ -55,20 +55,48 @@ fn parse_ostp_link(link: &str) -> Result { let host = parsed.host_str().ok_or_else(|| anyhow!("Missing host in share link"))?; let port = parsed.port().ok_or_else(|| anyhow!("Missing port in share link"))?; let server = format!("{host}:{port}"); + let mut sni = String::new(); + let mut fp = String::new(); + let mut pbk = String::new(); + let mut sid = String::new(); + let mut spx = String::new(); + let mut transport_mode = String::from("udp"); + + for (k, v) in parsed.query_pairs() { + match k.as_ref() { + "sni" => sni = v.into_owned(), + "fp" => fp = v.into_owned(), + "pbk" => pbk = v.into_owned(), + "sid" => sid = v.into_owned(), + "spx" => spx = v.into_owned(), + "type" => transport_mode = v.into_owned(), + _ => {} + } + } Ok(ClientConfig { server, access_key, mtu: None, - transport: None, - socks5_bind: Some("127.0.0.1:1088".to_string()), // Fallback to standard SOCKS5 port + transport: Some(TransportConfigRaw { + mode: Some(transport_mode), + stealth_sni: Some(sni.clone()), + stealth_port: Some(443), + }), + socks5_bind: Some("127.0.0.1:1088".to_string()), tun: Some(TunConfig { - enable: false, // Default to proxy, configurable via settings GUI + enable: false, wintun_path: Some("./wintun.dll".to_string()), ipv4_address: Some("10.1.0.2/24".to_string()), dns: None, }), - turn: None, + reality: Some(RealityConfigRaw { + sni, + fp, + pbk, + sid, + spx, + }), debug: Some(false), exclude: None, mux: None, @@ -141,7 +169,7 @@ impl UnifiedConfig { struct ServerConfig { listen: ListenConfig, access_keys: Vec, - turn_server: Option, + reality: Option, debug: Option, outbound: Option, api: Option, @@ -193,7 +221,7 @@ struct ClientConfig { mtu: Option, socks5_bind: Option, tun: Option, - turn: Option, + reality: Option, debug: Option, exclude: Option, mux: Option, @@ -215,12 +243,24 @@ struct TunConfig { dns: Option, } -#[derive(Debug, Deserialize, Serialize)] -struct TurnConfigRaw { +#[derive(Debug, Deserialize, Serialize, Clone)] +struct RealityConfigRaw { + sni: String, + fp: String, + pbk: String, + sid: String, + spx: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct RealityServerConfigRaw { + #[serde(default)] enabled: bool, - server_addr: String, - username: Option, - access_key: Option, + dest: String, + private_key: String, + pbk: String, + sid: String, + sni_list: Vec, } #[derive(Debug, Deserialize, Serialize)] @@ -469,6 +509,16 @@ async fn run_app() -> Result<()> { // Target web server (e.g., local nginx or caddy) "target": "127.0.0.1:8080" }}, + + // Reality (XTLS) / UoT Masquerade parameters + "reality": {{ + "enabled": false, + "dest": "www.microsoft.com:443", + "private_key": "", + "pbk": "", + "sid": "", + "sni_list": ["www.microsoft.com"] + }}, "debug": false }}"#, key) } else { @@ -501,18 +551,19 @@ async fn run_app() -> Result<()> { "processes": [] }}, - // STUN/TURN server settings to bypass UDP blocks by mimicking WebRTC call traffic - "turn": {{ - "enabled": false, - "server_addr": "127.0.0.1:3478", - "username": "ostpuser", - "access_key": "ostppassword" + // Reality (XTLS) / WebRTC Masquerade parameters + "reality": {{ + "dest": "www.microsoft.com:443", + "private_key": "", + "pbk": "", + "sid": "", + "sni_list": ["www.microsoft.com"] }}, - // Transport Mode: "udp" (default) or "uot" (xHTTP Stealth / UDP over TCP) + // Transport Mode: "udp" (default WebRTC masquerade) or "uot" (TCP XTLS-Reality) "transport": {{ "mode": "udp", - "stealth_sni": "vk.com", + "stealth_sni": "www.microsoft.com", "stealth_port": 443 }}, @@ -529,11 +580,15 @@ async fn run_app() -> Result<()> { if is_server { let mut stripped = json_comments::StripComments::new(content.as_bytes()); if let Ok(config) = serde_json::from_reader::<_, UnifiedConfig>(&mut stripped) { - if let AppMode::Server(s) = config.mode { + if let AppMode::Server(s) = &config.mode { let key = &s.access_keys[0]; let host = get_or_ask_public_ip(&args.config); + let mut link = format!("ostp://{}@{}:50000", key, host); + if let Some(r) = &s.reality { + link = format!("{}?security=reality&sni={}&pbk={}&sid={}&type=udp", link, r.sni_list.first().unwrap_or(&String::new()), r.pbk, r.sid); + } println!("\n Share link for client distribution:"); - println!(" ostp://{}@{}:50000", key, host); + println!(" {}", link); } } } @@ -575,7 +630,11 @@ async fn run_app() -> Result<()> { println!("\n Client share links from {:?}:", args.config); for (idx, key) in server_cfg.access_keys.iter().enumerate() { - println!(" [{}] ostp://{}@{}:{}", idx + 1, key, host, port); + let mut link = format!("ostp://{}@{}:{}", key, host, port); + if let Some(r) = &server_cfg.reality { + link = format!("{}?security=reality&sni={}&pbk={}&sid={}&type=udp", link, r.sni_list.first().unwrap_or(&String::new()), r.pbk, r.sid); + } + println!(" [{}] {}", idx + 1, link); } return Ok(()); } @@ -589,8 +648,10 @@ async fn run_app() -> Result<()> { AppMode::Server(server_cfg) => { let listen_addrs = server_cfg.listen.addresses(); println!("[ostp] Starting server on {:?}", listen_addrs); - if let Some(turn) = server_cfg.turn_server { - println!("[ostp] TURN relay enabled: {}", turn); + if let Some(ref reality) = server_cfg.reality { + if reality.enabled { + println!("[ostp] Reality mode enabled (dest: {})", reality.dest); + } } let debug = server_cfg.debug.unwrap_or(false); let outbound = server_cfg.outbound.map(|o| ostp_server::OutboundConfig { @@ -619,8 +680,22 @@ async fn run_app() -> Result<()> { listen: f.listen.unwrap_or_else(|| "0.0.0.0:443".to_string()), target: f.target.unwrap_or_else(|| "127.0.0.1:8080".to_string()), }); + let mut rq = None; + let mut rc = None; + if let Some(r) = server_cfg.reality { + if r.enabled { + rq = Some(format!("?security=reality&sni={}&pbk={}&sid={}&type=udp", r.sni_list.first().unwrap_or(&String::new()), r.pbk, r.sid)); + rc = Some(ostp_server::RealityServerConfig { + sni_list: r.sni_list.clone(), + dest: r.dest, + private_key: r.private_key, + pbk: r.pbk, + sid: r.sid, + }); + } + } // Pass all listen addresses for multi-listener support - ostp_server::run_server(listen_addrs, server_cfg.access_keys, outbound, api_config, fallback_config, debug).await?; + ostp_server::run_server(listen_addrs, server_cfg.access_keys, outbound, api_config, fallback_config, debug, rq, rc).await?; } AppMode::Client(client_cfg) => { run_client_directly(client_cfg).await?; @@ -635,7 +710,7 @@ async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> { let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false); let mode_str = if is_tun_enabled { "tun" } else { "proxy" }; println!("[ostp] Starting client (mode={}, server={})", mode_str, client_cfg.server); - let turn_cfg = client_cfg.turn.as_ref(); + let reality_cfg = client_cfg.reality.as_ref(); let client_conf = ostp_client::config::ClientConfig { mode: if is_tun_enabled { "tun".to_string() } else { "proxy".to_string() }, debug: client_cfg.debug.unwrap_or(false), @@ -644,18 +719,20 @@ async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> { local_bind_addr: "0.0.0.0:0".to_string(), access_key: client_cfg.access_key.clone(), handshake_timeout_ms: 5000, - io_timeout_ms: 5000, + io_timeout_ms: 2500, mtu: client_cfg.mtu.unwrap_or(1350), + keepalive_interval_sec: 5, }, local_proxy: ostp_client::config::LocalProxyConfig { bind_addr: client_cfg.socks5_bind.clone().unwrap_or_else(|| "127.0.0.1:1088".to_string()), connect_timeout_ms: 5000, }, - turn: ostp_client::config::TurnConfig { - enabled: turn_cfg.map(|t| t.enabled).unwrap_or(false), - server_addr: turn_cfg.map(|t| t.server_addr.clone()).unwrap_or_default(), - username: turn_cfg.and_then(|t| t.username.clone()).unwrap_or_default(), - access_key: turn_cfg.and_then(|t| t.access_key.clone()).unwrap_or_default(), + reality: ostp_client::config::RealityConfig { + sni: reality_cfg.map(|t| t.sni.clone()).unwrap_or_default(), + fp: reality_cfg.map(|t| t.fp.clone()).unwrap_or_default(), + pbk: reality_cfg.map(|t| t.pbk.clone()).unwrap_or_default(), + sid: reality_cfg.map(|t| t.sid.clone()).unwrap_or_default(), + spx: reality_cfg.map(|t| t.spx.clone()).unwrap_or_default(), }, exclusions: ostp_client::config::ExclusionConfig { domains: client_cfg.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(),