mirror of
https://github.com/cfal/shoes.git
synced 2026-11-06 16:14:11 +00:00
0.2.5 update
This commit is contained in:
+160
-1
@@ -1,4 +1,163 @@
|
||||
# Changelog - v0.2.1
|
||||
# Changelog
|
||||
|
||||
## v0.2.5
|
||||
|
||||
### New Features
|
||||
|
||||
#### AnyTLS Protocol
|
||||
|
||||
**Server:**
|
||||
```yaml
|
||||
protocol:
|
||||
type: tls
|
||||
tls_targets:
|
||||
"example.com":
|
||||
cert: cert.pem
|
||||
key: key.pem
|
||||
protocol:
|
||||
type: anytls
|
||||
users:
|
||||
- name: user1
|
||||
password: secret123
|
||||
udp_enabled: true
|
||||
padding_scheme: ["stop=8", "0=30-30"] # Optional custom padding
|
||||
fallback: "127.0.0.1:80" # Optional fallback
|
||||
```
|
||||
|
||||
**Client:**
|
||||
```yaml
|
||||
client_chain:
|
||||
address: "example.com:443"
|
||||
protocol:
|
||||
type: tls
|
||||
protocol:
|
||||
type: anytls
|
||||
password: secret123
|
||||
```
|
||||
|
||||
#### NaiveProxy Protocol
|
||||
|
||||
**Server:**
|
||||
```yaml
|
||||
protocol:
|
||||
type: tls
|
||||
tls_targets:
|
||||
"example.com":
|
||||
cert: cert.pem
|
||||
key: key.pem
|
||||
alpn_protocols: ["h2"]
|
||||
protocol:
|
||||
type: naiveproxy
|
||||
users:
|
||||
- username: user1
|
||||
password: secret123
|
||||
padding: true
|
||||
fallback: "/var/www/html" # Optional static file fallback
|
||||
```
|
||||
|
||||
**Client:**
|
||||
```yaml
|
||||
client_chain:
|
||||
address: "example.com:443"
|
||||
protocol:
|
||||
type: tls
|
||||
alpn_protocols: ["h2"]
|
||||
protocol:
|
||||
type: naiveproxy
|
||||
username: user1
|
||||
password: secret123
|
||||
```
|
||||
|
||||
#### Mixed Port (HTTP + SOCKS5)
|
||||
Auto-detects HTTP or SOCKS5 protocol.
|
||||
|
||||
```yaml
|
||||
- address: "0.0.0.0:7890"
|
||||
protocol:
|
||||
type: mixed
|
||||
username: user
|
||||
password: pass
|
||||
udp_enabled: true # Enable SOCKS5 UDP ASSOCIATE
|
||||
```
|
||||
|
||||
#### TUN/VPN Support
|
||||
Layer 3 VPN mode using TUN devices for transparent proxying. Supports Linux, Android, and iOS.
|
||||
|
||||
```yaml
|
||||
- device_name: "tun0"
|
||||
address: "10.0.0.1"
|
||||
netmask: "255.255.255.0"
|
||||
mtu: 1500
|
||||
tcp_enabled: true
|
||||
udp_enabled: true
|
||||
icmp_enabled: true
|
||||
rules:
|
||||
- masks: "0.0.0.0/0"
|
||||
action: allow
|
||||
client_chain:
|
||||
address: "proxy.example.com:443"
|
||||
protocol:
|
||||
type: vless
|
||||
user_id: "uuid"
|
||||
```
|
||||
|
||||
**Platform support:**
|
||||
- Linux: Creates TUN device with specified name/address (requires root)
|
||||
- Android: Use `device_fd` from `VpnService.Builder.establish()`
|
||||
- iOS: Use `device_fd` from `NEPacketTunnelProvider.packetFlow`
|
||||
|
||||
#### SOCKS5 UDP ASSOCIATE
|
||||
Full UDP support for SOCKS5 servers including UDP ASSOCIATE command. Enable with `udp_enabled: true` (default).
|
||||
|
||||
```yaml
|
||||
protocol:
|
||||
type: socks
|
||||
udp_enabled: true # Default: true
|
||||
```
|
||||
|
||||
#### VLESS Fallback
|
||||
Route failed authentication attempts to a fallback destination instead of rejecting them.
|
||||
|
||||
```yaml
|
||||
protocol:
|
||||
type: vless
|
||||
user_id: "uuid"
|
||||
fallback: "127.0.0.1:80" # Serve web content for invalid clients
|
||||
```
|
||||
|
||||
#### Reality `dest_client_chain`
|
||||
Route Reality fallback (dest) connections through a proxy chain.
|
||||
|
||||
```yaml
|
||||
reality_targets:
|
||||
"www.example.com":
|
||||
private_key: "..."
|
||||
dest: "www.example.com:443"
|
||||
dest_client_chain:
|
||||
address: "proxy.example.com:1080"
|
||||
protocol:
|
||||
type: socks
|
||||
protocol:
|
||||
type: vless
|
||||
user_id: "uuid"
|
||||
```
|
||||
|
||||
### Improvements
|
||||
|
||||
- **UDP routing**: Comprehensive rewrite of UDP session routing with better multiplexing support
|
||||
- **Reality**: Improved active probing resistance with TLS 1.3 verification
|
||||
- **Performance**: Optimized buffer handling and reduced allocations
|
||||
- **QUIC**: Better buffer sizing based on quic-go recommendations
|
||||
|
||||
### Mobile Support
|
||||
|
||||
- **iOS FFI**: Added iOS bindings via `NEPacketTunnelProvider` integration
|
||||
- **Android FFI**: Added Android bindings via `VpnService` integration
|
||||
- Library now builds as `rlib`, `cdylib`, and `staticlib` for mobile embedding
|
||||
|
||||
---
|
||||
|
||||
## v0.2.1
|
||||
|
||||
## New Features
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ shoes uses YAML configuration files. Multiple configuration types can be combine
|
||||
- [Configuration Structure](#configuration-structure)
|
||||
- [Server Config](#server-config)
|
||||
- [Server Protocols](#server-protocols)
|
||||
- [TUN Config](#tun-config)
|
||||
- [Client Config](#client-config)
|
||||
- [Client Protocols](#client-protocols)
|
||||
- [Rules System](#rules-system)
|
||||
@@ -19,6 +20,7 @@ shoes uses YAML configuration files. Multiple configuration types can be combine
|
||||
A configuration file is a YAML array containing one or more configuration entries. Each entry can be:
|
||||
|
||||
- **Server Config** - Defines a proxy server instance
|
||||
- **TUN Config** - Defines a TUN/VPN device for transparent proxying
|
||||
- **Client Config Group** - Defines reusable upstream proxy configurations
|
||||
- **Rule Config Group** - Defines reusable routing rules
|
||||
- **Named PEM** - Defines reusable certificate/key data
|
||||
@@ -28,6 +30,11 @@ A configuration file is a YAML array containing one or more configuration entrie
|
||||
- address: "0.0.0.0:8080"
|
||||
protocol: ...
|
||||
|
||||
# TUN configs have 'device_name' or 'device_fd'
|
||||
- device_name: "tun0"
|
||||
address: "10.0.0.1"
|
||||
...
|
||||
|
||||
# Client config groups have 'client_group'
|
||||
- client_group: my-upstream
|
||||
client_proxy: ...
|
||||
@@ -91,8 +98,20 @@ protocol:
|
||||
type: socks # Aliases: socks5
|
||||
username: string?
|
||||
password: string?
|
||||
udp_enabled: true # Default: true (enables UDP ASSOCIATE)
|
||||
```
|
||||
|
||||
### Mixed (HTTP + SOCKS5)
|
||||
```yaml
|
||||
protocol:
|
||||
type: mixed # Aliases: http+socks, socks+http
|
||||
username: string?
|
||||
password: string?
|
||||
udp_enabled: true # Default: true (enables UDP ASSOCIATE for SOCKS5)
|
||||
```
|
||||
|
||||
Auto-detects HTTP or SOCKS5 protocol from the first byte of the connection.
|
||||
|
||||
### Shadowsocks
|
||||
```yaml
|
||||
protocol:
|
||||
@@ -126,6 +145,7 @@ protocol:
|
||||
type: vless
|
||||
user_id: string # UUID
|
||||
udp_enabled: true # Default: true (enables XUDP)
|
||||
fallback: string? # Optional fallback destination for failed auth (e.g., "127.0.0.1:80")
|
||||
```
|
||||
|
||||
### Trojan
|
||||
@@ -177,6 +197,7 @@ protocol:
|
||||
private_key: string # X25519 private key (base64url)
|
||||
short_ids: [string] # Valid client IDs (hex, 0-16 chars)
|
||||
dest: string # Fallback destination (e.g., "example.com:443")
|
||||
dest_client_chain: ClientChain? # Optional proxy chain for reaching dest
|
||||
max_time_diff: 60000 # Max timestamp diff in ms (default: 60000)
|
||||
min_client_version: [1, 8, 0] # Optional [major, minor, patch]
|
||||
max_client_version: [2, 0, 0] # Optional [major, minor, patch]
|
||||
@@ -243,6 +264,84 @@ protocol:
|
||||
zero_rtt_handshake: false # Default: false (enables 0-RTT for lower latency)
|
||||
```
|
||||
|
||||
### AnyTLS
|
||||
```yaml
|
||||
protocol:
|
||||
type: anytls
|
||||
users: # One or more users
|
||||
- name: string? # Optional display name
|
||||
password: string # User password
|
||||
udp_enabled: true # Default: true (enables UDP over TCP)
|
||||
padding_scheme: [string]? # Optional custom padding (e.g., ["stop=8", "0=30-30"])
|
||||
fallback: string? # Optional fallback destination for failed auth
|
||||
```
|
||||
|
||||
AnyTLS is a TLS-based multiplexing proxy protocol with traffic obfuscation. Should be used within TLS or Reality.
|
||||
|
||||
### NaiveProxy
|
||||
```yaml
|
||||
protocol:
|
||||
type: naiveproxy # Aliases: naive
|
||||
users: # One or more users
|
||||
- name: string? # Optional display name
|
||||
username: string # Basic Auth username
|
||||
password: string # Basic Auth password
|
||||
padding: true # Default: true (enables padding protocol)
|
||||
udp_enabled: true # Default: true (enables UDP over TCP)
|
||||
fallback: string? # Optional path to serve static files for probe resistance
|
||||
```
|
||||
|
||||
NaiveProxy implements HTTP/2 CONNECT with padding for censorship resistance. Should be used within TLS with `alpn_protocols: ["h2"]`.
|
||||
|
||||
## TUN Config
|
||||
|
||||
TUN (network TUNnel) devices operate at the IP layer (Layer 3), allowing shoes to act as a transparent VPN.
|
||||
|
||||
```yaml
|
||||
# Linux: Create TUN device by name
|
||||
device_name: string # Device name (e.g., "tun0")
|
||||
address: string # Device IP address (e.g., "10.0.0.1")
|
||||
netmask: string? # Netmask (e.g., "255.255.255.0")
|
||||
destination: string? # Gateway/destination (Linux only)
|
||||
|
||||
# iOS/Android: Use existing file descriptor
|
||||
device_fd: int # FD from VpnService (Android) or NEPacketTunnelProvider (iOS)
|
||||
|
||||
# Common settings
|
||||
mtu: 1500 # Default: 1500 (Linux), 9000 (Android), 4064 (iOS)
|
||||
tcp_enabled: true # Default: true
|
||||
udp_enabled: true # Default: true
|
||||
icmp_enabled: true # Default: true
|
||||
|
||||
# Routing rules
|
||||
rules: [RuleConfig]
|
||||
```
|
||||
|
||||
**Platform notes:**
|
||||
- **Linux**: Requires root or `CAP_NET_ADMIN`. Creates device with specified name/address.
|
||||
- **Android**: Use `device_fd` from `VpnService.Builder.establish()`. Routes configured via VpnService.
|
||||
- **iOS**: Use `device_fd` from `NEPacketTunnelProvider.packetFlow`.
|
||||
|
||||
**Example (Linux):**
|
||||
```yaml
|
||||
- device_name: "tun0"
|
||||
address: "10.0.0.1"
|
||||
netmask: "255.255.255.0"
|
||||
mtu: 1500
|
||||
tcp_enabled: true
|
||||
udp_enabled: true
|
||||
rules:
|
||||
- masks: "0.0.0.0/0"
|
||||
action: allow
|
||||
client_chain:
|
||||
address: "proxy.example.com:443"
|
||||
protocol:
|
||||
type: tls
|
||||
protocol:
|
||||
type: vless
|
||||
user_id: "uuid"
|
||||
```
|
||||
|
||||
## Client Config
|
||||
|
||||
Used in rules to specify upstream proxies.
|
||||
@@ -389,6 +488,24 @@ protocol:
|
||||
|
||||
Passes through the raw connection without protocol wrapping. Useful for testing or transparent proxying.
|
||||
|
||||
### AnyTLS Client
|
||||
```yaml
|
||||
protocol:
|
||||
type: anytls
|
||||
password: string # User password
|
||||
udp_enabled: true # Default: true (enables UDP over TCP)
|
||||
padding_scheme: [string]? # Optional custom padding scheme
|
||||
```
|
||||
|
||||
### NaiveProxy Client
|
||||
```yaml
|
||||
protocol:
|
||||
type: naiveproxy # Aliases: naive
|
||||
username: string # Basic Auth username
|
||||
password: string # Basic Auth password
|
||||
padding: true # Default: true (enables padding protocol)
|
||||
```
|
||||
|
||||
## Rules System
|
||||
|
||||
Rules determine how incoming connections are routed.
|
||||
|
||||
Generated
+555
-93
File diff suppressed because it is too large
Load Diff
+43
-11
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "shoes"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "A multi-protocol proxy server."
|
||||
@@ -10,14 +10,31 @@ readme = "README.md"
|
||||
keywords = ["proxy", "proxy-server", "shadowsocks", "v2ray", "vmess"]
|
||||
categories = ["command-line-utilities", "network-programming"]
|
||||
|
||||
[lib]
|
||||
name = "shoes"
|
||||
# rlib = Rust library (for Rust dependencies)
|
||||
# cdylib = C dynamic library (.so on Linux/Android, .dylib on macOS)
|
||||
# staticlib = C static library (.a) - required for iOS
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "shoes"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
ffi = [] # Enable FFI module for non-Android builds
|
||||
|
||||
[dependencies]
|
||||
aes = "*"
|
||||
argon2 = "*"
|
||||
async-trait = "*"
|
||||
aws-lc-rs = { version = "*", default-features = false }
|
||||
base64 = "*"
|
||||
blake3 = "*"
|
||||
bytes = "*"
|
||||
h2 = "*"
|
||||
hyper = { version = "*", features = ["server", "http1", "http2"] }
|
||||
hyper-util = { version = "*", features = ["tokio", "server"] }
|
||||
http-body-util = "*"
|
||||
dashmap = "*"
|
||||
digest = "*"
|
||||
env_logger = "*"
|
||||
@@ -25,18 +42,21 @@ futures = "*"
|
||||
h3 = "*"
|
||||
h3-quinn = "*"
|
||||
http = "*"
|
||||
indexmap = "*"
|
||||
log = { version = "*", features = ["release_max_level_info"] }
|
||||
lru = "*"
|
||||
md-5 = "*"
|
||||
memchr = "*"
|
||||
mime_guess = "*"
|
||||
notify = "*"
|
||||
parking_lot = "*"
|
||||
percent-encoding = "*"
|
||||
quinn = { version = "*", default-features = false, features = ["log", "platform-verifier", "runtime-tokio", "rustls-aws-lc-rs"] }
|
||||
rand = "*"
|
||||
rand_core = "*"
|
||||
rcgen = { version = "*", features = ["x509-parser"] }
|
||||
rcgen = { version = "*", default-features = false, features = ["aws_lc_rs", "pem"] }
|
||||
rustc-hash = "*"
|
||||
rust-argon2 = "*"
|
||||
rustls = "*"
|
||||
serde = { version = "*", features = ["derive", "std"] }
|
||||
subtle = "*"
|
||||
@@ -52,24 +72,36 @@ tokio = { version = "*", features = [
|
||||
"sync",
|
||||
"time",
|
||||
] }
|
||||
tokio-util = "*"
|
||||
tokio-util = { version = "*", features = ["time"] }
|
||||
webpki-roots = { version = "*" }
|
||||
x509-parser = "*"
|
||||
|
||||
# TUN device support (for VPN mode)
|
||||
tun = { version = "0.8", features = ["async"] }
|
||||
smoltcp = { version = "0.12", default-features = false, features = ["std", "log", "medium-ip", "proto-ipv4", "proto-ipv6", "socket-icmp", "socket-udp", "socket-tcp", "socket-tcp-cubic", "phy-tuntap_interface", "assembler-max-segment-count-32"] }
|
||||
etherparse = "0.16"
|
||||
|
||||
x509-parser = { version = "*", default-features = false, features = ["verify-aws"] }
|
||||
libc = "0.2.179"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "*"
|
||||
hyper = { version = "1", features = ["server", "http1"] }
|
||||
hyper-util = { version = "0.1", features = ["tokio", "server"] }
|
||||
http-body-util = "0.1"
|
||||
rustls-pemfile = "*"
|
||||
rcgen = "*"
|
||||
rcgen = { version = "*", default-features = false, features = ["aws_lc_rs", "pem"] }
|
||||
tokio-rustls = "*"
|
||||
serde_json = "*"
|
||||
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
# jemalloc for better memory performance (not supported on Windows MSVC or iOS)
|
||||
[target.'cfg(not(any(target_env = "msvc", target_os = "ios")))'.dependencies]
|
||||
tikv-jemallocator = "0.6"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
jni = "0.21"
|
||||
android_logger = "0.14"
|
||||
ndk-sys = "0.6"
|
||||
chrono = "0.4"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
strip = true
|
||||
|
||||
|
||||
@@ -6,24 +6,31 @@ shoes is a high-performance multi-protocol proxy server written in Rust.
|
||||
|
||||
### Proxy Protocols
|
||||
- **HTTP/HTTPS**
|
||||
- **SOCKS5**
|
||||
- **SOCKS5** (with UDP ASSOCIATE)
|
||||
- **Mixed** (auto-detect HTTP/SOCKS5)
|
||||
- **VMess AEAD**
|
||||
- **VLESS**
|
||||
- **VLESS** (with fallback support)
|
||||
- **Shadowsocks**
|
||||
- **Trojan**
|
||||
- **Snell v3**
|
||||
- **Hysteria2**
|
||||
- **TUIC v5**
|
||||
- **AnyTLS** (TLS-based multiplexing with traffic obfuscation)
|
||||
- **NaiveProxy** (HTTP/2 CONNECT with padding)
|
||||
|
||||
### Transport Protocols
|
||||
All server protocols plus:
|
||||
- **SagerNet UDP over TCP** (for Shadowsocks and SOCKS5)
|
||||
- **SagerNet UDP over TCP** (for Shadowsocks, SOCKS5, AnyTLS, NaiveProxy)
|
||||
- **ShadowTLS v3**
|
||||
- **TLS**
|
||||
- **WebSocket** (Shadowsocks SIP003)
|
||||
- **XTLS Reality**
|
||||
- **XTLS Vision** (for VLESS)
|
||||
|
||||
### TUN/VPN Mode
|
||||
- **TUN device support** - Layer 3 VPN for transparent proxying
|
||||
- Supported platforms: Linux, Android, iOS
|
||||
|
||||
### Supported Ciphers
|
||||
- **VMess**: `aes-128-gcm`, `chacha20-poly1305`, `none`
|
||||
- **Shadowsocks**: `aes-128-gcm`, `aes-256-gcm`, `chacha20-ietf-poly1305`, `2022-blake3-aes-128-gcm`, `2022-blake3-aes-256-gcm`, `2022-blake3-chacha20-ietf-poly1305`
|
||||
@@ -189,6 +196,70 @@ See the [examples](./examples) directory for all examples.
|
||||
password: supersecret
|
||||
```
|
||||
|
||||
### Mixed HTTP/SOCKS5 Server
|
||||
```yaml
|
||||
- address: 0.0.0.0:7890
|
||||
protocol:
|
||||
type: mixed
|
||||
username: myuser
|
||||
password: mypassword
|
||||
```
|
||||
|
||||
### AnyTLS Server
|
||||
```yaml
|
||||
- address: 0.0.0.0:443
|
||||
protocol:
|
||||
type: tls
|
||||
tls_targets:
|
||||
"anytls.example.com":
|
||||
cert: cert.pem
|
||||
key: key.pem
|
||||
protocol:
|
||||
type: anytls
|
||||
users:
|
||||
- name: user1
|
||||
password: secret123
|
||||
udp_enabled: true
|
||||
```
|
||||
|
||||
### NaiveProxy Server
|
||||
```yaml
|
||||
- address: 0.0.0.0:443
|
||||
protocol:
|
||||
type: tls
|
||||
tls_targets:
|
||||
"naive.example.com":
|
||||
cert: cert.pem
|
||||
key: key.pem
|
||||
alpn_protocols: ["h2"]
|
||||
protocol:
|
||||
type: naiveproxy
|
||||
users:
|
||||
- username: user1
|
||||
password: secret123
|
||||
padding: true
|
||||
```
|
||||
|
||||
### TUN VPN
|
||||
```yaml
|
||||
- device_name: tun0
|
||||
address: 10.0.0.1
|
||||
netmask: 255.255.255.0
|
||||
mtu: 1500
|
||||
tcp_enabled: true
|
||||
udp_enabled: true
|
||||
rules:
|
||||
- masks: "0.0.0.0/0"
|
||||
action: allow
|
||||
client_chain:
|
||||
address: "proxy.example.com:443"
|
||||
protocol:
|
||||
type: tls
|
||||
protocol:
|
||||
type: vless
|
||||
user_id: b85798ef-e9dc-46a4-9a87-8da4499d36d0
|
||||
```
|
||||
|
||||
## Similar Projects
|
||||
|
||||
- [apernet/hysteria](https://github.com/apernet/hysteria)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# AnyTLS server and client examples
|
||||
# AnyTLS is a TLS-based multiplexing proxy protocol with traffic obfuscation
|
||||
|
||||
# Basic AnyTLS server over TLS
|
||||
- address: "0.0.0.0:443"
|
||||
protocol:
|
||||
type: tls
|
||||
tls_targets:
|
||||
"anytls.example.com":
|
||||
cert: "cert.pem"
|
||||
key: "key.pem"
|
||||
protocol:
|
||||
type: anytls
|
||||
users:
|
||||
- name: user1
|
||||
password: "secret123"
|
||||
udp_enabled: true
|
||||
|
||||
# AnyTLS server with multiple users
|
||||
- address: "0.0.0.0:8443"
|
||||
protocol:
|
||||
type: tls
|
||||
tls_targets:
|
||||
"multi.example.com":
|
||||
cert: "cert.pem"
|
||||
key: "key.pem"
|
||||
protocol:
|
||||
type: anytls
|
||||
users:
|
||||
- name: alice
|
||||
password: "alice_password"
|
||||
- name: bob
|
||||
password: "bob_password"
|
||||
- name: charlie
|
||||
password: "charlie_password"
|
||||
|
||||
# AnyTLS server with custom padding scheme
|
||||
- address: "0.0.0.0:9443"
|
||||
protocol:
|
||||
type: tls
|
||||
tls_targets:
|
||||
"padded.example.com":
|
||||
cert: "cert.pem"
|
||||
key: "key.pem"
|
||||
protocol:
|
||||
type: anytls
|
||||
users:
|
||||
- name: user1
|
||||
password: "secret123"
|
||||
padding_scheme:
|
||||
- "stop=8"
|
||||
- "0=30-30"
|
||||
- "1=50-100"
|
||||
|
||||
# AnyTLS server with fallback for failed authentication
|
||||
- address: "0.0.0.0:10443"
|
||||
protocol:
|
||||
type: tls
|
||||
tls_targets:
|
||||
"fallback.example.com":
|
||||
cert: "cert.pem"
|
||||
key: "key.pem"
|
||||
protocol:
|
||||
type: anytls
|
||||
users:
|
||||
- name: user1
|
||||
password: "secret123"
|
||||
fallback: "127.0.0.1:80" # Serve web content for invalid clients
|
||||
|
||||
# AnyTLS over Reality (maximum censorship resistance)
|
||||
- address: "0.0.0.0:443"
|
||||
protocol:
|
||||
type: tls
|
||||
reality_targets:
|
||||
"www.microsoft.com":
|
||||
private_key: "YOUR_PRIVATE_KEY_HERE"
|
||||
short_ids: ["0123456789abcdef", ""]
|
||||
dest: "www.microsoft.com:443"
|
||||
protocol:
|
||||
type: anytls
|
||||
users:
|
||||
- name: user1
|
||||
password: "secret123"
|
||||
udp_enabled: true
|
||||
|
||||
# Local SOCKS5 client connecting to AnyTLS server
|
||||
- address: "127.0.0.1:1080"
|
||||
protocol:
|
||||
type: socks
|
||||
rules:
|
||||
- masks: "0.0.0.0/0"
|
||||
action: allow
|
||||
client_chain:
|
||||
address: "anytls.example.com:443"
|
||||
protocol:
|
||||
type: tls
|
||||
sni_hostname: "anytls.example.com"
|
||||
protocol:
|
||||
type: anytls
|
||||
password: "secret123"
|
||||
udp_enabled: true
|
||||
@@ -0,0 +1,58 @@
|
||||
# Mixed HTTP+SOCKS5 server examples
|
||||
# Auto-detects protocol from first byte - useful for clients that support both
|
||||
|
||||
# Basic mixed server (no authentication)
|
||||
- address: "0.0.0.0:7890"
|
||||
protocol:
|
||||
type: mixed
|
||||
|
||||
# Mixed server with authentication
|
||||
- address: "0.0.0.0:7891"
|
||||
protocol:
|
||||
type: mixed
|
||||
username: "myuser"
|
||||
password: "mypassword"
|
||||
|
||||
# Mixed server with UDP disabled
|
||||
- address: "0.0.0.0:7892"
|
||||
protocol:
|
||||
type: mixed
|
||||
udp_enabled: false
|
||||
|
||||
# Mixed server with custom routing rules
|
||||
- address: "0.0.0.0:7893"
|
||||
protocol:
|
||||
type: mixed
|
||||
username: "admin"
|
||||
password: "secure123"
|
||||
udp_enabled: true
|
||||
rules:
|
||||
# Direct connection for local networks
|
||||
- masks: ["192.168.0.0/16", "10.0.0.0/8", "172.16.0.0/12"]
|
||||
action: allow
|
||||
client_chain:
|
||||
protocol:
|
||||
type: direct
|
||||
# Block ad domains
|
||||
- masks: ["*.doubleclick.net", "*.googlesyndication.com"]
|
||||
action: block
|
||||
# Route everything else through upstream
|
||||
- masks: "0.0.0.0/0"
|
||||
action: allow
|
||||
client_chain:
|
||||
address: "upstream.proxy.com:1080"
|
||||
protocol:
|
||||
type: socks
|
||||
|
||||
# Mixed server over TLS (for secure local proxy)
|
||||
- address: "0.0.0.0:7894"
|
||||
protocol:
|
||||
type: tls
|
||||
tls_targets:
|
||||
"proxy.local":
|
||||
cert: "cert.pem"
|
||||
key: "key.pem"
|
||||
protocol:
|
||||
type: mixed
|
||||
username: "secure_user"
|
||||
password: "secure_pass"
|
||||
@@ -0,0 +1,92 @@
|
||||
# NaiveProxy server and client examples
|
||||
# NaiveProxy uses HTTP/2 CONNECT with padding for censorship resistance
|
||||
|
||||
# Basic NaiveProxy server over TLS
|
||||
- address: "0.0.0.0:443"
|
||||
protocol:
|
||||
type: tls
|
||||
tls_targets:
|
||||
"naive.example.com":
|
||||
cert: "cert.pem"
|
||||
key: "key.pem"
|
||||
alpn_protocols: ["h2"] # HTTP/2 required for NaiveProxy
|
||||
protocol:
|
||||
type: naiveproxy
|
||||
users:
|
||||
- username: user1
|
||||
password: "secret123"
|
||||
padding: true # Enable padding (default)
|
||||
udp_enabled: true # Enable UDP over TCP (default)
|
||||
|
||||
# NaiveProxy server with multiple users
|
||||
- address: "0.0.0.0:8443"
|
||||
protocol:
|
||||
type: tls
|
||||
tls_targets:
|
||||
"multi.example.com":
|
||||
cert: "cert.pem"
|
||||
key: "key.pem"
|
||||
alpn_protocols: ["h2"]
|
||||
protocol:
|
||||
type: naiveproxy
|
||||
users:
|
||||
- name: alice
|
||||
username: alice
|
||||
password: "alice_password"
|
||||
- name: bob
|
||||
username: bob
|
||||
password: "bob_password"
|
||||
- name: charlie
|
||||
username: charlie
|
||||
password: "charlie_password"
|
||||
|
||||
# NaiveProxy server with fallback for probe resistance
|
||||
# Serves static files when authentication fails
|
||||
- address: "0.0.0.0:9443"
|
||||
protocol:
|
||||
type: tls
|
||||
tls_targets:
|
||||
"camouflage.example.com":
|
||||
cert: "cert.pem"
|
||||
key: "key.pem"
|
||||
alpn_protocols: ["h2"]
|
||||
protocol:
|
||||
type: naiveproxy
|
||||
users:
|
||||
- username: user1
|
||||
password: "secret123"
|
||||
fallback: "/var/www/html" # Serve static files for failed auth
|
||||
|
||||
# NaiveProxy over Reality
|
||||
- address: "0.0.0.0:443"
|
||||
protocol:
|
||||
type: tls
|
||||
reality_targets:
|
||||
"www.microsoft.com":
|
||||
private_key: "YOUR_PRIVATE_KEY_HERE"
|
||||
short_ids: ["0123456789abcdef", ""]
|
||||
dest: "www.microsoft.com:443"
|
||||
protocol:
|
||||
type: naiveproxy
|
||||
users:
|
||||
- username: user1
|
||||
password: "secret123"
|
||||
udp_enabled: true
|
||||
|
||||
# Local SOCKS5 client connecting to NaiveProxy server
|
||||
- address: "127.0.0.1:1080"
|
||||
protocol:
|
||||
type: socks
|
||||
rules:
|
||||
- masks: "0.0.0.0/0"
|
||||
action: allow
|
||||
client_chain:
|
||||
address: "naive.example.com:443"
|
||||
protocol:
|
||||
type: tls
|
||||
sni_hostname: "naive.example.com"
|
||||
alpn_protocols: ["h2"]
|
||||
protocol:
|
||||
type: naiveproxy
|
||||
username: user1
|
||||
password: "secret123"
|
||||
@@ -6,6 +6,7 @@
|
||||
type: shadowsocks
|
||||
cipher: aes-128-gcm
|
||||
password: "mypassword123"
|
||||
udp_enabled: true # Enable UDP over TCP (default: true)
|
||||
|
||||
# Shadowsocks with ChaCha20-Poly1305 cipher
|
||||
- address: "0.0.0.0:8389"
|
||||
@@ -14,6 +15,14 @@
|
||||
cipher: chacha20-ietf-poly1305
|
||||
password: "anotherpassword456"
|
||||
|
||||
# Shadowsocks with UDP disabled (TCP only)
|
||||
- address: "0.0.0.0:8393"
|
||||
protocol:
|
||||
type: shadowsocks
|
||||
cipher: aes-256-gcm
|
||||
password: "tcponlypassword"
|
||||
udp_enabled: false
|
||||
|
||||
# Shadowsocks 2022 with Blake3 (requires base64 encoded key)
|
||||
- address: "0.0.0.0:8390"
|
||||
protocol:
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Basic SOCKS5 server examples
|
||||
|
||||
# Simple SOCKS5 server (no authentication)
|
||||
- address: "0.0.0.0:1080"
|
||||
protocol:
|
||||
type: socks
|
||||
|
||||
# SOCKS5 server with authentication
|
||||
- address: "0.0.0.0:1081"
|
||||
protocol:
|
||||
type: socks
|
||||
username: "myuser"
|
||||
password: "mypassword"
|
||||
|
||||
# SOCKS5 server with UDP ASSOCIATE enabled (default)
|
||||
- address: "0.0.0.0:1082"
|
||||
protocol:
|
||||
type: socks
|
||||
udp_enabled: true # This is the default
|
||||
|
||||
# SOCKS5 server with UDP disabled (TCP only)
|
||||
- address: "0.0.0.0:1083"
|
||||
protocol:
|
||||
type: socks
|
||||
udp_enabled: false
|
||||
|
||||
# SOCKS5 server with custom routing rules
|
||||
- address: "0.0.0.0:1084"
|
||||
protocol:
|
||||
type: socks
|
||||
username: "admin"
|
||||
password: "secure123"
|
||||
rules:
|
||||
# Direct connection for local networks
|
||||
- masks: ["192.168.0.0/16", "10.0.0.0/8"]
|
||||
action: allow
|
||||
client_chain:
|
||||
protocol:
|
||||
type: direct
|
||||
# Route everything else through upstream proxy
|
||||
- masks: "0.0.0.0/0"
|
||||
action: allow
|
||||
client_chain:
|
||||
address: "upstream.proxy.com:8080"
|
||||
protocol:
|
||||
type: http
|
||||
|
||||
# SOCKS5 chained through another SOCKS5 proxy
|
||||
- address: "127.0.0.1:1085"
|
||||
protocol:
|
||||
type: socks
|
||||
rules:
|
||||
- masks: "0.0.0.0/0"
|
||||
action: allow
|
||||
client_chain:
|
||||
address: "127.0.0.1:1080"
|
||||
protocol:
|
||||
type: socks
|
||||
@@ -0,0 +1,173 @@
|
||||
# TUN VPN Server Examples
|
||||
#
|
||||
# TUN (network TUNnel) devices operate at the IP layer (Layer 3), allowing
|
||||
# shoes to act as a transparent VPN that captures all IP traffic.
|
||||
#
|
||||
# IMPORTANT: TUN servers require root privileges on Linux.
|
||||
# Run with: sudo shoes -c examples/tun_vpn.yaml
|
||||
#
|
||||
# Platform Support:
|
||||
# - Linux: Creates TUN device with specified name/address (requires root)
|
||||
# - Android: Use device_fd from VpnService.Builder.establish()
|
||||
# - iOS: Use device_fd from NEPacketTunnelProvider.packetFlow
|
||||
|
||||
# =============================================================================
|
||||
# Example 1: Basic TUN VPN (Direct Connection)
|
||||
# =============================================================================
|
||||
# Routes all traffic directly to the destination.
|
||||
# Useful for testing or as a base configuration.
|
||||
|
||||
- device_name: "tun0"
|
||||
address: "10.0.0.1"
|
||||
netmask: "255.255.255.0"
|
||||
mtu: 1500
|
||||
tcp_enabled: true
|
||||
udp_enabled: true
|
||||
icmp_enabled: true
|
||||
rules:
|
||||
- masks: "0.0.0.0/0"
|
||||
action: allow
|
||||
client_chain:
|
||||
- protocol:
|
||||
type: direct
|
||||
|
||||
# =============================================================================
|
||||
# Example 2: TUN VPN through VLESS Proxy
|
||||
# =============================================================================
|
||||
# Routes all TUN traffic through a VLESS server.
|
||||
# This is a typical VPN client setup.
|
||||
|
||||
# - device_name: "tun_vless"
|
||||
# address: "10.0.1.1"
|
||||
# netmask: "255.255.255.0"
|
||||
# mtu: 1500
|
||||
# tcp_enabled: true
|
||||
# udp_enabled: true
|
||||
# icmp_enabled: true
|
||||
# rules:
|
||||
# - masks: "0.0.0.0/0"
|
||||
# action: allow
|
||||
# client_chain:
|
||||
# - address: "your-vless-server.com:443"
|
||||
# protocol:
|
||||
# type: tls
|
||||
# sni_hostname: "your-vless-server.com"
|
||||
# protocol:
|
||||
# type: vless
|
||||
# user_id: "your-uuid-here"
|
||||
# udp_enabled: true
|
||||
|
||||
# =============================================================================
|
||||
# Example 3: TUN VPN with Split Tunneling
|
||||
# =============================================================================
|
||||
# Routes local networks directly, everything else through proxy.
|
||||
|
||||
# - device_name: "tun_split"
|
||||
# address: "10.0.2.1"
|
||||
# netmask: "255.255.255.0"
|
||||
# mtu: 1500
|
||||
# tcp_enabled: true
|
||||
# udp_enabled: true
|
||||
# icmp_enabled: true
|
||||
# rules:
|
||||
# # Local/private networks go direct
|
||||
# - masks: ["192.168.0.0/16", "10.0.0.0/8", "172.16.0.0/12"]
|
||||
# action: allow
|
||||
# client_chain:
|
||||
# - protocol:
|
||||
# type: direct
|
||||
# # Everything else through Shadowsocks
|
||||
# - masks: "0.0.0.0/0"
|
||||
# action: allow
|
||||
# client_chain:
|
||||
# - address: "your-ss-server.com:8388"
|
||||
# protocol:
|
||||
# type: shadowsocks
|
||||
# cipher: "2022-blake3-aes-256-gcm"
|
||||
# password: "your-password-here"
|
||||
|
||||
# =============================================================================
|
||||
# Example 4: TUN VPN through Trojan Proxy
|
||||
# =============================================================================
|
||||
# Routes TUN traffic through a Trojan server with TLS.
|
||||
|
||||
# - device_name: "tun_trojan"
|
||||
# address: "10.0.3.1"
|
||||
# netmask: "255.255.255.0"
|
||||
# mtu: 1500
|
||||
# tcp_enabled: true
|
||||
# udp_enabled: true
|
||||
# icmp_enabled: false # Disable ICMP if not needed
|
||||
# rules:
|
||||
# - masks: "0.0.0.0/0"
|
||||
# action: allow
|
||||
# client_chain:
|
||||
# - address: "your-trojan-server.com:443"
|
||||
# protocol:
|
||||
# type: tls
|
||||
# sni_hostname: "your-trojan-server.com"
|
||||
# protocol:
|
||||
# type: trojan
|
||||
# password: "your-trojan-password"
|
||||
|
||||
# =============================================================================
|
||||
# Example 5: TUN with TCP-only (No UDP/ICMP)
|
||||
# =============================================================================
|
||||
# Useful when you only need TCP traffic proxied.
|
||||
|
||||
# - device_name: "tun_tcp"
|
||||
# address: "10.0.4.1"
|
||||
# netmask: "255.255.255.0"
|
||||
# mtu: 1500
|
||||
# tcp_enabled: true
|
||||
# udp_enabled: false
|
||||
# icmp_enabled: false
|
||||
# rules:
|
||||
# - masks: "0.0.0.0/0"
|
||||
# action: allow
|
||||
# client_chain:
|
||||
# - protocol:
|
||||
# type: direct
|
||||
|
||||
# =============================================================================
|
||||
# Example 6: TUN with Custom MTU
|
||||
# =============================================================================
|
||||
# Lower MTU can help with some proxy protocols that add overhead.
|
||||
|
||||
# - device_name: "tun_mtu"
|
||||
# address: "10.0.5.1"
|
||||
# netmask: "255.255.255.0"
|
||||
# mtu: 1400 # Lower MTU for protocols with overhead
|
||||
# tcp_enabled: true
|
||||
# udp_enabled: true
|
||||
# icmp_enabled: true
|
||||
# rules:
|
||||
# - masks: "0.0.0.0/0"
|
||||
# action: allow
|
||||
# client_chain:
|
||||
# - protocol:
|
||||
# type: direct
|
||||
|
||||
# =============================================================================
|
||||
# Usage Notes
|
||||
# =============================================================================
|
||||
#
|
||||
# 1. After starting the TUN server, add routes to direct traffic to it:
|
||||
# sudo ip route add <destination>/32 dev tun0
|
||||
#
|
||||
# 2. For full VPN mode, you may want to:
|
||||
# - Set tun0 as the default gateway
|
||||
# - Add a specific route to your proxy server via the original gateway
|
||||
# - Configure DNS to use the TUN interface
|
||||
#
|
||||
# 3. Example routing setup:
|
||||
# # Save original gateway
|
||||
# ORIG_GW=$(ip route | grep default | awk '{print $3}')
|
||||
# # Add route to proxy server via original gateway
|
||||
# sudo ip route add <proxy-server-ip>/32 via $ORIG_GW
|
||||
# # Set TUN as default gateway
|
||||
# sudo ip route del default
|
||||
# sudo ip route add default dev tun0
|
||||
#
|
||||
# 4. For Android/iOS, the device_fd is provided by the OS VPN service:
|
||||
# - device_fd: 123 # FD from VpnService/NEPacketTunnelProvider
|
||||
@@ -95,3 +95,17 @@
|
||||
type: vless
|
||||
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
|
||||
udp_enabled: true
|
||||
|
||||
# VLESS with fallback for failed authentication
|
||||
# Invalid clients are proxied to the fallback destination instead of rejected
|
||||
- address: "0.0.0.0:11443"
|
||||
protocol:
|
||||
type: tls
|
||||
tls_targets:
|
||||
"secure.example.com":
|
||||
cert: "cert.pem"
|
||||
key: "key.pem"
|
||||
protocol:
|
||||
type: vless
|
||||
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
|
||||
fallback: "127.0.0.1:80" # Serve web content for invalid clients
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ impl NetLocation {
|
||||
Self { address, port }
|
||||
}
|
||||
|
||||
pub fn _is_unspecified(&self) -> bool {
|
||||
pub fn is_unspecified(&self) -> bool {
|
||||
self == &Self::UNSPECIFIED
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
//! AnyTLS Client Handler
|
||||
//!
|
||||
//! Implements TcpClientHandler for AnyTLS protocol outbound connections.
|
||||
//!
|
||||
//! TODO: Implement session pooling to enable real multiplexing.
|
||||
//! Currently each request creates a new TLS connection + AnyTLS session with a single stream.
|
||||
//! To benefit from AnyTLS multiplexing:
|
||||
//! - Pool/reuse AnyTlsClientSession instances across multiple client requests
|
||||
//! - Open multiple streams on the same session for different destinations
|
||||
//! - Add config options like idle_session_timeout, min_idle_session (similar to sing-box)
|
||||
|
||||
use async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::address::{Address, NetLocation};
|
||||
use crate::anytls::anytls_client_session::AnyTlsClientSession;
|
||||
use crate::anytls::anytls_padding::PaddingFactory;
|
||||
use crate::async_stream::AsyncMessageStream;
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::tcp::tcp_handler::{TcpClientHandler, TcpClientSetupResult};
|
||||
use crate::uot::UOT_V2_MAGIC_ADDRESS;
|
||||
use crate::vless::VlessMessageStream;
|
||||
|
||||
/// AnyTLS client handler implementing TcpClientHandler.
|
||||
///
|
||||
/// Creates an AnyTLS session on the provided transport (expected to be TLS-wrapped),
|
||||
/// opens multiplexed streams to destinations, and supports UDP-over-TCP via UoT magic addresses.
|
||||
#[derive(Clone)]
|
||||
pub struct AnyTlsClientHandler {
|
||||
/// Authentication password
|
||||
password: String,
|
||||
/// Padding factory for traffic obfuscation
|
||||
padding: Arc<PaddingFactory>,
|
||||
/// UDP enabled
|
||||
udp_enabled: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AnyTlsClientHandler {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AnyTlsClientHandler")
|
||||
.field("udp_enabled", &self.udp_enabled)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyTlsClientHandler {
|
||||
/// Create a new AnyTLS client handler
|
||||
pub fn new(password: String, padding: Arc<PaddingFactory>, udp_enabled: bool) -> Self {
|
||||
Self {
|
||||
password,
|
||||
padding,
|
||||
udp_enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TcpClientHandler for AnyTlsClientHandler {
|
||||
async fn setup_client_tcp_stream(
|
||||
&self,
|
||||
client_stream: Box<dyn AsyncStream>,
|
||||
remote_location: NetLocation,
|
||||
) -> std::io::Result<TcpClientSetupResult> {
|
||||
let session =
|
||||
AnyTlsClientSession::new(client_stream, &self.password, Arc::clone(&self.padding))
|
||||
.await?;
|
||||
|
||||
let stream = session.open_stream(remote_location).await?;
|
||||
|
||||
Ok(TcpClientSetupResult {
|
||||
client_stream: Box::new(stream),
|
||||
early_data: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn supports_udp_over_tcp(&self) -> bool {
|
||||
self.udp_enabled
|
||||
}
|
||||
|
||||
async fn setup_client_udp_bidirectional(
|
||||
&self,
|
||||
client_stream: Box<dyn AsyncStream>,
|
||||
target: NetLocation,
|
||||
) -> std::io::Result<Box<dyn AsyncMessageStream>> {
|
||||
if !self.udp_enabled {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
"UDP not enabled for AnyTLS client",
|
||||
));
|
||||
}
|
||||
|
||||
let session =
|
||||
AnyTlsClientSession::new(client_stream, &self.password, Arc::clone(&self.padding))
|
||||
.await?;
|
||||
|
||||
// UoT V2 Connect Mode: single destination via magic address
|
||||
let uot_dest = NetLocation::new(Address::Hostname(UOT_V2_MAGIC_ADDRESS.to_string()), 0);
|
||||
let mut stream = session.open_stream(uot_dest).await?;
|
||||
|
||||
// UoT V2 header: isConnect(1) + destination
|
||||
stream.write_u8(1).await?;
|
||||
stream.write_all(&encode_socks_address(&target)).await?;
|
||||
stream.flush().await?;
|
||||
|
||||
let message_stream = VlessMessageStream::new(stream);
|
||||
|
||||
Ok(Box::new(message_stream))
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode a NetLocation to SOCKS address format
|
||||
fn encode_socks_address(location: &NetLocation) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(32);
|
||||
|
||||
match location.address() {
|
||||
Address::Ipv4(ip) => {
|
||||
buf.push(0x01); // IPv4
|
||||
buf.extend_from_slice(&ip.octets());
|
||||
}
|
||||
Address::Ipv6(ip) => {
|
||||
buf.push(0x04); // IPv6
|
||||
buf.extend_from_slice(&ip.octets());
|
||||
}
|
||||
Address::Hostname(host) => {
|
||||
buf.push(0x03); // Domain
|
||||
buf.push(host.len() as u8);
|
||||
buf.extend_from_slice(host.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
buf.extend_from_slice(&location.port().to_be_bytes());
|
||||
buf
|
||||
}
|
||||
@@ -0,0 +1,735 @@
|
||||
//! AnyTLS Client Implementation
|
||||
//!
|
||||
//! Provides AnyTLS client support for outbound connections.
|
||||
//! Creates multiplexed streams over a single TLS connection.
|
||||
|
||||
use aws_lc_rs::digest::{SHA256, digest};
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, Ordering};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::sync::{Mutex, RwLock, mpsc, oneshot};
|
||||
|
||||
use crate::address::NetLocation;
|
||||
use crate::anytls::anytls_padding::PaddingFactory;
|
||||
use crate::anytls::anytls_stream::{AnyTlsStream, STREAM_CHANNEL_BUFFER};
|
||||
use crate::anytls::anytls_types::{Command, FRAME_HEADER_SIZE, Frame, FrameCodec, StringMap};
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::socks_handler::write_location_to_vec;
|
||||
|
||||
/// Outgoing message types for the unified writer channel
|
||||
enum OutgoingMessage {
|
||||
/// Buffered frames (Settings + SYN + destination) - sent as single TLS record
|
||||
/// This is used for the first stream to avoid fingerprinting
|
||||
Buffered { data: Bytes },
|
||||
/// Control frame (Settings, SYN, etc.) - encoded in writer loop
|
||||
Control {
|
||||
cmd: Command,
|
||||
stream_id: u32,
|
||||
data: Bytes,
|
||||
},
|
||||
/// Data frame for a stream (PSH) - encoded in writer loop
|
||||
Data { stream_id: u32, data: Bytes },
|
||||
/// FIN frame for a stream - encoded in writer loop
|
||||
Fin { stream_id: u32 },
|
||||
}
|
||||
|
||||
/// AnyTLS client session - manages multiplexed streams over a connection
|
||||
///
|
||||
/// Each session handles:
|
||||
/// - Authentication with the server
|
||||
/// - Protocol version negotiation
|
||||
/// - Stream multiplexing (multiple logical streams over one connection)
|
||||
/// - Frame-based communication
|
||||
pub struct AnyTlsClientSession {
|
||||
/// Stream management
|
||||
streams: RwLock<HashMap<u32, mpsc::Sender<Bytes>>>,
|
||||
stream_id_counter: AtomicU32,
|
||||
|
||||
/// Unified channel for all outgoing messages (control frames and data)
|
||||
/// Using a single channel ensures proper ordering of SYN/data frames
|
||||
outgoing_tx: mpsc::UnboundedSender<OutgoingMessage>,
|
||||
|
||||
/// Session state
|
||||
is_closed: Arc<AtomicBool>,
|
||||
|
||||
/// Padding configuration
|
||||
padding: Arc<PaddingFactory>,
|
||||
|
||||
/// Protocol version negotiation
|
||||
peer_version: AtomicU8,
|
||||
|
||||
/// Pending stream opens (stream_id -> completion sender)
|
||||
pending_opens: Mutex<HashMap<u32, oneshot::Sender<Result<(), String>>>>,
|
||||
|
||||
/// Padding state (client only) - true until stop packets sent
|
||||
send_padding: AtomicBool,
|
||||
/// Packet counter for padding
|
||||
pkt_counter: AtomicU32,
|
||||
|
||||
/// Initial buffer for coalescing Settings + first SYN + first destination
|
||||
/// This ensures they are sent as a single TLS record to avoid fingerprinting.
|
||||
/// Once taken (by first open_stream), this is None and subsequent streams
|
||||
/// are sent normally through the channel.
|
||||
initial_buffer: std::sync::Mutex<Option<BytesMut>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AnyTlsClientSession {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AnyTlsClientSession")
|
||||
.field("is_closed", &self.is_closed.load(Ordering::Relaxed))
|
||||
.field("peer_version", &self.peer_version.load(Ordering::Relaxed))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyTlsClientSession {
|
||||
/// Create a new client session on the given transport.
|
||||
///
|
||||
/// This performs:
|
||||
/// 1. Send authentication frame (password_hash + padding)
|
||||
/// 2. Buffer client Settings frame (sent with first stream's SYN + destination)
|
||||
/// 3. Start reader/writer tasks
|
||||
///
|
||||
/// The Settings frame is NOT sent immediately - it's buffered and will be
|
||||
/// sent together with the first stream's SYN and destination address as a
|
||||
/// single TLS record to avoid fingerprinting.
|
||||
///
|
||||
/// Returns the session wrapped in Arc for shared ownership.
|
||||
pub async fn new(
|
||||
mut transport: Box<dyn AsyncStream>,
|
||||
password: &str,
|
||||
padding: Arc<PaddingFactory>,
|
||||
) -> io::Result<Arc<Self>> {
|
||||
let hash_result = digest(&SHA256, password.as_bytes());
|
||||
let mut password_hash = [0u8; 32];
|
||||
password_hash.copy_from_slice(hash_result.as_ref());
|
||||
|
||||
// Send authentication (this is packet 0, sent separately)
|
||||
Self::send_auth(&mut transport, &password_hash, &padding).await?;
|
||||
|
||||
// Create unified channel for all outgoing messages
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::unbounded_channel();
|
||||
|
||||
// Pre-encode Settings frame into initial buffer
|
||||
// This will be sent together with first SYN + destination as one TLS record
|
||||
let initial_buffer = Self::create_initial_buffer(&padding);
|
||||
|
||||
let session = Arc::new(Self {
|
||||
streams: RwLock::new(HashMap::new()),
|
||||
stream_id_counter: AtomicU32::new(0),
|
||||
outgoing_tx,
|
||||
is_closed: Arc::new(AtomicBool::new(false)),
|
||||
padding: Arc::clone(&padding),
|
||||
peer_version: AtomicU8::new(1), // Assume v1 until server confirms v2
|
||||
pending_opens: Mutex::new(HashMap::new()),
|
||||
send_padding: AtomicBool::new(true),
|
||||
pkt_counter: AtomicU32::new(0), // Start at 0, incremented before use
|
||||
initial_buffer: std::sync::Mutex::new(Some(initial_buffer)),
|
||||
});
|
||||
|
||||
// NOTE: Settings is NOT sent here - it's in initial_buffer and will be
|
||||
// sent with the first stream's SYN + destination
|
||||
|
||||
// Spawn background tasks
|
||||
let (read_half, write_half) = tokio::io::split(transport);
|
||||
Self::spawn_tasks(Arc::clone(&session), read_half, write_half, outgoing_rx);
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Create initial buffer with Settings frame pre-encoded
|
||||
fn create_initial_buffer(padding: &PaddingFactory) -> BytesMut {
|
||||
let mut settings = StringMap::new();
|
||||
settings.insert("v", "2");
|
||||
settings.insert("client", "shoes-anytls/1.0");
|
||||
settings.insert("padding-md5", padding.md5());
|
||||
|
||||
let settings_frame =
|
||||
Frame::with_data(Command::Settings, 0, Bytes::from(settings.to_bytes()));
|
||||
|
||||
// Allocate buffer with room for Settings + SYN + typical destination
|
||||
let mut buffer = BytesMut::with_capacity(256);
|
||||
settings_frame.encode_into(&mut buffer);
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Send authentication frame
|
||||
async fn send_auth(
|
||||
transport: &mut Box<dyn AsyncStream>,
|
||||
password_hash: &[u8; 32],
|
||||
padding: &PaddingFactory,
|
||||
) -> io::Result<()> {
|
||||
// Calculate padding for packet 0
|
||||
let padding_sizes = padding.generate_record_payload_sizes(0);
|
||||
let padding_len = padding_sizes.first().copied().unwrap_or(0).max(0) as u16;
|
||||
|
||||
// Build auth frame: SHA256(password) + padding_len(u16) + padding
|
||||
let mut auth_frame = Vec::with_capacity(34 + padding_len as usize);
|
||||
auth_frame.extend_from_slice(password_hash);
|
||||
auth_frame.extend_from_slice(&padding_len.to_be_bytes());
|
||||
if padding_len > 0 {
|
||||
auth_frame.resize(34 + padding_len as usize, 0);
|
||||
}
|
||||
|
||||
transport.write_all(&auth_frame).await?;
|
||||
transport.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a control frame through the writer channel (zero-copy)
|
||||
fn send_control_frame(&self, cmd: Command, stream_id: u32, data: Bytes) -> io::Result<()> {
|
||||
self.outgoing_tx
|
||||
.send(OutgoingMessage::Control {
|
||||
cmd,
|
||||
stream_id,
|
||||
data,
|
||||
})
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "Session writer closed"))
|
||||
}
|
||||
|
||||
/// Send buffered frames (Settings + SYN + destination) as single message
|
||||
fn send_buffered(&self, data: Bytes) -> io::Result<()> {
|
||||
self.outgoing_tx
|
||||
.send(OutgoingMessage::Buffered { data })
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "Session writer closed"))
|
||||
}
|
||||
|
||||
/// Spawn reader and writer tasks
|
||||
fn spawn_tasks<R, W>(
|
||||
session: Arc<Self>,
|
||||
reader: R,
|
||||
writer: W,
|
||||
outgoing_rx: mpsc::UnboundedReceiver<OutgoingMessage>,
|
||||
) where
|
||||
R: tokio::io::AsyncRead + Send + Unpin + 'static,
|
||||
W: tokio::io::AsyncWrite + Send + Unpin + 'static,
|
||||
{
|
||||
// Writer task - handles all outgoing messages (control and data)
|
||||
let session_writer = Arc::clone(&session);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = Self::writer_loop(session_writer, writer, outgoing_rx).await {
|
||||
log::debug!("AnyTLS client writer ended: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Reader task
|
||||
let session_reader = Arc::clone(&session);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = Self::reader_loop(session_reader, reader).await {
|
||||
log::debug!("AnyTLS client reader ended: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Writer loop - sends frames to the transport with padding
|
||||
///
|
||||
/// Uses reusable buffers to minimize allocations in the hot path:
|
||||
/// - write_buf: for encoding frames
|
||||
/// - padding_buf: for constructing payload + padding in single writes
|
||||
///
|
||||
/// Key optimizations:
|
||||
/// - Buffered messages (Settings + SYN + destination) sent as single TLS record
|
||||
/// - Padding frames concatenated with payload before write (single syscall)
|
||||
/// - Zero-allocation padding using put_bytes()
|
||||
async fn writer_loop<W>(
|
||||
session: Arc<Self>,
|
||||
mut writer: W,
|
||||
mut outgoing_rx: mpsc::UnboundedReceiver<OutgoingMessage>,
|
||||
) -> io::Result<()>
|
||||
where
|
||||
W: tokio::io::AsyncWrite + Send + Unpin,
|
||||
{
|
||||
log::debug!("AnyTLS client writer loop started");
|
||||
|
||||
// Pre-allocate write buffer for max frame size (64KB payload + header + margin)
|
||||
// This buffer is reused for all frames to avoid per-frame allocations
|
||||
let mut write_buf = BytesMut::with_capacity(65536 + FRAME_HEADER_SIZE + 64);
|
||||
|
||||
// Pre-allocate padding buffer for combining payload + WASTE frames
|
||||
// Used to ensure single write() call per padding segment
|
||||
let mut padding_buf = BytesMut::with_capacity(65536 + FRAME_HEADER_SIZE * 2 + 64);
|
||||
|
||||
while let Some(msg) = outgoing_rx.recv().await {
|
||||
if session.is_closed.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Clear and reuse buffer for each frame
|
||||
write_buf.clear();
|
||||
|
||||
match msg {
|
||||
OutgoingMessage::Buffered { data } => {
|
||||
// Send buffered frames as single TLS record to avoid fingerprinting
|
||||
log::debug!("AnyTLS client writer: buffered frames {} bytes", data.len());
|
||||
Self::write_with_padding(&session, &mut writer, &data, &mut padding_buf)
|
||||
.await?;
|
||||
writer.flush().await?;
|
||||
}
|
||||
OutgoingMessage::Control {
|
||||
cmd,
|
||||
stream_id,
|
||||
data,
|
||||
} => {
|
||||
Frame::with_data(cmd, stream_id, data).encode_into(&mut write_buf);
|
||||
log::debug!(
|
||||
"AnyTLS client writer: control frame {} bytes",
|
||||
write_buf.len()
|
||||
);
|
||||
Self::write_with_padding(&session, &mut writer, &write_buf, &mut padding_buf)
|
||||
.await?;
|
||||
writer.flush().await?;
|
||||
}
|
||||
OutgoingMessage::Data { stream_id, data } => {
|
||||
Frame::data(stream_id, data).encode_into(&mut write_buf);
|
||||
log::debug!(
|
||||
"AnyTLS client writer: stream {} data {} bytes",
|
||||
stream_id,
|
||||
write_buf.len()
|
||||
);
|
||||
Self::write_with_padding(&session, &mut writer, &write_buf, &mut padding_buf)
|
||||
.await?;
|
||||
writer.flush().await?;
|
||||
}
|
||||
OutgoingMessage::Fin { stream_id } => {
|
||||
Frame::control(Command::Fin, stream_id).encode_into(&mut write_buf);
|
||||
log::debug!("AnyTLS client writer: stream {} FIN", stream_id);
|
||||
Self::write_with_padding(&session, &mut writer, &write_buf, &mut padding_buf)
|
||||
.await?;
|
||||
writer.flush().await?;
|
||||
|
||||
let mut streams = session.streams.write().await;
|
||||
streams.remove(&stream_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
log::debug!("AnyTLS client writer loop: channel closed, exiting");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write data with padding applied (client-side padding)
|
||||
///
|
||||
/// Key optimizations for protocol conformance and performance:
|
||||
/// 1. Payload + padding are concatenated BEFORE write (single TLS record)
|
||||
/// 2. Uses put_bytes() for zero-fill (no Vec allocation)
|
||||
/// 3. Reuses padding_buf across calls
|
||||
///
|
||||
/// This matches the reference Go implementation which uses slices.Concat()
|
||||
/// to combine payload and padding before writing.
|
||||
async fn write_with_padding<W>(
|
||||
session: &Arc<Self>,
|
||||
writer: &mut W,
|
||||
data: &[u8],
|
||||
padding_buf: &mut BytesMut,
|
||||
) -> io::Result<()>
|
||||
where
|
||||
W: tokio::io::AsyncWrite + Send + Unpin,
|
||||
{
|
||||
use crate::anytls::anytls_padding::CHECK_MARK;
|
||||
|
||||
if !session.send_padding.load(Ordering::Relaxed) {
|
||||
// Padding disabled, write directly
|
||||
return writer.write_all(data).await;
|
||||
}
|
||||
|
||||
// Increment packet counter and check if we should still pad
|
||||
let pkt = session.pkt_counter.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
let stop = session.padding.stop();
|
||||
|
||||
if pkt >= stop {
|
||||
session.send_padding.store(false, Ordering::Relaxed);
|
||||
return writer.write_all(data).await;
|
||||
}
|
||||
|
||||
// Get padding sizes for this packet
|
||||
let pkt_sizes = session.padding.generate_record_payload_sizes(pkt);
|
||||
if pkt_sizes.is_empty() {
|
||||
return writer.write_all(data).await;
|
||||
}
|
||||
|
||||
let mut remaining = data;
|
||||
|
||||
for size in pkt_sizes {
|
||||
if size == CHECK_MARK {
|
||||
// Check mark: stop if no more payload
|
||||
if remaining.is_empty() {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let l = size as usize;
|
||||
let remain_len = remaining.len();
|
||||
|
||||
if remain_len > l {
|
||||
// This segment is all payload, no padding needed
|
||||
writer.write_all(&remaining[..l]).await?;
|
||||
remaining = &remaining[l..];
|
||||
} else if remain_len > 0 {
|
||||
// This segment contains payload + padding
|
||||
// We need to combine them into single write for correct TLS record
|
||||
let padding_len = l.saturating_sub(remain_len + FRAME_HEADER_SIZE);
|
||||
if padding_len > 0 {
|
||||
// Combine payload + WASTE frame into single buffer
|
||||
padding_buf.clear();
|
||||
padding_buf.reserve(remain_len + FRAME_HEADER_SIZE + padding_len);
|
||||
|
||||
// Add payload
|
||||
padding_buf.extend_from_slice(remaining);
|
||||
|
||||
// Add WASTE frame header (7 bytes) - NO Vec ALLOCATION
|
||||
padding_buf.put_u8(Command::Waste as u8);
|
||||
padding_buf.put_u32(0); // stream_id = 0 for padding
|
||||
padding_buf.put_u16(padding_len as u16);
|
||||
|
||||
// Add padding zeros - put_bytes does NOT allocate Vec!
|
||||
padding_buf.put_bytes(0, padding_len);
|
||||
|
||||
// Single write for payload + padding (one TLS record)
|
||||
writer.write_all(padding_buf).await?;
|
||||
} else {
|
||||
// Padding would be negative/zero, just write payload
|
||||
writer.write_all(remaining).await?;
|
||||
}
|
||||
remaining = &[];
|
||||
} else {
|
||||
// This segment is pure padding (no payload left)
|
||||
// Build WASTE frame directly in padding_buf - NO Vec ALLOCATION
|
||||
padding_buf.clear();
|
||||
padding_buf.reserve(FRAME_HEADER_SIZE + l);
|
||||
|
||||
// WASTE frame header
|
||||
padding_buf.put_u8(Command::Waste as u8);
|
||||
padding_buf.put_u32(0); // stream_id = 0
|
||||
padding_buf.put_u16(l as u16);
|
||||
|
||||
// Padding zeros - put_bytes does NOT allocate Vec!
|
||||
padding_buf.put_bytes(0, l);
|
||||
|
||||
writer.write_all(padding_buf).await?;
|
||||
}
|
||||
}
|
||||
|
||||
// Write any remaining payload after padding scheme exhausted
|
||||
if !remaining.is_empty() {
|
||||
writer.write_all(remaining).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reader loop - receives frames from the transport
|
||||
async fn reader_loop<R>(session: Arc<Self>, mut reader: R) -> io::Result<()>
|
||||
where
|
||||
R: tokio::io::AsyncRead + Send + Unpin,
|
||||
{
|
||||
log::debug!("AnyTLS client reader loop started");
|
||||
let mut buffer = BytesMut::with_capacity(8192);
|
||||
|
||||
loop {
|
||||
if session.is_closed.load(Ordering::Relaxed) {
|
||||
log::debug!("AnyTLS client reader loop: session closed, exiting");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Decode any frames already in buffer
|
||||
while let Some(frame) = FrameCodec::decode(&mut buffer)? {
|
||||
log::debug!(
|
||||
"AnyTLS client received frame: {:?} stream={} len={}",
|
||||
frame.cmd,
|
||||
frame.stream_id,
|
||||
frame.data.len()
|
||||
);
|
||||
if let Err(e) = session.handle_frame(frame).await {
|
||||
log::warn!("AnyTLS client error handling frame: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Read more data
|
||||
let n = reader.read_buf(&mut buffer).await?;
|
||||
if n == 0 {
|
||||
log::debug!("AnyTLS client reader loop: connection closed (EOF)");
|
||||
return Ok(()); // Connection closed
|
||||
}
|
||||
log::debug!("AnyTLS client reader: read {} bytes", n);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a received frame
|
||||
async fn handle_frame(&self, frame: Frame) -> io::Result<()> {
|
||||
match frame.cmd {
|
||||
Command::Psh => {
|
||||
// Data for a stream
|
||||
if frame.data.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let tx = {
|
||||
let streams = self.streams.read().await;
|
||||
streams.get(&frame.stream_id).cloned()
|
||||
};
|
||||
|
||||
if let Some(tx) = tx {
|
||||
if tx.send(frame.data).await.is_err() {
|
||||
log::trace!("Stream {} channel closed", frame.stream_id);
|
||||
}
|
||||
} else {
|
||||
log::trace!("Data for unknown stream {}", frame.stream_id);
|
||||
}
|
||||
}
|
||||
|
||||
Command::Fin => {
|
||||
// Stream closed by server
|
||||
let tx = {
|
||||
let mut streams = self.streams.write().await;
|
||||
streams.remove(&frame.stream_id)
|
||||
};
|
||||
|
||||
// Signal EOF
|
||||
if let Some(tx) = tx {
|
||||
let _ = tx.send(Bytes::new()).await;
|
||||
}
|
||||
}
|
||||
|
||||
Command::SynAck => {
|
||||
// Stream open acknowledged (v2)
|
||||
let mut pending = self.pending_opens.lock().await;
|
||||
if let Some(sender) = pending.remove(&frame.stream_id) {
|
||||
if frame.data.is_empty() {
|
||||
let _ = sender.send(Ok(()));
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&frame.data).to_string();
|
||||
let _ = sender.send(Err(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Command::ServerSettings => {
|
||||
// Server settings response (v2)
|
||||
let settings = StringMap::from_bytes(&frame.data);
|
||||
if let Some(v) = settings.get("v").and_then(|s| s.parse::<u8>().ok()) {
|
||||
self.peer_version.store(v, Ordering::Relaxed);
|
||||
log::debug!("AnyTLS server version: {}", v);
|
||||
}
|
||||
}
|
||||
|
||||
Command::UpdatePaddingScheme => {
|
||||
// Server sent new padding scheme for censorship resistance.
|
||||
// Per protocol: "subsequent new sessions must use the server's padding scheme"
|
||||
//
|
||||
// TODO: Implement UpdatePaddingScheme support:
|
||||
// 1. Change AnyTlsClientHandler.padding from Arc<PaddingFactory> to
|
||||
// Arc<arc_swap::ArcSwap<PaddingFactory>> (or similar atomic wrapper)
|
||||
// 2. Pass a reference to that atomic into AnyTlsClientSession
|
||||
// 3. Here, parse frame.data as raw padding scheme bytes and call:
|
||||
// if let Ok(new_factory) = PaddingFactory::new(&frame.data) {
|
||||
// shared_padding.store(Arc::new(new_factory));
|
||||
// log::info!("AnyTLS padding scheme updated: {}", new_factory.md5());
|
||||
// }
|
||||
// 4. New sessions created by the handler will automatically use the updated scheme
|
||||
//
|
||||
// Reference: anytls-go/proxy/session/session.go:319-332
|
||||
// Reference: sing-anytls/session/session.go:314-327
|
||||
log::debug!(
|
||||
"AnyTLS received padding scheme update ({} bytes) - not yet implemented",
|
||||
frame.data.len()
|
||||
);
|
||||
}
|
||||
|
||||
Command::Alert => {
|
||||
// Server alert - fatal
|
||||
let msg = String::from_utf8_lossy(&frame.data);
|
||||
log::warn!("AnyTLS server alert: {}", msg);
|
||||
self.is_closed.store(true, Ordering::Relaxed);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::ConnectionAborted,
|
||||
format!("Server alert: {}", msg),
|
||||
));
|
||||
}
|
||||
|
||||
Command::HeartRequest => {
|
||||
// Respond to heartbeat
|
||||
let _ =
|
||||
self.send_control_frame(Command::HeartResponse, frame.stream_id, Bytes::new());
|
||||
}
|
||||
|
||||
Command::HeartResponse => {
|
||||
// Heartbeat response - acknowledge
|
||||
log::trace!("AnyTLS heartbeat response received");
|
||||
}
|
||||
|
||||
Command::Waste => {
|
||||
// Padding - discard
|
||||
}
|
||||
|
||||
_ => {
|
||||
log::debug!("Unexpected command: {:?}", frame.cmd);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Open a new stream to the given destination
|
||||
///
|
||||
/// Takes `self: &Arc<Self>` to allow the stream to hold a reference
|
||||
/// to the session, keeping it alive for the stream's lifetime.
|
||||
///
|
||||
/// For the FIRST stream opened on a session, Settings + SYN + destination
|
||||
/// are sent together as a single TLS record to avoid fingerprinting.
|
||||
/// Subsequent streams use normal frame-by-frame transmission.
|
||||
pub async fn open_stream(
|
||||
self: &Arc<Self>,
|
||||
destination: NetLocation,
|
||||
) -> io::Result<AnyTlsStream> {
|
||||
if self.is_closed.load(Ordering::Relaxed) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotConnected,
|
||||
"Session is closed",
|
||||
));
|
||||
}
|
||||
|
||||
// Allocate stream ID (sequential starting from 1, matching Go implementation)
|
||||
// fetch_add returns old value, so +1 gives us 1, 2, 3, ...
|
||||
let stream_id = self.stream_id_counter.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
|
||||
// Create stream channels
|
||||
let (data_tx, data_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
|
||||
// Register stream
|
||||
{
|
||||
let mut streams = self.streams.write().await;
|
||||
streams.insert(stream_id, data_tx);
|
||||
}
|
||||
|
||||
// Set up SYNACK receiver if v2
|
||||
let synack_rx = if self.peer_version.load(Ordering::Relaxed) >= 2 {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let mut pending = self.pending_opens.lock().await;
|
||||
pending.insert(stream_id, tx);
|
||||
Some(rx)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Encode destination address
|
||||
let dest_data = write_location_to_vec(&destination);
|
||||
|
||||
// Try to take the initial buffer (only first stream gets it)
|
||||
// If present, we send Settings + SYN + destination as single message
|
||||
let buffered_data = {
|
||||
let mut buf_guard = self.initial_buffer.lock().unwrap();
|
||||
if let Some(ref mut buf) = *buf_guard {
|
||||
// First stream - add SYN and destination to buffer
|
||||
// This creates: [Settings frame][SYN frame][PSH frame with destination]
|
||||
Frame::control(Command::Syn, stream_id).encode_into(buf);
|
||||
Frame::data(stream_id, Bytes::from(dest_data.clone())).encode_into(buf);
|
||||
|
||||
// Take the buffer (subsequent streams won't have it)
|
||||
buf_guard.take().map(|b| b.freeze())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(data) = buffered_data {
|
||||
// First stream: send buffered Settings + SYN + destination as one message
|
||||
// This ensures they go out as a single TLS record
|
||||
log::debug!(
|
||||
"AnyTLS client: sending buffered frames ({} bytes) for first stream {}",
|
||||
data.len(),
|
||||
stream_id
|
||||
);
|
||||
self.send_buffered(data)?;
|
||||
} else {
|
||||
// Subsequent streams: send SYN and destination normally
|
||||
self.send_control_frame(Command::Syn, stream_id, Bytes::new())?;
|
||||
self.send_control_frame(Command::Psh, stream_id, Bytes::from(dest_data))?;
|
||||
}
|
||||
|
||||
// Wait for SYNACK if v2
|
||||
if let Some(synack_rx) = synack_rx {
|
||||
// 3-second timeout matches Go implementation's deadline watcher
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(3), synack_rx).await {
|
||||
Ok(Ok(Ok(()))) => {
|
||||
log::debug!("AnyTLS stream {} opened", stream_id);
|
||||
}
|
||||
Ok(Ok(Err(error))) => {
|
||||
// Remove stream on error
|
||||
let mut streams = self.streams.write().await;
|
||||
streams.remove(&stream_id);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::ConnectionRefused,
|
||||
format!("Stream open failed: {}", error),
|
||||
));
|
||||
}
|
||||
Ok(Err(_)) => {
|
||||
// Sender dropped
|
||||
let mut streams = self.streams.write().await;
|
||||
streams.remove(&stream_id);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::ConnectionAborted,
|
||||
"Stream open cancelled",
|
||||
));
|
||||
}
|
||||
Err(_) => {
|
||||
// Timeout - remove from pending and streams
|
||||
{
|
||||
let mut pending = self.pending_opens.lock().await;
|
||||
pending.remove(&stream_id);
|
||||
}
|
||||
let mut streams = self.streams.write().await;
|
||||
streams.remove(&stream_id);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"Stream open timeout",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create bounded channel for the stream's write path
|
||||
// This provides backpressure while keeping proper ordering via the unified channel
|
||||
let (stream_write_tx, mut stream_write_rx) =
|
||||
mpsc::channel::<(u32, Bytes)>(STREAM_CHANNEL_BUFFER);
|
||||
|
||||
// Spawn forwarding task: bounded channel -> unified channel
|
||||
// Converts stream writes to OutgoingMessage variants
|
||||
let outgoing_tx = self.outgoing_tx.clone();
|
||||
let is_closed = Arc::clone(&self.is_closed);
|
||||
tokio::spawn(async move {
|
||||
while let Some((sid, data)) = stream_write_rx.recv().await {
|
||||
if is_closed.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
// Empty data signals FIN, non-empty is PSH data
|
||||
let msg = if data.is_empty() {
|
||||
OutgoingMessage::Fin { stream_id: sid }
|
||||
} else {
|
||||
OutgoingMessage::Data {
|
||||
stream_id: sid,
|
||||
data,
|
||||
}
|
||||
};
|
||||
if outgoing_tx.send(msg).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create stream wrapper with session keepalive reference
|
||||
let stream = AnyTlsStream::with_keepalive(
|
||||
stream_id,
|
||||
data_rx,
|
||||
stream_write_tx,
|
||||
Arc::clone(&self.is_closed),
|
||||
Arc::clone(self),
|
||||
);
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
//! AnyTLS padding scheme implementation
|
||||
//!
|
||||
//! Provides configurable packet padding to obfuscate TLS-in-TLS fingerprints.
|
||||
//! Based on the AnyTLS protocol specification.
|
||||
|
||||
use crate::anytls::anytls_types::StringMap;
|
||||
use md5::{Digest, Md5};
|
||||
use rand::Rng;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Check mark constant - indicates "stop if no more data" in padding scheme
|
||||
pub const CHECK_MARK: i32 = -1;
|
||||
|
||||
/// Default padding scheme from the AnyTLS specification
|
||||
pub const DEFAULT_PADDING_SCHEME: &str = r#"stop=8
|
||||
0=30-30
|
||||
1=100-400
|
||||
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
|
||||
3=9-9,500-1000
|
||||
4=500-1000
|
||||
5=500-1000
|
||||
6=500-1000
|
||||
7=500-1000"#;
|
||||
|
||||
/// PaddingFactory generates padding sizes according to the configured scheme
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PaddingFactory {
|
||||
/// Parsed scheme as key-value map
|
||||
scheme: StringMap,
|
||||
/// Raw scheme bytes (for transmission to clients)
|
||||
raw_scheme: Vec<u8>,
|
||||
/// Stop padding after this many packets
|
||||
stop: u32,
|
||||
/// MD5 hash of the scheme (for comparison)
|
||||
md5: String,
|
||||
}
|
||||
|
||||
impl PaddingFactory {
|
||||
/// Create a new PaddingFactory from raw scheme bytes
|
||||
pub fn new(raw_scheme: &[u8]) -> Result<Self, String> {
|
||||
let scheme = StringMap::from_bytes(raw_scheme);
|
||||
|
||||
let stop = scheme
|
||||
.get("stop")
|
||||
.ok_or_else(|| "missing 'stop' in padding scheme".to_string())?
|
||||
.parse::<u32>()
|
||||
.map_err(|_| "invalid 'stop' value in padding scheme".to_string())?;
|
||||
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(raw_scheme);
|
||||
let md5_result: [u8; 16] = hasher.finalize().into();
|
||||
let md5 = md5_result
|
||||
.iter()
|
||||
.map(|b| format!("{:02x}", b))
|
||||
.collect::<String>();
|
||||
|
||||
Ok(Self {
|
||||
scheme,
|
||||
raw_scheme: raw_scheme.to_vec(),
|
||||
stop,
|
||||
md5,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create the default padding factory
|
||||
pub fn default_factory() -> Arc<Self> {
|
||||
Arc::new(
|
||||
Self::new(DEFAULT_PADDING_SCHEME.as_bytes())
|
||||
.expect("default padding scheme should be valid"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the stop value (number of packets to pad)
|
||||
pub fn stop(&self) -> u32 {
|
||||
self.stop
|
||||
}
|
||||
|
||||
/// Get the MD5 hash of the scheme
|
||||
pub fn md5(&self) -> &str {
|
||||
&self.md5
|
||||
}
|
||||
|
||||
/// Get the raw scheme bytes
|
||||
pub fn raw_scheme(&self) -> &[u8] {
|
||||
&self.raw_scheme
|
||||
}
|
||||
|
||||
/// Generate record payload sizes for a given packet number
|
||||
///
|
||||
/// Returns a vector of sizes, where CHECK_MARK (-1) indicates
|
||||
/// "stop processing if no more payload data"
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `pkt` - The packet number (0-indexed)
|
||||
pub fn generate_record_payload_sizes(&self, pkt: u32) -> Vec<i32> {
|
||||
let key = pkt.to_string();
|
||||
let Some(spec) = self.scheme.get(&key) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut sizes = Vec::new();
|
||||
let parts: Vec<&str> = spec.split(',').collect();
|
||||
|
||||
for part in parts {
|
||||
let part = part.trim();
|
||||
|
||||
// Check mark - indicates "stop if no more data"
|
||||
if part == "c" {
|
||||
sizes.push(CHECK_MARK);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse range: "min-max"
|
||||
if let Some((min_str, max_str)) = part.split_once('-') {
|
||||
let min_val: i64 = match min_str.trim().parse() {
|
||||
Ok(v) if v > 0 => v,
|
||||
_ => continue,
|
||||
};
|
||||
let max_val: i64 = match max_str.trim().parse() {
|
||||
Ok(v) if v > 0 => v,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
// Ensure min <= max
|
||||
let (min_val, max_val) = (min_val.min(max_val), min_val.max(max_val));
|
||||
|
||||
if min_val == max_val {
|
||||
// Fixed size
|
||||
sizes.push(min_val as i32);
|
||||
} else {
|
||||
// Random size in range [min, max]
|
||||
let mut rng = rand::rng();
|
||||
let size = rng.random_range(min_val..=max_val);
|
||||
sizes.push(size as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sizes
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PaddingFactory {
|
||||
fn default() -> Self {
|
||||
Self::new(DEFAULT_PADDING_SCHEME.as_bytes())
|
||||
.expect("default padding scheme should be valid")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_factory() {
|
||||
let factory = PaddingFactory::default();
|
||||
assert_eq!(factory.stop(), 8);
|
||||
assert!(!factory.md5().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_fixed_size() {
|
||||
let factory = PaddingFactory::default();
|
||||
|
||||
// Packet 0 should be 30-30 (fixed)
|
||||
let sizes = factory.generate_record_payload_sizes(0);
|
||||
assert_eq!(sizes, vec![30]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_random_range() {
|
||||
let factory = PaddingFactory::default();
|
||||
|
||||
// Packet 1 should be 100-400 (random in range)
|
||||
for _ in 0..100 {
|
||||
let sizes = factory.generate_record_payload_sizes(1);
|
||||
assert_eq!(sizes.len(), 1);
|
||||
assert!(sizes[0] >= 100 && sizes[0] <= 400);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_mark() {
|
||||
let scheme = "stop=3\n2=400-500,c,500-1000";
|
||||
let factory = PaddingFactory::new(scheme.as_bytes()).unwrap();
|
||||
let sizes = factory.generate_record_payload_sizes(2);
|
||||
|
||||
assert_eq!(sizes.len(), 3);
|
||||
assert!(sizes[0] >= 400 && sizes[0] <= 500);
|
||||
assert_eq!(sizes[1], CHECK_MARK);
|
||||
assert!(sizes[2] >= 500 && sizes[2] <= 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_beyond_stop() {
|
||||
let factory = PaddingFactory::default();
|
||||
|
||||
// Packet 10 is beyond stop=8, should return empty
|
||||
let sizes = factory.generate_record_payload_sizes(10);
|
||||
assert!(sizes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_md5_consistency() {
|
||||
let factory1 = PaddingFactory::default();
|
||||
let factory2 = PaddingFactory::new(DEFAULT_PADDING_SCHEME.as_bytes()).unwrap();
|
||||
|
||||
assert_eq!(factory1.md5(), factory2.md5());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_scheme() {
|
||||
let scheme = "stop=2\n0=100-100\n1=200-200";
|
||||
let factory = PaddingFactory::new(scheme.as_bytes()).unwrap();
|
||||
|
||||
assert_eq!(factory.stop(), 2);
|
||||
assert_eq!(factory.generate_record_payload_sizes(0), vec![100]);
|
||||
assert_eq!(factory.generate_record_payload_sizes(1), vec![200]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_scheme_missing_stop() {
|
||||
let scheme = "0=100-100";
|
||||
let result = PaddingFactory::new(scheme.as_bytes());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
//! AnyTLS Server Handler
|
||||
//!
|
||||
//! Implements TcpServerHandler for AnyTLS protocol.
|
||||
//! This handler:
|
||||
//! 1. Authenticates clients via SHA256(password)
|
||||
//! 2. Creates an AnyTlsSession with all routing dependencies
|
||||
//! 3. Runs the session which handles streams internally
|
||||
|
||||
use async_trait::async_trait;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::address::NetLocation;
|
||||
use crate::anytls::anytls_padding::PaddingFactory;
|
||||
use crate::anytls::anytls_server_session::AnyTlsSession;
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::client_proxy_selector::ClientProxySelector;
|
||||
use crate::copy_bidirectional::copy_bidirectional;
|
||||
use crate::resolver::Resolver;
|
||||
use crate::stream_reader::StreamReader;
|
||||
use crate::tcp::tcp_handler::{TcpServerHandler, TcpServerSetupResult};
|
||||
use crate::util::write_all;
|
||||
use aws_lc_rs::digest::{SHA256, digest};
|
||||
|
||||
/// AnyTLS server handler implementing TcpServerHandler
|
||||
///
|
||||
/// This handler receives a post-TLS stream and handles AnyTLS protocol.
|
||||
/// It authenticates the client, creates a session with routing dependencies,
|
||||
/// and runs the session which handles all streams internally.
|
||||
#[derive(Debug)]
|
||||
pub struct AnyTlsServerHandler {
|
||||
/// Authenticated users (password_hash -> user name)
|
||||
users: HashMap<[u8; 32], String>,
|
||||
/// 8-byte prefixes of all user password hashes for quick fallback.
|
||||
/// If incoming data doesn't match any prefix, we can fallback immediately
|
||||
/// without waiting for the full 32-byte hash.
|
||||
hash_prefixes: HashSet<[u8; 8]>,
|
||||
/// Padding factory for traffic obfuscation
|
||||
padding: Arc<PaddingFactory>,
|
||||
/// Resolver for destination addresses
|
||||
resolver: Arc<dyn Resolver>,
|
||||
/// Proxy provider for routing decisions
|
||||
proxy_provider: Arc<ClientProxySelector>,
|
||||
/// UDP enabled for UoT support
|
||||
udp_enabled: bool,
|
||||
/// Fallback destination for failed authentication
|
||||
fallback: Option<NetLocation>,
|
||||
}
|
||||
|
||||
impl AnyTlsServerHandler {
|
||||
/// Create a new AnyTLS server handler.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `users` - Vec of (name, password) tuples for authentication
|
||||
/// * `padding` - Padding factory for traffic obfuscation
|
||||
/// * `resolver` - DNS resolver for destination addresses
|
||||
/// * `proxy_provider` - Proxy selector for routing decisions
|
||||
/// * `udp_enabled` - Whether UDP-over-TCP is enabled
|
||||
/// * `fallback` - Optional fallback destination for failed auth
|
||||
pub fn new(
|
||||
users: Vec<(String, String)>,
|
||||
padding: Arc<PaddingFactory>,
|
||||
resolver: Arc<dyn Resolver>,
|
||||
proxy_provider: Arc<ClientProxySelector>,
|
||||
udp_enabled: bool,
|
||||
fallback: Option<NetLocation>,
|
||||
) -> Self {
|
||||
// Build hash -> name map and collect prefixes
|
||||
let mut user_map = HashMap::with_capacity(users.len());
|
||||
let mut hash_prefixes = HashSet::with_capacity(users.len());
|
||||
|
||||
for (name, password) in users {
|
||||
let hash_result = digest(&SHA256, password.as_bytes());
|
||||
let mut password_hash = [0u8; 32];
|
||||
password_hash.copy_from_slice(hash_result.as_ref());
|
||||
|
||||
// Extract 8-byte prefix for quick fallback lookup
|
||||
let prefix: [u8; 8] = password_hash[..8].try_into().unwrap();
|
||||
hash_prefixes.insert(prefix);
|
||||
|
||||
user_map.insert(password_hash, name);
|
||||
}
|
||||
|
||||
Self {
|
||||
users: user_map,
|
||||
hash_prefixes,
|
||||
padding,
|
||||
resolver,
|
||||
proxy_provider,
|
||||
udp_enabled,
|
||||
fallback,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TcpServerHandler for AnyTlsServerHandler {
|
||||
async fn setup_server_stream(
|
||||
&self,
|
||||
mut server_stream: Box<dyn AsyncStream>,
|
||||
) -> std::io::Result<TcpServerSetupResult> {
|
||||
// Use StreamReader to peek at auth header without consuming
|
||||
let mut reader = StreamReader::new();
|
||||
|
||||
// First, peek at the 8-byte prefix for quick fallback.
|
||||
// This allows us to reject non-AnyTLS traffic (e.g., small HTTP requests)
|
||||
// without hanging waiting for the full 32-byte hash.
|
||||
//
|
||||
// Timing side-channel note: This creates a timing difference between prefix
|
||||
// match and mismatch, but is not exploitable since enumerating 2^64 prefixes
|
||||
// is infeasible, and discovering a valid prefix doesn't help recover the
|
||||
// password or the remaining 24 bytes of the SHA256 hash.
|
||||
let prefix_data = reader.peek_slice(&mut server_stream, 8).await?;
|
||||
|
||||
if !self.hash_prefixes.contains(prefix_data) {
|
||||
log::debug!("AnyTLS quick fallback: 8-byte prefix doesn't match any user");
|
||||
if let Some(ref fallback) = self.fallback {
|
||||
return self.fallback_to_dest(server_stream, reader, fallback).await;
|
||||
}
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"authentication failed (prefix mismatch)",
|
||||
));
|
||||
}
|
||||
|
||||
// Prefix matches - now read the full 32-byte hash
|
||||
let auth_data = reader.peek_slice(&mut server_stream, 32).await?;
|
||||
|
||||
let user_name = match self.users.get(auth_data) {
|
||||
Some(name) => {
|
||||
log::debug!("AnyTLS user authenticated: {}", name);
|
||||
// Auth succeeded - consume the header bytes
|
||||
reader.consume(32);
|
||||
name.clone()
|
||||
}
|
||||
None => {
|
||||
log::debug!("AnyTLS authentication failed: unknown password");
|
||||
// If fallback is configured, forward the connection there
|
||||
if let Some(ref fallback) = self.fallback {
|
||||
return self.fallback_to_dest(server_stream, reader, fallback).await;
|
||||
}
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"authentication failed",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let padding_len = reader.read_u16_be(&mut server_stream).await?;
|
||||
|
||||
// Skip padding bytes (consume them from the reader)
|
||||
if padding_len > 0 {
|
||||
let _ = reader
|
||||
.read_slice(&mut *server_stream, padding_len as usize)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Get any remaining unparsed data that may have been buffered
|
||||
let initial_data = reader.unparsed_data_owned();
|
||||
|
||||
// Create session with all dependencies for internal stream handling
|
||||
let session = AnyTlsSession::new_server_with_initial_data(
|
||||
server_stream,
|
||||
Arc::clone(&self.padding),
|
||||
Arc::clone(&self.resolver),
|
||||
Arc::clone(&self.proxy_provider),
|
||||
self.udp_enabled,
|
||||
user_name,
|
||||
initial_data,
|
||||
);
|
||||
|
||||
// Run the session in a background task
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = session.run().await {
|
||||
log::debug!("AnyTLS session ended: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(TcpServerSetupResult::AlreadyHandled)
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyTlsServerHandler {
|
||||
/// Forward the connection to a fallback destination when authentication fails.
|
||||
///
|
||||
/// This makes the server indistinguishable from a legitimate server by transparently
|
||||
/// proxying failed auth attempts to the configured fallback destination.
|
||||
async fn fallback_to_dest(
|
||||
&self,
|
||||
mut client_stream: Box<dyn AsyncStream>,
|
||||
reader: StreamReader,
|
||||
fallback: &NetLocation,
|
||||
) -> std::io::Result<TcpServerSetupResult> {
|
||||
log::debug!("AnyTLS FALLBACK: Connecting to fallback: {}", fallback);
|
||||
|
||||
// Get the unconsumed data from the reader (includes auth header)
|
||||
let unconsumed_data = reader.unparsed_data();
|
||||
|
||||
// Resolve and connect to the fallback destination
|
||||
let dest_addr = crate::resolver::resolve_single_address(&self.resolver, fallback).await?;
|
||||
|
||||
log::debug!("AnyTLS FALLBACK: Resolved {} to {}", fallback, dest_addr);
|
||||
|
||||
let mut dest_stream: Box<dyn AsyncStream> = Box::new(TcpStream::connect(dest_addr).await?);
|
||||
|
||||
log::debug!(
|
||||
"AnyTLS FALLBACK: Connected to fallback, forwarding {} bytes",
|
||||
unconsumed_data.len()
|
||||
);
|
||||
|
||||
// Forward the unconsumed data (auth header that the client sent)
|
||||
if !unconsumed_data.is_empty() {
|
||||
write_all(&mut dest_stream, unconsumed_data).await?;
|
||||
dest_stream.flush().await?;
|
||||
}
|
||||
|
||||
log::debug!("AnyTLS FALLBACK: Spawning bidirectional copy");
|
||||
|
||||
// Spawn the long-running bidirectional copy as a background task.
|
||||
// This allows the setup to complete within the timeout while the actual
|
||||
// data transfer runs indefinitely.
|
||||
tokio::spawn(async move {
|
||||
let result = copy_bidirectional(
|
||||
&mut *client_stream,
|
||||
&mut *dest_stream,
|
||||
false, // client doesn't need initial flush
|
||||
false, // dest doesn't need initial flush
|
||||
)
|
||||
.await;
|
||||
|
||||
let _ = client_stream.shutdown().await;
|
||||
let _ = dest_stream.shutdown().await;
|
||||
|
||||
if let Err(e) = result {
|
||||
log::debug!("AnyTLS FALLBACK: Connection ended: {}", e);
|
||||
} else {
|
||||
log::debug!("AnyTLS FALLBACK: Connection completed");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(TcpServerSetupResult::AlreadyHandled)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Helper to compute password hash the same way the handler does
|
||||
fn compute_password_hash(password: &str) -> [u8; 32] {
|
||||
let hash_result = digest(&SHA256, password.as_bytes());
|
||||
let mut hash = [0u8; 32];
|
||||
hash.copy_from_slice(hash_result.as_ref());
|
||||
hash
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_hashing() {
|
||||
let hash = compute_password_hash("secret123");
|
||||
|
||||
let expected = digest(&SHA256, b"secret123");
|
||||
let mut expected_bytes = [0u8; 32];
|
||||
expected_bytes.copy_from_slice(expected.as_ref());
|
||||
|
||||
assert_eq!(hash, expected_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_passwords_different_hashes() {
|
||||
let hash1 = compute_password_hash("pass1");
|
||||
let hash2 = compute_password_hash("pass2");
|
||||
|
||||
assert_ne!(hash1, hash2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_map_and_prefix_construction() {
|
||||
// Test that the handler correctly builds user map and prefix set
|
||||
let users = vec![
|
||||
("alice".to_string(), "password1".to_string()),
|
||||
("bob".to_string(), "password2".to_string()),
|
||||
];
|
||||
|
||||
// Compute expected hashes
|
||||
let hash1 = compute_password_hash("password1");
|
||||
let hash2 = compute_password_hash("password2");
|
||||
|
||||
// Build the maps the same way the handler does
|
||||
let mut user_map = HashMap::with_capacity(users.len());
|
||||
let mut hash_prefixes = HashSet::with_capacity(users.len());
|
||||
|
||||
for (name, password) in users {
|
||||
let hash = compute_password_hash(&password);
|
||||
let prefix: [u8; 8] = hash[..8].try_into().unwrap();
|
||||
hash_prefixes.insert(prefix);
|
||||
user_map.insert(hash, name);
|
||||
}
|
||||
|
||||
assert_eq!(user_map.len(), 2);
|
||||
assert_eq!(hash_prefixes.len(), 2);
|
||||
|
||||
// Verify slice lookups work via Borrow<[u8]>
|
||||
let prefix1_slice: &[u8] = &hash1[..8];
|
||||
let prefix2_slice: &[u8] = &hash2[..8];
|
||||
assert!(hash_prefixes.contains(prefix1_slice));
|
||||
assert!(hash_prefixes.contains(prefix2_slice));
|
||||
|
||||
// Verify a random prefix is NOT in the set
|
||||
let random_prefix: &[u8] = &[0x47, 0x45, 0x54, 0x20, 0x2f, 0x20, 0x48, 0x54]; // "GET / HT"
|
||||
assert!(!hash_prefixes.contains(random_prefix));
|
||||
|
||||
// Verify full hash lookup returns correct name
|
||||
let hash1_slice: &[u8] = &hash1[..];
|
||||
assert!(user_map.get(hash1_slice).is_some());
|
||||
assert_eq!(user_map.get(hash1_slice).unwrap(), "alice");
|
||||
|
||||
let hash2_slice: &[u8] = &hash2[..];
|
||||
assert_eq!(user_map.get(hash2_slice).unwrap(), "bob");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,549 @@
|
||||
//! AnyTLS Stream implementation
|
||||
//!
|
||||
//! A Stream represents a single multiplexed connection within an AnyTLS Session.
|
||||
//! It implements AsyncRead and AsyncWrite for transparent integration.
|
||||
//!
|
||||
//! ## Shutdown Behavior
|
||||
//!
|
||||
//! Unlike many async stream implementations, `poll_shutdown` properly blocks until
|
||||
//! the FIN frame is queued for transmission. This matches the Go reference
|
||||
//! implementation which blocks on `streamClosed()` until the FIN is written.
|
||||
//!
|
||||
//! This is important because:
|
||||
//! 1. The peer needs to receive the FIN to know the stream is done
|
||||
//! 2. `copy_bidirectional` expects shutdown to complete before marking direction as done
|
||||
//! 3. Best-effort FIN could be silently dropped if the channel is full
|
||||
|
||||
use crate::async_stream::{AsyncPing, AsyncStream};
|
||||
use bytes::Bytes;
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::PollSender;
|
||||
|
||||
/// Buffer size for bounded channels (number of messages, not bytes)
|
||||
/// Each message is typically a frame's worth of data (up to 64KB)
|
||||
/// 16 messages = up to ~1MB buffered per direction per stream
|
||||
pub const STREAM_CHANNEL_BUFFER: usize = 16;
|
||||
|
||||
/// AnyTlsStream represents a multiplexed stream within an AnyTLS session
|
||||
///
|
||||
/// Reads come from data pushed by the session's recv loop.
|
||||
/// Writes are sent to the session for framing and transmission.
|
||||
///
|
||||
/// Uses bounded channels with backpressure to prevent OOM during
|
||||
/// speed-mismatched transfers.
|
||||
pub struct AnyTlsStream {
|
||||
/// Stream ID (unique within session)
|
||||
id: u32,
|
||||
|
||||
/// Receiver for incoming data from session (bounded for backpressure)
|
||||
/// The session's recv loop pushes PSH frame data here
|
||||
data_rx: mpsc::Receiver<Bytes>,
|
||||
|
||||
/// Buffer for partial reads
|
||||
read_buffer: Vec<u8>,
|
||||
|
||||
/// Poll-based sender for outgoing data to session (bounded with backpressure)
|
||||
/// Data sent here will be framed as PSH and written to the connection.
|
||||
/// PollSender provides poll_reserve() for non-blocking backpressure.
|
||||
data_tx: PollSender<(u32, Bytes)>,
|
||||
|
||||
/// Shared flag indicating session closure
|
||||
session_closed: Arc<AtomicBool>,
|
||||
|
||||
/// Local stream closed flag (set once shutdown completes or stream is dropped)
|
||||
stream_closed: bool,
|
||||
|
||||
/// Flag indicating shutdown is in progress (FIN being sent)
|
||||
/// This tracks the multi-poll cycle of poll_reserve -> send_item
|
||||
shutdown_in_progress: bool,
|
||||
|
||||
/// Flag to track if we've received EOF
|
||||
eof: bool,
|
||||
|
||||
/// Keepalive reference to the session (client-side only)
|
||||
/// This ensures the session stays alive as long as any stream exists
|
||||
_session_keepalive: Option<Arc<dyn std::any::Any + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl AnyTlsStream {
|
||||
/// Create a new AnyTlsStream
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Stream ID
|
||||
/// * `data_rx` - Receiver for incoming data from session (bounded)
|
||||
/// * `data_tx` - Sender for outgoing data to session (bounded, wrapped in PollSender)
|
||||
/// * `session_closed` - Shared flag for session closure
|
||||
pub fn new(
|
||||
id: u32,
|
||||
data_rx: mpsc::Receiver<Bytes>,
|
||||
data_tx: mpsc::Sender<(u32, Bytes)>,
|
||||
session_closed: Arc<AtomicBool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
data_rx,
|
||||
read_buffer: Vec::new(),
|
||||
data_tx: PollSender::new(data_tx),
|
||||
session_closed,
|
||||
stream_closed: false,
|
||||
shutdown_in_progress: false,
|
||||
eof: false,
|
||||
_session_keepalive: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new AnyTlsStream with a session keepalive reference
|
||||
///
|
||||
/// This variant holds an Arc reference to the session, ensuring it stays
|
||||
/// alive as long as the stream exists. Used by client-side streams.
|
||||
pub fn with_keepalive<S: Send + Sync + 'static>(
|
||||
id: u32,
|
||||
data_rx: mpsc::Receiver<Bytes>,
|
||||
data_tx: mpsc::Sender<(u32, Bytes)>,
|
||||
session_closed: Arc<AtomicBool>,
|
||||
session: Arc<S>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
data_rx,
|
||||
read_buffer: Vec::new(),
|
||||
data_tx: PollSender::new(data_tx),
|
||||
session_closed,
|
||||
stream_closed: false,
|
||||
shutdown_in_progress: false,
|
||||
eof: false,
|
||||
_session_keepalive: Some(session),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the stream ID
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Best-effort FIN send for Drop (cannot block in Drop)
|
||||
///
|
||||
/// This is only used when the stream is dropped without being properly shutdown.
|
||||
/// It uses try_send which may fail if the channel is full, but that's acceptable
|
||||
/// for the Drop path since we can't block there.
|
||||
///
|
||||
/// For proper shutdown semantics, use `poll_shutdown` which blocks until FIN is sent.
|
||||
fn send_fin_best_effort(&mut self) {
|
||||
// Don't abort pending data - just try to send FIN if there's room
|
||||
if let Some(sender) = self.data_tx.get_ref() {
|
||||
let _ = sender.try_send((self.id, Bytes::new()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for AnyTlsStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
// Check if stream/session is closed
|
||||
if self.stream_closed {
|
||||
return Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"stream closed",
|
||||
)));
|
||||
}
|
||||
|
||||
// Check EOF with empty buffer
|
||||
if self.eof && self.read_buffer.is_empty() {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// First, drain any buffered data
|
||||
if !self.read_buffer.is_empty() {
|
||||
let n = std::cmp::min(self.read_buffer.len(), buf.remaining());
|
||||
buf.put_slice(&self.read_buffer[..n]);
|
||||
self.read_buffer.drain(..n);
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// Try to receive more data from the session
|
||||
match Pin::new(&mut self.data_rx).poll_recv(cx) {
|
||||
Poll::Ready(Some(data)) => {
|
||||
if data.is_empty() {
|
||||
// Empty data signals EOF
|
||||
self.eof = true;
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
let n = std::cmp::min(data.len(), buf.remaining());
|
||||
buf.put_slice(&data[..n]);
|
||||
|
||||
// Buffer remaining data
|
||||
if n < data.len() {
|
||||
self.read_buffer.extend_from_slice(&data[n..]);
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
// Channel closed - EOF
|
||||
self.eof = true;
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for AnyTlsStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
if self.stream_closed {
|
||||
return Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"stream closed",
|
||||
)));
|
||||
}
|
||||
|
||||
if self.shutdown_in_progress {
|
||||
return Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"stream is shutting down",
|
||||
)));
|
||||
}
|
||||
|
||||
if self.session_closed.load(Ordering::Relaxed) {
|
||||
return Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"session closed",
|
||||
)));
|
||||
}
|
||||
|
||||
// Use poll_reserve for backpressure - this will return Pending if channel is full
|
||||
match self.data_tx.poll_reserve(cx) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
// We have capacity - send the data
|
||||
let data = Bytes::copy_from_slice(buf);
|
||||
let id = self.id;
|
||||
match self.data_tx.send_item((id, data)) {
|
||||
Ok(()) => Poll::Ready(Ok(buf.len())),
|
||||
Err(_) => Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"session channel closed",
|
||||
))),
|
||||
}
|
||||
}
|
||||
Poll::Ready(Err(_)) => {
|
||||
// Channel closed
|
||||
Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"session channel closed",
|
||||
)))
|
||||
}
|
||||
Poll::Pending => {
|
||||
// Channel full - backpressure! Return Pending to slow down the writer
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
// Data is sent through channels, actual flushing happens in process_outgoing
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
// Already fully closed
|
||||
if self.stream_closed {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// Session closed - can't send FIN anyway
|
||||
if self.session_closed.load(Ordering::Relaxed) {
|
||||
self.stream_closed = true;
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// Mark that we're in the shutdown process
|
||||
// This ensures we don't try to send data while shutting down
|
||||
self.shutdown_in_progress = true;
|
||||
|
||||
// Use poll_reserve to wait for channel capacity (respects backpressure)
|
||||
// This ensures any pending data in the channel is sent before FIN
|
||||
match self.data_tx.poll_reserve(cx) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
// We have capacity - send the FIN (empty bytes)
|
||||
let id = self.id;
|
||||
match self.data_tx.send_item((id, Bytes::new())) {
|
||||
Ok(()) => {
|
||||
self.stream_closed = true;
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
Err(_) => {
|
||||
// Channel closed - session is gone, mark as closed
|
||||
self.stream_closed = true;
|
||||
Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"session channel closed during shutdown",
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
Poll::Ready(Err(_)) => {
|
||||
// Channel closed - can't send FIN
|
||||
self.stream_closed = true;
|
||||
Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"session channel closed",
|
||||
)))
|
||||
}
|
||||
Poll::Pending => {
|
||||
// Channel full - wait for capacity (backpressure)
|
||||
// This ensures FIN is queued after any pending data
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AnyTlsStream {
|
||||
fn drop(&mut self) {
|
||||
// If stream wasn't properly shutdown, try best-effort FIN
|
||||
// This handles cases where the stream is dropped without calling shutdown()
|
||||
if !self.stream_closed {
|
||||
self.stream_closed = true;
|
||||
self.send_fin_best_effort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncPing for AnyTlsStream {
|
||||
fn supports_ping(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn poll_write_ping(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<bool>> {
|
||||
// AnyTLS doesn't have a ping mechanism at the stream level
|
||||
Poll::Ready(Ok(false))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncStream for AnyTlsStream {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_write() {
|
||||
let (data_tx, mut data_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let (_incoming_tx, incoming_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let session_closed = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut stream = AnyTlsStream::new(1, incoming_rx, data_tx, session_closed);
|
||||
|
||||
// Write data
|
||||
stream.write_all(b"hello").await.unwrap();
|
||||
|
||||
// Verify data was sent to channel
|
||||
let (stream_id, data) = data_rx.recv().await.unwrap();
|
||||
assert_eq!(stream_id, 1);
|
||||
assert_eq!(data.as_ref(), b"hello");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_read() {
|
||||
let (data_tx, _data_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let (incoming_tx, incoming_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let session_closed = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut stream = AnyTlsStream::new(1, incoming_rx, data_tx, session_closed);
|
||||
|
||||
// Send data to stream
|
||||
incoming_tx.send(Bytes::from("world")).await.unwrap();
|
||||
|
||||
// Read data
|
||||
let mut buf = vec![0u8; 10];
|
||||
let n = stream.read(&mut buf).await.unwrap();
|
||||
|
||||
assert_eq!(n, 5);
|
||||
assert_eq!(&buf[..n], b"world");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_read_buffering() {
|
||||
let (data_tx, _data_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let (incoming_tx, incoming_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let session_closed = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut stream = AnyTlsStream::new(1, incoming_rx, data_tx, session_closed);
|
||||
|
||||
// Send larger data
|
||||
incoming_tx.send(Bytes::from("hello world")).await.unwrap();
|
||||
|
||||
// Read in small chunks
|
||||
let mut buf = vec![0u8; 5];
|
||||
|
||||
let n1 = stream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(n1, 5);
|
||||
assert_eq!(&buf[..n1], b"hello");
|
||||
|
||||
let n2 = stream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(n2, 5);
|
||||
assert_eq!(&buf[..n2], b" worl");
|
||||
|
||||
let n3 = stream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(n3, 1);
|
||||
assert_eq!(&buf[..n3], b"d");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_eof() {
|
||||
let (data_tx, _data_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let (incoming_tx, incoming_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let session_closed = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut stream = AnyTlsStream::new(1, incoming_rx, data_tx, session_closed);
|
||||
|
||||
// Close the channel
|
||||
drop(incoming_tx);
|
||||
|
||||
// Read should return 0 (EOF)
|
||||
let mut buf = vec![0u8; 10];
|
||||
let n = stream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(n, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_shutdown_sends_fin() {
|
||||
let (data_tx, mut data_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let (_incoming_tx, incoming_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let session_closed = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut stream = AnyTlsStream::new(42, incoming_rx, data_tx, session_closed);
|
||||
|
||||
// Shutdown the stream
|
||||
stream.shutdown().await.unwrap();
|
||||
|
||||
// Should receive empty bytes (FIN signal)
|
||||
let (stream_id, data) = data_rx.recv().await.unwrap();
|
||||
assert_eq!(stream_id, 42);
|
||||
assert!(data.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_backpressure() {
|
||||
// Create a channel with very small buffer
|
||||
let (data_tx, mut data_rx) = mpsc::channel(2);
|
||||
let (_incoming_tx, incoming_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let session_closed = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut stream = AnyTlsStream::new(1, incoming_rx, data_tx, session_closed);
|
||||
|
||||
// Fill the channel
|
||||
stream.write_all(b"msg1").await.unwrap();
|
||||
stream.write_all(b"msg2").await.unwrap();
|
||||
|
||||
// Third write should succeed after we drain one
|
||||
let write_future = stream.write_all(b"msg3");
|
||||
|
||||
// Drain one message to make room
|
||||
let _ = data_rx.recv().await.unwrap();
|
||||
|
||||
// Now the write should complete
|
||||
write_future.await.unwrap();
|
||||
|
||||
// Verify remaining messages
|
||||
let (_, data) = data_rx.recv().await.unwrap();
|
||||
assert_eq!(data.as_ref(), b"msg2");
|
||||
let (_, data) = data_rx.recv().await.unwrap();
|
||||
assert_eq!(data.as_ref(), b"msg3");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_shutdown_blocks_when_channel_full() {
|
||||
// Create a channel with very small buffer (2 slots)
|
||||
let (data_tx, mut data_rx) = mpsc::channel(2);
|
||||
let (_incoming_tx, incoming_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let session_closed = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut stream = AnyTlsStream::new(1, incoming_rx, data_tx, session_closed);
|
||||
|
||||
// Fill the channel with data
|
||||
stream.write_all(b"msg1").await.unwrap();
|
||||
stream.write_all(b"msg2").await.unwrap();
|
||||
|
||||
// Channel is now full - shutdown should block
|
||||
let shutdown_future = stream.shutdown();
|
||||
|
||||
// Drain one message to make room for FIN
|
||||
let (_, data1) = data_rx.recv().await.unwrap();
|
||||
assert_eq!(data1.as_ref(), b"msg1");
|
||||
|
||||
// Now shutdown should complete
|
||||
shutdown_future.await.unwrap();
|
||||
|
||||
// Verify data was received before FIN
|
||||
let (_, data2) = data_rx.recv().await.unwrap();
|
||||
assert_eq!(data2.as_ref(), b"msg2");
|
||||
|
||||
// FIN should be last (empty bytes)
|
||||
let (stream_id, fin) = data_rx.recv().await.unwrap();
|
||||
assert_eq!(stream_id, 1);
|
||||
assert!(fin.is_empty(), "FIN should be empty bytes");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_shutdown_preserves_data_order() {
|
||||
// Verify that data written before shutdown is sent before FIN
|
||||
let (data_tx, mut data_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let (_incoming_tx, incoming_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let session_closed = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut stream = AnyTlsStream::new(1, incoming_rx, data_tx, session_closed);
|
||||
|
||||
// Write some data
|
||||
stream.write_all(b"data1").await.unwrap();
|
||||
stream.write_all(b"data2").await.unwrap();
|
||||
stream.write_all(b"data3").await.unwrap();
|
||||
|
||||
// Then shutdown
|
||||
stream.shutdown().await.unwrap();
|
||||
|
||||
// Verify order: data1, data2, data3, FIN
|
||||
let (_, d1) = data_rx.recv().await.unwrap();
|
||||
assert_eq!(d1.as_ref(), b"data1");
|
||||
|
||||
let (_, d2) = data_rx.recv().await.unwrap();
|
||||
assert_eq!(d2.as_ref(), b"data2");
|
||||
|
||||
let (_, d3) = data_rx.recv().await.unwrap();
|
||||
assert_eq!(d3.as_ref(), b"data3");
|
||||
|
||||
let (_, fin) = data_rx.recv().await.unwrap();
|
||||
assert!(fin.is_empty(), "FIN should be last and empty");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_write_after_shutdown_fails() {
|
||||
let (data_tx, _data_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let (_incoming_tx, incoming_rx) = mpsc::channel(STREAM_CHANNEL_BUFFER);
|
||||
let session_closed = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut stream = AnyTlsStream::new(1, incoming_rx, data_tx, session_closed);
|
||||
|
||||
// Shutdown the stream
|
||||
stream.shutdown().await.unwrap();
|
||||
|
||||
// Write after shutdown should fail
|
||||
let result = stream.write_all(b"should fail").await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
//! AnyTLS protocol types - Frame and Command definitions
|
||||
//!
|
||||
//! Based on the AnyTLS protocol specification.
|
||||
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use std::io;
|
||||
|
||||
/// Frame header size: 1 (cmd) + 4 (stream_id) + 2 (data_len) = 7 bytes
|
||||
pub const FRAME_HEADER_SIZE: usize = 7;
|
||||
|
||||
/// Command types for AnyTLS protocol frames
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Command {
|
||||
/// Padding data (waste bytes for traffic obfuscation)
|
||||
Waste = 0,
|
||||
/// Open a new stream (client -> server)
|
||||
Syn = 1,
|
||||
/// Push data through the stream
|
||||
Psh = 2,
|
||||
/// Close the stream (EOF mark)
|
||||
Fin = 3,
|
||||
/// Client settings sent to server
|
||||
Settings = 4,
|
||||
/// Alert message (server -> client, fatal)
|
||||
Alert = 5,
|
||||
/// Update padding scheme (server -> client)
|
||||
UpdatePaddingScheme = 6,
|
||||
/// Server acknowledges stream open (protocol v2)
|
||||
SynAck = 7,
|
||||
/// Keep-alive request
|
||||
HeartRequest = 8,
|
||||
/// Keep-alive response
|
||||
HeartResponse = 9,
|
||||
/// Server settings sent to client (protocol v2)
|
||||
ServerSettings = 10,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for Command {
|
||||
type Error = io::Error;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Command::Waste),
|
||||
1 => Ok(Command::Syn),
|
||||
2 => Ok(Command::Psh),
|
||||
3 => Ok(Command::Fin),
|
||||
4 => Ok(Command::Settings),
|
||||
5 => Ok(Command::Alert),
|
||||
6 => Ok(Command::UpdatePaddingScheme),
|
||||
7 => Ok(Command::SynAck),
|
||||
8 => Ok(Command::HeartRequest),
|
||||
9 => Ok(Command::HeartResponse),
|
||||
10 => Ok(Command::ServerSettings),
|
||||
_ => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("unknown command: {}", value),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Frame defines a packet for multiplexing over a single connection
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Frame {
|
||||
pub cmd: Command,
|
||||
pub stream_id: u32,
|
||||
pub data: Bytes,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// Create a new control frame (no data)
|
||||
pub fn control(cmd: Command, stream_id: u32) -> Self {
|
||||
Self {
|
||||
cmd,
|
||||
stream_id,
|
||||
data: Bytes::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new frame with data
|
||||
pub fn with_data(cmd: Command, stream_id: u32, data: Bytes) -> Self {
|
||||
Self {
|
||||
cmd,
|
||||
stream_id,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a data (PSH) frame
|
||||
pub fn data(stream_id: u32, data: Bytes) -> Self {
|
||||
Self::with_data(Command::Psh, stream_id, data)
|
||||
}
|
||||
|
||||
/// Encode frame into bytes (allocates new BytesMut) - test helper
|
||||
#[cfg(test)]
|
||||
pub fn encode(&self) -> BytesMut {
|
||||
let mut buf = BytesMut::with_capacity(FRAME_HEADER_SIZE + self.data.len());
|
||||
self.encode_into(&mut buf);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Encode frame into an existing buffer (zero allocation)
|
||||
#[inline]
|
||||
pub fn encode_into(&self, buf: &mut BytesMut) {
|
||||
buf.reserve(FRAME_HEADER_SIZE + self.data.len());
|
||||
buf.put_u8(self.cmd as u8);
|
||||
buf.put_u32(self.stream_id);
|
||||
buf.put_u16(self.data.len() as u16);
|
||||
if !self.data.is_empty() {
|
||||
buf.extend_from_slice(&self.data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode frame header
|
||||
pub fn decode_header(header: &[u8; FRAME_HEADER_SIZE]) -> io::Result<(Command, u32, u16)> {
|
||||
let cmd = Command::try_from(header[0])?;
|
||||
let stream_id = u32::from_be_bytes([header[1], header[2], header[3], header[4]]);
|
||||
let length = u16::from_be_bytes([header[5], header[6]]);
|
||||
Ok((cmd, stream_id, length))
|
||||
}
|
||||
}
|
||||
|
||||
/// StringMap - simple key=value format used for settings
|
||||
/// Format: "key1=value1\nkey2=value2\n..."
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct StringMap(std::collections::HashMap<String, String>);
|
||||
|
||||
impl StringMap {
|
||||
pub fn new() -> Self {
|
||||
Self(std::collections::HashMap::new())
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
|
||||
self.0.insert(key.into(), value.into());
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Option<&String> {
|
||||
self.0.get(key)
|
||||
}
|
||||
|
||||
/// Parse from bytes (newline-separated key=value pairs)
|
||||
pub fn from_bytes(data: &[u8]) -> Self {
|
||||
let mut map = Self::new();
|
||||
let text = String::from_utf8_lossy(data);
|
||||
for line in text.lines() {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/// Serialize to bytes
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let lines: Vec<String> = self.0.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
|
||||
lines.join("\n").into_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
/// Frame codec for reading/writing frames from a buffer
|
||||
pub struct FrameCodec;
|
||||
|
||||
impl FrameCodec {
|
||||
/// Try to decode a frame from the buffer
|
||||
/// Returns None if not enough data available
|
||||
pub fn decode(buf: &mut BytesMut) -> io::Result<Option<Frame>> {
|
||||
if buf.len() < FRAME_HEADER_SIZE {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Peek at header to get length
|
||||
let header: [u8; FRAME_HEADER_SIZE] = buf[..FRAME_HEADER_SIZE].try_into().unwrap();
|
||||
let (cmd, stream_id, data_len) = Frame::decode_header(&header)?;
|
||||
let total_len = FRAME_HEADER_SIZE + data_len as usize;
|
||||
|
||||
if buf.len() < total_len {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Consume header
|
||||
buf.advance(FRAME_HEADER_SIZE);
|
||||
|
||||
// Read data - zero-copy: split_to + freeze avoids allocation
|
||||
let data = if data_len > 0 {
|
||||
buf.split_to(data_len as usize).freeze()
|
||||
} else {
|
||||
Bytes::new()
|
||||
};
|
||||
|
||||
Ok(Some(Frame {
|
||||
cmd,
|
||||
stream_id,
|
||||
data,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_command_conversion() {
|
||||
assert_eq!(Command::try_from(0).unwrap(), Command::Waste);
|
||||
assert_eq!(Command::try_from(1).unwrap(), Command::Syn);
|
||||
assert_eq!(Command::try_from(2).unwrap(), Command::Psh);
|
||||
assert!(Command::try_from(255).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frame_encode_decode() {
|
||||
let frame = Frame::data(123, Bytes::from("hello"));
|
||||
let encoded = frame.encode();
|
||||
|
||||
assert_eq!(encoded.len(), FRAME_HEADER_SIZE + 5);
|
||||
|
||||
let mut buf = encoded;
|
||||
let decoded = FrameCodec::decode(&mut buf).unwrap().unwrap();
|
||||
|
||||
assert_eq!(decoded.cmd, Command::Psh);
|
||||
assert_eq!(decoded.stream_id, 123);
|
||||
assert_eq!(decoded.data.as_ref(), b"hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frame_control() {
|
||||
let frame = Frame::control(Command::Syn, 42);
|
||||
assert_eq!(frame.data.len(), 0);
|
||||
assert!(frame.data.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_map() {
|
||||
let mut map = StringMap::new();
|
||||
map.insert("v", "2");
|
||||
map.insert("client", "test");
|
||||
|
||||
let bytes = map.to_bytes();
|
||||
let parsed = StringMap::from_bytes(&bytes);
|
||||
|
||||
assert_eq!(parsed.get("v"), Some(&"2".to_string()));
|
||||
assert_eq!(parsed.get("client"), Some(&"test".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//! AnyTLS protocol implementation for shoes
|
||||
//!
|
||||
//! AnyTLS is a TLS-based proxy protocol with:
|
||||
//! - Session multiplexing: Multiple proxy streams over a single TLS connection
|
||||
//! - Configurable padding: First N packets are padded to obscure fingerprints
|
||||
//! - Password authentication: SHA256-based user authentication
|
||||
|
||||
mod anytls_client_handler;
|
||||
mod anytls_client_session;
|
||||
mod anytls_padding;
|
||||
mod anytls_server_handler;
|
||||
mod anytls_server_session;
|
||||
mod anytls_stream;
|
||||
mod anytls_types;
|
||||
|
||||
pub use anytls_client_handler::AnyTlsClientHandler;
|
||||
pub use anytls_padding::PaddingFactory;
|
||||
pub use anytls_server_handler::AnyTlsServerHandler;
|
||||
+30
-1
@@ -1,3 +1,4 @@
|
||||
use std::future::Future;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
@@ -46,6 +47,34 @@ pub trait AsyncShutdownMessage {
|
||||
) -> Poll<std::io::Result<()>>;
|
||||
}
|
||||
|
||||
/// Extension trait that provides an async `shutdown_message()` method for types
|
||||
/// implementing `AsyncShutdownMessage`. Similar to `AsyncWriteExt::shutdown()`.
|
||||
pub trait AsyncShutdownMessageExt: AsyncShutdownMessage {
|
||||
/// Shuts down the message stream, signaling that no more messages will be sent.
|
||||
fn shutdown_message(&mut self) -> ShutdownMessageFuture<'_, Self>
|
||||
where
|
||||
Self: Unpin,
|
||||
{
|
||||
ShutdownMessageFuture { stream: self }
|
||||
}
|
||||
}
|
||||
|
||||
/// Future returned by `AsyncShutdownMessageExt::shutdown_message()`.
|
||||
pub struct ShutdownMessageFuture<'a, T: ?Sized> {
|
||||
stream: &'a mut T,
|
||||
}
|
||||
|
||||
impl<T: AsyncShutdownMessage + Unpin + ?Sized> Future for ShutdownMessageFuture<'_, T> {
|
||||
type Output = std::io::Result<()>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut *self.stream).poll_shutdown_message(cx)
|
||||
}
|
||||
}
|
||||
|
||||
// Blanket implementation for all types that implement AsyncShutdownMessage
|
||||
impl<T: AsyncShutdownMessage + ?Sized> AsyncShutdownMessageExt for T {}
|
||||
|
||||
pub trait AsyncReadTargetedMessage {
|
||||
fn poll_read_targeted_message(
|
||||
self: Pin<&mut Self>,
|
||||
@@ -141,7 +170,7 @@ impl AsyncShutdownMessage for UdpSocket {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AsyncStream: AsyncRead + AsyncWrite + AsyncPing + Unpin + Send {}
|
||||
pub trait AsyncStream: AsyncRead + AsyncWrite + AsyncPing + Unpin + Send + Sync {}
|
||||
|
||||
pub trait AsyncMessageStream:
|
||||
AsyncReadMessage
|
||||
|
||||
@@ -13,6 +13,10 @@ impl<'a> BufReader<'a> {
|
||||
self.pos == self.buf.len()
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> usize {
|
||||
self.buf.len() - self.pos
|
||||
}
|
||||
|
||||
pub fn position(&self) -> usize {
|
||||
self.pos
|
||||
}
|
||||
|
||||
+39
-44
@@ -22,13 +22,14 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use log::{debug, info};
|
||||
use log::debug;
|
||||
|
||||
use crate::address::NetLocation;
|
||||
use crate::async_stream::AsyncMessageStream;
|
||||
use crate::resolver::Resolver;
|
||||
use crate::tcp::proxy_connector::ProxyConnector;
|
||||
use crate::tcp::socket_connector::SocketConnector;
|
||||
use crate::tcp_handler::{TcpClientSetupResult, TcpClientUdpSetupResult, UdpStreamRequest};
|
||||
use crate::tcp::tcp_handler::TcpClientSetupResult;
|
||||
|
||||
/// Entry in the initial hop (hop 0) pool.
|
||||
///
|
||||
@@ -224,7 +225,7 @@ impl ClientProxyChain {
|
||||
// Select proxy connectors for subsequent hops
|
||||
let subsequent_proxies = self.select_subsequent_proxies();
|
||||
|
||||
info!(
|
||||
debug!(
|
||||
"Chain TCP connect: 1 initial + {} subsequent hop(s) -> {}",
|
||||
subsequent_proxies.len(),
|
||||
remote_location
|
||||
@@ -303,12 +304,14 @@ impl ClientProxyChain {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Connect for UDP traffic through the chain.
|
||||
pub async fn connect_udp(
|
||||
/// Connect for bidirectional UDP traffic through the chain.
|
||||
///
|
||||
/// Returns an AsyncMessageStream that sends/receives UDP packets to the target.
|
||||
pub async fn connect_udp_bidirectional(
|
||||
&self,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
request: UdpStreamRequest,
|
||||
) -> std::io::Result<TcpClientUdpSetupResult> {
|
||||
target: NetLocation,
|
||||
) -> std::io::Result<Box<dyn AsyncMessageStream>> {
|
||||
// Check if UDP is supported
|
||||
if self.udp_final_hop_indices.is_empty() {
|
||||
return Err(std::io::Error::new(
|
||||
@@ -326,15 +329,15 @@ impl ClientProxyChain {
|
||||
let pool_idx = self.udp_final_hop_indices[idx % self.udp_final_hop_indices.len()];
|
||||
let entry = &self.initial_hop[pool_idx];
|
||||
|
||||
info!(
|
||||
"Chain UDP connect: 1 hop (initial IS final), request={}",
|
||||
request
|
||||
debug!(
|
||||
"Chain UDP connect: 1 hop (initial IS final), target={}",
|
||||
target
|
||||
);
|
||||
|
||||
match entry {
|
||||
InitialHopEntry::Direct(socket) => {
|
||||
debug!("Chain UDP: Direct connection (native UDP)");
|
||||
socket.connect_udp(resolver, request).await
|
||||
socket.connect_udp_bidirectional(resolver, target).await
|
||||
}
|
||||
InitialHopEntry::Proxy { socket, proxy } => {
|
||||
debug!(
|
||||
@@ -342,7 +345,7 @@ impl ClientProxyChain {
|
||||
proxy.proxy_location()
|
||||
);
|
||||
let stream = socket.connect(resolver, proxy.proxy_location()).await?;
|
||||
proxy.setup_udp_stream(stream, request).await
|
||||
proxy.setup_udp_bidirectional(stream, target).await
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -377,10 +380,10 @@ impl ClientProxyChain {
|
||||
let pool_idx = self.udp_final_hop_indices[idx % self.udp_final_hop_indices.len()];
|
||||
let final_proxy = final_hop_pool[pool_idx].as_ref();
|
||||
|
||||
info!(
|
||||
"Chain UDP connect: 1 initial + {} intermediate + 1 final (UDP) hop(s), request={}",
|
||||
debug!(
|
||||
"Chain UDP connect: 1 initial + {} intermediate + 1 final (UDP) hop(s), target={}",
|
||||
intermediate_proxies.len(),
|
||||
request
|
||||
target
|
||||
);
|
||||
|
||||
// Build the chain: initial -> intermediates -> final (UDP)
|
||||
@@ -398,7 +401,7 @@ impl ClientProxyChain {
|
||||
|
||||
// Process intermediate hops (all TCP)
|
||||
for (i, proxy) in intermediate_proxies.iter().enumerate() {
|
||||
let target = intermediate_proxies
|
||||
let next_target = intermediate_proxies
|
||||
.get(i + 1)
|
||||
.map(|p| p.proxy_location())
|
||||
.unwrap_or(final_proxy.proxy_location());
|
||||
@@ -407,9 +410,9 @@ impl ClientProxyChain {
|
||||
i + 1,
|
||||
intermediate_proxies.len(),
|
||||
proxy.proxy_location(),
|
||||
target
|
||||
next_target
|
||||
);
|
||||
let result = proxy.setup_tcp_stream(stream, target).await?;
|
||||
let result = proxy.setup_tcp_stream(stream, next_target).await?;
|
||||
stream = result.client_stream;
|
||||
}
|
||||
|
||||
@@ -418,7 +421,7 @@ impl ClientProxyChain {
|
||||
"Chain UDP final hop: {} (UDP)",
|
||||
final_proxy.proxy_location()
|
||||
);
|
||||
final_proxy.setup_udp_stream(stream, request).await
|
||||
final_proxy.setup_udp_bidirectional(stream, target).await
|
||||
}
|
||||
InitialHopEntry::Proxy { socket, proxy } => {
|
||||
// Determine first target after initial hop
|
||||
@@ -439,7 +442,7 @@ impl ClientProxyChain {
|
||||
|
||||
// Process intermediate hops (all TCP)
|
||||
for (i, proxy) in intermediate_proxies.iter().enumerate() {
|
||||
let target = intermediate_proxies
|
||||
let next_target = intermediate_proxies
|
||||
.get(i + 1)
|
||||
.map(|p| p.proxy_location())
|
||||
.unwrap_or(final_proxy.proxy_location());
|
||||
@@ -448,9 +451,9 @@ impl ClientProxyChain {
|
||||
i + 1,
|
||||
intermediate_proxies.len(),
|
||||
proxy.proxy_location(),
|
||||
target
|
||||
next_target
|
||||
);
|
||||
let result = proxy.setup_tcp_stream(stream, target).await?;
|
||||
let result = proxy.setup_tcp_stream(stream, next_target).await?;
|
||||
stream = result.client_stream;
|
||||
}
|
||||
|
||||
@@ -459,7 +462,7 @@ impl ClientProxyChain {
|
||||
"Chain UDP final hop: {} (UDP)",
|
||||
final_proxy.proxy_location()
|
||||
);
|
||||
final_proxy.setup_udp_stream(stream, request).await
|
||||
final_proxy.setup_udp_bidirectional(stream, target).await
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,11 +518,11 @@ impl ClientChainGroup {
|
||||
chain.connect_tcp(remote_location, resolver).await
|
||||
}
|
||||
|
||||
pub async fn connect_udp(
|
||||
pub async fn connect_udp_bidirectional(
|
||||
&self,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
request: UdpStreamRequest,
|
||||
) -> std::io::Result<TcpClientUdpSetupResult> {
|
||||
target: NetLocation,
|
||||
) -> std::io::Result<Box<dyn AsyncMessageStream>> {
|
||||
if self.udp_chain_indices.is_empty() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
@@ -530,7 +533,7 @@ impl ClientChainGroup {
|
||||
let idx = self.next_udp_index.fetch_add(1, Ordering::Relaxed) as usize;
|
||||
let chain_idx = self.udp_chain_indices[idx % self.udp_chain_indices.len()];
|
||||
let chain = &self.chains[chain_idx];
|
||||
chain.connect_udp(resolver, request).await
|
||||
chain.connect_udp_bidirectional(resolver, target).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -568,14 +571,14 @@ mod tests {
|
||||
))
|
||||
}
|
||||
|
||||
async fn connect_udp(
|
||||
async fn connect_udp_bidirectional(
|
||||
&self,
|
||||
_resolver: &Arc<dyn Resolver>,
|
||||
_request: UdpStreamRequest,
|
||||
) -> std::io::Result<TcpClientUdpSetupResult> {
|
||||
_target: NetLocation,
|
||||
) -> std::io::Result<Box<dyn AsyncMessageStream>> {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"MockSocketConnector::connect_udp not implemented",
|
||||
"MockSocketConnector::connect_udp_bidirectional not implemented",
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -617,14 +620,14 @@ mod tests {
|
||||
))
|
||||
}
|
||||
|
||||
async fn setup_udp_stream(
|
||||
async fn setup_udp_bidirectional(
|
||||
&self,
|
||||
_stream: Box<dyn AsyncStream>,
|
||||
_request: UdpStreamRequest,
|
||||
) -> std::io::Result<TcpClientUdpSetupResult> {
|
||||
_target: NetLocation,
|
||||
) -> std::io::Result<Box<dyn AsyncMessageStream>> {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"MockProxyConnector::setup_udp_stream not implemented",
|
||||
"MockProxyConnector::setup_udp_bidirectional not implemented",
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -648,8 +651,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// --- InitialHopEntry Tests ---
|
||||
|
||||
#[test]
|
||||
fn test_initial_hop_entry_direct_supports_udp() {
|
||||
let entry = direct_entry(0);
|
||||
@@ -668,8 +669,6 @@ mod tests {
|
||||
assert!(!entry.supports_udp());
|
||||
}
|
||||
|
||||
// --- ClientProxyChain Tests ---
|
||||
|
||||
#[test]
|
||||
fn test_chain_single_direct() {
|
||||
let chain = ClientProxyChain::new(vec![direct_entry(0)], vec![]);
|
||||
@@ -780,8 +779,6 @@ mod tests {
|
||||
ClientProxyChain::new(vec![], vec![]);
|
||||
}
|
||||
|
||||
// --- ClientChainGroup Tests ---
|
||||
|
||||
#[test]
|
||||
fn test_group_single_chain() {
|
||||
let chain = ClientProxyChain::new(vec![direct_entry(0)], vec![]);
|
||||
@@ -926,8 +923,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// --- UDP Compatibility Fix Tests (Part 6) ---
|
||||
|
||||
#[test]
|
||||
fn test_udp_selection_with_subsequent_hops() {
|
||||
// Test that when udp_uses_initial_hop = false, we select:
|
||||
@@ -949,7 +944,7 @@ mod tests {
|
||||
assert_eq!(chain.udp_final_hop_indices, vec![1, 2]);
|
||||
|
||||
// Verify that initial hop selection would use all entries (indices 0 and 1)
|
||||
// We can't easily test this without calling connect_udp(), but we can verify
|
||||
// We can't easily test this without calling connect_udp_bidirectional(), but we can verify
|
||||
// that the normal round-robin will cycle through both
|
||||
for i in 0..4 {
|
||||
let entry = chain.select_initial_hop_entry();
|
||||
|
||||
+44
-149
@@ -2,7 +2,7 @@ use log::{debug, error};
|
||||
use lru::LruCache;
|
||||
use parking_lot::RwLock;
|
||||
use std::hash::Hash;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -179,7 +179,6 @@ const CACHE_RULE_THRESHOLD: usize = 16;
|
||||
#[derive(Debug)]
|
||||
pub struct ClientProxySelector {
|
||||
rules: Vec<ConnectRule>,
|
||||
default_rule_index: Option<usize>,
|
||||
/// If false, hostname rules will not trigger DNS resolution to match against IP-based
|
||||
/// destinations. This is useful when a huge blocklist or rule list is provided.
|
||||
/// However, this means that the user needs to make sure DNS resolutions are not done
|
||||
@@ -243,28 +242,6 @@ impl ClientProxySelector {
|
||||
resolve_rule_hostnames: bool,
|
||||
cache_capacity: usize,
|
||||
) -> Self {
|
||||
let mut default_rule_index: Option<usize> = None;
|
||||
// find a default rule which we'll use for multidirectional forwarding..
|
||||
// TODO: ideally, we'd check the rule for each target during multidirectional forwarding
|
||||
// and use the right one instead.
|
||||
for (i, rule) in rules.iter().enumerate() {
|
||||
// if it allows forwarding only to a single address, we don't want to use that as
|
||||
// the default decision.
|
||||
if let ConnectAction::Allow {
|
||||
ref override_address,
|
||||
..
|
||||
} = rule.action
|
||||
&& override_address.is_some()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let is_cover_rule = rule.masks.iter().any(|mask| mask.address_mask.netmask == 0);
|
||||
if is_cover_rule {
|
||||
default_rule_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Enable caching if:
|
||||
// 1. DNS resolution is enabled (expensive operation), OR
|
||||
// 2. Many rules (linear scan becomes expensive)
|
||||
@@ -276,24 +253,11 @@ impl ClientProxySelector {
|
||||
|
||||
Self {
|
||||
rules,
|
||||
default_rule_index,
|
||||
resolve_rule_hostnames,
|
||||
cache,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_decision(&self) -> ConnectDecision<'_> {
|
||||
match self.default_rule_index {
|
||||
Some(i) => {
|
||||
let rule = &self.rules[i];
|
||||
// the remote location is unused because we don't choose a default rule with
|
||||
// an override_address, so just pass a port of 0.
|
||||
rule.action.to_decision(NetLocation::UNSPECIFIED)
|
||||
}
|
||||
None => ConnectDecision::Block,
|
||||
}
|
||||
}
|
||||
|
||||
/// Judge a connection request, using the cache for faster repeated lookups.
|
||||
///
|
||||
/// This is the primary method for routing decisions. It first checks the cache,
|
||||
@@ -301,15 +265,19 @@ impl ClientProxySelector {
|
||||
///
|
||||
/// Note: Caching is only enabled when `resolve_rule_hostnames` is true or there are
|
||||
/// more than 16 rules. For simple configurations, direct rule matching is faster.
|
||||
pub async fn judge<'a>(
|
||||
#[inline]
|
||||
pub async fn judge_with_resolved_address<'a>(
|
||||
&'a self,
|
||||
location: NetLocation,
|
||||
resolved_address: Option<SocketAddr>,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
) -> std::io::Result<ConnectDecision<'a>> {
|
||||
let resolved_ip = resolved_address.map(|addr| ip_to_u128(addr.ip()));
|
||||
|
||||
// If caching is disabled, go directly to rule matching
|
||||
let cache = match &self.cache {
|
||||
Some(c) => c,
|
||||
None => return self.judge_uncached(location, resolver).await,
|
||||
None => return self.judge_uncached(location, resolved_ip, resolver).await,
|
||||
};
|
||||
|
||||
// Fast path: check cache first
|
||||
@@ -321,6 +289,7 @@ impl ClientProxySelector {
|
||||
match match_rule(
|
||||
&self.rules,
|
||||
&location,
|
||||
resolved_ip,
|
||||
resolver,
|
||||
self.resolve_rule_hostnames,
|
||||
)
|
||||
@@ -339,15 +308,28 @@ impl ClientProxySelector {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn judge<'a>(
|
||||
&'a self,
|
||||
location: NetLocation,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
) -> std::io::Result<ConnectDecision<'a>> {
|
||||
self.judge_with_resolved_address(location, None, resolver)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Judge without using the cache. Useful for testing or when cache bypass is needed.
|
||||
#[inline]
|
||||
pub async fn judge_uncached<'a>(
|
||||
&'a self,
|
||||
location: NetLocation,
|
||||
resolved_ip: Option<u128>,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
) -> std::io::Result<ConnectDecision<'a>> {
|
||||
match match_rule(
|
||||
&self.rules,
|
||||
&location,
|
||||
resolved_ip,
|
||||
resolver,
|
||||
self.resolve_rule_hostnames,
|
||||
)
|
||||
@@ -430,12 +412,10 @@ fn matches_domain(base_domain: &str, hostname: &str) -> bool {
|
||||
async fn match_rule<'a>(
|
||||
rules: &'a [ConnectRule],
|
||||
location: &NetLocation,
|
||||
mut resolved_ip: Option<u128>,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
resolve_rule_hostnames: bool,
|
||||
) -> std::io::Result<Option<(usize, &'a ConnectRule)>> {
|
||||
// We only resolve when necessary.
|
||||
let mut resolved_ip: Option<u128> = None;
|
||||
|
||||
for (rule_index, rule) in rules.iter().enumerate() {
|
||||
for mask in rule.masks.iter() {
|
||||
match match_mask(
|
||||
@@ -568,10 +548,6 @@ mod tests {
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::pin::Pin;
|
||||
|
||||
// =========================================================================
|
||||
// Test Infrastructure
|
||||
// =========================================================================
|
||||
|
||||
/// A mock resolver for testing that returns predefined results
|
||||
#[derive(Debug)]
|
||||
struct MockResolver {
|
||||
@@ -671,10 +647,6 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Domain Matching Tests (matches_domain function)
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_matches_domain_exact_match() {
|
||||
assert!(matches_domain_for_test("example.com", "example.com"));
|
||||
@@ -730,10 +702,6 @@ mod tests {
|
||||
assert!(!matches_domain_for_test("host", "localhost")); // partial suffix
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// IPv4 Address Matching Tests
|
||||
// =========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ipv4_exact_match() {
|
||||
let rules = vec![
|
||||
@@ -867,10 +835,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// IPv6 Address Matching Tests
|
||||
// =========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ipv6_exact_match() {
|
||||
let rules = vec![
|
||||
@@ -923,10 +887,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Hostname/Domain Matching Tests
|
||||
// =========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hostname_exact_match() {
|
||||
let rules = vec![
|
||||
@@ -1013,10 +973,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Port Matching Tests
|
||||
// =========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_port_specific_rule() {
|
||||
let rules = vec![
|
||||
@@ -1087,10 +1043,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Rule Priority Tests (First Match Wins)
|
||||
// =========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_first_rule_wins() {
|
||||
let rules = vec![
|
||||
@@ -1147,10 +1099,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Block Action Tests
|
||||
// =========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_block_rule() {
|
||||
let rules = vec![
|
||||
@@ -1203,10 +1151,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Default Rule Tests
|
||||
// =========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_no_default_rule_blocks() {
|
||||
let rules = vec![allow_rule(vec!["192.168.1.0/24"], "lan")];
|
||||
@@ -1222,56 +1166,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_default_decision() {
|
||||
let rules = vec![
|
||||
allow_rule(vec!["192.168.1.0/24"], "lan"),
|
||||
allow_rule(vec!["0.0.0.0/0"], "default"),
|
||||
];
|
||||
let selector = ClientProxySelector::new(rules);
|
||||
|
||||
// default_decision should return the catch-all rule
|
||||
let decision = selector.default_decision();
|
||||
match decision {
|
||||
ConnectDecision::Allow { .. } => {}
|
||||
ConnectDecision::Block => panic!("Expected Allow"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_default_decision_no_catchall() {
|
||||
// Rules without a catch-all should return Block for default_decision
|
||||
let rules = vec![allow_rule(vec!["192.168.1.0/24"], "lan")];
|
||||
let selector = ClientProxySelector::new(rules);
|
||||
|
||||
let decision = selector.default_decision();
|
||||
match decision {
|
||||
ConnectDecision::Allow { .. } => panic!("Expected Block when no catch-all rule"),
|
||||
ConnectDecision::Block => {} // Expected
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_default_decision_skips_override_rules() {
|
||||
// Rules with override_address should not be used as default
|
||||
let rules = vec![
|
||||
allow_rule_with_override(vec!["0.0.0.0/0"], "redirect", "10.0.0.1:8080"),
|
||||
allow_rule(vec!["192.168.0.0/16"], "fallback"),
|
||||
];
|
||||
let selector = ClientProxySelector::new(rules);
|
||||
|
||||
// default_decision should skip the override rule and use the next catch-all
|
||||
let decision = selector.default_decision();
|
||||
match decision {
|
||||
ConnectDecision::Allow { .. } => {}
|
||||
ConnectDecision::Block => {} // Also acceptable if no valid default found
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Round-Robin Proxy Selection Tests
|
||||
// =========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_round_robin_proxy_selection() {
|
||||
// Note: Since ClientProxyChain now handles round-robin internally,
|
||||
@@ -1312,10 +1206,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Address Override Tests
|
||||
// =========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_address_override() {
|
||||
let rules = vec![allow_rule_with_override(
|
||||
@@ -1363,10 +1253,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Multiple Masks Per Rule Tests
|
||||
// =========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_masks_in_rule() {
|
||||
let rules = vec![
|
||||
@@ -1433,10 +1319,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Edge Cases and Security Tests
|
||||
// =========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_rules_blocks_all() {
|
||||
let rules: Vec<ConnectRule> = vec![];
|
||||
@@ -1547,10 +1429,6 @@ mod tests {
|
||||
// If we got here without panic, concurrent access is safe
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Regression Tests for Specific Scenarios
|
||||
// =========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_localhost_bypass() {
|
||||
// Common pattern: bypass proxy for localhost
|
||||
@@ -1618,10 +1496,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Routing Cache Tests
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_routing_cache_key_from_location_hostname() {
|
||||
let location = NetLocation::new(Address::Hostname("Example.COM".to_string()), 443);
|
||||
@@ -1936,7 +1810,7 @@ mod tests {
|
||||
|
||||
// Use judge_uncached - should NOT populate cache
|
||||
let decision = selector
|
||||
.judge_uncached(location.clone(), &resolver)
|
||||
.judge_uncached(location.clone(), None, &resolver)
|
||||
.await
|
||||
.unwrap();
|
||||
match decision {
|
||||
@@ -2128,4 +2002,25 @@ mod tests {
|
||||
_ => panic!("Expected subnet"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_netmask_value_for_cidr_0() {
|
||||
// Verify that 0.0.0.0/0 actually produces netmask == 0
|
||||
let mask = NetLocationMask::from("0.0.0.0/0").unwrap();
|
||||
assert_eq!(
|
||||
mask.address_mask.netmask, 0,
|
||||
"0.0.0.0/0 should have netmask == 0, got {}",
|
||||
mask.address_mask.netmask
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_netmask_value_for_cidr_24() {
|
||||
// Verify that /24 does NOT produce netmask == 0
|
||||
let mask = NetLocationMask::from("172.17.0.0/24").unwrap();
|
||||
assert_ne!(
|
||||
mask.address_mask.netmask, 0,
|
||||
"172.17.0.0/24 should have netmask != 0"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-3
@@ -4,11 +4,14 @@
|
||||
//! - [`types`]: All configuration types (server, client, rules, etc.)
|
||||
//! - [`pem`]: PEM file handling and certificate loading
|
||||
//! - [`validate`]: Configuration validation and server config creation
|
||||
//! - [`singbox`]: Sing-box JSON configuration conversion
|
||||
//! - [`convert_util`]: Utilities for preprocessing JSON-like configs
|
||||
//!
|
||||
//! The main entry points are:
|
||||
//! - [`load_configs`]: Load config files from disk
|
||||
//! - [`convert_cert_paths`]: Convert PEM file paths to inline data
|
||||
//! - [`create_server_configs`]: Validate and create final server configs
|
||||
//! - [`singbox::convert_singbox_config`]: Convert sing-box configs to shoes format
|
||||
|
||||
mod pem;
|
||||
mod types;
|
||||
@@ -18,9 +21,6 @@ pub use pem::convert_cert_paths;
|
||||
pub use types::*;
|
||||
pub use validate::create_server_configs;
|
||||
|
||||
// Re-export ConfigSelection for use in pem.rs
|
||||
pub(crate) use types::ConfigSelection;
|
||||
|
||||
/// Loads configuration files from the provided paths.
|
||||
///
|
||||
/// Reads each file, parses it as YAML, and returns the combined list of configs.
|
||||
@@ -61,3 +61,14 @@ pub async fn load_configs(args: &Vec<String>) -> std::io::Result<Vec<Config>> {
|
||||
|
||||
Ok(all_configs)
|
||||
}
|
||||
|
||||
/// Load config from a string (used by FFI targets)
|
||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "ffi"))]
|
||||
pub fn load_config_str(config_str: &str) -> std::io::Result<Vec<Config>> {
|
||||
serde_yaml::from_str::<Vec<Config>>(&config_str).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Could not parse config string as config YAML: {e}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
+179
-29
@@ -7,11 +7,103 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::address::NetLocation;
|
||||
use crate::option_util::{NoneOrOne, NoneOrSome};
|
||||
|
||||
use super::common::{default_reality_client_short_id, default_true, unspecified_address};
|
||||
use super::common::{
|
||||
default_reality_client_short_id, default_true, is_false, is_true, unspecified_address,
|
||||
};
|
||||
use super::server::WebsocketPingType;
|
||||
use super::shadowsocks::ShadowsocksConfig;
|
||||
use super::transport::{ClientQuicConfig, TcpConfig, Transport};
|
||||
|
||||
/// Custom deserializer for ClientProxyConfig::Shadowsocks
|
||||
fn deserialize_shadowsocks_client<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<(ShadowsocksConfig, bool), D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ShadowsocksClientTemp {
|
||||
cipher: String,
|
||||
password: String,
|
||||
#[serde(default = "default_true")]
|
||||
udp_enabled: bool,
|
||||
}
|
||||
|
||||
let temp = ShadowsocksClientTemp::deserialize(deserializer)?;
|
||||
let config =
|
||||
ShadowsocksConfig::from_fields(&temp.cipher, &temp.password).map_err(Error::custom)?;
|
||||
|
||||
Ok((config, temp.udp_enabled))
|
||||
}
|
||||
|
||||
/// Custom serializer for ClientProxyConfig::Shadowsocks - flattens config fields
|
||||
fn serialize_shadowsocks_client<S>(
|
||||
config: &ShadowsocksConfig,
|
||||
udp_enabled: &bool,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
|
||||
// Only serialize udp_enabled if it's not the default (true)
|
||||
let field_count = if *udp_enabled { 2 } else { 3 };
|
||||
let mut state = serializer.serialize_struct("Shadowsocks", field_count)?;
|
||||
config.serialize_fields(&mut state)?;
|
||||
if !*udp_enabled {
|
||||
state.serialize_field("udp_enabled", udp_enabled)?;
|
||||
}
|
||||
state.end()
|
||||
}
|
||||
|
||||
/// Custom deserializer for ClientProxyConfig::Snell - flattens config fields
|
||||
fn deserialize_snell_client<'de, D>(deserializer: D) -> Result<(ShadowsocksConfig, bool), D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct SnellClientTemp {
|
||||
cipher: String,
|
||||
password: String,
|
||||
#[serde(default = "default_true")]
|
||||
udp_enabled: bool,
|
||||
}
|
||||
|
||||
let temp = SnellClientTemp::deserialize(deserializer)?;
|
||||
let config =
|
||||
ShadowsocksConfig::from_fields(&temp.cipher, &temp.password).map_err(Error::custom)?;
|
||||
|
||||
Ok((config, temp.udp_enabled))
|
||||
}
|
||||
|
||||
/// Custom serializer for ClientProxyConfig::Snell - flattens config fields
|
||||
fn serialize_snell_client<S>(
|
||||
config: &ShadowsocksConfig,
|
||||
udp_enabled: &bool,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
|
||||
// Only serialize udp_enabled if it's not the default (true)
|
||||
let field_count = if *udp_enabled { 2 } else { 3 };
|
||||
let mut state = serializer.serialize_struct("Snell", field_count)?;
|
||||
config.serialize_fields(&mut state)?;
|
||||
if !*udp_enabled {
|
||||
state.serialize_field("udp_enabled", udp_enabled)?;
|
||||
}
|
||||
state.end()
|
||||
}
|
||||
|
||||
/// Custom deserializer for ClientProxyConfig::Vmess that validates legacy aead field
|
||||
fn deserialize_vmess_client<'de, D>(deserializer: D) -> Result<(String, String, bool), D::Error>
|
||||
where
|
||||
@@ -143,16 +235,19 @@ where
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ClientConfig {
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "NoneOrOne::is_unspecified")]
|
||||
pub bind_interface: NoneOrOne<String>,
|
||||
#[serde(default = "unspecified_address")]
|
||||
#[serde(
|
||||
default = "unspecified_address",
|
||||
skip_serializing_if = "NetLocation::is_unspecified"
|
||||
)]
|
||||
pub address: NetLocation,
|
||||
pub protocol: ClientProxyConfig,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Transport::is_default")]
|
||||
pub transport: Transport,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tcp_settings: Option<TcpConfig>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub quic_settings: Option<ClientQuicConfig>,
|
||||
}
|
||||
|
||||
@@ -174,43 +269,65 @@ impl Default for ClientConfig {
|
||||
pub enum ClientProxyConfig {
|
||||
Direct,
|
||||
Http {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
username: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
password: Option<String>,
|
||||
},
|
||||
#[serde(alias = "socks5")]
|
||||
Socks {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
username: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
password: Option<String>,
|
||||
},
|
||||
#[serde(alias = "ss")]
|
||||
Shadowsocks(ShadowsocksConfig),
|
||||
Snell(ShadowsocksConfig),
|
||||
#[serde(
|
||||
alias = "ss",
|
||||
deserialize_with = "deserialize_shadowsocks_client",
|
||||
serialize_with = "serialize_shadowsocks_client"
|
||||
)]
|
||||
Shadowsocks {
|
||||
config: ShadowsocksConfig,
|
||||
udp_enabled: bool,
|
||||
},
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_snell_client",
|
||||
serialize_with = "serialize_snell_client"
|
||||
)]
|
||||
Snell {
|
||||
config: ShadowsocksConfig,
|
||||
udp_enabled: bool,
|
||||
},
|
||||
Vless {
|
||||
user_id: String,
|
||||
#[serde(default = "default_true")]
|
||||
#[serde(default = "default_true", skip_serializing_if = "is_true")]
|
||||
udp_enabled: bool,
|
||||
},
|
||||
Trojan {
|
||||
password: String,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
shadowsocks: Option<ShadowsocksConfig>,
|
||||
},
|
||||
Reality {
|
||||
public_key: String,
|
||||
#[serde(default = "default_reality_client_short_id")]
|
||||
short_id: String,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
sni_hostname: Option<String>,
|
||||
|
||||
/// TLS 1.3 cipher suites to use (optional)
|
||||
/// Valid values: "TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256"
|
||||
/// If empty or not specified, all three cipher suites are offered.
|
||||
#[serde(alias = "cipher_suite", default)]
|
||||
#[serde(
|
||||
alias = "cipher_suite",
|
||||
default,
|
||||
skip_serializing_if = "NoneOrSome::is_unspecified"
|
||||
)]
|
||||
cipher_suites: NoneOrSome<crate::reality::CipherSuite>,
|
||||
|
||||
/// Enable XTLS-Vision protocol for TLS-in-TLS optimization.
|
||||
/// When enabled, the inner protocol MUST be VLESS.
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
vision: bool,
|
||||
|
||||
protocol: Box<ClientProxyConfig>,
|
||||
@@ -221,7 +338,7 @@ pub enum ClientProxyConfig {
|
||||
password: String,
|
||||
|
||||
/// Optional SNI hostname override
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
sni_hostname: Option<String>,
|
||||
|
||||
/// Inner protocol (typically VLESS, Trojan, etc.)
|
||||
@@ -233,13 +350,36 @@ pub enum ClientProxyConfig {
|
||||
Vmess {
|
||||
cipher: String,
|
||||
user_id: String,
|
||||
#[serde(default = "default_true")]
|
||||
#[serde(default = "default_true", skip_serializing_if = "is_true")]
|
||||
udp_enabled: bool,
|
||||
},
|
||||
#[serde(alias = "ws")]
|
||||
Websocket(WebsocketClientConfig),
|
||||
#[serde(alias = "noop")]
|
||||
PortForward,
|
||||
/// AnyTLS outbound protocol
|
||||
Anytls {
|
||||
/// Authentication password
|
||||
password: String,
|
||||
/// UDP over TCP support (default: true)
|
||||
#[serde(default = "default_true", skip_serializing_if = "is_true")]
|
||||
udp_enabled: bool,
|
||||
/// Custom padding scheme (optional, uses default if not specified)
|
||||
/// Each line is a key=value pair like "stop=8" or "0=30-30"
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
padding_scheme: Option<Vec<String>>,
|
||||
},
|
||||
/// NaiveProxy client protocol (HTTP/2 CONNECT with padding)
|
||||
#[serde(alias = "naive")]
|
||||
Naiveproxy {
|
||||
/// Username for Basic Auth
|
||||
username: String,
|
||||
/// Password for Basic Auth
|
||||
password: String,
|
||||
/// Enable padding protocol (default: true)
|
||||
#[serde(default = "default_true", skip_serializing_if = "is_true")]
|
||||
padding: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl ClientProxyConfig {
|
||||
@@ -253,8 +393,8 @@ impl ClientProxyConfig {
|
||||
ClientProxyConfig::Direct => "Direct",
|
||||
ClientProxyConfig::Http { .. } => "HTTP",
|
||||
ClientProxyConfig::Socks { .. } => "SOCKS5",
|
||||
ClientProxyConfig::Shadowsocks(..) => "Shadowsocks",
|
||||
ClientProxyConfig::Snell(..) => "Snell",
|
||||
ClientProxyConfig::Shadowsocks { .. } => "Shadowsocks",
|
||||
ClientProxyConfig::Snell { .. } => "Snell",
|
||||
ClientProxyConfig::Vless { .. } => "VLESS",
|
||||
ClientProxyConfig::Trojan { .. } => "Trojan",
|
||||
ClientProxyConfig::Reality { .. } => "Reality",
|
||||
@@ -263,31 +403,41 @@ impl ClientProxyConfig {
|
||||
ClientProxyConfig::Vmess { .. } => "VMess",
|
||||
ClientProxyConfig::Websocket(..) => "WebSocket",
|
||||
ClientProxyConfig::PortForward => "PortForward",
|
||||
ClientProxyConfig::Anytls { .. } => "AnyTLS",
|
||||
ClientProxyConfig::Naiveproxy { .. } => "NaiveProxy",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct TlsClientConfig {
|
||||
#[serde(default = "default_true")]
|
||||
#[serde(default = "default_true", skip_serializing_if = "is_true")]
|
||||
pub verify: bool,
|
||||
#[serde(alias = "server_fingerprint", default)]
|
||||
#[serde(
|
||||
alias = "server_fingerprint",
|
||||
default,
|
||||
skip_serializing_if = "NoneOrSome::is_unspecified"
|
||||
)]
|
||||
pub server_fingerprints: NoneOrSome<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "NoneOrOne::is_unspecified")]
|
||||
pub sni_hostname: NoneOrOne<String>,
|
||||
#[serde(alias = "alpn_protocol", default)]
|
||||
#[serde(
|
||||
alias = "alpn_protocol",
|
||||
default,
|
||||
skip_serializing_if = "NoneOrSome::is_unspecified"
|
||||
)]
|
||||
pub alpn_protocols: NoneOrSome<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tls_buffer_size: Option<usize>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub key: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cert: Option<String>,
|
||||
|
||||
/// Enable XTLS-Vision protocol for TLS-in-TLS optimization.
|
||||
/// When enabled, the inner protocol MUST be VLESS.
|
||||
/// Requires TLS 1.3.
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub vision: bool,
|
||||
|
||||
pub protocol: Box<ClientProxyConfig>,
|
||||
@@ -295,11 +445,11 @@ pub struct TlsClientConfig {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct WebsocketClientConfig {
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub matching_path: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub matching_headers: Option<HashMap<String, String>>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "WebsocketPingType::is_default")]
|
||||
pub ping_type: WebsocketPingType,
|
||||
pub protocol: Box<ClientProxyConfig>,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,14 @@ pub fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn is_false(b: &bool) -> bool {
|
||||
!*b
|
||||
}
|
||||
|
||||
pub fn is_true(b: &bool) -> bool {
|
||||
*b
|
||||
}
|
||||
|
||||
pub fn default_reality_client_short_id() -> String {
|
||||
DEFAULT_REALITY_SHORT_ID.to_string()
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use super::client::ClientConfig;
|
||||
use super::rules::RuleConfig;
|
||||
use super::selection::ConfigSelection;
|
||||
use super::server::ServerConfig;
|
||||
use super::tun::TunConfig;
|
||||
|
||||
/// A named group of client proxies.
|
||||
///
|
||||
@@ -148,6 +149,9 @@ impl Serialize for NamedPem {
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Config {
|
||||
Server(ServerConfig),
|
||||
/// TUN device server - accepts IP packets from a TUN device.
|
||||
/// This is separate from Server because TUN doesn't use bind_location or transport.
|
||||
TunServer(TunConfig),
|
||||
ClientConfigGroup(ClientConfigGroup),
|
||||
RuleConfigGroup(RuleConfigGroup),
|
||||
NamedPem(NamedPem),
|
||||
@@ -176,6 +180,13 @@ impl<'de> serde::de::Deserialize<'de> for Config {
|
||||
let has_path_field = map.contains_key(Value::String("path".to_string()));
|
||||
let has_pem = map.contains_key(Value::String("pem".to_string()));
|
||||
|
||||
// Check if this is a TUN config
|
||||
// TUN configs have 'device_name' (Linux) or 'device_fd' (iOS/Android)
|
||||
// These fields are unique to TUN and don't appear in other config types
|
||||
let has_device_name = map.contains_key(Value::String("device_name".to_string()));
|
||||
let has_device_fd = map.contains_key(Value::String("device_fd".to_string()));
|
||||
let is_tun_config = has_device_name || has_device_fd;
|
||||
|
||||
// Try to determine which variant based on fields
|
||||
if has_pem {
|
||||
// NamedPem (pem field is unique to NamedPem)
|
||||
@@ -192,6 +203,11 @@ impl<'de> serde::de::Deserialize<'de> for Config {
|
||||
serde_yaml::from_value(value)
|
||||
.map(Config::RuleConfigGroup)
|
||||
.map_err(|e| Error::custom(format!("invalid rule config group: {e}")))
|
||||
} else if is_tun_config {
|
||||
// TunConfig - identified by having 'name' or 'raw_fd' without 'protocol' wrapper
|
||||
serde_yaml::from_value(value)
|
||||
.map(Config::TunServer)
|
||||
.map_err(|e| Error::custom(format!("invalid TUN config: {e}")))
|
||||
} else if has_address || has_path_field {
|
||||
// ServerConfig - its custom deserializer validates unknown fields
|
||||
serde_yaml::from_value(value)
|
||||
@@ -221,6 +237,7 @@ impl serde::ser::Serialize for Config {
|
||||
{
|
||||
match self {
|
||||
Config::Server(server) => server.serialize(serializer),
|
||||
Config::TunServer(tun) => tun.serialize(serializer),
|
||||
Config::ClientConfigGroup(group) => group.serialize(serializer),
|
||||
Config::RuleConfigGroup(group) => group.serialize(serializer),
|
||||
Config::NamedPem(pem) => pem.serialize(serializer),
|
||||
|
||||
@@ -20,6 +20,7 @@ pub mod selection;
|
||||
pub mod server;
|
||||
pub mod shadowsocks;
|
||||
pub mod transport;
|
||||
pub mod tun;
|
||||
|
||||
// Re-export all public types for convenience
|
||||
pub use client::{ClientConfig, ClientProxyConfig, TlsClientConfig, WebsocketClientConfig};
|
||||
@@ -34,3 +35,4 @@ pub use server::{
|
||||
};
|
||||
pub use shadowsocks::ShadowsocksConfig;
|
||||
pub use transport::{BindLocation, ClientQuicConfig, ServerQuicConfig, TcpConfig, Transport};
|
||||
pub use tun::TunConfig;
|
||||
|
||||
+219
-24
@@ -13,9 +13,104 @@ use super::selection::ConfigSelection;
|
||||
use super::shadowsocks::ShadowsocksConfig;
|
||||
use super::transport::{BindLocation, ServerQuicConfig, TcpConfig, Transport};
|
||||
|
||||
/// AnyTLS user configuration
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AnyTlsUserConfig {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub name: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// NaiveProxy user configuration
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NaiveUserConfig {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub name: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// NaiveProxy fallback configuration for probe resistance
|
||||
///
|
||||
/// Must be an absolute path to a directory to serve static files from.
|
||||
/// External server fallback (http/https URLs) is no longer supported.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NaiveFallbackConfig(pub std::path::PathBuf);
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for NaiveFallbackConfig {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
let s = String::deserialize(deserializer)?;
|
||||
if s.starts_with('/') {
|
||||
Ok(NaiveFallbackConfig(std::path::PathBuf::from(s)))
|
||||
} else {
|
||||
Err(Error::custom(
|
||||
"Fallback must be an absolute path (starting with '/')",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for NaiveFallbackConfig {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.0.display().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Forward declarations for client types (used in ShadowTlsRemoteHandshake)
|
||||
use super::client::ClientConfig;
|
||||
|
||||
/// Custom deserializer for ServerProxyConfig::Shadowsocks
|
||||
fn deserialize_shadowsocks_server<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<(ShadowsocksConfig, bool), D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ShadowsocksServerTemp {
|
||||
cipher: String,
|
||||
password: String,
|
||||
#[serde(default = "default_true")]
|
||||
udp_enabled: bool,
|
||||
}
|
||||
|
||||
let temp = ShadowsocksServerTemp::deserialize(deserializer)?;
|
||||
let config =
|
||||
ShadowsocksConfig::from_fields(&temp.cipher, &temp.password).map_err(Error::custom)?;
|
||||
|
||||
Ok((config, temp.udp_enabled))
|
||||
}
|
||||
|
||||
/// Custom serializer for ServerProxyConfig::Shadowsocks - flattens config fields
|
||||
fn serialize_shadowsocks_server<S>(
|
||||
config: &ShadowsocksConfig,
|
||||
udp_enabled: &bool,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
|
||||
let mut state = serializer.serialize_struct("Shadowsocks", 3)?;
|
||||
config.serialize_fields(&mut state)?;
|
||||
state.serialize_field("udp_enabled", udp_enabled)?;
|
||||
state.end()
|
||||
}
|
||||
|
||||
/// Custom deserializer for ServerProxyConfig::Vmess that validates legacy force_aead field
|
||||
fn deserialize_vmess_server<'de, D>(deserializer: D) -> Result<(String, String, bool), D::Error>
|
||||
where
|
||||
@@ -63,13 +158,21 @@ pub struct ServerConfig {
|
||||
#[serde(flatten)]
|
||||
pub bind_location: BindLocation,
|
||||
pub protocol: ServerProxyConfig,
|
||||
#[serde(alias = "transport", default)]
|
||||
#[serde(
|
||||
alias = "transport",
|
||||
default,
|
||||
skip_serializing_if = "Transport::is_default"
|
||||
)]
|
||||
pub transport: Transport,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tcp_settings: Option<TcpConfig>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub quic_settings: Option<ServerQuicConfig>,
|
||||
#[serde(alias = "rule", default = "direct_allow_rule")]
|
||||
#[serde(
|
||||
alias = "rule",
|
||||
default = "direct_allow_rule",
|
||||
skip_serializing_if = "NoneOrSome::is_unspecified"
|
||||
)]
|
||||
pub rules: NoneOrSome<ConfigSelection<RuleConfig>>,
|
||||
}
|
||||
|
||||
@@ -179,8 +282,6 @@ impl<'de> serde::de::Deserialize<'de> for ServerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// REALITY Protocol Configuration
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct RealityServerConfig {
|
||||
/// X25519 private key (32 bytes, base64url encoded)
|
||||
@@ -220,6 +321,11 @@ pub struct RealityServerConfig {
|
||||
/// Inner protocol (VLESS, Trojan, etc.)
|
||||
pub protocol: ServerProxyConfig,
|
||||
|
||||
/// Client chain for connecting to dest server (for fallback connections).
|
||||
/// If not specified, connects directly to dest.
|
||||
#[serde(default)]
|
||||
pub dest_client_chain: NoneOrSome<ClientChainHop>,
|
||||
|
||||
/// Override rules
|
||||
#[serde(alias = "override_rule", default)]
|
||||
pub override_rules: NoneOrSome<ConfigSelection<RuleConfig>>,
|
||||
@@ -508,20 +614,42 @@ pub enum WebsocketPingType {
|
||||
EmptyFrame,
|
||||
}
|
||||
|
||||
impl WebsocketPingType {
|
||||
pub fn is_default(&self) -> bool {
|
||||
matches!(self, WebsocketPingType::PingFrame)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
pub enum ServerProxyConfig {
|
||||
Http {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
username: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
password: Option<String>,
|
||||
},
|
||||
#[serde(alias = "socks5")]
|
||||
Socks {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
username: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
password: Option<String>,
|
||||
/// Enable UDP functionality (UDP ASSOCIATE and UDP-over-TCP).
|
||||
/// When false (default), UDP ASSOCIATE returns "command not supported".
|
||||
#[serde(default = "default_true")]
|
||||
udp_enabled: bool,
|
||||
},
|
||||
#[serde(
|
||||
alias = "ss",
|
||||
deserialize_with = "deserialize_shadowsocks_server",
|
||||
serialize_with = "serialize_shadowsocks_server"
|
||||
)]
|
||||
Shadowsocks {
|
||||
config: ShadowsocksConfig,
|
||||
#[serde(default = "default_true")]
|
||||
udp_enabled: bool,
|
||||
},
|
||||
#[serde(alias = "ss")]
|
||||
Shadowsocks(ShadowsocksConfig),
|
||||
Snell {
|
||||
cipher: String,
|
||||
password: String,
|
||||
@@ -532,10 +660,14 @@ pub enum ServerProxyConfig {
|
||||
user_id: String,
|
||||
#[serde(default = "default_true")]
|
||||
udp_enabled: bool,
|
||||
/// Fallback destination for failed authentication (optional)
|
||||
/// When set, failed auth attempts are proxied here instead of rejected
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
fallback: Option<NetLocation>,
|
||||
},
|
||||
Trojan {
|
||||
password: String,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
shadowsocks: Option<ShadowsocksConfig>,
|
||||
},
|
||||
Tls {
|
||||
@@ -543,14 +675,18 @@ pub enum ServerProxyConfig {
|
||||
#[serde(default, alias = "sni_targets", alias = "targets")]
|
||||
tls_targets: HashMap<String, TlsServerConfig>,
|
||||
// default_target is the previous field name
|
||||
#[serde(default, alias = "default_target")]
|
||||
#[serde(
|
||||
default,
|
||||
alias = "default_target",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
default_tls_target: Option<Box<TlsServerConfig>>,
|
||||
#[serde(default)]
|
||||
shadowtls_targets: HashMap<String, ShadowTlsServerConfig>,
|
||||
#[serde(default)]
|
||||
reality_targets: HashMap<String, RealityServerConfig>,
|
||||
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
tls_buffer_size: Option<usize>,
|
||||
},
|
||||
#[serde(deserialize_with = "deserialize_vmess_server")]
|
||||
@@ -560,6 +696,20 @@ pub enum ServerProxyConfig {
|
||||
#[serde(default = "default_true")]
|
||||
udp_enabled: bool,
|
||||
},
|
||||
/// AnyTLS inner protocol handler
|
||||
/// Used as inner protocol within TLS/Reality/etc.
|
||||
Anytls {
|
||||
#[serde(alias = "user")]
|
||||
users: OneOrSome<AnyTlsUserConfig>,
|
||||
#[serde(alias = "padding")]
|
||||
padding_scheme: Option<Vec<String>>,
|
||||
#[serde(default = "default_true")]
|
||||
udp_enabled: bool,
|
||||
/// Fallback destination for failed authentication (optional)
|
||||
/// When set, failed auth attempts are proxied here instead of rejected
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
fallback: Option<NetLocation>,
|
||||
},
|
||||
#[serde(alias = "ws")]
|
||||
Websocket {
|
||||
#[serde(alias = "target")]
|
||||
@@ -585,6 +735,36 @@ pub enum ServerProxyConfig {
|
||||
#[serde(default)]
|
||||
zero_rtt_handshake: bool,
|
||||
},
|
||||
/// Mixed HTTP+SOCKS5 server (auto-detects protocol from first byte)
|
||||
/// Similar to mihomo's mixed-port feature.
|
||||
#[serde(alias = "http+socks", alias = "socks+http")]
|
||||
Mixed {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
username: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
password: Option<String>,
|
||||
/// Enable UDP functionality for SOCKS5 (UDP ASSOCIATE and UDP-over-TCP)
|
||||
#[serde(default = "default_true")]
|
||||
udp_enabled: bool,
|
||||
},
|
||||
/// NaiveProxy server (HTTP/2 CONNECT with padding)
|
||||
/// Should be used within TLS for proper camouflage
|
||||
#[serde(alias = "naive")]
|
||||
Naiveproxy {
|
||||
/// List of authorized users
|
||||
#[serde(alias = "user")]
|
||||
users: OneOrSome<NaiveUserConfig>,
|
||||
/// Enable padding protocol (default: true)
|
||||
#[serde(default = "default_true")]
|
||||
padding: bool,
|
||||
/// Fallback for probe resistance (optional)
|
||||
/// Can be either a URL (http:// or https://) to reverse proxy, or an absolute path to serve files from
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
fallback: Option<NaiveFallbackConfig>,
|
||||
/// Enable UDP-over-TCP (UoT) support (default: true)
|
||||
#[serde(default = "default_true")]
|
||||
udp_enabled: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ServerProxyConfig {
|
||||
@@ -629,6 +809,9 @@ impl std::fmt::Display for ServerProxyConfig {
|
||||
Self::PortForward { .. } => write!(f, "Portforward"),
|
||||
Self::Hysteria2 { .. } => write!(f, "Hysteria2"),
|
||||
Self::TuicV5 { .. } => write!(f, "TuicV5"),
|
||||
Self::Mixed { .. } => write!(f, "Mixed (HTTP+SOCKS5)"),
|
||||
Self::Anytls { .. } => write!(f, "AnyTLS"),
|
||||
Self::Naiveproxy { .. } => write!(f, "NaiveProxy"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -663,6 +846,7 @@ mod tests {
|
||||
protocol: ServerProxyConfig::Socks {
|
||||
username: None,
|
||||
password: None,
|
||||
udp_enabled: false,
|
||||
},
|
||||
transport: Transport::Tcp,
|
||||
tcp_settings: None,
|
||||
@@ -674,10 +858,13 @@ mod tests {
|
||||
fn create_test_server_config_shadowsocks() -> ServerConfig {
|
||||
ServerConfig {
|
||||
bind_location: BindLocation::Path(PathBuf::from("/tmp/ss.sock")),
|
||||
protocol: ServerProxyConfig::Shadowsocks(ShadowsocksConfig::Legacy {
|
||||
cipher: "aes-256-gcm".try_into().unwrap(),
|
||||
password: "secret123".to_string(),
|
||||
}),
|
||||
protocol: ServerProxyConfig::Shadowsocks {
|
||||
config: ShadowsocksConfig::Legacy {
|
||||
cipher: "aes-256-gcm".try_into().unwrap(),
|
||||
password: "secret123".to_string(),
|
||||
},
|
||||
udp_enabled: true,
|
||||
},
|
||||
transport: Transport::Tcp,
|
||||
tcp_settings: None,
|
||||
quic_settings: None,
|
||||
@@ -694,6 +881,7 @@ mod tests {
|
||||
protocol: ServerProxyConfig::Vless {
|
||||
user_id: "550e8400-e29b-41d4-a716-446655440000".to_string(),
|
||||
udp_enabled: true,
|
||||
fallback: None,
|
||||
},
|
||||
transport: Transport::Quic,
|
||||
tcp_settings: None,
|
||||
@@ -918,7 +1106,7 @@ mod tests {
|
||||
serde_yaml::from_str(&yaml_str).expect("Failed to deserialize");
|
||||
assert!(matches!(
|
||||
deserialized.protocol,
|
||||
ServerProxyConfig::Shadowsocks(_)
|
||||
ServerProxyConfig::Shadowsocks { .. }
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1120,23 +1308,30 @@ protocol:
|
||||
);
|
||||
}
|
||||
|
||||
// Note: This test was for reject unknown fields in Shadowsocks config.
|
||||
// After adding udp_enabled with #[serde(flatten)] for ShadowsocksConfig,
|
||||
// serde can no longer enforce deny_unknown_fields due to a serde limitation.
|
||||
// See: https://github.com/serde-rs/serde/issues/1547
|
||||
//
|
||||
// The test is kept here but modified to verify that the known fields work correctly.
|
||||
#[test]
|
||||
fn test_rejects_unknown_field_in_shadowsocks() {
|
||||
fn test_shadowsocks_with_udp_enabled() {
|
||||
let yaml = r#"
|
||||
address: "127.0.0.1:8080"
|
||||
protocol:
|
||||
type: shadowsocks
|
||||
cipher: aes-256-gcm
|
||||
password: "secret"
|
||||
invalid_field: 123
|
||||
udp_enabled: false
|
||||
"#;
|
||||
let result: Result<ServerConfig, _> = serde_yaml::from_str(yaml);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
err.contains("unknown field `invalid_field`"),
|
||||
"Error should mention unknown field: {err}"
|
||||
);
|
||||
assert!(result.is_ok(), "Valid Shadowsocks config should parse");
|
||||
let config = result.unwrap();
|
||||
if let ServerProxyConfig::Shadowsocks { udp_enabled, .. } = config.protocol {
|
||||
assert!(!udp_enabled, "udp_enabled should be false");
|
||||
} else {
|
||||
panic!("Expected Shadowsocks protocol");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -17,58 +17,43 @@ pub enum ShadowsocksConfig {
|
||||
},
|
||||
}
|
||||
|
||||
impl<'de> serde::de::Deserialize<'de> for ShadowsocksConfig {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ShadowsocksConfigTemp {
|
||||
cipher: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
let temp = ShadowsocksConfigTemp::deserialize(deserializer)?;
|
||||
|
||||
match temp.cipher.strip_prefix("2022-blake3-") {
|
||||
impl ShadowsocksConfig {
|
||||
/// Create a ShadowsocksConfig from cipher and password strings.
|
||||
/// Handles both legacy ciphers and 2022-blake3-* ciphers.
|
||||
pub fn from_fields(cipher: &str, password: &str) -> std::io::Result<Self> {
|
||||
match cipher.strip_prefix("2022-blake3-") {
|
||||
Some(stripped) => {
|
||||
let cipher: ShadowsocksCipher =
|
||||
stripped.try_into().map_err(serde::de::Error::custom)?;
|
||||
let key_bytes = BASE64
|
||||
.decode(&temp.password)
|
||||
.map_err(|e| {
|
||||
serde::de::Error::custom(format!(
|
||||
let cipher: ShadowsocksCipher = stripped.try_into()?;
|
||||
let key_bytes = BASE64.decode(password).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Failed to base64 decode password for 2022-blake3 cipher: {}",
|
||||
e
|
||||
))
|
||||
})?
|
||||
.into_boxed_slice();
|
||||
Ok(ShadowsocksConfig::Aead2022 { cipher, key_bytes })
|
||||
),
|
||||
)
|
||||
})?;
|
||||
Ok(ShadowsocksConfig::Aead2022 {
|
||||
cipher,
|
||||
key_bytes: key_bytes.into_boxed_slice(),
|
||||
})
|
||||
}
|
||||
None => {
|
||||
let cipher: ShadowsocksCipher = temp
|
||||
.cipher
|
||||
.as_str()
|
||||
.try_into()
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
let cipher: ShadowsocksCipher = cipher.try_into()?;
|
||||
Ok(ShadowsocksConfig::Legacy {
|
||||
cipher,
|
||||
password: temp.password,
|
||||
password: password.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::Serialize for ShadowsocksConfig {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::ser::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
|
||||
let mut state = serializer.serialize_struct("ShadowsocksConfig", 2)?;
|
||||
/// Serialize cipher and password fields to a SerializeStruct.
|
||||
/// Used by custom serializers to flatten ShadowsocksConfig fields.
|
||||
pub fn serialize_fields<S: serde::ser::SerializeStruct>(
|
||||
&self,
|
||||
state: &mut S,
|
||||
) -> Result<(), S::Error> {
|
||||
match self {
|
||||
ShadowsocksConfig::Legacy { cipher, password } => {
|
||||
state.serialize_field("cipher", cipher.name())?;
|
||||
@@ -80,6 +65,35 @@ impl serde::ser::Serialize for ShadowsocksConfig {
|
||||
state.serialize_field("password", &BASE64.encode(key_bytes))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::de::Deserialize<'de> for ShadowsocksConfig {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct ShadowsocksConfigTemp {
|
||||
cipher: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
let temp = ShadowsocksConfigTemp::deserialize(deserializer)?;
|
||||
Self::from_fields(&temp.cipher, &temp.password).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::Serialize for ShadowsocksConfig {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::ser::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
|
||||
let mut state = serializer.serialize_struct("ShadowsocksConfig", 2)?;
|
||||
self.serialize_fields(&mut state)?;
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,13 @@ pub enum Transport {
|
||||
Udp,
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
/// Returns true if this is the default transport (TCP)
|
||||
pub fn is_default(&self) -> bool {
|
||||
matches!(self, Transport::Tcp)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct TcpConfig {
|
||||
#[serde(default = "default_true")]
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//! TUN device configuration types.
|
||||
//!
|
||||
//! TUN (network TUNnel) devices are virtual network interfaces that operate at
|
||||
//! the IP layer (Layer 3). Unlike regular server configs that bind to a
|
||||
//! TCP/UDP port, TUN servers receive raw IP packets from applications.
|
||||
//!
|
||||
//! # Platform Differences
|
||||
//!
|
||||
//! - **Linux**: Creates a new TUN device with the specified name and address.
|
||||
//! Requires root privileges or `CAP_NET_ADMIN` capability.
|
||||
//!
|
||||
//! - **Android**: Requires a file descriptor from `VpnService.Builder.establish()`.
|
||||
//! The VPN configuration (routes, DNS, etc.) is handled by the Android VpnService.
|
||||
//!
|
||||
//! - **iOS**: Requires a file descriptor from `NEPacketTunnelProvider.packetFlow`.
|
||||
//! Use `packet_information: true` if using the socket FD directly.
|
||||
|
||||
use std::net::IpAddr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::option_util::NoneOrSome;
|
||||
|
||||
use super::common::default_true;
|
||||
use super::rules::RuleConfig;
|
||||
use super::selection::ConfigSelection;
|
||||
|
||||
fn default_mtu() -> u16 {
|
||||
// Platform-specific MTU defaults based on sing-box research:
|
||||
// - iOS Network Extension: 4064 max (4096 - 32 byte UTUN_IF_HEADROOM_SIZE)
|
||||
// Performance drops significantly above this value
|
||||
// - Android: 9000 (some devices report ENOBUFS with 65535)
|
||||
// - Other platforms: 1500 (standard Ethernet MTU)
|
||||
#[cfg(target_os = "ios")]
|
||||
return 4064;
|
||||
#[cfg(target_os = "android")]
|
||||
return 9000;
|
||||
#[cfg(not(any(target_os = "ios", target_os = "android")))]
|
||||
return 1500;
|
||||
}
|
||||
|
||||
/// TUN device server configuration.
|
||||
///
|
||||
/// This is a top-level config type (not nested under ServerConfig) because TUN
|
||||
/// devices are fundamentally different from TCP/UDP servers:
|
||||
/// - No bind address (binds to a virtual network device, not a socket)
|
||||
/// - No transport layer (receives raw IP packets)
|
||||
/// - Platform-specific device creation (Linux name/address vs iOS/Android raw_fd)
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TunConfig {
|
||||
/// TUN device name (Linux only, e.g., "tun0").
|
||||
/// Ignored on iOS/Android where the device is provided via device_fd.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub device_name: Option<String>,
|
||||
|
||||
/// Raw file descriptor for the TUN device (iOS/Android).
|
||||
/// - **Android**: from `VpnService.Builder.establish()`
|
||||
/// - **iOS**: from `NEPacketTunnelProvider.packetFlow`
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub device_fd: Option<i32>,
|
||||
|
||||
/// TUN device IP address (e.g., "10.0.0.1").
|
||||
/// - **Linux**: Sets the device's IP address
|
||||
/// - **iOS/Android**: Informational only (address is set by VPN service)
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub address: Option<IpAddr>,
|
||||
|
||||
/// TUN device netmask (e.g., "255.255.255.0").
|
||||
/// - **Linux**: Sets the device's netmask
|
||||
/// - **iOS/Android**: Informational only
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub netmask: Option<IpAddr>,
|
||||
|
||||
/// TUN device destination/gateway (Linux only).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub destination: Option<IpAddr>,
|
||||
|
||||
/// MTU size for the TUN interface.
|
||||
/// Default: 1500
|
||||
#[serde(default = "default_mtu")]
|
||||
pub mtu: u16,
|
||||
|
||||
/// Enable TCP connection handling.
|
||||
/// Default: true
|
||||
#[serde(default = "default_true")]
|
||||
pub tcp_enabled: bool,
|
||||
|
||||
/// Enable UDP packet handling.
|
||||
/// Default: true
|
||||
#[serde(default = "default_true")]
|
||||
pub udp_enabled: bool,
|
||||
|
||||
/// Enable ICMP (ping) handling.
|
||||
/// Note: ICMP requires TCP to be enabled as well.
|
||||
/// Default: true
|
||||
#[serde(default = "default_true")]
|
||||
pub icmp_enabled: bool,
|
||||
|
||||
/// Routing rules for traffic coming through the TUN device.
|
||||
/// Default: Allow all traffic directly
|
||||
#[serde(
|
||||
alias = "rule",
|
||||
default,
|
||||
skip_serializing_if = "NoneOrSome::is_unspecified"
|
||||
)]
|
||||
pub rules: NoneOrSome<ConfigSelection<RuleConfig>>,
|
||||
}
|
||||
+232
-27
@@ -6,26 +6,28 @@ use crate::address::NetLocationMask;
|
||||
use crate::option_util::{NoneOrSome, OneOrSome};
|
||||
use crate::reality::{decode_private_key, decode_short_id};
|
||||
use crate::thread_util::get_num_threads;
|
||||
use crate::util::parse_uuid;
|
||||
use crate::uuid_util::parse_uuid;
|
||||
|
||||
use super::pem::{embed_optional_pem_from_map, embed_pem_from_map};
|
||||
use super::types::{
|
||||
ClientChain, ClientChainHop, ClientConfig, ClientProxyConfig, Config, ConfigSelection,
|
||||
DEFAULT_REALITY_SHORT_ID, PemSource, RuleActionConfig, RuleConfig, ServerConfig,
|
||||
ServerProxyConfig, ServerQuicConfig, ShadowTlsServerConfig, ShadowTlsServerHandshakeConfig,
|
||||
ShadowsocksConfig, TlsServerConfig, Transport, WebsocketServerConfig, direct_allow_rule,
|
||||
ShadowsocksConfig, TlsServerConfig, Transport, TunConfig, WebsocketServerConfig,
|
||||
direct_allow_rule,
|
||||
};
|
||||
|
||||
const MIN_TLS_BUFFER_SIZE: usize = 16 * 1024;
|
||||
|
||||
/// Creates validated ServerConfigs from a list of Config entries.
|
||||
/// Validates configs and returns only the startable server configs.
|
||||
///
|
||||
/// This function:
|
||||
/// - Builds client_groups and rule_groups from ClientConfigGroup and RuleConfigGroup entries
|
||||
/// - Resolves group references using topological sort
|
||||
/// - Collects named PEMs
|
||||
/// - Validates all ServerConfigs against the groups and PEMs
|
||||
pub async fn create_server_configs(all_configs: Vec<Config>) -> std::io::Result<Vec<ServerConfig>> {
|
||||
/// - Validates all ServerConfigs and TunConfigs against the groups and PEMs
|
||||
/// - Returns only Config::Server and Config::TunServer variants (groups/pems are consumed)
|
||||
pub async fn create_server_configs(all_configs: Vec<Config>) -> std::io::Result<Vec<Config>> {
|
||||
// First pass: collect raw groups with unresolved references
|
||||
let mut raw_client_groups: HashMap<String, OneOrSome<ConfigSelection<ClientConfig>>> =
|
||||
HashMap::new();
|
||||
@@ -54,6 +56,7 @@ pub async fn create_server_configs(all_configs: Vec<Config>) -> std::io::Result<
|
||||
);
|
||||
|
||||
let mut server_configs: Vec<ServerConfig> = vec![];
|
||||
let mut tun_configs: Vec<TunConfig> = vec![];
|
||||
let mut named_pems: HashMap<String, String> = HashMap::new();
|
||||
|
||||
for config in all_configs.into_iter() {
|
||||
@@ -83,6 +86,9 @@ pub async fn create_server_configs(all_configs: Vec<Config>) -> std::io::Result<
|
||||
Config::Server(server_config) => {
|
||||
server_configs.push(server_config);
|
||||
}
|
||||
Config::TunServer(tun_config) => {
|
||||
tun_configs.push(tun_config);
|
||||
}
|
||||
Config::NamedPem(pem) => {
|
||||
let pem_data = match pem.source {
|
||||
PemSource::Data(data) => data,
|
||||
@@ -115,7 +121,16 @@ pub async fn create_server_configs(all_configs: Vec<Config>) -> std::io::Result<
|
||||
validate_server_config(config, &client_groups, &rule_groups, &named_pems)?;
|
||||
}
|
||||
|
||||
Ok(server_configs)
|
||||
// Validate TUN configs
|
||||
for config in tun_configs.iter_mut() {
|
||||
validate_tun_config(config, &client_groups, &rule_groups)?;
|
||||
}
|
||||
|
||||
// Combine into Config list (only Server and TunServer variants)
|
||||
let mut result: Vec<Config> = server_configs.into_iter().map(Config::Server).collect();
|
||||
result.extend(tun_configs.into_iter().map(Config::TunServer));
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Resolves client group references using topological sort.
|
||||
@@ -338,6 +353,7 @@ fn validate_server_config(
|
||||
client_groups,
|
||||
rule_groups,
|
||||
named_pems,
|
||||
false, // top-level, not inside TLS/Reality
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
@@ -628,8 +644,16 @@ fn validate_server_proxy_config(
|
||||
client_groups: &HashMap<String, Vec<ClientConfig>>,
|
||||
rule_groups: &HashMap<String, Vec<RuleConfig>>,
|
||||
named_pems: &HashMap<String, String>,
|
||||
inside_tls_or_reality: bool,
|
||||
) -> std::io::Result<()> {
|
||||
match server_proxy_config {
|
||||
ServerProxyConfig::Naiveproxy { .. } if !inside_tls_or_reality => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"NaiveProxy must be used inside a TLS or Reality protocol. \
|
||||
Configure it as the inner protocol of tls: or reality: targets.",
|
||||
));
|
||||
}
|
||||
ServerProxyConfig::Vless { user_id, .. } => {
|
||||
parse_uuid(user_id)?;
|
||||
}
|
||||
@@ -669,7 +693,13 @@ fn validate_server_proxy_config(
|
||||
|
||||
validate_client_fingerprints(client_fingerprints)?;
|
||||
|
||||
validate_server_proxy_config(protocol, client_groups, rule_groups, named_pems)?;
|
||||
validate_server_proxy_config(
|
||||
protocol,
|
||||
client_groups,
|
||||
rule_groups,
|
||||
named_pems,
|
||||
true,
|
||||
)?;
|
||||
|
||||
ConfigSelection::replace_none_or_some_groups(override_rules, rule_groups)?;
|
||||
|
||||
@@ -693,7 +723,13 @@ fn validate_server_proxy_config(
|
||||
ref mut override_rules,
|
||||
..
|
||||
} = **tls_server_config;
|
||||
validate_server_proxy_config(protocol, client_groups, rule_groups, named_pems)?;
|
||||
validate_server_proxy_config(
|
||||
protocol,
|
||||
client_groups,
|
||||
rule_groups,
|
||||
named_pems,
|
||||
true,
|
||||
)?;
|
||||
|
||||
ConfigSelection::replace_none_or_some_groups(override_rules, rule_groups)?;
|
||||
|
||||
@@ -727,7 +763,13 @@ fn validate_server_proxy_config(
|
||||
validate_client_fingerprints(&mut local_handshake.client_fingerprints)?;
|
||||
}
|
||||
|
||||
validate_server_proxy_config(protocol, client_groups, rule_groups, named_pems)?;
|
||||
validate_server_proxy_config(
|
||||
protocol,
|
||||
client_groups,
|
||||
rule_groups,
|
||||
named_pems,
|
||||
true,
|
||||
)?;
|
||||
|
||||
ConfigSelection::replace_none_or_some_groups(override_rules, rule_groups)?;
|
||||
|
||||
@@ -766,6 +808,7 @@ fn validate_server_proxy_config(
|
||||
client_groups,
|
||||
rule_groups,
|
||||
named_pems,
|
||||
true,
|
||||
)?;
|
||||
|
||||
ConfigSelection::replace_none_or_some_groups(
|
||||
@@ -798,7 +841,13 @@ fn validate_server_proxy_config(
|
||||
override_rules,
|
||||
..
|
||||
} = websocket_server_config;
|
||||
validate_server_proxy_config(protocol, client_groups, rule_groups, named_pems)?;
|
||||
validate_server_proxy_config(
|
||||
protocol,
|
||||
client_groups,
|
||||
rule_groups,
|
||||
named_pems,
|
||||
false,
|
||||
)?;
|
||||
|
||||
ConfigSelection::replace_none_or_some_groups(override_rules, rule_groups)?;
|
||||
|
||||
@@ -835,6 +884,62 @@ fn validate_server_proxy_config(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validates a TUN configuration.
|
||||
fn validate_tun_config(
|
||||
config: &mut TunConfig,
|
||||
client_groups: &HashMap<String, Vec<ClientConfig>>,
|
||||
rule_groups: &HashMap<String, Vec<RuleConfig>>,
|
||||
) -> std::io::Result<()> {
|
||||
// Validate ICMP requires TCP
|
||||
if !config.tcp_enabled && config.icmp_enabled {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"TUN: TCP must be enabled for ICMP",
|
||||
));
|
||||
}
|
||||
|
||||
// Validate that we have either Linux config (device_name/address) or mobile config (device_fd)
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if config.device_fd.is_none() && (config.device_name.is_none() || config.address.is_none())
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"TUN on Linux requires either 'device_fd' or both 'device_name' and 'address'",
|
||||
));
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
if config.device_fd.is_none() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"TUN on Android requires 'device_fd' from VpnService.Builder.establish()",
|
||||
));
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
if config.device_fd.is_none() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"TUN on iOS requires 'device_fd' from NEPacketTunnelProvider.packetFlow",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve rule group references
|
||||
ConfigSelection::replace_none_or_some_groups(&mut config.rules, rule_groups)?;
|
||||
|
||||
// Validate rules
|
||||
for rule in config.rules.iter_mut() {
|
||||
let rule = rule.unwrap_config_mut();
|
||||
validate_rule_config(rule, client_groups, &HashMap::new())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_rule_config(
|
||||
rule_config: &mut RuleConfig,
|
||||
client_groups: &HashMap<String, Vec<ClientConfig>>,
|
||||
@@ -958,15 +1063,7 @@ fn validate_and_expand_selection(
|
||||
format!("Unknown client_group in chain: {group_name}"),
|
||||
)
|
||||
})?;
|
||||
// We can't directly replace a GroupName with multiple configs in the selection.
|
||||
// The expansion happens at a higher level (in validate_client_chain_hop_expand).
|
||||
// For now, just validate that the group exists.
|
||||
// The actual expansion to multiple configs happens in tcp_handler_util.rs
|
||||
// Wait - we need to expand here. Let me think...
|
||||
//
|
||||
// Actually, we need a different approach. The chain hop expansion should happen
|
||||
// in a way that replaces the entire hop, not individual selections.
|
||||
// Let's validate and then expand.
|
||||
// Validate all configs in the group; expansion happens in expand_client_chain
|
||||
for mut config in group_configs.clone() {
|
||||
validate_client_config(&mut config, named_pems)?;
|
||||
}
|
||||
@@ -1050,7 +1147,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::config::pem::convert_cert_paths;
|
||||
|
||||
async fn validate_configs_test(configs: Vec<Config>) -> std::io::Result<Vec<ServerConfig>> {
|
||||
async fn validate_configs_test(configs: Vec<Config>) -> std::io::Result<Vec<Config>> {
|
||||
let (converted_configs, _) = convert_cert_paths(configs).await?;
|
||||
create_server_configs(converted_configs).await
|
||||
}
|
||||
@@ -1282,7 +1379,7 @@ mod tests {
|
||||
protocol:
|
||||
type: vmess
|
||||
cipher: auto
|
||||
user_id: "123e4567-e89b-12d3-a456-426614174000"
|
||||
user_id: "123e4567-e89b-42d3-a456-426614174000"
|
||||
shadowtls_targets:
|
||||
"shadow.com":
|
||||
password: "shadowpass"
|
||||
@@ -1325,8 +1422,10 @@ mod tests {
|
||||
|
||||
assert_eq!(load_count, 11);
|
||||
|
||||
let server_configs = create_server_configs(converted_configs).await.unwrap();
|
||||
let server_config = &server_configs[0];
|
||||
let configs = create_server_configs(converted_configs).await.unwrap();
|
||||
let Config::Server(server_config) = &configs[0] else {
|
||||
panic!("expected Config::Server");
|
||||
};
|
||||
|
||||
let quic_settings = server_config.quic_settings.as_ref().unwrap();
|
||||
assert!(quic_settings.cert.contains("BEGIN CERTIFICATE"));
|
||||
@@ -1350,10 +1449,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests for direct connector position validation
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_direct_connector_at_hop_0_allowed() {
|
||||
// Single direct connector at hop 0 should be allowed
|
||||
@@ -1501,4 +1596,114 @@ mod tests {
|
||||
|
||||
assert!(validate_direct_connector_positions(&hops, 0).is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tun_config_parsing() {
|
||||
let yaml = r#"
|
||||
- device_name: "tun0"
|
||||
address: "10.0.0.1"
|
||||
netmask: "255.255.255.0"
|
||||
mtu: 1400
|
||||
tcp_enabled: true
|
||||
udp_enabled: true
|
||||
icmp_enabled: false
|
||||
rules:
|
||||
- masks: "0.0.0.0/0"
|
||||
action: allow
|
||||
client_chain:
|
||||
- protocol:
|
||||
type: direct
|
||||
"#;
|
||||
let configs: Vec<Config> = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(configs.len(), 1);
|
||||
|
||||
match &configs[0] {
|
||||
Config::TunServer(tun) => {
|
||||
assert_eq!(tun.device_name, Some("tun0".to_string()));
|
||||
assert_eq!(tun.address, Some("10.0.0.1".parse().unwrap()));
|
||||
assert_eq!(tun.netmask, Some("255.255.255.0".parse().unwrap()));
|
||||
assert_eq!(tun.mtu, 1400);
|
||||
assert!(tun.tcp_enabled);
|
||||
assert!(tun.udp_enabled);
|
||||
assert!(!tun.icmp_enabled);
|
||||
}
|
||||
_ => panic!("Expected TunServer config"),
|
||||
}
|
||||
|
||||
// Validate the config
|
||||
let result = validate_configs_test(configs).await;
|
||||
assert!(result.is_ok(), "TUN config validation failed: {:?}", result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tun_config_with_device_fd() {
|
||||
let yaml = r#"
|
||||
- device_fd: 42
|
||||
mtu: 1500
|
||||
rules:
|
||||
- masks: "0.0.0.0/0"
|
||||
action: allow
|
||||
client_chain:
|
||||
- protocol:
|
||||
type: direct
|
||||
"#;
|
||||
let configs: Vec<Config> = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(configs.len(), 1);
|
||||
|
||||
match &configs[0] {
|
||||
Config::TunServer(tun) => {
|
||||
assert_eq!(tun.device_fd, Some(42));
|
||||
assert_eq!(tun.device_name, None);
|
||||
assert_eq!(tun.mtu, 1500);
|
||||
}
|
||||
_ => panic!("Expected TunServer config"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tun_config_defaults() {
|
||||
let yaml = r#"
|
||||
- device_name: "tun0"
|
||||
address: "10.0.0.1"
|
||||
"#;
|
||||
let configs: Vec<Config> = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(configs.len(), 1);
|
||||
|
||||
match &configs[0] {
|
||||
Config::TunServer(tun) => {
|
||||
// Check defaults
|
||||
assert_eq!(tun.mtu, 1500); // default
|
||||
assert!(tun.tcp_enabled); // default true
|
||||
assert!(tun.udp_enabled); // default true
|
||||
assert!(tun.icmp_enabled); // default true
|
||||
}
|
||||
_ => panic!("Expected TunServer config"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tun_icmp_requires_tcp() {
|
||||
// ICMP requires TCP to be enabled
|
||||
let tun_config = TunConfig {
|
||||
device_name: Some("tun0".to_string()),
|
||||
device_fd: None,
|
||||
address: Some("10.0.0.1".parse().unwrap()),
|
||||
netmask: None,
|
||||
destination: None,
|
||||
mtu: 1500,
|
||||
tcp_enabled: false, // TCP disabled
|
||||
udp_enabled: true,
|
||||
icmp_enabled: true, // but ICMP enabled - should fail
|
||||
rules: NoneOrSome::Unspecified,
|
||||
};
|
||||
|
||||
let configs = vec![Config::TunServer(tun_config)];
|
||||
let result = validate_configs_test(configs).await;
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
err.contains("TCP must be enabled for ICMP"),
|
||||
"Expected ICMP/TCP error, got: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// - Don't bother initializing buffer
|
||||
// - Read and write whenever there's a space
|
||||
// - Circular buffer
|
||||
// - Cooperative yielding via tokio's coop budget to prevent task starvation
|
||||
|
||||
use futures::ready;
|
||||
use tokio::io::ReadBuf;
|
||||
@@ -54,6 +55,11 @@ impl CopyBuffer {
|
||||
R: AsyncStream + ?Sized,
|
||||
W: AsyncStream + ?Sized,
|
||||
{
|
||||
// Check tokio's cooperative budget at the start of each poll.
|
||||
// This ensures we yield to the runtime periodically during heavy I/O,
|
||||
// allowing other tasks (like QUIC keepalives) to run.
|
||||
let coop = ready!(tokio::task::coop::poll_proceed(cx));
|
||||
|
||||
loop {
|
||||
let mut read_pending = false;
|
||||
let mut write_pending = false;
|
||||
@@ -82,6 +88,7 @@ impl CopyBuffer {
|
||||
self.read_done = true;
|
||||
} else {
|
||||
self.cache_length += n;
|
||||
coop.made_progress();
|
||||
}
|
||||
}
|
||||
Poll::Pending => {
|
||||
@@ -100,6 +107,7 @@ impl CopyBuffer {
|
||||
self.need_write_ping = false;
|
||||
if written {
|
||||
self.need_flush = true;
|
||||
coop.made_progress();
|
||||
}
|
||||
}
|
||||
Poll::Pending => {
|
||||
@@ -140,6 +148,7 @@ impl CopyBuffer {
|
||||
self.start_index = (self.start_index + written) % self.size;
|
||||
}
|
||||
self.need_flush = true;
|
||||
coop.made_progress();
|
||||
}
|
||||
}
|
||||
Poll::Pending => {
|
||||
@@ -152,6 +161,7 @@ impl CopyBuffer {
|
||||
if self.need_flush {
|
||||
ready!(writer.as_mut().poll_flush(cx))?;
|
||||
self.need_flush = false;
|
||||
coop.made_progress();
|
||||
}
|
||||
|
||||
// If we've written all the data and we've seen EOF, finish the transfer.
|
||||
@@ -159,13 +169,8 @@ impl CopyBuffer {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// Previously we kept going until both read and write were pending, but
|
||||
// this might starve other tasks.
|
||||
// Return Pending to prevent task starvation
|
||||
if read_pending || write_pending {
|
||||
// If we got here,
|
||||
// 1) we hit read_pending on the current iteration.
|
||||
// 2) all data has been written successfully
|
||||
// 3) there is no data left to write and we need to read more.
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
@@ -225,7 +230,6 @@ where
|
||||
type Output = io::Result<()>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
// Unpack self into mut refs to each field to avoid borrow check issues.
|
||||
let CopyBidirectional {
|
||||
a,
|
||||
b,
|
||||
|
||||
@@ -8,17 +8,17 @@ use std::task::{Context, Poll};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::async_stream::AsyncMessageStream;
|
||||
use crate::util::allocate_vec;
|
||||
|
||||
// Informed by https://stackoverflow.com/questions/14856639/udp-hole-punching-timeout
|
||||
pub const DEFAULT_ASSOCIATION_TIMEOUT_SECS: u32 = 200;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CopyBuffer {
|
||||
read_done: bool,
|
||||
need_flush: bool,
|
||||
need_write_ping: bool,
|
||||
cache_length: usize,
|
||||
buf: [u8; 65535],
|
||||
buf: Box<[u8]>,
|
||||
read_count: usize,
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ impl CopyBuffer {
|
||||
need_flush,
|
||||
need_write_ping: false,
|
||||
cache_length: 0,
|
||||
buf: [0u8; 65535],
|
||||
buf: allocate_vec(65535).into_boxed_slice(),
|
||||
read_count: 0,
|
||||
}
|
||||
}
|
||||
@@ -44,15 +44,10 @@ impl CopyBuffer {
|
||||
R: AsyncMessageStream + ?Sized,
|
||||
W: AsyncMessageStream + ?Sized,
|
||||
{
|
||||
loop {
|
||||
// Make sure we flush any existing messages first.
|
||||
// We don't want to read fast else bandwidth estimators will think we are able
|
||||
// to handle all the messages and start sending even better quality.
|
||||
//if self.need_flush {
|
||||
//ready!(writer.as_mut().poll_flush_message(cx))?;
|
||||
//self.need_flush = false;
|
||||
//}
|
||||
// Check tokio's cooperative budget to prevent task starvation
|
||||
let coop = ready!(tokio::task::coop::poll_proceed(cx));
|
||||
|
||||
loop {
|
||||
let mut did_read = false;
|
||||
let mut did_write = false;
|
||||
let mut read_pending = false;
|
||||
@@ -71,6 +66,7 @@ impl CopyBuffer {
|
||||
self.cache_length = n;
|
||||
did_read = true;
|
||||
self.read_count = self.read_count.wrapping_add(n);
|
||||
coop.made_progress();
|
||||
}
|
||||
}
|
||||
Poll::Pending => {
|
||||
@@ -92,6 +88,7 @@ impl CopyBuffer {
|
||||
// Don't bother writing ping, since we just wrote.
|
||||
self.need_write_ping = false;
|
||||
did_write = true;
|
||||
coop.made_progress();
|
||||
}
|
||||
Poll::Pending => {
|
||||
write_pending = true;
|
||||
@@ -106,6 +103,7 @@ impl CopyBuffer {
|
||||
self.need_write_ping = false;
|
||||
if written {
|
||||
self.need_flush = true;
|
||||
coop.made_progress();
|
||||
}
|
||||
}
|
||||
Poll::Pending => {
|
||||
@@ -121,6 +119,7 @@ impl CopyBuffer {
|
||||
if self.need_flush {
|
||||
ready!(writer.as_mut().poll_flush_message(cx))?;
|
||||
self.need_flush = false;
|
||||
coop.made_progress();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -129,13 +128,8 @@ impl CopyBuffer {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// Previously we kept going until both read and write were pending, but
|
||||
// this might starve other tasks.
|
||||
// Return Pending to prevent task starvation
|
||||
if read_pending || write_pending {
|
||||
// If we got here,
|
||||
// 1) we hit read_pending on the current iteration.
|
||||
// 2) all data has been written successfully
|
||||
// 3) there is no data left to write and we need to read more.
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
@@ -196,7 +190,6 @@ where
|
||||
type Output = io::Result<()>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
// Unpack self into mut refs to each field to avoid borrow check issues.
|
||||
let CopyBidirectional {
|
||||
a,
|
||||
b,
|
||||
|
||||
@@ -1,486 +0,0 @@
|
||||
use futures::ready;
|
||||
use tokio::io::ReadBuf;
|
||||
|
||||
use std::future::Future;
|
||||
use std::io;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::address::NetLocation;
|
||||
use crate::async_stream::{AsyncSourcedMessageStream, AsyncTargetedMessageStream};
|
||||
|
||||
// Informed by https://stackoverflow.com/questions/14856639/udp-hole-punching-timeout
|
||||
pub const DEFAULT_ASSOCIATION_TIMEOUT_SECS: u32 = 200;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CopyProxyBuffer {
|
||||
read_done: bool,
|
||||
need_flush: bool,
|
||||
need_write_ping: bool,
|
||||
cache_length: usize,
|
||||
buf: [u8; 65535],
|
||||
target: NetLocation,
|
||||
read_count: usize,
|
||||
write_count: usize,
|
||||
}
|
||||
|
||||
impl CopyProxyBuffer {
|
||||
pub fn new(need_flush: bool) -> Self {
|
||||
Self {
|
||||
read_done: false,
|
||||
need_flush,
|
||||
need_write_ping: false,
|
||||
cache_length: 0,
|
||||
buf: [0u8; 65535],
|
||||
target: NetLocation::UNSPECIFIED,
|
||||
read_count: 0,
|
||||
write_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_copy_targeted_messages<R, W>(
|
||||
&mut self,
|
||||
cx: &mut Context<'_>,
|
||||
mut reader: Pin<&mut R>,
|
||||
mut writer: Pin<&mut W>,
|
||||
) -> Poll<io::Result<()>>
|
||||
where
|
||||
R: AsyncTargetedMessageStream + ?Sized,
|
||||
W: AsyncSourcedMessageStream + ?Sized,
|
||||
{
|
||||
loop {
|
||||
// Make sure we flush any existing messages first.
|
||||
// We don't want to read fast else bandwidth estimators will think we are able
|
||||
// to handle all the messages and start sending even better quality.
|
||||
//if self.need_flush {
|
||||
//ready!(writer.as_mut().poll_flush_message(cx))?;
|
||||
//self.need_flush = false;
|
||||
//}
|
||||
|
||||
let mut did_read = false;
|
||||
let mut did_write = false;
|
||||
let mut read_pending = false;
|
||||
let mut write_pending = false;
|
||||
|
||||
if !self.read_done && self.cache_length == 0 {
|
||||
let me = &mut *self;
|
||||
let mut buf = ReadBuf::new(&mut me.buf);
|
||||
match reader.as_mut().poll_read_targeted_message(cx, &mut buf) {
|
||||
Poll::Ready(val) => {
|
||||
let target = val?;
|
||||
let n = buf.filled().len();
|
||||
if n == 0 {
|
||||
self.read_done = true;
|
||||
} else {
|
||||
self.target = target;
|
||||
self.cache_length = n;
|
||||
did_read = true;
|
||||
self.read_count = self.read_count.wrapping_add(n);
|
||||
}
|
||||
}
|
||||
Poll::Pending => {
|
||||
read_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.cache_length > 0 {
|
||||
let me = &mut *self;
|
||||
match writer.as_mut().poll_write_targeted_message(
|
||||
cx,
|
||||
&me.buf[0..me.cache_length],
|
||||
&me.target,
|
||||
) {
|
||||
Poll::Ready(val) => {
|
||||
// TODO: check that n == cache length
|
||||
val?;
|
||||
self.write_count = self.write_count.wrapping_add(1);
|
||||
self.cache_length = 0;
|
||||
self.need_flush = true;
|
||||
// Don't bother writing ping, since we just wrote.
|
||||
self.need_write_ping = false;
|
||||
did_write = true;
|
||||
}
|
||||
Poll::Pending => {
|
||||
write_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !write_pending && self.need_write_ping {
|
||||
match writer.as_mut().poll_write_ping(cx) {
|
||||
Poll::Ready(val) => {
|
||||
let written = val?;
|
||||
self.need_write_ping = false;
|
||||
if written {
|
||||
self.need_flush = true;
|
||||
}
|
||||
}
|
||||
Poll::Pending => {
|
||||
write_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if did_read && did_write && !read_pending && !write_pending {
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.need_flush {
|
||||
ready!(writer.as_mut().poll_flush_message(cx))?;
|
||||
self.need_flush = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we've written all the data and we've seen EOF, finish the transfer.
|
||||
if self.read_done && self.cache_length == 0 {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// Previously we kept going until both read and write were pending, but
|
||||
// this might starve other tasks.
|
||||
if read_pending || write_pending {
|
||||
// If we got here,
|
||||
// 1) we hit read_pending on the current iteration.
|
||||
// 2) all data has been written successfully
|
||||
// 3) there is no data left to write and we need to read more.
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CopyManyBuffer {
|
||||
read_done: bool,
|
||||
need_flush: bool,
|
||||
need_write_ping: bool,
|
||||
cache_length: usize,
|
||||
buf: [u8; 65535],
|
||||
source: SocketAddr,
|
||||
read_count: usize,
|
||||
write_count: usize,
|
||||
}
|
||||
|
||||
impl CopyManyBuffer {
|
||||
pub fn new(need_flush: bool) -> Self {
|
||||
Self {
|
||||
read_done: false,
|
||||
need_flush,
|
||||
need_write_ping: false,
|
||||
cache_length: 0,
|
||||
buf: [0u8; 65535],
|
||||
source: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
|
||||
read_count: 0,
|
||||
write_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_copy_sourced_messages<R, W>(
|
||||
&mut self,
|
||||
cx: &mut Context<'_>,
|
||||
mut reader: Pin<&mut R>,
|
||||
mut writer: Pin<&mut W>,
|
||||
) -> Poll<io::Result<()>>
|
||||
where
|
||||
R: AsyncSourcedMessageStream + ?Sized,
|
||||
W: AsyncTargetedMessageStream + ?Sized,
|
||||
{
|
||||
loop {
|
||||
// Make sure we flush any existing messages first.
|
||||
// We don't want to read fast else bandwidth estimators will think we are able
|
||||
// to handle all the messages and start sending even better quality.
|
||||
//if self.need_flush {
|
||||
//ready!(writer.as_mut().poll_flush_message(cx))?;
|
||||
//self.need_flush = false;
|
||||
//}
|
||||
|
||||
let mut did_read = false;
|
||||
let mut did_write = false;
|
||||
let mut read_pending = false;
|
||||
let mut write_pending = false;
|
||||
|
||||
if !self.read_done && self.cache_length == 0 {
|
||||
let me = &mut *self;
|
||||
let mut buf = ReadBuf::new(&mut me.buf);
|
||||
match reader.as_mut().poll_read_sourced_message(cx, &mut buf) {
|
||||
Poll::Ready(val) => {
|
||||
let source = val?;
|
||||
let n = buf.filled().len();
|
||||
if n == 0 {
|
||||
self.read_done = true;
|
||||
} else {
|
||||
self.source = source;
|
||||
self.cache_length = n;
|
||||
did_read = true;
|
||||
self.read_count = self.read_count.wrapping_add(n);
|
||||
}
|
||||
}
|
||||
Poll::Pending => {
|
||||
read_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.cache_length > 0 {
|
||||
let me = &mut *self;
|
||||
match writer.as_mut().poll_write_sourced_message(
|
||||
cx,
|
||||
&me.buf[0..me.cache_length],
|
||||
&me.source,
|
||||
) {
|
||||
Poll::Ready(val) => {
|
||||
val?;
|
||||
self.write_count = self.write_count.wrapping_add(1);
|
||||
self.cache_length = 0;
|
||||
self.need_flush = true;
|
||||
// Don't bother writing ping, since we just wrote.
|
||||
self.need_write_ping = false;
|
||||
did_write = true;
|
||||
}
|
||||
Poll::Pending => {
|
||||
write_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !write_pending && self.need_write_ping {
|
||||
match writer.as_mut().poll_write_ping(cx) {
|
||||
Poll::Ready(val) => {
|
||||
let written = val?;
|
||||
self.need_write_ping = false;
|
||||
if written {
|
||||
self.need_flush = true;
|
||||
}
|
||||
}
|
||||
Poll::Pending => {
|
||||
write_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if did_read && did_write && !read_pending && !write_pending {
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.need_flush {
|
||||
ready!(writer.as_mut().poll_flush_message(cx))?;
|
||||
self.need_flush = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we've written all the data and we've seen EOF, finish the transfer.
|
||||
if self.read_done && self.cache_length == 0 {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// Previously we kept going until both read and write were pending, but
|
||||
// this might starve other tasks.
|
||||
if read_pending || write_pending {
|
||||
// If we got here,
|
||||
// 1) we hit read_pending on the current iteration.
|
||||
// 2) all data has been written successfully
|
||||
// 3) there is no data left to write and we need to read more.
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TransferState {
|
||||
Running,
|
||||
ShuttingDown,
|
||||
Done,
|
||||
}
|
||||
|
||||
struct CopyMultidirectional<'a, A: ?Sized, B: ?Sized> {
|
||||
a: &'a mut A,
|
||||
b: &'a mut B,
|
||||
a_buf: CopyProxyBuffer,
|
||||
b_buf: CopyManyBuffer,
|
||||
a_to_b: TransferState,
|
||||
b_to_a: TransferState,
|
||||
sleep_future: Pin<Box<tokio::time::Sleep>>,
|
||||
a_last_active: Instant,
|
||||
b_last_active: Instant,
|
||||
}
|
||||
|
||||
fn transfer_targeted_messages<A, B>(
|
||||
cx: &mut Context<'_>,
|
||||
state: &mut TransferState,
|
||||
buf: &mut CopyProxyBuffer,
|
||||
r: &mut A,
|
||||
w: &mut B,
|
||||
) -> Poll<io::Result<()>>
|
||||
where
|
||||
A: AsyncTargetedMessageStream + ?Sized,
|
||||
B: AsyncSourcedMessageStream + ?Sized,
|
||||
{
|
||||
let mut r = Pin::new(r);
|
||||
let mut w = Pin::new(w);
|
||||
|
||||
loop {
|
||||
match state {
|
||||
TransferState::Running => {
|
||||
ready!(buf.poll_copy_targeted_messages(cx, r.as_mut(), w.as_mut()))?;
|
||||
*state = TransferState::ShuttingDown;
|
||||
}
|
||||
TransferState::ShuttingDown => {
|
||||
ready!(w.as_mut().poll_shutdown_message(cx))?;
|
||||
*state = TransferState::Done;
|
||||
}
|
||||
TransferState::Done => return Poll::Ready(Ok(())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn transfer_sourced_messages<A, B>(
|
||||
cx: &mut Context<'_>,
|
||||
state: &mut TransferState,
|
||||
buf: &mut CopyManyBuffer,
|
||||
r: &mut A,
|
||||
w: &mut B,
|
||||
) -> Poll<io::Result<()>>
|
||||
where
|
||||
A: AsyncSourcedMessageStream + ?Sized,
|
||||
B: AsyncTargetedMessageStream + ?Sized,
|
||||
{
|
||||
let mut r = Pin::new(r);
|
||||
let mut w = Pin::new(w);
|
||||
|
||||
loop {
|
||||
match state {
|
||||
TransferState::Running => {
|
||||
ready!(buf.poll_copy_sourced_messages(cx, r.as_mut(), w.as_mut()))?;
|
||||
*state = TransferState::ShuttingDown;
|
||||
}
|
||||
TransferState::ShuttingDown => {
|
||||
ready!(w.as_mut().poll_shutdown_message(cx))?;
|
||||
*state = TransferState::Done;
|
||||
}
|
||||
TransferState::Done => return Poll::Ready(Ok(())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B> Future for CopyMultidirectional<'_, A, B>
|
||||
where
|
||||
A: AsyncTargetedMessageStream + ?Sized,
|
||||
B: AsyncSourcedMessageStream + ?Sized,
|
||||
{
|
||||
type Output = io::Result<()>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
// Unpack self into mut refs to each field to avoid borrow check issues.
|
||||
let CopyMultidirectional {
|
||||
a,
|
||||
b,
|
||||
a_buf,
|
||||
b_buf,
|
||||
a_to_b,
|
||||
b_to_a,
|
||||
sleep_future,
|
||||
a_last_active,
|
||||
b_last_active,
|
||||
} = &mut *self;
|
||||
|
||||
let ping_fired = sleep_future.as_mut().poll(cx).is_ready();
|
||||
if ping_fired {
|
||||
// a_buf writes to b - so we need to check if b supports ping, and similarly
|
||||
// for b_buf.
|
||||
a_buf.need_write_ping = b.supports_ping();
|
||||
b_buf.need_write_ping = a.supports_ping();
|
||||
sleep_future
|
||||
.as_mut()
|
||||
.reset(tokio::time::Instant::now() + std::time::Duration::from_secs(60));
|
||||
}
|
||||
|
||||
let a_read_count = a_buf.read_count;
|
||||
let a_write_count = a_buf.write_count;
|
||||
let b_read_count = b_buf.read_count;
|
||||
let b_write_count = b_buf.write_count;
|
||||
|
||||
let a_to_b = transfer_targeted_messages(cx, a_to_b, &mut *a_buf, &mut *a, &mut *b);
|
||||
let b_to_a = transfer_sourced_messages(cx, b_to_a, &mut *b_buf, &mut *b, &mut *a);
|
||||
|
||||
if a_buf.read_count != a_read_count || a_buf.write_count != a_write_count {
|
||||
*a_last_active = Instant::now();
|
||||
} else if a_last_active.elapsed().as_secs() >= DEFAULT_ASSOCIATION_TIMEOUT_SECS.into() {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
if b_buf.read_count != b_read_count || b_buf.write_count != b_write_count {
|
||||
*b_last_active = Instant::now();
|
||||
} else if b_last_active.elapsed().as_secs() >= DEFAULT_ASSOCIATION_TIMEOUT_SECS.into() {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
if a_to_b.is_ready() {
|
||||
return a_to_b;
|
||||
} else if b_to_a.is_ready() {
|
||||
return b_to_a;
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies data in both directions between `a` and `b`.
|
||||
///
|
||||
/// This function returns a future that will read from both streams,
|
||||
/// writing any data read to the opposing stream.
|
||||
/// This happens in both directions concurrently.
|
||||
///
|
||||
/// If an EOF is observed on one stream, [`shutdown()`] will be invoked on
|
||||
/// the other, and reading from that stream will stop. Copying of data in
|
||||
/// the other direction will continue.
|
||||
///
|
||||
/// The future will complete successfully once both directions of communication has been shut down.
|
||||
/// A direction is shut down when the reader reports EOF,
|
||||
/// at which point [`shutdown()`] is called on the corresponding writer. When finished,
|
||||
/// it will return a tuple of the number of bytes copied from a to b
|
||||
/// and the number of bytes copied from b to a, in that order.
|
||||
///
|
||||
/// [`shutdown()`]: crate::io::AsyncWriteExt::shutdown
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The future will immediately return an error if any IO operation on `a`
|
||||
/// or `b` returns an error. Some data read from either stream may be lost (not
|
||||
/// written to the other stream) in this case.
|
||||
///
|
||||
/// # Return value
|
||||
///
|
||||
/// Returns a tuple of bytes copied `a` to `b` and bytes copied `b` to `a`.
|
||||
pub async fn copy_multidirectional_message<A, B>(
|
||||
a: &mut A,
|
||||
b: &mut B,
|
||||
a_initial_flush: bool,
|
||||
b_initial_flush: bool,
|
||||
) -> Result<(), std::io::Error>
|
||||
where
|
||||
A: AsyncTargetedMessageStream + ?Sized,
|
||||
B: AsyncSourcedMessageStream + ?Sized,
|
||||
{
|
||||
// Unlike tcp copy_bidirectional, we always run a sleep future so that we can expire
|
||||
// connections.
|
||||
let sleep_future = Box::pin(tokio::time::sleep(std::time::Duration::from_secs(60)));
|
||||
|
||||
CopyMultidirectional {
|
||||
a,
|
||||
b,
|
||||
// this is correctly reversed - CopyBuffer will copy from a (reader) to b (writer) using
|
||||
// a_buf, which means that the need_flush signal is for the writer (b), and vice versa for
|
||||
// b_buf.
|
||||
a_buf: CopyProxyBuffer::new(b_initial_flush),
|
||||
b_buf: CopyManyBuffer::new(a_initial_flush),
|
||||
a_to_b: TransferState::Running,
|
||||
b_to_a: TransferState::Running,
|
||||
sleep_future,
|
||||
a_last_active: Instant::now(),
|
||||
b_last_active: Instant::now(),
|
||||
}
|
||||
.await
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
use futures::ready;
|
||||
use tokio::io::ReadBuf;
|
||||
|
||||
use std::future::Future;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::async_stream::AsyncSessionMessageStream;
|
||||
|
||||
// Informed by https://stackoverflow.com/questions/14856639/udp-hole-punching-timeout
|
||||
pub const DEFAULT_ASSOCIATION_TIMEOUT_SECS: u32 = 200;
|
||||
|
||||
/// Buffer for copying messages from one session stream to another
|
||||
#[derive(Debug)]
|
||||
struct CopySessionToSessionBuffer {
|
||||
read_done: bool,
|
||||
need_flush: bool,
|
||||
need_write_ping: bool,
|
||||
cache_length: usize,
|
||||
buf: [u8; 65535],
|
||||
session_id: u16,
|
||||
source_addr: SocketAddr,
|
||||
read_count: usize,
|
||||
write_count: usize,
|
||||
}
|
||||
|
||||
impl CopySessionToSessionBuffer {
|
||||
pub fn new(need_flush: bool) -> Self {
|
||||
Self {
|
||||
read_done: false,
|
||||
need_flush,
|
||||
need_write_ping: false,
|
||||
cache_length: 0,
|
||||
buf: [0u8; 65535],
|
||||
session_id: 0,
|
||||
source_addr: SocketAddr::from(([0, 0, 0, 0], 0)),
|
||||
read_count: 0,
|
||||
write_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_copy_session_to_session<R, W>(
|
||||
&mut self,
|
||||
cx: &mut Context<'_>,
|
||||
mut reader: Pin<&mut R>,
|
||||
mut writer: Pin<&mut W>,
|
||||
) -> Poll<io::Result<()>>
|
||||
where
|
||||
R: AsyncSessionMessageStream + ?Sized,
|
||||
W: AsyncSessionMessageStream + ?Sized,
|
||||
{
|
||||
loop {
|
||||
let mut did_read = false;
|
||||
let mut did_write = false;
|
||||
let mut read_pending = false;
|
||||
let mut write_pending = false;
|
||||
|
||||
// Read from session stream
|
||||
if !self.read_done && self.cache_length == 0 {
|
||||
let me = &mut *self;
|
||||
let mut buf = ReadBuf::new(&mut me.buf);
|
||||
match reader.as_mut().poll_read_session_message(cx, &mut buf) {
|
||||
Poll::Ready(val) => {
|
||||
let (session_id, source_addr) = val?;
|
||||
let n = buf.filled().len();
|
||||
if n == 0 {
|
||||
self.read_done = true;
|
||||
} else {
|
||||
self.session_id = session_id;
|
||||
self.source_addr = source_addr;
|
||||
self.cache_length = n;
|
||||
did_read = true;
|
||||
self.read_count = self.read_count.wrapping_add(n);
|
||||
}
|
||||
}
|
||||
Poll::Pending => {
|
||||
read_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write to session stream
|
||||
if self.cache_length > 0 {
|
||||
let me = &mut *self;
|
||||
match writer.as_mut().poll_write_session_message(
|
||||
cx,
|
||||
me.session_id,
|
||||
&me.buf[0..me.cache_length],
|
||||
&me.source_addr,
|
||||
) {
|
||||
Poll::Ready(val) => {
|
||||
val?;
|
||||
self.write_count = self.write_count.wrapping_add(1);
|
||||
self.cache_length = 0;
|
||||
self.need_flush = true;
|
||||
self.need_write_ping = false;
|
||||
did_write = true;
|
||||
}
|
||||
Poll::Pending => {
|
||||
write_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !write_pending && self.need_write_ping {
|
||||
match writer.as_mut().poll_write_ping(cx) {
|
||||
Poll::Ready(val) => {
|
||||
let written = val?;
|
||||
self.need_write_ping = false;
|
||||
if written {
|
||||
self.need_flush = true;
|
||||
}
|
||||
}
|
||||
Poll::Pending => {
|
||||
write_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if did_read && did_write && !read_pending && !write_pending {
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.need_flush {
|
||||
ready!(writer.as_mut().poll_flush_message(cx))?;
|
||||
self.need_flush = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.read_done && self.cache_length == 0 {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
if read_pending || write_pending {
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TransferState {
|
||||
Running,
|
||||
ShuttingDown,
|
||||
Done,
|
||||
}
|
||||
|
||||
struct CopySessionMessages<'a, A: ?Sized, B: ?Sized> {
|
||||
a: &'a mut A,
|
||||
b: &'a mut B,
|
||||
a_buf: CopySessionToSessionBuffer,
|
||||
b_buf: CopySessionToSessionBuffer,
|
||||
a_to_b: TransferState,
|
||||
b_to_a: TransferState,
|
||||
sleep_future: Pin<Box<tokio::time::Sleep>>,
|
||||
a_last_active: Instant,
|
||||
b_last_active: Instant,
|
||||
}
|
||||
|
||||
fn transfer_session_to_session<A, B>(
|
||||
cx: &mut Context<'_>,
|
||||
state: &mut TransferState,
|
||||
buf: &mut CopySessionToSessionBuffer,
|
||||
r: &mut A,
|
||||
w: &mut B,
|
||||
) -> Poll<io::Result<()>>
|
||||
where
|
||||
A: AsyncSessionMessageStream + ?Sized,
|
||||
B: AsyncSessionMessageStream + ?Sized,
|
||||
{
|
||||
let mut r = Pin::new(r);
|
||||
let mut w = Pin::new(w);
|
||||
|
||||
loop {
|
||||
match state {
|
||||
TransferState::Running => {
|
||||
ready!(buf.poll_copy_session_to_session(cx, r.as_mut(), w.as_mut()))?;
|
||||
*state = TransferState::ShuttingDown;
|
||||
}
|
||||
TransferState::ShuttingDown => {
|
||||
ready!(w.as_mut().poll_shutdown_message(cx))?;
|
||||
*state = TransferState::Done;
|
||||
}
|
||||
TransferState::Done => return Poll::Ready(Ok(())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B> Future for CopySessionMessages<'_, A, B>
|
||||
where
|
||||
A: AsyncSessionMessageStream + ?Sized,
|
||||
B: AsyncSessionMessageStream + ?Sized,
|
||||
{
|
||||
type Output = io::Result<()>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let CopySessionMessages {
|
||||
a,
|
||||
b,
|
||||
a_buf,
|
||||
b_buf,
|
||||
a_to_b,
|
||||
b_to_a,
|
||||
sleep_future,
|
||||
a_last_active,
|
||||
b_last_active,
|
||||
} = &mut *self;
|
||||
|
||||
let ping_fired = sleep_future.as_mut().poll(cx).is_ready();
|
||||
if ping_fired {
|
||||
a_buf.need_write_ping = b.supports_ping();
|
||||
b_buf.need_write_ping = a.supports_ping();
|
||||
sleep_future
|
||||
.as_mut()
|
||||
.reset(tokio::time::Instant::now() + std::time::Duration::from_secs(60));
|
||||
}
|
||||
|
||||
let a_read_count = a_buf.read_count;
|
||||
let a_write_count = a_buf.write_count;
|
||||
let b_read_count = b_buf.read_count;
|
||||
let b_write_count = b_buf.write_count;
|
||||
|
||||
let a_to_b = transfer_session_to_session(cx, a_to_b, &mut *a_buf, &mut *a, &mut *b);
|
||||
let b_to_a = transfer_session_to_session(cx, b_to_a, &mut *b_buf, &mut *b, &mut *a);
|
||||
|
||||
if a_buf.read_count != a_read_count || a_buf.write_count != a_write_count {
|
||||
*a_last_active = Instant::now();
|
||||
} else if a_last_active.elapsed().as_secs() >= DEFAULT_ASSOCIATION_TIMEOUT_SECS.into() {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
if b_buf.read_count != b_read_count || b_buf.write_count != b_write_count {
|
||||
*b_last_active = Instant::now();
|
||||
} else if b_last_active.elapsed().as_secs() >= DEFAULT_ASSOCIATION_TIMEOUT_SECS.into() {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
if a_to_b.is_ready() {
|
||||
return a_to_b;
|
||||
} else if b_to_a.is_ready() {
|
||||
return b_to_a;
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies data in both directions between two session-based streams.
|
||||
///
|
||||
/// This is used for session-based protocols like XUDP where:
|
||||
/// - `a` is the protocol stream (e.g., VlessXudpMessageStream) that reads/writes XUDP frames with session IDs
|
||||
/// - `b` is the UDP manager (e.g., SessionUdpManager) that manages dedicated UDP sockets per session
|
||||
///
|
||||
/// The function returns a future that will:
|
||||
/// - Read from `a` (XUDP frames with session IDs) and write to `b` (create/use UDP sockets for each session)
|
||||
/// - Read from `b` (UDP socket responses with session IDs) and write to `a` (encode as XUDP frames)
|
||||
///
|
||||
/// This happens in both directions concurrently with timeout handling.
|
||||
pub async fn copy_session_messages<A, B>(
|
||||
a: &mut A,
|
||||
b: &mut B,
|
||||
a_initial_flush: bool,
|
||||
b_initial_flush: bool,
|
||||
) -> Result<(), std::io::Error>
|
||||
where
|
||||
A: AsyncSessionMessageStream + ?Sized,
|
||||
B: AsyncSessionMessageStream + ?Sized,
|
||||
{
|
||||
let sleep_future = Box::pin(tokio::time::sleep(std::time::Duration::from_secs(60)));
|
||||
|
||||
CopySessionMessages {
|
||||
a,
|
||||
b,
|
||||
// a_buf copies from a (XUDP reader) to b (UDP manager writer), so need_flush is for b
|
||||
a_buf: CopySessionToSessionBuffer::new(b_initial_flush),
|
||||
// b_buf copies from b (UDP manager reader) to a (XUDP writer), so need_flush is for a
|
||||
b_buf: CopySessionToSessionBuffer::new(a_initial_flush),
|
||||
a_to_b: TransferState::Running,
|
||||
b_to_a: TransferState::Running,
|
||||
sleep_future,
|
||||
a_last_active: Instant::now(),
|
||||
b_last_active: Instant::now(),
|
||||
}
|
||||
.await
|
||||
}
|
||||
@@ -87,6 +87,32 @@ impl CryptoConnection {
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the negotiated ALPN protocol, if any
|
||||
///
|
||||
/// Returns the ALPN protocol that was negotiated during the handshake.
|
||||
/// For REALITY connections, this always returns None since REALITY
|
||||
/// does not perform real TLS negotiation.
|
||||
pub fn alpn_protocol(&self) -> Option<&[u8]> {
|
||||
match self {
|
||||
CryptoConnection::RustlsServer(conn) => conn.alpn_protocol(),
|
||||
CryptoConnection::RustlsClient(conn) => conn.alpn_protocol(),
|
||||
// REALITY doesn't have real TLS ALPN negotiation
|
||||
CryptoConnection::RealityServer(_) => None,
|
||||
CryptoConnection::RealityClient(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this is a REALITY connection
|
||||
///
|
||||
/// REALITY connections have already authenticated the client during
|
||||
/// the handshake, so they can be trusted for protocol handling.
|
||||
pub fn is_reality(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
CryptoConnection::RealityServer(_) | CryptoConnection::RealityClient(_)
|
||||
)
|
||||
}
|
||||
|
||||
/// Read TLS messages from `rd` into internal buffers
|
||||
///
|
||||
/// Returns the number of bytes read, or 0 if the connection is closed.
|
||||
@@ -239,7 +265,7 @@ impl CryptoConnection {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
pub fn feed_crypto_connection(
|
||||
connection: &mut CryptoConnection,
|
||||
data: &[u8],
|
||||
|
||||
@@ -11,7 +11,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::crypto::{CryptoConnection, feed_crypto_connection};
|
||||
use crate::util;
|
||||
use crate::util::{allocate_vec, write_all};
|
||||
|
||||
/// Perform a complete TLS handshake using the provided rustls connection and stream.
|
||||
///
|
||||
@@ -47,6 +47,8 @@ use crate::util;
|
||||
/// perform_crypto_handshake(&mut connection, &mut tcp_stream, 16384).await?;
|
||||
/// // Handshake complete, connection ready for application data
|
||||
/// ```
|
||||
///
|
||||
/// TODO: remove buffer_size parameter, use max tls frame size by default
|
||||
pub async fn perform_crypto_handshake(
|
||||
connection: &mut CryptoConnection,
|
||||
stream: &mut Box<dyn AsyncStream>,
|
||||
@@ -58,7 +60,7 @@ pub async fn perform_crypto_handshake(
|
||||
let mut eof = false;
|
||||
|
||||
// Pre-allocate read buffer once, reused across all iterations
|
||||
let mut read_buf = vec![0u8; buffer_size];
|
||||
let mut read_buf = allocate_vec(buffer_size).into_boxed_slice();
|
||||
|
||||
// Infinite loop - we explicitly break when done
|
||||
// This matches tokio-rustls's structure: loop { ... return match ... }
|
||||
@@ -230,7 +232,7 @@ async fn write_pending_data_no_flush(
|
||||
|
||||
// Send to peer if we have data
|
||||
if !write_buf.is_empty() {
|
||||
util::write_all(stream, &write_buf).await?;
|
||||
write_all(stream, &write_buf).await?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
@@ -300,7 +302,7 @@ async fn try_last_gasp_write(connection: &mut CryptoConnection, stream: &mut Box
|
||||
"TLS: sending last-gasp alert ({} bytes) before closing",
|
||||
alert_buf.len()
|
||||
);
|
||||
let _ = util::write_all(stream, &alert_buf).await;
|
||||
let _ = write_all(stream, &alert_buf).await;
|
||||
let _ = stream.flush().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,22 @@ where
|
||||
(self.io, self.session)
|
||||
}
|
||||
|
||||
/// Get the negotiated ALPN protocol, if any
|
||||
///
|
||||
/// Returns the ALPN protocol that was negotiated during the TLS handshake.
|
||||
/// For REALITY connections, this always returns None.
|
||||
pub fn alpn_protocol(&self) -> Option<&[u8]> {
|
||||
self.session.alpn_protocol()
|
||||
}
|
||||
|
||||
/// Check if this is a REALITY connection
|
||||
///
|
||||
/// REALITY connections have already authenticated the client during
|
||||
/// the handshake, so they can be trusted for protocol handling.
|
||||
pub fn is_reality(&self) -> bool {
|
||||
self.session.is_reality()
|
||||
}
|
||||
|
||||
/// Write TLS data directly to the underlying stream
|
||||
///
|
||||
/// Returns Poll::Ready(Ok(n)) with bytes written, or Poll::Pending if would block.
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
//! Android JNI bindings for shoes TUN service.
|
||||
//!
|
||||
//! This module provides JNI-compatible functions for Android integration.
|
||||
//!
|
||||
//! # JNI Function Naming
|
||||
//!
|
||||
//! JNI function names follow the pattern:
|
||||
//! `Java_<package>_<class>_<method>`
|
||||
//!
|
||||
//! For example, if your Kotlin class is:
|
||||
//! ```kotlin
|
||||
//! package com.shoesproxy
|
||||
//!
|
||||
//! class ShoesNative {
|
||||
//! external fun init(logLevel: String): Int
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! The JNI function would be:
|
||||
//! `Java_com_shoesproxy_ShoesNative_init`
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObject, JString, JValue};
|
||||
use jni::sys::{JNI_FALSE, JNI_TRUE, jboolean, jint, jlong};
|
||||
use log::{LevelFilter, Log, Metadata, Record, error, info};
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::tun::{FnSocketProtector, set_global_socket_protector};
|
||||
|
||||
use super::common::{
|
||||
self, LOG_FILE, LOGGER_INITIALIZED, TUN_SERVICE, TunServiceHandle, flush_log_file,
|
||||
parse_log_level, setup_log_file,
|
||||
};
|
||||
|
||||
/// Android logger that writes to both logcat and file.
|
||||
struct AndroidLogger {
|
||||
level: LevelFilter,
|
||||
}
|
||||
|
||||
impl AndroidLogger {
|
||||
fn new(level: LevelFilter) -> Self {
|
||||
Self { level }
|
||||
}
|
||||
}
|
||||
|
||||
impl Log for AndroidLogger {
|
||||
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||
metadata.level() <= self.level
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
if !self.enabled(record.metadata()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let level = record.level();
|
||||
let target = record.target();
|
||||
let message = format!("{}", record.args());
|
||||
|
||||
// Write to Android logcat
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
use std::ffi::CString;
|
||||
let tag = CString::new("shoes").unwrap_or_default();
|
||||
let msg = CString::new(message.as_str()).unwrap_or_default();
|
||||
let priority = match level {
|
||||
log::Level::Error => 6, // ANDROID_LOG_ERROR
|
||||
log::Level::Warn => 5, // ANDROID_LOG_WARN
|
||||
log::Level::Info => 4, // ANDROID_LOG_INFO
|
||||
log::Level::Debug => 3, // ANDROID_LOG_DEBUG
|
||||
log::Level::Trace => 2, // ANDROID_LOG_VERBOSE
|
||||
};
|
||||
unsafe {
|
||||
ndk_sys::__android_log_write(priority as i32, tag.as_ptr(), msg.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
// Write to log file if configured (with timestamp for Android)
|
||||
if let Some(file_mutex) = LOG_FILE.get() {
|
||||
if let Ok(mut guard) = file_mutex.lock() {
|
||||
if let Some(ref mut file) = *guard {
|
||||
use std::io::Write;
|
||||
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
||||
let _ = writeln!(file, "{} {} [{}] {}", timestamp, level, target, message);
|
||||
let _ = file.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
flush_log_file();
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the shoes library.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `env` - JNI environment
|
||||
/// * `_class` - Java class (unused)
|
||||
/// * `log_level` - Log level string ("error", "warn", "info", "debug", "trace")
|
||||
///
|
||||
/// # Returns
|
||||
/// * 0 on success
|
||||
/// * -1 on error
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_shoesproxy_ShoesNative_init(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
log_level: JString,
|
||||
) -> jint {
|
||||
let level_str: String = match env.get_string(&log_level) {
|
||||
Ok(s) => s.into(),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get log level string: {}", e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let level = parse_log_level(&level_str);
|
||||
|
||||
// Initialize logger (only once)
|
||||
if !LOGGER_INITIALIZED.swap(true, Ordering::SeqCst) {
|
||||
LOG_FILE.get_or_init(|| std::sync::Mutex::new(None));
|
||||
|
||||
let logger = AndroidLogger::new(level);
|
||||
if log::set_boxed_logger(Box::new(logger)).is_ok() {
|
||||
log::set_max_level(level);
|
||||
}
|
||||
}
|
||||
|
||||
info!("shoes initialized with log level: {}", level_str);
|
||||
0
|
||||
}
|
||||
|
||||
/// Get the shoes library version.
|
||||
///
|
||||
/// # Returns
|
||||
/// * Version string from Cargo.toml (e.g., "0.1.0")
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_shoesproxy_ShoesNative_getVersion<'local>(
|
||||
env: JNIEnv<'local>,
|
||||
_class: JClass<'local>,
|
||||
) -> JString<'local> {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
env.new_string(version)
|
||||
.unwrap_or_else(|_| JObject::null().into())
|
||||
}
|
||||
|
||||
/// Set the log file path for file-based logging.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `env` - JNI environment
|
||||
/// * `_class` - Java class (unused)
|
||||
/// * `log_path` - Absolute path to the log file
|
||||
///
|
||||
/// # Returns
|
||||
/// * 0 on success
|
||||
/// * -1 on error
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_shoesproxy_ShoesNative_setLogFile(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
log_path: JString,
|
||||
) -> jint {
|
||||
let path_str: String = match env.get_string(&log_path) {
|
||||
Ok(s) => s.into(),
|
||||
Err(e) => {
|
||||
error!("Failed to get log path string: {}", e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
setup_log_file(&path_str)
|
||||
}
|
||||
|
||||
/// Start the shoes service.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `env` - JNI environment
|
||||
/// * `_class` - Java class (unused)
|
||||
/// * `config_yaml` - YAML configuration string (includes TUN config with device_fd and optional Server configs like mixed)
|
||||
/// * `protect_callback` - Java object with protect(int fd) method
|
||||
///
|
||||
/// # Returns
|
||||
/// * Handle (> 0) on success
|
||||
/// * -1 on error
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_shoesproxy_ShoesNative_start<'local>(
|
||||
mut env: JNIEnv<'local>,
|
||||
_class: JClass<'local>,
|
||||
config_yaml: JString<'local>,
|
||||
protect_callback: JObject<'local>,
|
||||
) -> jlong {
|
||||
info!("Starting shoes service");
|
||||
|
||||
let config_str: String = match env.get_string(&config_yaml) {
|
||||
Ok(s) => s.into(),
|
||||
Err(e) => {
|
||||
error!("Failed to get config string: {}", e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let callback_ref = match env.new_global_ref(protect_callback) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Failed to create global ref for callback: {}", e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let jvm = match env.get_java_vm() {
|
||||
Ok(vm) => Arc::new(vm),
|
||||
Err(e) => {
|
||||
error!("Failed to get JavaVM: {}", e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
// Socket protector calls VpnService.protect() to exempt sockets from VPN routing
|
||||
let callback_ref = Arc::new(callback_ref);
|
||||
let jvm_clone = jvm.clone();
|
||||
let callback_clone = callback_ref.clone();
|
||||
|
||||
let protector = FnSocketProtector::new(move |fd: i32| {
|
||||
let mut env = match jvm_clone.attach_current_thread() {
|
||||
Ok(env) => env,
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Failed to attach to JVM: {}", e),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let result = env.call_method(&*callback_clone, "protect", "(I)Z", &[JValue::Int(fd)]);
|
||||
|
||||
match result {
|
||||
Ok(v) => {
|
||||
if v.z().unwrap_or(false) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"VpnService.protect() returned false",
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(e) => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Failed to call protect(): {}", e),
|
||||
)),
|
||||
}
|
||||
});
|
||||
|
||||
set_global_socket_protector(Arc::new(protector));
|
||||
|
||||
let runtime = match Runtime::new() {
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
error!("Failed to create tokio runtime: {}", e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
let running = Arc::new(std::sync::atomic::AtomicBool::new(true));
|
||||
let running_clone = running.clone();
|
||||
|
||||
runtime.spawn(async move {
|
||||
info!("Shoes service task started");
|
||||
|
||||
match common::start_from_config(&config_str, shutdown_rx).await {
|
||||
Ok(()) => info!("Shoes service stopped normally"),
|
||||
Err(e) => error!("Shoes service error: {}", e),
|
||||
}
|
||||
|
||||
running_clone.store(false, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
let handle = TunServiceHandle {
|
||||
runtime,
|
||||
shutdown_tx: Some(shutdown_tx),
|
||||
running,
|
||||
};
|
||||
|
||||
let service = TUN_SERVICE.get_or_init(|| parking_lot::Mutex::new(None));
|
||||
*service.lock() = Some(handle);
|
||||
|
||||
info!("TUN service started successfully");
|
||||
1
|
||||
}
|
||||
|
||||
/// Stop the TUN service.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `env` - JNI environment
|
||||
/// * `_class` - Java class (unused)
|
||||
/// * `handle` - Handle returned by startTun (currently unused, we use global state)
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_shoesproxy_ShoesNative_stop(
|
||||
_env: JNIEnv,
|
||||
_class: JClass,
|
||||
_handle: jlong,
|
||||
) {
|
||||
common::stop_service();
|
||||
}
|
||||
|
||||
/// Check if the TUN service is running.
|
||||
///
|
||||
/// # Returns
|
||||
/// * JNI_TRUE if running
|
||||
/// * JNI_FALSE if not running
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_shoesproxy_ShoesNative_isRunning(
|
||||
_env: JNIEnv,
|
||||
_class: JClass,
|
||||
) -> jboolean {
|
||||
if common::is_service_running() {
|
||||
JNI_TRUE
|
||||
} else {
|
||||
JNI_FALSE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
//! Common FFI utilities shared between iOS and Android.
|
||||
//!
|
||||
//! This module contains platform-independent code that both iOS and Android use.
|
||||
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use log::{LevelFilter, Log, Metadata, Record, info};
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::config::{Config, convert_cert_paths, create_server_configs, load_config_str};
|
||||
use crate::tcp::tcp_server::start_servers;
|
||||
use crate::tun::run_tun_from_config;
|
||||
|
||||
/// Global log file handle for file-based logging.
|
||||
pub static LOG_FILE: OnceLock<Mutex<Option<File>>> = OnceLock::new();
|
||||
|
||||
/// Global flag to track if logger has been initialized.
|
||||
pub static LOGGER_INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Global state for the TUN service.
|
||||
pub static TUN_SERVICE: OnceLock<parking_lot::Mutex<Option<TunServiceHandle>>> = OnceLock::new();
|
||||
|
||||
/// Global flag to track initialization.
|
||||
pub static INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Handle to a running TUN service.
|
||||
pub struct TunServiceHandle {
|
||||
/// Tokio runtime running the service.
|
||||
pub runtime: tokio::runtime::Runtime,
|
||||
/// Channel to signal shutdown.
|
||||
pub shutdown_tx: Option<oneshot::Sender<()>>,
|
||||
/// Flag indicating if service is running.
|
||||
pub running: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
/// Parse log level string to LevelFilter.
|
||||
pub fn parse_log_level(level_str: &str) -> LevelFilter {
|
||||
match level_str.to_lowercase().as_str() {
|
||||
"error" => LevelFilter::Error,
|
||||
"warn" => LevelFilter::Warn,
|
||||
"info" => LevelFilter::Info,
|
||||
"debug" => LevelFilter::Debug,
|
||||
"trace" => LevelFilter::Trace,
|
||||
_ => LevelFilter::Info,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set up log file for file-based logging.
|
||||
///
|
||||
/// Returns 0 on success, -1 on error.
|
||||
pub fn setup_log_file(path_str: &str) -> i32 {
|
||||
let file_mutex = LOG_FILE.get_or_init(|| Mutex::new(None));
|
||||
|
||||
match OpenOptions::new().create(true).append(true).open(path_str) {
|
||||
Ok(file) => {
|
||||
let result = if let Ok(mut guard) = file_mutex.lock() {
|
||||
*guard = Some(file);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if result {
|
||||
info!("Log file set to: {}", path_str);
|
||||
0
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
Err(_) => -1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a log message to the log file if configured.
|
||||
pub fn write_to_log_file(level: log::Level, target: &str, message: &str) {
|
||||
if let Some(file_mutex) = LOG_FILE.get() {
|
||||
if let Ok(mut guard) = file_mutex.lock() {
|
||||
if let Some(ref mut file) = *guard {
|
||||
let _ = writeln!(file, "{} [{}] {}", level, target, message);
|
||||
let _ = file.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Flush the log file.
|
||||
pub fn flush_log_file() {
|
||||
if let Some(file_mutex) = LOG_FILE.get() {
|
||||
if let Ok(mut guard) = file_mutex.lock() {
|
||||
if let Some(ref mut file) = *guard {
|
||||
let _ = file.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Base logger that handles file logging.
|
||||
/// Platform-specific loggers can wrap this and add their own output.
|
||||
pub struct FileLogger {
|
||||
level: LevelFilter,
|
||||
}
|
||||
|
||||
impl FileLogger {
|
||||
pub fn new(level: LevelFilter) -> Self {
|
||||
Self { level }
|
||||
}
|
||||
}
|
||||
|
||||
impl Log for FileLogger {
|
||||
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||
metadata.level() <= self.level
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
write_to_log_file(
|
||||
record.level(),
|
||||
record.target(),
|
||||
&format!("{}", record.args()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
flush_log_file();
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop the TUN service and wait for shutdown.
|
||||
///
|
||||
/// This is the common shutdown logic used by both iOS and Android.
|
||||
pub fn stop_service() {
|
||||
info!("Stopping TUN service");
|
||||
|
||||
let handle = if let Some(service) = TUN_SERVICE.get() {
|
||||
service.lock().take()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(mut handle) = handle {
|
||||
if let Some(tx) = handle.shutdown_tx.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
|
||||
// Wait up to 5 seconds for service to stop
|
||||
let running = handle.running.clone();
|
||||
for i in 0..50 {
|
||||
if !running.load(Ordering::SeqCst) {
|
||||
info!("TUN service stopped after {}ms", i * 100);
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
drop(handle.runtime);
|
||||
info!("TUN runtime dropped");
|
||||
}
|
||||
|
||||
info!("TUN service stop completed");
|
||||
}
|
||||
|
||||
/// Check if the TUN service is running.
|
||||
pub fn is_service_running() -> bool {
|
||||
if let Some(service) = TUN_SERVICE.get() {
|
||||
let guard = service.lock();
|
||||
if let Some(ref handle) = *guard {
|
||||
return handle.running.load(Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Start the service from a config YAML string.
|
||||
///
|
||||
/// This parses the config YAML and starts both TUN and any Server configs
|
||||
/// (like mixed HTTP+SOCKS5 servers) that are defined in the config.
|
||||
/// The config YAML must already have device_fd set in the TUN config.
|
||||
pub async fn start_from_config(
|
||||
config_yaml: &str,
|
||||
shutdown_rx: oneshot::Receiver<()>,
|
||||
) -> std::io::Result<()> {
|
||||
info!("Parsing config for TUN server");
|
||||
|
||||
let configs: Vec<Config> = load_config_str(config_yaml)?;
|
||||
|
||||
let (configs, pem_count) = convert_cert_paths(configs).await?;
|
||||
if pem_count > 0 {
|
||||
info!("Loaded {} PEM files", pem_count);
|
||||
}
|
||||
|
||||
let validated_configs = create_server_configs(configs).await?;
|
||||
|
||||
// Separate TUN config from server configs, validate exactly one TUN with device_fd
|
||||
let mut tun_config = None;
|
||||
let mut server_configs = Vec::new();
|
||||
|
||||
for config in validated_configs {
|
||||
match config {
|
||||
Config::TunServer(tc) => {
|
||||
if tun_config.is_some() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Multiple TUN configs found - only one is allowed for mobile",
|
||||
));
|
||||
}
|
||||
if tc.device_fd.is_none() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"TUN config missing device_fd - must be injected by caller",
|
||||
));
|
||||
}
|
||||
info!(
|
||||
"TUN config: fd={}, mtu={}, tcp={}, udp={}, icmp={}",
|
||||
tc.device_fd.unwrap(),
|
||||
tc.mtu,
|
||||
tc.tcp_enabled,
|
||||
tc.udp_enabled,
|
||||
tc.icmp_enabled
|
||||
);
|
||||
tun_config = Some(tc);
|
||||
}
|
||||
Config::Server(sc) => {
|
||||
server_configs.push(sc);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let tun_config = tun_config.ok_or_else(|| {
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidData, "No TUN config found")
|
||||
})?;
|
||||
|
||||
// Start TCP servers (like mixed)
|
||||
let mut join_handles: Vec<JoinHandle<()>> = Vec::new();
|
||||
|
||||
for server_config in server_configs {
|
||||
join_handles.extend(start_servers(Config::Server(server_config)).await?);
|
||||
}
|
||||
|
||||
// Run TUN server (blocks until shutdown). close_fd_on_drop = false because mobile owns the FD
|
||||
let result = run_tun_from_config(tun_config, shutdown_rx, false).await;
|
||||
|
||||
// Cleanup any servers when TUN stops
|
||||
for handle in join_handles {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
//! iOS FFI bindings for shoes.
|
||||
//!
|
||||
//! This module provides C-compatible functions that can be called from Swift
|
||||
//! via the iOS Network Extension (PacketTunnelProvider).
|
||||
//!
|
||||
//! # Usage from Swift
|
||||
//!
|
||||
//! ```swift
|
||||
//! // Initialize logging
|
||||
//! shoes_init("info")
|
||||
//!
|
||||
//! // Set log file (optional, for debugging)
|
||||
//! shoes_set_log_file("/path/to/log.txt")
|
||||
//!
|
||||
//! // Start VPN with config YAML and packet tunnel fd
|
||||
//! let handle = shoes_start(configYaml, protectCallback)
|
||||
//!
|
||||
//! // Stop VPN
|
||||
//! shoes_stop(handle)
|
||||
//! ```
|
||||
|
||||
use std::ffi::{CStr, c_char, c_int, c_long};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use log::{Level, LevelFilter, Log, Metadata, Record, error, info};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use super::common::{
|
||||
self, INITIALIZED, LOG_FILE, LOGGER_INITIALIZED, TUN_SERVICE, TunServiceHandle, flush_log_file,
|
||||
parse_log_level, setup_log_file, write_to_log_file,
|
||||
};
|
||||
|
||||
/// Socket protector callback type.
|
||||
/// Called from Rust to protect sockets from VPN routing.
|
||||
/// The callback receives a file descriptor and should return true if protected successfully.
|
||||
pub type ProtectSocketCallback = extern "C" fn(fd: c_int) -> bool;
|
||||
|
||||
/// Global socket protector callback.
|
||||
static PROTECT_CALLBACK: OnceLock<Mutex<Option<ProtectSocketCallback>>> = OnceLock::new();
|
||||
|
||||
/// iOS logger that writes to both file and stderr (captured by Console.app).
|
||||
struct IosLogger {
|
||||
level: LevelFilter,
|
||||
}
|
||||
|
||||
impl IosLogger {
|
||||
fn new(level: LevelFilter) -> Self {
|
||||
Self { level }
|
||||
}
|
||||
}
|
||||
|
||||
impl Log for IosLogger {
|
||||
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||
metadata.level() <= self.level
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
if !self.enabled(record.metadata()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let level = record.level();
|
||||
let target = record.target();
|
||||
let message = format!("{}", record.args());
|
||||
|
||||
// Write to log file if configured
|
||||
write_to_log_file(level, target, &message);
|
||||
|
||||
// Write to stderr (captured by Console.app on iOS)
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
let level_str = match level {
|
||||
Level::Error => "ERROR",
|
||||
Level::Warn => "WARN",
|
||||
Level::Info => "INFO",
|
||||
Level::Debug => "DEBUG",
|
||||
Level::Trace => "TRACE",
|
||||
};
|
||||
eprintln!("[shoes] {} [{}] {}", level_str, target, message);
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
flush_log_file();
|
||||
}
|
||||
}
|
||||
|
||||
/// Socket protector implementation for iOS.
|
||||
struct IosSocketProtector;
|
||||
|
||||
impl crate::tun::SocketProtector for IosSocketProtector {
|
||||
fn protect(&self, fd: std::os::unix::io::RawFd) -> std::io::Result<()> {
|
||||
let callback_guard = PROTECT_CALLBACK.get_or_init(|| Mutex::new(None)).lock();
|
||||
|
||||
if let Some(callback) = *callback_guard {
|
||||
if callback(fd as c_int) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Socket protection failed",
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the shoes library.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `log_level` - Log level string: "error", "warn", "info", "debug", "trace"
|
||||
///
|
||||
/// # Returns
|
||||
/// * 0 on success
|
||||
/// * -1 on error
|
||||
///
|
||||
/// # Safety
|
||||
/// `log_level` must be a valid null-terminated C string.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn shoes_init(log_level: *const c_char) -> c_int {
|
||||
if INITIALIZED.swap(true, Ordering::SeqCst) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let level_str = if log_level.is_null() {
|
||||
"info"
|
||||
} else {
|
||||
match CStr::from_ptr(log_level).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => "info",
|
||||
}
|
||||
};
|
||||
|
||||
let filter = parse_log_level(level_str);
|
||||
|
||||
if !LOGGER_INITIALIZED.swap(true, Ordering::SeqCst) {
|
||||
LOG_FILE.get_or_init(|| std::sync::Mutex::new(None));
|
||||
|
||||
let logger = IosLogger::new(filter);
|
||||
if log::set_boxed_logger(Box::new(logger)).is_ok() {
|
||||
log::set_max_level(filter);
|
||||
}
|
||||
}
|
||||
|
||||
TUN_SERVICE.get_or_init(|| Mutex::new(None));
|
||||
PROTECT_CALLBACK.get_or_init(|| Mutex::new(None));
|
||||
|
||||
info!("shoes iOS initialized with log level: {}", level_str);
|
||||
0
|
||||
}
|
||||
|
||||
/// Start the shoes VPN service.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `config_yaml` - YAML configuration string (must include device_fd in TUN config)
|
||||
/// * `protect_callback` - Callback function to protect sockets from VPN routing
|
||||
///
|
||||
/// # Returns
|
||||
/// * Handle (> 0) on success
|
||||
/// * -1 on error
|
||||
///
|
||||
/// # Safety
|
||||
/// `config_yaml` must be a valid null-terminated C string.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn shoes_start(
|
||||
config_yaml: *const c_char,
|
||||
protect_callback: ProtectSocketCallback,
|
||||
) -> c_long {
|
||||
if config_yaml.is_null() {
|
||||
error!("shoes_start: config_yaml is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let config_str = match CStr::from_ptr(config_yaml).to_str() {
|
||||
Ok(s) => s.to_string(),
|
||||
Err(e) => {
|
||||
error!("shoes_start: invalid UTF-8 in config_yaml: {}", e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
info!("shoes_start: config length = {} bytes", config_str.len());
|
||||
|
||||
{
|
||||
let mut callback_guard = PROTECT_CALLBACK.get_or_init(|| Mutex::new(None)).lock();
|
||||
*callback_guard = Some(protect_callback);
|
||||
}
|
||||
|
||||
crate::tun::set_global_socket_protector(Arc::new(IosSocketProtector));
|
||||
|
||||
let runtime = match tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.worker_threads(2)
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
error!("shoes_start: failed to create runtime: {}", e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
let running = Arc::new(std::sync::atomic::AtomicBool::new(true));
|
||||
let running_clone = running.clone();
|
||||
|
||||
runtime.spawn(async move {
|
||||
match common::start_from_config(&config_str, shutdown_rx).await {
|
||||
Ok(()) => info!("shoes service stopped normally"),
|
||||
Err(e) => error!("shoes service error: {}", e),
|
||||
}
|
||||
running_clone.store(false, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
let handle = TunServiceHandle {
|
||||
shutdown_tx: Some(shutdown_tx),
|
||||
running,
|
||||
runtime,
|
||||
};
|
||||
|
||||
let mut guard = TUN_SERVICE.get().unwrap().lock();
|
||||
*guard = Some(handle);
|
||||
|
||||
1
|
||||
}
|
||||
|
||||
/// Stop the shoes VPN service.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `handle` - Handle returned by shoes_start (currently unused, we use global state)
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn shoes_stop(_handle: c_long) {
|
||||
common::stop_service();
|
||||
|
||||
if let Some(callback) = PROTECT_CALLBACK.get() {
|
||||
let mut guard = callback.lock();
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the shoes service is running.
|
||||
///
|
||||
/// # Returns
|
||||
/// * true if running
|
||||
/// * false if not running
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn shoes_is_running() -> bool {
|
||||
common::is_service_running()
|
||||
}
|
||||
|
||||
/// Get the shoes library version.
|
||||
///
|
||||
/// # Returns
|
||||
/// A static string containing the version. Do not free this pointer.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn shoes_get_version() -> *const c_char {
|
||||
concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
|
||||
}
|
||||
|
||||
/// Set the log file path for file-based logging.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `path` - Absolute path to the log file
|
||||
///
|
||||
/// # Returns
|
||||
/// * 0 on success
|
||||
/// * -1 on error
|
||||
///
|
||||
/// # Safety
|
||||
/// `path` must be a valid null-terminated C string.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn shoes_set_log_file(path: *const c_char) -> c_int {
|
||||
if path.is_null() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let path_str = match CStr::from_ptr(path).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return -1,
|
||||
};
|
||||
|
||||
setup_log_file(path_str)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//! FFI (Foreign Function Interface) for mobile platforms.
|
||||
//!
|
||||
//! This module provides C-compatible functions that can be called from
|
||||
//! Android (via JNI) and iOS (via Swift/ObjC).
|
||||
//!
|
||||
//! # Android Usage
|
||||
//!
|
||||
//! ```kotlin
|
||||
//! // Load native library
|
||||
//! System.loadLibrary("shoes")
|
||||
//!
|
||||
//! // Declare native methods
|
||||
//! external fun shoesInit(logLevel: String): Int
|
||||
//! external fun shoesStartTun(
|
||||
//! fd: Int,
|
||||
//! configYaml: String,
|
||||
//! protectCallback: (Int) -> Boolean
|
||||
//! ): Long
|
||||
//! external fun shoesStop(handle: Long)
|
||||
//! ```
|
||||
//!
|
||||
//! # iOS Usage
|
||||
//!
|
||||
//! ```swift
|
||||
//! // Initialize
|
||||
//! shoes_init("info")
|
||||
//!
|
||||
//! // Start VPN
|
||||
//! let handle = shoes_start(configYaml, protectCallback)
|
||||
//!
|
||||
//! // Stop VPN
|
||||
//! shoes_stop(handle)
|
||||
//! ```
|
||||
//!
|
||||
//! # Thread Safety
|
||||
//!
|
||||
//! - `shoes_init` must be called once before any other function
|
||||
//! - `shoes_start` / `shoes_start_tun` starts a background thread for the TUN service
|
||||
//! - `shoes_stop` signals shutdown and waits for cleanup
|
||||
|
||||
// Common utilities shared between iOS and Android
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
mod common;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod android;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub use android::*;
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
mod ios;
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
pub use ios::*;
|
||||
|
||||
// Re-export for non-mobile platforms (stub implementations)
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
mod stub;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use stub::*;
|
||||
@@ -0,0 +1,10 @@
|
||||
//! Stub FFI implementations for non-Android platforms.
|
||||
//!
|
||||
//! These are placeholder implementations that allow the code to compile
|
||||
//! on desktop platforms for testing purposes.
|
||||
|
||||
/// Stub: shoes is not compiled for Android on this platform.
|
||||
pub fn stub_warning() {
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!("Warning: FFI functions are stubs on this platform. Compile for Android to use.");
|
||||
}
|
||||
+246
-214
@@ -1,3 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
use log::debug;
|
||||
@@ -5,9 +7,9 @@ use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::address::{Address, NetLocation};
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::option_util::NoneOrOne;
|
||||
use crate::client_proxy_selector::ClientProxySelector;
|
||||
use crate::stream_reader::StreamReader;
|
||||
use crate::tcp_handler::{
|
||||
use crate::tcp::tcp_handler::{
|
||||
TcpClientHandler, TcpClientSetupResult, TcpServerHandler, TcpServerSetupResult,
|
||||
};
|
||||
|
||||
@@ -22,16 +24,23 @@ fn create_http_auth_token(username: &str, password: &str) -> String {
|
||||
#[derive(Debug)]
|
||||
pub struct HttpTcpServerHandler {
|
||||
auth_token: Option<String>,
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
}
|
||||
|
||||
unsafe impl Send for HttpTcpServerHandler {}
|
||||
unsafe impl Sync for HttpTcpServerHandler {}
|
||||
|
||||
impl HttpTcpServerHandler {
|
||||
pub fn new(auth_credentials: Option<(String, String)>) -> Self {
|
||||
pub fn new(
|
||||
auth_credentials: Option<(String, String)>,
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
) -> Self {
|
||||
let auth_token = auth_credentials
|
||||
.map(|(username, password)| create_http_auth_token(&username, &password));
|
||||
Self { auth_token }
|
||||
Self {
|
||||
auth_token,
|
||||
proxy_selector,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,246 +48,269 @@ impl HttpTcpServerHandler {
|
||||
impl TcpServerHandler for HttpTcpServerHandler {
|
||||
async fn setup_server_stream(
|
||||
&self,
|
||||
mut server_stream: Box<dyn AsyncStream>,
|
||||
server_stream: Box<dyn AsyncStream>,
|
||||
) -> std::io::Result<TcpServerSetupResult> {
|
||||
let mut stream_reader = StreamReader::new();
|
||||
let stream_reader = StreamReader::new();
|
||||
setup_http_server_stream_inner(
|
||||
self.auth_token.as_deref(),
|
||||
server_stream,
|
||||
stream_reader,
|
||||
self.proxy_selector.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
let line = stream_reader.read_line(&mut server_stream).await?;
|
||||
if !line.ends_with(" HTTP/1.0") && !line.ends_with(" HTTP/1.1") {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Unrecognized http request: {line}"),
|
||||
));
|
||||
}
|
||||
/// Core HTTP proxy server setup logic.
|
||||
/// Can be called from HttpTcpServerHandler or MixedTcpServerHandler.
|
||||
///
|
||||
/// Takes ownership of `server_stream` and returns it in the result.
|
||||
pub async fn setup_http_server_stream_inner(
|
||||
auth_token: Option<&str>,
|
||||
mut server_stream: Box<dyn AsyncStream>,
|
||||
mut stream_reader: StreamReader,
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
) -> std::io::Result<TcpServerSetupResult> {
|
||||
let line = stream_reader.read_line(&mut server_stream).await?;
|
||||
if !line.ends_with(" HTTP/1.0") && !line.ends_with(" HTTP/1.1") {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Unrecognized http request: {line}"),
|
||||
));
|
||||
}
|
||||
|
||||
let http_version = line[line.len() - 8..].to_string();
|
||||
let (remote_location, connection_success_response, initial_remote_data, need_initial_flush) =
|
||||
if line.starts_with("CONNECT ") {
|
||||
let address = &line[8..line.len() - 9];
|
||||
// GET = 3 (smaller than CONNECT)
|
||||
// HTTP/1.1 = 8
|
||||
// min address a.ab = 4
|
||||
// port 1
|
||||
// 3 spaces
|
||||
// total = 19
|
||||
if line.len() < 19 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Invalid http request: {line}"),
|
||||
));
|
||||
}
|
||||
|
||||
let separator_index = address.find(':').ok_or_else(|| {
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid address format")
|
||||
})?;
|
||||
let http_version = line[line.len() - 8..].to_string();
|
||||
let (remote_location, connection_success_response, initial_remote_data, need_initial_flush) =
|
||||
if line.starts_with("CONNECT ") {
|
||||
let address = &line[8..line.len() - 9];
|
||||
|
||||
if address.len() <= separator_index + 1 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Invalid address format",
|
||||
));
|
||||
let separator_index = address.find(':').ok_or_else(|| {
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid address format")
|
||||
})?;
|
||||
|
||||
if address.len() <= separator_index + 1 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Invalid address format",
|
||||
));
|
||||
}
|
||||
|
||||
let domain_name = &address[0..separator_index];
|
||||
|
||||
let port = address[separator_index + 1..]
|
||||
.parse::<u16>()
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
let remote_location = NetLocation::new(Address::from(domain_name)?, port);
|
||||
|
||||
// wait for an empty \r\n before connecting, and check for auth header line if needed.
|
||||
let mut need_auth = auth_token.is_some();
|
||||
|
||||
loop {
|
||||
let line = stream_reader.read_line(&mut server_stream).await?;
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
if need_auth
|
||||
&& line.len() > PROXY_AUTH_HEADER_PREFIX.len() + 1
|
||||
&& line[0..PROXY_AUTH_HEADER_PREFIX.len()].to_ascii_lowercase()
|
||||
== PROXY_AUTH_HEADER_PREFIX
|
||||
{
|
||||
if &line[PROXY_AUTH_HEADER_PREFIX.len()..] != auth_token.unwrap() {
|
||||
debug!(
|
||||
"Received incorrect HTTP CONNECT authentication: {}",
|
||||
&line[PROXY_AUTH_HEADER_PREFIX.len()..]
|
||||
);
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Incorrect HTTP CONNECT authentication",
|
||||
));
|
||||
}
|
||||
need_auth = false;
|
||||
continue;
|
||||
}
|
||||
debug!("Ignored HTTP CONNECT request header: {line}");
|
||||
}
|
||||
|
||||
if need_auth {
|
||||
// FoxyProxy and similar clients require Proxy-Authenticate header to send credentials
|
||||
server_stream.write_all(
|
||||
&format!("{http_version} 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\nContent-Length: 0\r\nConnection: close\r\n\r\n").into_bytes()
|
||||
).await?;
|
||||
server_stream.flush().await?;
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Missing HTTP CONNECT authentication",
|
||||
));
|
||||
}
|
||||
|
||||
// We need an initial flush for this line.
|
||||
let connection_success_response = Some(
|
||||
format!("{http_version} 200 Connection established\r\n\r\n")
|
||||
.into_bytes()
|
||||
.into_boxed_slice(),
|
||||
);
|
||||
|
||||
(
|
||||
remote_location,
|
||||
connection_success_response,
|
||||
stream_reader.unparsed_data_owned(),
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
// Request looks a normal HTTP request but with protocol and address:
|
||||
// GET http://ipinfo.io/ HTTP/1.1
|
||||
// <headers follow..>
|
||||
// <empty line>
|
||||
|
||||
let line = &line[0..line.len() - 9];
|
||||
|
||||
let space_index = line.find(' ').ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Unrecognized http request: {line} {http_version}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let directive = &line[0..space_index];
|
||||
let url = &line[space_index + 1..];
|
||||
|
||||
if !url.starts_with("http://") {
|
||||
// we can't handle https
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Unsupported http forward url: {url}"),
|
||||
));
|
||||
}
|
||||
|
||||
let url = &url[7..]; // strip "http://"
|
||||
|
||||
let (address, location) = match url.find('/') {
|
||||
Some(i) => (&url[0..i], &url[i..]),
|
||||
None => (url, "/"),
|
||||
};
|
||||
|
||||
let remote_location = match address.find(':') {
|
||||
Some(i) => {
|
||||
let port = address[i + 1..]
|
||||
.parse::<u16>()
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
NetLocation::new(Address::from(&address[0..i])?, port)
|
||||
}
|
||||
None => NetLocation::new(Address::from(address)?, 80),
|
||||
};
|
||||
|
||||
let mut request = format!("{directive} {location} {http_version}\r\n");
|
||||
|
||||
// wait for an empty \r\n before connecting, and check for auth header line if needed.
|
||||
let mut need_auth = auth_token.is_some();
|
||||
|
||||
loop {
|
||||
let line = stream_reader.read_line(&mut server_stream).await?;
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let domain_name = &address[0..separator_index];
|
||||
let lowercase_line = line.to_ascii_lowercase();
|
||||
|
||||
let port = address[separator_index + 1..]
|
||||
.parse::<u16>()
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
let remote_location = NetLocation::new(Address::from(domain_name)?, port);
|
||||
|
||||
// wait for an empty \r\n before connecting, and check for auth header line if needed.
|
||||
let mut need_auth = self.auth_token.is_some();
|
||||
|
||||
loop {
|
||||
let line = stream_reader.read_line(&mut server_stream).await?;
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
if need_auth
|
||||
&& line.len() > PROXY_AUTH_HEADER_PREFIX.len() + 1
|
||||
&& line[0..PROXY_AUTH_HEADER_PREFIX.len()].to_ascii_lowercase()
|
||||
== PROXY_AUTH_HEADER_PREFIX
|
||||
{
|
||||
if &line[PROXY_AUTH_HEADER_PREFIX.len()..]
|
||||
!= self.auth_token.as_ref().unwrap()
|
||||
{
|
||||
// && lowercase_line[0..PROXY_AUTH_HEADER_PREFIX.len()] == PROXY_AUTH_HEADER_PREFIX
|
||||
if line.len() > PROXY_AUTH_HEADER_PREFIX.len() + 1
|
||||
&& lowercase_line.starts_with(PROXY_AUTH_HEADER_PREFIX)
|
||||
{
|
||||
if need_auth {
|
||||
if &line[PROXY_AUTH_HEADER_PREFIX.len()..] != auth_token.unwrap() {
|
||||
debug!(
|
||||
"Received incorrect HTTP CONNECT authentication: {}",
|
||||
"Received incorrect HTTP GET authentication: {}",
|
||||
&line[PROXY_AUTH_HEADER_PREFIX.len()..]
|
||||
);
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Incorrect HTTP CONNECT authentication",
|
||||
"Incorrect HTTP GET authentication",
|
||||
));
|
||||
}
|
||||
need_auth = false;
|
||||
continue;
|
||||
}
|
||||
debug!("Ignored HTTP CONNECT request header: {line}");
|
||||
// If some auth header was passed in and we don't have auth configured,
|
||||
// simply ignore it.
|
||||
continue;
|
||||
} else if lowercase_line.starts_with(CONNECTION_HEADER_PREFIX)
|
||||
|| lowercase_line.starts_with(PROXY_CONNECTION_HEADER_PREFIX)
|
||||
{
|
||||
// We can't support 'Connection' or 'Proxy-Connection' for GET style proxy requests.
|
||||
// Because then we'd have to parse the remote server's response to know when it ends,
|
||||
// in order to handle subsequent GET requests.
|
||||
// So filter them out, and then we make sure to add a 'Connection: close' header to prevent
|
||||
// having to worry about that.
|
||||
continue;
|
||||
}
|
||||
|
||||
if need_auth {
|
||||
// We need to write Proxy-Authenticate or apps such as FoxyProxy will never send Proxy-Authorization.
|
||||
server_stream.write_all(
|
||||
&format!("{http_version} 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\nContent-Length: 0\r\nConnection: close\r\n\r\n").into_bytes()
|
||||
).await?;
|
||||
server_stream.flush().await?;
|
||||
request.push_str(line);
|
||||
request.push_str("\r\n");
|
||||
|
||||
if request.len() > 16384 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Missing HTTP CONNECT authentication",
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"HTTP GET request is too long",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// We need an initial flush for this line.
|
||||
let connection_success_response = Some(
|
||||
format!("{http_version} 200 Connection established\r\n\r\n")
|
||||
.into_bytes()
|
||||
.into_boxed_slice(),
|
||||
);
|
||||
if need_auth {
|
||||
// FoxyProxy and similar clients require Proxy-Authenticate header to send credentials
|
||||
server_stream.write_all(
|
||||
&format!("{http_version} 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\nContent-Length: 0\r\nConnection: close\r\n\r\n").into_bytes()
|
||||
).await?;
|
||||
server_stream.flush().await?;
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Missing HTTP GET authentication",
|
||||
));
|
||||
}
|
||||
|
||||
(
|
||||
remote_location,
|
||||
connection_success_response,
|
||||
stream_reader.unparsed_data_owned(),
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
// Request looks a normal HTTP request but with protocol and address:
|
||||
// GET http://ipinfo.io/ HTTP/1.1
|
||||
// <headers follow..>
|
||||
// <empty line>
|
||||
request.push_str("Connection: close\r\n\r\n");
|
||||
|
||||
// get rid of the space and http version info.
|
||||
let line = &line[0..line.len() - 9];
|
||||
// We don't write "HTTP/xxx 200 Connection established\r\n\r\n" for this type of
|
||||
// request, the server's response (eg. "HTTP/1.1 200 OK") is what the client
|
||||
// expects as a response.
|
||||
|
||||
let space_index = line.find(' ').ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Unrecognized http request: {line} {http_version}"),
|
||||
)
|
||||
})?;
|
||||
let request_bytes = request.into_bytes();
|
||||
|
||||
let directive = &line[0..space_index];
|
||||
let url = &line[space_index + 1..];
|
||||
let unparsed_data = stream_reader.unparsed_data();
|
||||
|
||||
if !url.starts_with("http://") {
|
||||
// we can't handle https
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Unsupported http forward url: {url}"),
|
||||
));
|
||||
}
|
||||
let mut initial_remote_data =
|
||||
Vec::with_capacity(request_bytes.len() + unparsed_data.len());
|
||||
initial_remote_data.extend(request_bytes.iter());
|
||||
initial_remote_data.extend(unparsed_data.iter());
|
||||
|
||||
// get rid of http://
|
||||
let url = &url[7..];
|
||||
(
|
||||
remote_location,
|
||||
None,
|
||||
Some(initial_remote_data.into_boxed_slice()),
|
||||
false,
|
||||
)
|
||||
};
|
||||
|
||||
let (address, location) = match url.find('/') {
|
||||
Some(i) => (&url[0..i], &url[i..]),
|
||||
None => (url, "/"),
|
||||
};
|
||||
|
||||
let remote_location = match address.find(':') {
|
||||
Some(i) => {
|
||||
// Port is specified.
|
||||
let port = address[i + 1..]
|
||||
.parse::<u16>()
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
NetLocation::new(Address::from(&address[0..i])?, port)
|
||||
}
|
||||
None => NetLocation::new(Address::from(address)?, 80),
|
||||
};
|
||||
|
||||
let mut request = format!("{directive} {location} {http_version}\r\n");
|
||||
|
||||
// wait for an empty \r\n before connecting, and check for auth header line if needed.
|
||||
let mut need_auth = self.auth_token.is_some();
|
||||
|
||||
loop {
|
||||
let line = stream_reader.read_line(&mut server_stream).await?;
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let lowercase_line = line.to_ascii_lowercase();
|
||||
|
||||
// && lowercase_line[0..PROXY_AUTH_HEADER_PREFIX.len()] == PROXY_AUTH_HEADER_PREFIX
|
||||
if line.len() > PROXY_AUTH_HEADER_PREFIX.len() + 1
|
||||
&& lowercase_line.starts_with(PROXY_AUTH_HEADER_PREFIX)
|
||||
{
|
||||
if need_auth {
|
||||
if &line[PROXY_AUTH_HEADER_PREFIX.len()..]
|
||||
!= self.auth_token.as_ref().unwrap()
|
||||
{
|
||||
debug!(
|
||||
"Received incorrect HTTP GET authentication: {}",
|
||||
&line[PROXY_AUTH_HEADER_PREFIX.len()..]
|
||||
);
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Incorrect HTTP GET authentication",
|
||||
));
|
||||
}
|
||||
need_auth = false;
|
||||
}
|
||||
// If some auth header was passed in and we don't have auth configured,
|
||||
// simply ignore it.
|
||||
continue;
|
||||
} else if lowercase_line.starts_with(CONNECTION_HEADER_PREFIX)
|
||||
|| lowercase_line.starts_with(PROXY_CONNECTION_HEADER_PREFIX)
|
||||
{
|
||||
// We can't support 'Connection' or 'Proxy-Connection' for GET style proxy requests.
|
||||
// Because then we'd have to parse the remote server's response to know when it ends,
|
||||
// in order to handle subsequent GET requests.
|
||||
// So filter them out, and then we make sure to add a 'Connection: close' header to prevent
|
||||
// having to worry about that.
|
||||
continue;
|
||||
}
|
||||
|
||||
request.push_str(line);
|
||||
request.push_str("\r\n");
|
||||
|
||||
if request.len() > 16384 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"HTTP GET request is too long",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if need_auth {
|
||||
// We need to write Proxy-Authenticate or apps such as FoxyProxy will never send Proxy-Authorization.
|
||||
server_stream.write_all(
|
||||
&format!("{http_version} 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\nContent-Length: 0\r\nConnection: close\r\n\r\n").into_bytes()
|
||||
).await?;
|
||||
server_stream.flush().await?;
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Missing HTTP GET authentication",
|
||||
));
|
||||
}
|
||||
|
||||
// Add our Connection header and end the request with a new line.
|
||||
request.push_str("Connection: close\r\n\r\n");
|
||||
|
||||
// We don't write "HTTP/xxx 200 Connection established\r\n\r\n" for this type of
|
||||
// request, the server's response (eg. "HTTP/1.1 200 OK") is what the client
|
||||
// expects as a response.
|
||||
|
||||
let request_bytes = request.into_bytes();
|
||||
|
||||
let unparsed_data = stream_reader.unparsed_data();
|
||||
|
||||
let mut initial_remote_data =
|
||||
Vec::with_capacity(request_bytes.len() + unparsed_data.len());
|
||||
initial_remote_data.extend(request_bytes.iter());
|
||||
initial_remote_data.extend(unparsed_data.iter());
|
||||
|
||||
(
|
||||
remote_location,
|
||||
None,
|
||||
Some(initial_remote_data.into_boxed_slice()),
|
||||
false,
|
||||
)
|
||||
};
|
||||
|
||||
Ok(TcpServerSetupResult::TcpForward {
|
||||
remote_location,
|
||||
stream: server_stream,
|
||||
need_initial_flush,
|
||||
connection_success_response,
|
||||
initial_remote_data,
|
||||
override_proxy_provider: NoneOrOne::Unspecified,
|
||||
})
|
||||
}
|
||||
Ok(TcpServerSetupResult::TcpForward {
|
||||
remote_location,
|
||||
stream: server_stream,
|
||||
need_initial_flush,
|
||||
connection_success_response,
|
||||
initial_remote_data,
|
||||
proxy_selector,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_http_auth_header_line(username: &str, password: &str) -> String {
|
||||
|
||||
+107
-82
@@ -7,7 +7,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use log::{error, warn};
|
||||
use log::{debug, error, warn};
|
||||
use rand::distr::Alphanumeric;
|
||||
use rand::{Rng, RngCore};
|
||||
use rustc_hash::FxHashMap;
|
||||
@@ -25,15 +25,18 @@ const MAX_FRAGMENT_CACHE_SIZE: usize = 256;
|
||||
/// Default is 3 seconds per sing-box reference implementation.
|
||||
const AUTH_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
|
||||
/// HTTP/3 error code for normal closure.
|
||||
/// Per official hysteria reference: https://github.com/apernet/hysteria/blob/master/core/server/server.go#L20
|
||||
const CLOSE_ERR_CODE_OK: u32 = 0x100; // HTTP3 ErrCodeNoError
|
||||
|
||||
use crate::address::NetLocation;
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::client_proxy_selector::{ClientProxySelector, ConnectDecision};
|
||||
use crate::copy_bidirectional::copy_bidirectional_with_sizes;
|
||||
use crate::quic_stream::QuicStream;
|
||||
use crate::resolver::{Resolver, ResolverCache};
|
||||
use crate::socket_util::new_socket2_udp_socket;
|
||||
use crate::stream_reader::StreamReader;
|
||||
use crate::tcp_server::setup_client_tcp_stream;
|
||||
use crate::tcp::tcp_server::setup_client_tcp_stream;
|
||||
use crate::util::allocate_vec;
|
||||
|
||||
async fn process_connection(
|
||||
@@ -62,8 +65,7 @@ async fn process_connection(
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
|
||||
// Authentication with timeout - per sing-box reference, default 3 seconds.
|
||||
// This prevents malicious clients from holding connections open without authenticating.
|
||||
// Per sing-box reference, authentication timeout is 3 seconds
|
||||
match timeout(
|
||||
AUTH_TIMEOUT,
|
||||
auth_connection(&mut h3_conn, password, udp_enabled),
|
||||
@@ -72,12 +74,12 @@ async fn process_connection(
|
||||
{
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(e)) => {
|
||||
connection.close(0u32.into(), b"auth failed");
|
||||
connection.close(CLOSE_ERR_CODE_OK.into(), b"auth failed");
|
||||
return Err(e);
|
||||
}
|
||||
Err(_elapsed) => {
|
||||
error!("Authentication timeout");
|
||||
connection.close(0u32.into(), b"auth timeout");
|
||||
connection.close(CLOSE_ERR_CODE_OK.into(), b"auth timeout");
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
"authentication timeout",
|
||||
@@ -85,59 +87,65 @@ async fn process_connection(
|
||||
}
|
||||
}
|
||||
|
||||
let mut join_handles = vec![];
|
||||
if udp_enabled {
|
||||
let connection = connection.clone();
|
||||
let client_proxy_selector = client_proxy_selector.clone();
|
||||
let resolver = resolver.clone();
|
||||
let cancel_token = cancel_token.clone();
|
||||
join_handles.push(tokio::spawn(async move {
|
||||
if let Err(e) = run_udp_local_to_remote_loop(
|
||||
connection,
|
||||
client_proxy_selector,
|
||||
resolver,
|
||||
cancel_token,
|
||||
let udp_connection = connection.clone();
|
||||
let udp_client_proxy_selector = client_proxy_selector.clone();
|
||||
let udp_resolver = resolver.clone();
|
||||
let udp_cancel_token = cancel_token.clone();
|
||||
|
||||
let uni_connection = connection.clone();
|
||||
|
||||
// Use try_join! to run all loops concurrently within the same task, like Quinn's perf example.
|
||||
// This reduces task count and avoids spawning separate tasks for the main loops.
|
||||
let udp_loop = async {
|
||||
if udp_enabled {
|
||||
run_udp_local_to_remote_loop(
|
||||
udp_connection,
|
||||
udp_client_proxy_selector,
|
||||
udp_resolver,
|
||||
udp_cancel_token,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("UDP local-to-remote write loop ended with error: {e}");
|
||||
}
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
// depending on the client, unidirectional streams could still be sent, accept and drop.
|
||||
{
|
||||
let connection = connection.clone();
|
||||
join_handles.push(tokio::spawn(async move {
|
||||
loop {
|
||||
match connection.accept_uni().await {
|
||||
Ok(mut recv_stream) => {
|
||||
let _ = recv_stream.stop(0u32.into());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Unidirectional loop ended with error: {e}");
|
||||
break;
|
||||
}
|
||||
let uni_loop = async {
|
||||
// Depending on the client, unidirectional streams could still be sent, accept and drop.
|
||||
loop {
|
||||
match uni_connection.accept_uni().await {
|
||||
Ok(mut recv_stream) => {
|
||||
let _ = recv_stream.stop(0u32.into());
|
||||
}
|
||||
Err(quinn::ConnectionError::ApplicationClosed(_)) => break,
|
||||
Err(quinn::ConnectionError::ConnectionClosed(_)) => break,
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"unidirectional loop error: {e}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
join_handles.push(tokio::spawn(async move {
|
||||
if let Err(e) = run_tcp_loop(connection, client_proxy_selector, resolver).await {
|
||||
error!("TCP loop ended with error: {e}");
|
||||
}
|
||||
}));
|
||||
Ok(())
|
||||
};
|
||||
|
||||
// Use try_join to fail fast if any task errors
|
||||
let results = futures::future::join_all(join_handles).await;
|
||||
for result in results {
|
||||
result?;
|
||||
}
|
||||
let tcp_connection = connection.clone();
|
||||
let tcp_loop = run_tcp_loop(tcp_connection, client_proxy_selector, resolver);
|
||||
|
||||
let result = tokio::try_join!(udp_loop, uni_loop, tcp_loop);
|
||||
|
||||
// Cancel all remaining tasks (UDP session loops)
|
||||
cancel_token.cancel();
|
||||
|
||||
Ok(())
|
||||
// Per sing-box reference (service.go:277-293), close connection on error
|
||||
if let Err(ref e) = result {
|
||||
error!("Connection failed: {e}");
|
||||
connection.close(CLOSE_ERR_CODE_OK.into(), b"");
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_auth_request<T>(req: http::Request<T>, password: &str) -> std::io::Result<()> {
|
||||
@@ -244,7 +252,6 @@ struct UdpSession {
|
||||
last_socket_addr: SocketAddr,
|
||||
override_remote_write_address: Option<SocketAddr>,
|
||||
last_activity: std::time::Instant,
|
||||
// Cancellation token for this session's background task
|
||||
cancel_token: CancellationToken,
|
||||
}
|
||||
|
||||
@@ -322,13 +329,13 @@ async fn run_udp_remote_to_local_loop(
|
||||
};
|
||||
|
||||
let mut next_packet_id: u16 = 0;
|
||||
let mut buf = [0u8; 65535];
|
||||
let mut buf = allocate_vec(65535);
|
||||
let mut loop_count: u8 = 0;
|
||||
|
||||
loop {
|
||||
let (payload_len, src_addr) = match socket.try_recv_from(&mut buf) {
|
||||
Ok(res) => res,
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
// Use select! to allow cancellation while waiting for socket to be readable
|
||||
tokio::select! {
|
||||
_ = cancel_token.cancelled() => {
|
||||
return Ok(());
|
||||
@@ -346,6 +353,13 @@ async fn run_udp_remote_to_local_loop(
|
||||
}
|
||||
};
|
||||
|
||||
// Yield periodically to allow quinn's internal tasks to run (keepalives, ACKs, etc.)
|
||||
// This prevents starvation during heavy UDP traffic.
|
||||
loop_count = loop_count.wrapping_add(1);
|
||||
if loop_count == 0 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
|
||||
let packet_id = next_packet_id;
|
||||
next_packet_id = next_packet_id.wrapping_add(1);
|
||||
|
||||
@@ -415,18 +429,18 @@ async fn run_udp_local_to_remote_loop(
|
||||
let mut sessions: FxHashMap<u32, UdpSession> = FxHashMap::default();
|
||||
let mut last_cleanup = std::time::Instant::now();
|
||||
|
||||
const CLEANUP_INTERVAL: Duration = Duration::from_secs(100);
|
||||
const IDLE_TIMEOUT: Duration = Duration::from_secs(200);
|
||||
// Match reference implementation defaults for UDP session management
|
||||
const CLEANUP_INTERVAL: Duration = Duration::from_secs(10);
|
||||
const IDLE_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
loop {
|
||||
// Periodically clean up stale sessions
|
||||
let now = std::time::Instant::now();
|
||||
if (now - last_cleanup) > CLEANUP_INTERVAL {
|
||||
sessions.retain(|session_id, session| {
|
||||
if session.last_activity.elapsed() > IDLE_TIMEOUT {
|
||||
// Cancel the session's background task before removing
|
||||
session.cancel_token.cancel();
|
||||
error!("Removing inactive UDP session {session_id}");
|
||||
debug!("Removing inactive UDP session {session_id}");
|
||||
false
|
||||
} else {
|
||||
true
|
||||
@@ -439,11 +453,12 @@ async fn run_udp_local_to_remote_loop(
|
||||
.read_datagram()
|
||||
.await
|
||||
.map_err(|err| std::io::Error::other(format!("failed to read datagram: {err}")))?;
|
||||
|
||||
// Per official hysteria reference (server.go:332-353), parse errors are ignored
|
||||
// and we continue waiting for the next message. Only connection errors are fatal.
|
||||
if data.len() < 9 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"datagram length too short",
|
||||
));
|
||||
debug!("Ignoring short datagram (len={})", data.len());
|
||||
continue;
|
||||
}
|
||||
let session_id = u32::from_be_bytes(data[0..4].try_into().unwrap());
|
||||
let packet_id = u16::from_be_bytes(data[4..6].try_into().unwrap());
|
||||
@@ -477,17 +492,18 @@ async fn run_udp_local_to_remote_loop(
|
||||
};
|
||||
|
||||
if address_len == 0 {
|
||||
error!("Ignoring packet with empty address");
|
||||
debug!("Ignoring packet with empty address");
|
||||
continue;
|
||||
}
|
||||
|
||||
if address_len > 2048 {
|
||||
error!("Ignoring packet with address length {address_len}");
|
||||
debug!("Ignoring packet with address length {address_len}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if data.len() < next_index + address_len {
|
||||
return Err(std::io::Error::other("invalid address length"));
|
||||
debug!("Ignoring datagram with truncated address");
|
||||
continue;
|
||||
}
|
||||
let address_bytes = &data[next_index..next_index + address_len];
|
||||
let payload_fragment = data.slice(next_index + address_len..);
|
||||
@@ -495,7 +511,7 @@ async fn run_udp_local_to_remote_loop(
|
||||
let addr_str = match str::from_utf8(address_bytes) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Invalid UTF-8 in address: {e}");
|
||||
debug!("Invalid UTF-8 in address: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -503,7 +519,7 @@ async fn run_udp_local_to_remote_loop(
|
||||
let remote_location = match NetLocation::from_str(addr_str, None) {
|
||||
Ok(loc) => loc,
|
||||
Err(e) => {
|
||||
error!("Failed to parse remote location from {addr_str}: {e}");
|
||||
debug!("Failed to parse address '{addr_str}': {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -592,11 +608,9 @@ async fn run_udp_local_to_remote_loop(
|
||||
} else if fragment_count == 1 {
|
||||
(payload_fragment, remote_location)
|
||||
} else {
|
||||
// Check if we already have this packet in the LRU cache
|
||||
let is_new = !session.fragments.contains(&packet_id);
|
||||
|
||||
if is_new {
|
||||
// Insert new fragmented packet entry
|
||||
session.fragments.put(
|
||||
packet_id,
|
||||
FragmentedPacket {
|
||||
@@ -702,7 +716,6 @@ async fn run_udp_local_to_remote_loop(
|
||||
.await
|
||||
{
|
||||
error!("Failed to forward UDP payload for session {session_id}: {e}");
|
||||
// Remove the failed session
|
||||
sessions.remove(&session_id);
|
||||
}
|
||||
}
|
||||
@@ -878,19 +891,15 @@ async fn process_tcp_stream(
|
||||
};
|
||||
drop(stream_reader);
|
||||
|
||||
// unlike tokio's implementation, we read as much as possible to fill up the
|
||||
// buffer size before sending. reduce the buffer sizes compared to tcp -> tcp.
|
||||
// also see https://www.privateoctopus.com/2023/12/12/quic-performance.html
|
||||
// Use 32KB buffers to match hysteria2/sing-box reference implementations
|
||||
let copy_result = copy_bidirectional_with_sizes(
|
||||
&mut server_stream,
|
||||
&mut client_stream,
|
||||
// no need to flush even through we wrote this response since it's quic
|
||||
false,
|
||||
client_requires_flush,
|
||||
// quic -> tcp
|
||||
8192,
|
||||
// tcp -> quic
|
||||
16384,
|
||||
32768,
|
||||
32768,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -976,15 +985,31 @@ pub async fn start_hysteria2_server(
|
||||
.max_concurrent_bidi_streams(4096_u32.into())
|
||||
// required for HTTP/3 QPACK updates
|
||||
.max_concurrent_uni_streams(1024_u32.into())
|
||||
.max_idle_timeout(Some(Duration::from_secs(60).try_into().unwrap()))
|
||||
.keep_alive_interval(Some(Duration::from_secs(15)))
|
||||
.max_idle_timeout(Some(Duration::from_secs(30).try_into().unwrap()))
|
||||
.keep_alive_interval(Some(Duration::from_secs(10)))
|
||||
.send_window(16 * 1024 * 1024)
|
||||
.receive_window((20u32 * 1024 * 1024).into())
|
||||
.stream_receive_window((8u32 * 1024 * 1024).into());
|
||||
.stream_receive_window((8u32 * 1024 * 1024).into())
|
||||
// MTU settings per official TUIC reference
|
||||
.initial_mtu(1200)
|
||||
.min_mtu(1200)
|
||||
// Enable MTU discovery for larger packets on capable networks
|
||||
.mtu_discovery_config(Some(quinn::MtuDiscoveryConfig::default()))
|
||||
// Enable GSO (Generic Segmentation Offload) for better throughput
|
||||
.enable_segmentation_offload(true)
|
||||
// Lower initial RTT estimate for faster initial window growth
|
||||
.initial_rtt(Duration::from_millis(100));
|
||||
|
||||
let socket2_socket =
|
||||
new_socket2_udp_socket(bind_address.is_ipv6(), None, Some(bind_address), true)
|
||||
.unwrap();
|
||||
// Use 7.5MB socket buffers for high-throughput QUIC (8.625MB on BSD for 15% kernel overhead)
|
||||
// https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes
|
||||
let socket2_socket = crate::socket_util::new_socket2_udp_socket_with_buffer_size(
|
||||
bind_address.is_ipv6(),
|
||||
None,
|
||||
Some(bind_address),
|
||||
true,
|
||||
Some(8_625_000),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let endpoint = quinn::Endpoint::new(
|
||||
quinn::EndpointConfig::default(),
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
// This library shares code with the shoes binary. Server-side code appears "unused"
|
||||
// in lib builds but is used by: (1) the binary for server mode, (2) FFI for mobile.
|
||||
// The client/server code is intermingled within modules - a proper fix would require
|
||||
// splitting into separate client/server modules or using feature flags.
|
||||
#![allow(dead_code)]
|
||||
|
||||
//! shoes - A high-performance multi-protocol proxy server.
|
||||
//!
|
||||
//! This library provides the core functionality for shoes, enabling it to be
|
||||
//! embedded in mobile applications (Android/iOS) as a VPN backend.
|
||||
//!
|
||||
//! # Features
|
||||
//!
|
||||
//! - **Multi-protocol support**: VLESS, VMess, Trojan, Shadowsocks, and more
|
||||
//! - **TUN device support**: Virtual network interface for VPN mode
|
||||
//! - **Proxy chaining**: Connect through multiple proxies
|
||||
//! - **Flexible routing**: Rule-based traffic routing
|
||||
//!
|
||||
//! # Mobile Integration
|
||||
//!
|
||||
//! For Android, use the FFI module:
|
||||
//!
|
||||
//! ```kotlin
|
||||
//! // Load native library
|
||||
//! System.loadLibrary("shoes")
|
||||
//!
|
||||
//! // Initialize
|
||||
//! ShoesNative.init("info")
|
||||
//!
|
||||
//! // Start VPN with TUN fd from VpnService
|
||||
//! val handle = ShoesNative.startTun(tunFd, configYaml, protectCallback)
|
||||
//!
|
||||
//! // Stop VPN
|
||||
//! ShoesNative.stop(handle)
|
||||
//! ```
|
||||
//!
|
||||
//! For iOS, use the C FFI module from Swift:
|
||||
//!
|
||||
//! ```swift
|
||||
//! // Initialize
|
||||
//! shoes_init("info")
|
||||
//!
|
||||
//! // Start VPN with packet tunnel fd
|
||||
//! let handle = shoes_start(configYaml, protectCallback)
|
||||
//!
|
||||
//! // Stop VPN
|
||||
//! shoes_stop(handle)
|
||||
//! ```
|
||||
//!
|
||||
//! # Platform Support
|
||||
//!
|
||||
//! - Linux (x86_64, aarch64)
|
||||
//! - Android (arm64-v8a, armeabi-v7a, x86_64)
|
||||
//! - iOS (arm64)
|
||||
|
||||
// Modules are declared here (mirroring main.rs) so the library crate can
|
||||
// expose them for FFI/mobile integration.
|
||||
mod address;
|
||||
mod anytls;
|
||||
mod async_stream;
|
||||
mod buf_reader;
|
||||
mod client_proxy_chain;
|
||||
mod client_proxy_selector;
|
||||
mod copy_bidirectional;
|
||||
mod copy_bidirectional_message;
|
||||
mod crypto;
|
||||
mod http_handler;
|
||||
mod hysteria2_server;
|
||||
mod mixed_handler;
|
||||
mod naiveproxy;
|
||||
mod option_util;
|
||||
mod port_forward_handler;
|
||||
mod quic_server;
|
||||
mod quic_stream;
|
||||
mod reality;
|
||||
mod reality_client_handler;
|
||||
mod resolver;
|
||||
mod routing;
|
||||
mod rustls_config_util;
|
||||
mod rustls_connection_util;
|
||||
mod shadow_tls;
|
||||
mod shadowsocks;
|
||||
mod slide_buffer;
|
||||
mod snell;
|
||||
mod socket_util;
|
||||
mod socks5_udp_relay;
|
||||
mod socks_handler;
|
||||
mod stream_reader;
|
||||
mod sync_adapter;
|
||||
mod tcp;
|
||||
mod thread_util;
|
||||
mod tls_client_handler;
|
||||
mod tls_server_handler;
|
||||
mod trojan_handler;
|
||||
mod tuic_server;
|
||||
mod uot;
|
||||
mod util;
|
||||
mod uuid_util;
|
||||
mod vless;
|
||||
mod vmess;
|
||||
mod websocket;
|
||||
mod xudp;
|
||||
|
||||
/// Configuration types.
|
||||
pub mod config;
|
||||
|
||||
/// TUN device support for VPN mode.
|
||||
pub mod tun;
|
||||
|
||||
/// FFI bindings for mobile platforms.
|
||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "ffi"))]
|
||||
pub mod ffi;
|
||||
+53
-51
@@ -1,4 +1,5 @@
|
||||
mod address;
|
||||
mod anytls;
|
||||
mod async_stream;
|
||||
mod buf_reader;
|
||||
mod client_proxy_chain;
|
||||
@@ -6,11 +7,11 @@ mod client_proxy_selector;
|
||||
mod config;
|
||||
mod copy_bidirectional;
|
||||
mod copy_bidirectional_message;
|
||||
mod copy_multidirectional_message;
|
||||
mod copy_session_messages;
|
||||
mod crypto;
|
||||
mod http_handler;
|
||||
mod hysteria2_server;
|
||||
mod mixed_handler;
|
||||
mod naiveproxy;
|
||||
mod option_util;
|
||||
mod port_forward_handler;
|
||||
mod quic_server;
|
||||
@@ -18,6 +19,7 @@ mod quic_stream;
|
||||
mod reality;
|
||||
mod reality_client_handler;
|
||||
mod resolver;
|
||||
mod routing;
|
||||
mod rustls_config_util;
|
||||
mod rustls_connection_util;
|
||||
mod shadow_tls;
|
||||
@@ -25,6 +27,7 @@ mod shadowsocks;
|
||||
mod slide_buffer;
|
||||
mod snell;
|
||||
mod socket_util;
|
||||
mod socks5_udp_relay;
|
||||
mod socks_handler;
|
||||
mod stream_reader;
|
||||
mod sync_adapter;
|
||||
@@ -34,20 +37,20 @@ mod tls_client_handler;
|
||||
mod tls_server_handler;
|
||||
mod trojan_handler;
|
||||
mod tuic_server;
|
||||
mod tun;
|
||||
mod udp_message_stream;
|
||||
mod udp_multi_message_stream;
|
||||
mod udp_session_message_stream;
|
||||
mod uot;
|
||||
mod util;
|
||||
mod uuid_util;
|
||||
mod vless;
|
||||
mod vmess;
|
||||
mod websocket;
|
||||
mod xudp;
|
||||
|
||||
#[cfg(not(target_env = "msvc"))]
|
||||
#[cfg(not(any(target_env = "msvc", target_os = "ios")))]
|
||||
use tikv_jemallocator::Jemalloc;
|
||||
|
||||
#[cfg(not(target_env = "msvc"))]
|
||||
#[cfg(not(any(target_env = "msvc", target_os = "ios")))]
|
||||
#[global_allocator]
|
||||
static GLOBAL: Jemalloc = Jemalloc;
|
||||
|
||||
@@ -58,13 +61,10 @@ use aws_lc_rs::rand::{SecureRandom, SystemRandom};
|
||||
use base64::engine::{Engine as _, general_purpose::STANDARD};
|
||||
use log::debug;
|
||||
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use tcp_server::start_tcp_servers;
|
||||
use tcp_server::start_servers;
|
||||
use tokio::runtime::Builder;
|
||||
use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::config::{ServerConfig, Transport};
|
||||
use crate::quic_server::start_quic_servers;
|
||||
use crate::reality::generate_keypair;
|
||||
use crate::shadowsocks::ShadowsocksCipher;
|
||||
use crate::thread_util::set_num_threads;
|
||||
@@ -97,45 +97,6 @@ fn start_notify_thread(
|
||||
(watcher, rx)
|
||||
}
|
||||
|
||||
async fn start_servers(config: ServerConfig) -> std::io::Result<Vec<JoinHandle<()>>> {
|
||||
let mut join_handles = Vec::with_capacity(3);
|
||||
|
||||
match config.transport {
|
||||
Transport::Tcp => match start_tcp_servers(config.clone()).await {
|
||||
Ok(handles) => {
|
||||
join_handles.extend(handles);
|
||||
}
|
||||
Err(e) => {
|
||||
for join_handle in join_handles {
|
||||
join_handle.abort();
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
},
|
||||
Transport::Quic => match start_quic_servers(config.clone()).await {
|
||||
Ok(handles) => {
|
||||
join_handles.extend(handles);
|
||||
}
|
||||
Err(e) => {
|
||||
for join_handle in join_handles {
|
||||
join_handle.abort();
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
},
|
||||
Transport::Udp => todo!(),
|
||||
}
|
||||
|
||||
if join_handles.is_empty() {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"failed to start servers at {}",
|
||||
&config.bind_location
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(join_handles)
|
||||
}
|
||||
|
||||
fn print_usage_and_exit(arg0: String) {
|
||||
eprintln!("{arg0} [OPTIONS] <config.yaml> [config.yaml...]");
|
||||
eprintln!();
|
||||
@@ -149,11 +110,41 @@ fn print_usage_and_exit(arg0: String) {
|
||||
" generate-reality-keypair Generate a new Reality X25519 keypair"
|
||||
);
|
||||
eprintln!(" generate-shadowsocks-2022-password <cipher> Generate a Shadowsocks password");
|
||||
eprintln!(
|
||||
" generate-vless-user-id Generate a random VLESS/VMESS user ID (UUID v4)"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
fn get_debug_file_log_level() -> Option<log::LevelFilter> {
|
||||
// Check for debug marker files in the same directory as the binary
|
||||
let exe_dir = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|p| p.to_path_buf()));
|
||||
|
||||
if let Some(dir) = exe_dir {
|
||||
if dir.join(".shoes-trace").exists() {
|
||||
eprintln!("Found debug marker file .shoes-trace, setting log level to TRACE");
|
||||
return Some(log::LevelFilter::Trace);
|
||||
}
|
||||
if dir.join(".shoes-debug").exists() {
|
||||
eprintln!("Found debug marker file .shoes-debug, setting log level to DEBUG");
|
||||
return Some(log::LevelFilter::Debug);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::builder()
|
||||
let mut builder = env_logger::builder();
|
||||
|
||||
if let Some(level) = get_debug_file_log_level() {
|
||||
builder.filter_level(level);
|
||||
} else {
|
||||
builder.parse_default_env();
|
||||
}
|
||||
|
||||
builder
|
||||
.format(|buf, record| {
|
||||
let timestamp = buf.timestamp();
|
||||
let level_style = buf.default_level_style(record.level());
|
||||
@@ -284,6 +275,18 @@ fn main() {
|
||||
return;
|
||||
}
|
||||
|
||||
if args.iter().any(|s| s == "generate-vless-user-id") {
|
||||
let uuid = uuid_util::generate_uuid();
|
||||
println!(
|
||||
"--------------------------------------------------------------------------------"
|
||||
);
|
||||
println!("VLESS/VMESS User ID: {}", uuid);
|
||||
println!(
|
||||
"--------------------------------------------------------------------------------"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if args.is_empty() {
|
||||
println!("No config specified, assuming loading from file config.shoes.yaml");
|
||||
args.push("config.shoes.yaml".to_string())
|
||||
@@ -388,7 +391,6 @@ fn main() {
|
||||
|
||||
match reload_state.as_mut() {
|
||||
Some((_watcher, rx)) => {
|
||||
// Wait for config change
|
||||
rx.recv().await.unwrap();
|
||||
|
||||
println!("Configs changed, restarting servers in 3 seconds..");
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! Mixed HTTP+SOCKS5 server handler.
|
||||
//!
|
||||
//! This module provides a server handler that auto-detects whether the client
|
||||
//! is speaking HTTP or SOCKS5 based on the first byte of the connection:
|
||||
//! - 0x05 = SOCKS5 (RFC 1928 specifies version byte first)
|
||||
//! - Anything else = HTTP
|
||||
//!
|
||||
//! This is similar to mihomo's mixed-port feature.
|
||||
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::client_proxy_selector::ClientProxySelector;
|
||||
use crate::http_handler::setup_http_server_stream_inner;
|
||||
use crate::resolver::Resolver;
|
||||
use crate::socks_handler::{VER_SOCKS5, setup_socks_server_stream_inner};
|
||||
use crate::stream_reader::StreamReader;
|
||||
use crate::tcp::tcp_handler::{TcpServerHandler, TcpServerSetupResult};
|
||||
|
||||
/// Mixed HTTP+SOCKS5 server handler.
|
||||
///
|
||||
/// Auto-detects the protocol from the first byte and delegates to the
|
||||
/// appropriate handler implementation.
|
||||
#[derive(Debug)]
|
||||
pub struct MixedTcpServerHandler {
|
||||
/// Authentication for both HTTP and SOCKS5
|
||||
auth_info: Option<(String, String)>,
|
||||
/// Pre-computed HTTP auth token (base64 encoded)
|
||||
http_auth_token: Option<String>,
|
||||
/// Enable UDP functionality for SOCKS5 (UDP ASSOCIATE and UDP-over-TCP)
|
||||
udp_enabled: bool,
|
||||
/// IP address to bind UDP sockets on (same as TCP server)
|
||||
bind_ip: IpAddr,
|
||||
/// Proxy selector for outbound connections
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
/// DNS resolver
|
||||
resolver: Arc<dyn Resolver>,
|
||||
}
|
||||
|
||||
impl MixedTcpServerHandler {
|
||||
/// Create a new mixed HTTP+SOCKS5 server handler.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `auth_info` - Optional username/password for authentication (used for both HTTP and SOCKS5)
|
||||
/// * `udp_enabled` - Enable UDP functionality for SOCKS5 (UDP ASSOCIATE and UDP-over-TCP)
|
||||
/// * `bind_ip` - IP address to bind UDP sockets on (should match TCP server)
|
||||
/// * `proxy_selector` - Proxy selector for outbound connections
|
||||
/// * `resolver` - DNS resolver
|
||||
pub fn new(
|
||||
auth_info: Option<(String, String)>,
|
||||
udp_enabled: bool,
|
||||
bind_ip: IpAddr,
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
resolver: Arc<dyn Resolver>,
|
||||
) -> Self {
|
||||
let http_auth_token = auth_info
|
||||
.as_ref()
|
||||
.map(|(username, password)| BASE64.encode(format!("{username}:{password}")));
|
||||
|
||||
Self {
|
||||
auth_info,
|
||||
http_auth_token,
|
||||
udp_enabled,
|
||||
bind_ip,
|
||||
proxy_selector,
|
||||
resolver,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TcpServerHandler for MixedTcpServerHandler {
|
||||
async fn setup_server_stream(
|
||||
&self,
|
||||
mut server_stream: Box<dyn AsyncStream>,
|
||||
) -> std::io::Result<TcpServerSetupResult> {
|
||||
let mut stream_reader = StreamReader::new_with_buffer_size(400);
|
||||
|
||||
// Peek at first byte to detect protocol
|
||||
let first_byte = stream_reader.peek_u8(&mut server_stream).await?;
|
||||
|
||||
if first_byte == VER_SOCKS5 {
|
||||
// SOCKS5 protocol
|
||||
log::debug!("Mixed handler: detected SOCKS5 protocol");
|
||||
|
||||
let udp_bind_ip = if self.udp_enabled {
|
||||
Some(self.bind_ip)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
setup_socks_server_stream_inner(
|
||||
self.auth_info.as_ref(),
|
||||
udp_bind_ip,
|
||||
&self.proxy_selector,
|
||||
&self.resolver,
|
||||
server_stream,
|
||||
stream_reader,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
// HTTP protocol
|
||||
log::debug!(
|
||||
"Mixed handler: detected HTTP protocol (first byte: 0x{:02x})",
|
||||
first_byte
|
||||
);
|
||||
|
||||
setup_http_server_stream_inner(
|
||||
self.http_auth_token.as_deref(),
|
||||
server_stream,
|
||||
stream_reader,
|
||||
self.proxy_selector.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//! H2MultiStream - H2 stream for multiplexed connections
|
||||
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use bytes::Bytes;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
|
||||
use crate::async_stream::{AsyncPing, AsyncStream};
|
||||
|
||||
/// H2 stream for multiplexed connections.
|
||||
/// The connection driver is managed by NaiveServerSession and stays alive
|
||||
/// as long as any stream is active.
|
||||
pub struct H2MultiStream {
|
||||
send: h2::SendStream<Bytes>,
|
||||
recv: h2::RecvStream,
|
||||
/// Buffered received data
|
||||
recv_buf: Bytes,
|
||||
/// Whether we've sent END_STREAM
|
||||
shutdown_sent: bool,
|
||||
}
|
||||
|
||||
impl H2MultiStream {
|
||||
pub fn new(send: h2::SendStream<Bytes>, recv: h2::RecvStream) -> Self {
|
||||
Self {
|
||||
send,
|
||||
recv,
|
||||
recv_buf: Bytes::new(),
|
||||
shutdown_sent: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for H2MultiStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
if !self.recv_buf.is_empty() {
|
||||
let to_copy = self.recv_buf.len().min(buf.remaining());
|
||||
buf.put_slice(&self.recv_buf[..to_copy]);
|
||||
self.recv_buf = self.recv_buf.slice(to_copy..);
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
match Pin::new(&mut self.recv).poll_data(cx) {
|
||||
Poll::Ready(Some(Ok(data))) => {
|
||||
// Release capacity so sender can continue
|
||||
let _ = self.recv.flow_control().release_capacity(data.len());
|
||||
|
||||
let to_copy = data.len().min(buf.remaining());
|
||||
buf.put_slice(&data[..to_copy]);
|
||||
|
||||
if to_copy < data.len() {
|
||||
self.recv_buf = data.slice(to_copy..);
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => Poll::Ready(Err(io::Error::other(e))),
|
||||
Poll::Ready(None) => Poll::Ready(Ok(())),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for H2MultiStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
// Only reserve capacity when needed to avoid resetting h2's internal state on re-polls
|
||||
if self.send.capacity() == 0 {
|
||||
self.send.reserve_capacity(buf.len());
|
||||
}
|
||||
|
||||
match self.send.poll_capacity(cx) {
|
||||
Poll::Ready(Some(Ok(capacity))) => {
|
||||
let to_send = buf.len().min(capacity);
|
||||
self.send
|
||||
.send_data(Bytes::copy_from_slice(&buf[..to_send]), false)
|
||||
.map_err(io::Error::other)?;
|
||||
Poll::Ready(Ok(to_send))
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => Poll::Ready(Err(io::Error::other(e))),
|
||||
Poll::Ready(None) => Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"H2 stream closed",
|
||||
))),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
// h2 has no per-stream flush; the connection driver handles transmission
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
if !self.shutdown_sent {
|
||||
match self.send.send_data(Bytes::new(), true) {
|
||||
Ok(()) => self.shutdown_sent = true,
|
||||
Err(e) => return Poll::Ready(Err(io::Error::other(e))),
|
||||
}
|
||||
}
|
||||
|
||||
// Don't wait for peer ACK; the connection driver transmits END_STREAM reliably
|
||||
match self.send.poll_reset(cx) {
|
||||
Poll::Ready(_) | Poll::Pending => Poll::Ready(Ok(())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncPing for H2MultiStream {
|
||||
fn supports_ping(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn poll_write_ping(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<bool>> {
|
||||
Poll::Ready(Ok(false))
|
||||
}
|
||||
}
|
||||
|
||||
impl Unpin for H2MultiStream {}
|
||||
|
||||
impl AsyncStream for H2MultiStream {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_h2_multi_stream_is_async_stream() {
|
||||
fn assert_async_stream<T: AsyncStream>() {}
|
||||
assert_async_stream::<H2MultiStream>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_h2_multi_stream_is_unpin() {
|
||||
fn assert_unpin<T: Unpin>() {}
|
||||
assert_unpin::<H2MultiStream>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_h2_multi_stream_is_send_sync() {
|
||||
fn assert_send<T: Send>() {}
|
||||
fn assert_sync<T: Sync>() {}
|
||||
assert_send::<H2MultiStream>();
|
||||
assert_sync::<H2MultiStream>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mod h2_multi_stream;
|
||||
mod naive_client_handler;
|
||||
mod naive_client_session;
|
||||
mod naive_hyper_service;
|
||||
mod naive_padding_stream;
|
||||
mod naive_server_handler;
|
||||
mod user_lookup;
|
||||
|
||||
pub use naive_client_handler::NaiveProxyTcpClientHandler;
|
||||
pub use naive_server_handler::setup_naive_server_stream;
|
||||
pub use user_lookup::UserLookup;
|
||||
@@ -0,0 +1,159 @@
|
||||
//! NaiveProxy client handler with HTTP/2 multiplexing support.
|
||||
//!
|
||||
//! This handler maintains a persistent H2 session and multiplexes all outgoing
|
||||
//! connections over the same underlying TLS connection, matching the behavior
|
||||
//! of the reference NaiveProxy client.
|
||||
//!
|
||||
//! ## Multiplexing Design
|
||||
//!
|
||||
//! Following the h2 crate's pattern (see their benchmarks), `NaiveClientSession`
|
||||
//! is cheaply cloneable because h2's `SendRequest` internally uses `Arc<Mutex<...>>`.
|
||||
//!
|
||||
//! The handler maintains `Arc<Mutex<Option<NaiveClientSession>>>` only for:
|
||||
//! - Lazy initialization (session created on first request)
|
||||
//! - Reconnection (recreate session if connection dies)
|
||||
//!
|
||||
//! Once obtained, the session is cloned and used directly without holding locks,
|
||||
//! enabling concurrent stream creation.
|
||||
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use base64::engine::{Engine as _, general_purpose::STANDARD as BASE64};
|
||||
use log::debug;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::address::NetLocation;
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::tcp::tcp_handler::{TcpClientHandler, TcpClientSetupResult};
|
||||
|
||||
use super::naive_client_session::NaiveClientSession;
|
||||
|
||||
/// NaiveProxy client handler with HTTP/2 multiplexing.
|
||||
///
|
||||
/// Establishes HTTP/2 CONNECT tunnels with padding support, reusing H2 sessions
|
||||
/// across multiple connections for efficient multiplexing.
|
||||
pub struct NaiveProxyTcpClientHandler {
|
||||
/// Base64-encoded credentials for Basic Auth
|
||||
auth_header: String,
|
||||
/// Enable padding
|
||||
padding_enabled: bool,
|
||||
/// Session slot for lazy init and reconnection (session itself is cheap to clone)
|
||||
session: Arc<Mutex<Option<NaiveClientSession>>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for NaiveProxyTcpClientHandler {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("NaiveProxyTcpClientHandler")
|
||||
.field("padding_enabled", &self.padding_enabled)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for NaiveProxyTcpClientHandler {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
auth_header: self.auth_header.clone(),
|
||||
padding_enabled: self.padding_enabled,
|
||||
// Share the same session slot across clones for multiplexing
|
||||
session: Arc::clone(&self.session),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NaiveProxyTcpClientHandler {
|
||||
pub fn new(username: &str, password: &str, padding_enabled: bool) -> Self {
|
||||
let credentials = format!("{}:{}", username, password);
|
||||
let auth_header = format!("Basic {}", BASE64.encode(&credentials));
|
||||
|
||||
Self {
|
||||
auth_header,
|
||||
padding_enabled,
|
||||
session: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TcpClientHandler for NaiveProxyTcpClientHandler {
|
||||
async fn setup_client_tcp_stream(
|
||||
&self,
|
||||
client_stream: Box<dyn AsyncStream>,
|
||||
remote_location: NetLocation,
|
||||
) -> io::Result<TcpClientSetupResult> {
|
||||
let mut session = self.get_or_create_session(client_stream).await?;
|
||||
|
||||
let stream = session
|
||||
.open_stream(&remote_location, &self.auth_header, self.padding_enabled)
|
||||
.await?;
|
||||
|
||||
Ok(TcpClientSetupResult {
|
||||
client_stream: stream,
|
||||
early_data: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl NaiveProxyTcpClientHandler {
|
||||
/// Get an existing session or create a new one, returning a clone.
|
||||
///
|
||||
/// The session is cloned so we can release the lock before calling open_stream.
|
||||
/// Cloning is cheap because h2's SendRequest uses internal Arc.
|
||||
async fn get_or_create_session(
|
||||
&self,
|
||||
client_stream: Box<dyn AsyncStream>,
|
||||
) -> io::Result<NaiveClientSession> {
|
||||
let mut guard = self.session.lock().await;
|
||||
|
||||
if let Some(ref session) = *guard {
|
||||
if session.is_ready() {
|
||||
debug!("NaiveProxy: reusing existing session");
|
||||
return Ok(session.clone());
|
||||
}
|
||||
debug!("NaiveProxy: existing session not ready, creating new session");
|
||||
}
|
||||
|
||||
debug!("NaiveProxy: creating new H2 session for multiplexing");
|
||||
let session = NaiveClientSession::new(client_stream).await?;
|
||||
*guard = Some(session.clone());
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_handler_new_encodes_credentials() {
|
||||
let handler = NaiveProxyTcpClientHandler::new("user", "pass", true);
|
||||
// Base64 of "user:pass" is "dXNlcjpwYXNz"
|
||||
assert_eq!(handler.auth_header, "Basic dXNlcjpwYXNz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handler_new_special_chars_in_credentials() {
|
||||
let handler = NaiveProxyTcpClientHandler::new("user@domain", "p@ss:word!", false);
|
||||
// Verify it encodes without panicking
|
||||
assert!(handler.auth_header.starts_with("Basic "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handler_clone_shares_session_slot() {
|
||||
let handler1 = NaiveProxyTcpClientHandler::new("user", "pass", true);
|
||||
let handler2 = handler1.clone();
|
||||
|
||||
// Both handlers should share the same session slot
|
||||
assert!(Arc::ptr_eq(&handler1.session, &handler2.session));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handler_is_send_sync() {
|
||||
fn assert_send<T: Send>() {}
|
||||
fn assert_sync<T: Sync>() {}
|
||||
assert_send::<NaiveProxyTcpClientHandler>();
|
||||
assert_sync::<NaiveProxyTcpClientHandler>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
//! NaiveProxy client session for HTTP/2 multiplexing.
|
||||
//!
|
||||
//! This module manages a persistent H2 connection that can handle multiple
|
||||
//! concurrent CONNECT streams, enabling true HTTP/2 multiplexing on the client side.
|
||||
//!
|
||||
//! ## Design
|
||||
//!
|
||||
//! `NaiveClientSession` is cheaply cloneable - it wraps h2's `SendRequest` which
|
||||
//! internally uses `Arc<Mutex<...>>` for shared state. This follows the same pattern
|
||||
//! as the h2 crate's own examples and benchmarks.
|
||||
//!
|
||||
//! The handler maintains `Arc<Mutex<Option<NaiveClientSession>>>` only for:
|
||||
//! - Lazy initialization (session created on first request)
|
||||
//! - Reconnection (recreate session if connection dies)
|
||||
//!
|
||||
//! Once a session is obtained, it's cloned and used directly without holding locks.
|
||||
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytes::Bytes;
|
||||
use http::{Method, Request, Version};
|
||||
use log::debug;
|
||||
use rand::Rng;
|
||||
|
||||
use crate::address::{Address, NetLocation};
|
||||
use crate::async_stream::AsyncStream;
|
||||
|
||||
use super::h2_multi_stream::H2MultiStream;
|
||||
use super::naive_padding_stream::{
|
||||
NaivePaddingStream, PaddingDirection, PaddingType, generate_padding_header,
|
||||
};
|
||||
|
||||
/// A client session managing a single H2 connection with multiplexing support.
|
||||
///
|
||||
/// This session maintains a persistent HTTP/2 connection to a NaiveProxy server
|
||||
/// and can create multiple CONNECT streams over the same connection.
|
||||
///
|
||||
/// `NaiveClientSession` is cheaply cloneable - cloning shares the underlying
|
||||
/// H2 connection (via h2's internal `Arc<Mutex<...>>`).
|
||||
pub struct NaiveClientSession {
|
||||
/// The SendRequest handle - has internal Arc, cheap to clone
|
||||
send_request: h2::client::SendRequest<Bytes>,
|
||||
/// Handle to abort the connection driver on drop (shared across clones)
|
||||
driver_handle: Arc<DriverHandle>,
|
||||
}
|
||||
|
||||
/// Wrapper to abort the driver when all session clones are dropped.
|
||||
struct DriverHandle(tokio::task::AbortHandle);
|
||||
|
||||
impl Drop for DriverHandle {
|
||||
fn drop(&mut self) {
|
||||
debug!("NaiveClientSession: all clones dropped, aborting connection driver");
|
||||
self.0.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for NaiveClientSession {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
send_request: self.send_request.clone(),
|
||||
driver_handle: Arc::clone(&self.driver_handle),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NaiveClientSession {
|
||||
/// Create a new client session from an established TLS stream.
|
||||
///
|
||||
/// Performs H2 handshake and spawns the connection driver.
|
||||
pub async fn new(stream: Box<dyn AsyncStream>) -> io::Result<Self> {
|
||||
// Use larger window/frame sizes for better throughput
|
||||
const WINDOW_SIZE: u32 = 16 * 1024 * 1024; // 16 MB
|
||||
const MAX_FRAME_SIZE: u32 = (1 << 24) - 1; // ~16 MB (max allowed by HTTP/2)
|
||||
|
||||
let (send_request, connection) = h2::client::Builder::new()
|
||||
.initial_window_size(WINDOW_SIZE)
|
||||
.initial_connection_window_size(WINDOW_SIZE)
|
||||
.max_frame_size(MAX_FRAME_SIZE)
|
||||
.handshake(stream)
|
||||
.await
|
||||
.map_err(|e| io::Error::other(format!("H2 client handshake failed: {}", e)))?;
|
||||
|
||||
let abort_handle = tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
debug!("NaiveProxy client H2 connection ended: {}", e);
|
||||
}
|
||||
})
|
||||
.abort_handle();
|
||||
|
||||
debug!("NaiveClientSession: H2 handshake complete, session ready for multiplexing");
|
||||
|
||||
Ok(Self {
|
||||
send_request,
|
||||
driver_handle: Arc::new(DriverHandle(abort_handle)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if this session is still usable for new streams.
|
||||
pub fn is_ready(&self) -> bool {
|
||||
// SendRequest::poll_ready would be more accurate, but this is a good heuristic
|
||||
// The actual check happens when we try to send a request
|
||||
true // Optimistic - let send_request fail if not ready
|
||||
}
|
||||
|
||||
/// Open a new CONNECT stream to the specified target.
|
||||
///
|
||||
/// Returns a stream wrapped with padding if enabled.
|
||||
pub async fn open_stream(
|
||||
&mut self,
|
||||
target: &NetLocation,
|
||||
auth_header: &str,
|
||||
padding_enabled: bool,
|
||||
) -> io::Result<Box<dyn AsyncStream>> {
|
||||
let authority = format_authority(target);
|
||||
|
||||
let mut request = Request::builder()
|
||||
.method(Method::CONNECT)
|
||||
.uri(&authority)
|
||||
.version(Version::HTTP_2)
|
||||
.header("proxy-authorization", auth_header);
|
||||
|
||||
if padding_enabled {
|
||||
let padding_len = rand::rng().random_range(16..=32);
|
||||
request = request.header("padding", generate_padding_header(padding_len));
|
||||
request = request.header("padding-type-request", "1, 0");
|
||||
}
|
||||
|
||||
let request = request
|
||||
.body(())
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
|
||||
|
||||
// No ready() call needed - matches h2 benchmarks pattern
|
||||
let (response_future, send_stream) = self
|
||||
.send_request
|
||||
.send_request(request, false)
|
||||
.map_err(|e| io::Error::other(format!("Failed to send CONNECT: {}", e)))?;
|
||||
|
||||
let response = response_future
|
||||
.await
|
||||
.map_err(|e| io::Error::other(format!("CONNECT response error: {}", e)))?;
|
||||
|
||||
debug!(
|
||||
"NaiveClientSession: CONNECT response: status={}, headers={:?}",
|
||||
response.status(),
|
||||
response.headers()
|
||||
);
|
||||
|
||||
if response.status() != http::StatusCode::OK {
|
||||
return Err(io::Error::other(format!(
|
||||
"CONNECT failed with status: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
let padding_type = if padding_enabled {
|
||||
if let Some(reply) = response.headers().get("padding-type-reply") {
|
||||
let reply_str = reply.to_str().unwrap_or("1");
|
||||
reply_str
|
||||
.trim()
|
||||
.parse::<u8>()
|
||||
.ok()
|
||||
.and_then(PaddingType::from_u8)
|
||||
.unwrap_or(PaddingType::Variant1)
|
||||
} else if response.headers().contains_key("padding") {
|
||||
// Backward compat: padding header without type means Variant1
|
||||
PaddingType::Variant1
|
||||
} else {
|
||||
PaddingType::None
|
||||
}
|
||||
} else {
|
||||
PaddingType::None
|
||||
};
|
||||
|
||||
let recv_stream = response.into_body();
|
||||
let h2_stream = H2MultiStream::new(send_stream, recv_stream);
|
||||
|
||||
let client_stream: Box<dyn AsyncStream> = if padding_type != PaddingType::None {
|
||||
Box::new(NaivePaddingStream::new(
|
||||
h2_stream,
|
||||
PaddingDirection::Client,
|
||||
padding_type,
|
||||
))
|
||||
} else {
|
||||
Box::new(h2_stream)
|
||||
};
|
||||
|
||||
debug!("NaiveClientSession: opened stream to {}", target);
|
||||
|
||||
Ok(client_stream)
|
||||
}
|
||||
}
|
||||
|
||||
/// Format authority for CONNECT request
|
||||
fn format_authority(location: &NetLocation) -> String {
|
||||
match location.address() {
|
||||
Address::Ipv6(addr) => format!("[{}]:{}", addr, location.port()),
|
||||
Address::Ipv4(addr) => format!("{}:{}", addr, location.port()),
|
||||
Address::Hostname(host) => format!("{}:{}", host, location.port()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_authority_ipv4() {
|
||||
use std::net::Ipv4Addr;
|
||||
let loc = NetLocation::new(Address::Ipv4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
|
||||
assert_eq!(format_authority(&loc), "192.168.1.1:8080");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_authority_ipv6() {
|
||||
use std::net::Ipv6Addr;
|
||||
let loc = NetLocation::new(Address::Ipv6(Ipv6Addr::LOCALHOST), 443);
|
||||
assert_eq!(format_authority(&loc), "[::1]:443");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_authority_hostname() {
|
||||
let loc = NetLocation::new(Address::Hostname("example.com".to_string()), 443);
|
||||
assert_eq!(format_authority(&loc), "example.com:443");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,608 @@
|
||||
//! Hyper-based NaiveProxy service
|
||||
//!
|
||||
//! This module provides a hyper-based HTTP/2 server for NaiveProxy connections.
|
||||
//! It handles CONNECT requests with padding support and built-in static file fallback.
|
||||
|
||||
use std::convert::Infallible;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytes::Bytes;
|
||||
use http::{Method, Request, Response, StatusCode};
|
||||
use http_body_util::{BodyExt, Empty, Full, combinators::BoxBody};
|
||||
use hyper::body::Incoming;
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use log::debug;
|
||||
use rand::Rng;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::address::{Address, NetLocation};
|
||||
use crate::async_stream::{AsyncMessageStream, AsyncStream};
|
||||
use crate::client_proxy_selector::ClientProxySelector;
|
||||
use crate::crypto::CryptoTlsStream;
|
||||
use crate::resolver::Resolver;
|
||||
use crate::routing::{ServerStream, run_udp_routing};
|
||||
use crate::socks_handler::read_location_direct;
|
||||
use crate::tcp::tcp_handler::TcpServerSetupResult;
|
||||
use crate::tcp::tcp_server::run_udp_copy;
|
||||
use crate::tls_server_handler::NaiveConfig;
|
||||
use crate::uot::{UOT_V1_MAGIC_ADDRESS, UOT_V2_MAGIC_ADDRESS, UotV1ServerStream, UotV2Stream};
|
||||
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::naive_padding_stream::{
|
||||
NaivePaddingStream, PaddingDirection, PaddingType, generate_padding_header,
|
||||
parse_padding_type_request,
|
||||
};
|
||||
use super::user_lookup::UserLookup;
|
||||
|
||||
/// Wrapper for hyper's upgraded stream that implements AsyncStream.
|
||||
///
|
||||
/// This is needed because `TokioIo<Upgraded>` doesn't implement `AsyncStream`
|
||||
/// (which requires `Sync`), but we need `AsyncStream` for UoT stream wrappers.
|
||||
struct HyperUpgradedStream(TokioIo<hyper::upgrade::Upgraded>);
|
||||
|
||||
impl tokio::io::AsyncRead for HyperUpgradedStream {
|
||||
fn poll_read(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
std::pin::Pin::new(&mut self.0).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl tokio::io::AsyncWrite for HyperUpgradedStream {
|
||||
fn poll_write(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<io::Result<usize>> {
|
||||
std::pin::Pin::new(&mut self.0).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
std::pin::Pin::new(&mut self.0).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
std::pin::Pin::new(&mut self.0).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::async_stream::AsyncPing for HyperUpgradedStream {
|
||||
fn supports_ping(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn poll_write_ping(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<io::Result<bool>> {
|
||||
std::task::Poll::Ready(Ok(false))
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: The underlying hyper Upgraded stream is used only from async contexts
|
||||
// in a single-threaded manner per connection. The Sync bound is required by
|
||||
// AsyncStream but the stream is never actually shared across threads.
|
||||
unsafe impl Sync for HyperUpgradedStream {}
|
||||
|
||||
impl AsyncStream for HyperUpgradedStream {}
|
||||
|
||||
/// Service configuration for hyper NaiveProxy handler
|
||||
struct NaiveServiceConfig {
|
||||
users: Arc<UserLookup>,
|
||||
fallback_path: Option<PathBuf>,
|
||||
resolver: Arc<dyn Resolver>,
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
udp_enabled: bool,
|
||||
padding_enabled: bool,
|
||||
}
|
||||
|
||||
fn empty_body() -> BoxBody<Bytes, io::Error> {
|
||||
Empty::<Bytes>::new()
|
||||
.map_err(|never| match never {})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn full_body(data: Bytes) -> BoxBody<Bytes, io::Error> {
|
||||
Full::new(data).map_err(|never| match never {}).boxed()
|
||||
}
|
||||
|
||||
/// Run the hyper-based NaiveProxy service
|
||||
///
|
||||
/// This is an internal function called by `setup_naive_server_stream` after
|
||||
/// determining the HTTP version to use.
|
||||
pub(super) async fn run_naive_hyper_service<IO: AsyncStream + 'static>(
|
||||
tls_stream: CryptoTlsStream<IO>,
|
||||
naive_cfg: &NaiveConfig,
|
||||
effective_selector: Arc<ClientProxySelector>,
|
||||
resolver: Arc<dyn Resolver>,
|
||||
use_h2: bool,
|
||||
) -> io::Result<TcpServerSetupResult> {
|
||||
let io = TokioIo::new(tls_stream);
|
||||
|
||||
let service_config = Arc::new(NaiveServiceConfig {
|
||||
users: naive_cfg.users.clone(),
|
||||
fallback_path: naive_cfg.fallback_path.clone(),
|
||||
resolver,
|
||||
proxy_selector: effective_selector,
|
||||
udp_enabled: naive_cfg.udp_enabled,
|
||||
padding_enabled: naive_cfg.padding_enabled,
|
||||
});
|
||||
|
||||
if use_h2 {
|
||||
// HTTP/2 for NaiveProxy clients
|
||||
tokio::spawn(async move {
|
||||
let service = hyper::service::service_fn(move |req| {
|
||||
let config = service_config.clone();
|
||||
async move { naive_service(req, config).await }
|
||||
});
|
||||
|
||||
// Use larger window/frame sizes for better throughput
|
||||
const WINDOW_SIZE: u32 = 16 * 1024 * 1024; // 16 MB
|
||||
const MAX_FRAME_SIZE: u32 = (1 << 24) - 1; // ~16 MB (max allowed by HTTP/2)
|
||||
|
||||
let result = hyper::server::conn::http2::Builder::new(TokioExecutor::new())
|
||||
.auto_date_header(false)
|
||||
.initial_stream_window_size(WINDOW_SIZE)
|
||||
.initial_connection_window_size(WINDOW_SIZE)
|
||||
.max_frame_size(MAX_FRAME_SIZE)
|
||||
.serve_connection(io, service)
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
debug!("Naive HTTP/2 connection error: {}", e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// HTTP/1.1 for browsers and censors - serve static files only, no proxy
|
||||
let fallback_path = naive_cfg.fallback_path.clone();
|
||||
tokio::spawn(async move {
|
||||
let service = hyper::service::service_fn(move |req| {
|
||||
let path = fallback_path.clone();
|
||||
async move { http1_fallback_service(req, path).await }
|
||||
});
|
||||
|
||||
let result = hyper::server::conn::http1::Builder::new()
|
||||
.auto_date_header(false)
|
||||
.serve_connection(io, service)
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
debug!("Naive HTTP/1.1 fallback error: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(TcpServerSetupResult::AlreadyHandled)
|
||||
}
|
||||
|
||||
/// HTTP/1.1 fallback service - only serves static files, no proxy functionality
|
||||
async fn http1_fallback_service(
|
||||
req: Request<Incoming>,
|
||||
fallback_path: Option<PathBuf>,
|
||||
) -> Result<Response<BoxBody<Bytes, io::Error>>, Infallible> {
|
||||
match *req.method() {
|
||||
Method::GET | Method::HEAD => {
|
||||
let path = req.uri().path();
|
||||
let is_head = req.method() == Method::HEAD;
|
||||
debug!("NaiveProxy HTTP/1.1: serving fallback for {}", path);
|
||||
serve_fallback(path, &fallback_path, is_head).await
|
||||
}
|
||||
Method::OPTIONS => Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("allow", "GET, HEAD, OPTIONS")
|
||||
.body(empty_body())
|
||||
.unwrap()),
|
||||
_ => Ok(Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(empty_body())
|
||||
.unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Main NaiveProxy service handler for HTTP/2 (hyper)
|
||||
async fn naive_service(
|
||||
mut req: Request<Incoming>,
|
||||
config: Arc<NaiveServiceConfig>,
|
||||
) -> Result<Response<BoxBody<Bytes, io::Error>>, Infallible> {
|
||||
match *req.method() {
|
||||
Method::CONNECT => {}
|
||||
Method::GET | Method::HEAD => {
|
||||
let is_head = req.method() == Method::HEAD;
|
||||
debug!(
|
||||
"NaiveProxy HTTP/2: serving fallback for {}",
|
||||
req.uri().path()
|
||||
);
|
||||
return serve_fallback(req.uri().path(), &config.fallback_path, is_head).await;
|
||||
}
|
||||
Method::OPTIONS => {
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("allow", "GET, HEAD, OPTIONS")
|
||||
.body(empty_body())
|
||||
.unwrap());
|
||||
}
|
||||
_ => {
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(empty_body())
|
||||
.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
// Return 400 for anything that might reveal proxy support
|
||||
let has_padding = req.headers().get("padding").is_some();
|
||||
if !has_padding && config.padding_enabled {
|
||||
debug!("NaiveProxy: missing padding header, returning 400");
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(empty_body())
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
let username = match req.headers().get("proxy-authorization") {
|
||||
Some(auth) => match auth.to_str().ok().and_then(|s| config.users.validate(s)) {
|
||||
Some(user) => user.to_string(),
|
||||
None => {
|
||||
debug!("NaiveProxy: invalid credentials, returning 400");
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(empty_body())
|
||||
.unwrap());
|
||||
}
|
||||
},
|
||||
None => {
|
||||
debug!("NaiveProxy: missing auth header, returning 400");
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(empty_body())
|
||||
.unwrap());
|
||||
}
|
||||
};
|
||||
|
||||
let destination = match parse_connect_destination(&req) {
|
||||
Some(dest) => dest,
|
||||
None => {
|
||||
log::warn!("NaiveProxy: invalid CONNECT destination");
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(empty_body())
|
||||
.unwrap());
|
||||
}
|
||||
};
|
||||
|
||||
debug!("[{}] NaiveProxy CONNECT to {}", username, destination);
|
||||
|
||||
let padding_type = if config.padding_enabled && has_padding {
|
||||
if let Some(types) = req.headers().get("padding-type-request") {
|
||||
let types_str = types.to_str().unwrap_or("1");
|
||||
parse_padding_type_request(types_str)
|
||||
.into_iter()
|
||||
.find(|&t| t == PaddingType::Variant1)
|
||||
.unwrap_or(PaddingType::Variant1)
|
||||
} else {
|
||||
PaddingType::Variant1
|
||||
}
|
||||
} else {
|
||||
PaddingType::None
|
||||
};
|
||||
|
||||
// Get upgrade future before moving the request
|
||||
let on_upgrade = hyper::upgrade::on(&mut req);
|
||||
let resolver = config.resolver.clone();
|
||||
let proxy_selector = config.proxy_selector.clone();
|
||||
let udp_enabled = config.udp_enabled;
|
||||
|
||||
tokio::spawn(async move {
|
||||
match on_upgrade.await {
|
||||
Ok(upgraded) => {
|
||||
let io = HyperUpgradedStream(TokioIo::new(upgraded));
|
||||
|
||||
if padding_type != PaddingType::None {
|
||||
let stream =
|
||||
NaivePaddingStream::new(io, PaddingDirection::Server, padding_type);
|
||||
if let Err(e) = handle_naive_stream(
|
||||
stream,
|
||||
destination,
|
||||
resolver,
|
||||
proxy_selector,
|
||||
udp_enabled,
|
||||
&username,
|
||||
)
|
||||
.await
|
||||
{
|
||||
debug!("NaiveProxy tunnel error: {}", e);
|
||||
}
|
||||
} else if let Err(e) = handle_naive_stream(
|
||||
io,
|
||||
destination,
|
||||
resolver,
|
||||
proxy_selector,
|
||||
udp_enabled,
|
||||
&username,
|
||||
)
|
||||
.await
|
||||
{
|
||||
debug!("NaiveProxy tunnel error: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("NaiveProxy upgrade failed: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut response = Response::builder().status(StatusCode::OK);
|
||||
|
||||
if padding_type != PaddingType::None {
|
||||
let padding_len = rand::rng().random_range(30..=62);
|
||||
response = response.header("padding", generate_padding_header(padding_len));
|
||||
response = response.header("padding-type-reply", (padding_type as u8).to_string());
|
||||
}
|
||||
|
||||
Ok(response.body(empty_body()).unwrap())
|
||||
}
|
||||
|
||||
fn parse_connect_destination(req: &Request<Incoming>) -> Option<NetLocation> {
|
||||
let authority = req.uri().authority()?;
|
||||
parse_authority(authority.as_str()).ok()
|
||||
}
|
||||
|
||||
/// Parse authority string (host:port) into NetLocation
|
||||
fn parse_authority(authority: &str) -> io::Result<NetLocation> {
|
||||
// Handle IPv6: [::1]:443
|
||||
if authority.starts_with('[') {
|
||||
let end_bracket = authority
|
||||
.find(']')
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid IPv6 address"))?;
|
||||
|
||||
let host = &authority[1..end_bracket];
|
||||
|
||||
let port =
|
||||
if authority.len() > end_bracket + 1 && authority.as_bytes()[end_bracket + 1] == b':' {
|
||||
authority[end_bracket + 2..].parse::<u16>().map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, format!("Invalid port: {}", e))
|
||||
})?
|
||||
} else {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Missing port"));
|
||||
};
|
||||
|
||||
let addr = host.parse().map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, format!("Invalid IPv6: {}", e))
|
||||
})?;
|
||||
|
||||
return Ok(NetLocation::new(Address::Ipv6(addr), port));
|
||||
}
|
||||
|
||||
// Handle host:port
|
||||
let colon = authority
|
||||
.rfind(':')
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Missing port"))?;
|
||||
|
||||
let host = &authority[..colon];
|
||||
let port = authority[colon + 1..]
|
||||
.parse::<u16>()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("Invalid port: {}", e)))?;
|
||||
|
||||
let address = Address::from(host)?;
|
||||
Ok(NetLocation::new(address, port))
|
||||
}
|
||||
|
||||
/// Serve static files or return 401 Unauthorized
|
||||
async fn serve_fallback(
|
||||
uri_path: &str,
|
||||
fallback_path: &Option<PathBuf>,
|
||||
is_head: bool,
|
||||
) -> Result<Response<BoxBody<Bytes, io::Error>>, Infallible> {
|
||||
let Some(base_path) = fallback_path else {
|
||||
// Return 401 instead of 407 to avoid revealing proxy
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::UNAUTHORIZED)
|
||||
.body(empty_body())
|
||||
.unwrap());
|
||||
};
|
||||
|
||||
// Sanitize path to prevent directory traversal
|
||||
let request_path = uri_path.trim_start_matches('/');
|
||||
let mut file_path = base_path.clone();
|
||||
|
||||
for component in std::path::Path::new(request_path).components() {
|
||||
match component {
|
||||
std::path::Component::Normal(c) => file_path.push(c),
|
||||
std::path::Component::ParentDir => {
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::FORBIDDEN)
|
||||
.body(empty_body())
|
||||
.unwrap());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if file_path.is_dir() {
|
||||
file_path.push("index.html");
|
||||
}
|
||||
|
||||
match tokio::fs::read(&file_path).await {
|
||||
Ok(contents) => {
|
||||
let mime = mime_guess::from_path(&file_path)
|
||||
.first_or_octet_stream()
|
||||
.to_string();
|
||||
|
||||
let body = if is_head {
|
||||
empty_body()
|
||||
} else {
|
||||
full_body(Bytes::from(contents.clone()))
|
||||
};
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("content-type", mime)
|
||||
.header("content-length", contents.len())
|
||||
.body(body)
|
||||
.unwrap())
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(empty_body())
|
||||
.unwrap()),
|
||||
Err(_) => Ok(Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(empty_body())
|
||||
.unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a single NaiveProxy stream after setup
|
||||
///
|
||||
/// This handles both TCP and UDP-over-TCP (UoT) connections.
|
||||
async fn handle_naive_stream<S: AsyncStream + 'static>(
|
||||
mut stream: S,
|
||||
remote_location: NetLocation,
|
||||
resolver: Arc<dyn Resolver>,
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
udp_enabled: bool,
|
||||
user_name: &str,
|
||||
) -> io::Result<()> {
|
||||
use crate::client_proxy_selector::ConnectDecision;
|
||||
|
||||
if let Address::Hostname(host) = remote_location.address() {
|
||||
if host == UOT_V1_MAGIC_ADDRESS {
|
||||
if !udp_enabled {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"UDP-over-TCP not enabled",
|
||||
));
|
||||
}
|
||||
|
||||
debug!("NaiveProxy stream (user: {}): UoT V1 mode", user_name);
|
||||
let uot_stream = UotV1ServerStream::new(stream);
|
||||
|
||||
return run_udp_routing(
|
||||
ServerStream::Targeted(Box::new(uot_stream)),
|
||||
proxy_selector,
|
||||
resolver,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
} else if host == UOT_V2_MAGIC_ADDRESS {
|
||||
if !udp_enabled {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"UDP-over-TCP not enabled",
|
||||
));
|
||||
}
|
||||
|
||||
// UoT V2 header: destination uses SOCKS5 address format
|
||||
let is_connect = stream.read_u8().await?;
|
||||
let destination = read_location_direct(&mut stream).await?;
|
||||
|
||||
debug!(
|
||||
"NaiveProxy stream (user: {}): UoT V2 connect={} -> {}",
|
||||
user_name, is_connect, destination
|
||||
);
|
||||
|
||||
if is_connect == 1 {
|
||||
let uot_v2_stream = UotV2Stream::new(stream);
|
||||
|
||||
let action = proxy_selector.judge(destination.clone(), &resolver).await?;
|
||||
|
||||
match action {
|
||||
ConnectDecision::Allow {
|
||||
chain_group,
|
||||
remote_location,
|
||||
} => {
|
||||
let client_stream = chain_group
|
||||
.connect_udp_bidirectional(&resolver, remote_location)
|
||||
.await?;
|
||||
|
||||
return run_udp_copy(
|
||||
Box::new(uot_v2_stream) as Box<dyn AsyncMessageStream>,
|
||||
client_stream,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ConnectDecision::Block => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::ConnectionRefused,
|
||||
"UDP blocked by rules",
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// V2 non-connect mode (same as V1)
|
||||
let uot_stream = UotV1ServerStream::new(stream);
|
||||
|
||||
return run_udp_routing(
|
||||
ServerStream::Targeted(Box::new(uot_stream)),
|
||||
proxy_selector,
|
||||
resolver,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!(
|
||||
"NaiveProxy stream (user: {}): TCP -> {}",
|
||||
user_name, remote_location
|
||||
);
|
||||
|
||||
let action = proxy_selector
|
||||
.judge(remote_location.clone(), &resolver)
|
||||
.await?;
|
||||
|
||||
let mut client_stream: Box<dyn AsyncStream> = match action {
|
||||
ConnectDecision::Allow {
|
||||
chain_group,
|
||||
remote_location,
|
||||
} => {
|
||||
let result = chain_group.connect_tcp(remote_location, &resolver).await?;
|
||||
result.client_stream
|
||||
}
|
||||
ConnectDecision::Block => {
|
||||
debug!("NaiveProxy: connection blocked by rules");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Use larger buffers for better throughput (default 8KB is too small)
|
||||
const COPY_BUF_SIZE: usize = 256 * 1024;
|
||||
let result = tokio::io::copy_bidirectional_with_sizes(
|
||||
&mut stream,
|
||||
&mut client_stream,
|
||||
COPY_BUF_SIZE,
|
||||
COPY_BUF_SIZE,
|
||||
)
|
||||
.await;
|
||||
|
||||
let _ = stream.shutdown().await;
|
||||
let _ = client_stream.shutdown().await;
|
||||
|
||||
match result {
|
||||
Ok((to_client, to_remote)) => {
|
||||
debug!(
|
||||
"NaiveProxy stream (user: {}): done, {} bytes to client, {} bytes to remote",
|
||||
user_name, to_client, to_remote
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("NaiveProxy stream (user: {}): error: {}", user_name, e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,925 @@
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use rand::Rng;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
|
||||
use crate::async_stream::{AsyncPing, AsyncStream};
|
||||
use crate::util::allocate_vec;
|
||||
|
||||
/// Number of frames to pad in each direction
|
||||
pub const NUM_FIRST_PADDINGS: usize = 8;
|
||||
|
||||
/// Maximum padding size per frame
|
||||
pub const MAX_PADDING_SIZE: u8 = 255;
|
||||
|
||||
/// Padding frame header size (2 bytes payload length + 1 byte padding length)
|
||||
pub const PADDING_HEADER_SIZE: usize = 3;
|
||||
|
||||
/// Maximum payload size (limited by 2-byte length field)
|
||||
pub const MAX_PAYLOAD_SIZE: usize = 65535;
|
||||
|
||||
/// Maximum frame size including header and padding
|
||||
const MAX_FRAME_SIZE: usize = PADDING_HEADER_SIZE + MAX_PAYLOAD_SIZE + MAX_PADDING_SIZE as usize;
|
||||
|
||||
/// Characters that won't be Huffman-coded efficiently by HPACK (>= 8 bits)
|
||||
/// These are the first 17 ASCII printable chars with Huffman code length >= 8
|
||||
/// from RFC 7541 Appendix B, matching the reference naiveproxy implementation.
|
||||
/// Order: !"#$&'()*+,;<>?@X
|
||||
pub const NONINDEX_CHARS: &[u8] = b"!\"#$&'()*+,;<>?@X";
|
||||
|
||||
/// Padding type negotiation
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PaddingType {
|
||||
/// No padding
|
||||
None = 0,
|
||||
/// Variant 1: 3-byte header (payload_len_be16 + padding_len_u8)
|
||||
Variant1 = 1,
|
||||
}
|
||||
|
||||
impl PaddingType {
|
||||
pub fn from_u8(v: u8) -> Option<Self> {
|
||||
match v {
|
||||
0 => Some(PaddingType::None),
|
||||
1 => Some(PaddingType::Variant1),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Direction of the connection (kept for API compatibility).
|
||||
/// Note: Padding behavior is now uniform for both directions per NaiveProxy spec.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PaddingDirection {
|
||||
/// Client side
|
||||
Client,
|
||||
/// Server side
|
||||
Server,
|
||||
}
|
||||
|
||||
/// Generate a random padding header value for HTTP headers.
|
||||
/// Uses characters that won't be Huffman-coded efficiently by HPACK.
|
||||
/// Matches the reference naiveproxy FillNonindexHeaderValue implementation.
|
||||
pub fn generate_padding_header(len: usize) -> String {
|
||||
let mut rng = rand::rng();
|
||||
let mut buf = Vec::with_capacity(len);
|
||||
|
||||
// First 16 chars use 4 bits of entropy each to select from first 16 chars
|
||||
for _ in 0..len.min(16) {
|
||||
let idx = rng.random_range(0..16);
|
||||
buf.push(NONINDEX_CHARS[idx]);
|
||||
}
|
||||
|
||||
// Remaining chars use the 17th character (index 16 = 'X')
|
||||
for _ in 16..len {
|
||||
buf.push(NONINDEX_CHARS[16]); // 'X'
|
||||
}
|
||||
|
||||
// Safe because NONINDEX_CHARS are all ASCII
|
||||
String::from_utf8(buf).unwrap()
|
||||
}
|
||||
|
||||
/// Parse Padding-Type-Request header value.
|
||||
/// Returns list of supported types in preference order.
|
||||
pub fn parse_padding_type_request(value: &str) -> Vec<PaddingType> {
|
||||
value
|
||||
.split(',')
|
||||
.filter_map(|s| s.trim().parse::<u8>().ok())
|
||||
.filter_map(PaddingType::from_u8)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Threshold for small payload bias on server side
|
||||
const SMALL_PAYLOAD_THRESHOLD: usize = 100;
|
||||
/// Threshold for medium payload fragmentation (lower bound)
|
||||
const MEDIUM_PAYLOAD_LOW: usize = 400;
|
||||
/// Threshold for medium payload fragmentation (upper bound)
|
||||
const MEDIUM_PAYLOAD_HIGH: usize = 1024;
|
||||
/// Minimum fragment size for server-side write fragmentation
|
||||
const FRAGMENT_SIZE_MIN: usize = 200;
|
||||
/// Maximum fragment size for server-side write fragmentation
|
||||
const FRAGMENT_SIZE_MAX: usize = 300;
|
||||
|
||||
/// Wrapper stream that applies NaiveProxy padding.
|
||||
///
|
||||
/// Pads the first 8 reads and writes in each direction to obscure
|
||||
/// traffic fingerprints from initial handshakes.
|
||||
pub struct NaivePaddingStream<S> {
|
||||
inner: S,
|
||||
padding_type: PaddingType,
|
||||
direction: PaddingDirection,
|
||||
/// Number of frames read so far
|
||||
num_read_frames: usize,
|
||||
/// Number of frames written so far
|
||||
num_written_frames: usize,
|
||||
/// Pre-allocated buffer for incoming frame data
|
||||
read_buffer: Box<[u8]>,
|
||||
read_buffer_start: usize,
|
||||
read_buffer_end: usize,
|
||||
/// Pending frame info (payload_len, padding_len) when we've parsed header but need more data
|
||||
pending_frame: Option<(usize, usize)>,
|
||||
/// Pre-allocated buffer for decoded payload
|
||||
decoded_buffer: Box<[u8]>,
|
||||
decoded_start: usize,
|
||||
decoded_end: usize,
|
||||
/// Pre-allocated buffer for encoded frame writes
|
||||
write_buffer: Box<[u8]>,
|
||||
write_start: usize,
|
||||
write_end: usize,
|
||||
/// Original payload length for current write (to return correct count)
|
||||
write_payload_len: usize,
|
||||
/// Current fragment limit for server-side write fragmentation (0 = no limit)
|
||||
write_fragment_limit: usize,
|
||||
}
|
||||
|
||||
impl<S> NaivePaddingStream<S> {
|
||||
pub fn new(inner: S, direction: PaddingDirection, padding_type: PaddingType) -> Self {
|
||||
// Pre-allocate buffers sized for maximum frame
|
||||
let read_buffer = allocate_vec(MAX_FRAME_SIZE).into_boxed_slice();
|
||||
let decoded_buffer = allocate_vec(MAX_PAYLOAD_SIZE).into_boxed_slice();
|
||||
let write_buffer = allocate_vec(MAX_FRAME_SIZE).into_boxed_slice();
|
||||
|
||||
Self {
|
||||
inner,
|
||||
padding_type,
|
||||
direction,
|
||||
num_read_frames: 0,
|
||||
num_written_frames: 0,
|
||||
read_buffer,
|
||||
read_buffer_start: 0,
|
||||
read_buffer_end: 0,
|
||||
pending_frame: None,
|
||||
decoded_buffer,
|
||||
decoded_start: 0,
|
||||
decoded_end: 0,
|
||||
write_buffer,
|
||||
write_start: 0,
|
||||
write_end: 0,
|
||||
write_payload_len: 0,
|
||||
write_fragment_limit: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if we should still be padding reads
|
||||
#[inline]
|
||||
fn should_pad_reads(&self) -> bool {
|
||||
self.padding_type != PaddingType::None && self.num_read_frames < NUM_FIRST_PADDINGS
|
||||
}
|
||||
|
||||
/// Check if we should still be padding writes
|
||||
#[inline]
|
||||
fn should_pad_writes(&self) -> bool {
|
||||
self.padding_type != PaddingType::None && self.num_written_frames < NUM_FIRST_PADDINGS
|
||||
}
|
||||
|
||||
/// Generate random padding size for a write.
|
||||
/// Server-side biases padding towards larger values for small payloads
|
||||
/// to prevent fingerprinting of small response packets.
|
||||
fn generate_padding_size(&self, payload_len: usize) -> u8 {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
if self.direction == PaddingDirection::Server && payload_len < SMALL_PAYLOAD_THRESHOLD {
|
||||
// For small payloads on server side, bias towards larger padding
|
||||
// Range: [255 - payload_len, 255]
|
||||
let min_padding = (MAX_PADDING_SIZE as usize).saturating_sub(payload_len);
|
||||
rng.random_range(min_padding as u8..=MAX_PADDING_SIZE)
|
||||
} else {
|
||||
// Uniform distribution for client or larger payloads
|
||||
rng.random_range(0..=MAX_PADDING_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute fragment limit for server-side write fragmentation.
|
||||
/// Returns 0 if no fragmentation needed, otherwise returns the max bytes to write.
|
||||
fn compute_fragment_limit(&self, payload_len: usize, remaining: usize) -> usize {
|
||||
if self.direction == PaddingDirection::Server
|
||||
&& payload_len > MEDIUM_PAYLOAD_LOW
|
||||
&& payload_len < MEDIUM_PAYLOAD_HIGH
|
||||
{
|
||||
// Fragment medium-sized server writes to 200-300 bytes
|
||||
let limit = rand::rng().random_range(FRAGMENT_SIZE_MIN..=FRAGMENT_SIZE_MAX);
|
||||
remaining.min(limit)
|
||||
} else {
|
||||
remaining
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode a payload into the write buffer as a padded frame.
|
||||
fn encode_frame(&mut self, payload: &[u8]) -> io::Result<()> {
|
||||
if payload.len() > MAX_PAYLOAD_SIZE {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"Payload too large: {} > {}",
|
||||
payload.len(),
|
||||
MAX_PAYLOAD_SIZE
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let padding_size = self.generate_padding_size(payload.len());
|
||||
let payload_len = payload.len() as u16;
|
||||
let frame_size = PADDING_HEADER_SIZE + payload.len() + padding_size as usize;
|
||||
|
||||
// Write header: payload_len (2 bytes BE) + padding_size (1 byte)
|
||||
self.write_buffer[0] = (payload_len >> 8) as u8;
|
||||
self.write_buffer[1] = (payload_len & 0xff) as u8;
|
||||
self.write_buffer[2] = padding_size;
|
||||
|
||||
// Write payload
|
||||
self.write_buffer[PADDING_HEADER_SIZE..PADDING_HEADER_SIZE + payload.len()]
|
||||
.copy_from_slice(payload);
|
||||
|
||||
// Write padding (zeros)
|
||||
let padding_start = PADDING_HEADER_SIZE + payload.len();
|
||||
self.write_buffer[padding_start..padding_start + padding_size as usize].fill(0);
|
||||
|
||||
self.write_start = 0;
|
||||
self.write_end = frame_size;
|
||||
self.write_payload_len = payload.len();
|
||||
|
||||
// Compute initial fragment limit for server-side fragmentation
|
||||
self.write_fragment_limit = self.compute_fragment_limit(payload.len(), frame_size);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reset read buffer offset by moving remaining data to the front.
|
||||
#[inline]
|
||||
fn reset_read_buffer_offset(&mut self) {
|
||||
if self.read_buffer_start > 0 && self.read_buffer_end > self.read_buffer_start {
|
||||
self.read_buffer
|
||||
.copy_within(self.read_buffer_start..self.read_buffer_end, 0);
|
||||
self.read_buffer_end -= self.read_buffer_start;
|
||||
self.read_buffer_start = 0;
|
||||
} else if self.read_buffer_start == self.read_buffer_end {
|
||||
self.read_buffer_start = 0;
|
||||
self.read_buffer_end = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to decode a frame from buffered data.
|
||||
/// Returns true if a complete frame was decoded into decoded_buffer.
|
||||
#[inline]
|
||||
fn try_decode_frame(&mut self) -> bool {
|
||||
let available = self.read_buffer_end - self.read_buffer_start;
|
||||
|
||||
// Check if we have a pending frame (header already parsed)
|
||||
let (payload_len, padding_len) = match self.pending_frame {
|
||||
Some((pl, pd)) => (pl, pd),
|
||||
None => {
|
||||
// Need to parse header first
|
||||
if available < PADDING_HEADER_SIZE {
|
||||
return false;
|
||||
}
|
||||
|
||||
let payload_len = u16::from_be_bytes([
|
||||
self.read_buffer[self.read_buffer_start],
|
||||
self.read_buffer[self.read_buffer_start + 1],
|
||||
]) as usize;
|
||||
let padding_len = self.read_buffer[self.read_buffer_start + 2] as usize;
|
||||
|
||||
self.read_buffer_start += PADDING_HEADER_SIZE;
|
||||
self.pending_frame = Some((payload_len, padding_len));
|
||||
|
||||
(payload_len, padding_len)
|
||||
}
|
||||
};
|
||||
|
||||
// Check if we have enough data for payload + padding
|
||||
let frame_data_len = payload_len + padding_len;
|
||||
let available = self.read_buffer_end - self.read_buffer_start;
|
||||
|
||||
if available < frame_data_len {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Copy payload to decoded buffer (skip padding)
|
||||
self.decoded_buffer[0..payload_len].copy_from_slice(
|
||||
&self.read_buffer[self.read_buffer_start..self.read_buffer_start + payload_len],
|
||||
);
|
||||
self.decoded_start = 0;
|
||||
self.decoded_end = payload_len;
|
||||
|
||||
// Advance past payload + padding
|
||||
self.read_buffer_start += frame_data_len;
|
||||
self.pending_frame = None;
|
||||
self.num_read_frames += 1;
|
||||
|
||||
// Reset offsets if buffer is empty
|
||||
if self.read_buffer_start == self.read_buffer_end {
|
||||
self.read_buffer_start = 0;
|
||||
self.read_buffer_end = 0;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsyncRead + Unpin> AsyncRead for NaivePaddingStream<S> {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
let this = &mut *self;
|
||||
|
||||
if this.decoded_end > this.decoded_start {
|
||||
let available = this.decoded_end - this.decoded_start;
|
||||
let to_copy = available.min(buf.remaining());
|
||||
buf.put_slice(&this.decoded_buffer[this.decoded_start..this.decoded_start + to_copy]);
|
||||
this.decoded_start += to_copy;
|
||||
|
||||
if this.decoded_start >= this.decoded_end {
|
||||
this.decoded_start = 0;
|
||||
this.decoded_end = 0;
|
||||
}
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
if !this.should_pad_reads() {
|
||||
// Return leftover buffered data from padding phase before switching to passthrough
|
||||
if this.read_buffer_end > this.read_buffer_start {
|
||||
let available = this.read_buffer_end - this.read_buffer_start;
|
||||
let to_copy = available.min(buf.remaining());
|
||||
buf.put_slice(
|
||||
&this.read_buffer[this.read_buffer_start..this.read_buffer_start + to_copy],
|
||||
);
|
||||
this.read_buffer_start += to_copy;
|
||||
|
||||
if this.read_buffer_start >= this.read_buffer_end {
|
||||
this.read_buffer_start = 0;
|
||||
this.read_buffer_end = 0;
|
||||
}
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
return Pin::new(&mut this.inner).poll_read(cx, buf);
|
||||
}
|
||||
|
||||
// Decode frames, skipping pure padding frames (payload_len == 0)
|
||||
loop {
|
||||
if this.try_decode_frame() {
|
||||
if this.decoded_end > this.decoded_start {
|
||||
let available = this.decoded_end - this.decoded_start;
|
||||
let to_copy = available.min(buf.remaining());
|
||||
buf.put_slice(
|
||||
&this.decoded_buffer[this.decoded_start..this.decoded_start + to_copy],
|
||||
);
|
||||
this.decoded_start += to_copy;
|
||||
|
||||
if this.decoded_start >= this.decoded_end {
|
||||
this.decoded_start = 0;
|
||||
this.decoded_end = 0;
|
||||
}
|
||||
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if this.read_buffer_end >= this.read_buffer.len() {
|
||||
this.reset_read_buffer_offset();
|
||||
}
|
||||
|
||||
let read_slice = &mut this.read_buffer[this.read_buffer_end..];
|
||||
let mut temp_read_buf = ReadBuf::new(read_slice);
|
||||
|
||||
match Pin::new(&mut this.inner).poll_read(cx, &mut temp_read_buf) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
let filled_len = temp_read_buf.filled().len();
|
||||
if filled_len == 0 {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
this.read_buffer_end += filled_len;
|
||||
}
|
||||
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum payload size per padded frame (matches reference 64KB buffer minus overhead)
|
||||
const MAX_PAYLOAD_PER_FRAME: usize = 64 * 1024 - PADDING_HEADER_SIZE - MAX_PADDING_SIZE as usize;
|
||||
|
||||
impl<S: AsyncWrite + Unpin> AsyncWrite for NaivePaddingStream<S> {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
let this = &mut *self;
|
||||
|
||||
if !this.should_pad_writes() {
|
||||
return Pin::new(&mut this.inner).poll_write(cx, buf);
|
||||
}
|
||||
|
||||
// Finish writing buffered frame data first
|
||||
if this.write_start < this.write_end {
|
||||
let remaining_bytes = this.write_end - this.write_start;
|
||||
let write_limit = this.compute_fragment_limit(this.write_payload_len, remaining_bytes);
|
||||
let to_write = &this.write_buffer[this.write_start..this.write_start + write_limit];
|
||||
|
||||
match Pin::new(&mut this.inner).poll_write(cx, to_write) {
|
||||
Poll::Ready(Ok(n)) => {
|
||||
this.write_start += n;
|
||||
if this.write_start >= this.write_end {
|
||||
let payload_len = this.write_payload_len;
|
||||
this.write_start = 0;
|
||||
this.write_end = 0;
|
||||
this.write_payload_len = 0;
|
||||
this.write_fragment_limit = 0;
|
||||
this.num_written_frames += 1;
|
||||
return Poll::Ready(Ok(payload_len));
|
||||
}
|
||||
cx.waker().wake_by_ref();
|
||||
return Poll::Pending;
|
||||
}
|
||||
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
let payload = if buf.len() > MAX_PAYLOAD_PER_FRAME {
|
||||
&buf[..MAX_PAYLOAD_PER_FRAME]
|
||||
} else {
|
||||
buf
|
||||
};
|
||||
|
||||
this.encode_frame(payload)?;
|
||||
|
||||
let remaining_bytes = this.write_end - this.write_start;
|
||||
let write_limit = this.compute_fragment_limit(this.write_payload_len, remaining_bytes);
|
||||
let to_write = &this.write_buffer[this.write_start..this.write_start + write_limit];
|
||||
|
||||
match Pin::new(&mut this.inner).poll_write(cx, to_write) {
|
||||
Poll::Ready(Ok(n)) => {
|
||||
this.write_start += n;
|
||||
if this.write_start >= this.write_end {
|
||||
let payload_len = this.write_payload_len;
|
||||
this.write_start = 0;
|
||||
this.write_end = 0;
|
||||
this.write_payload_len = 0;
|
||||
this.write_fragment_limit = 0;
|
||||
this.num_written_frames += 1;
|
||||
Poll::Ready(Ok(payload_len))
|
||||
} else {
|
||||
cx.waker().wake_by_ref();
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsyncPing> AsyncPing for NaivePaddingStream<S> {
|
||||
fn supports_ping(&self) -> bool {
|
||||
self.inner.supports_ping()
|
||||
}
|
||||
|
||||
fn poll_write_ping(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<bool>> {
|
||||
// Cannot safely implement ping through padding layer
|
||||
Poll::Ready(Ok(false))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsyncStream> AsyncStream for NaivePaddingStream<S> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::VecDeque;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
#[test]
|
||||
fn test_generate_padding_header() {
|
||||
let header = generate_padding_header(20);
|
||||
assert_eq!(header.len(), 20);
|
||||
// All characters should be from NONINDEX_CHARS (first 16 chars use indices 0-15, rest use index 16 = 'X')
|
||||
assert!(header.chars().all(|c| NONINDEX_CHARS.contains(&(c as u8))));
|
||||
// For headers > 16 chars, the last 4 should be 'X'
|
||||
assert!(header.chars().skip(16).all(|c| c == 'X'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_padding_type_parse() {
|
||||
let types = parse_padding_type_request("1, 0");
|
||||
assert_eq!(types, vec![PaddingType::Variant1, PaddingType::None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_padding_type_from_u8() {
|
||||
assert_eq!(PaddingType::from_u8(0), Some(PaddingType::None));
|
||||
assert_eq!(PaddingType::from_u8(1), Some(PaddingType::Variant1));
|
||||
assert_eq!(PaddingType::from_u8(2), None);
|
||||
}
|
||||
|
||||
/// Helper to manually encode a padded frame for testing.
|
||||
/// Format: [payload_len_be16][padding_len_u8][payload][padding_zeros]
|
||||
fn encode_test_frame(payload: &[u8], padding_len: u8) -> Vec<u8> {
|
||||
let payload_len = payload.len() as u16;
|
||||
let mut frame = Vec::with_capacity(3 + payload.len() + padding_len as usize);
|
||||
frame.extend_from_slice(&payload_len.to_be_bytes());
|
||||
frame.push(padding_len);
|
||||
frame.extend_from_slice(payload);
|
||||
frame.extend(std::iter::repeat(0u8).take(padding_len as usize));
|
||||
frame
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frame_encoding_format() {
|
||||
// Verify our test helper matches expected format
|
||||
let frame = encode_test_frame(b"hello", 10);
|
||||
assert_eq!(frame.len(), 3 + 5 + 10); // header + payload + padding
|
||||
assert_eq!(&frame[0..2], &[0x00, 0x05]); // payload_len = 5 (big-endian)
|
||||
assert_eq!(frame[2], 10); // padding_len
|
||||
assert_eq!(&frame[3..8], b"hello"); // payload
|
||||
assert!(frame[8..].iter().all(|&b| b == 0)); // padding zeros
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frame_encoding_empty_payload() {
|
||||
// Pure padding frame (payload_len = 0)
|
||||
let frame = encode_test_frame(b"", 50);
|
||||
assert_eq!(frame.len(), 3 + 0 + 50);
|
||||
assert_eq!(&frame[0..2], &[0x00, 0x00]); // payload_len = 0
|
||||
assert_eq!(frame[2], 50); // padding_len
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_frame_encoding_max_payload() {
|
||||
// Maximum payload size
|
||||
let payload = vec![0xAB; MAX_PAYLOAD_SIZE];
|
||||
let frame = encode_test_frame(&payload, 0);
|
||||
assert_eq!(frame.len(), 3 + MAX_PAYLOAD_SIZE);
|
||||
assert_eq!(&frame[0..2], &[0xFF, 0xFF]); // payload_len = 65535
|
||||
}
|
||||
|
||||
/// A mock async stream that returns pre-defined chunks of data.
|
||||
/// Useful for testing partial reads and specific data patterns.
|
||||
struct MockStream {
|
||||
/// Data chunks to return on each read
|
||||
read_chunks: VecDeque<Vec<u8>>,
|
||||
/// Data written to the stream
|
||||
written: Vec<u8>,
|
||||
}
|
||||
|
||||
impl MockStream {
|
||||
fn new(chunks: Vec<Vec<u8>>) -> Self {
|
||||
Self {
|
||||
read_chunks: chunks.into(),
|
||||
written: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_data(data: Vec<u8>) -> Self {
|
||||
Self::new(vec![data])
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for MockStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
if let Some(chunk) = self.read_chunks.pop_front() {
|
||||
let to_copy = chunk.len().min(buf.remaining());
|
||||
buf.put_slice(&chunk[..to_copy]);
|
||||
// If we didn't use the whole chunk, put the rest back
|
||||
if to_copy < chunk.len() {
|
||||
self.read_chunks.push_front(chunk[to_copy..].to_vec());
|
||||
}
|
||||
Poll::Ready(Ok(()))
|
||||
} else {
|
||||
// EOF
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for MockStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
self.written.extend_from_slice(buf);
|
||||
Poll::Ready(Ok(buf.len()))
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Unpin for MockStream {}
|
||||
|
||||
impl AsyncPing for MockStream {
|
||||
fn supports_ping(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn poll_write_ping(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<bool>> {
|
||||
Poll::Ready(Ok(false))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncStream for MockStream {}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_single_padded_frame() {
|
||||
let payload = b"hello world";
|
||||
let frame = encode_test_frame(payload, 20);
|
||||
let mock = MockStream::from_data(frame);
|
||||
|
||||
let mut stream =
|
||||
NaivePaddingStream::new(mock, PaddingDirection::Client, PaddingType::Variant1);
|
||||
|
||||
let mut buf = vec![0u8; 100];
|
||||
let n = stream.read(&mut buf).await.unwrap();
|
||||
|
||||
assert_eq!(n, payload.len());
|
||||
assert_eq!(&buf[..n], payload);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_multiple_padded_frames() {
|
||||
// Create 3 frames with different payloads
|
||||
let payloads = [b"frame1".as_slice(), b"frame2", b"frame3"];
|
||||
let mut data = Vec::new();
|
||||
for payload in &payloads {
|
||||
data.extend(encode_test_frame(payload, 10));
|
||||
}
|
||||
|
||||
let mock = MockStream::from_data(data);
|
||||
let mut stream =
|
||||
NaivePaddingStream::new(mock, PaddingDirection::Client, PaddingType::Variant1);
|
||||
|
||||
for expected in &payloads {
|
||||
let mut buf = vec![0u8; 100];
|
||||
let n = stream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(&buf[..n], *expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_pure_padding_frame_skipped() {
|
||||
// Create: pure_padding_frame + real_frame
|
||||
// The pure padding frame should be skipped automatically
|
||||
let mut data = Vec::new();
|
||||
data.extend(encode_test_frame(b"", 50)); // Pure padding (payload_len = 0)
|
||||
data.extend(encode_test_frame(b"real data", 10)); // Real frame
|
||||
|
||||
let mock = MockStream::from_data(data);
|
||||
let mut stream =
|
||||
NaivePaddingStream::new(mock, PaddingDirection::Client, PaddingType::Variant1);
|
||||
|
||||
let mut buf = vec![0u8; 100];
|
||||
let n = stream.read(&mut buf).await.unwrap();
|
||||
|
||||
// Should get "real data", skipping the pure padding frame
|
||||
assert_eq!(&buf[..n], b"real data");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_partial_frame_header() {
|
||||
// Split the frame header across two reads
|
||||
let payload = b"test payload";
|
||||
let frame = encode_test_frame(payload, 5);
|
||||
|
||||
// Split: first 2 bytes, then the rest
|
||||
let chunk1 = frame[..2].to_vec();
|
||||
let chunk2 = frame[2..].to_vec();
|
||||
|
||||
let mock = MockStream::new(vec![chunk1, chunk2]);
|
||||
let mut stream =
|
||||
NaivePaddingStream::new(mock, PaddingDirection::Client, PaddingType::Variant1);
|
||||
|
||||
let mut buf = vec![0u8; 100];
|
||||
let n = stream.read(&mut buf).await.unwrap();
|
||||
|
||||
assert_eq!(&buf[..n], payload);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_partial_frame_payload() {
|
||||
// Split the frame payload across two reads
|
||||
let payload = b"split payload data";
|
||||
let frame = encode_test_frame(payload, 5);
|
||||
|
||||
// Split after header + half payload
|
||||
let split_point = 3 + payload.len() / 2;
|
||||
let chunk1 = frame[..split_point].to_vec();
|
||||
let chunk2 = frame[split_point..].to_vec();
|
||||
|
||||
let mock = MockStream::new(vec![chunk1, chunk2]);
|
||||
let mut stream =
|
||||
NaivePaddingStream::new(mock, PaddingDirection::Client, PaddingType::Variant1);
|
||||
|
||||
let mut buf = vec![0u8; 100];
|
||||
let n = stream.read(&mut buf).await.unwrap();
|
||||
|
||||
assert_eq!(&buf[..n], payload);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_transition_to_raw_mode() {
|
||||
// Create exactly 8 padded frames + raw data
|
||||
// This tests the bug fix for leftover data in read_buffer
|
||||
let mut data = Vec::new();
|
||||
|
||||
// 8 padded frames
|
||||
for i in 0..NUM_FIRST_PADDINGS {
|
||||
let payload = format!("frame{}", i);
|
||||
data.extend(encode_test_frame(payload.as_bytes(), 5));
|
||||
}
|
||||
|
||||
// Raw data after the padded frames
|
||||
let raw_data = b"raw data after padding";
|
||||
data.extend_from_slice(raw_data);
|
||||
|
||||
let mock = MockStream::from_data(data);
|
||||
let mut stream =
|
||||
NaivePaddingStream::new(mock, PaddingDirection::Client, PaddingType::Variant1);
|
||||
|
||||
// Read all 8 padded frames
|
||||
for i in 0..NUM_FIRST_PADDINGS {
|
||||
let mut buf = vec![0u8; 100];
|
||||
let n = stream.read(&mut buf).await.unwrap();
|
||||
let expected = format!("frame{}", i);
|
||||
assert_eq!(&buf[..n], expected.as_bytes(), "frame {} mismatch", i);
|
||||
}
|
||||
|
||||
// Now read the raw data (should transition to pass-through mode)
|
||||
let mut buf = vec![0u8; 100];
|
||||
let n = stream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(&buf[..n], raw_data, "raw data after transition was lost!");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_transition_with_coalesced_data() {
|
||||
// This is the specific bug scenario:
|
||||
// Last padded frame and raw data arrive in the same TCP read
|
||||
let mut data = Vec::new();
|
||||
|
||||
// First 7 frames (will be read separately)
|
||||
for i in 0..7 {
|
||||
let payload = format!("f{}", i);
|
||||
data.extend(encode_test_frame(payload.as_bytes(), 3));
|
||||
}
|
||||
|
||||
let first_7_frames = data.clone();
|
||||
data.clear();
|
||||
|
||||
// Frame 8 + raw data (coalesced in same read)
|
||||
data.extend(encode_test_frame(b"f7", 3));
|
||||
data.extend_from_slice(b"RAWDATA");
|
||||
|
||||
// Provide as two chunks: first 7 frames, then frame8+raw
|
||||
let mock = MockStream::new(vec![first_7_frames, data]);
|
||||
let mut stream =
|
||||
NaivePaddingStream::new(mock, PaddingDirection::Client, PaddingType::Variant1);
|
||||
|
||||
// Read all 8 padded frames
|
||||
for i in 0..8 {
|
||||
let mut buf = vec![0u8; 100];
|
||||
let n = stream.read(&mut buf).await.unwrap();
|
||||
let expected = format!("f{}", i);
|
||||
assert_eq!(&buf[..n], expected.as_bytes());
|
||||
}
|
||||
|
||||
// The raw data must not be lost!
|
||||
let mut buf = vec![0u8; 100];
|
||||
let n = stream.read(&mut buf).await.unwrap();
|
||||
assert_eq!(&buf[..n], b"RAWDATA", "coalesced raw data was lost!");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_write_adds_padding_header() {
|
||||
let mock = MockStream::new(vec![]);
|
||||
let mut stream =
|
||||
NaivePaddingStream::new(mock, PaddingDirection::Client, PaddingType::Variant1);
|
||||
|
||||
let payload = b"test write";
|
||||
stream.write_all(payload).await.unwrap();
|
||||
stream.flush().await.unwrap();
|
||||
|
||||
// Check the written data has a valid frame header
|
||||
let written = &stream.inner.written;
|
||||
assert!(written.len() >= 3 + payload.len());
|
||||
|
||||
// Parse header
|
||||
let payload_len = u16::from_be_bytes([written[0], written[1]]) as usize;
|
||||
let padding_len = written[2] as usize;
|
||||
|
||||
assert_eq!(payload_len, payload.len());
|
||||
assert_eq!(&written[3..3 + payload_len], payload);
|
||||
assert_eq!(written.len(), 3 + payload_len + padding_len);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_write_transition_to_raw_mode() {
|
||||
let mock = MockStream::new(vec![]);
|
||||
let mut stream =
|
||||
NaivePaddingStream::new(mock, PaddingDirection::Client, PaddingType::Variant1);
|
||||
|
||||
// Write 8 padded frames
|
||||
for i in 0..NUM_FIRST_PADDINGS {
|
||||
let payload = format!("w{}", i);
|
||||
stream.write_all(payload.as_bytes()).await.unwrap();
|
||||
}
|
||||
|
||||
// Mark where padded frames end
|
||||
let padded_end = stream.inner.written.len();
|
||||
|
||||
// Write raw data (should not have padding header)
|
||||
let raw_payload = b"raw write data";
|
||||
stream.write_all(raw_payload).await.unwrap();
|
||||
|
||||
// The raw data should be written directly without header
|
||||
let raw_written = &stream.inner.written[padded_end..];
|
||||
assert_eq!(raw_written, raw_payload);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_write_frame_count_tracking() {
|
||||
let mock = MockStream::new(vec![]);
|
||||
let mut stream =
|
||||
NaivePaddingStream::new(mock, PaddingDirection::Client, PaddingType::Variant1);
|
||||
|
||||
assert_eq!(stream.num_written_frames, 0);
|
||||
|
||||
for i in 0..5 {
|
||||
stream.write_all(b"x").await.unwrap();
|
||||
assert_eq!(stream.num_written_frames, i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_padding_bias_for_small_payloads() {
|
||||
// Server should bias towards larger padding for small payloads (< 100 bytes)
|
||||
// We can't test the exact random values, but we can verify the logic exists
|
||||
let mock = MockStream::new(vec![]);
|
||||
let mut stream =
|
||||
NaivePaddingStream::new(mock, PaddingDirection::Server, PaddingType::Variant1);
|
||||
|
||||
// Write a small payload
|
||||
let small_payload = b"tiny";
|
||||
stream.write_all(small_payload).await.unwrap();
|
||||
|
||||
// Parse the written frame
|
||||
let written = &stream.inner.written;
|
||||
let padding_len = written[2] as usize;
|
||||
|
||||
// For server with small payload, padding should be biased high
|
||||
// min_padding = 255 - payload_len = 255 - 4 = 251
|
||||
// So padding should be >= 251
|
||||
assert!(
|
||||
padding_len >= (MAX_PADDING_SIZE as usize).saturating_sub(small_payload.len()),
|
||||
"server padding for small payload should be biased high, got {}",
|
||||
padding_len
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_padding_type_none_passthrough() {
|
||||
// With PaddingType::None, data should pass through unchanged
|
||||
let test_data = b"passthrough data";
|
||||
let mock = MockStream::from_data(test_data.to_vec());
|
||||
let mut stream = NaivePaddingStream::new(mock, PaddingDirection::Client, PaddingType::None);
|
||||
|
||||
let mut buf = vec![0u8; 100];
|
||||
let n = stream.read(&mut buf).await.unwrap();
|
||||
|
||||
assert_eq!(&buf[..n], test_data);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_eof_handling() {
|
||||
// Empty stream should return EOF (0 bytes)
|
||||
let mock = MockStream::new(vec![]);
|
||||
let mut stream =
|
||||
NaivePaddingStream::new(mock, PaddingDirection::Client, PaddingType::Variant1);
|
||||
|
||||
let mut buf = vec![0u8; 100];
|
||||
let n = stream.read(&mut buf).await.unwrap();
|
||||
|
||||
assert_eq!(n, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//! NaiveProxy server handler
|
||||
//!
|
||||
//! This module provides the entry point for handling NaiveProxy connections
|
||||
//! after TLS/Reality termination.
|
||||
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::client_proxy_selector::ClientProxySelector;
|
||||
use crate::crypto::CryptoTlsStream;
|
||||
use crate::resolver::Resolver;
|
||||
use crate::tcp::tcp_handler::TcpServerSetupResult;
|
||||
use crate::tls_server_handler::NaiveConfig;
|
||||
|
||||
use super::naive_hyper_service::run_naive_hyper_service;
|
||||
|
||||
/// Setup a NaiveProxy server stream after TLS/Reality termination
|
||||
///
|
||||
/// Determines HTTP version based on:
|
||||
/// - Reality connections: always HTTP/2 (client already authenticated)
|
||||
/// - TLS with ALPN "h2": HTTP/2 (NaiveProxy clients)
|
||||
/// - TLS with other ALPN or None: HTTP/1.1 (fallback/static files only)
|
||||
pub async fn setup_naive_server_stream<IO: AsyncStream + 'static>(
|
||||
tls_stream: CryptoTlsStream<IO>,
|
||||
naive_cfg: &NaiveConfig,
|
||||
effective_selector: Arc<ClientProxySelector>,
|
||||
resolver: Arc<dyn Resolver>,
|
||||
) -> io::Result<TcpServerSetupResult> {
|
||||
let use_h2 = tls_stream.is_reality() || tls_stream.alpn_protocol() == Some(b"h2");
|
||||
run_naive_hyper_service(tls_stream, naive_cfg, effective_selector, resolver, use_h2).await
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
//! User authentication lookup for NaiveProxy
|
||||
//!
|
||||
//! Provides O(1) user lookup with constant-time credential comparison
|
||||
//! to prevent timing attacks.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use base64::engine::{Engine as _, general_purpose::STANDARD as BASE64};
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
/// Single user credential entry
|
||||
struct UserEntry {
|
||||
/// Base64-encoded "user:pass" for comparison
|
||||
encoded: Vec<u8>,
|
||||
/// Display name (for logging)
|
||||
name: String,
|
||||
}
|
||||
|
||||
/// O(1) user lookup with constant-time credential comparison.
|
||||
///
|
||||
/// Uses BLAKE3 hash for fast lookup, then constant-time comparison
|
||||
/// of actual credentials to prevent timing attacks.
|
||||
pub struct UserLookup {
|
||||
/// Hash of encoded credentials -> index in users vec
|
||||
lookup: HashMap<[u8; 32], usize>,
|
||||
/// User entries
|
||||
users: Vec<UserEntry>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for UserLookup {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("UserLookup")
|
||||
.field("num_users", &self.users.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl UserLookup {
|
||||
/// Create a new user lookup table from (name, username, password) tuples.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if credentials is empty (config validation should prevent this).
|
||||
pub fn new(credentials: Vec<(String, String, String)>) -> Self {
|
||||
assert!(
|
||||
!credentials.is_empty(),
|
||||
"NaiveProxy requires at least one user"
|
||||
);
|
||||
let mut lookup = HashMap::with_capacity(credentials.len());
|
||||
let mut users = Vec::with_capacity(credentials.len());
|
||||
|
||||
for (i, (name, username, password)) in credentials.into_iter().enumerate() {
|
||||
let cred_string = format!("{}:{}", username, password);
|
||||
let encoded = BASE64.encode(&cred_string).into_bytes();
|
||||
let hash = blake3::hash(&encoded);
|
||||
lookup.insert(*hash.as_bytes(), i);
|
||||
users.push(UserEntry { encoded, name });
|
||||
}
|
||||
|
||||
Self { lookup, users }
|
||||
}
|
||||
|
||||
/// Validate credentials, returning the user's name if valid.
|
||||
///
|
||||
/// O(1) lookup via hash, then constant-time comparison for security.
|
||||
pub fn validate(&self, auth_header: &str) -> Option<&str> {
|
||||
let encoded = auth_header.strip_prefix("Basic ")?.as_bytes();
|
||||
let hash = blake3::hash(encoded);
|
||||
let idx = self.lookup.get(hash.as_bytes())?;
|
||||
let user = &self.users[*idx];
|
||||
|
||||
// Constant-time comparison as defense in depth
|
||||
if user.encoded.ct_eq(encoded).unwrap_u8() == 1 {
|
||||
Some(&user.name)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_user_lookup_basic() {
|
||||
let lookup = UserLookup::new(vec![(
|
||||
"alice".to_string(),
|
||||
"user".to_string(),
|
||||
"pass".to_string(),
|
||||
)]);
|
||||
// Base64 of "user:pass" is "dXNlcjpwYXNz"
|
||||
assert_eq!(lookup.validate("Basic dXNlcjpwYXNz"), Some("alice"));
|
||||
assert_eq!(lookup.users.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_lookup_special_chars() {
|
||||
// Test with special characters in password
|
||||
let lookup = UserLookup::new(vec![(
|
||||
"bob".to_string(),
|
||||
"user".to_string(),
|
||||
"p@ss:w0rd!".to_string(),
|
||||
)]);
|
||||
// Encode "user:p@ss:w0rd!" to base64
|
||||
let encoded = BASE64.encode("user:p@ss:w0rd!");
|
||||
let header = format!("Basic {}", encoded);
|
||||
assert_eq!(lookup.validate(&header), Some("bob"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_lookup_empty_password() {
|
||||
let lookup = UserLookup::new(vec![(
|
||||
"test".to_string(),
|
||||
"user".to_string(),
|
||||
"".to_string(),
|
||||
)]);
|
||||
let encoded = BASE64.encode("user:");
|
||||
let header = format!("Basic {}", encoded);
|
||||
assert_eq!(lookup.validate(&header), Some("test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_lookup_invalid_credentials() {
|
||||
let lookup = UserLookup::new(vec![(
|
||||
"alice".to_string(),
|
||||
"user".to_string(),
|
||||
"pass".to_string(),
|
||||
)]);
|
||||
assert_eq!(lookup.validate("Basic invalid"), None);
|
||||
assert_eq!(lookup.validate("Basic d3Jvbmc6cGFzcw=="), None); // wrong:pass
|
||||
assert_eq!(lookup.validate("Bearer token"), None);
|
||||
assert_eq!(lookup.validate(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_lookup_multiple_users() {
|
||||
let lookup = UserLookup::new(vec![
|
||||
(
|
||||
"alice".to_string(),
|
||||
"alice".to_string(),
|
||||
"alice123".to_string(),
|
||||
),
|
||||
("bob".to_string(), "bob".to_string(), "bob456".to_string()),
|
||||
(
|
||||
"charlie".to_string(),
|
||||
"charlie".to_string(),
|
||||
"charlie789".to_string(),
|
||||
),
|
||||
]);
|
||||
assert_eq!(lookup.users.len(), 3);
|
||||
|
||||
let alice_header = format!("Basic {}", BASE64.encode("alice:alice123"));
|
||||
let bob_header = format!("Basic {}", BASE64.encode("bob:bob456"));
|
||||
let charlie_header = format!("Basic {}", BASE64.encode("charlie:charlie789"));
|
||||
|
||||
assert_eq!(lookup.validate(&alice_header), Some("alice"));
|
||||
assert_eq!(lookup.validate(&bob_header), Some("bob"));
|
||||
assert_eq!(lookup.validate(&charlie_header), Some("charlie"));
|
||||
}
|
||||
}
|
||||
+6
-13
@@ -15,25 +15,18 @@ impl<T> NoneOrOne<T> {
|
||||
matches!(self, NoneOrOne::Unspecified)
|
||||
}
|
||||
|
||||
pub fn is_one(&self) -> bool {
|
||||
matches!(self, NoneOrOne::One(..))
|
||||
}
|
||||
|
||||
pub fn unwrap(self) -> T {
|
||||
match self {
|
||||
NoneOrOne::One(item) => item,
|
||||
_ => {
|
||||
panic!("Tried to unwrap a non-one NoneOrOne");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_option(self) -> Option<T> {
|
||||
match self {
|
||||
NoneOrOne::One(item) => Some(item),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// Used on non-Linux platforms (macOS, Windows, iOS) for bind_interface validation
|
||||
#[allow(dead_code)]
|
||||
pub fn is_one(&self) -> bool {
|
||||
matches!(self, NoneOrOne::One(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::address::NetLocation;
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::option_util::NoneOrOne;
|
||||
use crate::tcp_handler::{TcpClientHandler, TcpClientSetupResult};
|
||||
use crate::tcp_handler::{TcpServerHandler, TcpServerSetupResult};
|
||||
use crate::client_proxy_selector::ClientProxySelector;
|
||||
use crate::tcp::tcp_handler::{TcpClientHandler, TcpClientSetupResult};
|
||||
use crate::tcp::tcp_handler::{TcpServerHandler, TcpServerSetupResult};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PortForwardServerHandler {
|
||||
targets: Vec<NetLocation>,
|
||||
next_target_index: AtomicU32,
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
}
|
||||
|
||||
impl PortForwardServerHandler {
|
||||
pub fn new(targets: Vec<NetLocation>) -> Self {
|
||||
pub fn new(targets: Vec<NetLocation>, proxy_selector: Arc<ClientProxySelector>) -> Self {
|
||||
Self {
|
||||
targets,
|
||||
next_target_index: AtomicU32::new(0),
|
||||
proxy_selector,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +45,7 @@ impl TcpServerHandler for PortForwardServerHandler {
|
||||
need_initial_flush: true,
|
||||
connection_success_response: None,
|
||||
initial_remote_data: None,
|
||||
override_proxy_provider: NoneOrOne::Unspecified,
|
||||
proxy_selector: self.proxy_selector.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+52
-186
@@ -2,37 +2,32 @@ use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use log::{debug, error, warn};
|
||||
use log::{debug, error};
|
||||
use quinn::EndpointConfig;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::client_proxy_selector::{ClientProxySelector, ConnectDecision};
|
||||
use crate::client_proxy_selector::ConnectDecision;
|
||||
use crate::config::{
|
||||
BindLocation, ConfigSelection, ServerConfig, ServerProxyConfig, ServerQuicConfig,
|
||||
};
|
||||
use crate::copy_bidirectional::copy_bidirectional;
|
||||
use crate::copy_bidirectional_message::copy_bidirectional_message;
|
||||
use crate::copy_multidirectional_message::copy_multidirectional_message;
|
||||
use crate::copy_session_messages::copy_session_messages;
|
||||
use crate::quic_stream::QuicStream;
|
||||
use crate::resolver::{NativeResolver, Resolver};
|
||||
use crate::routing::{ServerStream, run_udp_routing};
|
||||
use crate::rustls_config_util::create_server_config;
|
||||
use crate::socket_util::new_socket2_udp_socket;
|
||||
use crate::tcp::tcp_client_handler_factory::create_tcp_client_proxy_selector;
|
||||
use crate::tcp::tcp_handler::{TcpServerHandler, TcpServerSetupResult};
|
||||
use crate::tcp::tcp_server::{run_udp_copy, setup_client_tcp_stream};
|
||||
use crate::tcp::tcp_server_handler_factory::create_tcp_server_handler;
|
||||
use crate::tcp_handler::{
|
||||
TcpClientUdpSetupResult, TcpServerHandler, TcpServerSetupResult, UdpStreamRequest,
|
||||
};
|
||||
use crate::tcp_server::setup_client_tcp_stream;
|
||||
use crate::util::parse_uuid;
|
||||
use crate::uuid_util::parse_uuid;
|
||||
|
||||
async fn start_quic_server(
|
||||
bind_address: SocketAddr,
|
||||
quic_server_config: Arc<quinn::crypto::rustls::QuicServerConfig>,
|
||||
client_proxy_selector: Arc<ClientProxySelector>,
|
||||
resolver: Arc<dyn Resolver>,
|
||||
server_handler: Arc<dyn TcpServerHandler>,
|
||||
num_endpoints: usize,
|
||||
@@ -59,19 +54,14 @@ async fn start_quic_server(
|
||||
Arc::new(quinn::TokioRuntime),
|
||||
)?;
|
||||
|
||||
let client_proxy_selector = client_proxy_selector.clone();
|
||||
let resolver = resolver.clone();
|
||||
let server_handler = server_handler.clone();
|
||||
let join_handle = tokio::spawn(async move {
|
||||
while let Some(conn) = endpoint.accept().await {
|
||||
let client_proxy_selector = client_proxy_selector.clone();
|
||||
let resolver = resolver.clone();
|
||||
let server_handler = server_handler.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
process_connection(client_proxy_selector, resolver, server_handler, conn)
|
||||
.await
|
||||
{
|
||||
if let Err(e) = process_connection(resolver, server_handler, conn).await {
|
||||
error!("Connection ended with error: {e}");
|
||||
}
|
||||
});
|
||||
@@ -85,7 +75,6 @@ async fn start_quic_server(
|
||||
}
|
||||
|
||||
async fn process_connection(
|
||||
client_proxy_selector: Arc<ClientProxySelector>,
|
||||
resolver: Arc<dyn Resolver>,
|
||||
server_handler: Arc<dyn TcpServerHandler>,
|
||||
conn: quinn::Incoming,
|
||||
@@ -103,13 +92,10 @@ async fn process_connection(
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
let cloned_selector = client_proxy_selector.clone();
|
||||
let cloned_resolver = resolver.clone();
|
||||
let cloned_handler = server_handler.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
process_streams(cloned_selector, cloned_resolver, cloned_handler, stream).await
|
||||
{
|
||||
if let Err(e) = process_streams(cloned_resolver, cloned_handler, stream).await {
|
||||
error!("Failed to process streams: {e}");
|
||||
}
|
||||
});
|
||||
@@ -119,7 +105,6 @@ async fn process_connection(
|
||||
}
|
||||
|
||||
async fn process_streams(
|
||||
client_proxy_selector: Arc<ClientProxySelector>,
|
||||
resolver: Arc<dyn Resolver>,
|
||||
server_handler: Arc<dyn TcpServerHandler>,
|
||||
(send, recv): (quinn::SendStream, quinn::RecvStream),
|
||||
@@ -152,21 +137,15 @@ async fn process_streams(
|
||||
remote_location,
|
||||
stream: mut server_stream,
|
||||
need_initial_flush: server_need_initial_flush,
|
||||
override_proxy_provider,
|
||||
proxy_selector,
|
||||
connection_success_response,
|
||||
initial_remote_data,
|
||||
} => {
|
||||
let selected_proxy_provider = if override_proxy_provider.is_one() {
|
||||
override_proxy_provider.unwrap()
|
||||
} else {
|
||||
client_proxy_selector
|
||||
};
|
||||
|
||||
let setup_client_stream_future = timeout(
|
||||
Duration::from_secs(60),
|
||||
setup_client_tcp_stream(
|
||||
&mut server_stream,
|
||||
selected_proxy_provider,
|
||||
proxy_selector,
|
||||
resolver,
|
||||
remote_location.clone(),
|
||||
),
|
||||
@@ -226,173 +205,60 @@ async fn process_streams(
|
||||
remote_location,
|
||||
stream: server_stream,
|
||||
need_initial_flush: server_need_initial_flush,
|
||||
override_proxy_provider,
|
||||
proxy_selector,
|
||||
} => {
|
||||
let selected_proxy_provider = if override_proxy_provider.is_one() {
|
||||
override_proxy_provider.unwrap()
|
||||
} else {
|
||||
client_proxy_selector
|
||||
};
|
||||
|
||||
let action = selected_proxy_provider
|
||||
.judge(remote_location, &resolver)
|
||||
.await?;
|
||||
let action = proxy_selector.judge(remote_location, &resolver).await?;
|
||||
match action {
|
||||
ConnectDecision::Allow {
|
||||
chain_group,
|
||||
remote_location,
|
||||
} => {
|
||||
// Use chain_group's UDP connection with Bidirectional mode
|
||||
// Pass server stream so handler can return matched pair
|
||||
let udp_result = chain_group
|
||||
.connect_udp(
|
||||
&resolver,
|
||||
UdpStreamRequest::Bidirectional {
|
||||
server_stream,
|
||||
target: remote_location,
|
||||
},
|
||||
)
|
||||
let client_stream = chain_group
|
||||
.connect_udp_bidirectional(&resolver, remote_location)
|
||||
.await?;
|
||||
|
||||
match udp_result {
|
||||
TcpClientUdpSetupResult::Bidirectional {
|
||||
mut server_stream,
|
||||
mut client_stream,
|
||||
} => {
|
||||
let copy_result = copy_bidirectional_message(
|
||||
&mut server_stream,
|
||||
&mut client_stream,
|
||||
server_need_initial_flush,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
copy_result?;
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
// Handler should have returned Bidirectional or errored
|
||||
unreachable!(
|
||||
"Handler must return Bidirectional stream type when requested, or error"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
ConnectDecision::Block => {
|
||||
// Must have been blocked.
|
||||
Ok(())
|
||||
run_udp_copy(
|
||||
server_stream,
|
||||
client_stream,
|
||||
server_need_initial_flush,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
ConnectDecision::Block => Ok(()),
|
||||
}
|
||||
}
|
||||
TcpServerSetupResult::MultiDirectionalUdp {
|
||||
stream: server_stream,
|
||||
need_initial_flush: server_need_initial_flush,
|
||||
override_proxy_provider,
|
||||
need_initial_flush,
|
||||
proxy_selector,
|
||||
} => {
|
||||
let selected_proxy_provider = if override_proxy_provider.is_one() {
|
||||
override_proxy_provider.unwrap()
|
||||
} else {
|
||||
client_proxy_selector
|
||||
};
|
||||
let action = selected_proxy_provider.default_decision();
|
||||
match action {
|
||||
ConnectDecision::Allow {
|
||||
chain_group,
|
||||
remote_location: _,
|
||||
} => {
|
||||
// Use chain_group's UDP connection with MultiDirectional mode
|
||||
// Pass server stream so handler can return matched pair
|
||||
let udp_result = chain_group
|
||||
.connect_udp(
|
||||
&resolver,
|
||||
UdpStreamRequest::MultiDirectional { server_stream },
|
||||
)
|
||||
.await?;
|
||||
|
||||
match udp_result {
|
||||
TcpClientUdpSetupResult::MultiDirectional {
|
||||
mut server_stream,
|
||||
mut client_stream,
|
||||
} => {
|
||||
let copy_result = copy_multidirectional_message(
|
||||
&mut server_stream,
|
||||
&mut client_stream,
|
||||
server_need_initial_flush,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
copy_result?;
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
// Handler should have returned MultiDirectional or errored
|
||||
unreachable!(
|
||||
"Handler must return MultiDirectional stream type when requested, or error"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
ConnectDecision::Block => {
|
||||
warn!(
|
||||
"Blocked multidirectional udp forward, because the default action is to block."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// Routes each packet based on its destination
|
||||
run_udp_routing(
|
||||
ServerStream::Targeted(server_stream),
|
||||
proxy_selector,
|
||||
resolver,
|
||||
need_initial_flush,
|
||||
)
|
||||
.await
|
||||
}
|
||||
TcpServerSetupResult::SessionBasedUdp {
|
||||
stream: server_stream,
|
||||
need_initial_flush: server_need_initial_flush,
|
||||
override_proxy_provider,
|
||||
need_initial_flush,
|
||||
proxy_selector,
|
||||
} => {
|
||||
let selected_proxy_provider = if override_proxy_provider.is_one() {
|
||||
override_proxy_provider.unwrap()
|
||||
} else {
|
||||
client_proxy_selector
|
||||
};
|
||||
let action = selected_proxy_provider.default_decision();
|
||||
match action {
|
||||
ConnectDecision::Allow {
|
||||
chain_group,
|
||||
remote_location: _,
|
||||
} => {
|
||||
// Use chain_group's UDP connection with SessionBased mode
|
||||
// Pass server stream so handler can return matched pair
|
||||
let udp_result = chain_group
|
||||
.connect_udp(&resolver, UdpStreamRequest::SessionBased { server_stream })
|
||||
.await?;
|
||||
|
||||
match udp_result {
|
||||
TcpClientUdpSetupResult::SessionBased {
|
||||
mut server_stream,
|
||||
mut client_stream,
|
||||
} => {
|
||||
// Both server and client are session-based (XUDP) - use copy_session_messages
|
||||
let copy_result = copy_session_messages(
|
||||
&mut server_stream,
|
||||
&mut client_stream,
|
||||
server_need_initial_flush,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
copy_result?;
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
// Handler should have returned SessionBased or errored
|
||||
unreachable!(
|
||||
"Handler must return SessionBased stream type when requested, or error"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
ConnectDecision::Block => {
|
||||
warn!(
|
||||
"Blocked session-based udp forward, because the default action is to block."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// Routes each session based on its destination
|
||||
run_udp_routing(
|
||||
ServerStream::Session(server_stream),
|
||||
proxy_selector,
|
||||
resolver,
|
||||
need_initial_flush,
|
||||
)
|
||||
.await
|
||||
}
|
||||
TcpServerSetupResult::AlreadyHandled => {
|
||||
// Connection already handled by a spawned task (e.g., Reality fallback)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -409,7 +275,7 @@ pub async fn start_quic_servers(config: ServerConfig) -> std::io::Result<Vec<Joi
|
||||
println!("Starting {} QUIC server at {}", &protocol, &bind_location);
|
||||
|
||||
let rules = rules.map(ConfigSelection::unwrap_config).into_vec();
|
||||
// We should always have a direct entry.
|
||||
// A direct entry must always exist
|
||||
assert!(!rules.is_empty());
|
||||
|
||||
let bind_addresses = match bind_location {
|
||||
@@ -454,7 +320,7 @@ pub async fn start_quic_servers(config: ServerConfig) -> std::io::Result<Vec<Joi
|
||||
let quic_server_config = Arc::new(quic_server_config);
|
||||
|
||||
let client_proxy_selector = Arc::new(create_tcp_client_proxy_selector(rules.clone()));
|
||||
let resolver = Arc::new(NativeResolver::new());
|
||||
let resolver: Arc<dyn Resolver> = Arc::new(NativeResolver::new());
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
@@ -509,19 +375,19 @@ pub async fn start_quic_servers(config: ServerConfig) -> std::io::Result<Vec<Joi
|
||||
}
|
||||
}
|
||||
tcp_protocol => {
|
||||
let mut rules_stack = vec![rules];
|
||||
let bind_ip = bind_addresses.first().map(|addr| addr.ip());
|
||||
|
||||
let tcp_handler: Arc<dyn TcpServerHandler> =
|
||||
create_tcp_server_handler(tcp_protocol, &mut rules_stack).into();
|
||||
create_tcp_server_handler(tcp_protocol, &client_proxy_selector, &resolver, bind_ip)
|
||||
.into();
|
||||
|
||||
for bind_address in bind_addresses.into_iter() {
|
||||
let quic_server_config = quic_server_config.clone();
|
||||
let client_proxy_selector = client_proxy_selector.clone();
|
||||
let resolver = resolver.clone();
|
||||
let tcp_handler = tcp_handler.clone();
|
||||
let quic_handles = start_quic_server(
|
||||
bind_address,
|
||||
quic_server_config,
|
||||
client_proxy_selector,
|
||||
resolver,
|
||||
tcp_handler,
|
||||
num_endpoints,
|
||||
|
||||
+169
-67
@@ -1,12 +1,6 @@
|
||||
// Common constants and helpers shared between REALITY client and server implementations
|
||||
//
|
||||
// This module provides:
|
||||
// - TLS constants (content types, alert codes, version bytes, handshake types)
|
||||
// - Close notify alert construction
|
||||
// TLS constants and utilities for REALITY client/server implementations
|
||||
|
||||
use super::reality_aead::encrypt_tls13_record;
|
||||
use super::reality_cipher_suite::CipherSuite;
|
||||
use std::io;
|
||||
use std::io::{self, Error, ErrorKind};
|
||||
|
||||
// TLS ContentType values
|
||||
pub const CONTENT_TYPE_CHANGE_CIPHER_SPEC: u8 = 0x14;
|
||||
@@ -18,9 +12,7 @@ pub const CONTENT_TYPE_APPLICATION_DATA: u8 = 0x17;
|
||||
pub const ALERT_LEVEL_WARNING: u8 = 0x01;
|
||||
pub const ALERT_DESC_CLOSE_NOTIFY: u8 = 0x00;
|
||||
|
||||
// TLS version bytes (used on wire for compatibility)
|
||||
// TLS 1.2 version bytes: 0x03, 0x03
|
||||
// Used in TLS 1.3 for compatibility (appears in record layer)
|
||||
// TLS 1.2 version bytes (0x03, 0x03) used in TLS 1.3 record layer for compatibility
|
||||
pub const VERSION_TLS_1_2_MAJOR: u8 = 0x03;
|
||||
pub const VERSION_TLS_1_2_MINOR: u8 = 0x03;
|
||||
|
||||
@@ -77,66 +69,176 @@ pub const CIPHERTEXT_READ_BUF_CAPACITY: usize = TLS_MAX_RECORD_SIZE * 2;
|
||||
/// Buffer capacity for plaintext read
|
||||
pub const PLAINTEXT_READ_BUF_CAPACITY: usize = TLS_MAX_RECORD_SIZE * 2;
|
||||
|
||||
/// Increment a TLS 1.3 sequence number, checking for overflow.
|
||||
/// Buffer capacity for outgoing data (matches rustls DEFAULT_BUFFER_LIMIT)
|
||||
///
|
||||
/// Per RFC 8446 Section 5.3: "Sequence numbers MUST NOT wrap."
|
||||
/// This function returns an error if incrementing would cause wrap-around.
|
||||
#[inline(always)]
|
||||
pub fn increment_seq(seq: &mut u64) -> io::Result<()> {
|
||||
if *seq == u64::MAX {
|
||||
return Err(io::Error::other(
|
||||
"TLS sequence number exhausted (RFC 8446 Section 5.3)",
|
||||
/// This controls the size of both the plaintext write buffer (pre-encryption)
|
||||
/// and ciphertext write buffer (post-encryption). rustls uses 64KB for both.
|
||||
pub const OUTGOING_BUFFER_LIMIT: usize = 64 * 1024;
|
||||
|
||||
/// Strip TLS 1.3 content type trailer from decrypted plaintext slice.
|
||||
///
|
||||
/// TLS 1.3 format: content || type_byte
|
||||
/// Returns (content_type, valid_content_length) without modifying the slice.
|
||||
///
|
||||
/// This is the zero-allocation version for use with in-place decryption.
|
||||
/// NOTE: Does NOT strip padding zeros - our implementation doesn't add padding.
|
||||
#[inline]
|
||||
pub fn strip_content_type_slice(plaintext: &[u8]) -> io::Result<(u8, usize)> {
|
||||
if plaintext.is_empty() {
|
||||
return Err(Error::new(ErrorKind::InvalidData, "Empty plaintext"));
|
||||
}
|
||||
|
||||
// No padding in our implementation
|
||||
let content_type = plaintext[plaintext.len() - 1];
|
||||
|
||||
if content_type != CONTENT_TYPE_HANDSHAKE
|
||||
&& content_type != CONTENT_TYPE_APPLICATION_DATA
|
||||
&& content_type != CONTENT_TYPE_ALERT
|
||||
{
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("Invalid content type: 0x{:02x}", content_type),
|
||||
));
|
||||
}
|
||||
*seq += 1;
|
||||
Ok(())
|
||||
|
||||
Ok((content_type, plaintext.len() - 1))
|
||||
}
|
||||
|
||||
/// Build an encrypted close_notify alert for TLS 1.3
|
||||
/// Strip TLS 1.3 content type trailer from decrypted plaintext.
|
||||
///
|
||||
/// In TLS 1.3, alerts must be encrypted like application data.
|
||||
pub fn build_close_notify_alert(
|
||||
cipher_suite: CipherSuite,
|
||||
key: &[u8],
|
||||
iv: &[u8],
|
||||
seq_num: u64,
|
||||
) -> io::Result<Vec<u8>> {
|
||||
// Build alert message: level(1) + description(0) + ContentType
|
||||
let alert_with_type = vec![
|
||||
ALERT_LEVEL_WARNING,
|
||||
ALERT_DESC_CLOSE_NOTIFY,
|
||||
CONTENT_TYPE_ALERT, // ContentType byte for TLS 1.3
|
||||
];
|
||||
|
||||
// Build TLS header with correct ciphertext length
|
||||
let ciphertext_len = (alert_with_type.len() + 16) as u16; // plaintext + tag
|
||||
let mut tls_header = [
|
||||
CONTENT_TYPE_APPLICATION_DATA,
|
||||
VERSION_TLS_1_2_MAJOR,
|
||||
VERSION_TLS_1_2_MINOR,
|
||||
0x00,
|
||||
0x00, // Length will be set
|
||||
];
|
||||
tls_header[3..5].copy_from_slice(&ciphertext_len.to_be_bytes());
|
||||
|
||||
// Encrypt the alert
|
||||
let ciphertext = encrypt_tls13_record(
|
||||
cipher_suite,
|
||||
key,
|
||||
iv,
|
||||
seq_num,
|
||||
&alert_with_type,
|
||||
&tls_header,
|
||||
)?;
|
||||
|
||||
// Build complete TLS record
|
||||
let mut record = Vec::with_capacity(5 + ciphertext.len());
|
||||
record.push(CONTENT_TYPE_APPLICATION_DATA);
|
||||
record.push(VERSION_TLS_1_2_MAJOR);
|
||||
record.push(VERSION_TLS_1_2_MINOR);
|
||||
record.push(((ciphertext.len() >> 8) & 0xff) as u8);
|
||||
record.push((ciphertext.len() & 0xff) as u8);
|
||||
record.extend_from_slice(&ciphertext);
|
||||
|
||||
Ok(record)
|
||||
/// TLS 1.3 format: content || type_byte
|
||||
/// Returns the actual content type and modifies plaintext to contain only content.
|
||||
///
|
||||
/// NOTE: This function does NOT strip padding zeros. Our REALITY implementation
|
||||
/// does not add padding, so stripping zeros could corrupt data that legitimately
|
||||
/// ends with zero bytes. Use `strip_content_type_with_padding` for messages from
|
||||
/// external implementations that may use padding.
|
||||
///
|
||||
/// Only used by tests - the hot path uses `strip_content_type_slice` for zero-allocation.
|
||||
#[cfg(test)]
|
||||
pub fn strip_content_type(plaintext: &mut Vec<u8>) -> io::Result<u8> {
|
||||
let (content_type, valid_len) = strip_content_type_slice(plaintext)?;
|
||||
plaintext.truncate(valid_len);
|
||||
Ok(content_type)
|
||||
}
|
||||
|
||||
/// Strip TLS 1.3 content type trailer and padding from decrypted plaintext.
|
||||
///
|
||||
/// TLS 1.3 format: content || type_byte || padding_zeros
|
||||
/// Returns the actual content type and modifies plaintext to contain only content.
|
||||
///
|
||||
/// Use this for messages from external TLS implementations (e.g., sing-box) that
|
||||
/// may add optional padding per RFC 8446 Section 5.4.
|
||||
pub fn strip_content_type_with_padding(plaintext: &mut Vec<u8>) -> io::Result<u8> {
|
||||
if plaintext.is_empty() {
|
||||
return Err(Error::new(ErrorKind::InvalidData, "Empty plaintext"));
|
||||
}
|
||||
|
||||
// Remove trailing zeros (padding) per RFC 8446 Section 5.4
|
||||
while !plaintext.is_empty() && *plaintext.last().unwrap() == 0 {
|
||||
plaintext.pop();
|
||||
}
|
||||
|
||||
if plaintext.is_empty() {
|
||||
return Err(Error::new(ErrorKind::InvalidData, "Plaintext is all zeros"));
|
||||
}
|
||||
|
||||
let content_type = plaintext.pop().unwrap();
|
||||
|
||||
if content_type != CONTENT_TYPE_HANDSHAKE
|
||||
&& content_type != CONTENT_TYPE_APPLICATION_DATA
|
||||
&& content_type != CONTENT_TYPE_ALERT
|
||||
{
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("Invalid content type: 0x{:02x}", content_type),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(content_type)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_strip_content_type_app_data() {
|
||||
let mut plaintext = vec![0x01, 0x02, 0x03, CONTENT_TYPE_APPLICATION_DATA];
|
||||
let ct = strip_content_type(&mut plaintext).unwrap();
|
||||
assert_eq!(ct, CONTENT_TYPE_APPLICATION_DATA);
|
||||
assert_eq!(plaintext, vec![0x01, 0x02, 0x03]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_content_type_handshake() {
|
||||
let mut plaintext = vec![0xAA, 0xBB, CONTENT_TYPE_HANDSHAKE];
|
||||
let ct = strip_content_type(&mut plaintext).unwrap();
|
||||
assert_eq!(ct, CONTENT_TYPE_HANDSHAKE);
|
||||
assert_eq!(plaintext, vec![0xAA, 0xBB]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_content_type_alert() {
|
||||
let mut plaintext = vec![0x01, 0x00, CONTENT_TYPE_ALERT];
|
||||
let ct = strip_content_type(&mut plaintext).unwrap();
|
||||
assert_eq!(ct, CONTENT_TYPE_ALERT);
|
||||
assert_eq!(plaintext, vec![0x01, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_content_type_preserves_zeros() {
|
||||
// Trailing zeros in data should be preserved (not treated as padding)
|
||||
let mut plaintext = vec![0x01, 0x00, 0x00, CONTENT_TYPE_APPLICATION_DATA];
|
||||
let ct = strip_content_type(&mut plaintext).unwrap();
|
||||
assert_eq!(ct, CONTENT_TYPE_APPLICATION_DATA);
|
||||
assert_eq!(plaintext, vec![0x01, 0x00, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_content_type_empty() {
|
||||
let mut plaintext = Vec::new();
|
||||
assert!(strip_content_type(&mut plaintext).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_content_type_invalid() {
|
||||
let mut plaintext = vec![0x01, 0xFF]; // 0xFF is invalid
|
||||
assert!(strip_content_type(&mut plaintext).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_with_padding_no_padding() {
|
||||
let mut plaintext = vec![0x01, 0x02, CONTENT_TYPE_APPLICATION_DATA];
|
||||
let ct = strip_content_type_with_padding(&mut plaintext).unwrap();
|
||||
assert_eq!(ct, CONTENT_TYPE_APPLICATION_DATA);
|
||||
assert_eq!(plaintext, vec![0x01, 0x02]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_with_padding_strips_zeros() {
|
||||
// TLS 1.3 format: content || type || padding
|
||||
let mut plaintext = vec![0x01, 0x02, CONTENT_TYPE_HANDSHAKE, 0x00, 0x00, 0x00];
|
||||
let ct = strip_content_type_with_padding(&mut plaintext).unwrap();
|
||||
assert_eq!(ct, CONTENT_TYPE_HANDSHAKE);
|
||||
assert_eq!(plaintext, vec![0x01, 0x02]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_with_padding_empty() {
|
||||
let mut plaintext = Vec::new();
|
||||
assert!(strip_content_type_with_padding(&mut plaintext).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_with_padding_all_zeros() {
|
||||
let mut plaintext = vec![0x00, 0x00, 0x00];
|
||||
assert!(strip_content_type_with_padding(&mut plaintext).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_with_padding_invalid_type() {
|
||||
let mut plaintext = vec![0x01, 0xFF, 0x00]; // 0xFF with padding
|
||||
assert!(strip_content_type_with_padding(&mut plaintext).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+8
-9
@@ -1,11 +1,6 @@
|
||||
// REALITY Protocol Implementation
|
||||
//
|
||||
// This module implements the REALITY obfuscation protocol for TLS connections.
|
||||
// REALITY disguises proxy traffic as legitimate HTTPS connections using:
|
||||
// - X25519 ECDH key exchange
|
||||
// - HKDF-SHA256 key derivation
|
||||
// - AES-256-GCM encryption
|
||||
// - HMAC-SHA512 authentication
|
||||
// Server parts not used by library's public API
|
||||
|
||||
// REALITY protocol: TLS obfuscation using X25519, HKDF-SHA256, AES-256-GCM, HMAC-SHA512
|
||||
|
||||
mod common;
|
||||
mod reality_aead;
|
||||
@@ -18,14 +13,17 @@ mod reality_io_state;
|
||||
mod reality_reader_writer;
|
||||
mod reality_records;
|
||||
mod reality_server_connection;
|
||||
mod reality_server_handler;
|
||||
mod reality_tls13_keys;
|
||||
mod reality_tls13_messages;
|
||||
mod reality_util;
|
||||
|
||||
pub use reality_cipher_suite::{CipherSuite, DEFAULT_CIPHER_SUITES};
|
||||
// generate_keypair is used by binary, not by library's public API
|
||||
#[allow(unused_imports)]
|
||||
pub use reality_util::{decode_private_key, decode_public_key, decode_short_id, generate_keypair};
|
||||
|
||||
// Re-export connection types for crypto_connection module
|
||||
// Re-exports for crypto_connection module
|
||||
pub use reality_client_connection::{
|
||||
RealityClientConfig, RealityClientConnection, feed_reality_client_connection,
|
||||
};
|
||||
@@ -33,3 +31,4 @@ pub use reality_reader_writer::{RealityReader, RealityWriter};
|
||||
pub use reality_server_connection::{
|
||||
RealityServerConfig, RealityServerConnection, feed_reality_server_connection,
|
||||
};
|
||||
pub use reality_server_handler::{RealityServerTarget, setup_reality_server_stream};
|
||||
|
||||
+257
-215
@@ -1,227 +1,196 @@
|
||||
// TLS 1.3 Encryption/Decryption Helpers
|
||||
//
|
||||
// AEAD encryption for TLS 1.3 records using aws-lc-rs
|
||||
// Supports AES-128-GCM, AES-256-GCM, and ChaCha20-Poly1305
|
||||
// TLS 1.3 AEAD encryption/decryption using aws-lc-rs.
|
||||
// Record framing is handled by reality_records.rs.
|
||||
|
||||
use super::common::{
|
||||
CONTENT_TYPE_ALERT, CONTENT_TYPE_APPLICATION_DATA, VERSION_TLS_1_2_MAJOR, VERSION_TLS_1_2_MINOR,
|
||||
};
|
||||
use super::reality_cipher_suite::CipherSuite;
|
||||
use aws_lc_rs::aead::{Aad, LessSafeKey, Nonce, UnboundKey};
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
use std::io::{self, Error, ErrorKind};
|
||||
|
||||
/// Encrypt TLS 1.3 record using CipherSuite
|
||||
use super::common::strip_content_type_with_padding;
|
||||
use super::reality_cipher_suite::CipherSuite;
|
||||
|
||||
/// AEAD key for TLS 1.3 encryption/decryption.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `cipher_suite` - CipherSuite with AEAD algorithm
|
||||
/// * `key` - AEAD key (16 bytes for AES-128, 32 bytes for AES-256/ChaCha20)
|
||||
/// * `iv` - Base IV (12 bytes)
|
||||
/// * `sequence_number` - TLS record sequence number
|
||||
/// * `plaintext` - Plaintext data (including ContentType trailer)
|
||||
/// * `additional_data` - TLS record header for AEAD
|
||||
///
|
||||
/// # Returns
|
||||
/// Ciphertext with authentication tag appended
|
||||
pub fn encrypt_tls13_record(
|
||||
cipher_suite: CipherSuite,
|
||||
key: &[u8],
|
||||
iv: &[u8],
|
||||
sequence_number: u64,
|
||||
plaintext: &[u8],
|
||||
additional_data: &[u8],
|
||||
) -> Result<Vec<u8>> {
|
||||
let algorithm = cipher_suite.algorithm();
|
||||
let expected_key_len = cipher_suite.key_len();
|
||||
/// Wraps aws-lc-rs LessSafeKey and provides a cleaner API.
|
||||
/// Create once per connection direction and reuse for all records.
|
||||
pub struct AeadKey(LessSafeKey);
|
||||
|
||||
if key.len() != expected_key_len {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"Invalid key length for {:?}: {} (expected {})",
|
||||
cipher_suite,
|
||||
key.len(),
|
||||
expected_key_len
|
||||
),
|
||||
));
|
||||
}
|
||||
if iv.len() != 12 {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Invalid IV length: {} (expected 12)", iv.len()),
|
||||
));
|
||||
impl AeadKey {
|
||||
/// Create a new AEAD key from raw key bytes.
|
||||
pub fn new(cipher_suite: CipherSuite, key: &[u8]) -> io::Result<Self> {
|
||||
let algorithm = cipher_suite.algorithm();
|
||||
let expected_len = cipher_suite.key_len();
|
||||
|
||||
if key.len() != expected_len {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"Invalid key length for {:?}: {} (expected {})",
|
||||
cipher_suite,
|
||||
key.len(),
|
||||
expected_len
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let unbound = UnboundKey::new(algorithm, key)
|
||||
.map_err(|e| Error::new(ErrorKind::InvalidInput, format!("Invalid key: {:?}", e)))?;
|
||||
|
||||
Ok(Self(LessSafeKey::new(unbound)))
|
||||
}
|
||||
|
||||
// Construct nonce: IV XOR sequence_number
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
nonce_bytes.copy_from_slice(iv);
|
||||
|
||||
// XOR the last 8 bytes with the sequence number
|
||||
let seq_bytes = sequence_number.to_be_bytes();
|
||||
for i in 0..8 {
|
||||
nonce_bytes[4 + i] ^= seq_bytes[i];
|
||||
/// Encrypt in-place, appending 16-byte auth tag.
|
||||
///
|
||||
/// The buffer is modified: plaintext -> ciphertext || tag
|
||||
#[inline]
|
||||
pub fn seal_in_place(
|
||||
&self,
|
||||
buf: &mut Vec<u8>,
|
||||
iv: &[u8],
|
||||
seq: u64,
|
||||
aad: &[u8],
|
||||
) -> io::Result<()> {
|
||||
let nonce = Self::make_nonce(iv, seq)?;
|
||||
self.0
|
||||
.seal_in_place_append_tag(nonce, Aad::from(aad), buf)
|
||||
.map_err(|e| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("Encryption failed: {:?}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Create key and nonce for aws-lc-rs
|
||||
let unbound_key = UnboundKey::new(algorithm, key)
|
||||
.map_err(|e| Error::new(ErrorKind::InvalidInput, format!("Invalid key: {:?}", e)))?;
|
||||
let sealing_key = LessSafeKey::new(unbound_key);
|
||||
/// Encrypt with copy, returning new Vec containing ciphertext || tag.
|
||||
///
|
||||
/// Use this for small buffers where allocation overhead doesn't matter.
|
||||
#[inline]
|
||||
#[cfg_attr(not(test), allow(dead_code))] // Used by test helpers
|
||||
pub fn seal(&self, plaintext: &[u8], iv: &[u8], seq: u64, aad: &[u8]) -> io::Result<Vec<u8>> {
|
||||
let mut buf = plaintext.to_vec();
|
||||
self.seal_in_place(&mut buf, iv, seq, aad)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
let nonce = Nonce::try_assume_unique_for_key(&nonce_bytes)
|
||||
.map_err(|e| Error::new(ErrorKind::InvalidInput, format!("Invalid nonce: {:?}", e)))?;
|
||||
/// Decrypt in-place on a mutable slice, returning the plaintext portion.
|
||||
///
|
||||
/// This is the zero-allocation decryption API. The slice is decrypted in-place
|
||||
/// and a sub-slice containing only the plaintext (without the auth tag) is returned.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `buf` - Mutable slice containing ciphertext + auth tag
|
||||
/// * `iv` - 12-byte IV/nonce base
|
||||
/// * `seq` - Sequence number to XOR with IV
|
||||
/// * `aad` - Additional authenticated data
|
||||
///
|
||||
/// # Returns
|
||||
/// Sub-slice of `buf` containing only the decrypted plaintext
|
||||
#[inline]
|
||||
pub fn open_in_place_slice<'a>(
|
||||
&self,
|
||||
buf: &'a mut [u8],
|
||||
iv: &[u8],
|
||||
seq: u64,
|
||||
aad: &[u8],
|
||||
) -> io::Result<&'a mut [u8]> {
|
||||
let nonce = Self::make_nonce(iv, seq)?;
|
||||
self.0
|
||||
.open_in_place(nonce, Aad::from(aad), buf)
|
||||
.map_err(|e| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("Decryption failed: {:?}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
let aad = Aad::from(additional_data);
|
||||
/// Decrypt with copy, returning new Vec containing plaintext.
|
||||
///
|
||||
/// Used for handshake decryption and tests where allocation is acceptable.
|
||||
#[inline]
|
||||
pub fn open(&self, ciphertext: &[u8], iv: &[u8], seq: u64, aad: &[u8]) -> io::Result<Vec<u8>> {
|
||||
let mut buf = ciphertext.to_vec();
|
||||
let plaintext = self.open_in_place_slice(&mut buf, iv, seq, aad)?;
|
||||
let plaintext_len = plaintext.len();
|
||||
buf.truncate(plaintext_len);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
// Note: The caller is responsible for adding the ContentType byte to plaintext
|
||||
// For handshake: plaintext = handshake_msg || 0x16
|
||||
// For app data: plaintext = app_data || 0x17
|
||||
let mut in_out = plaintext.to_vec();
|
||||
/// Construct TLS 1.3 nonce: IV XOR sequence_number
|
||||
fn make_nonce(iv: &[u8], seq: u64) -> io::Result<Nonce> {
|
||||
if iv.len() != 12 {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Invalid IV length: {} (expected 12)", iv.len()),
|
||||
));
|
||||
}
|
||||
|
||||
sealing_key
|
||||
.seal_in_place_append_tag(nonce, aad, &mut in_out)
|
||||
.map_err(|e| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("Encryption failed: {:?}", e),
|
||||
)
|
||||
})?;
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
nonce_bytes.copy_from_slice(iv);
|
||||
|
||||
Ok(in_out)
|
||||
// XOR last 8 bytes with sequence number (big-endian)
|
||||
let seq_bytes = seq.to_be_bytes();
|
||||
for i in 0..8 {
|
||||
nonce_bytes[4 + i] ^= seq_bytes[i];
|
||||
}
|
||||
|
||||
Nonce::try_assume_unique_for_key(&nonce_bytes)
|
||||
.map_err(|e| Error::new(ErrorKind::InvalidInput, format!("Invalid nonce: {:?}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypt TLS 1.3 record using CipherSuite
|
||||
/// Decrypt a TLS 1.3 handshake message.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `cipher_suite` - CipherSuite with AEAD algorithm
|
||||
/// * `key` - AEAD key (16 bytes for AES-128, 32 bytes for AES-256/ChaCha20)
|
||||
/// * `iv` - Base IV (12 bytes)
|
||||
/// * `sequence_number` - TLS record sequence number
|
||||
/// * `ciphertext` - Ciphertext with authentication tag
|
||||
/// * `additional_data` - TLS record header for AEAD
|
||||
///
|
||||
/// # Returns
|
||||
/// Plaintext data (including ContentType trailer)
|
||||
pub fn decrypt_tls13_record(
|
||||
cipher_suite: CipherSuite,
|
||||
key: &[u8],
|
||||
iv: &[u8],
|
||||
sequence_number: u64,
|
||||
ciphertext: &[u8],
|
||||
additional_data: &[u8],
|
||||
) -> Result<Vec<u8>> {
|
||||
let algorithm = cipher_suite.algorithm();
|
||||
let expected_key_len = cipher_suite.key_len();
|
||||
|
||||
if key.len() != expected_key_len {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"Invalid key length for {:?}: {} (expected {})",
|
||||
cipher_suite,
|
||||
key.len(),
|
||||
expected_key_len
|
||||
),
|
||||
));
|
||||
}
|
||||
if iv.len() != 12 {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Invalid IV length: {} (expected 12)", iv.len()),
|
||||
));
|
||||
}
|
||||
|
||||
// Construct nonce: IV XOR sequence_number
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
nonce_bytes.copy_from_slice(iv);
|
||||
|
||||
// XOR the last 8 bytes with the sequence number
|
||||
let seq_bytes = sequence_number.to_be_bytes();
|
||||
for i in 0..8 {
|
||||
nonce_bytes[4 + i] ^= seq_bytes[i];
|
||||
}
|
||||
|
||||
// Create key and nonce for aws-lc-rs
|
||||
let unbound_key = UnboundKey::new(algorithm, key)
|
||||
.map_err(|e| Error::new(ErrorKind::InvalidInput, format!("Invalid key: {:?}", e)))?;
|
||||
let opening_key = LessSafeKey::new(unbound_key);
|
||||
|
||||
let nonce = Nonce::try_assume_unique_for_key(&nonce_bytes)
|
||||
.map_err(|e| Error::new(ErrorKind::InvalidInput, format!("Invalid nonce: {:?}", e)))?;
|
||||
|
||||
let aad = Aad::from(additional_data);
|
||||
|
||||
// aws-lc-rs requires in-place decryption
|
||||
let mut in_out = ciphertext.to_vec();
|
||||
let plaintext = opening_key
|
||||
.open_in_place(nonce, aad, &mut in_out)
|
||||
.map_err(|e| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("Decryption failed: {:?}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(plaintext.to_vec())
|
||||
}
|
||||
|
||||
/// Decrypt TLS 1.3 handshake message using CipherSuite
|
||||
///
|
||||
/// Decrypts and extracts handshake message, removing ContentType trailer
|
||||
/// Builds the AAD from the record length and decrypts.
|
||||
/// Returns plaintext with content type trailer stripped.
|
||||
pub fn decrypt_handshake_message(
|
||||
cipher_suite: CipherSuite,
|
||||
key: &[u8],
|
||||
iv: &[u8],
|
||||
sequence_number: u64,
|
||||
seq: u64,
|
||||
ciphertext: &[u8],
|
||||
record_length: u16,
|
||||
) -> Result<Vec<u8>> {
|
||||
// Additional data for decryption
|
||||
let mut additional_data = Vec::new();
|
||||
additional_data.push(0x17); // ApplicationData
|
||||
additional_data.extend_from_slice(&[VERSION_TLS_1_2_MAJOR, VERSION_TLS_1_2_MINOR]); // TLS 1.2
|
||||
additional_data.extend_from_slice(&record_length.to_be_bytes());
|
||||
record_len: u16,
|
||||
) -> io::Result<Vec<u8>> {
|
||||
// Build AAD: TLS record header
|
||||
let aad = [
|
||||
0x17, // ApplicationData
|
||||
0x03,
|
||||
0x03, // TLS 1.2 version
|
||||
(record_len >> 8) as u8,
|
||||
(record_len & 0xff) as u8,
|
||||
];
|
||||
|
||||
let mut plaintext = decrypt_tls13_record(
|
||||
cipher_suite,
|
||||
key,
|
||||
iv,
|
||||
sequence_number,
|
||||
ciphertext,
|
||||
&additional_data,
|
||||
)?;
|
||||
let aead_key = AeadKey::new(cipher_suite, key)?;
|
||||
let mut plaintext = aead_key.open(ciphertext, iv, seq, &aad)?;
|
||||
|
||||
// Remove ContentType trailer
|
||||
if plaintext.is_empty() {
|
||||
return Err(Error::new(ErrorKind::InvalidData, "Empty plaintext"));
|
||||
}
|
||||
|
||||
// TLS 1.3 has format: content | type_byte | padding (zeros)
|
||||
// We need to find the type byte by removing trailing zeros first
|
||||
|
||||
// Remove trailing zeros (padding)
|
||||
while !plaintext.is_empty() && plaintext[plaintext.len() - 1] == 0 {
|
||||
plaintext.pop();
|
||||
}
|
||||
|
||||
if plaintext.is_empty() {
|
||||
return Err(Error::new(ErrorKind::InvalidData, "Plaintext is all zeros"));
|
||||
}
|
||||
|
||||
// Now the last byte should be the content type
|
||||
let content_type = plaintext.pop().unwrap();
|
||||
|
||||
if content_type != 0x16
|
||||
&& content_type != CONTENT_TYPE_APPLICATION_DATA
|
||||
&& content_type != CONTENT_TYPE_ALERT
|
||||
{
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("Invalid content type: 0x{:02x}", content_type),
|
||||
));
|
||||
}
|
||||
// Strip content type and optional padding (external implementations may pad)
|
||||
let _ = strip_content_type_with_padding(&mut plaintext)?;
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn encrypt_tls13_record(
|
||||
cipher_suite: CipherSuite,
|
||||
key: &[u8],
|
||||
iv: &[u8],
|
||||
seq: u64,
|
||||
plaintext: &[u8],
|
||||
aad: &[u8],
|
||||
) -> io::Result<Vec<u8>> {
|
||||
AeadKey::new(cipher_suite, key)?.seal(plaintext, iv, seq, aad)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn decrypt_tls13_record(
|
||||
cipher_suite: CipherSuite,
|
||||
key: &[u8],
|
||||
iv: &[u8],
|
||||
seq: u64,
|
||||
ciphertext: &[u8],
|
||||
aad: &[u8],
|
||||
) -> io::Result<Vec<u8>> {
|
||||
AeadKey::new(cipher_suite, key)?.open(ciphertext, iv, seq, aad)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -243,30 +212,33 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_handshake() {
|
||||
let key = vec![0x11u8; 16];
|
||||
let iv = vec![0x22u8; 12];
|
||||
let handshake_msg = vec![0x33u8; 50];
|
||||
fn test_aead_key_seal_open() {
|
||||
let key = AeadKey::new(CS, &[0x42u8; 16]).unwrap();
|
||||
let iv = [0x99u8; 12];
|
||||
let plaintext = b"Test message";
|
||||
let aad = b"aad";
|
||||
|
||||
// Manually encrypt like encrypt_handshake_to_records does
|
||||
let mut plaintext = handshake_msg.clone();
|
||||
plaintext.push(0x16); // ContentType: Handshake
|
||||
let ciphertext = key.seal(plaintext, &iv, 0, aad).unwrap();
|
||||
let decrypted = key.open(&ciphertext, &iv, 0, aad).unwrap();
|
||||
|
||||
let ciphertext_length = (plaintext.len() + 16) as u16;
|
||||
let aad: [u8; 5] = [
|
||||
0x17, // ApplicationData
|
||||
0x03,
|
||||
0x03, // TLS 1.2
|
||||
(ciphertext_length >> 8) as u8,
|
||||
(ciphertext_length & 0xff) as u8,
|
||||
];
|
||||
assert_eq!(&decrypted[..], plaintext);
|
||||
}
|
||||
|
||||
let ciphertext = encrypt_tls13_record(CS, &key, &iv, 0, &plaintext, &aad).unwrap();
|
||||
#[test]
|
||||
fn test_aead_key_in_place() {
|
||||
let key = AeadKey::new(CS, &[0x42u8; 16]).unwrap();
|
||||
let iv = [0x99u8; 12];
|
||||
let plaintext = b"Test in-place";
|
||||
let aad = b"aad";
|
||||
|
||||
let decrypted =
|
||||
decrypt_handshake_message(CS, &key, &iv, 0, &ciphertext, ciphertext_length).unwrap();
|
||||
let mut buf = plaintext.to_vec();
|
||||
key.seal_in_place(&mut buf, &iv, 0, aad).unwrap();
|
||||
|
||||
assert_eq!(decrypted, handshake_msg);
|
||||
// buf now contains ciphertext + tag
|
||||
assert_eq!(buf.len(), plaintext.len() + 16);
|
||||
|
||||
let decrypted = key.open_in_place_slice(&mut buf, &iv, 0, aad).unwrap();
|
||||
assert_eq!(decrypted, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -393,4 +365,74 @@ mod tests {
|
||||
|
||||
assert_eq!(decrypted, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_handshake_message() {
|
||||
use super::decrypt_handshake_message;
|
||||
use crate::reality::common::CONTENT_TYPE_HANDSHAKE;
|
||||
|
||||
let key = vec![0x42u8; 16];
|
||||
let iv = vec![0x99u8; 12];
|
||||
let handshake_msg = vec![0xABu8; 100]; // Simulated handshake message
|
||||
|
||||
// Build plaintext: handshake_msg || content_type
|
||||
let mut plaintext_with_type = handshake_msg.clone();
|
||||
plaintext_with_type.push(CONTENT_TYPE_HANDSHAKE);
|
||||
|
||||
// Calculate ciphertext length for AAD
|
||||
let ciphertext_len = (plaintext_with_type.len() + 16) as u16;
|
||||
|
||||
// Build AAD (TLS record header)
|
||||
let aad = [
|
||||
0x17, // ApplicationData
|
||||
0x03,
|
||||
0x03, // TLS 1.2 version
|
||||
(ciphertext_len >> 8) as u8,
|
||||
(ciphertext_len & 0xff) as u8,
|
||||
];
|
||||
|
||||
// Encrypt
|
||||
let ciphertext =
|
||||
encrypt_tls13_record(CS, &key, &iv, 0, &plaintext_with_type, &aad).unwrap();
|
||||
|
||||
// Decrypt using decrypt_handshake_message
|
||||
let decrypted =
|
||||
decrypt_handshake_message(CS, &key, &iv, 0, &ciphertext, ciphertext_len).unwrap();
|
||||
|
||||
assert_eq!(decrypted, handshake_msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_handshake_message_with_padding() {
|
||||
use super::decrypt_handshake_message;
|
||||
use crate::reality::common::CONTENT_TYPE_HANDSHAKE;
|
||||
|
||||
let key = vec![0x42u8; 16];
|
||||
let iv = vec![0x99u8; 12];
|
||||
let handshake_msg = vec![0xCDu8; 50];
|
||||
|
||||
// Build plaintext with padding: handshake_msg || content_type || padding_zeros
|
||||
let mut plaintext_with_type_and_padding = handshake_msg.clone();
|
||||
plaintext_with_type_and_padding.push(CONTENT_TYPE_HANDSHAKE);
|
||||
plaintext_with_type_and_padding.extend_from_slice(&[0x00, 0x00, 0x00]); // 3 bytes padding
|
||||
|
||||
let ciphertext_len = (plaintext_with_type_and_padding.len() + 16) as u16;
|
||||
|
||||
let aad = [
|
||||
0x17,
|
||||
0x03,
|
||||
0x03,
|
||||
(ciphertext_len >> 8) as u8,
|
||||
(ciphertext_len & 0xff) as u8,
|
||||
];
|
||||
|
||||
let ciphertext =
|
||||
encrypt_tls13_record(CS, &key, &iv, 0, &plaintext_with_type_and_padding, &aad).unwrap();
|
||||
|
||||
// Padding should be stripped
|
||||
let decrypted =
|
||||
decrypt_handshake_message(CS, &key, &iv, 0, &ciphertext, ciphertext_len).unwrap();
|
||||
|
||||
assert_eq!(decrypted, handshake_msg);
|
||||
}
|
||||
}
|
||||
|
||||
+175
-173
@@ -1,166 +1,123 @@
|
||||
use aws_lc_rs::hmac;
|
||||
use aws_lc_rs::signature::Ed25519KeyPair;
|
||||
use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair};
|
||||
use rcgen::SignatureAlgorithm;
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
|
||||
/// Generate a minimal Ed25519 certificate matching XTLS/REALITY reference size (~178 bytes)
|
||||
/// A signing key that computes HMAC-SHA512(auth_key, public_key) as the "signature".
|
||||
///
|
||||
/// The reference implementation creates a minimal certificate with:
|
||||
/// - Empty Subject and Issuer
|
||||
/// - Serial number 0
|
||||
/// - No SAN (Subject Alternative Name)
|
||||
///
|
||||
/// This matches the reference: `x509.Certificate{SerialNumber: &big.Int{}}`
|
||||
fn generate_template_cert(_hostname: &str) -> (Vec<u8>, Ed25519KeyPair) {
|
||||
// Generate Ed25519 keypair using rcgen
|
||||
let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519)
|
||||
.expect("Failed to generate Ed25519 key pair");
|
||||
|
||||
// Create minimal certificate params matching reference implementation
|
||||
let mut params = rcgen::CertificateParams::default();
|
||||
|
||||
// No Subject Alternative Names (reference has empty DNSNames)
|
||||
params.subject_alt_names = vec![];
|
||||
|
||||
// Empty distinguished name (reference has empty Subject/Issuer)
|
||||
params.distinguished_name = rcgen::DistinguishedName::new();
|
||||
|
||||
// Serial number 0 (reference uses &big.Int{} which is 0)
|
||||
params.serial_number = Some(rcgen::SerialNumber::from(vec![0u8]));
|
||||
|
||||
// Note: rcgen uses 1975-01-01 to 4096-01-01 by default
|
||||
// Reference uses 0001-01-01 for both, but this is close enough
|
||||
// and results in ~176 bytes (vs 178 bytes reference)
|
||||
|
||||
// Create self-signed certificate
|
||||
let cert = params
|
||||
.self_signed(&key_pair)
|
||||
.expect("Failed to create self-signed certificate");
|
||||
|
||||
let cert_der = cert.der().to_vec();
|
||||
|
||||
// Create the Ed25519KeyPair from the PKCS#8 document
|
||||
let signing_key = Ed25519KeyPair::from_pkcs8(key_pair.serialized_der())
|
||||
.expect("Failed to parse generated key");
|
||||
|
||||
use aws_lc_rs::signature::KeyPair;
|
||||
let public_key_bytes = signing_key.public_key().as_ref();
|
||||
|
||||
log::debug!(
|
||||
"REALITY DEBUG: Generated minimal certificate ({} bytes, reference ~178 bytes)",
|
||||
cert_der.len()
|
||||
);
|
||||
log::debug!(
|
||||
"REALITY DEBUG: Using public key: {:02x?}",
|
||||
&public_key_bytes[..16.min(public_key_bytes.len())]
|
||||
);
|
||||
|
||||
(cert_der, signing_key)
|
||||
/// This implements rcgen's SigningKey trait but ignores the TBS (to-be-signed) data,
|
||||
/// instead computing the REALITY HMAC. This allows rcgen to place our HMAC directly
|
||||
/// into the certificate signature field.
|
||||
struct HmacSigningKey {
|
||||
/// The HMAC key derived from auth_key
|
||||
hmac_key: hmac::Key,
|
||||
/// The Ed25519 public key bytes (32 bytes)
|
||||
public_key: [u8; 32],
|
||||
}
|
||||
|
||||
/// Generate HMAC-signed Ed25519 certificate
|
||||
impl rcgen::SigningKey for HmacSigningKey {
|
||||
fn sign(&self, _msg: &[u8]) -> std::result::Result<Vec<u8>, rcgen::Error> {
|
||||
// Ignore the TBS data - compute HMAC-SHA512(auth_key, public_key)
|
||||
let tag = hmac::sign(&self.hmac_key, &self.public_key);
|
||||
Ok(tag.as_ref().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl rcgen::PublicKeyData for HmacSigningKey {
|
||||
fn der_bytes(&self) -> &[u8] {
|
||||
&self.public_key
|
||||
}
|
||||
|
||||
fn algorithm(&self) -> &'static SignatureAlgorithm {
|
||||
&rcgen::PKCS_ED25519
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate HMAC-signed Ed25519 certificate.
|
||||
///
|
||||
/// This follows the uTLS approach:
|
||||
/// 1. Generate a certificate for the destination hostname
|
||||
/// 2. Replace the signature (last 64 bytes) with HMAC-SHA512(auth_key, ed25519_public_key)
|
||||
/// This creates a minimal X.509 certificate where:
|
||||
/// - The public key is a real Ed25519 public key
|
||||
/// - The signature is HMAC-SHA512(auth_key, public_key) instead of a real signature
|
||||
///
|
||||
/// This is NOT a cryptographically valid certificate!
|
||||
/// The returned Ed25519KeyPair is used for:
|
||||
/// 1. Its public key goes in the certificate (and HMAC is computed over it)
|
||||
/// 2. Its private key signs the CertificateVerify message during TLS handshake
|
||||
///
|
||||
/// The client verifies both the HMAC and the CertificateVerify signature.
|
||||
///
|
||||
/// Returns the certificate (call `.der()` to get DER bytes) and the signing keypair.
|
||||
pub fn generate_hmac_certificate(
|
||||
auth_key: &[u8; 32],
|
||||
hostname: &str,
|
||||
) -> Result<(Vec<u8>, Ed25519KeyPair)> {
|
||||
// Generate a new certificate for the destination hostname
|
||||
let (cert_der, signing_key) = generate_template_cert(hostname);
|
||||
) -> Result<(rcgen::Certificate, Ed25519KeyPair)> {
|
||||
// Keypair is needed for CertificateVerify signing
|
||||
let ed25519_keypair = Ed25519KeyPair::generate()
|
||||
.map_err(|_| Error::other("Failed to generate Ed25519 keypair"))?;
|
||||
|
||||
let public_key: [u8; 32] = ed25519_keypair
|
||||
.public_key()
|
||||
.as_ref()
|
||||
.try_into()
|
||||
.map_err(|_| Error::new(ErrorKind::InvalidData, "Ed25519 public key is not 32 bytes"))?;
|
||||
|
||||
let hmac_key = HmacSigningKey {
|
||||
hmac_key: hmac::Key::new(hmac::HMAC_SHA512, auth_key),
|
||||
public_key,
|
||||
};
|
||||
|
||||
// Create minimal certificate params matching reference implementation
|
||||
let mut params = rcgen::CertificateParams::default();
|
||||
// Reference impl doesn't have a hostname, but we set it anyway
|
||||
params.subject_alt_names =
|
||||
vec![rcgen::SanType::DnsName(hostname.try_into().map_err(
|
||||
|_| Error::new(ErrorKind::InvalidInput, "Invalid hostname"),
|
||||
)?)];
|
||||
params.distinguished_name = rcgen::DistinguishedName::new();
|
||||
params.serial_number = Some(rcgen::SerialNumber::from(vec![0u8]));
|
||||
|
||||
let cert = params
|
||||
.self_signed(&hmac_key)
|
||||
.map_err(|e| Error::other(format!("Failed to create certificate: {e}")))?;
|
||||
|
||||
log::debug!(
|
||||
"REALITY DEBUG: Using template certificate ({} bytes)",
|
||||
cert_der.len()
|
||||
"REALITY: Generated HMAC certificate ({} bytes)",
|
||||
cert.der().len()
|
||||
);
|
||||
|
||||
// Extract the Ed25519 public key from the signing key
|
||||
use aws_lc_rs::signature::KeyPair;
|
||||
let public_key_bytes = signing_key.public_key().as_ref();
|
||||
|
||||
// Replace signature with HMAC
|
||||
let cert_with_hmac = replace_signature_with_hmac(cert_der, auth_key, public_key_bytes)?;
|
||||
|
||||
log::debug!("REALITY DEBUG: Replaced signature with HMAC");
|
||||
|
||||
// Return the HMAC-signed certificate with the signing key
|
||||
Ok((cert_with_hmac, signing_key))
|
||||
}
|
||||
|
||||
/// Replace the certificate signature with HMAC-SHA512(auth_key, ed25519_public_key)
|
||||
fn replace_signature_with_hmac(
|
||||
mut cert_der: Vec<u8>,
|
||||
auth_key: &[u8; 32],
|
||||
public_key_bytes: &[u8],
|
||||
) -> Result<Vec<u8>> {
|
||||
// The signature is the last BIT STRING in the certificate DER
|
||||
// For Ed25519, it's always 64 bytes
|
||||
|
||||
// Find the signature BIT STRING tag
|
||||
// It should be near the end of the certificate
|
||||
// BIT STRING tag = 0x03
|
||||
// Length = 0x41 (65 bytes: 1 byte unused bits + 64 bytes signature)
|
||||
|
||||
// Search backwards for BIT STRING tag
|
||||
let mut sig_offset = None;
|
||||
for i in (0..cert_der.len().saturating_sub(66)).rev() {
|
||||
if cert_der[i] == 0x03 && cert_der[i + 1] == 0x41 && cert_der[i + 2] == 0x00 {
|
||||
// Found it!
|
||||
sig_offset = Some(i + 3); // Skip tag, length, unused bits
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let sig_offset = sig_offset.ok_or_else(|| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"Could not find signature in certificate DER",
|
||||
)
|
||||
})?;
|
||||
|
||||
if sig_offset + 64 > cert_der.len() {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"Invalid signature offset",
|
||||
));
|
||||
}
|
||||
|
||||
// Compute HMAC-SHA512(auth_key, ed25519_public_key)
|
||||
let key = hmac::Key::new(hmac::HMAC_SHA512, auth_key);
|
||||
let tag = hmac::sign(&key, public_key_bytes);
|
||||
let hmac_bytes = tag.as_ref();
|
||||
|
||||
// Replace signature bytes with HMAC
|
||||
cert_der[sig_offset..sig_offset + 64].copy_from_slice(hmac_bytes);
|
||||
|
||||
log::debug!(
|
||||
"REALITY DEBUG: HMAC signature (first 16 bytes): {:?}",
|
||||
&hmac_bytes[..16]
|
||||
);
|
||||
|
||||
Ok(cert_der)
|
||||
Ok((cert, ed25519_keypair))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use aws_lc_rs::signature::KeyPair;
|
||||
|
||||
/// Find the signature bytes in a DER-encoded certificate.
|
||||
/// Returns the offset where the 64-byte Ed25519 signature starts.
|
||||
fn find_signature_offset(cert_der: &[u8]) -> Option<usize> {
|
||||
// The signature is the last BIT STRING in the certificate DER
|
||||
// For Ed25519, it's always 64 bytes
|
||||
// BIT STRING tag = 0x03
|
||||
// Length = 0x41 (65 bytes: 1 byte unused bits + 64 bytes signature)
|
||||
for i in (0..cert_der.len().saturating_sub(66)).rev() {
|
||||
if cert_der[i] == 0x03 && cert_der[i + 1] == 0x41 && cert_der[i + 2] == 0x00 {
|
||||
return Some(i + 3); // Skip tag, length, unused bits
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_hmac_certificate() {
|
||||
let auth_key = [42u8; 32];
|
||||
let hostname = "test.example.com";
|
||||
let result = generate_hmac_certificate(&auth_key, hostname);
|
||||
let result = generate_hmac_certificate(&auth_key, "test.example.com");
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
let (cert_der, _signing_key) = result.unwrap();
|
||||
let (cert, _signing_key) = result.unwrap();
|
||||
let cert_der = cert.der();
|
||||
|
||||
// Print actual size for comparison with reference (178 bytes)
|
||||
println!("shoes certificate size: {} bytes", cert_der.len());
|
||||
|
||||
// Certificate should be a reasonable size (few hundred bytes)
|
||||
// Certificate should be a reasonable size
|
||||
assert!(cert_der.len() > 100);
|
||||
assert!(cert_der.len() < 1000);
|
||||
|
||||
@@ -169,58 +126,103 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minimal_certificate_size() {
|
||||
use rcgen::{DistinguishedName, SerialNumber};
|
||||
fn test_hmac_placed_at_correct_offset() {
|
||||
// This test verifies that our HmacSigningKey places the HMAC bytes
|
||||
// at exactly the same location that find_signature_offset() identifies.
|
||||
|
||||
let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519).unwrap();
|
||||
let auth_key = [0x42u8; 32];
|
||||
|
||||
// Try to create minimal params similar to Go's x509.Certificate{SerialNumber: &big.Int{}}
|
||||
let mut params = rcgen::CertificateParams::default();
|
||||
let (cert, signing_key) = generate_hmac_certificate(&auth_key, "test.example.com").unwrap();
|
||||
let cert_der = cert.der();
|
||||
|
||||
// Clear subject_alt_names
|
||||
params.subject_alt_names = vec![];
|
||||
// Find where the signature should be
|
||||
let sig_offset =
|
||||
find_signature_offset(cert_der).expect("Should find signature offset in certificate");
|
||||
|
||||
// Empty distinguished name (no CN, etc.)
|
||||
params.distinguished_name = DistinguishedName::new();
|
||||
// Compute what the HMAC should be
|
||||
let public_key_bytes = signing_key.public_key().as_ref();
|
||||
let key = hmac::Key::new(hmac::HMAC_SHA512, &auth_key);
|
||||
let expected_hmac = hmac::sign(&key, public_key_bytes);
|
||||
|
||||
// Serial number 0
|
||||
params.serial_number = Some(SerialNumber::from(vec![0u8]));
|
||||
// Extract the signature bytes from the certificate
|
||||
let actual_signature = &cert_der[sig_offset..sig_offset + 64];
|
||||
|
||||
// Note: rcgen's default dates are 1975-01-01 to 4096-01-01
|
||||
// The reference uses 0001-01-01 for both, which rcgen doesn't easily support
|
||||
// This will make our cert slightly larger due to date encoding
|
||||
|
||||
let cert = params.self_signed(&key_pair).unwrap();
|
||||
let cert_der = cert.der().to_vec();
|
||||
|
||||
println!(
|
||||
"Minimal rcgen certificate size: {} bytes (reference: 178 bytes)",
|
||||
cert_der.len()
|
||||
// They should match exactly
|
||||
assert_eq!(
|
||||
actual_signature,
|
||||
expected_hmac.as_ref(),
|
||||
"HMAC signature should be placed at the correct offset in the certificate"
|
||||
);
|
||||
println!("Certificate hex: {:02x?}", &cert_der);
|
||||
|
||||
// Let's see if we can get close to 178 bytes
|
||||
// If not, we may need to use yasna directly
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hmac_signature_deterministic() {
|
||||
fn test_different_keys_produce_different_hmacs() {
|
||||
let auth_key = [99u8; 32];
|
||||
let hostname = "test.example.com";
|
||||
|
||||
let (cert1, key1) = generate_hmac_certificate(&auth_key, hostname).unwrap();
|
||||
use aws_lc_rs::signature::KeyPair;
|
||||
let public_key1 = key1.public_key().as_ref().to_vec();
|
||||
|
||||
// Generate another certificate with same auth_key
|
||||
let (cert2, key2) = generate_hmac_certificate(&auth_key, hostname).unwrap();
|
||||
let public_key2 = key2.public_key().as_ref().to_vec();
|
||||
let (cert1, key1) = generate_hmac_certificate(&auth_key, "test.example.com").unwrap();
|
||||
let (cert2, key2) = generate_hmac_certificate(&auth_key, "test.example.com").unwrap();
|
||||
let cert1_der = cert1.der();
|
||||
let cert2_der = cert2.der();
|
||||
|
||||
// Different Ed25519 keys (random generation)
|
||||
assert_ne!(public_key1, public_key2);
|
||||
assert_ne!(
|
||||
key1.public_key().as_ref(),
|
||||
key2.public_key().as_ref(),
|
||||
"Each call should generate a new random keypair"
|
||||
);
|
||||
|
||||
// But certificates should have different signatures
|
||||
// (because they're based on different public keys)
|
||||
assert_ne!(cert1, cert2);
|
||||
// Different certificates (different public keys -> different HMACs)
|
||||
assert_ne!(cert1_der.as_ref(), cert2_der.as_ref());
|
||||
|
||||
// But both should have valid HMAC signatures
|
||||
let sig_offset1 = find_signature_offset(cert1_der).unwrap();
|
||||
let sig_offset2 = find_signature_offset(cert2_der).unwrap();
|
||||
|
||||
// Verify HMAC for cert1
|
||||
let hmac_key = hmac::Key::new(hmac::HMAC_SHA512, &auth_key);
|
||||
let expected1 = hmac::sign(&hmac_key, key1.public_key().as_ref());
|
||||
assert_eq!(
|
||||
&cert1_der[sig_offset1..sig_offset1 + 64],
|
||||
expected1.as_ref()
|
||||
);
|
||||
|
||||
// Verify HMAC for cert2
|
||||
let expected2 = hmac::sign(&hmac_key, key2.public_key().as_ref());
|
||||
assert_eq!(
|
||||
&cert2_der[sig_offset2..sig_offset2 + 64],
|
||||
expected2.as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_offset_consistent() {
|
||||
// Generate multiple certificates and verify the signature is always
|
||||
// at the same relative position (since cert structure is identical)
|
||||
let auth_key = [0xABu8; 32];
|
||||
|
||||
let mut offsets = Vec::new();
|
||||
let mut sizes = Vec::new();
|
||||
|
||||
for _ in 0..5 {
|
||||
let (cert, _) = generate_hmac_certificate(&auth_key, "example.com").unwrap();
|
||||
let cert_der = cert.der();
|
||||
let offset = find_signature_offset(cert_der).unwrap();
|
||||
offsets.push(offset);
|
||||
sizes.push(cert_der.len());
|
||||
}
|
||||
|
||||
// All certificates should have the same size
|
||||
assert!(
|
||||
sizes.iter().all(|&s| s == sizes[0]),
|
||||
"All certificates should have identical size: {:?}",
|
||||
sizes
|
||||
);
|
||||
|
||||
// All signatures should be at the same offset
|
||||
assert!(
|
||||
offsets.iter().all(|&o| o == offsets[0]),
|
||||
"All signatures should be at the same offset: {:?}",
|
||||
offsets
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
// REALITY client-side connection
|
||||
//
|
||||
// This implements a rustls-compatible API for REALITY protocol client connections,
|
||||
// allowing REALITY to be used as a drop-in replacement for rustls.
|
||||
// REALITY client connection with rustls-compatible API
|
||||
|
||||
use std::io::{self, Read, Write};
|
||||
|
||||
use crate::slide_buffer::SlideBuffer;
|
||||
|
||||
use aws_lc_rs::{
|
||||
agreement, digest,
|
||||
rand::{SecureRandom, SystemRandom},
|
||||
};
|
||||
use aws_lc_rs::{agreement, digest};
|
||||
use rand::RngCore;
|
||||
|
||||
use super::common::{
|
||||
self, ALERT_DESC_CLOSE_NOTIFY, ALERT_LEVEL_WARNING, CIPHERTEXT_READ_BUF_CAPACITY,
|
||||
CONTENT_TYPE_ALERT, CONTENT_TYPE_APPLICATION_DATA, CONTENT_TYPE_CHANGE_CIPHER_SPEC,
|
||||
CONTENT_TYPE_HANDSHAKE, HANDSHAKE_TYPE_CERTIFICATE, HANDSHAKE_TYPE_CERTIFICATE_VERIFY,
|
||||
HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS, HANDSHAKE_TYPE_FINISHED, PLAINTEXT_READ_BUF_CAPACITY,
|
||||
TLS_MAX_RECORD_SIZE, TLS_RECORD_HEADER_SIZE, increment_seq,
|
||||
ALERT_DESC_CLOSE_NOTIFY, ALERT_LEVEL_WARNING, CIPHERTEXT_READ_BUF_CAPACITY, CONTENT_TYPE_ALERT,
|
||||
CONTENT_TYPE_APPLICATION_DATA, CONTENT_TYPE_CHANGE_CIPHER_SPEC, CONTENT_TYPE_HANDSHAKE,
|
||||
HANDSHAKE_TYPE_CERTIFICATE, HANDSHAKE_TYPE_CERTIFICATE_VERIFY,
|
||||
HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS, HANDSHAKE_TYPE_FINISHED, OUTGOING_BUFFER_LIMIT,
|
||||
PLAINTEXT_READ_BUF_CAPACITY, TLS_MAX_RECORD_SIZE, TLS_RECORD_HEADER_SIZE,
|
||||
};
|
||||
use super::reality_aead::{decrypt_handshake_message, decrypt_tls13_record};
|
||||
use super::reality_aead::{AeadKey, decrypt_handshake_message};
|
||||
use super::reality_auth::{derive_auth_key, encrypt_session_id, perform_ecdh};
|
||||
use super::reality_cipher_suite::{CipherSuite, DEFAULT_CIPHER_SUITES};
|
||||
use super::reality_client_verify::{extract_certificate_der, verify_certificate_hmac};
|
||||
use super::reality_client_verify::{
|
||||
extract_certificate_der, extract_certificate_verify_signature, extract_ed25519_public_key,
|
||||
verify_certificate_hmac, verify_certificate_verify_signature,
|
||||
};
|
||||
use super::reality_io_state::RealityIoState;
|
||||
use super::reality_reader_writer::{RealityReader, RealityWriter};
|
||||
use super::reality_records::{encrypt_handshake_to_records, encrypt_plaintext_to_records};
|
||||
use super::reality_records::{RecordDecryptor, RecordEncryptor};
|
||||
use super::reality_tls13_keys::{
|
||||
compute_finished_verify_data, derive_application_secrets, derive_handshake_keys,
|
||||
derive_traffic_keys,
|
||||
};
|
||||
use super::reality_tls13_messages::*;
|
||||
use super::reality_tls13_messages::{
|
||||
DEFAULT_ALPN_PROTOCOLS, construct_client_hello, construct_finished, write_record_header,
|
||||
};
|
||||
use super::reality_util::{extract_server_cipher_suite, extract_server_public_key};
|
||||
use crate::slide_buffer::SlideBuffer;
|
||||
use crate::util::allocate_vec;
|
||||
|
||||
/// Configuration for REALITY client connections
|
||||
#[derive(Clone)]
|
||||
@@ -62,6 +62,13 @@ enum HandshakeState {
|
||||
cipher_suite: CipherSuite,
|
||||
handshake_transcript_bytes: Vec<u8>, // Accumulated transcript for hash computation
|
||||
auth_key: [u8; 32], // REALITY authentication key for HMAC verification
|
||||
// State for handling multiple encrypted handshake records (separate mode)
|
||||
handshake_seq: u64, // Sequence number for decrypting records
|
||||
accumulated_plaintext: Vec<u8>, // Accumulated plaintext across records
|
||||
messages_found: u8, // Number of handshake messages found so far
|
||||
certificate_verified: bool, // Whether Certificate HMAC was verified
|
||||
ed25519_public_key: Option<[u8; 32]>, // Public key from Certificate for CV verification
|
||||
cert_verify_offset: Option<usize>, // Offset of CertificateVerify in accumulated plaintext
|
||||
},
|
||||
/// Handshake complete, ready for application data
|
||||
Complete,
|
||||
@@ -76,16 +83,17 @@ pub struct RealityClientConnection {
|
||||
handshake_state: HandshakeState,
|
||||
|
||||
// TLS 1.3 application traffic encryption (post-handshake)
|
||||
app_read_key: Option<Vec<u8>>,
|
||||
// Keys are cached as AeadKey to avoid per-record key setup overhead
|
||||
app_read_key: Option<AeadKey>,
|
||||
app_read_iv: Option<Vec<u8>>,
|
||||
app_write_key: Option<Vec<u8>>,
|
||||
app_write_key: Option<AeadKey>,
|
||||
app_write_iv: Option<Vec<u8>>,
|
||||
read_seq: u64,
|
||||
write_seq: u64,
|
||||
cipher_suite: Option<CipherSuite>,
|
||||
|
||||
// Pre-allocated buffer for TLS read operations (reused across calls)
|
||||
tls_read_buffer: Box<[u8; TLS_MAX_RECORD_SIZE]>,
|
||||
tls_read_buffer: Box<[u8]>,
|
||||
|
||||
// Buffers for I/O - using SlideBuffer for efficient zero-alloc operations
|
||||
ciphertext_read_buf: SlideBuffer, // Incoming encrypted TLS records
|
||||
@@ -115,16 +123,15 @@ impl RealityClientConnection {
|
||||
read_seq: 0,
|
||||
write_seq: 0,
|
||||
cipher_suite: None,
|
||||
tls_read_buffer: Box::new([0u8; TLS_MAX_RECORD_SIZE]),
|
||||
tls_read_buffer: allocate_vec(TLS_MAX_RECORD_SIZE).into_boxed_slice(),
|
||||
ciphertext_read_buf: SlideBuffer::new(CIPHERTEXT_READ_BUF_CAPACITY),
|
||||
ciphertext_write_buf: Vec::with_capacity(CIPHERTEXT_READ_BUF_CAPACITY),
|
||||
ciphertext_write_buf: Vec::with_capacity(OUTGOING_BUFFER_LIMIT),
|
||||
plaintext_read_buf: SlideBuffer::new(PLAINTEXT_READ_BUF_CAPACITY),
|
||||
plaintext_write_buf: Vec::with_capacity(TLS_MAX_RECORD_SIZE),
|
||||
plaintext_write_buf: Vec::with_capacity(OUTGOING_BUFFER_LIMIT),
|
||||
received_close_notify: false,
|
||||
fatal_error: None,
|
||||
};
|
||||
|
||||
// Generate ClientHello immediately
|
||||
conn.generate_client_hello()?;
|
||||
|
||||
Ok(conn)
|
||||
@@ -132,12 +139,10 @@ impl RealityClientConnection {
|
||||
|
||||
/// Generate and buffer ClientHello
|
||||
fn generate_client_hello(&mut self) -> io::Result<()> {
|
||||
let rng = SystemRandom::new();
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// Generate our X25519 keypair
|
||||
let mut our_private_bytes = [0u8; 32];
|
||||
rng.fill(&mut our_private_bytes)
|
||||
.map_err(|_| io::Error::other("RNG failed"))?;
|
||||
rng.fill_bytes(&mut our_private_bytes);
|
||||
|
||||
let our_private_key =
|
||||
agreement::PrivateKey::from_private_key(&agreement::X25519, &our_private_bytes)
|
||||
@@ -146,10 +151,8 @@ impl RealityClientConnection {
|
||||
.compute_public_key()
|
||||
.map_err(|_| io::Error::other("Failed to compute public key"))?;
|
||||
|
||||
// Generate client random
|
||||
let mut client_random = [0u8; 32];
|
||||
rng.fill(&mut client_random)
|
||||
.map_err(|_| io::Error::other("RNG failed"))?;
|
||||
rng.fill_bytes(&mut client_random);
|
||||
|
||||
// Perform ECDH with server's public key to derive auth key
|
||||
let shared_secret = perform_ecdh(&our_private_bytes, &self.config.public_key)
|
||||
@@ -193,6 +196,7 @@ impl RealityClientConnection {
|
||||
our_public_key_bytes.as_ref(),
|
||||
&self.config.server_name,
|
||||
&cipher_suite_ids,
|
||||
DEFAULT_ALPN_PROTOCOLS,
|
||||
)?;
|
||||
|
||||
// Now encrypt the SessionId using the ClientHello with zeroed SessionId as AAD
|
||||
@@ -222,14 +226,10 @@ impl RealityClientConnection {
|
||||
&encrypted_session_id
|
||||
);
|
||||
|
||||
// Replace the zeros with the encrypted SessionId
|
||||
client_hello[39..71].copy_from_slice(&encrypted_session_id);
|
||||
|
||||
// Wrap in TLS record
|
||||
let mut record = write_record_header(CONTENT_TYPE_HANDSHAKE, client_hello.len() as u16);
|
||||
record.extend_from_slice(&client_hello);
|
||||
|
||||
// Buffer for sending
|
||||
self.ciphertext_write_buf.extend_from_slice(&record);
|
||||
|
||||
// Update state - store raw ClientHello bytes for transcript hash computation
|
||||
@@ -252,12 +252,10 @@ impl RealityClientConnection {
|
||||
///
|
||||
/// Uses pre-allocated buffer to avoid allocation on every call.
|
||||
pub fn read_tls(&mut self, rd: &mut dyn Read) -> io::Result<usize> {
|
||||
// Compact if remaining capacity is insufficient for a full TLS record
|
||||
if self.ciphertext_read_buf.remaining_capacity() < TLS_MAX_RECORD_SIZE {
|
||||
self.ciphertext_read_buf.compact();
|
||||
}
|
||||
|
||||
// Read into pre-allocated buffer
|
||||
let n = rd.read(&mut self.tls_read_buffer[..])?;
|
||||
if n > 0 {
|
||||
self.ciphertext_read_buf
|
||||
@@ -271,21 +269,18 @@ impl RealityClientConnection {
|
||||
/// Like rustls, this loops until no more progress can be made, ensuring
|
||||
/// that piggybacked application data is processed in the same call.
|
||||
pub fn process_new_packets(&mut self) -> io::Result<RealityIoState> {
|
||||
// Return persisted error if connection is in fatal error state (rustls pattern)
|
||||
if let Some(error_kind) = self.fatal_error {
|
||||
return Err(io::Error::new(error_kind, "connection previously failed"));
|
||||
}
|
||||
|
||||
// Don't process more data after receiving close_notify (RFC 8446)
|
||||
// RFC 8446: don't process data after close_notify
|
||||
if self.received_close_notify {
|
||||
return Ok(RealityIoState::new(self.plaintext_read_buf.len()));
|
||||
}
|
||||
|
||||
let result = self.process_new_packets_inner();
|
||||
|
||||
// Persist fatal errors
|
||||
if let Err(ref e) = result {
|
||||
// Persist error for certain error kinds that indicate fatal connection failure
|
||||
match e.kind() {
|
||||
io::ErrorKind::InvalidData
|
||||
| io::ErrorKind::PermissionDenied
|
||||
@@ -327,35 +322,33 @@ impl RealityClientConnection {
|
||||
/// Returns true if a complete record was processed, false if more data needed
|
||||
#[inline]
|
||||
fn process_server_hello(&mut self) -> io::Result<bool> {
|
||||
// Extract state
|
||||
let (client_hello_bytes, client_private_key, auth_key) = match &self.handshake_state {
|
||||
HandshakeState::AwaitingServerHello {
|
||||
client_hello_bytes,
|
||||
client_private_key,
|
||||
auth_key,
|
||||
} => (client_hello_bytes.clone(), *client_private_key, *auth_key),
|
||||
_ => unreachable!(), // Wrong state
|
||||
let HandshakeState::AwaitingServerHello {
|
||||
client_hello_bytes,
|
||||
client_private_key,
|
||||
auth_key,
|
||||
} = &self.handshake_state
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
// Check if we have enough data for a TLS record header
|
||||
if self.ciphertext_read_buf.len() < TLS_RECORD_HEADER_SIZE {
|
||||
return Ok(false); // Need more data
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Parse TLS record length
|
||||
let record_len = self
|
||||
.ciphertext_read_buf
|
||||
.get_u16_be(3)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Buffer too short"))?
|
||||
as usize;
|
||||
|
||||
// Check if we have the complete record
|
||||
let total_record_len = TLS_RECORD_HEADER_SIZE + record_len;
|
||||
if self.ciphertext_read_buf.len() < total_record_len {
|
||||
return Ok(false); // Need more data
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Copy ServerHello record to Vec for processing
|
||||
// Clone fields before consuming buffer
|
||||
let client_hello_bytes = client_hello_bytes.clone();
|
||||
|
||||
let record: Vec<u8> = self.ciphertext_read_buf[..total_record_len].to_vec();
|
||||
self.ciphertext_read_buf.consume(total_record_len);
|
||||
let server_hello = &record[TLS_RECORD_HEADER_SIZE..]; // Skip TLS record header (includes handshake header)
|
||||
@@ -366,10 +359,7 @@ impl RealityClientConnection {
|
||||
server_hello
|
||||
);
|
||||
|
||||
// Extract server public key from ServerHello
|
||||
let server_public_key = extract_server_public_key(&record)?;
|
||||
|
||||
// Extract and validate server's chosen cipher suite
|
||||
let cipher_suite_id = extract_server_cipher_suite(&record)?;
|
||||
let cipher_suite = CipherSuite::from_id(cipher_suite_id).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
@@ -385,7 +375,7 @@ impl RealityClientConnection {
|
||||
cipher_suite_id
|
||||
);
|
||||
|
||||
// Now compute transcript hash using the negotiated cipher suite's hash algorithm
|
||||
// Compute transcript hash using negotiated cipher suite's algorithm
|
||||
let mut full_transcript = digest::Context::new(cipher_suite.digest_algorithm());
|
||||
log::debug!(
|
||||
"REALITY CLIENT: Transcript includes ClientHello ({} bytes), first bytes: {:02x?}",
|
||||
@@ -402,18 +392,16 @@ impl RealityClientConnection {
|
||||
let server_hello_hash = full_transcript.finish();
|
||||
let server_hello_hash_vec: Vec<u8> = server_hello_hash.as_ref().to_vec();
|
||||
|
||||
// Also compute client_hello_hash for key derivation
|
||||
let client_hello_hash_vec: Vec<u8> = {
|
||||
let mut ctx = digest::Context::new(cipher_suite.digest_algorithm());
|
||||
ctx.update(&client_hello_bytes);
|
||||
ctx.finish().as_ref().to_vec()
|
||||
};
|
||||
|
||||
// Perform ECDH for TLS 1.3 key derivation
|
||||
let peer_public_key =
|
||||
agreement::UnparsedPublicKey::new(&agreement::X25519, &server_public_key);
|
||||
let my_private_key =
|
||||
agreement::PrivateKey::from_private_key(&agreement::X25519, &client_private_key)
|
||||
agreement::PrivateKey::from_private_key(&agreement::X25519, client_private_key)
|
||||
.map_err(|_| io::Error::other("Failed to create private key"))?;
|
||||
|
||||
let mut tls_shared_secret = [0u8; 32];
|
||||
@@ -427,8 +415,6 @@ impl RealityClientConnection {
|
||||
},
|
||||
)?;
|
||||
|
||||
// Derive handshake keys
|
||||
// Removed verbose secret logging for security
|
||||
let hs_keys = derive_handshake_keys(
|
||||
cipher_suite,
|
||||
&tls_shared_secret,
|
||||
@@ -438,70 +424,68 @@ impl RealityClientConnection {
|
||||
|
||||
log::debug!("REALITY: ServerHello processed, handshake keys derived");
|
||||
|
||||
// Initialize transcript with actual ClientHello and ServerHello bytes (not hashes!)
|
||||
// Use actual bytes (not hashes) for transcript
|
||||
let mut transcript_bytes = Vec::new();
|
||||
transcript_bytes.extend_from_slice(&client_hello_bytes); // Actual ClientHello bytes
|
||||
transcript_bytes.extend_from_slice(server_hello); // Actual ServerHello bytes
|
||||
transcript_bytes.extend_from_slice(&client_hello_bytes);
|
||||
transcript_bytes.extend_from_slice(server_hello);
|
||||
|
||||
// Update state with server's chosen cipher suite
|
||||
self.handshake_state = HandshakeState::ProcessingHandshake {
|
||||
client_handshake_traffic_secret: hs_keys.client_handshake_traffic_secret.clone(),
|
||||
server_handshake_traffic_secret: hs_keys.server_handshake_traffic_secret.clone(),
|
||||
master_secret: hs_keys.master_secret.clone(),
|
||||
cipher_suite,
|
||||
handshake_transcript_bytes: transcript_bytes,
|
||||
auth_key, // Pass auth_key for certificate HMAC verification
|
||||
auth_key: *auth_key, // Pass auth_key for certificate HMAC verification
|
||||
// Initialize state for handling multiple encrypted handshake records
|
||||
handshake_seq: 0,
|
||||
accumulated_plaintext: Vec::new(),
|
||||
messages_found: 0,
|
||||
certificate_verified: false,
|
||||
ed25519_public_key: None,
|
||||
cert_verify_offset: None,
|
||||
};
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Process encrypted handshake messages (EncryptedExtensions, Certificate, CertificateVerify, Finished)
|
||||
/// Handles both combined (1 record) and separate (multiple records) modes.
|
||||
/// Returns true if a complete record was processed, false if more data needed
|
||||
#[inline]
|
||||
fn process_encrypted_handshake(&mut self) -> io::Result<bool> {
|
||||
// Extract state - we need to preserve it across multiple calls
|
||||
let (
|
||||
client_hs_secret,
|
||||
server_hs_secret,
|
||||
let HandshakeState::ProcessingHandshake {
|
||||
client_handshake_traffic_secret,
|
||||
server_handshake_traffic_secret,
|
||||
master_secret,
|
||||
cipher_suite,
|
||||
transcript_bytes,
|
||||
handshake_transcript_bytes,
|
||||
auth_key,
|
||||
) = match &self.handshake_state {
|
||||
HandshakeState::ProcessingHandshake {
|
||||
client_handshake_traffic_secret,
|
||||
server_handshake_traffic_secret,
|
||||
master_secret,
|
||||
cipher_suite,
|
||||
handshake_transcript_bytes,
|
||||
auth_key,
|
||||
} => (
|
||||
client_handshake_traffic_secret.clone(),
|
||||
server_handshake_traffic_secret.clone(),
|
||||
master_secret.clone(),
|
||||
*cipher_suite,
|
||||
handshake_transcript_bytes.clone(),
|
||||
*auth_key,
|
||||
),
|
||||
_ => unreachable!(), // Wrong state
|
||||
handshake_seq,
|
||||
accumulated_plaintext,
|
||||
messages_found,
|
||||
certificate_verified,
|
||||
ed25519_public_key,
|
||||
cert_verify_offset,
|
||||
} = &self.handshake_state
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
// Derive server handshake traffic keys for decryption
|
||||
let (server_hs_key, server_hs_iv) = derive_traffic_keys(&server_hs_secret, cipher_suite)?;
|
||||
let (server_hs_key, server_hs_iv) =
|
||||
derive_traffic_keys(server_handshake_traffic_secret, *cipher_suite)?;
|
||||
|
||||
log::debug!(
|
||||
"REALITY CLIENT: Server HS key={:02x?}, iv={:02x?}",
|
||||
&server_hs_key[..16],
|
||||
&server_hs_iv
|
||||
);
|
||||
if *handshake_seq == 0 {
|
||||
log::debug!(
|
||||
"REALITY CLIENT: Server HS key={:02x?}, iv={:02x?}",
|
||||
&server_hs_key[..16],
|
||||
&server_hs_iv
|
||||
);
|
||||
}
|
||||
|
||||
// Check if we have enough data for a TLS record header
|
||||
if self.ciphertext_read_buf.len() < TLS_RECORD_HEADER_SIZE {
|
||||
return Ok(false); // Need more data
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check record type
|
||||
let record_type = self.ciphertext_read_buf[0];
|
||||
let tls_version = self
|
||||
.ciphertext_read_buf
|
||||
@@ -520,24 +504,21 @@ impl RealityClientConnection {
|
||||
record_len
|
||||
);
|
||||
|
||||
// Check if we have the complete record
|
||||
let total_record_len = TLS_RECORD_HEADER_SIZE + record_len;
|
||||
if self.ciphertext_read_buf.len() < total_record_len {
|
||||
return Ok(false); // Need more data
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Skip ChangeCipherSpec records - these are dummy in TLS 1.3
|
||||
// Skip ChangeCipherSpec (dummy in TLS 1.3)
|
||||
if record_type == CONTENT_TYPE_CHANGE_CIPHER_SPEC {
|
||||
log::debug!(
|
||||
"REALITY CLIENT: Skipping ChangeCipherSpec record ({} bytes)",
|
||||
record_len
|
||||
);
|
||||
self.ciphertext_read_buf.consume(total_record_len);
|
||||
// Process next record recursively
|
||||
return self.process_encrypted_handshake();
|
||||
}
|
||||
|
||||
// If it's not Application Data, we have a problem
|
||||
if record_type != CONTENT_TYPE_APPLICATION_DATA {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
@@ -548,83 +529,71 @@ impl RealityClientConnection {
|
||||
));
|
||||
}
|
||||
|
||||
// Copy and extract the combined handshake record
|
||||
// NOW we're committed to processing - clone/copy fields we need to modify
|
||||
let client_hs_secret = client_handshake_traffic_secret.clone();
|
||||
let server_hs_secret = server_handshake_traffic_secret.clone();
|
||||
let master_secret = master_secret.clone();
|
||||
let transcript_bytes = handshake_transcript_bytes.clone();
|
||||
let mut accumulated_plaintext = accumulated_plaintext.clone();
|
||||
let cipher_suite = *cipher_suite;
|
||||
let auth_key = *auth_key;
|
||||
let mut handshake_seq = *handshake_seq;
|
||||
let mut messages_found = *messages_found;
|
||||
let mut certificate_verified = *certificate_verified;
|
||||
let mut ed25519_public_key = *ed25519_public_key;
|
||||
let mut cert_verify_offset = *cert_verify_offset;
|
||||
|
||||
// Copy and extract the encrypted handshake record
|
||||
let ciphertext: Vec<u8> =
|
||||
self.ciphertext_read_buf[TLS_RECORD_HEADER_SIZE..total_record_len].to_vec();
|
||||
self.ciphertext_read_buf.consume(total_record_len);
|
||||
|
||||
log::debug!(
|
||||
"REALITY CLIENT: Decrypting combined handshake record - record_type=0x{:02x}, record_len={}, seq=0",
|
||||
record_type,
|
||||
"REALITY CLIENT: Decrypting handshake record #{} - record_len={}",
|
||||
handshake_seq,
|
||||
record_len
|
||||
);
|
||||
|
||||
// Try different sequence numbers - other clients might count differently.
|
||||
let mut combined_plaintext = None;
|
||||
for seq in 0..3 {
|
||||
match decrypt_handshake_message(
|
||||
cipher_suite,
|
||||
&server_hs_key,
|
||||
&server_hs_iv,
|
||||
seq,
|
||||
&ciphertext,
|
||||
record_len as u16,
|
||||
) {
|
||||
Ok(plaintext) => {
|
||||
combined_plaintext = Some(plaintext);
|
||||
break;
|
||||
}
|
||||
Err(_) if seq < 2 => {
|
||||
log::debug!(
|
||||
"REALITY CLIENT: Decryption failed with seq={}, trying next",
|
||||
seq
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"REALITY CLIENT: Failed to decrypt combined handshake with all sequence numbers: {}",
|
||||
e
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Decrypt using current sequence number
|
||||
let plaintext = decrypt_handshake_message(
|
||||
cipher_suite,
|
||||
&server_hs_key,
|
||||
&server_hs_iv,
|
||||
handshake_seq,
|
||||
&ciphertext,
|
||||
record_len as u16,
|
||||
)?;
|
||||
|
||||
let combined_plaintext = combined_plaintext.unwrap();
|
||||
|
||||
log::info!(
|
||||
"REALITY CLIENT: Successfully decrypted combined handshake record ({} bytes plaintext)",
|
||||
combined_plaintext.len()
|
||||
log::debug!(
|
||||
"REALITY CLIENT: Decrypted record #{} ({} bytes plaintext)",
|
||||
handshake_seq,
|
||||
plaintext.len()
|
||||
);
|
||||
|
||||
// Now parse the individual handshake messages from the combined plaintext
|
||||
let mut offset = 0;
|
||||
let mut messages_found = 0;
|
||||
let mut certificate_verified = false;
|
||||
handshake_seq += 1;
|
||||
|
||||
while offset < combined_plaintext.len() && messages_found < 4 {
|
||||
// Track where new plaintext starts in accumulated buffer
|
||||
let prev_accumulated_len = accumulated_plaintext.len();
|
||||
accumulated_plaintext.extend_from_slice(&plaintext);
|
||||
|
||||
// Parse newly added messages from the plaintext we just added
|
||||
let mut offset = prev_accumulated_len;
|
||||
while offset < accumulated_plaintext.len() && messages_found < 4 {
|
||||
// Each handshake message has: type (1 byte) + length (3 bytes) + data
|
||||
if offset + 4 > combined_plaintext.len() {
|
||||
break;
|
||||
if offset + 4 > accumulated_plaintext.len() {
|
||||
break; // Incomplete message header, need more data
|
||||
}
|
||||
|
||||
let msg_type = combined_plaintext[offset];
|
||||
let msg_type = accumulated_plaintext[offset];
|
||||
let msg_len = u32::from_be_bytes([
|
||||
0,
|
||||
combined_plaintext[offset + 1],
|
||||
combined_plaintext[offset + 2],
|
||||
combined_plaintext[offset + 3],
|
||||
accumulated_plaintext[offset + 1],
|
||||
accumulated_plaintext[offset + 2],
|
||||
accumulated_plaintext[offset + 3],
|
||||
]) as usize;
|
||||
|
||||
if offset + 4 + msg_len > combined_plaintext.len() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Invalid handshake message length: {} at offset {}",
|
||||
msg_len, offset
|
||||
),
|
||||
));
|
||||
if offset + 4 + msg_len > accumulated_plaintext.len() {
|
||||
break; // Incomplete message body, need more data
|
||||
}
|
||||
|
||||
let msg_name = match msg_type {
|
||||
@@ -635,7 +604,7 @@ impl RealityClientConnection {
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"REALITY CLIENT: Found {} message (type={}, len={})",
|
||||
msg_name,
|
||||
msg_type,
|
||||
@@ -645,24 +614,48 @@ impl RealityClientConnection {
|
||||
// Verify HMAC signature when we encounter the Certificate message
|
||||
if msg_type == HANDSHAKE_TYPE_CERTIFICATE {
|
||||
let cert_der =
|
||||
extract_certificate_der(&combined_plaintext[offset..offset + 4 + msg_len])?;
|
||||
extract_certificate_der(&accumulated_plaintext[offset..offset + 4 + msg_len])?;
|
||||
verify_certificate_hmac(cert_der, &auth_key)?;
|
||||
ed25519_public_key = Some(extract_ed25519_public_key(cert_der)?);
|
||||
certificate_verified = true;
|
||||
}
|
||||
|
||||
// Record CertificateVerify offset for later verification
|
||||
if msg_type == HANDSHAKE_TYPE_CERTIFICATE_VERIFY {
|
||||
cert_verify_offset = Some(offset);
|
||||
}
|
||||
|
||||
messages_found += 1;
|
||||
offset += 4 + msg_len;
|
||||
}
|
||||
|
||||
// Check if we got all 4 messages AND the certificate was verified
|
||||
if messages_found != 4 {
|
||||
// Not all messages received yet - this shouldn't happen with combined record
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Expected 4 handshake messages, found {}", messages_found),
|
||||
));
|
||||
if messages_found < 4 {
|
||||
log::debug!(
|
||||
"REALITY CLIENT: Received {} of 4 handshake messages, waiting for more records",
|
||||
messages_found
|
||||
);
|
||||
self.handshake_state = HandshakeState::ProcessingHandshake {
|
||||
client_handshake_traffic_secret: client_hs_secret,
|
||||
server_handshake_traffic_secret: server_hs_secret,
|
||||
master_secret,
|
||||
cipher_suite,
|
||||
handshake_transcript_bytes: transcript_bytes,
|
||||
auth_key,
|
||||
handshake_seq,
|
||||
accumulated_plaintext,
|
||||
messages_found,
|
||||
certificate_verified,
|
||||
ed25519_public_key,
|
||||
cert_verify_offset,
|
||||
};
|
||||
return Ok(true); // Processed a record, but need more
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"REALITY CLIENT: All 4 handshake messages received ({} bytes total)",
|
||||
accumulated_plaintext.len()
|
||||
);
|
||||
|
||||
// Ensure the Certificate message was present and HMAC verified
|
||||
if !certificate_verified {
|
||||
return Err(io::Error::new(
|
||||
@@ -671,66 +664,92 @@ impl RealityClientConnection {
|
||||
));
|
||||
}
|
||||
|
||||
// Build handshake transcript using cipher suite's hash algorithm
|
||||
// We need the actual handshake message bytes, not their hashes!
|
||||
// The transcript_bytes already contains the actual ClientHello and ServerHello bytes
|
||||
// Verify CertificateVerify signature
|
||||
let mut cert_verify_verified = false;
|
||||
if let (Some(public_key), Some(cv_offset)) = (ed25519_public_key, cert_verify_offset) {
|
||||
// Transcript up to (not including) CertificateVerify
|
||||
let mut cv_transcript = digest::Context::new(cipher_suite.digest_algorithm());
|
||||
cv_transcript.update(&transcript_bytes);
|
||||
cv_transcript.update(&accumulated_plaintext[..cv_offset]);
|
||||
let cv_transcript_hash = cv_transcript.finish();
|
||||
|
||||
let cv_msg_len = u32::from_be_bytes([
|
||||
0,
|
||||
accumulated_plaintext[cv_offset + 1],
|
||||
accumulated_plaintext[cv_offset + 2],
|
||||
accumulated_plaintext[cv_offset + 3],
|
||||
]) as usize;
|
||||
let cv_message = &accumulated_plaintext[cv_offset..cv_offset + 4 + cv_msg_len];
|
||||
let signature = extract_certificate_verify_signature(cv_message)?;
|
||||
|
||||
verify_certificate_verify_signature(
|
||||
&public_key,
|
||||
&signature,
|
||||
cv_transcript_hash.as_ref(),
|
||||
)?;
|
||||
cert_verify_verified = true;
|
||||
}
|
||||
|
||||
if !cert_verify_verified {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"REALITY handshake failed: CertificateVerify not verified",
|
||||
));
|
||||
}
|
||||
|
||||
let mut handshake_transcript = digest::Context::new(cipher_suite.digest_algorithm());
|
||||
handshake_transcript.update(&transcript_bytes); // Contains actual ClientHello + ServerHello bytes
|
||||
handshake_transcript.update(&combined_plaintext); // EncryptedExtensions + Certificate + CertificateVerify + Finished
|
||||
handshake_transcript.update(&transcript_bytes);
|
||||
handshake_transcript.update(&accumulated_plaintext);
|
||||
|
||||
let handshake_hash = handshake_transcript.finish();
|
||||
let handshake_hash_vec: Vec<u8> = handshake_hash.as_ref().to_vec();
|
||||
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"REALITY CLIENT: Handshake hash for client Finished: {:02x?}",
|
||||
handshake_hash_vec
|
||||
);
|
||||
log::info!(
|
||||
"REALITY CLIENT: Transcript bytes len={}, combined_plaintext len={}",
|
||||
log::debug!(
|
||||
"REALITY CLIENT: Transcript bytes len={}, accumulated_plaintext len={}",
|
||||
transcript_bytes.len(),
|
||||
combined_plaintext.len()
|
||||
accumulated_plaintext.len()
|
||||
);
|
||||
|
||||
// Generate client Finished message
|
||||
let client_verify_data =
|
||||
compute_finished_verify_data(cipher_suite, &client_hs_secret, &handshake_hash_vec)?;
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"REALITY CLIENT: Client verify data: {:02x?}",
|
||||
client_verify_data
|
||||
);
|
||||
let client_finished = construct_finished(&client_verify_data)?;
|
||||
|
||||
// Derive client handshake traffic keys for encryption
|
||||
let (client_hs_key, client_hs_iv) = derive_traffic_keys(&client_hs_secret, cipher_suite)?;
|
||||
|
||||
// Encrypt Finished message
|
||||
let mut client_hs_seq = 0u64;
|
||||
let buf_len_before = self.ciphertext_write_buf.len();
|
||||
encrypt_handshake_to_records(
|
||||
cipher_suite,
|
||||
&client_finished,
|
||||
&client_hs_key,
|
||||
&client_hs_iv,
|
||||
&mut client_hs_seq,
|
||||
&mut self.ciphertext_write_buf,
|
||||
)?;
|
||||
let hs_aead_key = AeadKey::new(cipher_suite, &client_hs_key)?;
|
||||
{
|
||||
let mut encryptor =
|
||||
RecordEncryptor::new(&hs_aead_key, &client_hs_iv, &mut client_hs_seq);
|
||||
encryptor.encrypt_handshake(&client_finished, &mut self.ciphertext_write_buf)?;
|
||||
}
|
||||
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"REALITY CLIENT: Client Finished message generated and buffered ({} bytes)",
|
||||
self.ciphertext_write_buf.len() - buf_len_before
|
||||
);
|
||||
|
||||
// Derive application secrets
|
||||
let (client_app_secret, server_app_secret) =
|
||||
derive_application_secrets(cipher_suite, &master_secret, &handshake_hash_vec)?;
|
||||
|
||||
// Derive application traffic keys
|
||||
let (client_app_key, client_app_iv) =
|
||||
let (client_app_key_bytes, client_app_iv) =
|
||||
derive_traffic_keys(&client_app_secret, cipher_suite)?;
|
||||
let (server_app_key, server_app_iv) =
|
||||
let (server_app_key_bytes, server_app_iv) =
|
||||
derive_traffic_keys(&server_app_secret, cipher_suite)?;
|
||||
|
||||
// Store application keys
|
||||
// Cache AeadKey objects to avoid per-record key setup
|
||||
let client_app_key = AeadKey::new(cipher_suite, &client_app_key_bytes)?;
|
||||
let server_app_key = AeadKey::new(cipher_suite, &server_app_key_bytes)?;
|
||||
|
||||
self.app_read_key = Some(server_app_key);
|
||||
self.app_read_iv = Some(server_app_iv);
|
||||
self.app_write_key = Some(client_app_key);
|
||||
@@ -738,10 +757,8 @@ impl RealityClientConnection {
|
||||
self.read_seq = 0;
|
||||
self.write_seq = 0;
|
||||
self.cipher_suite = Some(cipher_suite);
|
||||
|
||||
// Mark handshake complete
|
||||
self.handshake_state = HandshakeState::Complete;
|
||||
log::info!("REALITY CLIENT: Handshake complete, application keys derived");
|
||||
log::debug!("REALITY CLIENT: Handshake complete, application keys derived");
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
@@ -767,45 +784,19 @@ impl RealityClientConnection {
|
||||
break;
|
||||
}
|
||||
|
||||
// Copy record header for decryption AAD
|
||||
let tls_header: [u8; TLS_RECORD_HEADER_SIZE] = [
|
||||
self.ciphertext_read_buf[0],
|
||||
self.ciphertext_read_buf[1],
|
||||
self.ciphertext_read_buf[2],
|
||||
self.ciphertext_read_buf[3],
|
||||
self.ciphertext_read_buf[4],
|
||||
];
|
||||
let ciphertext: Vec<u8> =
|
||||
self.ciphertext_read_buf[TLS_RECORD_HEADER_SIZE..total_record_len].to_vec();
|
||||
self.ciphertext_read_buf.consume(total_record_len);
|
||||
|
||||
let cipher_suite = self.cipher_suite.ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "Cipher suite not set")
|
||||
})?;
|
||||
let mut plaintext = decrypt_tls13_record(
|
||||
cipher_suite,
|
||||
app_read_key,
|
||||
app_read_iv,
|
||||
self.read_seq,
|
||||
&ciphertext,
|
||||
&tls_header,
|
||||
)?;
|
||||
|
||||
increment_seq(&mut self.read_seq)?;
|
||||
|
||||
// TLS 1.3: Remove ContentType trailer byte
|
||||
// The last byte of the plaintext is the actual ContentType
|
||||
if plaintext.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let content_type = plaintext.pop().unwrap();
|
||||
// Decrypt in-place: get mutable slice of ciphertext, decrypt, copy plaintext out
|
||||
let ciphertext_slice = self
|
||||
.ciphertext_read_buf
|
||||
.slice_mut(TLS_RECORD_HEADER_SIZE..total_record_len);
|
||||
let mut decryptor = RecordDecryptor::new(app_read_key, app_read_iv, &mut self.read_seq);
|
||||
let (content_type, plaintext) =
|
||||
decryptor.decrypt_record_in_place(ciphertext_slice, record_len as u16)?;
|
||||
|
||||
match content_type {
|
||||
CONTENT_TYPE_APPLICATION_DATA => {
|
||||
// Compact plaintext buffer if needed before extending
|
||||
self.plaintext_read_buf.maybe_compact(4096);
|
||||
self.plaintext_read_buf.extend_from_slice(&plaintext);
|
||||
self.plaintext_read_buf.extend_from_slice(plaintext);
|
||||
}
|
||||
CONTENT_TYPE_ALERT => {
|
||||
// Parse alert: level (1 byte) + description (1 byte)
|
||||
@@ -838,13 +829,16 @@ impl RealityClientConnection {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::warn!(
|
||||
"REALITY CLIENT: Unexpected ContentType: 0x{:02x}",
|
||||
content_type
|
||||
);
|
||||
}
|
||||
// CONTENT_TYPE_HANDSHAKE is invalid after handshake complete
|
||||
// strip_content_type() validates and returns error for invalid types
|
||||
_ => unreachable!(
|
||||
"strip_content_type validates content type; unexpected: 0x{:02x}",
|
||||
content_type
|
||||
),
|
||||
}
|
||||
|
||||
// Consume the processed record from the buffer (after plaintext borrow ends)
|
||||
self.ciphertext_read_buf.consume(total_record_len);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -887,15 +881,10 @@ impl RealityClientConnection {
|
||||
}
|
||||
};
|
||||
|
||||
let cipher_suite = self.cipher_suite.ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "Cipher suite not set")
|
||||
})?;
|
||||
encrypt_plaintext_to_records(
|
||||
cipher_suite,
|
||||
let mut encryptor =
|
||||
RecordEncryptor::new(app_write_key, app_write_iv, &mut self.write_seq);
|
||||
encryptor.encrypt_app_data(
|
||||
&mut self.plaintext_write_buf,
|
||||
app_write_key,
|
||||
app_write_iv,
|
||||
&mut self.write_seq,
|
||||
&mut self.ciphertext_write_buf,
|
||||
)?;
|
||||
}
|
||||
@@ -963,26 +952,10 @@ impl RealityClientConnection {
|
||||
}
|
||||
};
|
||||
|
||||
// Use common helper to build encrypted close_notify alert
|
||||
let cipher_suite = match self.cipher_suite {
|
||||
Some(cs) => cs,
|
||||
None => {
|
||||
log::debug!("REALITY CLIENT: Cannot send close_notify - cipher suite not set");
|
||||
return;
|
||||
}
|
||||
};
|
||||
match common::build_close_notify_alert(
|
||||
cipher_suite,
|
||||
app_write_key,
|
||||
app_write_iv,
|
||||
self.write_seq,
|
||||
) {
|
||||
Ok(record) => {
|
||||
if let Err(e) = increment_seq(&mut self.write_seq) {
|
||||
log::error!("REALITY CLIENT: {}", e);
|
||||
return;
|
||||
}
|
||||
self.ciphertext_write_buf.extend_from_slice(&record);
|
||||
// Encrypt close_notify alert using RecordEncryptor
|
||||
let mut encryptor = RecordEncryptor::new(app_write_key, app_write_iv, &mut self.write_seq);
|
||||
match encryptor.encrypt_close_notify(&mut self.ciphertext_write_buf) {
|
||||
Ok(()) => {
|
||||
log::debug!("REALITY CLIENT: Encrypted close_notify alert queued");
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -992,7 +965,7 @@ impl RealityClientConnection {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
pub fn feed_reality_client_connection(
|
||||
client_connection: &mut RealityClientConnection,
|
||||
data: &[u8],
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
// REALITY client certificate verification
|
||||
//
|
||||
// This module handles HMAC verification of REALITY server certificates.
|
||||
// In REALITY protocol, the server embeds HMAC-SHA512(auth_key, ed25519_public_key)
|
||||
// in the signature field of the certificate.
|
||||
// REALITY certificate verification (HMAC and CertificateVerify)
|
||||
|
||||
use std::io;
|
||||
|
||||
use aws_lc_rs::hmac;
|
||||
use aws_lc_rs::signature::{ED25519, UnparsedPublicKey};
|
||||
use subtle::ConstantTimeEq;
|
||||
use x509_parser::certificate::X509Certificate;
|
||||
use x509_parser::prelude::FromDer;
|
||||
@@ -32,7 +29,7 @@ pub fn extract_certificate_der(certificate_message: &[u8]) -> io::Result<&[u8]>
|
||||
));
|
||||
}
|
||||
|
||||
let mut pos = 4; // Skip handshake header
|
||||
let mut pos = 4;
|
||||
|
||||
// certificate_request_context length (1 byte)
|
||||
if pos >= certificate_message.len() {
|
||||
@@ -74,7 +71,6 @@ pub fn extract_certificate_der(certificate_message: &[u8]) -> io::Result<&[u8]>
|
||||
]) as usize;
|
||||
pos += 3;
|
||||
|
||||
// Extract the DER-encoded certificate
|
||||
if pos + cert_len > certificate_message.len() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
@@ -90,10 +86,9 @@ pub fn extract_certificate_der(certificate_message: &[u8]) -> io::Result<&[u8]>
|
||||
/// In REALITY protocol, the server embeds HMAC-SHA512(auth_key, ed25519_public_key)
|
||||
/// in the signature field of the certificate.
|
||||
///
|
||||
/// Uses proper X.509 parsing via x509-parser crate for robust extraction.
|
||||
/// Uses x509-parser for robust extraction.
|
||||
#[inline]
|
||||
pub fn verify_certificate_hmac(cert_der: &[u8], auth_key: &[u8; 32]) -> io::Result<()> {
|
||||
// Parse the X.509 certificate properly
|
||||
let (_, cert) = X509Certificate::from_der(cert_der).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
@@ -101,7 +96,6 @@ pub fn verify_certificate_hmac(cert_der: &[u8], auth_key: &[u8; 32]) -> io::Resu
|
||||
)
|
||||
})?;
|
||||
|
||||
// Extract the public key from SubjectPublicKeyInfo
|
||||
let spki = cert.public_key();
|
||||
let pubkey_data: &[u8] = &spki.subject_public_key.data;
|
||||
let signature: &[u8] = &cert.signature_value.data;
|
||||
@@ -156,13 +150,172 @@ pub fn verify_certificate_hmac(cert_der: &[u8], auth_key: &[u8; 32]) -> io::Resu
|
||||
// Compare full 64-byte signature with expected HMAC using constant-time comparison
|
||||
// to prevent timing attacks that could leak information about the expected signature
|
||||
if expected_signature.ct_eq(signature).unwrap_u8() == 0 {
|
||||
// HMAC mismatch - the certificate is not signed by a REALITY server.
|
||||
// Possible causes:
|
||||
// 1. Connection intercepted/MITM (received real certificate from target site)
|
||||
// 2. ISP traffic hijacking/redirection
|
||||
// 3. Misconfigured server (wrong keys)
|
||||
// 4. Connection to wrong server
|
||||
// Note: Unlike Xray-core, we don't fall back to X.509 validation and spider crawling.
|
||||
log::warn!(
|
||||
"REALITY CLIENT: Certificate HMAC mismatch - not a REALITY server (possible MITM or misconfiguration)"
|
||||
);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"Certificate HMAC verification failed - signature mismatch",
|
||||
"Certificate HMAC verification failed - not a REALITY-signed certificate",
|
||||
));
|
||||
}
|
||||
|
||||
log::info!("REALITY CLIENT: Certificate HMAC verified successfully");
|
||||
log::debug!("REALITY CLIENT: Certificate HMAC verified successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract the Ed25519 public key from a DER-encoded certificate
|
||||
///
|
||||
/// Returns the 32-byte Ed25519 public key.
|
||||
#[inline]
|
||||
pub fn extract_ed25519_public_key(cert_der: &[u8]) -> io::Result<[u8; 32]> {
|
||||
let (_, cert) = X509Certificate::from_der(cert_der).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Failed to parse X.509 certificate: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let spki = cert.public_key();
|
||||
let pubkey_data: &[u8] = &spki.subject_public_key.data;
|
||||
|
||||
if pubkey_data.len() != 32 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Expected Ed25519 public key (32 bytes), got {} bytes",
|
||||
pubkey_data.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let mut public_key = [0u8; 32];
|
||||
public_key.copy_from_slice(pubkey_data);
|
||||
Ok(public_key)
|
||||
}
|
||||
|
||||
/// Parse CertificateVerify message and extract the signature
|
||||
///
|
||||
/// CertificateVerify structure:
|
||||
/// - handshake_type (1 byte) = 0x0f
|
||||
/// - length (3 bytes)
|
||||
/// - signature_algorithm (2 bytes)
|
||||
/// - signature_length (2 bytes)
|
||||
/// - signature (variable)
|
||||
///
|
||||
/// Returns the signature bytes.
|
||||
#[inline]
|
||||
pub fn extract_certificate_verify_signature(cert_verify_message: &[u8]) -> io::Result<Vec<u8>> {
|
||||
// Minimum: 1 (type) + 3 (len) + 2 (alg) + 2 (sig len) + 64 (ed25519 sig) = 72
|
||||
if cert_verify_message.len() < 72 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"CertificateVerify message too short: {} bytes",
|
||||
cert_verify_message.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if cert_verify_message[0] != 0x0f {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Expected CertificateVerify type (0x0f), got 0x{:02x}",
|
||||
cert_verify_message[0]
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// Skip handshake header (4 bytes)
|
||||
let pos = 4;
|
||||
|
||||
// Signature algorithm (2 bytes)
|
||||
let sig_alg = u16::from_be_bytes([cert_verify_message[pos], cert_verify_message[pos + 1]]);
|
||||
|
||||
// We only support Ed25519 (0x0807)
|
||||
if sig_alg != 0x0807 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Unsupported signature algorithm: 0x{:04x}, expected Ed25519 (0x0807)",
|
||||
sig_alg
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// Signature length (2 bytes)
|
||||
let sig_len =
|
||||
u16::from_be_bytes([cert_verify_message[pos + 2], cert_verify_message[pos + 3]]) as usize;
|
||||
|
||||
// Ed25519 signatures are always 64 bytes
|
||||
if sig_len != 64 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid Ed25519 signature length: {}, expected 64", sig_len),
|
||||
));
|
||||
}
|
||||
|
||||
// Extract signature
|
||||
let sig_start = pos + 4;
|
||||
if sig_start + sig_len > cert_verify_message.len() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"CertificateVerify message truncated",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(cert_verify_message[sig_start..sig_start + sig_len].to_vec())
|
||||
}
|
||||
|
||||
/// Verify the CertificateVerify signature
|
||||
///
|
||||
/// This verifies that the server holds the private key corresponding to the
|
||||
/// public key in the certificate. The signature is over:
|
||||
/// 64 spaces + "TLS 1.3, server CertificateVerify\0" + transcript_hash
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `public_key` - The Ed25519 public key from the certificate (32 bytes)
|
||||
/// * `signature` - The signature from CertificateVerify message (64 bytes)
|
||||
/// * `transcript_hash` - Hash of handshake messages up to (but not including) CertificateVerify
|
||||
#[inline]
|
||||
pub fn verify_certificate_verify_signature(
|
||||
public_key: &[u8; 32],
|
||||
signature: &[u8],
|
||||
transcript_hash: &[u8],
|
||||
) -> io::Result<()> {
|
||||
if signature.len() != 64 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid signature length: {}, expected 64", signature.len()),
|
||||
));
|
||||
}
|
||||
|
||||
// Construct the signed content per RFC 8446 Section 4.4.3:
|
||||
// " " * 64 + context_string + 0x00 + content
|
||||
let mut signed_content = Vec::with_capacity(64 + 34 + transcript_hash.len());
|
||||
signed_content.extend_from_slice(&[0x20u8; 64]); // 64 spaces
|
||||
signed_content.extend_from_slice(b"TLS 1.3, server CertificateVerify");
|
||||
signed_content.push(0x00);
|
||||
signed_content.extend_from_slice(transcript_hash);
|
||||
|
||||
// Verify the Ed25519 signature
|
||||
let public_key = UnparsedPublicKey::new(&ED25519, public_key);
|
||||
public_key.verify(&signed_content, signature).map_err(|_| {
|
||||
log::warn!("REALITY CLIENT: CertificateVerify signature verification failed");
|
||||
io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"CertificateVerify signature verification failed",
|
||||
)
|
||||
})?;
|
||||
|
||||
log::debug!("REALITY CLIENT: CertificateVerify signature verified successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -312,4 +465,267 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
/// Test extracting Ed25519 public key from certificate
|
||||
#[test]
|
||||
fn test_extract_ed25519_public_key() {
|
||||
let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ED25519)
|
||||
.expect("Failed to generate Ed25519 key pair");
|
||||
|
||||
let params = rcgen::CertificateParams::new(vec!["test.example.com".to_string()])
|
||||
.expect("Failed to create certificate params");
|
||||
|
||||
let cert = params
|
||||
.self_signed(&key_pair)
|
||||
.expect("Failed to create self-signed certificate");
|
||||
|
||||
let cert_der = cert.der().to_vec();
|
||||
|
||||
// Extract the public key
|
||||
let result = extract_ed25519_public_key(&cert_der);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let public_key = result.unwrap();
|
||||
assert_eq!(public_key.len(), 32);
|
||||
|
||||
// Verify it matches the original key
|
||||
let signing_key =
|
||||
aws_lc_rs::signature::Ed25519KeyPair::from_pkcs8(key_pair.serialized_der())
|
||||
.expect("Failed to parse key");
|
||||
assert_eq!(public_key, signing_key.public_key().as_ref());
|
||||
}
|
||||
|
||||
/// Test extracting signature from CertificateVerify message
|
||||
#[test]
|
||||
fn test_extract_certificate_verify_signature() {
|
||||
// Build a CertificateVerify message:
|
||||
// - type (1 byte) = 0x0f
|
||||
// - length (3 bytes)
|
||||
// - signature_algorithm (2 bytes) = 0x0807 (Ed25519)
|
||||
// - signature_length (2 bytes) = 64
|
||||
// - signature (64 bytes)
|
||||
|
||||
let signature = [0xABu8; 64];
|
||||
let payload_len = 2 + 2 + 64; // sig_alg + sig_len + sig
|
||||
|
||||
let mut message = Vec::new();
|
||||
message.push(0x0f); // CertificateVerify type
|
||||
message.push(0x00);
|
||||
message.push(0x00);
|
||||
message.push(payload_len as u8);
|
||||
message.push(0x08); // Ed25519 algorithm
|
||||
message.push(0x07);
|
||||
message.push(0x00); // Signature length
|
||||
message.push(0x40); // 64
|
||||
message.extend_from_slice(&signature);
|
||||
|
||||
let result = extract_certificate_verify_signature(&message);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), signature.to_vec());
|
||||
}
|
||||
|
||||
/// Test CertificateVerify extraction with wrong message type
|
||||
#[test]
|
||||
fn test_extract_certificate_verify_wrong_type() {
|
||||
let mut message = vec![0x0b; 72]; // Certificate type instead of CertificateVerify
|
||||
message[4] = 0x08;
|
||||
message[5] = 0x07;
|
||||
message[6] = 0x00;
|
||||
message[7] = 0x40;
|
||||
|
||||
let result = extract_certificate_verify_signature(&message);
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Expected CertificateVerify type")
|
||||
);
|
||||
}
|
||||
|
||||
/// Test CertificateVerify extraction with unsupported algorithm
|
||||
#[test]
|
||||
fn test_extract_certificate_verify_unsupported_algorithm() {
|
||||
let mut message = vec![0x00; 72];
|
||||
message[0] = 0x0f; // CertificateVerify type
|
||||
message[4] = 0x04; // RSA-PKCS1-SHA256 instead of Ed25519
|
||||
message[5] = 0x01;
|
||||
|
||||
let result = extract_certificate_verify_signature(&message);
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Unsupported signature algorithm")
|
||||
);
|
||||
}
|
||||
|
||||
/// Test CertificateVerify extraction with message too short
|
||||
#[test]
|
||||
fn test_extract_certificate_verify_too_short() {
|
||||
let message = vec![0x0f; 10]; // Too short
|
||||
|
||||
let result = extract_certificate_verify_signature(&message);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("too short"));
|
||||
}
|
||||
|
||||
/// Test full CertificateVerify signature verification
|
||||
#[test]
|
||||
fn test_verify_certificate_verify_signature_valid() {
|
||||
// Generate an Ed25519 key pair
|
||||
let key_pair = aws_lc_rs::signature::Ed25519KeyPair::generate()
|
||||
.expect("Failed to generate Ed25519 key pair");
|
||||
|
||||
let public_key: [u8; 32] = key_pair.public_key().as_ref().try_into().unwrap();
|
||||
|
||||
// Create a fake transcript hash
|
||||
let transcript_hash = [0x42u8; 32];
|
||||
|
||||
// Construct the signed content per RFC 8446 Section 4.4.3
|
||||
let mut signed_content = Vec::new();
|
||||
signed_content.extend_from_slice(&[0x20u8; 64]); // 64 spaces
|
||||
signed_content.extend_from_slice(b"TLS 1.3, server CertificateVerify");
|
||||
signed_content.push(0x00);
|
||||
signed_content.extend_from_slice(&transcript_hash);
|
||||
|
||||
// Sign it
|
||||
let signature = key_pair.sign(&signed_content);
|
||||
|
||||
// Verify
|
||||
let result =
|
||||
verify_certificate_verify_signature(&public_key, signature.as_ref(), &transcript_hash);
|
||||
assert!(result.is_ok(), "Signature verification should succeed");
|
||||
}
|
||||
|
||||
/// Test CertificateVerify verification with wrong public key
|
||||
#[test]
|
||||
fn test_verify_certificate_verify_signature_wrong_key() {
|
||||
// Generate two different key pairs
|
||||
let key_pair1 = aws_lc_rs::signature::Ed25519KeyPair::generate()
|
||||
.expect("Failed to generate Ed25519 key pair");
|
||||
let key_pair2 = aws_lc_rs::signature::Ed25519KeyPair::generate()
|
||||
.expect("Failed to generate Ed25519 key pair");
|
||||
|
||||
let public_key2: [u8; 32] = key_pair2.public_key().as_ref().try_into().unwrap();
|
||||
|
||||
let transcript_hash = [0x42u8; 32];
|
||||
|
||||
// Construct and sign with key_pair1
|
||||
let mut signed_content = Vec::new();
|
||||
signed_content.extend_from_slice(&[0x20u8; 64]);
|
||||
signed_content.extend_from_slice(b"TLS 1.3, server CertificateVerify");
|
||||
signed_content.push(0x00);
|
||||
signed_content.extend_from_slice(&transcript_hash);
|
||||
|
||||
let signature = key_pair1.sign(&signed_content);
|
||||
|
||||
// Try to verify with key_pair2's public key - should fail
|
||||
let result =
|
||||
verify_certificate_verify_signature(&public_key2, signature.as_ref(), &transcript_hash);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::PermissionDenied);
|
||||
}
|
||||
|
||||
/// Test CertificateVerify verification with wrong transcript hash
|
||||
#[test]
|
||||
fn test_verify_certificate_verify_signature_wrong_transcript() {
|
||||
let key_pair = aws_lc_rs::signature::Ed25519KeyPair::generate()
|
||||
.expect("Failed to generate Ed25519 key pair");
|
||||
|
||||
let public_key: [u8; 32] = key_pair.public_key().as_ref().try_into().unwrap();
|
||||
|
||||
let transcript_hash1 = [0x42u8; 32];
|
||||
let transcript_hash2 = [0x43u8; 32]; // Different hash
|
||||
|
||||
// Sign with transcript_hash1
|
||||
let mut signed_content = Vec::new();
|
||||
signed_content.extend_from_slice(&[0x20u8; 64]);
|
||||
signed_content.extend_from_slice(b"TLS 1.3, server CertificateVerify");
|
||||
signed_content.push(0x00);
|
||||
signed_content.extend_from_slice(&transcript_hash1);
|
||||
|
||||
let signature = key_pair.sign(&signed_content);
|
||||
|
||||
// Try to verify with transcript_hash2 - should fail
|
||||
let result =
|
||||
verify_certificate_verify_signature(&public_key, signature.as_ref(), &transcript_hash2);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::PermissionDenied);
|
||||
}
|
||||
|
||||
/// Test CertificateVerify verification with invalid signature length
|
||||
#[test]
|
||||
fn test_verify_certificate_verify_signature_invalid_length() {
|
||||
let public_key = [0x00u8; 32];
|
||||
let transcript_hash = [0x42u8; 32];
|
||||
let invalid_signature = [0x00u8; 32]; // Should be 64 bytes
|
||||
|
||||
let result =
|
||||
verify_certificate_verify_signature(&public_key, &invalid_signature, &transcript_hash);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
/// Test end-to-end: generate certificate, create CertificateVerify, verify it
|
||||
#[test]
|
||||
fn test_certificate_verify_end_to_end() {
|
||||
use aws_lc_rs::digest;
|
||||
|
||||
// Generate Ed25519 key pair
|
||||
let key_pair = aws_lc_rs::signature::Ed25519KeyPair::generate()
|
||||
.expect("Failed to generate Ed25519 key pair");
|
||||
|
||||
let public_key: [u8; 32] = key_pair.public_key().as_ref().try_into().unwrap();
|
||||
|
||||
// Simulate a transcript hash (in real life, this would be hash of handshake messages)
|
||||
let fake_client_hello = b"ClientHello data here";
|
||||
let fake_server_hello = b"ServerHello data here";
|
||||
let fake_encrypted_extensions = b"EncryptedExtensions here";
|
||||
let fake_certificate = b"Certificate message here";
|
||||
|
||||
let mut transcript = digest::Context::new(&digest::SHA256);
|
||||
transcript.update(fake_client_hello);
|
||||
transcript.update(fake_server_hello);
|
||||
transcript.update(fake_encrypted_extensions);
|
||||
transcript.update(fake_certificate);
|
||||
let transcript_hash = transcript.finish();
|
||||
|
||||
// Create CertificateVerify message (simulating server)
|
||||
let mut signed_content = Vec::new();
|
||||
signed_content.extend_from_slice(&[0x20u8; 64]);
|
||||
signed_content.extend_from_slice(b"TLS 1.3, server CertificateVerify");
|
||||
signed_content.push(0x00);
|
||||
signed_content.extend_from_slice(transcript_hash.as_ref());
|
||||
|
||||
let signature = key_pair.sign(&signed_content);
|
||||
|
||||
// Build CertificateVerify message
|
||||
let payload_len = 2 + 2 + 64;
|
||||
let mut cv_message = Vec::new();
|
||||
cv_message.push(0x0f); // type
|
||||
cv_message.push(0x00);
|
||||
cv_message.push(0x00);
|
||||
cv_message.push(payload_len as u8);
|
||||
cv_message.push(0x08); // Ed25519
|
||||
cv_message.push(0x07);
|
||||
cv_message.push(0x00); // sig len
|
||||
cv_message.push(0x40);
|
||||
cv_message.extend_from_slice(signature.as_ref());
|
||||
|
||||
// Client side: extract and verify
|
||||
let extracted_sig =
|
||||
extract_certificate_verify_signature(&cv_message).expect("Failed to extract signature");
|
||||
|
||||
let result = verify_certificate_verify_signature(
|
||||
&public_key,
|
||||
&extracted_sig,
|
||||
transcript_hash.as_ref(),
|
||||
);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"End-to-end CertificateVerify verification should succeed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+523
-1329
File diff suppressed because it is too large
Load Diff
@@ -3,38 +3,39 @@
|
||||
// This implements a rustls-compatible API for REALITY protocol server connections,
|
||||
// allowing REALITY to be used as a drop-in replacement for rustls.
|
||||
|
||||
use aws_lc_rs::{agreement, digest};
|
||||
use rand::RngCore;
|
||||
use std::io::{self, Read, Write};
|
||||
|
||||
use crate::address::{Address, NetLocation};
|
||||
use crate::slide_buffer::SlideBuffer;
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
use super::common::{
|
||||
self, ALERT_DESC_CLOSE_NOTIFY, ALERT_LEVEL_WARNING, CIPHERTEXT_READ_BUF_CAPACITY,
|
||||
CONTENT_TYPE_ALERT, CONTENT_TYPE_APPLICATION_DATA, CONTENT_TYPE_CHANGE_CIPHER_SPEC,
|
||||
CONTENT_TYPE_HANDSHAKE, HANDSHAKE_TYPE_FINISHED, PLAINTEXT_READ_BUF_CAPACITY,
|
||||
TLS_MAX_RECORD_SIZE, TLS_RECORD_HEADER_SIZE, increment_seq,
|
||||
ALERT_DESC_CLOSE_NOTIFY, ALERT_LEVEL_WARNING, CIPHERTEXT_READ_BUF_CAPACITY, CONTENT_TYPE_ALERT,
|
||||
CONTENT_TYPE_APPLICATION_DATA, CONTENT_TYPE_CHANGE_CIPHER_SPEC, CONTENT_TYPE_HANDSHAKE,
|
||||
HANDSHAKE_TYPE_FINISHED, OUTGOING_BUFFER_LIMIT, PLAINTEXT_READ_BUF_CAPACITY,
|
||||
TLS_MAX_RECORD_SIZE, TLS_RECORD_HEADER_SIZE,
|
||||
};
|
||||
use super::reality_aead::{decrypt_handshake_message, decrypt_tls13_record};
|
||||
use super::reality_aead::{AeadKey, decrypt_handshake_message};
|
||||
use super::reality_auth::{decrypt_session_id, derive_auth_key, perform_ecdh};
|
||||
use super::reality_certificate::generate_hmac_certificate;
|
||||
use super::reality_cipher_suite::{CipherSuite, DEFAULT_CIPHER_SUITES};
|
||||
use super::reality_io_state::RealityIoState;
|
||||
use super::reality_reader_writer::{RealityReader, RealityWriter};
|
||||
use super::reality_records::{encrypt_handshake_to_records, encrypt_plaintext_to_records};
|
||||
use super::reality_records::{RecordDecryptor, RecordEncryptor};
|
||||
use super::reality_tls13_keys::{
|
||||
compute_finished_verify_data, derive_application_secrets, derive_handshake_keys,
|
||||
derive_traffic_keys,
|
||||
};
|
||||
use super::reality_tls13_messages::*;
|
||||
use super::reality_tls13_messages::{
|
||||
construct_certificate, construct_certificate_verify, construct_encrypted_extensions,
|
||||
construct_finished, construct_server_hello, write_record_header,
|
||||
};
|
||||
use super::reality_util::{
|
||||
extract_client_cipher_suites, extract_client_public_key, extract_client_random,
|
||||
extract_session_id_slice, negotiate_cipher_suite,
|
||||
};
|
||||
use aws_lc_rs::{
|
||||
agreement, digest,
|
||||
rand::{SecureRandom, SystemRandom},
|
||||
};
|
||||
use subtle::ConstantTimeEq;
|
||||
use crate::address::{Address, NetLocation};
|
||||
use crate::slide_buffer::SlideBuffer;
|
||||
use crate::util::allocate_vec;
|
||||
|
||||
/// Configuration for REALITY server connections
|
||||
#[derive(Clone)]
|
||||
@@ -59,9 +60,10 @@ pub struct RealityServerConfig {
|
||||
enum HandshakeState {
|
||||
/// Initial state, waiting for ClientHello
|
||||
Initial,
|
||||
/// ClientHello validated, waiting to build response with dest structure
|
||||
ClientHelloValidated { info: ClientHelloInfo },
|
||||
/// ServerHello and encrypted handshake messages sent, waiting for client Finished
|
||||
ServerHelloSent {
|
||||
handshake_hash: Vec<u8>, // Hash before server Finished
|
||||
handshake_hash_with_server_finished: Vec<u8>, // Hash including server Finished (for verifying client Finished)
|
||||
client_handshake_traffic_secret: Vec<u8>,
|
||||
master_secret: Vec<u8>,
|
||||
@@ -71,6 +73,22 @@ enum HandshakeState {
|
||||
Complete,
|
||||
}
|
||||
|
||||
/// Information extracted from ClientHello during validation phase
|
||||
/// This is passed to build_server_response() to construct the reply
|
||||
#[derive(Clone)]
|
||||
pub struct ClientHelloInfo {
|
||||
/// Session ID from ClientHello (echoed back in ServerHello)
|
||||
pub session_id: Vec<u8>,
|
||||
/// Client's X25519 public key from key_share extension
|
||||
pub client_public_key: [u8; 32],
|
||||
/// Derived auth key for HMAC certificate
|
||||
pub auth_key: [u8; 32],
|
||||
/// Negotiated cipher suite
|
||||
pub cipher_suite: CipherSuite,
|
||||
/// Raw ClientHello handshake bytes (for transcript hash)
|
||||
pub client_hello_handshake: Vec<u8>,
|
||||
}
|
||||
|
||||
/// REALITY server-side connection implementing rustls-compatible API
|
||||
pub struct RealityServerConnection {
|
||||
// Configuration
|
||||
@@ -80,16 +98,17 @@ pub struct RealityServerConnection {
|
||||
handshake_state: HandshakeState,
|
||||
|
||||
// TLS 1.3 application traffic encryption (post-handshake)
|
||||
app_read_key: Option<Vec<u8>>,
|
||||
// Keys are cached as AeadKey to avoid per-record key setup overhead
|
||||
app_read_key: Option<AeadKey>,
|
||||
app_read_iv: Option<Vec<u8>>,
|
||||
app_write_key: Option<Vec<u8>>,
|
||||
app_write_key: Option<AeadKey>,
|
||||
app_write_iv: Option<Vec<u8>>,
|
||||
read_seq: u64,
|
||||
write_seq: u64,
|
||||
cipher_suite: Option<CipherSuite>,
|
||||
|
||||
// Pre-allocated buffer for TLS read operations (reused across calls)
|
||||
tls_read_buffer: Box<[u8; TLS_MAX_RECORD_SIZE]>,
|
||||
tls_read_buffer: Box<[u8]>,
|
||||
|
||||
// Buffers for I/O - using SlideBuffer for efficient zero-alloc operations
|
||||
ciphertext_read_buf: SlideBuffer, // Incoming encrypted TLS records
|
||||
@@ -115,11 +134,11 @@ impl RealityServerConnection {
|
||||
read_seq: 0,
|
||||
write_seq: 0,
|
||||
cipher_suite: None,
|
||||
tls_read_buffer: Box::new([0u8; TLS_MAX_RECORD_SIZE]),
|
||||
tls_read_buffer: allocate_vec(TLS_MAX_RECORD_SIZE).into_boxed_slice(),
|
||||
ciphertext_read_buf: SlideBuffer::new(CIPHERTEXT_READ_BUF_CAPACITY),
|
||||
ciphertext_write_buf: Vec::with_capacity(CIPHERTEXT_READ_BUF_CAPACITY),
|
||||
ciphertext_write_buf: Vec::with_capacity(OUTGOING_BUFFER_LIMIT),
|
||||
plaintext_read_buf: SlideBuffer::new(PLAINTEXT_READ_BUF_CAPACITY),
|
||||
plaintext_write_buf: Vec::with_capacity(TLS_MAX_RECORD_SIZE),
|
||||
plaintext_write_buf: Vec::with_capacity(OUTGOING_BUFFER_LIMIT),
|
||||
received_close_notify: false,
|
||||
fatal_error: None,
|
||||
})
|
||||
@@ -183,10 +202,11 @@ impl RealityServerConnection {
|
||||
fn process_new_packets_inner(&mut self) -> io::Result<RealityIoState> {
|
||||
loop {
|
||||
match &self.handshake_state {
|
||||
HandshakeState::Initial => {
|
||||
if !self.process_client_hello()? {
|
||||
break;
|
||||
}
|
||||
HandshakeState::Initial | HandshakeState::ClientHelloValidated { .. } => {
|
||||
// Initial: ClientHello must be passed via validate_client_hello()
|
||||
// ClientHelloValidated: Response must be built via build_server_response()
|
||||
// Neither should be reached during process_new_packets
|
||||
break;
|
||||
}
|
||||
HandshakeState::ServerHelloSent { .. } => {
|
||||
if !self.process_client_finished()? {
|
||||
@@ -203,53 +223,95 @@ impl RealityServerConnection {
|
||||
Ok(RealityIoState::new(self.plaintext_read_buf.len()))
|
||||
}
|
||||
|
||||
/// Process ClientHello message and send ServerHello
|
||||
/// Returns true if a complete record was processed, false if more data needed
|
||||
/// Public API: Validate ClientHello
|
||||
///
|
||||
/// This performs authentication (ECDH, decrypt session_id, validate short_id/timestamp/version)
|
||||
/// but does NOT build the server response. Call build_server_response() after to complete.
|
||||
///
|
||||
/// Returns Ok(()) on success, Err(PermissionDenied) on auth failure.
|
||||
pub fn validate_client_hello(&mut self, client_hello: &[u8]) -> io::Result<()> {
|
||||
// Return persisted error if connection is in fatal error state
|
||||
if let Some(error_kind) = self.fatal_error {
|
||||
return Err(io::Error::new(error_kind, "connection previously failed"));
|
||||
}
|
||||
|
||||
// Must be in Initial state
|
||||
if !matches!(self.handshake_state, HandshakeState::Initial) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"validate_client_hello called in wrong state",
|
||||
));
|
||||
}
|
||||
|
||||
// Process ClientHello validation
|
||||
let result = self.process_client_hello_validation(client_hello);
|
||||
|
||||
// Persist fatal errors
|
||||
if let Err(ref e) = result {
|
||||
match e.kind() {
|
||||
io::ErrorKind::InvalidData
|
||||
| io::ErrorKind::PermissionDenied
|
||||
| io::ErrorKind::ConnectionAborted => {
|
||||
self.fatal_error = Some(e.kind());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Public API: Build server response using dest's record structure as template
|
||||
///
|
||||
/// Call this after validate_client_hello() succeeds. Pass the TLS records
|
||||
/// received from the destination server to match their structure.
|
||||
///
|
||||
/// dest_records should contain: [ServerHello, CCS, encrypted_handshake..., NewSessionTicket...]
|
||||
pub fn build_server_response(&mut self, dest_records: Vec<bytes::Bytes>) -> io::Result<()> {
|
||||
// Must be in ClientHelloValidated state
|
||||
if !matches!(
|
||||
self.handshake_state,
|
||||
HandshakeState::ClientHelloValidated { .. }
|
||||
) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"build_server_response called in wrong state",
|
||||
));
|
||||
}
|
||||
|
||||
self.build_server_response_internal(&dest_records)
|
||||
}
|
||||
|
||||
/// Phase 1: Validate ClientHello and extract info for later response building
|
||||
#[inline]
|
||||
fn process_client_hello(&mut self) -> io::Result<bool> {
|
||||
// Need at least TLS record header (5 bytes)
|
||||
if self.ciphertext_read_buf.len() < TLS_RECORD_HEADER_SIZE {
|
||||
return Ok(false); // Need more data
|
||||
fn process_client_hello_validation(&mut self, client_hello: &[u8]) -> io::Result<()> {
|
||||
// Validate minimum length (TLS record header + some content)
|
||||
if client_hello.len() < TLS_RECORD_HEADER_SIZE + 4 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"ClientHello too short",
|
||||
));
|
||||
}
|
||||
|
||||
// Parse TLS record length
|
||||
let record_len = self
|
||||
.ciphertext_read_buf
|
||||
.get_u16_be(3)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Buffer too short"))?
|
||||
as usize;
|
||||
|
||||
// Check if we have the complete record
|
||||
let total_record_len = TLS_RECORD_HEADER_SIZE + record_len;
|
||||
if self.ciphertext_read_buf.len() < total_record_len {
|
||||
return Ok(false); // Need more data
|
||||
}
|
||||
|
||||
// Copy the ClientHello record to a Vec for processing
|
||||
// (We need to keep it around for transcript hashing and AAD modification)
|
||||
let client_hello: Vec<u8> = self.ciphertext_read_buf[..total_record_len].to_vec();
|
||||
self.ciphertext_read_buf.consume(total_record_len);
|
||||
|
||||
// Step 1: Extract fields from ClientHello (using slice for session_id to avoid allocation)
|
||||
let client_random = extract_client_random(&client_hello)?;
|
||||
let session_id = extract_session_id_slice(&client_hello)?;
|
||||
let client_public_key = extract_client_public_key(&client_hello)?;
|
||||
// Extract fields from ClientHello
|
||||
let client_random = extract_client_random(client_hello)?;
|
||||
let session_id = extract_session_id_slice(client_hello)?;
|
||||
let client_public_key = extract_client_public_key(client_hello)?;
|
||||
|
||||
log::debug!(
|
||||
"REALITY: ClientHello received, client_random: {:?}",
|
||||
&client_random[..8]
|
||||
);
|
||||
|
||||
// Step 2: Perform ECDH to derive auth key
|
||||
// Perform ECDH to derive auth key
|
||||
let shared_secret = perform_ecdh(&self.config.private_key, &client_public_key)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
|
||||
|
||||
// Use slices directly from client_random to avoid copying
|
||||
let salt = &client_random[0..20];
|
||||
let auth_key = derive_auth_key(&shared_secret, salt, b"REALITY")
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
|
||||
|
||||
// Step 3: Validate session ID (contains encrypted metadata)
|
||||
// Validate session ID (contains encrypted metadata)
|
||||
if session_id.len() != 32 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
@@ -262,12 +324,9 @@ impl RealityServerConnection {
|
||||
encrypted_session_id_arr.copy_from_slice(session_id);
|
||||
|
||||
// Reconstruct AAD with zeros at SessionId location
|
||||
// The AAD during encryption had zeros where the session ID would go
|
||||
// SessionId is at offset 39 in ClientHello handshake (after type(1) + length(3) + version(2) + random(32) + sessionid_length(1))
|
||||
let client_hello_handshake = &client_hello[TLS_RECORD_HEADER_SIZE..];
|
||||
let mut aad_for_decryption = client_hello_handshake.to_vec();
|
||||
if aad_for_decryption.len() >= 39 + 32 {
|
||||
// Replace encrypted SessionId with zeros
|
||||
aad_for_decryption[39..39 + 32].fill(0);
|
||||
}
|
||||
|
||||
@@ -285,10 +344,6 @@ impl RealityServerConnection {
|
||||
})?;
|
||||
|
||||
// Validate session ID contents
|
||||
// Bytes 0-2: Version
|
||||
// Byte 3: Padding
|
||||
// Bytes 4-7: Timestamp (uint32)
|
||||
// Bytes 8-15: ShortId
|
||||
let client_version = &decrypted_session_id[0..3];
|
||||
let client_timestamp = u32::from_be_bytes([
|
||||
decrypted_session_id[4],
|
||||
@@ -302,10 +357,12 @@ impl RealityServerConnection {
|
||||
log::debug!("REALITY: Client timestamp: {}", client_timestamp);
|
||||
log::debug!("REALITY: Client short_id: {:02x?}", client_short_id);
|
||||
|
||||
// Validate short ID - check if client's short_id is in the configured list
|
||||
// Validate short ID using constant-time comparison
|
||||
let mut client_short_id_arr = [0u8; 8];
|
||||
client_short_id_arr.copy_from_slice(client_short_id);
|
||||
let short_id_ok = self.config.short_ids.contains(&client_short_id_arr);
|
||||
let short_id_ok = self.config.short_ids.iter().fold(false, |acc, valid_id| {
|
||||
acc | (client_short_id_arr.ct_eq(valid_id).unwrap_u8() == 1)
|
||||
});
|
||||
|
||||
if !short_id_ok {
|
||||
log::warn!(
|
||||
@@ -382,33 +439,15 @@ impl RealityServerConnection {
|
||||
));
|
||||
}
|
||||
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"REALITY: Client authentication successful - short_id: {:02x?}, version: {:?}, timestamp: {}",
|
||||
client_short_id,
|
||||
client_version,
|
||||
client_timestamp
|
||||
);
|
||||
|
||||
// Step 4: Generate our server X25519 keypair
|
||||
let rng = SystemRandom::new();
|
||||
let mut our_private_bytes = [0u8; 32];
|
||||
rng.fill(&mut our_private_bytes)
|
||||
.map_err(|_| io::Error::other("RNG failed"))?;
|
||||
|
||||
let our_private_key =
|
||||
agreement::PrivateKey::from_private_key(&agreement::X25519, &our_private_bytes)
|
||||
.map_err(|_| io::Error::other("Failed to create X25519 key"))?;
|
||||
let our_public_key_bytes = our_private_key
|
||||
.compute_public_key()
|
||||
.map_err(|_| io::Error::other("Failed to compute public key"))?;
|
||||
|
||||
// Step 5: Generate server random
|
||||
let mut server_random = [0u8; 32];
|
||||
rng.fill(&mut server_random)
|
||||
.map_err(|_| io::Error::other("RNG failed"))?;
|
||||
|
||||
// Step 6: Negotiate cipher suite with client
|
||||
let client_cipher_suites = extract_client_cipher_suites(&client_hello)?;
|
||||
// Negotiate cipher suite with client
|
||||
let client_cipher_suites = extract_client_cipher_suites(client_hello)?;
|
||||
let server_cipher_suites = if self.config.cipher_suites.is_empty() {
|
||||
DEFAULT_CIPHER_SUITES.to_vec()
|
||||
} else {
|
||||
@@ -427,33 +466,72 @@ impl RealityServerConnection {
|
||||
client_cipher_suites
|
||||
);
|
||||
|
||||
// Step 7: Build ServerHello
|
||||
// Store validated info for response building
|
||||
self.handshake_state = HandshakeState::ClientHelloValidated {
|
||||
info: ClientHelloInfo {
|
||||
session_id: session_id.to_vec(),
|
||||
client_public_key,
|
||||
auth_key,
|
||||
cipher_suite,
|
||||
client_hello_handshake: client_hello_handshake.to_vec(),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Phase 2: Build server response using dest's record structure as template
|
||||
fn build_server_response_internal(&mut self, dest_records: &[bytes::Bytes]) -> io::Result<()> {
|
||||
// Take ownership of info from state (state already validated by caller)
|
||||
let HandshakeState::ClientHelloValidated { info } =
|
||||
std::mem::replace(&mut self.handshake_state, HandshakeState::Initial)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let cipher_suite = info.cipher_suite;
|
||||
|
||||
// Generate our server X25519 keypair
|
||||
let mut rng = rand::rng();
|
||||
let mut our_private_bytes = [0u8; 32];
|
||||
rng.fill_bytes(&mut our_private_bytes);
|
||||
|
||||
let our_private_key =
|
||||
agreement::PrivateKey::from_private_key(&agreement::X25519, &our_private_bytes)
|
||||
.map_err(|_| io::Error::other("Failed to create X25519 key"))?;
|
||||
let our_public_key_bytes = our_private_key
|
||||
.compute_public_key()
|
||||
.map_err(|_| io::Error::other("Failed to compute public key"))?;
|
||||
|
||||
// Generate server random
|
||||
let mut server_random = [0u8; 32];
|
||||
rng.fill_bytes(&mut server_random);
|
||||
|
||||
// Build ServerHello
|
||||
let server_hello = construct_server_hello(
|
||||
&server_random,
|
||||
session_id,
|
||||
&info.session_id,
|
||||
cipher_suite.id(),
|
||||
our_public_key_bytes.as_ref(),
|
||||
)?;
|
||||
|
||||
// Step 8: Compute transcript hashes using cipher suite's digest algorithm
|
||||
let client_hello_handshake = &client_hello[TLS_RECORD_HEADER_SIZE..]; // Skip TLS record header
|
||||
// Compute transcript hashes using cipher suite's digest algorithm
|
||||
let digest_alg = cipher_suite.digest_algorithm();
|
||||
|
||||
let mut ch_transcript = digest::Context::new(digest_alg);
|
||||
ch_transcript.update(client_hello_handshake);
|
||||
ch_transcript.update(&info.client_hello_handshake);
|
||||
let client_hello_hash = ch_transcript.finish();
|
||||
|
||||
let mut ch_sh_transcript = digest::Context::new(digest_alg);
|
||||
ch_sh_transcript.update(client_hello_handshake);
|
||||
ch_sh_transcript.update(&info.client_hello_handshake);
|
||||
ch_sh_transcript.update(&server_hello);
|
||||
|
||||
// Clone before finalizing
|
||||
let mut handshake_transcript = ch_sh_transcript.clone();
|
||||
let server_hello_hash = ch_sh_transcript.finish();
|
||||
|
||||
// Step 9: Perform ECDH for TLS 1.3 key derivation
|
||||
// Perform ECDH for TLS 1.3 key derivation
|
||||
let peer_public_key =
|
||||
agreement::UnparsedPublicKey::new(&agreement::X25519, &client_public_key);
|
||||
agreement::UnparsedPublicKey::new(&agreement::X25519, &info.client_public_key);
|
||||
let mut tls_shared_secret = [0u8; 32];
|
||||
agreement::agree(
|
||||
&our_private_key,
|
||||
@@ -465,7 +543,7 @@ impl RealityServerConnection {
|
||||
},
|
||||
)?;
|
||||
|
||||
// Step 10: Derive TLS 1.3 keys
|
||||
// Derive TLS 1.3 keys
|
||||
let hs_keys = derive_handshake_keys(
|
||||
cipher_suite,
|
||||
&tls_shared_secret,
|
||||
@@ -484,29 +562,28 @@ impl RealityServerConnection {
|
||||
}
|
||||
};
|
||||
|
||||
// Step 11: Generate HMAC-signed certificate
|
||||
let (cert_der, signing_key) = generate_hmac_certificate(&auth_key, dest_hostname)?;
|
||||
// Generate HMAC-signed certificate
|
||||
let (cert, signing_key) = generate_hmac_certificate(&info.auth_key, dest_hostname)?;
|
||||
|
||||
// Step 12: Build encrypted handshake messages
|
||||
// Build encrypted handshake messages
|
||||
let encrypted_extensions = construct_encrypted_extensions()?;
|
||||
handshake_transcript.update(&encrypted_extensions);
|
||||
|
||||
let certificate = construct_certificate(&cert_der)?;
|
||||
let certificate = construct_certificate(cert)?;
|
||||
handshake_transcript.update(&certificate);
|
||||
|
||||
let cert_verify_hash = handshake_transcript.clone().finish();
|
||||
// Ed25519 signs the full transcript hash - do NOT truncate for SHA384
|
||||
let certificate_verify =
|
||||
construct_certificate_verify(&signing_key, cert_verify_hash.as_ref())?;
|
||||
handshake_transcript.update(&certificate_verify);
|
||||
|
||||
let handshake_hash_before_finished = handshake_transcript.clone().finish();
|
||||
|
||||
// Step 13: Derive server handshake traffic keys for encryption
|
||||
// Derive server handshake traffic keys for encryption
|
||||
let (server_hs_key, server_hs_iv) =
|
||||
derive_traffic_keys(&hs_keys.server_handshake_traffic_secret, cipher_suite)?;
|
||||
|
||||
// Step 14: Build server Finished message first (before encryption)
|
||||
// Build server Finished message
|
||||
let server_verify_data = compute_finished_verify_data(
|
||||
cipher_suite,
|
||||
&hs_keys.server_handshake_traffic_secret,
|
||||
@@ -514,42 +591,86 @@ impl RealityServerConnection {
|
||||
)?;
|
||||
let server_finished = construct_finished(&server_verify_data)?;
|
||||
|
||||
// Step 15: Combine all 4 handshake messages into one plaintext buffer
|
||||
// This is required by REALITY protocol - all 4 messages in one encrypted record
|
||||
let mut combined_plaintext = Vec::new();
|
||||
combined_plaintext.extend_from_slice(&encrypted_extensions);
|
||||
combined_plaintext.extend_from_slice(&certificate);
|
||||
combined_plaintext.extend_from_slice(&certificate_verify);
|
||||
combined_plaintext.extend_from_slice(&server_finished);
|
||||
// Analyze dest's record structure to determine how to encrypt
|
||||
// dest_records: [0]=ServerHello, [1]=CCS, [2..]=encrypted handshake, possibly NewSessionTicket
|
||||
let dest_encrypted_records: Vec<&bytes::Bytes> = if dest_records.len() > 2 {
|
||||
dest_records[2..].iter().collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
"REALITY SERVER: Combining 4 handshake messages: EE={}, Cert={}, CV={}, Fin={}, Total={}",
|
||||
encrypted_extensions.len(),
|
||||
certificate.len(),
|
||||
certificate_verify.len(),
|
||||
server_finished.len(),
|
||||
combined_plaintext.len()
|
||||
);
|
||||
// Build handshake messages array
|
||||
let messages: [&[u8]; 4] = [
|
||||
&encrypted_extensions,
|
||||
&certificate,
|
||||
&certificate_verify,
|
||||
&server_finished,
|
||||
];
|
||||
|
||||
// Step 16: Encrypt the combined handshake messages, fragmenting into multiple records if needed
|
||||
// TLS 1.3 limits records to 16,640 bytes ciphertext (16,623 bytes plaintext).
|
||||
// Large certificates can exceed this, so we fragment like uTLS does.
|
||||
// Encrypt handshake messages, matching dest's structure exactly
|
||||
let mut handshake_ciphertext = Vec::new();
|
||||
let mut handshake_seq = 0u64;
|
||||
encrypt_handshake_to_records(
|
||||
cipher_suite,
|
||||
&combined_plaintext,
|
||||
&server_hs_key,
|
||||
&server_hs_iv,
|
||||
&mut handshake_seq,
|
||||
&mut handshake_ciphertext,
|
||||
)?;
|
||||
let hs_aead_key = AeadKey::new(cipher_suite, &server_hs_key)?;
|
||||
|
||||
// Update transcript with server Finished (needed for client Finished verification)
|
||||
// Determine if dest uses combined mode or separate mode using 512-byte heuristic
|
||||
// Like XTLS/REALITY: if first encrypted record > 512 bytes, it's combined mode
|
||||
let is_combined_mode = match dest_encrypted_records.first() {
|
||||
Some(first_record) => first_record.len() > 512,
|
||||
None => true, // No encrypted records = default to combined mode
|
||||
};
|
||||
|
||||
if is_combined_mode {
|
||||
// Combined mode: all messages in one record (with optional padding)
|
||||
let mut combined_plaintext = Vec::new();
|
||||
for msg in &messages {
|
||||
combined_plaintext.extend_from_slice(msg);
|
||||
}
|
||||
|
||||
let target_size = dest_encrypted_records.first().map(|r| r.len()).unwrap_or(0);
|
||||
|
||||
log::debug!(
|
||||
"REALITY SERVER: Combined mode - EE={}, Cert={}, CV={}, Fin={}, Total={}, target={}",
|
||||
encrypted_extensions.len(),
|
||||
certificate.len(),
|
||||
certificate_verify.len(),
|
||||
server_finished.len(),
|
||||
combined_plaintext.len(),
|
||||
target_size
|
||||
);
|
||||
|
||||
let mut encryptor =
|
||||
RecordEncryptor::new(&hs_aead_key, &server_hs_iv, &mut handshake_seq);
|
||||
encryptor.encrypt_handshake_with_padding(
|
||||
&combined_plaintext,
|
||||
&mut handshake_ciphertext,
|
||||
target_size,
|
||||
)?;
|
||||
} else {
|
||||
// Separate mode: encrypt each message as its own record, matching dest's sizes
|
||||
log::debug!(
|
||||
"REALITY SERVER: Separate mode - {} dest records, encrypting {} messages separately",
|
||||
dest_encrypted_records.len(),
|
||||
messages.len()
|
||||
);
|
||||
|
||||
let mut encryptor =
|
||||
RecordEncryptor::new(&hs_aead_key, &server_hs_iv, &mut handshake_seq);
|
||||
|
||||
for (i, msg) in messages.iter().enumerate() {
|
||||
let target_size = dest_encrypted_records.get(i).map(|r| r.len()).unwrap_or(0);
|
||||
encryptor.encrypt_handshake_with_padding(
|
||||
msg,
|
||||
&mut handshake_ciphertext,
|
||||
target_size,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update transcript with server Finished
|
||||
handshake_transcript.update(&server_finished);
|
||||
let handshake_hash_with_server_finished = handshake_transcript.finish();
|
||||
|
||||
// Step 16: Buffer all handshake messages to write buffer
|
||||
// Buffer all handshake messages to write buffer
|
||||
// ServerHello (plaintext)
|
||||
self.ciphertext_write_buf
|
||||
.extend_from_slice(&write_record_header(
|
||||
@@ -563,18 +684,17 @@ impl RealityServerConnection {
|
||||
.extend_from_slice(&write_record_header(CONTENT_TYPE_CHANGE_CIPHER_SPEC, 1));
|
||||
self.ciphertext_write_buf.push(0x01);
|
||||
|
||||
// Encrypted handshake record(s) - may be fragmented into multiple records
|
||||
// Encrypted handshake record(s)
|
||||
self.ciphertext_write_buf
|
||||
.extend_from_slice(&handshake_ciphertext);
|
||||
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"REALITY: ServerHello and encrypted handshake messages buffered ({} bytes)",
|
||||
self.ciphertext_write_buf.len()
|
||||
);
|
||||
|
||||
// Step 17: Update handshake state
|
||||
// Update handshake state
|
||||
self.handshake_state = HandshakeState::ServerHelloSent {
|
||||
handshake_hash: handshake_hash_before_finished.as_ref().to_vec(),
|
||||
handshake_hash_with_server_finished: handshake_hash_with_server_finished
|
||||
.as_ref()
|
||||
.to_vec(),
|
||||
@@ -583,7 +703,7 @@ impl RealityServerConnection {
|
||||
cipher_suite,
|
||||
};
|
||||
|
||||
Ok(true)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process client's Finished message and complete handshake
|
||||
@@ -647,27 +767,14 @@ impl RealityServerConnection {
|
||||
// NOW we're committed to processing - take ownership of handshake state
|
||||
// This avoids cloning Vec<u8> fields
|
||||
let old_state = std::mem::replace(&mut self.handshake_state, HandshakeState::Complete);
|
||||
let (
|
||||
let HandshakeState::ServerHelloSent {
|
||||
client_handshake_traffic_secret,
|
||||
master_secret,
|
||||
cipher_suite,
|
||||
_handshake_hash,
|
||||
handshake_hash_with_server_finished,
|
||||
) = match old_state {
|
||||
HandshakeState::ServerHelloSent {
|
||||
client_handshake_traffic_secret,
|
||||
master_secret,
|
||||
cipher_suite,
|
||||
handshake_hash,
|
||||
handshake_hash_with_server_finished,
|
||||
} => (
|
||||
client_handshake_traffic_secret, // moved, not cloned
|
||||
master_secret, // moved, not cloned
|
||||
cipher_suite,
|
||||
handshake_hash,
|
||||
handshake_hash_with_server_finished,
|
||||
),
|
||||
_ => unreachable!(), // Wrong state
|
||||
} = old_state
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
// Extract the encrypted Finished record (copy to Vec for decryption)
|
||||
@@ -737,12 +844,16 @@ impl RealityServerConnection {
|
||||
)?;
|
||||
|
||||
// Derive application traffic keys
|
||||
let (client_app_key, client_app_iv) =
|
||||
let (client_app_key_bytes, client_app_iv) =
|
||||
derive_traffic_keys(&client_app_secret, cipher_suite)?;
|
||||
let (server_app_key, server_app_iv) =
|
||||
let (server_app_key_bytes, server_app_iv) =
|
||||
derive_traffic_keys(&server_app_secret, cipher_suite)?;
|
||||
|
||||
// Store application traffic keys
|
||||
// Create cached AeadKey objects to avoid per-record key setup overhead
|
||||
let client_app_key = AeadKey::new(cipher_suite, &client_app_key_bytes)?;
|
||||
let server_app_key = AeadKey::new(cipher_suite, &server_app_key_bytes)?;
|
||||
|
||||
// Store application traffic keys (as cached AeadKey)
|
||||
self.app_read_key = Some(client_app_key);
|
||||
self.app_read_iv = Some(client_app_iv);
|
||||
self.app_write_key = Some(server_app_key);
|
||||
@@ -783,46 +894,19 @@ impl RealityServerConnection {
|
||||
break; // Need more data
|
||||
}
|
||||
|
||||
// Copy record header for decryption AAD
|
||||
let tls_header: [u8; TLS_RECORD_HEADER_SIZE] = [
|
||||
self.ciphertext_read_buf[0],
|
||||
self.ciphertext_read_buf[1],
|
||||
self.ciphertext_read_buf[2],
|
||||
self.ciphertext_read_buf[3],
|
||||
self.ciphertext_read_buf[4],
|
||||
];
|
||||
let ciphertext: Vec<u8> =
|
||||
self.ciphertext_read_buf[TLS_RECORD_HEADER_SIZE..total_record_len].to_vec();
|
||||
self.ciphertext_read_buf.consume(total_record_len);
|
||||
|
||||
// Decrypt the application data
|
||||
let cipher_suite = self.cipher_suite.ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "Cipher suite not set")
|
||||
})?;
|
||||
let mut plaintext = decrypt_tls13_record(
|
||||
cipher_suite,
|
||||
app_read_key,
|
||||
app_read_iv,
|
||||
self.read_seq,
|
||||
&ciphertext,
|
||||
&tls_header,
|
||||
)?;
|
||||
|
||||
increment_seq(&mut self.read_seq)?;
|
||||
|
||||
// TLS 1.3: Remove ContentType trailer byte
|
||||
// The last byte of the plaintext is the actual ContentType
|
||||
if plaintext.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let content_type = plaintext.pop().unwrap();
|
||||
// Decrypt in-place: get mutable slice of ciphertext, decrypt, copy plaintext out
|
||||
let ciphertext_slice = self
|
||||
.ciphertext_read_buf
|
||||
.slice_mut(TLS_RECORD_HEADER_SIZE..total_record_len);
|
||||
let mut decryptor = RecordDecryptor::new(app_read_key, app_read_iv, &mut self.read_seq);
|
||||
let (content_type, plaintext) =
|
||||
decryptor.decrypt_record_in_place(ciphertext_slice, record_len as u16)?;
|
||||
|
||||
match content_type {
|
||||
CONTENT_TYPE_APPLICATION_DATA => {
|
||||
// Compact plaintext buffer if needed before extending
|
||||
self.plaintext_read_buf.maybe_compact(4096);
|
||||
self.plaintext_read_buf.extend_from_slice(&plaintext);
|
||||
self.plaintext_read_buf.extend_from_slice(plaintext);
|
||||
}
|
||||
CONTENT_TYPE_ALERT => {
|
||||
// Parse alert: level (1 byte) + description (1 byte)
|
||||
@@ -852,10 +936,16 @@ impl RealityServerConnection {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::warn!("REALITY: Unexpected ContentType: 0x{:02x}", content_type);
|
||||
}
|
||||
// CONTENT_TYPE_HANDSHAKE is invalid after handshake complete
|
||||
// strip_content_type() validates and returns error for invalid types
|
||||
_ => unreachable!(
|
||||
"strip_content_type validates content type; unexpected: 0x{:02x}",
|
||||
content_type
|
||||
),
|
||||
}
|
||||
|
||||
// Consume the processed record from the buffer (after plaintext borrow ends)
|
||||
self.ciphertext_read_buf.consume(total_record_len);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -899,15 +989,10 @@ impl RealityServerConnection {
|
||||
}
|
||||
};
|
||||
|
||||
let cipher_suite = self.cipher_suite.ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "Cipher suite not set")
|
||||
})?;
|
||||
encrypt_plaintext_to_records(
|
||||
cipher_suite,
|
||||
let mut encryptor =
|
||||
RecordEncryptor::new(app_write_key, app_write_iv, &mut self.write_seq);
|
||||
encryptor.encrypt_app_data(
|
||||
&mut self.plaintext_write_buf,
|
||||
app_write_key,
|
||||
app_write_iv,
|
||||
&mut self.write_seq,
|
||||
&mut self.ciphertext_write_buf,
|
||||
)?;
|
||||
}
|
||||
@@ -974,28 +1059,10 @@ impl RealityServerConnection {
|
||||
}
|
||||
};
|
||||
|
||||
// Get cipher suite
|
||||
let cipher_suite = match self.cipher_suite {
|
||||
Some(cs) => cs,
|
||||
None => {
|
||||
log::debug!("REALITY: Cannot send close_notify - cipher suite not set");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Use common helper to build encrypted close_notify alert
|
||||
match common::build_close_notify_alert(
|
||||
cipher_suite,
|
||||
app_write_key,
|
||||
app_write_iv,
|
||||
self.write_seq,
|
||||
) {
|
||||
Ok(record) => {
|
||||
if let Err(e) = increment_seq(&mut self.write_seq) {
|
||||
log::error!("REALITY: {}", e);
|
||||
return;
|
||||
}
|
||||
self.ciphertext_write_buf.extend_from_slice(&record);
|
||||
// Encrypt close_notify alert using RecordEncryptor
|
||||
let mut encryptor = RecordEncryptor::new(app_write_key, app_write_iv, &mut self.write_seq);
|
||||
match encryptor.encrypt_close_notify(&mut self.ciphertext_write_buf) {
|
||||
Ok(()) => {
|
||||
log::debug!("REALITY: Encrypted close_notify alert queued");
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -1005,7 +1072,7 @@ impl RealityServerConnection {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
pub fn feed_reality_server_connection(
|
||||
server_connection: &mut RealityServerConnection,
|
||||
data: &[u8],
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::Bytes;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::time::{Instant, timeout_at};
|
||||
|
||||
use crate::address::NetLocation;
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::client_proxy_chain::ClientProxyChain;
|
||||
use crate::client_proxy_selector::ClientProxySelector;
|
||||
use crate::crypto::{CryptoConnection, CryptoTlsStream, perform_crypto_handshake};
|
||||
use crate::resolver::Resolver;
|
||||
use crate::shadow_tls::{ParsedClientHello, parse_server_hello};
|
||||
use crate::tcp::tcp_handler::{TcpClientSetupResult, TcpServerSetupResult};
|
||||
use crate::tls_server_handler::InnerProtocol;
|
||||
use crate::util::allocate_vec;
|
||||
use crate::vless::tls_deframer::TlsDeframer;
|
||||
|
||||
use super::{RealityServerConfig, RealityServerConnection};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RealityServerTarget {
|
||||
pub private_key: [u8; 32],
|
||||
pub short_ids: Vec<[u8; 8]>,
|
||||
pub dest: NetLocation,
|
||||
pub max_time_diff: Option<u64>, // in milliseconds
|
||||
pub min_client_version: Option<[u8; 3]>,
|
||||
pub max_client_version: Option<[u8; 3]>,
|
||||
pub cipher_suites: Vec<super::CipherSuite>,
|
||||
/// The effective proxy selector for this REALITY target.
|
||||
/// For Vision mode, this is passed to the VLESS setup function.
|
||||
/// Inner handler already has this selector from construction.
|
||||
pub effective_selector: Arc<ClientProxySelector>,
|
||||
/// What to do after Reality termination - normal handler, Vision VLESS, or Naive
|
||||
pub inner_protocol: InnerProtocol,
|
||||
/// Client chain for connecting to dest server (for fallback connections).
|
||||
pub dest_client_chain: ClientProxyChain,
|
||||
}
|
||||
|
||||
/// Set up REALITY server stream with real-time mirroring for anti-probing.
|
||||
///
|
||||
/// Connect to dest IMMEDIATELY before auth processing, making timing
|
||||
/// indistinguishable from a real reverse proxy. This defeats active probing.
|
||||
///
|
||||
/// Flow:
|
||||
/// - Connect to dest immediately
|
||||
/// - Forward ClientHello immediately (starts dest's handshake)
|
||||
/// - Validate auth (fast, ~1ms, while dest is processing)
|
||||
/// - Read dest's response (it's been processing in parallel)
|
||||
/// - Branch based on auth:
|
||||
/// - Auth failed: forward dest's response, continue bidirectional copy
|
||||
/// - Auth succeeded: build REALITY response matching dest's structure
|
||||
#[inline]
|
||||
pub async fn setup_reality_server_stream(
|
||||
mut server_stream: Box<dyn AsyncStream>,
|
||||
target: &RealityServerTarget,
|
||||
parsed_client_hello: ParsedClientHello,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
) -> std::io::Result<TcpServerSetupResult> {
|
||||
let client_hello_frame = &parsed_client_hello.client_hello_frame;
|
||||
log::debug!(
|
||||
"REALITY ClientHello frame length: {}",
|
||||
client_hello_frame.len()
|
||||
);
|
||||
|
||||
// Connect to dest before auth processing to minimize timing differences
|
||||
let TcpClientSetupResult {
|
||||
client_stream: mut dest_stream,
|
||||
early_data,
|
||||
} = target
|
||||
.dest_client_chain
|
||||
.connect_tcp(target.dest.clone(), resolver)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::ConnectionRefused,
|
||||
format!("REALITY: Failed to connect to dest {}: {}", target.dest, e),
|
||||
)
|
||||
})?;
|
||||
|
||||
debug_assert!(
|
||||
early_data.is_none(),
|
||||
"unexpected early_data from dest connection"
|
||||
);
|
||||
|
||||
log::debug!(
|
||||
"REALITY: Connected to dest {}, forwarding ClientHello ({} bytes)",
|
||||
target.dest,
|
||||
client_hello_frame.len()
|
||||
);
|
||||
|
||||
dest_stream.write_all(client_hello_frame).await?;
|
||||
dest_stream.flush().await?;
|
||||
|
||||
if !parsed_client_hello.supports_tls13 {
|
||||
log::warn!("REALITY: Client does not support TLS 1.3, falling back to dest");
|
||||
return forward_to_dest_and_copy(server_stream, dest_stream, vec![], Bytes::new()).await;
|
||||
}
|
||||
|
||||
let reality_config = RealityServerConfig {
|
||||
private_key: target.private_key,
|
||||
short_ids: target.short_ids.clone(),
|
||||
dest: target.dest.clone(),
|
||||
max_time_diff: target.max_time_diff,
|
||||
min_client_version: target.min_client_version,
|
||||
max_client_version: target.max_client_version,
|
||||
cipher_suites: target.cipher_suites.clone(),
|
||||
};
|
||||
|
||||
let mut reality_conn = RealityServerConnection::new(reality_config)?;
|
||||
|
||||
let auth_result = reality_conn.validate_client_hello(client_hello_frame);
|
||||
|
||||
// Read dest response until we have enough records. Use 512-byte heuristic like XTLS/REALITY:
|
||||
// first encrypted record > 512 bytes = combined mode, <= 512 bytes = separate mode
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
let mut deframer = TlsDeframer::new();
|
||||
let mut dest_records: Vec<Bytes> = Vec::new();
|
||||
let mut buf = allocate_vec(8192).into_boxed_slice();
|
||||
let mut dest_handshake_success = false;
|
||||
|
||||
loop {
|
||||
let new_records = match timeout_at(deadline, dest_stream.read(&mut buf)).await {
|
||||
Ok(Ok(0)) => {
|
||||
return Err(std::io::Error::other(
|
||||
"REALITY: Dest connection closed during TLS handshake",
|
||||
));
|
||||
}
|
||||
Ok(Ok(n)) => {
|
||||
deframer.feed(&buf[..n]);
|
||||
match deframer.next_records() {
|
||||
Ok(records) => records,
|
||||
Err(e) => {
|
||||
log::error!("REALITY: Error parsing dest records: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"REALITY: Error reading from dest: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
Err(_) => {
|
||||
log::debug!("REALITY: Timeout reading from dest");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// When we get the first record (ServerHello), check if dest supports TLS 1.3
|
||||
if dest_records.is_empty() && !new_records.is_empty() {
|
||||
match parse_server_hello(&new_records[0]) {
|
||||
Ok(parsed) => {
|
||||
if !parsed.is_tls13 {
|
||||
log::error!(
|
||||
"REALITY: Dest {} is TLS 1.2, falling back to transparent forward",
|
||||
target.dest
|
||||
);
|
||||
return forward_to_dest_and_copy(
|
||||
server_stream,
|
||||
dest_stream,
|
||||
dest_records,
|
||||
deframer.into_remaining_data(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
log::debug!("REALITY: Dest confirmed TLS 1.3");
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"REALITY: Failed to parse dest ServerHello: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dest_records.extend(new_records);
|
||||
|
||||
// Separate mode: first encrypted record is small, need more records
|
||||
// Keep reading until we have 6 records (SH + CCS + 4 encrypted) or timeout
|
||||
// Note: Some servers send NewSessionTicket as a 7th record, but we don't need it
|
||||
if dest_records.len() >= 6 {
|
||||
log::debug!(
|
||||
"REALITY: Separate mode detected, got {} records",
|
||||
dest_records.len()
|
||||
);
|
||||
dest_handshake_success = true;
|
||||
break;
|
||||
} else if dest_records.len() >= 3 {
|
||||
// Check if we have enough records using the 512-byte heuristic
|
||||
// Records: [0]=ServerHello, [1]=CCS, [2..]=encrypted handshake
|
||||
let first_encrypted = &dest_records[2];
|
||||
if first_encrypted.len() > 512 {
|
||||
// Combined mode: first encrypted record > 512 bytes contains all messages
|
||||
log::debug!(
|
||||
"REALITY: Combined mode detected (first encrypted record {} bytes > 512)",
|
||||
first_encrypted.len()
|
||||
);
|
||||
dest_handshake_success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let remaining_data = deframer.into_remaining_data();
|
||||
|
||||
if !dest_handshake_success {
|
||||
log::warn!(
|
||||
"REALITY: Dest handshake failed (got {} records), falling back to transparent forward",
|
||||
dest_records.len()
|
||||
);
|
||||
return forward_to_dest_and_copy(server_stream, dest_stream, dest_records, remaining_data)
|
||||
.await;
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"REALITY: Read {} records from dest ({} bytes remaining)",
|
||||
dest_records.len(),
|
||||
remaining_data.len()
|
||||
);
|
||||
|
||||
// Branch based on auth result. We don't short circuit on permission denied before the
|
||||
// read loop above so that the timing is always the same.
|
||||
match auth_result {
|
||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||
log::warn!(
|
||||
"REALITY: Auth failed ({}), forwarding to dest transparently",
|
||||
e
|
||||
);
|
||||
return forward_to_dest_and_copy(
|
||||
server_stream,
|
||||
dest_stream,
|
||||
dest_records,
|
||||
remaining_data,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
log::error!("REALITY: Unexpected error during auth: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
Ok(()) => {}
|
||||
}
|
||||
|
||||
log::debug!("REALITY: Auth succeeded, building response matching dest structure");
|
||||
|
||||
drop(dest_stream);
|
||||
reality_conn.build_server_response(dest_records)?;
|
||||
|
||||
let mut connection = CryptoConnection::new_reality_server(reality_conn);
|
||||
perform_crypto_handshake(&mut connection, &mut server_stream, 16384).await?;
|
||||
|
||||
let tls_stream = CryptoTlsStream::new(server_stream, connection);
|
||||
log::debug!("REALITY: TLS 1.3 handshake completed successfully");
|
||||
|
||||
match &target.inner_protocol {
|
||||
InnerProtocol::Normal(handler) => handler.setup_server_stream(Box::new(tls_stream)).await,
|
||||
InnerProtocol::VisionVless(vision_cfg) => {
|
||||
crate::vless::vless_server_handler::setup_custom_tls_vision_vless_server_stream(
|
||||
tls_stream,
|
||||
&vision_cfg.user_id,
|
||||
vision_cfg.udp_enabled,
|
||||
target.effective_selector.clone(),
|
||||
resolver,
|
||||
vision_cfg.fallback.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
InnerProtocol::Naive(naive_cfg) => {
|
||||
crate::naiveproxy::setup_naive_server_stream(
|
||||
tls_stream,
|
||||
naive_cfg,
|
||||
target.effective_selector.clone(),
|
||||
resolver.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Forward dest records to client and spawn bidirectional copy
|
||||
///
|
||||
/// Used when Reality auth fails or client doesn't support TLS 1.3.
|
||||
/// Forwards any already-read dest records to the client, then spawns
|
||||
/// bidirectional copy for the rest of the connection.
|
||||
async fn forward_to_dest_and_copy(
|
||||
mut client_stream: Box<dyn AsyncStream>,
|
||||
mut dest_stream: Box<dyn AsyncStream>,
|
||||
dest_records: Vec<Bytes>,
|
||||
remaining_data: Bytes,
|
||||
) -> std::io::Result<TcpServerSetupResult> {
|
||||
for record in &dest_records {
|
||||
client_stream.write_all(record).await?;
|
||||
}
|
||||
|
||||
if !remaining_data.is_empty() {
|
||||
client_stream.write_all(&remaining_data).await?;
|
||||
}
|
||||
|
||||
client_stream.flush().await?;
|
||||
|
||||
log::debug!(
|
||||
"REALITY FALLBACK: Forwarded {} records + {} remaining bytes, spawning bidirectional copy",
|
||||
dest_records.len(),
|
||||
remaining_data.len()
|
||||
);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let result = crate::copy_bidirectional::copy_bidirectional(
|
||||
&mut *client_stream,
|
||||
&mut dest_stream,
|
||||
false, // client doesn't need initial flush
|
||||
false, // dest doesn't need initial flush
|
||||
)
|
||||
.await;
|
||||
|
||||
let _ = client_stream.shutdown().await;
|
||||
let _ = dest_stream.shutdown().await;
|
||||
|
||||
if let Err(e) = result {
|
||||
log::debug!("REALITY FALLBACK: Connection ended: {}", e);
|
||||
} else {
|
||||
log::debug!("REALITY FALLBACK: Connection completed");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(TcpServerSetupResult::AlreadyHandled)
|
||||
}
|
||||
@@ -28,7 +28,6 @@ pub fn hkdf_expand(
|
||||
info: &[u8],
|
||||
length: usize,
|
||||
) -> Result<Vec<u8>> {
|
||||
// Get hash length from the algorithm's digest
|
||||
let hash_len = hmac_algorithm.digest_algorithm().output_len();
|
||||
let n = length.div_ceil(hash_len); // Number of iterations
|
||||
|
||||
@@ -107,7 +106,6 @@ fn hkdf_expand_label_with_algorithm(
|
||||
|
||||
log::debug!("HKDF_LABEL_BYTES: {:02x?}", hkdf_label);
|
||||
|
||||
// Use HKDF expand with the specified algorithm
|
||||
hkdf_expand(hmac_algorithm, secret, &hkdf_label, length)
|
||||
}
|
||||
|
||||
@@ -167,8 +165,8 @@ pub fn derive_traffic_keys(
|
||||
let iv =
|
||||
hkdf_expand_label_with_algorithm(hmac_algorithm, traffic_secret, b"iv", b"", iv_length)?;
|
||||
|
||||
log::info!("TRAFFIC_KEY_DERIVE: key={:02x?}", key);
|
||||
log::info!("TRAFFIC_KEY_DERIVE: iv={:02x?}", iv);
|
||||
log::debug!("TRAFFIC_KEY_DERIVE: key={:02x?}", key);
|
||||
log::debug!("TRAFFIC_KEY_DERIVE: iv={:02x?}", iv);
|
||||
|
||||
Ok((key, iv))
|
||||
}
|
||||
@@ -315,7 +313,7 @@ pub fn derive_application_secrets(
|
||||
"TLS13 DEBUG: Deriving application secrets (Phase 2) with {:?}...",
|
||||
cipher_suite
|
||||
);
|
||||
log::info!(
|
||||
log::debug!(
|
||||
" handshake_hash (with Finished): {:?}",
|
||||
&handshake_hash[..8]
|
||||
);
|
||||
@@ -332,7 +330,7 @@ pub fn derive_application_secrets(
|
||||
" client_app_traffic: {:?}",
|
||||
&client_application_traffic_secret[..8]
|
||||
);
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"DERIVE_APP_SECRETS: ClientAppSecret(full)={:02x?}",
|
||||
client_application_traffic_secret
|
||||
);
|
||||
@@ -349,7 +347,7 @@ pub fn derive_application_secrets(
|
||||
" server_app_traffic: {:?}",
|
||||
&server_application_traffic_secret[..8]
|
||||
);
|
||||
log::info!(
|
||||
log::debug!(
|
||||
"DERIVE_APP_SECRETS: ServerAppSecret(full)={:02x?}",
|
||||
server_application_traffic_secret
|
||||
);
|
||||
|
||||
@@ -118,9 +118,9 @@ pub fn construct_encrypted_extensions() -> Result<Vec<u8>> {
|
||||
/// Construct Certificate message with HMAC-signed Ed25519 certificate
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `cert_der` - DER-encoded certificate (with HMAC signature)
|
||||
pub fn construct_certificate(cert_der: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut certificate = Vec::new();
|
||||
/// * `cert` - Certificate from rcgen (takes ownership to avoid allocation)
|
||||
pub fn construct_certificate(cert: rcgen::Certificate) -> Result<Vec<u8>> {
|
||||
let cert_der = cert.der();
|
||||
|
||||
// Certificate structure:
|
||||
// - handshake_type (1 byte) = 11
|
||||
@@ -131,44 +131,48 @@ pub fn construct_certificate(cert_der: &[u8]) -> Result<Vec<u8>> {
|
||||
// - cert_data (3 bytes length + DER)
|
||||
// - extensions (2 bytes length, usually empty)
|
||||
|
||||
let mut payload = Vec::new();
|
||||
// Pre-calculate sizes to allocate exact capacity
|
||||
// cert_list = 3 (len) + cert_der.len() + 2 (extensions)
|
||||
let cert_list_len = 3 + cert_der.len() + 2;
|
||||
// payload = 1 (context) + 3 (list len) + cert_list_len
|
||||
let payload_len = 1 + 3 + cert_list_len;
|
||||
// total = 1 (type) + 3 (payload len) + payload_len
|
||||
let total_len = 1 + 3 + payload_len;
|
||||
|
||||
// Certificate request context (empty for server certificates)
|
||||
payload.push(0x00);
|
||||
|
||||
// Certificate list
|
||||
let mut cert_list = Vec::new();
|
||||
|
||||
// Certificate entry
|
||||
// Cert data length (3 bytes)
|
||||
cert_list.extend_from_slice(&[
|
||||
((cert_der.len() >> 16) & 0xff) as u8,
|
||||
((cert_der.len() >> 8) & 0xff) as u8,
|
||||
(cert_der.len() & 0xff) as u8,
|
||||
]);
|
||||
cert_list.extend_from_slice(cert_der);
|
||||
|
||||
// Extensions (empty)
|
||||
cert_list.extend_from_slice(&[0x00, 0x00]);
|
||||
|
||||
// Certificate list length (3 bytes)
|
||||
payload.extend_from_slice(&[
|
||||
((cert_list.len() >> 16) & 0xff) as u8,
|
||||
((cert_list.len() >> 8) & 0xff) as u8,
|
||||
(cert_list.len() & 0xff) as u8,
|
||||
]);
|
||||
payload.extend_from_slice(&cert_list);
|
||||
let mut certificate = Vec::with_capacity(total_len);
|
||||
|
||||
// Handshake header
|
||||
certificate.push(HANDSHAKE_TYPE_CERTIFICATE);
|
||||
|
||||
// Payload length (3 bytes)
|
||||
certificate.extend_from_slice(&[
|
||||
((payload.len() >> 16) & 0xff) as u8,
|
||||
((payload.len() >> 8) & 0xff) as u8,
|
||||
(payload.len() & 0xff) as u8,
|
||||
((payload_len >> 16) & 0xff) as u8,
|
||||
((payload_len >> 8) & 0xff) as u8,
|
||||
(payload_len & 0xff) as u8,
|
||||
]);
|
||||
certificate.extend_from_slice(&payload);
|
||||
|
||||
// Certificate request context (empty for server certificates)
|
||||
certificate.push(0x00);
|
||||
|
||||
// Certificate list length (3 bytes)
|
||||
certificate.extend_from_slice(&[
|
||||
((cert_list_len >> 16) & 0xff) as u8,
|
||||
((cert_list_len >> 8) & 0xff) as u8,
|
||||
(cert_list_len & 0xff) as u8,
|
||||
]);
|
||||
|
||||
// Certificate entry - cert data length (3 bytes)
|
||||
certificate.extend_from_slice(&[
|
||||
((cert_der.len() >> 16) & 0xff) as u8,
|
||||
((cert_der.len() >> 8) & 0xff) as u8,
|
||||
(cert_der.len() & 0xff) as u8,
|
||||
]);
|
||||
|
||||
// Certificate DER data
|
||||
certificate.extend_from_slice(cert_der);
|
||||
|
||||
// Extensions (empty)
|
||||
certificate.extend_from_slice(&[0x00, 0x00]);
|
||||
|
||||
Ok(certificate)
|
||||
}
|
||||
@@ -252,6 +256,9 @@ pub fn construct_finished(verify_data: &[u8]) -> Result<Vec<u8>> {
|
||||
Ok(finished)
|
||||
}
|
||||
|
||||
/// Default ALPN protocols for REALITY client (matches browser fingerprints)
|
||||
pub const DEFAULT_ALPN_PROTOCOLS: &[&str] = &["h2", "http/1.1"];
|
||||
|
||||
/// Construct TLS 1.3 ClientHello message
|
||||
///
|
||||
/// Returns handshake message bytes (without record header)
|
||||
@@ -262,12 +269,14 @@ pub fn construct_finished(verify_data: &[u8]) -> Result<Vec<u8>> {
|
||||
/// * `client_public_key` - X25519 public key bytes
|
||||
/// * `server_name` - SNI hostname
|
||||
/// * `cipher_suites` - Cipher suite IDs to offer (e.g., &[0x1301, 0x1302, 0x1303])
|
||||
/// * `alpn_protocols` - ALPN protocols to offer (e.g., &["h2", "http/1.1"])
|
||||
pub fn construct_client_hello(
|
||||
client_random: &[u8; 32],
|
||||
session_id: &[u8; 32],
|
||||
client_public_key: &[u8],
|
||||
server_name: &str,
|
||||
cipher_suites: &[u16],
|
||||
alpn_protocols: &[&str],
|
||||
) -> Result<Vec<u8>> {
|
||||
let mut hello = Vec::with_capacity(512);
|
||||
|
||||
@@ -304,7 +313,7 @@ pub fn construct_client_hello(
|
||||
|
||||
let mut extensions = Vec::new();
|
||||
|
||||
// 1. server_name extension (type 0)
|
||||
// server_name extension (type 0)
|
||||
{
|
||||
let server_name_bytes = server_name.as_bytes();
|
||||
let server_name_len = server_name_bytes.len();
|
||||
@@ -318,7 +327,7 @@ pub fn construct_client_hello(
|
||||
extensions.extend_from_slice(server_name_bytes); // Server name
|
||||
}
|
||||
|
||||
// 2. supported_versions extension (type 43)
|
||||
// supported_versions extension (type 43)
|
||||
{
|
||||
extensions.extend_from_slice(&[0x00, 0x2b]); // Extension type: supported_versions
|
||||
extensions.extend_from_slice(&[0x00, 0x03]); // Extension length: 3
|
||||
@@ -326,7 +335,7 @@ pub fn construct_client_hello(
|
||||
extensions.extend_from_slice(&[0x03, 0x04]); // TLS 1.3
|
||||
}
|
||||
|
||||
// 3. supported_groups extension (type 10)
|
||||
// supported_groups extension (type 10)
|
||||
{
|
||||
extensions.extend_from_slice(&[0x00, 0x0a]); // Extension type: supported_groups
|
||||
extensions.extend_from_slice(&[0x00, 0x04]); // Extension length: 4
|
||||
@@ -334,7 +343,7 @@ pub fn construct_client_hello(
|
||||
extensions.extend_from_slice(&[0x00, 0x1d]); // x25519
|
||||
}
|
||||
|
||||
// 4. key_share extension (type 51)
|
||||
// key_share extension (type 51)
|
||||
{
|
||||
extensions.extend_from_slice(&[0x00, 0x33]); // Extension type: key_share
|
||||
let key_share_len = 2 + 4 + client_public_key.len();
|
||||
@@ -346,7 +355,7 @@ pub fn construct_client_hello(
|
||||
extensions.extend_from_slice(client_public_key); // Public key
|
||||
}
|
||||
|
||||
// 5. signature_algorithms extension (type 13)
|
||||
// signature_algorithms extension (type 13)
|
||||
{
|
||||
extensions.extend_from_slice(&[0x00, 0x0d]); // Extension type: signature_algorithms
|
||||
extensions.extend_from_slice(&[0x00, 0x04]); // Extension length: 4
|
||||
@@ -354,6 +363,30 @@ pub fn construct_client_hello(
|
||||
extensions.extend_from_slice(&[0x08, 0x07]); // ed25519
|
||||
}
|
||||
|
||||
// ALPN extension (type 16)
|
||||
if !alpn_protocols.is_empty() {
|
||||
extensions.extend_from_slice(&[0x00, 0x10]); // Extension type: ALPN (16)
|
||||
|
||||
// Calculate total length of protocol list
|
||||
let protocols_list_len: usize = alpn_protocols
|
||||
.iter()
|
||||
.map(|p| 1 + p.len()) // 1 byte length prefix + protocol bytes
|
||||
.sum();
|
||||
|
||||
// Extension length = 2 (list length field) + protocols_list_len
|
||||
let ext_len = 2 + protocols_list_len;
|
||||
extensions.extend_from_slice(&(ext_len as u16).to_be_bytes());
|
||||
|
||||
// Protocol list length
|
||||
extensions.extend_from_slice(&(protocols_list_len as u16).to_be_bytes());
|
||||
|
||||
// Each protocol: 1 byte length + protocol string
|
||||
for protocol in alpn_protocols {
|
||||
extensions.push(protocol.len() as u8);
|
||||
extensions.extend_from_slice(protocol.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
// Write extensions length
|
||||
let extensions_length = extensions.len();
|
||||
hello[extensions_offset..extensions_offset + 2]
|
||||
@@ -412,8 +445,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_construct_certificate() {
|
||||
let cert_der = vec![0xBBu8; 100];
|
||||
let result = construct_certificate(&cert_der);
|
||||
use crate::reality::reality_certificate::generate_hmac_certificate;
|
||||
let auth_key = [0x42u8; 32];
|
||||
let (cert, _) = generate_hmac_certificate(&auth_key, "test.example.com").unwrap();
|
||||
let result = construct_certificate(cert);
|
||||
assert!(result.is_ok());
|
||||
let msg = result.unwrap();
|
||||
assert_eq!(msg[0], HANDSHAKE_TYPE_CERTIFICATE);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use aws_lc_rs::{
|
||||
agreement,
|
||||
rand::{SecureRandom, SystemRandom},
|
||||
};
|
||||
use aws_lc_rs::agreement;
|
||||
use base64::engine::{Engine as _, general_purpose::URL_SAFE_NO_PAD};
|
||||
use rand::RngCore;
|
||||
|
||||
use super::reality_cipher_suite::CipherSuite;
|
||||
use crate::buf_reader::BufReader;
|
||||
@@ -461,23 +459,21 @@ pub fn negotiate_cipher_suite(
|
||||
}
|
||||
|
||||
pub fn generate_keypair() -> std::io::Result<(String, String)> {
|
||||
// Step 1: Generate 32 random bytes for private key
|
||||
let rng = SystemRandom::new();
|
||||
// Generate 32 random bytes for private key
|
||||
let mut private_key_bytes = [0u8; 32];
|
||||
rng.fill(&mut private_key_bytes)
|
||||
.map_err(|_| std::io::Error::other("RNG failed"))?;
|
||||
rand::rng().fill_bytes(&mut private_key_bytes);
|
||||
|
||||
// Step 2: Create X25519 private key from the random bytes
|
||||
// Create X25519 private key from the random bytes
|
||||
let private_key =
|
||||
agreement::PrivateKey::from_private_key(&agreement::X25519, &private_key_bytes)
|
||||
.map_err(|_| std::io::Error::other("Failed to create X25519 key"))?;
|
||||
|
||||
// Step 3: Derive public key from private key
|
||||
// Derive public key from private key
|
||||
let public_key_bytes = private_key
|
||||
.compute_public_key()
|
||||
.map_err(|_| std::io::Error::other("Failed to compute public key"))?;
|
||||
|
||||
// Step 4: Encode both keys as base64url (no padding)
|
||||
// Encode both keys as base64url (no padding)
|
||||
let private_key_b64 = URL_SAFE_NO_PAD.encode(private_key_bytes);
|
||||
let public_key_b64 = URL_SAFE_NO_PAD.encode(public_key_bytes.as_ref());
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::address::NetLocation;
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::crypto::{CryptoConnection, CryptoTlsStream, perform_crypto_handshake};
|
||||
use crate::reality::{CipherSuite, RealityClientConfig, RealityClientConnection};
|
||||
use crate::tcp_handler::{TcpClientHandler, TcpClientSetupResult};
|
||||
use crate::tcp::tcp_handler::{TcpClientHandler, TcpClientSetupResult};
|
||||
|
||||
/// REALITY client handler using buffered Connection API
|
||||
///
|
||||
@@ -71,7 +71,6 @@ impl RealityClientHandler {
|
||||
&self,
|
||||
mut client_stream: Box<dyn AsyncStream>,
|
||||
) -> std::io::Result<CryptoTlsStream<Box<dyn AsyncStream>>> {
|
||||
// Extract server name as string
|
||||
let server_name_str = match &self.server_name {
|
||||
rustls::pki_types::ServerName::DnsName(name) => name.as_ref(),
|
||||
rustls::pki_types::ServerName::IpAddress(ip) => {
|
||||
@@ -98,11 +97,9 @@ impl RealityClientHandler {
|
||||
|
||||
let reality_conn = RealityClientConnection::new(reality_config)?;
|
||||
|
||||
// Wrap in Connection enum
|
||||
log::debug!("REALITY CLIENT: Creating Connection");
|
||||
let mut connection = CryptoConnection::new_reality_client(reality_conn);
|
||||
|
||||
// Perform the TLS handshake
|
||||
perform_crypto_handshake(&mut connection, &mut client_stream, 16384).await?;
|
||||
log::debug!("REALITY CLIENT: Handshake completed successfully");
|
||||
|
||||
@@ -143,23 +140,22 @@ impl TcpClientHandler for RealityClientHandler {
|
||||
}
|
||||
}
|
||||
|
||||
async fn setup_client_udp_stream(
|
||||
async fn setup_client_udp_bidirectional(
|
||||
&self,
|
||||
client_stream: Box<dyn AsyncStream>,
|
||||
request: crate::tcp_handler::UdpStreamRequest,
|
||||
) -> std::io::Result<crate::tcp_handler::TcpClientUdpSetupResult> {
|
||||
target: NetLocation,
|
||||
) -> std::io::Result<Box<dyn crate::async_stream::AsyncMessageStream>> {
|
||||
let tls_stream = self.setup_client_stream_common(client_stream).await?;
|
||||
|
||||
match &self.handler {
|
||||
RealityInnerClientHandler::Default(handler) => {
|
||||
handler
|
||||
.setup_client_udp_stream(Box::new(tls_stream), request)
|
||||
.setup_client_udp_bidirectional(Box::new(tls_stream), target)
|
||||
.await
|
||||
}
|
||||
RealityInnerClientHandler::VisionVless { uuid, .. } => {
|
||||
// Vision VLESS UDP setup - use the VLESS handler's method
|
||||
crate::vless::vless_client_handler::setup_vless_udp_stream(
|
||||
tls_stream, uuid, request,
|
||||
crate::vless::vless_client_handler::setup_vless_udp_bidirectional(
|
||||
tls_stream, uuid, target,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -60,6 +60,8 @@ pub async fn resolve_single_address(
|
||||
Ok(resolve_results[0])
|
||||
}
|
||||
|
||||
// TODO: use this everywhere a Resolver is used
|
||||
// TODO: make it an LRU cache
|
||||
pub struct ResolverCache {
|
||||
resolver: Arc<dyn Resolver>,
|
||||
cache: FxHashMap<NetLocation, ResolveState>,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
//! UDP routing module for per-destination routing.
|
||||
//!
|
||||
//! This module provides the `UdpRouter` which handles multi-destination UDP streams
|
||||
//! (both `AsyncTargetedMessageStream` and `AsyncSessionMessageStream`) by creating
|
||||
//! per-destination sessions that each route through the appropriate upstream chain
|
||||
//! based on routing rules.
|
||||
|
||||
mod udp_router;
|
||||
|
||||
pub use udp_router::{ServerStream, run_udp_routing};
|
||||
File diff suppressed because it is too large
Load Diff
+22
-13
@@ -10,10 +10,16 @@ pub fn create_client_config(
|
||||
alpn_protocols: Vec<String>,
|
||||
enable_sni: bool,
|
||||
client_key_and_cert: Option<(Vec<u8>, Vec<u8>)>,
|
||||
tls13_only: bool,
|
||||
) -> rustls::ClientConfig {
|
||||
let builder = rustls::ClientConfig::builder_with_provider(get_crypto_provider())
|
||||
.with_safe_default_protocol_versions()
|
||||
.unwrap();
|
||||
let builder = rustls::ClientConfig::builder_with_provider(get_crypto_provider());
|
||||
let builder = if tls13_only {
|
||||
builder
|
||||
.with_protocol_versions(&[&rustls::version::TLS13])
|
||||
.unwrap()
|
||||
} else {
|
||||
builder.with_safe_default_protocol_versions().unwrap()
|
||||
};
|
||||
|
||||
let builder = if verify_webpki {
|
||||
let webpki_verifier = rustls::client::WebPkiServerVerifier::builder_with_provider(
|
||||
@@ -49,11 +55,10 @@ pub fn create_client_config(
|
||||
|
||||
let mut config = match client_key_and_cert {
|
||||
Some((key_bytes, cert_bytes)) => {
|
||||
let certs = vec![
|
||||
rustls::pki_types::CertificateDer::from_pem_slice(&cert_bytes)
|
||||
.unwrap()
|
||||
.into_owned(),
|
||||
];
|
||||
// Parse all certificates from the PEM file (client cert + intermediates if any)
|
||||
let certs: Vec<_> = rustls::pki_types::CertificateDer::pem_slice_iter(&cert_bytes)
|
||||
.map(|cert| cert.unwrap().into_owned())
|
||||
.collect();
|
||||
|
||||
let privkey = rustls::pki_types::PrivateKeyDer::from_pem_slice(&key_bytes).unwrap();
|
||||
builder
|
||||
@@ -221,11 +226,15 @@ pub fn create_server_config(
|
||||
alpn_protocols: &[String],
|
||||
client_fingerprints: &[String],
|
||||
) -> rustls::ServerConfig {
|
||||
let certs = vec![
|
||||
rustls::pki_types::CertificateDer::from_pem_slice(cert_bytes)
|
||||
.unwrap()
|
||||
.into_owned(),
|
||||
];
|
||||
// Parse all certificates from the PEM file (server cert + intermediates)
|
||||
let certs: Vec<_> = rustls::pki_types::CertificateDer::pem_slice_iter(cert_bytes)
|
||||
.map(|cert| cert.unwrap().into_owned())
|
||||
.collect();
|
||||
|
||||
log::debug!(
|
||||
"TLS server config: loaded {} certificate(s) in chain",
|
||||
certs.len()
|
||||
);
|
||||
|
||||
let privkey = rustls::pki_types::PrivateKeyDer::from_pem_slice(key_bytes).unwrap();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
pub fn feed_rustls_server_connection(
|
||||
connection: &mut rustls::ServerConnection,
|
||||
data: &[u8],
|
||||
@@ -29,7 +29,7 @@ pub fn feed_rustls_server_connection(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
pub fn feed_rustls_client_connection(
|
||||
connection: &mut rustls::ClientConnection,
|
||||
data: &[u8],
|
||||
|
||||
@@ -4,8 +4,8 @@ mod shadow_tls_server_handler;
|
||||
mod shadow_tls_stream;
|
||||
|
||||
pub use shadow_tls_server_handler::{
|
||||
ParsedClientHello, ShadowTlsServerTarget, ShadowTlsServerTargetHandshake, read_client_hello,
|
||||
setup_shadowtls_server_stream,
|
||||
ParsedClientHello, ShadowTlsServerTarget, ShadowTlsServerTargetHandshake, parse_server_hello,
|
||||
read_client_hello, setup_shadowtls_server_stream,
|
||||
};
|
||||
|
||||
pub use shadow_tls_client_handler::ShadowTlsClientHandler;
|
||||
|
||||
@@ -3,21 +3,20 @@ use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use rand::RngCore;
|
||||
use tokio::io::AsyncWriteExt; // For write_all // For random bytes
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::address::NetLocation;
|
||||
use crate::async_stream::AsyncMessageStream;
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::buf_reader::BufReader;
|
||||
use crate::rustls_connection_util::feed_rustls_client_connection;
|
||||
use crate::shadow_tls::shadow_tls_hmac::ShadowTlsHmac;
|
||||
use crate::shadow_tls::shadow_tls_stream::ShadowTlsStream;
|
||||
use crate::stream_reader::StreamReader;
|
||||
use crate::tcp_handler::{
|
||||
TcpClientHandler, TcpClientSetupResult, TcpClientUdpSetupResult, UdpStreamRequest,
|
||||
};
|
||||
use crate::util::{allocate_vec, write_all}; // Assuming write_all is from crate::util
|
||||
use crate::tcp::tcp_handler::{TcpClientHandler, TcpClientSetupResult};
|
||||
use crate::util::{allocate_vec, write_all};
|
||||
|
||||
use super::shadow_tls_server_handler::parse_server_hello;
|
||||
use super::shadow_tls_server_handler::parse_validated_server_hello;
|
||||
|
||||
// Constants from shadow_tls_server_handler
|
||||
// TODO: deduplicate consts
|
||||
@@ -90,7 +89,7 @@ impl ShadowTlsClientHandler {
|
||||
let server_hello_frame =
|
||||
read_tls_frame(&mut remote_reader, &mut client_stream, &mut frame_buf).await?;
|
||||
|
||||
let parsed_server_hello = parse_server_hello(server_hello_frame)?;
|
||||
let parsed_server_hello = parse_validated_server_hello(server_hello_frame)?;
|
||||
|
||||
feed_rustls_client_connection(&mut client_conn, server_hello_frame)?;
|
||||
client_conn.process_new_packets().map_err(|e| {
|
||||
@@ -132,12 +131,11 @@ impl ShadowTlsClientHandler {
|
||||
let content_type = server_frame[0];
|
||||
|
||||
if content_type == CONTENT_TYPE_APPLICATION_DATA {
|
||||
// since we are still handshaking, this must be an encrypted TLS 1.3 handshake record from
|
||||
// the handshake server, so we expect it to pass the ServerRandom hmac check.
|
||||
// once we get here, we are done with the initial handshake and can break.
|
||||
// Encrypted TLS 1.3 handshake record from the handshake server.
|
||||
// Validates HMAC and then breaks from handshake loop.
|
||||
let payload_len = u16::from_be_bytes([server_frame[3], server_frame[4]]) as usize;
|
||||
if payload_len < 4 + 1 {
|
||||
// must be at least 4 for the hmac digest and non-empty data after it
|
||||
// Requires at least 4 bytes for HMAC digest plus non-empty data.
|
||||
// TODO: should this check for a larger record size?
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
@@ -184,7 +182,7 @@ impl ShadowTlsClientHandler {
|
||||
Some(hmac_server_random),
|
||||
)?;
|
||||
|
||||
// Feed any unconsumed data from StreamReader from handshake phase
|
||||
// Feeds any unconsumed data from StreamReader from handshake phase
|
||||
let unparsed_handshake_data = remote_reader.unparsed_data();
|
||||
if !unparsed_handshake_data.is_empty() {
|
||||
shadow_tls_stream.feed_initial_read_data(unparsed_handshake_data)?;
|
||||
@@ -211,14 +209,14 @@ impl TcpClientHandler for ShadowTlsClientHandler {
|
||||
self.handler.supports_udp_over_tcp()
|
||||
}
|
||||
|
||||
async fn setup_client_udp_stream(
|
||||
async fn setup_client_udp_bidirectional(
|
||||
&self,
|
||||
client_stream: Box<dyn AsyncStream>,
|
||||
request: UdpStreamRequest,
|
||||
) -> std::io::Result<TcpClientUdpSetupResult> {
|
||||
target: NetLocation,
|
||||
) -> std::io::Result<Box<dyn AsyncMessageStream>> {
|
||||
let shadow_tls_stream = self.setup_client_stream_common(client_stream).await?;
|
||||
self.handler
|
||||
.setup_client_udp_stream(Box::new(shadow_tls_stream), request)
|
||||
.setup_client_udp_bidirectional(Box::new(shadow_tls_stream), target)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -356,7 +354,6 @@ async fn read_tls_frame<'a>(
|
||||
|
||||
let payload_len = u16::from_be_bytes([header_bytes[3], header_bytes[4]]) as usize;
|
||||
if payload_len > TLS_FRAME_MAX_LEN - TLS_HEADER_LEN {
|
||||
// Check against max possible payload
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"TLS frame payload too large",
|
||||
|
||||
@@ -16,12 +16,10 @@ use crate::address::NetLocation;
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::buf_reader::BufReader;
|
||||
use crate::client_proxy_chain::ClientProxyChain;
|
||||
use crate::client_proxy_selector::ClientProxySelector;
|
||||
use crate::option_util::NoneOrOne;
|
||||
use crate::resolver::Resolver;
|
||||
use crate::rustls_connection_util::feed_rustls_server_connection;
|
||||
use crate::stream_reader::StreamReader;
|
||||
use crate::tcp_handler::{TcpServerHandler, TcpServerSetupResult};
|
||||
use crate::tcp::tcp_handler::{TcpClientSetupResult, TcpServerHandler, TcpServerSetupResult};
|
||||
use crate::util::{allocate_vec, write_all};
|
||||
|
||||
// context wrapper because it's not Debug
|
||||
@@ -39,7 +37,6 @@ pub struct ShadowTlsServerTarget {
|
||||
initial_xor_context: ShadowTlsXorContext,
|
||||
handshake: ShadowTlsServerTargetHandshake,
|
||||
handler: Box<dyn TcpServerHandler>,
|
||||
override_proxy_provider: NoneOrOne<Arc<ClientProxySelector>>,
|
||||
}
|
||||
|
||||
impl ShadowTlsServerTarget {
|
||||
@@ -47,7 +44,6 @@ impl ShadowTlsServerTarget {
|
||||
password: String,
|
||||
handshake: ShadowTlsServerTargetHandshake,
|
||||
handler: Box<dyn TcpServerHandler>,
|
||||
override_proxy_provider: NoneOrOne<Arc<ClientProxySelector>>,
|
||||
) -> Self {
|
||||
let password_bytes = password.into_bytes();
|
||||
let hmac_key = aws_lc_rs::hmac::Key::new(
|
||||
@@ -62,7 +58,6 @@ impl ShadowTlsServerTarget {
|
||||
initial_xor_context: ShadowTlsXorContext(initial_xor_context),
|
||||
handshake,
|
||||
handler,
|
||||
override_proxy_provider,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,6 +97,8 @@ const CONTENT_TYPE_APPLICATION_DATA: u8 = 0x17;
|
||||
const HANDSHAKE_TYPE_SERVER_HELLO: u8 = 0x02;
|
||||
const HANDSHAKE_TYPE_CLIENT_HELLO: u8 = 0x01;
|
||||
|
||||
const TLS_EXT_SUPPORTED_VERSIONS: u16 = 0x002b;
|
||||
|
||||
// retry request random value, see https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.3
|
||||
// TODO: should we also check to disallow TLS1.2/TLS1.1 client downgrade requests?
|
||||
const RETRY_REQUEST_RANDOM_BYTES: [u8; 32] = [
|
||||
@@ -109,42 +106,36 @@ const RETRY_REQUEST_RANDOM_BYTES: [u8; 32] = [
|
||||
0xC2, 0xA2, 0x11, 0x16, 0x7A, 0xBB, 0x8C, 0x5E, 0x07, 0x9E, 0x09, 0xE2, 0xC8, 0xA8, 0x33, 0x9C,
|
||||
];
|
||||
|
||||
#[inline]
|
||||
pub async fn setup_shadowtls_server_stream(
|
||||
server_stream: Box<dyn AsyncStream>,
|
||||
target: &ShadowTlsServerTarget,
|
||||
parsed_client_hello: ParsedClientHello,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
) -> std::io::Result<TcpServerSetupResult> {
|
||||
let ParsedClientHello {
|
||||
client_hello_frame,
|
||||
/// Validates the ClientHello for ShadowTLS authentication.
|
||||
/// Returns Ok(()) on success, or Err with PermissionDenied on auth failure.
|
||||
fn validate_shadowtls_client_hello(
|
||||
parsed_client_hello: &ParsedClientHello,
|
||||
initial_hmac: &ShadowTlsHmac,
|
||||
) -> std::io::Result<()> {
|
||||
let &ParsedClientHello {
|
||||
ref client_hello_frame,
|
||||
client_hello_record_legacy_version_major,
|
||||
client_hello_record_legacy_version_minor,
|
||||
client_hello_content_version_major,
|
||||
client_hello_content_version_minor,
|
||||
parsed_digest,
|
||||
client_reader,
|
||||
supports_tls13: client_supports_tls13,
|
||||
ref parsed_digest,
|
||||
supports_tls13,
|
||||
..
|
||||
} = parsed_client_hello;
|
||||
|
||||
let ParsedClientHelloDigest {
|
||||
client_hello_digest,
|
||||
client_hello_digest_start_index,
|
||||
client_hello_digest_end_index,
|
||||
} = match parsed_digest {
|
||||
let digest = match parsed_digest {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"client did not send a 32-byte session id",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if !client_supports_tls13 {
|
||||
if !supports_tls13 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"client does not support TLS1.3",
|
||||
));
|
||||
}
|
||||
@@ -153,36 +144,152 @@ pub async fn setup_shadowtls_server_stream(
|
||||
|| client_hello_record_legacy_version_minor != 1
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
format!(
|
||||
"expected client TLS record protocol 1.0 (major/minor 3.1), got major/minor {client_hello_record_legacy_version_major}.{client_hello_record_legacy_version_minor}"
|
||||
"expected client TLS record protocol 1.0 (major/minor 3.1), got major/minor {}.{}",
|
||||
client_hello_record_legacy_version_major, client_hello_record_legacy_version_minor
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if client_hello_content_version_major != 3 || client_hello_content_version_minor != 3 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
format!(
|
||||
"expected client TLS content protocol 1.2 (major/minor 3.3), got major/minor {client_hello_content_version_major}.{client_hello_content_version_minor}"
|
||||
"expected client TLS content protocol 1.2 (major/minor 3.3), got major/minor {}.{}",
|
||||
client_hello_content_version_major, client_hello_content_version_minor
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// verify the hmac digest
|
||||
let mut hmac_client_hello = target.initial_hmac.clone();
|
||||
hmac_client_hello.update(&client_hello_frame[TLS_HEADER_LEN..client_hello_digest_start_index]);
|
||||
hmac_client_hello.update(&[0; 4]);
|
||||
hmac_client_hello.update(&client_hello_frame[client_hello_digest_end_index..]);
|
||||
let mut hmac = initial_hmac.clone();
|
||||
hmac.update(&client_hello_frame[TLS_HEADER_LEN..digest.client_hello_digest_start_index]);
|
||||
hmac.update(&[0; 4]);
|
||||
hmac.update(&client_hello_frame[digest.client_hello_digest_end_index..]);
|
||||
|
||||
if client_hello_digest != hmac_client_hello.finalized_digest() {
|
||||
// TODO: forward to handshake server
|
||||
if digest.client_hello_digest != hmac.finalized_digest() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"hmac tag mismatch",
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"HMAC tag mismatch",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fallback mechanism for ShadowTLS authentication failures.
|
||||
///
|
||||
/// When a client fails ShadowTLS authentication (invalid HMAC, wrong TLS version,
|
||||
/// missing session ID, etc.), instead of dropping the connection, we transparently
|
||||
/// forward it to the configured handshake server. This makes the server
|
||||
/// indistinguishable from a legitimate SNI proxy, defeating active probing attacks.
|
||||
async fn shadowtls_fallback_to_handshake_server(
|
||||
mut client_stream: Box<dyn AsyncStream>,
|
||||
client_hello_bytes: &[u8],
|
||||
location: &NetLocation,
|
||||
client_chain: &ClientProxyChain,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
) -> std::io::Result<TcpServerSetupResult> {
|
||||
log::debug!(
|
||||
"SHADOWTLS FALLBACK: Connecting to handshake server: {}",
|
||||
location
|
||||
);
|
||||
|
||||
let TcpClientSetupResult {
|
||||
client_stream: mut handshake_stream,
|
||||
early_data,
|
||||
} = client_chain
|
||||
.connect_tcp(location.clone(), resolver)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::ConnectionRefused,
|
||||
format!("SHADOWTLS FALLBACK: Failed to connect to handshake server: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
// early_data should never be present since this is a TLS server waiting for a ClientHello
|
||||
debug_assert!(
|
||||
early_data.is_none(),
|
||||
"unexpected early_data from handshake server connection"
|
||||
);
|
||||
|
||||
log::debug!(
|
||||
"SHADOWTLS FALLBACK: Connected, forwarding ClientHello ({} bytes)",
|
||||
client_hello_bytes.len()
|
||||
);
|
||||
|
||||
write_all(&mut handshake_stream, client_hello_bytes).await?;
|
||||
handshake_stream.flush().await?;
|
||||
|
||||
log::debug!("SHADOWTLS FALLBACK: ClientHello forwarded, spawning bidirectional copy");
|
||||
|
||||
// Spawn the long-running bidirectional copy as a background task.
|
||||
// This allows the setup to complete within the timeout while the actual
|
||||
// data transfer runs indefinitely.
|
||||
tokio::spawn(async move {
|
||||
let result = crate::copy_bidirectional::copy_bidirectional(
|
||||
&mut *client_stream,
|
||||
&mut *handshake_stream,
|
||||
false, // client doesn't need initial flush
|
||||
false, // handshake server doesn't need initial flush
|
||||
)
|
||||
.await;
|
||||
|
||||
let _ = client_stream.shutdown().await;
|
||||
let _ = handshake_stream.shutdown().await;
|
||||
|
||||
if let Err(e) = result {
|
||||
log::debug!("SHADOWTLS FALLBACK: Connection ended: {}", e);
|
||||
} else {
|
||||
log::debug!("SHADOWTLS FALLBACK: Connection completed");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(TcpServerSetupResult::AlreadyHandled)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn setup_shadowtls_server_stream(
|
||||
server_stream: Box<dyn AsyncStream>,
|
||||
target: &ShadowTlsServerTarget,
|
||||
parsed_client_hello: ParsedClientHello,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
) -> std::io::Result<TcpServerSetupResult> {
|
||||
// Validates ClientHello before consuming anything to allow fallback if needed.
|
||||
if let Err(e) = validate_shadowtls_client_hello(&parsed_client_hello, &target.initial_hmac) {
|
||||
if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
// Falls back to handshake server in Remote mode for auth failures.
|
||||
if let ShadowTlsServerTargetHandshake::Remote {
|
||||
ref location,
|
||||
ref client_chain,
|
||||
} = target.handshake
|
||||
{
|
||||
log::warn!(
|
||||
"ShadowTLS authentication failed, falling back to handshake server: {} - reason: {}",
|
||||
location,
|
||||
e
|
||||
);
|
||||
return shadowtls_fallback_to_handshake_server(
|
||||
server_stream,
|
||||
&parsed_client_hello.client_hello_frame,
|
||||
location,
|
||||
client_chain,
|
||||
resolver,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
// Local mode or non-auth error: propagate the error
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
let ParsedClientHello {
|
||||
client_hello_frame,
|
||||
client_reader,
|
||||
..
|
||||
} = parsed_client_hello;
|
||||
|
||||
let shadow_tls_stream = match target.handshake {
|
||||
ShadowTlsServerTargetHandshake::Remote {
|
||||
ref location,
|
||||
@@ -211,7 +318,7 @@ pub async fn setup_shadowtls_server_stream(
|
||||
.map_err(|e| std::io::Error::other(format!("failed to setup local handshake: {e}")))?,
|
||||
};
|
||||
|
||||
let mut target_setup_result = target
|
||||
let target_setup_result = target
|
||||
.handler
|
||||
.setup_server_stream(Box::new(shadow_tls_stream))
|
||||
.await
|
||||
@@ -221,14 +328,12 @@ pub async fn setup_shadowtls_server_stream(
|
||||
))
|
||||
});
|
||||
|
||||
if let Ok(ref mut setup_result) = target_setup_result {
|
||||
// TODO: do we need initial flush?
|
||||
if setup_result.override_proxy_provider_unspecified()
|
||||
&& !target.override_proxy_provider.is_unspecified()
|
||||
{
|
||||
setup_result.set_override_proxy_provider(target.override_proxy_provider.clone());
|
||||
}
|
||||
if let Ok(ref setup_result) = target_setup_result
|
||||
&& matches!(setup_result, TcpServerSetupResult::AlreadyHandled)
|
||||
{
|
||||
return target_setup_result;
|
||||
}
|
||||
// Inner handler already has effective_selector from construction
|
||||
|
||||
target_setup_result
|
||||
}
|
||||
@@ -255,10 +360,9 @@ pub struct ParsedClientHelloDigest {
|
||||
pub async fn read_client_hello(
|
||||
server_stream: &mut Box<dyn AsyncStream>,
|
||||
) -> std::io::Result<ParsedClientHello> {
|
||||
// enough for tls header + a max tls payload
|
||||
let mut client_reader = StreamReader::new_with_buffer_size(TLS_FRAME_MAX_LEN);
|
||||
|
||||
// read the first tls frame, allocate so that we can borrow and use the payload below
|
||||
// Allocates to allow borrowing the payload below.
|
||||
let client_tls_header_bytes = client_reader
|
||||
.read_slice(server_stream, TLS_HEADER_LEN)
|
||||
.await?
|
||||
@@ -311,15 +415,14 @@ pub async fn read_client_hello(
|
||||
));
|
||||
}
|
||||
|
||||
// skip client random
|
||||
client_hello.skip(32)?;
|
||||
client_hello.skip(32)?; // client random
|
||||
|
||||
let client_session_id_len = client_hello.read_u8()?;
|
||||
|
||||
let parsed_digest = if client_session_id_len == 32 {
|
||||
let client_session_id = client_hello.read_slice(32)?;
|
||||
|
||||
// save the hmac digest and session id position for validation once we know the server name
|
||||
// Saves HMAC digest and session ID position for later validation.
|
||||
let client_hello_digest = client_session_id[28..].to_vec();
|
||||
let post_session_id_index = client_hello.position();
|
||||
|
||||
@@ -357,7 +460,7 @@ pub async fn read_client_hello(
|
||||
let extension_len = client_extensions.read_u16_be()? as usize;
|
||||
|
||||
if extension_type == 0x0000 {
|
||||
// server name
|
||||
// server_name
|
||||
if requested_server_name.is_some() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
@@ -377,7 +480,7 @@ pub async fn read_client_hello(
|
||||
let server_name_str = client_extensions.read_str(server_name_len as usize)?;
|
||||
requested_server_name = Some(server_name_str.to_string());
|
||||
} else if extension_type == 0x002b {
|
||||
// supported versions
|
||||
// supported_versions
|
||||
let version_list_len = client_extensions.read_u8()?;
|
||||
if version_list_len % 2 != 0 {
|
||||
return Err(std::io::Error::new(
|
||||
@@ -419,158 +522,158 @@ pub async fn read_client_hello(
|
||||
|
||||
pub struct ParsedServerHello {
|
||||
pub server_random: Vec<u8>,
|
||||
pub cipher_suite: u16,
|
||||
pub session_id_len: u8,
|
||||
pub is_tls13: bool,
|
||||
}
|
||||
|
||||
/// Parses a ServerHello frame and extracts relevant fields.
|
||||
/// This is a generic parser that can be used by multiple protocols (ShadowTLS, Vision).
|
||||
/// It performs strict validation on structure but is lenient on TLS version requirements.
|
||||
/// Use `parse_validated_server_hello` for ShadowTLS-specific validation.
|
||||
pub fn parse_server_hello(server_hello_frame: &[u8]) -> std::io::Result<ParsedServerHello> {
|
||||
// we don't need to validate that the frame is large enough to contain the header, because
|
||||
// a full header was read in order to successfully read the complete frame that is passed in
|
||||
// to this function.
|
||||
let server_content_type = server_hello_frame[0];
|
||||
if server_content_type != CONTENT_TYPE_HANDSHAKE {
|
||||
// Minimum size when session_id_len=0 and no extensions:
|
||||
// 5 (record header) + 4 (handshake header) + 2 (version) + 32 (random)
|
||||
// + 1 (session_id_len byte) + 2 (cipher) + 1 (compression) = 47
|
||||
if server_hello_frame.len() < 47 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"expected server handshake",
|
||||
"ServerHello frame too short",
|
||||
));
|
||||
}
|
||||
|
||||
let server_legacy_version_major = server_hello_frame[1];
|
||||
let server_legacy_version_minor = server_hello_frame[2];
|
||||
if server_legacy_version_major != 3 || server_legacy_version_minor != 3 {
|
||||
let content_type = server_hello_frame[0];
|
||||
if content_type != CONTENT_TYPE_HANDSHAKE {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"unexpected server TLS version {server_legacy_version_major}.{server_legacy_version_minor}"
|
||||
),
|
||||
"expected handshake content type",
|
||||
));
|
||||
}
|
||||
|
||||
// we don't need to validate the frame payload length, because this value was used to
|
||||
// read the complete frame that is passed in to this function.
|
||||
let server_payload_len =
|
||||
u16::from_be_bytes([server_hello_frame[3], server_hello_frame[4]]) as usize;
|
||||
|
||||
let server_handshake_type = server_hello_frame[5];
|
||||
if server_handshake_type != HANDSHAKE_TYPE_SERVER_HELLO {
|
||||
let record_version_major = server_hello_frame[1];
|
||||
let record_version_minor = server_hello_frame[2];
|
||||
if record_version_major != 3 || record_version_minor != 3 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"expected ServerHello",
|
||||
format!("unexpected record TLS version {record_version_major}.{record_version_minor}"),
|
||||
));
|
||||
}
|
||||
|
||||
let server_hello_message_len = u32::from_be_bytes([
|
||||
0,
|
||||
server_hello_frame[6],
|
||||
server_hello_frame[7],
|
||||
server_hello_frame[8],
|
||||
]) as usize;
|
||||
let mut reader = BufReader::new(&server_hello_frame[TLS_HEADER_LEN..]);
|
||||
|
||||
// this should be 4 bytes less than the payload length (handshake type + 3 bytes length)
|
||||
if server_hello_message_len + 4 != server_payload_len {
|
||||
let handshake_type = reader.read_u8()?;
|
||||
if handshake_type != HANDSHAKE_TYPE_SERVER_HELLO {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"server hello message length mismatch",
|
||||
"expected ServerHello handshake type",
|
||||
));
|
||||
}
|
||||
|
||||
let server_version_major = server_hello_frame[9];
|
||||
let server_version_minor = server_hello_frame[10];
|
||||
if server_version_major != 3 || server_version_minor != 3 {
|
||||
let message_len = reader.read_u24_be()? as usize;
|
||||
if reader.remaining() < message_len {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"expected TLS 1.2 (major/minor 3.3), got major/minor {server_version_major}.{server_version_minor}"
|
||||
),
|
||||
"ServerHello message length exceeds frame",
|
||||
));
|
||||
}
|
||||
|
||||
let server_random = &server_hello_frame[11..43];
|
||||
// Legacy version (should be 0x0303 for TLS 1.2/1.3)
|
||||
let version_major = reader.read_u8()?;
|
||||
let version_minor = reader.read_u8()?;
|
||||
if version_major != 3 || version_minor != 3 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("expected TLS version 3.3, got {version_major}.{version_minor}"),
|
||||
));
|
||||
}
|
||||
|
||||
let server_random = reader.read_slice(32)?.to_vec();
|
||||
if server_random == RETRY_REQUEST_RANDOM_BYTES {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"server sent a HelloRetryRequest",
|
||||
));
|
||||
}
|
||||
let server_random = server_random.to_vec();
|
||||
|
||||
let server_session_id_len = server_hello_frame[43];
|
||||
if server_session_id_len != 32 {
|
||||
// Session ID (variable length, 0-32 bytes)
|
||||
let session_id_len = reader.read_u8()?;
|
||||
if session_id_len > 32 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("expected session id len 32, got {server_session_id_len}"),
|
||||
format!("invalid session_id_len {session_id_len}, max is 32"),
|
||||
));
|
||||
}
|
||||
reader.skip(session_id_len as usize)?;
|
||||
|
||||
// skip unused fields:
|
||||
// let _server_session_id = &server_hello_frame[44..76];
|
||||
// let _server_selected_cipher_suite =
|
||||
// u16::from_be_bytes([server_hello_frame[76], server_hello_frame[77]]);
|
||||
// let _server_compression_method = server_hello_frame[78];
|
||||
|
||||
// this length needs to be validated because it is unchecked when reading the complete
|
||||
// frame that is passed in to this function.
|
||||
let server_extensions_len =
|
||||
u16::from_be_bytes([server_hello_frame[79], server_hello_frame[80]]) as usize;
|
||||
if server_hello_frame.len() < 81 + server_extensions_len {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"server hello message too short for extensions",
|
||||
));
|
||||
}
|
||||
|
||||
let server_extension_bytes = &server_hello_frame[81..81 + server_extensions_len];
|
||||
|
||||
let mut server_extensions = BufReader::new(server_extension_bytes);
|
||||
let mut server_has_supported_version = false;
|
||||
while !server_extensions.is_consumed() {
|
||||
let extension_type = server_extensions.read_u16_be().map_err(|e| {
|
||||
std::io::Error::new(
|
||||
let cipher_suite = reader.read_u16_be()?;
|
||||
reader.skip(1)?; // compression method
|
||||
let mut is_tls13 = false;
|
||||
if !reader.is_consumed() {
|
||||
let extensions_len = reader.read_u16_be()? as usize;
|
||||
if reader.remaining() < extensions_len {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("failed to read extension type from ServerHello: {e}"),
|
||||
)
|
||||
})?;
|
||||
let extension_len = server_extensions.read_u16_be().map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("failed to read extension length from ServerHello: {e}"),
|
||||
)
|
||||
})? as usize;
|
||||
"extensions length exceeds remaining data",
|
||||
));
|
||||
}
|
||||
|
||||
if extension_type == 0x002b {
|
||||
// supported versions
|
||||
let version_bytes = server_extensions.read_slice(2).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("failed to read supported version from ServerHello: {e}"),
|
||||
)
|
||||
})?;
|
||||
if version_bytes[0] != 3 || version_bytes[1] != 4 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"expected server supported version to be TLS 1.3 (0x0304), got 0x{:02x}{:02x}",
|
||||
version_bytes[0], version_bytes[1]
|
||||
),
|
||||
));
|
||||
let extensions_data = reader.read_slice(extensions_len)?;
|
||||
let mut ext_reader = BufReader::new(extensions_data);
|
||||
|
||||
while !ext_reader.is_consumed() {
|
||||
let ext_type = ext_reader.read_u16_be()?;
|
||||
let ext_len = ext_reader.read_u16_be()?;
|
||||
|
||||
if ext_type == TLS_EXT_SUPPORTED_VERSIONS {
|
||||
// In ServerHello, supported_versions is exactly 2 bytes (single selected version).
|
||||
if ext_len != 2 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("supported_versions extension should be 2 bytes, got {ext_len}"),
|
||||
));
|
||||
}
|
||||
let version_bytes = ext_reader.read_slice(2)?;
|
||||
is_tls13 = version_bytes[0] == 0x03 && version_bytes[1] == 0x04; // TLS 1.3
|
||||
} else {
|
||||
ext_reader.skip(ext_len as usize)?;
|
||||
}
|
||||
server_has_supported_version = true;
|
||||
} else {
|
||||
server_extensions.skip(extension_len).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("failed to skip extension in ServerHello: {e}"),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
if !server_has_supported_version {
|
||||
Ok(ParsedServerHello {
|
||||
server_random,
|
||||
cipher_suite,
|
||||
session_id_len,
|
||||
is_tls13,
|
||||
})
|
||||
}
|
||||
|
||||
/// ShadowTLS-specific ServerHello parser with additional validation.
|
||||
/// Requires TLS 1.3 and 32-byte session_id.
|
||||
pub fn parse_validated_server_hello(
|
||||
server_hello_frame: &[u8],
|
||||
) -> std::io::Result<ParsedServerHello> {
|
||||
let parsed = parse_server_hello(server_hello_frame)?;
|
||||
|
||||
if !parsed.is_tls13 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"server did not have supported versions extension",
|
||||
"ShadowTLS requires TLS 1.3 (missing or invalid supported_versions extension)",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ParsedServerHello { server_random })
|
||||
// We sent a 32 byte session ID so this should never happen
|
||||
if parsed.session_id_len != 32 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"ShadowTLS expects session_id_len 32, got {}",
|
||||
parsed.session_id_len
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -585,9 +688,9 @@ async fn setup_remote_handshake(
|
||||
client_chain: &ClientProxyChain,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
) -> std::io::Result<ShadowTlsStream> {
|
||||
use crate::tcp_handler::TcpClientSetupResult;
|
||||
use crate::tcp::tcp_handler::TcpClientSetupResult;
|
||||
|
||||
// this is confusing, but the TLS server is called client_stream.
|
||||
// The TLS handshake server is called client_stream (we are a client to it).
|
||||
let TcpClientSetupResult {
|
||||
mut client_stream,
|
||||
early_data: _,
|
||||
@@ -646,15 +749,14 @@ async fn setup_remote_handshake(
|
||||
})?;
|
||||
server_hello_frame.extend_from_slice(server_payload_bytes);
|
||||
|
||||
let ParsedServerHello { server_random } =
|
||||
parse_server_hello(&server_hello_frame).map_err(|e| {
|
||||
let ParsedServerHello { server_random, .. } = parse_validated_server_hello(&server_hello_frame)
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("failed to parse ServerHello from remote server: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
// write the server hello frame to the client
|
||||
write_all(&mut server_stream, &server_hello_frame)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -872,13 +974,11 @@ async fn setup_local_handshake(
|
||||
|
||||
let server_hello_frame = &server_data[0..TLS_HEADER_LEN + server_hello_payload_size];
|
||||
|
||||
let ParsedServerHello { server_random } = parse_server_hello(server_hello_frame)?;
|
||||
let ParsedServerHello { server_random, .. } = parse_validated_server_hello(server_hello_frame)?;
|
||||
|
||||
// write the server hello frame to the client
|
||||
write_all(&mut server_stream, server_hello_frame).await?;
|
||||
|
||||
// the server sends multiple frames after ServerHello, make sure we process the remaining
|
||||
// data
|
||||
// The server sends multiple frames after ServerHello; process remaining data.
|
||||
let remaining_server_data_len =
|
||||
server_data_len - TLS_HEADER_LEN - server_hello_payload_size as usize;
|
||||
if remaining_server_data_len > 0 {
|
||||
@@ -905,10 +1005,8 @@ async fn setup_local_handshake(
|
||||
key_context.finish().as_ref().to_vec()
|
||||
};
|
||||
|
||||
// copy bidirectionally until we find a matching hmac at the front of
|
||||
// an application data frame
|
||||
// Copies bidirectionally until finding a matching HMAC at the front of an app data frame.
|
||||
loop {
|
||||
// server write loop
|
||||
loop {
|
||||
if server_data_end_index < TLS_HEADER_LEN {
|
||||
if server_connection.wants_write() {
|
||||
@@ -936,7 +1034,6 @@ async fn setup_local_handshake(
|
||||
let server_payload_size = u16::from_be_bytes([server_data[3], server_data[4]]) as usize;
|
||||
|
||||
if server_data_end_index < TLS_HEADER_LEN + server_payload_size {
|
||||
// not enough for a complete frame.
|
||||
if server_connection.wants_write() {
|
||||
let server_data_len = read_server_connection_once(
|
||||
&mut server_connection,
|
||||
@@ -949,15 +1046,13 @@ async fn setup_local_handshake(
|
||||
}
|
||||
|
||||
if server_content_type == CONTENT_TYPE_APPLICATION_DATA {
|
||||
// make sure there's enough space for the digest
|
||||
if server_payload_size > TLS_FRAME_MAX_LEN - 4 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"server payload too large to modify",
|
||||
));
|
||||
}
|
||||
// we need to modify the frame and push all the following frames back by 4
|
||||
// bytes as well
|
||||
// Modifying frame requires shifting all following frames back by 4 bytes.
|
||||
if server_data_end_index > TLS_FRAME_MAX_LEN + TLS_HEADER_LEN - 4 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
@@ -976,18 +1071,15 @@ async fn setup_local_handshake(
|
||||
*byte ^= key;
|
||||
}
|
||||
|
||||
// make space for the hmac digest
|
||||
server_data.copy_within(TLS_HEADER_LEN..server_data_end_index, TLS_HEADER_LEN + 4);
|
||||
server_data_end_index += 4;
|
||||
|
||||
// calculate the digest and place it at the front of the payload
|
||||
hmac_server_random.update(
|
||||
&server_data[TLS_HEADER_LEN + 4..TLS_HEADER_LEN + 4 + server_payload_size],
|
||||
);
|
||||
server_data[TLS_HEADER_LEN..TLS_HEADER_LEN + 4]
|
||||
.copy_from_slice(&hmac_server_random.digest());
|
||||
|
||||
// update the payload size
|
||||
let updated_payload_size = (server_payload_size as u16).wrapping_add(4);
|
||||
server_data[3..5].copy_from_slice(&updated_payload_size.to_be_bytes());
|
||||
|
||||
@@ -1042,7 +1134,7 @@ async fn setup_local_handshake(
|
||||
None,
|
||||
)?;
|
||||
|
||||
// Feed any leftover data from the reader to the stream
|
||||
// Feeds any leftover data from the reader to the stream.
|
||||
let leftover = client_reader.unparsed_data();
|
||||
if !leftover.is_empty() {
|
||||
shadow_tls_stream.feed_initial_read_data(leftover)?;
|
||||
|
||||
@@ -8,14 +8,15 @@ use tokio::io::AsyncWriteExt;
|
||||
use super::salt_checker::SaltChecker;
|
||||
use super::timed_salt_checker::TimedSaltChecker;
|
||||
use crate::address::{Address, NetLocation};
|
||||
use crate::async_stream::AsyncMessageStream;
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::option_util::NoneOrOne;
|
||||
use crate::client_proxy_selector::ClientProxySelector;
|
||||
use crate::socks_handler::{read_location, write_location_to_vec};
|
||||
use crate::stream_reader::StreamReader;
|
||||
use crate::tcp_handler::{
|
||||
use crate::tcp::tcp_handler::{
|
||||
TcpClientHandler, TcpClientSetupResult, TcpServerHandler, TcpServerSetupResult,
|
||||
};
|
||||
use crate::uot::{UOT_V1_MAGIC_ADDRESS, UOT_V2_MAGIC_ADDRESS, UotV1Stream, UotV2Stream};
|
||||
use crate::uot::{UOT_V1_MAGIC_ADDRESS, UOT_V2_MAGIC_ADDRESS, UotV1ServerStream, UotV2Stream};
|
||||
use crate::util::write_all;
|
||||
|
||||
use super::blake3_key::Blake3Key;
|
||||
@@ -31,10 +32,19 @@ pub struct ShadowsocksTcpHandler {
|
||||
key: Arc<Box<dyn ShadowsocksKey>>,
|
||||
aead2022: bool,
|
||||
salt_checker: Option<Arc<Mutex<dyn SaltChecker>>>,
|
||||
udp_enabled: bool,
|
||||
/// Proxy selector for server handler use. None when used as client handler.
|
||||
proxy_selector: Option<Arc<ClientProxySelector>>,
|
||||
}
|
||||
|
||||
impl ShadowsocksTcpHandler {
|
||||
pub fn new(cipher: ShadowsocksCipher, password: &str) -> Self {
|
||||
/// Create a new handler for server use (with proxy_selector for routing)
|
||||
pub fn new_server(
|
||||
cipher: ShadowsocksCipher,
|
||||
password: &str,
|
||||
udp_enabled: bool,
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
) -> Self {
|
||||
let key: Arc<Box<dyn ShadowsocksKey>> = Arc::new(Box::new(DefaultKey::new(
|
||||
password,
|
||||
cipher.algorithm().key_len(),
|
||||
@@ -44,10 +54,34 @@ impl ShadowsocksTcpHandler {
|
||||
key,
|
||||
aead2022: false,
|
||||
salt_checker: None,
|
||||
udp_enabled,
|
||||
proxy_selector: Some(proxy_selector),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_aead2022(cipher: ShadowsocksCipher, key_bytes: &[u8]) -> Self {
|
||||
/// Create a new handler for client use (no proxy_selector needed)
|
||||
pub fn new_client(cipher: ShadowsocksCipher, password: &str, udp_enabled: bool) -> Self {
|
||||
let key: Arc<Box<dyn ShadowsocksKey>> = Arc::new(Box::new(DefaultKey::new(
|
||||
password,
|
||||
cipher.algorithm().key_len(),
|
||||
)));
|
||||
Self {
|
||||
cipher,
|
||||
key,
|
||||
aead2022: false,
|
||||
salt_checker: None,
|
||||
udp_enabled,
|
||||
proxy_selector: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new AEAD2022 handler for server use
|
||||
pub fn new_aead2022_server(
|
||||
cipher: ShadowsocksCipher,
|
||||
key_bytes: &[u8],
|
||||
udp_enabled: bool,
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
) -> Self {
|
||||
let key: Arc<Box<dyn ShadowsocksKey>> = Arc::new(Box::new(Blake3Key::new(
|
||||
key_bytes.to_vec().into_boxed_slice(),
|
||||
cipher.algorithm().key_len(),
|
||||
@@ -57,6 +91,28 @@ impl ShadowsocksTcpHandler {
|
||||
key,
|
||||
aead2022: true,
|
||||
salt_checker: Some(Arc::new(Mutex::new(TimedSaltChecker::new(60)))),
|
||||
udp_enabled,
|
||||
proxy_selector: Some(proxy_selector),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new AEAD2022 handler for client use
|
||||
pub fn new_aead2022_client(
|
||||
cipher: ShadowsocksCipher,
|
||||
key_bytes: &[u8],
|
||||
udp_enabled: bool,
|
||||
) -> Self {
|
||||
let key: Arc<Box<dyn ShadowsocksKey>> = Arc::new(Box::new(Blake3Key::new(
|
||||
key_bytes.to_vec().into_boxed_slice(),
|
||||
cipher.algorithm().key_len(),
|
||||
)));
|
||||
Self {
|
||||
cipher,
|
||||
key,
|
||||
aead2022: true,
|
||||
salt_checker: Some(Arc::new(Mutex::new(TimedSaltChecker::new(60)))),
|
||||
udp_enabled,
|
||||
proxy_selector: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,8 +140,7 @@ impl TcpServerHandler for ShadowsocksTcpHandler {
|
||||
|
||||
let mut stream_reader = StreamReader::new_with_buffer_size(1024);
|
||||
|
||||
// We can do this in a blocking manner for the server, because we expect the client to
|
||||
// always send the location before we send anything.
|
||||
// Blocks waiting for the location since the client always sends it before expecting a response.
|
||||
let remote_location = read_location(&mut server_stream, &mut stream_reader).await?;
|
||||
|
||||
if self.aead2022 {
|
||||
@@ -104,14 +159,20 @@ impl TcpServerHandler for ShadowsocksTcpHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for UDP-over-TCP (UoT) magic addresses
|
||||
// Checks for UDP-over-TCP (UoT) magic addresses
|
||||
if let Address::Hostname(host) = remote_location.address() {
|
||||
if !self.udp_enabled && (host == UOT_V1_MAGIC_ADDRESS || host == UOT_V2_MAGIC_ADDRESS) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"UDP-over-TCP is disabled for this Shadowsocks server",
|
||||
));
|
||||
}
|
||||
if host == UOT_V1_MAGIC_ADDRESS {
|
||||
// UoT V1: Multi-destination UDP
|
||||
// Each packet has: ATYP + address + port + length + data
|
||||
let mut uot_stream = UotV1Stream::new(Box::new(server_stream));
|
||||
let mut uot_stream = UotV1ServerStream::new(server_stream);
|
||||
|
||||
// Feed any unparsed data (first UoT packet might be in same TCP segment)
|
||||
// Feeds unparsed data since first UoT packet might be in same TCP segment
|
||||
let unparsed_data = stream_reader.unparsed_data();
|
||||
if !unparsed_data.is_empty() {
|
||||
log::debug!(
|
||||
@@ -124,7 +185,10 @@ impl TcpServerHandler for ShadowsocksTcpHandler {
|
||||
return Ok(TcpServerSetupResult::MultiDirectionalUdp {
|
||||
stream: Box::new(uot_stream),
|
||||
need_initial_flush: false,
|
||||
override_proxy_provider: NoneOrOne::Unspecified,
|
||||
proxy_selector: self
|
||||
.proxy_selector
|
||||
.clone()
|
||||
.expect("proxy_selector required for server handler"),
|
||||
});
|
||||
} else if host == UOT_V2_MAGIC_ADDRESS {
|
||||
// UoT V2: Read request header first
|
||||
@@ -134,7 +198,7 @@ impl TcpServerHandler for ShadowsocksTcpHandler {
|
||||
let is_connect = stream_reader.read_u8(&mut server_stream).await?;
|
||||
log::debug!("Shadowsocks UoT V2: is_connect = {}", is_connect);
|
||||
|
||||
// Read destination address using SOCKS address format
|
||||
// Reads destination address using SOCKS address format
|
||||
let destination = read_location(&mut server_stream, &mut stream_reader).await?;
|
||||
log::debug!("Shadowsocks UoT V2: destination = {:?}", destination);
|
||||
|
||||
@@ -142,7 +206,7 @@ impl TcpServerHandler for ShadowsocksTcpHandler {
|
||||
// V2 Connect mode: Single destination, length-prefixed packets only
|
||||
// Reuse UotV2Stream which has identical format: length(u16be) + data
|
||||
let unparsed_data = stream_reader.unparsed_data();
|
||||
let mut uot_v2_stream = UotV2Stream::new(Box::new(server_stream));
|
||||
let mut uot_v2_stream = UotV2Stream::new(server_stream);
|
||||
if !unparsed_data.is_empty() {
|
||||
uot_v2_stream.feed_initial_read_data(unparsed_data)?;
|
||||
}
|
||||
@@ -151,13 +215,14 @@ impl TcpServerHandler for ShadowsocksTcpHandler {
|
||||
remote_location: destination,
|
||||
stream: Box::new(uot_v2_stream),
|
||||
need_initial_flush: false,
|
||||
override_proxy_provider: NoneOrOne::Unspecified,
|
||||
proxy_selector: self
|
||||
.proxy_selector
|
||||
.clone()
|
||||
.expect("proxy_selector required for server handler"),
|
||||
});
|
||||
} else {
|
||||
// V2 Non-connect mode: Same as V1 (multi-destination)
|
||||
let mut uot_stream = UotV1Stream::new(Box::new(server_stream));
|
||||
|
||||
// Feed any unparsed data
|
||||
let mut uot_stream = UotV1ServerStream::new(server_stream);
|
||||
let unparsed_data = stream_reader.unparsed_data();
|
||||
if !unparsed_data.is_empty() {
|
||||
log::debug!(
|
||||
@@ -170,7 +235,10 @@ impl TcpServerHandler for ShadowsocksTcpHandler {
|
||||
return Ok(TcpServerSetupResult::MultiDirectionalUdp {
|
||||
stream: Box::new(uot_stream),
|
||||
need_initial_flush: false,
|
||||
override_proxy_provider: NoneOrOne::Unspecified,
|
||||
proxy_selector: self
|
||||
.proxy_selector
|
||||
.clone()
|
||||
.expect("proxy_selector required for server handler"),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -179,11 +247,14 @@ impl TcpServerHandler for ShadowsocksTcpHandler {
|
||||
Ok(TcpServerSetupResult::TcpForward {
|
||||
remote_location,
|
||||
stream: Box::new(server_stream),
|
||||
// we don't need an initial flush, let the IV be written when data actually arrives.
|
||||
// Lets the IV be written when data actually arrives rather than flushing here.
|
||||
need_initial_flush: false,
|
||||
connection_success_response: None,
|
||||
initial_remote_data: stream_reader.unparsed_data_owned(),
|
||||
override_proxy_provider: NoneOrOne::Unspecified,
|
||||
proxy_selector: self
|
||||
.proxy_selector
|
||||
.clone()
|
||||
.expect("proxy_selector required for server handler"),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -233,4 +304,61 @@ impl TcpClientHandler for ShadowsocksTcpHandler {
|
||||
early_data: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn supports_udp_over_tcp(&self) -> bool {
|
||||
self.udp_enabled
|
||||
}
|
||||
|
||||
async fn setup_client_udp_bidirectional(
|
||||
&self,
|
||||
client_stream: Box<dyn AsyncStream>,
|
||||
target: NetLocation,
|
||||
) -> std::io::Result<Box<dyn AsyncMessageStream>> {
|
||||
use crate::uot::{UOT_V2_MAGIC_ADDRESS, UotV2Stream};
|
||||
|
||||
let stream_type = if self.aead2022 {
|
||||
ShadowsocksStreamType::AEAD2022Client
|
||||
} else {
|
||||
ShadowsocksStreamType::Aead
|
||||
};
|
||||
|
||||
let mut client_stream: Box<dyn AsyncStream> = Box::new(ShadowsocksStream::new(
|
||||
client_stream,
|
||||
stream_type,
|
||||
self.cipher.algorithm(),
|
||||
self.cipher.salt_len(),
|
||||
self.key.clone(),
|
||||
self.salt_checker.clone(),
|
||||
));
|
||||
|
||||
// UoT V2 connect mode: Single destination. Writes magic address first.
|
||||
let magic_location =
|
||||
NetLocation::new(Address::Hostname(UOT_V2_MAGIC_ADDRESS.to_string()), 0);
|
||||
let mut location_vec = write_location_to_vec(&magic_location);
|
||||
|
||||
if self.aead2022 {
|
||||
let location_len = location_vec.len();
|
||||
let mut rng = rand::rng();
|
||||
let padding_len: usize = rng.random_range(1..=900);
|
||||
location_vec.resize(location_len + padding_len + 2, 0);
|
||||
let padding_len_bytes = (padding_len as u16).to_be_bytes();
|
||||
location_vec[location_len..location_len + 2].copy_from_slice(&padding_len_bytes);
|
||||
rng.fill_bytes(&mut location_vec[location_len + 2..]);
|
||||
}
|
||||
|
||||
write_all(&mut client_stream, &location_vec).await?;
|
||||
|
||||
// Writes UoT V2 request header: isConnect(1) + SOCKS address
|
||||
let mut uot_header = Vec::with_capacity(64);
|
||||
uot_header.push(1u8); // isConnect = 1 (connect mode)
|
||||
let target_bytes = write_location_to_vec(&target);
|
||||
uot_header.extend_from_slice(&target_bytes);
|
||||
write_all(&mut client_stream, &uot_header).await?;
|
||||
client_stream.flush().await?;
|
||||
|
||||
// Uses UotV2Stream for length-prefixed packets
|
||||
let message_stream = UotV2Stream::new(client_stream);
|
||||
|
||||
Ok(Box::new(message_stream))
|
||||
}
|
||||
}
|
||||
|
||||
+32
-2
@@ -16,6 +16,8 @@
|
||||
|
||||
use std::io::{BufRead, Read};
|
||||
|
||||
use crate::util::allocate_vec;
|
||||
|
||||
/// A fixed-capacity sliding buffer with zero-allocation read/write operations.
|
||||
///
|
||||
/// This buffer maintains start and end offsets into a pre-allocated storage,
|
||||
@@ -47,7 +49,7 @@ impl SlideBuffer {
|
||||
#[inline]
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self {
|
||||
data: vec![0u8; capacity].into_boxed_slice(),
|
||||
data: allocate_vec(capacity).into_boxed_slice(),
|
||||
start: 0,
|
||||
end: 0,
|
||||
}
|
||||
@@ -186,6 +188,15 @@ impl SlideBuffer {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a mutable slice of the readable data for in-place modification.
|
||||
///
|
||||
/// This allows callers to modify data in-place (e.g., for decryption)
|
||||
/// without copying to a separate buffer.
|
||||
#[inline]
|
||||
pub fn slice_mut(&mut self, range: std::ops::Range<usize>) -> &mut [u8] {
|
||||
&mut self.data[self.start + range.start..self.start + range.end]
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for SlideBuffer {
|
||||
@@ -211,7 +222,6 @@ impl BufRead for SlideBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
// Implement indexing for convenient access
|
||||
impl std::ops::Index<usize> for SlideBuffer {
|
||||
type Output = u8;
|
||||
|
||||
@@ -436,4 +446,24 @@ mod tests {
|
||||
assert_eq!(buf.as_slice(), b"world!!!");
|
||||
assert_eq!(buf.remaining_capacity(), 100 - 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slice_mut() {
|
||||
let mut buf = SlideBuffer::new(100);
|
||||
buf.extend_from_slice(b"hello world");
|
||||
|
||||
// Modify middle portion
|
||||
let slice = buf.slice_mut(6..11);
|
||||
assert_eq!(slice, b"world");
|
||||
slice[0] = b'W';
|
||||
slice[4] = b'D';
|
||||
|
||||
assert_eq!(buf.as_slice(), b"hello WorlD");
|
||||
|
||||
// Test after consume
|
||||
buf.consume(6);
|
||||
let slice = buf.slice_mut(0..5);
|
||||
slice.copy_from_slice(b"EARTH");
|
||||
assert_eq!(buf.as_slice(), b"EARTH");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod snell_fixed_target_stream;
|
||||
pub mod snell_handler;
|
||||
pub mod snell_udp_stream;
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
//! Adapter that wraps a SnellUdpClientStream with a fixed target.
|
||||
//! This converts AsyncSourcedMessageStream to AsyncMessageStream by:
|
||||
//! - On write: adding the fixed target to each message
|
||||
//! - On read: stripping the source address from responses
|
||||
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::io::ReadBuf;
|
||||
|
||||
use crate::address::NetLocation;
|
||||
use crate::async_stream::{
|
||||
AsyncFlushMessage, AsyncMessageStream, AsyncPing, AsyncReadMessage, AsyncShutdownMessage,
|
||||
AsyncSourcedMessageStream, AsyncWriteMessage,
|
||||
};
|
||||
|
||||
pub struct SnellFixedTargetStream<S> {
|
||||
stream: S,
|
||||
target: NetLocation,
|
||||
}
|
||||
|
||||
impl<S: AsyncSourcedMessageStream> SnellFixedTargetStream<S> {
|
||||
pub fn new(stream: S, target: NetLocation) -> Self {
|
||||
Self { stream, target }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsyncSourcedMessageStream> AsyncReadMessage for SnellFixedTargetStream<S> {
|
||||
fn poll_read_message(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
let this = self.get_mut();
|
||||
// Reads the message and discards the source address.
|
||||
match Pin::new(&mut this.stream).poll_read_sourced_message(cx, buf) {
|
||||
Poll::Ready(Ok(_source)) => Poll::Ready(Ok(())),
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsyncSourcedMessageStream> AsyncWriteMessage for SnellFixedTargetStream<S> {
|
||||
fn poll_write_message(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
let this = self.get_mut();
|
||||
// Writes the message with the fixed target.
|
||||
Pin::new(&mut this.stream).poll_write_targeted_message(cx, buf, &this.target)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsyncSourcedMessageStream> AsyncFlushMessage for SnellFixedTargetStream<S> {
|
||||
fn poll_flush_message(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.get_mut().stream).poll_flush_message(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsyncSourcedMessageStream> AsyncShutdownMessage for SnellFixedTargetStream<S> {
|
||||
fn poll_shutdown_message(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.get_mut().stream).poll_shutdown_message(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsyncSourcedMessageStream> AsyncPing for SnellFixedTargetStream<S> {
|
||||
fn supports_ping(&self) -> bool {
|
||||
self.stream.supports_ping()
|
||||
}
|
||||
|
||||
fn poll_write_ping(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<bool>> {
|
||||
Pin::new(&mut self.get_mut().stream).poll_write_ping(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsyncSourcedMessageStream> AsyncMessageStream for SnellFixedTargetStream<S> {}
|
||||
+93
-24
@@ -1,22 +1,37 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use argon2::Argon2;
|
||||
use argon2::{Config as Argon2Config, ThreadMode, Variant, Version};
|
||||
use async_trait::async_trait;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use super::snell_udp_stream::SnellUdpStream;
|
||||
use super::snell_fixed_target_stream::SnellFixedTargetStream;
|
||||
use super::snell_udp_stream::{SnellUdpClientStream, SnellUdpStream};
|
||||
use crate::address::{Address, NetLocation};
|
||||
use crate::async_stream::AsyncMessageStream;
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::option_util::NoneOrOne;
|
||||
use crate::client_proxy_selector::ClientProxySelector;
|
||||
use crate::shadowsocks::{
|
||||
ShadowsocksCipher, ShadowsocksKey, ShadowsocksStream, ShadowsocksStreamType,
|
||||
};
|
||||
use crate::stream_reader::StreamReader;
|
||||
use crate::tcp_handler::{
|
||||
use crate::tcp::tcp_handler::{
|
||||
TcpClientHandler, TcpClientSetupResult, TcpServerHandler, TcpServerSetupResult,
|
||||
};
|
||||
use crate::util::{allocate_vec, write_all};
|
||||
use crate::util::write_all;
|
||||
|
||||
// Snell protocol Argon2 parameters
|
||||
// ref: https://github.com/icpz/open-snell/blob/master/components/aead/cipher.go#L48
|
||||
const SNELL_ARGON2_CONFIG: Argon2Config<'static> = Argon2Config {
|
||||
variant: Variant::Argon2id,
|
||||
version: Version::Version13,
|
||||
mem_cost: 8, // 8 KB
|
||||
time_cost: 3, // 3 iterations
|
||||
lanes: 1, // parallelism = 1
|
||||
thread_mode: ThreadMode::Sequential,
|
||||
secret: &[],
|
||||
ad: &[],
|
||||
hash_length: 32,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct SnellKey {
|
||||
@@ -35,18 +50,7 @@ impl SnellKey {
|
||||
|
||||
impl ShadowsocksKey for SnellKey {
|
||||
fn create_session_key(&self, salt: &[u8]) -> Box<[u8]> {
|
||||
static ARGON2: OnceLock<Argon2> = OnceLock::new();
|
||||
|
||||
let instance = ARGON2.get_or_init(|| {
|
||||
// ref: https://github.com/icpz/open-snell/blob/master/components/aead/cipher.go#L48
|
||||
let params = argon2::Params::new(8, 3, 1, Some(32)).unwrap();
|
||||
Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params)
|
||||
});
|
||||
|
||||
let mut output = allocate_vec(32);
|
||||
instance
|
||||
.hash_password_into(&self.password_bytes, salt, &mut output)
|
||||
.unwrap();
|
||||
let output = argon2::hash_raw(&self.password_bytes, salt, &SNELL_ARGON2_CONFIG).unwrap();
|
||||
|
||||
if self.key_len == 32 {
|
||||
output.into_boxed_slice()
|
||||
@@ -61,10 +65,16 @@ pub struct SnellServerHandler {
|
||||
cipher: ShadowsocksCipher,
|
||||
key: Arc<Box<dyn ShadowsocksKey>>,
|
||||
udp_enabled: bool,
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
}
|
||||
|
||||
impl SnellServerHandler {
|
||||
pub fn new(cipher: ShadowsocksCipher, password: &str, udp_enabled: bool) -> Self {
|
||||
pub fn new(
|
||||
cipher: ShadowsocksCipher,
|
||||
password: &str,
|
||||
udp_enabled: bool,
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
) -> Self {
|
||||
let key: Arc<Box<dyn ShadowsocksKey>> = Arc::new(Box::new(SnellKey::new(
|
||||
password,
|
||||
cipher.algorithm().key_len(),
|
||||
@@ -73,6 +83,7 @@ impl SnellServerHandler {
|
||||
cipher,
|
||||
key,
|
||||
udp_enabled,
|
||||
proxy_selector,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,10 +180,9 @@ impl TcpServerHandler for SnellServerHandler {
|
||||
need_initial_flush: true,
|
||||
connection_success_response: Some(TCP_TUNNEL_RESPONSE.to_vec().into_boxed_slice()),
|
||||
initial_remote_data: stream_reader.unparsed_data_owned(),
|
||||
override_proxy_provider: NoneOrOne::Unspecified,
|
||||
proxy_selector: self.proxy_selector.clone(),
|
||||
})
|
||||
} else {
|
||||
// write tunnel response.
|
||||
write_all(&mut server_stream, UDP_READY_RESPONSE).await?;
|
||||
|
||||
let udp_stream = SnellUdpStream::new(
|
||||
@@ -183,7 +193,7 @@ impl TcpServerHandler for SnellServerHandler {
|
||||
Ok(TcpServerSetupResult::MultiDirectionalUdp {
|
||||
stream: Box::new(udp_stream),
|
||||
need_initial_flush: true,
|
||||
override_proxy_provider: NoneOrOne::Unspecified,
|
||||
proxy_selector: self.proxy_selector.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -193,15 +203,20 @@ impl TcpServerHandler for SnellServerHandler {
|
||||
pub struct SnellClientHandler {
|
||||
cipher: ShadowsocksCipher,
|
||||
key: Arc<Box<dyn ShadowsocksKey>>,
|
||||
udp_enabled: bool,
|
||||
}
|
||||
|
||||
impl SnellClientHandler {
|
||||
pub fn new(cipher: ShadowsocksCipher, password: &str) -> Self {
|
||||
pub fn new(cipher: ShadowsocksCipher, password: &str, udp_enabled: bool) -> Self {
|
||||
let key: Arc<Box<dyn ShadowsocksKey>> = Arc::new(Box::new(SnellKey::new(
|
||||
password,
|
||||
cipher.algorithm().key_len(),
|
||||
)));
|
||||
Self { cipher, key }
|
||||
Self {
|
||||
cipher,
|
||||
key,
|
||||
udp_enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,4 +286,58 @@ impl TcpClientHandler for SnellClientHandler {
|
||||
early_data: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn supports_udp_over_tcp(&self) -> bool {
|
||||
self.udp_enabled
|
||||
}
|
||||
|
||||
async fn setup_client_udp_bidirectional(
|
||||
&self,
|
||||
client_stream: Box<dyn AsyncStream>,
|
||||
target: NetLocation,
|
||||
) -> std::io::Result<Box<dyn AsyncMessageStream>> {
|
||||
let mut ss_stream = ShadowsocksStream::new(
|
||||
client_stream,
|
||||
ShadowsocksStreamType::Aead,
|
||||
self.cipher.algorithm(),
|
||||
self.cipher.salt_len(),
|
||||
self.key.clone(),
|
||||
None,
|
||||
);
|
||||
|
||||
write_all(
|
||||
&mut ss_stream,
|
||||
&[
|
||||
1, // snell version
|
||||
6, // UDP command
|
||||
0, // client id length
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
ss_stream.flush().await?;
|
||||
|
||||
let mut response = [0u8; 1];
|
||||
let n = ss_stream.read(&mut response).await?;
|
||||
if n == 0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::UnexpectedEof,
|
||||
"unexpected EOF when reading UDP ready response",
|
||||
));
|
||||
}
|
||||
|
||||
if response[0] != 0 {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"Got non-UDP-ready response ({})",
|
||||
response[0]
|
||||
)));
|
||||
}
|
||||
|
||||
// Wraps multi-directional Snell UDP with a fixed target adapter for single-target mode.
|
||||
let max_payload_size = ShadowsocksStreamType::Aead.max_payload_len();
|
||||
let snell_udp_client_stream =
|
||||
SnellUdpClientStream::new(Box::new(ss_stream), max_payload_size);
|
||||
let fixed_target_stream = SnellFixedTargetStream::new(snell_udp_client_stream, target);
|
||||
|
||||
Ok(Box::new(fixed_target_stream))
|
||||
}
|
||||
}
|
||||
|
||||
+214
-11
@@ -6,17 +6,20 @@ use tokio::io::ReadBuf;
|
||||
|
||||
use crate::address::{Address, NetLocation};
|
||||
use crate::async_stream::{
|
||||
AsyncFlushMessage, AsyncMessageStream, AsyncPing, AsyncReadMessage, AsyncReadTargetedMessage,
|
||||
AsyncShutdownMessage, AsyncTargetedMessageStream, AsyncWriteMessage, AsyncWriteSourcedMessage,
|
||||
AsyncFlushMessage, AsyncMessageStream, AsyncPing, AsyncReadMessage, AsyncReadSourcedMessage,
|
||||
AsyncReadTargetedMessage, AsyncShutdownMessage, AsyncSourcedMessageStream,
|
||||
AsyncTargetedMessageStream, AsyncWriteMessage, AsyncWriteSourcedMessage,
|
||||
AsyncWriteTargetedMessage,
|
||||
};
|
||||
use crate::util::allocate_vec;
|
||||
|
||||
pub struct SnellUdpStream {
|
||||
stream: Box<dyn AsyncMessageStream>,
|
||||
max_payload_size: usize,
|
||||
|
||||
read_buf: [u8; 65535],
|
||||
read_buf: Box<[u8]>,
|
||||
|
||||
write_buf: [u8; 65535],
|
||||
write_buf: Box<[u8]>,
|
||||
write_buf_end_offset: usize,
|
||||
|
||||
is_eof: bool,
|
||||
@@ -28,9 +31,9 @@ impl SnellUdpStream {
|
||||
stream,
|
||||
max_payload_size,
|
||||
|
||||
read_buf: [0u8; 65535],
|
||||
read_buf: allocate_vec(65535).into_boxed_slice(),
|
||||
|
||||
write_buf: [0u8; 65535],
|
||||
write_buf: allocate_vec(65535).into_boxed_slice(),
|
||||
write_buf_end_offset: 0,
|
||||
|
||||
is_eof: false,
|
||||
@@ -136,8 +139,7 @@ impl AsyncWriteSourcedMessage for SnellUdpStream {
|
||||
let mut this = self.get_mut();
|
||||
|
||||
if this.write_buf_end_offset > 0 {
|
||||
// it's possible for the buffer to be written, but flush not to be completed.
|
||||
// we still want to continue in that scenario.
|
||||
// Buffer may be written but flush incomplete; continues in that case.
|
||||
if let Poll::Ready(Err(e)) = Pin::new(&mut this).poll_flush_message(cx) {
|
||||
return Poll::Ready(Err(e));
|
||||
}
|
||||
@@ -204,12 +206,213 @@ impl AsyncPing for SnellUdpStream {
|
||||
self.stream.supports_ping()
|
||||
}
|
||||
|
||||
// Write a ping message to the stream, if supported.
|
||||
// This should end up calling the highest level stream abstraction that supports
|
||||
// pings, and should only result in a single message.
|
||||
/// Writes a ping message to the highest level stream abstraction that supports pings.
|
||||
fn poll_write_ping(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<bool>> {
|
||||
Pin::new(&mut self.get_mut().stream).poll_write_ping(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncTargetedMessageStream for SnellUdpStream {}
|
||||
|
||||
/// Client-side Snell UDP stream.
|
||||
/// Writes requests with target (cmd + address format) and reads responses with source (ip_version format).
|
||||
pub struct SnellUdpClientStream {
|
||||
stream: Box<dyn AsyncMessageStream>,
|
||||
max_payload_size: usize,
|
||||
|
||||
read_buf: Box<[u8]>,
|
||||
|
||||
write_buf: Box<[u8]>,
|
||||
write_buf_end_offset: usize,
|
||||
|
||||
is_eof: bool,
|
||||
}
|
||||
|
||||
impl SnellUdpClientStream {
|
||||
pub fn new(stream: Box<dyn AsyncMessageStream>, max_payload_size: usize) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
max_payload_size,
|
||||
|
||||
read_buf: allocate_vec(65535).into_boxed_slice(),
|
||||
|
||||
write_buf: allocate_vec(65535).into_boxed_slice(),
|
||||
write_buf_end_offset: 0,
|
||||
|
||||
is_eof: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads response format: ip_version(1) + ip(4/16) + port(2) + data
|
||||
/// Returns the source SocketAddr
|
||||
impl AsyncReadSourcedMessage for SnellUdpClientStream {
|
||||
fn poll_read_sourced_message(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<SocketAddr>> {
|
||||
let this = self.get_mut();
|
||||
if this.is_eof {
|
||||
return Poll::Ready(Ok(SocketAddr::new(
|
||||
std::net::IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
||||
0,
|
||||
)));
|
||||
}
|
||||
|
||||
let mut read_buf = ReadBuf::new(&mut this.read_buf);
|
||||
ready!(Pin::new(&mut this.stream).poll_read_message(cx, &mut read_buf))?;
|
||||
|
||||
let len = read_buf.filled().len();
|
||||
if len == 0 {
|
||||
this.is_eof = true;
|
||||
return Poll::Ready(Ok(SocketAddr::new(
|
||||
std::net::IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
||||
0,
|
||||
)));
|
||||
}
|
||||
|
||||
// Response format: ip_version(1) + ip(4/16) + port(2) + data
|
||||
if len < 7 {
|
||||
return Poll::Ready(Err(std::io::Error::other(
|
||||
"snell response packet too small",
|
||||
)));
|
||||
}
|
||||
|
||||
if len > this.max_payload_size {
|
||||
return Poll::Ready(Err(std::io::Error::other("snell response packet too big")));
|
||||
}
|
||||
|
||||
let ip_version = this.read_buf[0];
|
||||
let (source_addr, data_offset) = if ip_version == 4 {
|
||||
if len < 7 {
|
||||
return Poll::Ready(Err(std::io::Error::other(
|
||||
"invalid snell response packet size for ipv4",
|
||||
)));
|
||||
}
|
||||
let ip_bytes: [u8; 4] = this.read_buf[1..5].try_into().unwrap();
|
||||
let ip_addr = Ipv4Addr::from(ip_bytes);
|
||||
let port = u16::from_be_bytes(this.read_buf[5..7].try_into().unwrap());
|
||||
(SocketAddr::new(std::net::IpAddr::V4(ip_addr), port), 7)
|
||||
} else if ip_version == 6 {
|
||||
if len < 19 {
|
||||
return Poll::Ready(Err(std::io::Error::other(
|
||||
"invalid snell response packet size for ipv6",
|
||||
)));
|
||||
}
|
||||
let ip_bytes: [u8; 16] = this.read_buf[1..17].try_into().unwrap();
|
||||
let ip_addr = Ipv6Addr::from(ip_bytes);
|
||||
let port = u16::from_be_bytes(this.read_buf[17..19].try_into().unwrap());
|
||||
(SocketAddr::new(std::net::IpAddr::V6(ip_addr), port), 19)
|
||||
} else {
|
||||
return Poll::Ready(Err(std::io::Error::other(format!(
|
||||
"invalid snell response ip version: {ip_version}"
|
||||
))));
|
||||
};
|
||||
|
||||
buf.put_slice(&this.read_buf[data_offset..len]);
|
||||
Poll::Ready(Ok(source_addr))
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes request format: cmd(1=0x01) + address_len(1) + [hostname bytes | ip_version(1) + ip(4/16)] + port(2) + data
|
||||
impl AsyncWriteTargetedMessage for SnellUdpClientStream {
|
||||
fn poll_write_targeted_message(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
target: &NetLocation,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
let mut this = self.get_mut();
|
||||
|
||||
if this.write_buf_end_offset > 0 {
|
||||
if let Poll::Ready(Err(e)) = Pin::new(&mut this).poll_flush_message(cx) {
|
||||
return Poll::Ready(Err(e));
|
||||
}
|
||||
if this.write_buf_end_offset > 0 {
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
|
||||
let buf_len = buf.len();
|
||||
// Max header: cmd(1) + addr_len(1) + ip_version(1) + ipv6(16) + port(2) = 21
|
||||
if buf_len + 21 > this.write_buf.len() {
|
||||
panic!("single message is larger than our write buf: {buf_len}");
|
||||
}
|
||||
|
||||
this.write_buf[0] = 1; // cmd = data
|
||||
|
||||
let offset = match target.address() {
|
||||
Address::Ipv4(ip) => {
|
||||
// address_len = 0 means IP address follows
|
||||
this.write_buf[1] = 0;
|
||||
this.write_buf[2] = 4; // ip_version
|
||||
this.write_buf[3..7].copy_from_slice(&ip.octets());
|
||||
this.write_buf[7..9].copy_from_slice(&target.port().to_be_bytes());
|
||||
9
|
||||
}
|
||||
Address::Ipv6(ip) => {
|
||||
this.write_buf[1] = 0;
|
||||
this.write_buf[2] = 6; // ip_version
|
||||
this.write_buf[3..19].copy_from_slice(&ip.octets());
|
||||
this.write_buf[19..21].copy_from_slice(&target.port().to_be_bytes());
|
||||
21
|
||||
}
|
||||
Address::Hostname(hostname) => {
|
||||
let hostname_bytes = hostname.as_bytes();
|
||||
let hostname_len = hostname_bytes.len();
|
||||
if hostname_len > 255 {
|
||||
return Poll::Ready(Err(std::io::Error::other("hostname too long")));
|
||||
}
|
||||
this.write_buf[1] = hostname_len as u8;
|
||||
this.write_buf[2..2 + hostname_len].copy_from_slice(hostname_bytes);
|
||||
let port_offset = 2 + hostname_len;
|
||||
this.write_buf[port_offset..port_offset + 2]
|
||||
.copy_from_slice(&target.port().to_be_bytes());
|
||||
port_offset + 2
|
||||
}
|
||||
};
|
||||
|
||||
this.write_buf[offset..offset + buf_len].copy_from_slice(buf);
|
||||
this.write_buf_end_offset = offset + buf_len;
|
||||
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncFlushMessage for SnellUdpClientStream {
|
||||
fn poll_flush_message(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
|
||||
let this = self.get_mut();
|
||||
if this.write_buf_end_offset > 0 {
|
||||
ready!(
|
||||
Pin::new(&mut this.stream)
|
||||
.poll_write_message(cx, &this.write_buf[0..this.write_buf_end_offset])
|
||||
)?;
|
||||
this.write_buf_end_offset = 0;
|
||||
}
|
||||
Pin::new(&mut this.stream).poll_flush_message(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncShutdownMessage for SnellUdpClientStream {
|
||||
fn poll_shutdown_message(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
let mut this = self.get_mut();
|
||||
ready!(Pin::new(&mut this).poll_flush_message(cx))?;
|
||||
Pin::new(&mut this.stream).poll_shutdown_message(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncPing for SnellUdpClientStream {
|
||||
fn supports_ping(&self) -> bool {
|
||||
self.stream.supports_ping()
|
||||
}
|
||||
|
||||
fn poll_write_ping(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<bool>> {
|
||||
Pin::new(&mut self.get_mut().stream).poll_write_ping(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncSourcedMessageStream for SnellUdpClientStream {}
|
||||
|
||||
+18
-30
@@ -19,36 +19,6 @@ pub fn new_udp_socket(
|
||||
into_tokio_udp_socket(socket)
|
||||
}
|
||||
|
||||
pub fn new_reuse_udp_sockets(
|
||||
is_ipv6: bool,
|
||||
bind_interface: Option<String>,
|
||||
count: usize,
|
||||
) -> std::io::Result<Vec<tokio::net::UdpSocket>> {
|
||||
let mut sockets = Vec::with_capacity(count);
|
||||
if count == 0 {
|
||||
return Ok(sockets);
|
||||
}
|
||||
|
||||
let socket = new_socket2_udp_socket(
|
||||
is_ipv6,
|
||||
bind_interface.clone(),
|
||||
Some(get_unspecified_socket_addr(is_ipv6)),
|
||||
true,
|
||||
)?;
|
||||
|
||||
let local_addr = socket.local_addr()?.as_socket().unwrap();
|
||||
|
||||
sockets.push(into_tokio_udp_socket(socket)?);
|
||||
|
||||
for _ in 1..count {
|
||||
let socket =
|
||||
new_socket2_udp_socket(is_ipv6, bind_interface.clone(), Some(local_addr), true)?;
|
||||
sockets.push(into_tokio_udp_socket(socket)?);
|
||||
}
|
||||
|
||||
Ok(sockets)
|
||||
}
|
||||
|
||||
fn get_unspecified_socket_addr(is_ipv6: bool) -> SocketAddr {
|
||||
if !is_ipv6 {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0)
|
||||
@@ -62,12 +32,30 @@ pub fn new_socket2_udp_socket(
|
||||
bind_interface: Option<String>,
|
||||
bind_address: Option<SocketAddr>,
|
||||
reuse_port: bool,
|
||||
) -> std::io::Result<socket2::Socket> {
|
||||
new_socket2_udp_socket_with_buffer_size(is_ipv6, bind_interface, bind_address, reuse_port, None)
|
||||
}
|
||||
|
||||
pub fn new_socket2_udp_socket_with_buffer_size(
|
||||
is_ipv6: bool,
|
||||
bind_interface: Option<String>,
|
||||
bind_address: Option<SocketAddr>,
|
||||
reuse_port: bool,
|
||||
buffer_size: Option<usize>,
|
||||
) -> std::io::Result<socket2::Socket> {
|
||||
let domain = if is_ipv6 { Domain::IPV6 } else { Domain::IPV4 };
|
||||
let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
|
||||
|
||||
socket.set_nonblocking(true)?;
|
||||
|
||||
// Set socket buffer sizes if specified.
|
||||
// This helps prevent packet drops during bursts for high-throughput connections.
|
||||
if let Some(size) = buffer_size {
|
||||
// Ignore errors - kernel may cap the value
|
||||
let _ = socket.set_recv_buffer_size(size);
|
||||
let _ = socket.set_send_buffer_size(size);
|
||||
}
|
||||
|
||||
if reuse_port {
|
||||
#[cfg(all(unix, not(any(target_os = "solaris", target_os = "illumos"))))]
|
||||
socket.set_reuse_port(true)?;
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
//! SOCKS5 UDP relay implementation.
|
||||
//!
|
||||
//! This module provides UDP relay functionality for SOCKS5 UDP ASSOCIATE.
|
||||
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use futures::ready;
|
||||
use tokio::io::ReadBuf;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::address::{Address, NetLocation};
|
||||
use crate::async_stream::{
|
||||
AsyncFlushMessage, AsyncPing, AsyncReadTargetedMessage, AsyncShutdownMessage,
|
||||
AsyncTargetedMessageStream, AsyncWriteSourcedMessage,
|
||||
};
|
||||
use crate::socks_handler::{ADDR_TYPE_DOMAIN_NAME, ADDR_TYPE_IPV4, ADDR_TYPE_IPV6};
|
||||
|
||||
/// Minimum SOCKS5 UDP header size: RSV(2) + FRAG(1) + ATYP(1) + min_addr(4 for IPv4) + PORT(2) = 10
|
||||
const MIN_HEADER_SIZE: usize = 10;
|
||||
|
||||
/// Maximum UDP packet size
|
||||
const MAX_UDP_SIZE: usize = 65535;
|
||||
|
||||
/// Parse a SOCKS5 UDP packet header, returning the target location and payload slice.
|
||||
///
|
||||
/// Packet format:
|
||||
/// ```text
|
||||
/// +------+------+------+----------+----------+----------+
|
||||
/// | RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA |
|
||||
/// +------+------+------+----------+----------+----------+
|
||||
/// | 2 | 1 | 1 | Variable | 2 | Variable |
|
||||
/// +------+------+------+----------+----------+----------+
|
||||
/// ```
|
||||
pub fn parse_socks5_udp_packet(data: &[u8]) -> std::io::Result<(NetLocation, &[u8])> {
|
||||
if data.len() < MIN_HEADER_SIZE {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("SOCKS5 UDP packet too short: {} bytes", data.len()),
|
||||
));
|
||||
}
|
||||
|
||||
// RSV must be 0x0000
|
||||
if data[0] != 0 || data[1] != 0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"SOCKS5 UDP packet has non-zero reserved bytes",
|
||||
));
|
||||
}
|
||||
|
||||
// FRAG must be 0 (we don't support fragmentation)
|
||||
let frag = data[2];
|
||||
if frag != 0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("SOCKS5 UDP fragmentation not supported (frag={})", frag),
|
||||
));
|
||||
}
|
||||
|
||||
let atyp = data[3];
|
||||
let (address, header_len) = match atyp {
|
||||
ADDR_TYPE_IPV4 => {
|
||||
// 4 bytes for IPv4 + 2 bytes for port
|
||||
if data.len() < 4 + 4 + 2 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"SOCKS5 UDP packet too short for IPv4 address",
|
||||
));
|
||||
}
|
||||
let addr = Ipv4Addr::new(data[4], data[5], data[6], data[7]);
|
||||
let port = u16::from_be_bytes([data[8], data[9]]);
|
||||
(NetLocation::new(Address::Ipv4(addr), port), 10)
|
||||
}
|
||||
ADDR_TYPE_IPV6 => {
|
||||
// 16 bytes for IPv6 + 2 bytes for port
|
||||
if data.len() < 4 + 16 + 2 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"SOCKS5 UDP packet too short for IPv6 address",
|
||||
));
|
||||
}
|
||||
let addr = Ipv6Addr::new(
|
||||
u16::from_be_bytes([data[4], data[5]]),
|
||||
u16::from_be_bytes([data[6], data[7]]),
|
||||
u16::from_be_bytes([data[8], data[9]]),
|
||||
u16::from_be_bytes([data[10], data[11]]),
|
||||
u16::from_be_bytes([data[12], data[13]]),
|
||||
u16::from_be_bytes([data[14], data[15]]),
|
||||
u16::from_be_bytes([data[16], data[17]]),
|
||||
u16::from_be_bytes([data[18], data[19]]),
|
||||
);
|
||||
let port = u16::from_be_bytes([data[20], data[21]]);
|
||||
(NetLocation::new(Address::Ipv6(addr), port), 22)
|
||||
}
|
||||
ADDR_TYPE_DOMAIN_NAME => {
|
||||
// 1 byte length + domain + 2 bytes port
|
||||
if data.len() < 5 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"SOCKS5 UDP packet too short for domain length",
|
||||
));
|
||||
}
|
||||
let domain_len = data[4] as usize;
|
||||
let header_len = 4 + 1 + domain_len + 2;
|
||||
if data.len() < header_len {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"SOCKS5 UDP packet too short for domain (need {}, have {})",
|
||||
header_len,
|
||||
data.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
let domain = std::str::from_utf8(&data[5..5 + domain_len]).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Invalid UTF-8 in domain name: {}", e),
|
||||
)
|
||||
})?;
|
||||
let port_offset = 5 + domain_len;
|
||||
let port = u16::from_be_bytes([data[port_offset], data[port_offset + 1]]);
|
||||
(
|
||||
NetLocation::new(Address::Hostname(domain.to_string()), port),
|
||||
header_len,
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Unknown SOCKS5 address type: {}", atyp),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok((address, &data[header_len..]))
|
||||
}
|
||||
|
||||
/// Encode a SOCKS5 UDP packet with the given source address and payload.
|
||||
///
|
||||
/// Returns the complete packet ready to send to the client.
|
||||
pub fn encode_socks5_udp_packet(source: &NetLocation, payload: &[u8]) -> Vec<u8> {
|
||||
let (address, port) = source.components();
|
||||
|
||||
let addr_size = match address {
|
||||
Address::Ipv4(_) => 1 + 4, // ATYP + 4 bytes
|
||||
Address::Ipv6(_) => 1 + 16, // ATYP + 16 bytes
|
||||
Address::Hostname(h) => 1 + 1 + h.len(), // ATYP + len + domain
|
||||
};
|
||||
let header_size = 2 + 1 + addr_size + 2; // RSV + FRAG + addr + PORT
|
||||
let mut packet = Vec::with_capacity(header_size + payload.len());
|
||||
|
||||
packet.extend_from_slice(&[0, 0, 0]); // RSV + FRAG
|
||||
|
||||
match address {
|
||||
Address::Ipv4(addr) => {
|
||||
packet.push(ADDR_TYPE_IPV4);
|
||||
packet.extend_from_slice(&addr.octets());
|
||||
}
|
||||
Address::Ipv6(addr) => {
|
||||
packet.push(ADDR_TYPE_IPV6);
|
||||
packet.extend_from_slice(&addr.octets());
|
||||
}
|
||||
Address::Hostname(domain) => {
|
||||
packet.push(ADDR_TYPE_DOMAIN_NAME);
|
||||
packet.push(domain.len() as u8);
|
||||
packet.extend_from_slice(domain.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
packet.extend_from_slice(&port.to_be_bytes());
|
||||
packet.extend_from_slice(payload);
|
||||
|
||||
packet
|
||||
}
|
||||
|
||||
/// SOCKS5 UDP relay stream.
|
||||
///
|
||||
/// This stream wraps a UDP socket and handles SOCKS5 UDP packet framing.
|
||||
/// It implements `AsyncTargetedMessageStream` for use with the proxy chain.
|
||||
///
|
||||
/// - Reading: Receives SOCKS5-framed UDP packets from the client, parses the header,
|
||||
/// and returns the payload with the target address.
|
||||
/// - Writing: Takes a payload and source address, encodes it as a SOCKS5 UDP packet,
|
||||
/// and sends it to the client.
|
||||
pub struct Socks5UdpRelayStream {
|
||||
socket: Arc<UdpSocket>,
|
||||
/// The client's UDP address, learned from the first received packet.
|
||||
client_addr: Option<SocketAddr>,
|
||||
/// Receiver for incoming packets from the socket reader task.
|
||||
receiver: mpsc::Receiver<(Box<[u8]>, SocketAddr)>,
|
||||
/// Handle to the socket reader task.
|
||||
reader_task: Option<tokio::task::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Socks5UdpRelayStream {
|
||||
/// Create a new SOCKS5 UDP relay stream.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `socket` - The bound UDP socket for the relay
|
||||
pub fn new(socket: UdpSocket) -> Self {
|
||||
let socket = Arc::new(socket);
|
||||
let local_addr = socket.local_addr().ok();
|
||||
log::debug!(
|
||||
"SOCKS5 UDP relay: creating Socks5UdpRelayStream at {:?}",
|
||||
local_addr
|
||||
);
|
||||
let (tx, rx) = mpsc::channel(64);
|
||||
let recv_socket = socket.clone();
|
||||
let reader_task = tokio::spawn(async move {
|
||||
let mut buf = vec![0u8; MAX_UDP_SIZE];
|
||||
loop {
|
||||
match recv_socket.recv_from(&mut buf).await {
|
||||
Ok((n, from_addr)) => {
|
||||
log::debug!("SOCKS5 UDP relay: received {} bytes from {}", n, from_addr);
|
||||
let packet = buf[..n].to_vec().into_boxed_slice();
|
||||
if tx.send((packet, from_addr)).await.is_err() {
|
||||
// Channel closed, stop reading
|
||||
log::debug!("SOCKS5 UDP relay: channel closed, stopping reader");
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("SOCKS5 UDP relay recv error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
socket,
|
||||
client_addr: None,
|
||||
receiver: rx,
|
||||
reader_task: Some(reader_task),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncReadTargetedMessage for Socks5UdpRelayStream {
|
||||
fn poll_read_targeted_message(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<NetLocation>> {
|
||||
let this = self.get_mut();
|
||||
|
||||
match Pin::new(&mut this.receiver).poll_recv(cx) {
|
||||
Poll::Ready(Some((packet, from_addr))) => {
|
||||
// Learn/verify client address
|
||||
if let Some(expected) = this.client_addr {
|
||||
if from_addr != expected {
|
||||
// Packet from unexpected source, ignore
|
||||
log::debug!(
|
||||
"SOCKS5 UDP relay: ignoring packet from {} (expected {})",
|
||||
from_addr,
|
||||
expected
|
||||
);
|
||||
cx.waker().wake_by_ref();
|
||||
return Poll::Pending;
|
||||
}
|
||||
} else {
|
||||
// Learn client address from first packet
|
||||
log::debug!("SOCKS5 UDP relay: learned client address: {}", from_addr);
|
||||
this.client_addr = Some(from_addr);
|
||||
}
|
||||
|
||||
match parse_socks5_udp_packet(&packet) {
|
||||
Ok((target, payload)) => {
|
||||
log::debug!(
|
||||
"SOCKS5 UDP relay: parsed packet, target={}, payload_len={}",
|
||||
target,
|
||||
payload.len()
|
||||
);
|
||||
// Skip empty payloads - the copy loop interprets 0-byte reads as EOF
|
||||
if payload.is_empty() {
|
||||
log::debug!("SOCKS5 UDP relay: skipping empty payload");
|
||||
cx.waker().wake_by_ref();
|
||||
return Poll::Pending;
|
||||
}
|
||||
if payload.len() > buf.remaining() {
|
||||
return Poll::Ready(Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"UDP payload too large: {} > {}",
|
||||
payload.len(),
|
||||
buf.remaining()
|
||||
),
|
||||
)));
|
||||
}
|
||||
buf.put_slice(payload);
|
||||
Poll::Ready(Ok(target))
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("SOCKS5 UDP relay: failed to parse packet: {}", e);
|
||||
// Try to get another packet
|
||||
cx.waker().wake_by_ref();
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
log::debug!("SOCKS5 UDP relay: channel closed (receiver got None)");
|
||||
Poll::Ready(Err(std::io::Error::new(
|
||||
std::io::ErrorKind::UnexpectedEof,
|
||||
"UDP relay channel closed",
|
||||
)))
|
||||
}
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWriteSourcedMessage for Socks5UdpRelayStream {
|
||||
fn poll_write_sourced_message(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
source: &SocketAddr,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
let this = self.get_mut();
|
||||
|
||||
let client_addr = match this.client_addr {
|
||||
Some(addr) => addr,
|
||||
None => {
|
||||
log::debug!("SOCKS5 UDP relay: cannot write, no client address learned yet");
|
||||
return Poll::Ready(Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotConnected,
|
||||
"No client address learned yet",
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
// Convert SocketAddr to NetLocation for encoding.
|
||||
// Handle IPv4-mapped IPv6 addresses (::ffff:w.x.y.z) by converting back to IPv4.
|
||||
let source_ip = source.ip();
|
||||
let address = match source_ip {
|
||||
IpAddr::V4(v4) => Address::Ipv4(v4),
|
||||
IpAddr::V6(v6) => {
|
||||
if let Some(v4) = v6.to_ipv4_mapped() {
|
||||
Address::Ipv4(v4)
|
||||
} else {
|
||||
Address::Ipv6(v6)
|
||||
}
|
||||
}
|
||||
};
|
||||
let source_location = NetLocation::new(address, source.port());
|
||||
let packet = encode_socks5_udp_packet(&source_location, buf);
|
||||
|
||||
log::debug!(
|
||||
"SOCKS5 UDP relay: sending {} byte response from {} to client {}",
|
||||
packet.len(),
|
||||
source_location,
|
||||
client_addr
|
||||
);
|
||||
|
||||
ready!(this.socket.poll_send_to(cx, &packet, client_addr))?;
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncFlushMessage for Socks5UdpRelayStream {
|
||||
fn poll_flush_message(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
// UDP doesn't need flushing
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncShutdownMessage for Socks5UdpRelayStream {
|
||||
fn poll_shutdown_message(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
let this = self.get_mut();
|
||||
if let Some(handle) = this.reader_task.take() {
|
||||
handle.abort();
|
||||
}
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncPing for Socks5UdpRelayStream {
|
||||
fn supports_ping(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn poll_write_ping(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<bool>> {
|
||||
Poll::Ready(Ok(false))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncTargetedMessageStream for Socks5UdpRelayStream {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_ipv4_packet() {
|
||||
// RSV(2) + FRAG(1) + ATYP(1) + IPv4(4) + PORT(2) + DATA
|
||||
let packet = [
|
||||
0x00, 0x00, // RSV
|
||||
0x00, // FRAG
|
||||
0x01, // ATYP = IPv4
|
||||
0x08, 0x08, 0x08, 0x08, // 8.8.8.8
|
||||
0x00, 0x35, // port 53
|
||||
0x48, 0x65, 0x6c, 0x6c, 0x6f, // "Hello"
|
||||
];
|
||||
|
||||
let (location, payload) = parse_socks5_udp_packet(&packet).unwrap();
|
||||
assert_eq!(
|
||||
location.address(),
|
||||
&Address::Ipv4(Ipv4Addr::new(8, 8, 8, 8))
|
||||
);
|
||||
assert_eq!(location.port(), 53);
|
||||
assert_eq!(payload, b"Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_ipv6_packet() {
|
||||
// RSV(2) + FRAG(1) + ATYP(1) + IPv6(16) + PORT(2) + DATA
|
||||
let mut packet = vec![
|
||||
0x00, 0x00, // RSV
|
||||
0x00, // FRAG
|
||||
0x04, // ATYP = IPv6
|
||||
];
|
||||
// 2001:4860:4860::8888
|
||||
packet.extend_from_slice(&[
|
||||
0x20, 0x01, 0x48, 0x60, 0x48, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x88, 0x88,
|
||||
]);
|
||||
packet.extend_from_slice(&[0x00, 0x35]); // port 53
|
||||
packet.extend_from_slice(b"Hello");
|
||||
|
||||
let (location, payload) = parse_socks5_udp_packet(&packet).unwrap();
|
||||
assert!(matches!(location.address(), Address::Ipv6(_)));
|
||||
assert_eq!(location.port(), 53);
|
||||
assert_eq!(payload, b"Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_domain_packet() {
|
||||
// RSV(2) + FRAG(1) + ATYP(1) + LEN(1) + DOMAIN + PORT(2) + DATA
|
||||
let mut packet = vec![
|
||||
0x00, 0x00, // RSV
|
||||
0x00, // FRAG
|
||||
0x03, // ATYP = domain
|
||||
0x0b, // length = 11
|
||||
];
|
||||
packet.extend_from_slice(b"example.com");
|
||||
packet.extend_from_slice(&[0x00, 0x50]); // port 80
|
||||
packet.extend_from_slice(b"GET /");
|
||||
|
||||
let (location, payload) = parse_socks5_udp_packet(&packet).unwrap();
|
||||
assert_eq!(
|
||||
location.address(),
|
||||
&Address::Hostname("example.com".to_string())
|
||||
);
|
||||
assert_eq!(location.port(), 80);
|
||||
assert_eq!(payload, b"GET /");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_ipv4_packet() {
|
||||
let location = NetLocation::new(Address::Ipv4(Ipv4Addr::new(8, 8, 8, 8)), 53);
|
||||
let packet = encode_socks5_udp_packet(&location, b"Hello");
|
||||
|
||||
// Verify by parsing it back
|
||||
let (parsed_loc, payload) = parse_socks5_udp_packet(&packet).unwrap();
|
||||
assert_eq!(parsed_loc.address(), location.address());
|
||||
assert_eq!(parsed_loc.port(), location.port());
|
||||
assert_eq!(payload, b"Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_domain() {
|
||||
let location = NetLocation::new(Address::Hostname("dns.google".to_string()), 443);
|
||||
let original_payload = b"test data";
|
||||
let packet = encode_socks5_udp_packet(&location, original_payload);
|
||||
|
||||
let (parsed_loc, payload) = parse_socks5_udp_packet(&packet).unwrap();
|
||||
assert_eq!(parsed_loc.address(), location.address());
|
||||
assert_eq!(parsed_loc.port(), location.port());
|
||||
assert_eq!(payload, original_payload);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reject_fragmented() {
|
||||
let packet = [
|
||||
0x00, 0x00, // RSV
|
||||
0x01, // FRAG = 1 (fragmented!)
|
||||
0x01, // ATYP = IPv4
|
||||
0x08, 0x08, 0x08, 0x08, 0x00, 0x35, 0x48, 0x65, 0x6c, 0x6c, 0x6f,
|
||||
];
|
||||
|
||||
let result = parse_socks5_udp_packet(&packet);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("fragmentation"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reject_bad_reserved() {
|
||||
let packet = [
|
||||
0x00, 0x01, // RSV = bad!
|
||||
0x00, 0x01, 0x08, 0x08, 0x08, 0x08, 0x00, 0x35,
|
||||
];
|
||||
|
||||
let result = parse_socks5_udp_packet(&packet);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("reserved"));
|
||||
}
|
||||
}
|
||||
+485
-203
@@ -1,4 +1,5 @@
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use async_trait::async_trait;
|
||||
@@ -6,12 +7,15 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::address::{Address, NetLocation};
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::option_util::NoneOrOne;
|
||||
use crate::client_proxy_selector::ClientProxySelector;
|
||||
use crate::resolver::Resolver;
|
||||
use crate::routing::{ServerStream, run_udp_routing};
|
||||
use crate::socks5_udp_relay::Socks5UdpRelayStream;
|
||||
use crate::stream_reader::StreamReader;
|
||||
use crate::tcp_handler::{
|
||||
use crate::tcp::tcp_handler::{
|
||||
TcpClientHandler, TcpClientSetupResult, TcpServerHandler, TcpServerSetupResult,
|
||||
};
|
||||
use crate::uot::{UOT_V1_MAGIC_ADDRESS, UOT_V2_MAGIC_ADDRESS, UotV1Stream, UotV2Stream};
|
||||
use crate::uot::{UOT_V1_MAGIC_ADDRESS, UOT_V2_MAGIC_ADDRESS, UotV1ServerStream, UotV2Stream};
|
||||
use crate::util::write_all;
|
||||
|
||||
pub const VER_SOCKS5: u8 = 0x05;
|
||||
@@ -30,14 +34,47 @@ pub const RESULT_SUCCESS: u8 = 0x0;
|
||||
pub const CMD_CONNECT: u8 = 0x01;
|
||||
pub const CMD_UDP_ASSOCIATE: u8 = 0x03;
|
||||
|
||||
// SOCKS5 reply codes
|
||||
pub const REPLY_SUCCESS: u8 = 0x00;
|
||||
pub const REPLY_GENERAL_FAILURE: u8 = 0x01;
|
||||
pub const REPLY_COMMAND_NOT_SUPPORTED: u8 = 0x07;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SocksTcpServerHandler {
|
||||
auth_info: Option<(String, String)>,
|
||||
/// Enable UDP functionality (UDP ASSOCIATE and UDP-over-TCP)
|
||||
udp_enabled: bool,
|
||||
/// IP address to bind UDP sockets on (same as TCP server)
|
||||
bind_ip: IpAddr,
|
||||
/// Proxy selector for outbound connections
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
/// DNS resolver
|
||||
resolver: Arc<dyn Resolver>,
|
||||
}
|
||||
|
||||
impl SocksTcpServerHandler {
|
||||
pub fn new(auth_info: Option<(String, String)>) -> Self {
|
||||
Self { auth_info }
|
||||
/// Create a new SOCKS5 server handler.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `auth_info` - Optional username/password for authentication
|
||||
/// * `udp_enabled` - Enable UDP functionality (UDP ASSOCIATE and UDP-over-TCP)
|
||||
/// * `bind_ip` - IP address to bind UDP sockets on (should match TCP server)
|
||||
/// * `proxy_selector` - Proxy selector for outbound connections
|
||||
/// * `resolver` - DNS resolver
|
||||
pub fn new(
|
||||
auth_info: Option<(String, String)>,
|
||||
udp_enabled: bool,
|
||||
bind_ip: IpAddr,
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
resolver: Arc<dyn Resolver>,
|
||||
) -> Self {
|
||||
Self {
|
||||
auth_info,
|
||||
udp_enabled,
|
||||
bind_ip,
|
||||
proxy_selector,
|
||||
resolver,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,177 +82,282 @@ impl SocksTcpServerHandler {
|
||||
impl TcpServerHandler for SocksTcpServerHandler {
|
||||
async fn setup_server_stream(
|
||||
&self,
|
||||
mut server_stream: Box<dyn AsyncStream>,
|
||||
server_stream: Box<dyn AsyncStream>,
|
||||
) -> std::io::Result<TcpServerSetupResult> {
|
||||
let mut stream_reader = StreamReader::new_with_buffer_size(400);
|
||||
let stream_reader = StreamReader::new_with_buffer_size(400);
|
||||
let udp_bind_ip = if self.udp_enabled {
|
||||
Some(self.bind_ip)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
setup_socks_server_stream_inner(
|
||||
self.auth_info.as_ref(),
|
||||
udp_bind_ip,
|
||||
&self.proxy_selector,
|
||||
&self.resolver,
|
||||
server_stream,
|
||||
stream_reader,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
let socks_version = stream_reader.read_u8(&mut server_stream).await?;
|
||||
if socks_version != VER_SOCKS5 {
|
||||
/// Core SOCKS5 server setup logic.
|
||||
/// Can be called from SocksTcpServerHandler or MixedTcpServerHandler.
|
||||
///
|
||||
/// Takes ownership of `server_stream` and returns it in the result.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `auth_info` - Optional username/password for authentication
|
||||
/// * `udp_bind_ip` - If Some, UDP is enabled and this is the IP to bind UDP sockets on
|
||||
/// * `proxy_selector` - Proxy selector for outbound connections (only cloned if UDP request)
|
||||
/// * `resolver` - DNS resolver (only cloned if UDP request)
|
||||
/// * `server_stream` - The client TCP stream
|
||||
/// * `stream_reader` - Stream reader for parsing
|
||||
pub async fn setup_socks_server_stream_inner(
|
||||
auth_info: Option<&(String, String)>,
|
||||
udp_bind_ip: Option<IpAddr>,
|
||||
proxy_selector: &Arc<ClientProxySelector>,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
mut server_stream: Box<dyn AsyncStream>,
|
||||
mut stream_reader: StreamReader,
|
||||
) -> std::io::Result<TcpServerSetupResult> {
|
||||
let socks_version = stream_reader.read_u8(&mut server_stream).await?;
|
||||
if socks_version != VER_SOCKS5 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Unsupported SOCKS version: {socks_version}"),
|
||||
));
|
||||
}
|
||||
|
||||
let method_len = stream_reader.read_u8(&mut server_stream).await? as usize;
|
||||
if method_len < 1 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Invalid method length: {method_len}"),
|
||||
));
|
||||
}
|
||||
|
||||
let methods = stream_reader
|
||||
.read_slice(&mut server_stream, method_len)
|
||||
.await?;
|
||||
|
||||
let supported_method = if auth_info.is_some() {
|
||||
METHOD_USERNAME
|
||||
} else {
|
||||
METHOD_NONE
|
||||
};
|
||||
|
||||
if !methods.contains(&supported_method) {
|
||||
// TODO: consider writing response: [VER_SOCKS5, METHOD_INVALID]
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Supported SOCKS method not found",
|
||||
));
|
||||
}
|
||||
|
||||
write_all(&mut server_stream, &[VER_SOCKS5, supported_method]).await?;
|
||||
|
||||
if let Some((target_username, target_password)) = auth_info {
|
||||
let auth_version = stream_reader.read_u8(&mut server_stream).await?;
|
||||
if auth_version != VER_AUTH {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Unsupported SOCKS version: {socks_version}"),
|
||||
"Unsupported SOCKS auth version",
|
||||
));
|
||||
}
|
||||
|
||||
let method_len = stream_reader.read_u8(&mut server_stream).await? as usize;
|
||||
if method_len < 1 {
|
||||
let username_len = stream_reader.read_u8(&mut server_stream).await? as usize;
|
||||
if username_len == 0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Invalid method length: {method_len}"),
|
||||
"Unsupported SOCKS username length",
|
||||
));
|
||||
}
|
||||
|
||||
let methods = stream_reader
|
||||
.read_slice(&mut server_stream, method_len)
|
||||
let username = stream_reader
|
||||
.read_slice(&mut server_stream, username_len)
|
||||
.await?;
|
||||
|
||||
let supported_method = if self.auth_info.is_some() {
|
||||
METHOD_USERNAME
|
||||
} else {
|
||||
METHOD_NONE
|
||||
let username_str = match std::str::from_utf8(username) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Failed to decode username: {e}"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if !methods.contains(&supported_method) {
|
||||
// TODO: consider writing response: [VER_SOCKS5, METHOD_INVALID]
|
||||
// TODO: consider reading both username and password before checking.
|
||||
if target_username != username_str {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Supported SOCKS method not found",
|
||||
"SOCKS username does not match",
|
||||
));
|
||||
}
|
||||
|
||||
// Write response: [VER_SOCKS5, <selected method>]
|
||||
write_all(&mut server_stream, &[VER_SOCKS5, supported_method]).await?;
|
||||
let password_len = stream_reader.read_u8(&mut server_stream).await? as usize;
|
||||
if password_len == 0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Unsupported SOCKS password length",
|
||||
));
|
||||
}
|
||||
|
||||
if let Some((target_username, target_password)) = self.auth_info.as_ref() {
|
||||
let auth_version = stream_reader.read_u8(&mut server_stream).await?;
|
||||
if auth_version != VER_AUTH {
|
||||
let password = stream_reader
|
||||
.read_slice(&mut server_stream, password_len)
|
||||
.await?;
|
||||
|
||||
let password_str = match std::str::from_utf8(password) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Unsupported SOCKS auth version",
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Failed to decode password: {e}"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if target_password != password_str {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"SOCKS password does not match",
|
||||
));
|
||||
}
|
||||
|
||||
write_all(&mut server_stream, &[VER_AUTH, RESULT_SUCCESS]).await?;
|
||||
}
|
||||
|
||||
let connection_request = stream_reader.read_slice(&mut server_stream, 3).await?;
|
||||
if connection_request[0] != VER_SOCKS5 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Invalid SOCKS version: {}", connection_request[0]),
|
||||
));
|
||||
}
|
||||
|
||||
if connection_request[1] == CMD_UDP_ASSOCIATE {
|
||||
let bind_ip = match udp_bind_ip {
|
||||
Some(ip) => ip,
|
||||
None => {
|
||||
let response = build_error_response(REPLY_COMMAND_NOT_SUPPORTED);
|
||||
write_all(&mut server_stream, &response).await?;
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
"UDP ASSOCIATE not enabled",
|
||||
));
|
||||
}
|
||||
};
|
||||
return handle_udp_associate(
|
||||
bind_ip,
|
||||
proxy_selector,
|
||||
resolver,
|
||||
server_stream,
|
||||
&mut stream_reader,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if connection_request[1] != CMD_CONNECT {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Invalid command code: {}", connection_request[1]),
|
||||
));
|
||||
}
|
||||
|
||||
if connection_request[2] != 0x0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Invalid reserved bit",
|
||||
));
|
||||
}
|
||||
|
||||
static SUCCESS_RESPONSE: OnceLock<Box<[u8]>> = OnceLock::new();
|
||||
|
||||
let connection_success_response = SUCCESS_RESPONSE.get_or_init(|| {
|
||||
let mut response_bytes = vec![VER_SOCKS5, RESULT_SUCCESS, 0];
|
||||
let mut location_vec = write_location_to_vec(&NetLocation::new(
|
||||
Address::Ipv4(Ipv4Addr::new(127, 0, 0, 1)),
|
||||
65535,
|
||||
));
|
||||
response_bytes.append(&mut location_vec);
|
||||
response_bytes.into_boxed_slice()
|
||||
});
|
||||
|
||||
let location = read_location(&mut server_stream, &mut stream_reader).await?;
|
||||
|
||||
// Checks for UDP-over-TCP (UoT) magic addresses.
|
||||
if let Address::Hostname(host) = location.address() {
|
||||
if host == UOT_V1_MAGIC_ADDRESS {
|
||||
if udp_bind_ip.is_none() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
"UDP-over-TCP not enabled",
|
||||
));
|
||||
}
|
||||
|
||||
let username_len = stream_reader.read_u8(&mut server_stream).await? as usize;
|
||||
if username_len == 0 {
|
||||
// UoT V1: Multi-destination UDP (ATYP + address + port + length + data per packet).
|
||||
write_all(&mut server_stream, connection_success_response).await?;
|
||||
server_stream.flush().await?;
|
||||
|
||||
let mut uot_stream = UotV1ServerStream::new(server_stream);
|
||||
|
||||
// Feeds unparsed data since first UoT packet might be in same TCP segment.
|
||||
let unparsed_data = stream_reader.unparsed_data();
|
||||
if !unparsed_data.is_empty() {
|
||||
log::debug!(
|
||||
"SOCKS UoT V1: feeding {} bytes of initial data",
|
||||
unparsed_data.len()
|
||||
);
|
||||
uot_stream.feed_initial_data(unparsed_data);
|
||||
}
|
||||
|
||||
return Ok(TcpServerSetupResult::MultiDirectionalUdp {
|
||||
stream: Box::new(uot_stream),
|
||||
need_initial_flush: false,
|
||||
proxy_selector: proxy_selector.clone(),
|
||||
});
|
||||
} else if host == UOT_V2_MAGIC_ADDRESS {
|
||||
if udp_bind_ip.is_none() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Unsupported SOCKS username length",
|
||||
std::io::ErrorKind::Unsupported,
|
||||
"UDP-over-TCP not enabled",
|
||||
));
|
||||
}
|
||||
|
||||
let username = stream_reader
|
||||
.read_slice(&mut server_stream, username_len)
|
||||
.await?;
|
||||
// UoT V2: Request format is isConnect(u8) + ATYP + address + port.
|
||||
write_all(&mut server_stream, connection_success_response).await?;
|
||||
server_stream.flush().await?;
|
||||
|
||||
let username_str = match std::str::from_utf8(username) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Failed to decode username: {e}"),
|
||||
));
|
||||
let is_connect = stream_reader.read_u8(&mut server_stream).await?;
|
||||
log::debug!("SOCKS UoT V2: is_connect = {}", is_connect);
|
||||
|
||||
let destination = read_location(&mut server_stream, &mut stream_reader).await?;
|
||||
log::debug!("SOCKS UoT V2: destination = {:?}", destination);
|
||||
|
||||
if is_connect == 1 {
|
||||
// V2 Connect mode: Single destination, length-prefixed packets only
|
||||
let unparsed_data = stream_reader.unparsed_data();
|
||||
let mut uot_v2_stream = UotV2Stream::new(server_stream);
|
||||
if !unparsed_data.is_empty() {
|
||||
uot_v2_stream.feed_initial_read_data(unparsed_data)?;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: consider reading both username and password before checking.
|
||||
if target_username != username_str {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"SOCKS username does not match",
|
||||
));
|
||||
}
|
||||
return Ok(TcpServerSetupResult::BidirectionalUdp {
|
||||
remote_location: destination,
|
||||
stream: Box::new(uot_v2_stream),
|
||||
need_initial_flush: false,
|
||||
proxy_selector: proxy_selector.clone(),
|
||||
});
|
||||
} else {
|
||||
// V2 Non-connect mode: Same as V1 (multi-destination)
|
||||
let mut uot_stream = UotV1ServerStream::new(server_stream);
|
||||
|
||||
let password_len = stream_reader.read_u8(&mut server_stream).await? as usize;
|
||||
if password_len == 0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Unsupported SOCKS password length",
|
||||
));
|
||||
}
|
||||
|
||||
let password = stream_reader
|
||||
.read_slice(&mut server_stream, password_len)
|
||||
.await?;
|
||||
|
||||
let password_str = match std::str::from_utf8(password) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Failed to decode password: {e}"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if target_password != password_str {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"SOCKS password does not match",
|
||||
));
|
||||
}
|
||||
|
||||
write_all(&mut server_stream, &[VER_AUTH, RESULT_SUCCESS]).await?;
|
||||
}
|
||||
|
||||
let connection_request = stream_reader.read_slice(&mut server_stream, 3).await?;
|
||||
if connection_request[0] != VER_SOCKS5 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Invalid SOCKS version: {}", connection_request[0]),
|
||||
));
|
||||
}
|
||||
|
||||
if connection_request[1] == CMD_UDP_ASSOCIATE {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"UDP associate command is not supported",
|
||||
));
|
||||
}
|
||||
|
||||
if connection_request[1] != CMD_CONNECT {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Invalid command code: {}", connection_request[1]),
|
||||
));
|
||||
}
|
||||
|
||||
if connection_request[2] != 0x0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Invalid reserved bit",
|
||||
));
|
||||
}
|
||||
|
||||
static SUCCESS_RESPONSE: OnceLock<Box<[u8]>> = OnceLock::new();
|
||||
|
||||
let connection_success_response = SUCCESS_RESPONSE.get_or_init(|| {
|
||||
let mut response_bytes = vec![VER_SOCKS5, RESULT_SUCCESS, 0];
|
||||
let mut location_vec = write_location_to_vec(&NetLocation::new(
|
||||
Address::Ipv4(Ipv4Addr::new(127, 0, 0, 1)),
|
||||
65535,
|
||||
));
|
||||
response_bytes.append(&mut location_vec);
|
||||
response_bytes.into_boxed_slice()
|
||||
});
|
||||
|
||||
let location = read_location(&mut server_stream, &mut stream_reader).await?;
|
||||
|
||||
// Check for UDP-over-TCP (UoT) magic addresses
|
||||
if let Address::Hostname(host) = location.address() {
|
||||
if host == UOT_V1_MAGIC_ADDRESS {
|
||||
// UoT V1: Multi-destination UDP
|
||||
// Each packet has: ATYP + address + port + length + data
|
||||
// Send success response first
|
||||
write_all(&mut server_stream, connection_success_response).await?;
|
||||
server_stream.flush().await?;
|
||||
|
||||
let mut uot_stream = UotV1Stream::new(Box::new(server_stream));
|
||||
|
||||
// Feed any unparsed data (first UoT packet might be in same TCP segment)
|
||||
let unparsed_data = stream_reader.unparsed_data();
|
||||
if !unparsed_data.is_empty() {
|
||||
log::debug!(
|
||||
"SOCKS UoT V1: feeding {} bytes of initial data",
|
||||
"SOCKS UoT V2 non-connect: feeding {} bytes of initial data",
|
||||
unparsed_data.len()
|
||||
);
|
||||
uot_stream.feed_initial_data(unparsed_data);
|
||||
@@ -224,68 +366,155 @@ impl TcpServerHandler for SocksTcpServerHandler {
|
||||
return Ok(TcpServerSetupResult::MultiDirectionalUdp {
|
||||
stream: Box::new(uot_stream),
|
||||
need_initial_flush: false,
|
||||
override_proxy_provider: NoneOrOne::Unspecified,
|
||||
proxy_selector: proxy_selector.clone(),
|
||||
});
|
||||
} else if host == UOT_V2_MAGIC_ADDRESS {
|
||||
// UoT V2: Read request header first
|
||||
// Request: isConnect(u8) + ATYP + address + port
|
||||
// Send success response first
|
||||
write_all(&mut server_stream, connection_success_response).await?;
|
||||
server_stream.flush().await?;
|
||||
|
||||
let is_connect = stream_reader.read_u8(&mut server_stream).await?;
|
||||
log::debug!("SOCKS UoT V2: is_connect = {}", is_connect);
|
||||
|
||||
// Read destination address using SOCKS address format
|
||||
let destination = read_location(&mut server_stream, &mut stream_reader).await?;
|
||||
log::debug!("SOCKS UoT V2: destination = {:?}", destination);
|
||||
|
||||
if is_connect == 1 {
|
||||
// V2 Connect mode: Single destination, length-prefixed packets only
|
||||
let unparsed_data = stream_reader.unparsed_data();
|
||||
let mut uot_v2_stream = UotV2Stream::new(Box::new(server_stream));
|
||||
if !unparsed_data.is_empty() {
|
||||
uot_v2_stream.feed_initial_read_data(unparsed_data)?;
|
||||
}
|
||||
|
||||
return Ok(TcpServerSetupResult::BidirectionalUdp {
|
||||
remote_location: destination,
|
||||
stream: Box::new(uot_v2_stream),
|
||||
need_initial_flush: false,
|
||||
override_proxy_provider: NoneOrOne::Unspecified,
|
||||
});
|
||||
} else {
|
||||
// V2 Non-connect mode: Same as V1 (multi-destination)
|
||||
let mut uot_stream = UotV1Stream::new(Box::new(server_stream));
|
||||
|
||||
let unparsed_data = stream_reader.unparsed_data();
|
||||
if !unparsed_data.is_empty() {
|
||||
log::debug!(
|
||||
"SOCKS UoT V2 non-connect: feeding {} bytes of initial data",
|
||||
unparsed_data.len()
|
||||
);
|
||||
uot_stream.feed_initial_data(unparsed_data);
|
||||
}
|
||||
|
||||
return Ok(TcpServerSetupResult::MultiDirectionalUdp {
|
||||
stream: Box::new(uot_stream),
|
||||
need_initial_flush: false,
|
||||
override_proxy_provider: NoneOrOne::Unspecified,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TcpServerSetupResult::TcpForward {
|
||||
remote_location: location,
|
||||
stream: server_stream,
|
||||
need_initial_flush: true,
|
||||
connection_success_response: Some(
|
||||
connection_success_response.to_vec().into_boxed_slice(),
|
||||
),
|
||||
initial_remote_data: stream_reader.unparsed_data_owned(),
|
||||
override_proxy_provider: NoneOrOne::Unspecified,
|
||||
})
|
||||
Ok(TcpServerSetupResult::TcpForward {
|
||||
remote_location: location,
|
||||
stream: server_stream,
|
||||
need_initial_flush: true,
|
||||
connection_success_response: Some(connection_success_response.to_vec().into_boxed_slice()),
|
||||
initial_remote_data: stream_reader.unparsed_data_owned(),
|
||||
proxy_selector: proxy_selector.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Handle SOCKS5 UDP ASSOCIATE command.
|
||||
///
|
||||
/// Takes ownership of `server_stream` for use in the spawned UDP relay task.
|
||||
async fn handle_udp_associate(
|
||||
bind_ip: IpAddr,
|
||||
proxy_selector: &Arc<ClientProxySelector>,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
mut server_stream: Box<dyn AsyncStream>,
|
||||
stream_reader: &mut StreamReader,
|
||||
) -> std::io::Result<TcpServerSetupResult> {
|
||||
// Read client's hint address (DST.ADDR:DST.PORT) - we ignore this per RFC
|
||||
let _client_hint = read_location(&mut server_stream, stream_reader).await?;
|
||||
log::debug!("SOCKS5 UDP ASSOCIATE: client hint = {:?}", _client_hint);
|
||||
|
||||
// Uses 2MB buffer to prevent packet drops during bursts.
|
||||
const UDP_BUFFER_SIZE: usize = 2 * 1024 * 1024;
|
||||
let udp_bind_addr = SocketAddr::new(bind_ip, 0);
|
||||
let udp_socket: std::net::UdpSocket =
|
||||
match crate::socket_util::new_socket2_udp_socket_with_buffer_size(
|
||||
bind_ip.is_ipv6(),
|
||||
None,
|
||||
Some(udp_bind_addr),
|
||||
false,
|
||||
Some(UDP_BUFFER_SIZE),
|
||||
) {
|
||||
Ok(s) => s.into(),
|
||||
Err(e) => {
|
||||
log::error!("Failed to bind UDP socket: {}", e);
|
||||
let response = build_error_response(REPLY_GENERAL_FAILURE);
|
||||
write_all(&mut server_stream, &response).await?;
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let udp_socket = tokio::net::UdpSocket::from_std(udp_socket)?;
|
||||
|
||||
let bound_addr = udp_socket.local_addr()?;
|
||||
log::info!("SOCKS5 UDP ASSOCIATE: bound UDP relay at {}", bound_addr);
|
||||
|
||||
let response = build_udp_associate_response(bound_addr);
|
||||
write_all(&mut server_stream, &response).await?;
|
||||
server_stream.flush().await?;
|
||||
|
||||
let relay_stream = Socks5UdpRelayStream::new(udp_socket);
|
||||
let proxy_selector = proxy_selector.clone();
|
||||
let resolver = resolver.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
run_udp_associate(server_stream, relay_stream, proxy_selector, resolver).await
|
||||
{
|
||||
log::debug!("SOCKS5 UDP ASSOCIATE ended: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(TcpServerSetupResult::AlreadyHandled)
|
||||
}
|
||||
|
||||
/// Build a SOCKS5 error response.
|
||||
fn build_error_response(reply_code: u8) -> Vec<u8> {
|
||||
vec![
|
||||
VER_SOCKS5,
|
||||
reply_code,
|
||||
0x00, // RSV
|
||||
ADDR_TYPE_IPV4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0, // 0.0.0.0
|
||||
0,
|
||||
0, // port 0
|
||||
]
|
||||
}
|
||||
|
||||
/// Build a SOCKS5 UDP ASSOCIATE success response.
|
||||
fn build_udp_associate_response(bound_addr: SocketAddr) -> Vec<u8> {
|
||||
let mut response = vec![VER_SOCKS5, REPLY_SUCCESS, 0x00];
|
||||
|
||||
match bound_addr {
|
||||
SocketAddr::V4(v4) => {
|
||||
response.push(ADDR_TYPE_IPV4);
|
||||
response.extend_from_slice(&v4.ip().octets());
|
||||
response.extend_from_slice(&v4.port().to_be_bytes());
|
||||
}
|
||||
SocketAddr::V6(v6) => {
|
||||
response.push(ADDR_TYPE_IPV6);
|
||||
response.extend_from_slice(&v6.ip().octets());
|
||||
response.extend_from_slice(&v6.port().to_be_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
/// Run the UDP ASSOCIATE relay.
|
||||
///
|
||||
/// This function:
|
||||
/// 1. Uses per-destination routing for UDP packets
|
||||
/// 2. Monitors the TCP connection for termination
|
||||
///
|
||||
/// When the TCP connection closes, the UDP relay is terminated.
|
||||
async fn run_udp_associate(
|
||||
mut tcp_stream: Box<dyn AsyncStream>,
|
||||
relay_stream: Socks5UdpRelayStream,
|
||||
proxy_selector: Arc<ClientProxySelector>,
|
||||
resolver: Arc<dyn Resolver>,
|
||||
) -> std::io::Result<()> {
|
||||
let server_stream: Box<dyn crate::async_stream::AsyncTargetedMessageStream> =
|
||||
Box::new(relay_stream);
|
||||
|
||||
// Runs per-destination routing in parallel with TCP monitoring.
|
||||
tokio::select! {
|
||||
result = run_udp_routing(ServerStream::Targeted(server_stream), proxy_selector, resolver, false) => {
|
||||
result
|
||||
}
|
||||
_ = monitor_tcp_close(&mut tcp_stream) => {
|
||||
log::debug!("SOCKS5 UDP ASSOCIATE: TCP connection closed, terminating relay");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Monitor the TCP connection for closure.
|
||||
///
|
||||
/// Per RFC 1928, the UDP association terminates when the TCP connection closes.
|
||||
/// We read and discard any data until EOF or error.
|
||||
async fn monitor_tcp_close(stream: &mut Box<dyn AsyncStream>) -> std::io::Result<()> {
|
||||
let mut buf = [0u8; 1024];
|
||||
loop {
|
||||
match stream.read(&mut buf).await {
|
||||
Ok(0) => return Ok(()), // EOF
|
||||
Ok(_) => continue, // Discard data
|
||||
Err(_) => return Ok(()), // Error = close
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,7 +585,6 @@ impl TcpClientHandler for SocksTcpClientHandler {
|
||||
}
|
||||
|
||||
if self.has_auth {
|
||||
// read auth response
|
||||
let auth_version = stream_reader.read_u8(&mut client_stream).await?;
|
||||
if auth_version != VER_AUTH {
|
||||
return Err(std::io::Error::new(
|
||||
@@ -438,7 +666,7 @@ pub async fn read_location<T: AsyncReadExt + Unpin>(
|
||||
ADDR_TYPE_IPV6 => {
|
||||
let address_bytes = stream_reader.read_slice(stream, 18).await?;
|
||||
|
||||
let v6addr = Ipv6Addr::new(
|
||||
let v6addr = std::net::Ipv6Addr::new(
|
||||
u16::from_be_bytes(address_bytes[0..2].try_into().unwrap()),
|
||||
u16::from_be_bytes(address_bytes[2..4].try_into().unwrap()),
|
||||
u16::from_be_bytes(address_bytes[4..6].try_into().unwrap()),
|
||||
@@ -474,9 +702,7 @@ pub async fn read_location<T: AsyncReadExt + Unpin>(
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
// Although this is supposed to be a hostname, some clients will pass
|
||||
// ipv4 and ipv6 addresses as well, so parse it rather than directly
|
||||
// using Address:Hostname enum.
|
||||
// Parses as Address since some clients pass IP addresses as hostnames.
|
||||
Ok(NetLocation::new(Address::from(address_str)?, port))
|
||||
}
|
||||
|
||||
@@ -487,6 +713,62 @@ pub async fn read_location<T: AsyncReadExt + Unpin>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a SOCKS5-format address directly from an AsyncRead stream.
|
||||
///
|
||||
/// This is a simpler version of `read_location` that doesn't use `StreamReader`.
|
||||
/// Use this when the protocol has its own framing (e.g., H2 streams, AnyTLS frames).
|
||||
///
|
||||
/// SOCKS5 address format (also used in UoT V2 Request header):
|
||||
/// - 0x01: IPv4 (4 bytes) + port (2 bytes)
|
||||
/// - 0x03: Domain (1 byte len + domain) + port (2 bytes)
|
||||
/// - 0x04: IPv6 (16 bytes) + port (2 bytes)
|
||||
pub async fn read_location_direct<T: AsyncReadExt + Unpin>(
|
||||
stream: &mut T,
|
||||
) -> std::io::Result<NetLocation> {
|
||||
let mut addr_type = [0u8; 1];
|
||||
stream.read_exact(&mut addr_type).await?;
|
||||
|
||||
match addr_type[0] {
|
||||
ADDR_TYPE_IPV4 => {
|
||||
let mut buf = [0u8; 6];
|
||||
stream.read_exact(&mut buf).await?;
|
||||
let addr = Ipv4Addr::new(buf[0], buf[1], buf[2], buf[3]);
|
||||
let port = u16::from_be_bytes([buf[4], buf[5]]);
|
||||
Ok(NetLocation::new(Address::Ipv4(addr), port))
|
||||
}
|
||||
ADDR_TYPE_IPV6 => {
|
||||
let mut buf = [0u8; 18];
|
||||
stream.read_exact(&mut buf).await?;
|
||||
let addr = std::net::Ipv6Addr::from(<[u8; 16]>::try_from(&buf[0..16]).unwrap());
|
||||
let port = u16::from_be_bytes([buf[16], buf[17]]);
|
||||
Ok(NetLocation::new(Address::Ipv6(addr), port))
|
||||
}
|
||||
ADDR_TYPE_DOMAIN_NAME => {
|
||||
let mut len = [0u8; 1];
|
||||
stream.read_exact(&mut len).await?;
|
||||
let domain_len = len[0] as usize;
|
||||
|
||||
let mut buf = vec![0u8; domain_len + 2];
|
||||
stream.read_exact(&mut buf).await?;
|
||||
|
||||
let domain = std::str::from_utf8(&buf[..domain_len]).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Invalid domain encoding: {e}"),
|
||||
)
|
||||
})?;
|
||||
let port = u16::from_be_bytes([buf[domain_len], buf[domain_len + 1]]);
|
||||
|
||||
// Parse as Address to handle IP literals passed as domain
|
||||
Ok(NetLocation::new(Address::from(domain)?, port))
|
||||
}
|
||||
_ => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Unknown address type: {}", addr_type[0]),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_location_to_vec(location: &NetLocation) -> Vec<u8> {
|
||||
let (address, port) = location.components();
|
||||
let mut vec = match address {
|
||||
|
||||
+61
-9
@@ -18,7 +18,7 @@ impl StreamReader {
|
||||
}
|
||||
|
||||
pub fn new_with_buffer_size(buffer_size: usize) -> Self {
|
||||
// note that `buffer_size` also represents the maximum line length that can be read.
|
||||
// The buffer_size also determines the maximum line length that can be read.
|
||||
Self {
|
||||
buf: allocate_vec(buffer_size).into_boxed_slice(),
|
||||
start_offset: 0usize,
|
||||
@@ -56,7 +56,7 @@ impl StreamReader {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// strip crlf
|
||||
// Strips CRLF.
|
||||
let line = &mut self.buf[self.start_offset..newline_pos - 1];
|
||||
let new_start_offset = newline_pos + 1;
|
||||
if new_start_offset == search_end_offset {
|
||||
@@ -73,9 +73,9 @@ impl StreamReader {
|
||||
|
||||
self.read(stream).await?;
|
||||
|
||||
// Only search through new data.
|
||||
// Only searches through new data.
|
||||
if previous_start_offset != self.start_offset {
|
||||
// this can only move to zero when reset_buf_offset is called.
|
||||
// Can only move to zero when reset_buf_offset is called.
|
||||
assert!(self.start_offset == 0);
|
||||
search_start_offset = search_end_offset - previous_start_offset;
|
||||
} else {
|
||||
@@ -117,6 +117,56 @@ impl StreamReader {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Peek at the first byte without consuming it.
|
||||
/// Ensures at least 1 byte is buffered, then returns it.
|
||||
pub async fn peek_u8<T: AsyncReadExt + Unpin>(
|
||||
&mut self,
|
||||
stream: &mut T,
|
||||
) -> std::io::Result<u8> {
|
||||
while self.end_offset - self.start_offset < 1 {
|
||||
self.read(stream).await?;
|
||||
}
|
||||
// Returns the byte without advancing start_offset.
|
||||
Ok(self.buf[self.start_offset])
|
||||
}
|
||||
|
||||
/// Peek at the first `len` bytes without consuming them.
|
||||
/// Ensures at least `len` bytes are buffered, then returns a reference to them.
|
||||
pub async fn peek_slice<T: AsyncReadExt + Unpin + ?Sized>(
|
||||
&mut self,
|
||||
stream: &mut T,
|
||||
len: usize,
|
||||
) -> std::io::Result<&[u8]> {
|
||||
if len > self.buf.len() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"Requested length {} exceeds buffer size {}",
|
||||
len,
|
||||
self.buf.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
while self.end_offset - self.start_offset < len {
|
||||
self.read(stream).await?;
|
||||
}
|
||||
// Returns the slice without advancing start_offset.
|
||||
Ok(&self.buf[self.start_offset..self.start_offset + len])
|
||||
}
|
||||
|
||||
/// Consume (skip) `len` bytes that were previously peeked.
|
||||
/// This advances the read position without returning any data.
|
||||
pub fn consume(&mut self, len: usize) {
|
||||
let new_start_offset = self.start_offset + len;
|
||||
debug_assert!(new_start_offset <= self.end_offset);
|
||||
if new_start_offset == self.end_offset {
|
||||
self.start_offset = 0;
|
||||
self.end_offset = 0;
|
||||
} else {
|
||||
self.start_offset = new_start_offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_u16_be<T: AsyncReadExt + Unpin>(
|
||||
&mut self,
|
||||
stream: &mut T,
|
||||
@@ -136,7 +186,7 @@ impl StreamReader {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub async fn read_slice<T: AsyncReadExt + Unpin>(
|
||||
pub async fn read_slice<T: AsyncReadExt + Unpin + ?Sized>(
|
||||
&mut self,
|
||||
stream: &mut T,
|
||||
len: usize,
|
||||
@@ -188,9 +238,11 @@ impl StreamReader {
|
||||
}
|
||||
}
|
||||
|
||||
async fn read<T: AsyncReadExt + Unpin>(&mut self, stream: &mut T) -> std::io::Result<()> {
|
||||
// Note that read() needs to work for blocking I/O. So we need to return
|
||||
// immediately after a single read() call.
|
||||
async fn read<T: AsyncReadExt + Unpin + ?Sized>(
|
||||
&mut self,
|
||||
stream: &mut T,
|
||||
) -> std::io::Result<()> {
|
||||
// Returns immediately after a single read() call to support blocking I/O.
|
||||
if self.is_cache_full() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::ConnectionAborted,
|
||||
@@ -198,7 +250,7 @@ impl StreamReader {
|
||||
));
|
||||
}
|
||||
|
||||
// Clear the offset so there's space for the next line.
|
||||
// Clears the offset to make space for the next read.
|
||||
self.reset_buf_offset();
|
||||
|
||||
loop {
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
//! A worker pool for processing futures without spawning per-future tasks.
|
||||
//!
|
||||
//! This module provides a way to process many futures concurrently without spawning
|
||||
//! a separate tokio task for each one. Instead, a fixed number of worker tasks are
|
||||
//! spawned, and each worker uses `FuturesUnordered` to poll multiple futures.
|
||||
//!
|
||||
//! This is designed to solve the Quinn connection driver starvation issue documented
|
||||
//! in QUINN_HANG_BUG.md - when too many tasks are spawned via `tokio::spawn`, Quinn's
|
||||
//! `ConnectionDriver` task gets starved by tokio's scheduler, causing connection
|
||||
//! timeouts and hangs.
|
||||
//!
|
||||
//! By limiting the number of spawned tasks to a fixed pool size (e.g., 4-8 workers),
|
||||
//! the connection driver has much less competition for scheduler time.
|
||||
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use log::{debug, error};
|
||||
use std::future::Future;
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Type alias for boxed futures that the worker pool processes.
|
||||
/// Returns `std::io::Result<()>` to match copy_bidirectional and other copy utilities.
|
||||
pub type BoxedFuture = Pin<Box<dyn Future<Output = io::Result<()>> + Send + 'static>>;
|
||||
|
||||
/// A pool of worker tasks that process futures without spawning per-future tasks.
|
||||
///
|
||||
/// Each worker maintains a `FuturesUnordered` and receives new futures via a channel.
|
||||
/// This drastically reduces the number of spawned tasks compared to spawning one
|
||||
/// task per stream/future.
|
||||
///
|
||||
/// Uses type-erased boxed futures to allow submitting different future types to
|
||||
/// the same pool.
|
||||
pub struct StreamWorkerPool {
|
||||
/// Senders to each worker, for round-robin distribution
|
||||
senders: Vec<mpsc::UnboundedSender<BoxedFuture>>,
|
||||
/// Counter for round-robin distribution
|
||||
next_worker: AtomicUsize,
|
||||
}
|
||||
|
||||
impl StreamWorkerPool {
|
||||
/// Create a new worker pool with the specified number of workers.
|
||||
///
|
||||
/// Each worker is spawned as a tokio task and will process futures sent to it
|
||||
/// using `FuturesUnordered` for concurrent (but not parallel) execution within
|
||||
/// each worker.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `num_workers` - Number of worker tasks to spawn. Recommended: 4-8 for
|
||||
/// typical QUIC proxy workloads. More workers = more parallelism but more
|
||||
/// task competition with Quinn's driver.
|
||||
pub fn new(num_workers: usize) -> Self {
|
||||
assert!(num_workers > 0, "must have at least one worker");
|
||||
|
||||
let mut senders = Vec::with_capacity(num_workers);
|
||||
|
||||
for worker_id in 0..num_workers {
|
||||
let (tx, rx) = mpsc::unbounded_channel::<BoxedFuture>();
|
||||
senders.push(tx);
|
||||
|
||||
tokio::spawn(worker_loop(worker_id, rx));
|
||||
}
|
||||
|
||||
Self {
|
||||
senders,
|
||||
next_worker: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Submit a future to be processed by a worker.
|
||||
///
|
||||
/// The future is distributed to workers using round-robin scheduling.
|
||||
/// The future should return `std::io::Result<()>` to match copy_bidirectional
|
||||
/// and other copy utilities. Errors are logged by the worker.
|
||||
///
|
||||
/// Returns `true` if the future was successfully submitted, `false` if
|
||||
/// the worker channel is closed (worker task has terminated).
|
||||
pub fn submit<F>(&self, future: F) -> bool
|
||||
where
|
||||
F: Future<Output = io::Result<()>> + Send + 'static,
|
||||
{
|
||||
// Round-robin distribution
|
||||
let worker_idx = self.next_worker.fetch_add(1, Ordering::Relaxed) % self.senders.len();
|
||||
self.senders[worker_idx].send(Box::pin(future)).is_ok()
|
||||
}
|
||||
|
||||
/// Returns the number of workers in the pool.
|
||||
pub fn num_workers(&self) -> usize {
|
||||
self.senders.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// The worker loop that processes futures from the channel.
|
||||
async fn worker_loop(worker_id: usize, mut rx: mpsc::UnboundedReceiver<BoxedFuture>) {
|
||||
let mut futures: FuturesUnordered<BoxedFuture> = FuturesUnordered::new();
|
||||
let mut yield_counter: u8 = 0;
|
||||
|
||||
loop {
|
||||
// Use biased select to prioritize polling existing futures over accepting new ones.
|
||||
// This is critical because FuturesUnordered only polls futures when they have
|
||||
// wake-up notifications, and newly pushed futures need an initial poll to register
|
||||
// their wakers. By prioritizing futures.next(), we ensure existing work progresses
|
||||
// and new futures get their initial poll before we accept more work.
|
||||
tokio::select! {
|
||||
biased;
|
||||
|
||||
// Poll existing futures first (higher priority)
|
||||
Some(result) = futures.next(), if !futures.is_empty() => {
|
||||
// Log errors from completed futures
|
||||
if let Err(e) = result {
|
||||
error!("Stream worker {worker_id}: {e}");
|
||||
}
|
||||
}
|
||||
// Accept new futures from the channel (lower priority)
|
||||
Some(future) = rx.recv() => {
|
||||
futures.push(future);
|
||||
}
|
||||
// Channel closed and no more futures to process
|
||||
else => {
|
||||
if futures.is_empty() {
|
||||
debug!("Worker {worker_id} shutting down: channel closed and no pending futures");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic yield to give other tasks (especially Quinn's driver) a chance to run.
|
||||
yield_counter = yield_counter.wrapping_add(1);
|
||||
if yield_counter == 0 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A shared worker pool that can be cloned and used from multiple tasks.
|
||||
///
|
||||
/// This is a convenience wrapper around `Arc<StreamWorkerPool>`.
|
||||
#[derive(Clone)]
|
||||
pub struct SharedWorkerPool {
|
||||
inner: Arc<StreamWorkerPool>,
|
||||
}
|
||||
|
||||
impl SharedWorkerPool {
|
||||
/// Create a new shared worker pool with the specified number of workers.
|
||||
pub fn new(num_workers: usize) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(StreamWorkerPool::new(num_workers)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Submit a future to be processed by a worker.
|
||||
pub fn submit<F>(&self, future: F) -> bool
|
||||
where
|
||||
F: Future<Output = io::Result<()>> + Send + 'static,
|
||||
{
|
||||
self.inner.submit(future)
|
||||
}
|
||||
|
||||
/// Returns the number of workers in the pool.
|
||||
pub fn num_workers(&self) -> usize {
|
||||
self.inner.num_workers()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_worker_pool_basic() {
|
||||
let pool = StreamWorkerPool::new(4);
|
||||
let counter = Arc::new(AtomicU32::new(0));
|
||||
|
||||
// Submit 100 futures
|
||||
for _ in 0..100 {
|
||||
let counter = counter.clone();
|
||||
pool.submit(async move {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
// Give workers time to process
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
assert_eq!(counter.load(Ordering::Relaxed), 100);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_worker_pool_concurrent() {
|
||||
let pool = StreamWorkerPool::new(4);
|
||||
let counter = Arc::new(AtomicU32::new(0));
|
||||
let max_concurrent = Arc::new(AtomicU32::new(0));
|
||||
let current_concurrent = Arc::new(AtomicU32::new(0));
|
||||
|
||||
// Submit futures that take some time
|
||||
for _ in 0..20 {
|
||||
let counter = counter.clone();
|
||||
let max_concurrent = max_concurrent.clone();
|
||||
let current_concurrent = current_concurrent.clone();
|
||||
pool.submit(async move {
|
||||
let current = current_concurrent.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
max_concurrent.fetch_max(current, Ordering::Relaxed);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
|
||||
current_concurrent.fetch_sub(1, Ordering::Relaxed);
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for completion
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
assert_eq!(counter.load(Ordering::Relaxed), 20);
|
||||
// With 4 workers, we should see some concurrency
|
||||
assert!(max_concurrent.load(Ordering::Relaxed) > 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_shared_worker_pool() {
|
||||
let pool = SharedWorkerPool::new(2);
|
||||
let counter = Arc::new(AtomicU32::new(0));
|
||||
|
||||
// Clone and use from multiple "tasks"
|
||||
let pool2 = pool.clone();
|
||||
let counter2 = counter.clone();
|
||||
|
||||
pool.submit({
|
||||
let counter = counter.clone();
|
||||
async move {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
|
||||
pool2.submit(async move {
|
||||
counter2.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
|
||||
assert_eq!(counter.load(Ordering::Relaxed), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_worker_pool_error_handling() {
|
||||
let pool = StreamWorkerPool::new(2);
|
||||
let success_counter = Arc::new(AtomicU32::new(0));
|
||||
let error_counter = Arc::new(AtomicU32::new(0));
|
||||
|
||||
// Submit a mix of successful and failing futures
|
||||
for i in 0..10 {
|
||||
let success_counter = success_counter.clone();
|
||||
let error_counter = error_counter.clone();
|
||||
pool.submit(async move {
|
||||
if i % 2 == 0 {
|
||||
success_counter.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
} else {
|
||||
error_counter.fetch_add(1, Ordering::Relaxed);
|
||||
Err(io::Error::new(io::ErrorKind::Other, "test error"))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// All futures should complete, regardless of success/failure
|
||||
assert_eq!(success_counter.load(Ordering::Relaxed), 5);
|
||||
assert_eq!(error_counter.load(Ordering::Relaxed), 5);
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,8 @@ use async_trait::async_trait;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::address::NetLocation;
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::tcp_handler::{TcpClientSetupResult, TcpClientUdpSetupResult, UdpStreamRequest};
|
||||
use crate::async_stream::{AsyncMessageStream, AsyncStream};
|
||||
use crate::tcp::tcp_handler::TcpClientSetupResult;
|
||||
|
||||
/// Trait for proxy protocol connectors.
|
||||
///
|
||||
@@ -54,14 +54,17 @@ pub trait ProxyConnector: Send + Sync + Debug {
|
||||
target: &NetLocation,
|
||||
) -> std::io::Result<TcpClientSetupResult>;
|
||||
|
||||
/// Setup UDP-over-TCP on existing stream.
|
||||
/// Setup bidirectional UDP-over-TCP on existing stream.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `stream` - Existing transport stream
|
||||
/// * `request` - The type of UDP stream requested
|
||||
async fn setup_udp_stream(
|
||||
/// * `target` - The destination for all UDP packets
|
||||
///
|
||||
/// # Returns
|
||||
/// An AsyncMessageStream that sends/receives UDP packets to/from the target.
|
||||
async fn setup_udp_bidirectional(
|
||||
&self,
|
||||
stream: Box<dyn AsyncStream>,
|
||||
request: UdpStreamRequest,
|
||||
) -> std::io::Result<TcpClientUdpSetupResult>;
|
||||
target: NetLocation,
|
||||
) -> std::io::Result<Box<dyn AsyncMessageStream>>;
|
||||
}
|
||||
|
||||
@@ -6,15 +6,12 @@
|
||||
use async_trait::async_trait;
|
||||
use log::debug;
|
||||
|
||||
use super::proxy_connector::ProxyConnector;
|
||||
use super::tcp_client_handler_factory::create_tcp_client_handler;
|
||||
use crate::address::NetLocation;
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::async_stream::{AsyncMessageStream, AsyncStream};
|
||||
use crate::config::ClientConfig;
|
||||
use crate::tcp_handler::{
|
||||
TcpClientHandler, TcpClientSetupResult, TcpClientUdpSetupResult, UdpStreamRequest,
|
||||
};
|
||||
|
||||
use super::proxy_connector::ProxyConnector;
|
||||
use crate::tcp::tcp_handler::{TcpClientHandler, TcpClientSetupResult};
|
||||
|
||||
/// Implementation of ProxyConnector for proxy protocol setup.
|
||||
///
|
||||
@@ -81,17 +78,17 @@ impl ProxyConnector for ProxyConnectorImpl {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn setup_udp_stream(
|
||||
async fn setup_udp_bidirectional(
|
||||
&self,
|
||||
stream: Box<dyn AsyncStream>,
|
||||
request: UdpStreamRequest,
|
||||
) -> std::io::Result<TcpClientUdpSetupResult> {
|
||||
target: NetLocation,
|
||||
) -> std::io::Result<Box<dyn AsyncMessageStream>> {
|
||||
debug!(
|
||||
"[ProxyConnector] setup_udp_stream: {}, request: {}",
|
||||
self.location, request
|
||||
"[ProxyConnector] setup_udp_bidirectional: {} -> {}",
|
||||
self.location, target
|
||||
);
|
||||
self.client_handler
|
||||
.setup_client_udp_stream(stream, request)
|
||||
.setup_client_udp_bidirectional(stream, target)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
+10
-11
@@ -20,11 +20,9 @@ use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::async_stream::AsyncStream;
|
||||
use crate::resolver::Resolver;
|
||||
use crate::tcp_handler::{TcpClientUdpSetupResult, UdpStreamRequest};
|
||||
|
||||
use crate::address::NetLocation;
|
||||
use crate::async_stream::{AsyncMessageStream, AsyncStream};
|
||||
use crate::resolver::Resolver;
|
||||
|
||||
/// Trait for creating socket connections at hop 0.
|
||||
///
|
||||
@@ -43,16 +41,17 @@ pub trait SocketConnector: Send + Sync + Debug {
|
||||
address: &NetLocation,
|
||||
) -> std::io::Result<Box<dyn AsyncStream>>;
|
||||
|
||||
/// Create UDP socket(s) for the given request type.
|
||||
///
|
||||
/// Returns matched server/client stream pair ready for copying.
|
||||
/// Create a UDP socket for bidirectional communication with a fixed target.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `resolver` - DNS resolver for address resolution
|
||||
/// * `request` - The type of UDP stream requested
|
||||
async fn connect_udp(
|
||||
/// * `target` - The destination for all UDP packets
|
||||
///
|
||||
/// # Returns
|
||||
/// An AsyncMessageStream connected to the target.
|
||||
async fn connect_udp_bidirectional(
|
||||
&self,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
request: UdpStreamRequest,
|
||||
) -> std::io::Result<TcpClientUdpSetupResult>;
|
||||
target: NetLocation,
|
||||
) -> std::io::Result<Box<dyn AsyncMessageStream>>;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
//! Handles TCP and QUIC transports with bind_interface support.
|
||||
//! Created from the socket-related fields of any ClientConfig.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, error};
|
||||
use tokio::io::ReadBuf;
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
use crate::address::NetLocation;
|
||||
use crate::async_stream::AsyncStream;
|
||||
@@ -15,13 +20,8 @@ use crate::config::{ClientConfig, ClientQuicConfig, Transport};
|
||||
use crate::quic_stream::QuicStream;
|
||||
use crate::resolver::{Resolver, resolve_single_address};
|
||||
use crate::rustls_config_util::create_client_config;
|
||||
use crate::socket_util::{
|
||||
new_reuse_udp_sockets, new_tcp_socket, new_udp_socket, set_tcp_keepalive,
|
||||
};
|
||||
use crate::tcp_handler::{TcpClientUdpSetupResult, UdpStreamRequest};
|
||||
use crate::socket_util::{new_tcp_socket, new_udp_socket, set_tcp_keepalive};
|
||||
use crate::thread_util::get_num_threads;
|
||||
use crate::udp_multi_message_stream::UdpMultiMessageStream;
|
||||
use crate::udp_session_message_stream::{UdpSessionMessageStream, UdpSocketConfig};
|
||||
|
||||
use super::socket_connector::SocketConnector;
|
||||
|
||||
@@ -103,10 +103,10 @@ impl SocketConnectorImpl {
|
||||
} = config.quic_settings.clone().unwrap_or_default();
|
||||
|
||||
let sni_hostname = if sni_hostname.is_unspecified() {
|
||||
if default_sni_hostname.is_some() {
|
||||
if let Some(ref hostname) = default_sni_hostname {
|
||||
debug!(
|
||||
"Using default sni hostname for QUIC client connection: {}",
|
||||
default_sni_hostname.as_ref().unwrap()
|
||||
hostname
|
||||
);
|
||||
}
|
||||
default_sni_hostname.clone()
|
||||
@@ -134,6 +134,7 @@ impl SocketConnectorImpl {
|
||||
alpn_protocols.into_vec(),
|
||||
sni_hostname.is_some(),
|
||||
key_and_cert_bytes,
|
||||
false, // tls13_only - QUIC enforces TLS 1.3 anyway
|
||||
);
|
||||
|
||||
let quic_client_config = quinn::crypto::rustls::QuicClientConfig::with_initial(
|
||||
@@ -270,56 +271,112 @@ impl SocketConnector for SocketConnectorImpl {
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_udp(
|
||||
async fn connect_udp_bidirectional(
|
||||
&self,
|
||||
resolver: &Arc<dyn Resolver>,
|
||||
request: UdpStreamRequest,
|
||||
) -> std::io::Result<TcpClientUdpSetupResult> {
|
||||
debug!("[SocketConnector] connect_udp called, request: {}", request);
|
||||
target: NetLocation,
|
||||
) -> std::io::Result<Box<dyn crate::async_stream::AsyncMessageStream>> {
|
||||
debug!(
|
||||
"[SocketConnector] connect_udp_bidirectional called, target: {}",
|
||||
target
|
||||
);
|
||||
|
||||
match request {
|
||||
UdpStreamRequest::Bidirectional {
|
||||
server_stream,
|
||||
target,
|
||||
} => {
|
||||
let remote_addr = resolve_single_address(resolver, &target).await?;
|
||||
let client_socket =
|
||||
new_udp_socket(remote_addr.is_ipv6(), self.bind_interface.clone())?;
|
||||
client_socket.connect(remote_addr).await?;
|
||||
Ok(TcpClientUdpSetupResult::Bidirectional {
|
||||
server_stream,
|
||||
client_stream: Box::new(client_socket),
|
||||
})
|
||||
}
|
||||
let remote_addr = resolve_single_address(resolver, &target).await?;
|
||||
let client_socket = new_udp_socket(remote_addr.is_ipv6(), self.bind_interface.clone())?;
|
||||
|
||||
UdpStreamRequest::MultiDirectional { server_stream } => {
|
||||
const NUM_SOCKETS: usize = 4;
|
||||
let client_sockets =
|
||||
new_reuse_udp_sockets(true, self.bind_interface.clone(), NUM_SOCKETS)?;
|
||||
let client_sockets = client_sockets.into_iter().map(Arc::new).collect();
|
||||
Ok(TcpClientUdpSetupResult::MultiDirectional {
|
||||
server_stream,
|
||||
client_stream: Box::new(UdpMultiMessageStream::new(
|
||||
client_sockets,
|
||||
resolver.clone(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
// Don't use connect() - wrap in UnconnectedUdpSocket instead.
|
||||
// A connected UDP socket filters incoming packets by source address,
|
||||
// which breaks when bind_interface causes packets to arrive from
|
||||
// a different source than the target address.
|
||||
Ok(Box::new(UnconnectedUdpSocket::new(
|
||||
client_socket,
|
||||
remote_addr,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
UdpStreamRequest::SessionBased { server_stream } => {
|
||||
let config = UdpSocketConfig {
|
||||
bind_interface: self.bind_interface.clone(),
|
||||
};
|
||||
let session_manager = UdpSessionMessageStream::new(config);
|
||||
Ok(TcpClientUdpSetupResult::SessionBased {
|
||||
server_stream,
|
||||
client_stream: Box::new(session_manager),
|
||||
})
|
||||
}
|
||||
/// A UDP socket wrapper that tracks the destination and uses send_to/recv_from.
|
||||
/// Unlike a connected UDP socket, this accepts incoming packets from any source.
|
||||
struct UnconnectedUdpSocket {
|
||||
socket: UdpSocket,
|
||||
destination: SocketAddr,
|
||||
}
|
||||
|
||||
impl UnconnectedUdpSocket {
|
||||
fn new(socket: UdpSocket, destination: SocketAddr) -> Self {
|
||||
Self {
|
||||
socket,
|
||||
destination,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::async_stream::AsyncReadMessage for UnconnectedUdpSocket {
|
||||
fn poll_read_message(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
let this = self.get_mut();
|
||||
match this.socket.poll_recv_from(cx, buf) {
|
||||
Poll::Ready(Ok(addr)) => {
|
||||
log::debug!(
|
||||
"[UnconnectedUdp] Received {} bytes from {} (target: {})",
|
||||
buf.filled().len(),
|
||||
addr,
|
||||
this.destination
|
||||
);
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::async_stream::AsyncWriteMessage for UnconnectedUdpSocket {
|
||||
fn poll_write_message(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
let this = self.get_mut();
|
||||
this.socket
|
||||
.poll_send_to(cx, buf, this.destination)
|
||||
.map(|r| r.map(|_| ()))
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::async_stream::AsyncFlushMessage for UnconnectedUdpSocket {
|
||||
fn poll_flush_message(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::async_stream::AsyncShutdownMessage for UnconnectedUdpSocket {
|
||||
fn poll_shutdown_message(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::async_stream::AsyncPing for UnconnectedUdpSocket {
|
||||
fn supports_ping(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn poll_write_ping(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<bool>> {
|
||||
Poll::Ready(Ok(false))
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::async_stream::AsyncMessageStream for UnconnectedUdpSocket {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -4,12 +4,14 @@ use std::sync::Arc;
|
||||
|
||||
use log::debug;
|
||||
|
||||
use crate::anytls::{AnyTlsClientHandler, PaddingFactory};
|
||||
use crate::client_proxy_selector::{ClientProxySelector, ConnectAction, ConnectRule};
|
||||
use crate::config::{
|
||||
ClientProxyConfig, RuleActionConfig, RuleConfig, ShadowsocksConfig, TlsClientConfig,
|
||||
WebsocketClientConfig,
|
||||
};
|
||||
use crate::http_handler::HttpTcpClientHandler;
|
||||
use crate::naiveproxy::NaiveProxyTcpClientHandler;
|
||||
use crate::port_forward_handler::PortForwardClientHandler;
|
||||
use crate::rustls_config_util::create_client_config;
|
||||
use crate::shadow_tls::ShadowTlsClientHandler;
|
||||
@@ -17,9 +19,10 @@ use crate::shadowsocks::ShadowsocksTcpHandler;
|
||||
use crate::snell::snell_handler::SnellClientHandler;
|
||||
use crate::socks_handler::SocksTcpClientHandler;
|
||||
use crate::tcp::chain_builder::build_client_chain_group;
|
||||
use crate::tcp_handler::TcpClientHandler;
|
||||
use crate::tcp::tcp_handler::TcpClientHandler;
|
||||
use crate::tls_client_handler::TlsClientHandler;
|
||||
use crate::trojan_handler::TrojanTcpHandler;
|
||||
use crate::uuid_util::parse_uuid;
|
||||
use crate::vless::vless_client_handler::VlessTcpClientHandler;
|
||||
use crate::vmess::VmessTcpClientHandler;
|
||||
use crate::websocket::WebsocketTcpClientHandler;
|
||||
@@ -48,16 +51,25 @@ pub fn create_tcp_client_handler(
|
||||
ClientProxyConfig::Socks { username, password } => Box::new(SocksTcpClientHandler::new(
|
||||
create_auth_credentials(username, password),
|
||||
)),
|
||||
ClientProxyConfig::Shadowsocks(ShadowsocksConfig::Legacy { cipher, password }) => {
|
||||
Box::new(ShadowsocksTcpHandler::new(cipher, &password))
|
||||
}
|
||||
ClientProxyConfig::Shadowsocks(ShadowsocksConfig::Aead2022 { cipher, key_bytes }) => {
|
||||
Box::new(ShadowsocksTcpHandler::new_aead2022(cipher, &key_bytes))
|
||||
}
|
||||
ClientProxyConfig::Snell(ShadowsocksConfig::Legacy { cipher, password }) => {
|
||||
Box::new(SnellClientHandler::new(cipher, &password))
|
||||
}
|
||||
ClientProxyConfig::Snell(ShadowsocksConfig::Aead2022 { .. }) => {
|
||||
ClientProxyConfig::Shadowsocks {
|
||||
config,
|
||||
udp_enabled,
|
||||
} => match config {
|
||||
ShadowsocksConfig::Legacy { cipher, password } => Box::new(
|
||||
ShadowsocksTcpHandler::new_client(cipher, &password, udp_enabled),
|
||||
),
|
||||
ShadowsocksConfig::Aead2022 { cipher, key_bytes } => Box::new(
|
||||
ShadowsocksTcpHandler::new_aead2022_client(cipher, &key_bytes, udp_enabled),
|
||||
),
|
||||
},
|
||||
ClientProxyConfig::Snell {
|
||||
config: ShadowsocksConfig::Legacy { cipher, password },
|
||||
udp_enabled,
|
||||
} => Box::new(SnellClientHandler::new(cipher, &password, udp_enabled)),
|
||||
ClientProxyConfig::Snell {
|
||||
config: ShadowsocksConfig::Aead2022 { .. },
|
||||
..
|
||||
} => {
|
||||
panic!(
|
||||
"Snell does not support shadowsocks 2022 ciphers (checked during config validation)"
|
||||
)
|
||||
@@ -65,14 +77,11 @@ pub fn create_tcp_client_handler(
|
||||
ClientProxyConfig::Vless {
|
||||
user_id,
|
||||
udp_enabled,
|
||||
} => {
|
||||
// Plain VLESS without TLS
|
||||
Box::new(VlessTcpClientHandler::new(&user_id, udp_enabled))
|
||||
}
|
||||
} => Box::new(VlessTcpClientHandler::new(&user_id, udp_enabled)),
|
||||
ClientProxyConfig::Trojan {
|
||||
password,
|
||||
shadowsocks,
|
||||
} => Box::new(TrojanTcpHandler::new(&password, &shadowsocks)),
|
||||
} => Box::new(TrojanTcpHandler::new_client(&password, &shadowsocks)),
|
||||
ClientProxyConfig::Tls(tls_client_config) => {
|
||||
let TlsClientConfig {
|
||||
verify,
|
||||
@@ -87,10 +96,10 @@ pub fn create_tcp_client_handler(
|
||||
} = tls_client_config;
|
||||
|
||||
let sni_hostname = if sni_hostname.is_unspecified() {
|
||||
if default_sni_hostname.is_some() {
|
||||
if let Some(ref hostname) = default_sni_hostname {
|
||||
debug!(
|
||||
"Using default sni hostname for TLS client connection: {}",
|
||||
default_sni_hostname.as_ref().unwrap()
|
||||
hostname
|
||||
);
|
||||
}
|
||||
default_sni_hostname
|
||||
@@ -112,6 +121,7 @@ pub fn create_tcp_client_handler(
|
||||
alpn_protocols.into_vec(),
|
||||
sni_hostname.is_some(),
|
||||
key_and_cert_bytes,
|
||||
false, // tls13_only
|
||||
));
|
||||
|
||||
let server_name = match sni_hostname {
|
||||
@@ -130,7 +140,7 @@ pub fn create_tcp_client_handler(
|
||||
// Validated when loading config
|
||||
unreachable!();
|
||||
};
|
||||
let user_id_bytes = crate::util::parse_uuid(user_id)
|
||||
let user_id_bytes = parse_uuid(user_id)
|
||||
.expect("Invalid user_id UUID")
|
||||
.into_boxed_slice();
|
||||
Box::new(TlsClientHandler::new_vision_vless(
|
||||
@@ -159,8 +169,6 @@ pub fn create_tcp_client_handler(
|
||||
vision,
|
||||
protocol,
|
||||
} => {
|
||||
eprintln!("========== CREATING REALITY CLIENT HANDLER ==========");
|
||||
|
||||
// Decode public key from base64url
|
||||
let public_key_bytes =
|
||||
crate::reality::decode_public_key(&public_key).expect("Invalid REALITY public key");
|
||||
@@ -190,7 +198,7 @@ pub fn create_tcp_client_handler(
|
||||
else {
|
||||
unreachable!("Vision requires VLESS (should be validated during config load)")
|
||||
};
|
||||
let user_id_bytes = crate::util::parse_uuid(user_id)
|
||||
let user_id_bytes = parse_uuid(user_id)
|
||||
.expect("Invalid user_id UUID")
|
||||
.into_boxed_slice();
|
||||
Box::new(
|
||||
@@ -219,7 +227,6 @@ pub fn create_tcp_client_handler(
|
||||
sni_hostname,
|
||||
protocol,
|
||||
} => {
|
||||
// ShadowTLS client handler
|
||||
let sni_hostname = sni_hostname.or(default_sni_hostname);
|
||||
let enable_sni = sni_hostname.is_some();
|
||||
|
||||
@@ -228,16 +235,18 @@ pub fn create_tcp_client_handler(
|
||||
None => "example.com".try_into().unwrap(), // Fallback
|
||||
};
|
||||
|
||||
// Create TLS config for ShadowTLS
|
||||
// TODO: Ensure client_config is suitable for TLS 1.3.
|
||||
// Rustls ClientConfig by default supports TLS 1.3 if server does.
|
||||
// The server handler enforces TLS 1.3 from client and for negotiation.
|
||||
// Create TLS config for ShadowTLS - must be TLS 1.3 only.
|
||||
// ShadowTLS v3 requires TLS 1.3: we modify the ClientHello session_id to embed
|
||||
// an HMAC tag, and rustls doesn't validate session_id echo for TLS 1.3 ServerHello.
|
||||
// TLS 1.2 would fail anyway (no supported_versions extension), but restricting
|
||||
// here provides defense in depth and fails fast at the TLS level.
|
||||
let client_config = Arc::new(create_client_config(
|
||||
false, // No WebPKI verification needed for ShadowTLS
|
||||
Vec::new(), // No fingerprints
|
||||
Vec::new(), // No ALPN
|
||||
enable_sni, // Enable SNI if hostname provided
|
||||
None, // No client cert
|
||||
true, // tls13_only - required for ShadowTLS v3
|
||||
));
|
||||
|
||||
let handler = create_tcp_client_handler(*protocol, None);
|
||||
@@ -272,6 +281,30 @@ pub fn create_tcp_client_handler(
|
||||
))
|
||||
}
|
||||
ClientProxyConfig::PortForward => Box::new(PortForwardClientHandler),
|
||||
ClientProxyConfig::Anytls {
|
||||
password,
|
||||
udp_enabled,
|
||||
padding_scheme,
|
||||
} => {
|
||||
let padding = match padding_scheme {
|
||||
Some(lines) => {
|
||||
let scheme = lines.join("\n");
|
||||
Arc::new(
|
||||
PaddingFactory::new(scheme.as_bytes())
|
||||
.expect("Invalid padding scheme in AnyTLS config"),
|
||||
)
|
||||
}
|
||||
None => PaddingFactory::default_factory(),
|
||||
};
|
||||
Box::new(AnyTlsClientHandler::new(password, padding, udp_enabled))
|
||||
}
|
||||
ClientProxyConfig::Naiveproxy {
|
||||
username,
|
||||
password,
|
||||
padding,
|
||||
} => Box::new(NaiveProxyTcpClientHandler::new(
|
||||
&username, &password, padding,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user