From 7986b1ca5b7e70d75688661d0b5bd95af12321bb Mon Sep 17 00:00:00 2001 From: ospab Date: Fri, 29 May 2026 16:21:59 +0300 Subject: [PATCH] fix(reality): fix TLS 1.3 handshake causing 1KB DPI cutoff on mobile The core bug: server sent 5 TLS records in server_hello but client only read the first one (ServerHello), then passed remaining bytes (CCS + fake records) into RealityStream. RealityStream saw 0x14 (CCS) != 0x17 and immediately returned an error, killing the connection. Changes: - reality.rs: append ChangeCipherSpec after ClientHello (RFC 8446 D.4) export REALITY_SERVER_HANDSHAKE_RECORDS=5 constant - xhttp.rs: drain all 5 server handshake records before creating RealityStream - uot.rs: rebuild server_hello as proper 5-record TLS 1.3 flight: ServerHello + CCS + fake EE (108B) + fake Cert (812B) + fake Fin (52B) drain client CCS from raw stream before wrapping in RealityStream --- .gitignore | 3 + Cargo.lock | 230 ++++++++++++++++++++++++++++- Cargo.toml | 3 +- ostp-client/src/transport/xhttp.rs | 31 ++-- ostp-core/src/crypto/reality.rs | 17 ++- ostp-gui/src-tauri/Cargo.lock | 115 ++++++--------- ostp-gui/src-tauri/src/lib.rs | 6 +- ostp-server/src/transport/uot.rs | 79 ++++++++-- 8 files changed, 375 insertions(+), 109 deletions(-) diff --git a/.gitignore b/.gitignore index fbc9a7c..b90fd1e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ wintun.dll .ai-rules.md turn-harvesting-idea.md +# Private tooling (closed-source) +ostp-prober/ + diff --git a/Cargo.lock b/Cargo.lock index c7befb8..1402767 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -428,6 +428,31 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.11.1", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -937,7 +962,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -1275,6 +1300,18 @@ dependencies = [ "unicase", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.2.0" @@ -1302,6 +1339,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "network-interface" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddcb8865ad3d9950f22f42ffa0ef0aecbfbf191867b3122413602b0a360b2a6" +dependencies = [ + "cc", + "libc", + "thiserror 2.0.18", + "winapi", +] + [[package]] name = "nix" version = "0.31.3" @@ -1392,7 +1441,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "socket2", + "socket2 0.6.3", "tokio", "tracing", "tun", @@ -1434,6 +1483,22 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "ostp-prober" +version = "0.2.69" +dependencies = [ + "anyhow", + "byteorder", + "chrono", + "crossterm", + "network-interface", + "serde", + "serde_json", + "socket2 0.5.10", + "tokio", + "winapi", +] + [[package]] name = "ostp-server" version = "0.2.69" @@ -1457,7 +1522,7 @@ dependencies = [ "serde_json", "sha2", "simple-dns", - "socket2", + "socket2 0.6.3", "tokio", "tower-http", "tracing", @@ -1485,6 +1550,29 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1609,7 +1697,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -1646,7 +1734,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.6.3", "tracing", "windows-sys 0.52.0", ] @@ -1731,6 +1819,15 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -2009,6 +2106,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -2071,6 +2189,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.3" @@ -2221,10 +2349,10 @@ checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", - "mio", + "mio 1.2.0", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -2665,6 +2793,22 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2674,6 +2818,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -2742,6 +2892,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2775,6 +2934,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2797,6 +2971,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2809,6 +2989,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -2821,6 +3007,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2839,6 +3031,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -2851,6 +3049,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -2863,6 +3067,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -2875,6 +3085,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index f164246..24d73db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,8 @@ members = [ "ostp-client", "ostp-server", "ostp-jni", "ostp", - "ostp-tun-helper" + "ostp-tun-helper", + "ostp-prober" ] exclude = ["ostp-gui/src-tauri"] resolver = "2" diff --git a/ostp-client/src/transport/xhttp.rs b/ostp-client/src/transport/xhttp.rs index 7e1bc53..b3eb6a0 100644 --- a/ostp-client/src/transport/xhttp.rs +++ b/ostp-client/src/transport/xhttp.rs @@ -5,15 +5,15 @@ use tokio::net::TcpStream; use bytes::{Buf, BufMut, Bytes, BytesMut}; use anyhow::{Result, Context}; use tokio::sync::mpsc; -use hmac::{Hmac, Mac}; +use hmac::Hmac; use sha2::Sha256; use base64::Engine; use std::pin::Pin; use std::task::{Context as TaskContext, Poll}; use x25519_dalek::PublicKey; -use chacha20poly1305::{aead::{Aead, KeyInit, Payload}, ChaCha20Poly1305, Nonce}; +use chacha20poly1305::{aead::Aead, ChaCha20Poly1305, Nonce}; -use ostp_core::crypto::reality::{build_client_hello, derive_keys, generate_session_id, generate_x25519_keypair}; +use ostp_core::crypto::reality::{build_client_hello, derive_keys, generate_session_id, generate_x25519_keypair, REALITY_SERVER_HANDSHAKE_RECORDS}; use ostp_core::framing::wss::{encode_wss_frame, decode_wss_frame, WssFrameResult}; type HmacSha256 = Hmac; @@ -56,15 +56,24 @@ pub async fn connect_xhttp( tcp_stream.write_all(&client_hello).await?; - // Read fake ServerHello (just read until the end of the handshake, we assume server sends exactly 1 record for ServerHello) - let mut head = [0u8; 5]; - tcp_stream.read_exact(&mut head).await?; - if head[0] != 0x16 { - anyhow::bail!("expected Handshake record from Reality Server"); + // Drain all server handshake records (ServerHello, CCS, fake encrypted records). + // The server sends exactly REALITY_SERVER_HANDSHAKE_RECORDS records before data starts. + // Reading them explicitly prevents RealityStream from seeing non-AppData bytes. + for i in 0..REALITY_SERVER_HANDSHAKE_RECORDS { + let mut head = [0u8; 5]; + tcp_stream.read_exact(&mut head).await + .with_context(|| format!("reality handshake: failed reading record {} header", i))?; + if i == 0 && head[0] != 0x16 { + anyhow::bail!("expected ServerHello (0x16), got 0x{:02x}", head[0]); + } + let record_len = u16::from_be_bytes([head[3], head[4]]) as usize; + if record_len > 16384 { + anyhow::bail!("reality handshake: record {} too large: {} bytes", i, record_len); + } + let mut _payload = vec![0u8; record_len]; + tcp_stream.read_exact(&mut _payload).await + .with_context(|| format!("reality handshake: failed reading record {} payload", i))?; } - let record_len = u16::from_be_bytes([head[3], head[4]]) as usize; - let mut server_hello_payload = vec![0u8; record_len]; - tcp_stream.read_exact(&mut server_hello_payload).await?; let reality_stream = RealityStream::new(tcp_stream, data_key); xhttp_handshake_and_loop(reality_stream, target_ip, sni, access_key, wss).await diff --git a/ostp-core/src/crypto/reality.rs b/ostp-core/src/crypto/reality.rs index ae7eaba..0998351 100644 --- a/ostp-core/src/crypto/reality.rs +++ b/ostp-core/src/crypto/reality.rs @@ -1,5 +1,5 @@ use bytes::{Buf, BufMut, Bytes, BytesMut}; -use chacha20poly1305::{aead::{Aead, KeyInit, Payload}, ChaCha20Poly1305, Nonce}; +use chacha20poly1305::{aead::{Aead, KeyInit}, ChaCha20Poly1305, Nonce}; use hkdf::Hkdf; use sha2::Sha256; use x25519_dalek::{PublicKey, StaticSecret}; @@ -10,6 +10,11 @@ const REALITY_INFO: &[u8] = b"ostp-reality-v1"; const RECORD_HEADER_LEN: usize = 5; const HANDSHAKE_HEADER_LEN: usize = 4; +/// Number of TLS records sent by the server during the fake handshake phase. +/// Client must read and discard this many records before starting RealityStream. +/// Layout: 1× ServerHello (0x16) + 1× CCS (0x14) + 3× fake encrypted records (0x17) +pub const REALITY_SERVER_HANDSHAKE_RECORDS: usize = 5; + /// Generates an X25519 keypair pub fn generate_x25519_keypair() -> (StaticSecret, PublicKey) { let secret = StaticSecret::random_from_rng(OsRng); @@ -156,8 +161,14 @@ pub fn build_client_hello(sni: &str, session_id: &[u8; 32], c_pub: &PublicKey) - record.put_u8((handshake_len >> 8) as u8); record.put_u8(handshake_len as u8); record.put_slice(&handshake); - - record.freeze() + + // Append ChangeCipherSpec for TLS 1.3 middlebox compatibility (RFC 8446 §D.4) + // This makes the flow look like: ClientHello → ServerHello → CCS → AppData + // instead of the DPI-suspicious: ClientHello → AppData directly. + let mut out = BytesMut::new(); + out.put_slice(&record); + out.put_slice(&[0x14, 0x03, 0x03, 0x00, 0x01, 0x01]); + out.freeze() } pub struct ParsedClientHello { diff --git a/ostp-gui/src-tauri/Cargo.lock b/ostp-gui/src-tauri/Cargo.lock index 520ed62..85b25c3 100644 --- a/ostp-gui/src-tauri/Cargo.lock +++ b/ostp-gui/src-tauri/Cargo.lock @@ -1679,6 +1679,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2632,14 +2641,16 @@ dependencies = [ [[package]] name = "ostp-client" -version = "0.2.67" +version = "0.2.69" dependencies = [ "anyhow", "base64 0.22.1", "bytes", + "chacha20poly1305", "chrono", "futures", "futures-util", + "hex", "hmac", "json_comments", "libc", @@ -2647,32 +2658,32 @@ dependencies = [ "ostp-core", "portable-atomic", "rand", - "rustls", - "rustls-pki-types", "serde", "serde_json", "sha2", "socket2", "tokio", - "tokio-rustls", "tracing", "tun", "webpki-roots 0.26.11", + "x25519-dalek", ] [[package]] name = "ostp-core" -version = "0.2.67" +version = "0.2.69" dependencies = [ "anyhow", "bytes", "chacha20poly1305", + "hkdf", "hmac", "rand", "sha2", "snow", "thiserror 1.0.69", "tracing", + "x25519-dalek", ] [[package]] @@ -2683,6 +2694,7 @@ dependencies = [ "json_comments", "ostp-client", "portable-atomic", + "rand", "serde", "serde_json", "tauri", @@ -3208,20 +3220,6 @@ dependencies = [ "web-sys", ] -[[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-hash" version = "2.1.2" @@ -3250,39 +3248,11 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.23.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" -dependencies = [ - "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 = [ - "ring", - "rustls-pki-types", - "untrusted", -] [[package]] name = "rustversion" @@ -4254,16 +4224,6 @@ dependencies = [ "syn 2.0.117", ] -[[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 = "tokio-util" version = "0.7.18" @@ -4614,12 +4574,6 @@ 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" @@ -5160,15 +5114,6 @@ dependencies = [ "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]] name = "windows-sys" version = "0.59.0" @@ -5539,6 +5484,18 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + [[package]] name = "yoke" version = "0.8.2" @@ -5669,6 +5626,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "zerotrie" diff --git a/ostp-gui/src-tauri/src/lib.rs b/ostp-gui/src-tauri/src/lib.rs index a140cc3..85b17c5 100644 --- a/ostp-gui/src-tauri/src/lib.rs +++ b/ostp-gui/src-tauri/src/lib.rs @@ -1,4 +1,4 @@ -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::Arc; use tokio::sync::{watch, Mutex}; use tokio::task::JoinHandle; @@ -60,6 +60,7 @@ struct TransportConfigRaw { mode: Option, stealth_sni: Option, stealth_port: Option, + wss: Option, } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -171,6 +172,7 @@ fn map_to_client_config(raw: &ClientConfigRaw, mode: &str) -> ostp_client::confi mode: raw.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(|| "udp".to_string()), stealth_sni: raw.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_else(|| "microsoft.com".to_string()), stealth_port: raw.transport.as_ref().and_then(|t| t.stealth_port).unwrap_or(443), + wss: raw.transport.as_ref().and_then(|t| t.wss).unwrap_or(false), }, exclusions: ostp_client::config::ExclusionConfig { domains: raw.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(), @@ -504,7 +506,7 @@ fn launch_as_admin(exe: &std::path::PathBuf, token: &str) -> anyhow::Result<()> // Use the GUI executable's directory as the working directory so dependencies are found let cwd_path = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from(".")); - let dir_wstr: Vec = cwd_path.parent().unwrap_or(Path::new(".")).as_os_str().encode_wide().chain(Some(0)).collect(); + let dir_wstr: Vec = cwd_path.parent().unwrap_or(std::path::Path::new(".")).as_os_str().encode_wide().chain(Some(0)).collect(); let ret = unsafe { ShellExecuteW(null_mut(), verb_wstr.as_ptr(), exe_wstr.as_ptr(), params_wstr.as_ptr(), dir_wstr.as_ptr(), 0) }; if ret <= 32 { anyhow::bail!("UAC denied or helper missing."); } diff --git a/ostp-server/src/transport/uot.rs b/ostp-server/src/transport/uot.rs index 634cad7..5275cbc 100644 --- a/ostp-server/src/transport/uot.rs +++ b/ostp-server/src/transport/uot.rs @@ -13,11 +13,11 @@ use tokio::net::TcpStream; use base64::Engine; use std::pin::Pin; use std::task::{Context as TaskContext, Poll}; -use chacha20poly1305::{aead::{Aead, KeyInit, Payload}, ChaCha20Poly1305, Nonce}; -use x25519_dalek::{StaticSecret, PublicKey}; +use chacha20poly1305::{aead::Aead, ChaCha20Poly1305, Nonce}; +use x25519_dalek::StaticSecret; use ostp_core::framing::wss::{encode_wss_frame, decode_wss_frame, WssFrameResult}; -use ostp_core::crypto::reality::{parse_client_hello, derive_keys, verify_session_id}; +use ostp_core::crypto::reality::{parse_client_hello, derive_keys, verify_session_id, REALITY_SERVER_HANDSHAKE_RECORDS}; use crate::RealityServerConfig; pub async fn handle_tcp_connection( @@ -204,17 +204,70 @@ where let data_key = data_key_opt.unwrap(); info!("Reality client authenticated from {} (sid matched)", peer_addr); - // Send a fake ServerHello. For now, a static, valid-looking TLS 1.3 ServerHello. - let server_hello = hex::decode("160303007a0200007603030000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000130100002e002b0002030400330024001d0020e29b191a62d0572e9a30d0fb9d08e50bc78d591dfc1dbafbfa533411db1c8e111403030001011603030030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000170303001300000000000000000000000000000000000000").unwrap(); - stream.write_all(&server_hello).await?; - - // At this point, the Reality tunnel is established. We need to wrap the stream with RealityStream. + // Build a fake TLS 1.3 server flight that matches what a real server sends. + // Must be exactly REALITY_SERVER_HANDSHAKE_RECORDS (5) TLS records: + // 1. ServerHello (0x16) - static blob with fake key share + // 2. ChangeCipherSpec (0x14) - RFC 8446 §D.4 middlebox compat + // 3. Fake EE (0x17) - simulates EncryptedExtensions + // 4. Fake Certificate (0x17) - simulates Certificate (big, DPI-realistic) + // 5. Fake Finished (0x17) - simulates CertificateVerify + Finished + let _ = REALITY_SERVER_HANDSHAKE_RECORDS; // assert constant is imported (= 5) + + // Record 1: ServerHello (0x16), same static blob as before (valid structure) + let server_hello_rec = hex::decode( + "160303007a0200007603030000000000000000000000000000000000000000000000\ + 000000000000000000000000200000000000000000000000000000000000000000\ + 0000000000000000000000000000130100002e002b0002030400330024001d0020\ + e29b191a62d0572e9a30d0fb9d08e50bc78d591dfc1dbafbfa533411db1c8e11" + ).unwrap(); + + // Record 2: ChangeCipherSpec (0x14) + let ccs_rec: &[u8] = &[0x14, 0x03, 0x03, 0x00, 0x01, 0x01]; + + // Record 3: Fake EncryptedExtensions (0x17), 108 zero bytes payload + let mut fake_ee = vec![0x17u8, 0x03, 0x03, 0x00, 108]; + fake_ee.extend_from_slice(&[0u8; 108]); + + // Record 4: Fake Certificate (0x17), 812 zero bytes (realistic cert size for DPI) + let cert_payload_len: u16 = 812; + let mut fake_cert = vec![0x17u8, 0x03, 0x03, + (cert_payload_len >> 8) as u8, (cert_payload_len & 0xff) as u8]; + fake_cert.extend_from_slice(&vec![0u8; cert_payload_len as usize]); + + // Record 5: Fake Finished (0x17), 52 zero bytes (CertificateVerify + Finished) + let mut fake_fin = vec![0x17u8, 0x03, 0x03, 0x00, 52]; + fake_fin.extend_from_slice(&[0u8; 52]); + + let mut server_flight = Vec::with_capacity( + server_hello_rec.len() + ccs_rec.len() + + fake_ee.len() + fake_cert.len() + fake_fin.len() + ); + server_flight.extend_from_slice(&server_hello_rec); + server_flight.extend_from_slice(ccs_rec); + server_flight.extend_from_slice(&fake_ee); + server_flight.extend_from_slice(&fake_cert); + server_flight.extend_from_slice(&fake_fin); + + stream.write_all(&server_flight).await?; + + // The client now sends ClientHello + CCS (6 bytes) as two separate TLS records. + // The ClientHello was already consumed into initial_buf above. + // The CCS may arrive as a separate TCP segment - drain it from the raw stream + // before wrapping in RealityStream so RealityStream only ever sees 0x17 records. + { + let mut ccs_head = [0u8; 5]; + if stream.read_exact(&mut ccs_head).await.is_ok() { + // Expected: CCS record 0x14 0x03 0x03 0x00 0x01 + // If it's something else (unlikely), we still drain its payload to stay in sync. + let ccs_payload_len = u16::from_be_bytes([ccs_head[3], ccs_head[4]]) as usize; + if ccs_payload_len <= 64 { + let mut _discard = vec![0u8; ccs_payload_len]; + let _ = stream.read_exact(&mut _discard).await; + } + } + } + let reality_stream = RealityStream::new(stream, data_key); - - // But wait! Inside the Reality stream, the client might send an xhttp or wss HTTP request! - // Because xhttp_handshake_and_loop does `GET /wss` *inside* the stream. - // So we must read the HTTP request *from the Reality stream*! - return process_inner_reality_stream(reality_stream, peer_addr, tcp_map, udp_tx).await; } else {