Răsfoiți Sursa

memory oh baby

JackDoan 2 săptămâni în urmă
părinte
comite
ca69a0613b
6 a modificat fișierele cu 159 adăugiri și 315 ștergeri
  1. 10 35
      firewall.go
  2. 6 20
      firewall/config.go
  3. 61 7
      firewall/config_test.go
  4. 40 61
      firewall/rules_builder.go
  5. 32 8
      firewall/table.go
  6. 10 184
      firewall_test.go

+ 10 - 35
firewall.go

@@ -31,8 +31,7 @@ type conn struct {
 type Firewall struct {
 	Conntrack *FirewallConntrack
 
-	InRules  *firewall.Table
-	OutRules *firewall.Table
+	rules firewall.Rules
 
 	InboundSendReject  bool
 	OutboundSendReject bool
@@ -52,9 +51,6 @@ type Firewall struct {
 	// unsafeNetworks is the list of unsafe networks issued to us in the certificate
 	unsafeNetworks []netip.Prefix
 
-	// ruleHash and ruleHashFNV identify the rules, see firewall.Rules
-	ruleHash     string
-	ruleHashFNV  uint32
 	rulesVersion uint16
 
 	incomingMetrics firewallMetrics
@@ -109,11 +105,13 @@ func NewFirewall(l *slog.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Dur
 		routableNetworks.Insert(n)
 	}
 
-	fw := &Firewall{
+	return &Firewall{
 		Conntrack: &FirewallConntrack{
 			Conns:      make(map[firewall.Packet]*conn),
 			TimerWheel: NewTimerWheel[firewall.Packet](tmin, tmax),
 		},
+		// Nothing is allowed until there are rules
+		rules:            firewall.NewRulesBuilder(l).Build(assignedNetworks, unsafeNetworks),
 		TCPTimeout:       tcpTimeout,
 		UDPTimeout:       UDPTimeout,
 		DefaultTimeout:   defaultTimeout,
@@ -133,10 +131,6 @@ func NewFirewall(l *slog.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Dur
 			droppedNoRule:     metrics.GetOrRegisterCounter("firewall.outgoing.dropped.no_rule", nil),
 		},
 	}
-
-	// Nothing is allowed until there are rules
-	fw.setRules(firewall.NewRulesBuilder(l))
-	return fw
 }
 
 func NewFirewallFromConfig(l *slog.Logger, cs *CertState, c *config.C) (*Firewall, error) {
@@ -180,30 +174,23 @@ func NewFirewallFromConfig(l *slog.Logger, cs *CertState, c *config.C) (*Firewal
 		fw.OutboundSendReject = false
 	}
 
-	rules, err := firewall.RulesFromConfig(l, c)
+	rules, err := firewall.RulesFromConfig(l, c, fw.assignedNetworks, fw.unsafeNetworks)
 	if err != nil {
 		return nil, err
 	}
-	fw.setRules(rules)
+	fw.rules = rules
 
 	return fw, nil
 }
 
-// setRules builds rules into InRules and OutRules
-func (f *Firewall) setRules(rb *firewall.RulesBuilder) {
-	rules := rb.Build(f.assignedNetworks, f.unsafeNetworks)
-	f.InRules, f.OutRules = rules.In, rules.Out
-	f.ruleHash, f.ruleHashFNV = rules.Hash, rules.HashFNV
-}
-
 // GetRuleHash returns a hash representation of all inbound and outbound rules
 func (f *Firewall) GetRuleHash() string {
-	return f.ruleHash
+	return f.rules.Hash
 }
 
 // GetRuleHashFNV returns a uint32 FNV-1 hash representation the rules, for use as a metric value
 func (f *Firewall) GetRuleHashFNV() uint32 {
-	return f.ruleHashFNV
+	return f.rules.HashFNV
 }
 
 // GetRuleHashes returns both the sha256 and FNV-1 hashes, suitable for logging
@@ -258,13 +245,7 @@ func (f *Firewall) Drop(fp firewall.Packet, incoming bool, h *HostInfo, caPool *
 		return nil
 	}
 
-	table := f.OutRules
-	if incoming {
-		table = f.InRules
-	}
-
-	// We now know which firewall table to check against
-	if !table.Match(&fp, incoming, h.ConnectionState.peerCert, caPool) {
+	if !f.rules.Match(&fp, incoming, h.ConnectionState.peerCert, caPool) {
 		f.metrics(incoming).droppedNoRule.Inc(1)
 		return ErrNoMatchingRule
 	}
@@ -324,13 +305,7 @@ func (f *Firewall) inConns(fp *firewall.Packet, h *HostInfo, caPool *cert.CAPool
 	if c.rulesVersion != f.rulesVersion {
 		// This conntrack entry was for an older rule set, validate
 		// it still passes with the current rule set
-		table := f.OutRules
-		if c.incoming {
-			table = f.InRules
-		}
-
-		// We now know which firewall table to check against
-		if !table.Match(fp, c.incoming, h.ConnectionState.peerCert, caPool) {
+		if !f.rules.Match(fp, c.incoming, h.ConnectionState.peerCert, caPool) {
 			if f.l.Enabled(context.Background(), slog.LevelDebug) {
 				h.logger(f.l).Debug("dropping old conntrack entry, does not match new ruleset",
 					"fwPacket", *fp,

+ 6 - 20
firewall/config.go

@@ -19,19 +19,19 @@ type ruleAdder interface {
 	AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr string, caName string, caSha string) error
 }
 
-// RulesFromConfig reads the inbound and outbound firewall rules from c
-func RulesFromConfig(l *slog.Logger, c *config.C) (*RulesBuilder, error) {
+// RulesFromConfig reads the inbound and outbound firewall rules from c and builds them, see RulesBuilder.Build
+func RulesFromConfig(l *slog.Logger, c *config.C, vpnNetworks, unsafeNetworks []netip.Prefix) (Rules, error) {
 	b := NewRulesBuilder(l)
 	b.defaultLocalCIDRAny = c.GetBool("firewall.default_local_cidr_any", false)
 
 	if err := addRulesFromConfig(l, false, c, b); err != nil {
-		return nil, err
+		return Rules{}, err
 	}
 	if err := addRulesFromConfig(l, true, c, b); err != nil {
-		return nil, err
+		return Rules{}, err
 	}
 
-	return b, nil
+	return b.Build(vpnNetworks, unsafeNetworks), nil
 }
 
 func addRulesFromConfig(l *slog.Logger, inbound bool, c *config.C, ra ruleAdder) error {
@@ -114,20 +114,6 @@ func addRulesFromConfig(l *slog.Logger, inbound bool, c *config.C, ra ruleAdder)
 			return fmt.Errorf("%s rule #%v; %s %s", table, i, errPort, err)
 		}
 
-		if r.Cidr != "" && r.Cidr != "any" {
-			_, err = netip.ParsePrefix(r.Cidr)
-			if err != nil {
-				return fmt.Errorf("%s rule #%v; cidr did not parse; %s", table, i, err)
-			}
-		}
-
-		if r.LocalCidr != "" && r.LocalCidr != "any" {
-			_, err = netip.ParsePrefix(r.LocalCidr)
-			if err != nil {
-				return fmt.Errorf("%s rule #%v; local_cidr did not parse; %s", table, i, err)
-			}
-		}
-
 		if warning := r.sanity(); warning != nil {
 			l.Warn("firewall rule sanity check",
 				"table", table,
@@ -138,7 +124,7 @@ func addRulesFromConfig(l *slog.Logger, inbound bool, c *config.C, ra ruleAdder)
 
 		err = ra.AddRule(inbound, proto, startPort, endPort, r.Groups, r.Host, r.Cidr, r.LocalCidr, r.CAName, r.CASha)
 		if err != nil {
-			return fmt.Errorf("%s rule #%v; `%s`", table, i, err)
+			return fmt.Errorf("%s rule #%v; %s", table, i, err)
 		}
 	}
 

+ 61 - 7
firewall/config_test.go

@@ -224,11 +224,10 @@ func TestAddRulesFromConfig(t *testing.T) {
 	require.NoError(t, addRulesFromConfig(l, true, conf, mf))
 	assert.Equal(t, addRuleCall{incoming: true, proto: ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: "any", localIp: ""}, mf.lastCall)
 
-	// Test adding rule with junk cidr
+	// Test adding rule with junk cidr, which RulesBuilder rejects
 	conf = config.NewC(test.NewLogger())
-	mf = &mockFirewall{}
 	conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "cidr": "junk/junk"}}}
-	require.EqualError(t, addRulesFromConfig(l, true, conf, mf), "firewall.inbound rule #0; cidr did not parse; netip.ParsePrefix(\"junk/junk\"): ParseAddr(\"junk\"): unable to parse IP")
+	require.EqualError(t, addRulesFromConfig(l, true, conf, NewRulesBuilder(l)), "firewall.inbound rule #0; cidr did not parse; netip.ParsePrefix(\"junk/junk\"): ParseAddr(\"junk\"): unable to parse IP")
 
 	// Test adding rule with local_cidr ipv6
 	conf = config.NewC(test.NewLogger())
@@ -244,11 +243,10 @@ func TestAddRulesFromConfig(t *testing.T) {
 	require.NoError(t, addRulesFromConfig(l, true, conf, mf))
 	assert.Equal(t, addRuleCall{incoming: true, proto: ProtoAny, startPort: 1, endPort: 1, groups: nil, localIp: "any"}, mf.lastCall)
 
-	// Test adding rule with junk local_cidr
+	// Test adding rule with junk local_cidr, which RulesBuilder rejects
 	conf = config.NewC(test.NewLogger())
-	mf = &mockFirewall{}
 	conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "local_cidr": "junk/junk"}}}
-	require.EqualError(t, addRulesFromConfig(l, true, conf, mf), "firewall.inbound rule #0; local_cidr did not parse; netip.ParsePrefix(\"junk/junk\"): ParseAddr(\"junk\"): unable to parse IP")
+	require.EqualError(t, addRulesFromConfig(l, true, conf, NewRulesBuilder(l)), "firewall.inbound rule #0; local_cidr did not parse; netip.ParsePrefix(\"junk/junk\"): ParseAddr(\"junk\"): unable to parse IP")
 
 	// Test adding rule with ca_sha
 	conf = config.NewC(test.NewLogger())
@@ -290,7 +288,63 @@ func TestAddRulesFromConfig(t *testing.T) {
 	mf = &mockFirewall{}
 	mf.nextCallReturn = errors.New("test error")
 	conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "host": "a"}}}
-	require.EqualError(t, addRulesFromConfig(l, true, conf, mf), "firewall.inbound rule #0; `test error`")
+	require.EqualError(t, addRulesFromConfig(l, true, conf, mf), "firewall.inbound rule #0; test error")
+}
+
+// TestRulesFromConfig_defaultLocalCIDR ensures rules without a local_cidr allow only the vpn networks when there are
+// unsafe networks, unless default_local_cidr_any is set, and that a rule's own local_cidr is kept either way
+func TestRulesFromConfig_defaultLocalCIDR(t *testing.T) {
+	vpnNetworks := []netip.Prefix{netip.MustParsePrefix("10.1.0.5/16")}
+	unsafeNetworks := []netip.Prefix{netip.MustParsePrefix("192.168.0.0/24")}
+	vpnAddr := netip.MustParseAddr("10.1.9.9")
+	unsafeAddr := netip.MustParseAddr("192.168.0.3")
+
+	allows := func(lr *localRules, addr netip.Addr) bool {
+		return lr.match(&Packet{LocalAddr: addr}, nil)
+	}
+
+	for _, tc := range []struct {
+		name                string
+		unsafeNetworks      []netip.Prefix
+		defaultLocalCIDRAny bool
+		wantUnsafe          bool
+	}{
+		{"no unsafe networks", nil, false, true},
+		{"unsafe networks", unsafeNetworks, false, false},
+		{"unsafe networks and default_local_cidr_any", unsafeNetworks, true, true},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			conf := config.NewC(test.NewLogger())
+			conf.Settings["firewall"] = map[string]any{
+				"default_local_cidr_any": tc.defaultLocalCIDRAny,
+				"inbound": []any{
+					map[string]any{"port": "any", "proto": "any", "host": "any"},
+					map[string]any{"port": "22", "proto": "tcp", "host": "any"},
+					map[string]any{"port": "80", "proto": "tcp", "host": "any", "local_cidr": "192.168.0.0/24"},
+					map[string]any{"port": "443", "proto": "tcp", "host": "any", "local_cidr": "any"},
+				},
+			}
+			rules, err := RulesFromConfig(test.NewLogger(), conf, vpnNetworks, tc.unsafeNetworks)
+			require.NoError(t, err)
+
+			// The proto `any` rule, copied into a protocol with rules of its own and shared by one without
+			for _, proto := range []uint8{iputil.IPProtocolTCP, 47} {
+				lr := rules.In.protos[proto][PortAny].Any.Any
+				assert.True(t, allows(lr, vpnAddr), "proto %d", proto)
+				assert.Equal(t, tc.wantUnsafe, allows(lr, unsafeAddr), "proto %d", proto)
+			}
+
+			tcp := rules.In.protos[iputil.IPProtocolTCP]
+			assert.True(t, allows(tcp[22].Any.Any, vpnAddr))
+			assert.Equal(t, tc.wantUnsafe, allows(tcp[22].Any.Any, unsafeAddr))
+
+			assert.False(t, allows(tcp[80].Any.Any, vpnAddr))
+			assert.True(t, allows(tcp[80].Any.Any, unsafeAddr))
+
+			assert.True(t, allows(tcp[443].Any.Any, vpnAddr))
+			assert.True(t, allows(tcp[443].Any.Any, unsafeAddr))
+		})
+	}
 }
 
 func Test_convertRule(t *testing.T) {

+ 40 - 61
firewall/rules_builder.go

@@ -29,16 +29,6 @@ type RulesBuilder struct {
 	l *slog.Logger
 }
 
-// Rules are the Tables built for each direction, with hashes of the rules they were built from
-type Rules struct {
-	In, Out *Table
-
-	// Hash is a sha256 of the rules, HashFNV is an FNV-1a of them for use as a metric value.
-	// Rule sets with the same rules have the same hashes.
-	Hash    string
-	HashFNV uint32
-}
-
 // tableRules are one direction's rules, waiting to be built into a Table
 type tableRules struct {
 	anyRules []rule
@@ -140,13 +130,13 @@ func parseRule(startPort, endPort int32, groups []string, host, cidr, localCidr,
 	var err error
 	r.cidr, r.anyCIDR, err = parseCIDR(cidr)
 	if err != nil {
-		return rule{}, err
+		return rule{}, fmt.Errorf("cidr did not parse; %w", err)
 	}
 
 	var localCIDR netip.Prefix
 	localCIDR, r.anyLocalCIDR, err = parseCIDR(localCidr)
 	if err != nil {
-		return rule{}, err
+		return rule{}, fmt.Errorf("local_cidr did not parse; %w", err)
 	}
 	if localCIDR.IsValid() {
 		r.localCIDRs = []netip.Prefix{localCIDR}
@@ -186,19 +176,19 @@ func (tr *tableRules) build(defaultLocalAny bool, defaultLocalCIDRs []netip.Pref
 	}
 
 	t := &Table{}
-	for proto := range t.Protos {
+	for proto := range t.protos {
 		rules := tr.protoRules[proto]
 		if len(rules) == 0 {
-			t.Protos[proto] = anyProto
+			t.protos[proto] = anyProto
 			continue
 		}
 
 		pr := portRules{}
 		add(pr, tr.anyRules)
 		add(pr, rules)
-		t.Protos[proto] = pr
+		t.protos[proto] = pr
 	}
-	t.Protos[iputil.IPProtocolICMPv6] = t.Protos[iputil.IPProtocolICMP]
+	t.protos[iputil.IPProtocolICMPv6] = t.protos[iputil.IPProtocolICMP]
 
 	return t
 }
@@ -227,70 +217,45 @@ func (r rule) isAny() bool {
 
 func (pr portRules) add(r rule) {
 	for i := r.startPort; i <= r.endPort; i++ {
-		if _, ok := pr[i]; !ok {
-			pr[i] = &caRules{
-				CANames: make(map[string]*remoteRules),
-				CAShas:  make(map[string]*remoteRules),
-			}
+		cr := pr[i]
+		if cr == nil {
+			cr = &caRules{}
+			pr[i] = cr
 		}
-
-		pr[i].add(r)
+		cr.add(r)
 	}
 }
 
 func (cr *caRules) add(r rule) {
-	newRemoteRules := func() *remoteRules {
-		return &remoteRules{
-			Hosts:  make(map[string]*localRules),
-			Groups: make([]*groupsRule, 0),
-			CIDR:   new(bart.Table[*localRules]),
-		}
-	}
-
 	if r.caSha == "" && r.caName == "" {
 		if cr.Any == nil {
-			cr.Any = newRemoteRules()
+			cr.Any = &remoteRules{}
 		}
-
 		cr.Any.add(r)
 		return
 	}
 
 	if r.caSha != "" {
-		if _, ok := cr.CAShas[r.caSha]; !ok {
-			cr.CAShas[r.caSha] = newRemoteRules()
-		}
-		cr.CAShas[r.caSha].add(r)
+		getOrNew(&cr.CAShas, r.caSha).add(r)
 	}
 
 	if r.caName != "" {
-		if _, ok := cr.CANames[r.caName]; !ok {
-			cr.CANames[r.caName] = newRemoteRules()
-		}
-		cr.CANames[r.caName].add(r)
+		getOrNew(&cr.CANames, r.caName).add(r)
 	}
 }
 
 func (rr *remoteRules) add(r rule) {
-	newLocalRules := func() *localRules {
-		return &localRules{
-			LocalCIDR: new(bart.Lite),
-		}
-	}
-
 	if r.isAny() {
 		if rr.Any == nil {
-			rr.Any = newLocalRules()
+			rr.Any = &localRules{}
 		}
-
 		rr.Any.add(r)
 		return
 	}
 
 	if len(r.groups) > 0 {
-		lr := newLocalRules()
+		lr := &localRules{}
 		lr.add(r)
-
 		rr.Groups = append(rr.Groups, &groupsRule{
 			Groups:    r.groups,
 			LocalCIDR: lr,
@@ -298,21 +263,19 @@ func (rr *remoteRules) add(r rule) {
 	}
 
 	if r.host != "" {
-		lr := rr.Hosts[r.host]
-		if lr == nil {
-			lr = newLocalRules()
-		}
-		lr.add(r)
-		rr.Hosts[r.host] = lr
+		getOrNew(&rr.Hosts, r.host).add(r)
 	}
 
 	if r.cidr.IsValid() {
-		lr, _ := rr.CIDR.Get(r.cidr)
-		if lr == nil {
-			lr = newLocalRules()
+		if rr.CIDR == nil {
+			rr.CIDR = new(bart.Table[*localRules])
+		}
+		lr, ok := rr.CIDR.Get(r.cidr)
+		if !ok {
+			lr = &localRules{}
+			rr.CIDR.Insert(r.cidr, lr)
 		}
 		lr.add(r)
-		rr.CIDR.Insert(r.cidr, lr)
 	}
 }
 
@@ -322,7 +285,23 @@ func (lr *localRules) add(r rule) {
 		return
 	}
 
+	if lr.LocalCIDR == nil {
+		lr.LocalCIDR = new(bart.Lite)
+	}
 	for _, prefix := range r.localCIDRs {
 		lr.LocalCIDR.Insert(prefix)
 	}
 }
+
+// getOrNew returns m[k], adding a new value for k first when there isn't one. m is made if it's nil.
+func getOrNew[M ~map[K]*V, K comparable, V any](m *M, k K) *V {
+	if *m == nil {
+		*m = make(M)
+	}
+	v := (*m)[k]
+	if v == nil {
+		v = new(V)
+		(*m)[k] = v
+	}
+	return v
+}

+ 32 - 8
firewall/table.go

@@ -8,21 +8,33 @@ import (
 	"github.com/slackhq/nebula/iputil"
 )
 
+// Rules are the Tables built for each direction, with hashes of the rules they were built from
+type Rules struct {
+	In, Out *Table
+
+	// Hash is a sha256 of the rules, HashFNV is an FNV-1a of them for use as a metric value.
+	// Rule sets with the same rules have the same hashes.
+	Hash    string
+	HashFNV uint32
+}
+
 // Table holds the rules for one direction, the evaluation order is:
 // Proto AND port AND (CA SHA or CA name) AND local CIDR AND (group OR groups OR name OR remote CIDR)
 // A Table doesn't change once built, see RulesBuilder.
 type Table struct {
-	// Protos holds the rules for each IP protocol number, so a packet is only checked against one set of rules.
+	// protos holds the rules for each IP protocol number, so a packet is only checked against one set of rules.
 	// A protocol with rules of its own also holds a copy of every proto `any` rule, the rest share the proto `any`
-	// rules, which are at Protos[ProtoAny]. nil when a protocol has no rules at all.
+	// rules, which are at protos[ProtoAny]. nil when a protocol has no rules at all.
 	// ICMP and ICMPv6 share one set of rules.
-	Protos [256]portRules
+	protos [256]portRules
 }
 
 // Even though ports are uint16, int32 maps are faster for lookup
 // Plus we can use `-1` for fragment rules
 type portRules map[int32]*caRules
 
+// caRules and the types below it only make their maps and tables once a rule needs them, a range of ports
+// gets a copy for every port so empty ones add up
 type caRules struct {
 	Any     *remoteRules
 	CANames map[string]*remoteRules
@@ -43,13 +55,23 @@ type groupsRule struct {
 }
 
 type localRules struct {
-	Any       bool
+	Any bool
+	// LocalCIDR is always set when Any isn't
 	LocalCIDR *bart.Lite
 }
 
+// Match reports whether a rule for the direction p is going allows it
+func (r *Rules) Match(p *Packet, incoming bool, c *cert.CachedCertificate, caPool *cert.CAPool) bool {
+	t := r.Out
+	if incoming {
+		t = r.In
+	}
+	return t.Match(p, incoming, c, caPool)
+}
+
 // Match reports whether a rule allows p
 func (t *Table) Match(p *Packet, incoming bool, c *cert.CachedCertificate, caPool *cert.CAPool) bool {
-	return t.Protos[p.Protocol].match(p, incoming, c, caPool)
+	return t.protos[p.Protocol].match(p, incoming, c, caPool)
 }
 
 func (pr portRules) match(p *Packet, incoming bool, c *cert.CachedCertificate, caPool *cert.CAPool) bool {
@@ -142,9 +164,11 @@ func (rr *remoteRules) match(p *Packet, c *cert.CachedCertificate) bool {
 		}
 	}
 
-	for _, v := range rr.CIDR.Supernets(netip.PrefixFrom(p.RemoteAddr, p.RemoteAddr.BitLen())) {
-		if v.match(p, c) {
-			return true
+	if rr.CIDR != nil {
+		for _, v := range rr.CIDR.Supernets(netip.PrefixFrom(p.RemoteAddr, p.RemoteAddr.BitLen())) {
+			if v.match(p, c) {
+				return true
+			}
 		}
 	}
 

+ 10 - 184
firewall_test.go

@@ -3,7 +3,6 @@ package nebula
 import (
 	"bytes"
 	"log/slog"
-	"math"
 	"net/netip"
 	"testing"
 	"time"
@@ -26,8 +25,11 @@ func TestNewFirewall(t *testing.T) {
 	assert.NotNil(t, conntrack)
 	assert.NotNil(t, conntrack.Conns)
 	assert.NotNil(t, conntrack.TimerWheel)
-	assert.NotNil(t, fw.InRules)
-	assert.NotNil(t, fw.OutRules)
+	assert.NotNil(t, fw.rules.In)
+	assert.NotNil(t, fw.rules.Out)
+	// Nothing is allowed until there are rules
+	assert.False(t, fw.rules.Match(&firewall.Packet{Protocol: iputil.IPProtocolTCP}, true, nil, nil))
+	assert.False(t, fw.rules.Match(&firewall.Packet{Protocol: iputil.IPProtocolTCP}, false, nil, nil))
 	assert.Equal(t, time.Second, fw.TCPTimeout)
 	assert.Equal(t, time.Minute, fw.UDPTimeout)
 	assert.Equal(t, time.Hour, fw.DefaultTimeout)
@@ -65,185 +67,6 @@ func TestNewFirewall(t *testing.T) {
 	assert.Equal(t, 3602, conntrack.TimerWheel.wheelLen)
 }
 
-func TestFirewall_AddRule(t *testing.T) {
-	ob := &bytes.Buffer{}
-	l := test.NewLoggerWithOutput(ob)
-
-	c := &dummyCert{}
-	fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb := firewall.NewRulesBuilder(l)
-	assert.NotNil(t, fw.InRules)
-	assert.NotNil(t, fw.OutRules)
-
-	ti, err := netip.ParsePrefix("1.2.3.4/32")
-	require.NoError(t, err)
-
-	ti6, err := netip.ParsePrefix("fd12::34/128")
-	require.NoError(t, err)
-
-	require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, 1, 1, []string{}, "", "", "", "", ""))
-	fw.setRules(rb)
-	// An empty rule is any
-	assert.True(t, fw.InRules.Protos[iputil.IPProtocolTCP][1].Any.Any.Any)
-	assert.Empty(t, fw.InRules.Protos[iputil.IPProtocolTCP][1].Any.Groups)
-	assert.Empty(t, fw.InRules.Protos[iputil.IPProtocolTCP][1].Any.Hosts)
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(true, iputil.IPProtocolUDP, 1, 1, []string{"g1"}, "", "", "", "", ""))
-	fw.setRules(rb)
-	assert.Nil(t, fw.InRules.Protos[iputil.IPProtocolUDP][1].Any.Any)
-	assert.Contains(t, fw.InRules.Protos[iputil.IPProtocolUDP][1].Any.Groups[0].Groups, "g1")
-	assert.Empty(t, fw.InRules.Protos[iputil.IPProtocolUDP][1].Any.Hosts)
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(true, iputil.IPProtocolICMP, 1, 1, []string{}, "h1", "", "", "", ""))
-	require.NoError(t, rb.AddRule(true, iputil.IPProtocolICMPv6, 1, 1, []string{}, "h2", "", "", "", ""))
-	fw.setRules(rb)
-	//no matter what port is given for icmp, it should end up as "any"
-	assert.Nil(t, fw.InRules.Protos[iputil.IPProtocolICMP][firewall.PortAny].Any.Any)
-	assert.Empty(t, fw.InRules.Protos[iputil.IPProtocolICMP][firewall.PortAny].Any.Groups)
-	assert.Contains(t, fw.InRules.Protos[iputil.IPProtocolICMP][firewall.PortAny].Any.Hosts, "h1")
-
-	// ICMP and ICMPv6 share one set of rules
-	assert.Contains(t, fw.InRules.Protos[iputil.IPProtocolICMP][firewall.PortAny].Any.Hosts, "h2")
-	assert.Contains(t, fw.InRules.Protos[iputil.IPProtocolICMPv6][firewall.PortAny].Any.Hosts, "h1")
-
-	// Any other protocol number gets its own rules
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(true, 47, 0, 0, []string{"g1"}, "", "", "", "", "")) // GRE
-	fw.setRules(rb)
-	assert.Contains(t, fw.InRules.Protos[47][firewall.PortAny].Any.Groups[0].Groups, "g1")
-	assert.Nil(t, fw.InRules.Protos[iputil.IPProtocolTCP])
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(false, firewall.ProtoAny, 1, 1, []string{}, "", ti.String(), "", "", ""))
-	fw.setRules(rb)
-	assert.Nil(t, fw.OutRules.Protos[firewall.ProtoAny][1].Any.Any)
-	_, ok := fw.OutRules.Protos[firewall.ProtoAny][1].Any.CIDR.Get(ti)
-	assert.True(t, ok)
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(false, firewall.ProtoAny, 1, 1, []string{}, "", ti6.String(), "", "", ""))
-	fw.setRules(rb)
-	assert.Nil(t, fw.OutRules.Protos[firewall.ProtoAny][1].Any.Any)
-	_, ok = fw.OutRules.Protos[firewall.ProtoAny][1].Any.CIDR.Get(ti6)
-	assert.True(t, ok)
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(false, firewall.ProtoAny, 1, 1, []string{}, "", "", ti.String(), "", ""))
-	fw.setRules(rb)
-	assert.NotNil(t, fw.OutRules.Protos[firewall.ProtoAny][1].Any.Any)
-	ok = fw.OutRules.Protos[firewall.ProtoAny][1].Any.Any.LocalCIDR.Get(ti)
-	assert.True(t, ok)
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(false, firewall.ProtoAny, 1, 1, []string{}, "", "", ti6.String(), "", ""))
-	fw.setRules(rb)
-	assert.NotNil(t, fw.OutRules.Protos[firewall.ProtoAny][1].Any.Any)
-	ok = fw.OutRules.Protos[firewall.ProtoAny][1].Any.Any.LocalCIDR.Get(ti6)
-	assert.True(t, ok)
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(true, iputil.IPProtocolUDP, 1, 1, []string{"g1"}, "", "", "", "ca-name", ""))
-	fw.setRules(rb)
-	assert.Contains(t, fw.InRules.Protos[iputil.IPProtocolUDP][1].CANames, "ca-name")
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(true, iputil.IPProtocolUDP, 1, 1, []string{"g1"}, "", "", "", "", "ca-sha"))
-	fw.setRules(rb)
-	assert.Contains(t, fw.InRules.Protos[iputil.IPProtocolUDP][1].CAShas, "ca-sha")
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(false, firewall.ProtoAny, 0, 0, []string{}, "any", "", "", "", ""))
-	fw.setRules(rb)
-	assert.True(t, fw.OutRules.Protos[firewall.ProtoAny][0].Any.Any.Any)
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	anyIp, err := netip.ParsePrefix("0.0.0.0/0")
-	require.NoError(t, err)
-
-	require.NoError(t, rb.AddRule(false, firewall.ProtoAny, 0, 0, []string{}, "", anyIp.String(), "", "", ""))
-	fw.setRules(rb)
-	assert.Nil(t, fw.OutRules.Protos[firewall.ProtoAny][0].Any.Any)
-	table, ok := fw.OutRules.Protos[firewall.ProtoAny][0].Any.CIDR.Lookup(netip.MustParseAddr("1.1.1.1"))
-	assert.True(t, table.Any)
-	table, ok = fw.OutRules.Protos[firewall.ProtoAny][0].Any.CIDR.Lookup(netip.MustParseAddr("9::9"))
-	assert.False(t, ok)
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	anyIp6, err := netip.ParsePrefix("::/0")
-	require.NoError(t, err)
-
-	require.NoError(t, rb.AddRule(false, firewall.ProtoAny, 0, 0, []string{}, "", anyIp6.String(), "", "", ""))
-	fw.setRules(rb)
-	assert.Nil(t, fw.OutRules.Protos[firewall.ProtoAny][0].Any.Any)
-	table, ok = fw.OutRules.Protos[firewall.ProtoAny][0].Any.CIDR.Lookup(netip.MustParseAddr("9::9"))
-	assert.True(t, table.Any)
-	table, ok = fw.OutRules.Protos[firewall.ProtoAny][0].Any.CIDR.Lookup(netip.MustParseAddr("1.1.1.1"))
-	assert.False(t, ok)
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(false, firewall.ProtoAny, 0, 0, []string{}, "", "any", "", "", ""))
-	fw.setRules(rb)
-	assert.True(t, fw.OutRules.Protos[firewall.ProtoAny][0].Any.Any.Any)
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(false, firewall.ProtoAny, 0, 0, []string{}, "", "", anyIp.String(), "", ""))
-	fw.setRules(rb)
-	assert.False(t, fw.OutRules.Protos[firewall.ProtoAny][0].Any.Any.Any)
-	assert.True(t, fw.OutRules.Protos[firewall.ProtoAny][0].Any.Any.LocalCIDR.Lookup(netip.MustParseAddr("1.1.1.1")))
-	assert.False(t, fw.OutRules.Protos[firewall.ProtoAny][0].Any.Any.LocalCIDR.Lookup(netip.MustParseAddr("9::9")))
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(false, firewall.ProtoAny, 0, 0, []string{}, "", "", anyIp6.String(), "", ""))
-	fw.setRules(rb)
-	assert.False(t, fw.OutRules.Protos[firewall.ProtoAny][0].Any.Any.Any)
-	assert.True(t, fw.OutRules.Protos[firewall.ProtoAny][0].Any.Any.LocalCIDR.Lookup(netip.MustParseAddr("9::9")))
-	assert.False(t, fw.OutRules.Protos[firewall.ProtoAny][0].Any.Any.LocalCIDR.Lookup(netip.MustParseAddr("1.1.1.1")))
-
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(false, firewall.ProtoAny, 0, 0, []string{}, "", "", "any", "", ""))
-	fw.setRules(rb)
-	assert.True(t, fw.OutRules.Protos[firewall.ProtoAny][0].Any.Any.Any)
-
-	// Every protocol number is accepted
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(true, math.MaxUint8, 0, 0, []string{}, "", "", "", "", ""))
-	fw.setRules(rb)
-	assert.True(t, fw.InRules.Protos[math.MaxUint8][firewall.PortAny].Any.Any.Any)
-
-	// Test error conditions, a bad rule is reported when it's added
-	rb = firewall.NewRulesBuilder(l)
-	require.Error(t, rb.AddRule(true, firewall.ProtoAny, 10, 0, []string{}, "", "", "", "", ""))
-	require.Error(t, rb.AddRule(true, firewall.ProtoAny, 0, 0, []string{}, "", "junk", "", "", ""))
-	require.Error(t, rb.AddRule(true, firewall.ProtoAny, 0, 0, []string{}, "", "", "junk", "", ""))
-
-	// A new firewall allows nothing until its rules are set
-	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
-	rb = firewall.NewRulesBuilder(l)
-	require.NoError(t, rb.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
-	assert.Nil(t, fw.InRules.Protos[iputil.IPProtocolTCP])
-	fw.setRules(rb)
-	assert.NotNil(t, fw.InRules.Protos[iputil.IPProtocolTCP])
-}
-
 func TestFirewall_Drop(t *testing.T) {
 	ob := &bytes.Buffer{}
 	l := test.NewLoggerWithOutput(ob)
@@ -1108,7 +931,6 @@ func TestFirewall_ProtocolRules(t *testing.T) {
 
 		require.NoError(t, fw.Drop(pkt(iputil.IPProtocolICMP), true, &h, cp, nil), "anyFirst %v", anyFirst)
 		require.NoError(t, fw.Drop(pkt(iputil.IPProtocolICMPv6), true, &h, cp, nil), "anyFirst %v", anyFirst)
-		assert.Len(t, fw.InRules.Protos[iputil.IPProtocolICMP][firewall.PortAny].Any.Groups, 2, "anyFirst %v", anyFirst)
 	}
 
 	// Protocols without rules of their own have no rules to check until there is a proto `any` rule to share
@@ -1116,7 +938,6 @@ func TestFirewall_ProtocolRules(t *testing.T) {
 	rb = firewall.NewRulesBuilder(l)
 	require.NoError(t, rb.AddRule(true, iputil.IPProtocolTCP, 22, 22, []string{"any"}, "", "", "", "", ""))
 	fw.setRules(rb)
-	assert.Nil(t, fw.InRules.Protos[47])
 	assert.Equal(t, ErrNoMatchingRule, fw.Drop(pkt(47), true, &h, cp, nil))
 
 	fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
@@ -1635,6 +1456,11 @@ func TestFirewall_Drop_EnforceIPMatch(t *testing.T) {
 	})
 }
 
+// setRules builds rb for fw's networks and makes them its rules
+func (f *Firewall) setRules(rb *firewall.RulesBuilder) {
+	f.rules = rb.Build(f.assignedNetworks, f.unsafeNetworks)
+}
+
 func resetConntrack(fw *Firewall) {
 	fw.Conntrack.Lock()
 	fw.Conntrack.Conns = map[firewall.Packet]*conn{}