fix(start-os): align PCP and UPnP port-check behavior behind a second NAT - #3840
Merged
Conversation
dr-bonez
requested changes
Aug 26, 2026
dr-bonez
left a comment
Member
There was a problem hiding this comment.
wrong direction. we want to limit use of external echoip services where possible. sounds like we just need to fix the mismatch between upnp and pcp behavior when it comes to double-nat
Dominion5254
marked this pull request as draft
September 1, 2026 19:13
…rotocols Fixes #3784. `check_port` short-circuited to "open" on the port-map client's record of its last grant, and the two protocols disagreed about what the address in that record had to be. PCP reported whatever the gateway returned, so behind a second NAT a private address passed the check and nothing outside could connect. UPnP discarded a non-routable address at record time and fell through to the echo probe, so the same topology answered red. Same reachability, opposite verdicts by protocol. The shard's `ExternalIp` handler is where both protocols' addresses converge, so the filter moves there and applies to whichever protocol reported it. v6 passes through untouched: the GUA is the box's own address and `check_gua_port` never reads the mapping's. No new echoip traffic beyond what a UPnP gateway already generates on this topology: the fall-through is the path UPnP has always taken, and PCP now joins it only where the gateway's own address rules out a direct answer. Claude-Session: https://claude.ai/code/session_01RwXsV4feB6gSVXTedUrzxT
`Command::ExternalIp`'s body was inline in the shard's select loop, so nothing could reach it without spawning a shard against real interfaces. Extract it as `external_ip_of`, with the per-protocol match funnelling through one `routable_external_ip`. The UPnP arm and the key selection (right port, TCP only) are covered directly. The PCP arm is not constructible here — `crab_nat::PortMapping` has private fields and no public constructor — so it reaches the filter by construction: `external_ip_of` is the one call site and it wraps both arms. A live check is what proves a real PCP gateway behind a second NAT reports the private address this rejects. Claude-Session: https://claude.ai/code/session_018ZmVQMf744jaqpKUm5gthK
Dominion5254
force-pushed
the
os/port-check-probes-reachability
branch
from
September 1, 2026 20:05
4a2a1dc to
8178ae7
Compare
Dominion5254
marked this pull request as ready for review
September 1, 2026 20:56
dr-bonez
previously approved these changes
Sep 2, 2026
dr-bonez
approved these changes
Sep 2, 2026
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.
(Replaces the previous description, which described a wider rework: always-probe, mapping re-assertion, an RPC/sync-task ordering wait, and an
Option<bool>tri-state. Per review — "we want to limit use of external echoip services where possible; sounds like we just need to fix the mismatch between upnp and pcp behavior when it comes to double-nat" — this is now the mismatch alone.)What was wrong
check_portshort-circuits to "open" whenever the port-map client holds a mapping for the port, and the two protocols disagreed about what the address in that record had to be:Same reachability, opposite verdicts by protocol — the issue as filed.
What this does
The shard's
Command::ExternalIphandler (port_map/client.rs) is where both protocols' addresses converge. The routability filter moves there, so it applies to whichever protocol reported the address. A gateway with no public address of its own falls through to the probe on both protocols, which is what UPnP already did.v6 passes through untouched — the GUA is the box's own address, and
check_gua_portonly tests that a pinhole exists, never reading its address.gateway.rsandupnp.rsare untouched;is_wan_candidateis the filter UPnP already used, applied unchanged.On echoip usage
No new echoip traffic beyond what a UPnP gateway already generates on this topology. The fall-through is the path UPnP has always taken; PCP now joins it only where the gateway's own reported address rules out a direct answer. The only alternative that would cut echoip further is answering
falseoutright on a non-routable address — but that is wrong when the outer router carries a manual forward, and it would push a user whose port already works into the manual-forward modal.Verified
cargo test -p start-core --features=test port_map: 80 passed.cargo fmt --check: clean.Command::ExternalIp's body was inline in the shard's select loop and unreachable from a test; it is extracted asexternal_ip_of, with both protocol arms funnelling through oneroutable_external_ip. New tests cover the UPnP arm, key selection (right port, TCP only), a v6 pinhole passing through unfiltered, an addressless NAT-PMP grant, and the filter over private and public v4.crab_nat::PortMappinghas private fields and no public constructor — so it reaches the filter by construction:external_ip_ofis the single call site and wraps both arms.check_portreports the port not open. That is the premise of Port Forwarding check flips pass/fail behind double NAT depending on the granting protocol — PCP fast path trusts a private external IP, UPnP-held mappings discard it and probe the true public IP (net/gateway.rs:714) #3784 confirmed — a real PCP gateway behind a second NAT does report a private external address — and the verdict this changes. Master's green on the same setup is by inspection of the unfiltered short-circuit, not an A/B run.check_portis byte-identical to master and the only difference is oneis_wan_candidatecall on the reported address.a_routable_address_is_reported_as_givenpins that public addresses pass, anda_upnp_mapping_answers_only_for_its_own_tcp_portexercises it end to end throughexternal_ip_of— the functioncheck_portcalls.upnp.rs:109), and this adds a second, idempotent filter at read time.Not in this PR
Real but separate; each wants its own issue:
is_wan_candidatedoes not reject CGNAT (100.64.0.0/10) despite its doc comment saying it does, so a gateway holding a carrier-assigned address passes the shortcut on both protocols. Left out here because the same function feeds the gateway poll's WAN-IP discovery, and rejecting it there changes what the box shows as its public IP and when it asks echoip.Fixes #3784
https://claude.ai/code/session_01RwXsV4feB6gSVXTedUrzxT