fable fixes

This commit is contained in:
JackDoan
2026-07-30 17:32:00 -05:00
parent e8d6be1dd9
commit 16878eec1c
4 changed files with 303 additions and 26 deletions
+176
View File
@@ -1437,3 +1437,179 @@ func TestCoalescerAtomicRandomIDsCoalesce(t *testing.T) {
t.Fatalf("DF=1 chain with arbitrary IDs must coalesce: gso=%d", len(w.gsoWrites))
}
}
// buildTCPv4TS is buildTCPv4 with a TCP timestamp option in the standard
// Linux layout (NOP,NOP,TS — a 32-byte TCP header).
func buildTCPv4TS(seq uint32, flags byte, tsVal, tsEcr uint32, payload []byte) []byte {
const ipHdrLen = 20
const tcpHdrLen = 32
total := ipHdrLen + tcpHdrLen + len(payload)
pkt := make([]byte, total)
pkt[0] = 0x45
pkt[1] = 0x00
binary.BigEndian.PutUint16(pkt[2:4], uint16(total))
binary.BigEndian.PutUint16(pkt[4:6], 0)
binary.BigEndian.PutUint16(pkt[6:8], 0x4000)
pkt[8] = 64
pkt[9] = ipProtoTCP
copy(pkt[12:16], []byte{10, 0, 0, 1})
copy(pkt[16:20], []byte{10, 0, 0, 2})
binary.BigEndian.PutUint16(pkt[20:22], 1000)
binary.BigEndian.PutUint16(pkt[22:24], 2000)
binary.BigEndian.PutUint32(pkt[24:28], seq)
binary.BigEndian.PutUint32(pkt[28:32], 12345)
pkt[32] = 0x80 // doff=8: 32-byte TCP header
pkt[33] = flags
binary.BigEndian.PutUint16(pkt[34:36], 0xffff)
pkt[40] = 0x01 // NOP
pkt[41] = 0x01 // NOP
pkt[42] = 0x08 // TS kind
pkt[43] = 10 // TS length
binary.BigEndian.PutUint32(pkt[44:48], tsVal)
binary.BigEndian.PutUint32(pkt[48:52], tsEcr)
copy(pkt[52:], payload)
return pkt
}
func TestParseTCPOptions(t *testing.T) {
ts := func(val, ecr uint32) []byte {
b := make([]byte, 10)
b[0], b[1] = 0x08, 10
binary.BigEndian.PutUint32(b[2:6], val)
binary.BigEndian.PutUint32(b[6:10], ecr)
return b
}
cases := []struct {
name string
opts []byte
wantVal uint32
wantEcr uint32
wantOK bool
}{
{"empty", nil, 0, 0, false},
{"bare TS filling the block exactly", ts(100, 200), 100, 200, true},
{"standard linux NOP,NOP,TS", append([]byte{1, 1}, ts(7, 9)...), 7, 9, true},
{"unknown option then TS", append([]byte{254, 4, 0, 0}, ts(3, 4)...), 3, 4, true},
{"EOL terminates before garbage", append([]byte{0, 0}, ts(1, 2)...), 0, 0, false},
{"zero-length option must not hang", []byte{254, 0, 8, 10, 0, 0, 0, 1, 0, 0, 0, 2}, 0, 0, false},
{"TS with wrong length", []byte{8, 4, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1}, 0, 0, false},
{"truncated TS", append([]byte{1, 1, 1}, ts(5, 6)[:9]...), 0, 0, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
val, ecr, ok := parseTCPOptions(tc.opts)
if val != tc.wantVal || ecr != tc.wantEcr || ok != tc.wantOK {
t.Fatalf("parseTCPOptions(%v) = (%d, %d, %v), want (%d, %d, %v)",
tc.opts, val, ecr, ok, tc.wantVal, tc.wantEcr, tc.wantOK)
}
})
}
}
// TestCompareCoalesceSlotsAntisymmetric pins the comparator contract for the
// retransmit shape: a lower seq with a newer TSval (retransmit) versus a
// higher seq with an older TSval (delayed original). The TSval must win in
// BOTH directions — an asymmetric comparator gives SortStableFunc an
// inconsistent order and unspecified output.
func TestCompareCoalesceSlotsAntisymmetric(t *testing.T) {
mk := func(seq, tsVal uint32, hasTS bool) *coalesceSlot {
return &coalesceSlot{nextSeq: seq, tsVal: tsVal, hasTS: hasTS}
}
original := mk(5000, 100, true) // sent first, delayed in flight
retransmit := mk(1000, 105, true) // sent later, lower seq
if got := compareCoalesceSlots(original, retransmit); got != -1 {
t.Fatalf("compare(original, retransmit) = %d, want -1 (older TSval first)", got)
}
if got := compareCoalesceSlots(retransmit, original); got != 1 {
t.Fatalf("compare(retransmit, original) = %d, want 1", got)
}
// Equal TSvals (a burst within one tick) fall back to seq order,
// still antisymmetrically.
a, b := mk(1000, 50, true), mk(2000, 50, true)
if compareCoalesceSlots(a, b) != -1 || compareCoalesceSlots(b, a) != 1 {
t.Fatal("equal-TSval slots must order by seq in both directions")
}
// Timestamp-less flows keep pure seq order.
c, d := mk(2000, 0, false), mk(1000, 99, true)
if compareCoalesceSlots(c, d) != 1 || compareCoalesceSlots(d, c) != -1 {
t.Fatal("mixed/absent timestamps must fall back to seq in both directions")
}
}
// TestCoalescerRetransmitEmitsAfterDelayedOriginal: a retransmit (lower seq,
// newer TSval) and a delayed original (higher seq, older TSval) land in one
// flush window. Seq-only sorting would emit the retransmit first; the
// receiver would advance ts_recent past the original's TSval and PAWS would
// drop the original. TSval-first ordering must emit the original first.
func TestCoalescerRetransmitEmitsAfterDelayedOriginal(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true}
c := newTestTCPCoalescer(t, w)
pay := make([]byte, 100)
original := buildTCPv4TS(5000, tcpAck, 100, 1, pay)
retransmit := buildTCPv4TS(1000, tcpAck, 105, 1, pay)
if err := c.Commit(original); err != nil {
t.Fatal(err)
}
if err := c.Commit(retransmit); err != nil {
t.Fatal(err)
}
if err := c.Flush(); err != nil {
t.Fatal(err)
}
if len(w.writes) != 2 {
t.Fatalf("want 2 plain writes (non-contiguous single-segment slots), got %d writes, %d gso", len(w.writes), len(w.gsoWrites))
}
firstSeq := binary.BigEndian.Uint32(w.writes[0][24:28])
secondSeq := binary.BigEndian.Uint32(w.writes[1][24:28])
if firstSeq != 5000 || secondSeq != 1000 {
t.Fatalf("emission order (%d, %d), want (5000, 1000): retransmit must not overtake the older-TSval original", firstSeq, secondSeq)
}
}
// TestCoalescerACKDoesNotSplitSortRun: an interleaved pure ACK must not stop
// wire-reordered same-flow data on either side of it from sorting adjacent
// and merging — the contract explicitly allows data to overtake a bare ACK.
// Arrival is D2, ACK, D1; the two data slots must still merge into one
// superpacket, with the ACK emitted after (its seq is the peer's snd_nxt,
// which orders it behind the data it followed).
func TestCoalescerACKDoesNotSplitSortRun(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true}
c := newTestTCPCoalescer(t, w)
pay := make([]byte, 1200)
d2 := buildTCPv4(2200, tcpAck, pay)
ack := buildTCPv4(3400, tcpAck, nil)
d1 := buildTCPv4(1000, tcpAck, pay)
for _, pkt := range [][]byte{d2, ack, d1} {
if err := c.Commit(pkt); err != nil {
t.Fatal(err)
}
}
if err := c.Flush(); err != nil {
t.Fatal(err)
}
if len(w.gsoWrites) != 1 {
t.Fatalf("want the two data slots merged into 1 gso write across the ACK, got %d gso + %d plain", len(w.gsoWrites), len(w.writes))
}
if got := w.gsoWrites[0].payLen(); got != 2400 {
t.Fatalf("merged payload = %d, want 2400", got)
}
if len(w.writes) != 1 {
t.Fatalf("want the ACK as 1 plain write, got %d", len(w.writes))
}
if got := binary.BigEndian.Uint32(w.writes[0][24:28]); got != 3400 {
t.Fatalf("plain write seq = %d, want the ACK (3400)", got)
}
if len(w.order) != 2 || w.order[0] != "gso" || w.order[1] != "write" {
t.Fatalf("emission order = %v, want [gso write]", w.order)
}
}