mirror of
https://github.com/netbirdio/netbird.git
synced 2024-12-12 18:00:49 +01:00
332 lines
7.9 KiB
Go
332 lines
7.9 KiB
Go
package nftables
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os/exec"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/nftables"
|
|
"github.com/google/nftables/binaryutil"
|
|
"github.com/google/nftables/expr"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sys/unix"
|
|
|
|
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
|
"github.com/netbirdio/netbird/client/iface"
|
|
)
|
|
|
|
var ifaceMock = &iFaceMock{
|
|
NameFunc: func() string {
|
|
return "lo"
|
|
},
|
|
AddressFunc: func() iface.WGAddress {
|
|
return iface.WGAddress{
|
|
IP: net.ParseIP("100.96.0.1"),
|
|
Network: &net.IPNet{
|
|
IP: net.ParseIP("100.96.0.0"),
|
|
Mask: net.IPv4Mask(255, 255, 255, 0),
|
|
},
|
|
}
|
|
},
|
|
}
|
|
|
|
// iFaceMapper defines subset methods of interface required for manager
|
|
type iFaceMock struct {
|
|
NameFunc func() string
|
|
AddressFunc func() iface.WGAddress
|
|
}
|
|
|
|
func (i *iFaceMock) Name() string {
|
|
if i.NameFunc != nil {
|
|
return i.NameFunc()
|
|
}
|
|
panic("NameFunc is not set")
|
|
}
|
|
|
|
func (i *iFaceMock) Address() iface.WGAddress {
|
|
if i.AddressFunc != nil {
|
|
return i.AddressFunc()
|
|
}
|
|
panic("AddressFunc is not set")
|
|
}
|
|
|
|
func (i *iFaceMock) IsUserspaceBind() bool { return false }
|
|
|
|
func TestNftablesManager(t *testing.T) {
|
|
|
|
// just check on the local interface
|
|
manager, err := Create(ifaceMock)
|
|
require.NoError(t, err)
|
|
require.NoError(t, manager.Init(nil))
|
|
time.Sleep(time.Second * 3)
|
|
|
|
defer func() {
|
|
err = manager.Reset(nil)
|
|
require.NoError(t, err, "failed to reset")
|
|
time.Sleep(time.Second)
|
|
}()
|
|
|
|
ip := net.ParseIP("100.96.0.1")
|
|
|
|
testClient := &nftables.Conn{}
|
|
|
|
rule, err := manager.AddPeerFiltering(
|
|
ip,
|
|
fw.ProtocolTCP,
|
|
nil,
|
|
&fw.Port{Values: []int{53}},
|
|
fw.RuleDirectionIN,
|
|
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)
|
|
require.NoError(t, err, "failed to get rules")
|
|
|
|
require.Len(t, rules, 2, "expected 2 rules")
|
|
|
|
expectedExprs1 := []expr.Any{
|
|
&expr.Ct{
|
|
Key: expr.CtKeySTATE,
|
|
Register: 1,
|
|
},
|
|
&expr.Bitwise{
|
|
SourceRegister: 1,
|
|
DestRegister: 1,
|
|
Len: 4,
|
|
Mask: binaryutil.NativeEndian.PutUint32(expr.CtStateBitESTABLISHED | expr.CtStateBitRELATED),
|
|
Xor: binaryutil.NativeEndian.PutUint32(0),
|
|
},
|
|
&expr.Cmp{
|
|
Op: expr.CmpOpNeq,
|
|
Register: 1,
|
|
Data: []byte{0, 0, 0, 0},
|
|
},
|
|
&expr.Counter{},
|
|
&expr.Verdict{
|
|
Kind: expr.VerdictAccept,
|
|
},
|
|
}
|
|
require.ElementsMatch(t, rules[0].Exprs, expectedExprs1, "expected the same expressions")
|
|
|
|
ipToAdd, _ := netip.AddrFromSlice(ip)
|
|
add := ipToAdd.Unmap()
|
|
expectedExprs2 := []expr.Any{
|
|
&expr.Payload{
|
|
DestRegister: 1,
|
|
Base: expr.PayloadBaseNetworkHeader,
|
|
Offset: uint32(9),
|
|
Len: uint32(1),
|
|
},
|
|
&expr.Cmp{
|
|
Register: 1,
|
|
Op: expr.CmpOpEq,
|
|
Data: []byte{unix.IPPROTO_TCP},
|
|
},
|
|
&expr.Payload{
|
|
DestRegister: 1,
|
|
Base: expr.PayloadBaseNetworkHeader,
|
|
Offset: 12,
|
|
Len: 4,
|
|
},
|
|
&expr.Cmp{
|
|
Op: expr.CmpOpEq,
|
|
Register: 1,
|
|
Data: add.AsSlice(),
|
|
},
|
|
&expr.Payload{
|
|
DestRegister: 1,
|
|
Base: expr.PayloadBaseTransportHeader,
|
|
Offset: 2,
|
|
Len: 2,
|
|
},
|
|
&expr.Cmp{
|
|
Op: expr.CmpOpEq,
|
|
Register: 1,
|
|
Data: []byte{0, 53},
|
|
},
|
|
&expr.Verdict{Kind: expr.VerdictDrop},
|
|
}
|
|
require.ElementsMatch(t, rules[1].Exprs, expectedExprs2, "expected the same expressions")
|
|
|
|
for _, r := range rule {
|
|
err = manager.DeletePeerRule(r)
|
|
require.NoError(t, err, "failed to delete rule")
|
|
}
|
|
|
|
err = manager.Flush()
|
|
require.NoError(t, err, "failed to flush")
|
|
|
|
rules, err = testClient.GetRules(manager.aclManager.workTable, manager.aclManager.chainInputRules)
|
|
require.NoError(t, err, "failed to get rules")
|
|
// established rule remains
|
|
require.Len(t, rules, 1, "expected 1 rules after deletion")
|
|
|
|
err = manager.Reset(nil)
|
|
require.NoError(t, err, "failed to reset")
|
|
}
|
|
|
|
func TestNFtablesCreatePerformance(t *testing.T) {
|
|
mock := &iFaceMock{
|
|
NameFunc: func() string {
|
|
return "lo"
|
|
},
|
|
AddressFunc: func() iface.WGAddress {
|
|
return iface.WGAddress{
|
|
IP: net.ParseIP("100.96.0.1"),
|
|
Network: &net.IPNet{
|
|
IP: net.ParseIP("100.96.0.0"),
|
|
Mask: net.IPv4Mask(255, 255, 255, 0),
|
|
},
|
|
}
|
|
},
|
|
}
|
|
|
|
for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} {
|
|
t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) {
|
|
// just check on the local interface
|
|
manager, err := Create(mock)
|
|
require.NoError(t, err)
|
|
require.NoError(t, manager.Init(nil))
|
|
time.Sleep(time.Second * 3)
|
|
|
|
defer func() {
|
|
if err := manager.Reset(nil); err != nil {
|
|
t.Errorf("clear the manager state: %v", err)
|
|
}
|
|
time.Sleep(time.Second)
|
|
}()
|
|
|
|
ip := net.ParseIP("10.20.0.100")
|
|
start := time.Now()
|
|
for i := 0; i < testMax; i++ {
|
|
port := &fw.Port{Values: []int{1000 + i}}
|
|
if i%2 == 0 {
|
|
_, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic")
|
|
} else {
|
|
_, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTP traffic")
|
|
}
|
|
require.NoError(t, err, "failed to add rule")
|
|
|
|
if i%100 == 0 {
|
|
err = manager.Flush()
|
|
require.NoError(t, err, "failed to flush")
|
|
}
|
|
}
|
|
|
|
t.Logf("execution avg per rule: %s", time.Since(start)/time.Duration(testMax))
|
|
})
|
|
}
|
|
}
|
|
|
|
func runIptablesSave(t *testing.T) (string, string) {
|
|
t.Helper()
|
|
var stdout, stderr bytes.Buffer
|
|
cmd := exec.Command("iptables-save")
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
err := cmd.Run()
|
|
require.NoError(t, err, "iptables-save failed to run")
|
|
|
|
return stdout.String(), stderr.String()
|
|
}
|
|
|
|
func verifyIptablesOutput(t *testing.T, stdout, stderr string) {
|
|
t.Helper()
|
|
// Check for any incompatibility warnings
|
|
require.NotContains(t,
|
|
stderr,
|
|
"incompatible",
|
|
"iptables-save produced compatibility warning. Full stderr: %s",
|
|
stderr,
|
|
)
|
|
|
|
// Verify standard tables are present
|
|
expectedTables := []string{
|
|
"*filter",
|
|
"*nat",
|
|
"*mangle",
|
|
}
|
|
|
|
for _, table := range expectedTables {
|
|
require.Contains(t,
|
|
stdout,
|
|
table,
|
|
"iptables-save output missing expected table: %s\nFull stdout: %s",
|
|
table,
|
|
stdout,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestNftablesManagerCompatibilityWithIptables(t *testing.T) {
|
|
if check() != NFTABLES {
|
|
t.Skip("nftables not supported on this system")
|
|
}
|
|
|
|
if _, err := exec.LookPath("iptables-save"); err != nil {
|
|
t.Skipf("iptables-save not available on this system: %v", err)
|
|
}
|
|
|
|
// First ensure iptables-nft tables exist by running iptables-save
|
|
stdout, stderr := runIptablesSave(t)
|
|
verifyIptablesOutput(t, stdout, stderr)
|
|
|
|
manager, err := Create(ifaceMock)
|
|
require.NoError(t, err, "failed to create manager")
|
|
require.NoError(t, manager.Init(nil))
|
|
|
|
t.Cleanup(func() {
|
|
err := manager.Reset(nil)
|
|
require.NoError(t, err, "failed to reset manager state")
|
|
|
|
// Verify iptables output after reset
|
|
stdout, stderr := runIptablesSave(t)
|
|
verifyIptablesOutput(t, stdout, stderr)
|
|
})
|
|
|
|
ip := net.ParseIP("100.96.0.1")
|
|
_, err = manager.AddPeerFiltering(
|
|
ip,
|
|
fw.ProtocolTCP,
|
|
nil,
|
|
&fw.Port{Values: []int{80}},
|
|
fw.RuleDirectionIN,
|
|
fw.ActionAccept,
|
|
"",
|
|
"test rule",
|
|
)
|
|
require.NoError(t, err, "failed to add peer filtering rule")
|
|
|
|
_, err = manager.AddRouteFiltering(
|
|
[]netip.Prefix{netip.MustParsePrefix("192.168.2.0/24")},
|
|
netip.MustParsePrefix("10.1.0.0/24"),
|
|
fw.ProtocolTCP,
|
|
nil,
|
|
&fw.Port{Values: []int{443}},
|
|
fw.ActionAccept,
|
|
)
|
|
require.NoError(t, err, "failed to add route filtering rule")
|
|
|
|
pair := fw.RouterPair{
|
|
Source: netip.MustParsePrefix("192.168.1.0/24"),
|
|
Destination: netip.MustParsePrefix("10.0.0.0/24"),
|
|
Masquerade: true,
|
|
}
|
|
err = manager.AddNatRule(pair)
|
|
require.NoError(t, err, "failed to add NAT rule")
|
|
|
|
stdout, stderr = runIptablesSave(t)
|
|
verifyIptablesOutput(t, stdout, stderr)
|
|
}
|