mirror of https://github.com/ospab/ostp.git
Implement XTLS-Reality masquerade for UoT/TCP and fix MTU/config settings
This commit is contained in:
parent
ef242bf6f4
commit
3e511f1fc5
|
|
@ -123,6 +123,28 @@ version = "1.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.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]]
|
||||
name = "axum"
|
||||
version = "0.8.9"
|
||||
|
|
@ -224,6 +246,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
|
|
@ -327,6 +351,15 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
|
|
@ -404,6 +437,15 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
|
@ -426,6 +468,12 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
|
|
@ -457,6 +505,12 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
|
|
@ -523,6 +577,18 @@ dependencies = [
|
|||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
|
|
@ -820,6 +886,16 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.98"
|
||||
|
|
@ -909,6 +985,12 @@ dependencies = [
|
|||
"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]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
|
|
@ -970,6 +1052,7 @@ dependencies = [
|
|||
"portable-atomic",
|
||||
"rand",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
|
|
@ -977,6 +1060,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tracing",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1022,11 +1106,14 @@ dependencies = [
|
|||
"ostp-core",
|
||||
"portable-atomic",
|
||||
"rand",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
]
|
||||
|
|
@ -1045,6 +1132,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
|
|
@ -1095,6 +1192,12 @@ dependencies = [
|
|||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
|
|
@ -1122,6 +1225,12 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
|
|
@ -1149,7 +1258,20 @@ version = "0.6.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
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]]
|
||||
|
|
@ -1177,7 +1299,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
|||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
|
|
@ -1198,6 +1320,8 @@ version = "0.23.40"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
|
|
@ -1221,6 +1345,7 @@ version = "0.103.13"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
|
|
@ -1468,6 +1593,25 @@ dependencies = [
|
|||
"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]]
|
||||
name = "tinystr"
|
||||
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"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.121"
|
||||
|
|
@ -1753,6 +1906,24 @@ dependencies = [
|
|||
"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]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
|
|
@ -1978,12 +2149,27 @@ dependencies = [
|
|||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||
|
||||
[[package]]
|
||||
name = "yasna"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||
dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.2"
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@ json_comments = "0.2"
|
|||
portable-atomic.workspace = true
|
||||
chrono = "0.4"
|
||||
socket2 = "0.6.3"
|
||||
rustls = { version = "0.23.40", default-features = false, features = ["ring", "std"] }
|
||||
tokio-rustls = { version = "0.26.0", default-features = false, features = ["ring"] }
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
||||
futures-util = "0.3.32"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.8"
|
||||
base64 = "0.22.1"
|
||||
webpki-roots = "0.26"
|
||||
rustls-pki-types = "1.7"
|
||||
|
|
|
|||
|
|
@ -38,11 +38,16 @@ pub struct BridgeMetrics {
|
|||
pub connection_state: AtomicU8,
|
||||
}
|
||||
|
||||
async fn send_datagram(socket: &crate::transport::Transport, frame: &Bytes, turn_enabled: bool) -> std::io::Result<usize> {
|
||||
if turn_enabled {
|
||||
let mut out = bytes::BytesMut::with_capacity(4 + frame.len());
|
||||
bytes::BufMut::put_u16(&mut out, 0x4000);
|
||||
bytes::BufMut::put_u16(&mut out, frame.len() as u16);
|
||||
async fn send_datagram(socket: &crate::transport::Transport, frame: &Bytes, webrtc_masquerade: bool) -> std::io::Result<usize> {
|
||||
if webrtc_masquerade {
|
||||
let mut out = bytes::BytesMut::with_capacity(12 + frame.len());
|
||||
// Fake SRTP Header:
|
||||
// [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);
|
||||
socket.send(&out.freeze()).await
|
||||
} else {
|
||||
|
|
@ -66,10 +71,7 @@ pub struct Bridge {
|
|||
handshake_timeout_ms: u64,
|
||||
io_timeout_ms: u64,
|
||||
|
||||
pub turn_enabled: bool,
|
||||
pub turn_server: String,
|
||||
pub turn_username: String,
|
||||
pub turn_password: String,
|
||||
pub keepalive_interval_sec: u64,
|
||||
pub mode: String,
|
||||
pub mux_enabled: bool,
|
||||
pub mux_sessions: usize,
|
||||
|
|
@ -78,6 +80,7 @@ pub struct Bridge {
|
|||
pub stealth_sni: String,
|
||||
pub stealth_port: u16,
|
||||
pub mtu: usize,
|
||||
pub reality_enabled: bool,
|
||||
|
||||
metrics: Arc<BridgeMetrics>,
|
||||
sample_sent: u64,
|
||||
|
|
@ -100,10 +103,7 @@ impl Bridge {
|
|||
handshake_timeout_ms: config.ostp.handshake_timeout_ms,
|
||||
io_timeout_ms: config.ostp.io_timeout_ms,
|
||||
|
||||
turn_enabled: config.turn.enabled,
|
||||
turn_server: config.turn.server_addr.clone(),
|
||||
turn_username: config.turn.username.clone(),
|
||||
turn_password: config.turn.access_key.clone(),
|
||||
keepalive_interval_sec: config.ostp.keepalive_interval_sec,
|
||||
mode: config.mode.clone(),
|
||||
mux_enabled: config.multiplex.enabled,
|
||||
mux_sessions: config.multiplex.sessions.max(1),
|
||||
|
|
@ -112,6 +112,7 @@ impl Bridge {
|
|||
stealth_sni: config.transport.stealth_sni.clone(),
|
||||
stealth_port: config.transport.stealth_port,
|
||||
mtu: config.ostp.mtu,
|
||||
reality_enabled: !config.reality.pbk.is_empty(),
|
||||
|
||||
metrics,
|
||||
sample_sent: 0,
|
||||
|
|
@ -131,7 +132,7 @@ impl Bridge {
|
|||
proxy_tx: mpsc::UnboundedSender<(u16, ProxyToClientMsg)>,
|
||||
) -> Result<()> {
|
||||
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 init_msg = if self.mode == "tun" {
|
||||
"Bridge initialized (TUN mode)".to_string()
|
||||
|
|
@ -220,7 +221,7 @@ impl Bridge {
|
|||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -273,19 +274,14 @@ impl Bridge {
|
|||
let session_index = sessions.len();
|
||||
let socket_clone = sock.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 {
|
||||
let mut buf = vec![0_u8; 65535];
|
||||
loop {
|
||||
match socket_clone.recv(&mut buf).await {
|
||||
Ok(n) => {
|
||||
let inbound = if is_turn && n >= 4 && buf[0] == 0x40 && buf[1] == 0x00 {
|
||||
let len = u16::from_be_bytes([buf[2], buf[3]]) as usize;
|
||||
if 4 + len <= n {
|
||||
Bytes::copy_from_slice(&buf[4..4+len])
|
||||
} else {
|
||||
Bytes::copy_from_slice(&buf[..n])
|
||||
}
|
||||
let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 {
|
||||
Bytes::copy_from_slice(&buf[12..n])
|
||||
} else {
|
||||
Bytes::copy_from_slice(&buf[..n])
|
||||
};
|
||||
|
|
@ -368,15 +364,14 @@ impl Bridge {
|
|||
let session_index = new_sessions.len();
|
||||
let socket_clone = sock.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 {
|
||||
let mut buf = vec![0_u8; 65535];
|
||||
loop {
|
||||
match socket_clone.recv(&mut buf).await {
|
||||
Ok(n) => {
|
||||
let inbound = if is_turn && n >= 4 && buf[0] == 0x40 && buf[1] == 0x00 {
|
||||
let len = u16::from_be_bytes([buf[2], buf[3]]) as usize;
|
||||
if 4 + len <= n { Bytes::copy_from_slice(&buf[4..4+len]) } else { Bytes::copy_from_slice(&buf[..n]) }
|
||||
let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 {
|
||||
Bytes::copy_from_slice(&buf[12..n])
|
||||
} else {
|
||||
Bytes::copy_from_slice(&buf[..n])
|
||||
};
|
||||
|
|
@ -480,19 +475,14 @@ impl Bridge {
|
|||
let session_index = new_sessions.len();
|
||||
let socket_clone = sock.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 {
|
||||
let mut buf = vec![0_u8; 65535];
|
||||
loop {
|
||||
match socket_clone.recv(&mut buf).await {
|
||||
Ok(n) => {
|
||||
let inbound = if is_turn && n >= 4 && buf[0] == 0x40 && buf[1] == 0x00 {
|
||||
let len = u16::from_be_bytes([buf[2], buf[3]]) as usize;
|
||||
if 4 + len <= n {
|
||||
Bytes::copy_from_slice(&buf[4..4+len])
|
||||
} else {
|
||||
Bytes::copy_from_slice(&buf[..n])
|
||||
}
|
||||
let inbound = if is_webrtc && n >= 12 && buf[0] == 0x80 {
|
||||
Bytes::copy_from_slice(&buf[12..n])
|
||||
} else {
|
||||
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)) {
|
||||
// Must go through send_datagram() for TURN-mode wrapping;
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Send Relay KeepAlive (Force NAT/Server Persistence)
|
||||
let ka_payload = Bytes::from(RelayMessage::KeepAlive.encode());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -569,7 +559,7 @@ impl Bridge {
|
|||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -632,7 +622,7 @@ impl Bridge {
|
|||
let out_payload = Bytes::from(relay_msg.encode());
|
||||
match session.machine.on_event(OstpEvent::Outbound(stream_id, out_payload)) {
|
||||
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);
|
||||
if self.debug {
|
||||
let _ = tx.send(UiEvent::Log(format!(
|
||||
|
|
@ -646,7 +636,7 @@ impl Bridge {
|
|||
let mut sent = 0usize;
|
||||
for item in list {
|
||||
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);
|
||||
sent += 1;
|
||||
}
|
||||
|
|
@ -817,64 +807,7 @@ impl Bridge {
|
|||
}
|
||||
};
|
||||
|
||||
if self.turn_enabled && self.transport_mode != "wss" {
|
||||
let udp_socket = match &socket {
|
||||
crate::transport::Transport::Udp(sock) => sock,
|
||||
_ => return Err(anyhow::anyhow!("TURN requires UDP transport")),
|
||||
};
|
||||
let turn_addr = if self.turn_server.contains(':') {
|
||||
self.turn_server.clone()
|
||||
} else {
|
||||
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
|
||||
// Connection to remote is handled inside try_connect_transport
|
||||
|
||||
let start = Instant::now();
|
||||
let action = machine.on_event(OstpEvent::Start)?;
|
||||
|
|
@ -897,7 +830,7 @@ impl Bridge {
|
|||
if attempt > 0 {
|
||||
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);
|
||||
|
||||
match timeout(Duration::from_millis(attempt_timeout_ms), socket.recv(&mut buf)).await {
|
||||
|
|
@ -923,7 +856,7 @@ impl Bridge {
|
|||
if attempt > 0 {
|
||||
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 {
|
||||
Ok(Ok(n)) => {
|
||||
size = n;
|
||||
|
|
@ -950,13 +883,8 @@ impl Bridge {
|
|||
self.metrics.bytes_recv.fetch_add(size as u64, Ordering::Relaxed);
|
||||
tracing::info!("Handshake response received: {} bytes", size);
|
||||
|
||||
let inbound = if self.turn_enabled && size >= 4 && buf[0] == 0x40 && buf[1] == 0x00 {
|
||||
let len = u16::from_be_bytes([buf[2], buf[3]]) as usize;
|
||||
if 4 + len <= size {
|
||||
Bytes::copy_from_slice(&buf[4..4+len])
|
||||
} else {
|
||||
Bytes::copy_from_slice(&buf[..size])
|
||||
}
|
||||
let inbound = if (self.transport_mode == "udp") && size >= 12 && buf[0] == 0x80 {
|
||||
Bytes::copy_from_slice(&buf[12..size])
|
||||
} else {
|
||||
Bytes::copy_from_slice(&buf[..size])
|
||||
};
|
||||
|
|
@ -975,15 +903,12 @@ impl Bridge {
|
|||
self.handshake_timeout_ms = cfg.ostp.handshake_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.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_sessions = cfg.multiplex.sessions.max(1);
|
||||
self.transport_mode = cfg.transport.mode.clone();
|
||||
self.stealth_sni = cfg.transport.stealth_sni.clone();
|
||||
self.stealth_port = cfg.transport.stealth_port;
|
||||
self.reality_enabled = !cfg.reality.pbk.is_empty();
|
||||
}
|
||||
|
||||
async fn try_connect_transport(
|
||||
|
|
@ -1002,7 +927,7 @@ impl Bridge {
|
|||
port
|
||||
};
|
||||
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?;
|
||||
Ok(crate::transport::Transport::Uot { tx, rx })
|
||||
} else {
|
||||
|
|
@ -1053,3 +978,4 @@ fn synthesize_nat64(ip: std::net::Ipv4Addr) -> std::net::Ipv6Addr {
|
|||
)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ pub struct ClientConfig {
|
|||
pub debug: bool,
|
||||
pub ostp: OstpConfig,
|
||||
pub local_proxy: LocalProxyConfig,
|
||||
pub turn: TurnConfig,
|
||||
pub reality: RealityConfig,
|
||||
#[serde(default)]
|
||||
pub transport: TransportConfig,
|
||||
#[serde(default)]
|
||||
|
|
@ -48,8 +48,12 @@ pub struct OstpConfig {
|
|||
pub io_timeout_ms: u64,
|
||||
#[serde(default = "default_mtu")]
|
||||
pub mtu: usize,
|
||||
#[serde(default = "default_keepalive")]
|
||||
pub keepalive_interval_sec: u64,
|
||||
}
|
||||
|
||||
fn default_keepalive() -> u64 { 5 }
|
||||
|
||||
fn default_mtu() -> usize { 1350 }
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -88,11 +92,17 @@ impl Default for TransportConfig {
|
|||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct TurnConfig {
|
||||
pub enabled: bool,
|
||||
pub server_addr: String,
|
||||
pub username: String,
|
||||
pub access_key: String,
|
||||
pub struct RealityConfig {
|
||||
#[serde(default)]
|
||||
pub sni: String,
|
||||
#[serde(default)]
|
||||
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,
|
||||
io_timeout_ms: 2500,
|
||||
mtu: default_mtu(),
|
||||
keepalive_interval_sec: default_keepalive(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -126,7 +137,7 @@ impl Default for ClientConfig {
|
|||
debug: false,
|
||||
ostp: OstpConfig::default(),
|
||||
local_proxy: LocalProxyConfig::default(),
|
||||
turn: TurnConfig::default(),
|
||||
reality: RealityConfig::default(),
|
||||
transport: TransportConfig::default(),
|
||||
exclusions: ExclusionConfig::default(),
|
||||
multiplex: MultiplexConfig::default(),
|
||||
|
|
@ -158,7 +169,7 @@ struct RawUnifiedConfig {
|
|||
tun: Option<RawTunSection>,
|
||||
exclude: Option<RawExcludeSection>,
|
||||
mux: Option<RawMuxSection>,
|
||||
turn: Option<RawTurnSection>,
|
||||
reality: Option<RawRealitySection>,
|
||||
transport: Option<RawTransportSection>,
|
||||
}
|
||||
|
||||
|
|
@ -189,11 +200,12 @@ struct RawMuxSection {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawTurnSection {
|
||||
enabled: Option<bool>,
|
||||
server_addr: Option<String>,
|
||||
username: Option<String>,
|
||||
access_key: Option<String>,
|
||||
struct RawRealitySection {
|
||||
sni: Option<String>,
|
||||
fp: Option<String>,
|
||||
pbk: Option<String>,
|
||||
sid: Option<String>,
|
||||
spx: Option<String>,
|
||||
}
|
||||
|
||||
impl ClientConfig {
|
||||
|
|
@ -235,16 +247,18 @@ impl ClientConfig {
|
|||
handshake_timeout_ms: 5000,
|
||||
io_timeout_ms: 2500,
|
||||
mtu,
|
||||
keepalive_interval_sec: default_keepalive(),
|
||||
},
|
||||
local_proxy: LocalProxyConfig {
|
||||
bind_addr: socks5,
|
||||
connect_timeout_ms: 15000,
|
||||
},
|
||||
turn: TurnConfig {
|
||||
enabled: raw.turn.as_ref().and_then(|t| t.enabled).unwrap_or(false),
|
||||
server_addr: raw.turn.as_ref().and_then(|t| t.server_addr.clone()).unwrap_or_default(),
|
||||
username: raw.turn.as_ref().and_then(|t| t.username.clone()).unwrap_or_default(),
|
||||
access_key: raw.turn.as_ref().and_then(|t| t.access_key.clone()).unwrap_or_default(),
|
||||
reality: RealityConfig {
|
||||
sni: raw.reality.as_ref().and_then(|t| t.sni.clone()).unwrap_or_default(),
|
||||
fp: raw.reality.as_ref().and_then(|t| t.fp.clone()).unwrap_or_default(),
|
||||
pbk: raw.reality.as_ref().and_then(|t| t.pbk.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 {
|
||||
mode: raw.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(|| "udp".to_string()),
|
||||
|
|
|
|||
|
|
@ -6,5 +6,5 @@ pub mod sysproxy;
|
|||
pub mod transport;
|
||||
pub mod tunnel;
|
||||
|
||||
pub mod turn;
|
||||
|
||||
pub mod runner;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,62 @@ use tokio::sync::mpsc;
|
|||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
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>;
|
||||
|
||||
|
|
@ -16,12 +72,45 @@ pub async fn connect_xhttp(
|
|||
port: u16,
|
||||
sni: &str,
|
||||
access_key: &[u8],
|
||||
tls_enabled: bool,
|
||||
) -> Result<(mpsc::Sender<Bytes>, Arc<tokio::sync::Mutex<mpsc::Receiver<Bytes>>>)> {
|
||||
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))?;
|
||||
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]
|
||||
let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
|
||||
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() };
|
||||
|
||||
// 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!(
|
||||
"GET /stream HTTP/1.1\r\n\
|
||||
Host: {}\r\n\
|
||||
|
|
@ -52,14 +139,14 @@ pub async fn connect_xhttp(
|
|||
http_host, auth_token
|
||||
);
|
||||
|
||||
tcp_stream.write_all(req.as_bytes()).await?;
|
||||
tcp_stream.flush().await?;
|
||||
stream.write_all(req.as_bytes()).await?;
|
||||
stream.flush().await?;
|
||||
|
||||
// 3. Read server response headers
|
||||
let mut buf = vec![0u8; 4096];
|
||||
let mut header_len = 0;
|
||||
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"); }
|
||||
header_len += n;
|
||||
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();
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ pub async fn run_linux_tunnel(
|
|||
// 4. Setup commands (Using standard /1 routing trick for fail-proof overriding)
|
||||
let setup_script = format!(
|
||||
"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 link set dev ostp_tun up; \
|
||||
ip route add {} via {} dev {}; \
|
||||
|
|
|
|||
|
|
@ -174,8 +174,9 @@ pub async fn run_wintun_tunnel(
|
|||
tracing::info!("Applying network configuration...");
|
||||
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 subinterface \"{TUN_NAME}\" mtu=1300 store=persistent\n\
|
||||
netsh interface ipv4 set interface name=\"{TUN_NAME}\" metric=5\n"
|
||||
netsh interface ipv4 set subinterface \"{TUN_NAME}\" mtu={} store=persistent\n\
|
||||
netsh interface ipv4 set interface name=\"{TUN_NAME}\" metric=5\n",
|
||||
config.ostp.mtu
|
||||
);
|
||||
|
||||
if let Some(ref dns) = config.dns_server {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -20,3 +20,6 @@ portable-atomic.workspace = true
|
|||
hmac.workspace = true
|
||||
sha2.workspace = true
|
||||
base64 = "0.22"
|
||||
rustls = "0.23"
|
||||
tokio-rustls = "0.26"
|
||||
rcgen = "0.13"
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ pub struct ApiState {
|
|||
pub api_token: String,
|
||||
/// Server address for subscription links (e.g. "example.com")
|
||||
pub server_host: String,
|
||||
/// Server listen port
|
||||
pub server_port: u16,
|
||||
pub reality_query: String,
|
||||
}
|
||||
|
||||
// ── API configuration ────────────────────────────────────────────────────────
|
||||
|
|
@ -137,6 +137,7 @@ pub async fn start_api_server(
|
|||
user_stats: Arc<RwLock<HashMap<String, Arc<UserStats>>>>,
|
||||
server_host: String,
|
||||
server_port: u16,
|
||||
reality_query: String,
|
||||
) {
|
||||
let state = ApiState {
|
||||
access_keys,
|
||||
|
|
@ -145,6 +146,7 @@ pub async fn start_api_server(
|
|||
api_token: config.token.clone(),
|
||||
server_host,
|
||||
server_port,
|
||||
reality_query,
|
||||
};
|
||||
|
||||
let app = create_api_router(state);
|
||||
|
|
@ -416,7 +418,7 @@ async fn handle_subscribe(
|
|||
|
||||
// If client requests plain text, return ostp:// share link
|
||||
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!({
|
||||
"ok": true,
|
||||
"data": link
|
||||
|
|
|
|||
|
|
@ -22,6 +22,15 @@ pub use outbound::{OutboundAction, OutboundConfig, OutboundRule};
|
|||
pub use api::ApiConfig;
|
||||
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 ─────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -59,6 +68,8 @@ pub async fn run_server(
|
|||
api_config: Option<ApiConfig>,
|
||||
fallback_config: Option<FallbackConfig>,
|
||||
debug: bool,
|
||||
reality_query: Option<String>,
|
||||
reality_config: Option<RealityServerConfig>,
|
||||
) -> Result<()> {
|
||||
let mut keys_map = HashMap::new();
|
||||
for key in access_keys {
|
||||
|
|
@ -161,8 +172,9 @@ pub async fn run_server(
|
|||
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_host = parts.get(1).unwrap_or(&"0.0.0.0").to_string();
|
||||
let rq = reality_query.clone().unwrap_or_default();
|
||||
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();
|
||||
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");
|
||||
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! {
|
||||
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 {
|
||||
tracing::error!("Server error: {e}");
|
||||
}
|
||||
|
|
@ -239,6 +268,7 @@ async fn run_server_loop(
|
|||
shared_keys: std::sync::Arc<std::sync::RwLock<HashMap<String, ()>>>,
|
||||
outbound: Option<OutboundConfig>,
|
||||
debug: bool,
|
||||
tls_config: Option<std::sync::Arc<rustls::ServerConfig>>,
|
||||
) -> Result<()> {
|
||||
let mut remotes: HashMap<(u32, u16), RemoteState> = HashMap::new();
|
||||
let (stream_tx, mut stream_rx) = mpsc::unbounded_channel::<(u32, u16, Vec<u8>)>();
|
||||
|
|
@ -257,7 +287,11 @@ async fn run_server_loop(
|
|||
loop {
|
||||
match sock_clone.recv_from(&mut buf).await {
|
||||
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() {
|
||||
break;
|
||||
}
|
||||
|
|
@ -275,6 +309,7 @@ async fn run_server_loop(
|
|||
let shared_keys_clone = shared_keys.clone();
|
||||
let udp_tx_clone = udp_tx.clone();
|
||||
|
||||
let tls_cfg = tls_config.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Ok(listener) = tokio::net::TcpListener::bind(&addr).await {
|
||||
tracing::info!("TCP (UoT) listener bound to {}", addr);
|
||||
|
|
@ -312,10 +347,25 @@ async fn run_server_loop(
|
|||
let tm = tcp_map_clone.clone();
|
||||
let keys = shared_keys_clone.clone();
|
||||
let tx = udp_tx_clone.clone();
|
||||
let tls = tls_cfg.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Some(cfg) = tls {
|
||||
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 {
|
||||
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 });
|
||||
}
|
||||
|
|
@ -475,7 +528,10 @@ async fn run_server_loop(
|
|||
}
|
||||
}
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -11,13 +11,16 @@ use tokio::net::TcpStream;
|
|||
use tokio::sync::{mpsc, RwLock};
|
||||
use tracing::info;
|
||||
|
||||
pub async fn handle_tcp_connection(
|
||||
mut stream: TcpStream,
|
||||
pub async fn handle_tcp_connection<S>(
|
||||
mut stream: S,
|
||||
peer_addr: SocketAddr,
|
||||
shared_keys: Arc<StdRwLock<HashMap<String, ()>>>,
|
||||
udp_tx: mpsc::Sender<(Bytes, SocketAddr)>,
|
||||
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
|
||||
let mut buf = [0u8; 4096];
|
||||
let mut header_len = 0;
|
||||
|
|
@ -123,7 +126,7 @@ pub async fn handle_tcp_connection(
|
|||
let leftover = &buf[headers_end..header_len];
|
||||
|
||||
// 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
|
||||
let peer_clone = peer_addr;
|
||||
|
|
@ -181,7 +184,10 @@ pub async fn handle_tcp_connection(
|
|||
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 resp = format!(
|
||||
"HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
|
|
|
|||
141
ostp/src/main.rs
141
ostp/src/main.rs
|
|
@ -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 port = parsed.port().ok_or_else(|| anyhow!("Missing port in share link"))?;
|
||||
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 {
|
||||
server,
|
||||
access_key,
|
||||
mtu: None,
|
||||
transport: None,
|
||||
socks5_bind: Some("127.0.0.1:1088".to_string()), // Fallback to standard SOCKS5 port
|
||||
transport: Some(TransportConfigRaw {
|
||||
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 {
|
||||
enable: false, // Default to proxy, configurable via settings GUI
|
||||
enable: false,
|
||||
wintun_path: Some("./wintun.dll".to_string()),
|
||||
ipv4_address: Some("10.1.0.2/24".to_string()),
|
||||
dns: None,
|
||||
}),
|
||||
turn: None,
|
||||
reality: Some(RealityConfigRaw {
|
||||
sni,
|
||||
fp,
|
||||
pbk,
|
||||
sid,
|
||||
spx,
|
||||
}),
|
||||
debug: Some(false),
|
||||
exclude: None,
|
||||
mux: None,
|
||||
|
|
@ -141,7 +169,7 @@ impl UnifiedConfig {
|
|||
struct ServerConfig {
|
||||
listen: ListenConfig,
|
||||
access_keys: Vec<String>,
|
||||
turn_server: Option<String>,
|
||||
reality: Option<RealityServerConfigRaw>,
|
||||
debug: Option<bool>,
|
||||
outbound: Option<OutboundConfig>,
|
||||
api: Option<ApiConfig>,
|
||||
|
|
@ -193,7 +221,7 @@ struct ClientConfig {
|
|||
mtu: Option<usize>,
|
||||
socks5_bind: Option<String>,
|
||||
tun: Option<TunConfig>,
|
||||
turn: Option<TurnConfigRaw>,
|
||||
reality: Option<RealityConfigRaw>,
|
||||
debug: Option<bool>,
|
||||
exclude: Option<ExcludeConfig>,
|
||||
mux: Option<MuxConfig>,
|
||||
|
|
@ -215,12 +243,24 @@ struct TunConfig {
|
|||
dns: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct TurnConfigRaw {
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
struct RealityConfigRaw {
|
||||
sni: String,
|
||||
fp: String,
|
||||
pbk: String,
|
||||
sid: String,
|
||||
spx: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
struct RealityServerConfigRaw {
|
||||
#[serde(default)]
|
||||
enabled: bool,
|
||||
server_addr: String,
|
||||
username: Option<String>,
|
||||
access_key: Option<String>,
|
||||
dest: String,
|
||||
private_key: String,
|
||||
pbk: String,
|
||||
sid: String,
|
||||
sni_list: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
|
@ -469,6 +509,16 @@ async fn run_app() -> Result<()> {
|
|||
// Target web server (e.g., local nginx or caddy)
|
||||
"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
|
||||
}}"#, key)
|
||||
} else {
|
||||
|
|
@ -501,18 +551,19 @@ async fn run_app() -> Result<()> {
|
|||
"processes": []
|
||||
}},
|
||||
|
||||
// STUN/TURN server settings to bypass UDP blocks by mimicking WebRTC call traffic
|
||||
"turn": {{
|
||||
"enabled": false,
|
||||
"server_addr": "127.0.0.1:3478",
|
||||
"username": "ostpuser",
|
||||
"access_key": "ostppassword"
|
||||
// Reality (XTLS) / WebRTC Masquerade parameters
|
||||
"reality": {{
|
||||
"dest": "www.microsoft.com:443",
|
||||
"private_key": "",
|
||||
"pbk": "",
|
||||
"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": {{
|
||||
"mode": "udp",
|
||||
"stealth_sni": "vk.com",
|
||||
"stealth_sni": "www.microsoft.com",
|
||||
"stealth_port": 443
|
||||
}},
|
||||
|
||||
|
|
@ -529,11 +580,15 @@ async fn run_app() -> Result<()> {
|
|||
if is_server {
|
||||
let mut stripped = json_comments::StripComments::new(content.as_bytes());
|
||||
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 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!(" ostp://{}@{}:50000", key, host);
|
||||
println!(" {}", link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -575,7 +630,11 @@ async fn run_app() -> Result<()> {
|
|||
|
||||
println!("\n Client share links from {:?}:", args.config);
|
||||
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(());
|
||||
}
|
||||
|
|
@ -589,8 +648,10 @@ async fn run_app() -> Result<()> {
|
|||
AppMode::Server(server_cfg) => {
|
||||
let listen_addrs = server_cfg.listen.addresses();
|
||||
println!("[ostp] Starting server on {:?}", listen_addrs);
|
||||
if let Some(turn) = server_cfg.turn_server {
|
||||
println!("[ostp] TURN relay enabled: {}", turn);
|
||||
if let Some(ref reality) = server_cfg.reality {
|
||||
if reality.enabled {
|
||||
println!("[ostp] Reality mode enabled (dest: {})", reality.dest);
|
||||
}
|
||||
}
|
||||
let debug = server_cfg.debug.unwrap_or(false);
|
||||
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()),
|
||||
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
|
||||
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) => {
|
||||
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 mode_str = if is_tun_enabled { "tun" } else { "proxy" };
|
||||
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 {
|
||||
mode: if is_tun_enabled { "tun".to_string() } else { "proxy".to_string() },
|
||||
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(),
|
||||
access_key: client_cfg.access_key.clone(),
|
||||
handshake_timeout_ms: 5000,
|
||||
io_timeout_ms: 5000,
|
||||
io_timeout_ms: 2500,
|
||||
mtu: client_cfg.mtu.unwrap_or(1350),
|
||||
keepalive_interval_sec: 5,
|
||||
},
|
||||
local_proxy: ostp_client::config::LocalProxyConfig {
|
||||
bind_addr: client_cfg.socks5_bind.clone().unwrap_or_else(|| "127.0.0.1:1088".to_string()),
|
||||
connect_timeout_ms: 5000,
|
||||
},
|
||||
turn: ostp_client::config::TurnConfig {
|
||||
enabled: turn_cfg.map(|t| t.enabled).unwrap_or(false),
|
||||
server_addr: turn_cfg.map(|t| t.server_addr.clone()).unwrap_or_default(),
|
||||
username: turn_cfg.and_then(|t| t.username.clone()).unwrap_or_default(),
|
||||
access_key: turn_cfg.and_then(|t| t.access_key.clone()).unwrap_or_default(),
|
||||
reality: ostp_client::config::RealityConfig {
|
||||
sni: reality_cfg.map(|t| t.sni.clone()).unwrap_or_default(),
|
||||
fp: reality_cfg.map(|t| t.fp.clone()).unwrap_or_default(),
|
||||
pbk: reality_cfg.map(|t| t.pbk.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 {
|
||||
domains: client_cfg.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue