├── .gitignore ├── src ├── shadowsocks │ ├── aead_util.rs │ ├── salt_checker.rs │ ├── shadowsocks_key.rs │ ├── mod.rs │ ├── shadowsocks_stream_type.rs │ ├── blake3_key.rs │ ├── timed_salt_checker.rs │ ├── default_key.rs │ └── shadowsocks_cipher.rs ├── snell │ └── mod.rs ├── vmess │ ├── typed.rs │ ├── mod.rs │ ├── fnv1a.rs │ ├── md5.rs │ ├── nonce.rs │ ├── crc32.rs │ └── sha2.rs ├── websocket │ └── mod.rs ├── xudp │ └── mod.rs ├── tcp │ ├── mod.rs │ ├── socket_connector.rs │ ├── proxy_connector.rs │ └── proxy_connector_impl.rs ├── thread_util.rs ├── shadow_tls │ ├── mod.rs │ └── shadow_tls_hmac.rs ├── vless │ ├── mod.rs │ ├── vision_pad.rs │ ├── vless_response_stream.rs │ ├── vless_message_stream.rs │ └── vless_util.rs ├── crypto │ ├── mod.rs │ └── crypto_reader_writer.rs ├── reality │ ├── reality_io_state.rs │ ├── mod.rs │ ├── reality_cipher_suite.rs │ ├── reality_reader_writer.rs │ └── common.rs ├── uot │ └── mod.rs ├── config │ ├── types │ │ ├── mod.rs │ │ ├── transport.rs │ │ ├── shadowsocks.rs │ │ ├── common.rs │ │ └── selection.rs │ └── mod.rs ├── util.rs ├── rustls_connection_util.rs ├── port_forward_handler.rs ├── quic_stream.rs ├── sync_adapter.rs ├── udp_message_stream.rs ├── buf_reader.rs ├── tls_client_handler.rs ├── resolver.rs ├── reality_client_handler.rs ├── socket_util.rs ├── udp_multi_message_stream.rs └── option_util.rs ├── .cargo └── config.toml ├── examples ├── hysteria2.yaml ├── vless_over_tls.yaml ├── socks_test.yaml ├── tuic_v5_port_range.yaml ├── snell.yaml ├── vmess.yaml ├── vmess_through_socks.yaml ├── tuic_v5.yaml ├── socks_through_vmess.yaml ├── socks_through_tls_vmess.yaml ├── https.yaml ├── snell_forward_rules.yaml ├── http_through_socks.yaml ├── shadowtls_v3_local_handshake.yaml ├── tls_sni_vmess_and_snell.yaml ├── trojan_tls_through_socks.yaml ├── wss_vmess.yaml ├── socks_through_wss_vmess.yaml ├── reality_vision.yaml ├── trojan_basic.yaml ├── reality_vision_client.yaml ├── wss_vmess_and_shadowsocks.yaml ├── shadowsocks_basic.yaml ├── reality_client.yaml ├── http_proxy_basic.yaml ├── reality_trojan.yaml ├── reality_basic.yaml ├── shadowtls_v3_remote_handshake.yaml ├── vless_variants.yaml ├── named_certificates.yaml └── multi_hop_chain.yaml ├── LICENSE ├── .github └── workflows │ ├── test.yml │ ├── lint.yml │ └── build.yml ├── Cargo.toml ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /*.yaml 3 | *.pem 4 | -------------------------------------------------------------------------------- /src/shadowsocks/aead_util.rs: -------------------------------------------------------------------------------- 1 | pub const TAG_LEN: usize = 16; 2 | -------------------------------------------------------------------------------- /src/snell/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod snell_handler; 2 | pub mod snell_udp_stream; 3 | -------------------------------------------------------------------------------- /src/vmess/typed.rs: -------------------------------------------------------------------------------- 1 | pub type VmessReader = digest::core_api::XofReaderCoreWrapper; 2 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # Static linking for musl targets 2 | [target.'cfg(target_env = "musl")'] 3 | rustflags = ["-C", "target-feature=+crt-static"] 4 | -------------------------------------------------------------------------------- /src/shadowsocks/salt_checker.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | pub trait SaltChecker: Send + Sync + Debug { 4 | fn insert_and_check(&mut self, salt: &[u8]) -> bool; 5 | } 6 | -------------------------------------------------------------------------------- /src/shadowsocks/shadowsocks_key.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | pub trait ShadowsocksKey: Send + Sync + Debug { 4 | fn create_session_key(&self, salt: &[u8]) -> Box<[u8]>; 5 | } 6 | -------------------------------------------------------------------------------- /src/websocket/mod.rs: -------------------------------------------------------------------------------- 1 | mod websocket_handler; 2 | mod websocket_stream; 3 | 4 | pub use websocket_handler::{ 5 | WebsocketServerTarget, WebsocketTcpClientHandler, WebsocketTcpServerHandler, 6 | }; 7 | -------------------------------------------------------------------------------- /src/vmess/mod.rs: -------------------------------------------------------------------------------- 1 | mod crc32; 2 | mod fnv1a; 3 | mod md5; 4 | mod nonce; 5 | mod sha2; 6 | mod typed; 7 | mod vmess_handler; 8 | mod vmess_stream; 9 | pub use vmess_handler::{VmessTcpClientHandler, VmessTcpServerHandler}; 10 | -------------------------------------------------------------------------------- /src/xudp/mod.rs: -------------------------------------------------------------------------------- 1 | // XUDP (Extended UDP) protocol implementation 2 | // Protocol-agnostic UDP multiplexing over TCP connections 3 | // Used by both VLESS and VMess protocols 4 | 5 | pub mod frame; 6 | pub mod message_stream; 7 | 8 | pub use message_stream::XudpMessageStream; 9 | -------------------------------------------------------------------------------- /examples/hysteria2.yaml: -------------------------------------------------------------------------------- 1 | - address: 0.0.0.0:443 2 | transport: quic 3 | quic_settings: 4 | cert: cert.pem 5 | key: cert.pem 6 | alpn_protocols: 7 | - h3 8 | protocol: 9 | type: hysteria2 10 | password: abc128459 11 | rules: 12 | - allow-all-direct 13 | -------------------------------------------------------------------------------- /src/tcp/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod chain_builder; 2 | pub mod proxy_connector; 3 | pub mod proxy_connector_impl; 4 | pub mod socket_connector; 5 | pub mod socket_connector_impl; 6 | pub mod tcp_client_handler_factory; 7 | pub mod tcp_handler; 8 | pub mod tcp_server; 9 | pub mod tcp_server_handler_factory; 10 | -------------------------------------------------------------------------------- /src/thread_util.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | static NUM_THREADS: OnceLock = OnceLock::new(); 4 | 5 | pub fn set_num_threads(num_threads: usize) { 6 | let _ = NUM_THREADS.set(num_threads); 7 | } 8 | 9 | pub fn get_num_threads() -> usize { 10 | *NUM_THREADS.get().unwrap() 11 | } 12 | -------------------------------------------------------------------------------- /examples/vless_over_tls.yaml: -------------------------------------------------------------------------------- 1 | - address: 0.0.0.0:443 2 | transport: tcp 3 | protocol: 4 | type: tls 5 | sni_targets: 6 | my.domain.com: 7 | cert: cert.cer 8 | key: key.key 9 | protocol: 10 | type: vless 11 | user_id: 88dd7a01-4325-4575-835c-57dda13fbd58 12 | 13 | -------------------------------------------------------------------------------- /examples/socks_test.yaml: -------------------------------------------------------------------------------- 1 | - address: 127.0.0.1:50000 2 | protocol: 3 | type: socks 4 | - address: 127.0.0.1:50001 5 | protocol: 6 | type: socks 7 | rules: 8 | # allow connections to all ips 9 | mask: 0.0.0.0/0 10 | action: allow 11 | client_chain: 12 | address: 127.0.0.1:50000 13 | protocol: 14 | type: socks 15 | -------------------------------------------------------------------------------- /examples/tuic_v5_port_range.yaml: -------------------------------------------------------------------------------- 1 | # specify a port range to allow port hopping. 2 | - address: 0.0.0.0:5000-6000 3 | transport: quic 4 | quic_settings: 5 | cert: cert.pem 6 | key: cert.pem 7 | protocol: 8 | type: tuic 9 | uuid: d685aef3-b3c4-4932-9a9d-d0c2f6727dfa 10 | password: abc128459 11 | rules: 12 | - allow-all-direct 13 | -------------------------------------------------------------------------------- /examples/snell.yaml: -------------------------------------------------------------------------------- 1 | # Simple snell v3 server, supporting TCP and UDP-over-TCP. 2 | - address: 127.0.0.1:55555 3 | transport: tcp 4 | protocol: 5 | type: snell 6 | cipher: any 7 | password: secretpass 8 | rules: 9 | - mask: 0.0.0.0/0 10 | action: allow 11 | # Direct connection, don't forward requests through another proxy. 12 | client_chain: direct 13 | -------------------------------------------------------------------------------- /src/shadow_tls/mod.rs: -------------------------------------------------------------------------------- 1 | mod shadow_tls_client_handler; 2 | mod shadow_tls_hmac; 3 | mod shadow_tls_server_handler; 4 | mod shadow_tls_stream; 5 | 6 | pub use shadow_tls_server_handler::{ 7 | ParsedClientHello, ShadowTlsServerTarget, ShadowTlsServerTargetHandshake, read_client_hello, 8 | setup_shadowtls_server_stream, 9 | }; 10 | 11 | pub use shadow_tls_client_handler::ShadowTlsClientHandler; 12 | -------------------------------------------------------------------------------- /examples/vmess.yaml: -------------------------------------------------------------------------------- 1 | # Simple vmess server, supporting TCP and UDP-over-TCP. 2 | - address: 127.0.0.1:55555 3 | transport: tcp 4 | protocol: 5 | type: vmess 6 | cipher: any 7 | user_id: b0e80a62-8a51-47f0-91f1-f0f7faf8d9d4 8 | rules: 9 | - mask: 0.0.0.0/0 10 | action: allow 11 | # Direct connection, don't forward requests through another proxy. 12 | client_chain: direct 13 | -------------------------------------------------------------------------------- /examples/vmess_through_socks.yaml: -------------------------------------------------------------------------------- 1 | - address: 127.0.0.1:55555 2 | transport: tcp 3 | protocol: 4 | type: vmess 5 | cipher: any 6 | user_id: b0e80a62-8a51-47f0-91f1-f0f7faf8d9d4 7 | rules: 8 | # allow connections to all ips 9 | - mask: 0.0.0.0/0 10 | action: allow 11 | client_chain: 12 | address: 1.2.3.4:6666 13 | protocol: 14 | type: socks 15 | username: socksuser 16 | password: secretpass 17 | -------------------------------------------------------------------------------- /examples/tuic_v5.yaml: -------------------------------------------------------------------------------- 1 | # Basic TUIC v5 server 2 | - address: 0.0.0.0:443 3 | transport: quic 4 | quic_settings: 5 | cert: cert.pem 6 | key: cert.pem 7 | protocol: 8 | type: tuic 9 | uuid: d685aef3-b3c4-4932-9a9d-d0c2f6727dfa 10 | password: abc128459 11 | # Enable 0-RTT handshake for lower latency (default: false) 12 | # Note: 0-RTT is vulnerable to replay attacks, only enable if acceptable 13 | zero_rtt_handshake: false 14 | rules: 15 | - allow-all-direct 16 | -------------------------------------------------------------------------------- /examples/socks_through_vmess.yaml: -------------------------------------------------------------------------------- 1 | # A SOCKS5 server that forwards connections through a VMess TLS proxy. 2 | - address: 127.0.0.1:1080 3 | protocol: 4 | # no auth - add 'username' and 'password' fields for authentication. 5 | type: socks 6 | rules: 7 | # allow connections to all ips 8 | mask: 0.0.0.0/0 9 | action: allow 10 | client_chain: 11 | address: 127.0.0.1:5432 12 | protocol: 13 | type: vmess 14 | cipher: aes-128-gcm 15 | user_id: bb0e80a62-8a51-47f0-91f1-f0f7faf8d9d4 16 | -------------------------------------------------------------------------------- /src/vmess/fnv1a.rs: -------------------------------------------------------------------------------- 1 | pub struct Fnv1aHasher(u32); 2 | 3 | const FNV_PRIME: u32 = 16777619; 4 | 5 | impl Fnv1aHasher { 6 | pub fn new() -> Self { 7 | Self(0x811c9dc5) 8 | } 9 | 10 | pub fn write(&mut self, data: &[u8]) { 11 | let mut hash = self.0; 12 | for byte in data.iter() { 13 | hash ^= *byte as u32; 14 | hash = hash.wrapping_mul(FNV_PRIME); 15 | } 16 | self.0 = hash; 17 | } 18 | 19 | pub fn finish(self) -> u32 { 20 | self.0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/shadowsocks/mod.rs: -------------------------------------------------------------------------------- 1 | mod aead_util; 2 | mod blake3_key; 3 | mod default_key; 4 | mod salt_checker; 5 | mod shadowsocks_cipher; 6 | mod shadowsocks_key; 7 | mod shadowsocks_stream; 8 | mod shadowsocks_stream_type; 9 | mod shadowsocks_tcp_handler; 10 | mod timed_salt_checker; 11 | 12 | pub use default_key::DefaultKey; 13 | pub use shadowsocks_cipher::ShadowsocksCipher; 14 | pub use shadowsocks_key::ShadowsocksKey; 15 | pub use shadowsocks_stream::ShadowsocksStream; 16 | pub use shadowsocks_stream_type::ShadowsocksStreamType; 17 | pub use shadowsocks_tcp_handler::ShadowsocksTcpHandler; 18 | -------------------------------------------------------------------------------- /examples/socks_through_tls_vmess.yaml: -------------------------------------------------------------------------------- 1 | # A SOCKS5 server that forwards connections through a VMess TLS proxy. 2 | - address: 127.0.0.1:1080 3 | protocol: 4 | # no auth - add 'username' and 'password' fields for authentication. 5 | type: socks 6 | rules: 7 | # allow connections to all ips 8 | mask: 0.0.0.0/0 9 | action: allow 10 | client_chain: 11 | address: 127.0.0.1:2000 12 | protocol: 13 | type: tls 14 | verify: false 15 | protocol: 16 | type: vmess 17 | cipher: any 18 | user_id: b0e80a62-8a51-47f0-91f1-f0f7faf8d9d4 19 | -------------------------------------------------------------------------------- /src/vless/mod.rs: -------------------------------------------------------------------------------- 1 | // VLESS protocol implementation with VISION support 2 | 3 | // Public API 4 | pub mod vless_client_handler; 5 | pub mod vless_server_handler; 6 | 7 | // Internal implementation details 8 | pub mod tls_deframer; 9 | mod tls_fuzzy_deframer; 10 | mod tls_handshake_util; 11 | mod vision_filter; 12 | mod vision_pad; 13 | mod vision_stream; 14 | mod vision_unpad; 15 | mod vless_message_stream; 16 | mod vless_response_stream; 17 | mod vless_util; 18 | 19 | // Re-export VlessMessageStream for use by other protocols (e.g., Shadowsocks UoT V2) 20 | pub use vless_message_stream::VlessMessageStream; 21 | -------------------------------------------------------------------------------- /examples/https.yaml: -------------------------------------------------------------------------------- 1 | # An HTTPS server with Basic Authentication. 2 | - address: 127.0.0.1:443 3 | transport: tcp 4 | protocol: 5 | type: tls 6 | default_target: 7 | cert: cert.pem 8 | key: key.pem 9 | protocol: 10 | type: http 11 | username: secretuser 12 | password: secretpass 13 | rules: 14 | - mask: 1.2.3.4/32 15 | action: allow 16 | override_address: 192.168.0.1 17 | client_chain: direct 18 | - mask: 0.0.0.0/0 19 | action: allow 20 | # Direct connection, don't forward requests through another proxy. 21 | client_chain: direct 22 | -------------------------------------------------------------------------------- /src/vmess/md5.rs: -------------------------------------------------------------------------------- 1 | use md5::{Digest, Md5}; 2 | 3 | #[inline] 4 | pub fn compute_md5(data: &[u8]) -> [u8; 16] { 5 | let mut context = Md5::new(); 6 | md5::Digest::update(&mut context, data); 7 | context.finalize().into() 8 | } 9 | 10 | #[inline] 11 | pub fn create_chacha_key(data: &[u8]) -> [u8; 32] { 12 | let mut ret = [0u8; 32]; 13 | let mut context = Md5::new(); 14 | md5::Digest::update(&mut context, data); 15 | context.finalize_into_reset((&mut ret[0..16]).into()); 16 | md5::Digest::update(&mut context, &ret[0..16]); 17 | context.finalize_into((&mut ret[16..]).into()); 18 | ret 19 | } 20 | -------------------------------------------------------------------------------- /src/crypto/mod.rs: -------------------------------------------------------------------------------- 1 | // Unified cryptographic connection abstraction 2 | // 3 | // This module provides a common interface for different TLS-like protocols, 4 | // allowing rustls and REALITY to be used interchangeably throughout the codebase. 5 | 6 | mod crypto_connection; 7 | mod crypto_handshake; 8 | mod crypto_reader_writer; 9 | mod crypto_tls_stream; 10 | 11 | // Re-export core types 12 | pub use crypto_connection::{CryptoConnection, feed_crypto_connection}; 13 | pub use crypto_handshake::perform_crypto_handshake; 14 | pub use crypto_tls_stream::CryptoTlsStream; 15 | // CryptoReader, CryptoWriter, and IoState are used internally within this module 16 | -------------------------------------------------------------------------------- /src/reality/reality_io_state.rs: -------------------------------------------------------------------------------- 1 | /// Represents the I/O state after processing packets 2 | #[derive(Debug, Clone, Copy)] 3 | pub struct RealityIoState { 4 | /// Number of plaintext bytes available to read 5 | plaintext_bytes_to_read: usize, 6 | } 7 | 8 | impl RealityIoState { 9 | /// Create a new RealityIoState 10 | pub fn new(plaintext_bytes_to_read: usize) -> Self { 11 | Self { 12 | plaintext_bytes_to_read, 13 | } 14 | } 15 | 16 | /// How many plaintext bytes could be obtained via Read without further I/O 17 | pub fn plaintext_bytes_to_read(&self) -> usize { 18 | self.plaintext_bytes_to_read 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/snell_forward_rules.yaml: -------------------------------------------------------------------------------- 1 | # Snell server that only allows requests to certain hostnames and IPs. 2 | - address: 127.0.0.1:55555 3 | transport: tcp 4 | protocol: 5 | type: snell 6 | cipher: any 7 | password: secretpass 8 | rules: 9 | # Clients trying to connect to hello.com on port 80 are allowed, but instead forwarded to 10 | # a local server on port 55556. 11 | - mask: hello.com:80 12 | action: allow 13 | override_address: 127.0.0.1:55556 14 | client_chain: direct 15 | # Clients can access 10.x addresses. 16 | - mask: 10.0.0.0/8 17 | action: allow 18 | client_chain: direct 19 | # Block all other addresses. 20 | - mask: 0.0.0.0/0 21 | action: block 22 | -------------------------------------------------------------------------------- /examples/http_through_socks.yaml: -------------------------------------------------------------------------------- 1 | # An HTTP server (no TLS) that forwards connections via a SOCKS5 server. 2 | - address: 127.0.0.1:8080 3 | transport: tcp 4 | protocol: 5 | type: http 6 | username: secretuser 7 | password: secretpass 8 | rules: 9 | # Directly connect to 192.168.* 10 | - mask: 192.168.0.0/16 11 | action: allow 12 | # client_chain: direct is optional - direct is the default when omitted 13 | client_chain: direct 14 | # Forward all other connection requests through a SOCKS server. 15 | - mask: 0.0.0.0/0 16 | action: allow 17 | client_chain: 18 | address: 127.0.0.1:1234 19 | protocol: 20 | type: socks 21 | username: socksuser 22 | password: sockspass 23 | -------------------------------------------------------------------------------- /src/shadow_tls/shadow_tls_hmac.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub struct ShadowTlsHmac { 3 | context: aws_lc_rs::hmac::Context, 4 | } 5 | 6 | impl ShadowTlsHmac { 7 | pub fn new(key: &aws_lc_rs::hmac::Key) -> Self { 8 | Self { 9 | context: aws_lc_rs::hmac::Context::with_key(key), 10 | } 11 | } 12 | 13 | pub fn update(&mut self, data: &[u8]) { 14 | self.context.update(data); 15 | } 16 | 17 | pub fn digest(&self) -> [u8; 4] { 18 | let tag = self.context.clone().sign(); 19 | let mut out = [0u8; 4]; 20 | out.copy_from_slice(&tag.as_ref()[0..4]); 21 | out 22 | } 23 | 24 | pub fn finalized_digest(self) -> [u8; 4] { 25 | let tag = self.context.sign(); 26 | let mut out = [0u8; 4]; 27 | out.copy_from_slice(&tag.as_ref()[0..4]); 28 | out 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/shadowtls_v3_local_handshake.yaml: -------------------------------------------------------------------------------- 1 | - address: 127.0.0.1:44300 2 | transport: tcp 3 | protocol: 4 | type: tls 5 | # specify different configs based on ShadowTLS SNI 6 | shadowtls_targets: 7 | example.com: 8 | password: asdf123 9 | handshake: 10 | # provide a cert and key to handle the server handshake in-process, 11 | # without requiring a separate handshake server. 12 | # NOTE: most ShadowTLS clients require the cert to still be signed 13 | # by a known CA, so a self-signed cert will most likely fail. 14 | cert: example.com.pem 15 | key: example.com.pem 16 | protocol: 17 | type: socks 18 | username: socks-user 19 | password: socks-pass 20 | 21 | rules: 22 | - mask: 0.0.0.0/0 23 | action: allow 24 | # client_chain: direct is optional - direct is the default when omitted 25 | client_chain: direct 26 | -------------------------------------------------------------------------------- /examples/tls_sni_vmess_and_snell.yaml: -------------------------------------------------------------------------------- 1 | # A TLS vmess and snell server, forwarded to different protocols by SNI. 2 | - address: 127.0.0.1:55555 3 | transport: tcp 4 | protocol: 5 | type: tls 6 | sni_targets: 7 | # Clients requesting SNI google.com will get forwarded to the vmess server 8 | google.com: 9 | cert: cert.pem 10 | key: key.pem 11 | protocol: 12 | type: vmess 13 | cipher: any 14 | user_id: b0e80a62-8a51-47f0-91f1-f0f7faf8d9d4 15 | # Clients requesting SNI yahoo.com will get forwarded to the snell server 16 | yahoo.com: 17 | cert: cert.pem 18 | key: key.com 19 | protocol: 20 | type: snell 21 | cipher: any 22 | password: secretpass 23 | rules: 24 | # Allow connections to any IP. 25 | - mask: 0.0.0.0/0 26 | action: allow 27 | # client_chain: direct is optional - direct is the default when omitted 28 | client_chain: direct 29 | -------------------------------------------------------------------------------- /examples/trojan_tls_through_socks.yaml: -------------------------------------------------------------------------------- 1 | # A TLS Trojan server that selectively forwards. 2 | - address: 127.0.0.1:15555 3 | transport: tcp 4 | protocol: 5 | type: tls 6 | # target for all SNIs 7 | default_target: 8 | # can be generated using openssl, eg: openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365000 -nodes -subj "/CN=localhost" 9 | cert: cert.pem 10 | key: key.pem 11 | protocol: 12 | type: trojan 13 | password: helloworld 14 | rules: 15 | # For example.com, connect through a SOCKS proxy. 16 | - mask: example.com 17 | action: allow 18 | client_chain: 19 | address: 127.0.0.1:15556 20 | protocol: 21 | type: socks 22 | username: username 23 | password: password 24 | 25 | # Allow all other connections to connect directly. 26 | - mask: 0.0.0.0/0 27 | action: allow 28 | # client_chain: direct is optional - direct is the default when omitted 29 | client_chain: direct 30 | -------------------------------------------------------------------------------- /examples/wss_vmess.yaml: -------------------------------------------------------------------------------- 1 | # Listen on all IPv4 interfaces, port 443 (HTTPS) 2 | - address: 0.0.0.0:443 3 | transport: tcp 4 | # Use TLS as the first protocol layer 5 | protocol: 6 | type: tls 7 | # Set a default target, for any (or no) SNI 8 | default_target: 9 | cert: cert.pem 10 | key: key.pem 11 | # ..which goes to a websocket server 12 | protocol: 13 | type: ws 14 | # .. where we have different supported proxy protocols, based on HTTP request path and headers. 15 | targets: 16 | - matching_path: /vmess 17 | matching_headers: 18 | X-Secret-Key: "secret" 19 | protocol: 20 | type: vmess 21 | # allow any cipher, which means: none, aes-128-gcm, or chacha20-poly1305. 22 | cipher: any 23 | user_id: b0e80a62-8a51-47f0-91f1-f0f7faf8d9d4 24 | rules: 25 | - mask: 0.0.0.0/0 26 | action: allow 27 | # client_chain: direct is optional - direct is the default when omitted 28 | client_chain: direct 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2023 Alex Lau 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/socks_through_wss_vmess.yaml: -------------------------------------------------------------------------------- 1 | # A SOCKS5 server that forwards connections through a VMess WSS proxy. 2 | - address: 127.0.0.1:5443 3 | transport: tcp 4 | protocol: 5 | type: socks 6 | username: secretuser 7 | password: secretpass 8 | rules: 9 | # Directly connect to 192.168.* 10 | - mask: 192.168.0.0/16 11 | action: allow 12 | # client_chain: direct is optional - direct is the default when omitted 13 | client_chain: direct 14 | # Forward all other connection requests through a Vmess WSS server. 15 | - mask: 0.0.0.0/0 16 | action: allow 17 | client_chain: 18 | address: 127.0.0.1:1234 19 | protocol: 20 | type: tls 21 | # Set to false to disable TLS verification, if, for example, the upstream server 22 | # uses a self-signed certificate 23 | verify: true 24 | protocol: 25 | type: ws 26 | # The path to the websocket server 27 | matching_path: /ws 28 | protocol: 29 | type: vmess 30 | cipher: aes-128-gcm 31 | user_id: b0e80a62-8a51-47f0-91f1-f0f7faf8d9d4 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUST_BACKTRACE: 1 12 | 13 | jobs: 14 | test: 15 | name: Unit Tests 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Install Rust 22 | uses: dtolnay/rust-toolchain@stable 23 | 24 | - name: Cache cargo registry 25 | uses: actions/cache@v4 26 | with: 27 | path: ~/.cargo/registry 28 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} 29 | 30 | - name: Cache cargo index 31 | uses: actions/cache@v4 32 | with: 33 | path: ~/.cargo/git 34 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} 35 | 36 | - name: Cache cargo build 37 | uses: actions/cache@v4 38 | with: 39 | path: target 40 | key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} 41 | 42 | - name: Build 43 | run: cargo build --verbose 44 | 45 | - name: Run tests 46 | run: cargo test --verbose 47 | -------------------------------------------------------------------------------- /examples/reality_vision.yaml: -------------------------------------------------------------------------------- 1 | # Reality + Vision server 2 | # Combines censorship resistance with TLS-in-TLS optimization 3 | # 4 | # Reality provides censorship resistance through TLS 1.3 mimicry 5 | # Vision provides zero-copy performance when TLS-in-TLS is detected 6 | # Together they offer both benefits 7 | 8 | - address: "0.0.0.0:443" 9 | protocol: 10 | type: tls 11 | reality_targets: 12 | "www.google.com": 13 | private_key: "YOUR_PRIVATE_KEY_HERE" 14 | short_ids: ["0123456789abcdef"] 15 | dest: "www.google.com:443" 16 | 17 | # Enable XTLS-Vision for TLS-in-TLS optimization 18 | # When enabled, Vision detects TLS-in-TLS scenarios and switches 19 | # to Direct mode for zero-copy performance 20 | # IMPORTANT: Vision requires VLESS as the inner protocol 21 | vision: true 22 | 23 | protocol: 24 | type: vless # Vision REQUIRES VLESS 25 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 26 | udp_enabled: true 27 | 28 | # Benefits of Reality + Vision: 29 | # - Reality: Censorship resistance, looks like real TLS traffic 30 | # - Vision: Zero-copy performance, better anti-detection 31 | # - Combined: Maximum security and performance 32 | -------------------------------------------------------------------------------- /examples/trojan_basic.yaml: -------------------------------------------------------------------------------- 1 | # Basic Trojan server examples 2 | 3 | # Simple Trojan server over TLS 4 | - address: "0.0.0.0:443" 5 | protocol: 6 | type: tls 7 | sni_targets: 8 | "trojan.example.com": 9 | cert: "cert.pem" 10 | key: "key.pem" 11 | protocol: 12 | type: trojan 13 | password: "trojan-secret-password" 14 | 15 | # Trojan with Shadowsocks obfuscation 16 | - address: "0.0.0.0:8443" 17 | protocol: 18 | type: tls 19 | default_target: 20 | cert: "cert.pem" 21 | key: "key.pem" 22 | protocol: 23 | type: trojan 24 | password: "trojan-password" 25 | shadowsocks: 26 | cipher: aes-256-gcm 27 | password: "shadowsocks-password" 28 | 29 | # Trojan over WebSocket 30 | - address: "0.0.0.0:443" 31 | protocol: 32 | type: tls 33 | sni_targets: 34 | "ws.example.com": 35 | cert: "cert.pem" 36 | key: "key.pem" 37 | protocol: 38 | type: websocket 39 | targets: 40 | - matching_path: "/trojan" 41 | matching_headers: 42 | User-Agent: "Mozilla/5.0" 43 | protocol: 44 | type: trojan 45 | password: "websocket-trojan-password" -------------------------------------------------------------------------------- /src/shadowsocks/shadowsocks_stream_type.rs: -------------------------------------------------------------------------------- 1 | pub enum ShadowsocksStreamType { 2 | Aead, 3 | AEAD2022Server, 4 | AEAD2022Client, 5 | } 6 | 7 | impl ShadowsocksStreamType { 8 | pub fn max_payload_len(&self) -> usize { 9 | match self { 10 | ShadowsocksStreamType::Aead => { 11 | // for AEAD ciphers: 12 | // from https://shadowsocks.org/guide/aead.html#tcp 13 | // 14 | // "Payload length is a 2-byte big-endian unsigned integer capped at 0x3FFF. 15 | // The higher two bits are reserved and must be set to zero. Payload is therefore 16 | // limited to 16*1024 - 1 bytes." 17 | 0x3fff 18 | } 19 | ShadowsocksStreamType::AEAD2022Server | ShadowsocksStreamType::AEAD2022Client => { 20 | // for AEAD 2022 ciphers: 21 | // from https://github.com/Shadowsocks-NET/shadowsocks-specs/blob/main/2022-1-shadowsocks-2022-edition.md 22 | // "A payload chunk can have up to 0xFFFF (65535) bytes of unencrypted payload. The 0x3FFF (16383) 23 | // length cap in Shadowsocks AEAD does not apply to this edition." 24 | 0xffff 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/reality_vision_client.yaml: -------------------------------------------------------------------------------- 1 | # Reality + Vision client configuration 2 | # Maximum performance and censorship resistance 3 | # 4 | # Reality provides censorship resistance through TLS 1.3 mimicry 5 | # Vision provides zero-copy performance for TLS-in-TLS scenarios 6 | # Together they offer both benefits with VLESS as the inner protocol 7 | 8 | # Local SOCKS proxy using Reality + Vision 9 | - address: "127.0.0.1:1080" 10 | protocol: 11 | type: socks 12 | rules: 13 | - masks: "0.0.0.0/0" 14 | action: allow 15 | client_chain: 16 | address: "your-server.example.com:443" 17 | protocol: 18 | type: reality 19 | public_key: "SERVER_PUBLIC_KEY_HERE" 20 | short_id: "0123456789abcdef" 21 | sni_hostname: "www.google.com" 22 | vision: true # Enable Vision (must use VLESS) 23 | protocol: 24 | type: vless # Vision REQUIRES VLESS 25 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 26 | 27 | # Benefits of Reality + Vision client: 28 | # - Censorship resistance: Looks like normal TLS traffic to censors 29 | # - Performance: Vision provides zero-copy for TLS-in-TLS 30 | # - Anti-detection: Vision adds padding to mimic real TLS patterns 31 | # - Compatibility: Works with CDNs and middleboxes 32 | -------------------------------------------------------------------------------- /examples/wss_vmess_and_shadowsocks.yaml: -------------------------------------------------------------------------------- 1 | # Listen on all IPv4 interfaces, port 443 (HTTPS) 2 | - address: 0.0.0.0:443 3 | transport: tcp 4 | # Use TLS as the first protocol layer 5 | protocol: 6 | type: tls 7 | # Set a default target, for any (or no) SNI 8 | default_target: 9 | cert: cert.pem 10 | key: key.pem 11 | # ..which goes to a websocket server 12 | protocol: 13 | type: ws 14 | # .. where we have different supported proxy protocols, based on HTTP request path and headers. 15 | targets: 16 | - matching_path: /vmess 17 | matching_headers: 18 | X-Secret-Key: "secret" 19 | protocol: 20 | type: vmess 21 | # allow any cipher, which means: none, aes-128-gcm, or chacha20-poly1305. 22 | cipher: any 23 | user_id: b0e80a62-8a51-47f0-91f1-f0f7faf8d9d4 24 | - matching_path: /shadowsocks 25 | protocol: 26 | type: shadowsocks 27 | cipher: 2022-blake3-aes-256-gcm 28 | password: Hax8btYlNao5qcaN/l/NUl9JgbwapfqG5QyAtH+aKPg= 29 | rules: 30 | - mask: 0.0.0.0/0 31 | action: allow 32 | # client_chain: direct is optional - direct is the default when omitted 33 | client_chain: direct 34 | -------------------------------------------------------------------------------- /src/reality/mod.rs: -------------------------------------------------------------------------------- 1 | // REALITY Protocol Implementation 2 | // 3 | // This module implements the REALITY obfuscation protocol for TLS connections. 4 | // REALITY disguises proxy traffic as legitimate HTTPS connections using: 5 | // - X25519 ECDH key exchange 6 | // - HKDF-SHA256 key derivation 7 | // - AES-256-GCM encryption 8 | // - HMAC-SHA512 authentication 9 | 10 | mod common; 11 | mod reality_aead; 12 | mod reality_auth; 13 | mod reality_certificate; 14 | mod reality_cipher_suite; 15 | mod reality_client_connection; 16 | mod reality_client_verify; 17 | mod reality_io_state; 18 | mod reality_reader_writer; 19 | mod reality_records; 20 | mod reality_server_connection; 21 | mod reality_tls13_keys; 22 | mod reality_tls13_messages; 23 | mod reality_util; 24 | 25 | pub use reality_cipher_suite::{CipherSuite, DEFAULT_CIPHER_SUITES}; 26 | pub use reality_util::{decode_private_key, decode_public_key, decode_short_id, generate_keypair}; 27 | 28 | // Re-export connection types for crypto_connection module 29 | pub use reality_client_connection::{ 30 | RealityClientConfig, RealityClientConnection, feed_reality_client_connection, 31 | }; 32 | pub use reality_reader_writer::{RealityReader, RealityWriter}; 33 | pub use reality_server_connection::{ 34 | RealityServerConfig, RealityServerConnection, feed_reality_server_connection, 35 | }; 36 | -------------------------------------------------------------------------------- /src/shadowsocks/blake3_key.rs: -------------------------------------------------------------------------------- 1 | use super::shadowsocks_key::ShadowsocksKey; 2 | use crate::util::allocate_vec; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct Blake3Key { 6 | key_bytes: Box<[u8]>, 7 | session_key_len: usize, 8 | } 9 | 10 | impl Blake3Key { 11 | pub fn new(key_bytes: Box<[u8]>, session_key_len: usize) -> Self { 12 | Self { 13 | key_bytes, 14 | session_key_len, 15 | } 16 | } 17 | } 18 | 19 | const CONTEXT_STR: &str = "shadowsocks 2022 session subkey"; 20 | 21 | impl ShadowsocksKey for Blake3Key { 22 | fn create_session_key(&self, salt: &[u8]) -> Box<[u8]> { 23 | let salt_len = salt.len(); 24 | // both are 16 for aes-128-gcm, and both are 32 for aes-256-gcm 25 | // TODO: validate password length before we hit this assertion 26 | assert!(self.key_bytes.len() == salt_len); 27 | 28 | let mut key_material = allocate_vec(salt_len * 2); 29 | key_material[0..salt_len].copy_from_slice(&self.key_bytes); 30 | key_material[salt_len..].copy_from_slice(salt); 31 | 32 | let mut hasher = blake3::Hasher::new_derive_key(CONTEXT_STR); 33 | hasher.update(&key_material); 34 | let mut output_reader = hasher.finalize_xof(); 35 | 36 | let mut session_key = allocate_vec(self.session_key_len); 37 | output_reader.fill(&mut session_key); 38 | 39 | session_key.into_boxed_slice() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | fmt: 14 | name: Format 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install Rust 20 | uses: dtolnay/rust-toolchain@stable 21 | with: 22 | components: rustfmt 23 | 24 | - name: Check formatting 25 | run: cargo fmt -- --check 26 | 27 | clippy: 28 | name: Clippy 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - name: Install Rust 34 | uses: dtolnay/rust-toolchain@stable 35 | with: 36 | components: clippy 37 | 38 | - name: Cache cargo registry 39 | uses: actions/cache@v4 40 | with: 41 | path: ~/.cargo/registry 42 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} 43 | 44 | - name: Cache cargo index 45 | uses: actions/cache@v4 46 | with: 47 | path: ~/.cargo/git 48 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} 49 | 50 | - name: Cache cargo build 51 | uses: actions/cache@v4 52 | with: 53 | path: target 54 | key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} 55 | 56 | - name: Run clippy 57 | run: cargo clippy -- -D warnings 58 | -------------------------------------------------------------------------------- /src/shadowsocks/timed_salt_checker.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashSet, VecDeque}; 2 | use std::time::Instant; 3 | 4 | use super::salt_checker::SaltChecker; 5 | 6 | #[derive(Debug)] 7 | struct TimeEntry { 8 | instant: Instant, 9 | salt: Box<[u8]>, 10 | } 11 | 12 | #[derive(Debug)] 13 | pub struct TimedSaltChecker { 14 | last_salts: VecDeque, 15 | known_salts: HashSet>, 16 | timeout_secs: u64, 17 | } 18 | 19 | impl TimedSaltChecker { 20 | pub fn new(timeout_secs: u64) -> Self { 21 | Self { 22 | last_salts: VecDeque::with_capacity(2000), 23 | known_salts: HashSet::with_capacity(2000), 24 | timeout_secs, 25 | } 26 | } 27 | } 28 | 29 | impl SaltChecker for TimedSaltChecker { 30 | fn insert_and_check(&mut self, salt: &[u8]) -> bool { 31 | while let Some(time_entry) = self.last_salts.front() { 32 | if time_entry.instant.elapsed().as_secs() < self.timeout_secs { 33 | break; 34 | } 35 | self.known_salts.remove(&time_entry.salt); 36 | self.last_salts.pop_front(); 37 | } 38 | 39 | if self.known_salts.contains(salt) { 40 | return false; 41 | } 42 | 43 | self.known_salts.insert(salt.to_vec().into_boxed_slice()); 44 | self.last_salts.push_back(TimeEntry { 45 | instant: Instant::now(), 46 | salt: salt.to_vec().into_boxed_slice(), 47 | }); 48 | 49 | true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/vmess/nonce.rs: -------------------------------------------------------------------------------- 1 | use aws_lc_rs::aead::{Nonce, NonceSequence}; 2 | use aws_lc_rs::error::Unspecified; 3 | 4 | pub struct VmessNonceSequence { 5 | count: u16, 6 | nonce: [u8; 12], 7 | } 8 | 9 | impl VmessNonceSequence { 10 | pub fn new(data: &[u8]) -> Self { 11 | let mut nonce = [0u8; 12]; 12 | nonce[2..].copy_from_slice(&data[2..12]); 13 | Self { count: 0, nonce } 14 | } 15 | } 16 | 17 | impl NonceSequence for VmessNonceSequence { 18 | fn advance(&mut self) -> Result { 19 | // the nonce is correct for the first packet since the first two 20 | // bytes are already zero. 21 | let ret = Nonce::assume_unique_for_key(self.nonce); 22 | self.count = self.count.wrapping_add(1); 23 | self.nonce[0] = (self.count >> 8) as u8; 24 | self.nonce[1] = (self.count & 0xff) as u8; 25 | Ok(ret) 26 | } 27 | } 28 | 29 | pub struct SingleUseNonce { 30 | nonce: [u8; 12], 31 | used: bool, 32 | } 33 | 34 | impl SingleUseNonce { 35 | pub fn new(data: &[u8]) -> Self { 36 | let mut nonce = [0u8; 12]; 37 | nonce.copy_from_slice(data); 38 | Self { nonce, used: false } 39 | } 40 | } 41 | 42 | impl NonceSequence for SingleUseNonce { 43 | fn advance(&mut self) -> Result { 44 | if self.used { 45 | panic!("SingleUseNonce used twice"); 46 | } 47 | self.used = true; 48 | Nonce::try_assume_unique_for_key(&self.nonce) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/shadowsocks_basic.yaml: -------------------------------------------------------------------------------- 1 | # Basic Shadowsocks server examples 2 | 3 | # Shadowsocks with AES-128-GCM cipher 4 | - address: "0.0.0.0:8388" 5 | protocol: 6 | type: shadowsocks 7 | cipher: aes-128-gcm 8 | password: "mypassword123" 9 | 10 | # Shadowsocks with ChaCha20-Poly1305 cipher 11 | - address: "0.0.0.0:8389" 12 | protocol: 13 | type: shadowsocks 14 | cipher: chacha20-ietf-poly1305 15 | password: "anotherpassword456" 16 | 17 | # Shadowsocks 2022 with Blake3 (requires base64 encoded key) 18 | - address: "0.0.0.0:8390" 19 | protocol: 20 | type: shadowsocks 21 | cipher: 2022-blake3-aes-256-gcm 22 | password: "N5tp51JibWA9kIPEUoDHzyyjfgkDYToOERh+u9L47fk=" 23 | 24 | # Shadowsocks over QUIC transport 25 | - address: "0.0.0.0:8391" 26 | transport: quic 27 | quic_settings: 28 | cert: "cert.pem" 29 | key: "key.pem" 30 | protocol: 31 | type: shadowsocks 32 | cipher: aes-256-gcm 33 | password: "securepassword789" 34 | 35 | # Shadowsocks with custom routing rules 36 | - address: "0.0.0.0:8392" 37 | protocol: 38 | type: shadowsocks 39 | cipher: chacha20-ietf-poly1305 40 | password: "routingpassword" 41 | rules: 42 | # Allow local network direct connection 43 | - masks: ["192.168.0.0/16", "10.0.0.0/8"] 44 | action: allow 45 | client_chain: direct 46 | # Route everything else through upstream proxy 47 | - masks: "0.0.0.0/0" 48 | action: allow 49 | client_chain: 50 | address: "upstream.proxy.com:1080" 51 | protocol: 52 | type: socks 53 | -------------------------------------------------------------------------------- /src/uot/mod.rs: -------------------------------------------------------------------------------- 1 | //! SagerNet UDP-over-TCP (UoT) protocol implementation 2 | //! 3 | //! This module implements the sing-box UoT protocol for tunneling UDP over TCP. 4 | //! It supports both V1 and V2 formats. 5 | //! 6 | //! ## Magic Addresses 7 | //! - V1: `sp.udp-over-tcp.arpa` - Multi-destination mode, each packet has full address 8 | //! - V2: `sp.v2.udp-over-tcp.arpa` - Optional connect mode for single destination 9 | //! 10 | //! ## V1 Packet Format 11 | //! ```text 12 | //! | ATYP | address | port | length | data | 13 | //! | u8 | variable | u16be | u16be | variable | 14 | //! ``` 15 | //! 16 | //! ## V2 Request Format 17 | //! ```text 18 | //! | isConnect | ATYP | address | port | 19 | //! | u8 | u8 | variable | u16be | 20 | //! ``` 21 | //! 22 | //! If isConnect=1, subsequent packets are length-prefixed only (V2 connect mode). 23 | //! If isConnect=0, subsequent packets use V1 format (multi-destination). 24 | //! 25 | //! ## ATYP Values (different from SOCKS5!) 26 | //! - 0x00: IPv4 Address (4 bytes) 27 | //! - 0x01: IPv6 Address (16 bytes) 28 | //! - 0x02: Domain Name (1 byte length + domain) 29 | 30 | mod uot_v1_stream; 31 | 32 | pub use uot_v1_stream::UotV1Stream; 33 | 34 | /// UoT V2 connect mode stream - identical format to VlessMessageStream (length-prefixed u16be + data) 35 | pub type UotV2Stream = crate::vless::VlessMessageStream; 36 | 37 | /// Magic address used to signal UoT V1 mode (multi-destination) 38 | pub const UOT_V1_MAGIC_ADDRESS: &str = "sp.udp-over-tcp.arpa"; 39 | 40 | /// Magic address used to signal UoT V2 mode (optional connect mode) 41 | pub const UOT_V2_MAGIC_ADDRESS: &str = "sp.v2.udp-over-tcp.arpa"; 42 | -------------------------------------------------------------------------------- /src/config/types/mod.rs: -------------------------------------------------------------------------------- 1 | //! Configuration types for the proxy server. 2 | //! 3 | //! This module contains all the configuration types used by the proxy server, 4 | //! organized into submodules by functionality: 5 | //! 6 | //! - [`common`]: Shared helpers and constants 7 | //! - [`transport`]: Transport layer types (TCP, QUIC, UDP) 8 | //! - [`shadowsocks`]: Shadowsocks protocol configuration 9 | //! - [`selection`]: ConfigSelection for referencing groups or inline configs 10 | //! - [`server`]: Server-side protocol configurations 11 | //! - [`client`]: Client-side protocol configurations 12 | //! - [`rules`]: Rule configurations for traffic routing 13 | //! - [`groups`]: Top-level configuration groups and the Config enum 14 | 15 | pub mod client; 16 | pub mod common; 17 | pub mod groups; 18 | pub mod rules; 19 | pub mod selection; 20 | pub mod server; 21 | pub mod shadowsocks; 22 | pub mod transport; 23 | 24 | // Re-export all public types for convenience 25 | pub use client::{ClientConfig, ClientProxyConfig, TlsClientConfig, WebsocketClientConfig}; 26 | pub use common::DEFAULT_REALITY_SHORT_ID; 27 | pub use groups::{ClientConfigGroup, Config, NamedPem, PemSource}; 28 | pub use rules::{ClientChain, ClientChainHop, RuleActionConfig, RuleConfig}; 29 | pub use selection::ConfigSelection; 30 | pub use server::{ 31 | RealityServerConfig, ServerConfig, ServerProxyConfig, ShadowTlsServerConfig, 32 | ShadowTlsServerHandshakeConfig, TlsServerConfig, WebsocketPingType, WebsocketServerConfig, 33 | direct_allow_rule, 34 | }; 35 | pub use shadowsocks::ShadowsocksConfig; 36 | pub use transport::{BindLocation, ClientQuicConfig, ServerQuicConfig, TcpConfig, Transport}; 37 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use tokio::io::AsyncWriteExt; 2 | 3 | #[inline] 4 | #[allow(clippy::uninit_vec)] 5 | pub fn allocate_vec(len: usize) -> Vec { 6 | let mut ret = Vec::with_capacity(len); 7 | unsafe { 8 | ret.set_len(len); 9 | } 10 | ret 11 | } 12 | 13 | #[inline] 14 | pub fn parse_uuid(uuid_str: &str) -> std::io::Result> { 15 | let mut bytes = Vec::with_capacity(16); 16 | let mut first_nibble: Option = None; 17 | for &c in uuid_str.as_bytes() { 18 | let hex = match c { 19 | b'0'..=b'9' => c - b'0', 20 | b'a'..=b'f' => c - b'a' + 10, 21 | b'A'..=b'F' => c - b'A' + 10, 22 | b'-' => continue, 23 | _ => { 24 | return Err(std::io::Error::new( 25 | std::io::ErrorKind::InvalidData, 26 | format!("Invalid uuid: {uuid_str}"), 27 | )); 28 | } 29 | }; 30 | if let Some(first) = first_nibble.take() { 31 | bytes.push((first << 4) | hex); 32 | } else { 33 | first_nibble = Some(hex); 34 | } 35 | } 36 | if first_nibble.is_some() || bytes.len() != 16 { 37 | return Err(std::io::Error::new( 38 | std::io::ErrorKind::InvalidData, 39 | format!("Invalid uuid: {uuid_str}"), 40 | )); 41 | } 42 | Ok(bytes) 43 | } 44 | 45 | // a cancellable alternative to AsyncWriteExt::write_all 46 | pub async fn write_all( 47 | stream: &mut T, 48 | buf: &[u8], 49 | ) -> std::io::Result<()> { 50 | let mut i = 0; 51 | let n = buf.len(); 52 | while i < n { 53 | let n = stream.write(&buf[i..]).await?; 54 | i += n; 55 | } 56 | Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /examples/reality_client.yaml: -------------------------------------------------------------------------------- 1 | # Reality client configuration 2 | # Connects to a Reality server with censorship resistance 3 | # 4 | # This example shows how to configure a client to connect to a Reality server 5 | # The client needs the server's public key and a valid short ID 6 | 7 | # Local SOCKS proxy that uses Reality client 8 | - address: "127.0.0.1:1080" 9 | protocol: 10 | type: socks 11 | rules: 12 | - masks: "0.0.0.0/0" 13 | action: allow 14 | client_chain: 15 | address: "your-server.example.com:443" 16 | protocol: 17 | type: reality 18 | # Server's public key (from keypair generation) 19 | public_key: "SERVER_PUBLIC_KEY_HERE" 20 | # Your client ID (must match server's short_ids list) 21 | short_id: "0123456789abcdef" 22 | # Hostname to use (should match server's reality_targets key) 23 | sni_hostname: "www.cloudflare.com" 24 | # Optional: TLS 1.3 cipher suites to offer 25 | # cipher_suites: ["TLS_AES_256_GCM_SHA384"] 26 | # Inner protocol - must match what the server expects 27 | protocol: 28 | type: vless 29 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 30 | 31 | # Alternative: HTTP proxy using Reality 32 | - address: "127.0.0.1:8080" 33 | protocol: 34 | type: http 35 | rules: 36 | - masks: "0.0.0.0/0" 37 | action: allow 38 | client_chain: 39 | address: "your-server.example.com:443" 40 | protocol: 41 | type: reality 42 | public_key: "SERVER_PUBLIC_KEY_HERE" 43 | short_id: "0123456789abcdef" 44 | sni_hostname: "www.cloudflare.com" 45 | protocol: 46 | type: vless 47 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 48 | -------------------------------------------------------------------------------- /examples/http_proxy_basic.yaml: -------------------------------------------------------------------------------- 1 | # Basic HTTP/HTTPS proxy examples 2 | 3 | # Simple HTTP proxy without authentication 4 | - address: "0.0.0.0:8080" 5 | protocol: 6 | type: http 7 | 8 | # HTTP proxy with authentication 9 | - address: "0.0.0.0:8081" 10 | protocol: 11 | type: http 12 | username: "proxyuser" 13 | password: "proxypass" 14 | 15 | # HTTPS proxy with TLS termination 16 | - address: "0.0.0.0:8443" 17 | protocol: 18 | type: tls 19 | default_target: 20 | cert: "cert.pem" 21 | key: "key.pem" 22 | protocol: 23 | type: http 24 | username: "secureuser" 25 | password: "securepass" 26 | 27 | # HTTP proxy with client certificate authentication 28 | - address: "0.0.0.0:9443" 29 | protocol: 30 | type: tls 31 | default_target: 32 | cert: "server-cert.pem" 33 | key: "server-key.pem" 34 | client_ca_certs: 35 | - "ca-cert.pem" 36 | client_fingerprints: 37 | - "SHA256:1234567890abcdef..." 38 | protocol: 39 | type: http 40 | 41 | # HTTP proxy with custom routing rules 42 | - address: "0.0.0.0:3128" 43 | protocol: 44 | type: http 45 | rules: 46 | # Block access to private networks 47 | - masks: ["192.168.0.0/16", "10.0.0.0/8", "172.16.0.0/12"] 48 | action: block 49 | # Allow everything else directly 50 | - masks: "0.0.0.0/0" 51 | action: allow 52 | # client_chain: direct is optional - direct is the default when omitted 53 | client_chain: direct 54 | 55 | # HTTP proxy chained through SOCKS5 56 | - address: "0.0.0.0:8082" 57 | protocol: 58 | type: http 59 | username: "chainuser" 60 | password: "chainpass" 61 | rules: 62 | - masks: "0.0.0.0/0" 63 | action: allow 64 | client_chain: 65 | address: "socks5.proxy.com:1080" 66 | protocol: 67 | type: socks 68 | username: "socksuser" 69 | password: "sockspass" -------------------------------------------------------------------------------- /src/rustls_connection_util.rs: -------------------------------------------------------------------------------- 1 | use std::io::Cursor; 2 | 3 | #[inline(always)] 4 | pub fn feed_rustls_server_connection( 5 | connection: &mut rustls::ServerConnection, 6 | data: &[u8], 7 | ) -> std::io::Result<()> { 8 | let mut cursor = Cursor::new(data); 9 | let mut i = 0; 10 | while i < data.len() { 11 | let n = connection.read_tls(&mut cursor).map_err(|e| { 12 | std::io::Error::new( 13 | std::io::ErrorKind::InvalidData, 14 | format!("failed to feed rustls server connection: {e}"), 15 | ) 16 | })?; 17 | if n == 0 { 18 | return Err(std::io::Error::new( 19 | std::io::ErrorKind::InvalidData, 20 | format!( 21 | "rustls server connection did not consume all bytes: fed {}/{} bytes", 22 | i, 23 | data.len() 24 | ), 25 | )); 26 | } 27 | i += n; 28 | } 29 | Ok(()) 30 | } 31 | 32 | #[inline(always)] 33 | pub fn feed_rustls_client_connection( 34 | connection: &mut rustls::ClientConnection, 35 | data: &[u8], 36 | ) -> std::io::Result<()> { 37 | let mut cursor = Cursor::new(data); 38 | let mut i = 0; 39 | while i < data.len() { 40 | let n = connection.read_tls(&mut cursor).map_err(|e| { 41 | std::io::Error::new( 42 | std::io::ErrorKind::InvalidData, 43 | format!("failed to feed rustls client connection: {e}"), 44 | ) 45 | })?; 46 | if n == 0 { 47 | return Err(std::io::Error::new( 48 | std::io::ErrorKind::InvalidData, 49 | format!( 50 | "rustls client connection did not consume all bytes: fed {}/{} bytes", 51 | i, 52 | data.len() 53 | ), 54 | )); 55 | } 56 | i += n; 57 | } 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /src/shadowsocks/default_key.rs: -------------------------------------------------------------------------------- 1 | use md5::{Digest, Md5}; 2 | 3 | use super::shadowsocks_key::ShadowsocksKey; 4 | use crate::util::allocate_vec; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct DefaultKey { 8 | key_bytes: Box<[u8]>, 9 | key_len: usize, 10 | } 11 | 12 | impl DefaultKey { 13 | pub fn new(password: &str, key_len: usize) -> Self { 14 | Self { 15 | key_bytes: get_key_bytes(password, key_len), 16 | key_len, 17 | } 18 | } 19 | } 20 | 21 | const SS_SUBKEY_INFO: &[&[u8]] = &[b"ss-subkey"]; 22 | 23 | struct SliceKeyType<'a>(&'a [u8]); 24 | 25 | impl aws_lc_rs::hkdf::KeyType for SliceKeyType<'_> { 26 | fn len(&self) -> usize { 27 | self.0.len() 28 | } 29 | } 30 | 31 | impl ShadowsocksKey for DefaultKey { 32 | fn create_session_key(&self, salt: &[u8]) -> Box<[u8]> { 33 | let mut session_key = allocate_vec(self.key_len); 34 | aws_lc_rs::hkdf::Salt::new(aws_lc_rs::hkdf::HKDF_SHA1_FOR_LEGACY_USE_ONLY, salt) 35 | .extract(&self.key_bytes) 36 | .expand(SS_SUBKEY_INFO, SliceKeyType(&self.key_bytes)) 37 | .unwrap() 38 | .fill(&mut session_key) 39 | .unwrap(); 40 | session_key.into_boxed_slice() 41 | } 42 | } 43 | 44 | fn get_key_bytes(key: &str, cipher_key_len: usize) -> Box<[u8]> { 45 | // TODO: This is the same as openssl::pkcs5::bytes_to_key. 46 | // Is it possible not to depend on the md5 crate when openssl is enabled? 47 | let key = key.as_bytes(); 48 | let mut ret = vec![]; 49 | let mut context = Md5::new(); 50 | loop { 51 | context.update(key); 52 | let digest: [u8; 16] = context.finalize().into(); 53 | ret.extend(digest.iter()); 54 | if ret.len() >= cipher_key_len { 55 | break; 56 | } 57 | context = Md5::new(); 58 | context.update(digest); 59 | } 60 | ret.truncate(cipher_key_len); 61 | ret.into_boxed_slice() 62 | } 63 | -------------------------------------------------------------------------------- /examples/reality_trojan.yaml: -------------------------------------------------------------------------------- 1 | # Reality server with Trojan inner protocol 2 | # Shows Reality works with protocols other than VLESS (when Vision is disabled) 3 | # 4 | # Reality provides censorship resistance regardless of the inner protocol 5 | # However, Vision (TLS-in-TLS optimization) only works with VLESS 6 | # This example uses Trojan without Vision 7 | 8 | - address: "0.0.0.0:443" 9 | protocol: 10 | type: tls 11 | reality_targets: 12 | "www.cloudflare.com": 13 | private_key: "YOUR_PRIVATE_KEY_HERE" 14 | short_ids: ["0123456789abcdef"] 15 | dest: "www.cloudflare.com:443" 16 | 17 | # vision: false (default) 18 | # Vision is not enabled because Trojan is the inner protocol 19 | # Vision only works with VLESS 20 | 21 | protocol: 22 | type: trojan 23 | password: "your-trojan-password" 24 | 25 | # Multiple Reality targets with different inner protocols 26 | - address: "0.0.0.0:8443" 27 | protocol: 28 | type: tls 29 | reality_targets: 30 | # VLESS target 31 | "vless.example.com": 32 | private_key: "YOUR_PRIVATE_KEY_1" 33 | short_ids: ["0123456789abcdef"] 34 | dest: "www.cloudflare.com:443" 35 | protocol: 36 | type: vless 37 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 38 | 39 | # Trojan target 40 | "trojan.example.com": 41 | private_key: "YOUR_PRIVATE_KEY_2" 42 | short_ids: ["1234567890abcdef"] 43 | dest: "www.google.com:443" 44 | protocol: 45 | type: trojan 46 | password: "trojan-password" 47 | 48 | # Shadowsocks target 49 | "ss.example.com": 50 | private_key: "YOUR_PRIVATE_KEY_3" 51 | short_ids: ["abcdef0123456789"] 52 | dest: "www.microsoft.com:443" 53 | protocol: 54 | type: shadowsocks 55 | cipher: 2022-blake3-chacha20-ietf-poly1305 56 | password: "N5tp51JibWA9kIPEUoDHzyyjfgkDYToOERh+u9L47fk=" 57 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shoes" 3 | version = "0.2.2" 4 | edition = "2024" 5 | license = "MIT" 6 | description = "A multi-protocol proxy server." 7 | homepage = "https://github.com/cfal/shoes/" 8 | repository = "https://github.com/cfal/shoes/" 9 | readme = "README.md" 10 | keywords = ["proxy", "proxy-server", "shadowsocks", "v2ray", "vmess"] 11 | categories = ["command-line-utilities", "network-programming"] 12 | 13 | [dependencies] 14 | aes = "*" 15 | argon2 = "*" 16 | async-trait = "*" 17 | aws-lc-rs = { version = "*", default-features = false } 18 | base64 = "*" 19 | blake3 = "*" 20 | bytes = "*" 21 | dashmap = "*" 22 | digest = "*" 23 | env_logger = "*" 24 | futures = "*" 25 | h3 = "*" 26 | h3-quinn = "*" 27 | http = "*" 28 | log = { version = "*", features = ["release_max_level_info"] } 29 | lru = "*" 30 | md-5 = "*" 31 | memchr = "*" 32 | notify = "*" 33 | parking_lot = "*" 34 | percent-encoding = "*" 35 | quinn = { version = "*", default-features = false, features = ["log", "platform-verifier", "runtime-tokio", "rustls-aws-lc-rs"] } 36 | rand = "*" 37 | rand_core = "*" 38 | rcgen = { version = "*", features = ["x509-parser"] } 39 | rustc-hash = "*" 40 | rustls = "*" 41 | serde = { version = "*", features = ["derive", "std"] } 42 | subtle = "*" 43 | serde_yaml = "*" 44 | sha3 = "*" 45 | socket2 = "*" 46 | tokio = { version = "*", features = [ 47 | "fs", 48 | "io-util", 49 | "macros", 50 | "net", 51 | "rt-multi-thread", 52 | "sync", 53 | "time", 54 | ] } 55 | tokio-util = "*" 56 | webpki-roots = { version = "*" } 57 | x509-parser = "*" 58 | 59 | [dev-dependencies] 60 | tempfile = "*" 61 | hyper = { version = "1", features = ["server", "http1"] } 62 | hyper-util = { version = "0.1", features = ["tokio", "server"] } 63 | http-body-util = "0.1" 64 | rustls-pemfile = "*" 65 | rcgen = "*" 66 | tokio-rustls = "*" 67 | serde_json = "*" 68 | 69 | [target.'cfg(not(target_env = "msvc"))'.dependencies] 70 | tikv-jemallocator = "0.6" 71 | 72 | [profile.release] 73 | opt-level = 3 74 | lto = "fat" 75 | strip = true 76 | -------------------------------------------------------------------------------- /src/shadowsocks/shadowsocks_cipher.rs: -------------------------------------------------------------------------------- 1 | // TODO: investigate using SIV variants for nonce reuse resistance 2 | use aws_lc_rs::aead::{AES_128_GCM, AES_256_GCM, Algorithm, CHACHA20_POLY1305}; 3 | 4 | use super::aead_util::TAG_LEN; 5 | 6 | #[derive(Debug, Clone, Copy)] 7 | pub struct ShadowsocksCipher { 8 | algorithm: &'static Algorithm, 9 | salt_len: usize, 10 | name: &'static str, 11 | } 12 | 13 | impl ShadowsocksCipher { 14 | fn chacha20_ietf_poly1305() -> Self { 15 | Self::new(&CHACHA20_POLY1305, 32, "chacha20-ietf-poly1305") 16 | } 17 | 18 | fn aes_256_gcm() -> Self { 19 | Self::new(&AES_256_GCM, 32, "aes-256-gcm") 20 | } 21 | 22 | fn aes_128_gcm() -> Self { 23 | Self::new(&AES_128_GCM, 16, "aes-128-gcm") 24 | } 25 | 26 | fn new(algorithm: &'static Algorithm, salt_len: usize, name: &'static str) -> Self { 27 | if algorithm.tag_len() != TAG_LEN { 28 | panic!("Unexpected tag length: {}", algorithm.tag_len()); 29 | } 30 | Self { 31 | algorithm, 32 | salt_len, 33 | name, 34 | } 35 | } 36 | 37 | pub fn algorithm(&self) -> &'static Algorithm { 38 | self.algorithm 39 | } 40 | 41 | pub fn salt_len(&self) -> usize { 42 | self.salt_len 43 | } 44 | 45 | pub fn key_len(&self) -> usize { 46 | self.algorithm.key_len() 47 | } 48 | 49 | pub fn name(&self) -> &'static str { 50 | self.name 51 | } 52 | } 53 | 54 | impl TryFrom<&str> for ShadowsocksCipher { 55 | type Error = std::io::Error; 56 | 57 | fn try_from(name: &str) -> Result { 58 | match name { 59 | "chacha20-ietf-poly1305" | "chacha20-poly1305" => { 60 | Ok(ShadowsocksCipher::chacha20_ietf_poly1305()) 61 | } 62 | "aes-256-gcm" => Ok(ShadowsocksCipher::aes_256_gcm()), 63 | "aes-128-gcm" => Ok(ShadowsocksCipher::aes_128_gcm()), 64 | _ => Err(std::io::Error::other(format!("Unknown cipher: {name}"))), 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/crypto/crypto_reader_writer.rs: -------------------------------------------------------------------------------- 1 | // Reader and Writer types for crypto connections 2 | // 3 | // These types provide a consistent API for reading decrypted plaintext 4 | // and writing plaintext to be encrypted, working with both rustls and REALITY. 5 | 6 | use std::io::{self, BufRead, Read, Write}; 7 | 8 | use crate::reality::{RealityReader, RealityWriter}; 9 | 10 | /// Unified reader that works with both Rustls and REALITY connections 11 | pub enum CryptoReader<'a> { 12 | Rustls(rustls::Reader<'a>), 13 | Reality(RealityReader<'a>), 14 | } 15 | 16 | impl<'a> Read for CryptoReader<'a> { 17 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 18 | match self { 19 | CryptoReader::Rustls(reader) => reader.read(buf), 20 | CryptoReader::Reality(reader) => reader.read(buf), 21 | } 22 | } 23 | } 24 | 25 | impl<'a> BufRead for CryptoReader<'a> { 26 | fn fill_buf(&mut self) -> io::Result<&[u8]> { 27 | match self { 28 | CryptoReader::Rustls(reader) => reader.fill_buf(), 29 | CryptoReader::Reality(reader) => reader.fill_buf(), 30 | } 31 | } 32 | 33 | fn consume(&mut self, amt: usize) { 34 | match self { 35 | CryptoReader::Rustls(reader) => reader.consume(amt), 36 | CryptoReader::Reality(reader) => reader.consume(amt), 37 | } 38 | } 39 | } 40 | 41 | /// Unified writer that works with both Rustls and REALITY connections 42 | pub enum CryptoWriter<'a> { 43 | Rustls(rustls::Writer<'a>), 44 | Reality(RealityWriter<'a>), 45 | } 46 | 47 | impl<'a> Write for CryptoWriter<'a> { 48 | fn write(&mut self, buf: &[u8]) -> io::Result { 49 | match self { 50 | CryptoWriter::Rustls(writer) => writer.write(buf), 51 | CryptoWriter::Reality(writer) => writer.write(buf), 52 | } 53 | } 54 | 55 | fn flush(&mut self) -> io::Result<()> { 56 | match self { 57 | CryptoWriter::Rustls(writer) => writer.flush(), 58 | CryptoWriter::Reality(writer) => writer.flush(), 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/port_forward_handler.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicU32, Ordering}; 2 | 3 | use async_trait::async_trait; 4 | 5 | use crate::address::NetLocation; 6 | use crate::async_stream::AsyncStream; 7 | use crate::option_util::NoneOrOne; 8 | use crate::tcp_handler::{TcpClientHandler, TcpClientSetupResult}; 9 | use crate::tcp_handler::{TcpServerHandler, TcpServerSetupResult}; 10 | 11 | #[derive(Debug)] 12 | pub struct PortForwardServerHandler { 13 | targets: Vec, 14 | next_target_index: AtomicU32, 15 | } 16 | 17 | impl PortForwardServerHandler { 18 | pub fn new(targets: Vec) -> Self { 19 | Self { 20 | targets, 21 | next_target_index: AtomicU32::new(0), 22 | } 23 | } 24 | } 25 | 26 | #[async_trait] 27 | impl TcpServerHandler for PortForwardServerHandler { 28 | async fn setup_server_stream( 29 | &self, 30 | server_stream: Box, 31 | ) -> std::io::Result { 32 | let location = if self.targets.len() == 1 { 33 | &self.targets[0] 34 | } else { 35 | let target_index = self.next_target_index.fetch_add(1, Ordering::Relaxed) as usize; 36 | &self.targets[target_index % self.targets.len()] 37 | }; 38 | 39 | Ok(TcpServerSetupResult::TcpForward { 40 | remote_location: location.clone(), 41 | stream: server_stream, 42 | need_initial_flush: true, 43 | connection_success_response: None, 44 | initial_remote_data: None, 45 | override_proxy_provider: NoneOrOne::Unspecified, 46 | }) 47 | } 48 | } 49 | 50 | #[derive(Debug)] 51 | pub struct PortForwardClientHandler; 52 | 53 | #[async_trait] 54 | impl TcpClientHandler for PortForwardClientHandler { 55 | async fn setup_client_tcp_stream( 56 | &self, 57 | client_stream: Box, 58 | _remote_location: NetLocation, 59 | ) -> std::io::Result { 60 | Ok(TcpClientSetupResult { 61 | client_stream, 62 | early_data: None, 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/quic_stream.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | use std::task::{Context, Poll}; 3 | 4 | use quinn::{RecvStream, SendStream}; 5 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 6 | 7 | use crate::async_stream::{AsyncPing, AsyncStream}; 8 | 9 | pub struct QuicStream { 10 | send_stream: SendStream, 11 | recv_stream: RecvStream, 12 | } 13 | 14 | impl QuicStream { 15 | pub fn from(send_stream: quinn::SendStream, recv_stream: quinn::RecvStream) -> Self { 16 | Self { 17 | send_stream, 18 | recv_stream, 19 | } 20 | } 21 | } 22 | 23 | impl AsyncRead for QuicStream { 24 | fn poll_read( 25 | self: Pin<&mut Self>, 26 | cx: &mut Context<'_>, 27 | buf: &mut ReadBuf<'_>, 28 | ) -> Poll> { 29 | let this = self.get_mut(); 30 | Pin::new(&mut this.recv_stream).poll_read(cx, buf) 31 | } 32 | } 33 | 34 | impl AsyncWrite for QuicStream { 35 | fn poll_write( 36 | self: Pin<&mut Self>, 37 | cx: &mut Context<'_>, 38 | buf: &[u8], 39 | ) -> Poll> { 40 | let this = self.get_mut(); 41 | Pin::new(&mut this.send_stream) 42 | .poll_write(cx, buf) 43 | .map_err(|err| err.into()) 44 | } 45 | 46 | fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 47 | // this is a no-op, so return ready directly 48 | Poll::Ready(Ok(())) 49 | } 50 | 51 | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 52 | // TODO: this was previously disabled because, because it caused a panic with a mutex poison error in quinn. 53 | // ref: https://github.com/quinn-rs/quinn/issues/1298 54 | let this = self.get_mut(); 55 | Pin::new(&mut this.send_stream).poll_shutdown(cx) 56 | } 57 | } 58 | 59 | impl AsyncPing for QuicStream { 60 | fn supports_ping(&self) -> bool { 61 | false 62 | } 63 | 64 | fn poll_write_ping(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 65 | unimplemented!(); 66 | } 67 | } 68 | 69 | impl AsyncStream for QuicStream {} 70 | -------------------------------------------------------------------------------- /src/tcp/socket_connector.rs: -------------------------------------------------------------------------------- 1 | //! SocketConnector trait - Creates socket connections (TCP or QUIC). 2 | //! 3 | //! This trait handles the socket-level connection at hop 0 of a chain. 4 | //! It is responsible for: 5 | //! - Creating TCP sockets with bind_interface 6 | //! - Creating and caching QUIC endpoints 7 | //! - UDP socket creation for direct connections 8 | //! 9 | //! ## Design 10 | //! 11 | //! Every `ClientConfig` implicitly defines a `SocketConnector` through its 12 | //! socket-related fields: `bind_interface`, `transport`, `tcp_settings`, `quic_settings`. 13 | //! 14 | //! When a config is used: 15 | //! - **As hop 0**: The SocketConnector is used to create the connection 16 | //! - **As hop 1+**: The SocketConnector config is ignored (connection comes from previous hop) 17 | 18 | use std::fmt::Debug; 19 | use std::sync::Arc; 20 | 21 | use async_trait::async_trait; 22 | 23 | use crate::async_stream::AsyncStream; 24 | use crate::resolver::Resolver; 25 | use crate::tcp_handler::{TcpClientUdpSetupResult, UdpStreamRequest}; 26 | 27 | use crate::address::NetLocation; 28 | 29 | /// Trait for creating socket connections at hop 0. 30 | /// 31 | /// Only used at the first hop of a chain. Handles TCP and QUIC transports 32 | /// with optional bind_interface. 33 | #[async_trait] 34 | pub trait SocketConnector: Send + Sync + Debug { 35 | /// Create a TCP/QUIC connection to the given address. 36 | /// 37 | /// # Arguments 38 | /// * `resolver` - DNS resolver for address resolution 39 | /// * `address` - Target address to connect to 40 | async fn connect( 41 | &self, 42 | resolver: &Arc, 43 | address: &NetLocation, 44 | ) -> std::io::Result>; 45 | 46 | /// Create UDP socket(s) for the given request type. 47 | /// 48 | /// Returns matched server/client stream pair ready for copying. 49 | /// 50 | /// # Arguments 51 | /// * `resolver` - DNS resolver for address resolution 52 | /// * `request` - The type of UDP stream requested 53 | async fn connect_udp( 54 | &self, 55 | resolver: &Arc, 56 | request: UdpStreamRequest, 57 | ) -> std::io::Result; 58 | } 59 | -------------------------------------------------------------------------------- /examples/reality_basic.yaml: -------------------------------------------------------------------------------- 1 | # Basic Reality server with VLESS 2 | # Provides enhanced privacy through TLS 1.3 mimicry 3 | # 4 | # Reality uses X25519 key exchange and HMAC-signed certificates to provide 5 | # enhanced privacy. Non-Reality traffic is forwarded to the fallback 6 | # destination, making the server look like a real website to observers. 7 | # 8 | # Generate a Reality keypair with: an X25519 keypair generation tool 9 | 10 | - address: "0.0.0.0:443" 11 | protocol: 12 | type: tls 13 | reality_targets: 14 | "www.cloudflare.com": 15 | # X25519 private key (32 bytes, base64url encoded) 16 | # Keep this secret! Clients need the corresponding public key. 17 | private_key: "YOUR_PRIVATE_KEY_HERE" 18 | 19 | # List of valid short IDs (hex strings, 0-16 chars) 20 | # Clients must use one of these IDs to connect 21 | # Empty string "" allows all clients (less secure but convenient) 22 | short_ids: 23 | - "0123456789abcdef" # Specific client ID 24 | - "" # Allow all clients 25 | 26 | # Fallback destination for non-Reality traffic 27 | # This should be a real website - traffic that doesn't match Reality 28 | # protocol will be forwarded here, making observers see a real site 29 | dest: "www.cloudflare.com:443" 30 | 31 | # Optional: Maximum timestamp difference in milliseconds (default: 60000) 32 | # Prevents replay attacks by rejecting connections with old timestamps 33 | max_time_diff: 60000 # 1 minute 34 | 35 | # Optional: Client version restrictions [major, minor, patch] 36 | # min_client_version: [1, 8, 0] 37 | # max_client_version: [2, 0, 0] 38 | 39 | # Optional: TLS 1.3 cipher suites to support 40 | # Valid: TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256 41 | # cipher_suites: ["TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256"] 42 | 43 | # Inner protocol - the actual proxy protocol used after Reality handshake 44 | protocol: 45 | type: vless 46 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 47 | udp_enabled: true # Enable XUDP for UDP multiplexing 48 | -------------------------------------------------------------------------------- /examples/shadowtls_v3_remote_handshake.yaml: -------------------------------------------------------------------------------- 1 | # server example of a single server that serves both normal TLS and ShadowTLS 2 | - address: 127.0.0.1:44300 3 | transport: tcp 4 | protocol: 5 | type: tls 6 | 7 | # specify different passwords and proxy protocols based on ShadowTLS SNI 8 | shadowtls_targets: 9 | example.com: 10 | password: shadowtls-password 11 | handshake: 12 | # connect to example.com:443 as the handshake server 13 | address: example.com:443 14 | # optionally, use a client proxy for the handshake server connection. 15 | # specify multiple proxies to load balance. 16 | client_proxies: 17 | # direct connection 18 | - protocol: 19 | type: direct 20 | # connect through socks 21 | - address: 127.0.0.1:5555 22 | protocol: 23 | type: socks 24 | username: internal-socks-user 25 | password: internal-socks-pass 26 | protocol: 27 | type: socks 28 | username: socks-user 29 | password: socks-pass 30 | 31 | google.com: 32 | password: shadowtls-password2 33 | handshake: 34 | # connect directly to google.com as the handshake server 35 | address: google.com:443 36 | protocol: 37 | type: socks 38 | username: google-socks-user 39 | password: google-socks-pass 40 | 41 | # regular TLS targets 42 | tls_targets: 43 | # provide a regular HTTPS proxy 44 | example2.com: 45 | key: private_key.pem 46 | cert: cert.pem 47 | protocol: 48 | type: http 49 | username: https-user 50 | password: https-password 51 | 52 | # default TLS target, when no SNI is provided or no SNI from `targets` matches. 53 | default_tls_target: 54 | key: default_key.pem 55 | cert: default_cert.pem 56 | protocol: 57 | type: socks 58 | username: default-socks-user 59 | password: default-socks-pass 60 | 61 | rules: 62 | - mask: 0.0.0.0/0 63 | action: allow 64 | # client_chain: direct is optional - direct is the default when omitted 65 | client_chain: direct 66 | -------------------------------------------------------------------------------- /src/sync_adapter.rs: -------------------------------------------------------------------------------- 1 | /// Sync adapters for bridging async I/O with rustls's synchronous API 2 | /// 3 | /// These adapters allow rustls's synchronous `read_tls()` and `write_tls()` methods 4 | /// to work with Tokio's async I/O primitives. 5 | /// 6 | /// Adapted from tokio-rustls: 7 | /// https://github.com/rustls/tokio-rustls/blob/ba767aeb51611107e7cb6aa756f10a2f49e70926/src/common/mod.rs#L403 8 | use std::io::{self, Write}; 9 | use std::pin::Pin; 10 | use std::task::{Context, Poll}; 11 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 12 | 13 | /// Adapter to bridge async read to synchronous read for rustls 14 | /// 15 | /// This allows rustls's `read_tls()` method to read from async TCP sockets. 16 | /// When the async socket would block (Poll::Pending), this returns WouldBlock error. 17 | pub struct SyncReadAdapter<'a, 'b, T> { 18 | pub io: &'a mut T, 19 | pub cx: &'a mut Context<'b>, 20 | } 21 | 22 | impl std::io::Read for SyncReadAdapter<'_, '_, T> { 23 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 24 | let mut read_buf = ReadBuf::new(buf); 25 | match Pin::new(&mut self.io).poll_read(self.cx, &mut read_buf) { 26 | Poll::Ready(Ok(())) => Ok(read_buf.filled().len()), 27 | Poll::Ready(Err(e)) => Err(e), 28 | Poll::Pending => Err(io::ErrorKind::WouldBlock.into()), 29 | } 30 | } 31 | } 32 | 33 | /// Adapter to bridge async write to synchronous write for rustls 34 | /// 35 | /// This allows rustls's `write_tls()` method to write to async TCP sockets. 36 | /// When the async socket would block (Poll::Pending), this returns WouldBlock error. 37 | pub struct SyncWriteAdapter<'a, 'b, T> { 38 | pub io: &'a mut T, 39 | pub cx: &'a mut Context<'b>, 40 | } 41 | 42 | impl Write for SyncWriteAdapter<'_, '_, T> { 43 | fn write(&mut self, buf: &[u8]) -> io::Result { 44 | match Pin::new(&mut self.io).poll_write(self.cx, buf) { 45 | Poll::Ready(result) => result, 46 | Poll::Pending => Err(io::ErrorKind::WouldBlock.into()), 47 | } 48 | } 49 | 50 | fn flush(&mut self) -> io::Result<()> { 51 | match Pin::new(&mut self.io).poll_flush(self.cx) { 52 | Poll::Ready(result) => result, 53 | Poll::Pending => Err(io::ErrorKind::WouldBlock.into()), 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | //! Configuration module for the proxy server. 2 | //! 3 | //! This module provides: 4 | //! - [`types`]: All configuration types (server, client, rules, etc.) 5 | //! - [`pem`]: PEM file handling and certificate loading 6 | //! - [`validate`]: Configuration validation and server config creation 7 | //! 8 | //! The main entry points are: 9 | //! - [`load_configs`]: Load config files from disk 10 | //! - [`convert_cert_paths`]: Convert PEM file paths to inline data 11 | //! - [`create_server_configs`]: Validate and create final server configs 12 | 13 | mod pem; 14 | mod types; 15 | mod validate; 16 | 17 | pub use pem::convert_cert_paths; 18 | pub use types::*; 19 | pub use validate::create_server_configs; 20 | 21 | // Re-export ConfigSelection for use in pem.rs 22 | pub(crate) use types::ConfigSelection; 23 | 24 | /// Loads configuration files from the provided paths. 25 | /// 26 | /// Reads each file, parses it as YAML, and returns the combined list of configs. 27 | pub async fn load_configs(args: &Vec) -> std::io::Result> { 28 | let mut all_configs = vec![]; 29 | for config_filename in args { 30 | let config_bytes = match tokio::fs::read(config_filename).await { 31 | Ok(b) => b, 32 | Err(e) => { 33 | return Err(std::io::Error::new( 34 | std::io::ErrorKind::InvalidInput, 35 | format!("Could not read config file {config_filename}: {e}"), 36 | )); 37 | } 38 | }; 39 | 40 | let config_str = match String::from_utf8(config_bytes) { 41 | Ok(s) => s, 42 | Err(e) => { 43 | return Err(std::io::Error::new( 44 | std::io::ErrorKind::InvalidInput, 45 | format!("Could not parse config file {config_filename} as UTF8: {e}"), 46 | )); 47 | } 48 | }; 49 | 50 | let mut configs = match serde_yaml::from_str::>(&config_str) { 51 | Ok(c) => c, 52 | Err(e) => { 53 | return Err(std::io::Error::new( 54 | std::io::ErrorKind::InvalidInput, 55 | format!("Could not parse config file {config_filename} as config YAML: {e}"), 56 | )); 57 | } 58 | }; 59 | all_configs.append(&mut configs) 60 | } 61 | 62 | Ok(all_configs) 63 | } 64 | -------------------------------------------------------------------------------- /src/vmess/crc32.rs: -------------------------------------------------------------------------------- 1 | // Copied and modified from rust-snappy 2 | use std::sync::OnceLock; 3 | 4 | const POLY: u32 = 0xEDB88320; 5 | 6 | /// Returns the CRC32 checksum of `buf` using the Castagnoli polynomial. 7 | pub fn crc32c(buf: &[u8]) -> u32 { 8 | // I can't measure any difference between slice8 and slice16. 9 | let ret = crc32c_slice8(buf, !0); 10 | !ret 11 | } 12 | 13 | /// Returns the CRC32 checksum of `buf` using the Castagnoli polynomial. 14 | fn crc32c_slice8(mut buf: &[u8], initial_crc: u32) -> u32 { 15 | static TABLE: OnceLock<[u32; 256]> = OnceLock::new(); 16 | static TABLE16: OnceLock<[[u32; 256]; 16]> = OnceLock::new(); 17 | 18 | let tab = TABLE.get_or_init(|| make_table(POLY)); 19 | let tab8 = &TABLE16.get_or_init(|| { 20 | let mut tab = [[0; 256]; 16]; 21 | tab[0] = make_table(POLY); 22 | for i in 0..256 { 23 | let mut crc = tab[0][i]; 24 | for j in 1..16 { 25 | crc = (crc >> 8) ^ tab[0][crc as u8 as usize]; 26 | tab[j][i] = crc; 27 | } 28 | } 29 | tab 30 | }); 31 | 32 | let mut crc: u32 = initial_crc; 33 | while buf.len() >= 8 { 34 | crc ^= u32::from_le_bytes(buf[0..4].try_into().unwrap()); 35 | crc = tab8[0][buf[7] as usize] 36 | ^ tab8[1][buf[6] as usize] 37 | ^ tab8[2][buf[5] as usize] 38 | ^ tab8[3][buf[4] as usize] 39 | ^ tab8[4][(crc >> 24) as u8 as usize] 40 | ^ tab8[5][(crc >> 16) as u8 as usize] 41 | ^ tab8[6][(crc >> 8) as u8 as usize] 42 | ^ tab8[7][(crc) as u8 as usize]; 43 | buf = &buf[8..]; 44 | } 45 | for &b in buf { 46 | crc = tab[((crc as u8) ^ b) as usize] ^ (crc >> 8); 47 | } 48 | crc 49 | } 50 | 51 | fn make_table(poly: u32) -> [u32; 256] { 52 | let mut tab = [0; 256]; 53 | let mut rev_tab = [0; 256]; 54 | for i in 0u32..256u32 { 55 | let mut crc = i; 56 | let mut rev = i << 24; 57 | for _ in 0..8 { 58 | if crc & 1 == 1 { 59 | crc = (crc >> 1) ^ poly; 60 | } else { 61 | crc >>= 1; 62 | } 63 | 64 | if (rev & 0x80000000) != 0 { 65 | rev = ((rev ^ poly) << 1) | 1; 66 | } else { 67 | rev <<= 1; 68 | } 69 | } 70 | tab[i as usize] = crc; 71 | rev_tab[i as usize] = rev; 72 | } 73 | tab 74 | } 75 | -------------------------------------------------------------------------------- /src/vless/vision_pad.rs: -------------------------------------------------------------------------------- 1 | use bytes::{BufMut, Bytes, BytesMut}; 2 | use rand::Rng; 3 | 4 | // Padding parameters 5 | const LONG_PADDING_MIN: usize = 900; 6 | const LONG_PADDING_RANDOM_MAX: usize = 500; 7 | const SHORT_PADDING_RANDOM_MAX: usize = 256; 8 | const MAX_PADDING_SIZE: usize = 8171; // buf.Size - 21 9 | 10 | pub fn pad_with_uuid_and_command(data: &[u8], uuid: &[u8; 16], command: u8, is_tls: bool) -> Bytes { 11 | pad(data, Some(uuid), command, is_tls) 12 | } 13 | // 14 | pub fn pad_with_command(data: &[u8], command: u8, is_tls: bool) -> Bytes { 15 | pad(data, None, command, is_tls) 16 | } 17 | 18 | fn pad(data: &[u8], uuid: Option<&[u8; 16]>, command: u8, is_tls: bool) -> Bytes { 19 | let content_len = data.len() as u16; 20 | let padding_len = calculate_padding_length(content_len as usize, is_tls); 21 | 22 | let uuid_len = if uuid.is_some() { 16 } else { 0 }; 23 | let total_size = uuid_len + 1 + 2 + 2 + data.len() + padding_len; 24 | 25 | let mut output = BytesMut::with_capacity(total_size); 26 | 27 | if let Some(uuid) = uuid { 28 | output.put_slice(uuid); 29 | } 30 | 31 | output.put_u8(command); 32 | output.put_u16(content_len); 33 | output.put_u16(padding_len as u16); 34 | output.put_slice(data); 35 | 36 | // Write random padding 37 | if padding_len > 0 { 38 | let padding_start = output.len(); 39 | output.resize(padding_start + padding_len, 0); 40 | rand::rng().fill(&mut output[padding_start..]); 41 | } 42 | 43 | output.freeze() 44 | } 45 | 46 | /// Calculate padding length based on content size and TLS detection 47 | fn calculate_padding_length(content_len: usize, is_tls: bool) -> usize { 48 | let mut rng = rand::rng(); 49 | 50 | // Calculate maximum allowable padding (avoid overflow) 51 | // Matches Xray-core logic: buf.Size - 21 - contentLen 52 | // MAX_PADDING_SIZE is already (buf.Size - 21), so we only subtract content_len 53 | let max_allowable = MAX_PADDING_SIZE.saturating_sub(content_len); 54 | 55 | if is_tls && content_len < LONG_PADDING_MIN { 56 | // Long padding for TLS handshake phase 57 | let random_part = rng.random_range(0..LONG_PADDING_RANDOM_MAX); 58 | let padding = LONG_PADDING_MIN 59 | .saturating_sub(content_len) 60 | .saturating_add(random_part); 61 | std::cmp::min(padding, max_allowable) 62 | } else { 63 | // Short random padding 64 | let padding = rng.random_range(0..SHORT_PADDING_RANDOM_MAX); 65 | std::cmp::min(padding, max_allowable) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/tcp/proxy_connector.rs: -------------------------------------------------------------------------------- 1 | //! ProxyConnector trait - Wraps protocols on existing streams. 2 | //! 3 | //! This trait handles protocol setup for proxy connections. It is responsible for: 4 | //! - Setting up proxy protocols (VLESS, VMess, SOCKS5, etc.) on existing streams 5 | //! - UDP-over-TCP tunneling through proxy protocols 6 | //! 7 | //! ## Design 8 | //! 9 | //! Every `ClientConfig` with a non-direct protocol implicitly defines a `ProxyConnector` 10 | //! through its `protocol` and `address` fields. 11 | //! 12 | //! When a config is used: 13 | //! - **As hop 0**: The ProxyConnector wraps the stream from SocketConnector 14 | //! - **As hop 1+**: The ProxyConnector wraps the stream from the previous hop 15 | //! 16 | //! `protocol: direct` does NOT create a ProxyConnector - it only creates a SocketConnector. 17 | 18 | use async_trait::async_trait; 19 | use std::fmt::Debug; 20 | 21 | use crate::address::NetLocation; 22 | use crate::async_stream::AsyncStream; 23 | use crate::tcp_handler::{TcpClientSetupResult, TcpClientUdpSetupResult, UdpStreamRequest}; 24 | 25 | /// Trait for proxy protocol connectors. 26 | /// 27 | /// Used to wrap protocols on existing streams. The stream may come from: 28 | /// - A SocketConnector (at hop 0) 29 | /// - A previous ProxyConnector (at hop 1+) 30 | /// 31 | /// ## Implementations 32 | /// 33 | /// - `TcpClientConnector`: For all proxy protocols (SOCKS5, HTTP, VMess, VLESS, etc.) 34 | #[async_trait] 35 | pub trait ProxyConnector: Send + Sync + Debug { 36 | /// Returns the proxy server address. 37 | /// 38 | /// This is used to determine where the SocketConnector should connect to 39 | /// when this is the first ProxyConnector in the chain. 40 | fn proxy_location(&self) -> &NetLocation; 41 | 42 | /// Check if this connector supports UDP-over-TCP tunneling. 43 | fn supports_udp_over_tcp(&self) -> bool; 44 | 45 | /// Setup protocol on existing stream. 46 | /// 47 | /// # Arguments 48 | /// * `stream` - Existing transport stream 49 | /// * `target` - Where traffic should reach through this hop 50 | /// (either the next proxy, or the final destination) 51 | async fn setup_tcp_stream( 52 | &self, 53 | stream: Box, 54 | target: &NetLocation, 55 | ) -> std::io::Result; 56 | 57 | /// Setup UDP-over-TCP on existing stream. 58 | /// 59 | /// # Arguments 60 | /// * `stream` - Existing transport stream 61 | /// * `request` - The type of UDP stream requested 62 | async fn setup_udp_stream( 63 | &self, 64 | stream: Box, 65 | request: UdpStreamRequest, 66 | ) -> std::io::Result; 67 | } 68 | -------------------------------------------------------------------------------- /src/udp_message_stream.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::net::SocketAddr; 4 | use std::pin::Pin; 5 | use std::sync::Arc; 6 | use std::task::{Context, Poll}; 7 | 8 | use futures::ready; 9 | use tokio::io::ReadBuf; 10 | use tokio::net::UdpSocket; 11 | 12 | use crate::address::NetLocation; 13 | use crate::async_stream::{ 14 | AsyncFlushMessage, AsyncPing, AsyncReadSourcedMessage, AsyncShutdownMessage, 15 | AsyncSourcedMessageStream, AsyncWriteTargetedMessage, 16 | }; 17 | use crate::resolver::{Resolver, ResolverCache}; 18 | 19 | /// A thin wrapper around a directly connecting UdpSocket to support hostname resolution. 20 | pub struct UdpMessageStream { 21 | socket: UdpSocket, 22 | resolver_cache: ResolverCache, 23 | } 24 | 25 | impl UdpMessageStream { 26 | pub fn new(socket: UdpSocket, resolver: Arc) -> Self { 27 | Self { 28 | socket, 29 | resolver_cache: ResolverCache::new(resolver), 30 | } 31 | } 32 | } 33 | 34 | impl AsyncReadSourcedMessage for UdpMessageStream { 35 | fn poll_read_sourced_message( 36 | self: Pin<&mut Self>, 37 | cx: &mut Context<'_>, 38 | buf: &mut ReadBuf<'_>, 39 | ) -> Poll> { 40 | self.get_mut().socket.poll_recv_from(cx, buf) 41 | } 42 | } 43 | 44 | impl AsyncWriteTargetedMessage for UdpMessageStream { 45 | fn poll_write_targeted_message( 46 | self: Pin<&mut Self>, 47 | cx: &mut Context<'_>, 48 | buf: &[u8], 49 | target: &NetLocation, 50 | ) -> Poll> { 51 | let this = self.get_mut(); 52 | 53 | let socket_addr = ready!(this.resolver_cache.poll_resolve_location(cx, target))?; 54 | 55 | // TODO: do we need to check usize result here? 56 | this.socket 57 | .poll_send_to(cx, buf, socket_addr) 58 | .map(|result| result.map(|_| ())) 59 | } 60 | } 61 | 62 | impl AsyncFlushMessage for UdpMessageStream { 63 | fn poll_flush_message( 64 | self: Pin<&mut Self>, 65 | _cx: &mut Context<'_>, 66 | ) -> Poll> { 67 | Poll::Ready(Ok(())) 68 | } 69 | } 70 | 71 | impl AsyncShutdownMessage for UdpMessageStream { 72 | fn poll_shutdown_message( 73 | self: Pin<&mut Self>, 74 | _cx: &mut Context<'_>, 75 | ) -> Poll> { 76 | Poll::Ready(Ok(())) 77 | } 78 | } 79 | 80 | impl AsyncPing for UdpMessageStream { 81 | fn supports_ping(&self) -> bool { 82 | false 83 | } 84 | 85 | fn poll_write_ping(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 86 | unimplemented!() 87 | } 88 | } 89 | 90 | impl AsyncSourcedMessageStream for UdpMessageStream {} 91 | -------------------------------------------------------------------------------- /examples/vless_variants.yaml: -------------------------------------------------------------------------------- 1 | # Various VLESS server configurations 2 | 3 | # Basic VLESS server 4 | - address: "0.0.0.0:16823" 5 | protocol: 6 | type: vless 7 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 8 | 9 | # VLESS over WebSocket 10 | - address: "0.0.0.0:443" 11 | protocol: 12 | type: tls 13 | sni_targets: 14 | "vless.example.com": 15 | cert: "cert.pem" 16 | key: "key.pem" 17 | protocol: 18 | type: websocket 19 | targets: 20 | - matching_path: "/vless" 21 | protocol: 22 | type: vless 23 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 24 | 25 | # VLESS over QUIC 26 | - address: "0.0.0.0:4443" 27 | transport: quic 28 | quic_settings: 29 | cert: "cert.pem" 30 | key: "key.pem" 31 | alpn_protocols: ["h3"] 32 | protocol: 33 | type: vless 34 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 35 | 36 | # VLESS with custom rules and upstream proxy 37 | - address: "0.0.0.0:16824" 38 | protocol: 39 | type: vless 40 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 41 | rules: 42 | # Direct connection for local addresses 43 | - masks: ["192.168.0.0/16"] 44 | action: allow 45 | # client_chain: direct is optional - direct is the default when omitted 46 | client_chain: direct 47 | # Route international traffic through proxy 48 | - masks: "0.0.0.0/0" 49 | action: allow 50 | client_chain: 51 | address: "proxy.example.com:1080" 52 | protocol: 53 | type: socks 54 | 55 | # VLESS over Reality (censorship resistance) 56 | - address: "0.0.0.0:8443" 57 | protocol: 58 | type: tls 59 | reality_targets: 60 | "www.cloudflare.com": 61 | private_key: "YOUR_PRIVATE_KEY_HERE" 62 | short_ids: ["0123456789abcdef", ""] 63 | dest: "www.cloudflare.com:443" 64 | protocol: 65 | type: vless 66 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 67 | udp_enabled: true 68 | 69 | # VLESS over Reality with Vision (censorship + performance) 70 | - address: "0.0.0.0:9443" 71 | protocol: 72 | type: tls 73 | reality_targets: 74 | "www.google.com": 75 | private_key: "YOUR_PRIVATE_KEY_HERE" 76 | short_ids: ["0123456789abcdef"] 77 | dest: "www.google.com:443" 78 | vision: true # Enable Vision 79 | protocol: 80 | type: vless # Vision requires VLESS 81 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 82 | udp_enabled: true 83 | 84 | # VLESS over TLS with Vision (no Reality) 85 | - address: "0.0.0.0:10443" 86 | protocol: 87 | type: tls 88 | tls_targets: 89 | "vless.example.com": 90 | cert: "cert.pem" 91 | key: "key.pem" 92 | vision: true # Enable Vision 93 | alpn_protocols: ["http/1.1"] 94 | protocol: 95 | type: vless 96 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 97 | udp_enabled: true 98 | -------------------------------------------------------------------------------- /src/config/types/transport.rs: -------------------------------------------------------------------------------- 1 | //! Transport-related configuration types. 2 | 3 | use std::path::PathBuf; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::address::{NetLocation, NetLocationPortRange}; 8 | use crate::option_util::{NoneOrOne, NoneOrSome}; 9 | 10 | use super::common::default_true; 11 | 12 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] 13 | #[serde(rename_all = "lowercase")] 14 | pub enum BindLocation { 15 | Address(NetLocationPortRange), 16 | Path(PathBuf), 17 | } 18 | 19 | impl std::fmt::Display for BindLocation { 20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 21 | match self { 22 | BindLocation::Address(n) => write!(f, "{n}"), 23 | BindLocation::Path(p) => write!(f, "{}", p.display()), 24 | } 25 | } 26 | } 27 | 28 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)] 29 | #[serde(rename_all = "lowercase")] 30 | pub enum Transport { 31 | #[default] 32 | Tcp, 33 | Quic, 34 | Udp, 35 | } 36 | 37 | #[derive(Debug, Clone, Deserialize, Serialize)] 38 | pub struct TcpConfig { 39 | #[serde(default = "default_true")] 40 | pub no_delay: bool, 41 | } 42 | 43 | impl Default for TcpConfig { 44 | fn default() -> Self { 45 | TcpConfig { no_delay: true } 46 | } 47 | } 48 | 49 | #[derive(Debug, Clone, Deserialize, Serialize)] 50 | pub struct ServerQuicConfig { 51 | pub cert: String, 52 | pub key: String, 53 | #[serde(alias = "alpn_protocol", default)] 54 | pub alpn_protocols: NoneOrSome, 55 | #[serde(alias = "client_ca_cert", default)] 56 | pub client_ca_certs: NoneOrSome, 57 | #[serde(alias = "client_fingerprint", default)] 58 | pub client_fingerprints: NoneOrSome, 59 | // num_endpoints of 0 will use the number of threads as the default value. 60 | #[serde(default)] 61 | pub num_endpoints: usize, 62 | } 63 | 64 | #[derive(Debug, Clone, Deserialize, Serialize)] 65 | pub struct ClientQuicConfig { 66 | #[serde(default = "default_true")] 67 | pub verify: bool, 68 | #[serde(alias = "server_fingerprint", default)] 69 | pub server_fingerprints: NoneOrSome, 70 | #[serde(default)] 71 | pub sni_hostname: NoneOrOne, 72 | #[serde(alias = "alpn_protocol", default)] 73 | pub alpn_protocols: NoneOrSome, 74 | #[serde(default)] 75 | pub key: Option, 76 | #[serde(default)] 77 | pub cert: Option, 78 | } 79 | 80 | impl Default for ClientQuicConfig { 81 | fn default() -> Self { 82 | Self { 83 | verify: true, 84 | server_fingerprints: NoneOrSome::Unspecified, 85 | sni_hostname: NoneOrOne::Unspecified, 86 | alpn_protocols: NoneOrSome::Unspecified, 87 | key: None, 88 | cert: None, 89 | } 90 | } 91 | } 92 | 93 | impl From for BindLocation { 94 | fn from(loc: NetLocation) -> Self { 95 | BindLocation::Address(loc.into()) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/buf_reader.rs: -------------------------------------------------------------------------------- 1 | /// Read slices similar to std::io::Cursor with byteorder extension 2 | pub struct BufReader<'a> { 3 | buf: &'a [u8], 4 | pos: usize, 5 | } 6 | 7 | impl<'a> BufReader<'a> { 8 | pub fn new(buf: &'a [u8]) -> Self { 9 | Self { buf, pos: 0 } 10 | } 11 | 12 | pub fn is_consumed(&self) -> bool { 13 | self.pos == self.buf.len() 14 | } 15 | 16 | pub fn position(&self) -> usize { 17 | self.pos 18 | } 19 | 20 | pub fn read_u8(&mut self) -> std::io::Result { 21 | if self.pos >= self.buf.len() { 22 | return Err(std::io::Error::new( 23 | std::io::ErrorKind::UnexpectedEof, 24 | "Read past end of buffer", 25 | )); 26 | } 27 | let value = self.buf[self.pos]; 28 | self.pos += 1; 29 | Ok(value) 30 | } 31 | 32 | pub fn read_u16_be(&mut self) -> std::io::Result { 33 | if self.pos + 1 >= self.buf.len() { 34 | return Err(std::io::Error::new( 35 | std::io::ErrorKind::UnexpectedEof, 36 | "Read past end of buffer", 37 | )); 38 | } 39 | let value = u16::from_be_bytes([self.buf[self.pos], self.buf[self.pos + 1]]); 40 | self.pos += 2; 41 | Ok(value) 42 | } 43 | 44 | pub fn read_u24_be(&mut self) -> std::io::Result { 45 | if self.pos + 2 >= self.buf.len() { 46 | return Err(std::io::Error::new( 47 | std::io::ErrorKind::UnexpectedEof, 48 | "Read past end of buffer", 49 | )); 50 | } 51 | let value = u32::from_be_bytes([ 52 | 0, 53 | self.buf[self.pos], 54 | self.buf[self.pos + 1], 55 | self.buf[self.pos + 2], 56 | ]); 57 | self.pos += 3; 58 | Ok(value) 59 | } 60 | 61 | pub fn read_slice(&mut self, len: usize) -> std::io::Result<&[u8]> { 62 | if self.pos + len > self.buf.len() { 63 | return Err(std::io::Error::new( 64 | std::io::ErrorKind::UnexpectedEof, 65 | "Read past end of buffer", 66 | )); 67 | } 68 | let slice = &self.buf[self.pos..self.pos + len]; 69 | self.pos += len; 70 | Ok(slice) 71 | } 72 | 73 | pub fn read_str(&mut self, len: usize) -> std::io::Result<&str> { 74 | let slice = self.read_slice(len)?; 75 | std::str::from_utf8(slice).map_err(|e| { 76 | std::io::Error::new( 77 | std::io::ErrorKind::InvalidData, 78 | format!("Invalid UTF-8 sequence: {e}"), 79 | ) 80 | }) 81 | } 82 | 83 | pub fn skip(&mut self, amount: usize) -> std::io::Result<()> { 84 | if self.pos + amount > self.buf.len() { 85 | return Err(std::io::Error::new( 86 | std::io::ErrorKind::UnexpectedEof, 87 | "Read past end of buffer", 88 | )); 89 | } 90 | self.pos += amount; 91 | Ok(()) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/config/types/shadowsocks.rs: -------------------------------------------------------------------------------- 1 | //! Shadowsocks configuration types. 2 | 3 | use base64::engine::{Engine as _, general_purpose::STANDARD as BASE64}; 4 | use serde::Deserialize; 5 | 6 | use crate::shadowsocks::ShadowsocksCipher; 7 | 8 | #[derive(Debug, Clone)] 9 | pub enum ShadowsocksConfig { 10 | Legacy { 11 | cipher: ShadowsocksCipher, 12 | password: String, 13 | }, 14 | Aead2022 { 15 | cipher: ShadowsocksCipher, 16 | key_bytes: Box<[u8]>, 17 | }, 18 | } 19 | 20 | impl<'de> serde::de::Deserialize<'de> for ShadowsocksConfig { 21 | fn deserialize(deserializer: D) -> Result 22 | where 23 | D: serde::de::Deserializer<'de>, 24 | { 25 | #[derive(Deserialize)] 26 | #[serde(deny_unknown_fields)] 27 | struct ShadowsocksConfigTemp { 28 | cipher: String, 29 | password: String, 30 | } 31 | 32 | let temp = ShadowsocksConfigTemp::deserialize(deserializer)?; 33 | 34 | match temp.cipher.strip_prefix("2022-blake3-") { 35 | Some(stripped) => { 36 | let cipher: ShadowsocksCipher = 37 | stripped.try_into().map_err(serde::de::Error::custom)?; 38 | let key_bytes = BASE64 39 | .decode(&temp.password) 40 | .map_err(|e| { 41 | serde::de::Error::custom(format!( 42 | "Failed to base64 decode password for 2022-blake3 cipher: {}", 43 | e 44 | )) 45 | })? 46 | .into_boxed_slice(); 47 | Ok(ShadowsocksConfig::Aead2022 { cipher, key_bytes }) 48 | } 49 | None => { 50 | let cipher: ShadowsocksCipher = temp 51 | .cipher 52 | .as_str() 53 | .try_into() 54 | .map_err(serde::de::Error::custom)?; 55 | Ok(ShadowsocksConfig::Legacy { 56 | cipher, 57 | password: temp.password, 58 | }) 59 | } 60 | } 61 | } 62 | } 63 | 64 | impl serde::ser::Serialize for ShadowsocksConfig { 65 | fn serialize(&self, serializer: S) -> Result 66 | where 67 | S: serde::ser::Serializer, 68 | { 69 | use serde::ser::SerializeStruct; 70 | 71 | let mut state = serializer.serialize_struct("ShadowsocksConfig", 2)?; 72 | match self { 73 | ShadowsocksConfig::Legacy { cipher, password } => { 74 | state.serialize_field("cipher", cipher.name())?; 75 | state.serialize_field("password", password)?; 76 | } 77 | ShadowsocksConfig::Aead2022 { cipher, key_bytes } => { 78 | let cipher_name = format!("2022-blake3-{}", cipher.name()); 79 | state.serialize_field("cipher", &cipher_name)?; 80 | state.serialize_field("password", &BASE64.encode(key_bytes))?; 81 | } 82 | } 83 | state.end() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog - v0.2.1 2 | 3 | ## New Features 4 | 5 | ### Client Chaining (`client_chains`) 6 | Multi-hop proxy chains with load balancing support. Traffic can now be routed through multiple proxies in sequence. 7 | 8 | - **Multi-hop chains**: Route traffic through multiple proxies sequentially (e.g., `proxy1 -> proxy2 -> target`) 9 | - **Round-robin chains**: Specify multiple chains and rotate between them for load distribution 10 | - **Pool-based load balancing**: At each hop, use a pool of proxies for load balancing 11 | - New config fields: `client_chain` (singular) and `client_chains` (multiple) 12 | - See `examples/multi_hop_chain.yaml` for usage examples 13 | 14 | ### TUIC v5 Zero-RTT Handshake 15 | New `zero_rtt_handshake` option for TUIC v5 servers enables 0-RTT (0.5-RTT for server) handshakes for faster connection establishment. 16 | 17 | ```yaml 18 | protocol: 19 | type: tuic 20 | uuid: "..." 21 | password: "..." 22 | zero_rtt_handshake: true # Default: false 23 | ``` 24 | 25 | Note: 0-RTT is vulnerable to replay attacks. Only enable if the latency benefit outweighs security concerns. 26 | 27 | ### Reality Cipher Suites 28 | Both Reality server and client now support specifying TLS 1.3 cipher suites. 29 | 30 | ```yaml 31 | # Server 32 | reality_targets: 33 | "example.com": 34 | cipher_suites: ["TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256"] 35 | ... 36 | 37 | # Client 38 | protocol: 39 | type: reality 40 | cipher_suites: ["TLS_AES_256_GCM_SHA384"] 41 | ... 42 | ``` 43 | 44 | Valid values: `TLS_AES_128_GCM_SHA256`, `TLS_AES_256_GCM_SHA384`, `TLS_CHACHA20_POLY1305_SHA256` 45 | 46 | ### Reality Client Version Control 47 | Server-side Reality configuration can now restrict client versions: 48 | 49 | ```yaml 50 | reality_targets: 51 | "example.com": 52 | min_client_version: [1, 8, 0] # [major, minor, patch] 53 | max_client_version: [2, 0, 0] 54 | ... 55 | ``` 56 | 57 | ## Deprecations 58 | 59 | ### `client_proxy` / `client_proxies` in Rules 60 | The `client_proxy` and `client_proxies` fields in rule configurations are deprecated in favor of `client_chain` and `client_chains`. 61 | 62 | **Migration**: Replace `client_proxy:` with `client_chain:` in your configuration files. The old fields still work but will emit a warning and may be removed in a future version. 63 | 64 | Before: 65 | ```yaml 66 | rules: 67 | - masks: "0.0.0.0/0" 68 | action: allow 69 | client_proxy: my-proxy-group 70 | ``` 71 | 72 | After: 73 | ```yaml 74 | rules: 75 | - masks: "0.0.0.0/0" 76 | action: allow 77 | client_chain: my-proxy-group 78 | ``` 79 | 80 | ### VMess `force_aead` / `aead` Fields 81 | The `force_aead` and `aead` fields in VMess configuration are deprecated. AEAD mode is now always enabled, and non-AEAD (legacy) mode is no longer supported. 82 | 83 | **Migration**: Remove `force_aead` and `aead` fields from your VMess configurations. They have no effect and will be ignored. 84 | 85 | ## Removed / Breaking Changes 86 | 87 | ### VMess Non-AEAD Mode Removed 88 | VMess non-AEAD (legacy) mode is no longer supported. All VMess connections now use AEAD encryption exclusively. This improves security but breaks compatibility with very old VMess clients that don't support AEAD. 89 | 90 | ## Other Changes 91 | 92 | - Hysteria2 and TUIC servers now have authentication timeouts (3 seconds by default) to prevent connection hogging 93 | - Improved fragment packet handling with LRU cache eviction 94 | - TUIC server now sends heartbeat packets to maintain connection liveness 95 | -------------------------------------------------------------------------------- /src/vmess/sha2.rs: -------------------------------------------------------------------------------- 1 | use aws_lc_rs::digest::{Context, SHA256}; 2 | 3 | trait VmessHash: std::fmt::Debug { 4 | fn setup_new(&self) -> Box; 5 | fn update(&mut self, data: &[u8]); 6 | fn finalize(&mut self) -> [u8; 32]; 7 | } 8 | 9 | struct Sha256Hash(Context); 10 | 11 | impl std::fmt::Debug for Sha256Hash { 12 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 13 | f.debug_tuple("Sha256Hash").field(&"Context").finish() 14 | } 15 | } 16 | 17 | impl Sha256Hash { 18 | fn create() -> Self { 19 | Self(Context::new(&SHA256)) 20 | } 21 | } 22 | 23 | impl VmessHash for Sha256Hash { 24 | fn setup_new(&self) -> Box { 25 | Box::new(Sha256Hash(self.0.clone())) 26 | } 27 | 28 | fn update(&mut self, data: &[u8]) { 29 | self.0.update(data); 30 | } 31 | 32 | fn finalize(&mut self) -> [u8; 32] { 33 | let mut out = [0u8; 32]; 34 | out.copy_from_slice(self.0.clone().finish().as_ref()); 35 | out 36 | } 37 | } 38 | 39 | #[derive(Debug)] 40 | struct RecursiveHash { 41 | inner: Box, 42 | outer: Box, 43 | default_inner: [u8; 64], 44 | default_outer: [u8; 64], 45 | } 46 | 47 | impl RecursiveHash { 48 | fn create(key: &[u8], hash: Box) -> Self { 49 | // for hmac, we would normally have to get a derived key 50 | // by hashing the key when it's longer than 64 bytes, but 51 | // that doesn't happen for vmess's usecase. 52 | assert!(key.len() <= 64); 53 | 54 | let mut default_outer = [0x5c; 64]; 55 | let mut default_inner = [0x36; 64]; 56 | 57 | for (i, &b) in key.iter().enumerate() { 58 | default_outer[i] ^= b; 59 | default_inner[i] ^= b; 60 | } 61 | 62 | let mut inner = hash.setup_new(); 63 | let outer = hash; 64 | inner.update(&default_inner); 65 | Self { 66 | inner, 67 | outer, 68 | default_inner, 69 | default_outer, 70 | } 71 | } 72 | } 73 | 74 | impl VmessHash for RecursiveHash { 75 | fn setup_new(&self) -> Box { 76 | let new_inner = self.inner.setup_new(); 77 | let new_outer = self.outer.setup_new(); 78 | 79 | let mut new_default_inner = [0u8; 64]; 80 | let mut new_default_outer = [0u8; 64]; 81 | new_default_inner.copy_from_slice(&self.default_inner); 82 | new_default_outer.copy_from_slice(&self.default_outer); 83 | 84 | Box::new(RecursiveHash { 85 | inner: new_inner, 86 | outer: new_outer, 87 | default_inner: new_default_inner, 88 | default_outer: new_default_outer, 89 | }) 90 | } 91 | 92 | fn update(&mut self, data: &[u8]) { 93 | self.inner.update(data); 94 | } 95 | 96 | fn finalize(&mut self) -> [u8; 32] { 97 | self.outer.update(&self.default_outer); 98 | self.outer.update(&self.inner.finalize()); 99 | self.outer.finalize() 100 | } 101 | } 102 | 103 | pub fn kdf(key: &[u8], path: &[&[u8]]) -> [u8; 32] { 104 | let mut current = Box::new(RecursiveHash::create( 105 | b"VMess AEAD KDF", 106 | Box::new(Sha256Hash::create()), 107 | )); 108 | for path_item in path.iter() { 109 | current = Box::new(RecursiveHash::create(path_item, current)) 110 | } 111 | current.update(key); 112 | current.finalize() 113 | } 114 | 115 | pub fn compute_sha256(data: &[u8]) -> [u8; 32] { 116 | let mut out = [0u8; 32]; 117 | out.copy_from_slice(aws_lc_rs::digest::digest(&SHA256, data).as_ref()); 118 | out 119 | } 120 | -------------------------------------------------------------------------------- /examples/named_certificates.yaml: -------------------------------------------------------------------------------- 1 | # Example demonstrating named PEM files (certificates and keys) 2 | # This shows how to define PEM files once and use them in multiple places 3 | 4 | # Define named PEM files (can contain both cert and key) 5 | - pem: "main-server-cert" 6 | path: "cert.pem" 7 | 8 | - pem: "main-server-key" 9 | path: "key.pem" 10 | 11 | - pem: "client-ca" 12 | path: "ca-cert.pem" 13 | 14 | # Define a self-signed certificate inline 15 | - pem: "self-signed-cert" 16 | data: | 17 | -----BEGIN CERTIFICATE----- 18 | certificate data 19 | -----END CERTIFICATE----- 20 | 21 | - pem: "self-signed-key" 22 | data: | 23 | -----BEGIN PRIVATE KEY----- 24 | private key data 25 | -----END PRIVATE KEY----- 26 | 27 | # QUIC server using named PEM files 28 | - address: "0.0.0.0:443" 29 | transport: quic 30 | quic_settings: 31 | cert: "main-server-cert" # Reference named PEM file 32 | key: "main-server-key" # Reference named PEM file 33 | client_ca_certs: 34 | - "client-ca" # Reference named CA certificate 35 | protocol: 36 | type: hysteria2 37 | password: "my-secret-password" 38 | 39 | # TLS server reusing the same certificates 40 | - address: "0.0.0.0:8443" 41 | protocol: 42 | type: tls 43 | sni_targets: 44 | "example.com": 45 | cert: "main-server-cert" # Reuse the same certificate 46 | key: "main-server-key" # Reuse the same key 47 | client_ca_certs: 48 | - "client-ca" 49 | protocol: 50 | type: vmess 51 | cipher: auto 52 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 53 | "test.example.com": 54 | cert: "self-signed-cert" # Use different certificate for this SNI 55 | key: "self-signed-key" 56 | protocol: 57 | type: vless 58 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 59 | 60 | # ShadowTLS with named certificates 61 | - address: "0.0.0.0:9443" 62 | protocol: 63 | type: tls 64 | shadowtls_targets: 65 | "shadow.example.com": 66 | password: "shadow-example-password" 67 | handshake: 68 | cert: "main-server-cert" 69 | key: "main-server-key" 70 | protocol: 71 | type: shadowsocks 72 | cipher: 2022-blake3-chacha20-ietf-poly1305 73 | password: "N5tp51JibWA9kIPEUoDHzyyjfgkDYToOERh+u9L47fk=" 74 | 75 | # Reality server (note: Reality uses X25519 keys, not certificates) 76 | # Reality doesn't use named PEMs for its keys since they're not certificates 77 | - address: "0.0.0.0:10443" 78 | protocol: 79 | type: tls 80 | reality_targets: 81 | "www.cloudflare.com": 82 | private_key: "YOUR_REALITY_PRIVATE_KEY_HERE" 83 | short_ids: ["0123456789abcdef", ""] 84 | dest: "www.cloudflare.com:443" 85 | protocol: 86 | type: vless 87 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 88 | udp_enabled: true 89 | 90 | # Mixed configuration: Regular TLS and Reality on same server 91 | - address: "0.0.0.0:11443" 92 | protocol: 93 | type: tls 94 | # Regular TLS target using named certificates 95 | tls_targets: 96 | "tls.example.com": 97 | cert: "main-server-cert" 98 | key: "main-server-key" 99 | protocol: 100 | type: vless 101 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 102 | # Reality target (uses its own X25519 keys) 103 | reality_targets: 104 | "www.google.com": 105 | private_key: "YOUR_REALITY_PRIVATE_KEY_HERE" 106 | short_ids: ["0123456789abcdef"] 107 | dest: "www.google.com:443" 108 | vision: true 109 | protocol: 110 | type: vless 111 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 112 | udp_enabled: true -------------------------------------------------------------------------------- /src/config/types/common.rs: -------------------------------------------------------------------------------- 1 | //! Common types and helpers shared across config modules. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::address::{NetLocation, NetLocationMask, NetLocationPortRange}; 6 | use crate::option_util::OneOrSome; 7 | 8 | /// Default Reality short_id: all zeros (16 hex chars = 8 bytes of zeros) 9 | pub const DEFAULT_REALITY_SHORT_ID: &str = "0000000000000000"; 10 | 11 | pub fn default_true() -> bool { 12 | true 13 | } 14 | 15 | pub fn default_reality_client_short_id() -> String { 16 | DEFAULT_REALITY_SHORT_ID.to_string() 17 | } 18 | 19 | pub fn default_reality_server_short_ids() -> OneOrSome { 20 | OneOrSome::One(DEFAULT_REALITY_SHORT_ID.to_string()) 21 | } 22 | 23 | pub fn default_reality_time_diff() -> Option { 24 | // 1 minute 25 | Some(1000 * 60) 26 | } 27 | 28 | pub fn unspecified_address() -> NetLocation { 29 | NetLocation::UNSPECIFIED 30 | } 31 | 32 | /// Implement Serialize for CipherSuite - serializes as the standard TLS cipher suite name 33 | impl Serialize for crate::reality::CipherSuite { 34 | fn serialize(&self, serializer: S) -> Result 35 | where 36 | S: serde::Serializer, 37 | { 38 | serializer.serialize_str(self.name()) 39 | } 40 | } 41 | 42 | /// Implement Deserialize for CipherSuite - deserializes from standard TLS cipher suite name 43 | impl<'de> Deserialize<'de> for crate::reality::CipherSuite { 44 | fn deserialize(deserializer: D) -> Result 45 | where 46 | D: serde::Deserializer<'de>, 47 | { 48 | use serde::de::Error; 49 | let name = String::deserialize(deserializer)?; 50 | crate::reality::CipherSuite::from_name(&name).ok_or_else(|| { 51 | let valid_names: Vec<&str> = crate::reality::DEFAULT_CIPHER_SUITES 52 | .iter() 53 | .map(|cs| cs.name()) 54 | .collect(); 55 | D::Error::custom(format!( 56 | "invalid cipher suite '{}', valid values are: {}", 57 | name, 58 | valid_names.join(", ") 59 | )) 60 | }) 61 | } 62 | } 63 | 64 | pub fn deserialize_net_location<'de, D>( 65 | deserializer: D, 66 | default_port: Option, 67 | ) -> Result 68 | where 69 | D: serde::de::Deserializer<'de>, 70 | { 71 | let value = String::deserialize(deserializer)?; 72 | let net_location = NetLocation::from_str(&value, default_port).map_err(|_| { 73 | serde::de::Error::invalid_value( 74 | serde::de::Unexpected::Other("invalid net location"), 75 | &"invalid net location", 76 | ) 77 | })?; 78 | 79 | Ok(net_location) 80 | } 81 | 82 | impl<'de> serde::de::Deserialize<'de> for NetLocation { 83 | fn deserialize(deserializer: D) -> Result 84 | where 85 | D: serde::de::Deserializer<'de>, 86 | { 87 | deserialize_net_location(deserializer, None) 88 | } 89 | } 90 | 91 | impl<'de> serde::de::Deserialize<'de> for NetLocationMask { 92 | fn deserialize(deserializer: D) -> Result 93 | where 94 | D: serde::de::Deserializer<'de>, 95 | { 96 | let value = String::deserialize(deserializer)?; 97 | let net_location_mask = NetLocationMask::from(&value).map_err(|_| { 98 | serde::de::Error::invalid_value( 99 | serde::de::Unexpected::Other("invalid net location mask"), 100 | &"invalid net location mask", 101 | ) 102 | })?; 103 | 104 | Ok(net_location_mask) 105 | } 106 | } 107 | 108 | impl<'de> serde::de::Deserialize<'de> for NetLocationPortRange { 109 | fn deserialize(deserializer: D) -> Result 110 | where 111 | D: serde::de::Deserializer<'de>, 112 | { 113 | let value = String::deserialize(deserializer)?; 114 | let net_location_port_range = NetLocationPortRange::from_str(&value).map_err(|e| { 115 | serde::de::Error::invalid_value( 116 | serde::de::Unexpected::Other(&format!("invalid net location port range: {e}")), 117 | &"valid net location port range (address:port[-port][,port])", 118 | ) 119 | })?; 120 | 121 | Ok(net_location_port_range) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/tcp/proxy_connector_impl.rs: -------------------------------------------------------------------------------- 1 | //! ProxyConnectorImpl - Implementation of ProxyConnector trait. 2 | //! 3 | //! Handles protocol setup for proxy connections on existing streams. 4 | //! Created from the protocol-related fields of a ClientConfig. 5 | 6 | use async_trait::async_trait; 7 | use log::debug; 8 | 9 | use super::tcp_client_handler_factory::create_tcp_client_handler; 10 | use crate::address::NetLocation; 11 | use crate::async_stream::AsyncStream; 12 | use crate::config::ClientConfig; 13 | use crate::tcp_handler::{ 14 | TcpClientHandler, TcpClientSetupResult, TcpClientUdpSetupResult, UdpStreamRequest, 15 | }; 16 | 17 | use super::proxy_connector::ProxyConnector; 18 | 19 | /// Implementation of ProxyConnector for proxy protocol setup. 20 | /// 21 | /// Created from the protocol-related fields of a ClientConfig: 22 | /// - `protocol` 23 | /// - `address` 24 | /// 25 | /// This connector only wraps protocols on existing streams - it does not 26 | /// create socket connections. Socket creation is handled by SocketConnector. 27 | #[derive(Debug)] 28 | pub struct ProxyConnectorImpl { 29 | location: NetLocation, 30 | client_handler: Box, 31 | } 32 | 33 | impl ProxyConnectorImpl { 34 | /// Create a ProxyConnector from a ClientConfig's protocol-related fields. 35 | /// 36 | /// Returns None for direct protocol (direct has no ProxyConnector). 37 | pub fn from_config(config: ClientConfig) -> Option { 38 | if config.protocol.is_direct() { 39 | return None; 40 | } 41 | 42 | let default_sni_hostname = config.address.address().hostname().map(ToString::to_string); 43 | 44 | Some(Self { 45 | location: config.address, 46 | client_handler: create_tcp_client_handler(config.protocol, default_sni_hostname), 47 | }) 48 | } 49 | 50 | /// Create a ProxyConnector directly from components. 51 | #[cfg(test)] 52 | pub fn new(location: NetLocation, handler: Box) -> Self { 53 | Self { 54 | location, 55 | client_handler: handler, 56 | } 57 | } 58 | } 59 | 60 | #[async_trait] 61 | impl ProxyConnector for ProxyConnectorImpl { 62 | fn proxy_location(&self) -> &NetLocation { 63 | &self.location 64 | } 65 | 66 | fn supports_udp_over_tcp(&self) -> bool { 67 | self.client_handler.supports_udp_over_tcp() 68 | } 69 | 70 | async fn setup_tcp_stream( 71 | &self, 72 | stream: Box, 73 | target: &NetLocation, 74 | ) -> std::io::Result { 75 | debug!( 76 | "[ProxyConnector] setup_tcp_stream: {} -> {}", 77 | self.location, target 78 | ); 79 | self.client_handler 80 | .setup_client_tcp_stream(stream, target.clone()) 81 | .await 82 | } 83 | 84 | async fn setup_udp_stream( 85 | &self, 86 | stream: Box, 87 | request: UdpStreamRequest, 88 | ) -> std::io::Result { 89 | debug!( 90 | "[ProxyConnector] setup_udp_stream: {}, request: {}", 91 | self.location, request 92 | ); 93 | self.client_handler 94 | .setup_client_udp_stream(stream, request) 95 | .await 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod tests { 101 | use super::*; 102 | use crate::config::ClientProxyConfig; 103 | use std::net::{IpAddr, Ipv4Addr}; 104 | 105 | #[test] 106 | fn test_from_direct_config_returns_none() { 107 | let config = ClientConfig::default(); 108 | assert!(config.protocol.is_direct()); 109 | assert!(ProxyConnectorImpl::from_config(config).is_none()); 110 | } 111 | 112 | #[test] 113 | fn test_from_proxy_config_returns_some() { 114 | let config = ClientConfig { 115 | address: NetLocation::from_ip_addr(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1080), 116 | protocol: ClientProxyConfig::Socks { 117 | username: None, 118 | password: None, 119 | }, 120 | ..Default::default() 121 | }; 122 | let connector = ProxyConnectorImpl::from_config(config); 123 | assert!(connector.is_some()); 124 | let connector = connector.unwrap(); 125 | assert_eq!(connector.proxy_location().port(), 1080); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /examples/multi_hop_chain.yaml: -------------------------------------------------------------------------------- 1 | # Multi-hop proxy chain examples 2 | # 3 | # Round-robin behavior happens at multiple levels: 4 | # 1. Between chains: when using client_chains (plural), each connection picks the next chain 5 | # 2. Between servers in a group: when a hop references a group with multiple proxies 6 | # 3. Between servers in a pool: when using pool: [...] syntax 7 | # 8 | # Traffic always flows: client -> hop1 -> hop2 -> ... -> target 9 | 10 | # Define reusable proxy groups (each group round-robins between its members) 11 | - client_group: us-proxies 12 | client_proxies: 13 | - address: "us1.example.com:1080" 14 | protocol: 15 | type: socks 16 | - address: "us2.example.com:1080" 17 | protocol: 18 | type: socks 19 | 20 | - client_group: eu-proxies 21 | client_proxies: 22 | - address: "eu1.example.com:1080" 23 | protocol: 24 | type: socks 25 | 26 | - client_group: exit-proxy 27 | client_proxies: 28 | - address: "exit.example.com:443" 29 | protocol: 30 | type: tls 31 | protocol: 32 | type: vless 33 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 34 | 35 | # 36 | # Example 1: Single chain with multiple hops (inline configs) 37 | # Each connection goes through BOTH proxies in sequence. 38 | # No round-robin here - just one fixed path. 39 | # 40 | - address: "127.0.0.1:1080" 41 | protocol: 42 | type: socks 43 | rules: 44 | - masks: "0.0.0.0/0" 45 | action: allow 46 | client_chain: 47 | # Array of hops - traffic goes through each in order 48 | - address: "proxy1.example.com:1080" 49 | protocol: 50 | type: socks 51 | - address: "proxy2.example.com:443" 52 | protocol: 53 | type: tls 54 | protocol: 55 | type: vless 56 | user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0" 57 | 58 | # 59 | # Example 2: Single chain using named groups 60 | # Round-robin happens WITHIN each hop (between group members). 61 | # Connection 1: us1 -> exit 62 | # Connection 2: us2 -> exit 63 | # Connection 3: us1 -> exit (cycles back) 64 | # 65 | - address: "127.0.0.1:1081" 66 | protocol: 67 | type: socks 68 | rules: 69 | - masks: "0.0.0.0/0" 70 | action: allow 71 | client_chain: 72 | - us-proxies # Round-robins between us1 and us2 73 | - exit-proxy # Single exit proxy 74 | 75 | # 76 | # Example 3: Two chains - round-robin between ROUTES 77 | # Each connection picks the next chain, alternating between US and EU routes. 78 | # Connection 1: us-proxies -> exit (US route) 79 | # Connection 2: eu-proxies -> exit (EU route) 80 | # Connection 3: us-proxies -> exit (US route, cycles back) 81 | # 82 | - address: "127.0.0.1:1082" 83 | protocol: 84 | type: http 85 | rules: 86 | - masks: "0.0.0.0/0" 87 | action: allow 88 | client_chains: 89 | # Chain 1: US route 90 | - chain: 91 | - us-proxies 92 | - exit-proxy 93 | # Chain 2: EU route 94 | - chain: 95 | - eu-proxies 96 | - exit-proxy 97 | 98 | # 99 | # Example 4: Two chains - one multi-hop, one single-hop 100 | # Alternates between a 2-hop chain and a direct 1-hop chain. 101 | # Connection 1: us-proxies -> exit-proxy (2 hops) 102 | # Connection 2: eu-proxies (1 hop, direct to target) 103 | # Connection 3: us-proxies -> exit-proxy (cycles back) 104 | # 105 | - address: "127.0.0.1:1083" 106 | protocol: 107 | type: http 108 | rules: 109 | - masks: "0.0.0.0/0" 110 | action: allow 111 | client_chains: 112 | # Chain 1: Two hops 113 | - chain: 114 | - us-proxies 115 | - exit-proxy 116 | # Chain 2: Single hop (no chain: needed for one hop) 117 | - eu-proxies 118 | 119 | # 120 | # Example 5: Pool at a hop - combine multiple sources 121 | # A pool merges proxies from groups and inline configs into one round-robin set. 122 | # First hop round-robins between: us1, us2, backup 123 | # Second hop is always exit-proxy. 124 | # 125 | - address: "127.0.0.1:1084" 126 | protocol: 127 | type: socks 128 | rules: 129 | - masks: "0.0.0.0/0" 130 | action: allow 131 | client_chain: 132 | # First hop: pool combines us-proxies group + inline backup 133 | - pool: 134 | - us-proxies # Expands to us1, us2 135 | - address: "backup.example.com:1080" 136 | protocol: 137 | type: socks 138 | # Second hop 139 | - exit-proxy 140 | 141 | # 142 | # Example 6: Simple single-hop (backwards compatible) 143 | # Just reference a group directly - equivalent to old client_proxy syntax. 144 | # Round-robins between us1 and us2. 145 | # 146 | - address: "127.0.0.1:1085" 147 | protocol: 148 | type: http 149 | rules: 150 | - masks: "0.0.0.0/0" 151 | action: allow 152 | client_chain: us-proxies 153 | -------------------------------------------------------------------------------- /src/config/types/selection.rs: -------------------------------------------------------------------------------- 1 | //! ConfigSelection type for referencing configs by group name or inline. 2 | 3 | use std::collections::HashMap; 4 | 5 | use crate::option_util::NoneOrSome; 6 | 7 | #[derive(Debug, Clone)] 8 | pub enum ConfigSelection { 9 | Config(T), 10 | GroupName(String), 11 | } 12 | 13 | impl ConfigSelection { 14 | pub fn unwrap_config(self) -> T { 15 | match self { 16 | ConfigSelection::Config(config) => config, 17 | ConfigSelection::GroupName(_) => { 18 | panic!("Tried to unwrap a ConfigSelection::GroupName"); 19 | } 20 | } 21 | } 22 | 23 | pub fn unwrap_config_mut(&mut self) -> &mut T { 24 | match self { 25 | ConfigSelection::Config(config) => config, 26 | ConfigSelection::GroupName(_) => { 27 | panic!("Tried to unwrap a ConfigSelection::GroupName"); 28 | } 29 | } 30 | } 31 | 32 | fn replace<'a, U>( 33 | iter: impl Iterator>, 34 | client_groups: &HashMap>, 35 | ) -> std::io::Result>> 36 | where 37 | U: Clone + 'a, 38 | { 39 | let mut ret = vec![]; 40 | for selection in iter { 41 | match selection { 42 | ConfigSelection::Config(client_config) => { 43 | ret.push(ConfigSelection::Config(client_config.clone())); 44 | } 45 | ConfigSelection::GroupName(client_group) => { 46 | match client_groups.get(client_group.as_str()) { 47 | Some(client_configs) => { 48 | ret.extend(client_configs.iter().cloned().map(ConfigSelection::Config)); 49 | } 50 | None => { 51 | return Err(std::io::Error::new( 52 | std::io::ErrorKind::InvalidInput, 53 | format!("No such client group: {client_group}"), 54 | )); 55 | } 56 | } 57 | } 58 | } 59 | } 60 | Ok(ret) 61 | } 62 | 63 | pub fn replace_none_or_some_groups( 64 | selections: &mut NoneOrSome>, 65 | client_groups: &HashMap>, 66 | ) -> std::io::Result<()> 67 | where 68 | T: Clone + Sync, 69 | { 70 | if selections.is_empty() { 71 | return Ok(()); 72 | } 73 | 74 | let ret = Self::replace(selections.iter(), client_groups)?; 75 | let _ = std::mem::replace(selections, NoneOrSome::Some(ret)); 76 | Ok(()) 77 | } 78 | } 79 | 80 | impl<'de, T> serde::de::Deserialize<'de> for ConfigSelection 81 | where 82 | T: serde::de::Deserialize<'de>, 83 | { 84 | fn deserialize(deserializer: D) -> Result 85 | where 86 | D: serde::de::Deserializer<'de>, 87 | { 88 | use serde::de::{Error, Visitor}; 89 | use std::fmt; 90 | use std::marker::PhantomData; 91 | 92 | struct ConfigSelectionVisitor(PhantomData); 93 | 94 | impl<'de, T> Visitor<'de> for ConfigSelectionVisitor 95 | where 96 | T: serde::de::Deserialize<'de>, 97 | { 98 | type Value = ConfigSelection; 99 | 100 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 101 | formatter.write_str( 102 | "either a string (group name reference) or an inline configuration object", 103 | ) 104 | } 105 | 106 | fn visit_str(self, value: &str) -> Result 107 | where 108 | E: Error, 109 | { 110 | Ok(ConfigSelection::GroupName(value.to_string())) 111 | } 112 | 113 | fn visit_map(self, map: A) -> Result 114 | where 115 | A: serde::de::MapAccess<'de>, 116 | { 117 | let config = T::deserialize(serde::de::value::MapAccessDeserializer::new(map)) 118 | .map_err(|e| Error::custom(format!( 119 | "Failed to parse inline configuration: {e}. \ 120 | Expected either a string referencing a named group or a valid configuration object" 121 | )))?; 122 | Ok(ConfigSelection::Config(config)) 123 | } 124 | } 125 | 126 | deserializer.deserialize_any(ConfigSelectionVisitor(PhantomData)) 127 | } 128 | } 129 | 130 | impl serde::ser::Serialize for ConfigSelection 131 | where 132 | T: serde::ser::Serialize, 133 | { 134 | fn serialize(&self, serializer: S) -> Result 135 | where 136 | S: serde::ser::Serializer, 137 | { 138 | match self { 139 | ConfigSelection::Config(config) => config.serialize(serializer), 140 | ConfigSelection::GroupName(name) => serializer.serialize_str(name), 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/vless/vless_response_stream.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | use std::task::{Context, Poll}; 3 | 4 | use futures::ready; 5 | use log::info; 6 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 7 | 8 | use crate::async_stream::{AsyncPing, AsyncStream}; 9 | 10 | /// A wrapper stream that reads VLESS response header on first read, similar 11 | /// to vmess and shadowsocks. 12 | pub struct VlessResponseStream { 13 | inner: IO, 14 | response_pending: bool, 15 | response_buffer: Vec, 16 | } 17 | 18 | impl VlessResponseStream 19 | where 20 | IO: AsyncStream, 21 | { 22 | pub fn new(inner: IO) -> Self { 23 | Self { 24 | inner, 25 | response_pending: true, 26 | response_buffer: Vec::new(), 27 | } 28 | } 29 | 30 | fn poll_read_response(&mut self, cx: &mut Context<'_>) -> Poll> { 31 | // Need to read at least 2 bytes for the response header 32 | while self.response_buffer.len() < 2 { 33 | let mut buf = [0u8; 2]; 34 | let mut read_buf = ReadBuf::new(&mut buf); 35 | ready!(Pin::new(&mut self.inner).poll_read(cx, &mut read_buf))?; 36 | 37 | let filled = read_buf.filled(); 38 | if filled.is_empty() { 39 | return Poll::Ready(Err(std::io::Error::new( 40 | std::io::ErrorKind::UnexpectedEof, 41 | "Connection closed while reading VLESS response", 42 | ))); 43 | } 44 | self.response_buffer.extend_from_slice(filled); 45 | } 46 | 47 | // Validate version 48 | let version = self.response_buffer[0]; 49 | if version != 0 { 50 | return Poll::Ready(Err(std::io::Error::other(format!( 51 | "invalid server protocol version, expected 0, got {version}" 52 | )))); 53 | } 54 | 55 | // Check if we need to read addons 56 | let addon_length = self.response_buffer[1] as usize; 57 | let total_response_len = 2 + addon_length; 58 | 59 | // Read addon data if needed 60 | while self.response_buffer.len() < total_response_len { 61 | let remaining = total_response_len - self.response_buffer.len(); 62 | let mut buf = vec![0u8; remaining]; 63 | let mut read_buf = ReadBuf::new(&mut buf); 64 | ready!(Pin::new(&mut self.inner).poll_read(cx, &mut read_buf))?; 65 | 66 | let filled = read_buf.filled(); 67 | if filled.is_empty() { 68 | return Poll::Ready(Err(std::io::Error::new( 69 | std::io::ErrorKind::UnexpectedEof, 70 | "Connection closed while reading VLESS response addons", 71 | ))); 72 | } 73 | self.response_buffer.extend_from_slice(filled); 74 | } 75 | 76 | info!( 77 | "VLESS: Successfully read and consumed {} byte response header (version={}, addon_length={})", 78 | total_response_len, version, addon_length 79 | ); 80 | 81 | Poll::Ready(Ok(())) 82 | } 83 | } 84 | 85 | impl AsyncRead for VlessResponseStream 86 | where 87 | IO: AsyncStream, 88 | { 89 | fn poll_read( 90 | mut self: Pin<&mut Self>, 91 | cx: &mut Context<'_>, 92 | buf: &mut ReadBuf<'_>, 93 | ) -> Poll> { 94 | // Read VLESS response header on first read 95 | if self.response_pending { 96 | match self.poll_read_response(cx) { 97 | Poll::Ready(Ok(())) => { 98 | self.response_pending = false; 99 | // Response is consumed, now read actual data 100 | } 101 | Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), 102 | Poll::Pending => return Poll::Pending, 103 | } 104 | } 105 | 106 | // Pass through to inner stream 107 | Pin::new(&mut self.inner).poll_read(cx, buf) 108 | } 109 | } 110 | 111 | impl AsyncWrite for VlessResponseStream 112 | where 113 | IO: AsyncStream, 114 | { 115 | fn poll_write( 116 | mut self: Pin<&mut Self>, 117 | cx: &mut Context<'_>, 118 | buf: &[u8], 119 | ) -> Poll> { 120 | Pin::new(&mut self.inner).poll_write(cx, buf) 121 | } 122 | 123 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 124 | Pin::new(&mut self.inner).poll_flush(cx) 125 | } 126 | 127 | fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 128 | Pin::new(&mut self.inner).poll_shutdown(cx) 129 | } 130 | } 131 | 132 | impl AsyncPing for VlessResponseStream 133 | where 134 | IO: AsyncStream, 135 | { 136 | fn supports_ping(&self) -> bool { 137 | self.inner.supports_ping() 138 | } 139 | 140 | fn poll_write_ping( 141 | mut self: Pin<&mut Self>, 142 | cx: &mut Context<'_>, 143 | ) -> Poll> { 144 | Pin::new(&mut self.inner).poll_write_ping(cx) 145 | } 146 | } 147 | 148 | impl AsyncStream for VlessResponseStream where IO: AsyncStream {} 149 | -------------------------------------------------------------------------------- /src/reality/reality_cipher_suite.rs: -------------------------------------------------------------------------------- 1 | //! TLS 1.3 Cipher Suite definitions for REALITY protocol 2 | //! 3 | //! This module defines the CipherSuite type which bundles cipher suite ID 4 | //! with its associated algorithms for AEAD encryption, transcript hashing, 5 | //! and HKDF key derivation. 6 | 7 | use aws_lc_rs::{ 8 | aead::{AES_128_GCM, AES_256_GCM, Algorithm, CHACHA20_POLY1305}, 9 | digest, 10 | hmac::{self, HMAC_SHA256, HMAC_SHA384}, 11 | }; 12 | 13 | /// Default TLS 1.3 cipher suites in preference order 14 | pub const DEFAULT_CIPHER_SUITES: &[CipherSuite] = &[ 15 | CipherSuite::AES_128_GCM_SHA256, 16 | CipherSuite::AES_256_GCM_SHA384, 17 | CipherSuite::CHACHA20_POLY1305_SHA256, 18 | ]; 19 | 20 | /// TLS 1.3 Cipher Suite with all associated algorithms 21 | /// 22 | /// This struct bundles the cipher suite ID with its corresponding AEAD algorithm, 23 | /// digest algorithm (for transcript hashing), and HMAC algorithm (for HKDF operations). 24 | /// Per RFC 8446, different cipher suites require different hash algorithms: 25 | /// - TLS_AES_128_GCM_SHA256 (0x1301): SHA256 26 | /// - TLS_AES_256_GCM_SHA384 (0x1302): SHA384 27 | /// - TLS_CHACHA20_POLY1305_SHA256 (0x1303): SHA256 28 | #[derive(Clone, Copy)] 29 | pub struct CipherSuite { 30 | id: u16, 31 | algorithm: &'static Algorithm, 32 | digest_algorithm: &'static digest::Algorithm, 33 | hmac_algorithm: hmac::Algorithm, 34 | } 35 | 36 | impl PartialEq for CipherSuite { 37 | fn eq(&self, other: &Self) -> bool { 38 | self.id == other.id 39 | } 40 | } 41 | 42 | impl Eq for CipherSuite {} 43 | 44 | impl CipherSuite { 45 | pub const AES_128_GCM_SHA256: Self = Self { 46 | id: 0x1301, 47 | algorithm: &AES_128_GCM, 48 | digest_algorithm: &digest::SHA256, 49 | hmac_algorithm: HMAC_SHA256, 50 | }; 51 | 52 | pub const AES_256_GCM_SHA384: Self = Self { 53 | id: 0x1302, 54 | algorithm: &AES_256_GCM, 55 | digest_algorithm: &digest::SHA384, 56 | hmac_algorithm: HMAC_SHA384, 57 | }; 58 | 59 | pub const CHACHA20_POLY1305_SHA256: Self = Self { 60 | id: 0x1303, 61 | algorithm: &CHACHA20_POLY1305, 62 | digest_algorithm: &digest::SHA256, 63 | hmac_algorithm: HMAC_SHA256, 64 | }; 65 | 66 | /// Get CipherSuite from wire format ID 67 | pub fn from_id(id: u16) -> Option { 68 | match id { 69 | 0x1301 => Some(Self::AES_128_GCM_SHA256), 70 | 0x1302 => Some(Self::AES_256_GCM_SHA384), 71 | 0x1303 => Some(Self::CHACHA20_POLY1305_SHA256), 72 | _ => None, 73 | } 74 | } 75 | 76 | /// Get CipherSuite from standard TLS name 77 | pub fn from_name(name: &str) -> Option { 78 | match name { 79 | "TLS_AES_128_GCM_SHA256" => Some(Self::AES_128_GCM_SHA256), 80 | "TLS_AES_256_GCM_SHA384" => Some(Self::AES_256_GCM_SHA384), 81 | "TLS_CHACHA20_POLY1305_SHA256" => Some(Self::CHACHA20_POLY1305_SHA256), 82 | _ => None, 83 | } 84 | } 85 | 86 | /// Get standard TLS name for this cipher suite 87 | pub fn name(&self) -> &'static str { 88 | match self.id { 89 | 0x1301 => "TLS_AES_128_GCM_SHA256", 90 | 0x1302 => "TLS_AES_256_GCM_SHA384", 91 | 0x1303 => "TLS_CHACHA20_POLY1305_SHA256", 92 | _ => unreachable!(), 93 | } 94 | } 95 | 96 | /// Wire format ID (e.g., 0x1301) 97 | #[inline] 98 | pub fn id(&self) -> u16 { 99 | self.id 100 | } 101 | 102 | /// AEAD algorithm (AES_128_GCM, AES_256_GCM, or CHACHA20_POLY1305) 103 | #[inline] 104 | pub fn algorithm(&self) -> &'static Algorithm { 105 | self.algorithm 106 | } 107 | 108 | /// Key length in bytes (16 for AES-128, 32 for AES-256/ChaCha20) 109 | #[inline] 110 | pub fn key_len(&self) -> usize { 111 | self.algorithm.key_len() 112 | } 113 | 114 | /// Nonce/IV length in bytes (always 12 for TLS 1.3) 115 | #[inline] 116 | pub fn nonce_len(&self) -> usize { 117 | self.algorithm.nonce_len() 118 | } 119 | 120 | /// Hash output length in bytes (32 for SHA256, 48 for SHA384) 121 | #[inline] 122 | pub fn hash_len(&self) -> usize { 123 | self.digest_algorithm.output_len() 124 | } 125 | 126 | /// Digest algorithm for transcript hashing 127 | #[inline] 128 | pub fn digest_algorithm(&self) -> &'static digest::Algorithm { 129 | self.digest_algorithm 130 | } 131 | 132 | /// HMAC algorithm for HKDF operations 133 | #[inline] 134 | pub fn hmac_algorithm(&self) -> hmac::Algorithm { 135 | self.hmac_algorithm 136 | } 137 | } 138 | 139 | impl std::fmt::Debug for CipherSuite { 140 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 141 | write!(f, "{}", self.name()) 142 | } 143 | } 144 | 145 | impl std::fmt::Display for CipherSuite { 146 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 147 | write!(f, "{}", self.name()) 148 | } 149 | } 150 | 151 | impl std::fmt::LowerHex for CipherSuite { 152 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 153 | std::fmt::LowerHex::fmt(&self.id, f) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/reality/reality_reader_writer.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufRead, Read, Write}; 2 | 3 | use crate::slide_buffer::SlideBuffer; 4 | 5 | /// Reader for accessing decrypted plaintext from REALITY connections 6 | /// 7 | /// This reader provides a view over a SlideBuffer and consumes data from it 8 | /// as it is read. The SlideBuffer handles efficient memory management. 9 | /// 10 | /// Mirrors rustls::Reader behavior for fill_buf(): 11 | /// - Ok(data) when data is available 12 | /// - Ok(&[]) when close_notify received (clean EOF) 13 | /// - Err(WouldBlock) when no data and connection still active 14 | pub struct RealityReader<'a> { 15 | buffer: &'a mut SlideBuffer, 16 | received_close_notify: bool, 17 | } 18 | 19 | impl<'a> RealityReader<'a> { 20 | pub fn new(buffer: &'a mut SlideBuffer, received_close_notify: bool) -> Self { 21 | RealityReader { 22 | buffer, 23 | received_close_notify, 24 | } 25 | } 26 | } 27 | 28 | impl<'a> Read for RealityReader<'a> { 29 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 30 | let available = self.buffer.len(); 31 | let to_read = buf.len().min(available); 32 | if to_read > 0 { 33 | buf[..to_read].copy_from_slice(&self.buffer[..to_read]); 34 | self.buffer.consume(to_read); 35 | } 36 | Ok(to_read) 37 | } 38 | } 39 | 40 | impl<'a> BufRead for RealityReader<'a> { 41 | fn fill_buf(&mut self) -> std::io::Result<&[u8]> { 42 | if !self.buffer.is_empty() { 43 | // Data available 44 | Ok(self.buffer.as_slice()) 45 | } else if self.received_close_notify { 46 | // Clean EOF - close_notify received (mirrors rustls behavior) 47 | Ok(&[]) 48 | } else { 49 | // No data, connection still active 50 | Err(std::io::ErrorKind::WouldBlock.into()) 51 | } 52 | } 53 | 54 | fn consume(&mut self, amt: usize) { 55 | let actual = amt.min(self.buffer.len()); 56 | self.buffer.consume(actual); 57 | } 58 | } 59 | 60 | /// Writer for buffering plaintext to be encrypted in REALITY connections 61 | pub struct RealityWriter<'a> { 62 | buffer: &'a mut Vec, 63 | } 64 | 65 | impl<'a> RealityWriter<'a> { 66 | pub fn new(buffer: &'a mut Vec) -> Self { 67 | RealityWriter { buffer } 68 | } 69 | } 70 | 71 | impl<'a> Write for RealityWriter<'a> { 72 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 73 | self.buffer.extend_from_slice(buf); 74 | Ok(buf.len()) 75 | } 76 | 77 | fn flush(&mut self) -> std::io::Result<()> { 78 | Ok(()) 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use super::*; 85 | 86 | #[test] 87 | fn test_reality_crypto_reader() { 88 | let mut buffer = SlideBuffer::new(1024); 89 | buffer.extend_from_slice(b"hello world"); 90 | 91 | { 92 | let mut reader = RealityReader::new(&mut buffer, false); 93 | let mut buf = [0u8; 5]; 94 | assert_eq!(reader.read(&mut buf).unwrap(), 5); 95 | assert_eq!(&buf, b"hello"); 96 | } 97 | 98 | // Buffer should have consumed 5 bytes 99 | assert_eq!(buffer.len(), 6); // " world" remaining 100 | 101 | { 102 | let mut reader = RealityReader::new(&mut buffer, false); 103 | let mut buf = [0u8; 5]; 104 | assert_eq!(reader.read(&mut buf).unwrap(), 5); 105 | assert_eq!(&buf[..5], b" worl"); 106 | } 107 | 108 | assert_eq!(buffer.len(), 1); // "d" remaining 109 | } 110 | 111 | #[test] 112 | fn test_reality_crypto_reader_bufread() { 113 | let mut buffer = SlideBuffer::new(1024); 114 | buffer.extend_from_slice(b"test data"); 115 | 116 | let mut reader = RealityReader::new(&mut buffer, false); 117 | 118 | let buf = reader.fill_buf().unwrap(); 119 | assert_eq!(buf, b"test data"); 120 | 121 | reader.consume(5); 122 | let buf = reader.fill_buf().unwrap(); 123 | assert_eq!(buf, b"data"); 124 | } 125 | 126 | #[test] 127 | fn test_reality_crypto_reader_empty_would_block() { 128 | let mut buffer = SlideBuffer::new(1024); 129 | // Empty buffer, no close_notify -> WouldBlock 130 | let mut reader = RealityReader::new(&mut buffer, false); 131 | let result = reader.fill_buf(); 132 | assert!(result.is_err()); 133 | assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::WouldBlock); 134 | } 135 | 136 | #[test] 137 | fn test_reality_crypto_reader_close_notify_eof() { 138 | let mut buffer = SlideBuffer::new(1024); 139 | // Empty buffer, close_notify received -> Ok(&[]) (EOF) 140 | let mut reader = RealityReader::new(&mut buffer, true); 141 | let result = reader.fill_buf(); 142 | assert!(result.is_ok()); 143 | assert!(result.unwrap().is_empty()); 144 | } 145 | 146 | #[test] 147 | fn test_reality_crypto_writer() { 148 | let mut buffer = Vec::new(); 149 | let mut writer = RealityWriter::new(&mut buffer); 150 | 151 | assert_eq!(writer.write(b"hello").unwrap(), 5); 152 | assert_eq!(writer.write(b" world").unwrap(), 6); 153 | assert_eq!(buffer.as_slice(), b"hello world"); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/reality/common.rs: -------------------------------------------------------------------------------- 1 | // Common constants and helpers shared between REALITY client and server implementations 2 | // 3 | // This module provides: 4 | // - TLS constants (content types, alert codes, version bytes, handshake types) 5 | // - Close notify alert construction 6 | 7 | use super::reality_aead::encrypt_tls13_record; 8 | use super::reality_cipher_suite::CipherSuite; 9 | use std::io; 10 | 11 | // TLS ContentType values 12 | pub const CONTENT_TYPE_CHANGE_CIPHER_SPEC: u8 = 0x14; 13 | pub const CONTENT_TYPE_ALERT: u8 = 0x15; 14 | pub const CONTENT_TYPE_HANDSHAKE: u8 = 0x16; 15 | pub const CONTENT_TYPE_APPLICATION_DATA: u8 = 0x17; 16 | 17 | // TLS alert levels and descriptions 18 | pub const ALERT_LEVEL_WARNING: u8 = 0x01; 19 | pub const ALERT_DESC_CLOSE_NOTIFY: u8 = 0x00; 20 | 21 | // TLS version bytes (used on wire for compatibility) 22 | // TLS 1.2 version bytes: 0x03, 0x03 23 | // Used in TLS 1.3 for compatibility (appears in record layer) 24 | pub const VERSION_TLS_1_2_MAJOR: u8 = 0x03; 25 | pub const VERSION_TLS_1_2_MINOR: u8 = 0x03; 26 | 27 | // TLS 1.3 handshake message types 28 | pub const HANDSHAKE_TYPE_SERVER_HELLO: u8 = 2; 29 | pub const HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS: u8 = 8; 30 | pub const HANDSHAKE_TYPE_CERTIFICATE: u8 = 11; 31 | pub const HANDSHAKE_TYPE_CERTIFICATE_VERIFY: u8 = 15; 32 | pub const HANDSHAKE_TYPE_FINISHED: u8 = 20; 33 | 34 | // TLS 1.3 record size limits per RFC 8446 35 | // 36 | // The TLS record header's `length` field specifies the size of the ENCRYPTED payload. 37 | // Per RFC 8446, the TLS 1.3 limit is stricter than TLS 1.2: 38 | // 39 | // - TLS 1.3: Plaintext limit = 16,384 bytes (2^14) 40 | // Encryption overhead allowance = 256 bytes 41 | // Ciphertext limit = 16,384 + 256 = 16,640 bytes 42 | // 43 | // - TLS 1.2: Plaintext limit = 16,384 bytes (2^14) 44 | // Encryption overhead allowance = 2,048 bytes 45 | // Ciphertext limit = 16,384 + 2,048 = 18,432 bytes 46 | // 47 | // REALITY uses TLS 1.3, so we MUST use the TLS 1.3 limit. Using the larger 48 | // TLS 1.2 limit causes "record overflow" errors in libraries like utls. 49 | 50 | /// Maximum TLS 1.3 ciphertext payload size (16,640 bytes) 51 | pub const MAX_TLS_CIPHERTEXT_LEN: usize = 16384 + 256; 52 | 53 | /// Maximum plaintext payload size for a single TLS 1.3 record 54 | /// 55 | /// RFC 8446 Section 5.1: "The record layer fragments information blocks into 56 | /// TLSPlaintext records carrying data in chunks of 2^14 bytes or less." 57 | /// 58 | /// This is the hard limit enforced by TLS implementations. 59 | /// The 256-byte allowance in MAX_TLS_CIPHERTEXT_LEN is for: 60 | /// - AEAD tag (16 bytes for AES-GCM) 61 | /// - Content type byte (1 byte) 62 | /// - Optional padding (up to 239 bytes) 63 | /// 64 | /// We MUST NOT exceed 16384 bytes of actual plaintext per record, or clients 65 | /// will reject with "record overflow" error. 66 | pub const MAX_TLS_PLAINTEXT_LEN: usize = 16384; 67 | 68 | /// TLS record header size (ContentType + ProtocolVersion + Length) 69 | pub const TLS_RECORD_HEADER_SIZE: usize = 5; 70 | 71 | /// Maximum TLS record size (ciphertext + header) 72 | pub const TLS_MAX_RECORD_SIZE: usize = MAX_TLS_CIPHERTEXT_LEN + TLS_RECORD_HEADER_SIZE; 73 | 74 | /// Buffer capacity for ciphertext read (2x TLS max record for safety) 75 | pub const CIPHERTEXT_READ_BUF_CAPACITY: usize = TLS_MAX_RECORD_SIZE * 2; 76 | 77 | /// Buffer capacity for plaintext read 78 | pub const PLAINTEXT_READ_BUF_CAPACITY: usize = TLS_MAX_RECORD_SIZE * 2; 79 | 80 | /// Increment a TLS 1.3 sequence number, checking for overflow. 81 | /// 82 | /// Per RFC 8446 Section 5.3: "Sequence numbers MUST NOT wrap." 83 | /// This function returns an error if incrementing would cause wrap-around. 84 | #[inline(always)] 85 | pub fn increment_seq(seq: &mut u64) -> io::Result<()> { 86 | if *seq == u64::MAX { 87 | return Err(io::Error::other( 88 | "TLS sequence number exhausted (RFC 8446 Section 5.3)", 89 | )); 90 | } 91 | *seq += 1; 92 | Ok(()) 93 | } 94 | 95 | /// Build an encrypted close_notify alert for TLS 1.3 96 | /// 97 | /// In TLS 1.3, alerts must be encrypted like application data. 98 | pub fn build_close_notify_alert( 99 | cipher_suite: CipherSuite, 100 | key: &[u8], 101 | iv: &[u8], 102 | seq_num: u64, 103 | ) -> io::Result> { 104 | // Build alert message: level(1) + description(0) + ContentType 105 | let alert_with_type = vec![ 106 | ALERT_LEVEL_WARNING, 107 | ALERT_DESC_CLOSE_NOTIFY, 108 | CONTENT_TYPE_ALERT, // ContentType byte for TLS 1.3 109 | ]; 110 | 111 | // Build TLS header with correct ciphertext length 112 | let ciphertext_len = (alert_with_type.len() + 16) as u16; // plaintext + tag 113 | let mut tls_header = [ 114 | CONTENT_TYPE_APPLICATION_DATA, 115 | VERSION_TLS_1_2_MAJOR, 116 | VERSION_TLS_1_2_MINOR, 117 | 0x00, 118 | 0x00, // Length will be set 119 | ]; 120 | tls_header[3..5].copy_from_slice(&ciphertext_len.to_be_bytes()); 121 | 122 | // Encrypt the alert 123 | let ciphertext = encrypt_tls13_record( 124 | cipher_suite, 125 | key, 126 | iv, 127 | seq_num, 128 | &alert_with_type, 129 | &tls_header, 130 | )?; 131 | 132 | // Build complete TLS record 133 | let mut record = Vec::with_capacity(5 + ciphertext.len()); 134 | record.push(CONTENT_TYPE_APPLICATION_DATA); 135 | record.push(VERSION_TLS_1_2_MAJOR); 136 | record.push(VERSION_TLS_1_2_MINOR); 137 | record.push(((ciphertext.len() >> 8) & 0xff) as u8); 138 | record.push((ciphertext.len() & 0xff) as u8); 139 | record.extend_from_slice(&ciphertext); 140 | 141 | Ok(record) 142 | } 143 | -------------------------------------------------------------------------------- /src/tls_client_handler.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use async_trait::async_trait; 4 | 5 | use crate::address::NetLocation; 6 | use crate::async_stream::AsyncStream; 7 | use crate::crypto::{CryptoConnection, CryptoTlsStream, perform_crypto_handshake}; 8 | use crate::tcp_handler::{ 9 | TcpClientHandler, TcpClientSetupResult, TcpClientUdpSetupResult, UdpStreamRequest, 10 | }; 11 | 12 | #[derive(Debug)] 13 | pub struct TlsClientHandler { 14 | pub client_config: Arc, 15 | pub tls_buffer_size: Option, 16 | pub server_name: rustls::pki_types::ServerName<'static>, 17 | pub handler: TlsInnerClientHandler, 18 | } 19 | 20 | #[derive(Debug)] 21 | pub enum TlsInnerClientHandler { 22 | Default(Box), 23 | VisionVless { uuid: Box<[u8]>, udp_enabled: bool }, 24 | } 25 | 26 | impl TlsClientHandler { 27 | pub fn new( 28 | client_config: Arc, 29 | tls_buffer_size: Option, 30 | server_name: rustls::pki_types::ServerName<'static>, 31 | handler: Box, 32 | ) -> Self { 33 | Self { 34 | client_config, 35 | tls_buffer_size, 36 | server_name, 37 | handler: TlsInnerClientHandler::Default(handler), 38 | } 39 | } 40 | 41 | pub fn new_vision_vless( 42 | client_config: Arc, 43 | tls_buffer_size: Option, 44 | server_name: rustls::pki_types::ServerName<'static>, 45 | uuid: Box<[u8]>, 46 | udp_enabled: bool, 47 | ) -> Self { 48 | Self { 49 | client_config, 50 | tls_buffer_size, 51 | server_name, 52 | handler: TlsInnerClientHandler::VisionVless { uuid, udp_enabled }, 53 | } 54 | } 55 | } 56 | 57 | #[async_trait] 58 | impl TcpClientHandler for TlsClientHandler { 59 | async fn setup_client_tcp_stream( 60 | &self, 61 | mut client_stream: Box, 62 | remote_location: NetLocation, 63 | ) -> std::io::Result { 64 | // Create rustls ClientConnection 65 | let mut client_conn = 66 | rustls::ClientConnection::new(self.client_config.clone(), self.server_name.clone()) 67 | .map_err(|e| { 68 | std::io::Error::new( 69 | std::io::ErrorKind::InvalidInput, 70 | format!("Failed to create client connection: {e}"), 71 | ) 72 | })?; 73 | 74 | // Set buffer limits if configured 75 | if let Some(size) = self.tls_buffer_size { 76 | client_conn.set_buffer_limit(Some(size)); 77 | } 78 | 79 | let mut connection = CryptoConnection::new_rustls_client(client_conn); 80 | 81 | // Perform the TLS handshake using the generic helper 82 | // This works for both TLS 1.2 and TLS 1.3 83 | perform_crypto_handshake(&mut connection, &mut client_stream, 16384).await?; 84 | 85 | // Wrap in CryptoTlsStream 86 | let tls_stream = CryptoTlsStream::new(client_stream, connection); 87 | 88 | match &self.handler { 89 | TlsInnerClientHandler::Default(handler) => { 90 | handler 91 | .setup_client_tcp_stream(Box::new(tls_stream), remote_location) 92 | .await 93 | } 94 | TlsInnerClientHandler::VisionVless { uuid, .. } => { 95 | crate::vless::vless_client_handler::setup_custom_tls_vision_vless_client_stream( 96 | tls_stream, 97 | uuid, 98 | &remote_location, 99 | ) 100 | .await 101 | } 102 | } 103 | } 104 | 105 | fn supports_udp_over_tcp(&self) -> bool { 106 | match &self.handler { 107 | TlsInnerClientHandler::Default(handler) => handler.supports_udp_over_tcp(), 108 | TlsInnerClientHandler::VisionVless { udp_enabled, .. } => *udp_enabled, // VLESS supports XUDP when enabled 109 | } 110 | } 111 | 112 | async fn setup_client_udp_stream( 113 | &self, 114 | mut client_stream: Box, 115 | request: UdpStreamRequest, 116 | ) -> std::io::Result { 117 | // Create rustls ClientConnection 118 | let mut client_conn = 119 | rustls::ClientConnection::new(self.client_config.clone(), self.server_name.clone()) 120 | .map_err(|e| { 121 | std::io::Error::new( 122 | std::io::ErrorKind::InvalidInput, 123 | format!("Failed to create client connection: {e}"), 124 | ) 125 | })?; 126 | 127 | // Set buffer limits if configured 128 | if let Some(size) = self.tls_buffer_size { 129 | client_conn.set_buffer_limit(Some(size)); 130 | } 131 | 132 | let mut connection = CryptoConnection::new_rustls_client(client_conn); 133 | 134 | // Perform the TLS handshake 135 | perform_crypto_handshake(&mut connection, &mut client_stream, 16384).await?; 136 | 137 | // Wrap in CryptoTlsStream 138 | let tls_stream = CryptoTlsStream::new(client_stream, connection); 139 | 140 | match &self.handler { 141 | TlsInnerClientHandler::Default(handler) => { 142 | handler 143 | .setup_client_udp_stream(Box::new(tls_stream), request) 144 | .await 145 | } 146 | TlsInnerClientHandler::VisionVless { uuid, .. } => { 147 | // Vision VLESS UDP setup - use the VLESS handler's method 148 | crate::vless::vless_client_handler::setup_vless_udp_stream( 149 | tls_stream, uuid, request, 150 | ) 151 | .await 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shoes 2 | 3 | shoes is a high-performance multi-protocol proxy server written in Rust. 4 | 5 | ## Supported Protocols 6 | 7 | ### Proxy Protocols 8 | - **HTTP/HTTPS** 9 | - **SOCKS5** 10 | - **VMess AEAD** 11 | - **VLESS** 12 | - **Shadowsocks** 13 | - **Trojan** 14 | - **Snell v3** 15 | - **Hysteria2** 16 | - **TUIC v5** 17 | 18 | ### Transport Protocols 19 | All server protocols plus: 20 | - **SagerNet UDP over TCP** (for Shadowsocks and SOCKS5) 21 | - **ShadowTLS v3** 22 | - **TLS** 23 | - **WebSocket** (Shadowsocks SIP003) 24 | - **XTLS Reality** 25 | - **XTLS Vision** (for VLESS) 26 | 27 | ### Supported Ciphers 28 | - **VMess**: `aes-128-gcm`, `chacha20-poly1305`, `none` 29 | - **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` 30 | - **Snell v3**: `aes-128-gcm`, `aes-256-gcm`, `chacha20-ietf-poly1305` 31 | 32 | ## Features 33 | 34 | - **Multi-transport**: TCP or QUIC for all protocols 35 | - **TLS with SNI routing**: Route by Server Name Indication 36 | - **Upstream proxy chaining**: Multi-hop chains with load balancing 37 | - **Rule-based routing**: Route by IP/CIDR or hostname masks 38 | - **Named PEM certificates**: Define once, reference everywhere 39 | - **TLS fingerprint authentication**: Certificate pinning for TLS/QUIC 40 | - **Hot reloading**: Apply config changes without restart 41 | - **Unix socket support**: Bind to Unix domain sockets 42 | 43 | For advanced access control (IP allowlist/blocklists), see [tobaru](https://github.com/cfal/tobaru). 44 | 45 | ## Installation 46 | 47 | Precompiled binaries for x86_64 and Apple aarch64 are available on [Github Releases](https://github.com/cfal/shoes/releases). 48 | 49 | Or install with cargo: 50 | 51 | ```bash 52 | cargo install shoes 53 | ``` 54 | 55 | ## Usage 56 | 57 | ``` 58 | shoes [OPTIONS] [config.yaml...] 59 | 60 | OPTIONS: 61 | -t, --threads NUM Set the number of worker threads (default: CPU count) 62 | -d, --dry-run Parse the config and exit 63 | --no-reload Disable automatic config reloading on file changes 64 | 65 | COMMANDS: 66 | generate-reality-keypair Generate a new Reality X25519 keypair 67 | generate-shadowsocks-2022-password Generate a Shadowsocks password 68 | ``` 69 | 70 | ### Examples 71 | ```bash 72 | # Run with a single config file 73 | shoes config.yaml 74 | 75 | # Run with multiple config files 76 | shoes server1.yaml server2.yaml rules.yaml 77 | 78 | # Run with custom thread count 79 | shoes --threads 8 config.yaml 80 | 81 | # Validate configuration without starting 82 | shoes --dry-run config.yaml 83 | 84 | # Run without hot-reloading 85 | shoes --no-reload config.yaml 86 | 87 | # Generate Reality keypair 88 | shoes generate-reality-keypair 89 | 90 | # Generate Shadowsocks 2022 cipher password 91 | shoes generate-shadowsocks-2022-password 2022-blake3-aes-256-gcm 92 | ``` 93 | 94 | ## Configuration 95 | 96 | See [CONFIG.md](./CONFIG.md) for the complete YAML configuration reference. 97 | 98 | ## Examples 99 | 100 | See the [examples](./examples) directory for all examples. 101 | 102 | ### Basic VMess Server 103 | ```yaml 104 | - address: 0.0.0.0:16823 105 | protocol: 106 | type: vmess 107 | cipher: chacha20-poly1305 108 | user_id: b0e80a62-8a51-47f0-91f1-f0f7faf8d9d4 109 | udp_enabled: true 110 | ``` 111 | 112 | ### VLESS with Vision over TLS 113 | ```yaml 114 | - address: 0.0.0.0:443 115 | protocol: 116 | type: tls 117 | tls_targets: 118 | "vless.example.com": 119 | cert: cert.pem 120 | key: key.pem 121 | vision: true 122 | alpn_protocols: ["http/1.1"] 123 | protocol: 124 | type: vless 125 | user_id: b85798ef-e9dc-46a4-9a87-8da4499d36d0 126 | udp_enabled: true 127 | ``` 128 | 129 | ### Reality Server 130 | ```yaml 131 | - address: 0.0.0.0:443 132 | protocol: 133 | type: tls 134 | reality_targets: 135 | "www.example.com": 136 | private_key: "YOUR_BASE64URL_PRIVATE_KEY" 137 | short_ids: ["0123456789abcdef", ""] 138 | dest: "www.example.com:443" 139 | protocol: 140 | type: vless 141 | user_id: b85798ef-e9dc-46a4-9a87-8da4499d36d0 142 | udp_enabled: true 143 | ``` 144 | 145 | ### Reality Client 146 | ```yaml 147 | - address: 127.0.0.1:1080 148 | protocol: 149 | type: socks 150 | rules: 151 | - masks: "0.0.0.0/0" 152 | action: allow 153 | client_chain: 154 | address: "server.example.com:443" 155 | protocol: 156 | type: reality 157 | public_key: "SERVER_PUBLIC_KEY" 158 | short_id: "0123456789abcdef" 159 | sni_hostname: "www.example.com" 160 | protocol: 161 | type: vless 162 | user_id: b85798ef-e9dc-46a4-9a87-8da4499d36d0 163 | ``` 164 | 165 | ### Hysteria2 Server 166 | ```yaml 167 | - address: 0.0.0.0:443 168 | transport: quic 169 | quic_settings: 170 | cert: cert.pem 171 | key: key.pem 172 | alpn_protocols: ["h3"] 173 | protocol: 174 | type: hysteria2 175 | password: supersecret 176 | udp_enabled: true 177 | ``` 178 | 179 | ### TUIC v5 Server 180 | ```yaml 181 | - address: 0.0.0.0:443 182 | transport: quic 183 | quic_settings: 184 | cert: cert.pem 185 | key: key.pem 186 | protocol: 187 | type: tuic 188 | uuid: d685aef3-b3c4-4932-9a9d-d0c2f6727dfa 189 | password: supersecret 190 | ``` 191 | 192 | ## Similar Projects 193 | 194 | - [apernet/hysteria](https://github.com/apernet/hysteria) 195 | - [ihciah/shadow-tls](https://github.com/ihciah/shadow-tls) 196 | - [SagerNet/sing-box](https://github.com/SagerNet/sing-box) 197 | - [shadowsocks/shadowsocks-rust](https://github.com/shadowsocks/shadowsocks-rust) 198 | - [EAimTY/tuic](https://github.com/EAimTY/tuic) 199 | - [v2fly/v2ray-core](https://github.com/v2fly/v2ray-core) 200 | - [XTLS/Xray-core](https://github.com/XTLS/Xray-core) 201 | -------------------------------------------------------------------------------- /src/resolver.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::future::Future; 3 | use std::net::SocketAddr; 4 | use std::pin::Pin; 5 | use std::sync::Arc; 6 | use std::task::{Context, Poll}; 7 | use std::time::{Duration, Instant}; 8 | 9 | use futures::future::FutureExt; 10 | use log::debug; 11 | use rustc_hash::FxHashMap; 12 | 13 | use crate::address::NetLocation; 14 | 15 | type ResolveFuture = Pin>> + Send>>; 16 | 17 | pub trait Resolver: Send + Sync + Debug { 18 | fn resolve_location(&self, location: &NetLocation) -> ResolveFuture; 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct NativeResolver; 23 | 24 | impl NativeResolver { 25 | pub fn new() -> Self { 26 | NativeResolver {} 27 | } 28 | } 29 | 30 | impl Resolver for NativeResolver { 31 | fn resolve_location(&self, location: &NetLocation) -> ResolveFuture { 32 | let address = location.address().clone(); 33 | let port = location.port(); 34 | Box::pin( 35 | tokio::net::lookup_host((address.to_string(), port)).map(move |result| { 36 | let ret = result.map(|r| { 37 | r.filter(|addr| !addr.ip().is_unspecified()) 38 | .collect::>() 39 | }); 40 | debug!("NativeResolver resolved {address}:{port} -> {ret:?}"); 41 | ret 42 | }), 43 | ) 44 | } 45 | } 46 | 47 | pub async fn resolve_single_address( 48 | resolver: &Arc, 49 | location: &NetLocation, 50 | ) -> std::io::Result { 51 | if let Some(socket_addr) = location.to_socket_addr_nonblocking() { 52 | return Ok(socket_addr); 53 | } 54 | let resolve_results = resolver.resolve_location(location).await?; 55 | if resolve_results.is_empty() { 56 | return Err(std::io::Error::other(format!( 57 | "could not resolve location: {location}" 58 | ))); 59 | } 60 | Ok(resolve_results[0]) 61 | } 62 | 63 | pub struct ResolverCache { 64 | resolver: Arc, 65 | cache: FxHashMap, 66 | result_timeout_secs: u64, 67 | } 68 | 69 | enum ResolveState { 70 | Resolving(ResolveFuture), 71 | Resolved(Instant, SocketAddr), 72 | } 73 | 74 | impl ResolverCache { 75 | pub const DEFAULT_RESULT_TIMEOUT_SECS: u64 = 60 * 60; 76 | 77 | pub fn new_with_timeout(resolver: Arc, result_timeout_secs: u64) -> Self { 78 | Self { 79 | resolver, 80 | cache: FxHashMap::default(), 81 | result_timeout_secs, 82 | } 83 | } 84 | 85 | pub fn new(resolver: Arc) -> Self { 86 | Self::new_with_timeout(resolver, Self::DEFAULT_RESULT_TIMEOUT_SECS) 87 | } 88 | 89 | pub fn resolve_location<'a, 'b>( 90 | &'a mut self, 91 | target: &'b NetLocation, 92 | ) -> ResolveLocation<'a, 'b> { 93 | ResolveLocation { 94 | resolver_cache: self, 95 | target, 96 | } 97 | } 98 | 99 | pub fn poll_resolve_location( 100 | &mut self, 101 | cx: &mut Context<'_>, 102 | target: &NetLocation, 103 | ) -> Poll> { 104 | if let Some(socket_addr) = target.to_socket_addr_nonblocking() { 105 | return Poll::Ready(Ok(socket_addr)); 106 | } 107 | 108 | if let Some(ResolveState::Resolved(ts, socket_addr)) = self.cache.get(target) { 109 | if Instant::now().duration_since(*ts) <= Duration::from_secs(self.result_timeout_secs) { 110 | return Poll::Ready(Ok(*socket_addr)); 111 | } else { 112 | self.cache.remove(target); 113 | } 114 | } 115 | 116 | if let Some(ResolveState::Resolving(resolve_future)) = self.cache.get_mut(target) { 117 | match resolve_future.as_mut().poll(cx) { 118 | Poll::Pending => return Poll::Pending, 119 | Poll::Ready(result) => match result { 120 | Ok(v) => { 121 | if v.is_empty() { 122 | return Poll::Ready(Err(std::io::Error::other(format!( 123 | "Failed to resolve {target}, no results" 124 | )))); 125 | } 126 | let socket_addr = v.into_iter().next().unwrap(); 127 | self.cache.insert( 128 | target.clone(), 129 | ResolveState::Resolved(Instant::now(), socket_addr), 130 | ); 131 | return Poll::Ready(Ok(socket_addr)); 132 | } 133 | Err(e) => return Poll::Ready(Err(e)), 134 | }, 135 | } 136 | } 137 | 138 | let mut resolve_future = self.resolver.resolve_location(target); 139 | match resolve_future.as_mut().poll(cx) { 140 | Poll::Pending => { 141 | self.cache 142 | .insert(target.clone(), ResolveState::Resolving(resolve_future)); 143 | Poll::Pending 144 | } 145 | Poll::Ready(result) => match result { 146 | Ok(v) => { 147 | if v.is_empty() { 148 | return Poll::Ready(Err(std::io::Error::other(format!( 149 | "Failed to resolve {target}, no results" 150 | )))); 151 | } 152 | let socket_addr = v.into_iter().next().unwrap(); 153 | self.cache.insert( 154 | target.clone(), 155 | ResolveState::Resolved(Instant::now(), socket_addr), 156 | ); 157 | Poll::Ready(Ok(socket_addr)) 158 | } 159 | Err(e) => Poll::Ready(Err(e)), 160 | }, 161 | } 162 | } 163 | } 164 | 165 | pub struct ResolveLocation<'a, 'b> { 166 | resolver_cache: &'a mut ResolverCache, 167 | target: &'b NetLocation, 168 | } 169 | 170 | impl Future for ResolveLocation<'_, '_> { 171 | type Output = std::io::Result; 172 | 173 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 174 | let this = self.get_mut(); 175 | this.resolver_cache.poll_resolve_location(cx, this.target) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/reality_client_handler.rs: -------------------------------------------------------------------------------- 1 | // REALITY client handler 2 | // 3 | // This file lives at src/ (not in reality/) for two reasons: 4 | // 1. Symmetry with tls_server_handler.rs which handles both TLS and REALITY server-side 5 | // 2. Avoids a dependency cycle: crypto → reality (core types), but this handler → crypto 6 | 7 | use async_trait::async_trait; 8 | 9 | use crate::address::NetLocation; 10 | use crate::async_stream::AsyncStream; 11 | use crate::crypto::{CryptoConnection, CryptoTlsStream, perform_crypto_handshake}; 12 | use crate::reality::{CipherSuite, RealityClientConfig, RealityClientConnection}; 13 | use crate::tcp_handler::{TcpClientHandler, TcpClientSetupResult}; 14 | 15 | /// REALITY client handler using buffered Connection API 16 | /// 17 | /// This handler establishes REALITY-obfuscated TLS connections using 18 | /// the buffered sans-I/O pattern with RealityClientConnection. 19 | #[derive(Debug)] 20 | pub struct RealityClientHandler { 21 | public_key: [u8; 32], 22 | short_id: [u8; 8], 23 | server_name: rustls::pki_types::ServerName<'static>, 24 | cipher_suites: Vec, 25 | handler: RealityInnerClientHandler, 26 | } 27 | 28 | #[derive(Debug)] 29 | pub enum RealityInnerClientHandler { 30 | Default(Box), 31 | VisionVless { uuid: Box<[u8]>, udp_enabled: bool }, 32 | } 33 | 34 | impl RealityClientHandler { 35 | pub fn new( 36 | public_key: [u8; 32], 37 | short_id: [u8; 8], 38 | server_name: rustls::pki_types::ServerName<'static>, 39 | cipher_suites: Vec, 40 | handler: Box, 41 | ) -> Self { 42 | Self { 43 | public_key, 44 | short_id, 45 | server_name, 46 | cipher_suites, 47 | handler: RealityInnerClientHandler::Default(handler), 48 | } 49 | } 50 | pub fn new_vision_vless( 51 | public_key: [u8; 32], 52 | short_id: [u8; 8], 53 | server_name: rustls::pki_types::ServerName<'static>, 54 | cipher_suites: Vec, 55 | user_id: Box<[u8]>, 56 | udp_enabled: bool, 57 | ) -> Self { 58 | Self { 59 | public_key, 60 | short_id, 61 | server_name, 62 | cipher_suites, 63 | handler: RealityInnerClientHandler::VisionVless { 64 | uuid: user_id, 65 | udp_enabled, 66 | }, 67 | } 68 | } 69 | 70 | async fn setup_client_stream_common( 71 | &self, 72 | mut client_stream: Box, 73 | ) -> std::io::Result>> { 74 | // Extract server name as string 75 | let server_name_str = match &self.server_name { 76 | rustls::pki_types::ServerName::DnsName(name) => name.as_ref(), 77 | rustls::pki_types::ServerName::IpAddress(ip) => { 78 | return Err(std::io::Error::new( 79 | std::io::ErrorKind::InvalidInput, 80 | format!("REALITY requires DNS name, got IP address: {:?}", ip), 81 | )); 82 | } 83 | _ => { 84 | return Err(std::io::Error::new( 85 | std::io::ErrorKind::InvalidInput, 86 | "REALITY requires DNS name", 87 | )); 88 | } 89 | }; 90 | 91 | log::debug!("REALITY CLIENT: Creating buffered RealityClientConnection"); 92 | let reality_config = RealityClientConfig { 93 | public_key: self.public_key, 94 | short_id: self.short_id, 95 | server_name: server_name_str.to_string(), 96 | cipher_suites: self.cipher_suites.clone(), 97 | }; 98 | 99 | let reality_conn = RealityClientConnection::new(reality_config)?; 100 | 101 | // Wrap in Connection enum 102 | log::debug!("REALITY CLIENT: Creating Connection"); 103 | let mut connection = CryptoConnection::new_reality_client(reality_conn); 104 | 105 | // Perform the TLS handshake 106 | perform_crypto_handshake(&mut connection, &mut client_stream, 16384).await?; 107 | log::debug!("REALITY CLIENT: Handshake completed successfully"); 108 | 109 | Ok(CryptoTlsStream::new(client_stream, connection)) 110 | } 111 | } 112 | 113 | #[async_trait] 114 | impl TcpClientHandler for RealityClientHandler { 115 | async fn setup_client_tcp_stream( 116 | &self, 117 | client_stream: Box, 118 | remote_location: NetLocation, 119 | ) -> std::io::Result { 120 | let tls_stream = self.setup_client_stream_common(client_stream).await?; 121 | 122 | match self.handler { 123 | RealityInnerClientHandler::Default(ref handler) => { 124 | handler 125 | .setup_client_tcp_stream(Box::new(tls_stream), remote_location) 126 | .await 127 | } 128 | RealityInnerClientHandler::VisionVless { ref uuid, .. } => { 129 | crate::vless::vless_client_handler::setup_custom_tls_vision_vless_client_stream( 130 | tls_stream, 131 | uuid, 132 | &remote_location, 133 | ) 134 | .await 135 | } 136 | } 137 | } 138 | 139 | fn supports_udp_over_tcp(&self) -> bool { 140 | match &self.handler { 141 | RealityInnerClientHandler::Default(handler) => handler.supports_udp_over_tcp(), 142 | RealityInnerClientHandler::VisionVless { udp_enabled, .. } => *udp_enabled, 143 | } 144 | } 145 | 146 | async fn setup_client_udp_stream( 147 | &self, 148 | client_stream: Box, 149 | request: crate::tcp_handler::UdpStreamRequest, 150 | ) -> std::io::Result { 151 | let tls_stream = self.setup_client_stream_common(client_stream).await?; 152 | 153 | match &self.handler { 154 | RealityInnerClientHandler::Default(handler) => { 155 | handler 156 | .setup_client_udp_stream(Box::new(tls_stream), request) 157 | .await 158 | } 159 | RealityInnerClientHandler::VisionVless { uuid, .. } => { 160 | // Vision VLESS UDP setup - use the VLESS handler's method 161 | crate::vless::vless_client_handler::setup_vless_udp_stream( 162 | tls_stream, uuid, request, 163 | ) 164 | .await 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/socket_util.rs: -------------------------------------------------------------------------------- 1 | use std::mem::ManuallyDrop; 2 | use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 3 | use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; 4 | use std::path::Path; 5 | 6 | use socket2::{Domain, Protocol, SockAddr, Socket, Type}; 7 | 8 | pub fn new_udp_socket( 9 | is_ipv6: bool, 10 | bind_interface: Option, 11 | ) -> std::io::Result { 12 | let socket = new_socket2_udp_socket( 13 | is_ipv6, 14 | bind_interface, 15 | Some(get_unspecified_socket_addr(is_ipv6)), 16 | false, 17 | )?; 18 | 19 | into_tokio_udp_socket(socket) 20 | } 21 | 22 | pub fn new_reuse_udp_sockets( 23 | is_ipv6: bool, 24 | bind_interface: Option, 25 | count: usize, 26 | ) -> std::io::Result> { 27 | let mut sockets = Vec::with_capacity(count); 28 | if count == 0 { 29 | return Ok(sockets); 30 | } 31 | 32 | let socket = new_socket2_udp_socket( 33 | is_ipv6, 34 | bind_interface.clone(), 35 | Some(get_unspecified_socket_addr(is_ipv6)), 36 | true, 37 | )?; 38 | 39 | let local_addr = socket.local_addr()?.as_socket().unwrap(); 40 | 41 | sockets.push(into_tokio_udp_socket(socket)?); 42 | 43 | for _ in 1..count { 44 | let socket = 45 | new_socket2_udp_socket(is_ipv6, bind_interface.clone(), Some(local_addr), true)?; 46 | sockets.push(into_tokio_udp_socket(socket)?); 47 | } 48 | 49 | Ok(sockets) 50 | } 51 | 52 | fn get_unspecified_socket_addr(is_ipv6: bool) -> SocketAddr { 53 | if !is_ipv6 { 54 | SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0) 55 | } else { 56 | "[::]:0".parse().unwrap() 57 | } 58 | } 59 | 60 | pub fn new_socket2_udp_socket( 61 | is_ipv6: bool, 62 | bind_interface: Option, 63 | bind_address: Option, 64 | reuse_port: bool, 65 | ) -> std::io::Result { 66 | let domain = if is_ipv6 { Domain::IPV6 } else { Domain::IPV4 }; 67 | let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?; 68 | 69 | socket.set_nonblocking(true)?; 70 | 71 | if reuse_port { 72 | #[cfg(all(unix, not(any(target_os = "solaris", target_os = "illumos"))))] 73 | socket.set_reuse_port(true)?; 74 | 75 | #[cfg(any(not(unix), target_os = "solaris", target_os = "illumos"))] 76 | panic!("Cannot support reuse sockets"); 77 | } 78 | 79 | if let Some(ref interface) = bind_interface { 80 | #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] 81 | socket.bind_device(Some(interface.as_bytes()))?; 82 | 83 | // This should be handled during config validation. 84 | #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] 85 | panic!("Could not bind to device, unsupported platform.") 86 | } 87 | 88 | if let Some(bind_address) = bind_address { 89 | socket.bind(&SockAddr::from(bind_address))?; 90 | } 91 | 92 | Ok(socket) 93 | } 94 | 95 | fn into_tokio_udp_socket(socket: socket2::Socket) -> std::io::Result { 96 | let raw_fd = socket.into_raw_fd(); 97 | let std_udp_socket = unsafe { std::net::UdpSocket::from_raw_fd(raw_fd) }; 98 | tokio::net::UdpSocket::from_std(std_udp_socket) 99 | } 100 | 101 | pub fn new_tcp_socket( 102 | bind_interface: Option, 103 | is_ipv6: bool, 104 | ) -> std::io::Result { 105 | let tcp_socket = if is_ipv6 { 106 | tokio::net::TcpSocket::new_v6()? 107 | } else { 108 | tokio::net::TcpSocket::new_v4()? 109 | }; 110 | 111 | if let Some(_b) = bind_interface { 112 | #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] 113 | tcp_socket.bind_device(Some(_b.as_bytes()))?; 114 | 115 | // This should be handled during config validation. 116 | #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] 117 | panic!("Could not bind to device, unsupported platform.") 118 | } 119 | 120 | Ok(tcp_socket) 121 | } 122 | 123 | pub fn set_tcp_keepalive( 124 | tcp_stream: &tokio::net::TcpStream, 125 | idle_time: std::time::Duration, 126 | send_interval: std::time::Duration, 127 | ) -> std::io::Result<()> { 128 | let raw_fd = tcp_stream.as_raw_fd(); 129 | let socket2_socket = ManuallyDrop::new(unsafe { Socket::from_raw_fd(raw_fd) }); 130 | if idle_time.is_zero() && send_interval.is_zero() { 131 | socket2_socket.set_keepalive(false)?; 132 | } else { 133 | let keepalive = socket2::TcpKeepalive::new() 134 | .with_time(idle_time) 135 | .with_interval(send_interval); 136 | socket2_socket.set_keepalive(true)?; 137 | socket2_socket.set_tcp_keepalive(&keepalive)?; 138 | } 139 | Ok(()) 140 | } 141 | 142 | // TODO: change backlog to Option and make configuration, backlog -1 uses somaxconn on linux 143 | // https://github.com/rust-lang/rust/blob/3534594029ed1495290e013647a1f53da561f7f1/library/std/src/os/unix/net/listener.rs#L93 144 | pub fn new_tcp_listener( 145 | bind_address: SocketAddr, 146 | backlog: u32, 147 | bind_interface: Option, 148 | ) -> std::io::Result { 149 | let domain = if bind_address.is_ipv6() { 150 | Domain::IPV6 151 | } else { 152 | Domain::IPV4 153 | }; 154 | let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?; 155 | 156 | socket.set_nonblocking(true)?; 157 | socket.set_reuse_address(true)?; 158 | 159 | if let Some(ref interface) = bind_interface { 160 | #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] 161 | socket.bind_device(Some(interface.as_bytes()))?; 162 | 163 | // This should be handled during config validation. 164 | #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] 165 | panic!("Could not bind to device, unsupported platform.") 166 | } 167 | 168 | socket.bind(&SockAddr::from(bind_address))?; 169 | 170 | let backlog = backlog.try_into().unwrap_or(4096); 171 | socket.listen(backlog)?; 172 | 173 | let std_listener: std::net::TcpListener = socket.into(); 174 | tokio::net::TcpListener::from_std(std_listener) 175 | } 176 | 177 | #[cfg(target_family = "unix")] 178 | pub fn new_unix_listener>( 179 | path: P, 180 | backlog: u32, 181 | ) -> std::io::Result { 182 | let path = path.as_ref(); 183 | 184 | let socket = Socket::new(Domain::UNIX, Type::STREAM, None)?; 185 | socket.set_nonblocking(true)?; 186 | 187 | let addr = SockAddr::unix(path)?; 188 | socket.bind(&addr)?; 189 | 190 | let backlog = backlog.try_into().unwrap_or(4096); 191 | socket.listen(backlog)?; 192 | 193 | let std_listener: std::os::unix::net::UnixListener = socket.into(); 194 | tokio::net::UnixListener::from_std(std_listener) 195 | } 196 | -------------------------------------------------------------------------------- /src/udp_multi_message_stream.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::pin::Pin; 3 | use std::sync::Arc; 4 | use std::sync::atomic::{AtomicBool, Ordering}; 5 | use std::task::{Context, Poll}; 6 | 7 | use futures::ready; 8 | use tokio::io::ReadBuf; 9 | use tokio::net::UdpSocket; 10 | use tokio::sync::mpsc::{self, Receiver}; 11 | use tokio::task::JoinHandle; 12 | 13 | use crate::address::NetLocation; 14 | use crate::async_stream::{ 15 | AsyncFlushMessage, AsyncPing, AsyncReadSourcedMessage, AsyncShutdownMessage, 16 | AsyncSourcedMessageStream, AsyncWriteTargetedMessage, 17 | }; 18 | use crate::resolver::{Resolver, ResolverCache}; 19 | 20 | pub struct UdpMultiMessageStream { 21 | // We use a dedicated send socket so that tasks waiting for writable get 22 | // awoken correctly. In addition, when sending to the same destination 23 | // from multiple sockets, the underlying network stack (and NIC) is 24 | // typically the main throughput limiter. In many scenarios, since UDP 25 | // sending is non-blocking and efficient, rotating among sockets will 26 | // only provide marginal throughput improvements. 27 | send_socket: Arc, 28 | resolver_cache: ResolverCache, 29 | receiver: Receiver<(Box<[u8]>, SocketAddr)>, 30 | notify_shutdown: Arc, 31 | join_handles: Vec>, 32 | } 33 | 34 | // NOTE: With multiple UDP sockets bound using SO_REUSEPORT, the OS will 35 | // distribute incoming packets based on a 4-tuple hash. If packets all come 36 | // from the same remote address and port, they will likely always be routed 37 | // to the same socket. 38 | // 39 | // tokio's mpsc channels require &mut self, while UdpSocket's poll_recv and 40 | // poll_send only require &self. Since we only use this for multidirectional 41 | // UDP, we implement the relevant traits directly instead. 42 | impl UdpMultiMessageStream { 43 | pub fn new(sockets: Vec>, resolver: Arc) -> Self { 44 | if sockets.is_empty() { 45 | panic!("at least one socket is required"); 46 | } 47 | 48 | let send_socket = sockets.first().unwrap().clone(); 49 | 50 | let shutdown_flag = Arc::new(AtomicBool::new(false)); 51 | let (tx, rx) = mpsc::channel(40); 52 | let mut join_handles = Vec::new(); 53 | 54 | for socket in sockets.into_iter() { 55 | let tx = tx.clone(); 56 | let shutdown_flag = shutdown_flag.clone(); 57 | let receiver_socket = socket; 58 | join_handles.push(tokio::spawn(async move { 59 | let mut buf = [0u8; 65535]; 60 | 'outer: loop { 61 | if shutdown_flag.load(Ordering::Relaxed) { 62 | break; 63 | } 64 | 65 | if receiver_socket.readable().await.is_err() { 66 | break; 67 | } 68 | 69 | loop { 70 | match receiver_socket.try_recv_from(&mut buf) { 71 | Ok((n, from_addr)) => { 72 | let message = Box::from(&buf[..n]); 73 | match tx.try_send((message, from_addr)) { 74 | Ok(_) => {} 75 | Err(err) => match err { 76 | mpsc::error::TrySendError::Full(_) => { 77 | // Channel is full; the packet is dropped to maintain throughput. 78 | } 79 | mpsc::error::TrySendError::Closed(_) => { 80 | break; 81 | } 82 | }, 83 | } 84 | } 85 | Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { 86 | continue 'outer; 87 | } 88 | Err(e) => { 89 | eprintln!("UDP recv error: {e:?}"); 90 | break; 91 | } 92 | } 93 | } 94 | } 95 | })); 96 | } 97 | 98 | UdpMultiMessageStream { 99 | send_socket, 100 | resolver_cache: ResolverCache::new(resolver), 101 | receiver: rx, 102 | notify_shutdown: shutdown_flag, 103 | join_handles, 104 | } 105 | } 106 | } 107 | 108 | impl AsyncReadSourcedMessage for UdpMultiMessageStream { 109 | fn poll_read_sourced_message( 110 | self: Pin<&mut Self>, 111 | cx: &mut Context<'_>, 112 | buf: &mut ReadBuf<'_>, 113 | ) -> Poll> { 114 | let this = self.get_mut(); 115 | match Pin::new(&mut this.receiver).poll_recv(cx) { 116 | Poll::Ready(Some((message, from_addr))) => { 117 | if message.len() > buf.remaining() { 118 | return Poll::Ready(Err(std::io::Error::other("buffer too small"))); 119 | } 120 | buf.put_slice(&message); 121 | Poll::Ready(Ok(from_addr)) 122 | } 123 | Poll::Ready(None) => Poll::Ready(Err(std::io::Error::new( 124 | std::io::ErrorKind::UnexpectedEof, 125 | "channel closed", 126 | ))), 127 | Poll::Pending => Poll::Pending, 128 | } 129 | } 130 | } 131 | 132 | impl AsyncWriteTargetedMessage for UdpMultiMessageStream { 133 | fn poll_write_targeted_message( 134 | self: Pin<&mut Self>, 135 | cx: &mut Context<'_>, 136 | buf: &[u8], 137 | target: &NetLocation, 138 | ) -> Poll> { 139 | let this = self.get_mut(); 140 | let socket_addr = ready!(this.resolver_cache.poll_resolve_location(cx, target))?; 141 | this.send_socket 142 | .poll_send_to(cx, buf, socket_addr) 143 | .map(|result| result.map(|_| ())) 144 | } 145 | } 146 | 147 | impl AsyncFlushMessage for UdpMultiMessageStream { 148 | fn poll_flush_message( 149 | self: Pin<&mut Self>, 150 | _cx: &mut Context<'_>, 151 | ) -> Poll> { 152 | Poll::Ready(Ok(())) 153 | } 154 | } 155 | 156 | impl AsyncShutdownMessage for UdpMultiMessageStream { 157 | fn poll_shutdown_message( 158 | self: Pin<&mut Self>, 159 | _cx: &mut Context<'_>, 160 | ) -> Poll> { 161 | let this = self.get_mut(); 162 | this.notify_shutdown.store(true, Ordering::Relaxed); 163 | for handle in this.join_handles.drain(..) { 164 | handle.abort(); 165 | } 166 | this.join_handles.clear(); 167 | Poll::Ready(Ok(())) 168 | } 169 | } 170 | 171 | impl AsyncPing for UdpMultiMessageStream { 172 | fn supports_ping(&self) -> bool { 173 | false 174 | } 175 | 176 | fn poll_write_ping(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 177 | unimplemented!(); 178 | } 179 | } 180 | 181 | impl AsyncSourcedMessageStream for UdpMultiMessageStream {} 182 | -------------------------------------------------------------------------------- /src/option_util.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Default, Debug, Clone, Deserialize, Serialize)] 4 | #[serde(untagged)] 5 | pub enum NoneOrOne { 6 | #[serde(skip_deserializing)] 7 | #[default] 8 | Unspecified, 9 | None, 10 | One(T), 11 | } 12 | 13 | impl NoneOrOne { 14 | pub fn is_unspecified(&self) -> bool { 15 | matches!(self, NoneOrOne::Unspecified) 16 | } 17 | 18 | pub fn is_one(&self) -> bool { 19 | matches!(self, NoneOrOne::One(..)) 20 | } 21 | 22 | pub fn unwrap(self) -> T { 23 | match self { 24 | NoneOrOne::One(item) => item, 25 | _ => { 26 | panic!("Tried to unwrap a non-one NoneOrOne"); 27 | } 28 | } 29 | } 30 | 31 | pub fn into_option(self) -> Option { 32 | match self { 33 | NoneOrOne::One(item) => Some(item), 34 | _ => None, 35 | } 36 | } 37 | } 38 | 39 | #[derive(Default, Debug, Clone, Deserialize, Serialize)] 40 | #[serde(untagged)] 41 | pub enum NoneOrSome { 42 | #[serde(skip_deserializing)] 43 | #[default] 44 | Unspecified, 45 | None, 46 | One(T), 47 | Some(Vec), 48 | } 49 | 50 | impl NoneOrSome { 51 | pub fn is_unspecified(&self) -> bool { 52 | matches!(self, NoneOrSome::Unspecified) 53 | } 54 | 55 | pub fn len(&self) -> usize { 56 | match self { 57 | NoneOrSome::Unspecified => 0, 58 | NoneOrSome::None => 0, 59 | NoneOrSome::One(_) => 1, 60 | NoneOrSome::Some(v) => v.len(), 61 | } 62 | } 63 | 64 | pub fn into_vec(self) -> Vec { 65 | match self { 66 | NoneOrSome::Unspecified | NoneOrSome::None => vec![], 67 | NoneOrSome::One(item) => vec![item], 68 | NoneOrSome::Some(v) => v, 69 | } 70 | } 71 | 72 | pub fn into_iter(self) -> Box + Send> 73 | where 74 | T: Send + 'static, 75 | { 76 | match self { 77 | NoneOrSome::Unspecified | NoneOrSome::None => Box::new(std::iter::empty()), 78 | NoneOrSome::One(item) => Box::new(SingleItemIter(Some(item))), 79 | NoneOrSome::Some(v) => Box::new(v.into_iter()), 80 | } 81 | } 82 | 83 | pub fn iter<'a>(&'a self) -> Box + Send + 'a> 84 | where 85 | T: Sync, 86 | { 87 | match self { 88 | NoneOrSome::Unspecified | NoneOrSome::None => Box::new(std::iter::empty()), 89 | NoneOrSome::One(item) => Box::new(SingleItemIter(Some(item))), 90 | NoneOrSome::Some(v) => Box::new(v.iter()), 91 | } 92 | } 93 | 94 | pub fn iter_mut<'a>(&'a mut self) -> Box + Send + 'a> 95 | where 96 | T: Send, 97 | { 98 | match self { 99 | NoneOrSome::Unspecified | NoneOrSome::None => Box::new(std::iter::empty()), 100 | NoneOrSome::One(item) => Box::new(SingleItemIter(Some(item))), 101 | NoneOrSome::Some(v) => Box::new(v.iter_mut()), 102 | } 103 | } 104 | 105 | pub fn is_empty(&self) -> bool { 106 | match self { 107 | NoneOrSome::Unspecified => true, 108 | NoneOrSome::None => true, 109 | NoneOrSome::One(_) => false, 110 | NoneOrSome::Some(v) => v.is_empty(), 111 | } 112 | } 113 | 114 | pub fn map(self, mut f: F) -> NoneOrSome 115 | where 116 | F: FnMut(T) -> U, 117 | { 118 | match self { 119 | NoneOrSome::Unspecified => NoneOrSome::Unspecified, 120 | NoneOrSome::None => NoneOrSome::None, 121 | NoneOrSome::One(item) => NoneOrSome::One(f(item)), 122 | NoneOrSome::Some(v) => NoneOrSome::Some(v.into_iter().map(f).collect()), 123 | } 124 | } 125 | 126 | pub fn _filter(self, f: F) -> Self 127 | where 128 | F: Fn(&T) -> bool, 129 | { 130 | match self { 131 | NoneOrSome::Unspecified => NoneOrSome::Unspecified, 132 | NoneOrSome::None => NoneOrSome::None, 133 | NoneOrSome::One(item) => { 134 | if f(&item) { 135 | NoneOrSome::One(item) 136 | } else { 137 | NoneOrSome::None 138 | } 139 | } 140 | NoneOrSome::Some(v) => { 141 | let filtered: Vec = v.into_iter().filter(f).collect(); 142 | if filtered.is_empty() { 143 | NoneOrSome::None 144 | } else { 145 | NoneOrSome::Some(filtered) 146 | } 147 | } 148 | } 149 | } 150 | } 151 | 152 | #[derive(Debug, Clone, Deserialize, Serialize)] 153 | #[serde(untagged)] 154 | pub enum OneOrSome { 155 | One(T), 156 | #[serde(deserialize_with = "validate_non_empty")] 157 | Some(Vec), 158 | } 159 | 160 | fn validate_non_empty<'de, D, T>(d: D) -> Result, D::Error> 161 | where 162 | D: serde::de::Deserializer<'de>, 163 | T: Deserialize<'de>, 164 | { 165 | let value = Vec::deserialize(d)?; 166 | if value.is_empty() { 167 | return Err(serde::de::Error::invalid_value( 168 | serde::de::Unexpected::Other("empty"), 169 | &"need at least one element", 170 | )); 171 | } 172 | Ok(value) 173 | } 174 | 175 | impl OneOrSome { 176 | #[cfg(test)] 177 | pub fn len(&self) -> usize { 178 | match self { 179 | OneOrSome::One(_) => 1, 180 | OneOrSome::Some(v) => v.len(), 181 | } 182 | } 183 | 184 | pub fn into_vec(self) -> Vec { 185 | match self { 186 | OneOrSome::One(item) => vec![item], 187 | OneOrSome::Some(v) => v, 188 | } 189 | } 190 | 191 | pub fn iter<'a>(&'a self) -> Box + Send + 'a> 192 | where 193 | T: Sync, 194 | { 195 | match self { 196 | OneOrSome::One(item) => Box::new(SingleItemIter(Some(item))), 197 | OneOrSome::Some(v) => Box::new(v.iter()), 198 | } 199 | } 200 | 201 | pub fn iter_mut<'a>(&'a mut self) -> Box + Send + 'a> 202 | where 203 | T: Send, 204 | { 205 | match self { 206 | OneOrSome::One(item) => Box::new(SingleItemIter(Some(item))), 207 | OneOrSome::Some(v) => Box::new(v.iter_mut()), 208 | } 209 | } 210 | 211 | pub fn _contains(&self, x: &T) -> bool 212 | where 213 | T: PartialEq, 214 | { 215 | match self { 216 | OneOrSome::One(item) => item == x, 217 | OneOrSome::Some(v) => v.contains(x), 218 | } 219 | } 220 | } 221 | 222 | struct SingleItemIter(Option); 223 | 224 | impl Iterator for SingleItemIter { 225 | type Item = T; 226 | 227 | fn next(&mut self) -> Option { 228 | self.0.take() 229 | } 230 | } 231 | 232 | impl TryFrom> for OneOrSome { 233 | type Error = std::io::Error; 234 | fn try_from(vec: Vec) -> std::io::Result { 235 | match vec.len() { 236 | 0 => Err(std::io::Error::new( 237 | std::io::ErrorKind::InvalidData, 238 | "Cannot create OneOrSome from empty vector", 239 | )), 240 | 1 => Ok(OneOrSome::One(vec.into_iter().next().unwrap())), 241 | _ => Ok(OneOrSome::Some(vec)), 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/vless/vless_message_stream.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error, ErrorKind, Result}; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 5 | 6 | // Assume these traits are defined in your M-bM-^@M-^\async_streamM-bM-^@M-^] module. 7 | use crate::async_stream::{ 8 | AsyncFlushMessage, AsyncMessageStream, AsyncPing, AsyncReadMessage, AsyncShutdownMessage, 9 | AsyncStream, AsyncWriteMessage, 10 | }; 11 | 12 | pub struct VlessMessageStream { 13 | stream: Box, 14 | read_buf: [u8; 65537], 15 | read_end_index: usize, 16 | pending_write: Vec, 17 | write_offset: usize, 18 | is_eof: bool, 19 | } 20 | 21 | impl VlessMessageStream { 22 | pub fn new(stream: Box) -> Self { 23 | Self { 24 | stream, 25 | read_buf: [0u8; 65537], 26 | read_end_index: 0, 27 | pending_write: Vec::with_capacity(65537), 28 | write_offset: 0, 29 | is_eof: false, 30 | } 31 | } 32 | 33 | pub fn feed_initial_read_data(&mut self, data: &[u8]) -> std::io::Result<()> { 34 | if data.len() > self.read_buf.len() { 35 | return Err(std::io::Error::other( 36 | "feed_initial_read_data called with too much data", 37 | )); 38 | } 39 | self.read_buf[0..data.len()].copy_from_slice(data); 40 | self.read_end_index = data.len(); 41 | Ok(()) 42 | } 43 | } 44 | 45 | impl AsyncReadMessage for VlessMessageStream { 46 | fn poll_read_message( 47 | self: Pin<&mut Self>, 48 | cx: &mut Context<'_>, 49 | out_buf: &mut ReadBuf<'_>, 50 | ) -> Poll> { 51 | let this = self.get_mut(); 52 | 53 | if this.is_eof { 54 | return Poll::Ready(Ok(())); 55 | } 56 | 57 | loop { 58 | if this.read_end_index >= 2 { 59 | let payload_len = u16::from_be_bytes([this.read_buf[0], this.read_buf[1]]) as usize; 60 | let total_len = 2 + payload_len; 61 | if this.read_end_index >= total_len { 62 | if out_buf.remaining() < payload_len { 63 | return Poll::Ready(Err(Error::other( 64 | "out_buf is too small to hold the message", 65 | ))); 66 | } 67 | out_buf.put_slice(&this.read_buf[2..total_len]); 68 | if this.read_end_index > total_len { 69 | this.read_buf.copy_within(total_len..this.read_end_index, 0); 70 | this.read_end_index -= total_len; 71 | } else { 72 | // this.read_end_index == total_len 73 | this.read_end_index = 0; 74 | } 75 | return Poll::Ready(Ok(())); 76 | } 77 | } 78 | 79 | let read_buf_slice = &mut this.read_buf[this.read_end_index..]; 80 | // this is impossible because our buffer size is u16::MAX + 2, so there should always 81 | // be space for a full message. 82 | assert!(!read_buf_slice.is_empty()); 83 | let mut tmp = ReadBuf::new(read_buf_slice); 84 | match Pin::new(&mut this.stream).poll_read(cx, &mut tmp) { 85 | Poll::Ready(Ok(())) => { 86 | let n = tmp.filled().len(); 87 | if n == 0 { 88 | this.is_eof = true; 89 | if this.read_end_index == 0 { 90 | return Poll::Ready(Ok(())); 91 | } else { 92 | return Poll::Ready(Err(Error::new( 93 | ErrorKind::UnexpectedEof, 94 | "EOF reached in the middle of a message", 95 | ))); 96 | } 97 | } 98 | this.read_end_index += n; 99 | } 100 | Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), 101 | Poll::Pending => return Poll::Pending, 102 | } 103 | } 104 | } 105 | } 106 | 107 | impl AsyncWriteMessage for VlessMessageStream { 108 | fn poll_write_message( 109 | self: Pin<&mut Self>, 110 | cx: &mut Context<'_>, 111 | buf: &[u8], 112 | ) -> Poll> { 113 | let mut this = self.get_mut(); 114 | 115 | if !this.pending_write.is_empty() { 116 | if let Poll::Ready(Err(e)) = Pin::new(&mut this).poll_flush_message(cx) { 117 | return Poll::Ready(Err(e)); 118 | } 119 | // previously this checked this.write_offset < this.pending_write.len(), but 120 | // we want to make sure the message was flushed in the underlying stream. 121 | if !this.pending_write.is_empty() { 122 | return Poll::Pending; 123 | } 124 | } 125 | 126 | if buf.len() > 65535 { 127 | return Poll::Ready(Err(Error::new( 128 | ErrorKind::InvalidInput, 129 | "message size too large", 130 | ))); 131 | } 132 | 133 | this.pending_write 134 | .extend_from_slice(&(buf.len() as u16).to_be_bytes()); 135 | this.pending_write.extend_from_slice(buf); 136 | this.write_offset = 0; 137 | Poll::Ready(Ok(())) 138 | } 139 | } 140 | 141 | impl AsyncFlushMessage for VlessMessageStream { 142 | fn poll_flush_message(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 143 | let this = self.get_mut(); 144 | while this.write_offset < this.pending_write.len() { 145 | let chunk = &this.pending_write[this.write_offset..]; 146 | match Pin::new(&mut this.stream).poll_write(cx, chunk) { 147 | Poll::Ready(Ok(n)) => { 148 | if n == 0 { 149 | return Poll::Ready(Err(Error::new( 150 | ErrorKind::WriteZero, 151 | "failed to write message", 152 | ))); 153 | } 154 | this.write_offset += n; 155 | } 156 | Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), 157 | Poll::Pending => return Poll::Pending, 158 | } 159 | } 160 | // Once complete, flush the underlying stream. 161 | match Pin::new(&mut this.stream).poll_flush(cx) { 162 | Poll::Ready(Ok(())) => { 163 | this.pending_write.clear(); 164 | this.write_offset = 0; 165 | Poll::Ready(Ok(())) 166 | } 167 | Poll::Ready(Err(e)) => Poll::Ready(Err(e)), 168 | Poll::Pending => Poll::Pending, 169 | } 170 | } 171 | } 172 | 173 | impl AsyncShutdownMessage for VlessMessageStream { 174 | fn poll_shutdown_message(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 175 | let this = self.get_mut(); 176 | match ::poll_flush_message(Pin::new(this), cx) { 177 | Poll::Ready(Ok(())) => {} 178 | other => return other, 179 | } 180 | Pin::new(&mut this.stream).poll_shutdown(cx) 181 | } 182 | } 183 | 184 | impl AsyncPing for VlessMessageStream { 185 | fn supports_ping(&self) -> bool { 186 | self.stream.supports_ping() 187 | } 188 | 189 | fn poll_write_ping( 190 | mut self: Pin<&mut Self>, 191 | cx: &mut Context<'_>, 192 | ) -> Poll> { 193 | Pin::new(&mut self.stream).poll_write_ping(cx) 194 | } 195 | } 196 | 197 | impl AsyncMessageStream for VlessMessageStream {} 198 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Binaries 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags: [ 'v*' ] 7 | pull_request: 8 | branches: [ master ] 9 | workflow_dispatch: 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build: 16 | name: Build ${{ matrix.target }} 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | # Linux builds (using cross for cross-compilation) 23 | - target: x86_64-unknown-linux-gnu 24 | os: ubuntu-latest 25 | use_cross: false 26 | binary_name: shoes 27 | - target: x86_64-unknown-linux-musl 28 | os: ubuntu-latest 29 | use_cross: true 30 | binary_name: shoes 31 | - target: aarch64-unknown-linux-gnu 32 | os: ubuntu-latest 33 | use_cross: true 34 | binary_name: shoes 35 | - target: aarch64-unknown-linux-musl 36 | os: ubuntu-latest 37 | use_cross: true 38 | binary_name: shoes 39 | 40 | # macOS builds (native compilation) 41 | - target: x86_64-apple-darwin 42 | os: macos-latest 43 | use_cross: false 44 | binary_name: shoes 45 | - target: aarch64-apple-darwin 46 | os: macos-latest 47 | use_cross: false 48 | binary_name: shoes 49 | 50 | # Windows builds 51 | # TODO: enable when working 52 | # - target: x86_64-pc-windows-msvc 53 | # os: windows-latest 54 | # use_cross: false 55 | # binary_name: shoes.exe 56 | # - target: aarch64-pc-windows-msvc 57 | # os: windows-latest 58 | # use_cross: false 59 | # binary_name: shoes.exe 60 | 61 | steps: 62 | - uses: actions/checkout@v4 63 | 64 | - name: Install Rust 65 | uses: dtolnay/rust-toolchain@stable 66 | with: 67 | targets: ${{ matrix.target }} 68 | 69 | - name: Install system dependencies 70 | if: matrix.use_cross == true 71 | run: | 72 | sudo apt-get update 73 | case "${{ matrix.target }}" in 74 | "aarch64-unknown-linux-gnu") 75 | sudo apt-get install -y gcc-aarch64-linux-gnu 76 | ;; 77 | "aarch64-unknown-linux-musl") 78 | sudo apt-get install -y gcc-aarch64-linux-gnu musl-tools 79 | ;; 80 | "x86_64-unknown-linux-musl") 81 | sudo apt-get install -y musl-tools 82 | ;; 83 | esac 84 | 85 | - name: Install cross-compilation tool 86 | if: matrix.use_cross == true 87 | run: | 88 | cargo install cross 89 | 90 | - name: Setup cache 91 | uses: Swatinem/rust-cache@v2 92 | with: 93 | key: ${{ matrix.target }} 94 | 95 | - name: Build binary (using cross) 96 | if: matrix.use_cross == true 97 | run: cross build --release --target ${{ matrix.target }} 98 | 99 | - name: Build binary (native) 100 | if: matrix.use_cross == false 101 | run: cargo build --release --target ${{ matrix.target }} 102 | 103 | - name: Verify binary exists 104 | run: | 105 | if [ -f "target/${{ matrix.target }}/release/${{ matrix.binary_name }}" ]; then 106 | echo "Binary found: target/${{ matrix.target }}/release/${{ matrix.binary_name }}" 107 | ls -la "target/${{ matrix.target }}/release/${{ matrix.binary_name }}" 108 | 109 | # Show file type and architecture 110 | file "target/${{ matrix.target }}/release/${{ matrix.binary_name }}" || true 111 | 112 | # Test the binary can be executed (for native builds only) 113 | if [[ "${{ matrix.target }}" == "x86_64-unknown-linux-gnu" ]]; then 114 | echo "Testing binary execution..." 115 | "./target/${{ matrix.target }}/release/${{ matrix.binary_name }}" --help || echo "Binary test failed, but file exists" 116 | fi 117 | else 118 | echo "Binary not found!" 119 | echo "Contents of release directory:" 120 | ls -la "target/${{ matrix.target }}/release/" || echo "Release directory doesn't exist" 121 | exit 1 122 | fi 123 | 124 | - name: Prepare binary (Unix) 125 | if: "!contains(matrix.os, 'windows')" 126 | run: | 127 | mkdir -p artifacts 128 | cp "target/${{ matrix.target }}/release/${{ matrix.binary_name }}" "artifacts/${{ matrix.binary_name }}" 129 | 130 | # Strip the binary to reduce size 131 | if command -v strip &> /dev/null; then 132 | strip "artifacts/${{ matrix.binary_name }}" || echo "Strip failed, continuing..." 133 | fi 134 | 135 | # Create tarball 136 | cd artifacts 137 | tar -czf ${{ github.event.repository.name }}-${{ matrix.target }}.tar.gz "${{ matrix.binary_name }}" 138 | 139 | # Debug: show what files were created 140 | echo "Contents of artifacts directory:" 141 | ls -la 142 | 143 | # Verify the tarball was created 144 | if [ -f "${{ github.event.repository.name }}-${{ matrix.target }}.tar.gz" ]; then 145 | echo "Tarball created successfully: ${{ github.event.repository.name }}-${{ matrix.target }}.tar.gz" 146 | else 147 | echo "ERROR: Tarball was not created!" 148 | exit 1 149 | fi 150 | 151 | rm "${{ matrix.binary_name }}" 152 | 153 | - name: Prepare binary (Windows) 154 | if: contains(matrix.os, 'windows') 155 | shell: bash 156 | run: | 157 | mkdir -p artifacts 158 | cp "target/${{ matrix.target }}/release/${{ matrix.binary_name }}" "artifacts/${{ matrix.binary_name }}" 159 | cd artifacts 160 | 7z a ${{ github.event.repository.name }}-${{ matrix.target }}.zip "${{ matrix.binary_name }}" 161 | 162 | - name: Verify artifacts before upload 163 | run: | 164 | echo "Checking for artifacts to upload..." 165 | ls -la artifacts/ || echo "artifacts directory not found" 166 | 167 | # Check for expected file patterns 168 | if ls artifacts/*.tar.gz 1> /dev/null 2>&1; then 169 | echo "Found .tar.gz files:" 170 | ls -la artifacts/*.tar.gz 171 | fi 172 | 173 | if ls artifacts/*.zip 1> /dev/null 2>&1; then 174 | echo "Found .zip files:" 175 | ls -la artifacts/*.zip 176 | fi 177 | 178 | - name: Upload artifacts 179 | uses: actions/upload-artifact@v4 180 | with: 181 | name: ${{ github.event.repository.name }}-${{ matrix.target }} 182 | path: | 183 | artifacts/*.tar.gz 184 | artifacts/*.zip 185 | if-no-files-found: error 186 | 187 | release: 188 | name: Create Release 189 | needs: build 190 | if: startsWith(github.ref, 'refs/tags/') 191 | runs-on: ubuntu-latest 192 | steps: 193 | - name: Download all artifacts 194 | uses: actions/download-artifact@v4 195 | with: 196 | path: artifacts 197 | 198 | - name: List artifacts 199 | run: | 200 | echo "Downloaded artifacts:" 201 | find artifacts -type f -name "*.tar.gz" -o -name "*.zip" | sort 202 | 203 | - name: Flatten artifacts 204 | run: | 205 | mkdir -p release-artifacts 206 | find artifacts -type f \( -name "*.tar.gz" -o -name "*.zip" \) -exec cp {} release-artifacts/ \; 207 | ls -la release-artifacts/ 208 | 209 | - name: Create Release 210 | uses: softprops/action-gh-release@v2 211 | with: 212 | files: release-artifacts/* 213 | draft: true 214 | generate_release_notes: true -------------------------------------------------------------------------------- /src/vless/vless_util.rs: -------------------------------------------------------------------------------- 1 | use std::net::{Ipv4Addr, Ipv6Addr}; 2 | use std::sync::LazyLock; 3 | 4 | use tokio::io::AsyncReadExt; 5 | 6 | use crate::address::{Address, NetLocation}; 7 | use crate::stream_reader::StreamReader; 8 | 9 | // VLESS protocol command types 10 | pub const COMMAND_TCP: u8 = 1; 11 | pub const COMMAND_UDP: u8 = 2; 12 | pub const COMMAND_MUX: u8 = 3; // Also known as XUDP - multiplexes UDP over single TCP connection 13 | 14 | pub const XTLS_VISION_FLOW: &str = "xtls-rprx-vision"; 15 | 16 | pub async fn parse_addons_from_reader( 17 | stream_reader: &mut StreamReader, 18 | stream: &mut S, 19 | addon_length: u8, 20 | ) -> std::io::Result { 21 | let addon_bytes = stream_reader 22 | .read_slice(stream, addon_length as usize) 23 | .await?; 24 | 25 | log::debug!( 26 | "Parsing addons: length={}, bytes={:?}", 27 | addon_length, 28 | addon_bytes 29 | ); 30 | 31 | // Parse protobuf-encoded addons 32 | // Format: field_tag length data [field_tag length data ...] 33 | // Field 1 = flow (string) 34 | // Field 2 = seed (bytes) 35 | 36 | let mut addon_cursor = 0; 37 | let mut flow_string = String::new(); 38 | 39 | while addon_cursor < addon_bytes.len() { 40 | // Read field tag 41 | let field_tag = addon_bytes[addon_cursor]; 42 | addon_cursor += 1; 43 | 44 | let field_number = field_tag >> 3; 45 | let wire_type = field_tag & 0x07; 46 | 47 | if wire_type != 2 { 48 | return Err(std::io::Error::other(format!( 49 | "Unexpected wire type {} for field {}", 50 | wire_type, field_number 51 | ))); 52 | } 53 | 54 | // Read length 55 | if addon_cursor >= addon_bytes.len() { 56 | return Err(std::io::Error::other( 57 | "Unexpected end of addon data reading length", 58 | )); 59 | } 60 | let (field_length, bytes_used) = read_varint(&addon_bytes[addon_cursor..])?; 61 | addon_cursor += bytes_used; 62 | 63 | // Validate field_length is within bounds 64 | if addon_cursor + field_length as usize > addon_bytes.len() { 65 | return Err(std::io::Error::other(format!( 66 | "Field {} length {} exceeds remaining addon bytes (cursor: {}, total: {})", 67 | field_number, 68 | field_length, 69 | addon_cursor, 70 | addon_bytes.len() 71 | ))); 72 | } 73 | 74 | // Read field data 75 | let field_data = &addon_bytes[addon_cursor..addon_cursor + field_length as usize]; 76 | addon_cursor += field_length as usize; 77 | 78 | match field_number { 79 | 1 => { 80 | // Flow field 81 | flow_string = std::str::from_utf8(field_data) 82 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))? 83 | .to_string(); 84 | log::debug!("Parsed flow: {}", flow_string); 85 | } 86 | 2 => { 87 | // Seed field (ignored for now) 88 | log::debug!("Parsed seed: {} bytes", field_data.len()); 89 | } 90 | _ => { 91 | log::debug!( 92 | "Unknown field {}, skipping {} bytes", 93 | field_number, 94 | field_data.len() 95 | ); 96 | } 97 | } 98 | } 99 | 100 | Ok(flow_string) 101 | } 102 | 103 | /// Parse VLESS remote location from stream 104 | pub async fn parse_remote_location_from_reader( 105 | stream_reader: &mut StreamReader, 106 | stream: &mut S, 107 | ) -> std::io::Result { 108 | let port = stream_reader.read_u16_be(stream).await?; 109 | let address_type = stream_reader.read_u8(stream).await?; 110 | 111 | match address_type { 112 | 1 => { 113 | // 4 byte ipv4 address 114 | let address_bytes = stream_reader.read_slice(stream, 4).await?; 115 | let v4addr = Ipv4Addr::new( 116 | address_bytes[0], 117 | address_bytes[1], 118 | address_bytes[2], 119 | address_bytes[3], 120 | ); 121 | Ok(NetLocation::new(Address::Ipv4(v4addr), port)) 122 | } 123 | 2 => { 124 | // domain name 125 | let domain_name_len = stream_reader.read_u8(stream).await?; 126 | let domain_name_bytes = stream_reader 127 | .read_slice(stream, domain_name_len as usize) 128 | .await?; 129 | 130 | let address_str = std::str::from_utf8(domain_name_bytes).map_err(|e| { 131 | std::io::Error::new( 132 | std::io::ErrorKind::InvalidData, 133 | format!("Failed to decode address: {e}"), 134 | ) 135 | })?; 136 | 137 | Ok(NetLocation::new(Address::from(address_str)?, port)) 138 | } 139 | 3 => { 140 | // 16 byte ipv6 address 141 | let address_bytes = stream_reader.read_slice(stream, 16).await?; 142 | let v6addr = Ipv6Addr::new( 143 | ((address_bytes[0] as u16) << 8) | (address_bytes[1] as u16), 144 | ((address_bytes[2] as u16) << 8) | (address_bytes[3] as u16), 145 | ((address_bytes[4] as u16) << 8) | (address_bytes[5] as u16), 146 | ((address_bytes[6] as u16) << 8) | (address_bytes[7] as u16), 147 | ((address_bytes[8] as u16) << 8) | (address_bytes[9] as u16), 148 | ((address_bytes[10] as u16) << 8) | (address_bytes[11] as u16), 149 | ((address_bytes[12] as u16) << 8) | (address_bytes[13] as u16), 150 | ((address_bytes[14] as u16) << 8) | (address_bytes[15] as u16), 151 | ); 152 | 153 | Ok(NetLocation::new(Address::Ipv6(v6addr), port)) 154 | } 155 | invalid_type => Err(std::io::Error::new( 156 | std::io::ErrorKind::InvalidData, 157 | format!("Invalid address type: {invalid_type}"), 158 | )), 159 | } 160 | } 161 | 162 | pub fn vision_flow_addon_data() -> &'static [u8] { 163 | static INSTANCE: LazyLock> = LazyLock::new(|| { 164 | encode_flow_addon(XTLS_VISION_FLOW) 165 | .expect("Failed to encode vision flow addon at initialization") 166 | }); 167 | &INSTANCE 168 | } 169 | 170 | fn read_varint(data: &[u8]) -> std::io::Result<(u64, usize)> { 171 | let mut cursor = 0usize; 172 | let mut length = 0u64; 173 | loop { 174 | let byte = data[cursor]; 175 | if (byte & 0b10000000) != 0 { 176 | length = (length << 8) | ((byte ^ 0b10000000) as u64); 177 | } else { 178 | length = (length << 8) | (byte as u64); 179 | return Ok((length, cursor + 1)); 180 | } 181 | if cursor == 7 || cursor == data.len() { 182 | return Err(std::io::Error::other("Varint is too long")); 183 | } 184 | cursor += 1; 185 | } 186 | } 187 | 188 | /// Encode a flow string as protobuf addon data 189 | /// Format: field_tag(0x0a) + length + data 190 | /// Field 1 = flow (string), wire type 2 (length-delimited) 191 | fn encode_flow_addon(flow: &str) -> std::io::Result> { 192 | let flow_bytes = flow.as_bytes(); 193 | let flow_len = flow_bytes.len(); 194 | 195 | if flow_len > 127 { 196 | return Err(std::io::Error::new( 197 | std::io::ErrorKind::InvalidInput, 198 | "Flow string too long for simple varint encoding", 199 | )); 200 | } 201 | 202 | let mut result = Vec::new(); 203 | 204 | // Field 1, wire type 2 (0x0a = (1 << 3) | 2) 205 | result.push(0x0a); 206 | 207 | // Length as varint (simple case: < 128) 208 | result.push(flow_len as u8); 209 | 210 | // Flow string data 211 | result.extend_from_slice(flow_bytes); 212 | 213 | Ok(result) 214 | } 215 | --------------------------------------------------------------------------------