docs: record frag_send and the current testing state
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a7dccf7da2
commit
35744c609e
@@ -728,3 +728,49 @@ tenfold asymmetry that a round-trip measurement cannot see at all.
|
|||||||
|
|
||||||
10 unit tests on the arithmetic (a wrong denominator here does not crash, it produces a plausible
|
10 unit tests on the arithmetic (a wrong denominator here does not crash, it produces a plausible
|
||||||
number pointing at the wrong half of the network) plus the live correlation check.
|
number pointing at the wrong half of the network) plus the live correlation check.
|
||||||
|
|
||||||
|
### frag_send: crafted IP fragments, so *ordering* is testable (server-v0.6.0, 2026-08-01)
|
||||||
|
`big_send` with `df=false` answers one question — do fragments get through. It cannot answer the
|
||||||
|
more interesting one, because the kernel always emits fragments in order, first one first.
|
||||||
|
|
||||||
|
The classic middlebox fault is exactly about that ordering. Only the **first** fragment carries the
|
||||||
|
UDP header, and therefore the ports; a stateful firewall or NAT that has not seen it has no flow to
|
||||||
|
match the rest against, and many simply drop them. That is invisible to every in-order test, and in
|
||||||
|
the field it looks like "large DNS answers fail on this network" or "the tunnel breaks when the MTU
|
||||||
|
drops" — it works until the network reorders, then fails intermittently, which is the hardest kind
|
||||||
|
of fault to chase.
|
||||||
|
|
||||||
|
So the server builds the fragments itself (raw socket, `IP_HDRINCL`) and controls their order:
|
||||||
|
`in_order` (baseline), `reversed` (last fragment first), `first_last` (first fragment held back
|
||||||
|
250 ms). The datagram is assembled and **signed whole** before being cut up, so what the client
|
||||||
|
reassembles is indistinguishable from an ordinary packet — otherwise the test would be measuring
|
||||||
|
our sender rather than the path. New test type `mtu.frag_ordering`; findings
|
||||||
|
`mtu.fragments_blocked` and `mtu.fragment_reorder_sensitive`.
|
||||||
|
|
||||||
|
Two details that would otherwise produce confidently wrong answers:
|
||||||
|
- **The UDP checksum is computed, not left zero.** Zero is legal in IPv4 and would be less code,
|
||||||
|
but zero-checksum datagrams are dropped by some middleboxes — and that drop would be recorded as
|
||||||
|
a fragmentation failure, which is the wrong conclusion entirely.
|
||||||
|
- **Fragment offsets are in 8-byte units**, so non-final fragments are rounded down to a multiple
|
||||||
|
of 8. A 100-byte fragment is not an error; it is a datagram no host will ever reassemble.
|
||||||
|
|
||||||
|
`frag-send` is advertised only when a raw socket can actually be opened — checked by opening one,
|
||||||
|
because a permission model has more ways to say no (userns, seccomp, LSM) than a capability bit has
|
||||||
|
to say yes. fmr runs as root with `cap_net_raw` in its bounding set, so it is available there.
|
||||||
|
|
||||||
|
Fragment ordering runs only after `mtu.frag_delivery` shows fragments arrive at all; otherwise the
|
||||||
|
three orderings would each report "not delivered" and read as three faults instead of one.
|
||||||
|
|
||||||
|
The header arithmetic is unit-tested (reassembly coverage with no gaps or double-delivery, MF
|
||||||
|
flags, shared IP ID, 8-byte offsets, checksum verification over odd and even lengths). Because the
|
||||||
|
code is `//go:build linux`, the tests are **cross-compiled and run on fmr** — there is no Go
|
||||||
|
toolchain there, so `go test -c` plus scp is the loop.
|
||||||
|
|
||||||
|
Live against fmr: 4 fragments per burst, and all three orderings reassembled — a healthy path, and
|
||||||
|
the baseline against which a mobile network will be interesting.
|
||||||
|
|
||||||
|
### Testing state (2026-08-01)
|
||||||
|
Six live tests against fmr, all green, no device involved: `LiveServerTest`, `LiveMeasurement`,
|
||||||
|
`LiveGranted`, `LiveDownstream`, `LiveUpload`, `LiveCompat`, `LiveEnrollment`. Plus 74 client unit
|
||||||
|
tests and the full Go suite. Everything in the last several entries is verified from the PC; the
|
||||||
|
app's UI (settings, history, deep-link enrollment) and `mtu.pmtud_up` remain device-only.
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ class LiveDownstreamTest {
|
|||||||
for (t in tests) println("${t.type} status=${t.status} metrics=${t.metrics}")
|
for (t in tests) println("${t.type} status=${t.status} metrics=${t.metrics}")
|
||||||
for (f in findings) println("finding ${f.code} [${f.severity}] ${f.title}")
|
for (f in findings) println("finding ${f.code} [${f.severity}] ${f.title}")
|
||||||
|
|
||||||
assertEquals(3, tests.size, "expected pmtud_down, frag_delivery and a downstream train")
|
// Assert on what is present, not on how many: adding a measurement should not be a
|
||||||
|
// test edit. (It was, once — hence the note.)
|
||||||
|
assertTrue(tests.size >= 3, "expected at least the three downstream tests, got ${tests.size}")
|
||||||
val byType = tests.associateBy { it.type }
|
val byType = tests.associateBy { it.type }
|
||||||
|
|
||||||
val pmtud = assertNotNull(byType[TestType.MTU_PMTUD_DOWN], "no mtu.pmtud_down test")
|
val pmtud = assertNotNull(byType[TestType.MTU_PMTUD_DOWN], "no mtu.pmtud_down test")
|
||||||
@@ -58,6 +60,17 @@ class LiveDownstreamTest {
|
|||||||
val frag = assertNotNull(byType[TestType.MTU_FRAG_DELIVERY], "no mtu.frag_delivery test")
|
val frag = assertNotNull(byType[TestType.MTU_FRAG_DELIVERY], "no mtu.frag_delivery test")
|
||||||
assertNotNull(frag.metrics?.get("largest_delivered_bytes"))
|
assertNotNull(frag.metrics?.get("largest_delivered_bytes"))
|
||||||
|
|
||||||
|
// Fragment ordering runs only when fragments arrive at all, and only against a server
|
||||||
|
// that can craft them — so it is checked when present rather than required.
|
||||||
|
byType[TestType.MTU_FRAG_ORDERING]?.let { fo ->
|
||||||
|
val m = fo.metrics?.toString() ?: ""
|
||||||
|
println("fragment ordering: ${fo.status} $m")
|
||||||
|
if (fo.status != TestStatus.UNSUPPORTED) {
|
||||||
|
assertTrue(m.contains("in_order"), "no per-ordering result: $m")
|
||||||
|
assertTrue(m.contains("reversed"), "reversed ordering was never attempted: $m")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val train = assertNotNull(byType[TestType.TRAIN_UDP_DOWNSTREAM], "no downstream train")
|
val train = assertNotNull(byType[TestType.TRAIN_UDP_DOWNSTREAM], "no downstream train")
|
||||||
assertNotNull(train.evidence, "a train without columnar evidence is not recomputable")
|
assertNotNull(train.evidence, "a train without columnar evidence is not recomputable")
|
||||||
val received = train.metrics?.get("received")?.toString()?.toIntOrNull() ?: 0
|
val received = train.metrics?.get("received")?.toString()?.toIntOrNull() ?: 0
|
||||||
|
|||||||
Reference in New Issue
Block a user