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"
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"

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. |
| **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" }
}
```

View File

@ -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,

View File

@ -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"

View File

@ -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<usize> {
async fn send_datagram(socket: &crate::transport::Transport, frame: &Bytes, turn_enabled: bool) -> std::io::Result<usize> {
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<UdpSocket>,
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<BridgeMetrics>,
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<UiEvent>,
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,13 +972,22 @@ 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<UdpSocket> {
) -> Result<crate::transport::Transport> {
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 {
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 {
@ -995,7 +1013,8 @@ impl Bridge {
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)
Ok(crate::transport::Transport::Udp(Arc::new(socket)))
}
}
}

View File

@ -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(),
}
}
}

View File

@ -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;

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"
}},
// 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