mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 18:37:00 +02:00
226 lines
7.1 KiB
Go
226 lines
7.1 KiB
Go
package batch
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"io"
|
|
"testing"
|
|
|
|
"github.com/slackhq/nebula/test"
|
|
)
|
|
|
|
// newTestMultiCoalescer builds a batcher over w and asserts it really is
|
|
// multi-lane. NewMultiCoalescer collapses to a bare Passthrough when w can
|
|
// offload neither protocol, and a test that meant to exercise a lane would
|
|
// otherwise pass vacuously.
|
|
func newTestMultiCoalescer(tb testing.TB, w io.Writer) *MultiCoalescer {
|
|
tb.Helper()
|
|
b := NewMultiCoalescer(w, test.NewLogger())
|
|
m, ok := b.(*MultiCoalescer)
|
|
if !ok {
|
|
tb.Fatalf("want a *MultiCoalescer, got %T", b)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// 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)
|
|
|
|
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)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Commit(buildTCPv4(2200, tcpAck, tcpPay)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Commit(buildUDPv4(2000, 53, udpPay)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Commit(buildUDPv4(2000, 53, udpPay)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Commit(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))
|
|
}
|
|
}
|
|
|
|
// 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 passthrough rather than being lost.
|
|
func TestMultiCoalescerNoUSOFallsThrough(t *testing.T) {
|
|
w := &fakeTunWriter{gsoEnabled: true, noUSO: true}
|
|
m := newTestMultiCoalescer(t, w)
|
|
if m.udp != nil {
|
|
t.Fatal("UDP lane must not come up without USO")
|
|
}
|
|
|
|
if err := m.Commit(buildUDPv4(1000, 53, make([]byte, 800))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Commit(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))
|
|
}
|
|
}
|
|
|
|
// TestMultiCoalescerNoOffloadsIsPassthrough covers a queue that can't offload
|
|
// anything. Both lane constructors refuse, so there's nothing left to
|
|
// dispatch between and NewMultiCoalescer hands back the passthrough lane
|
|
// itself — no wrapper, no per-packet protocol demux, and every packet reaches
|
|
// the kernel in arrival order. This is the case Interface.activate used to
|
|
// special-case with a bare Passthrough.
|
|
func TestMultiCoalescerNoOffloadsIsPassthrough(t *testing.T) {
|
|
w := &fakeTunWriter{gsoEnabled: false}
|
|
m := NewMultiCoalescer(w, test.NewLogger())
|
|
|
|
if _, ok := m.(*Passthrough); !ok {
|
|
t.Fatalf("want a bare *Passthrough when neither offload is available, got %T", m)
|
|
}
|
|
pkts := [][]byte{
|
|
buildTCPv4(1000, tcpAck, make([]byte, 1200)),
|
|
buildUDPv4(1000, 53, make([]byte, 800)),
|
|
buildTCPv4(2200, tcpAck, make([]byte, 1200)),
|
|
}
|
|
for _, p := range pkts {
|
|
if err := m.Commit(p); 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 arrival 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 passthrough — emitted ahead of later same-flow datagrams —
|
|
// not the passthrough 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)
|
|
|
|
if err := m.Commit(buildUDPv6Fragment(2000, 53, make([]byte, 512))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Commit(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))
|
|
}
|
|
// Arrival 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 passthrough), order=%v", w.order)
|
|
}
|
|
}
|
|
|
|
// TestMultiCoalescerNoTSOFallsThrough mirrors the no-TSO case.
|
|
func TestMultiCoalescerNoTSOFallsThrough(t *testing.T) {
|
|
w := &fakeTunWriter{gsoEnabled: true, noTSO: true}
|
|
m := newTestMultiCoalescer(t, w)
|
|
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)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Commit(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))
|
|
}
|
|
}
|