| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- package batch
- import (
- "bytes"
- "encoding/binary"
- "io"
- "testing"
- "github.com/slackhq/nebula/firewall"
- "github.com/slackhq/nebula/test"
- )
- // keySeq hands out SortKeys with ascending counters in a fixed epoch, for
- // tests where commit order IS transmission order.
- type keySeq struct {
- epoch, counter uint64
- }
- func (k *keySeq) next() SortKey {
- k.counter++
- return SortKey{Epoch: k.epoch, Counter: k.counter}
- }
- // newTestMultiCoalescer builds a batcher over w.
- func newTestMultiCoalescer(tb testing.TB, w io.Writer) *MultiCoalescer {
- tb.Helper()
- return NewMultiCoalescer(w, test.NewLogger())
- }
- // TestMultiCoalescerRoutesByProto confirms TCP/UDP/other land in the right
- // lane: TCP and UDP get coalesced when their lanes are enabled, anything
- // else (ICMP here) falls through to plain Write.
- func TestMultiCoalescerRoutesByProto(t *testing.T) {
- w := &fakeTunWriter{gsoEnabled: true}
- m := newTestMultiCoalescer(t, w)
- k := &keySeq{epoch: 1}
- tcpPay := make([]byte, 1200)
- udpPay := make([]byte, 1200)
- icmp := make([]byte, 28)
- icmp[0] = 0x45
- icmp[2] = 0
- icmp[3] = 28
- icmp[9] = 1
- if err := m.Commit(buildTCPv4(1000, tcpAck, tcpPay), k.next(), testPP(buildTCPv4(1000, tcpAck, tcpPay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildTCPv4(2200, tcpAck, tcpPay), k.next(), testPP(buildTCPv4(2200, tcpAck, tcpPay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildUDPv4(2000, 53, udpPay), k.next(), testPP(buildUDPv4(2000, 53, udpPay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildUDPv4(2000, 53, udpPay), k.next(), testPP(buildUDPv4(2000, 53, udpPay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(icmp, k.next(), testPP(icmp)); err != nil {
- t.Fatal(err)
- }
- if err := m.Flush(); err != nil {
- t.Fatal(err)
- }
- // 1 TCP super (2 segments) + 1 UDP super (2 segments) = 2 gso writes.
- if len(w.gsoWrites) != 2 {
- t.Fatalf("want 2 gso writes (one TCP + one UDP), got %d", len(w.gsoWrites))
- }
- if len(w.writes) != 1 {
- t.Fatalf("want 1 plain write (ICMP), got %d", len(w.writes))
- }
- }
- // TestMultiCoalescerRestoresTransmissionOrder is the core staging-sort
- // property: packets committed out of counter order (wire reorder inside one
- // flush batch) are replayed into the lanes in transmission order, so the
- // reorder never fragments the coalesce chain — one superpacket, in seq
- // order, exactly as if the wire had never reordered. The retransmit shape
- // falls out of the same key: a retransmit carries a lower seq but a HIGHER
- // counter (it was encrypted later), so it emits after the data it trails.
- func TestMultiCoalescerRestoresTransmissionOrder(t *testing.T) {
- w := &fakeTunWriter{gsoEnabled: true}
- m := newTestMultiCoalescer(t, w)
- pay := make([]byte, 1200)
- // Transmission order: seq 1000 (c1), 2200 (c2), 3400 (c3).
- // Arrival order: 3400, 1000, 2200.
- if err := m.Commit(buildTCPv4(3400, tcpAck, pay), SortKey{Epoch: 1, Counter: 3}, testPP(buildTCPv4(3400, tcpAck, pay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildTCPv4(1000, tcpAck, pay), SortKey{Epoch: 1, Counter: 1}, testPP(buildTCPv4(1000, tcpAck, pay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildTCPv4(2200, tcpAck, pay), SortKey{Epoch: 1, Counter: 2}, testPP(buildTCPv4(2200, tcpAck, pay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Flush(); err != nil {
- t.Fatal(err)
- }
- if len(w.gsoWrites) != 1 || len(w.writes) != 0 {
- t.Fatalf("want 1 gso write (unfragmented chain), got gso=%d plain=%d", len(w.gsoWrites), len(w.writes))
- }
- g := w.gsoWrites[0]
- if len(g.pays) != 3 {
- t.Fatalf("segs=%d want 3", len(g.pays))
- }
- const ipHdrLen = 20
- if seedSeq := binary.BigEndian.Uint32(g.hdr[ipHdrLen+4 : ipHdrLen+8]); seedSeq != 1000 {
- t.Errorf("seed seq=%d want 1000", seedSeq)
- }
- // Retransmit: seq 1000 again but counter 4 — sorts after seq 4600 (c3).
- w.writes, w.gsoWrites, w.order = nil, nil, nil
- if err := m.Commit(buildTCPv4(1000, tcpAck, pay), SortKey{Epoch: 1, Counter: 4}, testPP(buildTCPv4(1000, tcpAck, pay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildTCPv4(4600, tcpAck, pay), SortKey{Epoch: 1, Counter: 3}, testPP(buildTCPv4(4600, tcpAck, pay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Flush(); err != nil {
- t.Fatal(err)
- }
- if len(w.writes) != 2 {
- t.Fatalf("want 2 plain writes, got %d (gso=%d)", len(w.writes), len(w.gsoWrites))
- }
- first := binary.BigEndian.Uint32(w.writes[0][24:28])
- second := binary.BigEndian.Uint32(w.writes[1][24:28])
- if first != 4600 || second != 1000 {
- t.Fatalf("emission (%d, %d), want (4600, 1000): retransmit must not overtake in-flight data", first, second)
- }
- }
- // TestMultiCoalescerRestoresOrderAcrossFlows scrambles two interleaved flows;
- // the staging sort must repair each flow into one superpacket without any
- // cross-flow contamination.
- func TestMultiCoalescerRestoresOrderAcrossFlows(t *testing.T) {
- w := &fakeTunWriter{gsoEnabled: true}
- m := newTestMultiCoalescer(t, w)
- pay := make([]byte, 1200)
- // Transmission: A.100 (c1), B.500 (c2), A.1300 (c3), B.1700 (c4).
- // Arrival: A.1300, B.1700, A.100, B.500.
- if err := m.Commit(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay), SortKey{Epoch: 1, Counter: 3}, testPP(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay), SortKey{Epoch: 1, Counter: 4}, testPP(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay), SortKey{Epoch: 1, Counter: 1}, testPP(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay), SortKey{Epoch: 1, Counter: 2}, testPP(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Flush(); err != nil {
- t.Fatal(err)
- }
- if len(w.gsoWrites) != 2 {
- t.Fatalf("want 2 gso writes (one per flow), got %d (plain=%d)", len(w.gsoWrites), len(w.writes))
- }
- for i, g := range w.gsoWrites {
- if len(g.pays) != 2 {
- t.Errorf("gso[%d] segs=%d want 2", i, len(g.pays))
- }
- const ipHdrLen = 20
- seedSeq := binary.BigEndian.Uint32(g.hdr[ipHdrLen+4 : ipHdrLen+8])
- sport := binary.BigEndian.Uint16(g.hdr[ipHdrLen : ipHdrLen+2])
- switch sport {
- case 1000:
- if seedSeq != 100 {
- t.Errorf("flow A seed seq=%d want 100", seedSeq)
- }
- case 3000:
- if seedSeq != 500 {
- t.Errorf("flow B seed seq=%d want 500", seedSeq)
- }
- default:
- t.Errorf("unexpected sport %d", sport)
- }
- }
- }
- // TestMultiCoalescerEpochOrdersAcrossRehandshake: a re-handshake replaces
- // the tunnel, and the replacement's counter space starts near zero — raw
- // counter order would emit the new tunnel's packets first while the old
- // tunnel's backlog is still arriving. The epoch key must dominate:
- // everything from the old tunnel emits before anything from the new one.
- func TestMultiCoalescerEpochOrdersAcrossRehandshake(t *testing.T) {
- w := &fakeTunWriter{gsoEnabled: true}
- m := newTestMultiCoalescer(t, w)
- pay := make([]byte, 1200)
- // New session's first data arrives before the old session's last data.
- if err := m.Commit(buildTCPv4(2200, tcpAck, pay), SortKey{Epoch: 8, Counter: 1}, testPP(buildTCPv4(2200, tcpAck, pay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildTCPv4(1000, tcpAck, pay), SortKey{Epoch: 7, Counter: 9_000_000}, testPP(buildTCPv4(1000, tcpAck, pay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Flush(); err != nil {
- t.Fatal(err)
- }
- // Same flow, contiguous seq, identical headers: after the epoch sort the
- // two segments append into one superpacket seeded by the OLD session's
- // packet.
- if len(w.gsoWrites) != 1 {
- t.Fatalf("want 1 gso write, got %d (plain=%d)", len(w.gsoWrites), len(w.writes))
- }
- const ipHdrLen = 20
- if seedSeq := binary.BigEndian.Uint32(w.gsoWrites[0].hdr[ipHdrLen+4 : ipHdrLen+8]); seedSeq != 1000 {
- t.Errorf("seed seq=%d want 1000 (old session first)", seedSeq)
- }
- }
- // TestMultiCoalescerNoUSOFallsThrough verifies that on a queue without USO
- // (older kernel: TSO but no GSO_UDP_L4) the UDP lane never comes up and UDP
- // packets still reach the kernel via verbatim rather than being lost.
- func TestMultiCoalescerNoUSOFallsThrough(t *testing.T) {
- w := &fakeTunWriter{gsoEnabled: true, noUSO: true}
- m := newTestMultiCoalescer(t, w)
- k := &keySeq{epoch: 1}
- if m.udp != nil {
- t.Fatal("UDP lane must not come up without USO")
- }
- if err := m.Commit(buildUDPv4(1000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv4(1000, 53, make([]byte, 800)))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildUDPv4(1000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv4(1000, 53, make([]byte, 800)))); err != nil {
- t.Fatal(err)
- }
- if err := m.Flush(); err != nil {
- t.Fatal(err)
- }
- if len(w.gsoWrites) != 0 {
- t.Errorf("UDP must NOT be coalesced when USO disabled, got %d gso writes", len(w.gsoWrites))
- }
- if len(w.writes) != 2 {
- t.Errorf("UDP must pass through as 2 plain writes, got %d", len(w.writes))
- }
- }
- // TestMultiCoalescerNoOffloadsStillSorts covers a queue that can't offload
- // anything. Both lane constructors refuse, so every packet rides the
- // verbatim lane — but the staging sort still applies, so emission follows
- // transmission order even without GSO.
- func TestMultiCoalescerNoOffloadsStillSorts(t *testing.T) {
- w := &fakeTunWriter{gsoEnabled: false}
- m := newTestMultiCoalescer(t, w)
- if m.tcp != nil || m.udp != nil {
- t.Fatal("no lane may come up without offloads")
- }
- pkts := [][]byte{
- buildTCPv4(1000, tcpAck, make([]byte, 1200)),
- buildUDPv4(1000, 53, make([]byte, 800)),
- buildTCPv4(2200, tcpAck, make([]byte, 1200)),
- }
- // Committed in reverse transmission order; keys carry the truth.
- for i := len(pkts) - 1; i >= 0; i-- {
- if err := m.Commit(pkts[i], SortKey{Epoch: 1, Counter: uint64(i + 1)}, testPP(pkts[i])); err != nil {
- t.Fatal(err)
- }
- }
- if err := m.Flush(); err != nil {
- t.Fatal(err)
- }
- if len(w.gsoWrites) != 0 {
- t.Errorf("no GSO writes possible, got %d", len(w.gsoWrites))
- }
- if len(w.writes) != len(pkts) {
- t.Fatalf("want %d plain writes, got %d", len(pkts), len(w.writes))
- }
- // One lane for everything means the sorted order survives end to end.
- for i, want := range pkts {
- if !bytes.Equal(w.writes[i], want) {
- t.Errorf("write %d out of order or corrupt", i)
- }
- }
- }
- // buildUDPv6Fragment builds an IPv6 packet whose extension chain is a
- // single fragment header (NH=44) naming UDP as the terminal protocol —
- // a first fragment (offset 0, MF set) carrying the UDP header and a
- // partial payload.
- func buildUDPv6Fragment(sport, dport uint16, payload []byte) []byte {
- const ipHdrLen = 40
- const fragHdrLen = 8
- const udpHdrLen = 8
- total := ipHdrLen + fragHdrLen + udpHdrLen + len(payload)
- pkt := make([]byte, total)
- pkt[0] = 0x60
- binary.BigEndian.PutUint16(pkt[4:6], uint16(total-ipHdrLen))
- pkt[6] = 44 // fragment extension header
- pkt[7] = 64
- pkt[8] = 0xfe
- pkt[9] = 0x80
- pkt[23] = 1
- pkt[24] = 0xfe
- pkt[25] = 0x80
- pkt[39] = 2
- pkt[40] = ipProtoUDP // fragment's next header
- binary.BigEndian.PutUint16(pkt[42:44], 0x0001) // offset 0, MF set
- binary.BigEndian.PutUint32(pkt[44:48], 0x1badf00) // identification
- binary.BigEndian.PutUint16(pkt[48:50], sport)
- binary.BigEndian.PutUint16(pkt[50:52], dport)
- binary.BigEndian.PutUint16(pkt[52:54], uint16(udpHdrLen+len(payload)))
- copy(pkt[56:], payload)
- return pkt
- }
- // TestMultiCoalescerIPv6FragmentStaysInLane locks in extension-header
- // routing: a fragment whose chain terminates in UDP must ride the UDP lane
- // as an in-lane verbatim — emitted ahead of later same-flow datagrams —
- // not the verbatim lane, which flushes after every coalescer lane and
- // would reorder it behind data that arrived after it.
- func TestMultiCoalescerIPv6FragmentStaysInLane(t *testing.T) {
- w := &fakeTunWriter{gsoEnabled: true}
- m := newTestMultiCoalescer(t, w)
- k := &keySeq{epoch: 1}
- if err := m.Commit(buildUDPv6Fragment(2000, 53, make([]byte, 512)), k.next(), testPP(buildUDPv6Fragment(2000, 53, make([]byte, 512)))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
- t.Fatal(err)
- }
- if err := m.Flush(); err != nil {
- t.Fatal(err)
- }
- if len(w.writes) != 1 {
- t.Fatalf("want the fragment as 1 plain write, got %d", len(w.writes))
- }
- if len(w.gsoWrites) != 1 {
- t.Fatalf("want the two whole datagrams coalesced into 1 gso write, got %d", len(w.gsoWrites))
- }
- // Transmission order was fragment-then-data; same-lane routing must keep it.
- if w.order[0] != "write" {
- t.Fatalf("fragment must be emitted before later data (in-lane verbatim), order=%v", w.order)
- }
- }
- // TestMultiCoalescerFragmentSealsUDPChains: an unparseable datagram
- // (fragment) seals every open UDP chain, so datagrams from before and after
- // it land in separate superpackets and the fragment holds its transmission-
- // order position between them.
- func TestMultiCoalescerFragmentSealsUDPChains(t *testing.T) {
- w := &fakeTunWriter{gsoEnabled: true}
- m := newTestMultiCoalescer(t, w)
- k := &keySeq{epoch: 1}
- if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildUDPv6Fragment(2000, 53, make([]byte, 512)), k.next(), testPP(buildUDPv6Fragment(2000, 53, make([]byte, 512)))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
- t.Fatal(err)
- }
- if err := m.Flush(); err != nil {
- t.Fatal(err)
- }
- if len(w.gsoWrites) != 2 {
- t.Fatalf("want 2 gso writes (chains sealed around the fragment), got %d", len(w.gsoWrites))
- }
- if len(w.writes) != 1 {
- t.Fatalf("want the fragment as 1 plain write, got %d", len(w.writes))
- }
- want := []string{"gso", "write", "gso"}
- if len(w.order) != 3 || w.order[0] != want[0] || w.order[1] != want[1] || w.order[2] != want[2] {
- t.Fatalf("emission order = %v, want %v", w.order, want)
- }
- }
- // TestMultiCoalescerNoTSOFallsThrough mirrors the no-TSO case.
- func TestMultiCoalescerNoTSOFallsThrough(t *testing.T) {
- w := &fakeTunWriter{gsoEnabled: true, noTSO: true}
- m := newTestMultiCoalescer(t, w)
- k := &keySeq{epoch: 1}
- if m.tcp != nil {
- t.Fatal("TCP lane must not come up without TSO")
- }
- pay := make([]byte, 1200)
- if err := m.Commit(buildTCPv4(1000, tcpAck, pay), k.next(), testPP(buildTCPv4(1000, tcpAck, pay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Commit(buildTCPv4(2200, tcpAck, pay), k.next(), testPP(buildTCPv4(2200, tcpAck, pay))); err != nil {
- t.Fatal(err)
- }
- if err := m.Flush(); err != nil {
- t.Fatal(err)
- }
- if len(w.gsoWrites) != 0 {
- t.Errorf("TCP must NOT be coalesced when TSO disabled, got %d gso writes", len(w.gsoWrites))
- }
- if len(w.writes) != 2 {
- t.Errorf("TCP must pass through as 2 plain writes, got %d", len(w.writes))
- }
- }
- // testPP derives the ParsedPacket newPacket would produce for the packet
- // shapes the tests build: plain v4/v6, v4 with options or fragment bits set,
- // and the single-fragment-header v6 shape from buildUDPv6Fragment. Anything
- // unrecognizable stays zero (proto 0 routes to the passthrough lane).
- func testPP(pkt []byte) *firewall.ParsedPacket {
- pp := &firewall.ParsedPacket{}
- if len(pkt) < 20 {
- return pp
- }
- switch pkt[0] >> 4 {
- case 4:
- pp.Protocol = pkt[9]
- pp.IPHdrLen = int(pkt[0]&0x0f) * 4
- pp.FragAny = binary.BigEndian.Uint16(pkt[6:8])&0x3fff != 0
- case 6:
- pp.Protocol = pkt[6]
- pp.IPHdrLen = 40
- if pp.Protocol == 44 { // fragment extension header
- pp.Protocol = pkt[40]
- pp.IPHdrLen = 48
- pp.FragAny = true
- }
- }
- return pp
- }
|