frag_send: crafted IP fragments, so ordering can be tested and not just delivery
Letting the kernel fragment an oversized datagram answers one question — do
fragments get through. It cannot answer the more interesting one, because the
kernel always emits them 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
drop them. That is invisible to any in-order test and shows up in the field as
"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 now builds the fragments itself (raw socket, IP_HDRINCL) and
controls their order: in_order as a baseline, reversed, and first-fragment-last.
The datagram is assembled and signed whole before being cut up, so what the
client reassembles is indistinguishable from an ordinary packet — otherwise it
would be measuring our sender rather than the path.
Two details that would silently produce wrong answers:
- The UDP checksum is computed rather than left zero. A zero-checksum datagram
is 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 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, since a permission model has more ways to say no than a
capability bit has to say yes.
Fragment header arithmetic is unit-tested (reassembly coverage, MF flags, shared
IP ID, 8-byte offsets, checksum verification), cross-compiled and run on Linux
since the code is build-tagged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4ffa6e4ae2
commit
a7dccf7da2
@@ -59,6 +59,9 @@ type Server struct {
|
||||
BigSend func(sess *session.Session, g *session.Grant, sizes []int, df bool) ([]dataplane.BigSendResult, error)
|
||||
// Runs stores uploaded measurement documents (may be nil: uploads unsupported).
|
||||
Runs *runs.Store
|
||||
// FragSend emits one datagram as hand-built IP fragments in a chosen order (may be nil:
|
||||
// needs a raw socket, so it is unavailable to an unprivileged server).
|
||||
FragSend func(sess *session.Session, g *session.Grant, sizeBytes int, mode dataplane.FragMode, fragSize int) (dataplane.FragResult, error)
|
||||
// EgressMTU reports the server's own measured egress path MTU (0 = unknown). With DF set
|
||||
// we cannot emit a datagram larger than this, so requested sizes above it are refused up
|
||||
// front and reported as such — the client must not read that as a downstream path limit.
|
||||
@@ -241,6 +244,8 @@ func (s *Server) actions(w http.ResponseWriter, r *http.Request) {
|
||||
IntervalUs int `json:"interval_us"`
|
||||
SizesBytes []int `json:"sizes_bytes"`
|
||||
DF *bool `json:"df"`
|
||||
Mode string `json:"mode"`
|
||||
FragBytes int `json:"frag_bytes"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "bad body"})
|
||||
@@ -373,6 +378,45 @@ func (s *Server) actions(w http.ResponseWriter, r *http.Request) {
|
||||
"grant": map[string]any{"max_bytes": g.MaxBytes, "max_kbps": g.MaxKbps},
|
||||
})
|
||||
|
||||
case "frag_send":
|
||||
if s.FragSend == nil {
|
||||
writeJSON(w, http.StatusNotImplemented, map[string]string{
|
||||
"error": "frag_send needs a raw socket, which this server does not have",
|
||||
})
|
||||
return
|
||||
}
|
||||
size := clamp(req.SizeBytes, 1600, 8000) // must exceed the path MTU or nothing fragments
|
||||
mode := dataplane.FragMode(req.Mode)
|
||||
switch mode {
|
||||
case dataplane.FragInOrder, dataplane.FragReversed, dataplane.FragFirstLast:
|
||||
default:
|
||||
mode = dataplane.FragInOrder
|
||||
}
|
||||
fragBytes := clamp(req.FragBytes, 8, 1400)
|
||||
g := sess.NewGrant(actionID, int64(size), 0, session.DefaultGrantLimits)
|
||||
if g == nil {
|
||||
writeJSON(w, http.StatusConflict, noDataPlaneYet)
|
||||
return
|
||||
}
|
||||
// Synchronous: the whole burst is a few kB and at most a few hundred milliseconds, and
|
||||
// the caller wants to know it was actually emitted before it starts listening. An
|
||||
// asynchronous send would make "nothing arrived" ambiguous between a path drop and a
|
||||
// send that never happened — the one distinction this test exists to make.
|
||||
result, err := s.FragSend(sess, g, size, mode, fragBytes)
|
||||
slog.Info("frag_send finished", "action", actionID, "mode", mode,
|
||||
"size", size, "fragments", result.Fragments, "err", err)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusConflict, map[string]any{
|
||||
"error": err.Error(), "action_id": actionID, "result": result,
|
||||
})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusAccepted, map[string]any{
|
||||
"action_id": actionID, "mode": string(mode), "size_bytes": size,
|
||||
"frag_bytes": fragBytes, "fragments": result.Fragments,
|
||||
"grant": map[string]any{"max_bytes": g.MaxBytes, "max_kbps": g.MaxKbps},
|
||||
})
|
||||
|
||||
default:
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "unknown or unimplemented action"})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user