|
|
@@ -0,0 +1,356 @@
|
|
|
+package nebula
|
|
|
+
|
|
|
+import (
|
|
|
+ "encoding/binary"
|
|
|
+ "testing"
|
|
|
+)
|
|
|
+
|
|
|
+// A minimal stub writer that records each plain Write and each WriteGSO
|
|
|
+// call without touching a real TUN fd.
|
|
|
+type fakeTunWriter struct {
|
|
|
+ gsoEnabled bool
|
|
|
+ writes [][]byte
|
|
|
+ gsoWrites []fakeGSOWrite
|
|
|
+}
|
|
|
+
|
|
|
+type fakeGSOWrite struct {
|
|
|
+ pkt []byte
|
|
|
+ gsoSize uint16
|
|
|
+ isV6 bool
|
|
|
+ hdrLen uint16
|
|
|
+ csumStart uint16
|
|
|
+}
|
|
|
+
|
|
|
+func (w *fakeTunWriter) Write(p []byte) (int, error) {
|
|
|
+ buf := make([]byte, len(p))
|
|
|
+ copy(buf, p)
|
|
|
+ w.writes = append(w.writes, buf)
|
|
|
+ return len(p), nil
|
|
|
+}
|
|
|
+
|
|
|
+func (w *fakeTunWriter) WriteGSO(pkt []byte, gsoSize uint16, isV6 bool, hdrLen, csumStart uint16) error {
|
|
|
+ buf := make([]byte, len(pkt))
|
|
|
+ copy(buf, pkt)
|
|
|
+ w.gsoWrites = append(w.gsoWrites, fakeGSOWrite{pkt: buf, gsoSize: gsoSize, isV6: isV6, hdrLen: hdrLen, csumStart: csumStart})
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (w *fakeTunWriter) GSOSupported() bool { return w.gsoEnabled }
|
|
|
+
|
|
|
+// buildTCPv4 constructs a minimal IPv4+TCP packet with the given payload,
|
|
|
+// seq, and flags. Assumes no IP options and a 20-byte TCP header.
|
|
|
+func buildTCPv4(seq uint32, flags byte, payload []byte) []byte {
|
|
|
+ const ipHdrLen = 20
|
|
|
+ const tcpHdrLen = 20
|
|
|
+ total := ipHdrLen + tcpHdrLen + len(payload)
|
|
|
+ pkt := make([]byte, total)
|
|
|
+
|
|
|
+ // IPv4 header.
|
|
|
+ pkt[0] = 0x45 // version 4, IHL 5
|
|
|
+ pkt[1] = 0x00 // TOS
|
|
|
+ binary.BigEndian.PutUint16(pkt[2:4], uint16(total))
|
|
|
+ binary.BigEndian.PutUint16(pkt[4:6], 0) // id
|
|
|
+ binary.BigEndian.PutUint16(pkt[6:8], 0x4000) // DF
|
|
|
+ pkt[8] = 64 // TTL
|
|
|
+ pkt[9] = ipProtoTCP
|
|
|
+ // csum left zero — coalescer recomputes on emit.
|
|
|
+ copy(pkt[12:16], []byte{10, 0, 0, 1}) // src
|
|
|
+ copy(pkt[16:20], []byte{10, 0, 0, 2}) // dst
|
|
|
+
|
|
|
+ // TCP header.
|
|
|
+ binary.BigEndian.PutUint16(pkt[20:22], 1000) // sport
|
|
|
+ binary.BigEndian.PutUint16(pkt[22:24], 2000) // dport
|
|
|
+ binary.BigEndian.PutUint32(pkt[24:28], seq)
|
|
|
+ binary.BigEndian.PutUint32(pkt[28:32], 12345) // ack
|
|
|
+ pkt[32] = 0x50 // data offset = 5 << 4
|
|
|
+ pkt[33] = flags
|
|
|
+ binary.BigEndian.PutUint16(pkt[34:36], 0xffff) // window
|
|
|
+ // tcp csum zero
|
|
|
+ // urgent zero
|
|
|
+
|
|
|
+ copy(pkt[40:], payload)
|
|
|
+ return pkt
|
|
|
+}
|
|
|
+
|
|
|
+const (
|
|
|
+ tcpAck = 0x10
|
|
|
+ tcpPsh = 0x08
|
|
|
+ tcpSyn = 0x02
|
|
|
+ tcpFin = 0x01
|
|
|
+ tcpAckPsh = tcpAck | tcpPsh
|
|
|
+)
|
|
|
+
|
|
|
+func TestCoalescerPassthroughWhenGSOUnavailable(t *testing.T) {
|
|
|
+ w := &fakeTunWriter{gsoEnabled: false}
|
|
|
+ c := newTCPCoalescer(w)
|
|
|
+ pkt := buildTCPv4(1000, tcpAck, []byte("hello"))
|
|
|
+ if err := c.Add(pkt); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
|
|
|
+ t.Fatalf("want single plain write, got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestCoalescerNonTCPPassthrough(t *testing.T) {
|
|
|
+ w := &fakeTunWriter{gsoEnabled: true}
|
|
|
+ c := newTCPCoalescer(w)
|
|
|
+ // ICMP packet: proto=1.
|
|
|
+ pkt := make([]byte, 28)
|
|
|
+ pkt[0] = 0x45
|
|
|
+ binary.BigEndian.PutUint16(pkt[2:4], 28)
|
|
|
+ pkt[9] = 1
|
|
|
+ copy(pkt[12:16], []byte{10, 0, 0, 1})
|
|
|
+ copy(pkt[16:20], []byte{10, 0, 0, 2})
|
|
|
+ if err := c.Add(pkt); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
|
|
|
+ t.Fatalf("ICMP should pass through unchanged")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestCoalescerSeedThenFlushAlone(t *testing.T) {
|
|
|
+ w := &fakeTunWriter{gsoEnabled: true}
|
|
|
+ c := newTCPCoalescer(w)
|
|
|
+ pkt := buildTCPv4(1000, tcpAck, make([]byte, 1000))
|
|
|
+ if err := c.Add(pkt); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ // No flush yet — still pending.
|
|
|
+ if len(w.writes) != 0 || len(w.gsoWrites) != 0 {
|
|
|
+ t.Fatalf("unexpected output before flush")
|
|
|
+ }
|
|
|
+ if err := c.Flush(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ // Single segment — should use plain write, not gso.
|
|
|
+ if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
|
|
|
+ t.Fatalf("single-seg flush: writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestCoalescerCoalescesAdjacentACKs(t *testing.T) {
|
|
|
+ w := &fakeTunWriter{gsoEnabled: true}
|
|
|
+ c := newTCPCoalescer(w)
|
|
|
+ pay := make([]byte, 1200)
|
|
|
+ if err := c.Add(buildTCPv4(1000, tcpAck, pay)); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if err := c.Add(buildTCPv4(2200, tcpAck, pay)); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if err := c.Add(buildTCPv4(3400, tcpAck, pay)); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if err := c.Flush(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if len(w.gsoWrites) != 1 {
|
|
|
+ t.Fatalf("want 1 gso write, got %d (plain=%d)", len(w.gsoWrites), len(w.writes))
|
|
|
+ }
|
|
|
+ g := w.gsoWrites[0]
|
|
|
+ if g.gsoSize != 1200 {
|
|
|
+ t.Errorf("gsoSize=%d want 1200", g.gsoSize)
|
|
|
+ }
|
|
|
+ if g.hdrLen != 40 {
|
|
|
+ t.Errorf("hdrLen=%d want 40", g.hdrLen)
|
|
|
+ }
|
|
|
+ if g.csumStart != 20 {
|
|
|
+ t.Errorf("csumStart=%d want 20", g.csumStart)
|
|
|
+ }
|
|
|
+ if len(g.pkt) != 40+3*1200 {
|
|
|
+ t.Errorf("superpacket len=%d want %d", len(g.pkt), 40+3*1200)
|
|
|
+ }
|
|
|
+ // IP total length should reflect superpacket.
|
|
|
+ if tot := binary.BigEndian.Uint16(g.pkt[2:4]); int(tot) != len(g.pkt) {
|
|
|
+ t.Errorf("ip total_length=%d want %d", tot, len(g.pkt))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestCoalescerRejectsSeqGap(t *testing.T) {
|
|
|
+ w := &fakeTunWriter{gsoEnabled: true}
|
|
|
+ c := newTCPCoalescer(w)
|
|
|
+ pay := make([]byte, 1200)
|
|
|
+ if err := c.Add(buildTCPv4(1000, tcpAck, pay)); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ // seq should be 2200; use 3000 to simulate a gap.
|
|
|
+ if err := c.Add(buildTCPv4(3000, tcpAck, pay)); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if err := c.Flush(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ // First packet should have been flushed as a plain write (single seg),
|
|
|
+ // then second packet seeded and flushed likewise.
|
|
|
+ if len(w.writes) != 2 || len(w.gsoWrites) != 0 {
|
|
|
+ t.Fatalf("seq gap: want 2 plain writes got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestCoalescerRejectsFlagMismatch(t *testing.T) {
|
|
|
+ w := &fakeTunWriter{gsoEnabled: true}
|
|
|
+ c := newTCPCoalescer(w)
|
|
|
+ pay := make([]byte, 1200)
|
|
|
+ if err := c.Add(buildTCPv4(1000, tcpAck, pay)); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ // SYN flag — not admissible at all. Should flush first packet + plain-write second.
|
|
|
+ syn := buildTCPv4(2200, tcpSyn|tcpAck, pay)
|
|
|
+ if err := c.Add(syn); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if err := c.Flush(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if len(w.writes) != 2 || len(w.gsoWrites) != 0 {
|
|
|
+ t.Fatalf("flag mismatch: want 2 plain writes got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestCoalescerRejectsFIN(t *testing.T) {
|
|
|
+ w := &fakeTunWriter{gsoEnabled: true}
|
|
|
+ c := newTCPCoalescer(w)
|
|
|
+ fin := buildTCPv4(1000, tcpAck|tcpFin, []byte("x"))
|
|
|
+ if err := c.Add(fin); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if err := c.Flush(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
|
|
|
+ t.Fatalf("FIN should be passthrough, got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestCoalescerShortLastSegmentClosesChain(t *testing.T) {
|
|
|
+ w := &fakeTunWriter{gsoEnabled: true}
|
|
|
+ c := newTCPCoalescer(w)
|
|
|
+ full := make([]byte, 1200)
|
|
|
+ half := make([]byte, 500)
|
|
|
+ if err := c.Add(buildTCPv4(1000, tcpAck, full)); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if err := c.Add(buildTCPv4(2200, tcpAck, half)); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ // Next full-size would have to start at 2700 but chain is closed —
|
|
|
+ // should flush + seed.
|
|
|
+ if err := c.Add(buildTCPv4(2700, tcpAck, full)); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if err := c.Flush(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ // Expect: one gso write (first two coalesced) + one plain write (the
|
|
|
+ // third, flushed alone).
|
|
|
+ if len(w.gsoWrites) != 1 {
|
|
|
+ t.Fatalf("want 1 gso write got %d", len(w.gsoWrites))
|
|
|
+ }
|
|
|
+ if len(w.writes) != 1 {
|
|
|
+ t.Fatalf("want 1 plain write got %d", len(w.writes))
|
|
|
+ }
|
|
|
+ if w.gsoWrites[0].gsoSize != 1200 {
|
|
|
+ t.Errorf("gsoSize=%d want 1200", w.gsoWrites[0].gsoSize)
|
|
|
+ }
|
|
|
+ if got, want := len(w.gsoWrites[0].pkt), 40+1200+500; got != want {
|
|
|
+ t.Errorf("super len=%d want %d", got, want)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestCoalescerPSHFinalizesChain(t *testing.T) {
|
|
|
+ w := &fakeTunWriter{gsoEnabled: true}
|
|
|
+ c := newTCPCoalescer(w)
|
|
|
+ pay := make([]byte, 1200)
|
|
|
+ if err := c.Add(buildTCPv4(1000, tcpAck, pay)); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ // Last full-size segment with PSH — admitted but chain is now closed.
|
|
|
+ if err := c.Add(buildTCPv4(2200, tcpAckPsh, pay)); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ // Further full-size would not coalesce.
|
|
|
+ if err := c.Add(buildTCPv4(3400, tcpAck, pay)); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if err := c.Flush(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if len(w.gsoWrites) != 1 {
|
|
|
+ t.Fatalf("want 1 gso write got %d", len(w.gsoWrites))
|
|
|
+ }
|
|
|
+ if len(w.writes) != 1 {
|
|
|
+ t.Fatalf("want 1 plain write got %d", len(w.writes))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestCoalescerRejectsDifferentFlow(t *testing.T) {
|
|
|
+ w := &fakeTunWriter{gsoEnabled: true}
|
|
|
+ c := newTCPCoalescer(w)
|
|
|
+ pay := make([]byte, 1200)
|
|
|
+ p1 := buildTCPv4(1000, tcpAck, pay)
|
|
|
+ p2 := buildTCPv4(2200, tcpAck, pay)
|
|
|
+ // Mutate p2's source port to break flow match.
|
|
|
+ binary.BigEndian.PutUint16(p2[20:22], 9999)
|
|
|
+ if err := c.Add(p1); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if err := c.Add(p2); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if err := c.Flush(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ // Both flushed as plain writes.
|
|
|
+ if len(w.writes) != 2 || len(w.gsoWrites) != 0 {
|
|
|
+ t.Fatalf("diff flow: writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestCoalescerRejectsIPOptions(t *testing.T) {
|
|
|
+ w := &fakeTunWriter{gsoEnabled: true}
|
|
|
+ c := newTCPCoalescer(w)
|
|
|
+ pay := make([]byte, 500)
|
|
|
+ pkt := buildTCPv4(1000, tcpAck, pay)
|
|
|
+ // Bump IHL to 6 to simulate 4 bytes of IP options. Don't actually add
|
|
|
+ // bytes — parser should bail before it matters.
|
|
|
+ pkt[0] = 0x46
|
|
|
+ if err := c.Add(pkt); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if err := c.Flush(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
|
|
|
+ t.Fatalf("IP options should passthrough, got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestCoalescerCapBySegments(t *testing.T) {
|
|
|
+ w := &fakeTunWriter{gsoEnabled: true}
|
|
|
+ c := newTCPCoalescer(w)
|
|
|
+ pay := make([]byte, 512) // small so we can fit many before byte cap
|
|
|
+ seq := uint32(1000)
|
|
|
+ for i := 0; i < tcpCoalesceMaxSegs+5; i++ {
|
|
|
+ if err := c.Add(buildTCPv4(seq, tcpAck, pay)); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ seq += uint32(len(pay))
|
|
|
+ }
|
|
|
+ if err := c.Flush(); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ // We expect the first tcpCoalesceMaxSegs to form one gso, then 5 more:
|
|
|
+ // The 5 follow-ons seed a new super that completes as another gso if >=2,
|
|
|
+ // or a mix. Just assert we never exceed the cap per super.
|
|
|
+ for _, g := range w.gsoWrites {
|
|
|
+ segs := (len(g.pkt) - int(g.hdrLen)) / int(g.gsoSize)
|
|
|
+ if rem := (len(g.pkt) - int(g.hdrLen)) % int(g.gsoSize); rem != 0 {
|
|
|
+ segs++
|
|
|
+ }
|
|
|
+ if segs > tcpCoalesceMaxSegs {
|
|
|
+ t.Fatalf("super exceeded seg cap: %d > %d", segs, tcpCoalesceMaxSegs)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|