feat: UoT and xHTTP stealth

This commit is contained in:
ospab 2026-05-21 02:11:02 +03:00
parent 9329bcef45
commit 83f7ff2119
11 changed files with 588 additions and 71 deletions

264
Cargo.lock generated
View File

@ -123,6 +123,28 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 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]] [[package]]
name = "axum" name = "axum"
version = "0.8.9" version = "0.8.9"
@ -224,6 +246,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver",
"libc",
"shlex", "shlex",
] ]
@ -327,6 +351,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.5" version = "1.0.5"
@ -426,6 +459,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.14" version = "0.3.14"
@ -457,6 +496,12 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.32" version = "0.3.32"
@ -472,6 +517,17 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" 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]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.32" version = "0.3.32"
@ -485,6 +541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-macro",
"futures-task", "futures-task",
"pin-project-lite", "pin-project-lite",
"slab", "slab",
@ -511,6 +568,18 @@ dependencies = [
"wasi", "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]] [[package]]
name = "ghash" name = "ghash"
version = "0.5.1" version = "0.5.1"
@ -808,6 +877,16 @@ dependencies = [
"syn", "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]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.98" version = "0.3.98"
@ -948,16 +1027,22 @@ name = "ostp-client"
version = "0.2.5" version = "0.2.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64",
"bytes", "bytes",
"chrono", "chrono",
"futures-util",
"hmac",
"json_comments", "json_comments",
"ostp-core", "ostp-core",
"portable-atomic", "portable-atomic",
"rand", "rand",
"rustls",
"serde", "serde",
"serde_json", "serde_json",
"sha2",
"socket2", "socket2",
"tokio", "tokio",
"tokio-rustls",
"tracing", "tracing",
] ]
@ -1101,6 +1186,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -1128,7 +1219,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.17",
] ]
[[package]] [[package]]
@ -1148,6 +1239,20 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" 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]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@ -1157,6 +1262,43 @@ dependencies = [
"semver", "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]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.22" version = "1.0.22"
@ -1436,6 +1578,16 @@ dependencies = [
"syn", "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]] [[package]]
name = "toml" name = "toml"
version = "0.5.11" version = "0.5.11"
@ -1571,6 +1723,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.8" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 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]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.121" version = "0.2.121"
@ -1742,7 +1909,16 @@ version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [ 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]] [[package]]
@ -1760,13 +1936,29 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm", "windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc", "windows_aarch64_msvc 0.42.2",
"windows_i686_gnu", "windows_i686_gnu 0.42.2",
"windows_i686_msvc", "windows_i686_msvc 0.42.2",
"windows_x86_64_gnu", "windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm", "windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc", "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]] [[package]]
@ -1775,42 +1967,90 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.42.2" version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.42.2" version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 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]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.42.2" version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.42.2" version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 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]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.42.2" version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 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]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.42.2" version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 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]] [[package]]
name = "winres" name = "winres"
version = "0.1.12" version = "0.1.12"
@ -1820,6 +2060,12 @@ dependencies = [
"toml", "toml",
] ]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.3" version = "0.6.3"

View File

@ -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. | | **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). | | **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. | | **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. | | **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). | | **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. | | **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", "server": "YOUR_SERVER_IP:50000",
"access_key": "YOUR_SECRET_KEY", "access_key": "YOUR_SECRET_KEY",
"socks5_bind": "127.0.0.1:1088", "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" } "tun": { "enable": false, "dns": "1.1.1.1" }
} }
``` ```

View File

@ -20,6 +20,7 @@ OSTP — высокопроизводительный транспортный
| **Мультиплексирование** | Несколько логических TCP-потоков поверх одной зашифрованной UDP-сессии с per-stream flow control. | | **Мультиплексирование** | Несколько логических TCP-потоков поверх одной зашифрованной UDP-сессии с per-stream flow control. |
| **Бесшовный роуминг** | Клиент может менять сети (WiFi ↔ 4G) без разрыва сессии — сервер отслеживает session-ID, а не IP-адрес. | | **Бесшовный роуминг** | Клиент может менять сети (WiFi ↔ 4G) без разрыва сессии — сервер отслеживает session-ID, а не IP-адрес. |
| **TUN-режим** | Полносистемный VPN через интеграцию с `tun2socks` на Windows и Linux. | | **TUN-режим** | Полносистемный VPN через интеграцию с `tun2socks` на Windows и Linux. |
| **xHTTP Стелс (UoT)** | Туннель UDP-over-TCP, замаскированный под обычный HTTP/1.1 или TLS трафик для обхода белых списков ТСПУ (DPI). |
| **TURN Relay** | RFC 5766 TURN для окружений, где прямой UDP заблокирован. | | **TURN Relay** | RFC 5766 TURN для окружений, где прямой UDP заблокирован. |
| **Hot-Reload** | Перезагрузка конфига в рантайме без перезапуска (ключи, исключения, mux, TURN). | | **Hot-Reload** | Перезагрузка конфига в рантайме без перезапуска (ключи, исключения, mux, TURN). |
| **Кросс-платформа** | Windows, Linux, macOS, Android. Один бинарник, без зависимостей. | | **Кросс-платформа** | Windows, Linux, macOS, Android. Один бинарник, без зависимостей. |
@ -107,6 +108,12 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie
"access_key": "ВАШ_КЛЮЧ", "access_key": "ВАШ_КЛЮЧ",
"socks5_bind": "127.0.0.1:1088", "socks5_bind": "127.0.0.1:1088",
"debug": false, "debug": false,
// Настройки транспорта (udp или uot)
"transport": {
"mode": "udp",
"stealth_sni": "vk.com",
"stealth_port": 443
},
// TUN-режим (полносистемный VPN) // TUN-режим (полносистемный VPN)
"tun": { "tun": {
"enable": false, "enable": false,

View File

@ -17,3 +17,9 @@ json_comments = "0.2"
portable-atomic.workspace = true portable-atomic.workspace = true
chrono = "0.4" chrono = "0.4"
socket2 = "0.6.3" 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"

View File

@ -38,20 +38,20 @@ pub struct BridgeMetrics {
pub connection_state: AtomicU8, pub connection_state: AtomicU8,
} }
async fn send_datagram(socket: &UdpSocket, frame: &Bytes, turn_enabled: bool) -> std::io::Result<usize> { async fn send_datagram(socket: &crate::transport::Transport, frame: &Bytes, turn_enabled: bool) -> std::io::Result<usize> {
if turn_enabled { if turn_enabled {
let mut out = bytes::BytesMut::with_capacity(4 + frame.len()); let mut out = bytes::BytesMut::with_capacity(4 + frame.len());
bytes::BufMut::put_u16(&mut out, 0x4000); bytes::BufMut::put_u16(&mut out, 0x4000);
bytes::BufMut::put_u16(&mut out, frame.len() as u16); bytes::BufMut::put_u16(&mut out, frame.len() as u16);
out.extend_from_slice(frame); out.extend_from_slice(frame);
socket.send(&out).await socket.send(&out.freeze()).await
} else { } else {
socket.send(frame).await socket.send(frame).await
} }
} }
struct SessionState { struct SessionState {
socket: Arc<UdpSocket>, socket: crate::transport::Transport,
machine: ProtocolMachine, machine: ProtocolMachine,
} }
@ -74,6 +74,10 @@ pub struct Bridge {
pub mux_enabled: bool, pub mux_enabled: bool,
pub mux_sessions: usize, pub mux_sessions: usize,
pub transport_mode: String,
pub stealth_sni: String,
pub stealth_port: u16,
metrics: Arc<BridgeMetrics>, metrics: Arc<BridgeMetrics>,
sample_sent: u64, sample_sent: u64,
sample_recv: u64, sample_recv: u64,
@ -103,6 +107,10 @@ impl Bridge {
mux_enabled: config.multiplex.enabled, mux_enabled: config.multiplex.enabled,
mux_sessions: config.multiplex.sessions.max(1), 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, metrics,
sample_sent: 0, sample_sent: 0,
sample_recv: 0, sample_recv: 0,
@ -261,8 +269,7 @@ impl Bridge {
match self.perform_handshake_with_id(&tx, session_id).await { match self.perform_handshake_with_id(&tx, session_id).await {
Ok((sock, mach, rtt)) => { Ok((sock, mach, rtt)) => {
let session_index = sessions.len(); let session_index = sessions.len();
let socket = Arc::new(sock); let socket_clone = sock.clone();
let socket_clone = socket.clone();
let udp_tx_clone = udp_tx.clone(); let udp_tx_clone = udp_tx.clone();
let is_turn = self.turn_enabled; let is_turn = self.turn_enabled;
tokio::spawn(async move { 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; rtt_sum += rtt;
successful_sessions += 1; successful_sessions += 1;
} }
@ -357,8 +364,7 @@ impl Bridge {
match self.perform_handshake_with_id(&tx, session_id).await { match self.perform_handshake_with_id(&tx, session_id).await {
Ok((sock, mach, rtt)) => { Ok((sock, mach, rtt)) => {
let session_index = new_sessions.len(); let session_index = new_sessions.len();
let socket = Arc::new(sock); let socket_clone = sock.clone();
let socket_clone = socket.clone();
let udp_tx_clone = udp_tx.clone(); let udp_tx_clone = udp_tx.clone();
let is_turn = self.turn_enabled; let is_turn = self.turn_enabled;
tokio::spawn(async move { 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; rtt_sum += rtt;
successful_sessions += 1; successful_sessions += 1;
} }
@ -470,8 +476,7 @@ impl Bridge {
match self.perform_handshake_with_id(&tx, session_id).await { match self.perform_handshake_with_id(&tx, session_id).await {
Ok((sock, mach, rtt)) => { Ok((sock, mach, rtt)) => {
let session_index = new_sessions.len(); let session_index = new_sessions.len();
let socket = Arc::new(sock); let socket_clone = sock.clone();
let socket_clone = socket.clone();
let udp_tx_clone = udp_tx.clone(); let udp_tx_clone = udp_tx.clone();
let is_turn = self.turn_enabled; let is_turn = self.turn_enabled;
tokio::spawn(async move { 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; rtt_sum += rtt;
successful_sessions += 1; successful_sessions += 1;
} }
@ -750,7 +755,7 @@ impl Bridge {
&mut self, &mut self,
tx: &mpsc::Sender<UiEvent>, tx: &mpsc::Sender<UiEvent>,
session_id: u32, session_id: u32,
) -> Result<(UdpSocket, ProtocolMachine, f64)> { ) -> Result<(crate::transport::Transport, ProtocolMachine, f64)> {
let timestamp = std::time::SystemTime::now() let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default() .unwrap_or_default()
@ -791,13 +796,13 @@ impl Bridge {
tx.send(UiEvent::Log(format!("Connecting to remote server: {}...", target_addr))).await.ok(); 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, Ok(sock) => sock,
Err(e) => { Err(e) => {
if let std::net::IpAddr::V4(ipv4) = target_ip { if let std::net::IpAddr::V4(ipv4) = target_ip {
tx.send(UiEvent::Log(format!("Direct IPv4 connection failed: {}. Trying NAT64 fallback...", e))).await.ok(); tx.send(UiEvent::Log(format!("Direct IPv4 connection failed: {}. Trying NAT64 fallback...", e))).await.ok();
let nat64_ipv6 = synthesize_nat64(ipv4); 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, Ok(sock) => sock,
Err(fallback_err) => { Err(fallback_err) => {
return Err(anyhow::anyhow!("Direct IPv4 failed: {}. NAT64 fallback failed: {}", e, 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(':') { let turn_addr = if self.turn_server.contains(':') {
self.turn_server.clone() self.turn_server.clone()
} else { } else {
@ -817,7 +826,7 @@ impl Bridge {
}; };
tx.send(UiEvent::Log(format!("Allocating TURN relay via {}", turn_addr))).await.ok(); 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) => { Ok(relay_addr) => {
tx.send(UiEvent::Log(format!("TURN relay allocated ({})", relay_addr))).await.ok(); tx.send(UiEvent::Log(format!("TURN relay allocated ({})", relay_addr))).await.ok();
@ -826,7 +835,7 @@ impl Bridge {
.collect(); .collect();
let turn_target = resolved_turn.first().ok_or_else(|| anyhow::anyhow!("no IP resolved for TURN {}", turn_addr))?; 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() { if let std::net::IpAddr::V4(ipv4) = turn_target.ip() {
std::net::IpAddr::V6(synthesize_nat64(ipv4)) std::net::IpAddr::V6(synthesize_nat64(ipv4))
} else { } else {
@ -837,14 +846,14 @@ impl Bridge {
}; };
let connect_addr = std::net::SocketAddr::new(connect_ip, turn_target.port()); let connect_addr = std::net::SocketAddr::new(connect_ip, turn_target.port());
socket udp_socket
.connect(connect_addr) .connect(connect_addr)
.await .await
.with_context(|| format!("failed to re-connect to TURN {}", connect_addr))?; .with_context(|| format!("failed to re-connect to TURN {}", connect_addr))?;
} }
Err(e) => { Err(e) => {
tx.send(UiEvent::Log(format!("TURN allocation failed: {}. Using direct UDP.", e))).await.ok(); 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 { if let std::net::IpAddr::V4(ipv4) = target_ip {
std::net::IpAddr::V6(synthesize_nat64(ipv4)) std::net::IpAddr::V6(synthesize_nat64(ipv4))
} else { } else {
@ -854,7 +863,7 @@ impl Bridge {
target_ip target_ip
}; };
let connect_addr = std::net::SocketAddr::new(connect_ip, port); let connect_addr = std::net::SocketAddr::new(connect_ip, port);
socket udp_socket
.connect(connect_addr) .connect(connect_addr)
.await .await
.with_context(|| format!("failed to connect udp to {}", connect_addr))?; .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 { 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(); tx.send(UiEvent::Log("Direct IPv4 handshake timed out. Trying NAT64 fallback...".to_string())).await.ok();
let nat64_ipv6 = synthesize_nat64(ipv4); 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) => { Ok(fallback_socket) => {
let mut fallback_success = false; let mut fallback_success = false;
for attempt in 0..4 { for attempt in 0..4 {
@ -963,39 +972,49 @@ impl Bridge {
self.turn_password = cfg.turn.access_key.clone(); self.turn_password = cfg.turn.access_key.clone();
self.mux_enabled = cfg.multiplex.enabled; self.mux_enabled = cfg.multiplex.enabled;
self.mux_sessions = cfg.multiplex.sessions.max(1); 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, &self,
target_ip: std::net::IpAddr, target_ip: std::net::IpAddr,
port: u16, port: u16,
) -> Result<UdpSocket> { ) -> Result<crate::transport::Transport> {
let is_ipv6 = target_ip.is_ipv6(); if self.transport_mode == "uot" {
let domain = if is_ipv6 { socket2::Domain::IPV6 } else { socket2::Domain::IPV4 }; let (tx, rx) = crate::transport::xhttp::connect_xhttp(
let bind_addr = if is_ipv6 { target_ip, self.stealth_port, &self.stealth_sni, &self.access_key
std::net::SocketAddr::new(std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED), 0) ).await?;
Ok(crate::transport::Transport::Uot { tx, rx })
} else { } 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))?; let sock = socket2::Socket::new(domain, socket2::Type::DGRAM, Some(socket2::Protocol::UDP))?;
#[cfg(unix)] #[cfg(unix)]
{ {
use std::os::unix::io::AsRawFd; use std::os::unix::io::AsRawFd;
protect_socket(sock.as_raw_fd()); 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)
} }
} }

View File

@ -55,33 +55,29 @@ pub struct LocalProxyConfig {
} }
/// Transport layer configuration. /// 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransportConfig { pub struct TransportConfig {
/// "udp" or "wss" /// "udp" or "uot"
#[serde(default = "default_transport_mode")] #[serde(default = "default_transport_mode")]
pub mode: String, pub mode: String,
/// WebSocket host (domain for TLS connect and HTTP Host header) /// TLS SNI and HTTP Host for stealth routing
#[serde(default)] #[serde(default)]
pub wss_host: String, pub stealth_sni: String,
/// WebSocket HTTP path, e.g. "/ostp" /// TCP Port for the stealth connection
#[serde(default = "default_wss_path")] #[serde(default = "default_stealth_port")]
pub wss_path: String, pub stealth_port: u16,
/// TLS SNI override; defaults to wss_host if empty
#[serde(default)]
pub wss_sni: String,
} }
fn default_transport_mode() -> String { "udp".to_string() } 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 { impl Default for TransportConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
mode: default_transport_mode(), mode: default_transport_mode(),
wss_host: String::new(), stealth_sni: String::new(),
wss_path: default_wss_path(), stealth_port: default_stealth_port(),
wss_sni: String::new(),
} }
} }
} }

View File

@ -3,6 +3,8 @@ pub mod bridge;
pub mod config; pub mod config;
pub mod signal; pub mod signal;
pub mod sysproxy; pub mod sysproxy;
pub mod transport;
pub mod tunnel; pub mod tunnel;
pub mod turn; pub mod turn;
pub mod runner; pub mod runner;

View File

@ -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<UdpSocket>),
Uot {
tx: tokio::sync::mpsc::Sender<Bytes>,
rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Bytes>>>,
}
}
impl Transport {
pub async fn send(&self, frame: &Bytes) -> std::io::Result<usize> {
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<usize> {
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<usize> {
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<std::net::SocketAddr> {
match self {
Self::Udp(sock) => sock.local_addr(),
Self::Uot { .. } => Ok("0.0.0.0:0".parse().unwrap()),
}
}
}

View File

@ -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<Sha256>;
#[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<ServerCertVerified, rustls::Error> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
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<Bytes>, Arc<tokio::sync::Mutex<mpsc::Receiver<Bytes>>>)> {
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(&timestamp.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<R, W>(
mut net_rx: R,
mut net_tx: W
) -> Result<(mpsc::Sender<Bytes>, Arc<tokio::sync::Mutex<mpsc::Receiver<Bytes>>>)>
where
R: tokio::io::AsyncRead + Unpin + Send + 'static,
W: tokio::io::AsyncWrite + Unpin + Send + 'static,
{
let (app_tx, bridge_rx) = mpsc::channel::<Bytes>(1024);
let (bridge_tx, app_rx) = mpsc::channel::<Bytes>(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))))
}

@ -1 +1 @@
Subproject commit 64efa677aca2551b61f71f2168611d884619e5ca Subproject commit fe1cb7f196d79d0d5175f776dbeb297dd3ffe49a

View File

@ -498,6 +498,13 @@ async fn run_app() -> Result<()> {
"access_key": "ostppassword" "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": {{ "mux": {{
"enabled": false, "enabled": false,
"sessions": 1 "sessions": 1