Skip to content

Add SOCKS5 UDP ASSOCIATE; shard UDP session map (v0.5.0) - #36

Merged
guyte149 merged 1 commit into
masterfrom
feat/socks5-udp-associate-and-dashmap
May 3, 2026
Merged

Add SOCKS5 UDP ASSOCIATE; shard UDP session map (v0.5.0)#36
guyte149 merged 1 commit into
masterfrom
feat/socks5-udp-associate-and-dashmap

Conversation

@guyte149

@guyte149 guyte149 commented May 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Feature: UDP over SOCKS5 (RFC 1928 §6 UDP ASSOCIATE, CMD=0x03) for both forward (socks) and reverse (R:socks) dynamic tunnels. The SOCKS server binds a UDP relay socket, parses the per-datagram SOCKS5 UDP header on each received packet, and ferries the inner payload through a per (source, target) QUIC bi-stream that reuses the existing length-prefixed UDP framing. Replies are re-wrapped with the SOCKS5 UDP header and sent back to the original UDP source. Lifetime is tied to the TCP control connection + a 60 s idle timeout. IPv4, IPv6, and domain targets all work; fragmented datagrams (FRAG ≠ 0) are rejected, same as virtually every other SOCKS5 implementation.
  • Bonus: IPv6 SOCKS5 CONNECT (ATYP=0x04). Previously rejected with "address type not supported"; now decoded and tunneled like IPv4/domain. The pre-existing edge-case test was retargeted to use a genuinely unknown ATYP (0x05).
  • Perf/scalability: shard the UDP session map. Arc<Mutex<HashMap<SocketAddr, _>>>Arc<DashMap<SocketAddr, _>> so the UDP receive loop's per-datagram lookup doesn't serialize on a single global mutex under high pps from many sources. (Effect not visible in iperf since iperf is TCP — would require a UDP fan-in benchmark we don't have today.)
  • Tried and reverted: single-copy QUIC→TCP via RecvStream::read_chunk. Replacing the QUIC-side BufReader + copy_buf with read_chunk + write_all regressed loopback iperf throughput from 763 MB/s to 458 MB/s (-40 %): on loopback quinn returns chunks at QUIC-frame granularity, and the per-chunk syscall overhead overwhelmed the saved memcpy. Reverted; README bullet stays open with the rationale and a "revisit when quinn exposes a vectored Bytes-returning read API" note.

Net effect on loopback iperf bench (after revert): essentially flat vs. previous release — ~780 MB/s Rusnel vs ~377 MB/s Chisel, p99 0.46 ms vs 2.00 ms. PNGs in benchmark/{iperf,chisel-bench}/results/loopback/ regenerated.

Wire format

Unchanged from 0.4.0 — 0.4.0 ↔ 0.5.0 peers interop. HostPort now derives Hash (needed to key a DashMap) and RemoteRequest::dynamic_udp(target) is added as the UDP analog of dynamic_tcp for the SOCKS5 UDP relay.

Test plan

  • cargo fmt --all -- --check
  • cargo clippy --all -- -D warnings
  • cargo test --all — 41 tests pass, including 2 new integration tests:
    • test_socks5_udp_associate_forward — forward socks + IPv4 UDP echo
    • test_socks5_udp_associate_reverse — reverse R:socks + IPv4 UDP echo
    • 5 new unit tests for the SOCKS5 UDP header codec (parse + wrap roundtrip across IPv4/IPv6/domain)
  • NETEM_PROFILES=loopback ./benchmark/run.sh — loopback iperf + chisel-bench refreshed; numbers within run-to-run variance of prior PNGs

Files

  • src/common/socks.rs — UDP ASSOCIATE handler, SOCKS5 UDP header codec, IPv6 ATYP support
  • src/common/remote.rsdynamic_udp, Hash on HostPort
  • src/common/udp.rsArc<Mutex<HashMap>>Arc<DashMap> on the per-source session table; read_datagram/write_datagram made pub(crate) for reuse from socks.rs
  • tests/common/mod.rssocks5_udp_associate, socks5_udp_wrap_ipv4, socks5_udp_unwrap_ipv4 helpers
  • tests/tunnels.rs — forward + reverse UDP-over-SOCKS5 integration tests
  • tests/edge_cases.rs — retarget the "unsupported ATYP" assertion now that 0x04 (IPv6) works
  • Cargo.{toml,lock}dashmap = "6.1.0", version bumped to 0.5.0
  • CHANGELOG.md, README.md — release notes; TODO list updated
  • benchmark/{iperf,chisel-bench}/results/loopback/*.png + chisel-bench/results/wan/*.png — regenerated

Made with Cursor

Adds UDP-over-SOCKS5 (RFC 1928 §6 CMD=0x03) for both forward (`socks`) and
reverse (`R:socks`) tunnels. The SOCKS server binds a UDP relay socket,
parses the SOCKS5 UDP datagram header on every received packet, and
ferries the inner payload through a per (source, target) QUIC bi-stream
that reuses the existing length-prefixed UDP framing. Replies are
re-wrapped with the SOCKS5 UDP header and sent back to the original UDP
source. Per-session lifetime is tied to the TCP control connection plus a
60s idle timeout. IPv4, IPv6, and domain targets all work; fragmented
datagrams (FRAG ≠ 0) are rejected.

While extending the SOCKS5 handshake also accepted IPv6 ATYP=0x04 for
TCP CONNECT — previously it returned "address type not supported".

UDP forward client's per-source session table swapped from
`Arc<Mutex<HashMap<SocketAddr, _>>>` to `Arc<DashMap<SocketAddr, _>>` so
the receive loop's per-datagram lookup doesn't serialize on a single
mutex under high pps from many sources.

New integration tests cover forward + reverse UDP-over-SOCKS5 round
trips against a localhost UDP echo target. Unit tests cover the SOCKS5
UDP header codec.

Benchmarks regenerated (`./benchmark/run.sh`). Loopback iperf and
chisel-bench results are essentially unchanged from the prior PNGs —
this PR is a feature/scalability release, not a throughput release.
The `read_chunk` single-copy bullet was tried and reverted: on loopback
quinn returns small per-frame chunks and the per-chunk syscall overhead
regressed throughput by ~40 % vs the existing 256 KB BufReader+copy_buf
path. Bullet stays open in the README with that rationale.

Co-authored-by: Cursor <cursoragent@cursor.com>
@guyte149
guyte149 merged commit d6d918a into master May 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant