Implement XTLS-Reality masquerade for UoT/TCP and fix MTU/config settings

This commit is contained in:
ospab 2026-05-24 22:49:51 +03:00
parent ef242bf6f4
commit 3e511f1fc5
14 changed files with 551 additions and 588 deletions

190
Cargo.lock generated
View File

@ -123,6 +123,28 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-lc-rs"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
]
[[package]] [[package]]
name = "axum" name = "axum"
version = "0.8.9" version = "0.8.9"
@ -224,6 +246,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver",
"libc",
"shlex", "shlex",
] ]
@ -327,6 +351,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.5" version = "1.0.5"
@ -404,6 +437,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -426,6 +468,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.14" version = "0.3.14"
@ -457,6 +505,12 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.32" version = "0.3.32"
@ -523,6 +577,18 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]] [[package]]
name = "ghash" name = "ghash"
version = "0.5.1" version = "0.5.1"
@ -820,6 +886,16 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"libc",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.98" version = "0.3.98"
@ -909,6 +985,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "num-conv"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -970,6 +1052,7 @@ dependencies = [
"portable-atomic", "portable-atomic",
"rand", "rand",
"rustls", "rustls",
"rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@ -977,6 +1060,7 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tracing", "tracing",
"webpki-roots 0.26.11",
] ]
[[package]] [[package]]
@ -1022,11 +1106,14 @@ dependencies = [
"ostp-core", "ostp-core",
"portable-atomic", "portable-atomic",
"rand", "rand",
"rcgen",
"rustls",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"socket2", "socket2",
"tokio", "tokio",
"tokio-rustls",
"tower-http", "tower-http",
"tracing", "tracing",
] ]
@ -1045,6 +1132,16 @@ dependencies = [
"winres", "winres",
] ]
[[package]]
name = "pem"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
dependencies = [
"base64",
"serde_core",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@ -1095,6 +1192,12 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.21" version = "0.2.21"
@ -1122,6 +1225,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -1149,7 +1258,20 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.17",
]
[[package]]
name = "rcgen"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
dependencies = [
"pem",
"ring",
"rustls-pki-types",
"time",
"yasna",
] ]
[[package]] [[package]]
@ -1177,7 +1299,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"getrandom", "getrandom 0.2.17",
"libc", "libc",
"untrusted", "untrusted",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@ -1198,6 +1320,8 @@ version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [ dependencies = [
"aws-lc-rs",
"log",
"once_cell", "once_cell",
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
@ -1221,6 +1345,7 @@ version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [ dependencies = [
"aws-lc-rs",
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
"untrusted", "untrusted",
@ -1468,6 +1593,25 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "time"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
]
[[package]]
name = "time-core"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.3" version = "0.8.3"
@ -1708,6 +1852,15 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.3+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
dependencies = [
"wit-bindgen",
]
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.121" version = "0.2.121"
@ -1753,6 +1906,24 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "webpki-roots"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.7",
]
[[package]]
name = "webpki-roots"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.11" version = "0.1.11"
@ -1978,12 +2149,27 @@ dependencies = [
"toml", "toml",
] ]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.3" version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "yasna"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
dependencies = [
"time",
]
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.2" version = "0.8.2"

View File

@ -17,9 +17,11 @@ json_comments = "0.2"
portable-atomic.workspace = true portable-atomic.workspace = true
chrono = "0.4" chrono = "0.4"
socket2 = "0.6.3" socket2 = "0.6.3"
rustls = { version = "0.23.40", default-features = false, features = ["ring", "std"] } rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
tokio-rustls = { version = "0.26.0", default-features = false, features = ["ring"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
futures-util = "0.3.32" futures-util = "0.3.32"
hmac = "0.12.1" hmac = "0.12.1"
sha2 = "0.10.8" sha2 = "0.10.8"
base64 = "0.22.1" base64 = "0.22.1"
webpki-roots = "0.26"
rustls-pki-types = "1.7"

View File

@ -38,11 +38,16 @@ pub struct BridgeMetrics {
pub connection_state: AtomicU8, pub connection_state: AtomicU8,
} }
async fn send_datagram(socket: &crate::transport::Transport, frame: &Bytes, turn_enabled: bool) -> std::io::Result<usize> { async fn send_datagram(socket: &crate::transport::Transport, frame: &Bytes, webrtc_masquerade: bool) -> std::io::Result<usize> {
if turn_enabled { if webrtc_masquerade {
let mut out = bytes::BytesMut::with_capacity(4 + frame.len()); let mut out = bytes::BytesMut::with_capacity(12 + frame.len());
bytes::BufMut::put_u16(&mut out, 0x4000); // Fake SRTP Header:
bytes::BufMut::put_u16(&mut out, frame.len() as u16); // [0] 0x80 (Version 2)
// [1] 0x60 (Payload Type 96 - dynamic video)
// [2..3] Sequence number (dummy 0x1234)
// [4..7] Timestamp (dummy)
// [8..11] SSRC (dummy)
out.extend_from_slice(&[0x80, 0x60, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44]);
out.extend_from_slice(frame); out.extend_from_slice(frame);
socket.send(&out.freeze()).await socket.send(&out.freeze()).await
} else { } else {
@ -66,10 +71,7 @@ pub struct Bridge {
handshake_timeout_ms: u64, handshake_timeout_ms: u64,
io_timeout_ms: u64, io_timeout_ms: u64,
pub turn_enabled: bool, pub keepalive_interval_sec: u64,
pub turn_server: String,
pub turn_username: String,
pub turn_password: String,
pub mode: String, pub mode: String,
pub mux_enabled: bool, pub mux_enabled: bool,
pub mux_sessions: usize, pub mux_sessions: usize,
@ -78,6 +80,7 @@ pub struct Bridge {
pub stealth_sni: String, pub stealth_sni: String,
pub stealth_port: u16, pub stealth_port: u16,
pub mtu: usize, pub mtu: usize,
pub reality_enabled: bool,
metrics: Arc<BridgeMetrics>, metrics: Arc<BridgeMetrics>,
sample_sent: u64, sample_sent: u64,
@ -100,10 +103,7 @@ impl Bridge {
handshake_timeout_ms: config.ostp.handshake_timeout_ms, handshake_timeout_ms: config.ostp.handshake_timeout_ms,
io_timeout_ms: config.ostp.io_timeout_ms, io_timeout_ms: config.ostp.io_timeout_ms,
turn_enabled: config.turn.enabled, keepalive_interval_sec: config.ostp.keepalive_interval_sec,
turn_server: config.turn.server_addr.clone(),
turn_username: config.turn.username.clone(),
turn_password: config.turn.access_key.clone(),
mode: config.mode.clone(), mode: config.mode.clone(),
mux_enabled: config.multiplex.enabled, mux_enabled: config.multiplex.enabled,
mux_sessions: config.multiplex.sessions.max(1), mux_sessions: config.multiplex.sessions.max(1),
@ -112,6 +112,7 @@ impl Bridge {
stealth_sni: config.transport.stealth_sni.clone(), stealth_sni: config.transport.stealth_sni.clone(),
stealth_port: config.transport.stealth_port, stealth_port: config.transport.stealth_port,
mtu: config.ostp.mtu, mtu: config.ostp.mtu,
reality_enabled: !config.reality.pbk.is_empty(),
metrics, metrics,
sample_sent: 0, sample_sent: 0,
@ -131,7 +132,7 @@ impl Bridge {
proxy_tx: mpsc::UnboundedSender<(u16, ProxyToClientMsg)>, proxy_tx: mpsc::UnboundedSender<(u16, ProxyToClientMsg)>,
) -> Result<()> { ) -> Result<()> {
let mut metrics_tick = interval(Duration::from_millis(500)); let mut metrics_tick = interval(Duration::from_millis(500));
let mut keepalive_tick = tokio::time::interval(Duration::from_secs(5)); let mut keepalive_tick = tokio::time::interval(Duration::from_secs(self.keepalive_interval_sec.max(1)));
let mut retransmit_tick = tokio::time::interval(Duration::from_millis(50)); let mut retransmit_tick = tokio::time::interval(Duration::from_millis(50));
let init_msg = if self.mode == "tun" { let init_msg = if self.mode == "tun" {
"Bridge initialized (TUN mode)".to_string() "Bridge initialized (TUN mode)".to_string()
@ -220,7 +221,7 @@ impl Bridge {
} }
} }
ProtocolAction::SendDatagram(frame) => { ProtocolAction::SendDatagram(frame) => {
let _ = send_datagram(&session.socket, &frame, self.turn_enabled).await; let _ = send_datagram(&session.socket, &frame, (self.transport_mode == "udp")).await;
self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed);
} }
_ => {} _ => {}
@ -273,19 +274,14 @@ impl Bridge {
let session_index = sessions.len(); let session_index = sessions.len();
let socket_clone = sock.clone(); let socket_clone = sock.clone();
let udp_tx_clone = udp_tx.clone(); let udp_tx_clone = udp_tx.clone();
let is_turn = self.turn_enabled; let is_webrtc = (self.transport_mode == "udp");
tokio::spawn(async move { tokio::spawn(async move {
let mut buf = vec![0_u8; 65535]; let mut buf = vec![0_u8; 65535];
loop { loop {
match socket_clone.recv(&mut buf).await { match socket_clone.recv(&mut buf).await {
Ok(n) => { Ok(n) => {
let inbound = if is_turn && n >= 4 && buf[0] == 0x40 && buf[1] == 0x00 { let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 {
let len = u16::from_be_bytes([buf[2], buf[3]]) as usize; Bytes::copy_from_slice(&buf[12..n])
if 4 + len <= n {
Bytes::copy_from_slice(&buf[4..4+len])
} else {
Bytes::copy_from_slice(&buf[..n])
}
} else { } else {
Bytes::copy_from_slice(&buf[..n]) Bytes::copy_from_slice(&buf[..n])
}; };
@ -368,15 +364,14 @@ impl Bridge {
let session_index = new_sessions.len(); let session_index = new_sessions.len();
let socket_clone = sock.clone(); let socket_clone = sock.clone();
let udp_tx_clone = udp_tx.clone(); let udp_tx_clone = udp_tx.clone();
let is_turn = self.turn_enabled; let is_webrtc = (self.transport_mode == "udp");
tokio::spawn(async move { tokio::spawn(async move {
let mut buf = vec![0_u8; 65535]; let mut buf = vec![0_u8; 65535];
loop { loop {
match socket_clone.recv(&mut buf).await { match socket_clone.recv(&mut buf).await {
Ok(n) => { Ok(n) => {
let inbound = if is_turn && n >= 4 && buf[0] == 0x40 && buf[1] == 0x00 { let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 {
let len = u16::from_be_bytes([buf[2], buf[3]]) as usize; Bytes::copy_from_slice(&buf[12..n])
if 4 + len <= n { Bytes::copy_from_slice(&buf[4..4+len]) } else { Bytes::copy_from_slice(&buf[..n]) }
} else { } else {
Bytes::copy_from_slice(&buf[..n]) Bytes::copy_from_slice(&buf[..n])
}; };
@ -480,19 +475,14 @@ impl Bridge {
let session_index = new_sessions.len(); let session_index = new_sessions.len();
let socket_clone = sock.clone(); let socket_clone = sock.clone();
let udp_tx_clone = udp_tx.clone(); let udp_tx_clone = udp_tx.clone();
let is_turn = self.turn_enabled; let is_webrtc = (self.transport_mode == "udp");
tokio::spawn(async move { tokio::spawn(async move {
let mut buf = vec![0_u8; 65535]; let mut buf = vec![0_u8; 65535];
loop { loop {
match socket_clone.recv(&mut buf).await { match socket_clone.recv(&mut buf).await {
Ok(n) => { Ok(n) => {
let inbound = if is_turn && n >= 4 && buf[0] == 0x40 && buf[1] == 0x00 { let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 {
let len = u16::from_be_bytes([buf[2], buf[3]]) as usize; Bytes::copy_from_slice(&buf[12..n])
if 4 + len <= n {
Bytes::copy_from_slice(&buf[4..4+len])
} else {
Bytes::copy_from_slice(&buf[..n])
}
} else { } else {
Bytes::copy_from_slice(&buf[..n]) Bytes::copy_from_slice(&buf[..n])
}; };
@ -539,14 +529,14 @@ impl Bridge {
if let Ok(ProtocolAction::SendDatagram(frame)) = session.machine.on_event(OstpEvent::Outbound(0, ping_payload)) { if let Ok(ProtocolAction::SendDatagram(frame)) = session.machine.on_event(OstpEvent::Outbound(0, ping_payload)) {
// Must go through send_datagram() for TURN-mode wrapping; // Must go through send_datagram() for TURN-mode wrapping;
// raw socket.send() bypasses the ChannelData header and breaks RTT in TURN. // raw socket.send() bypasses the ChannelData header and breaks RTT in TURN.
let _ = send_datagram(&session.socket, &frame, self.turn_enabled).await; let _ = send_datagram(&session.socket, &frame, (self.transport_mode == "udp")).await;
self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed);
} }
// Send Relay KeepAlive (Force NAT/Server Persistence) // Send Relay KeepAlive (Force NAT/Server Persistence)
let ka_payload = Bytes::from(RelayMessage::KeepAlive.encode()); let ka_payload = Bytes::from(RelayMessage::KeepAlive.encode());
if let Ok(ProtocolAction::SendDatagram(frame)) = session.machine.on_event(OstpEvent::Outbound(0, ka_payload)) { if let Ok(ProtocolAction::SendDatagram(frame)) = session.machine.on_event(OstpEvent::Outbound(0, ka_payload)) {
let _ = send_datagram(&session.socket, &frame, self.turn_enabled).await; let _ = send_datagram(&session.socket, &frame, (self.transport_mode == "udp")).await;
self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed);
} }
} }
@ -569,7 +559,7 @@ impl Bridge {
} }
} }
ProtocolAction::SendDatagram(frame) => { ProtocolAction::SendDatagram(frame) => {
let _ = send_datagram(&session.socket, &frame, self.turn_enabled).await; let _ = send_datagram(&session.socket, &frame, (self.transport_mode == "udp")).await;
self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed);
} }
_ => {} _ => {}
@ -632,7 +622,7 @@ impl Bridge {
let out_payload = Bytes::from(relay_msg.encode()); let out_payload = Bytes::from(relay_msg.encode());
match session.machine.on_event(OstpEvent::Outbound(stream_id, out_payload)) { match session.machine.on_event(OstpEvent::Outbound(stream_id, out_payload)) {
Ok(ProtocolAction::SendDatagram(frame)) => { Ok(ProtocolAction::SendDatagram(frame)) => {
if send_datagram(&session.socket, &frame, self.turn_enabled).await.is_ok() { if send_datagram(&session.socket, &frame, (self.transport_mode == "udp")).await.is_ok() {
self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed);
if self.debug { if self.debug {
let _ = tx.send(UiEvent::Log(format!( let _ = tx.send(UiEvent::Log(format!(
@ -646,7 +636,7 @@ impl Bridge {
let mut sent = 0usize; let mut sent = 0usize;
for item in list { for item in list {
if let ProtocolAction::SendDatagram(frame) = item { if let ProtocolAction::SendDatagram(frame) = item {
if send_datagram(&session.socket, &frame, self.turn_enabled).await.is_ok() { if send_datagram(&session.socket, &frame, (self.transport_mode == "udp")).await.is_ok() {
self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed);
sent += 1; sent += 1;
} }
@ -817,64 +807,7 @@ impl Bridge {
} }
}; };
if self.turn_enabled && self.transport_mode != "wss" { // Connection to remote is handled inside try_connect_transport
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 {
format!("{}:3478", self.turn_server)
};
tx.send(UiEvent::Log(format!("Allocating TURN relay via {}", turn_addr))).await.ok();
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();
let resolved_turn: Vec<std::net::SocketAddr> = tokio::net::lookup_host(&turn_addr).await
.map_err(|e| anyhow::anyhow!("failed to resolve TURN {}: {}", turn_addr, e))?
.collect();
let turn_target = resolved_turn.first().ok_or_else(|| anyhow::anyhow!("no IP resolved for TURN {}", turn_addr))?;
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 {
turn_target.ip()
}
} else {
turn_target.ip()
};
let connect_addr = std::net::SocketAddr::new(connect_ip, turn_target.port());
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 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 {
target_ip
}
} else {
target_ip
};
let connect_addr = std::net::SocketAddr::new(connect_ip, port);
udp_socket
.connect(connect_addr)
.await
.with_context(|| format!("failed to connect udp to {}", connect_addr))?;
}
}
}
// Connection to remote is handled inside the TURN/direct branches above
let start = Instant::now(); let start = Instant::now();
let action = machine.on_event(OstpEvent::Start)?; let action = machine.on_event(OstpEvent::Start)?;
@ -897,7 +830,7 @@ impl Bridge {
if attempt > 0 { if attempt > 0 {
tx.send(UiEvent::Log(format!("Handshake attempt {} lost. Retransmitting...", attempt))).await.ok(); tx.send(UiEvent::Log(format!("Handshake attempt {} lost. Retransmitting...", attempt))).await.ok();
} }
send_datagram(&socket, &handshake_frame, self.turn_enabled).await?; send_datagram(&socket, &handshake_frame, (self.transport_mode == "udp")).await?;
self.metrics.bytes_sent.fetch_add(handshake_frame.len() as u64, Ordering::Relaxed); self.metrics.bytes_sent.fetch_add(handshake_frame.len() as u64, Ordering::Relaxed);
match timeout(Duration::from_millis(attempt_timeout_ms), socket.recv(&mut buf)).await { match timeout(Duration::from_millis(attempt_timeout_ms), socket.recv(&mut buf)).await {
@ -923,7 +856,7 @@ impl Bridge {
if attempt > 0 { if attempt > 0 {
tx.send(UiEvent::Log(format!("NAT64 handshake attempt {} lost. Retransmitting...", attempt))).await.ok(); tx.send(UiEvent::Log(format!("NAT64 handshake attempt {} lost. Retransmitting...", attempt))).await.ok();
} }
send_datagram(&fallback_socket, &handshake_frame, self.turn_enabled).await?; send_datagram(&fallback_socket, &handshake_frame, (self.transport_mode == "udp")).await?;
match timeout(Duration::from_millis(1200), fallback_socket.recv(&mut buf)).await { match timeout(Duration::from_millis(1200), fallback_socket.recv(&mut buf)).await {
Ok(Ok(n)) => { Ok(Ok(n)) => {
size = n; size = n;
@ -950,13 +883,8 @@ impl Bridge {
self.metrics.bytes_recv.fetch_add(size as u64, Ordering::Relaxed); self.metrics.bytes_recv.fetch_add(size as u64, Ordering::Relaxed);
tracing::info!("Handshake response received: {} bytes", size); tracing::info!("Handshake response received: {} bytes", size);
let inbound = if self.turn_enabled && size >= 4 && buf[0] == 0x40 && buf[1] == 0x00 { let inbound = if (self.transport_mode == "udp") && size >= 12 && buf[0] == 0x80 {
let len = u16::from_be_bytes([buf[2], buf[3]]) as usize; Bytes::copy_from_slice(&buf[12..size])
if 4 + len <= size {
Bytes::copy_from_slice(&buf[4..4+len])
} else {
Bytes::copy_from_slice(&buf[..size])
}
} else { } else {
Bytes::copy_from_slice(&buf[..size]) Bytes::copy_from_slice(&buf[..size])
}; };
@ -975,15 +903,12 @@ impl Bridge {
self.handshake_timeout_ms = cfg.ostp.handshake_timeout_ms; self.handshake_timeout_ms = cfg.ostp.handshake_timeout_ms;
self.io_timeout_ms = cfg.ostp.io_timeout_ms; self.io_timeout_ms = cfg.ostp.io_timeout_ms;
self.mode = cfg.mode.clone(); // Bug fix: mode was never updated on hot-reload self.mode = cfg.mode.clone(); // Bug fix: mode was never updated on hot-reload
self.turn_enabled = cfg.turn.enabled;
self.turn_server = cfg.turn.server_addr.clone();
self.turn_username = cfg.turn.username.clone();
self.turn_password = cfg.turn.access_key.clone();
self.mux_enabled = cfg.multiplex.enabled; self.mux_enabled = cfg.multiplex.enabled;
self.mux_sessions = cfg.multiplex.sessions.max(1); self.mux_sessions = cfg.multiplex.sessions.max(1);
self.transport_mode = cfg.transport.mode.clone(); self.transport_mode = cfg.transport.mode.clone();
self.stealth_sni = cfg.transport.stealth_sni.clone(); self.stealth_sni = cfg.transport.stealth_sni.clone();
self.stealth_port = cfg.transport.stealth_port; self.stealth_port = cfg.transport.stealth_port;
self.reality_enabled = !cfg.reality.pbk.is_empty();
} }
async fn try_connect_transport( async fn try_connect_transport(
@ -1002,7 +927,7 @@ impl Bridge {
port port
}; };
let (tx, rx) = crate::transport::xhttp::connect_xhttp( let (tx, rx) = crate::transport::xhttp::connect_xhttp(
target_ip, uot_port, &self.stealth_sni, &self.access_key target_ip, uot_port, &self.stealth_sni, &self.access_key, self.reality_enabled
).await?; ).await?;
Ok(crate::transport::Transport::Uot { tx, rx }) Ok(crate::transport::Transport::Uot { tx, rx })
} else { } else {
@ -1053,3 +978,4 @@ fn synthesize_nat64(ip: std::net::Ipv4Addr) -> std::net::Ipv6Addr {
) )
} }

View File

@ -12,7 +12,7 @@ pub struct ClientConfig {
pub debug: bool, pub debug: bool,
pub ostp: OstpConfig, pub ostp: OstpConfig,
pub local_proxy: LocalProxyConfig, pub local_proxy: LocalProxyConfig,
pub turn: TurnConfig, pub reality: RealityConfig,
#[serde(default)] #[serde(default)]
pub transport: TransportConfig, pub transport: TransportConfig,
#[serde(default)] #[serde(default)]
@ -48,8 +48,12 @@ pub struct OstpConfig {
pub io_timeout_ms: u64, pub io_timeout_ms: u64,
#[serde(default = "default_mtu")] #[serde(default = "default_mtu")]
pub mtu: usize, pub mtu: usize,
#[serde(default = "default_keepalive")]
pub keepalive_interval_sec: u64,
} }
fn default_keepalive() -> u64 { 5 }
fn default_mtu() -> usize { 1350 } fn default_mtu() -> usize { 1350 }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -88,11 +92,17 @@ impl Default for TransportConfig {
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TurnConfig { pub struct RealityConfig {
pub enabled: bool, #[serde(default)]
pub server_addr: String, pub sni: String,
pub username: String, #[serde(default)]
pub access_key: String, pub fp: String,
#[serde(default)]
pub pbk: String,
#[serde(default)]
pub sid: String,
#[serde(default)]
pub spx: String,
} }
@ -105,6 +115,7 @@ impl Default for OstpConfig {
handshake_timeout_ms: 5000, handshake_timeout_ms: 5000,
io_timeout_ms: 2500, io_timeout_ms: 2500,
mtu: default_mtu(), mtu: default_mtu(),
keepalive_interval_sec: default_keepalive(),
} }
} }
} }
@ -126,7 +137,7 @@ impl Default for ClientConfig {
debug: false, debug: false,
ostp: OstpConfig::default(), ostp: OstpConfig::default(),
local_proxy: LocalProxyConfig::default(), local_proxy: LocalProxyConfig::default(),
turn: TurnConfig::default(), reality: RealityConfig::default(),
transport: TransportConfig::default(), transport: TransportConfig::default(),
exclusions: ExclusionConfig::default(), exclusions: ExclusionConfig::default(),
multiplex: MultiplexConfig::default(), multiplex: MultiplexConfig::default(),
@ -158,7 +169,7 @@ struct RawUnifiedConfig {
tun: Option<RawTunSection>, tun: Option<RawTunSection>,
exclude: Option<RawExcludeSection>, exclude: Option<RawExcludeSection>,
mux: Option<RawMuxSection>, mux: Option<RawMuxSection>,
turn: Option<RawTurnSection>, reality: Option<RawRealitySection>,
transport: Option<RawTransportSection>, transport: Option<RawTransportSection>,
} }
@ -189,11 +200,12 @@ struct RawMuxSection {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct RawTurnSection { struct RawRealitySection {
enabled: Option<bool>, sni: Option<String>,
server_addr: Option<String>, fp: Option<String>,
username: Option<String>, pbk: Option<String>,
access_key: Option<String>, sid: Option<String>,
spx: Option<String>,
} }
impl ClientConfig { impl ClientConfig {
@ -235,16 +247,18 @@ impl ClientConfig {
handshake_timeout_ms: 5000, handshake_timeout_ms: 5000,
io_timeout_ms: 2500, io_timeout_ms: 2500,
mtu, mtu,
keepalive_interval_sec: default_keepalive(),
}, },
local_proxy: LocalProxyConfig { local_proxy: LocalProxyConfig {
bind_addr: socks5, bind_addr: socks5,
connect_timeout_ms: 15000, connect_timeout_ms: 15000,
}, },
turn: TurnConfig { reality: RealityConfig {
enabled: raw.turn.as_ref().and_then(|t| t.enabled).unwrap_or(false), sni: raw.reality.as_ref().and_then(|t| t.sni.clone()).unwrap_or_default(),
server_addr: raw.turn.as_ref().and_then(|t| t.server_addr.clone()).unwrap_or_default(), fp: raw.reality.as_ref().and_then(|t| t.fp.clone()).unwrap_or_default(),
username: raw.turn.as_ref().and_then(|t| t.username.clone()).unwrap_or_default(), pbk: raw.reality.as_ref().and_then(|t| t.pbk.clone()).unwrap_or_default(),
access_key: raw.turn.as_ref().and_then(|t| t.access_key.clone()).unwrap_or_default(), sid: raw.reality.as_ref().and_then(|t| t.sid.clone()).unwrap_or_default(),
spx: raw.reality.as_ref().and_then(|t| t.spx.clone()).unwrap_or_default(),
}, },
transport: TransportConfig { transport: TransportConfig {
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()),

View File

@ -6,5 +6,5 @@ pub mod sysproxy;
pub mod transport; pub mod transport;
pub mod tunnel; pub mod tunnel;
pub mod turn;
pub mod runner; pub mod runner;

View File

@ -8,6 +8,62 @@ use tokio::sync::mpsc;
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use sha2::Sha256; use sha2::Sha256;
use base64::Engine; use base64::Engine;
use rustls::ClientConfig;
use rustls::pki_types::ServerName;
use std::sync::Arc as StdArc;
use tokio_rustls::TlsConnector;
mod danger {
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::DigitallySignedStruct;
use rustls::crypto::CryptoProvider;
#[derive(Debug)]
pub struct NoCertificateVerification;
impl ServerCertVerifier for NoCertificateVerification {
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::RSA_PKCS1_SHA384,
rustls::SignatureScheme::RSA_PKCS1_SHA512,
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::ED25519,
]
}
}
}
type HmacSha256 = Hmac<Sha256>; type HmacSha256 = Hmac<Sha256>;
@ -16,12 +72,45 @@ pub async fn connect_xhttp(
port: u16, port: u16,
sni: &str, sni: &str,
access_key: &[u8], access_key: &[u8],
tls_enabled: bool,
) -> Result<(mpsc::Sender<Bytes>, Arc<tokio::sync::Mutex<mpsc::Receiver<Bytes>>>)> { ) -> Result<(mpsc::Sender<Bytes>, Arc<tokio::sync::Mutex<mpsc::Receiver<Bytes>>>)> {
let addr = std::net::SocketAddr::new(target_ip, port); let addr = std::net::SocketAddr::new(target_ip, port);
let mut tcp_stream = TcpStream::connect(addr).await let tcp_stream = TcpStream::connect(addr).await
.with_context(|| format!("failed to connect to {}", addr))?; .with_context(|| format!("failed to connect to {}", addr))?;
tcp_stream.set_nodelay(true)?; tcp_stream.set_nodelay(true)?;
if tls_enabled {
// Setup rustls client skipping cert validation (Reality self-signed certs)
let mut config = ClientConfig::builder_with_provider(rustls::crypto::ring::default_provider().into())
.with_safe_default_protocol_versions()
.unwrap()
.dangerous()
.with_custom_certificate_verifier(StdArc::new(danger::NoCertificateVerification))
.with_no_client_auth();
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let connector = TlsConnector::from(StdArc::new(config));
let server_name = ServerName::try_from(sni.to_string())
.unwrap_or_else(|_| ServerName::try_from("www.microsoft.com").unwrap())
.to_owned();
let tls_stream = connector.connect(server_name, tcp_stream).await
.with_context(|| "TLS handshake failed")?;
xhttp_handshake_and_loop(tls_stream, target_ip, sni, access_key).await
} else {
xhttp_handshake_and_loop(tcp_stream, target_ip, sni, access_key).await
}
}
async fn xhttp_handshake_and_loop<S>(
mut stream: S,
target_ip: IpAddr,
sni: &str,
access_key: &[u8],
) -> Result<(mpsc::Sender<Bytes>, Arc<tokio::sync::Mutex<mpsc::Receiver<Bytes>>>)>
where
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
{
// 1. Generate auth token: [8-byte timestamp BE] ++ [HMAC-SHA256] // 1. Generate auth token: [8-byte timestamp BE] ++ [HMAC-SHA256]
let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
let ts_bytes = timestamp.to_be_bytes(); let ts_bytes = timestamp.to_be_bytes();
@ -38,8 +127,6 @@ pub async fn connect_xhttp(
let http_host = if sni.is_empty() { target_ip.to_string() } else { sni.to_string() }; let http_host = if sni.is_empty() { target_ip.to_string() } else { sni.to_string() };
// 2. Send fake WebSocket upgrade — looks like a legit browser request to bypass DPI/proxies. // 2. Send fake WebSocket upgrade — looks like a legit browser request to bypass DPI/proxies.
// The server responds with 101 Switching Protocols and we stream raw UoT frames after that.
// NOTE: always plain TCP — TLS is NOT used. Obfuscation comes from the fake WS headers.
let req = format!( let req = format!(
"GET /stream HTTP/1.1\r\n\ "GET /stream HTTP/1.1\r\n\
Host: {}\r\n\ Host: {}\r\n\
@ -52,14 +139,14 @@ pub async fn connect_xhttp(
http_host, auth_token http_host, auth_token
); );
tcp_stream.write_all(req.as_bytes()).await?; stream.write_all(req.as_bytes()).await?;
tcp_stream.flush().await?; stream.flush().await?;
// 3. Read server response headers // 3. Read server response headers
let mut buf = vec![0u8; 4096]; let mut buf = vec![0u8; 4096];
let mut header_len = 0; let mut header_len = 0;
loop { loop {
let n = tcp_stream.read(&mut buf[header_len..]).await?; let n = stream.read(&mut buf[header_len..]).await?;
if n == 0 { anyhow::bail!("connection closed before handshake complete"); } if n == 0 { anyhow::bail!("connection closed before handshake complete"); }
header_len += n; header_len += n;
if buf[..header_len].windows(4).any(|w| w == b"\r\n\r\n") { break; } if buf[..header_len].windows(4).any(|w| w == b"\r\n\r\n") { break; }
@ -80,7 +167,7 @@ pub async fn connect_xhttp(
let leftover = buf[headers_end..header_len].to_vec(); let leftover = buf[headers_end..header_len].to_vec();
// 5. Split into read/write halves and start UoT loops // 5. Split into read/write halves and start UoT loops
let (rx, tx) = tcp_stream.into_split(); let (rx, tx) = tokio::io::split(stream);
start_uot_loops(rx, tx, leftover) start_uot_loops(rx, tx, leftover)
} }

View File

@ -138,7 +138,7 @@ pub async fn run_linux_tunnel(
// 4. Setup commands (Using standard /1 routing trick for fail-proof overriding) // 4. Setup commands (Using standard /1 routing trick for fail-proof overriding)
let setup_script = format!( let setup_script = format!(
"ip tuntap add name ostp_tun mode tun || true; \ "ip tuntap add name ostp_tun mode tun || true; \
ip link set dev ostp_tun mtu 1300; \ ip link set dev ostp_tun mtu {}; \
ip addr add 10.1.0.2/24 dev ostp_tun || true; \ ip addr add 10.1.0.2/24 dev ostp_tun || true; \
ip link set dev ostp_tun up; \ ip link set dev ostp_tun up; \
ip route add {} via {} dev {}; \ ip route add {} via {} dev {}; \

View File

@ -174,8 +174,9 @@ pub async fn run_wintun_tunnel(
tracing::info!("Applying network configuration..."); tracing::info!("Applying network configuration...");
let mut net_setup = format!( let mut net_setup = format!(
"netsh interface ipv4 set address name=\"{TUN_NAME}\" static 10.1.0.2 255.255.255.0 10.1.0.1\n\ "netsh interface ipv4 set address name=\"{TUN_NAME}\" static 10.1.0.2 255.255.255.0 10.1.0.1\n\
netsh interface ipv4 set subinterface \"{TUN_NAME}\" mtu=1300 store=persistent\n\ netsh interface ipv4 set subinterface \"{TUN_NAME}\" mtu={} store=persistent\n\
netsh interface ipv4 set interface name=\"{TUN_NAME}\" metric=5\n" netsh interface ipv4 set interface name=\"{TUN_NAME}\" metric=5\n",
config.ostp.mtu
); );
if let Some(ref dns) = config.dns_server { if let Some(ref dns) = config.dns_server {

View File

@ -1,397 +0,0 @@
//! TURN (RFC 5766) allocation and channel binding for NAT traversal.
//!
//! Implements the minimal STUN/TURN message flow needed to allocate a relay
//! address and bind a channel to the OSTP server. All crypto (MD5, SHA-1,
//! HMAC-SHA1) is implemented inline to avoid external dependencies.
use std::time::Duration;
use anyhow::Result;
use tokio::net::UdpSocket;
use tokio::time::timeout;
/// Real RFC-5766 TURN allocation with HMAC-SHA1 long-term credentials.
///
/// Flow:
/// 1. Send Allocate (unauthenticated) -> get 401 with realm + nonce
/// 2. Compute HMAC-SHA1 key = MD5(username:realm:password)
/// 3. Re-send Allocate with MESSAGE-INTEGRITY
/// 4. Extract XOR-RELAYED-ADDRESS from success response
/// 5. Send ChannelBind to bind channel 0x4000 to the OSTP server addr
///
/// Returns the relay address string like "1.2.3.4:12345".
pub async fn perform_turn_allocation(
socket: &UdpSocket,
turn_addr: &str,
username: &str,
password: &str,
ostp_server_addr: &str,
) -> Result<String> {
use std::net::ToSocketAddrs;
let turn_sock: std::net::SocketAddr = turn_addr
.to_socket_addrs()
.map_err(|e| anyhow::anyhow!("TURN DNS resolution failed: {e}"))?
.next()
.ok_or_else(|| anyhow::anyhow!("TURN addr resolved to nothing"))?;
let transaction_id = {
use rand::Rng;
let mut id = [0u8; 12];
rand::thread_rng().fill(&mut id);
id
};
// ── Step 1: unauthenticated Allocate ─────────────────────────────
// REQUESTED-TRANSPORT attr: 0x0019, value = 17 (UDP) + 3 reserved bytes
let req_transport = stun_attr(0x0019, &[17u8, 0, 0, 0]);
let alloc_req = build_stun_msg(0x0003, &transaction_id, &req_transport);
socket.send_to(&alloc_req, turn_sock).await
.map_err(|e| anyhow::anyhow!("TURN send Allocate failed: {e}"))?;
let mut buf = [0u8; 2048];
let (n, _) = timeout(Duration::from_millis(3000), socket.recv_from(&mut buf))
.await
.map_err(|_| anyhow::anyhow!("TURN Allocate response timed out"))?
.map_err(|e| anyhow::anyhow!("TURN recv failed: {e}"))?;
let resp = &buf[..n];
if resp.len() < 20 {
anyhow::bail!("TURN response too short");
}
let msg_type = u16::from_be_bytes([resp[0], resp[1]]);
// 0x0113 = Allocate Error Response
if msg_type != 0x0113 {
anyhow::bail!("Expected TURN 401 error response, got type 0x{:04x}", msg_type);
}
// Parse realm and nonce from the error response attributes
let mut realm: Option<String> = None;
let mut nonce: Option<String> = None;
{
let mut idx = 20usize;
while idx + 4 <= n {
let atype = u16::from_be_bytes([resp[idx], resp[idx + 1]]);
let alen = u16::from_be_bytes([resp[idx + 2], resp[idx + 3]]) as usize;
idx += 4;
if idx + alen > n { break; }
let val = &resp[idx..idx + alen];
match atype {
0x0014 => realm = Some(String::from_utf8_lossy(val).to_string()), // REALM
0x0015 => nonce = Some(String::from_utf8_lossy(val).to_string()), // NONCE
_ => {}
}
idx += alen;
let pad = (4 - (alen % 4)) % 4;
idx += pad;
}
}
let realm = realm.ok_or_else(|| anyhow::anyhow!("TURN 401: no REALM in response"))?;
let nonce = nonce.ok_or_else(|| anyhow::anyhow!("TURN 401: no NONCE in response"))?;
// ── Step 2: Compute long-term credential key per RFC 5389 §15.4 ──
// key = MD5(username ":" realm ":" password)
let key_input = format!("{}:{}:{}", username, realm, password);
let key = md5_hash(key_input.as_bytes());
// HMAC-SHA1 of the message (MESSAGE-INTEGRITY attribute, RFC 5389 §15.4)
let mut attrs2 = Vec::new();
attrs2.extend_from_slice(&stun_attr(0x0006, username.as_bytes())); // USERNAME
attrs2.extend_from_slice(&stun_attr(0x0014, realm.as_bytes())); // REALM
attrs2.extend_from_slice(&stun_attr(0x0015, nonce.as_bytes())); // NONCE
attrs2.extend_from_slice(&req_transport); // REQUESTED-TRANSPORT
// For MESSAGE-INTEGRITY we need the full message length including the MI attr (24 bytes)
let mi_placeholder_len = attrs2.len() + 4 + 20; // +4 header, +20 HMAC-SHA1
let mut msg_for_hmac = build_stun_msg(0x0003, &transaction_id, &attrs2);
// Set length field to include the upcoming MI attr
let new_len = (mi_placeholder_len - 20) as u16; // total attrs length including MI
msg_for_hmac[2..4].copy_from_slice(&new_len.to_be_bytes());
// Append MI header (without value)
msg_for_hmac.extend_from_slice(&0x0008_u16.to_be_bytes()); // attr type
msg_for_hmac.extend_from_slice(&20_u16.to_be_bytes()); // attr len
let hmac = hmac_sha1(&key, &msg_for_hmac);
let mut final_attrs = attrs2.clone();
final_attrs.extend_from_slice(&stun_attr(0x0008, &hmac)); // MESSAGE-INTEGRITY
let alloc_req2 = build_stun_msg(0x0003, &transaction_id, &final_attrs);
socket.send_to(&alloc_req2, turn_sock).await
.map_err(|e| anyhow::anyhow!("TURN authenticated Allocate send failed: {e}"))?;
let (n2, _) = timeout(Duration::from_millis(5000), socket.recv_from(&mut buf))
.await
.map_err(|_| anyhow::anyhow!("TURN authenticated Allocate timed out"))?
.map_err(|e| anyhow::anyhow!("TURN recv2 failed: {e}"))?;
let resp2 = &buf[..n2];
if resp2.len() < 20 {
anyhow::bail!("TURN auth response too short");
}
let msg_type2 = u16::from_be_bytes([resp2[0], resp2[1]]);
// 0x0103 = Allocate Success Response
if msg_type2 != 0x0103 {
anyhow::bail!("TURN Allocate auth failed, response type 0x{:04x}", msg_type2);
}
// ── Step 3: Parse XOR-RELAYED-ADDRESS ────────────────────────────
let relay_addr_str = {
let mut relayed: Option<String> = None;
let mut idx = 20usize;
while idx + 4 <= n2 {
let atype = u16::from_be_bytes([resp2[idx], resp2[idx + 1]]);
let alen = u16::from_be_bytes([resp2[idx + 2], resp2[idx + 3]]) as usize;
idx += 4;
if idx + alen > n2 { break; }
let val = &resp2[idx..idx + alen];
if atype == 0x0016 && alen >= 8 { // XOR-RELAYED-ADDRESS
let x_port = u16::from_be_bytes([val[2], val[3]]) ^ 0x2112;
let x_ip = [val[4], val[5], val[6], val[7]];
let ip = std::net::Ipv4Addr::new(
x_ip[0] ^ 0x21, x_ip[1] ^ 0x12, x_ip[2] ^ 0xA4, x_ip[3] ^ 0x42,
);
relayed = Some(format!("{}:{}", ip, x_port));
}
idx += alen;
let pad = (4 - (alen % 4)) % 4;
idx += pad;
}
relayed.ok_or_else(|| anyhow::anyhow!("TURN: no XOR-RELAYED-ADDRESS in response"))?
};
// ── Step 4: ChannelBind to the OSTP server ────────────────────────
let ostp_sock: std::net::SocketAddr = ostp_server_addr
.to_socket_addrs()
.map_err(|e| anyhow::anyhow!("OSTP server DNS resolution failed: {e}"))?
.next()
.ok_or_else(|| anyhow::anyhow!("OSTP server addr resolved to nothing"))?;
let channel_number: u16 = 0x4000;
let mut peer_addr_attr = Vec::new();
peer_addr_attr.push(0u8); // reserved
peer_addr_attr.push(0x01u8); // family IPv4
peer_addr_attr.extend_from_slice(&(ostp_sock.port() ^ 0x2112).to_be_bytes()); // XOR port
if let std::net::IpAddr::V4(ipv4) = ostp_sock.ip() {
let octets = ipv4.octets();
peer_addr_attr.push(octets[0] ^ 0x21);
peer_addr_attr.push(octets[1] ^ 0x12);
peer_addr_attr.push(octets[2] ^ 0xA4);
peer_addr_attr.push(octets[3] ^ 0x42);
} else {
anyhow::bail!("TURN ChannelBind: IPv6 OSTP server not yet supported");
}
let mut cb_attrs = Vec::new();
// CHANNEL-NUMBER attr: 0x000C
cb_attrs.extend_from_slice(&stun_attr(0x000C, &[
(channel_number >> 8) as u8, channel_number as u8, 0, 0
]));
// XOR-PEER-ADDRESS attr: 0x0012
cb_attrs.extend_from_slice(&stun_attr(0x0012, &peer_addr_attr));
cb_attrs.extend_from_slice(&stun_attr(0x0006, username.as_bytes()));
cb_attrs.extend_from_slice(&stun_attr(0x0014, realm.as_bytes()));
cb_attrs.extend_from_slice(&stun_attr(0x0015, nonce.as_bytes()));
// Compute MESSAGE-INTEGRITY for ChannelBind too
let mi_len2 = cb_attrs.len() + 4 + 20;
let mut cb_for_hmac = build_stun_msg(0x0009, &transaction_id, &cb_attrs);
cb_for_hmac[2..4].copy_from_slice(&((mi_len2 - 20) as u16).to_be_bytes());
cb_for_hmac.extend_from_slice(&0x0008_u16.to_be_bytes());
cb_for_hmac.extend_from_slice(&20_u16.to_be_bytes());
let cb_hmac = hmac_sha1(&key, &cb_for_hmac);
cb_attrs.extend_from_slice(&stun_attr(0x0008, &cb_hmac));
let cb_req = build_stun_msg(0x0009, &transaction_id, &cb_attrs);
socket.send_to(&cb_req, turn_sock).await
.map_err(|e| anyhow::anyhow!("TURN ChannelBind send failed: {e}"))?;
let (n3, _) = timeout(Duration::from_millis(3000), socket.recv_from(&mut buf))
.await
.map_err(|_| anyhow::anyhow!("TURN ChannelBind response timed out"))?
.map_err(|e| anyhow::anyhow!("TURN ChannelBind recv failed: {e}"))?;
let resp3 = &buf[..n3];
if resp3.len() < 4 {
anyhow::bail!("TURN ChannelBind response too short");
}
let cb_resp_type = u16::from_be_bytes([resp3[0], resp3[1]]);
// 0x0109 = ChannelBind Success Response
if cb_resp_type != 0x0109 {
anyhow::bail!("TURN ChannelBind failed, response type 0x{:04x}", cb_resp_type);
}
Ok(relay_addr_str)
}
// ── STUN message helpers ─────────────────────────────────────────────────────
fn build_stun_msg(msg_type: u16, tx_id: &[u8; 12], attrs: &[u8]) -> Vec<u8> {
let mut msg = Vec::with_capacity(20 + attrs.len());
msg.extend_from_slice(&msg_type.to_be_bytes());
msg.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
msg.extend_from_slice(&0x2112A442_u32.to_be_bytes()); // Magic Cookie
msg.extend_from_slice(tx_id);
msg.extend_from_slice(attrs);
msg
}
fn stun_attr(attr_type: u16, value: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&attr_type.to_be_bytes());
out.extend_from_slice(&(value.len() as u16).to_be_bytes());
out.extend_from_slice(value);
// Pad to 4-byte boundary
let pad = (4 - (value.len() % 4)) % 4;
out.extend(std::iter::repeat_n(0u8, pad));
out
}
// ── Cryptographic primitives (inline, zero external deps) ────────────────────
/// Pure-Rust MD5 hash (16 bytes). Used for TURN long-term credential key derivation.
fn md5_hash(input: &[u8]) -> [u8; 16] {
// RFC 1321 MD5 constants
const S: [u32; 64] = [
7,12,17,22, 7,12,17,22, 7,12,17,22, 7,12,17,22,
5, 9,14,20, 5, 9,14,20, 5, 9,14,20, 5, 9,14,20,
4,11,16,23, 4,11,16,23, 4,11,16,23, 4,11,16,23,
6,10,15,21, 6,10,15,21, 6,10,15,21, 6,10,15,21,
];
const K: [u32; 64] = [
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a,
0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340,
0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8,
0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa,
0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92,
0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
];
let msg_len = input.len();
let bit_len = (msg_len as u64) * 8;
let mut padded = input.to_vec();
padded.push(0x80);
while padded.len() % 64 != 56 {
padded.push(0);
}
padded.extend_from_slice(&bit_len.to_le_bytes());
let mut a0: u32 = 0x67452301;
let mut b0: u32 = 0xefcdab89;
let mut c0: u32 = 0x98badcfe;
let mut d0: u32 = 0x10325476;
for chunk in padded.chunks(64) {
let mut m = [0u32; 16];
for (i, item) in m.iter_mut().enumerate() {
*item = u32::from_le_bytes([chunk[i*4], chunk[i*4+1], chunk[i*4+2], chunk[i*4+3]]);
}
let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
for i in 0..64usize {
let (f, g) = match i {
0..=15 => ((b & c) | (!b & d), i),
16..=31 => ((d & b) | (!d & c), (5*i + 1) % 16),
32..=47 => (b ^ c ^ d, (3*i + 5) % 16),
_ => (c ^ (b | !d), (7*i) % 16),
};
let temp = d;
d = c;
c = b;
b = b.wrapping_add((a.wrapping_add(f).wrapping_add(K[i]).wrapping_add(m[g])).rotate_left(S[i]));
a = temp;
}
a0 = a0.wrapping_add(a);
b0 = b0.wrapping_add(b);
c0 = c0.wrapping_add(c);
d0 = d0.wrapping_add(d);
}
let mut result = [0u8; 16];
result[0..4].copy_from_slice(&a0.to_le_bytes());
result[4..8].copy_from_slice(&b0.to_le_bytes());
result[8..12].copy_from_slice(&c0.to_le_bytes());
result[12..16].copy_from_slice(&d0.to_le_bytes());
result
}
/// HMAC-SHA1 for TURN MESSAGE-INTEGRITY (RFC 2104 + RFC 5389 §15.4).
fn hmac_sha1(key: &[u8], message: &[u8]) -> [u8; 20] {
const BLOCK_SIZE: usize = 64;
let mut k = [0u8; BLOCK_SIZE];
if key.len() > BLOCK_SIZE {
let h = sha1_hash(key);
k[..20].copy_from_slice(&h);
} else {
k[..key.len()].copy_from_slice(key);
}
let mut ipad = [0u8; BLOCK_SIZE];
let mut opad = [0u8; BLOCK_SIZE];
for i in 0..BLOCK_SIZE {
ipad[i] = k[i] ^ 0x36;
opad[i] = k[i] ^ 0x5C;
}
let mut inner = ipad.to_vec();
inner.extend_from_slice(message);
let inner_hash = sha1_hash(&inner);
let mut outer = opad.to_vec();
outer.extend_from_slice(&inner_hash);
sha1_hash(&outer)
}
/// Pure-Rust SHA-1 (RFC 3174).
fn sha1_hash(input: &[u8]) -> [u8; 20] {
let msg_len = input.len();
let bit_len = (msg_len as u64) * 8;
let mut padded = input.to_vec();
padded.push(0x80);
while padded.len() % 64 != 56 {
padded.push(0);
}
padded.extend_from_slice(&bit_len.to_be_bytes());
let mut h: [u32; 5] = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0];
for chunk in padded.chunks(64) {
let mut w = [0u32; 80];
for i in 0..16 {
w[i] = u32::from_be_bytes([chunk[i*4], chunk[i*4+1], chunk[i*4+2], chunk[i*4+3]]);
}
for i in 16..80 {
w[i] = (w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16]).rotate_left(1);
}
let (mut a, mut b, mut c, mut d, mut e) = (h[0], h[1], h[2], h[3], h[4]);
for i in 0..80usize {
let (f, k) = match i {
0..=19 => ((b & c) | (!b & d), 0x5A827999u32),
20..=39 => (b ^ c ^ d, 0x6ED9EBA1),
40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1BBCDC),
_ => (b ^ c ^ d, 0xCA62C1D6),
};
let temp = a.rotate_left(5).wrapping_add(f).wrapping_add(e).wrapping_add(k).wrapping_add(w[i]);
e = d; d = c; c = b.rotate_left(30); b = a; a = temp;
}
h[0] = h[0].wrapping_add(a); h[1] = h[1].wrapping_add(b);
h[2] = h[2].wrapping_add(c); h[3] = h[3].wrapping_add(d);
h[4] = h[4].wrapping_add(e);
}
let mut out = [0u8; 20];
for (i, &v) in h.iter().enumerate() {
out[i*4..(i+1)*4].copy_from_slice(&v.to_be_bytes());
}
out
}

View File

@ -20,3 +20,6 @@ portable-atomic.workspace = true
hmac.workspace = true hmac.workspace = true
sha2.workspace = true sha2.workspace = true
base64 = "0.22" base64 = "0.22"
rustls = "0.23"
tokio-rustls = "0.26"
rcgen = "0.13"

View File

@ -42,8 +42,8 @@ pub struct ApiState {
pub api_token: String, pub api_token: String,
/// Server address for subscription links (e.g. "example.com") /// Server address for subscription links (e.g. "example.com")
pub server_host: String, pub server_host: String,
/// Server listen port
pub server_port: u16, pub server_port: u16,
pub reality_query: String,
} }
// ── API configuration ──────────────────────────────────────────────────────── // ── API configuration ────────────────────────────────────────────────────────
@ -137,6 +137,7 @@ pub async fn start_api_server(
user_stats: Arc<RwLock<HashMap<String, Arc<UserStats>>>>, user_stats: Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
server_host: String, server_host: String,
server_port: u16, server_port: u16,
reality_query: String,
) { ) {
let state = ApiState { let state = ApiState {
access_keys, access_keys,
@ -145,6 +146,7 @@ pub async fn start_api_server(
api_token: config.token.clone(), api_token: config.token.clone(),
server_host, server_host,
server_port, server_port,
reality_query,
}; };
let app = create_api_router(state); let app = create_api_router(state);
@ -416,7 +418,7 @@ async fn handle_subscribe(
// If client requests plain text, return ostp:// share link // If client requests plain text, return ostp:// share link
if accept.contains("text/plain") { if accept.contains("text/plain") {
let link = format!("ostp://{}@{}:{}", key, state.server_host, state.server_port); let link = format!("ostp://{}@{}:{}{}", key, state.server_host, state.server_port, state.reality_query);
return (StatusCode::OK, Json(serde_json::json!({ return (StatusCode::OK, Json(serde_json::json!({
"ok": true, "ok": true,
"data": link "data": link

View File

@ -22,6 +22,15 @@ pub use outbound::{OutboundAction, OutboundConfig, OutboundRule};
pub use api::ApiConfig; pub use api::ApiConfig;
pub use fallback::FallbackConfig; pub use fallback::FallbackConfig;
#[derive(Debug, Clone)]
pub struct RealityServerConfig {
pub dest: String,
pub private_key: String,
pub pbk: String,
pub sid: String,
pub sni_list: Vec<String>,
}
// ── Internal event types ───────────────────────────────────────────────────── // ── Internal event types ─────────────────────────────────────────────────────
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -59,6 +68,8 @@ pub async fn run_server(
api_config: Option<ApiConfig>, api_config: Option<ApiConfig>,
fallback_config: Option<FallbackConfig>, fallback_config: Option<FallbackConfig>,
debug: bool, debug: bool,
reality_query: Option<String>,
reality_config: Option<RealityServerConfig>,
) -> Result<()> { ) -> Result<()> {
let mut keys_map = HashMap::new(); let mut keys_map = HashMap::new();
for key in access_keys { for key in access_keys {
@ -161,8 +172,9 @@ pub async fn run_server(
let parts: Vec<&str> = primary.rsplitn(2, ':').collect(); let parts: Vec<&str> = primary.rsplitn(2, ':').collect();
let server_port: u16 = parts.first().and_then(|p| p.parse().ok()).unwrap_or(50000); let server_port: u16 = parts.first().and_then(|p| p.parse().ok()).unwrap_or(50000);
let server_host = parts.get(1).unwrap_or(&"0.0.0.0").to_string(); let server_host = parts.get(1).unwrap_or(&"0.0.0.0").to_string();
let rq = reality_query.clone().unwrap_or_default();
tokio::spawn(async move { tokio::spawn(async move {
api::start_api_server(api_cfg, api_keys, api_stats, server_host, server_port).await; api::start_api_server(api_cfg, api_keys, api_stats, server_host, server_port, rq).await;
}); });
} }
} }
@ -213,8 +225,25 @@ pub async fn run_server(
let key_count = shared_keys.read().unwrap().len(); let key_count = shared_keys.read().unwrap().len();
tracing::info!(listeners = bind_addrs.len(), keys = key_count, "server started"); tracing::info!(listeners = bind_addrs.len(), keys = key_count, "server started");
tracing::info!("ARQ config: max_reorder=16384, reorder_buf=8192, sent_history=32768, rto=100ms"); tracing::info!("ARQ config: max_reorder=16384, reorder_buf=8192, sent_history=32768, rto=100ms");
let tls_config = if let Some(rc) = reality_config {
let subject_alt_names = rc.sni_list.clone();
let cert = rcgen::generate_simple_self_signed(subject_alt_names)?;
let cert_der = cert.cert.der().to_vec();
let priv_key = cert.key_pair.serialize_der();
let server_config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(
vec![rustls::pki_types::CertificateDer::from(cert_der)],
rustls::pki_types::PrivatePkcs8KeyDer::from(priv_key).into(),
)?;
Some(std::sync::Arc::new(server_config))
} else {
None
};
tokio::select! { tokio::select! {
res = run_server_loop(bind_addrs.clone(), primary_socket, sockets, dispatcher, ui_cmd_rx, ui_event_tx, shared_keys, outbound, debug) => { res = run_server_loop(bind_addrs.clone(), primary_socket, sockets, dispatcher, ui_cmd_rx, ui_event_tx, shared_keys, outbound, debug, tls_config) => {
if let Err(e) = res { if let Err(e) = res {
tracing::error!("Server error: {e}"); tracing::error!("Server error: {e}");
} }
@ -239,6 +268,7 @@ async fn run_server_loop(
shared_keys: std::sync::Arc<std::sync::RwLock<HashMap<String, ()>>>, shared_keys: std::sync::Arc<std::sync::RwLock<HashMap<String, ()>>>,
outbound: Option<OutboundConfig>, outbound: Option<OutboundConfig>,
debug: bool, debug: bool,
tls_config: Option<std::sync::Arc<rustls::ServerConfig>>,
) -> Result<()> { ) -> Result<()> {
let mut remotes: HashMap<(u32, u16), RemoteState> = HashMap::new(); let mut remotes: HashMap<(u32, u16), RemoteState> = HashMap::new();
let (stream_tx, mut stream_rx) = mpsc::unbounded_channel::<(u32, u16, Vec<u8>)>(); let (stream_tx, mut stream_rx) = mpsc::unbounded_channel::<(u32, u16, Vec<u8>)>();
@ -257,7 +287,11 @@ async fn run_server_loop(
loop { loop {
match sock_clone.recv_from(&mut buf).await { match sock_clone.recv_from(&mut buf).await {
Ok((size, peer)) => { Ok((size, peer)) => {
let packet = Bytes::copy_from_slice(&buf[..size]); let packet = if size >= 12 && buf[0] == 0x80 {
Bytes::copy_from_slice(&buf[12..size])
} else {
Bytes::copy_from_slice(&buf[..size])
};
if tx.send((packet, peer)).await.is_err() { if tx.send((packet, peer)).await.is_err() {
break; break;
} }
@ -275,6 +309,7 @@ async fn run_server_loop(
let shared_keys_clone = shared_keys.clone(); let shared_keys_clone = shared_keys.clone();
let udp_tx_clone = udp_tx.clone(); let udp_tx_clone = udp_tx.clone();
let tls_cfg = tls_config.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Ok(listener) = tokio::net::TcpListener::bind(&addr).await { if let Ok(listener) = tokio::net::TcpListener::bind(&addr).await {
tracing::info!("TCP (UoT) listener bound to {}", addr); tracing::info!("TCP (UoT) listener bound to {}", addr);
@ -312,9 +347,24 @@ async fn run_server_loop(
let tm = tcp_map_clone.clone(); let tm = tcp_map_clone.clone();
let keys = shared_keys_clone.clone(); let keys = shared_keys_clone.clone();
let tx = udp_tx_clone.clone(); let tx = udp_tx_clone.clone();
let tls = tls_cfg.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = crate::transport::uot::handle_tcp_connection(stream, peer_addr, keys, tx, tm).await { if let Some(cfg) = tls {
tracing::warn!("UoT connection from {} closed: {}", peer_addr, e); let acceptor = tokio_rustls::TlsAcceptor::from(cfg);
match acceptor.accept(stream).await {
Ok(tls_stream) => {
if let Err(e) = crate::transport::uot::handle_tcp_connection(tls_stream, peer_addr, keys, tx, tm).await {
tracing::warn!("UoT TLS connection from {} closed: {}", peer_addr, e);
}
}
Err(e) => {
tracing::warn!("UoT TLS handshake from {} failed: {}", peer_addr, e);
}
}
} else {
if let Err(e) = crate::transport::uot::handle_tcp_connection(stream, peer_addr, keys, tx, tm).await {
tracing::warn!("UoT connection from {} closed: {}", peer_addr, e);
}
} }
}); });
} }
@ -391,7 +441,10 @@ async fn run_server_loop(
} }
} }
if !sent_tcp { if !sent_tcp {
let _ = socket.send_to(&resp, peer_addr).await?; let mut out = bytes::BytesMut::with_capacity(12 + resp.len());
out.extend_from_slice(&[0x80, 0x60, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44]);
out.extend_from_slice(&resp);
let _ = socket.send_to(&out.freeze(), peer_addr).await?;
} }
let _ = ui_event_tx.send(UiEvent::Tx { peer: peer_ip, bytes: resp_len }); let _ = ui_event_tx.send(UiEvent::Tx { peer: peer_ip, bytes: resp_len });
} }
@ -475,7 +528,10 @@ async fn run_server_loop(
} }
} }
if !sent_tcp { if !sent_tcp {
let _ = socket.send_to(&frame, peer_addr).await?; let mut out = bytes::BytesMut::with_capacity(12 + frame.len());
out.extend_from_slice(&[0x80, 0x60, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44]);
out.extend_from_slice(&frame);
let _ = socket.send_to(&out.freeze(), peer_addr).await?;
} }
} }
for sid in dropped_sessions { for sid in dropped_sessions {

View File

@ -11,13 +11,16 @@ use tokio::net::TcpStream;
use tokio::sync::{mpsc, RwLock}; use tokio::sync::{mpsc, RwLock};
use tracing::info; use tracing::info;
pub async fn handle_tcp_connection( pub async fn handle_tcp_connection<S>(
mut stream: TcpStream, mut stream: S,
peer_addr: SocketAddr, peer_addr: SocketAddr,
shared_keys: Arc<StdRwLock<HashMap<String, ()>>>, shared_keys: Arc<StdRwLock<HashMap<String, ()>>>,
udp_tx: mpsc::Sender<(Bytes, SocketAddr)>, udp_tx: mpsc::Sender<(Bytes, SocketAddr)>,
tcp_map: Arc<RwLock<HashMap<SocketAddr, mpsc::Sender<Bytes>>>>, tcp_map: Arc<RwLock<HashMap<SocketAddr, mpsc::Sender<Bytes>>>>,
) -> Result<()> { ) -> Result<()>
where
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
{
// 1. Read HTTP Handshake // 1. Read HTTP Handshake
let mut buf = [0u8; 4096]; let mut buf = [0u8; 4096];
let mut header_len = 0; let mut header_len = 0;
@ -123,7 +126,7 @@ pub async fn handle_tcp_connection(
let leftover = &buf[headers_end..header_len]; let leftover = &buf[headers_end..header_len];
// Process streams // Process streams
let (mut read_half, mut write_half) = stream.into_split(); let (mut read_half, mut write_half) = tokio::io::split(stream);
// Spawn writer task // Spawn writer task
let peer_clone = peer_addr; let peer_clone = peer_addr;
@ -181,7 +184,10 @@ pub async fn handle_tcp_connection(
Ok(()) Ok(())
} }
async fn send_404(stream: &mut TcpStream) -> Result<()> { async fn send_404<S>(stream: &mut S) -> Result<()>
where
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
{
let body = "Not Found"; let body = "Not Found";
let resp = format!( let resp = format!(
"HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",

View File

@ -55,20 +55,48 @@ fn parse_ostp_link(link: &str) -> Result<ClientConfig> {
let host = parsed.host_str().ok_or_else(|| anyhow!("Missing host in share link"))?; let host = parsed.host_str().ok_or_else(|| anyhow!("Missing host in share link"))?;
let port = parsed.port().ok_or_else(|| anyhow!("Missing port in share link"))?; let port = parsed.port().ok_or_else(|| anyhow!("Missing port in share link"))?;
let server = format!("{host}:{port}"); let server = format!("{host}:{port}");
let mut sni = String::new();
let mut fp = String::new();
let mut pbk = String::new();
let mut sid = String::new();
let mut spx = String::new();
let mut transport_mode = String::from("udp");
for (k, v) in parsed.query_pairs() {
match k.as_ref() {
"sni" => sni = v.into_owned(),
"fp" => fp = v.into_owned(),
"pbk" => pbk = v.into_owned(),
"sid" => sid = v.into_owned(),
"spx" => spx = v.into_owned(),
"type" => transport_mode = v.into_owned(),
_ => {}
}
}
Ok(ClientConfig { Ok(ClientConfig {
server, server,
access_key, access_key,
mtu: None, mtu: None,
transport: None, transport: Some(TransportConfigRaw {
socks5_bind: Some("127.0.0.1:1088".to_string()), // Fallback to standard SOCKS5 port mode: Some(transport_mode),
stealth_sni: Some(sni.clone()),
stealth_port: Some(443),
}),
socks5_bind: Some("127.0.0.1:1088".to_string()),
tun: Some(TunConfig { tun: Some(TunConfig {
enable: false, // Default to proxy, configurable via settings GUI enable: false,
wintun_path: Some("./wintun.dll".to_string()), wintun_path: Some("./wintun.dll".to_string()),
ipv4_address: Some("10.1.0.2/24".to_string()), ipv4_address: Some("10.1.0.2/24".to_string()),
dns: None, dns: None,
}), }),
turn: None, reality: Some(RealityConfigRaw {
sni,
fp,
pbk,
sid,
spx,
}),
debug: Some(false), debug: Some(false),
exclude: None, exclude: None,
mux: None, mux: None,
@ -141,7 +169,7 @@ impl UnifiedConfig {
struct ServerConfig { struct ServerConfig {
listen: ListenConfig, listen: ListenConfig,
access_keys: Vec<String>, access_keys: Vec<String>,
turn_server: Option<String>, reality: Option<RealityServerConfigRaw>,
debug: Option<bool>, debug: Option<bool>,
outbound: Option<OutboundConfig>, outbound: Option<OutboundConfig>,
api: Option<ApiConfig>, api: Option<ApiConfig>,
@ -193,7 +221,7 @@ struct ClientConfig {
mtu: Option<usize>, mtu: Option<usize>,
socks5_bind: Option<String>, socks5_bind: Option<String>,
tun: Option<TunConfig>, tun: Option<TunConfig>,
turn: Option<TurnConfigRaw>, reality: Option<RealityConfigRaw>,
debug: Option<bool>, debug: Option<bool>,
exclude: Option<ExcludeConfig>, exclude: Option<ExcludeConfig>,
mux: Option<MuxConfig>, mux: Option<MuxConfig>,
@ -215,12 +243,24 @@ struct TunConfig {
dns: Option<String>, dns: Option<String>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize, Clone)]
struct TurnConfigRaw { struct RealityConfigRaw {
sni: String,
fp: String,
pbk: String,
sid: String,
spx: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
struct RealityServerConfigRaw {
#[serde(default)]
enabled: bool, enabled: bool,
server_addr: String, dest: String,
username: Option<String>, private_key: String,
access_key: Option<String>, pbk: String,
sid: String,
sni_list: Vec<String>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
@ -469,6 +509,16 @@ async fn run_app() -> Result<()> {
// Target web server (e.g., local nginx or caddy) // Target web server (e.g., local nginx or caddy)
"target": "127.0.0.1:8080" "target": "127.0.0.1:8080"
}}, }},
// Reality (XTLS) / UoT Masquerade parameters
"reality": {{
"enabled": false,
"dest": "www.microsoft.com:443",
"private_key": "",
"pbk": "",
"sid": "",
"sni_list": ["www.microsoft.com"]
}},
"debug": false "debug": false
}}"#, key) }}"#, key)
} else { } else {
@ -501,18 +551,19 @@ async fn run_app() -> Result<()> {
"processes": [] "processes": []
}}, }},
// STUN/TURN server settings to bypass UDP blocks by mimicking WebRTC call traffic // Reality (XTLS) / WebRTC Masquerade parameters
"turn": {{ "reality": {{
"enabled": false, "dest": "www.microsoft.com:443",
"server_addr": "127.0.0.1:3478", "private_key": "",
"username": "ostpuser", "pbk": "",
"access_key": "ostppassword" "sid": "",
"sni_list": ["www.microsoft.com"]
}}, }},
// Transport Mode: "udp" (default) or "uot" (xHTTP Stealth / UDP over TCP) // Transport Mode: "udp" (default WebRTC masquerade) or "uot" (TCP XTLS-Reality)
"transport": {{ "transport": {{
"mode": "udp", "mode": "udp",
"stealth_sni": "vk.com", "stealth_sni": "www.microsoft.com",
"stealth_port": 443 "stealth_port": 443
}}, }},
@ -529,11 +580,15 @@ async fn run_app() -> Result<()> {
if is_server { if is_server {
let mut stripped = json_comments::StripComments::new(content.as_bytes()); let mut stripped = json_comments::StripComments::new(content.as_bytes());
if let Ok(config) = serde_json::from_reader::<_, UnifiedConfig>(&mut stripped) { if let Ok(config) = serde_json::from_reader::<_, UnifiedConfig>(&mut stripped) {
if let AppMode::Server(s) = config.mode { if let AppMode::Server(s) = &config.mode {
let key = &s.access_keys[0]; let key = &s.access_keys[0];
let host = get_or_ask_public_ip(&args.config); let host = get_or_ask_public_ip(&args.config);
let mut link = format!("ostp://{}@{}:50000", key, host);
if let Some(r) = &s.reality {
link = format!("{}?security=reality&sni={}&pbk={}&sid={}&type=udp", link, r.sni_list.first().unwrap_or(&String::new()), r.pbk, r.sid);
}
println!("\n Share link for client distribution:"); println!("\n Share link for client distribution:");
println!(" ostp://{}@{}:50000", key, host); println!(" {}", link);
} }
} }
} }
@ -575,7 +630,11 @@ async fn run_app() -> Result<()> {
println!("\n Client share links from {:?}:", args.config); println!("\n Client share links from {:?}:", args.config);
for (idx, key) in server_cfg.access_keys.iter().enumerate() { for (idx, key) in server_cfg.access_keys.iter().enumerate() {
println!(" [{}] ostp://{}@{}:{}", idx + 1, key, host, port); let mut link = format!("ostp://{}@{}:{}", key, host, port);
if let Some(r) = &server_cfg.reality {
link = format!("{}?security=reality&sni={}&pbk={}&sid={}&type=udp", link, r.sni_list.first().unwrap_or(&String::new()), r.pbk, r.sid);
}
println!(" [{}] {}", idx + 1, link);
} }
return Ok(()); return Ok(());
} }
@ -589,8 +648,10 @@ async fn run_app() -> Result<()> {
AppMode::Server(server_cfg) => { AppMode::Server(server_cfg) => {
let listen_addrs = server_cfg.listen.addresses(); let listen_addrs = server_cfg.listen.addresses();
println!("[ostp] Starting server on {:?}", listen_addrs); println!("[ostp] Starting server on {:?}", listen_addrs);
if let Some(turn) = server_cfg.turn_server { if let Some(ref reality) = server_cfg.reality {
println!("[ostp] TURN relay enabled: {}", turn); if reality.enabled {
println!("[ostp] Reality mode enabled (dest: {})", reality.dest);
}
} }
let debug = server_cfg.debug.unwrap_or(false); let debug = server_cfg.debug.unwrap_or(false);
let outbound = server_cfg.outbound.map(|o| ostp_server::OutboundConfig { let outbound = server_cfg.outbound.map(|o| ostp_server::OutboundConfig {
@ -619,8 +680,22 @@ async fn run_app() -> Result<()> {
listen: f.listen.unwrap_or_else(|| "0.0.0.0:443".to_string()), listen: f.listen.unwrap_or_else(|| "0.0.0.0:443".to_string()),
target: f.target.unwrap_or_else(|| "127.0.0.1:8080".to_string()), target: f.target.unwrap_or_else(|| "127.0.0.1:8080".to_string()),
}); });
let mut rq = None;
let mut rc = None;
if let Some(r) = server_cfg.reality {
if r.enabled {
rq = Some(format!("?security=reality&sni={}&pbk={}&sid={}&type=udp", r.sni_list.first().unwrap_or(&String::new()), r.pbk, r.sid));
rc = Some(ostp_server::RealityServerConfig {
sni_list: r.sni_list.clone(),
dest: r.dest,
private_key: r.private_key,
pbk: r.pbk,
sid: r.sid,
});
}
}
// Pass all listen addresses for multi-listener support // Pass all listen addresses for multi-listener support
ostp_server::run_server(listen_addrs, server_cfg.access_keys, outbound, api_config, fallback_config, debug).await?; ostp_server::run_server(listen_addrs, server_cfg.access_keys, outbound, api_config, fallback_config, debug, rq, rc).await?;
} }
AppMode::Client(client_cfg) => { AppMode::Client(client_cfg) => {
run_client_directly(client_cfg).await?; run_client_directly(client_cfg).await?;
@ -635,7 +710,7 @@ async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> {
let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false); let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false);
let mode_str = if is_tun_enabled { "tun" } else { "proxy" }; let mode_str = if is_tun_enabled { "tun" } else { "proxy" };
println!("[ostp] Starting client (mode={}, server={})", mode_str, client_cfg.server); println!("[ostp] Starting client (mode={}, server={})", mode_str, client_cfg.server);
let turn_cfg = client_cfg.turn.as_ref(); let reality_cfg = client_cfg.reality.as_ref();
let client_conf = ostp_client::config::ClientConfig { let client_conf = ostp_client::config::ClientConfig {
mode: if is_tun_enabled { "tun".to_string() } else { "proxy".to_string() }, mode: if is_tun_enabled { "tun".to_string() } else { "proxy".to_string() },
debug: client_cfg.debug.unwrap_or(false), debug: client_cfg.debug.unwrap_or(false),
@ -644,18 +719,20 @@ async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> {
local_bind_addr: "0.0.0.0:0".to_string(), local_bind_addr: "0.0.0.0:0".to_string(),
access_key: client_cfg.access_key.clone(), access_key: client_cfg.access_key.clone(),
handshake_timeout_ms: 5000, handshake_timeout_ms: 5000,
io_timeout_ms: 5000, io_timeout_ms: 2500,
mtu: client_cfg.mtu.unwrap_or(1350), mtu: client_cfg.mtu.unwrap_or(1350),
keepalive_interval_sec: 5,
}, },
local_proxy: ostp_client::config::LocalProxyConfig { local_proxy: ostp_client::config::LocalProxyConfig {
bind_addr: client_cfg.socks5_bind.clone().unwrap_or_else(|| "127.0.0.1:1088".to_string()), bind_addr: client_cfg.socks5_bind.clone().unwrap_or_else(|| "127.0.0.1:1088".to_string()),
connect_timeout_ms: 5000, connect_timeout_ms: 5000,
}, },
turn: ostp_client::config::TurnConfig { reality: ostp_client::config::RealityConfig {
enabled: turn_cfg.map(|t| t.enabled).unwrap_or(false), sni: reality_cfg.map(|t| t.sni.clone()).unwrap_or_default(),
server_addr: turn_cfg.map(|t| t.server_addr.clone()).unwrap_or_default(), fp: reality_cfg.map(|t| t.fp.clone()).unwrap_or_default(),
username: turn_cfg.and_then(|t| t.username.clone()).unwrap_or_default(), pbk: reality_cfg.map(|t| t.pbk.clone()).unwrap_or_default(),
access_key: turn_cfg.and_then(|t| t.access_key.clone()).unwrap_or_default(), sid: reality_cfg.map(|t| t.sid.clone()).unwrap_or_default(),
spx: reality_cfg.map(|t| t.spx.clone()).unwrap_or_default(),
}, },
exclusions: ostp_client::config::ExclusionConfig { exclusions: ostp_client::config::ExclusionConfig {
domains: client_cfg.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(), domains: client_cfg.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(),