| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576 |
- package nebula
- import (
- "encoding/binary"
- "testing"
- )
- // fakeTunWriter records plain Writes and WriteGSO calls without touching a
- // real TUN fd. WriteGSO preserves the split between hdr and borrowed pays
- // so tests can inspect each independently.
- type fakeTunWriter struct {
- gsoEnabled bool
- writes [][]byte
- gsoWrites []fakeGSOWrite
- }
- type fakeGSOWrite struct {
- hdr []byte
- pays [][]byte
- gsoSize uint16
- isV6 bool
- csumStart uint16
- }
- // total returns hdrLen + sum of pay lens.
- func (g fakeGSOWrite) total() int {
- n := len(g.hdr)
- for _, p := range g.pays {
- n += len(p)
- }
- return n
- }
- // payLen sums the pays.
- func (g fakeGSOWrite) payLen() int {
- var n int
- for _, p := range g.pays {
- n += len(p)
- }
- return n
- }
- 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(hdr []byte, pays [][]byte, gsoSize uint16, isV6 bool, csumStart uint16) error {
- hcopy := make([]byte, len(hdr))
- copy(hcopy, hdr)
- paysCopy := make([][]byte, len(pays))
- for i, p := range pays {
- pc := make([]byte, len(p))
- copy(pc, p)
- paysCopy[i] = pc
- }
- w.gsoWrites = append(w.gsoWrites, fakeGSOWrite{
- hdr: hcopy,
- pays: paysCopy,
- gsoSize: gsoSize,
- isV6: isV6,
- 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 {
- return buildTCPv4Ports(1000, 2000, seq, flags, payload)
- }
- // buildTCPv4Ports is buildTCPv4 with caller-specified ports so tests can
- // build distinct flows.
- func buildTCPv4Ports(sport, dport uint16, seq uint32, flags byte, payload []byte) []byte {
- const ipHdrLen = 20
- const tcpHdrLen = 20
- 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], sport)
- binary.BigEndian.PutUint16(pkt[22:24], dport)
- binary.BigEndian.PutUint32(pkt[24:28], seq)
- binary.BigEndian.PutUint32(pkt[28:32], 12345)
- pkt[32] = 0x50
- pkt[33] = flags
- binary.BigEndian.PutUint16(pkt[34:36], 0xffff)
- 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)
- }
- // No sync write — passthrough is deferred to Flush.
- if len(w.writes) != 0 || len(w.gsoWrites) != 0 {
- t.Fatalf("no Add-time writes: got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
- }
- if err := c.Flush(); 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)
- 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 err := c.Flush(); 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)
- }
- 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 flush now goes through WriteGSO with GSO_NONE
- // (virtio NEEDS_CSUM lets the kernel fill in the L4 csum).
- if len(w.gsoWrites) != 1 || len(w.writes) != 0 {
- t.Fatalf("single-seg flush: writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
- }
- g := w.gsoWrites[0]
- if g.total() != 40+1000 {
- t.Errorf("super total=%d want %d", g.total(), 40+1000)
- }
- if g.payLen() != 1000 {
- t.Errorf("payLen=%d want 1000", g.payLen())
- }
- }
- 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 len(g.hdr) != 40 {
- t.Errorf("hdrLen=%d want 40", len(g.hdr))
- }
- if g.csumStart != 20 {
- t.Errorf("csumStart=%d want 20", g.csumStart)
- }
- if len(g.pays) != 3 {
- t.Errorf("pay count=%d want 3", len(g.pays))
- }
- if g.total() != 40+3*1200 {
- t.Errorf("superpacket len=%d want %d", g.total(), 40+3*1200)
- }
- if tot := binary.BigEndian.Uint16(g.hdr[2:4]); int(tot) != g.total() {
- t.Errorf("ip total_length=%d want %d", tot, g.total())
- }
- }
- 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)
- }
- if err := c.Add(buildTCPv4(3000, tcpAck, pay)); err != nil {
- t.Fatal(err)
- }
- if err := c.Flush(); err != nil {
- t.Fatal(err)
- }
- // Each packet flushes as its own single-segment WriteGSO now.
- if len(w.gsoWrites) != 2 || len(w.writes) != 0 {
- t.Fatalf("seq gap: want 2 gso 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|ACK is non-admissible. Must flush matching flow's slot (gso)
- // and then plain-write the SYN packet itself.
- 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) != 1 || len(w.gsoWrites) != 1 {
- t.Fatalf("flag mismatch: want 1 plain + 1 gso, 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)
- }
- // FIN isn't admissible — passthrough as plain, no slot, no gso.
- 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)
- }
- // Chain now closed; next packet seeds a new slot on the same flow
- // after flushing the old one.
- if err := c.Add(buildTCPv4(2700, tcpAck, full)); err != nil {
- t.Fatal(err)
- }
- if err := c.Flush(); err != nil {
- t.Fatal(err)
- }
- // Expect two gso writes: the first two packets coalesced, then the
- // third flushed alone (single-seg via GSO_NONE).
- if len(w.gsoWrites) != 2 {
- t.Fatalf("want 2 gso writes got %d", len(w.gsoWrites))
- }
- if len(w.writes) != 0 {
- t.Fatalf("want 0 plain writes got %d", len(w.writes))
- }
- if w.gsoWrites[0].gsoSize != 1200 {
- t.Errorf("gsoSize=%d want 1200", w.gsoWrites[0].gsoSize)
- }
- if got, want := w.gsoWrites[0].total(), 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)
- }
- if err := c.Add(buildTCPv4(2200, tcpAckPsh, 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)
- }
- // First two coalesce; the third seeds a fresh slot that flushes alone.
- if len(w.gsoWrites) != 2 {
- t.Fatalf("want 2 gso writes got %d", len(w.gsoWrites))
- }
- if len(w.writes) != 0 {
- t.Fatalf("want 0 plain writes 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)
- 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)
- }
- // Two independent flows, each flushes its own single-segment WriteGSO.
- if len(w.gsoWrites) != 2 || len(w.writes) != 0 {
- t.Fatalf("diff flow: want 2 gso writes got 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)
- }
- // Non-admissible parse → passthrough as plain.
- 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)
- 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)
- }
- for _, g := range w.gsoWrites {
- segs := len(g.pays)
- if segs > tcpCoalesceMaxSegs {
- t.Fatalf("super exceeded seg cap: %d > %d", segs, tcpCoalesceMaxSegs)
- }
- }
- }
- // TestCoalescerMultipleFlowsInSameBatch proves two interleaved bulk TCP
- // flows coalesce independently in a single Flush.
- func TestCoalescerMultipleFlowsInSameBatch(t *testing.T) {
- w := &fakeTunWriter{gsoEnabled: true}
- c := newTCPCoalescer(w)
- pay := make([]byte, 1200)
- // Flow A: sport 1000. Flow B: sport 3000.
- if err := c.Add(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay)); err != nil {
- t.Fatal(err)
- }
- if err := c.Add(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay)); err != nil {
- t.Fatal(err)
- }
- if err := c.Add(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay)); err != nil {
- t.Fatal(err)
- }
- if err := c.Add(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay)); err != nil {
- t.Fatal(err)
- }
- if err := c.Add(buildTCPv4Ports(1000, 2000, 2500, tcpAck, pay)); err != nil {
- t.Fatal(err)
- }
- if err := c.Add(buildTCPv4Ports(3000, 2000, 2900, tcpAck, pay)); err != nil {
- t.Fatal(err)
- }
- if err := c.Flush(); err != nil {
- t.Fatal(err)
- }
- if len(w.gsoWrites) != 2 {
- t.Fatalf("want 2 gso writes (one per flow), got %d", len(w.gsoWrites))
- }
- if len(w.writes) != 0 {
- t.Fatalf("want no plain writes, got %d", len(w.writes))
- }
- // Each superpacket should carry 3 segments.
- for i, g := range w.gsoWrites {
- if len(g.pays) != 3 {
- t.Errorf("gso[%d]: segs=%d want 3", i, len(g.pays))
- }
- if g.gsoSize != 1200 {
- t.Errorf("gso[%d]: gsoSize=%d want 1200", i, g.gsoSize)
- }
- }
- // Verify each superpacket carries the source port it was seeded with.
- seenSports := map[uint16]bool{}
- for _, g := range w.gsoWrites {
- sp := binary.BigEndian.Uint16(g.hdr[20:22])
- seenSports[sp] = true
- }
- if !seenSports[1000] || !seenSports[3000] {
- t.Errorf("expected superpackets for sports 1000 and 3000, got %v", seenSports)
- }
- }
- // TestCoalescerPreservesArrivalOrder confirms that with passthrough and
- // coalesced events both queued, Flush emits them in Add order rather than
- // writing passthrough packets synchronously.
- func TestCoalescerPreservesArrivalOrder(t *testing.T) {
- w := &orderedFakeWriter{gsoEnabled: true}
- c := newTCPCoalescer(w)
- // Sequence: coalesceable TCP, ICMP (passthrough), coalesceable TCP on
- // a different flow. Expected emit order: gso(X), plain(ICMP), gso(Y).
- pay := make([]byte, 1200)
- if err := c.Add(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay)); err != nil {
- t.Fatal(err)
- }
- icmp := make([]byte, 28)
- icmp[0] = 0x45
- binary.BigEndian.PutUint16(icmp[2:4], 28)
- icmp[9] = 1
- copy(icmp[12:16], []byte{10, 0, 0, 1})
- copy(icmp[16:20], []byte{10, 0, 0, 3})
- if err := c.Add(icmp); err != nil {
- t.Fatal(err)
- }
- if err := c.Add(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay)); err != nil {
- t.Fatal(err)
- }
- // Nothing should have hit the writer synchronously.
- if len(w.events) != 0 {
- t.Fatalf("Add emitted events synchronously: %v", w.events)
- }
- if err := c.Flush(); err != nil {
- t.Fatal(err)
- }
- if got, want := w.events, []string{"gso", "plain", "gso"}; !stringSliceEq(got, want) {
- t.Fatalf("flush order=%v want %v", got, want)
- }
- }
- // orderedFakeWriter records only the sequence of call types so tests can
- // assert arrival order without inspecting bytes.
- type orderedFakeWriter struct {
- gsoEnabled bool
- events []string
- }
- func (w *orderedFakeWriter) Write(p []byte) (int, error) {
- w.events = append(w.events, "plain")
- return len(p), nil
- }
- func (w *orderedFakeWriter) WriteGSO(hdr []byte, pays [][]byte, gsoSize uint16, isV6 bool, csumStart uint16) error {
- w.events = append(w.events, "gso")
- return nil
- }
- func (w *orderedFakeWriter) GSOSupported() bool { return w.gsoEnabled }
- func stringSliceEq(a, b []string) bool {
- if len(a) != len(b) {
- return false
- }
- for i := range a {
- if a[i] != b[i] {
- return false
- }
- }
- return true
- }
- // TestCoalescerInterleavedFlowsPreserveOrdering checks that a non-admissible
- // packet (SYN) mid-flow only flushes its own flow, not others.
- func TestCoalescerInterleavedFlowsPreserveOrdering(t *testing.T) {
- w := &fakeTunWriter{gsoEnabled: true}
- c := newTCPCoalescer(w)
- pay := make([]byte, 1200)
- // Flow A two segments.
- if err := c.Add(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay)); err != nil {
- t.Fatal(err)
- }
- if err := c.Add(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay)); err != nil {
- t.Fatal(err)
- }
- // Flow B two segments.
- if err := c.Add(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay)); err != nil {
- t.Fatal(err)
- }
- if err := c.Add(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay)); err != nil {
- t.Fatal(err)
- }
- // Flow A SYN (non-admissible) — must flush only flow A's slot.
- syn := buildTCPv4Ports(1000, 2000, 9999, tcpSyn|tcpAck, pay)
- if err := c.Add(syn); err != nil {
- t.Fatal(err)
- }
- // Flow B continues — should still be coalesced with its seed.
- if err := c.Add(buildTCPv4Ports(3000, 2000, 2900, tcpAck, pay)); err != nil {
- t.Fatal(err)
- }
- if err := c.Flush(); err != nil {
- t.Fatal(err)
- }
- // Expected:
- // - 1 gso for flow A (first 2 segments)
- // - 1 plain for flow A SYN
- // - 1 gso for flow B (3 segments)
- if len(w.gsoWrites) != 2 {
- t.Fatalf("want 2 gso writes, got %d", len(w.gsoWrites))
- }
- if len(w.writes) != 1 {
- t.Fatalf("want 1 plain write (SYN), got %d", len(w.writes))
- }
- // Find the 3-segment gso (flow B) and the 2-segment gso (flow A).
- var segCounts []int
- for _, g := range w.gsoWrites {
- segCounts = append(segCounts, len(g.pays))
- }
- if !(segCounts[0] == 2 && segCounts[1] == 3) && !(segCounts[0] == 3 && segCounts[1] == 2) {
- t.Errorf("unexpected segment counts: %v (want 2 and 3)", segCounts)
- }
- }
|