mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-25 16:19:07 +02:00
[client] fall back to per-IP ACL rules when ipset is unavailable (#6332)
This commit is contained in:
@@ -42,6 +42,7 @@ type aclManager struct {
|
|||||||
optionalEntries map[string][]entry
|
optionalEntries map[string][]entry
|
||||||
ipsetStore *ipsetStore
|
ipsetStore *ipsetStore
|
||||||
v6 bool
|
v6 bool
|
||||||
|
ipsetSupported bool
|
||||||
|
|
||||||
stateManager *statemanager.Manager
|
stateManager *statemanager.Manager
|
||||||
}
|
}
|
||||||
@@ -60,6 +61,8 @@ func newAclManager(iptablesClient *iptables.IPTables, wgIface iFaceMapper) (*acl
|
|||||||
func (m *aclManager) init(stateManager *statemanager.Manager) error {
|
func (m *aclManager) init(stateManager *statemanager.Manager) error {
|
||||||
m.stateManager = stateManager
|
m.stateManager = stateManager
|
||||||
|
|
||||||
|
m.ipsetSupported = m.probeIPSetSupport()
|
||||||
|
|
||||||
m.seedInitialEntries()
|
m.seedInitialEntries()
|
||||||
m.seedInitialOptionalEntries()
|
m.seedInitialOptionalEntries()
|
||||||
|
|
||||||
@@ -91,6 +94,12 @@ func (m *aclManager) AddPeerFiltering(
|
|||||||
if m.v6 && ipsetName != "" {
|
if m.v6 && ipsetName != "" {
|
||||||
ipsetName += "-v6"
|
ipsetName += "-v6"
|
||||||
}
|
}
|
||||||
|
// When the kernel lacks the required ipset hash module, fall back to
|
||||||
|
// per-IP iptables rules (pre-0.68 behavior) so ACLs keep working instead
|
||||||
|
// of silently leaving the chain empty.
|
||||||
|
if ipsetName != "" && !m.ipsetSupported {
|
||||||
|
ipsetName = ""
|
||||||
|
}
|
||||||
proto := protoForFamily(protocol, m.v6)
|
proto := protoForFamily(protocol, m.v6)
|
||||||
specs := filterRuleSpecs(ip, proto, sPort, dPort, action, ipsetName)
|
specs := filterRuleSpecs(ip, proto, sPort, dPort, action, ipsetName)
|
||||||
|
|
||||||
@@ -498,6 +507,40 @@ func transformIPsetName(ipsetName string, sPort, dPort *firewall.Port, action fi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// probeIPSetSupport checks whether the kernel can create the ipset type used for
|
||||||
|
// ACL rules. On kernels lacking the required ipset hash module, ipset creation
|
||||||
|
// fails (e.g. "invalid argument"), which would otherwise leave the ACL chain
|
||||||
|
// empty and silently drop all policy-permitted inbound traffic. When unsupported,
|
||||||
|
// the manager falls back to per-IP iptables rules.
|
||||||
|
func (m *aclManager) 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]
|
||||||
|
|
||||||
|
opts := ipset.CreateOptions{
|
||||||
|
Replace: true,
|
||||||
|
}
|
||||||
|
if m.v6 {
|
||||||
|
opts.Family = ipset.FamilyIPV6
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ipset.Create(probeName, ipset.TypeHashNet, opts); 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
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := ipset.Destroy(probeName); err != nil {
|
||||||
|
log.Debugf("destroy ipset probe set %q: %v", probeName, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (m *aclManager) createIPSet(name string) error {
|
func (m *aclManager) createIPSet(name string) error {
|
||||||
opts := ipset.CreateOptions{
|
opts := ipset.CreateOptions{
|
||||||
Replace: true,
|
Replace: true,
|
||||||
|
|||||||
@@ -291,3 +291,40 @@ func TestIptablesCreatePerformance(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestIptablesACLIPSetFallback verifies that when the kernel lacks ipset support,
|
||||||
|
// the ACL manager falls back to per-IP iptables rules (-s <ip>) instead of
|
||||||
|
// silently leaving the chain empty. See discussion #6125.
|
||||||
|
func TestIptablesACLIPSetFallback(t *testing.T) {
|
||||||
|
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Use Create()/Init() so the router-owned chains (chainRTFWDIN/OUT) are
|
||||||
|
// created before the ACL manager's createDefaultChains() references them.
|
||||||
|
manager, err := Create(ifaceMock, iface.DefaultMTU)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, manager.Init(nil))
|
||||||
|
|
||||||
|
aclMgr := manager.aclMgr
|
||||||
|
// Simulate a kernel without the ipset hash module.
|
||||||
|
aclMgr.ipsetSupported = false
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
require.NoError(t, manager.Close(nil))
|
||||||
|
}()
|
||||||
|
|
||||||
|
ip := netip.MustParseAddr("10.20.0.42")
|
||||||
|
port := &fw.Port{Values: []uint16{22}}
|
||||||
|
|
||||||
|
rules, err := aclMgr.AddPeerFiltering(nil, ip.AsSlice(), "tcp", nil, port, fw.ActionAccept, "nb0000001")
|
||||||
|
require.NoError(t, err, "AddPeerFiltering should succeed via fallback")
|
||||||
|
require.NotEmpty(t, rules)
|
||||||
|
|
||||||
|
rule := rules[0].(*Rule)
|
||||||
|
require.Empty(t, rule.ipsetName, "fallback rule must not reference an ipset")
|
||||||
|
require.Contains(t, strings.Join(rule.specs, " "), "-s 10.20.0.42", "fallback rule must match by source IP")
|
||||||
|
require.NotContains(t, strings.Join(rule.specs, " "), "--match-set", "fallback rule must not use ipset matching")
|
||||||
|
|
||||||
|
// The rule must actually be present in the ACL chain (not silently dropped).
|
||||||
|
checkRuleSpecs(t, ipv4Client, rule.chain, true, rule.specs...)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user