mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-26 00:29:06 +02:00
[client] Unify peer and route ACL filtering with multi-source rules (#6322)
* Unify peer and route ACL filtering with multi-source peer rules * Remove partial userspace firewall mode and open foreign chains via a table-less allower * Snapshot iptables rule maps before persisting state * Scope userspace firewall wildcard source rules per address family * Install nftables peer filter and mangle rules in a single transaction * Share the iptables jump rule spec between install and cleanup * Fix legacy ACL source wildcard and keep rollback tracking on delete failure * Fix CI: recognize multi-value port set lookups in tests and correct PeerIP lint suppression * Fall back to per-prefix filter rules when ipset is unavailable * Annotate legacy PeerIP usages in ACL tests and fix import formatting * Keep firewall rule bookkeeping in step with the kernel on replace and teardown * Release the routing reference when the route manager shuts down * Keep set references and rule tracking consistent when a routing rule fails
This commit is contained in:
@@ -72,13 +72,13 @@ func TestNftablesManager(t *testing.T) {
|
||||
|
||||
testClient := &nftables.Conn{}
|
||||
|
||||
rule, err := manager.AddPeerFiltering(nil, ip.AsSlice(), fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionDrop, "")
|
||||
rule, err := manager.AddFilterRule(nil, pfx(ip.AsSlice()), fw.Network{}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionDrop)
|
||||
require.NoError(t, err, "failed to add rule")
|
||||
|
||||
err = manager.Flush()
|
||||
require.NoError(t, err, "failed to flush")
|
||||
|
||||
rules, err := testClient.GetRules(manager.aclManager.workTable, manager.aclManager.chainInputRules)
|
||||
rules, err := testClient.GetRules(manager.family4.workTable, manager.family4.chainInputRules)
|
||||
require.NoError(t, err, "failed to get rules")
|
||||
|
||||
require.Len(t, rules, 2, "expected 2 rules")
|
||||
@@ -149,15 +149,12 @@ func TestNftablesManager(t *testing.T) {
|
||||
// Compare connection tracking rule at position 1 (pushed down by DROP rule insertion)
|
||||
compareExprsIgnoringCounters(t, rules[1].Exprs, expectedExprs1)
|
||||
|
||||
for _, r := range rule {
|
||||
err = manager.DeletePeerRule(r)
|
||||
require.NoError(t, err, "failed to delete rule")
|
||||
}
|
||||
require.NoError(t, manager.DeleteFilterRule(rule), "failed to delete rule")
|
||||
|
||||
err = manager.Flush()
|
||||
require.NoError(t, err, "failed to flush")
|
||||
|
||||
rules, err = testClient.GetRules(manager.aclManager.workTable, manager.aclManager.chainInputRules)
|
||||
rules, err = testClient.GetRules(manager.family4.workTable, manager.family4.chainInputRules)
|
||||
require.NoError(t, err, "failed to get rules")
|
||||
// established rule remains
|
||||
require.Len(t, rules, 1, "expected 1 rules after deletion")
|
||||
@@ -182,47 +179,39 @@ func TestNftablesManagerRuleOrder(t *testing.T) {
|
||||
testClient := &nftables.Conn{}
|
||||
|
||||
// Add accept rule first
|
||||
_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "accept-http")
|
||||
_, err = manager.AddFilterRule(nil, pfx(ip.AsSlice()), fw.Network{}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept)
|
||||
require.NoError(t, err, "failed to add accept rule")
|
||||
|
||||
// Add deny rule second for the same traffic
|
||||
_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionDrop, "deny-http")
|
||||
_, err = manager.AddFilterRule(nil, pfx(ip.AsSlice()), fw.Network{}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionDrop)
|
||||
require.NoError(t, err, "failed to add deny rule")
|
||||
|
||||
err = manager.Flush()
|
||||
require.NoError(t, err, "failed to flush")
|
||||
|
||||
rules, err := testClient.GetRules(manager.aclManager.workTable, manager.aclManager.chainInputRules)
|
||||
rules, err := testClient.GetRules(manager.family4.workTable, manager.family4.chainInputRules)
|
||||
require.NoError(t, err, "failed to get rules")
|
||||
|
||||
t.Logf("Found %d rules in nftables chain", len(rules))
|
||||
|
||||
// Find the accept and deny rules and verify deny comes before accept
|
||||
// Single-source rules emit a direct payload+cmp on the source IP
|
||||
// (no set lookup). Match by source-IP + port + verdict instead of
|
||||
// the legacy per-(action,port) set names ("deny-http"/"accept-http")
|
||||
// that this test predates.
|
||||
wantSrc := ip.AsSlice()
|
||||
var acceptRuleIndex, denyRuleIndex = -1, -1
|
||||
for i, rule := range rules {
|
||||
hasAcceptHTTPSet := false
|
||||
hasDenyHTTPSet := false
|
||||
hasPort80 := false
|
||||
var hasSrc, hasPort80 bool
|
||||
var action string
|
||||
|
||||
for _, e := range rule.Exprs {
|
||||
// Check for set lookup
|
||||
if lookup, ok := e.(*expr.Lookup); ok {
|
||||
switch lookup.SetName {
|
||||
case "accept-http":
|
||||
hasAcceptHTTPSet = true
|
||||
case "deny-http":
|
||||
hasDenyHTTPSet = true
|
||||
if cmp, ok := e.(*expr.Cmp); ok && cmp.Op == expr.CmpOpEq {
|
||||
if bytes.Equal(cmp.Data, wantSrc) {
|
||||
hasSrc = true
|
||||
}
|
||||
|
||||
}
|
||||
// Check for port 80
|
||||
if cmp, ok := e.(*expr.Cmp); ok {
|
||||
if cmp.Op == expr.CmpOpEq && len(cmp.Data) == 2 && binary.BigEndian.Uint16(cmp.Data) == 80 {
|
||||
if len(cmp.Data) == 2 && binary.BigEndian.Uint16(cmp.Data) == 80 {
|
||||
hasPort80 = true
|
||||
}
|
||||
}
|
||||
// Check for verdict
|
||||
if verdict, ok := e.(*expr.Verdict); ok {
|
||||
switch verdict.Kind {
|
||||
case expr.VerdictAccept:
|
||||
@@ -233,11 +222,15 @@ func TestNftablesManagerRuleOrder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if hasAcceptHTTPSet && hasPort80 && action == "ACCEPT" {
|
||||
t.Logf("Rule [%d]: accept-http set + Port 80 + ACCEPT", i)
|
||||
if !hasSrc || !hasPort80 {
|
||||
continue
|
||||
}
|
||||
switch action {
|
||||
case "ACCEPT":
|
||||
t.Logf("Rule [%d]: src=%s port=80 ACCEPT", i, ip)
|
||||
acceptRuleIndex = i
|
||||
} else if hasDenyHTTPSet && hasPort80 && action == "DROP" {
|
||||
t.Logf("Rule [%d]: deny-http set + Port 80 + DROP", i)
|
||||
case "DROP":
|
||||
t.Logf("Rule [%d]: src=%s port=80 DROP", i, ip)
|
||||
denyRuleIndex = i
|
||||
}
|
||||
}
|
||||
@@ -281,7 +274,7 @@ func TestNFtablesCreatePerformance(t *testing.T) {
|
||||
start := time.Now()
|
||||
for i := 0; i < testMax; i++ {
|
||||
port := &fw.Port{Values: []uint16{uint16(1000 + i)}}
|
||||
_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", nil, port, fw.ActionAccept, "")
|
||||
_, err = manager.AddFilterRule(nil, pfx(ip.AsSlice()), fw.Network{}, "tcp", nil, port, fw.ActionAccept)
|
||||
require.NoError(t, err, "failed to add rule")
|
||||
|
||||
if i%100 == 0 {
|
||||
@@ -363,10 +356,10 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) {
|
||||
})
|
||||
|
||||
ip := netip.MustParseAddr("100.96.0.1")
|
||||
_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "")
|
||||
_, err = manager.AddFilterRule(nil, pfx(ip.AsSlice()), fw.Network{}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept)
|
||||
require.NoError(t, err, "failed to add peer filtering rule")
|
||||
|
||||
_, err = manager.AddRouteFiltering(
|
||||
_, err = manager.AddFilterRule(
|
||||
nil,
|
||||
[]netip.Prefix{netip.MustParsePrefix("192.168.2.0/24")},
|
||||
fw.Network{Prefix: netip.MustParsePrefix("10.1.0.0/24")},
|
||||
@@ -439,10 +432,10 @@ func TestNftablesManagerIPv6CompatibilityWithIp6tables(t *testing.T) {
|
||||
})
|
||||
|
||||
ip := netip.MustParseAddr("fd00::2")
|
||||
_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "")
|
||||
_, err = manager.AddFilterRule(nil, pfx(ip.AsSlice()), fw.Network{}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept)
|
||||
require.NoError(t, err, "add v6 peer filtering rule")
|
||||
|
||||
_, err = manager.AddRouteFiltering(
|
||||
_, err = manager.AddFilterRule(
|
||||
nil,
|
||||
[]netip.Prefix{netip.MustParsePrefix("fd00:1::/64")},
|
||||
fw.Network{Prefix: netip.MustParsePrefix("2001:db8::/48")},
|
||||
@@ -552,7 +545,7 @@ func TestNftablesManagerCompatibilityWithIptablesFor6kPrefixes(t *testing.T) {
|
||||
prefixes = append(prefixes, netip.PrefixFrom(addr, 24))
|
||||
}
|
||||
}
|
||||
_, err = manager.AddRouteFiltering(
|
||||
_, err = manager.AddFilterRule(
|
||||
nil,
|
||||
prefixes,
|
||||
fw.Network{Prefix: netip.MustParsePrefix("10.2.0.0/24")},
|
||||
@@ -567,7 +560,7 @@ func TestNftablesManagerCompatibilityWithIptablesFor6kPrefixes(t *testing.T) {
|
||||
verifyIptablesOutput(t, stdout, stderr)
|
||||
}
|
||||
|
||||
func TestNftablesManagerCompatibilityWithIptablesForEmptyPrefixes(t *testing.T) {
|
||||
func TestNftablesManagerCompatibilityWithIptablesForWildcardSource(t *testing.T) {
|
||||
if check() != NFTABLES {
|
||||
t.Skip("nftables not supported on this system")
|
||||
}
|
||||
@@ -593,9 +586,9 @@ func TestNftablesManagerCompatibilityWithIptablesForEmptyPrefixes(t *testing.T)
|
||||
verifyIptablesOutput(t, stdout, stderr)
|
||||
})
|
||||
|
||||
_, err = manager.AddRouteFiltering(
|
||||
_, err = manager.AddFilterRule(
|
||||
nil,
|
||||
[]netip.Prefix{},
|
||||
[]netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||
fw.Network{Prefix: netip.MustParsePrefix("10.2.0.0/24")},
|
||||
fw.ProtocolTCP,
|
||||
nil,
|
||||
@@ -608,6 +601,73 @@ func TestNftablesManagerCompatibilityWithIptablesForEmptyPrefixes(t *testing.T)
|
||||
verifyIptablesOutput(t, stdout, stderr)
|
||||
}
|
||||
|
||||
func TestNftablesManagerMultiPortFilter(t *testing.T) {
|
||||
if check() != NFTABLES {
|
||||
t.Skip("nftables not supported on this system")
|
||||
}
|
||||
|
||||
manager, err := Create(ifaceMock, iface.DefaultMTU)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, manager.Init(nil))
|
||||
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, manager.Close(nil), "failed to reset manager state")
|
||||
})
|
||||
|
||||
ip := netip.MustParseAddr("100.96.0.1")
|
||||
|
||||
rule, err := manager.AddFilterRule(nil, pfx(ip.AsSlice()), fw.Network{}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80, 443}}, fw.ActionAccept)
|
||||
require.NoError(t, err, "failed to add multi-port rule")
|
||||
|
||||
testClient := &nftables.Conn{}
|
||||
rules, err := testClient.GetRules(manager.family4.workTable, manager.family4.chainInputRules)
|
||||
require.NoError(t, err, "failed to get rules")
|
||||
|
||||
var lookup *expr.Lookup
|
||||
for _, kernelRule := range rules {
|
||||
if string(kernelRule.UserData) != string(rule.ID()) {
|
||||
continue
|
||||
}
|
||||
for _, e := range kernelRule.Exprs {
|
||||
if l, ok := e.(*expr.Lookup); ok {
|
||||
lookup = l
|
||||
}
|
||||
}
|
||||
}
|
||||
require.NotNil(t, lookup, "multi-port rule must match ports via a set lookup")
|
||||
|
||||
sets, err := testClient.GetSets(manager.family4.workTable)
|
||||
require.NoError(t, err, "failed to get sets")
|
||||
|
||||
var portSet *nftables.Set
|
||||
for _, s := range sets {
|
||||
if s.Name == lookup.SetName {
|
||||
portSet = s
|
||||
}
|
||||
}
|
||||
require.NotNil(t, portSet, "anonymous port set not found in kernel")
|
||||
|
||||
portSet.Table = manager.family4.workTable
|
||||
elements, err := testClient.GetSetElements(portSet)
|
||||
require.NoError(t, err, "failed to get set elements")
|
||||
|
||||
ports := make(map[uint16]bool)
|
||||
for _, e := range elements {
|
||||
require.Len(t, e.Key, 2, "port set element key should be 2 bytes")
|
||||
ports[binary.BigEndian.Uint16(e.Key)] = true
|
||||
}
|
||||
require.True(t, ports[80], "port set should contain port 80")
|
||||
require.True(t, ports[443], "port set should contain port 443")
|
||||
|
||||
require.NoError(t, manager.DeleteFilterRule(rule), "failed to delete rule")
|
||||
|
||||
rules, err = testClient.GetRules(manager.family4.workTable, manager.family4.chainInputRules)
|
||||
require.NoError(t, err, "failed to get rules after delete")
|
||||
for _, kernelRule := range rules {
|
||||
require.NotEqual(t, string(rule.ID()), string(kernelRule.UserData), "rule should be removed from kernel")
|
||||
}
|
||||
}
|
||||
|
||||
func compareExprsIgnoringCounters(t *testing.T, got, want []expr.Any) {
|
||||
t.Helper()
|
||||
require.Equal(t, len(got), len(want), "expression count mismatch")
|
||||
|
||||
Reference in New Issue
Block a user