package nebula import ( "net" "net/netip" "sync" "testing" "time" "github.com/gaissmai/bart" "github.com/google/gopacket" "github.com/google/gopacket/layers" "github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/firewall" "github.com/slackhq/nebula/firewall/events" "github.com/slackhq/nebula/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // recordingReporter captures every event fired against it. Its methods take // the conntrack lock implicitly (via the firewall code path that invokes // them), so we synchronize accumulator mutations with a small mutex to keep // the race detector happy across goroutines in case a test introduces any. type recordingReporter struct { mu sync.Mutex drops []recordedDrop creates []recordedCreate evicts []recordedEvict reloads []recordedReload } type recordedDrop struct { incoming bool reason events.DropReason remote netip.Addr local netip.Addr peerName string rulesVersion uint16 ctx firewall.PacketContext } type recordedCreate struct { incoming bool remote netip.Addr local netip.Addr peerName string rulesVersion uint16 ctx firewall.PacketContext } type recordedEvict struct { incoming bool remote netip.Addr local netip.Addr rulesVersion uint16 expired bool } type recordedReload struct { oldVersion uint16 newVersion uint16 } func (r *recordingReporter) ReportDrop(e events.DropEvent) { r.mu.Lock() defer r.mu.Unlock() name := "" if e.PeerCert != nil && e.PeerCert.Certificate != nil { name = e.PeerCert.Certificate.Name() } r.drops = append(r.drops, recordedDrop{ incoming: e.Incoming, reason: e.Reason, remote: e.Packet.RemoteAddr, local: e.Packet.LocalAddr, peerName: name, rulesVersion: e.RulesVersion, ctx: e.Context, }) } func (r *recordingReporter) ReportFlowCreate(e events.FlowCreateEvent) { r.mu.Lock() defer r.mu.Unlock() name := "" if e.PeerCert != nil && e.PeerCert.Certificate != nil { name = e.PeerCert.Certificate.Name() } r.creates = append(r.creates, recordedCreate{ incoming: e.Incoming, remote: e.Packet.RemoteAddr, local: e.Packet.LocalAddr, peerName: name, rulesVersion: e.RulesVersion, ctx: e.Context, }) } func (r *recordingReporter) ReportFlowEvict(e events.FlowEvictEvent) { r.mu.Lock() defer r.mu.Unlock() r.evicts = append(r.evicts, recordedEvict{ incoming: e.Incoming, remote: e.Packet.RemoteAddr, local: e.Packet.LocalAddr, rulesVersion: e.RulesVersion, expired: e.Expired, }) } func (r *recordingReporter) ReportRulesReload(e events.RulesReloadEvent) { r.mu.Lock() defer r.mu.Unlock() r.reloads = append(r.reloads, recordedReload{ oldVersion: e.OldVersion, newVersion: e.NewVersion, }) } // eventFixture builds a Firewall wired to a Control plus a packet/hostinfo // pair that a test can reuse. By default the ruleset allows the packet; // callers mutate fw / p / h as needed before invoking Drop. type eventFixture struct { ctl *Control fw *Firewall p firewall.Packet h *HostInfo cp *cert.CAPool } func newEventFixture(t *testing.T) *eventFixture { t.Helper() l := test.NewLogger() // myVpnNetworksTable covers our single peer address so buildNetworks takes // the "simple case" path (h.networks stays nil); tests that want a populated // BART table overwrite h.networks directly. vpnNetworks := new(bart.Lite) vpnNetworks.Insert(netip.MustParsePrefix("1.2.3.0/24")) // Use the same cert for "peer" and "local" endpoints, matching the // TestFirewall_Drop fixture style: LocalAddr == RemoteAddr == peer vpn addr. c := &dummyCert{ name: "host1", networks: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/24")}, groups: []string{"default-group"}, issuer: "signer-shasum", } h := &HostInfo{ ConnectionState: &ConnectionState{ peerCert: &cert.CachedCertificate{ Certificate: c, InvertedGroups: map[string]struct{}{"default-group": {}}, }, }, vpnAddrs: []netip.Addr{netip.MustParseAddr("1.2.3.4")}, } h.buildNetworks(vpnNetworks, c) fw := NewFirewall(l, time.Minute, time.Minute, time.Minute, c) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", "")) require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", "")) ctl := &Control{ f: &Interface{firewall: fw}, l: l, } return &eventFixture{ ctl: ctl, fw: fw, p: firewall.Packet{ LocalAddr: netip.MustParseAddr("1.2.3.4"), RemoteAddr: netip.MustParseAddr("1.2.3.4"), LocalPort: 10, RemotePort: 90, Protocol: firewall.ProtoUDP, }, h: h, cp: cert.NewCAPool(), } } // firewall() returns the currently-installed firewall. Needed because // SetFirewallEventReporter replaces it via shallow-copy swap. func (f *eventFixture) firewall() *Firewall { return f.ctl.f.firewall } func TestEvents_ReportDrop_InvalidRemoteIP(t *testing.T) { f := newEventFixture(t) r := &recordingReporter{} f.ctl.SetFirewallEventReporter(r) // Packet to an address not in the cert's networks. f.p.RemoteAddr = netip.MustParseAddr("9.9.9.9") assert.Equal(t, ErrInvalidRemoteIP, f.firewall().Drop(f.p, firewall.PacketContext{}, false, f.h, f.cp, nil)) require.Len(t, r.drops, 1) assert.Equal(t, events.DropInvalidRemoteIP, r.drops[0].reason) assert.False(t, r.drops[0].incoming) assert.Equal(t, "host1", r.drops[0].peerName) assert.Empty(t, r.creates) assert.Empty(t, r.evicts) } func TestEvents_ReportDrop_InvalidLocalIP(t *testing.T) { f := newEventFixture(t) r := &recordingReporter{} f.ctl.SetFirewallEventReporter(r) // LocalAddr outside our routable networks. f.p.LocalAddr = netip.MustParseAddr("9.9.9.9") assert.Equal(t, ErrInvalidLocalIP, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)) require.Len(t, r.drops, 1) assert.Equal(t, events.DropInvalidLocalIP, r.drops[0].reason) assert.True(t, r.drops[0].incoming) } func TestEvents_ReportDrop_NoMatchingRule(t *testing.T) { f := newEventFixture(t) // Reset to a firewall with no matching rule. l := test.NewLogger() fw := NewFirewall(l, time.Minute, time.Minute, time.Minute, f.h.ConnectionState.peerCert.Certificate) // Rule that won't match (group not in peer's groups). require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "")) require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "")) f.ctl.f.firewall = fw r := &recordingReporter{} f.ctl.SetFirewallEventReporter(r) assert.Equal(t, ErrNoMatchingRule, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)) require.Len(t, r.drops, 1) assert.Equal(t, events.DropNoMatchingRule, r.drops[0].reason) } func TestEvents_ReportDrop_PeerRejected(t *testing.T) { f := newEventFixture(t) // Re-classify the remote as VPNPeer so it triggers DropPeerRejected. f.h.networks = new(bart.Table[NetworkType]) f.h.networks.Insert(netip.MustParsePrefix("1.2.3.0/24"), NetworkTypeVPNPeer) r := &recordingReporter{} f.ctl.SetFirewallEventReporter(r) assert.Equal(t, ErrPeerRejected, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)) require.Len(t, r.drops, 1) assert.Equal(t, events.DropPeerRejected, r.drops[0].reason) } func TestEvents_ReportDrop_UnknownNetwork(t *testing.T) { f := newEventFixture(t) // Insert an unrecognized NetworkType value to hit the default branch. f.h.networks = new(bart.Table[NetworkType]) f.h.networks.Insert(netip.MustParsePrefix("1.2.3.0/24"), NetworkTypeUnknown) r := &recordingReporter{} f.ctl.SetFirewallEventReporter(r) assert.Equal(t, ErrUnknownNetworkType, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)) require.Len(t, r.drops, 1) assert.Equal(t, events.DropUnknownNetwork, r.drops[0].reason) } func TestEvents_ReportFlowCreate_OnceOnly(t *testing.T) { f := newEventFixture(t) r := &recordingReporter{} f.ctl.SetFirewallEventReporter(r) // First allowed packet creates the conntrack entry. require.NoError(t, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)) // Second matching packet on the same tuple is short-circuited by conntrack // and must not fire another FlowCreate. require.NoError(t, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)) require.Len(t, r.creates, 1) assert.True(t, r.creates[0].incoming) assert.Equal(t, f.p.RemoteAddr, r.creates[0].remote) assert.Empty(t, r.drops) } func TestEvents_ReportFlowEvict_OnReloadPurge(t *testing.T) { f := newEventFixture(t) r := &recordingReporter{} f.ctl.SetFirewallEventReporter(r) // Create a flow under the current rules. require.NoError(t, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)) require.Len(t, r.creates, 1) // Simulate a reload that produces rules the existing flow no longer // matches. Bump rulesVersion and replace InRules with an empty table so // revalidation fails. fw := f.firewall() fw.Conntrack.Lock() fw.rulesVersion++ fw.InRules = newFirewallTable() fw.Conntrack.Unlock() // Next packet triggers re-validation, which fails and evicts the entry. err := fw.Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil) assert.Equal(t, ErrNoMatchingRule, err) require.Len(t, r.evicts, 1) assert.False(t, r.evicts[0].expired, "evict from reload purge is not expiration") assert.True(t, r.evicts[0].incoming) } func TestEvents_ReportFlowEvict_OnTimeout(t *testing.T) { f := newEventFixture(t) r := &recordingReporter{} f.ctl.SetFirewallEventReporter(r) require.NoError(t, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)) require.Len(t, r.creates, 1) // Force expiration by rewinding the entry's deadline. fw := f.firewall() fw.Conntrack.Lock() c := fw.Conntrack.Conns[f.p] require.NotNil(t, c) c.Expires = time.Now().Add(-time.Hour) fw.evict(f.p) fw.Conntrack.Unlock() require.Len(t, r.evicts, 1) assert.True(t, r.evicts[0].expired) } func TestEvents_SetNil_Clears(t *testing.T) { f := newEventFixture(t) r := &recordingReporter{} f.ctl.SetFirewallEventReporter(r) require.NoError(t, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)) require.Len(t, r.creates, 1) f.ctl.SetFirewallEventReporter(nil) resetConntrack(f.firewall()) require.NoError(t, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)) // No second create should be recorded. assert.Len(t, r.creates, 1) } func TestEvents_ReporterSurvivesSwap(t *testing.T) { f := newEventFixture(t) r := &recordingReporter{} f.ctl.SetFirewallEventReporter(r) // Simulate a reload by swapping in a fresh Firewall that carries the // reporter forward. Mirrors what reloadFirewall does with the shared // conntrack pointer. l := test.NewLogger() oldFw := f.firewall() newFw := NewFirewall(l, time.Minute, time.Minute, time.Minute, f.h.ConnectionState.peerCert.Certificate) require.NoError(t, newFw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", "")) require.NoError(t, newFw.AddRule(false, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", "")) newFw.Conntrack = oldFw.Conntrack newFw.rulesVersion = oldFw.rulesVersion + 1 newFw.reporter = oldFw.reporter f.ctl.f.firewall = newFw newFw.reportRulesReload(oldFw.rulesVersion, newFw.rulesVersion) require.Len(t, r.reloads, 1) assert.Equal(t, oldFw.rulesVersion, r.reloads[0].oldVersion) assert.Equal(t, newFw.rulesVersion, r.reloads[0].newVersion) // Events on the new firewall should still reach the same reporter. require.NoError(t, newFw.Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)) require.Len(t, r.creates, 1) assert.Equal(t, newFw.rulesVersion, r.creates[0].rulesVersion) } func TestEvents_InstallDoesNotMutateOldFirewall(t *testing.T) { f := newEventFixture(t) before := f.firewall() r := &recordingReporter{} f.ctl.SetFirewallEventReporter(r) after := f.firewall() assert.NotSame(t, before, after, "SetFirewallEventReporter must replace the Firewall pointer") assert.Nil(t, before.reporter, "the pre-install Firewall must remain untouched") assert.NotNil(t, after.reporter) } // --- PacketContext parse tests -------------------------------------------- func mustSerialize(t *testing.T, lrs ...gopacket.SerializableLayer) []byte { t.Helper() buf := gopacket.NewSerializeBuffer() opt := gopacket.SerializeOptions{ComputeChecksums: false, FixLengths: true} require.NoError(t, gopacket.SerializeLayers(buf, opt, lrs...)) return buf.Bytes() } func TestPacketContext_IPv4_TCPFlags(t *testing.T) { ip := &layers.IPv4{ Version: 4, TTL: 64, Protocol: layers.IPProtocolTCP, SrcIP: net.IPv4(10, 0, 0, 1), DstIP: net.IPv4(10, 0, 0, 2), } tcp := &layers.TCP{SrcPort: 1234, DstPort: 80, SYN: true, ACK: true} require.NoError(t, tcp.SetNetworkLayerForChecksum(ip)) data := mustSerialize(t, ip, tcp, gopacket.Payload([]byte("hello"))) var fp firewall.Packet var ctx firewall.PacketContext require.NoError(t, newPacket(data, true, &fp, &ctx)) assert.Equal(t, uint8(firewall.ProtoTCP), fp.Protocol) // SYN (0x02) + ACK (0x10) = 0x12 assert.Equal(t, uint8(0x12), ctx.TCPFlags) assert.Equal(t, uint16(len(data)), ctx.Length) assert.Equal(t, uint8(0), ctx.ICMPType) assert.Equal(t, uint8(0), ctx.ICMPCode) } func TestPacketContext_IPv4_ICMPTypeCode(t *testing.T) { ip := &layers.IPv4{ Version: 4, TTL: 64, Protocol: layers.IPProtocolICMPv4, SrcIP: net.IPv4(10, 0, 0, 1), DstIP: net.IPv4(10, 0, 0, 2), } // Destination Unreachable, code 3 (port unreachable) icmp := &layers.ICMPv4{ TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeDestinationUnreachable, layers.ICMPv4CodePort), } data := mustSerialize(t, ip, icmp, gopacket.Payload([]byte{0, 0, 0, 0})) var fp firewall.Packet var ctx firewall.PacketContext require.NoError(t, newPacket(data, true, &fp, &ctx)) assert.Equal(t, uint8(firewall.ProtoICMP), fp.Protocol) assert.Equal(t, uint8(layers.ICMPv4TypeDestinationUnreachable), ctx.ICMPType) assert.Equal(t, uint8(layers.ICMPv4CodePort), ctx.ICMPCode) assert.Equal(t, uint16(len(data)), ctx.Length) assert.Equal(t, uint8(0), ctx.TCPFlags) } func TestPacketContext_IPv4_UDPLengthOnly(t *testing.T) { ip := &layers.IPv4{ Version: 4, TTL: 64, Protocol: layers.IPProtocolUDP, SrcIP: net.IPv4(10, 0, 0, 1), DstIP: net.IPv4(10, 0, 0, 2), } udp := &layers.UDP{SrcPort: 1234, DstPort: 53} require.NoError(t, udp.SetNetworkLayerForChecksum(ip)) data := mustSerialize(t, ip, udp, gopacket.Payload([]byte("query"))) var fp firewall.Packet var ctx firewall.PacketContext require.NoError(t, newPacket(data, true, &fp, &ctx)) assert.Equal(t, uint8(firewall.ProtoUDP), fp.Protocol) assert.Equal(t, uint16(len(data)), ctx.Length) assert.Zero(t, ctx.TCPFlags) assert.Zero(t, ctx.ICMPType) assert.Zero(t, ctx.ICMPCode) } func TestPacketContext_IPv6_TCPFlags(t *testing.T) { ip := &layers.IPv6{ Version: 6, HopLimit: 64, NextHeader: layers.IPProtocolTCP, SrcIP: net.ParseIP("fd00::1"), DstIP: net.ParseIP("fd00::2"), } tcp := &layers.TCP{SrcPort: 1234, DstPort: 443, FIN: true, ACK: true} require.NoError(t, tcp.SetNetworkLayerForChecksum(ip)) data := mustSerialize(t, ip, tcp, gopacket.Payload([]byte("bye"))) var fp firewall.Packet var ctx firewall.PacketContext require.NoError(t, newPacket(data, true, &fp, &ctx)) assert.Equal(t, uint8(firewall.ProtoTCP), fp.Protocol) // FIN (0x01) + ACK (0x10) = 0x11 assert.Equal(t, uint8(0x11), ctx.TCPFlags) assert.Equal(t, uint16(len(data)), ctx.Length) } func TestPacketContext_IPv6_ICMPv6TypeCode(t *testing.T) { ip := &layers.IPv6{ Version: 6, HopLimit: 64, NextHeader: layers.IPProtocolICMPv6, SrcIP: net.ParseIP("fd00::1"), DstIP: net.ParseIP("fd00::2"), } icmp := &layers.ICMPv6{ TypeCode: layers.CreateICMPv6TypeCode(layers.ICMPv6TypeDestinationUnreachable, layers.ICMPv6CodePortUnreachable), } require.NoError(t, icmp.SetNetworkLayerForChecksum(ip)) data := mustSerialize(t, ip, icmp, gopacket.Payload([]byte{0, 0, 0, 0, 0, 0, 0, 0})) var fp firewall.Packet var ctx firewall.PacketContext require.NoError(t, newPacket(data, true, &fp, &ctx)) assert.Equal(t, uint8(firewall.ProtoICMPv6), fp.Protocol) assert.Equal(t, uint8(layers.ICMPv6TypeDestinationUnreachable), ctx.ICMPType) assert.Equal(t, uint8(layers.ICMPv6CodePortUnreachable), ctx.ICMPCode) assert.Equal(t, uint16(len(data)), ctx.Length) } // TestPacketContext_NilOK confirms a nil context pointer is accepted by // newPacket (the hot path may elect not to pass one). func TestPacketContext_NilOK(t *testing.T) { ip := &layers.IPv4{ Version: 4, TTL: 64, Protocol: layers.IPProtocolUDP, SrcIP: net.IPv4(10, 0, 0, 1), DstIP: net.IPv4(10, 0, 0, 2), } udp := &layers.UDP{SrcPort: 1, DstPort: 2} require.NoError(t, udp.SetNetworkLayerForChecksum(ip)) data := mustSerialize(t, ip, udp) var fp firewall.Packet require.NoError(t, newPacket(data, true, &fp, nil)) } // TestPacketContext_FlowCreateCarriesContext exercises the full Drop -> addConn // -> ReportFlowCreate path with a realistic TCP packet and confirms the // context makes it into the reporter. func TestPacketContext_FlowCreateCarriesContext(t *testing.T) { f := newEventFixture(t) r := &recordingReporter{} f.ctl.SetFirewallEventReporter(r) // Hand-construct a matching TCP packet. ctx := firewall.PacketContext{Length: 1500, TCPFlags: 0x12} p := f.p p.Protocol = firewall.ProtoTCP require.NoError(t, f.firewall().Drop(p, ctx, true, f.h, f.cp, nil)) require.Len(t, r.creates, 1) assert.Equal(t, uint16(1500), r.creates[0].ctx.Length) assert.Equal(t, uint8(0x12), r.creates[0].ctx.TCPFlags) } // --- benchmarks ------------------------------------------------------------ // noopReporter is the cheapest possible reporter. Methods discard the event. type noopReporter struct{} func (noopReporter) ReportDrop(events.DropEvent) {} func (noopReporter) ReportFlowCreate(events.FlowCreateEvent) {} func (noopReporter) ReportFlowEvict(events.FlowEvictEvent) {} func (noopReporter) ReportRulesReload(events.RulesReloadEvent) { } // bufferedReporter demonstrates a realistic zero-alloc reporter: each event // is forwarded to a value-typed channel. The channel send is a memcpy into // the channel's pre-allocated ring buffer -- no heap traffic. A background // goroutine would drain these; the bench skips draining to keep the report // path pure. type bufferedReporter struct { drops chan events.DropEvent flows chan events.FlowCreateEvent evicts chan events.FlowEvictEvent } func newBufferedReporter(cap int) *bufferedReporter { return &bufferedReporter{ drops: make(chan events.DropEvent, cap), flows: make(chan events.FlowCreateEvent, cap), evicts: make(chan events.FlowEvictEvent, cap), } } func (r *bufferedReporter) ReportDrop(e events.DropEvent) { select { case r.drops <- e: default: } } func (r *bufferedReporter) ReportFlowCreate(e events.FlowCreateEvent) { select { case r.flows <- e: default: } } func (r *bufferedReporter) ReportFlowEvict(e events.FlowEvictEvent) { select { case r.evicts <- e: default: } } func (r *bufferedReporter) ReportRulesReload(events.RulesReloadEvent) {} // pointerReporter is the anti-pattern: it takes the address of the incoming // event struct, which forces the callee-side copy onto the heap. Kept for // comparison so we can see the alloc cost an unwary reporter would incur. type pointerReporter struct { last *events.DropEvent } func (r *pointerReporter) ReportDrop(e events.DropEvent) { r.last = &e } func (r *pointerReporter) ReportFlowCreate(events.FlowCreateEvent) {} func (r *pointerReporter) ReportFlowEvict(events.FlowEvictEvent) {} func (r *pointerReporter) ReportRulesReload(events.RulesReloadEvent) { } func newBenchFixture(b *testing.B) *eventFixture { b.Helper() l := test.NewLogger() vpnNetworks := new(bart.Lite) vpnNetworks.Insert(netip.MustParsePrefix("1.2.3.0/24")) c := &dummyCert{ name: "host1", networks: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/24")}, groups: []string{"default-group"}, issuer: "signer-shasum", } h := &HostInfo{ ConnectionState: &ConnectionState{ peerCert: &cert.CachedCertificate{ Certificate: c, InvertedGroups: map[string]struct{}{"default-group": {}}, }, }, vpnAddrs: []netip.Addr{netip.MustParseAddr("1.2.3.4")}, } h.buildNetworks(vpnNetworks, c) fw := NewFirewall(l, time.Minute, time.Minute, time.Minute, c) // Inbound rule that matches our packet; outbound has no match so we can // also benchmark the no-rule drop path. if err := fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""); err != nil { b.Fatal(err) } ctl := &Control{f: &Interface{firewall: fw}, l: l} return &eventFixture{ ctl: ctl, fw: fw, p: firewall.Packet{ LocalAddr: netip.MustParseAddr("1.2.3.4"), RemoteAddr: netip.MustParseAddr("1.2.3.4"), LocalPort: 10, RemotePort: 90, Protocol: firewall.ProtoUDP, }, h: h, cp: cert.NewCAPool(), } } // BenchmarkFirewallDropPath measures the cost of Firewall.Drop on a packet // that reaches the no-matching-rule branch (the longest drop path). Compare // reporter shapes: // // nilReporter -- no reporter installed (feature cost when off) // noopReporter -- reporter installed, methods discard args (minimum on-cost) // bufferedReporter -- realistic zero-alloc reporter: value-typed channels // pointerReporter -- anti-pattern that takes &composite-literal (allocates) func BenchmarkFirewallDropPath(b *testing.B) { run := func(b *testing.B, install func(*Control)) { f := newBenchFixture(b) install(f.ctl) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = f.firewall().Drop(f.p, firewall.PacketContext{}, false, f.h, f.cp, nil) } } b.Run("nilReporter", func(b *testing.B) { run(b, func(*Control) {}) }) b.Run("noopReporter", func(b *testing.B) { run(b, func(c *Control) { c.SetFirewallEventReporter(noopReporter{}) }) }) b.Run("bufferedReporter", func(b *testing.B) { run(b, func(c *Control) { c.SetFirewallEventReporter(newBufferedReporter(1024)) }) }) b.Run("pointerReporter", func(b *testing.B) { run(b, func(c *Control) { c.SetFirewallEventReporter(&pointerReporter{}) }) }) } // BenchmarkConntrackCreate measures Firewall.Drop for an allowed inbound // packet on a fresh conntrack (so addConn fires each iteration). func BenchmarkConntrackCreate(b *testing.B) { run := func(b *testing.B, install func(*Control)) { f := newBenchFixture(b) install(f.ctl) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { resetConntrack(f.firewall()) _ = f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil) } } b.Run("nilReporter", func(b *testing.B) { run(b, func(*Control) {}) }) b.Run("noopReporter", func(b *testing.B) { run(b, func(c *Control) { c.SetFirewallEventReporter(noopReporter{}) }) }) b.Run("bufferedReporter", func(b *testing.B) { run(b, func(c *Control) { c.SetFirewallEventReporter(newBufferedReporter(1024)) }) }) } // BenchmarkConntrackHit measures the hot path where a flow is already in // conntrack and short-circuits rule evaluation. The reporter slot is checked // only on create/evict, so this bench should show the reporter having zero // impact regardless of install state. func BenchmarkConntrackHit(b *testing.B) { b.Run("nilReporter", func(b *testing.B) { f := newBenchFixture(b) // Prime conntrack. require.NoError(b, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil) } }) b.Run("noopReporter", func(b *testing.B) { f := newBenchFixture(b) f.ctl.SetFirewallEventReporter(noopReporter{}) require.NoError(b, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { _ = f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil) } }) }