From fec374abf59f0a64fe00cfef7b3ea88ac92f0f03 Mon Sep 17 00:00:00 2001 From: mrambossek Date: Sat, 1 Aug 2026 20:16:37 +0200 Subject: [PATCH] findings: v6.broken claimed a cause it had no evidence for A phone reported "IPv6 is configured but not working" while loading an IPv6-only site over TCP perfectly well. The finding fired on one signal - ICMPv6 echo getting no reply - at HIGH confidence. ICMPv6 echo is widely filtered on networks where IPv6 works, so the two cases are indistinguishable from where the app stands, and it was picking one. Same class of error as the multi-homed downstream-loss bug: a confident measurement of something that was not happening. Now v6.no_icmp_reply, low severity, medium confidence, naming both explanations. Still reported, because filtered ICMPv6 breaks Path MTU Discovery - large packets vanish instead of being reported as too big - which is a fault in its own right. Corroborating with a real IPv6 connection would separate the two properly, but needs a target, which runs into the hardcoded-deployment issue already open. Co-Authored-By: Claude Fable 5 --- docs/build-status.md | 26 +++++++++++++++++++ docs/findings-registry.md | 9 ++++++- .../kotlin/app/echo_lot/app/RunViewModel.kt | 10 +++---- .../echo_lot/measurement/FindingRegistry.kt | 22 ++++++++++++---- 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/docs/build-status.md b/docs/build-status.md index 671c248..af782a6 100644 --- a/docs/build-status.md +++ b/docs/build-status.md @@ -1082,3 +1082,29 @@ so a user with no server of their own still sends DNS and STUN traffic to fmr wi For a tool that goes to this much trouble over what leaves the device, an undisclosed dependency on a third party's infrastructure is the wrong default. It should prefer the configured server, and be explicit when there is none. **Open.** + +### v6.broken was a false positive waiting to happen (2026-08-01) +A phone could not open `https://fmr.echo-lot.app` while loading the same server by IP literal +perfectly well. Two things came out of chasing it. + +**The admin UI is IPv6-only, by consequence rather than intent.** `fmr.echo-lot.app` has an AAAA +and no A record — verified identical at Cloudflare, Google and Quad9, so DNS itself is healthy. +That follows from reserving all four measurement addresses for testing, which left only `::2` for +management, and `::2` has no IPv4 counterpart. Any client without working IPv6 sees an unreachable +admin interface — a poor property for the interface you reach *from the networks you are debugging*. + +**And the app's own `v6.broken` finding was unsound.** It fired on exactly one signal — ICMPv6 echo +getting no reply — with `Confidence.HIGH`. ICMPv6 echo is widely filtered on networks where IPv6 +works fine, which is precisely what that phone demonstrated: no ICMPv6 replies, working IPv6 TCP. +The finding asserted a cause it had no evidence for, which is the same class of error as the +multi-homed `100 % downstream loss` earlier: a confident measurement of something that was not +happening. + +Now `v6.no_icmp_reply`, severity low, confidence medium, and the text names *both* explanations +instead of choosing one. It is still worth reporting, because filtered ICMPv6 breaks Path MTU +Discovery — large packets vanish rather than being reported as too big — which is a real fault even +when IPv6 works. + +The proper fix is corroboration: attempt a real IPv6 connection and only call it broken when that +fails too. That needs a target, which runs into the hardcoded-reference-deployment issue already +open above. **Both remain open.** diff --git a/docs/findings-registry.md b/docs/findings-registry.md index 68bc3d1..bb8afa0 100644 --- a/docs/findings-registry.md +++ b/docs/findings-registry.md @@ -89,9 +89,16 @@ rolled up under *connectivity* instead — the third occurrence of rule 1 being | code | severity | means | rules out | |---|---|---|---| -| `v6.broken` | medium | IPv6 is configured on this network but does not work. | Absence of IPv6: it is provisioned, it simply fails. | +| `v6.no_icmp_reply` | low | IPv6 is configured but ICMPv6 echo gets no reply. | Nothing on its own: IPv6 may work fine with ICMP filtered. | | `v6.not_offered` | info | This network does not offer IPv6. | — | +`v6.no_icmp_reply` was `v6.broken` until a phone reported it while loading an IPv6-only site over +TCP perfectly well. The only evidence behind it is ICMPv6 echo, which is widely filtered on +networks where IPv6 works — so the finding now states what was observed and names both +explanations instead of choosing one. It is still worth reporting: filtered ICMPv6 breaks Path MTU +Discovery. Corroborating it with a real IPv6 connection would let the two cases be separated, and +is the proper fix. + `v6.not_offered` is **info and must stay info**. Most networks still do not offer IPv6 and that is not a fault; reporting it as a warning lights a yellow verdict on a healthy network, which teaches people to ignore the light — the one thing a diagnostic must never do. diff --git a/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunViewModel.kt b/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunViewModel.kt index b60040f..c61e216 100644 --- a/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunViewModel.kt +++ b/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunViewModel.kt @@ -479,11 +479,11 @@ class RunViewModel(app: Application) : AndroidViewModel(app) { if (ipv6Provisioned(networks)) { out.add( Finding( - id = ids.uuid(), code = FindingRegistry.V6_BROKEN.code, - category = FindingRegistry.V6_BROKEN.category, - severity = FindingRegistry.V6_BROKEN.severity, confidence = Confidence.HIGH, - title = "IPv6 is configured but not working", - description = "This network advertises IPv6 (a global address and/or a default route), but ICMPv6 got no reply on any network. Half-configured IPv6 is worse than none: connections try IPv6 first and stall before falling back.", + id = ids.uuid(), code = FindingRegistry.V6_NO_ICMP_REPLY.code, + category = FindingRegistry.V6_NO_ICMP_REPLY.category, + severity = FindingRegistry.V6_NO_ICMP_REPLY.severity, confidence = Confidence.MEDIUM, + title = "IPv6 is configured, but ICMPv6 gets no reply", + description = "This network advertises IPv6 (a global address and/or a default route), but ICMPv6 echo got no reply on any network. That has two explanations which look identical from here: IPv6 is broken, or ICMPv6 is filtered while IPv6 itself works. Filtering is common and is a fault in its own right — it breaks Path MTU Discovery, so large packets vanish rather than being reported as too big.", evidenceRefs = listOf(EvidenceRef(t.id)), ) ) diff --git a/echolot-app/core-measurement/src/main/kotlin/app/echo_lot/measurement/FindingRegistry.kt b/echolot-app/core-measurement/src/main/kotlin/app/echo_lot/measurement/FindingRegistry.kt index 00db4aa..8b55782 100644 --- a/echolot-app/core-measurement/src/main/kotlin/app/echo_lot/measurement/FindingRegistry.kt +++ b/echolot-app/core-measurement/src/main/kotlin/app/echo_lot/measurement/FindingRegistry.kt @@ -169,10 +169,22 @@ object FindingRegistry { // they silently rolled up under connectivity: the third instance of a prefix disagreeing with // its category and quietly moving a fault to a different verdict light. - val V6_BROKEN = FindingSpec( - "v6.broken", Category.IPV6, Severity.MEDIUM, - "IPv6 is configured on this network but does not work.", - rulesOut = "Absence of IPv6: it is provisioned, it simply fails.", + /** + * Renamed from `v6.broken`, which claimed more than the evidence supports. + * + * The only signal behind it is ICMPv6 echo getting no reply — and ICMPv6 echo is widely + * filtered on networks where IPv6 otherwise works perfectly. A phone that reported this while + * happily loading an IPv6-only site over TCP is what caught it. From here the two cases look + * identical, so the finding now says what was observed and names both explanations rather than + * picking one. + * + * It is worth reporting either way: filtered ICMPv6 breaks Path MTU Discovery, which is its + * own fault even when IPv6 works. + */ + val V6_NO_ICMP_REPLY = FindingSpec( + "v6.no_icmp_reply", Category.IPV6, Severity.LOW, + "IPv6 is configured but ICMPv6 echo gets no reply.", + rulesOut = "Nothing on its own: IPv6 may work fine with ICMP filtered.", ) /** @@ -196,7 +208,7 @@ object FindingRegistry { NAT_UDP_REBINDING, NAT_SYMMETRIC, THROUGHPUT_NO_DELIVERY, THROUGHPUT_BELOW_OFFERED, DNS_ANSWER_REWRITTEN, DNS_AUTHORITATIVE_UNREACHABLE, - V6_BROKEN, V6_NOT_OFFERED, + V6_NO_ICMP_REPLY, V6_NOT_OFFERED, ) private val byCode: Map = all.associateBy { it.code }