| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731 |
- 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)
- }
- })
- }
|