feat(local-tunnel): support dynamic addressing mode (0-RTT in-band relay)#2117
feat(local-tunnel): support dynamic addressing mode (0-RTT in-band relay)#2117wyzhou-com wants to merge 6 commits intoshadowsocks:masterfrom
Conversation
…lay)
The tunnel protocol previously required a fixed forward_addr at startup,
limiting it to a single static destination. This commit makes
forward_addr optional: when omitted, the tunnel reads the target address
from the head of each client stream/packet using the SOCKS5 address
encoding (ATYP + ADDR + PORT), then forwards the remaining payload
accordingly.
In essence, this is an unencrypted shadowsocks relay running locally.
The dynamic tunnel speaks the same wire format as the shadowsocks
protocol — ATYP+ADDR+PORT prefix followed by payload — but without the
encryption layer.
Wire format per connection/packet (no negotiation, no response):
TCP: [ATYP(1) ADDR(variable) PORT(2)] [payload stream...]
UDP: [ATYP(1) ADDR(variable) PORT(2)] [payload]
(response packets are prefixed with the same header)
Protocol overhead is exactly the target address itself — typically 7
bytes for IPv4 or 19 bytes for IPv6 — with zero round-trip handshake.
By comparison, a SOCKS5 session costs 2+ RTT (4 messages for
auth-request, auth-response, proxy-request, proxy-response) before the
first byte of payload can flow.
This 0-RTT property makes the tunnel ideal as a lightweight glue layer
between components that already know the destination address:
- Custom transparent proxy adapters: ss-local already ships a built-in
redir module for standard tproxy/redirect use cases, but third-party
adapters with custom functionality need a way to feed resolved
destinations into ss-local. The dynamic tunnel serves as a minimal
IPC protocol for these implementations — they prepend the address
header and connect, replacing the full SOCKS5 handshake that would
otherwise be wasted between two localhost processes.
- Chained relay topologies (relay₁ → relay₂ → ... → relayₙ → target):
each hop reads the address header once and forwards; no per-hop
handshake amplification. N hops cost O(N) address-header bytes
total, versus O(N) round-trip latencies with SOCKS5.
- forwarders: minimal code surface — a connect() plus a
single writev() of header + payload is the complete client
implementation; no state machine for multi-message negotiation.
- Programmatic proxies and service meshes: any process that can
prepend a 7-byte header to its outbound stream gains transparent
proxy-chain access, making this a drop-in building block for custom
routing layers.
Static mode (forward_addr present) is completely unchanged; the dynamic
path is only entered when forward_addr is None.
Changes:
- config.rs: relax forward_addr validation for tunnel protocol
- local/mod.rs: pass Option<Address> instead of .expect()
- tunnel/server.rs: propagate Option<Address> through builder
- tunnel/tcprelay.rs: read ATYP+ADDR+PORT from stream when dynamic
- tunnel/udprelay.rs: split into run_static/run_dynamic with
per-mode InboundWriter (response header
prepend in dynamic mode)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove redundant "read target address" suffix from TCP trace log and add matching trace log to UDP dynamic mode for consistency.
Malformed ATYP+ADDR+PORT from a dynamic TCP tunnel client was mapped to io::Error and returned, but the spawned task result is not joined, so the failure was silently dropped. Emit an error! on the failing match arm, matching the UDP dynamic path which already logs invalid packets.
The DOMAIN branch of `Address::read_cursor()` only checked `remaining() < domain_len`, then unconditionally called `cur.get_u16()` to read the 2-byte PORT. When the input ends exactly after the domain bytes (e.g. `03 03 61 62 63` — ATYP=DOMAIN, LEN=3, "abc", no PORT), `get_u16()` panics on buffer underflow instead of returning `Err`. Extend the check to `domain_len + 2` so PORT is validated before it is read, matching the IPv4 / IPv6 branches that already check `4 + 2` and `16 + 2` respectively. This is not limited to the dynamic UDP tunnel that surfaced the bug: `Address::read_cursor()` is also called on every inbound UDP packet from the core relay decoders — AEAD (`udprelay/aead.rs`), AEAD-2022 (`udprelay/aead_2022.rs`), stream ciphers (`udprelay/stream.rs`), and `udprelay/crypto_io.rs`. A crafted packet with a truncated DOMAIN header in any of those paths would abort the UDP task instead of being rejected as malformed. Add an integration test `udp_dynamic_tunnel` in `tests/tunnel.rs` that first sends the truncated `03 03 61 62 63` packet and then a valid IPv4-targeted dynamic packet, asserting the echo reply still comes back — i.e. the listener survived the bad packet. Also add `tcp_dynamic_tunnel` as a happy-path counterpart, styled consistently with the existing `tcp_tunnel` / `udp_tunnel` tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Restore the public TunnelBuilder static constructors to accept Address instead of Option<Address>, and add explicit dynamic constructors for the new in-band addressing mode. This keeps existing callers source-compatible while still allowing dynamic tunnels to be created intentionally. Also clean up the tunnel relay implementation: - route config construction through static or dynamic builder entry points - keep Option<Address> as an internal implementation detail - normalize IPv4-mapped IPv6 addresses in dynamic UDP responses, matching the SOCKS5 UDP writer behavior - share the UDP receive/association loop between static and dynamic modes while keeping mode-specific packet parsing and response writers separate - remove redundant explanatory comments added around dynamic mode This avoids an API break from the dynamic tunnel feature, keeps UDP response address encoding consistent with existing relay behavior, and reduces duplicate UDP loop logic without changing static tunnel semantics.
|
Requesting review for the latest commit 089798b: API Compatibility & Refactoring Logic Consolidation: Unified UDP inbound logic using a generic run_with_packet_parser function. This eliminates code duplication between static and dynamic forwarding modes. Address Standardisation: Implemented IPv4-mapped IPv6 normalization for UDP responses to ensure consistency with existing SOCKS5 logic. Status |
No description provided.