0.2.5 update

This commit is contained in:
cfal
2026-01-09 01:17:08 +04:00
parent 6c51fd1937
commit 4eafedc5ee
139 changed files with 22368 additions and 7485 deletions
+160 -1
View File
@@ -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
+117
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+43 -11
View File
@@ -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
+74 -3
View File
@@ -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)
+101
View File
@@ -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
+58
View File
@@ -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"
+92
View File
@@ -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"
+9
View File
@@ -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:
+58
View File
@@ -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
+173
View File
@@ -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
+14
View File
@@ -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
View File
@@ -94,7 +94,7 @@ impl NetLocation {
Self { address, port }
}
pub fn _is_unspecified(&self) -> bool {
pub fn is_unspecified(&self) -> bool {
self == &Self::UNSPECIFIED
}
+134
View File
@@ -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
}
+735
View File
@@ -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)
}
}
+227
View File
@@ -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());
}
}
+322
View File
@@ -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
+549
View File
@@ -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());
}
}
+245
View File
@@ -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()));
}
}
+18
View File
@@ -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
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>,
}
+8
View File
@@ -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()
}
+17
View File
@@ -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),
+2
View File
@@ -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
View File
@@ -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]
+54 -40
View File
@@ -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()
}
}
+7
View File
@@ -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")]
+108
View File
@@ -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
View File
@@ -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}"
);
}
}
+11 -7
View File
@@ -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,
+11 -18
View File
@@ -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,
-486
View File
@@ -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
}
-287
View File
@@ -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
}
+27 -1
View File
@@ -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],
+6 -4
View File
@@ -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;
}
}
+16
View File
@@ -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.
+329
View File
@@ -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
}
}
+253
View File
@@ -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
View File
@@ -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)
}
+62
View File
@@ -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::*;
+10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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..");
+121
View File
@@ -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
}
}
}
+154
View File
@@ -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>();
}
}
+11
View File
@@ -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;
+159
View File
@@ -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>();
}
}
+226
View File
@@ -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");
}
}
+608
View File
@@ -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)
}
}
}
+925
View File
@@ -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);
}
}
+32
View File
@@ -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
}
+160
View File
@@ -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
View File
@@ -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)]
+8 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
);
}
}
+247 -274
View File
@@ -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],
+428 -12
View File
@@ -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"
);
}
}
File diff suppressed because it is too large Load Diff
+293 -226
View File
@@ -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],
+333
View File
@@ -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)
}
+5 -7
View File
@@ -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
);
+75 -40
View File
@@ -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);
+7 -11
View File
@@ -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());
+7 -11
View File
@@ -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
}
+2
View File
@@ -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>,
+10
View File
@@ -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
View File
@@ -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();
+2 -2
View File
@@ -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],
+2 -2
View File
@@ -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;
+14 -17
View File
@@ -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",
+266 -174
View File
@@ -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)?;
+148 -20
View File
@@ -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
View File
@@ -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
View File
@@ -1,2 +1,3 @@
pub mod snell_fixed_target_stream;
pub mod snell_handler;
pub mod snell_udp_stream;
+80
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)?;
+518
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+280
View File
@@ -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);
}
}
+10 -7
View File
@@ -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>>;
}
+9 -12
View File
@@ -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
View File
@@ -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>>;
}
+106 -49
View File
@@ -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::*;
+60 -27
View File
@@ -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