Skip to content

docs(forwarder): document the subnet-route SSRF-guard scoping + lock the SOCKS5 reply ATYP reject#252

Merged
GeiserX merged 2 commits into
mainfrom
docs/forwarder-subnet-ssrf-scope-note
Jun 15, 2026
Merged

docs(forwarder): document the subnet-route SSRF-guard scoping + lock the SOCKS5 reply ATYP reject#252
GeiserX merged 2 commits into
mainfrom
docs/forwarder-subnet-ssrf-scope-note

Conversation

@GeiserX

@GeiserX GeiserX commented Jun 15, 2026

Copy link
Copy Markdown
Owner

What

Two related hardening items in the exit-egress forwarder (no behavior change):

  1. Deployer note (AGENTS.md): the exit-egress SSRF guard (exit_dst_is_forbidden) is scoped to ExitNode-class flows by design. A Subnet flow (a destination covered by a narrower advertised route) skips it, because a subnet route legitimately targets a private range — that range is the routed subnet. This is not an attacker-controllable SSRF (the flow class derives from the operator's own advertised routes, never the peer), but a deployer who advertises a sensitive internal range as a forwarded subnet route will have the forwarder dial into it. The note warns operators not to advertise link-local (169.254/16), the tailnet CGNAT range (100.64/10), or loopback as forwardable subnets. (0.0.0.0/0 as an exit node stays behind the guard.)

  2. Adversarial test (ts_forwarder/src/dialer.rs): lock the SOCKS5 connect-reply parser's reject-before-unbounded-read. A hostile reply with a non-IPv4 bound-address type (ATYP=0x03 domain + an attacker-chosen 0xFF length) is rejected at the head[3] != 0x01 IPv4-only guard before any variable-length read, so a malicious proxy can't drive a length-driven/oversized read. The parser was already correct; the test pins it.

Why the test is strong (per security review)

The fake proxy sends [0x05,0x00,0x00,0x03,0xff] (REP=success, ATYP=domain, len=255) then nothing. The assertion is discriminating: the ATYP guard returns ProxyHandshake explicitly, while every read_exact maps EOF/io errors to DialError::Io (not ProxyHandshake) — so a regression that tried to read the (never-sent) domain would surface Io(EOF) and fail the matches!(ProxyHandshake) assertion. A tokio::time::timeout(100ms) wrapper adds a timing bound so "reject before the read" is also proven independent of the error-type mapping (belt-and-suspenders).

Verification

Docs + test only; git diff --stat shows AGENTS.md + dialer.rs mod tests additions, 0 production-code deletions. cargo test -p geiserx_ts_forwarder (32 dialer + the rest), clippy -D warnings (0), fmt --check, cargo run -p checks (anti-leak guard) all green.

Created using Claude Code (Opus 4.8)

GeiserX added 2 commits June 15, 2026 07:49
…the SOCKS5 reply ATYP reject

The exit-egress SSRF guard (exit_dst_is_forbidden) gates only ExitNode-class
flows by design: a Subnet flow (a destination covered by a narrower advertised
route) skips it, because a subnet route legitimately targets a private range --
that range IS the routed subnet. This is NOT an attacker-controllable SSRF (the
flow class derives from the operator's own advertised_routes, never the peer),
but a deployer who advertises a sensitive internal range as a forwarded subnet
route WILL have the forwarder dial into it. Add a deployer note to AGENTS.md so
operators don't advertise link-local / CGNAT / loopback as forwardable subnets.

Also lock, with an adversarial test, the SOCKS5 connect-reply parser's
reject-before-unbounded-read: a hostile reply with a non-IPv4 bound-address type
(ATYP=0x03 domain + an attacker-chosen 0xFF length) is rejected at the
`head[3] != 0x01` IPv4-only guard BEFORE any variable-length read, so a
malicious proxy can't drive an oversized/length-driven read. The parser was
already correct; the test pins it against a future refactor that might add an
ATYP-dispatched read.

Docs + test only; no behavior change. cargo test -p ts_forwarder (32 dialer +
the rest), clippy -D warnings clean, fmt, cargo run -p checks all green.

Signed-off-by: Sergio <sergio@geiser.cloud>
… bound)

Belt-and-suspenders per review: wrap the dial in a 100ms timeout so 'reject
before the variable-length read' is proven by timing, independent of the
Io-vs-ProxyHandshake error-type discrimination — a future refactor collapsing
both into ProxyHandshake would otherwise weaken the assertion.

Signed-off-by: Sergio <sergio@geiser.cloud>
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@GeiserX, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 44 minutes and 29 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 43bca5b8-b629-4e5b-9ea5-0d53bf33e092

📥 Commits

Reviewing files that changed from the base of the PR and between a2aa337 and a914ddf.

📒 Files selected for processing (2)
  • AGENTS.md
  • ts_forwarder/src/dialer.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/forwarder-subnet-ssrf-scope-note

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@GeiserX GeiserX merged commit 2ad9862 into main Jun 15, 2026
13 of 19 checks passed
GeiserX pushed a commit that referenced this pull request Jun 15, 2026
Release 0.39.0. Bundles the parity + anti-leak batch: magicsock CallMeMaybe immediate-ping (#246), peerless-STUN-stop (#247), STUN SOFTWARE+FINGERPRINT (#241); MagicDNS RD/RA+compression (#242) + SERVFAIL-not-NXDOMAIN (#248); DERP send rate-limit (#249); IPv4 fragment handling (#253); control Hostinfo/ProtoPortRange/NetInfo wire fixes (#244/#245); netcheck StunProber deletion (#250); forwarder subnet-SSRF doc+test (#252); control panic-hardening (#254).
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