firewall_events_test.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. package nebula
  2. import (
  3. "net"
  4. "net/netip"
  5. "sync"
  6. "testing"
  7. "time"
  8. "github.com/gaissmai/bart"
  9. "github.com/google/gopacket"
  10. "github.com/google/gopacket/layers"
  11. "github.com/slackhq/nebula/cert"
  12. "github.com/slackhq/nebula/firewall"
  13. "github.com/slackhq/nebula/firewall/events"
  14. "github.com/slackhq/nebula/test"
  15. "github.com/stretchr/testify/assert"
  16. "github.com/stretchr/testify/require"
  17. )
  18. // recordingReporter captures every event fired against it. Its methods take
  19. // the conntrack lock implicitly (via the firewall code path that invokes
  20. // them), so we synchronize accumulator mutations with a small mutex to keep
  21. // the race detector happy across goroutines in case a test introduces any.
  22. type recordingReporter struct {
  23. mu sync.Mutex
  24. drops []recordedDrop
  25. creates []recordedCreate
  26. evicts []recordedEvict
  27. reloads []recordedReload
  28. }
  29. type recordedDrop struct {
  30. incoming bool
  31. reason events.DropReason
  32. remote netip.Addr
  33. local netip.Addr
  34. peerName string
  35. rulesVersion uint16
  36. ctx firewall.PacketContext
  37. }
  38. type recordedCreate struct {
  39. incoming bool
  40. remote netip.Addr
  41. local netip.Addr
  42. peerName string
  43. rulesVersion uint16
  44. ctx firewall.PacketContext
  45. }
  46. type recordedEvict struct {
  47. incoming bool
  48. remote netip.Addr
  49. local netip.Addr
  50. rulesVersion uint16
  51. expired bool
  52. }
  53. type recordedReload struct {
  54. oldVersion uint16
  55. newVersion uint16
  56. }
  57. func (r *recordingReporter) ReportDrop(e events.DropEvent) {
  58. r.mu.Lock()
  59. defer r.mu.Unlock()
  60. name := ""
  61. if e.PeerCert != nil && e.PeerCert.Certificate != nil {
  62. name = e.PeerCert.Certificate.Name()
  63. }
  64. r.drops = append(r.drops, recordedDrop{
  65. incoming: e.Incoming,
  66. reason: e.Reason,
  67. remote: e.Packet.RemoteAddr,
  68. local: e.Packet.LocalAddr,
  69. peerName: name,
  70. rulesVersion: e.RulesVersion,
  71. ctx: e.Context,
  72. })
  73. }
  74. func (r *recordingReporter) ReportFlowCreate(e events.FlowCreateEvent) {
  75. r.mu.Lock()
  76. defer r.mu.Unlock()
  77. name := ""
  78. if e.PeerCert != nil && e.PeerCert.Certificate != nil {
  79. name = e.PeerCert.Certificate.Name()
  80. }
  81. r.creates = append(r.creates, recordedCreate{
  82. incoming: e.Incoming,
  83. remote: e.Packet.RemoteAddr,
  84. local: e.Packet.LocalAddr,
  85. peerName: name,
  86. rulesVersion: e.RulesVersion,
  87. ctx: e.Context,
  88. })
  89. }
  90. func (r *recordingReporter) ReportFlowEvict(e events.FlowEvictEvent) {
  91. r.mu.Lock()
  92. defer r.mu.Unlock()
  93. r.evicts = append(r.evicts, recordedEvict{
  94. incoming: e.Incoming,
  95. remote: e.Packet.RemoteAddr,
  96. local: e.Packet.LocalAddr,
  97. rulesVersion: e.RulesVersion,
  98. expired: e.Expired,
  99. })
  100. }
  101. func (r *recordingReporter) ReportRulesReload(e events.RulesReloadEvent) {
  102. r.mu.Lock()
  103. defer r.mu.Unlock()
  104. r.reloads = append(r.reloads, recordedReload{
  105. oldVersion: e.OldVersion,
  106. newVersion: e.NewVersion,
  107. })
  108. }
  109. // eventFixture builds a Firewall wired to a Control plus a packet/hostinfo
  110. // pair that a test can reuse. By default the ruleset allows the packet;
  111. // callers mutate fw / p / h as needed before invoking Drop.
  112. type eventFixture struct {
  113. ctl *Control
  114. fw *Firewall
  115. p firewall.Packet
  116. h *HostInfo
  117. cp *cert.CAPool
  118. }
  119. func newEventFixture(t *testing.T) *eventFixture {
  120. t.Helper()
  121. l := test.NewLogger()
  122. // myVpnNetworksTable covers our single peer address so buildNetworks takes
  123. // the "simple case" path (h.networks stays nil); tests that want a populated
  124. // BART table overwrite h.networks directly.
  125. vpnNetworks := new(bart.Lite)
  126. vpnNetworks.Insert(netip.MustParsePrefix("1.2.3.0/24"))
  127. // Use the same cert for "peer" and "local" endpoints, matching the
  128. // TestFirewall_Drop fixture style: LocalAddr == RemoteAddr == peer vpn addr.
  129. c := &dummyCert{
  130. name: "host1",
  131. networks: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/24")},
  132. groups: []string{"default-group"},
  133. issuer: "signer-shasum",
  134. }
  135. h := &HostInfo{
  136. ConnectionState: &ConnectionState{
  137. peerCert: &cert.CachedCertificate{
  138. Certificate: c,
  139. InvertedGroups: map[string]struct{}{"default-group": {}},
  140. },
  141. },
  142. vpnAddrs: []netip.Addr{netip.MustParseAddr("1.2.3.4")},
  143. }
  144. h.buildNetworks(vpnNetworks, c)
  145. fw := NewFirewall(l, time.Minute, time.Minute, time.Minute, c)
  146. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
  147. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
  148. ctl := &Control{
  149. f: &Interface{firewall: fw},
  150. l: l,
  151. }
  152. return &eventFixture{
  153. ctl: ctl,
  154. fw: fw,
  155. p: firewall.Packet{
  156. LocalAddr: netip.MustParseAddr("1.2.3.4"),
  157. RemoteAddr: netip.MustParseAddr("1.2.3.4"),
  158. LocalPort: 10,
  159. RemotePort: 90,
  160. Protocol: firewall.ProtoUDP,
  161. },
  162. h: h,
  163. cp: cert.NewCAPool(),
  164. }
  165. }
  166. // firewall() returns the currently-installed firewall. Needed because
  167. // SetFirewallEventReporter replaces it via shallow-copy swap.
  168. func (f *eventFixture) firewall() *Firewall {
  169. return f.ctl.f.firewall
  170. }
  171. func TestEvents_ReportDrop_InvalidRemoteIP(t *testing.T) {
  172. f := newEventFixture(t)
  173. r := &recordingReporter{}
  174. f.ctl.SetFirewallEventReporter(r)
  175. // Packet to an address not in the cert's networks.
  176. f.p.RemoteAddr = netip.MustParseAddr("9.9.9.9")
  177. assert.Equal(t, ErrInvalidRemoteIP, f.firewall().Drop(f.p, firewall.PacketContext{}, false, f.h, f.cp, nil))
  178. require.Len(t, r.drops, 1)
  179. assert.Equal(t, events.DropInvalidRemoteIP, r.drops[0].reason)
  180. assert.False(t, r.drops[0].incoming)
  181. assert.Equal(t, "host1", r.drops[0].peerName)
  182. assert.Empty(t, r.creates)
  183. assert.Empty(t, r.evicts)
  184. }
  185. func TestEvents_ReportDrop_InvalidLocalIP(t *testing.T) {
  186. f := newEventFixture(t)
  187. r := &recordingReporter{}
  188. f.ctl.SetFirewallEventReporter(r)
  189. // LocalAddr outside our routable networks.
  190. f.p.LocalAddr = netip.MustParseAddr("9.9.9.9")
  191. assert.Equal(t, ErrInvalidLocalIP, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil))
  192. require.Len(t, r.drops, 1)
  193. assert.Equal(t, events.DropInvalidLocalIP, r.drops[0].reason)
  194. assert.True(t, r.drops[0].incoming)
  195. }
  196. func TestEvents_ReportDrop_NoMatchingRule(t *testing.T) {
  197. f := newEventFixture(t)
  198. // Reset to a firewall with no matching rule.
  199. l := test.NewLogger()
  200. fw := NewFirewall(l, time.Minute, time.Minute, time.Minute, f.h.ConnectionState.peerCert.Certificate)
  201. // Rule that won't match (group not in peer's groups).
  202. require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", ""))
  203. require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", ""))
  204. f.ctl.f.firewall = fw
  205. r := &recordingReporter{}
  206. f.ctl.SetFirewallEventReporter(r)
  207. assert.Equal(t, ErrNoMatchingRule, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil))
  208. require.Len(t, r.drops, 1)
  209. assert.Equal(t, events.DropNoMatchingRule, r.drops[0].reason)
  210. }
  211. func TestEvents_ReportDrop_PeerRejected(t *testing.T) {
  212. f := newEventFixture(t)
  213. // Re-classify the remote as VPNPeer so it triggers DropPeerRejected.
  214. f.h.networks = new(bart.Table[NetworkType])
  215. f.h.networks.Insert(netip.MustParsePrefix("1.2.3.0/24"), NetworkTypeVPNPeer)
  216. r := &recordingReporter{}
  217. f.ctl.SetFirewallEventReporter(r)
  218. assert.Equal(t, ErrPeerRejected, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil))
  219. require.Len(t, r.drops, 1)
  220. assert.Equal(t, events.DropPeerRejected, r.drops[0].reason)
  221. }
  222. func TestEvents_ReportDrop_UnknownNetwork(t *testing.T) {
  223. f := newEventFixture(t)
  224. // Insert an unrecognized NetworkType value to hit the default branch.
  225. f.h.networks = new(bart.Table[NetworkType])
  226. f.h.networks.Insert(netip.MustParsePrefix("1.2.3.0/24"), NetworkTypeUnknown)
  227. r := &recordingReporter{}
  228. f.ctl.SetFirewallEventReporter(r)
  229. assert.Equal(t, ErrUnknownNetworkType, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil))
  230. require.Len(t, r.drops, 1)
  231. assert.Equal(t, events.DropUnknownNetwork, r.drops[0].reason)
  232. }
  233. func TestEvents_ReportFlowCreate_OnceOnly(t *testing.T) {
  234. f := newEventFixture(t)
  235. r := &recordingReporter{}
  236. f.ctl.SetFirewallEventReporter(r)
  237. // First allowed packet creates the conntrack entry.
  238. require.NoError(t, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil))
  239. // Second matching packet on the same tuple is short-circuited by conntrack
  240. // and must not fire another FlowCreate.
  241. require.NoError(t, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil))
  242. require.Len(t, r.creates, 1)
  243. assert.True(t, r.creates[0].incoming)
  244. assert.Equal(t, f.p.RemoteAddr, r.creates[0].remote)
  245. assert.Empty(t, r.drops)
  246. }
  247. func TestEvents_ReportFlowEvict_OnReloadPurge(t *testing.T) {
  248. f := newEventFixture(t)
  249. r := &recordingReporter{}
  250. f.ctl.SetFirewallEventReporter(r)
  251. // Create a flow under the current rules.
  252. require.NoError(t, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil))
  253. require.Len(t, r.creates, 1)
  254. // Simulate a reload that produces rules the existing flow no longer
  255. // matches. Bump rulesVersion and replace InRules with an empty table so
  256. // revalidation fails.
  257. fw := f.firewall()
  258. fw.Conntrack.Lock()
  259. fw.rulesVersion++
  260. fw.InRules = newFirewallTable()
  261. fw.Conntrack.Unlock()
  262. // Next packet triggers re-validation, which fails and evicts the entry.
  263. err := fw.Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)
  264. assert.Equal(t, ErrNoMatchingRule, err)
  265. require.Len(t, r.evicts, 1)
  266. assert.False(t, r.evicts[0].expired, "evict from reload purge is not expiration")
  267. assert.True(t, r.evicts[0].incoming)
  268. }
  269. func TestEvents_ReportFlowEvict_OnTimeout(t *testing.T) {
  270. f := newEventFixture(t)
  271. r := &recordingReporter{}
  272. f.ctl.SetFirewallEventReporter(r)
  273. require.NoError(t, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil))
  274. require.Len(t, r.creates, 1)
  275. // Force expiration by rewinding the entry's deadline.
  276. fw := f.firewall()
  277. fw.Conntrack.Lock()
  278. c := fw.Conntrack.Conns[f.p]
  279. require.NotNil(t, c)
  280. c.Expires = time.Now().Add(-time.Hour)
  281. fw.evict(f.p)
  282. fw.Conntrack.Unlock()
  283. require.Len(t, r.evicts, 1)
  284. assert.True(t, r.evicts[0].expired)
  285. }
  286. func TestEvents_SetNil_Clears(t *testing.T) {
  287. f := newEventFixture(t)
  288. r := &recordingReporter{}
  289. f.ctl.SetFirewallEventReporter(r)
  290. require.NoError(t, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil))
  291. require.Len(t, r.creates, 1)
  292. f.ctl.SetFirewallEventReporter(nil)
  293. resetConntrack(f.firewall())
  294. require.NoError(t, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil))
  295. // No second create should be recorded.
  296. assert.Len(t, r.creates, 1)
  297. }
  298. func TestEvents_ReporterSurvivesSwap(t *testing.T) {
  299. f := newEventFixture(t)
  300. r := &recordingReporter{}
  301. f.ctl.SetFirewallEventReporter(r)
  302. // Simulate a reload by swapping in a fresh Firewall that carries the
  303. // reporter forward. Mirrors what reloadFirewall does with the shared
  304. // conntrack pointer.
  305. l := test.NewLogger()
  306. oldFw := f.firewall()
  307. newFw := NewFirewall(l, time.Minute, time.Minute, time.Minute, f.h.ConnectionState.peerCert.Certificate)
  308. require.NoError(t, newFw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
  309. require.NoError(t, newFw.AddRule(false, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
  310. newFw.Conntrack = oldFw.Conntrack
  311. newFw.rulesVersion = oldFw.rulesVersion + 1
  312. newFw.reporter = oldFw.reporter
  313. f.ctl.f.firewall = newFw
  314. newFw.reportRulesReload(oldFw.rulesVersion, newFw.rulesVersion)
  315. require.Len(t, r.reloads, 1)
  316. assert.Equal(t, oldFw.rulesVersion, r.reloads[0].oldVersion)
  317. assert.Equal(t, newFw.rulesVersion, r.reloads[0].newVersion)
  318. // Events on the new firewall should still reach the same reporter.
  319. require.NoError(t, newFw.Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil))
  320. require.Len(t, r.creates, 1)
  321. assert.Equal(t, newFw.rulesVersion, r.creates[0].rulesVersion)
  322. }
  323. func TestEvents_InstallDoesNotMutateOldFirewall(t *testing.T) {
  324. f := newEventFixture(t)
  325. before := f.firewall()
  326. r := &recordingReporter{}
  327. f.ctl.SetFirewallEventReporter(r)
  328. after := f.firewall()
  329. assert.NotSame(t, before, after, "SetFirewallEventReporter must replace the Firewall pointer")
  330. assert.Nil(t, before.reporter, "the pre-install Firewall must remain untouched")
  331. assert.NotNil(t, after.reporter)
  332. }
  333. // --- PacketContext parse tests --------------------------------------------
  334. func mustSerialize(t *testing.T, lrs ...gopacket.SerializableLayer) []byte {
  335. t.Helper()
  336. buf := gopacket.NewSerializeBuffer()
  337. opt := gopacket.SerializeOptions{ComputeChecksums: false, FixLengths: true}
  338. require.NoError(t, gopacket.SerializeLayers(buf, opt, lrs...))
  339. return buf.Bytes()
  340. }
  341. func TestPacketContext_IPv4_TCPFlags(t *testing.T) {
  342. ip := &layers.IPv4{
  343. Version: 4, TTL: 64, Protocol: layers.IPProtocolTCP,
  344. SrcIP: net.IPv4(10, 0, 0, 1), DstIP: net.IPv4(10, 0, 0, 2),
  345. }
  346. tcp := &layers.TCP{SrcPort: 1234, DstPort: 80, SYN: true, ACK: true}
  347. require.NoError(t, tcp.SetNetworkLayerForChecksum(ip))
  348. data := mustSerialize(t, ip, tcp, gopacket.Payload([]byte("hello")))
  349. var fp firewall.Packet
  350. var ctx firewall.PacketContext
  351. require.NoError(t, newPacket(data, true, &fp, &ctx))
  352. assert.Equal(t, uint8(firewall.ProtoTCP), fp.Protocol)
  353. // SYN (0x02) + ACK (0x10) = 0x12
  354. assert.Equal(t, uint8(0x12), ctx.TCPFlags)
  355. assert.Equal(t, uint16(len(data)), ctx.Length)
  356. assert.Equal(t, uint8(0), ctx.ICMPType)
  357. assert.Equal(t, uint8(0), ctx.ICMPCode)
  358. }
  359. func TestPacketContext_IPv4_ICMPTypeCode(t *testing.T) {
  360. ip := &layers.IPv4{
  361. Version: 4, TTL: 64, Protocol: layers.IPProtocolICMPv4,
  362. SrcIP: net.IPv4(10, 0, 0, 1), DstIP: net.IPv4(10, 0, 0, 2),
  363. }
  364. // Destination Unreachable, code 3 (port unreachable)
  365. icmp := &layers.ICMPv4{
  366. TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeDestinationUnreachable, layers.ICMPv4CodePort),
  367. }
  368. data := mustSerialize(t, ip, icmp, gopacket.Payload([]byte{0, 0, 0, 0}))
  369. var fp firewall.Packet
  370. var ctx firewall.PacketContext
  371. require.NoError(t, newPacket(data, true, &fp, &ctx))
  372. assert.Equal(t, uint8(firewall.ProtoICMP), fp.Protocol)
  373. assert.Equal(t, uint8(layers.ICMPv4TypeDestinationUnreachable), ctx.ICMPType)
  374. assert.Equal(t, uint8(layers.ICMPv4CodePort), ctx.ICMPCode)
  375. assert.Equal(t, uint16(len(data)), ctx.Length)
  376. assert.Equal(t, uint8(0), ctx.TCPFlags)
  377. }
  378. func TestPacketContext_IPv4_UDPLengthOnly(t *testing.T) {
  379. ip := &layers.IPv4{
  380. Version: 4, TTL: 64, Protocol: layers.IPProtocolUDP,
  381. SrcIP: net.IPv4(10, 0, 0, 1), DstIP: net.IPv4(10, 0, 0, 2),
  382. }
  383. udp := &layers.UDP{SrcPort: 1234, DstPort: 53}
  384. require.NoError(t, udp.SetNetworkLayerForChecksum(ip))
  385. data := mustSerialize(t, ip, udp, gopacket.Payload([]byte("query")))
  386. var fp firewall.Packet
  387. var ctx firewall.PacketContext
  388. require.NoError(t, newPacket(data, true, &fp, &ctx))
  389. assert.Equal(t, uint8(firewall.ProtoUDP), fp.Protocol)
  390. assert.Equal(t, uint16(len(data)), ctx.Length)
  391. assert.Zero(t, ctx.TCPFlags)
  392. assert.Zero(t, ctx.ICMPType)
  393. assert.Zero(t, ctx.ICMPCode)
  394. }
  395. func TestPacketContext_IPv6_TCPFlags(t *testing.T) {
  396. ip := &layers.IPv6{
  397. Version: 6, HopLimit: 64, NextHeader: layers.IPProtocolTCP,
  398. SrcIP: net.ParseIP("fd00::1"), DstIP: net.ParseIP("fd00::2"),
  399. }
  400. tcp := &layers.TCP{SrcPort: 1234, DstPort: 443, FIN: true, ACK: true}
  401. require.NoError(t, tcp.SetNetworkLayerForChecksum(ip))
  402. data := mustSerialize(t, ip, tcp, gopacket.Payload([]byte("bye")))
  403. var fp firewall.Packet
  404. var ctx firewall.PacketContext
  405. require.NoError(t, newPacket(data, true, &fp, &ctx))
  406. assert.Equal(t, uint8(firewall.ProtoTCP), fp.Protocol)
  407. // FIN (0x01) + ACK (0x10) = 0x11
  408. assert.Equal(t, uint8(0x11), ctx.TCPFlags)
  409. assert.Equal(t, uint16(len(data)), ctx.Length)
  410. }
  411. func TestPacketContext_IPv6_ICMPv6TypeCode(t *testing.T) {
  412. ip := &layers.IPv6{
  413. Version: 6, HopLimit: 64, NextHeader: layers.IPProtocolICMPv6,
  414. SrcIP: net.ParseIP("fd00::1"), DstIP: net.ParseIP("fd00::2"),
  415. }
  416. icmp := &layers.ICMPv6{
  417. TypeCode: layers.CreateICMPv6TypeCode(layers.ICMPv6TypeDestinationUnreachable, layers.ICMPv6CodePortUnreachable),
  418. }
  419. require.NoError(t, icmp.SetNetworkLayerForChecksum(ip))
  420. data := mustSerialize(t, ip, icmp, gopacket.Payload([]byte{0, 0, 0, 0, 0, 0, 0, 0}))
  421. var fp firewall.Packet
  422. var ctx firewall.PacketContext
  423. require.NoError(t, newPacket(data, true, &fp, &ctx))
  424. assert.Equal(t, uint8(firewall.ProtoICMPv6), fp.Protocol)
  425. assert.Equal(t, uint8(layers.ICMPv6TypeDestinationUnreachable), ctx.ICMPType)
  426. assert.Equal(t, uint8(layers.ICMPv6CodePortUnreachable), ctx.ICMPCode)
  427. assert.Equal(t, uint16(len(data)), ctx.Length)
  428. }
  429. // TestPacketContext_NilOK confirms a nil context pointer is accepted by
  430. // newPacket (the hot path may elect not to pass one).
  431. func TestPacketContext_NilOK(t *testing.T) {
  432. ip := &layers.IPv4{
  433. Version: 4, TTL: 64, Protocol: layers.IPProtocolUDP,
  434. SrcIP: net.IPv4(10, 0, 0, 1), DstIP: net.IPv4(10, 0, 0, 2),
  435. }
  436. udp := &layers.UDP{SrcPort: 1, DstPort: 2}
  437. require.NoError(t, udp.SetNetworkLayerForChecksum(ip))
  438. data := mustSerialize(t, ip, udp)
  439. var fp firewall.Packet
  440. require.NoError(t, newPacket(data, true, &fp, nil))
  441. }
  442. // TestPacketContext_FlowCreateCarriesContext exercises the full Drop -> addConn
  443. // -> ReportFlowCreate path with a realistic TCP packet and confirms the
  444. // context makes it into the reporter.
  445. func TestPacketContext_FlowCreateCarriesContext(t *testing.T) {
  446. f := newEventFixture(t)
  447. r := &recordingReporter{}
  448. f.ctl.SetFirewallEventReporter(r)
  449. // Hand-construct a matching TCP packet.
  450. ctx := firewall.PacketContext{Length: 1500, TCPFlags: 0x12}
  451. p := f.p
  452. p.Protocol = firewall.ProtoTCP
  453. require.NoError(t, f.firewall().Drop(p, ctx, true, f.h, f.cp, nil))
  454. require.Len(t, r.creates, 1)
  455. assert.Equal(t, uint16(1500), r.creates[0].ctx.Length)
  456. assert.Equal(t, uint8(0x12), r.creates[0].ctx.TCPFlags)
  457. }
  458. // --- benchmarks ------------------------------------------------------------
  459. // noopReporter is the cheapest possible reporter. Methods discard the event.
  460. type noopReporter struct{}
  461. func (noopReporter) ReportDrop(events.DropEvent) {}
  462. func (noopReporter) ReportFlowCreate(events.FlowCreateEvent) {}
  463. func (noopReporter) ReportFlowEvict(events.FlowEvictEvent) {}
  464. func (noopReporter) ReportRulesReload(events.RulesReloadEvent) {
  465. }
  466. // bufferedReporter demonstrates a realistic zero-alloc reporter: each event
  467. // is forwarded to a value-typed channel. The channel send is a memcpy into
  468. // the channel's pre-allocated ring buffer -- no heap traffic. A background
  469. // goroutine would drain these; the bench skips draining to keep the report
  470. // path pure.
  471. type bufferedReporter struct {
  472. drops chan events.DropEvent
  473. flows chan events.FlowCreateEvent
  474. evicts chan events.FlowEvictEvent
  475. }
  476. func newBufferedReporter(cap int) *bufferedReporter {
  477. return &bufferedReporter{
  478. drops: make(chan events.DropEvent, cap),
  479. flows: make(chan events.FlowCreateEvent, cap),
  480. evicts: make(chan events.FlowEvictEvent, cap),
  481. }
  482. }
  483. func (r *bufferedReporter) ReportDrop(e events.DropEvent) {
  484. select {
  485. case r.drops <- e:
  486. default:
  487. }
  488. }
  489. func (r *bufferedReporter) ReportFlowCreate(e events.FlowCreateEvent) {
  490. select {
  491. case r.flows <- e:
  492. default:
  493. }
  494. }
  495. func (r *bufferedReporter) ReportFlowEvict(e events.FlowEvictEvent) {
  496. select {
  497. case r.evicts <- e:
  498. default:
  499. }
  500. }
  501. func (r *bufferedReporter) ReportRulesReload(events.RulesReloadEvent) {}
  502. // pointerReporter is the anti-pattern: it takes the address of the incoming
  503. // event struct, which forces the callee-side copy onto the heap. Kept for
  504. // comparison so we can see the alloc cost an unwary reporter would incur.
  505. type pointerReporter struct {
  506. last *events.DropEvent
  507. }
  508. func (r *pointerReporter) ReportDrop(e events.DropEvent) {
  509. r.last = &e
  510. }
  511. func (r *pointerReporter) ReportFlowCreate(events.FlowCreateEvent) {}
  512. func (r *pointerReporter) ReportFlowEvict(events.FlowEvictEvent) {}
  513. func (r *pointerReporter) ReportRulesReload(events.RulesReloadEvent) {
  514. }
  515. func newBenchFixture(b *testing.B) *eventFixture {
  516. b.Helper()
  517. l := test.NewLogger()
  518. vpnNetworks := new(bart.Lite)
  519. vpnNetworks.Insert(netip.MustParsePrefix("1.2.3.0/24"))
  520. c := &dummyCert{
  521. name: "host1",
  522. networks: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/24")},
  523. groups: []string{"default-group"},
  524. issuer: "signer-shasum",
  525. }
  526. h := &HostInfo{
  527. ConnectionState: &ConnectionState{
  528. peerCert: &cert.CachedCertificate{
  529. Certificate: c,
  530. InvertedGroups: map[string]struct{}{"default-group": {}},
  531. },
  532. },
  533. vpnAddrs: []netip.Addr{netip.MustParseAddr("1.2.3.4")},
  534. }
  535. h.buildNetworks(vpnNetworks, c)
  536. fw := NewFirewall(l, time.Minute, time.Minute, time.Minute, c)
  537. // Inbound rule that matches our packet; outbound has no match so we can
  538. // also benchmark the no-rule drop path.
  539. if err := fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""); err != nil {
  540. b.Fatal(err)
  541. }
  542. ctl := &Control{f: &Interface{firewall: fw}, l: l}
  543. return &eventFixture{
  544. ctl: ctl,
  545. fw: fw,
  546. p: firewall.Packet{
  547. LocalAddr: netip.MustParseAddr("1.2.3.4"),
  548. RemoteAddr: netip.MustParseAddr("1.2.3.4"),
  549. LocalPort: 10,
  550. RemotePort: 90,
  551. Protocol: firewall.ProtoUDP,
  552. },
  553. h: h,
  554. cp: cert.NewCAPool(),
  555. }
  556. }
  557. // BenchmarkFirewallDropPath measures the cost of Firewall.Drop on a packet
  558. // that reaches the no-matching-rule branch (the longest drop path). Compare
  559. // reporter shapes:
  560. //
  561. // nilReporter -- no reporter installed (feature cost when off)
  562. // noopReporter -- reporter installed, methods discard args (minimum on-cost)
  563. // bufferedReporter -- realistic zero-alloc reporter: value-typed channels
  564. // pointerReporter -- anti-pattern that takes &composite-literal (allocates)
  565. func BenchmarkFirewallDropPath(b *testing.B) {
  566. run := func(b *testing.B, install func(*Control)) {
  567. f := newBenchFixture(b)
  568. install(f.ctl)
  569. b.ReportAllocs()
  570. b.ResetTimer()
  571. for i := 0; i < b.N; i++ {
  572. _ = f.firewall().Drop(f.p, firewall.PacketContext{}, false, f.h, f.cp, nil)
  573. }
  574. }
  575. b.Run("nilReporter", func(b *testing.B) { run(b, func(*Control) {}) })
  576. b.Run("noopReporter", func(b *testing.B) {
  577. run(b, func(c *Control) { c.SetFirewallEventReporter(noopReporter{}) })
  578. })
  579. b.Run("bufferedReporter", func(b *testing.B) {
  580. run(b, func(c *Control) { c.SetFirewallEventReporter(newBufferedReporter(1024)) })
  581. })
  582. b.Run("pointerReporter", func(b *testing.B) {
  583. run(b, func(c *Control) { c.SetFirewallEventReporter(&pointerReporter{}) })
  584. })
  585. }
  586. // BenchmarkConntrackCreate measures Firewall.Drop for an allowed inbound
  587. // packet on a fresh conntrack (so addConn fires each iteration).
  588. func BenchmarkConntrackCreate(b *testing.B) {
  589. run := func(b *testing.B, install func(*Control)) {
  590. f := newBenchFixture(b)
  591. install(f.ctl)
  592. b.ReportAllocs()
  593. b.ResetTimer()
  594. for i := 0; i < b.N; i++ {
  595. resetConntrack(f.firewall())
  596. _ = f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)
  597. }
  598. }
  599. b.Run("nilReporter", func(b *testing.B) { run(b, func(*Control) {}) })
  600. b.Run("noopReporter", func(b *testing.B) {
  601. run(b, func(c *Control) { c.SetFirewallEventReporter(noopReporter{}) })
  602. })
  603. b.Run("bufferedReporter", func(b *testing.B) {
  604. run(b, func(c *Control) { c.SetFirewallEventReporter(newBufferedReporter(1024)) })
  605. })
  606. }
  607. // BenchmarkConntrackHit measures the hot path where a flow is already in
  608. // conntrack and short-circuits rule evaluation. The reporter slot is checked
  609. // only on create/evict, so this bench should show the reporter having zero
  610. // impact regardless of install state.
  611. func BenchmarkConntrackHit(b *testing.B) {
  612. b.Run("nilReporter", func(b *testing.B) {
  613. f := newBenchFixture(b)
  614. // Prime conntrack.
  615. require.NoError(b, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil))
  616. b.ReportAllocs()
  617. b.ResetTimer()
  618. for i := 0; i < b.N; i++ {
  619. _ = f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)
  620. }
  621. })
  622. b.Run("noopReporter", func(b *testing.B) {
  623. f := newBenchFixture(b)
  624. f.ctl.SetFirewallEventReporter(noopReporter{})
  625. require.NoError(b, f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil))
  626. b.ReportAllocs()
  627. b.ResetTimer()
  628. for i := 0; i < b.N; i++ {
  629. _ = f.firewall().Drop(f.p, firewall.PacketContext{}, true, f.h, f.cp, nil)
  630. }
  631. })
  632. }