Checksum the header and refuse sequence numbers the sender never sent

This commit is contained in:
flamingcow
2026-08-04 20:41:16 -07:00
parent 703039587b
commit 1d54a1a1f6
9 changed files with 241 additions and 35 deletions
+42 -6
View File
@@ -13,6 +13,8 @@ const (
hdrMagic = 0x43424c54 hdrMagic = 0x43424c54
minFrame = ethHdrLen + hdrLen minFrame = ethHdrLen + hdrLen
maxFrame = 9216 maxFrame = 9216
hdrCheckOff = 18
) )
var crcTable = crc32.MakeTable(crc32.Castagnoli) var crcTable = crc32.MakeTable(crc32.Castagnoli)
@@ -113,6 +115,26 @@ func (f *frameSpec) expectedCRC(patIdx, payLen int) (uint32, bool) {
return crc, ok return crc, ok
} }
// A one's complement sum over the header, skipping the two bytes it is written
// into. The payload checksum covers the payload alone, so without this the
// sequence number is the one field on the wire that nothing checks, and a bit
// flipped in it arrives looking exactly like a legitimate frame from far in the
// future. Every single bit error changes the folded sum, which is the error a
// cable going marginal actually produces.
func headerCheck(h []byte) uint16 {
var sum uint32
for i := 0; i+1 < hdrLen; i += 2 {
if i == hdrCheckOff {
continue
}
sum += uint32(binary.BigEndian.Uint16(h[i:]))
}
for sum>>16 != 0 {
sum = sum&0xffff + sum>>16
}
return ^uint16(sum)
}
func putHeader(buf []byte, patIdx int, stream uint16, seq uint64, payLen int) { func putHeader(buf []byte, patIdx int, stream uint16, seq uint64, payLen int) {
h := buf[ethHdrLen:] h := buf[ethHdrLen:]
binary.BigEndian.PutUint32(h[0:4], hdrMagic) binary.BigEndian.PutUint32(h[0:4], hdrMagic)
@@ -121,7 +143,7 @@ func putHeader(buf []byte, patIdx int, stream uint16, seq uint64, payLen int) {
binary.BigEndian.PutUint16(h[6:8], stream) binary.BigEndian.PutUint16(h[6:8], stream)
binary.BigEndian.PutUint64(h[8:16], seq) binary.BigEndian.PutUint64(h[8:16], seq)
binary.BigEndian.PutUint16(h[16:18], uint16(payLen)) binary.BigEndian.PutUint16(h[16:18], uint16(payLen))
binary.BigEndian.PutUint16(h[18:20], 0) binary.BigEndian.PutUint16(h[hdrCheckOff:], headerCheck(h))
} }
type parsed struct { type parsed struct {
@@ -131,21 +153,35 @@ type parsed struct {
payLen int payLen int
} }
func parseHeader(buf []byte) (parsed, bool) { // Why a frame was turned away. A header that is not ours and a header of ours
// that arrived damaged are different things to report: the first is somebody
// else on the wire, the second is the fault being tested for.
type hdrStatus int
const (
hdrOK hdrStatus = iota
hdrForeign
hdrDamaged
)
func parseHeader(buf []byte) (parsed, hdrStatus) {
var p parsed var p parsed
if len(buf) < minFrame { if len(buf) < minFrame {
return p, false return p, hdrForeign
} }
h := buf[ethHdrLen:] h := buf[ethHdrLen:]
if binary.BigEndian.Uint32(h[0:4]) != hdrMagic { if binary.BigEndian.Uint32(h[0:4]) != hdrMagic {
return p, false return p, hdrForeign
}
if binary.BigEndian.Uint16(h[hdrCheckOff:]) != headerCheck(h) {
return p, hdrDamaged
} }
p.patIdx = int(h[5]) p.patIdx = int(h[5])
p.stream = binary.BigEndian.Uint16(h[6:8]) p.stream = binary.BigEndian.Uint16(h[6:8])
p.seq = binary.BigEndian.Uint64(h[8:16]) p.seq = binary.BigEndian.Uint64(h[8:16])
p.payLen = int(binary.BigEndian.Uint16(h[16:18])) p.payLen = int(binary.BigEndian.Uint16(h[16:18]))
if minFrame+p.payLen > len(buf) { if minFrame+p.payLen > len(buf) {
return p, false return p, hdrDamaged
} }
return p, true return p, hdrOK
} }
+66
View File
@@ -0,0 +1,66 @@
package main
import "testing"
func testFrame() []byte {
buf := make([]byte, 512)
spec := newFrameSpec([6]byte{1, 2, 3, 4, 5, 6}, [6]byte{7, 8, 9, 10, 11, 12},
etherBase, []int{512})
spec.prefill(buf, 3)
putHeader(buf, 3, 5, 0x0123456789ab, 512-minFrame)
return buf
}
func TestHeaderRoundTrip(t *testing.T) {
p, st := parseHeader(testFrame())
if st != hdrOK {
t.Fatalf("status = %v, want hdrOK", st)
}
want := parsed{patIdx: 3, stream: 5, seq: 0x0123456789ab, payLen: 512 - minFrame}
if p != want {
t.Errorf("parsed = %+v, want %+v", p, want)
}
}
// The sequence number is the one field on the wire that nothing else checks, and
// a single flipped bit in it is exactly what a marginal cable produces. Every
// bit of the header has to be covered, including the checksum's own.
func TestHeaderChecksumCatchesEverySingleBitFlip(t *testing.T) {
for byteIdx := 0; byteIdx < hdrLen; byteIdx++ {
for bit := 0; bit < 8; bit++ {
buf := testFrame()
buf[ethHdrLen+byteIdx] ^= 1 << bit
_, st := parseHeader(buf)
if st == hdrOK {
t.Errorf("header byte %d bit %d flipped and the frame was accepted",
byteIdx, bit)
}
// The magic is what says the frame is ours at all, so damage there is
// somebody else's traffic as far as we can tell.
wantForeign := byteIdx < 4
if wantForeign && st != hdrForeign {
t.Errorf("magic byte %d bit %d = %v, want hdrForeign", byteIdx, bit, st)
}
if !wantForeign && st != hdrDamaged {
t.Errorf("header byte %d bit %d = %v, want hdrDamaged", byteIdx, bit, st)
}
}
}
}
func TestHeaderShortFrameIsForeign(t *testing.T) {
if _, st := parseHeader(testFrame()[:minFrame-1]); st != hdrForeign {
t.Errorf("status = %v, want hdrForeign", st)
}
}
// A length that runs past the frame is damage rather than a foreign header: the
// magic and the checksum both said the header was ours.
func TestHeaderLengthPastFrameIsDamaged(t *testing.T) {
buf := testFrame()
putHeader(buf, 3, 5, 1, len(buf))
if _, st := parseHeader(buf); st != hdrDamaged {
t.Errorf("status = %v, want hdrDamaged", st)
}
}
+25 -4
View File
@@ -18,19 +18,39 @@ type lossWindow struct {
bits []uint64 bits []uint64
lost atomic.Uint64 lost atomic.Uint64
late atomic.Uint64 late atomic.Uint64
// The sending half of this stream. Both halves are in this process and the
// receiving socket ignores its own outgoing frames, so this window is fed by
// that sender alone and by nothing else on the wire.
sent *atomic.Uint64
} }
func newLossWindows(n int) []lossWindow { // Takes the sending halves rather than a count, so a window cannot be built
w := make([]lossWindow, n) // without the bound it judges sequence numbers against.
func newLossWindows(tx []*txStats) []lossWindow {
w := make([]lossWindow, len(tx))
for i := range w { for i := range w {
w[i].bits = make([]uint64, lossWords) w[i].bits = make([]uint64, lossWords)
w[i].sent = &tx[i].sent
} }
return w return w
} }
// A sequence number is only judged once it falls out of the window, so frames // A sequence number is only judged once it falls out of the window, so frames
// still queued in another rx worker are never miscounted as lost. // still queued in another rx worker are never miscounted as lost.
func (w *lossWindow) observe(seq uint64) { //
// Reports whether the sequence number could have come off the wire at all. One
// above what the sender has reached is a damaged header rather than a gap, and
// is refused before it can touch the base. Believing one would drag the window
// up past every real sequence, write off the span in between as lost, and leave
// every frame after it arriving below the base and counted late for the rest of
// the run. Nothing that arrives later is evidence enough to undo that, which is
// why the sender's own frontier is the guard rather than a limit on how far a
// single step may move.
func (w *lossWindow) observe(seq uint64) bool {
if seq >= w.sent.Load() {
return false
}
w.mu.Lock() w.mu.Lock()
if !w.inited { if !w.inited {
// Start half a window below the first sequence seen, so frames another // Start half a window below the first sequence seen, so frames another
@@ -43,7 +63,7 @@ func (w *lossWindow) observe(seq uint64) {
if seq < w.base { if seq < w.base {
w.mu.Unlock() w.mu.Unlock()
w.late.Add(1) w.late.Add(1)
return return true
} }
if seq >= w.base+lossSlots { if seq >= w.base+lossSlots {
w.evict(seq - lossSlots + 1) w.evict(seq - lossSlots + 1)
@@ -51,6 +71,7 @@ func (w *lossWindow) observe(seq uint64) {
idx := seq & (lossSlots - 1) idx := seq & (lossSlots - 1)
w.bits[idx>>6] |= 1 << (idx & 63) w.bits[idx>>6] |= 1 << (idx & 63)
w.mu.Unlock() w.mu.Unlock()
return true
} }
func (w *lossWindow) evict(newBase uint64) { func (w *lossWindow) evict(newBase uint64) {
+61 -8
View File
@@ -2,13 +2,17 @@ package main
import "testing" import "testing"
func newWindow() *lossWindow { // sent is the sender's frontier: sequence numbers at or above it were never put
w := newLossWindows(1) // on the wire.
func newWindow(sent uint64) *lossWindow {
tx := &txStats{}
tx.sent.Store(sent)
w := newLossWindows([]*txStats{tx})
return &w[0] return &w[0]
} }
func TestLossWindowContiguousLosesNothing(t *testing.T) { func TestLossWindowContiguousLosesNothing(t *testing.T) {
w := newWindow() w := newWindow(lossSlots + 1000)
for seq := uint64(0); seq < lossSlots+1000; seq++ { for seq := uint64(0); seq < lossSlots+1000; seq++ {
w.observe(seq) w.observe(seq)
} }
@@ -21,7 +25,7 @@ func TestLossWindowContiguousLosesNothing(t *testing.T) {
} }
func TestLossWindowCountsGapOnceEvicted(t *testing.T) { func TestLossWindowCountsGapOnceEvicted(t *testing.T) {
w := newWindow() w := newWindow(lossSlots + 1000)
for seq := uint64(0); seq < lossSlots+1000; seq++ { for seq := uint64(0); seq < lossSlots+1000; seq++ {
if seq == 100 { if seq == 100 {
continue continue
@@ -36,7 +40,7 @@ func TestLossWindowCountsGapOnceEvicted(t *testing.T) {
// Arriving out of order inside the window is not loss: a sequence is only // Arriving out of order inside the window is not loss: a sequence is only
// judged once it falls out the far end. // judged once it falls out the far end.
func TestLossWindowOutOfOrderIsNotLoss(t *testing.T) { func TestLossWindowOutOfOrderIsNotLoss(t *testing.T) {
w := newWindow() w := newWindow(lossSlots + 1000)
for seq := uint64(99); ; seq-- { for seq := uint64(99); ; seq-- {
w.observe(seq) w.observe(seq)
if seq == 0 { if seq == 0 {
@@ -54,7 +58,7 @@ func TestLossWindowOutOfOrderIsNotLoss(t *testing.T) {
// The other branch of evict: a jump past a whole window writes off everything // The other branch of evict: a jump past a whole window writes off everything
// the window held plus the sequences that never landed in it at all. // the window held plus the sequences that never landed in it at all.
func TestLossWindowJumpBeyondWindow(t *testing.T) { func TestLossWindowJumpBeyondWindow(t *testing.T) {
w := newWindow() w := newWindow(200001)
w.observe(0) w.observe(0)
w.observe(200000) w.observe(200000)
@@ -69,7 +73,7 @@ func TestLossWindowJumpBeyondWindow(t *testing.T) {
} }
func TestLossWindowBelowBaseIsLate(t *testing.T) { func TestLossWindowBelowBaseIsLate(t *testing.T) {
w := newWindow() w := newWindow(100001)
w.observe(100000) w.observe(100000)
w.observe(1000) w.observe(1000)
if got := w.late.Load(); got != 1 { if got := w.late.Load(); got != 1 {
@@ -83,9 +87,58 @@ func TestLossWindowBelowBaseIsLate(t *testing.T) {
// The first sequence seen starts the window half a span below it, so frames // The first sequence seen starts the window half a span below it, so frames
// another worker is still holding land inside rather than arriving late. // another worker is still holding land inside rather than arriving late.
func TestLossWindowStartsHalfAWindowBack(t *testing.T) { func TestLossWindowStartsHalfAWindowBack(t *testing.T) {
w := newWindow() w := newWindow(100001)
w.observe(100000) w.observe(100000)
if w.base != 100000-lossSlots/2 { if w.base != 100000-lossSlots/2 {
t.Errorf("base = %d, want %d", w.base, 100000-lossSlots/2) t.Errorf("base = %d, want %d", w.base, 100000-lossSlots/2)
} }
} }
// The failure this guard exists for. A sequence number the sender never reached
// used to drag the base up to it, write off the span in between as lost, and
// leave every real frame after it below the base and counted late for the rest
// of the run. It has to be refused outright, and the stream has to go on
// counting as if it had never arrived.
func TestLossWindowRefusesUnsentSeq(t *testing.T) {
const first = 100000
w := newWindow(first + 2000)
for seq := uint64(first); seq < first+1000; seq++ {
w.observe(seq)
}
base := w.base
if w.observe(1 << 62) {
t.Error("a sequence number far past the sender's frontier was accepted")
}
if got := w.lost.Load(); got != 0 {
t.Errorf("lost = %d after one impossible sequence number, want 0", got)
}
if w.base != base {
t.Errorf("base moved to %d, want it left at %d where the real traffic put it",
w.base, base)
}
// Still tracking the real traffic, rather than reporting every frame late
// against a base that ran away.
for seq := uint64(first + 1000); seq < first+2000; seq++ {
w.observe(seq)
}
if got := w.late.Load(); got != 0 {
t.Errorf("late = %d, want 0", got)
}
if got := w.lost.Load(); got != 0 {
t.Errorf("lost = %d, want 0", got)
}
}
// The sender's own frontier is the bound, so the sequence one past it is
// refused while the one below it is not.
func TestLossWindowBoundIsExclusive(t *testing.T) {
w := newWindow(500)
if !w.observe(499) {
t.Error("the last sequence the sender put on the wire was refused")
}
if w.observe(500) {
t.Error("a sequence the sender had not reached was accepted")
}
}
+15 -6
View File
@@ -169,6 +169,7 @@ type sample struct {
rxFrames, rxBytes uint64 rxFrames, rxBytes uint64
lost, late uint64 lost, late uint64
crcErr, badMagic uint64 crcErr, badMagic uint64
badHdr uint64
badLen uint64 badLen uint64
txErrs uint64 txErrs uint64
rxErrs uint64 rxErrs uint64
@@ -204,6 +205,7 @@ func (d *direction) snapshot() sample {
s.rxBytes += r.bytes.Load() s.rxBytes += r.bytes.Load()
s.crcErr += r.crcErr.Load() s.crcErr += r.crcErr.Load()
s.badMagic += r.badMagic.Load() s.badMagic += r.badMagic.Load()
s.badHdr += r.badHdr.Load()
s.badLen += r.badLen.Load() s.badLen += r.badLen.Load()
s.rxErrs += r.rxErrs.Load() s.rxErrs += r.rxErrs.Load()
} }
@@ -279,10 +281,11 @@ type view struct {
func errsBetween(b, n counterSet) errs { func errsBetween(b, n counterSet) errs {
return errs{ return errs{
lost: n.s.lost - b.s.lost, lost: n.s.lost - b.s.lost,
// Three ways of noticing one thing: a payload that does not match its // Four ways of noticing one thing: a payload that does not match its
// checksum, a header that is not ours, and a length that cannot be. // checksum, a header that does not match its own, a header that is not
corrupt: (n.s.crcErr - b.s.crcErr) + (n.s.badMagic - b.s.badMagic) + // ours, and a length that cannot be.
(n.s.badLen - b.s.badLen), corrupt: (n.s.crcErr - b.s.crcErr) + (n.s.badHdr - b.s.badHdr) +
(n.s.badMagic - b.s.badMagic) + (n.s.badLen - b.s.badLen),
// What the hardware reported. Nothing the host declined to send is here, // What the hardware reported. Nothing the host declined to send is here,
// so this one going red means the cable. // so this one going red means the cable.
link: (n.nic - b.nic) + (n.s.rxErrs - b.s.rxErrs), link: (n.nic - b.nic) + (n.s.rxErrs - b.s.rxErrs),
@@ -372,8 +375,15 @@ func (d *direction) primeCounters() {
} }
func buildDirection(label string, tx, rx endpoint) (*direction, error) { func buildDirection(label string, tx, rx endpoint) (*direction, error) {
// Built before the windows, since each window judges sequence numbers against
// the frontier its own sender publishes.
txs := make([]*txStats, numStreams)
for i := range txs {
txs[i] = &txStats{}
}
d := &direction{ d := &direction{
streams: newLossWindows(numStreams), txStats: txs,
streams: newLossWindows(txs),
cable: newCableStats(), cable: newCableStats(),
} }
// Held open for the life of the run: the stats ioctl is issued five times a // Held open for the life of the run: the stats ioctl is issued five times a
@@ -399,7 +409,6 @@ func buildDirection(label string, tx, rx endpoint) (*direction, error) {
return nil, fmt.Errorf("%s tx socket: %w", label, err) return nil, fmt.Errorf("%s tx socket: %w", label, err)
} }
d.txFDs = append(d.txFDs, fd) d.txFDs = append(d.txFDs, fd)
d.txStats = append(d.txStats, &txStats{})
fd, err = openRxSocket(rx.idx, et) fd, err = openRxSocket(rx.idx, et)
if err != nil { if err != nil {
+4 -4
View File
@@ -95,19 +95,19 @@ func TestErrsBetweenBuckets(t *testing.T) {
n := counterSet{ n := counterSet{
s: sample{ s: sample{
lost: 1, late: 7, lost: 1, late: 7,
crcErr: 2, badMagic: 3, badLen: 4, crcErr: 2, badMagic: 3, badLen: 4, badHdr: 10,
txErrs: 6, rxErrs: 5, txErrs: 6, rxErrs: 5,
}, },
drops: 9, drops: 9,
nic: 8, nic: 8,
} }
got := errsBetween(counterSet{}, n) got := errsBetween(counterSet{}, n)
want := errs{lost: 1, corrupt: 2 + 3 + 4, link: 8 + 5, internal: 9 + 7 + 6} want := errs{lost: 1, corrupt: 2 + 3 + 4 + 10, link: 8 + 5, internal: 9 + 7 + 6}
if got != want { if got != want {
t.Errorf("errsBetween = %+v, want %+v", got, want) t.Errorf("errsBetween = %+v, want %+v", got, want)
} }
if got.total() != 45 { if got.total() != 55 {
t.Errorf("total = %d, want 45", got.total()) t.Errorf("total = %d, want 55", got.total())
} }
} }
+2 -2
View File
@@ -210,8 +210,8 @@ func (r *probeReceiver) run(done *atomic.Bool) {
if err != nil { if err != nil {
continue continue
} }
h, ok := parseHeader(buf[:n]) h, st := parseHeader(buf[:n])
if !ok || h.stream != probeStream { if st != hdrOK || h.stream != probeStream {
continue continue
} }
ts, ok := hwTimestamp(oob[:oobn]) ts, ok := hwTimestamp(oob[:oobn])
+14 -4
View File
@@ -12,6 +12,7 @@ type rxStats struct {
frames atomic.Uint64 frames atomic.Uint64
bytes atomic.Uint64 bytes atomic.Uint64
badMagic atomic.Uint64 badMagic atomic.Uint64
badHdr atomic.Uint64
badLen atomic.Uint64 badLen atomic.Uint64
crcErr atomic.Uint64 crcErr atomic.Uint64
rxErrs atomic.Uint64 rxErrs atomic.Uint64
@@ -99,9 +100,13 @@ func (w *rxWorker) run(done *atomic.Bool) {
} }
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
buf := bufs[i][:int(hdrs[i].len)] buf := bufs[i][:int(hdrs[i].len)]
p, ok := parseHeader(buf) p, st := parseHeader(buf)
if !ok { if st != hdrOK {
if st == hdrForeign {
w.stats.badMagic.Add(1) w.stats.badMagic.Add(1)
} else {
w.stats.badHdr.Add(1)
}
continue continue
} }
w.stats.frames.Add(1) w.stats.frames.Add(1)
@@ -110,8 +115,13 @@ func (w *rxWorker) run(done *atomic.Bool) {
w.stats.observe(ts, uint64(len(buf))) w.stats.observe(ts, uint64(len(buf)))
} }
if int(p.stream) < len(w.streams) { // A sequence number the sender never reached got past the header
w.streams[p.stream].observe(p.seq) // checksum, so the frame is damaged whatever its payload says. Counted
// here rather than left to the payload check, which would report the
// same frame twice or, if only the header was hit, not at all.
if int(p.stream) < len(w.streams) && !w.streams[p.stream].observe(p.seq) {
w.stats.badHdr.Add(1)
continue
} }
want, ok := w.spec.expectedCRC(p.patIdx, p.payLen) want, ok := w.spec.expectedCRC(p.patIdx, p.payLen)
+11
View File
@@ -8,6 +8,12 @@ import (
type txStats struct { type txStats struct {
errs atomic.Uint64 errs atomic.Uint64
// The exclusive upper bound on any sequence number this stream can have put
// on the wire. Read by the receiving half of the same stream, which is the
// only thing that can tell a gap it must write off from a sequence number
// that was never sent at all.
sent atomic.Uint64
} }
type txWorker struct { type txWorker struct {
@@ -46,6 +52,11 @@ func (w *txWorker) run(done *atomic.Bool) {
iovs[i].Len = uint64(size) iovs[i].Len = uint64(size)
} }
// Published before the send rather than after it, so it is never behind a
// frame already in flight. Overshooting a partial send is harmless: it is
// a bound, and one the next pass raises again.
w.stats.sent.Store(seq + uint64(w.batch))
n, err := sendmmsg(w.fd, hdrs) n, err := sendmmsg(w.fd, hdrs)
if n > 0 { if n > 0 {
seq += uint64(n) seq += uint64(n)