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
This commit is contained in:
ospab 2026-05-29 16:21:59 +03:00
parent fe32703514
commit ec333470aa
8 changed files with 375 additions and 109 deletions

3
.gitignore vendored
View File

@ -32,3 +32,6 @@ wintun.dll
.ai-rules.md .ai-rules.md
turn-harvesting-idea.md turn-harvesting-idea.md
# Private tooling (closed-source)
ostp-prober/

230
Cargo.lock generated
View File

@ -428,6 +428,31 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 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]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.7" version = "0.1.7"
@ -937,7 +962,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2 0.6.3",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@ -1275,6 +1300,18 @@ dependencies = [
"unicase", "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]] [[package]]
name = "mio" name = "mio"
version = "1.2.0" version = "1.2.0"
@ -1302,6 +1339,18 @@ dependencies = [
"tracing", "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]] [[package]]
name = "nix" name = "nix"
version = "0.31.3" version = "0.31.3"
@ -1392,7 +1441,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"socket2", "socket2 0.6.3",
"tokio", "tokio",
"tracing", "tracing",
"tun", "tun",
@ -1434,6 +1483,22 @@ dependencies = [
"tracing-subscriber", "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]] [[package]]
name = "ostp-server" name = "ostp-server"
version = "0.2.69" version = "0.2.69"
@ -1457,7 +1522,7 @@ dependencies = [
"serde_json", "serde_json",
"sha2", "sha2",
"simple-dns", "simple-dns",
"socket2", "socket2 0.6.3",
"tokio", "tokio",
"tower-http", "tower-http",
"tracing", "tracing",
@ -1485,6 +1550,29 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@ -1609,7 +1697,7 @@ dependencies = [
"quinn-udp", "quinn-udp",
"rustc-hash", "rustc-hash",
"rustls", "rustls",
"socket2", "socket2 0.6.3",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tracing", "tracing",
@ -1646,7 +1734,7 @@ dependencies = [
"cfg_aliases", "cfg_aliases",
"libc", "libc",
"once_cell", "once_cell",
"socket2", "socket2 0.6.3",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -1731,6 +1819,15 @@ dependencies = [
"getrandom 0.3.4", "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]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.14" version = "0.4.14"
@ -2009,6 +2106,27 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 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]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.8" version = "1.4.8"
@ -2071,6 +2189,16 @@ dependencies = [
"subtle", "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]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.3" version = "0.6.3"
@ -2221,10 +2349,10 @@ checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio", "mio 1.2.0",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2 0.6.3",
"tokio-macros", "tokio-macros",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@ -2665,6 +2793,22 @@ dependencies = [
"rustls-pki-types", "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]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.11" version = "0.1.11"
@ -2674,6 +2818,12 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.62.2" version = "0.62.2"
@ -2742,6 +2892,15 @@ dependencies = [
"windows-targets 0.42.2", "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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@ -2775,6 +2934,21 @@ dependencies = [
"windows_x86_64_msvc 0.42.2", "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]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@ -2797,6 +2971,12 @@ 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.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.6" version = "0.52.6"
@ -2809,6 +2989,12 @@ 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.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.6" version = "0.52.6"
@ -2821,6 +3007,12 @@ 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.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.6" version = "0.52.6"
@ -2839,6 +3031,12 @@ 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.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.6" version = "0.52.6"
@ -2851,6 +3049,12 @@ 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.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.6" version = "0.52.6"
@ -2863,6 +3067,12 @@ 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.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.6" version = "0.52.6"
@ -2875,6 +3085,12 @@ 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.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"

View File

@ -4,7 +4,8 @@ members = [
"ostp-client", "ostp-client",
"ostp-server", "ostp-server",
"ostp-jni", "ostp", "ostp-jni", "ostp",
"ostp-tun-helper" "ostp-tun-helper",
"ostp-prober"
] ]
exclude = ["ostp-gui/src-tauri"] exclude = ["ostp-gui/src-tauri"]
resolver = "2" resolver = "2"

View File

@ -5,15 +5,15 @@ use tokio::net::TcpStream;
use bytes::{Buf, BufMut, Bytes, BytesMut}; use bytes::{Buf, BufMut, Bytes, BytesMut};
use anyhow::{Result, Context}; use anyhow::{Result, Context};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use hmac::{Hmac, Mac}; use hmac::Hmac;
use sha2::Sha256; use sha2::Sha256;
use base64::Engine; use base64::Engine;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context as TaskContext, Poll}; use std::task::{Context as TaskContext, Poll};
use x25519_dalek::PublicKey; 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}; use ostp_core::framing::wss::{encode_wss_frame, decode_wss_frame, WssFrameResult};
type HmacSha256 = Hmac<Sha256>; type HmacSha256 = Hmac<Sha256>;
@ -56,15 +56,24 @@ pub async fn connect_xhttp(
tcp_stream.write_all(&client_hello).await?; 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) // Drain all server handshake records (ServerHello, CCS, fake encrypted records).
let mut head = [0u8; 5]; // The server sends exactly REALITY_SERVER_HANDSHAKE_RECORDS records before data starts.
tcp_stream.read_exact(&mut head).await?; // Reading them explicitly prevents RealityStream from seeing non-AppData bytes.
if head[0] != 0x16 { for i in 0..REALITY_SERVER_HANDSHAKE_RECORDS {
anyhow::bail!("expected Handshake record from Reality Server"); 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); let reality_stream = RealityStream::new(tcp_stream, data_key);
xhttp_handshake_and_loop(reality_stream, target_ip, sni, access_key, wss).await xhttp_handshake_and_loop(reality_stream, target_ip, sni, access_key, wss).await

View File

@ -1,5 +1,5 @@
use bytes::{Buf, BufMut, Bytes, BytesMut}; 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 hkdf::Hkdf;
use sha2::Sha256; use sha2::Sha256;
use x25519_dalek::{PublicKey, StaticSecret}; use x25519_dalek::{PublicKey, StaticSecret};
@ -10,6 +10,11 @@ const REALITY_INFO: &[u8] = b"ostp-reality-v1";
const RECORD_HEADER_LEN: usize = 5; const RECORD_HEADER_LEN: usize = 5;
const HANDSHAKE_HEADER_LEN: usize = 4; 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 /// Generates an X25519 keypair
pub fn generate_x25519_keypair() -> (StaticSecret, PublicKey) { pub fn generate_x25519_keypair() -> (StaticSecret, PublicKey) {
let secret = StaticSecret::random_from_rng(OsRng); 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 >> 8) as u8);
record.put_u8(handshake_len as u8); record.put_u8(handshake_len as u8);
record.put_slice(&handshake); 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 { pub struct ParsedClientHello {

View File

@ -1679,6 +1679,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
"hmac",
]
[[package]] [[package]]
name = "hmac" name = "hmac"
version = "0.12.1" version = "0.12.1"
@ -2632,14 +2641,16 @@ dependencies = [
[[package]] [[package]]
name = "ostp-client" name = "ostp-client"
version = "0.2.67" version = "0.2.69"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
"chacha20poly1305",
"chrono", "chrono",
"futures", "futures",
"futures-util", "futures-util",
"hex",
"hmac", "hmac",
"json_comments", "json_comments",
"libc", "libc",
@ -2647,32 +2658,32 @@ dependencies = [
"ostp-core", "ostp-core",
"portable-atomic", "portable-atomic",
"rand", "rand",
"rustls",
"rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"socket2", "socket2",
"tokio", "tokio",
"tokio-rustls",
"tracing", "tracing",
"tun", "tun",
"webpki-roots 0.26.11", "webpki-roots 0.26.11",
"x25519-dalek",
] ]
[[package]] [[package]]
name = "ostp-core" name = "ostp-core"
version = "0.2.67" version = "0.2.69"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
"chacha20poly1305", "chacha20poly1305",
"hkdf",
"hmac", "hmac",
"rand", "rand",
"sha2", "sha2",
"snow", "snow",
"thiserror 1.0.69", "thiserror 1.0.69",
"tracing", "tracing",
"x25519-dalek",
] ]
[[package]] [[package]]
@ -2683,6 +2694,7 @@ dependencies = [
"json_comments", "json_comments",
"ostp-client", "ostp-client",
"portable-atomic", "portable-atomic",
"rand",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
@ -3208,20 +3220,6 @@ dependencies = [
"web-sys", "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]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.2" version = "2.1.2"
@ -3250,39 +3248,11 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.14.1" version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" 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]] [[package]]
name = "rustversion" name = "rustversion"
@ -4254,16 +4224,6 @@ dependencies = [
"syn 2.0.117", "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]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.18" version = "0.7.18"
@ -4614,12 +4574,6 @@ 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"
@ -5160,15 +5114,6 @@ dependencies = [
"windows-targets 0.42.2", "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]]
name = "windows-sys" name = "windows-sys"
version = "0.59.0" version = "0.59.0"
@ -5539,6 +5484,18 @@ dependencies = [
"pkg-config", "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]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.2" version = "0.8.2"
@ -5669,6 +5626,20 @@ name = "zeroize"
version = "1.8.2" version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 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]] [[package]]
name = "zerotrie" name = "zerotrie"

View File

@ -1,4 +1,4 @@
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{watch, Mutex}; use tokio::sync::{watch, Mutex};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
@ -60,6 +60,7 @@ struct TransportConfigRaw {
mode: Option<String>, mode: Option<String>,
stealth_sni: Option<String>, stealth_sni: Option<String>,
stealth_port: Option<u16>, stealth_port: Option<u16>,
wss: Option<bool>,
} }
#[derive(Debug, Deserialize, Serialize, Clone)] #[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()), 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_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), 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 { exclusions: ostp_client::config::ExclusionConfig {
domains: raw.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(), 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 // 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 cwd_path = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("."));
let dir_wstr: Vec<u16> = cwd_path.parent().unwrap_or(Path::new(".")).as_os_str().encode_wide().chain(Some(0)).collect(); let dir_wstr: Vec<u16> = 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) }; 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."); } if ret <= 32 { anyhow::bail!("UAC denied or helper missing."); }

View File

@ -13,11 +13,11 @@ use tokio::net::TcpStream;
use base64::Engine; use base64::Engine;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context as TaskContext, Poll}; use std::task::{Context as TaskContext, Poll};
use chacha20poly1305::{aead::{Aead, KeyInit, Payload}, ChaCha20Poly1305, Nonce}; use chacha20poly1305::{aead::Aead, ChaCha20Poly1305, Nonce};
use x25519_dalek::{StaticSecret, PublicKey}; use x25519_dalek::StaticSecret;
use ostp_core::framing::wss::{encode_wss_frame, decode_wss_frame, WssFrameResult}; 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; use crate::RealityServerConfig;
pub async fn handle_tcp_connection<S>( pub async fn handle_tcp_connection<S>(
@ -204,17 +204,70 @@ where
let data_key = data_key_opt.unwrap(); let data_key = data_key_opt.unwrap();
info!("Reality client authenticated from {} (sid matched)", peer_addr); info!("Reality client authenticated from {} (sid matched)", peer_addr);
// Send a fake ServerHello. For now, a static, valid-looking TLS 1.3 ServerHello. // Build a fake TLS 1.3 server flight that matches what a real server sends.
let server_hello = hex::decode("160303007a0200007603030000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000130100002e002b0002030400330024001d0020e29b191a62d0572e9a30d0fb9d08e50bc78d591dfc1dbafbfa533411db1c8e111403030001011603030030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000170303001300000000000000000000000000000000000000").unwrap(); // Must be exactly REALITY_SERVER_HANDSHAKE_RECORDS (5) TLS records:
stream.write_all(&server_hello).await?; // 1. ServerHello (0x16) - static blob with fake key share
// 2. ChangeCipherSpec (0x14) - RFC 8446 §D.4 middlebox compat
// At this point, the Reality tunnel is established. We need to wrap the stream with RealityStream. // 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); 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; return process_inner_reality_stream(reality_stream, peer_addr, tcp_map, udp_tx).await;
} else { } else {