From 8f5f9216f5305a76cdecd771985d1abf04213897 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Thu, 20 Aug 2026 20:08:07 +0200 Subject: [PATCH] Retry filter rules per prefix when a set fails and latch ipset off once confirmed --- client/firewall/iptables/family_linux.go | 14 +- client/firewall/iptables/filter_linux.go | 48 ++++- client/firewall/iptables/ipset_linux.go | 27 --- .../firewall/iptables/ipset_support_linux.go | 105 ++++++++++ client/firewall/iptables/manager_linux.go | 13 +- .../firewall/iptables/manager_linux_test.go | 184 +++++++++++++++--- client/firewall/iptables/router_linux_test.go | 8 +- 7 files changed, 322 insertions(+), 77 deletions(-) create mode 100644 client/firewall/iptables/ipset_support_linux.go diff --git a/client/firewall/iptables/family_linux.go b/client/firewall/iptables/family_linux.go index c5ed8cc20..9d434dc7a 100644 --- a/client/firewall/iptables/family_linux.go +++ b/client/firewall/iptables/family_linux.go @@ -112,10 +112,11 @@ type family struct { // AddFilterRule writes here; DeleteFilterRule looks up by id. filters map[nbid.RuleID]*Rule ipsetCounter *ipsetCounter - // ipsetSupported records whether the kernel can create the hash:net - // sets the source matches rely on; probed once at init. When false, - // multi-source rules expand to one rule per source prefix. - ipsetSupported bool + // ipsetSupport is shared by the families of both address families, + // so a kernel without usable ipset support degrades them together. + // When latched off, multi-source rules expand to one rule per + // source prefix. + ipsetSupport *ipsetSupport // rules holds NAT, jump, and MSS-clamping rules (auxiliary // plumbing that isn't a filter rule). @@ -129,7 +130,7 @@ type family struct { stateManager *statemanager.Manager } -func newFamily(iptablesClient *iptables.IPTables, wgIface iFaceMapper, mtu uint16) (*family, error) { +func newFamily(iptablesClient *iptables.IPTables, wgIface iFaceMapper, mtu uint16, ipsetSupport *ipsetSupport) (*family, error) { r := &family{ iptablesClient: iptablesClient, wgIface: wgIface, @@ -140,6 +141,7 @@ func newFamily(iptablesClient *iptables.IPTables, wgIface iFaceMapper, mtu uint1 rules: make(routeRules), mtu: mtu, ipFwdState: ipfwdstate.NewIPForwardingState(wgIface.Name()), + ipsetSupport: ipsetSupport, } r.ipsetCounter = refcounter.New( @@ -159,8 +161,6 @@ func newFamily(iptablesClient *iptables.IPTables, wgIface iFaceMapper, mtu uint1 func (r *family) init(stateManager *statemanager.Manager) error { r.stateManager = stateManager - r.ipsetSupported = r.probeIPSetSupport() - if err := r.cleanUpDefaultForwardRules(); err != nil { log.Errorf("failed to clean up rules from FORWARD chain: %s", err) } diff --git a/client/firewall/iptables/filter_linux.go b/client/firewall/iptables/filter_linux.go index dc606da2d..4f7b3f5fe 100644 --- a/client/firewall/iptables/filter_linux.go +++ b/client/firewall/iptables/filter_linux.go @@ -3,6 +3,7 @@ package iptables import ( + "errors" "fmt" "net/netip" "slices" @@ -38,7 +39,21 @@ func (r *family) AddFilterRule( return existing, nil } - rule, err := r.installFilterRules(ruleID, sources, destination, proto, sPort, dPort, action, r.ipsetSupported) + rule, err := r.installFilterRules(ruleID, sources, destination, proto, sPort, dPort, action, r.ipsetSupport.supported()) + + var unusable *ipsetUnusableError + if errors.As(err, &unusable) { + // The set could not be created or matched. Retry matching each source + // prefix on its own so the rule lands either way, and latch the + // capability off only once a probe confirms the kernel is the reason. + rule, err = r.installFilterRules(ruleID, sources, destination, proto, sPort, dPort, action, false) + if err != nil { + return nil, fmt.Errorf("add filter rule (ipset: %w): %w", unusable.cause, err) + } + if !r.ipsetUsable(filterChain(destination)) { + r.ipsetSupport.markUnsupported(unusable.cause) + } + } if err != nil { return nil, err } @@ -194,7 +209,7 @@ func (r *family) applySourceMatch(network firewall.Network, prefixes []netip.Pre } name := r.ipsetName(network.Set.HashedName()) if _, err := r.ipsetCounter.Increment(name, prefixes); err != nil { - return nil, fmt.Errorf("ipset increment %s: %w", name, err) + return nil, ipsetUnusable(fmt.Errorf("ipset increment %s: %w", name, err)) } return []string{"-m", "set", matchSet, name, "src"}, nil case network.IsPrefix(): @@ -262,11 +277,7 @@ func (r *family) installFilterRule( } } matchSpecs := filterMatchSpecs(proto, sPort, dPort) - - chain := chainACLInput - if isRoute { - chain = chainRTFwdIn - } + chain := filterChain(destination) var installed []filterSpecs for _, srcMatch := range srcMatches { @@ -291,7 +302,16 @@ func (r *family) installFilterRule( // partial rule would silently keep matching without being tracked. r.removeFilterSpecs(chain, installed) r.dropSourceMatch(destExp) - return nil, fmt.Errorf("install filter rule on %s: %w", chain, err) + err = fmt.Errorf("install filter rule on %s: %w", chain, err) + // A failure on a rule whose source carries a set match may mean + // iptables cannot match against sets (xt_set); report it as such + // so the caller can retry the rule in its per-prefix form. A + // destination set cannot be expanded per prefix, so its failures + // are not retryable. + if len(findSets(srcMatch)) > 0 { + err = ipsetUnusable(err) + } + return nil, err } // The mangle redirect-mark rule is best effort: the filter rule itself @@ -317,6 +337,16 @@ func (r *family) installFilterRule( }, nil } +// filterChain returns the ACL chain a filter rule belongs in: a rule with a +// destination filters routed traffic, one without filters traffic addressed to +// this peer. +func filterChain(destination firewall.Network) string { + if destination.IsZero() { + return chainACLInput + } + return chainRTFwdIn +} + // insertFilterRule writes one assembled rule spec into the given ACL // chain. Peer ACL drops are inserted at position 1 so they precede the // chain's catch-all; route ACL drops are inserted at position 2 to sit @@ -362,7 +392,7 @@ func (r *family) applyNetwork(flag string, network firewall.Network, prefixes [] // source set it cannot be expanded into per-prefix rules. Without // ipset such a rule is not expressible; report it instead of // installing something broader than the policy allows. - if flag == "-d" && !r.ipsetSupported { + if flag == "-d" && !r.ipsetSupport.supported() { return nil, fmt.Errorf("destination set %s requires ipset (ip_set_hash_net and xt_set)", network.Set.HashedName()) } diff --git a/client/firewall/iptables/ipset_linux.go b/client/firewall/iptables/ipset_linux.go index 2a3685af7..0d5f71d69 100644 --- a/client/firewall/iptables/ipset_linux.go +++ b/client/firewall/iptables/ipset_linux.go @@ -6,7 +6,6 @@ import ( "fmt" "net/netip" - "github.com/google/uuid" "github.com/hashicorp/go-multierror" "github.com/lrh3321/ipset-go" log "github.com/sirupsen/logrus" @@ -15,32 +14,6 @@ import ( firewall "github.com/netbirdio/netbird/client/firewall/manager" ) -// probeIPSetSupport checks whether the kernel can create the ipset type -// used for source and destination matches. On kernels lacking the -// required ipset hash module, set creation fails (e.g. "invalid -// argument"), which would otherwise fail every multi-source rule and -// leave traffic the policy permits blocked by the catch-all drop. When -// unsupported, multi-source rules fall back to one rule per prefix. -func (r *family) probeIPSetSupport() bool { - // Use a unique name so concurrent processes don't collide and we only ever - // destroy the set we created ourselves. ipset names are limited to 31 chars, - // so use a short random suffix. - probeName := "nb-probe-" + uuid.New().String()[:8] - - if err := r.createIPSet(probeName); err != nil { - log.Warnf("ipset is not available (failed to create probe set: %v); "+ - "falling back to per-IP iptables ACL rules. Ensure the kernel provides "+ - "the ipset hash:net module (ip_set_hash_net) for better performance with large rule sets", err) - return false - } - - if err := r.destroyIPSet(probeName); err != nil { - log.Debugf("destroy ipset probe set %q: %v", probeName, err) - } - - return true -} - func (r *family) createIpSet(setName string, sources []netip.Prefix) error { if err := r.createIPSet(setName); err != nil { return fmt.Errorf("create set %s: %w", setName, err) diff --git a/client/firewall/iptables/ipset_support_linux.go b/client/firewall/iptables/ipset_support_linux.go new file mode 100644 index 000000000..460fd4f72 --- /dev/null +++ b/client/firewall/iptables/ipset_support_linux.go @@ -0,0 +1,105 @@ +//go:build !android + +package iptables + +import ( + "sync" + + "github.com/google/uuid" + log "github.com/sirupsen/logrus" +) + +// ipsetSupport tracks whether ipset-backed firewall rules can be installed. +// +// It starts optimistic and latches to unsupported once the kernel has proven +// otherwise: either the hash:net set type is missing (ip_set_hash_net) or +// iptables cannot match against a set (xt_set). Callers then emit per-prefix +// rules instead. Without the fallback, a rule referencing an unusable set is +// never installed and the catch-all DROP silently blocks traffic the policy +// permits. +// +// One instance is shared by the families of both address families, because +// ipset availability is a property of the kernel rather than of any single +// table. +type ipsetSupport struct { + mu sync.RWMutex + unsupported bool +} + +func newIPSetSupport() *ipsetSupport { + return &ipsetSupport{} +} + +func (s *ipsetSupport) supported() bool { + s.mu.RLock() + defer s.mu.RUnlock() + + return !s.unsupported +} + +// markUnsupported records that ipset cannot be used, logging the reason once. +func (s *ipsetSupport) markUnsupported(cause error) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.unsupported { + return + } + s.unsupported = true + + log.Warnf("ipset is unavailable (%v); falling back to per-prefix firewall rules. "+ + "Ensure the kernel provides ip_set_hash_net and xt_set; without them rule "+ + "sets are larger and slower to converge on networks with many peers", cause) +} + +// ipsetUsable reports whether the kernel can create a hash:net set and match it +// from an iptables rule in the given chain. It confirms a suspected ipset +// failure before the capability is latched off: a rule can fail for reasons +// that say nothing about the kernel's ipset support (a set name already taken +// by an incompatible type, a transient xtables lock), and latching on one of +// those would drop set matching for the rest of the process lifetime, including +// for the dynamic destination sets that have no per-prefix form. +func (r *family) ipsetUsable(chain string) bool { + // A short unique name so concurrent processes don't collide and we only + // ever destroy the set we created ourselves. ipset names are limited to + // 31 characters. + name := "nb-probe-" + uuid.New().String()[:8] + + if err := r.createIPSet(name); err != nil { + log.Debugf("ipset probe: create %s: %v", name, err) + return false + } + defer func() { + if err := r.destroyIPSet(name); err != nil { + log.Debugf("ipset probe: destroy %s: %v", name, err) + } + }() + + // Match-only rule with no target: the set is empty, so while it is + // installed it matches nothing and reaches no verdict. + specs := []string{"-m", "set", matchSet, name, "src"} + if err := r.iptablesClient.Insert(tableFilter, chain, 1, specs...); err != nil { + log.Debugf("ipset probe: match a set from %s: %v", chain, err) + return false + } + + if err := r.iptablesClient.DeleteIfExists(tableFilter, chain, specs...); err != nil { + log.Errorf("remove ipset probe rule from %s: %v", chain, err) + } + + return true +} + +// ipsetUnusableError marks a failure attributable to ipset, so the caller can +// retry the same rule in its per-prefix form before latching the capability off. +type ipsetUnusableError struct { + cause error +} + +func (e *ipsetUnusableError) Error() string { return e.cause.Error() } + +func (e *ipsetUnusableError) Unwrap() error { return e.cause } + +func ipsetUnusable(cause error) error { + return &ipsetUnusableError{cause: cause} +} diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index 49b88f1ea..b39c8c923 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -29,6 +29,10 @@ type Manager struct { family4 *family rawSupported bool + // ipsetSupport is shared by both families, so a kernel without + // usable ipset support degrades them together. + ipsetSupport *ipsetSupport + // IPv6 counterparts, nil when no v6 overlay ipv6Client *iptables.IPTables family6 *family @@ -48,11 +52,12 @@ func Create(wgIface iFaceMapper, mtu uint16) (*Manager, error) { } m := &Manager{ - wgIface: wgIface, - ipv4Client: iptablesClient, + wgIface: wgIface, + ipv4Client: iptablesClient, + ipsetSupport: newIPSetSupport(), } - m.family4, err = newFamily(iptablesClient, wgIface, mtu) + m.family4, err = newFamily(iptablesClient, wgIface, mtu, m.ipsetSupport) if err != nil { return nil, fmt.Errorf("create family: %w", err) } @@ -72,7 +77,7 @@ func (m *Manager) createIPv6Components(wgIface iFaceMapper, mtu uint16) error { return fmt.Errorf("init ip6tables: %w", err) } - family6, err := newFamily(ip6Client, wgIface, mtu) + family6, err := newFamily(ip6Client, wgIface, mtu, m.ipsetSupport) if err != nil { return fmt.Errorf("create v6 family: %w", err) } diff --git a/client/firewall/iptables/manager_linux_test.go b/client/firewall/iptables/manager_linux_test.go index 9f53352e1..5d91f3b2a 100644 --- a/client/firewall/iptables/manager_linux_test.go +++ b/client/firewall/iptables/manager_linux_test.go @@ -3,6 +3,7 @@ package iptables import ( + "errors" "fmt" "net/netip" "slices" @@ -237,6 +238,10 @@ func TestIptablesManagerIPSet(t *testing.T) { require.NotNil(t, multi, "multi-source rule must produce one iptables rule") sets := findSets(multi.(*Rule).specs) require.Len(t, sets, 1, "multi-source rule must reference exactly one ipset") + // Guard the default: a regression that reported ipset as unusable + // would silently move every Linux client to per-prefix rules. + require.True(t, manager.ipsetSupport.supported(), + "ipset must not be latched off on a healthy kernel") require.NoError(t, manager.DeleteFilterRule(multi)) }) @@ -264,7 +269,7 @@ func TestIptablesFilterIPSetFallback(t *testing.T) { }() // Simulate a kernel without the ipset hash module. - manager.family4.ipsetSupported = false + manager.ipsetSupport.markUnsupported(errors.New("test: pretend the kernel has no ipset")) sources := []netip.Prefix{ netip.MustParsePrefix("10.20.0.42/32"), @@ -298,30 +303,6 @@ func TestIptablesFilterIPSetFallback(t *testing.T) { } } -// TestIptablesFilterDestinationSetRequiresIPSet documents that a dynamic -// (domain) destination cannot be expressed without ipset: its prefixes are only -// known after DNS resolution, so there is nothing to expand into per-prefix -// rules. The call must report that rather than install a broader rule than the -// policy allows. -func TestIptablesFilterDestinationSetRequiresIPSet(t *testing.T) { - manager, err := Create(ifaceMock, iface.DefaultMTU) - require.NoError(t, err) - require.NoError(t, manager.Init(nil)) - - defer func() { - require.NoError(t, manager.Close(nil)) - }() - - manager.family4.ipsetSupported = false - - destination := fw.Network{Set: fw.NewDomainSet(domain.List{"example.com"})} - - _, err = manager.AddFilterRule(nil, []netip.Prefix{netip.MustParsePrefix("172.16.0.0/16")}, - destination, fw.ProtocolALL, nil, nil, fw.ActionAccept) - require.Error(t, err, "a domain destination is not expressible without ipset") - require.ErrorContains(t, err, "requires ipset") -} - // TestIptablesNatRuleDropsSourceSetOnDestinationFailure covers a marking rule // whose source set is created but whose destination set is not: the source // reference has to go back, or the set it created stays in the kernel with a @@ -417,7 +398,7 @@ func TestIptablesRouteFilterIPSetFallback(t *testing.T) { require.NoError(t, manager.Close(nil)) }() - manager.family4.ipsetSupported = false + manager.ipsetSupport.markUnsupported(errors.New("test: pretend the kernel has no ipset")) sources := []netip.Prefix{ netip.MustParsePrefix("172.16.0.0/16"), @@ -449,6 +430,157 @@ func TestIptablesRouteFilterIPSetFallback(t *testing.T) { } } +// TestIptablesFilterFallsBackOnSetFailure drives the real failure path: a set +// with the rule's name already exists with an incompatible type, so the kernel +// rejects the hash:net creation. The rule must still land in the chain, +// matching each prefix directly; without the fallback it was dropped and the +// catch-all DROP silently blocked traffic the policy permits. The capability +// must survive, because this kernel does support ipset and a latch would also +// take down the destination sets that have no per-prefix form. +func TestIptablesFilterFallsBackOnSetFailure(t *testing.T) { + ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) + require.NoError(t, err) + + manager, err := Create(ifaceMock, iface.DefaultMTU) + require.NoError(t, err) + require.NoError(t, manager.Init(nil)) + + defer func() { + require.NoError(t, manager.Close(nil)) + }() + + sources := []netip.Prefix{ + netip.MustParsePrefix("10.20.0.42/32"), + netip.MustParsePrefix("10.20.0.43/32"), + } + + // Poison the name the rule's set would get so hash:net creation fails. + poisoned := fw.NewPrefixSet(sources).HashedName() + require.NoError(t, ipset.Create(poisoned, ipset.TypeHashIP, ipset.CreateOptions{})) + t.Cleanup(func() { + if err := ipset.Destroy(poisoned); err != nil { + t.Logf("destroy poisoned set %s: %v", poisoned, err) + } + }) + + port := &fw.Port{Values: []uint16{22}} + rule, err := manager.AddFilterRule(nil, sources, fw.Network{}, "tcp", nil, port, fw.ActionAccept) + require.NoError(t, err, "AddFilterRule must succeed by falling back") + + rr := rule.(*Rule) + all := rr.allSpecs() + require.Len(t, all, len(sources), "each source prefix needs its own rule") + for i, fs := range all { + joined := strings.Join(fs.specs, " ") + require.Contains(t, joined, "-s "+sources[i].String(), "fallback rule must match the source prefix") + require.NotContains(t, joined, matchSet) + checkRuleSpecs(t, ipv4Client, rr.chain, true, fs.specs...) + } + + require.True(t, manager.ipsetSupport.supported(), + "a failure specific to one set must not latch ipset off on a kernel that supports it") + + // A subsequent multi-source rule still gets a set. + next, err := manager.AddFilterRule(nil, []netip.Prefix{ + netip.MustParsePrefix("10.20.0.44/32"), + netip.MustParsePrefix("10.20.0.45/32"), + }, fw.Network{}, "tcp", nil, port, fw.ActionAccept) + require.NoError(t, err) + require.Len(t, findSets(next.(*Rule).specs), 1, "later rules must keep using ipset") + require.NoError(t, manager.DeleteFilterRule(next)) +} + +// TestIptablesIPSetProbe covers the confirmation step that decides whether a +// suspected ipset failure latches the capability off. On a kernel with the +// modules it must report ipset as usable and leave nothing behind: a probe rule +// or set left in place would be untracked. +func TestIptablesIPSetProbe(t *testing.T) { + ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) + require.NoError(t, err) + + manager, err := Create(ifaceMock, iface.DefaultMTU) + require.NoError(t, err) + require.NoError(t, manager.Init(nil)) + + defer func() { + require.NoError(t, manager.Close(nil)) + }() + + before, err := ipv4Client.List(tableFilter, chainACLInput) + require.NoError(t, err, "list acl chain") + + require.True(t, manager.family4.ipsetUsable(chainACLInput), "ipset must be usable on this kernel") + + after, err := ipv4Client.List(tableFilter, chainACLInput) + require.NoError(t, err, "list acl chain") + require.Equal(t, before, after, "the probe must leave the chain as it found it") + + sets, err := ipset.ListAll() + require.NoError(t, err, "list sets") + for _, s := range sets { + require.NotContains(t, s.SetName, "nb-probe-", "the probe must destroy its set") + } +} + +// TestIptablesFilterDestinationSetRequiresIPSet documents that a dynamic +// (domain) destination cannot be expressed without ipset: its prefixes are only +// known after DNS resolution, so there is nothing to expand into per-prefix +// rules. The call must report that rather than install a broader rule than the +// policy allows. +func TestIptablesFilterDestinationSetRequiresIPSet(t *testing.T) { + manager, err := Create(ifaceMock, iface.DefaultMTU) + require.NoError(t, err) + require.NoError(t, manager.Init(nil)) + + defer func() { + require.NoError(t, manager.Close(nil)) + }() + + manager.ipsetSupport.markUnsupported(errors.New("test: pretend the kernel has no ipset")) + + destination := fw.Network{Set: fw.NewDomainSet(domain.List{"example.com"})} + + _, err = manager.AddFilterRule(nil, []netip.Prefix{netip.MustParsePrefix("172.16.0.0/16")}, + destination, fw.ProtocolALL, nil, nil, fw.ActionAccept) + require.Error(t, err, "a domain destination is not expressible without ipset") + require.ErrorContains(t, err, "requires ipset") +} + +// TestIptablesFilterRollsBackPartialInstall covers a fallback rule whose second +// expanded rule cannot be installed. Nothing may be left behind: if the rule +// were tracked, a later call would short-circuit on it and report success while +// some sources were never installed, and an untracked leftover rule would keep +// matching with no way to remove it. +func TestIptablesFilterRollsBackPartialInstall(t *testing.T) { + ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) + require.NoError(t, err) + + manager, err := Create(ifaceMock, iface.DefaultMTU) + require.NoError(t, err) + require.NoError(t, manager.Init(nil)) + + defer func() { + require.NoError(t, manager.Close(nil)) + }() + + manager.ipsetSupport.markUnsupported(errors.New("test: pretend the kernel has no ipset")) + + // The v6 prefix is rejected by the v4 iptables binary, so the second rule + // of the expansion fails after the first has been installed. Call the + // family directly since the manager dispatches by the first source. + good := netip.MustParsePrefix("172.16.0.0/16") + sources := []netip.Prefix{good, netip.MustParsePrefix("2001:db8::/32")} + destination := fw.Network{Prefix: netip.MustParsePrefix("10.0.0.0/8")} + + _, err = manager.family4.AddFilterRule(nil, sources, destination, fw.ProtocolALL, nil, nil, fw.ActionDrop) + require.Error(t, err, "a source that iptables rejects must fail the whole rule") + + require.Empty(t, manager.family4.filters, "no rule may stay tracked") + + installed := []string{"-s", good.String(), "-d", "10.0.0.0/8", "-j", "DROP"} + checkRuleSpecs(t, ipv4Client, chainRTFwdIn, false, installed...) +} + // TestIptablesCloseRemovesAllState exercises a spread of rule kinds and then // asserts Close puts every table it touches back exactly as it found it. A // leaked chain, jump, or ipset survives the daemon and nothing can remove it diff --git a/client/firewall/iptables/router_linux_test.go b/client/firewall/iptables/router_linux_test.go index 6c4ae9425..f753626d2 100644 --- a/client/firewall/iptables/router_linux_test.go +++ b/client/firewall/iptables/router_linux_test.go @@ -31,7 +31,7 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) { iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) require.NoError(t, err, "failed to init iptables client") - manager, err := newFamily(iptablesClient, ifaceMock, iface.DefaultMTU) + manager, err := newFamily(iptablesClient, ifaceMock, iface.DefaultMTU, newIPSetSupport()) require.NoError(t, err, "should return a valid iptables manager") require.NoError(t, manager.init(nil)) @@ -84,7 +84,7 @@ func TestIptablesManager_AddNatRule(t *testing.T) { iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) require.NoError(t, err, "failed to init iptables client") - manager, err := newFamily(iptablesClient, ifaceMock, iface.DefaultMTU) + manager, err := newFamily(iptablesClient, ifaceMock, iface.DefaultMTU, newIPSetSupport()) require.NoError(t, err, "shouldn't return error") require.NoError(t, manager.init(nil)) @@ -157,7 +157,7 @@ func TestIptablesManager_RemoveNatRule(t *testing.T) { t.Run(testCase.Name, func(t *testing.T) { iptablesClient, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4) - manager, err := newFamily(iptablesClient, ifaceMock, iface.DefaultMTU) + manager, err := newFamily(iptablesClient, ifaceMock, iface.DefaultMTU, newIPSetSupport()) require.NoError(t, err, "shouldn't return error") require.NoError(t, manager.init(nil)) defer func() { @@ -219,7 +219,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) { iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4) require.NoError(t, err, "Failed to create iptables client") - r, err := newFamily(iptablesClient, ifaceMock, iface.DefaultMTU) + r, err := newFamily(iptablesClient, ifaceMock, iface.DefaultMTU, newIPSetSupport()) require.NoError(t, err, "Failed to create family manager") require.NoError(t, r.init(nil))