Repository navigation
feat(portmapper): add ts_portmapper, a NAT-PMP and PCP client - #643
Merged
Merged
Conversation
Add the ts_portmapper crate's wire layer, modelled on Go's net/portmapper: - strict NAT-PMP (RFC 6886) and PCP (RFC 6887) codecs; - default-gateway lookup from the macOS routing socket and Linux /proc/net/route, validated as private and on-link; - GatewayCap, the only code that sends to the LAN: sockets bound to the host's own address and, where the OS allows it, its interface, gateway:5351 as the only destination, and a policy checked before every send; - a one-shot probe, map and delete, and a probe tool that maps a test port and reports what the gateway supports; - behind the test-harness feature, a loopback fake NAT-PMP/PCP router with its own RFC decoder, and TestCap, a loopback transport. Signed-off-by: Kasper Nielsen <kasper0406@gmail.com> Co-Authored-By: GPT-6.1 (codex) <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
⛔ Files ignored due to path filters: 2
📒 Files selected for processing: 41
✨ Finishing Touches
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. Comment |
Add the port mapper's lifecycle client: create, renew, expire and release a mapping, with generation-guarded transitions and Go's retry, renewal and probe-epoch rules. A bounded ledger of four rules tracks every rule the router may hold for us, including unacknowledged deletes and rules on networks that went away. A slot is freed only by an acknowledged delete or proven router expiry. One private owner settles every transition (see the table in docs/PORTMAPPER.md). Shutdown and Drop release with cached capabilities, without a runtime and without fresh OS discovery. Tests run against the fake router. An independent ownership model checks conservation in the shared mapping and settling helpers, and router-inventory checks compare the ledger with the fake router's rule table in the cancellation, cleanup and reset scenarios, including every MAP and cleanup cancellation point. Signed-off-by: Kasper Nielsen <kasper0406@gmail.com> Co-Authored-By: GPT-6.1 (codex) <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cargo-fuzz crate with the same layout as ts_tka/fuzz, kept out of the workspace: NAT-PMP and PCP replies (including the MAP validators) and both routing-table parsers (the macOS routing-socket dump and Linux /proc/net/route). Signed-off-by: Kasper Nielsen <kasper0406@gmail.com> Co-Authored-By: GPT-6.1 (codex) <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tripwire for what the compiler cannot check. ts_portmapper's dependency closure has no TLS or HTTP crate, ts_control does not depend on it, and only dev-dependencies may enable its test-harness feature. Outside cap.rs and the test harness, no line of ts_portmapper/src may name a socket type, a send or connect call, a raw fd, a child process or the IPv6 wildcard. The kernel source-address lookup and a macOS netstat cross-check test are allowlisted by function name. Signed-off-by: Kasper Nielsen <kasper0406@gmail.com> Co-Authored-By: GPT-6.1 (codex) <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kasper0406
force-pushed
the
feat/portmapper-1-crate
branch
from
October 18, 2026 13:07
2065570 to
624278d
Compare
…ifetime Drop the 30 min NOT_AUTHORIZED hold (Go retries on the next ReSTUN), run the epoch-rollback check before recording the fresh probe answer as Go does, and bound an unanswered MAP's possible grant to the requested 7200 s so four left networks cannot stop port mapping for the process. State the remaining deliberate differences from Go v1.102.4 in docs/PORTMAPPER.md and correct the notification and fuzzer provenance claims.
Two tests pinned the old rule that an unanswered MAP holds its slot for ever. Now its slot frees after the requested lifetime from the last send; network loss alone still retires nothing.
# Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is PR 1 of 3 adding Go's NAT port mapper (
net/portmapper), for NAT-PMP and PCP. With an inbound mapping on the LAN gateway, a peer behind a hard NAT can reach this node directly instead of over DERP. This PR adds only the crate. Nothing in the node uses it yet.ts_portmappercrate.--portmap-probe. Depends on feat(runtime): map the underlay port with NAT-PMP or PCP, as Go does #644.What it adds
pmp,pcp): strict RFC 6886 and RFC 6887 encoders and parsers.gateway): the default route's gateway, read from the macOS routing socket or Linux/proc/net/route. It must be private (RFC 1918) and on-link. Other OSes getUnsupported.GatewayCap, the only code that sends to the LAN. Sockets are bound to the host's own address and, on macOS and on Linux with permission forSO_BINDTODEVICE, to its interface. Without interface binding, the route is checked again before each send. The only destination isgateway:5351, and a policy is checked before every send. No API takes an arbitrary address. The gateway lookup's kernel source-address check connects a UDP socket but never sends on it.oneshot,probe_tool): single probe, map and delete exchanges, and a probe that maps a test port and reports what the gateway supports. PR 3 exposes the probe as--portmap-probe.client): creates, renews, expires and releases the mapping. One private owner settles every transition; the table is indocs/PORTMAPPER.md.test-harness, dev-dependencies only): a loopback fake NAT-PMP/PCP router with its own RFC decoder, andTestCap.ts_portmapper/fuzz(outside the workspace, likets_tka/fuzz).checkstripwire,portmapper_lan_only. It requires:ts_control;test-harnessenabled only from dev-dependencies;cap.rsand the harness.It adds no new crate to the lockfile. Like the other published crates, it gets an
audit-as-crates-io = falsepolicy insupply-chain/config.toml.Mapping to Go's
net/portmapperSame as Go:
preferPCP;Go's tests are ported where they don't need a live router:
TestProbeIntegration(the PMP/PCP half),TestPCPIntegration,TestParsePCPMapResponse,TestUpdateEvent, the PCP fuzzers (upstream Go'snet/portmapper/fuzz_test.go, on tailscale main after v1.102.4), and netmon'sTestLikelyHomeRouterIP*and routing-table tests.Deliberate differences (full list in
docs/PORTMAPPER.md):SO_BINDTODEVICEpermission), the route is checked again before every send. Release at shutdown and onDropthen fails closed, and the rules are left to expire.gateway:5351, with every field validated. Go's probe checks only the source port.::ffff:0.0.0.0; Go sends the current external address.tsr-x4yg).Testing
cargo test -p geiserx_ts_portmapper: 244 unit tests and 3 doctests. One doctest checks that the test transport does not exist without the feature. All packets go to loopback fake routers. The only real-system reads are the routing table and the interface list.cargo +1.95.0, all passing:-D warnings: all targets, the CI lib/other/tunvariants, all features, andbin/check-windows(x86_64-pc-windows-msvc via cargo-xwin);cargo run -p checks;cargo docwith-D warnings;x86_64-unknown-linux-gnu;cargo vet --locked,cargo deny(all features, and the no-default-features lane) andcargo machete. Machete's one finding,bytesints_forwarder, is already onmain.SO_BINDTODEVICEis compiled and linted for Linux.Created using Claude Code
The code was written and reviewed with GPT-6.1 (Codex); the split, polish and gates were done with Claude Code.
🤖 Generated with Claude Code