mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-17 12:19:07 +02:00
Add support for IPv6 networks (on Linux clients) (#1459)
* Feat add basic support for IPv6 networks Newly generated networks automatically generate an IPv6 prefix of size 64 within the ULA address range, devices obtain a randomly generated address within this prefix. Currently, this is Linux only and does not yet support all features (routes currently cause an error). * Fix firewall configuration for IPv6 networks * Fix routing configuration for IPv6 networks * Feat provide info on IPv6 support for specific client to mgmt server * Feat allow configuration of IPv6 support through API, improve stability * Feat add IPv6 support to new firewall implementation * Fix peer list item response not containing IPv6 address * Fix nftables breaking on IPv6 address change * Fix build issues for non-linux systems * Fix intermittent disconnections when IPv6 is enabled * Fix test issues and make some minor revisions * Fix some more testing issues * Fix more CI issues due to IPv6 * Fix more testing issues * Add inheritance of IPv6 enablement status from groups * Fix IPv6 events not having associated messages * Address first review comments regarding IPv6 support * Fix IPv6 table being created even when IPv6 is disabled Also improved stability of IPv6 route and firewall handling on client side * Fix IPv6 routes not being removed * Fix DNS IPv6 issues, limit IPv6 nameservers to IPv6 peers * Improve code for IPv6 DNS server selection, add AAAA custom records * Ensure IPv6 routes can only exist for IPv6 routing peers * Fix IPv6 network generation randomness * Fix a bunch of compilation issues and test failures * Replace method calls that are unavailable in Go 1.21 * Fix nil dereference in cleanUpDefaultForwardRules6 * Fix nil pointer dereference when persisting IPv6 network in sqlite * Clean up of client-side code changes for IPv6 * Fix nil dereference in rule mangling and compilation issues * Add a bunch of client-side test cases for IPv6 * Fix IPv6 tests running on unsupported environments * Fix import cycle in tests * Add missing method SupportsIPv6() for windows * Require IPv6 default route for IPv6 tests * Fix panics in routemanager tests on non-linux * Fix some more route manager tests concerning IPv6 * Add some final client-side tests * Add IPv6 tests for management code, small fixes * Fix linting issues * Fix small test suite issues * Fix linter issues and builds on macOS and Windows again * fix builds for iOS because of IPv6 breakage
This commit is contained in:
@@ -30,3 +30,9 @@ func NewFirewall(context context.Context, iface IFaceMapper) (firewall.Manager,
|
||||
}
|
||||
return fm, nil
|
||||
}
|
||||
|
||||
// Returns true if the current firewall implementation supports IPv6.
|
||||
// Currently false for anything non-linux.
|
||||
func SupportsIPv6() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ func NewFirewall(context context.Context, iface IFaceMapper) (firewall.Manager,
|
||||
return nil, errUsp
|
||||
}
|
||||
|
||||
// Note for devs: When adding IPv6 support to userspace bind, the implementation of AllowNetbird() has to be
|
||||
// adjusted accordingly.
|
||||
if err := fm.AllowNetbird(); err != nil {
|
||||
log.Errorf("failed to allow netbird interface traffic: %v", err)
|
||||
}
|
||||
@@ -83,6 +85,12 @@ func NewFirewall(context context.Context, iface IFaceMapper) (firewall.Manager,
|
||||
return fm, nil
|
||||
}
|
||||
|
||||
// Returns true if the current firewall implementation supports IPv6.
|
||||
// Currently true if the firewall is nftables.
|
||||
func SupportsIPv6() bool {
|
||||
return check() == NFTABLES
|
||||
}
|
||||
|
||||
// check returns the firewall type based on common lib checks. It returns UNKNOWN if no firewall is found.
|
||||
func check() FWType {
|
||||
useIPTABLES := false
|
||||
|
||||
@@ -6,6 +6,7 @@ import "github.com/netbirdio/netbird/iface"
|
||||
type IFaceMapper interface {
|
||||
Name() string
|
||||
Address() iface.WGAddress
|
||||
Address6() *iface.WGAddress
|
||||
IsUserspaceBind() bool
|
||||
SetFilter(iface.PacketFilter) error
|
||||
}
|
||||
|
||||
@@ -24,6 +24,14 @@ type Manager struct {
|
||||
router *routerManager
|
||||
}
|
||||
|
||||
func (m *Manager) ResetV6Firewall() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) V6Active() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// iFaceMapper defines subset methods of interface required for manager
|
||||
type iFaceMapper interface {
|
||||
Name() string
|
||||
|
||||
@@ -73,6 +73,9 @@ func TestIptablesManager_InsertRoutingRules(t *testing.T) {
|
||||
|
||||
for _, testCase := range test.InsertRuleTestCases {
|
||||
t.Run(testCase.Name, func(t *testing.T) {
|
||||
if testCase.IsV6 {
|
||||
t.Skip("Environment does not support IPv6, skipping IPv6 test...")
|
||||
}
|
||||
iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||
require.NoError(t, err, "failed to init iptables client")
|
||||
|
||||
@@ -154,6 +157,9 @@ func TestIptablesManager_RemoveRoutingRules(t *testing.T) {
|
||||
|
||||
for _, testCase := range test.RemoveRuleTestCases {
|
||||
t.Run(testCase.Name, func(t *testing.T) {
|
||||
if testCase.IsV6 {
|
||||
t.Skip("Environment does not support IPv6, skipping IPv6 test...")
|
||||
}
|
||||
iptablesClient, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||
|
||||
manager, err := newRouterManager(context.TODO(), iptablesClient)
|
||||
|
||||
@@ -76,6 +76,13 @@ type Manager interface {
|
||||
// RemoveRoutingRules removes a routing firewall rule
|
||||
RemoveRoutingRules(pair RouterPair) error
|
||||
|
||||
// ResetV6Firewall makes changes to the firewall to adapt to the IP address changes.
|
||||
// It is expected that after calling this method ApplyFiltering will be called to re-add the firewall rules.
|
||||
ResetV6Firewall() error
|
||||
|
||||
// V6Active returns whether IPv6 rules should/may be created by upper layers.
|
||||
V6Active() bool
|
||||
|
||||
// Reset firewall to the default state
|
||||
Reset() error
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,17 +36,25 @@ func Create(context context.Context, wgIface iFaceMapper) (*Manager, error) {
|
||||
wgIface: wgIface,
|
||||
}
|
||||
|
||||
workTable, err := m.createWorkTable()
|
||||
workTable, err := m.createWorkTable(nftables.TableFamilyIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.router, err = newRouter(context, workTable)
|
||||
var workTable6 *nftables.Table
|
||||
if wgIface.Address6() != nil {
|
||||
workTable6, err = m.createWorkTable(nftables.TableFamilyIPv6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
m.router, err = newRouter(context, workTable, workTable6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.aclManager, err = newAclManager(workTable, wgIface, m.router.RouteingFwChainName())
|
||||
m.aclManager, err = newAclManager(workTable, workTable6, wgIface, m.router.RouteingFwChainName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -54,6 +62,54 @@ func Create(context context.Context, wgIface iFaceMapper) (*Manager, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Resets the IPv6 Firewall Table to adapt to changes in IP addresses
|
||||
func (m *Manager) ResetV6Firewall() error {
|
||||
|
||||
// First, prepare reset by deleting all currently active rules.
|
||||
workTable6, err := m.aclManager.PrepareV6Reset()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Depending on whether we now have an IPv6 address, we now either have to create/empty an IPv6 table, or delete it.
|
||||
if m.wgIface.Address6() != nil {
|
||||
if workTable6 != nil {
|
||||
m.rConn.FlushTable(workTable6)
|
||||
} else {
|
||||
workTable6, err = m.createWorkTable(nftables.TableFamilyIPv6)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m.rConn.DelTable(workTable6)
|
||||
workTable6 = nil
|
||||
}
|
||||
err = m.rConn.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restore routing rules.
|
||||
err = m.router.RestoreAfterV6Reset(workTable6)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restore basic firewall chains (needs to happen after routes because chains from router must exist).
|
||||
// Does not restore rules (will be done later during the update, when UpdateFiltering will be called at some point)
|
||||
err = m.aclManager.ReinitAfterV6Reset(workTable6)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.rConn.Flush()
|
||||
}
|
||||
|
||||
func (m *Manager) V6Active() bool {
|
||||
return m.aclManager.v6Active
|
||||
}
|
||||
|
||||
// AddFiltering rule to the firewall
|
||||
//
|
||||
// If comment argument is empty firewall manager should set
|
||||
@@ -72,7 +128,7 @@ func (m *Manager) AddFiltering(
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
rawIP := ip.To4()
|
||||
if rawIP == nil {
|
||||
if rawIP == nil && m.wgIface.Address6() == nil {
|
||||
return nil, fmt.Errorf("unsupported IP version: %s", ip.String())
|
||||
}
|
||||
|
||||
@@ -114,6 +170,8 @@ func (m *Manager) AllowNetbird() error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
// Note for devs: When adding IPv6 support to uspfilter, the implementation of createDefaultAllowRules()
|
||||
// must be adjusted to include IPv6 rules.
|
||||
err := m.aclManager.createDefaultAllowRules()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create default allow rules: %v", err)
|
||||
@@ -211,8 +269,8 @@ func (m *Manager) Flush() error {
|
||||
return m.aclManager.Flush()
|
||||
}
|
||||
|
||||
func (m *Manager) createWorkTable() (*nftables.Table, error) {
|
||||
tables, err := m.rConn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
||||
func (m *Manager) createWorkTable(tableFamily nftables.TableFamily) (*nftables.Table, error) {
|
||||
tables, err := m.rConn.ListTablesOfFamily(tableFamily)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list of tables: %w", err)
|
||||
}
|
||||
@@ -223,7 +281,7 @@ func (m *Manager) createWorkTable() (*nftables.Table, error) {
|
||||
}
|
||||
}
|
||||
|
||||
table := m.rConn.AddTable(&nftables.Table{Name: tableName, Family: nftables.TableFamilyIPv4})
|
||||
table := m.rConn.AddTable(&nftables.Table{Name: tableName, Family: tableFamily})
|
||||
err = m.rConn.Flush()
|
||||
return table, err
|
||||
}
|
||||
|
||||
@@ -19,8 +19,9 @@ import (
|
||||
|
||||
// iFaceMapper defines subset methods of interface required for manager
|
||||
type iFaceMock struct {
|
||||
NameFunc func() string
|
||||
AddressFunc func() iface.WGAddress
|
||||
NameFunc func() string
|
||||
AddressFunc func() iface.WGAddress
|
||||
Address6Func func() *iface.WGAddress
|
||||
}
|
||||
|
||||
func (i *iFaceMock) Name() string {
|
||||
@@ -37,6 +38,13 @@ func (i *iFaceMock) Address() iface.WGAddress {
|
||||
panic("AddressFunc is not set")
|
||||
}
|
||||
|
||||
func (i *iFaceMock) Address6() *iface.WGAddress {
|
||||
if i.Address6Func != nil {
|
||||
return i.Address6Func()
|
||||
}
|
||||
panic("AddressFunc is not set")
|
||||
}
|
||||
|
||||
func (i *iFaceMock) IsUserspaceBind() bool { return false }
|
||||
|
||||
func TestNftablesManager(t *testing.T) {
|
||||
@@ -53,6 +61,7 @@ func TestNftablesManager(t *testing.T) {
|
||||
},
|
||||
}
|
||||
},
|
||||
Address6Func: func() *iface.WGAddress { return nil },
|
||||
}
|
||||
|
||||
// just check on the local interface
|
||||
@@ -99,11 +108,9 @@ func TestNftablesManager(t *testing.T) {
|
||||
Register: 1,
|
||||
Data: ifname("lo"),
|
||||
},
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseNetworkHeader,
|
||||
Offset: uint32(9),
|
||||
Len: uint32(1),
|
||||
&expr.Meta{
|
||||
Key: expr.MetaKeyL4PROTO,
|
||||
Register: 1,
|
||||
},
|
||||
&expr.Cmp{
|
||||
Register: 1,
|
||||
@@ -152,6 +159,370 @@ func TestNftablesManager(t *testing.T) {
|
||||
require.NoError(t, err, "failed to reset")
|
||||
}
|
||||
|
||||
func TestNftablesManager6Disabled(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),
|
||||
},
|
||||
}
|
||||
},
|
||||
Address6Func: func() *iface.WGAddress { return nil },
|
||||
}
|
||||
|
||||
// just check on the local interface
|
||||
manager, err := Create(context.Background(), mock)
|
||||
require.NoError(t, err)
|
||||
time.Sleep(time.Second * 3)
|
||||
|
||||
defer func() {
|
||||
err = manager.Reset()
|
||||
require.NoError(t, err, "failed to reset")
|
||||
time.Sleep(time.Second)
|
||||
}()
|
||||
|
||||
ip := net.ParseIP("2001:db8::fedc:ba09:8765:4321")
|
||||
|
||||
testClient := &nftables.Conn{}
|
||||
|
||||
_, err = manager.AddFiltering(
|
||||
ip,
|
||||
fw.ProtocolTCP,
|
||||
nil,
|
||||
&fw.Port{Values: []int{53}},
|
||||
fw.RuleDirectionIN,
|
||||
fw.ActionDrop,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
require.Error(t, err, "IPv6 rule should not be added when IPv6 is disabled")
|
||||
|
||||
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, 0, "expected no rules")
|
||||
|
||||
err = manager.Reset()
|
||||
require.NoError(t, err, "failed to reset")
|
||||
}
|
||||
|
||||
func TestNftablesManager6(t *testing.T) {
|
||||
|
||||
if !iface.SupportsIPv6() {
|
||||
t.Skip("Environment does not support IPv6, skipping IPv6 test...")
|
||||
}
|
||||
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),
|
||||
},
|
||||
}
|
||||
},
|
||||
Address6Func: func() *iface.WGAddress {
|
||||
return &iface.WGAddress{
|
||||
IP: net.ParseIP("2001:db8::0123:4567:890a:bcde"),
|
||||
Network: &net.IPNet{
|
||||
IP: net.ParseIP("2001:db8::"),
|
||||
Mask: net.CIDRMask(64, 128),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// just check on the local interface
|
||||
manager, err := Create(context.Background(), mock)
|
||||
require.NoError(t, err)
|
||||
time.Sleep(time.Second * 3)
|
||||
|
||||
defer func() {
|
||||
err = manager.Reset()
|
||||
require.NoError(t, err, "failed to reset")
|
||||
time.Sleep(time.Second)
|
||||
}()
|
||||
|
||||
require.True(t, manager.V6Active(), "IPv6 is not active even though it should be.")
|
||||
|
||||
ip := net.ParseIP("2001:db8::fedc:ba09:8765:4321")
|
||||
|
||||
testClient := &nftables.Conn{}
|
||||
|
||||
rule, err := manager.AddFiltering(
|
||||
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.workTable6, manager.aclManager.chainInputRules6)
|
||||
require.NoError(t, err, "failed to get rules")
|
||||
|
||||
require.Len(t, rules, 1, "expected 1 rules")
|
||||
|
||||
ipToAdd, _ := netip.AddrFromSlice(ip)
|
||||
add := ipToAdd.Unmap()
|
||||
expectedExprs := []expr.Any{
|
||||
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpEq,
|
||||
Register: 1,
|
||||
Data: ifname("lo"),
|
||||
},
|
||||
&expr.Meta{
|
||||
Key: expr.MetaKeyL4PROTO,
|
||||
Register: 1,
|
||||
},
|
||||
&expr.Cmp{
|
||||
Register: 1,
|
||||
Op: expr.CmpOpEq,
|
||||
Data: []byte{unix.IPPROTO_TCP},
|
||||
},
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseNetworkHeader,
|
||||
Offset: 8,
|
||||
Len: 16,
|
||||
},
|
||||
&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[0].Exprs, expectedExprs, "expected the same expressions")
|
||||
|
||||
for _, r := range rule {
|
||||
err = manager.DeleteRule(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.workTable6, manager.aclManager.chainInputRules6)
|
||||
require.NoError(t, err, "failed to get rules")
|
||||
require.Len(t, rules, 0, "expected 0 rules after deletion")
|
||||
|
||||
err = manager.Reset()
|
||||
require.NoError(t, err, "failed to reset")
|
||||
}
|
||||
|
||||
func TestNftablesManagerAddressReset6(t *testing.T) {
|
||||
|
||||
if !iface.SupportsIPv6() {
|
||||
t.Skip("Environment does not support IPv6, skipping IPv6 test...")
|
||||
}
|
||||
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),
|
||||
},
|
||||
}
|
||||
},
|
||||
Address6Func: func() *iface.WGAddress {
|
||||
return &iface.WGAddress{
|
||||
IP: net.ParseIP("2001:db8::0123:4567:890a:bcde"),
|
||||
Network: &net.IPNet{
|
||||
IP: net.ParseIP("2001:db8::"),
|
||||
Mask: net.CIDRMask(64, 128),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// just check on the local interface
|
||||
manager, err := Create(context.Background(), mock)
|
||||
require.NoError(t, err)
|
||||
time.Sleep(time.Second * 3)
|
||||
|
||||
defer func() {
|
||||
err = manager.Reset()
|
||||
require.NoError(t, err, "failed to reset")
|
||||
time.Sleep(time.Second)
|
||||
}()
|
||||
|
||||
require.True(t, manager.V6Active(), "IPv6 is not active even though it should be.")
|
||||
|
||||
ip := net.ParseIP("2001:db8::fedc:ba09:8765:4321")
|
||||
|
||||
testClient := &nftables.Conn{}
|
||||
|
||||
_, err = manager.AddFiltering(
|
||||
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.workTable6, manager.aclManager.chainInputRules6)
|
||||
require.NoError(t, err, "failed to get rules")
|
||||
|
||||
require.Len(t, rules, 1, "expected 1 rules")
|
||||
|
||||
mock.Address6Func = func() *iface.WGAddress {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = manager.ResetV6Firewall()
|
||||
require.NoError(t, err, "failed to reset IPv6 firewall")
|
||||
|
||||
err = manager.Flush()
|
||||
require.NoError(t, err, "failed to flush")
|
||||
|
||||
require.False(t, manager.V6Active(), "IPv6 is active even though it shouldn't be.")
|
||||
|
||||
tables, err := testClient.ListTablesOfFamily(nftables.TableFamilyIPv6)
|
||||
require.NoError(t, err, "failed to list IPv6 tables")
|
||||
|
||||
for _, table := range tables {
|
||||
if table.Name == tableName {
|
||||
t.Errorf("When IPv6 is disabled, the netbird table should not exist.")
|
||||
}
|
||||
}
|
||||
|
||||
mock.Address6Func = func() *iface.WGAddress {
|
||||
return &iface.WGAddress{
|
||||
IP: net.ParseIP("2001:db8::0123:4567:890a:bcdf"),
|
||||
Network: &net.IPNet{
|
||||
IP: net.ParseIP("2001:db8::"),
|
||||
Mask: net.CIDRMask(64, 128),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
err = manager.ResetV6Firewall()
|
||||
require.NoError(t, err, "failed to reset IPv6 firewall")
|
||||
|
||||
require.True(t, manager.V6Active(), "IPv6 is not active even though it should be.")
|
||||
|
||||
rule, err := manager.AddFiltering(
|
||||
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.workTable6, manager.aclManager.chainInputRules6)
|
||||
require.NoError(t, err, "failed to get rules")
|
||||
|
||||
require.Len(t, rules, 1, "expected 1 rule")
|
||||
|
||||
ipToAdd, _ := netip.AddrFromSlice(ip)
|
||||
add := ipToAdd.Unmap()
|
||||
expectedExprs := []expr.Any{
|
||||
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpEq,
|
||||
Register: 1,
|
||||
Data: ifname("lo"),
|
||||
},
|
||||
&expr.Meta{
|
||||
Key: expr.MetaKeyL4PROTO,
|
||||
Register: 1,
|
||||
},
|
||||
&expr.Cmp{
|
||||
Register: 1,
|
||||
Op: expr.CmpOpEq,
|
||||
Data: []byte{unix.IPPROTO_TCP},
|
||||
},
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseNetworkHeader,
|
||||
Offset: 8,
|
||||
Len: 16,
|
||||
},
|
||||
&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[0].Exprs, expectedExprs, "expected the same expressions")
|
||||
|
||||
for _, r := range rule {
|
||||
err = manager.DeleteRule(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.workTable6, manager.aclManager.chainInputRules6)
|
||||
require.NoError(t, err, "failed to get rules")
|
||||
require.Len(t, rules, 0, "expected 0 rules after deletion")
|
||||
|
||||
err = manager.Reset()
|
||||
require.NoError(t, err, "failed to reset")
|
||||
}
|
||||
|
||||
func TestNFtablesCreatePerformance(t *testing.T) {
|
||||
mock := &iFaceMock{
|
||||
NameFunc: func() string {
|
||||
@@ -166,6 +537,16 @@ func TestNFtablesCreatePerformance(t *testing.T) {
|
||||
},
|
||||
}
|
||||
},
|
||||
Address6Func: func() *iface.WGAddress {
|
||||
v6addr, v6net, _ := net.ParseCIDR("fd00:1234:dead:beef::1/64")
|
||||
return &iface.WGAddress{
|
||||
IP: v6addr,
|
||||
Network: &net.IPNet{
|
||||
IP: v6net.IP,
|
||||
Mask: v6net.Mask,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} {
|
||||
|
||||
@@ -26,7 +26,8 @@ const (
|
||||
|
||||
// some presets for building nftable rules
|
||||
var (
|
||||
zeroXor = binaryutil.NativeEndian.PutUint32(0)
|
||||
zeroXor = binaryutil.NativeEndian.PutUint32(0)
|
||||
zeroXor6 = []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
|
||||
|
||||
exprCounterAccept = []expr.Any{
|
||||
&expr.Counter{},
|
||||
@@ -39,48 +40,69 @@ var (
|
||||
)
|
||||
|
||||
type router struct {
|
||||
ctx context.Context
|
||||
stop context.CancelFunc
|
||||
conn *nftables.Conn
|
||||
workTable *nftables.Table
|
||||
filterTable *nftables.Table
|
||||
chains map[string]*nftables.Chain
|
||||
ctx context.Context
|
||||
stop context.CancelFunc
|
||||
conn *nftables.Conn
|
||||
workTable *nftables.Table
|
||||
workTable6 *nftables.Table
|
||||
filterTable *nftables.Table
|
||||
filterTable6 *nftables.Table
|
||||
chains map[string]*nftables.Chain
|
||||
chains6 map[string]*nftables.Chain
|
||||
// rules is useful to avoid duplicates and to get missing attributes that we don't have when adding new rules
|
||||
rules map[string]*nftables.Rule
|
||||
isDefaultFwdRulesEnabled bool
|
||||
rules map[string]*nftables.Rule
|
||||
rules6 map[string]*nftables.Rule
|
||||
isDefaultFwdRulesEnabled bool
|
||||
isDefaultFwdRulesEnabled6 bool
|
||||
}
|
||||
|
||||
func newRouter(parentCtx context.Context, workTable *nftables.Table) (*router, error) {
|
||||
func newRouter(parentCtx context.Context, workTable *nftables.Table, workTable6 *nftables.Table) (*router, error) {
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
|
||||
r := &router{
|
||||
ctx: ctx,
|
||||
stop: cancel,
|
||||
conn: &nftables.Conn{},
|
||||
workTable: workTable,
|
||||
chains: make(map[string]*nftables.Chain),
|
||||
rules: make(map[string]*nftables.Rule),
|
||||
ctx: ctx,
|
||||
stop: cancel,
|
||||
conn: &nftables.Conn{},
|
||||
workTable: workTable,
|
||||
workTable6: workTable6,
|
||||
chains: make(map[string]*nftables.Chain),
|
||||
chains6: make(map[string]*nftables.Chain),
|
||||
rules: make(map[string]*nftables.Rule),
|
||||
rules6: make(map[string]*nftables.Rule),
|
||||
}
|
||||
|
||||
var err error
|
||||
r.filterTable, err = r.loadFilterTable()
|
||||
r.filterTable, r.filterTable6, err = r.loadFilterTables()
|
||||
if err != nil {
|
||||
if errors.Is(err, errFilterTableNotFound) {
|
||||
log.Warnf("table 'filter' not found for forward rules")
|
||||
log.Warnf("table 'filter' not found for forward rules for one of the supported address families-")
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = r.cleanUpDefaultForwardRules()
|
||||
err = r.cleanUpDefaultForwardRules(false)
|
||||
if err != nil {
|
||||
log.Errorf("failed to clean up rules from FORWARD chain: %s", err)
|
||||
}
|
||||
|
||||
err = r.createContainers()
|
||||
err = r.cleanUpDefaultForwardRules(true)
|
||||
if err != nil {
|
||||
log.Errorf("failed to clean up rules from IPv6 FORWARD chain: %s", err)
|
||||
}
|
||||
|
||||
err = r.createContainers(false)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create containers for route: %s", err)
|
||||
}
|
||||
|
||||
if r.workTable6 != nil {
|
||||
err = r.createContainers(true)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create v6 containers for route: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return r, err
|
||||
}
|
||||
|
||||
@@ -90,43 +112,99 @@ func (r *router) RouteingFwChainName() string {
|
||||
|
||||
// ResetForwardRules cleans existing nftables default forward rules from the system
|
||||
func (r *router) ResetForwardRules() {
|
||||
err := r.cleanUpDefaultForwardRules()
|
||||
err := r.cleanUpDefaultForwardRules(false)
|
||||
if err != nil {
|
||||
log.Errorf("failed to reset forward rules: %s", err)
|
||||
}
|
||||
err = r.cleanUpDefaultForwardRules(true)
|
||||
if err != nil {
|
||||
log.Errorf("failed to reset forward rules: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *router) loadFilterTable() (*nftables.Table, error) {
|
||||
func (r *router) RestoreAfterV6Reset(newWorktable6 *nftables.Table) error {
|
||||
r.workTable6 = newWorktable6
|
||||
if newWorktable6 != nil {
|
||||
|
||||
err := r.cleanUpDefaultForwardRules(true)
|
||||
if err != nil {
|
||||
log.Errorf("failed to clean up rules from IPv6 FORWARD chain: %s", err)
|
||||
}
|
||||
|
||||
err = r.createContainers(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name, rule := range r.rules6 {
|
||||
rule = &nftables.Rule{
|
||||
Table: r.workTable6,
|
||||
Chain: r.chains6[rule.Chain.Name],
|
||||
Exprs: rule.Exprs,
|
||||
UserData: rule.UserData,
|
||||
}
|
||||
r.rules6[name] = r.conn.AddRule(rule)
|
||||
}
|
||||
}
|
||||
return r.conn.Flush()
|
||||
}
|
||||
|
||||
func (r *router) loadFilterTables() (*nftables.Table, *nftables.Table, error) {
|
||||
tables, err := r.conn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("nftables: unable to list tables: %v", err)
|
||||
return nil, nil, fmt.Errorf("nftables: unable to list tables: %v", err)
|
||||
}
|
||||
|
||||
var table4 *nftables.Table = nil
|
||||
for _, table := range tables {
|
||||
if table.Name == "filter" {
|
||||
return table, nil
|
||||
table4 = table
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errFilterTableNotFound
|
||||
var table6 *nftables.Table = nil
|
||||
tables, err = r.conn.ListTablesOfFamily(nftables.TableFamilyIPv6)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("nftables: unable to list tables: %v", err)
|
||||
}
|
||||
for _, table := range tables {
|
||||
if table.Name == "filter" {
|
||||
table6 = table
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err = nil
|
||||
if table4 == nil || table6 == nil {
|
||||
err = errFilterTableNotFound
|
||||
}
|
||||
|
||||
return table4, table6, err
|
||||
}
|
||||
|
||||
func (r *router) createContainers() error {
|
||||
func (r *router) createContainers(forV6 bool) error {
|
||||
workTable := r.workTable
|
||||
chainStorage := r.chains
|
||||
if forV6 {
|
||||
workTable = r.workTable6
|
||||
chainStorage = r.chains6
|
||||
}
|
||||
|
||||
r.chains[chainNameRouteingFw] = r.conn.AddChain(&nftables.Chain{
|
||||
chainStorage[chainNameRouteingFw] = r.conn.AddChain(&nftables.Chain{
|
||||
Name: chainNameRouteingFw,
|
||||
Table: r.workTable,
|
||||
Table: workTable,
|
||||
})
|
||||
|
||||
r.chains[chainNameRoutingNat] = r.conn.AddChain(&nftables.Chain{
|
||||
chainStorage[chainNameRoutingNat] = r.conn.AddChain(&nftables.Chain{
|
||||
Name: chainNameRoutingNat,
|
||||
Table: r.workTable,
|
||||
Table: workTable,
|
||||
Hooknum: nftables.ChainHookPostrouting,
|
||||
Priority: nftables.ChainPriorityNATSource - 1,
|
||||
Type: nftables.ChainTypeNAT,
|
||||
})
|
||||
|
||||
err := r.refreshRulesMap()
|
||||
err := r.refreshRulesMap(forV6)
|
||||
if err != nil {
|
||||
log.Errorf("failed to clean up rules from FORWARD chain: %s", err)
|
||||
}
|
||||
@@ -140,7 +218,13 @@ func (r *router) createContainers() error {
|
||||
|
||||
// InsertRoutingRules inserts a nftable rule pair to the forwarding chain and if enabled, to the nat chain
|
||||
func (r *router) InsertRoutingRules(pair manager.RouterPair) error {
|
||||
err := r.refreshRulesMap()
|
||||
parsedIp, _, _ := net.ParseCIDR(pair.Source)
|
||||
|
||||
if parsedIp.To4() == nil && r.workTable6 == nil {
|
||||
return fmt.Errorf("nftables: attempted to add IPv6 routing rule even though IPv6 is not enabled for this host")
|
||||
}
|
||||
|
||||
err := r.refreshRulesMap(parsedIp.To4() == nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -165,7 +249,11 @@ func (r *router) InsertRoutingRules(pair manager.RouterPair) error {
|
||||
}
|
||||
}
|
||||
|
||||
if r.filterTable != nil && !r.isDefaultFwdRulesEnabled {
|
||||
filterTable := r.filterTable
|
||||
if parsedIp.To4() == nil {
|
||||
filterTable = r.filterTable6
|
||||
}
|
||||
if filterTable != nil && !r.isDefaultFwdRulesEnabled {
|
||||
log.Debugf("add default accept forward rule")
|
||||
r.acceptForwardRule(pair.Source)
|
||||
}
|
||||
@@ -191,7 +279,13 @@ func (r *router) insertRoutingRule(format, chainName string, pair manager.Router
|
||||
|
||||
ruleKey := manager.GenKey(format, pair.ID)
|
||||
|
||||
_, exists := r.rules[ruleKey]
|
||||
parsedIp, _, _ := net.ParseCIDR(pair.Source)
|
||||
rules := r.rules
|
||||
if parsedIp.To4() == nil {
|
||||
rules = r.rules6
|
||||
}
|
||||
|
||||
_, exists := rules[ruleKey]
|
||||
if exists {
|
||||
err := r.removeRoutingRule(format, pair)
|
||||
if err != nil {
|
||||
@@ -199,18 +293,35 @@ func (r *router) insertRoutingRule(format, chainName string, pair manager.Router
|
||||
}
|
||||
}
|
||||
|
||||
r.rules[ruleKey] = r.conn.InsertRule(&nftables.Rule{
|
||||
Table: r.workTable,
|
||||
Chain: r.chains[chainName],
|
||||
table, chain := r.workTable, r.chains[chainName]
|
||||
if parsedIp.To4() == nil {
|
||||
table, chain = r.workTable6, r.chains6[chainName]
|
||||
}
|
||||
|
||||
newRule := r.conn.InsertRule(&nftables.Rule{
|
||||
Table: table,
|
||||
Chain: chain,
|
||||
Exprs: expression,
|
||||
UserData: []byte(ruleKey),
|
||||
})
|
||||
|
||||
if parsedIp.To4() == nil {
|
||||
r.rules[ruleKey] = newRule
|
||||
} else {
|
||||
r.rules6[ruleKey] = newRule
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *router) acceptForwardRule(sourceNetwork string) {
|
||||
src := generateCIDRMatcherExpressions(true, sourceNetwork)
|
||||
dst := generateCIDRMatcherExpressions(false, "0.0.0.0/0")
|
||||
table := r.filterTable
|
||||
parsedIp, _, _ := net.ParseCIDR(sourceNetwork)
|
||||
if parsedIp.To4() == nil {
|
||||
dst = generateCIDRMatcherExpressions(false, "::/0")
|
||||
table = r.filterTable6
|
||||
}
|
||||
|
||||
var exprs []expr.Any
|
||||
exprs = append(src, append(dst, &expr.Verdict{ // nolint:gocritic
|
||||
@@ -218,10 +329,10 @@ func (r *router) acceptForwardRule(sourceNetwork string) {
|
||||
})...)
|
||||
|
||||
rule := &nftables.Rule{
|
||||
Table: r.filterTable,
|
||||
Table: table,
|
||||
Chain: &nftables.Chain{
|
||||
Name: "FORWARD",
|
||||
Table: r.filterTable,
|
||||
Table: table,
|
||||
Type: nftables.ChainTypeFilter,
|
||||
Hooknum: nftables.ChainHookForward,
|
||||
Priority: nftables.ChainPriorityFilter,
|
||||
@@ -233,6 +344,9 @@ func (r *router) acceptForwardRule(sourceNetwork string) {
|
||||
r.conn.AddRule(rule)
|
||||
|
||||
src = generateCIDRMatcherExpressions(true, "0.0.0.0/0")
|
||||
if parsedIp.To4() == nil {
|
||||
src = generateCIDRMatcherExpressions(true, "::/0")
|
||||
}
|
||||
dst = generateCIDRMatcherExpressions(false, sourceNetwork)
|
||||
|
||||
exprs = append(src, append(dst, &expr.Verdict{ //nolint:gocritic
|
||||
@@ -240,10 +354,10 @@ func (r *router) acceptForwardRule(sourceNetwork string) {
|
||||
})...)
|
||||
|
||||
rule = &nftables.Rule{
|
||||
Table: r.filterTable,
|
||||
Table: table,
|
||||
Chain: &nftables.Chain{
|
||||
Name: "FORWARD",
|
||||
Table: r.filterTable,
|
||||
Table: table,
|
||||
Type: nftables.ChainTypeFilter,
|
||||
Hooknum: nftables.ChainHookForward,
|
||||
Priority: nftables.ChainPriorityFilter,
|
||||
@@ -252,12 +366,21 @@ func (r *router) acceptForwardRule(sourceNetwork string) {
|
||||
UserData: []byte(userDataAcceptForwardRuleDst),
|
||||
}
|
||||
r.conn.AddRule(rule)
|
||||
r.isDefaultFwdRulesEnabled = true
|
||||
if parsedIp.To4() == nil {
|
||||
r.isDefaultFwdRulesEnabled6 = true
|
||||
} else {
|
||||
r.isDefaultFwdRulesEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveRoutingRules removes a nftable rule pair from forwarding and nat chains
|
||||
func (r *router) RemoveRoutingRules(pair manager.RouterPair) error {
|
||||
err := r.refreshRulesMap()
|
||||
parsedIp, _, _ := net.ParseCIDR(pair.Source)
|
||||
if parsedIp.To4() == nil && r.workTable6 == nil {
|
||||
return fmt.Errorf("nftables: attempted to remove IPv6 routing rule even though IPv6 is not enabled for this host")
|
||||
}
|
||||
|
||||
err := r.refreshRulesMap(parsedIp.To4() == nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -282,8 +405,12 @@ func (r *router) RemoveRoutingRules(pair manager.RouterPair) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(r.rules) == 0 {
|
||||
err := r.cleanUpDefaultForwardRules()
|
||||
rulesList := r.rules
|
||||
if parsedIp.To4() == nil {
|
||||
rulesList = r.rules6
|
||||
}
|
||||
if len(rulesList) == 0 {
|
||||
err := r.cleanUpDefaultForwardRules(parsedIp.To4() == nil)
|
||||
if err != nil {
|
||||
log.Errorf("failed to clean up rules from FORWARD chain: %s", err)
|
||||
}
|
||||
@@ -301,7 +428,13 @@ func (r *router) RemoveRoutingRules(pair manager.RouterPair) error {
|
||||
func (r *router) removeRoutingRule(format string, pair manager.RouterPair) error {
|
||||
ruleKey := manager.GenKey(format, pair.ID)
|
||||
|
||||
rule, found := r.rules[ruleKey]
|
||||
parsedIp, _, _ := net.ParseCIDR(pair.Source)
|
||||
rules := r.rules
|
||||
if parsedIp.To4() == nil {
|
||||
rules = r.rules6
|
||||
}
|
||||
|
||||
rule, found := rules[ruleKey]
|
||||
if found {
|
||||
ruleType := "forwarding"
|
||||
if rule.Chain.Type == nftables.ChainTypeNAT {
|
||||
@@ -315,49 +448,68 @@ func (r *router) removeRoutingRule(format string, pair manager.RouterPair) error
|
||||
|
||||
log.Debugf("nftables: removing %s rule for %s", ruleType, pair.Destination)
|
||||
|
||||
delete(r.rules, ruleKey)
|
||||
delete(rules, ruleKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// refreshRulesMap refreshes the rule map with the latest rules. this is useful to avoid
|
||||
// duplicates and to get missing attributes that we don't have when adding new rules
|
||||
func (r *router) refreshRulesMap() error {
|
||||
for _, chain := range r.chains {
|
||||
func (r *router) refreshRulesMap(forV6 bool) error {
|
||||
chainList := r.chains
|
||||
if forV6 {
|
||||
chainList = r.chains6
|
||||
}
|
||||
for _, chain := range chainList {
|
||||
rules, err := r.conn.GetRules(chain.Table, chain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("nftables: unable to list rules: %v", err)
|
||||
}
|
||||
for _, rule := range rules {
|
||||
if len(rule.UserData) > 0 {
|
||||
r.rules[string(rule.UserData)] = rule
|
||||
if forV6 {
|
||||
r.rules6[string(rule.UserData)] = rule
|
||||
} else {
|
||||
r.rules[string(rule.UserData)] = rule
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *router) cleanUpDefaultForwardRules() error {
|
||||
if r.filterTable == nil {
|
||||
r.isDefaultFwdRulesEnabled = false
|
||||
func (r *router) cleanUpDefaultForwardRules(forV6 bool) error {
|
||||
tableFamily := nftables.TableFamilyIPv4
|
||||
filterTable := r.filterTable
|
||||
if forV6 {
|
||||
tableFamily = nftables.TableFamilyIPv6
|
||||
filterTable = r.filterTable6
|
||||
}
|
||||
|
||||
if filterTable == nil {
|
||||
if forV6 {
|
||||
r.isDefaultFwdRulesEnabled6 = false
|
||||
} else {
|
||||
r.isDefaultFwdRulesEnabled = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
chains, err := r.conn.ListChainsOfTableFamily(nftables.TableFamilyIPv4)
|
||||
chains, err := r.conn.ListChainsOfTableFamily(tableFamily)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var rules []*nftables.Rule
|
||||
for _, chain := range chains {
|
||||
if chain.Table.Name != r.filterTable.Name {
|
||||
if chain.Table.Name != filterTable.Name {
|
||||
continue
|
||||
}
|
||||
if chain.Name != "FORWARD" {
|
||||
continue
|
||||
}
|
||||
|
||||
rules, err = r.conn.GetRules(r.filterTable, chain)
|
||||
rules, err = r.conn.GetRules(filterTable, chain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -371,7 +523,12 @@ func (r *router) cleanUpDefaultForwardRules() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
r.isDefaultFwdRulesEnabled = false
|
||||
|
||||
if forV6 {
|
||||
r.isDefaultFwdRulesEnabled6 = false
|
||||
} else {
|
||||
r.isDefaultFwdRulesEnabled = false
|
||||
}
|
||||
return r.conn.Flush()
|
||||
}
|
||||
|
||||
@@ -387,6 +544,18 @@ func generateCIDRMatcherExpressions(source bool, cidr string) []expr.Any {
|
||||
} else {
|
||||
offSet = 16 // dst offset
|
||||
}
|
||||
addrLen := uint32(4)
|
||||
zeroXor := zeroXor
|
||||
|
||||
if ip.To4() == nil {
|
||||
if source {
|
||||
offSet = 8 // src offset
|
||||
} else {
|
||||
offSet = 24 // dst offset
|
||||
}
|
||||
addrLen = 16
|
||||
zeroXor = zeroXor6
|
||||
}
|
||||
|
||||
return []expr.Any{
|
||||
// fetch src add
|
||||
@@ -394,13 +563,13 @@ func generateCIDRMatcherExpressions(source bool, cidr string) []expr.Any {
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseNetworkHeader,
|
||||
Offset: offSet,
|
||||
Len: 4,
|
||||
Len: addrLen,
|
||||
},
|
||||
// net mask
|
||||
&expr.Bitwise{
|
||||
DestRegister: 1,
|
||||
SourceRegister: 1,
|
||||
Len: 4,
|
||||
Len: addrLen,
|
||||
Mask: network.Mask,
|
||||
Xor: zeroXor,
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ package nftables
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/netbirdio/netbird/iface"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
@@ -29,16 +30,19 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) {
|
||||
t.Skip("nftables not supported on this OS")
|
||||
}
|
||||
|
||||
table, err := createWorkTable()
|
||||
table, table6, err := createWorkTables()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer deleteWorkTable()
|
||||
defer deleteWorkTables()
|
||||
|
||||
for _, testCase := range test.InsertRuleTestCases {
|
||||
t.Run(testCase.Name, func(t *testing.T) {
|
||||
manager, err := newRouter(context.TODO(), table)
|
||||
if testCase.IsV6 && table6 == nil {
|
||||
t.Skip("Environment does not support IPv6, skipping IPv6 test...")
|
||||
}
|
||||
manager, err := newRouter(context.TODO(), table, table6)
|
||||
require.NoError(t, err, "failed to create router")
|
||||
|
||||
nftablesTestingClient := &nftables.Conn{}
|
||||
@@ -58,8 +62,13 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) {
|
||||
testingExpression := append(sourceExp, destExp...) //nolint:gocritic
|
||||
fwdRuleKey := firewall.GenKey(firewall.ForwardingFormat, testCase.InputPair.ID)
|
||||
|
||||
chains := manager.chains
|
||||
if testCase.IsV6 {
|
||||
chains = manager.chains6
|
||||
}
|
||||
|
||||
found := 0
|
||||
for _, chain := range manager.chains {
|
||||
for _, chain := range chains {
|
||||
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||
for _, rule := range rules {
|
||||
@@ -75,7 +84,7 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) {
|
||||
if testCase.InputPair.Masquerade {
|
||||
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair.ID)
|
||||
found := 0
|
||||
for _, chain := range manager.chains {
|
||||
for _, chain := range chains {
|
||||
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||
for _, rule := range rules {
|
||||
@@ -94,7 +103,7 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) {
|
||||
inFwdRuleKey := firewall.GenKey(firewall.InForwardingFormat, testCase.InputPair.ID)
|
||||
|
||||
found = 0
|
||||
for _, chain := range manager.chains {
|
||||
for _, chain := range chains {
|
||||
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||
for _, rule := range rules {
|
||||
@@ -110,7 +119,7 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) {
|
||||
if testCase.InputPair.Masquerade {
|
||||
inNatRuleKey := firewall.GenKey(firewall.InNatFormat, testCase.InputPair.ID)
|
||||
found := 0
|
||||
for _, chain := range manager.chains {
|
||||
for _, chain := range chains {
|
||||
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||
for _, rule := range rules {
|
||||
@@ -131,16 +140,19 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
|
||||
t.Skip("nftables not supported on this OS")
|
||||
}
|
||||
|
||||
table, err := createWorkTable()
|
||||
table, table6, err := createWorkTables()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer deleteWorkTable()
|
||||
defer deleteWorkTables()
|
||||
|
||||
for _, testCase := range test.RemoveRuleTestCases {
|
||||
t.Run(testCase.Name, func(t *testing.T) {
|
||||
manager, err := newRouter(context.TODO(), table)
|
||||
if testCase.IsV6 && table6 == nil {
|
||||
t.Skip("Environment does not support IPv6, skipping IPv6 test...")
|
||||
}
|
||||
manager, err := newRouter(context.TODO(), table, table6)
|
||||
require.NoError(t, err, "failed to create router")
|
||||
|
||||
nftablesTestingClient := &nftables.Conn{}
|
||||
@@ -150,11 +162,18 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
|
||||
sourceExp := generateCIDRMatcherExpressions(true, testCase.InputPair.Source)
|
||||
destExp := generateCIDRMatcherExpressions(false, testCase.InputPair.Destination)
|
||||
|
||||
chains := manager.chains
|
||||
workTable := table
|
||||
if testCase.IsV6 {
|
||||
chains = manager.chains6
|
||||
workTable = table6
|
||||
}
|
||||
|
||||
forwardExp := append(sourceExp, append(destExp, exprCounterAccept...)...) //nolint:gocritic
|
||||
forwardRuleKey := firewall.GenKey(firewall.ForwardingFormat, testCase.InputPair.ID)
|
||||
insertedForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||
Table: manager.workTable,
|
||||
Chain: manager.chains[chainNameRouteingFw],
|
||||
Table: workTable,
|
||||
Chain: chains[chainNameRouteingFw],
|
||||
Exprs: forwardExp,
|
||||
UserData: []byte(forwardRuleKey),
|
||||
})
|
||||
@@ -163,8 +182,8 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
|
||||
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair.ID)
|
||||
|
||||
insertedNat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||
Table: manager.workTable,
|
||||
Chain: manager.chains[chainNameRoutingNat],
|
||||
Table: workTable,
|
||||
Chain: chains[chainNameRoutingNat],
|
||||
Exprs: natExp,
|
||||
UserData: []byte(natRuleKey),
|
||||
})
|
||||
@@ -175,8 +194,8 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
|
||||
forwardExp = append(sourceExp, append(destExp, exprCounterAccept...)...) //nolint:gocritic
|
||||
inForwardRuleKey := firewall.GenKey(firewall.InForwardingFormat, testCase.InputPair.ID)
|
||||
insertedInForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||
Table: manager.workTable,
|
||||
Chain: manager.chains[chainNameRouteingFw],
|
||||
Table: workTable,
|
||||
Chain: chains[chainNameRouteingFw],
|
||||
Exprs: forwardExp,
|
||||
UserData: []byte(inForwardRuleKey),
|
||||
})
|
||||
@@ -185,8 +204,8 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
|
||||
inNatRuleKey := firewall.GenKey(firewall.InNatFormat, testCase.InputPair.ID)
|
||||
|
||||
insertedInNat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||
Table: manager.workTable,
|
||||
Chain: manager.chains[chainNameRoutingNat],
|
||||
Table: workTable,
|
||||
Chain: chains[chainNameRoutingNat],
|
||||
Exprs: natExp,
|
||||
UserData: []byte(inNatRuleKey),
|
||||
})
|
||||
@@ -199,7 +218,7 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
|
||||
err = manager.RemoveRoutingRules(testCase.InputPair)
|
||||
require.NoError(t, err, "shouldn't return error")
|
||||
|
||||
for _, chain := range manager.chains {
|
||||
for _, chain := range chains {
|
||||
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||
for _, rule := range rules {
|
||||
@@ -238,30 +257,39 @@ func isIptablesClientAvailable(client *iptables.IPTables) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func createWorkTable() (*nftables.Table, error) {
|
||||
func createWorkTables() (*nftables.Table, *nftables.Table, error) {
|
||||
sConn, err := nftables.New(nftables.AsLasting())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
tables, err := sConn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, t := range tables {
|
||||
tables6, err := sConn.ListTablesOfFamily(nftables.TableFamilyIPv6)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, t := range append(tables, tables6...) {
|
||||
if t.Name == tableName {
|
||||
sConn.DelTable(t)
|
||||
}
|
||||
}
|
||||
|
||||
table := sConn.AddTable(&nftables.Table{Name: tableName, Family: nftables.TableFamilyIPv4})
|
||||
var table6 *nftables.Table
|
||||
if iface.SupportsIPv6() {
|
||||
table6 = sConn.AddTable(&nftables.Table{Name: tableName, Family: nftables.TableFamilyIPv6})
|
||||
}
|
||||
err = sConn.Flush()
|
||||
|
||||
return table, err
|
||||
return table, table6, err
|
||||
}
|
||||
|
||||
func deleteWorkTable() {
|
||||
func deleteWorkTables() {
|
||||
sConn, err := nftables.New(nftables.AsLasting())
|
||||
if err != nil {
|
||||
return
|
||||
@@ -272,6 +300,12 @@ func deleteWorkTable() {
|
||||
return
|
||||
}
|
||||
|
||||
tables6, err := sConn.ListTablesOfFamily(nftables.TableFamilyIPv6)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tables = append(tables, tables6...)
|
||||
|
||||
for _, t := range tables {
|
||||
if t.Name == tableName {
|
||||
sConn.DelTable(t)
|
||||
|
||||
@@ -8,6 +8,7 @@ var (
|
||||
InsertRuleTestCases = []struct {
|
||||
Name string
|
||||
InputPair firewall.RouterPair
|
||||
IsV6 bool
|
||||
}{
|
||||
{
|
||||
Name: "Insert Forwarding IPV4 Rule",
|
||||
@@ -27,12 +28,32 @@ var (
|
||||
Masquerade: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Insert Forwarding IPV6 Rule",
|
||||
InputPair: firewall.RouterPair{
|
||||
ID: "zxa",
|
||||
Source: "2001:db8:0123:4567::1/128",
|
||||
Destination: "2001:db8:0123:abcd::/64",
|
||||
Masquerade: false,
|
||||
},
|
||||
IsV6: true,
|
||||
},
|
||||
{
|
||||
Name: "Insert Forwarding And Nat IPV6 Rules",
|
||||
InputPair: firewall.RouterPair{
|
||||
ID: "zxa",
|
||||
Source: "2001:db8:0123:4567::1/128",
|
||||
Destination: "2001:db8:0123:abcd::/64",
|
||||
Masquerade: true,
|
||||
},
|
||||
IsV6: true,
|
||||
},
|
||||
}
|
||||
|
||||
RemoveRuleTestCases = []struct {
|
||||
Name string
|
||||
InputPair firewall.RouterPair
|
||||
IpVersion string
|
||||
IsV6 bool
|
||||
}{
|
||||
{
|
||||
Name: "Remove Forwarding And Nat IPV4 Rules",
|
||||
@@ -43,5 +64,15 @@ var (
|
||||
Masquerade: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Remove Forwarding And Nat IPV6 Rules",
|
||||
InputPair: firewall.RouterPair{
|
||||
ID: "zxa",
|
||||
Source: "2001:db8:0123:4567::1/128",
|
||||
Destination: "2001:db8:0123:abcd::/64",
|
||||
Masquerade: true,
|
||||
},
|
||||
IsV6: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ var (
|
||||
type IFaceMapper interface {
|
||||
SetFilter(iface.PacketFilter) error
|
||||
Address() iface.WGAddress
|
||||
Address6() *iface.WGAddress
|
||||
}
|
||||
|
||||
// RuleSet is a set of rules grouped by a string key
|
||||
@@ -69,6 +70,14 @@ func CreateWithNativeFirewall(iface IFaceMapper, nativeFirewall firewall.Manager
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
func (m *Manager) ResetV6Firewall() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) V6Active() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func create(iface IFaceMapper) (*Manager, error) {
|
||||
m := &Manager{
|
||||
decoders: sync.Pool{
|
||||
|
||||
@@ -33,6 +33,10 @@ func (i *IFaceMock) Address() iface.WGAddress {
|
||||
return i.AddressFunc()
|
||||
}
|
||||
|
||||
func (i *IFaceMock) Address6() *iface.WGAddress {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestManagerCreate(t *testing.T) {
|
||||
ifaceMock := &IFaceMock{
|
||||
SetFilterFunc: func(iface.PacketFilter) error { return nil },
|
||||
|
||||
Reference in New Issue
Block a user