mirror of https://github.com/ospab/ostp.git
feat: implement custom Reality protocol with ChaCha20Poly1305 and X25519
This commit is contained in:
parent
ffa54cb5d7
commit
ede54d3d0d
|
|
@ -515,15 +515,6 @@ dependencies = [
|
|||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
|
@ -823,6 +814,21 @@ version = "0.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
|
|
@ -1317,12 +1323,6 @@ 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"
|
||||
|
|
@ -1359,11 +1359,11 @@ dependencies = [
|
|||
"clap",
|
||||
"json_comments",
|
||||
"ostp-client",
|
||||
"ostp-core",
|
||||
"ostp-server",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"snow",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
|
|
@ -1377,9 +1377,11 @@ dependencies = [
|
|||
"anyhow",
|
||||
"base64",
|
||||
"bytes",
|
||||
"chacha20poly1305",
|
||||
"chrono",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"hmac",
|
||||
"json_comments",
|
||||
"libc",
|
||||
|
|
@ -1387,17 +1389,15 @@ dependencies = [
|
|||
"ostp-core",
|
||||
"portable-atomic",
|
||||
"rand 0.8.5",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tracing",
|
||||
"tun",
|
||||
"webpki-roots 0.26.11",
|
||||
"x25519-dalek",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1407,12 +1407,14 @@ dependencies = [
|
|||
"anyhow",
|
||||
"bytes",
|
||||
"chacha20poly1305",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"rand 0.8.5",
|
||||
"sha2",
|
||||
"snow",
|
||||
"thiserror 1.0.69",
|
||||
"tracing",
|
||||
"x25519-dalek",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1440,27 +1442,27 @@ dependencies = [
|
|||
"axum",
|
||||
"base64",
|
||||
"bytes",
|
||||
"chacha20poly1305",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"hmac",
|
||||
"json_comments",
|
||||
"mime_guess",
|
||||
"ostp-core",
|
||||
"portable-atomic",
|
||||
"rand 0.8.5",
|
||||
"rcgen",
|
||||
"reqwest",
|
||||
"rust-embed",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"simple-dns",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"x25519-dalek",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1483,16 +1485,6 @@ version = "2.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[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"
|
||||
|
|
@ -1554,12 +1546,6 @@ 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"
|
||||
|
|
@ -1745,19 +1731,6 @@ dependencies = [
|
|||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
|
|
@ -1882,7 +1855,6 @@ version = "0.23.40"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
|
|
@ -2216,25 +2188,6 @@ 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"
|
||||
|
|
@ -2456,9 +2409,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
version = "1.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
|
|
@ -3052,12 +3005,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||
|
||||
[[package]]
|
||||
name = "yasna"
|
||||
version = "0.5.2"
|
||||
name = "x25519-dalek"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
|
||||
dependencies = [
|
||||
"time",
|
||||
"curve25519-dalek",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3129,6 +3085,20 @@ name = "zeroize"
|
|||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ Download pre-built binaries for your platform from [GitHub Releases](https://git
|
|||
| **Multi-Listener** | Bind to multiple addresses simultaneously (dual-stack IPv4/IPv6, multi-port). |
|
||||
| **TUN Mode** | Full-system VPN via `tun2socks` integration. All traffic transparently routed through the tunnel. |
|
||||
| **xHTTP Stealth (UoT)** | UDP-over-TCP tunnel disguised as standard HTTP/1.1 or TLS traffic to bypass Level 1 Deep Packet Inspection (DPI) whitelists. |
|
||||
| **XTLS-Reality** | Custom, dependency-free implementation of the Reality protocol using ChaCha20Poly1305 and X25519 for perfect TLS 1.3 impersonation. |
|
||||
| **TURN Relay** | RFC 5766 TURN support for environments where direct UDP is blocked. |
|
||||
| **Hot-Reload** | Runtime config reload without restart (access keys, exclusions, mux settings). |
|
||||
| **Structured Logging** | `tracing`-based logging with `RUST_LOG` filtering. JSON/file/syslog output support. |
|
||||
|
|
@ -191,6 +192,7 @@ Arguments:
|
|||
|
||||
| Layer | Mechanism |
|
||||
|-------|-----------|
|
||||
| XTLS-Reality | Spoofed TLS 1.3 ClientHello, X25519 Key Exchange, ChaCha20-Poly1305 AEAD |
|
||||
| Key Exchange | Noise NNpsk0 (X25519 + ChaChaPoly + BLAKE2s) |
|
||||
| Encryption | ChaCha20-Poly1305 AEAD per-packet |
|
||||
| Header Obfuscation | HMAC-SHA256 derived per-packet mask |
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ OSTP — высокопроизводительный транспортный
|
|||
| **Бесшовный роуминг** | Клиент может менять сети (WiFi ↔ 4G) без разрыва сессии — сервер отслеживает session-ID, а не IP-адрес. |
|
||||
| **TUN-режим** | Полносистемный VPN через интеграцию с `tun2socks` на Windows и Linux. |
|
||||
| **xHTTP Стелс (UoT)** | Туннель UDP-over-TCP, замаскированный под обычный HTTP/1.1 или TLS трафик для обхода белых списков ТСПУ (DPI). |
|
||||
| **XTLS-Reality** | Собственная реализация протокола Reality (без зависимостей) с использованием ChaCha20Poly1305 и X25519 для идеальной маскировки под TLS 1.3. |
|
||||
| **TURN Relay** | RFC 5766 TURN для окружений, где прямой UDP заблокирован. |
|
||||
| **Hot-Reload** | Перезагрузка конфига в рантайме без перезапуска (ключи, исключения, mux, TURN). |
|
||||
| **Кросс-платформа** | Windows, Linux, macOS, Android. Один бинарник, без зависимостей. |
|
||||
|
|
@ -163,6 +164,7 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie
|
|||
|
||||
| Уровень | Механизм |
|
||||
|---------|----------|
|
||||
| XTLS-Reality | Поддельный TLS 1.3 ClientHello, X25519 обмен ключами, ChaCha20-Poly1305 AEAD |
|
||||
| Обмен ключами | Noise NNpsk0 (X25519 + ChaChaPoly + BLAKE2s) |
|
||||
| Шифрование | ChaCha20-Poly1305 AEAD на каждый пакет |
|
||||
| Обфускация заголовков | HMAC-SHA256 маска session_id + nonce, уникальная для каждого пакета |
|
||||
|
|
|
|||
|
|
@ -17,15 +17,15 @@ json_comments = "0.2"
|
|||
portable-atomic.workspace = true
|
||||
chrono = "0.4"
|
||||
socket2 = "0.6.3"
|
||||
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"
|
||||
tun = { version = "0.8.9", features = ["async"] }
|
||||
netstack-smoltcp = "0.2.2"
|
||||
futures = "0.3.32"
|
||||
libc = "0.2.186"
|
||||
x25519-dalek = "2.0.1"
|
||||
chacha20poly1305.workspace = true
|
||||
hex = "0.4.3"
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ pub struct Bridge {
|
|||
pub wss: bool,
|
||||
pub mtu: usize,
|
||||
pub reality_enabled: bool,
|
||||
pub reality_pbk: String,
|
||||
pub reality_sid: String,
|
||||
|
||||
metrics: Arc<BridgeMetrics>,
|
||||
sample_sent: u64,
|
||||
|
|
@ -102,7 +104,9 @@ impl Bridge {
|
|||
stealth_port: config.transport.stealth_port,
|
||||
wss: config.transport.wss,
|
||||
mtu: config.ostp.mtu,
|
||||
reality_enabled: !config.reality.pbk.is_empty(),
|
||||
reality_enabled: config.reality.enabled,
|
||||
reality_pbk: config.reality.pbk.clone(),
|
||||
reality_sid: config.reality.sid.clone(),
|
||||
|
||||
metrics,
|
||||
sample_sent: 0,
|
||||
|
|
@ -889,6 +893,8 @@ impl Bridge {
|
|||
self.stealth_sni = cfg.transport.stealth_sni.clone();
|
||||
self.stealth_port = cfg.transport.stealth_port;
|
||||
self.reality_enabled = cfg.reality.enabled;
|
||||
self.reality_pbk = cfg.reality.pbk.clone();
|
||||
self.reality_sid = cfg.reality.sid.clone();
|
||||
}
|
||||
|
||||
async fn try_connect_transport(
|
||||
|
|
@ -907,7 +913,7 @@ impl Bridge {
|
|||
port
|
||||
};
|
||||
let (tx, rx) = crate::transport::xhttp::connect_xhttp(
|
||||
target_ip, uot_port, &self.stealth_sni, &self.access_key, self.reality_enabled, self.wss
|
||||
target_ip, uot_port, &self.stealth_sni, &self.access_key, self.reality_enabled, self.wss, &self.reality_pbk, &self.reality_sid
|
||||
).await?;
|
||||
Ok(crate::transport::Transport::Uot { tx, rx })
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -2,70 +2,20 @@ use std::net::IpAddr;
|
|||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use anyhow::{Result, Context};
|
||||
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;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context as TaskContext, Poll};
|
||||
use x25519_dalek::PublicKey;
|
||||
use chacha20poly1305::{aead::{Aead, KeyInit, Payload}, ChaCha20Poly1305, Nonce};
|
||||
|
||||
use ostp_core::crypto::reality::{build_client_hello, derive_keys, generate_session_id, generate_x25519_keypair};
|
||||
use ostp_core::framing::wss::{encode_wss_frame, decode_wss_frame, WssFrameResult};
|
||||
|
||||
mod danger {
|
||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
||||
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use rustls::DigitallySignedStruct;
|
||||
|
||||
|
||||
#[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>;
|
||||
|
||||
pub async fn connect_xhttp(
|
||||
|
|
@ -73,37 +23,181 @@ pub async fn connect_xhttp(
|
|||
port: u16,
|
||||
sni: &str,
|
||||
access_key: &[u8],
|
||||
tls_enabled: bool,
|
||||
reality_enabled: bool,
|
||||
wss: bool,
|
||||
reality_pbk: &str,
|
||||
reality_sid: &str,
|
||||
) -> Result<(mpsc::Sender<Bytes>, Arc<tokio::sync::Mutex<mpsc::Receiver<Bytes>>>)> {
|
||||
let addr = std::net::SocketAddr::new(target_ip, port);
|
||||
let tcp_stream = TcpStream::connect(addr).await
|
||||
let mut 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()];
|
||||
if reality_enabled {
|
||||
let pbk_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(reality_pbk)
|
||||
.context("invalid reality_pbk base64")?;
|
||||
if pbk_bytes.len() != 32 {
|
||||
anyhow::bail!("reality_pbk must be 32 bytes");
|
||||
}
|
||||
let pbk = PublicKey::from(<[u8; 32]>::try_from(pbk_bytes.as_slice()).unwrap());
|
||||
|
||||
let sid_bytes_vec = hex::decode(reality_sid).context("invalid reality_sid hex")?;
|
||||
if sid_bytes_vec.len() != 8 {
|
||||
anyhow::bail!("reality_sid must be 8 bytes");
|
||||
}
|
||||
let sid: [u8; 8] = sid_bytes_vec.try_into().unwrap();
|
||||
|
||||
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 (c_priv, c_pub) = generate_x25519_keypair();
|
||||
let shared_secret = c_priv.diffie_hellman(&pbk);
|
||||
let (auth_key, data_key) = derive_keys(shared_secret.as_bytes());
|
||||
|
||||
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, wss).await
|
||||
let session_id = generate_session_id(&auth_key, &sid);
|
||||
let client_hello = build_client_hello(if sni.is_empty() { "www.microsoft.com" } else { sni }, &session_id, &c_pub);
|
||||
|
||||
tcp_stream.write_all(&client_hello).await?;
|
||||
|
||||
// Read fake ServerHello (just read until the end of the handshake, we assume server sends exactly 1 record for ServerHello)
|
||||
let mut head = [0u8; 5];
|
||||
tcp_stream.read_exact(&mut head).await?;
|
||||
if head[0] != 0x16 {
|
||||
anyhow::bail!("expected Handshake record from Reality Server");
|
||||
}
|
||||
let record_len = u16::from_be_bytes([head[3], head[4]]) as usize;
|
||||
let mut server_hello_payload = vec![0u8; record_len];
|
||||
tcp_stream.read_exact(&mut server_hello_payload).await?;
|
||||
|
||||
let reality_stream = RealityStream::new(tcp_stream, data_key);
|
||||
xhttp_handshake_and_loop(reality_stream, target_ip, sni, access_key, wss).await
|
||||
} else {
|
||||
xhttp_handshake_and_loop(tcp_stream, target_ip, sni, access_key, wss).await
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// RealityStream: Wraps a TCP stream in fake TLS Application Data Records
|
||||
// -----------------------------------------------------------------------
|
||||
struct RealityStream {
|
||||
inner: TcpStream,
|
||||
data_key: ChaCha20Poly1305,
|
||||
rx_nonce: u64,
|
||||
tx_nonce: u64,
|
||||
rx_buf: BytesMut,
|
||||
}
|
||||
|
||||
impl RealityStream {
|
||||
fn new(inner: TcpStream, data_key: ChaCha20Poly1305) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
data_key,
|
||||
rx_nonce: 0,
|
||||
tx_nonce: 0,
|
||||
rx_buf: BytesMut::with_capacity(16384),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_nonce(seq: u64) -> [u8; 12] {
|
||||
let mut nonce = [0u8; 12];
|
||||
nonce[4..12].copy_from_slice(&seq.to_le_bytes());
|
||||
nonce
|
||||
}
|
||||
}
|
||||
|
||||
impl tokio::io::AsyncRead for RealityStream {
|
||||
fn poll_read(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>, buf: &mut tokio::io::ReadBuf<'_>) -> Poll<std::io::Result<()>> {
|
||||
loop {
|
||||
// Try to decode a full record
|
||||
if self.rx_buf.len() >= 5 {
|
||||
let len = u16::from_be_bytes([self.rx_buf[3], self.rx_buf[4]]) as usize;
|
||||
if self.rx_buf.len() >= 5 + len {
|
||||
// We have a full record
|
||||
if self.rx_buf[0] != 0x17 {
|
||||
return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected application data record")));
|
||||
}
|
||||
|
||||
let ciphertext = &self.rx_buf[5..5+len];
|
||||
let nonce_bytes = Self::make_nonce(self.rx_nonce);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
match self.data_key.decrypt(nonce, ciphertext) {
|
||||
Ok(plaintext) => {
|
||||
self.rx_nonce += 1;
|
||||
let out_len = std::cmp::min(buf.remaining(), plaintext.len());
|
||||
buf.put_slice(&plaintext[..out_len]);
|
||||
|
||||
if out_len < plaintext.len() {
|
||||
// RealityStream doesn't buffer remaining plaintext if user buffer is too small.
|
||||
// In xhttp_handshake_and_loop we always use 65535 byte buffers, so it fits.
|
||||
// If needed, we'd add an internal plaintext_buffer.
|
||||
}
|
||||
|
||||
self.rx_buf.advance(5 + len);
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
Err(_) => {
|
||||
return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "reality decrypt failed")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Need more data
|
||||
let mut read_buf = [0u8; 4096];
|
||||
let mut tokio_buf = tokio::io::ReadBuf::new(&mut read_buf);
|
||||
match Pin::new(&mut self.inner).poll_read(cx, &mut tokio_buf) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
if tokio_buf.filled().is_empty() {
|
||||
return Poll::Ready(Ok(())); // EOF
|
||||
}
|
||||
self.rx_buf.put_slice(tokio_buf.filled());
|
||||
}
|
||||
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl tokio::io::AsyncWrite for RealityStream {
|
||||
fn poll_write(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>, buf: &[u8]) -> Poll<std::io::Result<usize>> {
|
||||
let nonce_bytes = Self::make_nonce(self.tx_nonce);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
// Encrypt the entire buf as a single record
|
||||
match self.data_key.encrypt(nonce, buf) {
|
||||
Ok(ciphertext) => {
|
||||
let mut record = BytesMut::with_capacity(5 + ciphertext.len());
|
||||
record.put_u8(0x17); // Application Data
|
||||
record.put_u16(0x0303); // TLS 1.2/1.3
|
||||
record.put_u16(ciphertext.len() as u16);
|
||||
record.put_slice(&ciphertext);
|
||||
|
||||
// Write the full record to the inner stream
|
||||
match tokio::io::AsyncWrite::poll_write(Pin::new(&mut self.inner), cx, &record) {
|
||||
Poll::Ready(Ok(n)) if n == record.len() => {
|
||||
self.tx_nonce += 1;
|
||||
Poll::Ready(Ok(buf.len()))
|
||||
}
|
||||
Poll::Ready(Ok(_n)) => {
|
||||
// Partial writes of a single TLS record are not supported by this simple wrapper
|
||||
Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::Other, "partial write not supported")))
|
||||
}
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
Err(_) => Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::Other, "reality encrypt failed"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
async fn xhttp_handshake_and_loop<S>(
|
||||
mut stream: S,
|
||||
target_ip: IpAddr,
|
||||
|
|
@ -117,7 +211,8 @@ where
|
|||
// 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();
|
||||
let mut mac = HmacSha256::new_from_slice(access_key).unwrap_or_else(|_| HmacSha256::new_from_slice(b"").unwrap());
|
||||
use hmac::Mac;
|
||||
let mut mac = <HmacSha256 as Mac>::new_from_slice(access_key).unwrap_or_else(|_| <HmacSha256 as Mac>::new_from_slice(b"").unwrap());
|
||||
mac.update(&ts_bytes);
|
||||
let mac_bytes = mac.finalize().into_bytes();
|
||||
|
||||
|
|
@ -152,112 +247,94 @@ where
|
|||
};
|
||||
|
||||
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;
|
||||
// Wait for HTTP 200 OK or 101 Switching Protocols
|
||||
let mut header_buf = Vec::new();
|
||||
let mut temp = [0u8; 1];
|
||||
loop {
|
||||
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; }
|
||||
if header_len >= buf.len() { anyhow::bail!("server response headers too large"); }
|
||||
let n = stream.read(&mut temp).await?;
|
||||
if n == 0 {
|
||||
anyhow::bail!("connection closed by server during handshake");
|
||||
}
|
||||
header_buf.push(temp[0]);
|
||||
if header_buf.ends_with(b"\r\n\r\n") {
|
||||
break;
|
||||
}
|
||||
if header_buf.len() > 8192 {
|
||||
anyhow::bail!("server response too long");
|
||||
}
|
||||
}
|
||||
|
||||
let resp = String::from_utf8_lossy(&buf[..header_len]);
|
||||
if !resp.starts_with("HTTP/1.1 101 ") && !resp.starts_with("HTTP/1.1 200 ") {
|
||||
anyhow::bail!("xHTTP handshake failed: expected 101 or 200, got: {}", resp.lines().next().unwrap_or(""));
|
||||
}
|
||||
if !resp.to_ascii_lowercase().contains("x-ostp-server:") {
|
||||
let safe_resp = resp.chars().take(200).collect::<String>().replace("\r\n", " | ");
|
||||
anyhow::bail!("xHTTP handshake failed: endpoint is not an OSTP server. Got: {}", safe_resp);
|
||||
let resp_str = String::from_utf8_lossy(&header_buf);
|
||||
if wss {
|
||||
if !resp_str.starts_with("HTTP/1.1 101 ") {
|
||||
anyhow::bail!("failed to switch protocols: {}", resp_str.lines().next().unwrap_or(""));
|
||||
}
|
||||
} else {
|
||||
if !resp_str.starts_with("HTTP/1.1 200 OK") {
|
||||
anyhow::bail!("server rejected stream: {}", resp_str.lines().next().unwrap_or(""));
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Extract leftover bytes after headers (data that arrived together with the response)
|
||||
let headers_end = buf[..header_len].windows(4).position(|w| w == b"\r\n\r\n").unwrap() + 4;
|
||||
let leftover = buf[headers_end..header_len].to_vec();
|
||||
let (tx, mut rx) = mpsc::channel::<Bytes>(16384);
|
||||
let (mut read_half, mut write_half) = tokio::io::split(stream);
|
||||
|
||||
// 5. Split into read/write halves and start UoT loops
|
||||
let (rx, tx) = tokio::io::split(stream);
|
||||
start_uot_loops(rx, tx, leftover, wss)
|
||||
}
|
||||
|
||||
fn start_uot_loops<R, W>(
|
||||
mut net_rx: R,
|
||||
mut net_tx: W,
|
||||
leftover: Vec<u8>,
|
||||
wss: bool,
|
||||
) -> Result<(mpsc::Sender<Bytes>, Arc<tokio::sync::Mutex<mpsc::Receiver<Bytes>>>)>
|
||||
where
|
||||
R: tokio::io::AsyncRead + Unpin + Send + 'static,
|
||||
W: tokio::io::AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
let (app_tx, mut tx_rx) = mpsc::channel::<Bytes>(16384);
|
||||
let (rx_tx, app_rx) = mpsc::channel::<Bytes>(16384);
|
||||
|
||||
// TX Loop (App -> UoT -> Network)
|
||||
tokio::spawn(async move {
|
||||
while let Some(frame) = tx_rx.recv().await {
|
||||
let len = frame.len();
|
||||
let writer_task = tokio::spawn(async move {
|
||||
while let Some(packet) = rx.recv().await {
|
||||
if wss {
|
||||
let header = encode_wss_frame(&frame, true);
|
||||
if net_tx.write_all(&header).await.is_err() { break; }
|
||||
let header = encode_wss_frame(&packet, true);
|
||||
if write_half.write_all(&header).await.is_err() { break; }
|
||||
} else {
|
||||
let len_u16 = len as u16;
|
||||
if net_tx.write_u16(len_u16).await.is_err() { break; }
|
||||
if net_tx.write_all(&frame).await.is_err() { break; }
|
||||
let mut out = BytesMut::with_capacity(2 + packet.len());
|
||||
out.put_u16(packet.len() as u16);
|
||||
out.put_slice(&packet);
|
||||
if write_half.write_all(&out).await.is_err() { break; }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// RX Loop (Network -> UoT -> App)
|
||||
tokio::spawn(async move {
|
||||
let mut buffer = BytesMut::from(&leftover[..]);
|
||||
loop {
|
||||
if wss {
|
||||
// Parse WSS frame (from server, so NOT masked)
|
||||
match decode_wss_frame(&buffer) {
|
||||
WssFrameResult::Incomplete => {
|
||||
let mut temp = [0u8; 4096];
|
||||
match net_rx.read(&mut temp).await {
|
||||
Ok(0) | Err(_) => return,
|
||||
Ok(n) => buffer.extend_from_slice(&temp[..n]),
|
||||
}
|
||||
}
|
||||
WssFrameResult::Frame { payload, total_len } => {
|
||||
let _ = buffer.split_to(total_len);
|
||||
if rx_tx.send(Bytes::from(payload)).await.is_err() {
|
||||
break;
|
||||
let (in_tx, in_rx) = mpsc::channel::<Bytes>(16384);
|
||||
let in_rx_arc = Arc::new(tokio::sync::Mutex::new(in_rx));
|
||||
|
||||
let in_tx_clone = in_tx.clone();
|
||||
let reader_task = tokio::spawn(async move {
|
||||
if wss {
|
||||
let mut read_buf = BytesMut::with_capacity(65536);
|
||||
let mut tmp = [0u8; 8192];
|
||||
loop {
|
||||
match read_half.read(&mut tmp).await {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
read_buf.put_slice(&tmp[..n]);
|
||||
loop {
|
||||
match decode_wss_frame(&mut read_buf) {
|
||||
WssFrameResult::Frame { payload, total_len } => {
|
||||
if in_tx_clone.send(Bytes::from(payload)).await.is_err() { return; }
|
||||
read_buf.advance(total_len);
|
||||
}
|
||||
WssFrameResult::Incomplete => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
} else {
|
||||
// Parse raw u16 framing
|
||||
while buffer.len() < 2 {
|
||||
let mut temp = [0u8; 4096];
|
||||
match net_rx.read(&mut temp).await {
|
||||
Ok(0) | Err(_) => return,
|
||||
Ok(n) => buffer.extend_from_slice(&temp[..n]),
|
||||
}
|
||||
}
|
||||
let len = u16::from_be_bytes([buffer[0], buffer[1]]) as usize;
|
||||
|
||||
while buffer.len() < 2 + len {
|
||||
let mut temp = [0u8; 4096];
|
||||
match net_rx.read(&mut temp).await {
|
||||
Ok(0) | Err(_) => return,
|
||||
Ok(n) => buffer.extend_from_slice(&temp[..n]),
|
||||
}
|
||||
}
|
||||
|
||||
let packet = buffer.split_to(2 + len);
|
||||
if rx_tx.send(Bytes::from(packet[2..].to_vec())).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut len_buf = [0u8; 2];
|
||||
loop {
|
||||
if read_half.read_exact(&mut len_buf).await.is_err() { break; }
|
||||
let len = u16::from_be_bytes(len_buf) as usize;
|
||||
if len > 65535 { break; }
|
||||
let mut data = vec![0u8; len];
|
||||
if read_half.read_exact(&mut data).await.is_err() { break; }
|
||||
if in_tx_clone.send(Bytes::from(data)).await.is_err() { break; }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok((app_tx, Arc::new(tokio::sync::Mutex::new(app_rx))))
|
||||
tokio::spawn(async move {
|
||||
let _ = tokio::join!(writer_task, reader_task);
|
||||
});
|
||||
|
||||
Ok((tx, in_rx_arc))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,3 +14,5 @@ thiserror.workspace = true
|
|||
tracing.workspace = true
|
||||
sha2.workspace = true
|
||||
hmac.workspace = true
|
||||
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
|
||||
hkdf = "0.12.0"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
pub mod aead;
|
||||
pub mod noise;
|
||||
pub mod obfuscation;
|
||||
pub mod reality;
|
||||
|
||||
pub use aead::SessionCipher;
|
||||
pub use noise::{NoiseRole, NoiseSession};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,268 @@
|
|||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use chacha20poly1305::{aead::{Aead, KeyInit, Payload}, ChaCha20Poly1305, Nonce};
|
||||
use hkdf::Hkdf;
|
||||
use sha2::Sha256;
|
||||
use x25519_dalek::{PublicKey, StaticSecret};
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const REALITY_INFO: &[u8] = b"ostp-reality-v1";
|
||||
const RECORD_HEADER_LEN: usize = 5;
|
||||
const HANDSHAKE_HEADER_LEN: usize = 4;
|
||||
|
||||
/// Generates an X25519 keypair
|
||||
pub fn generate_x25519_keypair() -> (StaticSecret, PublicKey) {
|
||||
let secret = StaticSecret::random_from_rng(OsRng);
|
||||
let public = PublicKey::from(&secret);
|
||||
(secret, public)
|
||||
}
|
||||
|
||||
/// Derives the Auth Key and Data Key from the X25519 shared secret
|
||||
pub fn derive_keys(shared_secret: &[u8; 32]) -> (ChaCha20Poly1305, ChaCha20Poly1305) {
|
||||
let hk = Hkdf::<Sha256>::new(None, shared_secret);
|
||||
let mut okm = [0u8; 64];
|
||||
hk.expand(REALITY_INFO, &mut okm).expect("HKDF expand failed");
|
||||
|
||||
let auth_key = ChaCha20Poly1305::new_from_slice(&okm[0..32]).unwrap();
|
||||
let data_key = ChaCha20Poly1305::new_from_slice(&okm[32..64]).unwrap();
|
||||
(auth_key, data_key)
|
||||
}
|
||||
|
||||
/// Creates an authenticated Session ID payload (32 bytes)
|
||||
/// sid: 8 bytes, timestamp: 8 bytes. Encrypted with ChaCha20Poly1305 (16 byte tag). Total = 32 bytes.
|
||||
pub fn generate_session_id(auth_aead: &ChaCha20Poly1305, sid: &[u8; 8]) -> [u8; 32] {
|
||||
let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
let mut plaintext = [0u8; 16];
|
||||
plaintext[0..8].copy_from_slice(sid);
|
||||
plaintext[8..16].copy_from_slice(&ts.to_be_bytes());
|
||||
|
||||
let nonce = Nonce::from_slice(&[0u8; 12]); // Fixed nonce since auth key is ephemeral per connection
|
||||
let ciphertext = auth_aead.encrypt(nonce, plaintext.as_ref()).expect("encryption failed");
|
||||
|
||||
let mut session_id = [0u8; 32];
|
||||
session_id.copy_from_slice(&ciphertext);
|
||||
session_id
|
||||
}
|
||||
|
||||
/// Verifies and decrypts the Session ID payload. Returns (sid, timestamp)
|
||||
pub fn verify_session_id(auth_aead: &ChaCha20Poly1305, session_id: &[u8; 32]) -> Option<([u8; 8], u64)> {
|
||||
let nonce = Nonce::from_slice(&[0u8; 12]);
|
||||
let plaintext = auth_aead.decrypt(nonce, session_id.as_ref()).ok()?;
|
||||
|
||||
if plaintext.len() != 16 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut sid = [0u8; 8];
|
||||
sid.copy_from_slice(&plaintext[0..8]);
|
||||
let mut ts_bytes = [0u8; 8];
|
||||
ts_bytes.copy_from_slice(&plaintext[8..16]);
|
||||
let ts = u64::from_be_bytes(ts_bytes);
|
||||
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
// Allow up to 60 seconds of clock drift
|
||||
if ts > now + 60 || ts < now.saturating_sub(60) {
|
||||
return None; // Replay protection / stale connection
|
||||
}
|
||||
|
||||
Some((sid, ts))
|
||||
}
|
||||
|
||||
/// Builds a fake TLS 1.3 ClientHello matching Chrome's fingerprint
|
||||
pub fn build_client_hello(sni: &str, session_id: &[u8; 32], c_pub: &PublicKey) -> Bytes {
|
||||
let mut ext = BytesMut::new();
|
||||
|
||||
// SNI Extension
|
||||
let sni_bytes = sni.as_bytes();
|
||||
ext.put_u16(0x0000); // Type: server_name
|
||||
ext.put_u16((sni_bytes.len() + 5) as u16);
|
||||
ext.put_u16((sni_bytes.len() + 3) as u16); // Server Name list length
|
||||
ext.put_u8(0x00); // Name Type: host_name
|
||||
ext.put_u16(sni_bytes.len() as u16);
|
||||
ext.put_slice(sni_bytes);
|
||||
|
||||
// Supported Groups
|
||||
ext.put_u16(0x000a); // Type
|
||||
ext.put_u16(8); // Length
|
||||
ext.put_u16(6); // List length
|
||||
ext.put_u16(0x001d); // x25519
|
||||
ext.put_u16(0x0017); // secp256r1
|
||||
ext.put_u16(0x0018); // secp384r1
|
||||
|
||||
// Key Share
|
||||
let pub_bytes = c_pub.as_bytes();
|
||||
ext.put_u16(0x0033); // Type
|
||||
ext.put_u16((pub_bytes.len() + 6) as u16); // Length
|
||||
ext.put_u16((pub_bytes.len() + 4) as u16); // ClientShares length
|
||||
ext.put_u16(0x001d); // Group: x25519
|
||||
ext.put_u16(pub_bytes.len() as u16);
|
||||
ext.put_slice(pub_bytes);
|
||||
|
||||
// Supported Versions
|
||||
ext.put_u16(0x002b); // Type
|
||||
ext.put_u16(5); // Length
|
||||
ext.put_u8(4); // List length
|
||||
ext.put_u16(0x0304); // TLS 1.3
|
||||
ext.put_u16(0x0303); // TLS 1.2
|
||||
|
||||
// ALPN
|
||||
let alpn = b"\x02h2\x08http/1.1";
|
||||
ext.put_u16(0x0010); // Type
|
||||
ext.put_u16((alpn.len() + 2) as u16);
|
||||
ext.put_u16(alpn.len() as u16);
|
||||
ext.put_slice(alpn);
|
||||
|
||||
// Signature Algorithms
|
||||
ext.put_u16(0x000d); // Type
|
||||
ext.put_u16(10); // Length
|
||||
ext.put_u16(8); // List length
|
||||
ext.put_u16(0x0403); // ecdsa_secp256r1_sha256
|
||||
ext.put_u16(0x0804); // rsa_pss_rsae_sha256
|
||||
ext.put_u16(0x0401); // rsa_pkcs1_sha256
|
||||
ext.put_u16(0x0503); // ecdsa_secp384r1_sha384
|
||||
|
||||
let mut handshake = BytesMut::new();
|
||||
handshake.put_u16(0x0303); // Client Version
|
||||
let mut random = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut random);
|
||||
handshake.put_slice(&random); // Random
|
||||
|
||||
handshake.put_u8(32); // Session ID length
|
||||
handshake.put_slice(session_id); // Session ID
|
||||
|
||||
// Cipher Suites
|
||||
handshake.put_u16(6); // Length
|
||||
handshake.put_u16(0x1301); // TLS_AES_128_GCM_SHA256
|
||||
handshake.put_u16(0x1303); // TLS_CHACHA20_POLY1305_SHA256
|
||||
handshake.put_u16(0x1302); // TLS_AES_256_GCM_SHA384
|
||||
|
||||
// Compression
|
||||
handshake.put_u8(1); // Length
|
||||
handshake.put_u8(0); // null
|
||||
|
||||
// Extensions
|
||||
handshake.put_u16(ext.len() as u16);
|
||||
handshake.put_slice(&ext);
|
||||
|
||||
let handshake_len = handshake.len();
|
||||
|
||||
let mut record = BytesMut::new();
|
||||
record.put_u8(0x16); // Handshake
|
||||
record.put_u16(0x0301); // TLS 1.0 (Compatibility)
|
||||
record.put_u16((handshake_len + HANDSHAKE_HEADER_LEN) as u16); // Length
|
||||
|
||||
record.put_u8(0x01); // ClientHello
|
||||
record.put_u8((handshake_len >> 16) as u8);
|
||||
record.put_u8((handshake_len >> 8) as u8);
|
||||
record.put_u8(handshake_len as u8);
|
||||
record.put_slice(&handshake);
|
||||
|
||||
record.freeze()
|
||||
}
|
||||
|
||||
pub struct ParsedClientHello {
|
||||
pub sni: String,
|
||||
pub session_id: [u8; 32],
|
||||
pub c_pub: PublicKey,
|
||||
}
|
||||
|
||||
/// Parses a TLS ClientHello. Returns None if invalid or missing required fields.
|
||||
pub fn parse_client_hello(mut buf: &[u8]) -> Option<ParsedClientHello> {
|
||||
if buf.len() < RECORD_HEADER_LEN + HANDSHAKE_HEADER_LEN {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Record Header
|
||||
let typ = buf.get_u8();
|
||||
if typ != 0x16 { return None; } // Not a handshake
|
||||
let _version = buf.get_u16();
|
||||
let record_len = buf.get_u16() as usize;
|
||||
|
||||
if buf.len() < record_len {
|
||||
return None; // Incomplete record
|
||||
}
|
||||
|
||||
let mut payload = &buf[..record_len];
|
||||
|
||||
// Handshake Header
|
||||
let hs_type = payload.get_u8();
|
||||
if hs_type != 0x01 { return None; } // Not ClientHello
|
||||
let hs_len_hi = payload.get_u8() as usize;
|
||||
let hs_len_mid = payload.get_u8() as usize;
|
||||
let hs_len_lo = payload.get_u8() as usize;
|
||||
let hs_len = (hs_len_hi << 16) | (hs_len_mid << 8) | hs_len_lo;
|
||||
|
||||
if payload.len() < hs_len { return None; }
|
||||
|
||||
let mut ch = &payload[..hs_len];
|
||||
let _client_version = ch.get_u16();
|
||||
if ch.len() < 32 { return None; }
|
||||
ch.advance(32); // Skip Random
|
||||
|
||||
let sid_len = ch.get_u8() as usize;
|
||||
if sid_len != 32 || ch.len() < 32 { return None; }
|
||||
|
||||
let mut session_id = [0u8; 32];
|
||||
session_id.copy_from_slice(&ch[..32]);
|
||||
ch.advance(32);
|
||||
|
||||
let ciphers_len = ch.get_u16() as usize;
|
||||
if ch.len() < ciphers_len { return None; }
|
||||
ch.advance(ciphers_len);
|
||||
|
||||
let comp_len = ch.get_u8() as usize;
|
||||
if ch.len() < comp_len { return None; }
|
||||
ch.advance(comp_len);
|
||||
|
||||
let ext_len = ch.get_u16() as usize;
|
||||
if ch.len() < ext_len { return None; }
|
||||
|
||||
let mut exts = &ch[..ext_len];
|
||||
|
||||
let mut parsed_sni = None;
|
||||
let mut parsed_c_pub = None;
|
||||
|
||||
while exts.len() >= 4 {
|
||||
let ext_type = exts.get_u16();
|
||||
let ext_len = exts.get_u16() as usize;
|
||||
if exts.len() < ext_len { break; }
|
||||
|
||||
let mut ext_data = &exts[..ext_len];
|
||||
|
||||
if ext_type == 0x0000 { // SNI
|
||||
let _list_len = ext_data.get_u16() as usize;
|
||||
if ext_data.len() >= 3 {
|
||||
let name_type = ext_data.get_u8();
|
||||
if name_type == 0x00 { // Hostname
|
||||
let name_len = ext_data.get_u16() as usize;
|
||||
if ext_data.len() >= name_len {
|
||||
if let Ok(name) = std::str::from_utf8(&ext_data[..name_len]) {
|
||||
parsed_sni = Some(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ext_type == 0x0033 { // Key Share
|
||||
let _client_shares_len = ext_data.get_u16() as usize;
|
||||
while ext_data.len() >= 4 {
|
||||
let group = ext_data.get_u16();
|
||||
let key_ex_len = ext_data.get_u16() as usize;
|
||||
if ext_data.len() < key_ex_len { break; }
|
||||
|
||||
if group == 0x001d && key_ex_len == 32 { // X25519
|
||||
let mut pub_bytes = [0u8; 32];
|
||||
pub_bytes.copy_from_slice(&ext_data[..32]);
|
||||
parsed_c_pub = Some(PublicKey::from(pub_bytes));
|
||||
}
|
||||
ext_data.advance(key_ex_len);
|
||||
}
|
||||
}
|
||||
|
||||
exts.advance(ext_len);
|
||||
}
|
||||
|
||||
match (parsed_sni, parsed_c_pub) {
|
||||
(Some(sni), Some(c_pub)) => Some(ParsedClientHello { sni, session_id, c_pub }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -21,12 +21,12 @@ portable-atomic.workspace = true
|
|||
hmac.workspace = true
|
||||
sha2.workspace = true
|
||||
base64 = "0.22"
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
|
||||
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "logging", "tls12"] }
|
||||
rust-embed = "8.4"
|
||||
mime_guess = "2.0"
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
rcgen = "0.13"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
|
||||
futures-util = "0.3"
|
||||
simple-dns = "0.11.3"
|
||||
hex = "0.4.3"
|
||||
chacha20poly1305.workspace = true
|
||||
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
|
||||
|
|
|
|||
|
|
@ -56,6 +56,14 @@ pub struct ApiState {
|
|||
|
||||
// ── API configuration ────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct RealityConfig {
|
||||
pub private_key: String,
|
||||
pub short_ids: Vec<String>,
|
||||
pub dest: String,
|
||||
pub sni_list: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ApiConfig {
|
||||
pub enabled: bool,
|
||||
|
|
|
|||
|
|
@ -311,25 +311,10 @@ 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
|
||||
};
|
||||
let reality_config_arc = reality_config.map(std::sync::Arc::new);
|
||||
|
||||
tokio::select! {
|
||||
res = run_server_loop(bind_addrs.clone(), primary_socket, sockets, dispatcher, ui_cmd_rx, ui_event_tx, shared_keys, outbound, debug, tls_config, dns_server) => {
|
||||
res = run_server_loop(bind_addrs.clone(), primary_socket, sockets, dispatcher, ui_cmd_rx, ui_event_tx, shared_keys, outbound, debug, reality_config_arc, dns_server) => {
|
||||
if let Err(e) = res {
|
||||
tracing::error!("Server error: {e}");
|
||||
}
|
||||
|
|
@ -354,7 +339,7 @@ async fn run_server_loop(
|
|||
shared_keys: std::sync::Arc<std::sync::RwLock<HashMap<String, crate::api::UserMeta>>>,
|
||||
outbound: Option<OutboundConfig>,
|
||||
debug: bool,
|
||||
tls_config: Option<std::sync::Arc<rustls::ServerConfig>>,
|
||||
reality_config: Option<std::sync::Arc<RealityServerConfig>>,
|
||||
dns_server: std::sync::Arc<dns::DnsServer>,
|
||||
) -> Result<()> {
|
||||
let mut remotes: HashMap<(u32, u16), RemoteState> = HashMap::new();
|
||||
|
|
@ -392,8 +377,8 @@ async fn run_server_loop(
|
|||
let tcp_map_clone = tcp_map.clone();
|
||||
let shared_keys_clone = shared_keys.clone();
|
||||
let udp_tx_clone = udp_tx.clone();
|
||||
let reality_config_outer = reality_config.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);
|
||||
|
|
@ -431,24 +416,10 @@ 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();
|
||||
let reality = reality_config_outer.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);
|
||||
}
|
||||
if let Err(e) = crate::transport::uot::handle_tcp_connection(stream, peer_addr, keys, tx, tm, reality).await {
|
||||
tracing::warn!("UoT connection from {} closed: {}", peer_addr, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use anyhow::Result;
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -9,7 +9,16 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tracing::info;
|
||||
use tokio::net::TcpStream;
|
||||
use base64::Engine;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context as TaskContext, Poll};
|
||||
use chacha20poly1305::{aead::{Aead, KeyInit, Payload}, ChaCha20Poly1305, Nonce};
|
||||
use x25519_dalek::{StaticSecret, PublicKey};
|
||||
|
||||
use ostp_core::framing::wss::{encode_wss_frame, decode_wss_frame, WssFrameResult};
|
||||
use ostp_core::crypto::reality::{parse_client_hello, derive_keys, verify_session_id};
|
||||
use crate::RealityServerConfig;
|
||||
|
||||
pub async fn handle_tcp_connection<S>(
|
||||
mut stream: S,
|
||||
|
|
@ -17,54 +26,73 @@ pub async fn handle_tcp_connection<S>(
|
|||
shared_keys: Arc<StdRwLock<HashMap<String, crate::api::UserMeta>>>,
|
||||
udp_tx: mpsc::Sender<(Bytes, SocketAddr)>,
|
||||
tcp_map: Arc<RwLock<HashMap<SocketAddr, mpsc::Sender<Bytes>>>>,
|
||||
reality_config: Option<Arc<RealityServerConfig>>,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
// 1. Read HTTP Handshake
|
||||
let mut buf = [0u8; 4096];
|
||||
let mut initial_buf = vec![0u8; 16384];
|
||||
let mut header_len = 0;
|
||||
loop {
|
||||
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;
|
||||
}
|
||||
if header_len == buf.len() {
|
||||
anyhow::bail!("handshake headers too large");
|
||||
|
||||
// Read the first chunk to determine if it's TLS or HTTP
|
||||
let n = stream.read(&mut initial_buf).await?;
|
||||
if n == 0 {
|
||||
anyhow::bail!("connection closed before data received");
|
||||
}
|
||||
header_len += n;
|
||||
|
||||
// Check if it's a TLS record (0x16 0x03 0x01)
|
||||
if initial_buf[0] == 0x16 && initial_buf[1] == 0x03 && initial_buf[2] == 0x01 {
|
||||
if let Some(rc) = reality_config {
|
||||
return handle_reality_connection(stream, initial_buf[..header_len].to_vec(), peer_addr, shared_keys, udp_tx, tcp_map, rc).await;
|
||||
} else {
|
||||
// Received TLS but Reality is not enabled, maybe forward to a default fallback?
|
||||
// For now, just drop
|
||||
anyhow::bail!("received TLS but Reality is not configured");
|
||||
}
|
||||
}
|
||||
|
||||
let headers_str = String::from_utf8_lossy(&buf[..header_len]);
|
||||
// Otherwise, assume it's HTTP (Standard xhttp/wss)
|
||||
loop {
|
||||
if initial_buf[..header_len].windows(4).any(|w| w == b"\r\n\r\n") {
|
||||
break;
|
||||
}
|
||||
if header_len == initial_buf.len() {
|
||||
anyhow::bail!("handshake headers too large");
|
||||
}
|
||||
let n = stream.read(&mut initial_buf[header_len..]).await?;
|
||||
if n == 0 {
|
||||
anyhow::bail!("connection closed before HTTP handshake complete");
|
||||
}
|
||||
header_len += n;
|
||||
}
|
||||
|
||||
// Fast-fail scanner bots
|
||||
let headers_str = String::from_utf8_lossy(&initial_buf[..header_len]);
|
||||
|
||||
let wss = if headers_str.starts_with("GET /wss HTTP/1.1\r\n") {
|
||||
true
|
||||
} else if headers_str.starts_with("GET /stream HTTP/1.1\r\n") {
|
||||
false
|
||||
} else {
|
||||
send_404(&mut stream).await?;
|
||||
// Not a valid OSTP path. If Reality fallback was configured but we received plain HTTP, maybe fallback?
|
||||
// Actually fallback is handled above for TLS. For HTTP, we just 404.
|
||||
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n").await;
|
||||
anyhow::bail!("invalid request line");
|
||||
};
|
||||
|
||||
// Extract Authorization or Cookie for signature
|
||||
// Extract Authorization
|
||||
let mut signature_base64 = None;
|
||||
for line in headers_str.lines() {
|
||||
let lower = line.to_ascii_lowercase();
|
||||
if lower.starts_with("authorization: bearer ") {
|
||||
signature_base64 = Some(line[22..].trim().to_string());
|
||||
} else if lower.starts_with("cookie: ostp_token=") {
|
||||
signature_base64 = Some(line[19..].trim().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let sig_b64 = match signature_base64 {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
send_404(&mut stream).await?;
|
||||
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n").await;
|
||||
anyhow::bail!("missing authorization");
|
||||
}
|
||||
};
|
||||
|
|
@ -72,13 +100,13 @@ where
|
|||
let sig_bytes = match base64::Engine::decode(&base64::engine::general_purpose::STANDARD_NO_PAD, &sig_b64) {
|
||||
Ok(b) => b,
|
||||
Err(_) => {
|
||||
send_404(&mut stream).await?;
|
||||
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n").await;
|
||||
anyhow::bail!("invalid base64 signature");
|
||||
}
|
||||
};
|
||||
|
||||
if sig_bytes.len() < 8 {
|
||||
send_404(&mut stream).await?;
|
||||
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n").await;
|
||||
anyhow::bail!("signature too short");
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +116,7 @@ where
|
|||
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
if client_ts > now + 30 || client_ts < now.saturating_sub(60) {
|
||||
send_404(&mut stream).await?;
|
||||
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n").await;
|
||||
anyhow::bail!("timestamp out of bounds (replay protection)");
|
||||
}
|
||||
|
||||
|
|
@ -100,8 +128,8 @@ where
|
|||
|
||||
let mut authenticated = false;
|
||||
for key in keys {
|
||||
let mut mac = Hmac::<Sha256>::new_from_slice(key.as_bytes())
|
||||
.unwrap_or_else(|_| Hmac::<Sha256>::new_from_slice(b"default").unwrap());
|
||||
let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(key.as_bytes())
|
||||
.unwrap_or_else(|_| <Hmac<Sha256> as Mac>::new_from_slice(b"default").unwrap());
|
||||
mac.update(&ts_bytes);
|
||||
if mac.verify_slice(provided_mac).is_ok() {
|
||||
authenticated = true;
|
||||
|
|
@ -110,7 +138,7 @@ where
|
|||
}
|
||||
|
||||
if !authenticated {
|
||||
send_404(&mut stream).await?;
|
||||
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n").await;
|
||||
anyhow::bail!("unauthorized (invalid HMAC)");
|
||||
}
|
||||
|
||||
|
|
@ -122,17 +150,149 @@ where
|
|||
stream.write_all(response.as_bytes()).await?;
|
||||
}
|
||||
|
||||
info!("UoT client authenticated from {}", peer_addr);
|
||||
info!("UoT client authenticated from {} (xhttp)", peer_addr);
|
||||
|
||||
start_uot_loops(stream, peer_addr, wss, tcp_map, udp_tx).await
|
||||
}
|
||||
|
||||
async fn handle_reality_connection<S>(
|
||||
mut stream: S,
|
||||
initial_buf: Vec<u8>,
|
||||
peer_addr: SocketAddr,
|
||||
_shared_keys: Arc<StdRwLock<HashMap<String, crate::api::UserMeta>>>, // Note: Reality uses its own keys (sid)
|
||||
udp_tx: mpsc::Sender<(Bytes, SocketAddr)>,
|
||||
tcp_map: Arc<RwLock<HashMap<SocketAddr, mpsc::Sender<Bytes>>>>,
|
||||
reality_config: Arc<RealityServerConfig>,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
// Try to parse ClientHello
|
||||
let parsed_ch = parse_client_hello(&initial_buf);
|
||||
|
||||
let mut authenticated = false;
|
||||
let mut data_key_opt = None;
|
||||
|
||||
if let Some(ch) = parsed_ch {
|
||||
// Validate SNI
|
||||
if reality_config.sni_list.contains(&ch.sni) {
|
||||
// Decode Server Private Key
|
||||
if let Ok(priv_bytes) = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(&reality_config.private_key) {
|
||||
if priv_bytes.len() == 32 {
|
||||
let mut secret_bytes = [0u8; 32];
|
||||
secret_bytes.copy_from_slice(&priv_bytes);
|
||||
let server_priv = StaticSecret::from(secret_bytes);
|
||||
|
||||
let shared_secret = server_priv.diffie_hellman(&ch.c_pub);
|
||||
let (auth_key, data_key) = derive_keys(shared_secret.as_bytes());
|
||||
|
||||
// Attempt to decrypt Session ID
|
||||
if let Some((sid, _ts)) = verify_session_id(&auth_key, &ch.session_id) {
|
||||
// Check if sid is in config
|
||||
let sid_hex = hex::encode(sid);
|
||||
if reality_config.sid == sid_hex {
|
||||
authenticated = true;
|
||||
data_key_opt = Some(data_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if authenticated {
|
||||
let data_key = data_key_opt.unwrap();
|
||||
info!("Reality client authenticated from {} (sid matched)", peer_addr);
|
||||
|
||||
// Send a fake ServerHello. For now, a static, valid-looking TLS 1.3 ServerHello.
|
||||
let server_hello = hex::decode("160303007a0200007603030000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000130100002e002b0002030400330024001d0020e29b191a62d0572e9a30d0fb9d08e50bc78d591dfc1dbafbfa533411db1c8e111403030001011603030030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000170303001300000000000000000000000000000000000000").unwrap();
|
||||
stream.write_all(&server_hello).await?;
|
||||
|
||||
// At this point, the Reality tunnel is established. We need to wrap the stream with RealityStream.
|
||||
let reality_stream = RealityStream::new(stream, data_key);
|
||||
|
||||
// But wait! Inside the Reality stream, the client might send an xhttp or wss HTTP request!
|
||||
// Because xhttp_handshake_and_loop does `GET /wss` *inside* the stream.
|
||||
// So we must read the HTTP request *from the Reality stream*!
|
||||
|
||||
return process_inner_reality_stream(reality_stream, peer_addr, tcp_map, udp_tx).await;
|
||||
|
||||
} else {
|
||||
// Fallback: act as a transparent proxy to `reality_config.dest`
|
||||
info!("Reality fallback triggered for {} -> {}", peer_addr, reality_config.dest);
|
||||
let mut dest_stream: TcpStream = TcpStream::connect(&reality_config.dest).await?;
|
||||
dest_stream.write_all(&initial_buf).await?;
|
||||
|
||||
tokio::io::copy_bidirectional(&mut stream, &mut dest_stream).await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_inner_reality_stream<S>(
|
||||
mut stream: S,
|
||||
peer_addr: SocketAddr,
|
||||
tcp_map: Arc<RwLock<HashMap<SocketAddr, mpsc::Sender<Bytes>>>>,
|
||||
udp_tx: mpsc::Sender<(Bytes, SocketAddr)>,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
// 1. Read the inner HTTP Handshake
|
||||
let mut buf = [0u8; 4096];
|
||||
let mut header_len = 0;
|
||||
loop {
|
||||
let n = stream.read(&mut buf[header_len..]).await?;
|
||||
if n == 0 {
|
||||
anyhow::bail!("inner connection closed before handshake complete");
|
||||
}
|
||||
header_len += n;
|
||||
if buf[..header_len].windows(4).any(|w| w == b"\r\n\r\n") {
|
||||
break;
|
||||
}
|
||||
if header_len == buf.len() {
|
||||
anyhow::bail!("inner handshake headers too large");
|
||||
}
|
||||
}
|
||||
|
||||
let headers_str = String::from_utf8_lossy(&buf[..header_len]);
|
||||
|
||||
let wss = if headers_str.starts_with("GET /wss HTTP/1.1\r\n") {
|
||||
true
|
||||
} else if headers_str.starts_with("GET /stream HTTP/1.1\r\n") {
|
||||
false
|
||||
} else {
|
||||
anyhow::bail!("invalid inner request line");
|
||||
};
|
||||
|
||||
// We skip signature validation because Reality already authenticated the user via Session ID!
|
||||
|
||||
if wss {
|
||||
let response = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\nX-Ostp-Server: 1\r\n\r\n";
|
||||
stream.write_all(response.as_bytes()).await?;
|
||||
} else {
|
||||
let response = "HTTP/1.1 200 OK\r\nX-Ostp-Server: 1\r\nContent-Type: application/octet-stream\r\n\r\n";
|
||||
stream.write_all(response.as_bytes()).await?;
|
||||
}
|
||||
|
||||
start_uot_loops(stream, peer_addr, wss, tcp_map, udp_tx).await
|
||||
}
|
||||
|
||||
async fn start_uot_loops<S>(
|
||||
stream: S,
|
||||
peer_addr: SocketAddr,
|
||||
wss: bool,
|
||||
tcp_map: Arc<RwLock<HashMap<SocketAddr, mpsc::Sender<Bytes>>>>,
|
||||
udp_tx: mpsc::Sender<(Bytes, SocketAddr)>,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
// Register this connection in the map
|
||||
let (tx, mut rx) = mpsc::channel::<Bytes>(16384);
|
||||
{
|
||||
tcp_map.write().await.insert(peer_addr, tx);
|
||||
}
|
||||
|
||||
let headers_end = buf[..header_len].windows(4).position(|w| w == b"\r\n\r\n").unwrap() + 4;
|
||||
let leftover = &buf[headers_end..header_len];
|
||||
|
||||
// Process streams
|
||||
let (mut read_half, mut write_half) = tokio::io::split(stream);
|
||||
|
||||
|
|
@ -148,87 +308,156 @@ where
|
|||
let mut out = BytesMut::with_capacity(2 + packet.len());
|
||||
out.put_u16(packet.len() as u16);
|
||||
out.put_slice(&packet);
|
||||
if write_half.write_all(&out).await.is_err() {
|
||||
break;
|
||||
}
|
||||
if write_half.write_all(&out).await.is_err() { break; }
|
||||
}
|
||||
}
|
||||
// Cleanup on writer exit
|
||||
tcp_map_clone.write().await.remove(&peer_clone);
|
||||
let _ = tcp_map_clone.write().await.remove(&peer_clone);
|
||||
});
|
||||
|
||||
// Reader loop
|
||||
let mut buffer = BytesMut::from(leftover);
|
||||
loop {
|
||||
// Spawn reader task
|
||||
let tcp_map_clone2 = tcp_map.clone();
|
||||
let reader_task = tokio::spawn(async move {
|
||||
if wss {
|
||||
match decode_wss_frame(&buffer) {
|
||||
WssFrameResult::Incomplete => {
|
||||
let mut temp = [0u8; 1024];
|
||||
match read_half.read(&mut temp).await {
|
||||
Ok(0) | Err(_) => {
|
||||
writer_task.abort();
|
||||
tcp_map.write().await.remove(&peer_addr);
|
||||
return Ok(());
|
||||
let mut read_buf = BytesMut::with_capacity(65536);
|
||||
let mut tmp = [0u8; 8192];
|
||||
loop {
|
||||
match read_half.read(&mut tmp).await {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
read_buf.put_slice(&tmp[..n]);
|
||||
loop {
|
||||
match decode_wss_frame(&mut read_buf) {
|
||||
WssFrameResult::Frame { payload, total_len } => {
|
||||
if udp_tx.send((Bytes::from(payload), peer_clone)).await.is_err() { return; }
|
||||
read_buf.advance(total_len);
|
||||
}
|
||||
WssFrameResult::Incomplete => break,
|
||||
}
|
||||
Ok(n) => buffer.extend_from_slice(&temp[..n]),
|
||||
}
|
||||
}
|
||||
WssFrameResult::Frame { payload, total_len } => {
|
||||
let _ = buffer.split_to(total_len);
|
||||
if udp_tx.send((Bytes::from(payload), peer_addr)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while buffer.len() < 2 {
|
||||
let mut temp = [0u8; 1024];
|
||||
match read_half.read(&mut temp).await {
|
||||
Ok(0) | Err(_) => {
|
||||
writer_task.abort();
|
||||
tcp_map.write().await.remove(&peer_addr);
|
||||
return Ok(());
|
||||
let mut len_buf = [0u8; 2];
|
||||
loop {
|
||||
if read_half.read_exact(&mut len_buf).await.is_err() { break; }
|
||||
let len = u16::from_be_bytes(len_buf) as usize;
|
||||
if len > 65535 { break; }
|
||||
let mut data = vec![0u8; len];
|
||||
if read_half.read_exact(&mut data).await.is_err() { break; }
|
||||
if udp_tx.send((Bytes::from(data), peer_clone)).await.is_err() { break; }
|
||||
}
|
||||
}
|
||||
let _ = tcp_map_clone2.write().await.remove(&peer_clone);
|
||||
});
|
||||
|
||||
let _ = tokio::join!(writer_task, reader_task);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// RealityStream: Wraps a TCP stream in fake TLS Application Data Records
|
||||
// -----------------------------------------------------------------------
|
||||
struct RealityStream<S> {
|
||||
inner: S,
|
||||
data_key: ChaCha20Poly1305,
|
||||
rx_nonce: u64,
|
||||
tx_nonce: u64,
|
||||
rx_buf: BytesMut,
|
||||
}
|
||||
|
||||
impl<S> RealityStream<S> {
|
||||
fn new(inner: S, data_key: ChaCha20Poly1305) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
data_key,
|
||||
rx_nonce: 0,
|
||||
tx_nonce: 0,
|
||||
rx_buf: BytesMut::with_capacity(16384),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_nonce(seq: u64) -> [u8; 12] {
|
||||
let mut nonce = [0u8; 12];
|
||||
nonce[4..12].copy_from_slice(&seq.to_le_bytes());
|
||||
nonce
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: tokio::io::AsyncRead + Unpin> tokio::io::AsyncRead for RealityStream<S> {
|
||||
fn poll_read(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>, buf: &mut tokio::io::ReadBuf<'_>) -> Poll<std::io::Result<()>> {
|
||||
loop {
|
||||
if self.rx_buf.len() >= 5 {
|
||||
let len = u16::from_be_bytes([self.rx_buf[3], self.rx_buf[4]]) as usize;
|
||||
if self.rx_buf.len() >= 5 + len {
|
||||
if self.rx_buf[0] != 0x17 {
|
||||
return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "expected application data record")));
|
||||
}
|
||||
|
||||
let ciphertext = &self.rx_buf[5..5+len];
|
||||
let nonce_bytes = Self::make_nonce(self.rx_nonce);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
match self.data_key.decrypt(nonce, ciphertext) {
|
||||
Ok(plaintext) => {
|
||||
self.rx_nonce += 1;
|
||||
let out_len = std::cmp::min::<usize>(buf.remaining(), plaintext.len());
|
||||
buf.put_slice(&plaintext[..out_len]);
|
||||
self.rx_buf.advance(5 + len);
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
Err(_) => return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "reality decrypt failed"))),
|
||||
}
|
||||
Ok(n) => buffer.extend_from_slice(&temp[..n]),
|
||||
}
|
||||
}
|
||||
|
||||
let len = u16::from_be_bytes([buffer[0], buffer[1]]) as usize;
|
||||
|
||||
while buffer.len() < 2 + len {
|
||||
let mut temp = [0u8; 1024];
|
||||
match read_half.read(&mut temp).await {
|
||||
Ok(0) | Err(_) => {
|
||||
writer_task.abort();
|
||||
tcp_map.write().await.remove(&peer_addr);
|
||||
return Ok(());
|
||||
}
|
||||
Ok(n) => buffer.extend_from_slice(&temp[..n]),
|
||||
let mut read_buf = [0u8; 4096];
|
||||
let mut tokio_buf = tokio::io::ReadBuf::new(&mut read_buf);
|
||||
match Pin::new(&mut self.inner).poll_read(cx, &mut tokio_buf) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
if tokio_buf.filled().is_empty() { return Poll::Ready(Ok(())); }
|
||||
self.rx_buf.put_slice(tokio_buf.filled());
|
||||
}
|
||||
}
|
||||
|
||||
let packet = buffer.split_to(2 + len);
|
||||
if udp_tx.send((Bytes::from(packet[2..].to_vec()), peer_addr)).await.is_err() {
|
||||
break;
|
||||
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer_task.abort();
|
||||
tcp_map.write().await.remove(&peer_addr);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
let _ = stream.write_all(resp.as_bytes()).await;
|
||||
Ok(())
|
||||
impl<S: tokio::io::AsyncWrite + Unpin> tokio::io::AsyncWrite for RealityStream<S> {
|
||||
fn poll_write(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>, buf: &[u8]) -> Poll<std::io::Result<usize>> {
|
||||
let nonce_bytes = Self::make_nonce(self.tx_nonce);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
match self.data_key.encrypt(nonce, buf) {
|
||||
Ok(ciphertext) => {
|
||||
let mut record: BytesMut = BytesMut::with_capacity(5 + ciphertext.len());
|
||||
record.put_u8(0x17);
|
||||
record.put_u16(0x0303);
|
||||
record.put_u16(ciphertext.len() as u16);
|
||||
record.put_slice(&ciphertext);
|
||||
|
||||
match tokio::io::AsyncWrite::poll_write(Pin::new(&mut self.inner), cx, &record) {
|
||||
Poll::Ready(Ok(n)) if n == record.len() => {
|
||||
self.tx_nonce += 1;
|
||||
Poll::Ready(Ok(buf.len()))
|
||||
}
|
||||
Poll::Ready(Ok(_n)) => Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::Other, "partial write not supported"))),
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
Err(_) => Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::Other, "reality encrypt failed"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,4 +18,4 @@ rand.workspace = true
|
|||
url = "2.5"
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
snow.workspace = true
|
||||
ostp-core = { version = "0.2.68", path = "../ostp-core" }
|
||||
|
|
|
|||
|
|
@ -137,10 +137,10 @@ fn generate_reality_keys() -> (String, String, String) {
|
|||
use rand::RngCore;
|
||||
use base64::Engine;
|
||||
|
||||
let builder = snow::Builder::new("Noise_NN_25519_ChaChaPoly_BLAKE2s".parse().unwrap());
|
||||
let keypair = builder.generate_keypair().expect("failed to generate reality keys");
|
||||
let priv_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&keypair.private);
|
||||
let pub_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&keypair.public);
|
||||
let (priv_key, pub_key) = ostp_core::crypto::reality::generate_x25519_keypair();
|
||||
|
||||
let priv_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&priv_key.to_bytes());
|
||||
let pub_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(pub_key.as_bytes());
|
||||
|
||||
let mut sid_bytes = [0u8; 8];
|
||||
rand::thread_rng().fill_bytes(&mut sid_bytes);
|
||||
|
|
|
|||
Loading…
Reference in New Issue