From 83f7ff21195c8cc139684fcacb0c2dd12a59a60d Mon Sep 17 00:00:00 2001 From: ospab Date: Thu, 21 May 2026 02:11:02 +0300 Subject: [PATCH] feat: UoT and xHTTP stealth --- Cargo.lock | 264 ++++++++++++++++++++++++++++- README.md | 2 + README.ru.md | 7 + ostp-client/Cargo.toml | 6 + ostp-client/src/bridge.rs | 113 +++++++----- ostp-client/src/config.rs | 24 ++- ostp-client/src/lib.rs | 2 + ostp-client/src/transport/mod.rs | 57 +++++++ ostp-client/src/transport/xhttp.rs | 175 +++++++++++++++++++ ostp-wiki | 2 +- ostp/src/main.rs | 7 + 11 files changed, 588 insertions(+), 71 deletions(-) create mode 100644 ostp-client/src/transport/mod.rs create mode 100644 ostp-client/src/transport/xhttp.rs diff --git a/Cargo.lock b/Cargo.lock index 95b57f7..ae1d54a 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.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +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" @@ -426,6 +459,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 +496,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" @@ -472,6 +517,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-task" version = "0.3.32" @@ -485,6 +541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-macro", "futures-task", "pin-project-lite", "slab", @@ -511,6 +568,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" @@ -808,6 +877,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" @@ -948,16 +1027,22 @@ name = "ostp-client" version = "0.2.5" dependencies = [ "anyhow", + "base64", "bytes", "chrono", + "futures-util", + "hmac", "json_comments", "ostp-core", "portable-atomic", "rand", + "rustls", "serde", "serde_json", + "sha2", "socket2", "tokio", + "tokio-rustls", "tracing", ] @@ -1101,6 +1186,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" @@ -1128,7 +1219,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", ] [[package]] @@ -1148,6 +1239,20 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -1157,6 +1262,43 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +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", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +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", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1436,6 +1578,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "toml" version = "0.5.11" @@ -1571,6 +1723,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -1623,6 +1781,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" @@ -1742,7 +1909,16 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -1760,13 +1936,29 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1775,42 +1967,90 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winres" version = "0.1.12" @@ -1820,6 +2060,12 @@ 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" diff --git a/README.md b/README.md index 33254a9..46d55ec 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Download pre-built binaries for your platform from [GitHub Releases](https://git | **Fallback Server** | TCP fallback proxy to a web server — makes OSTP indistinguishable from nginx during active probing. | | **Multi-Listener** | Bind to multiple addresses simultaneously (dual-stack IPv4/IPv6, multi-port). | | **TUN Mode** | Full-system VPN via `tun2socks` integration. All traffic transparently routed through the tunnel. | +| **xHTTP Stealth (UoT)** | UDP-over-TCP tunnel disguised as standard HTTP/1.1 or TLS traffic to bypass Level 1 Deep Packet Inspection (DPI) whitelists. | | **TURN Relay** | RFC 5766 TURN support for environments where direct UDP is blocked. | | **Hot-Reload** | Runtime config reload without restart (access keys, exclusions, mux settings). | | **Structured Logging** | `tracing`-based logging with `RUST_LOG` filtering. JSON/file/syslog output support. | @@ -121,6 +122,7 @@ Download pre-built binaries for your platform from [GitHub Releases](https://git "server": "YOUR_SERVER_IP:50000", "access_key": "YOUR_SECRET_KEY", "socks5_bind": "127.0.0.1:1088", + "transport": { "mode": "udp", "stealth_sni": "vk.com", "stealth_port": 443 }, "tun": { "enable": false, "dns": "1.1.1.1" } } ``` diff --git a/README.ru.md b/README.ru.md index 0ce29ac..faf42fd 100644 --- a/README.ru.md +++ b/README.ru.md @@ -20,6 +20,7 @@ OSTP — высокопроизводительный транспортный | **Мультиплексирование** | Несколько логических TCP-потоков поверх одной зашифрованной UDP-сессии с per-stream flow control. | | **Бесшовный роуминг** | Клиент может менять сети (WiFi ↔ 4G) без разрыва сессии — сервер отслеживает session-ID, а не IP-адрес. | | **TUN-режим** | Полносистемный VPN через интеграцию с `tun2socks` на Windows и Linux. | +| **xHTTP Стелс (UoT)** | Туннель UDP-over-TCP, замаскированный под обычный HTTP/1.1 или TLS трафик для обхода белых списков ТСПУ (DPI). | | **TURN Relay** | RFC 5766 TURN для окружений, где прямой UDP заблокирован. | | **Hot-Reload** | Перезагрузка конфига в рантайме без перезапуска (ключи, исключения, mux, TURN). | | **Кросс-платформа** | Windows, Linux, macOS, Android. Один бинарник, без зависимостей. | @@ -107,6 +108,12 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie "access_key": "ВАШ_КЛЮЧ", "socks5_bind": "127.0.0.1:1088", "debug": false, + // Настройки транспорта (udp или uot) + "transport": { + "mode": "udp", + "stealth_sni": "vk.com", + "stealth_port": 443 + }, // TUN-режим (полносистемный VPN) "tun": { "enable": false, diff --git a/ostp-client/Cargo.toml b/ostp-client/Cargo.toml index 6cc94c3..8cd8ba3 100644 --- a/ostp-client/Cargo.toml +++ b/ostp-client/Cargo.toml @@ -17,3 +17,9 @@ json_comments = "0.2" portable-atomic.workspace = true chrono = "0.4" socket2 = "0.6.3" +rustls = { version = "0.23.40", features = ["ring", "std"] } +tokio-rustls = "0.26.0" +futures-util = "0.3.32" +hmac = "0.12.1" +sha2 = "0.10.8" +base64 = "0.22.1" diff --git a/ostp-client/src/bridge.rs b/ostp-client/src/bridge.rs index d98d0d5..70a0c9f 100644 --- a/ostp-client/src/bridge.rs +++ b/ostp-client/src/bridge.rs @@ -38,20 +38,20 @@ pub struct BridgeMetrics { pub connection_state: AtomicU8, } -async fn send_datagram(socket: &UdpSocket, frame: &Bytes, turn_enabled: bool) -> std::io::Result { +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); out.extend_from_slice(frame); - socket.send(&out).await + socket.send(&out.freeze()).await } else { socket.send(frame).await } } struct SessionState { - socket: Arc, + socket: crate::transport::Transport, machine: ProtocolMachine, } @@ -74,6 +74,10 @@ pub struct Bridge { pub mux_enabled: bool, pub mux_sessions: usize, + pub transport_mode: String, + pub stealth_sni: String, + pub stealth_port: u16, + metrics: Arc, sample_sent: u64, sample_recv: u64, @@ -103,6 +107,10 @@ impl Bridge { 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(), + stealth_port: config.transport.stealth_port, + metrics, sample_sent: 0, sample_recv: 0, @@ -261,8 +269,7 @@ impl Bridge { match self.perform_handshake_with_id(&tx, session_id).await { Ok((sock, mach, rtt)) => { let session_index = sessions.len(); - let socket = Arc::new(sock); - let socket_clone = socket.clone(); + let socket_clone = sock.clone(); let udp_tx_clone = udp_tx.clone(); let is_turn = self.turn_enabled; tokio::spawn(async move { @@ -295,7 +302,7 @@ impl Bridge { } }); - sessions.push(SessionState { socket, machine: mach }); + sessions.push(SessionState { socket: sock, machine: mach }); rtt_sum += rtt; successful_sessions += 1; } @@ -357,8 +364,7 @@ impl Bridge { match self.perform_handshake_with_id(&tx, session_id).await { Ok((sock, mach, rtt)) => { let session_index = new_sessions.len(); - let socket = Arc::new(sock); - let socket_clone = socket.clone(); + let socket_clone = sock.clone(); let udp_tx_clone = udp_tx.clone(); let is_turn = self.turn_enabled; tokio::spawn(async move { @@ -381,7 +387,7 @@ impl Bridge { } } }); - new_sessions.push(SessionState { socket, machine: mach }); + new_sessions.push(SessionState { socket: sock, machine: mach }); rtt_sum += rtt; successful_sessions += 1; } @@ -470,8 +476,7 @@ impl Bridge { match self.perform_handshake_with_id(&tx, session_id).await { Ok((sock, mach, rtt)) => { let session_index = new_sessions.len(); - let socket = Arc::new(sock); - let socket_clone = socket.clone(); + let socket_clone = sock.clone(); let udp_tx_clone = udp_tx.clone(); let is_turn = self.turn_enabled; tokio::spawn(async move { @@ -501,7 +506,7 @@ impl Bridge { } }); - new_sessions.push(SessionState { socket, machine: mach }); + new_sessions.push(SessionState { socket: sock, machine: mach }); rtt_sum += rtt; successful_sessions += 1; } @@ -750,7 +755,7 @@ impl Bridge { &mut self, tx: &mpsc::Sender, session_id: u32, - ) -> Result<(UdpSocket, ProtocolMachine, f64)> { + ) -> Result<(crate::transport::Transport, ProtocolMachine, f64)> { let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() @@ -791,13 +796,13 @@ impl Bridge { tx.send(UiEvent::Log(format!("Connecting to remote server: {}...", target_addr))).await.ok(); - let socket = match self.try_connect_socket(target_ip, port).await { + 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); - match self.try_connect_socket(std::net::IpAddr::V6(nat64_ipv6), port).await { + match self.try_connect_transport(std::net::IpAddr::V6(nat64_ipv6), port).await { Ok(sock) => sock, Err(fallback_err) => { return Err(anyhow::anyhow!("Direct IPv4 failed: {}. NAT64 fallback failed: {}", e, fallback_err)); @@ -809,7 +814,11 @@ impl Bridge { } }; - if self.turn_enabled { + 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 { @@ -817,7 +826,7 @@ impl Bridge { }; tx.send(UiEvent::Log(format!("Allocating TURN relay via {}", turn_addr))).await.ok(); - match crate::turn::perform_turn_allocation(&socket, &turn_addr, &self.turn_username, &self.turn_password, &self.server_addr).await { + 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(); @@ -826,7 +835,7 @@ impl Bridge { .collect(); let turn_target = resolved_turn.first().ok_or_else(|| anyhow::anyhow!("no IP resolved for TURN {}", turn_addr))?; - let connect_ip = if socket.local_addr().map(|a| a.is_ipv6()).unwrap_or(false) && turn_target.is_ipv4() { + 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 { @@ -837,14 +846,14 @@ impl Bridge { }; let connect_addr = std::net::SocketAddr::new(connect_ip, turn_target.port()); - socket + 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 socket.local_addr().map(|a| a.is_ipv6()).unwrap_or(false) && target_ip.is_ipv4() { + 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 { @@ -854,7 +863,7 @@ impl Bridge { target_ip }; let connect_addr = std::net::SocketAddr::new(connect_ip, port); - socket + udp_socket .connect(connect_addr) .await .with_context(|| format!("failed to connect udp to {}", connect_addr))?; @@ -898,7 +907,7 @@ impl Bridge { 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); - match self.try_connect_socket(std::net::IpAddr::V6(nat64_ipv6), port).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 { @@ -963,39 +972,49 @@ impl Bridge { 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; } - async fn try_connect_socket( + async fn try_connect_transport( &self, target_ip: std::net::IpAddr, port: u16, - ) -> Result { - 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) + ) -> Result { + if self.transport_mode == "uot" { + let (tx, rx) = crate::transport::xhttp::connect_xhttp( + target_ip, self.stealth_port, &self.stealth_sni, &self.access_key + ).await?; + Ok(crate::transport::Transport::Uot { tx, rx }) } else { - std::net::SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0) - }; + 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 = 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))) } - 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(socket) } } diff --git a/ostp-client/src/config.rs b/ostp-client/src/config.rs index ae633d8..d8725dc 100644 --- a/ostp-client/src/config.rs +++ b/ostp-client/src/config.rs @@ -55,33 +55,29 @@ pub struct LocalProxyConfig { } /// Transport layer configuration. -/// `mode` = "udp" (default) or "wss" (WebSocket Secure — bypasses DPI whitelists). +/// `mode` = "udp" (default) or "uot" (UDP over TCP with xHTTP stealth). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TransportConfig { - /// "udp" or "wss" + /// "udp" or "uot" #[serde(default = "default_transport_mode")] pub mode: String, - /// WebSocket host (domain for TLS connect and HTTP Host header) + /// TLS SNI and HTTP Host for stealth routing #[serde(default)] - pub wss_host: String, - /// WebSocket HTTP path, e.g. "/ostp" - #[serde(default = "default_wss_path")] - pub wss_path: String, - /// TLS SNI override; defaults to wss_host if empty - #[serde(default)] - pub wss_sni: String, + pub stealth_sni: String, + /// TCP Port for the stealth connection + #[serde(default = "default_stealth_port")] + pub stealth_port: u16, } fn default_transport_mode() -> String { "udp".to_string() } -fn default_wss_path() -> String { "/ostp".to_string() } +fn default_stealth_port() -> u16 { 443 } impl Default for TransportConfig { fn default() -> Self { Self { mode: default_transport_mode(), - wss_host: String::new(), - wss_path: default_wss_path(), - wss_sni: String::new(), + stealth_sni: String::new(), + stealth_port: default_stealth_port(), } } } diff --git a/ostp-client/src/lib.rs b/ostp-client/src/lib.rs index 70193fc..5c6fcf0 100644 --- a/ostp-client/src/lib.rs +++ b/ostp-client/src/lib.rs @@ -3,6 +3,8 @@ pub mod bridge; pub mod config; pub mod signal; pub mod sysproxy; +pub mod transport; pub mod tunnel; + pub mod turn; pub mod runner; diff --git a/ostp-client/src/transport/mod.rs b/ostp-client/src/transport/mod.rs new file mode 100644 index 0000000..c3eb8e3 --- /dev/null +++ b/ostp-client/src/transport/mod.rs @@ -0,0 +1,57 @@ +pub mod xhttp; + +use std::sync::Arc; +use tokio::net::UdpSocket; +use bytes::Bytes; + +#[derive(Clone)] +pub enum Transport { + Udp(Arc), + Uot { + tx: tokio::sync::mpsc::Sender, + rx: Arc>>, + } +} + +impl Transport { + pub async fn send(&self, frame: &Bytes) -> std::io::Result { + match self { + Self::Udp(sock) => sock.send(frame).await, + Self::Uot { tx, .. } => { + tx.send(frame.clone()).await.map_err(|_| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "uot closed"))?; + Ok(frame.len()) + } + } + } + + pub async fn send_to(&self, frame: &Bytes, target: std::net::SocketAddr) -> std::io::Result { + match self { + Self::Udp(sock) => sock.send_to(frame, target).await, + Self::Uot { .. } => self.send(frame).await, + } + } + + pub async fn recv(&self, buf: &mut [u8]) -> std::io::Result { + match self { + Self::Udp(sock) => sock.recv(buf).await, + Self::Uot { rx, .. } => { + let mut rx = rx.lock().await; + match rx.recv().await { + Some(bytes) => { + let len = bytes.len().min(buf.len()); + buf[..len].copy_from_slice(&bytes[..len]); + Ok(len) + } + None => Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "uot closed")), + } + } + } + } + + pub fn local_addr(&self) -> std::io::Result { + match self { + Self::Udp(sock) => sock.local_addr(), + Self::Uot { .. } => Ok("0.0.0.0:0".parse().unwrap()), + } + } +} diff --git a/ostp-client/src/transport/xhttp.rs b/ostp-client/src/transport/xhttp.rs new file mode 100644 index 0000000..3455d8b --- /dev/null +++ b/ostp-client/src/transport/xhttp.rs @@ -0,0 +1,175 @@ +use std::net::IpAddr; +use std::sync::Arc; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; +use bytes::{Bytes, BytesMut}; +use anyhow::{Result, Context}; +use tokio::sync::mpsc; +use rustls::pki_types::{ServerName, CertificateDer, UnixTime}; +use rustls::client::danger::{ServerCertVerifier, ServerCertVerified, HandshakeSignatureValid}; +use rustls::DigitallySignedStruct; +use sha2::{Sha256, Digest}; +use hmac::{Hmac, Mac}; +use base64::Engine; + +type HmacSha256 = Hmac; + +#[derive(Debug)] +struct NoAuthVerifier; + +impl ServerCertVerifier for NoAuthVerifier { + 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::ECDSA_NISTP256_SHA256, + rustls::SignatureScheme::ECDSA_NISTP384_SHA384, + rustls::SignatureScheme::ED25519, + rustls::SignatureScheme::RSA_PSS_SHA256, + ] + } +} + +pub async fn connect_xhttp( + target_ip: IpAddr, + port: u16, + sni: &str, + access_key: &[u8], +) -> Result<(mpsc::Sender, Arc>>)> { + let addr = std::net::SocketAddr::new(target_ip, port); + let tcp_stream = TcpStream::connect(addr).await + .with_context(|| format!("failed to connect to {}", addr))?; + tcp_stream.set_nodelay(true)?; + + // 1. Generate auth token + let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); + let mut mac = HmacSha256::new_from_slice(access_key).unwrap_or_else(|_| HmacSha256::new_from_slice(b"").unwrap()); + mac.update(×tamp.to_be_bytes()); + let sig = base64::prelude::BASE64_STANDARD.encode(mac.finalize().into_bytes()); + let auth_token = format!("{}:{}", timestamp, sig); + + let http_host = if sni.is_empty() { target_ip.to_string() } else { sni.to_string() }; + + let req = format!( + "GET /stream HTTP/1.1\r\n\ + Host: {}\r\n\ + Authorization: Bearer {}\r\n\ + Connection: keep-alive\r\n\ + \r\n", + http_host, auth_token + ); + + // 2. TLS wrapping (if port 443) + if port == 443 { + let mut config = rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoAuthVerifier)) + .with_no_client_auth(); + config.alpn_protocols.push(b"http/1.1".to_vec()); + let tls_connector = tokio_rustls::TlsConnector::from(Arc::new(config)); + + let server_name = ServerName::try_from(http_host.as_str()) + .unwrap_or_else(|_| ServerName::try_from("localhost").unwrap()) + .to_owned(); + + let mut tls_stream = tls_connector.connect(server_name, tcp_stream).await?; + + // HTTP Handshake + tls_stream.write_all(req.as_bytes()).await?; + tls_stream.flush().await?; + + let mut buf = [0u8; 1024]; + let n = tls_stream.read(&mut buf).await?; + let resp = String::from_utf8_lossy(&buf[..n]); + if !resp.contains("200 OK") { + anyhow::bail!("xHTTP handshake failed: expected 200 OK, got: {}", resp.lines().next().unwrap_or("")); + } + + // Split stream + let (rx, tx) = tokio::io::split(tls_stream); + start_uot_loops(rx, tx) + } else { + let mut tcp_stream = tcp_stream; + tcp_stream.write_all(req.as_bytes()).await?; + tcp_stream.flush().await?; + + let mut buf = [0u8; 1024]; + let n = tcp_stream.read(&mut buf).await?; + let resp = String::from_utf8_lossy(&buf[..n]); + if !resp.contains("200 OK") { + anyhow::bail!("xHTTP handshake failed: expected 200 OK, got: {}", resp.lines().next().unwrap_or("")); + } + + let (rx, tx) = tcp_stream.into_split(); + start_uot_loops(rx, tx) + } +} + +fn start_uot_loops( + mut net_rx: R, + mut net_tx: W +) -> Result<(mpsc::Sender, Arc>>)> +where + R: tokio::io::AsyncRead + Unpin + Send + 'static, + W: tokio::io::AsyncWrite + Unpin + Send + 'static, +{ + let (app_tx, bridge_rx) = mpsc::channel::(1024); + let (bridge_tx, app_rx) = mpsc::channel::(1024); + + // TX Loop (App -> UoT -> Network) + tokio::spawn(async move { + let mut rx = bridge_rx; + while let Some(frame) = rx.recv().await { + let len = frame.len() as u16; + if net_tx.write_u16(len).await.is_err() { break; } + if net_tx.write_all(&frame).await.is_err() { break; } + } + }); + + // RX Loop (Network -> UoT -> App) + tokio::spawn(async move { + loop { + let len = match net_rx.read_u16().await { + Ok(l) => l, + Err(_) => break, + }; + let mut buf = vec![0u8; len as usize]; + if net_rx.read_exact(&mut buf).await.is_err() { + break; + } + if app_tx.send(Bytes::from(buf)).await.is_err() { + break; + } + } + }); + + Ok((bridge_tx, Arc::new(tokio::sync::Mutex::new(app_rx)))) +} diff --git a/ostp-wiki b/ostp-wiki index 64efa67..fe1cb7f 160000 --- a/ostp-wiki +++ b/ostp-wiki @@ -1 +1 @@ -Subproject commit 64efa677aca2551b61f71f2168611d884619e5ca +Subproject commit fe1cb7f196d79d0d5175f776dbeb297dd3ffe49a diff --git a/ostp/src/main.rs b/ostp/src/main.rs index ccdfcbf..72c6525 100644 --- a/ostp/src/main.rs +++ b/ostp/src/main.rs @@ -498,6 +498,13 @@ async fn run_app() -> Result<()> { "access_key": "ostppassword" }}, + // Transport Mode: "udp" (default) or "uot" (xHTTP Stealth / UDP over TCP) + "transport": {{ + "mode": "udp", + "stealth_sni": "vk.com", + "stealth_port": 443 + }}, + "mux": {{ "enabled": false, "sessions": 1