fix(reflect): drop LAN/private reflex addrs from NAT classification
Real-world report: a user with one LAN relay + one internet relay got "Multiple IPs — treating as symmetric" because the LAN relay saw the client's LAN IP (172.16.81.172) while the internet relay saw the WAN IP (150.228.49.65). Two observations of "different public IPs" from the classifier's perspective, but semantically they describe two different network paths and shouldn't be compared. The LAN relay's reflection is always true, just not useful for public NAT classification: there's no NAT between the client and the LAN relay, so that path's reflex addr is always the LAN interface IP regardless of what the public-facing NAT beyond it looks like. Fix: new `is_private_or_loopback` helper filters the probe set before classification. Drops: - 127.0.0.0/8 loopback - 10/8, 172.16/12, 192.168/16 RFC1918 private - 169.254/16 link-local - 100.64/10 CGNAT shared-transition (same reasoning: a relay that sees the client with a CGNAT addr is on the same carrier network and can't describe public NAT state) - IPv6 loopback, unspecified, fe80::/10 link-local Failed probes still filtered out of classification (they were already) but now dimmed in the UI list instead of highlighted amber. Same rationale: a momentarily-offline probe target isn't a warning-worthy state, it's just a fact about the probe run. UI palette rebalance: only Cone gets green, everything else neutral text-dim. Wording changed from warning-tone "⚠ must use relay" to informational "ℹ P2P falls back to relay, calls still work" — symmetric NAT isn't broken state, it just means media takes the relay path. Tests added (4 new in wzp_client::reflect): - classify_drops_private_ip_probes — LAN + public → Unknown - classify_drops_loopback_probes — loopback + 2 public → Cone - classify_drops_cgnat_probes — CGNAT + 2 public same-IP- diff-port → SymmetricPort - classify_two_lan_probes_is_unknown_not_cone — all LAN → Unknown Existing multi_reflect integration test updated: two loopback relays now correctly classify as Unknown (because loopback reflex addrs are filtered) with the plumbing-works invariant preserved. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -826,9 +826,18 @@ settingsBtnCall.addEventListener("click", openSettings);
|
||||
// shows its working state inline so the user knows it's waiting on
|
||||
// the relay rather than the network.
|
||||
// Phase 2 multi-relay NAT type detection. Probes every configured
|
||||
// relay in parallel through transient QUIC connections and
|
||||
// classifies the result. Green = Cone (P2P viable),
|
||||
// amber = SymmetricPort (must relay), gray = Multiple / Unknown.
|
||||
// relay in parallel and classifies the result.
|
||||
//
|
||||
// Cone = P2P direct path viable, green cue
|
||||
// SymmetricPort = per-destination port mapping, informational
|
||||
// (P2P will fall back to relay but calls still work)
|
||||
// Multiple = classifier saw different public IPs; informational
|
||||
// Unknown = not enough public probes, neutral
|
||||
//
|
||||
// The classifier drops LAN / private / CGNAT reflex addrs before
|
||||
// deciding, so a mixed "LAN relay + internet relay" setup does NOT
|
||||
// falsely flag as symmetric. Failed probes are shown in the list
|
||||
// for transparency but dimmed, not highlighted.
|
||||
sNatDetectBtn.addEventListener("click", async () => {
|
||||
const s = loadSettings();
|
||||
if (!s.relays || s.relays.length === 0) {
|
||||
@@ -859,17 +868,18 @@ sNatDetectBtn.addEventListener("click", async () => {
|
||||
detection.nat_type === "Cone"
|
||||
? `✓ Cone NAT — P2P viable (${detection.consensus_addr})`
|
||||
: detection.nat_type === "SymmetricPort"
|
||||
? "⚠ Symmetric NAT — must use relay"
|
||||
? "ℹ Symmetric NAT — P2P falls back to relay, calls still work"
|
||||
: detection.nat_type === "Multiple"
|
||||
? "⚠ Multiple IPs — treating as symmetric"
|
||||
: "? Unknown (not enough successful probes)";
|
||||
? "ℹ Multiple public IPs observed"
|
||||
: "? Unknown (not enough public probes)";
|
||||
|
||||
// Only Cone is "good news green". Everything else is neutral
|
||||
// informational — the user has configured relays so any
|
||||
// classification result just describes their network; none
|
||||
// are "wrong" per se.
|
||||
const verdictColor =
|
||||
detection.nat_type === "Cone"
|
||||
? "var(--green)"
|
||||
: detection.nat_type === "SymmetricPort" ||
|
||||
detection.nat_type === "Multiple"
|
||||
? "var(--yellow)"
|
||||
: "var(--text-dim)";
|
||||
|
||||
sNatType.textContent = verdictLabel;
|
||||
@@ -882,7 +892,10 @@ sNatDetectBtn.addEventListener("click", async () => {
|
||||
p.relay_addr
|
||||
)}) → ${escapeHtml(p.observed_addr)} [${p.latency_ms ?? "?"}ms]</div>`;
|
||||
} else {
|
||||
return `<div style="color:var(--yellow)">• ${escapeHtml(
|
||||
// Failed probes are dimmed, not highlighted — the classifier
|
||||
// already ignores them, and the user doesn't need to be
|
||||
// alarmed by a momentarily-offline relay.
|
||||
return `<div style="color:var(--text-dim);opacity:0.7">• ${escapeHtml(
|
||||
p.relay_name
|
||||
)} (${escapeHtml(p.relay_addr)}) → ${escapeHtml(
|
||||
p.error ?? "probe failed"
|
||||
|
||||
Reference in New Issue
Block a user