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:
Hugo Hakim Damer 2024-08-13 17:26:27 +02:00 committed by GitHub
parent 4da29451d0
commit 8b0398c0db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
85 changed files with 4311 additions and 795 deletions

View File

@ -30,3 +30,9 @@ func NewFirewall(context context.Context, iface IFaceMapper) (firewall.Manager,
} }
return fm, nil return fm, nil
} }
// Returns true if the current firewall implementation supports IPv6.
// Currently false for anything non-linux.
func SupportsIPv6() bool {
return false
}

View File

@ -70,6 +70,8 @@ func NewFirewall(context context.Context, iface IFaceMapper) (firewall.Manager,
return nil, errUsp 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 { if err := fm.AllowNetbird(); err != nil {
log.Errorf("failed to allow netbird interface traffic: %v", err) 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 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. // check returns the firewall type based on common lib checks. It returns UNKNOWN if no firewall is found.
func check() FWType { func check() FWType {
useIPTABLES := false useIPTABLES := false

View File

@ -6,6 +6,7 @@ import "github.com/netbirdio/netbird/iface"
type IFaceMapper interface { type IFaceMapper interface {
Name() string Name() string
Address() iface.WGAddress Address() iface.WGAddress
Address6() *iface.WGAddress
IsUserspaceBind() bool IsUserspaceBind() bool
SetFilter(iface.PacketFilter) error SetFilter(iface.PacketFilter) error
} }

View File

@ -24,6 +24,14 @@ type Manager struct {
router *routerManager 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 // iFaceMapper defines subset methods of interface required for manager
type iFaceMapper interface { type iFaceMapper interface {
Name() string Name() string

View File

@ -73,6 +73,9 @@ func TestIptablesManager_InsertRoutingRules(t *testing.T) {
for _, testCase := range test.InsertRuleTestCases { for _, testCase := range test.InsertRuleTestCases {
t.Run(testCase.Name, func(t *testing.T) { 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) iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
require.NoError(t, err, "failed to init iptables client") require.NoError(t, err, "failed to init iptables client")
@ -154,6 +157,9 @@ func TestIptablesManager_RemoveRoutingRules(t *testing.T) {
for _, testCase := range test.RemoveRuleTestCases { for _, testCase := range test.RemoveRuleTestCases {
t.Run(testCase.Name, func(t *testing.T) { 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) iptablesClient, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
manager, err := newRouterManager(context.TODO(), iptablesClient) manager, err := newRouterManager(context.TODO(), iptablesClient)

View File

@ -76,6 +76,13 @@ type Manager interface {
// RemoveRoutingRules removes a routing firewall rule // RemoveRoutingRules removes a routing firewall rule
RemoveRoutingRules(pair RouterPair) error 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 firewall to the default state
Reset() error Reset() error

File diff suppressed because it is too large Load Diff

View File

@ -36,17 +36,25 @@ func Create(context context.Context, wgIface iFaceMapper) (*Manager, error) {
wgIface: wgIface, wgIface: wgIface,
} }
workTable, err := m.createWorkTable() workTable, err := m.createWorkTable(nftables.TableFamilyIPv4)
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@ -54,6 +62,54 @@ func Create(context context.Context, wgIface iFaceMapper) (*Manager, error) {
return m, nil 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 // AddFiltering rule to the firewall
// //
// If comment argument is empty firewall manager should set // If comment argument is empty firewall manager should set
@ -72,7 +128,7 @@ func (m *Manager) AddFiltering(
defer m.mutex.Unlock() defer m.mutex.Unlock()
rawIP := ip.To4() rawIP := ip.To4()
if rawIP == nil { if rawIP == nil && m.wgIface.Address6() == nil {
return nil, fmt.Errorf("unsupported IP version: %s", ip.String()) return nil, fmt.Errorf("unsupported IP version: %s", ip.String())
} }
@ -114,6 +170,8 @@ func (m *Manager) AllowNetbird() error {
m.mutex.Lock() m.mutex.Lock()
defer m.mutex.Unlock() 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() err := m.aclManager.createDefaultAllowRules()
if err != nil { if err != nil {
return fmt.Errorf("failed to create default allow rules: %v", err) return fmt.Errorf("failed to create default allow rules: %v", err)
@ -211,8 +269,8 @@ func (m *Manager) Flush() error {
return m.aclManager.Flush() return m.aclManager.Flush()
} }
func (m *Manager) createWorkTable() (*nftables.Table, error) { func (m *Manager) createWorkTable(tableFamily nftables.TableFamily) (*nftables.Table, error) {
tables, err := m.rConn.ListTablesOfFamily(nftables.TableFamilyIPv4) tables, err := m.rConn.ListTablesOfFamily(tableFamily)
if err != nil { if err != nil {
return nil, fmt.Errorf("list of tables: %w", err) 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() err = m.rConn.Flush()
return table, err return table, err
} }

View File

@ -19,8 +19,9 @@ import (
// iFaceMapper defines subset methods of interface required for manager // iFaceMapper defines subset methods of interface required for manager
type iFaceMock struct { type iFaceMock struct {
NameFunc func() string NameFunc func() string
AddressFunc func() iface.WGAddress AddressFunc func() iface.WGAddress
Address6Func func() *iface.WGAddress
} }
func (i *iFaceMock) Name() string { func (i *iFaceMock) Name() string {
@ -37,6 +38,13 @@ func (i *iFaceMock) Address() iface.WGAddress {
panic("AddressFunc is not set") 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 (i *iFaceMock) IsUserspaceBind() bool { return false }
func TestNftablesManager(t *testing.T) { 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 // just check on the local interface
@ -99,11 +108,9 @@ func TestNftablesManager(t *testing.T) {
Register: 1, Register: 1,
Data: ifname("lo"), Data: ifname("lo"),
}, },
&expr.Payload{ &expr.Meta{
DestRegister: 1, Key: expr.MetaKeyL4PROTO,
Base: expr.PayloadBaseNetworkHeader, Register: 1,
Offset: uint32(9),
Len: uint32(1),
}, },
&expr.Cmp{ &expr.Cmp{
Register: 1, Register: 1,
@ -152,6 +159,370 @@ func TestNftablesManager(t *testing.T) {
require.NoError(t, err, "failed to reset") 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) { func TestNFtablesCreatePerformance(t *testing.T) {
mock := &iFaceMock{ mock := &iFaceMock{
NameFunc: func() string { 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} { for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} {

View File

@ -26,7 +26,8 @@ const (
// some presets for building nftable rules // some presets for building nftable rules
var ( 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{ exprCounterAccept = []expr.Any{
&expr.Counter{}, &expr.Counter{},
@ -39,48 +40,69 @@ var (
) )
type router struct { type router struct {
ctx context.Context ctx context.Context
stop context.CancelFunc stop context.CancelFunc
conn *nftables.Conn conn *nftables.Conn
workTable *nftables.Table workTable *nftables.Table
filterTable *nftables.Table workTable6 *nftables.Table
chains map[string]*nftables.Chain 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 is useful to avoid duplicates and to get missing attributes that we don't have when adding new rules
rules map[string]*nftables.Rule rules map[string]*nftables.Rule
isDefaultFwdRulesEnabled bool 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) ctx, cancel := context.WithCancel(parentCtx)
r := &router{ r := &router{
ctx: ctx, ctx: ctx,
stop: cancel, stop: cancel,
conn: &nftables.Conn{}, conn: &nftables.Conn{},
workTable: workTable, workTable: workTable,
chains: make(map[string]*nftables.Chain), workTable6: workTable6,
rules: make(map[string]*nftables.Rule), 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 var err error
r.filterTable, err = r.loadFilterTable() r.filterTable, r.filterTable6, err = r.loadFilterTables()
if err != nil { if err != nil {
if errors.Is(err, errFilterTableNotFound) { 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 { } else {
return nil, err return nil, err
} }
} }
err = r.cleanUpDefaultForwardRules() err = r.cleanUpDefaultForwardRules(false)
if err != nil { if err != nil {
log.Errorf("failed to clean up rules from FORWARD chain: %s", err) 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 { if err != nil {
log.Errorf("failed to create containers for route: %s", err) 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 return r, err
} }
@ -90,43 +112,99 @@ func (r *router) RouteingFwChainName() string {
// ResetForwardRules cleans existing nftables default forward rules from the system // ResetForwardRules cleans existing nftables default forward rules from the system
func (r *router) ResetForwardRules() { 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 { if err != nil {
log.Errorf("failed to reset forward rules: %s", err) 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) tables, err := r.conn.ListTablesOfFamily(nftables.TableFamilyIPv4)
if err != nil { 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 { for _, table := range tables {
if table.Name == "filter" { 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, Name: chainNameRouteingFw,
Table: r.workTable, Table: workTable,
}) })
r.chains[chainNameRoutingNat] = r.conn.AddChain(&nftables.Chain{ chainStorage[chainNameRoutingNat] = r.conn.AddChain(&nftables.Chain{
Name: chainNameRoutingNat, Name: chainNameRoutingNat,
Table: r.workTable, Table: workTable,
Hooknum: nftables.ChainHookPostrouting, Hooknum: nftables.ChainHookPostrouting,
Priority: nftables.ChainPriorityNATSource - 1, Priority: nftables.ChainPriorityNATSource - 1,
Type: nftables.ChainTypeNAT, Type: nftables.ChainTypeNAT,
}) })
err := r.refreshRulesMap() err := r.refreshRulesMap(forV6)
if err != nil { if err != nil {
log.Errorf("failed to clean up rules from FORWARD chain: %s", err) 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 // 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 { 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 { if err != nil {
return err 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") log.Debugf("add default accept forward rule")
r.acceptForwardRule(pair.Source) r.acceptForwardRule(pair.Source)
} }
@ -191,7 +279,13 @@ func (r *router) insertRoutingRule(format, chainName string, pair manager.Router
ruleKey := manager.GenKey(format, pair.ID) 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 { if exists {
err := r.removeRoutingRule(format, pair) err := r.removeRoutingRule(format, pair)
if err != nil { 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, chain := r.workTable, r.chains[chainName]
Table: r.workTable, if parsedIp.To4() == nil {
Chain: r.chains[chainName], table, chain = r.workTable6, r.chains6[chainName]
}
newRule := r.conn.InsertRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: expression, Exprs: expression,
UserData: []byte(ruleKey), UserData: []byte(ruleKey),
}) })
if parsedIp.To4() == nil {
r.rules[ruleKey] = newRule
} else {
r.rules6[ruleKey] = newRule
}
return nil return nil
} }
func (r *router) acceptForwardRule(sourceNetwork string) { func (r *router) acceptForwardRule(sourceNetwork string) {
src := generateCIDRMatcherExpressions(true, sourceNetwork) src := generateCIDRMatcherExpressions(true, sourceNetwork)
dst := generateCIDRMatcherExpressions(false, "0.0.0.0/0") 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 var exprs []expr.Any
exprs = append(src, append(dst, &expr.Verdict{ // nolint:gocritic exprs = append(src, append(dst, &expr.Verdict{ // nolint:gocritic
@ -218,10 +329,10 @@ func (r *router) acceptForwardRule(sourceNetwork string) {
})...) })...)
rule := &nftables.Rule{ rule := &nftables.Rule{
Table: r.filterTable, Table: table,
Chain: &nftables.Chain{ Chain: &nftables.Chain{
Name: "FORWARD", Name: "FORWARD",
Table: r.filterTable, Table: table,
Type: nftables.ChainTypeFilter, Type: nftables.ChainTypeFilter,
Hooknum: nftables.ChainHookForward, Hooknum: nftables.ChainHookForward,
Priority: nftables.ChainPriorityFilter, Priority: nftables.ChainPriorityFilter,
@ -233,6 +344,9 @@ func (r *router) acceptForwardRule(sourceNetwork string) {
r.conn.AddRule(rule) r.conn.AddRule(rule)
src = generateCIDRMatcherExpressions(true, "0.0.0.0/0") src = generateCIDRMatcherExpressions(true, "0.0.0.0/0")
if parsedIp.To4() == nil {
src = generateCIDRMatcherExpressions(true, "::/0")
}
dst = generateCIDRMatcherExpressions(false, sourceNetwork) dst = generateCIDRMatcherExpressions(false, sourceNetwork)
exprs = append(src, append(dst, &expr.Verdict{ //nolint:gocritic exprs = append(src, append(dst, &expr.Verdict{ //nolint:gocritic
@ -240,10 +354,10 @@ func (r *router) acceptForwardRule(sourceNetwork string) {
})...) })...)
rule = &nftables.Rule{ rule = &nftables.Rule{
Table: r.filterTable, Table: table,
Chain: &nftables.Chain{ Chain: &nftables.Chain{
Name: "FORWARD", Name: "FORWARD",
Table: r.filterTable, Table: table,
Type: nftables.ChainTypeFilter, Type: nftables.ChainTypeFilter,
Hooknum: nftables.ChainHookForward, Hooknum: nftables.ChainHookForward,
Priority: nftables.ChainPriorityFilter, Priority: nftables.ChainPriorityFilter,
@ -252,12 +366,21 @@ func (r *router) acceptForwardRule(sourceNetwork string) {
UserData: []byte(userDataAcceptForwardRuleDst), UserData: []byte(userDataAcceptForwardRuleDst),
} }
r.conn.AddRule(rule) 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 // RemoveRoutingRules removes a nftable rule pair from forwarding and nat chains
func (r *router) RemoveRoutingRules(pair manager.RouterPair) error { 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 { if err != nil {
return err return err
} }
@ -282,8 +405,12 @@ func (r *router) RemoveRoutingRules(pair manager.RouterPair) error {
return err return err
} }
if len(r.rules) == 0 { rulesList := r.rules
err := r.cleanUpDefaultForwardRules() if parsedIp.To4() == nil {
rulesList = r.rules6
}
if len(rulesList) == 0 {
err := r.cleanUpDefaultForwardRules(parsedIp.To4() == nil)
if err != nil { if err != nil {
log.Errorf("failed to clean up rules from FORWARD chain: %s", err) 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 { func (r *router) removeRoutingRule(format string, pair manager.RouterPair) error {
ruleKey := manager.GenKey(format, pair.ID) 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 { if found {
ruleType := "forwarding" ruleType := "forwarding"
if rule.Chain.Type == nftables.ChainTypeNAT { 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) log.Debugf("nftables: removing %s rule for %s", ruleType, pair.Destination)
delete(r.rules, ruleKey) delete(rules, ruleKey)
} }
return nil return nil
} }
// refreshRulesMap refreshes the rule map with the latest rules. this is useful to avoid // 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 // duplicates and to get missing attributes that we don't have when adding new rules
func (r *router) refreshRulesMap() error { func (r *router) refreshRulesMap(forV6 bool) error {
for _, chain := range r.chains { chainList := r.chains
if forV6 {
chainList = r.chains6
}
for _, chain := range chainList {
rules, err := r.conn.GetRules(chain.Table, chain) rules, err := r.conn.GetRules(chain.Table, chain)
if err != nil { if err != nil {
return fmt.Errorf("nftables: unable to list rules: %v", err) return fmt.Errorf("nftables: unable to list rules: %v", err)
} }
for _, rule := range rules { for _, rule := range rules {
if len(rule.UserData) > 0 { 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 return nil
} }
func (r *router) cleanUpDefaultForwardRules() error { func (r *router) cleanUpDefaultForwardRules(forV6 bool) error {
if r.filterTable == nil { tableFamily := nftables.TableFamilyIPv4
r.isDefaultFwdRulesEnabled = false 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 return nil
} }
chains, err := r.conn.ListChainsOfTableFamily(nftables.TableFamilyIPv4) chains, err := r.conn.ListChainsOfTableFamily(tableFamily)
if err != nil { if err != nil {
return err return err
} }
var rules []*nftables.Rule var rules []*nftables.Rule
for _, chain := range chains { for _, chain := range chains {
if chain.Table.Name != r.filterTable.Name { if chain.Table.Name != filterTable.Name {
continue continue
} }
if chain.Name != "FORWARD" { if chain.Name != "FORWARD" {
continue continue
} }
rules, err = r.conn.GetRules(r.filterTable, chain) rules, err = r.conn.GetRules(filterTable, chain)
if err != nil { if err != nil {
return err 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() return r.conn.Flush()
} }
@ -387,6 +544,18 @@ func generateCIDRMatcherExpressions(source bool, cidr string) []expr.Any {
} else { } else {
offSet = 16 // dst offset 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{ return []expr.Any{
// fetch src add // fetch src add
@ -394,13 +563,13 @@ func generateCIDRMatcherExpressions(source bool, cidr string) []expr.Any {
DestRegister: 1, DestRegister: 1,
Base: expr.PayloadBaseNetworkHeader, Base: expr.PayloadBaseNetworkHeader,
Offset: offSet, Offset: offSet,
Len: 4, Len: addrLen,
}, },
// net mask // net mask
&expr.Bitwise{ &expr.Bitwise{
DestRegister: 1, DestRegister: 1,
SourceRegister: 1, SourceRegister: 1,
Len: 4, Len: addrLen,
Mask: network.Mask, Mask: network.Mask,
Xor: zeroXor, Xor: zeroXor,
}, },

View File

@ -4,6 +4,7 @@ package nftables
import ( import (
"context" "context"
"github.com/netbirdio/netbird/iface"
"testing" "testing"
"github.com/coreos/go-iptables/iptables" "github.com/coreos/go-iptables/iptables"
@ -29,16 +30,19 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) {
t.Skip("nftables not supported on this OS") t.Skip("nftables not supported on this OS")
} }
table, err := createWorkTable() table, table6, err := createWorkTables()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer deleteWorkTable() defer deleteWorkTables()
for _, testCase := range test.InsertRuleTestCases { for _, testCase := range test.InsertRuleTestCases {
t.Run(testCase.Name, func(t *testing.T) { 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") require.NoError(t, err, "failed to create router")
nftablesTestingClient := &nftables.Conn{} nftablesTestingClient := &nftables.Conn{}
@ -58,8 +62,13 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) {
testingExpression := append(sourceExp, destExp...) //nolint:gocritic testingExpression := append(sourceExp, destExp...) //nolint:gocritic
fwdRuleKey := firewall.GenKey(firewall.ForwardingFormat, testCase.InputPair.ID) fwdRuleKey := firewall.GenKey(firewall.ForwardingFormat, testCase.InputPair.ID)
chains := manager.chains
if testCase.IsV6 {
chains = manager.chains6
}
found := 0 found := 0
for _, chain := range manager.chains { for _, chain := range chains {
rules, err := nftablesTestingClient.GetRules(chain.Table, chain) 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) require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
for _, rule := range rules { for _, rule := range rules {
@ -75,7 +84,7 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) {
if testCase.InputPair.Masquerade { if testCase.InputPair.Masquerade {
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair.ID) natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair.ID)
found := 0 found := 0
for _, chain := range manager.chains { for _, chain := range chains {
rules, err := nftablesTestingClient.GetRules(chain.Table, chain) 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) require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
for _, rule := range rules { for _, rule := range rules {
@ -94,7 +103,7 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) {
inFwdRuleKey := firewall.GenKey(firewall.InForwardingFormat, testCase.InputPair.ID) inFwdRuleKey := firewall.GenKey(firewall.InForwardingFormat, testCase.InputPair.ID)
found = 0 found = 0
for _, chain := range manager.chains { for _, chain := range chains {
rules, err := nftablesTestingClient.GetRules(chain.Table, chain) 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) require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
for _, rule := range rules { for _, rule := range rules {
@ -110,7 +119,7 @@ func TestNftablesManager_InsertRoutingRules(t *testing.T) {
if testCase.InputPair.Masquerade { if testCase.InputPair.Masquerade {
inNatRuleKey := firewall.GenKey(firewall.InNatFormat, testCase.InputPair.ID) inNatRuleKey := firewall.GenKey(firewall.InNatFormat, testCase.InputPair.ID)
found := 0 found := 0
for _, chain := range manager.chains { for _, chain := range chains {
rules, err := nftablesTestingClient.GetRules(chain.Table, chain) 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) require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
for _, rule := range rules { for _, rule := range rules {
@ -131,16 +140,19 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
t.Skip("nftables not supported on this OS") t.Skip("nftables not supported on this OS")
} }
table, err := createWorkTable() table, table6, err := createWorkTables()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer deleteWorkTable() defer deleteWorkTables()
for _, testCase := range test.RemoveRuleTestCases { for _, testCase := range test.RemoveRuleTestCases {
t.Run(testCase.Name, func(t *testing.T) { 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") require.NoError(t, err, "failed to create router")
nftablesTestingClient := &nftables.Conn{} nftablesTestingClient := &nftables.Conn{}
@ -150,11 +162,18 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
sourceExp := generateCIDRMatcherExpressions(true, testCase.InputPair.Source) sourceExp := generateCIDRMatcherExpressions(true, testCase.InputPair.Source)
destExp := generateCIDRMatcherExpressions(false, testCase.InputPair.Destination) 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 forwardExp := append(sourceExp, append(destExp, exprCounterAccept...)...) //nolint:gocritic
forwardRuleKey := firewall.GenKey(firewall.ForwardingFormat, testCase.InputPair.ID) forwardRuleKey := firewall.GenKey(firewall.ForwardingFormat, testCase.InputPair.ID)
insertedForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{ insertedForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
Table: manager.workTable, Table: workTable,
Chain: manager.chains[chainNameRouteingFw], Chain: chains[chainNameRouteingFw],
Exprs: forwardExp, Exprs: forwardExp,
UserData: []byte(forwardRuleKey), UserData: []byte(forwardRuleKey),
}) })
@ -163,8 +182,8 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair.ID) natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair.ID)
insertedNat := nftablesTestingClient.InsertRule(&nftables.Rule{ insertedNat := nftablesTestingClient.InsertRule(&nftables.Rule{
Table: manager.workTable, Table: workTable,
Chain: manager.chains[chainNameRoutingNat], Chain: chains[chainNameRoutingNat],
Exprs: natExp, Exprs: natExp,
UserData: []byte(natRuleKey), UserData: []byte(natRuleKey),
}) })
@ -175,8 +194,8 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
forwardExp = append(sourceExp, append(destExp, exprCounterAccept...)...) //nolint:gocritic forwardExp = append(sourceExp, append(destExp, exprCounterAccept...)...) //nolint:gocritic
inForwardRuleKey := firewall.GenKey(firewall.InForwardingFormat, testCase.InputPair.ID) inForwardRuleKey := firewall.GenKey(firewall.InForwardingFormat, testCase.InputPair.ID)
insertedInForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{ insertedInForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
Table: manager.workTable, Table: workTable,
Chain: manager.chains[chainNameRouteingFw], Chain: chains[chainNameRouteingFw],
Exprs: forwardExp, Exprs: forwardExp,
UserData: []byte(inForwardRuleKey), UserData: []byte(inForwardRuleKey),
}) })
@ -185,8 +204,8 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
inNatRuleKey := firewall.GenKey(firewall.InNatFormat, testCase.InputPair.ID) inNatRuleKey := firewall.GenKey(firewall.InNatFormat, testCase.InputPair.ID)
insertedInNat := nftablesTestingClient.InsertRule(&nftables.Rule{ insertedInNat := nftablesTestingClient.InsertRule(&nftables.Rule{
Table: manager.workTable, Table: workTable,
Chain: manager.chains[chainNameRoutingNat], Chain: chains[chainNameRoutingNat],
Exprs: natExp, Exprs: natExp,
UserData: []byte(inNatRuleKey), UserData: []byte(inNatRuleKey),
}) })
@ -199,7 +218,7 @@ func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
err = manager.RemoveRoutingRules(testCase.InputPair) err = manager.RemoveRoutingRules(testCase.InputPair)
require.NoError(t, err, "shouldn't return error") require.NoError(t, err, "shouldn't return error")
for _, chain := range manager.chains { for _, chain := range chains {
rules, err := nftablesTestingClient.GetRules(chain.Table, chain) 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) require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
for _, rule := range rules { for _, rule := range rules {
@ -238,30 +257,39 @@ func isIptablesClientAvailable(client *iptables.IPTables) bool {
return err == nil return err == nil
} }
func createWorkTable() (*nftables.Table, error) { func createWorkTables() (*nftables.Table, *nftables.Table, error) {
sConn, err := nftables.New(nftables.AsLasting()) sConn, err := nftables.New(nftables.AsLasting())
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
tables, err := sConn.ListTablesOfFamily(nftables.TableFamilyIPv4) tables, err := sConn.ListTablesOfFamily(nftables.TableFamilyIPv4)
if err != nil { 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 { if t.Name == tableName {
sConn.DelTable(t) sConn.DelTable(t)
} }
} }
table := sConn.AddTable(&nftables.Table{Name: tableName, Family: nftables.TableFamilyIPv4}) 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() err = sConn.Flush()
return table, err return table, table6, err
} }
func deleteWorkTable() { func deleteWorkTables() {
sConn, err := nftables.New(nftables.AsLasting()) sConn, err := nftables.New(nftables.AsLasting())
if err != nil { if err != nil {
return return
@ -272,6 +300,12 @@ func deleteWorkTable() {
return return
} }
tables6, err := sConn.ListTablesOfFamily(nftables.TableFamilyIPv6)
if err != nil {
return
}
tables = append(tables, tables6...)
for _, t := range tables { for _, t := range tables {
if t.Name == tableName { if t.Name == tableName {
sConn.DelTable(t) sConn.DelTable(t)

View File

@ -8,6 +8,7 @@ var (
InsertRuleTestCases = []struct { InsertRuleTestCases = []struct {
Name string Name string
InputPair firewall.RouterPair InputPair firewall.RouterPair
IsV6 bool
}{ }{
{ {
Name: "Insert Forwarding IPV4 Rule", Name: "Insert Forwarding IPV4 Rule",
@ -27,12 +28,32 @@ var (
Masquerade: true, 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 { RemoveRuleTestCases = []struct {
Name string Name string
InputPair firewall.RouterPair InputPair firewall.RouterPair
IpVersion string IsV6 bool
}{ }{
{ {
Name: "Remove Forwarding And Nat IPV4 Rules", Name: "Remove Forwarding And Nat IPV4 Rules",
@ -43,5 +64,15 @@ var (
Masquerade: true, 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,
},
} }
) )

View File

@ -24,6 +24,7 @@ var (
type IFaceMapper interface { type IFaceMapper interface {
SetFilter(iface.PacketFilter) error SetFilter(iface.PacketFilter) error
Address() iface.WGAddress Address() iface.WGAddress
Address6() *iface.WGAddress
} }
// RuleSet is a set of rules grouped by a string key // 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 return mgr, nil
} }
func (m *Manager) ResetV6Firewall() error {
return nil
}
func (m *Manager) V6Active() bool {
return false
}
func create(iface IFaceMapper) (*Manager, error) { func create(iface IFaceMapper) (*Manager, error) {
m := &Manager{ m := &Manager{
decoders: sync.Pool{ decoders: sync.Pool{

View File

@ -33,6 +33,10 @@ func (i *IFaceMock) Address() iface.WGAddress {
return i.AddressFunc() return i.AddressFunc()
} }
func (i *IFaceMock) Address6() *iface.WGAddress {
return nil
}
func TestManagerCreate(t *testing.T) { func TestManagerCreate(t *testing.T) {
ifaceMock := &IFaceMock{ ifaceMock := &IFaceMock{
SetFilterFunc: func(iface.PacketFilter) error { return nil }, SetFilterFunc: func(iface.PacketFilter) error { return nil },

View File

@ -16,9 +16,10 @@ import (
mgmProto "github.com/netbirdio/netbird/management/proto" mgmProto "github.com/netbirdio/netbird/management/proto"
) )
// Manager is a ACL rules manager // Manager is an ACL rules manager
type Manager interface { type Manager interface {
ApplyFiltering(networkMap *mgmProto.NetworkMap) ApplyFiltering(networkMap *mgmProto.NetworkMap)
ResetV6Acl() error
} }
// DefaultManager uses firewall manager to handle // DefaultManager uses firewall manager to handle
@ -26,16 +27,36 @@ type DefaultManager struct {
firewall firewall.Manager firewall firewall.Manager
ipsetCounter int ipsetCounter int
rulesPairs map[string][]firewall.Rule rulesPairs map[string][]firewall.Rule
rulesPairs6 map[string][]firewall.Rule
mutex sync.Mutex mutex sync.Mutex
} }
func NewDefaultManager(fm firewall.Manager) *DefaultManager { func NewDefaultManager(fm firewall.Manager) *DefaultManager {
return &DefaultManager{ return &DefaultManager{
firewall: fm, firewall: fm,
rulesPairs: make(map[string][]firewall.Rule), rulesPairs: make(map[string][]firewall.Rule),
rulesPairs6: make(map[string][]firewall.Rule),
} }
} }
func (d *DefaultManager) ResetV6Acl() error {
for _, rules := range d.rulesPairs6 {
for _, r := range rules {
err := d.firewall.DeleteRule(r)
if err != nil {
return err
}
}
}
err := d.firewall.ResetV6Firewall()
if err != nil {
return err
}
d.rulesPairs6 = make(map[string][]firewall.Rule)
return nil
}
// ApplyFiltering firewall rules to the local firewall manager processed by ACL policy. // ApplyFiltering firewall rules to the local firewall manager processed by ACL policy.
// //
// If allowByDefault is true it appends allow ALL traffic rules to input and output chains. // If allowByDefault is true it appends allow ALL traffic rules to input and output chains.
@ -83,6 +104,7 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
if enableSSH { if enableSSH {
rules = append(rules, &mgmProto.FirewallRule{ rules = append(rules, &mgmProto.FirewallRule{
PeerIP: "0.0.0.0", PeerIP: "0.0.0.0",
PeerIP6: "::",
Direction: mgmProto.FirewallRule_IN, Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_ACCEPT, Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_TCP, Protocol: mgmProto.FirewallRule_TCP,
@ -97,12 +119,14 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
rules = append(rules, rules = append(rules,
&mgmProto.FirewallRule{ &mgmProto.FirewallRule{
PeerIP: "0.0.0.0", PeerIP: "0.0.0.0",
PeerIP6: "::",
Direction: mgmProto.FirewallRule_IN, Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_ACCEPT, Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL, Protocol: mgmProto.FirewallRule_ALL,
}, },
&mgmProto.FirewallRule{ &mgmProto.FirewallRule{
PeerIP: "0.0.0.0", PeerIP: "0.0.0.0",
PeerIP6: "::",
Direction: mgmProto.FirewallRule_OUT, Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_ACCEPT, Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL, Protocol: mgmProto.FirewallRule_ALL,
@ -111,6 +135,7 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
} }
newRulePairs := make(map[string][]firewall.Rule) newRulePairs := make(map[string][]firewall.Rule)
newRulePairs6 := make(map[string][]firewall.Rule)
ipsetByRuleSelectors := make(map[string]string) ipsetByRuleSelectors := make(map[string]string)
for _, r := range rules { for _, r := range rules {
@ -123,7 +148,7 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
ipsetName = fmt.Sprintf("nb%07d", d.ipsetCounter) ipsetName = fmt.Sprintf("nb%07d", d.ipsetCounter)
ipsetByRuleSelectors[selector] = ipsetName ipsetByRuleSelectors[selector] = ipsetName
} }
pairID, rulePair, err := d.protoRuleToFirewallRule(r, ipsetName) pairID, rulePair, rulePair6, err := d.protoRuleToFirewallRule(r, ipsetName)
if err != nil { if err != nil {
log.Errorf("failed to apply firewall rule: %+v, %v", r, err) log.Errorf("failed to apply firewall rule: %+v, %v", r, err)
d.rollBack(newRulePairs) d.rollBack(newRulePairs)
@ -132,6 +157,8 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
if len(rules) > 0 { if len(rules) > 0 {
d.rulesPairs[pairID] = rulePair d.rulesPairs[pairID] = rulePair
newRulePairs[pairID] = rulePair newRulePairs[pairID] = rulePair
d.rulesPairs6[pairID] = rulePair6
newRulePairs6[pairID] = rulePair6
} }
} }
@ -146,59 +173,104 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
delete(d.rulesPairs, pairID) delete(d.rulesPairs, pairID)
} }
} }
for pairID, rules := range d.rulesPairs6 {
if _, ok := newRulePairs6[pairID]; !ok {
for _, rule := range rules {
if err := d.firewall.DeleteRule(rule); err != nil {
log.Errorf("failed to delete firewall rule: %v", err)
continue
}
}
delete(d.rulesPairs6, pairID)
}
}
d.rulesPairs = newRulePairs d.rulesPairs = newRulePairs
d.rulesPairs6 = newRulePairs6
} }
func (d *DefaultManager) protoRuleToFirewallRule( func (d *DefaultManager) protoRuleToFirewallRule(
r *mgmProto.FirewallRule, r *mgmProto.FirewallRule,
ipsetName string, ipsetName string,
) (string, []firewall.Rule, error) { ) (string, []firewall.Rule, []firewall.Rule, error) {
ip := net.ParseIP(r.PeerIP) ip := net.ParseIP(r.PeerIP)
if ip == nil { if ip == nil {
return "", nil, fmt.Errorf("invalid IP address, skipping firewall rule") return "", nil, nil, fmt.Errorf("invalid IP address, skipping firewall rule")
}
var ip6 *net.IP = nil
if d.firewall.V6Active() && r.PeerIP6 != "" {
ip6tmp := net.ParseIP(r.PeerIP6)
if ip6tmp == nil {
return "", nil, nil, fmt.Errorf("invalid IP address, skipping firewall rule")
}
ip6 = &ip6tmp
} }
protocol, err := convertToFirewallProtocol(r.Protocol) protocol, err := convertToFirewallProtocol(r.Protocol)
if err != nil { if err != nil {
return "", nil, fmt.Errorf("skipping firewall rule: %s", err) return "", nil, nil, fmt.Errorf("skipping firewall rule: %s", err)
} }
action, err := convertFirewallAction(r.Action) action, err := convertFirewallAction(r.Action)
if err != nil { if err != nil {
return "", nil, fmt.Errorf("skipping firewall rule: %s", err) return "", nil, nil, fmt.Errorf("skipping firewall rule: %s", err)
} }
var port *firewall.Port var port *firewall.Port
if r.Port != "" { if r.Port != "" {
value, err := strconv.Atoi(r.Port) value, err := strconv.Atoi(r.Port)
if err != nil { if err != nil {
return "", nil, fmt.Errorf("invalid port, skipping firewall rule") return "", nil, nil, fmt.Errorf("invalid port, skipping firewall rule")
} }
port = &firewall.Port{ port = &firewall.Port{
Values: []int{value}, Values: []int{value},
} }
} }
ruleID := d.getRuleID(ip, protocol, int(r.Direction), port, action, "") var rules []firewall.Rule
var rules6 []firewall.Rule
ruleID := d.getRuleID(ip, ip6, protocol, int(r.Direction), port, action, "")
if rulesPair, ok := d.rulesPairs[ruleID]; ok { if rulesPair, ok := d.rulesPairs[ruleID]; ok {
return ruleID, rulesPair, nil rules = rulesPair
}
if rulesPair6, ok := d.rulesPairs6[ruleID]; d.firewall.V6Active() && ok && ip6 != nil {
rules6 = rulesPair6
} }
var rules []firewall.Rule if rules == nil {
switch r.Direction { switch r.Direction {
case mgmProto.FirewallRule_IN: case mgmProto.FirewallRule_IN:
rules, err = d.addInRules(ip, protocol, port, action, ipsetName, "") rules, err = d.addInRules(ip, protocol, port, action, ipsetName, "")
case mgmProto.FirewallRule_OUT: case mgmProto.FirewallRule_OUT:
rules, err = d.addOutRules(ip, protocol, port, action, ipsetName, "") rules, err = d.addOutRules(ip, protocol, port, action, ipsetName, "")
default: default:
return "", nil, fmt.Errorf("invalid direction, skipping firewall rule") return "", nil, nil, fmt.Errorf("invalid direction, skipping firewall rule")
}
} }
if err != nil { if err != nil {
return "", nil, err return "", nil, nil, err
} }
return ruleID, rules, nil if d.firewall.V6Active() && ip6 != nil && rules6 == nil {
switch r.Direction {
case mgmProto.FirewallRule_IN:
rules6, err = d.addInRules(*ip6, protocol, port, action, ipsetName, "")
case mgmProto.FirewallRule_OUT:
rules6, err = d.addOutRules(*ip6, protocol, port, action, ipsetName, "")
default:
return "", nil, nil, fmt.Errorf("invalid direction, skipping firewall rule")
}
}
if err != nil && err.Error() != "failed to add firewall rule: attempted to configure filtering for IPv6 address even though IPv6 is not active" {
return "", rules, nil, err
}
return ruleID, rules, rules6, nil
} }
func (d *DefaultManager) addInRules( func (d *DefaultManager) addInRules(
@ -226,8 +298,9 @@ func (d *DefaultManager) addInRules(
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to add firewall rule: %v", err) return nil, fmt.Errorf("failed to add firewall rule: %v", err)
} }
rules = append(rules, rule...)
return append(rules, rule...), nil return rules, nil
} }
func (d *DefaultManager) addOutRules( func (d *DefaultManager) addOutRules(
@ -255,20 +328,26 @@ func (d *DefaultManager) addOutRules(
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to add firewall rule: %v", err) return nil, fmt.Errorf("failed to add firewall rule: %v", err)
} }
rules = append(rules, rule...)
return append(rules, rule...), nil return rules, nil
} }
// getRuleID() returns unique ID for the rule based on its parameters. // getRuleID() returns unique ID for the rule based on its parameters.
func (d *DefaultManager) getRuleID( func (d *DefaultManager) getRuleID(
ip net.IP, ip net.IP,
ip6 *net.IP,
proto firewall.Protocol, proto firewall.Protocol,
direction int, direction int,
port *firewall.Port, port *firewall.Port,
action firewall.Action, action firewall.Action,
comment string, comment string,
) string { ) string {
idStr := ip.String() + string(proto) + strconv.Itoa(direction) + strconv.Itoa(int(action)) + comment ip6Str := ""
if ip6 != nil {
ip6Str = ip6.String()
}
idStr := ip.String() + ip6Str + string(proto) + strconv.Itoa(direction) + strconv.Itoa(int(action)) + comment
if port != nil { if port != nil {
idStr += port.String() idStr += port.String()
} }
@ -321,6 +400,8 @@ func (d *DefaultManager) squashAcceptRules(
// it means that rules for that protocol was already optimized on the // it means that rules for that protocol was already optimized on the
// management side // management side
if r.PeerIP == "0.0.0.0" { if r.PeerIP == "0.0.0.0" {
// I don't _think_ that IPv6 is relevant here, as any optimization that has r.PeerIP6 == "::" should also
// implicitly have r.PeerIP == "0.0.0.0".
squashedRules = append(squashedRules, r) squashedRules = append(squashedRules, r)
squashedProtocols[r.Protocol] = struct{}{} squashedProtocols[r.Protocol] = struct{}{}
return return
@ -364,6 +445,7 @@ func (d *DefaultManager) squashAcceptRules(
// add special rule 0.0.0.0 which allows all IP's in our firewall implementations // add special rule 0.0.0.0 which allows all IP's in our firewall implementations
squashedRules = append(squashedRules, &mgmProto.FirewallRule{ squashedRules = append(squashedRules, &mgmProto.FirewallRule{
PeerIP: "0.0.0.0", PeerIP: "0.0.0.0",
PeerIP6: "::",
Direction: direction, Direction: direction,
Action: mgmProto.FirewallRule_ACCEPT, Action: mgmProto.FirewallRule_ACCEPT,
Protocol: protocol, Protocol: protocol,

View File

@ -19,6 +19,7 @@ func TestDefaultManager(t *testing.T) {
FirewallRules: []*mgmProto.FirewallRule{ FirewallRules: []*mgmProto.FirewallRule{
{ {
PeerIP: "10.93.0.1", PeerIP: "10.93.0.1",
PeerIP6: "2001:db8::fedc:ba09:8765:0001",
Direction: mgmProto.FirewallRule_OUT, Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_ACCEPT, Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_TCP, Protocol: mgmProto.FirewallRule_TCP,
@ -26,6 +27,7 @@ func TestDefaultManager(t *testing.T) {
}, },
{ {
PeerIP: "10.93.0.2", PeerIP: "10.93.0.2",
PeerIP6: "2001:db8::fedc:ba09:8765:0002",
Direction: mgmProto.FirewallRule_OUT, Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_DROP, Action: mgmProto.FirewallRule_DROP,
Protocol: mgmProto.FirewallRule_UDP, Protocol: mgmProto.FirewallRule_UDP,
@ -50,6 +52,14 @@ func TestDefaultManager(t *testing.T) {
IP: ip, IP: ip,
Network: network, Network: network,
}).AnyTimes() }).AnyTimes()
ip6, network6, err := net.ParseCIDR("2001:db8::fedc:ba09:8765:4321/64")
if err != nil {
t.Fatalf("failed to parse IP address: %v", err)
}
ifaceMock.EXPECT().Address6().Return(&iface.WGAddress{
IP: ip6,
Network: network6,
}).AnyTimes()
// we receive one rule from the management so for testing purposes ignore it // we receive one rule from the management so for testing purposes ignore it
fw, err := firewall.NewFirewall(context.Background(), ifaceMock) fw, err := firewall.NewFirewall(context.Background(), ifaceMock)
@ -83,6 +93,7 @@ func TestDefaultManager(t *testing.T) {
networkMap.FirewallRules, networkMap.FirewallRules,
&mgmProto.FirewallRule{ &mgmProto.FirewallRule{
PeerIP: "10.93.0.3", PeerIP: "10.93.0.3",
PeerIP6: "2001:db8::fedc:ba09:8765:0003",
Direction: mgmProto.FirewallRule_IN, Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_DROP, Action: mgmProto.FirewallRule_DROP,
Protocol: mgmProto.FirewallRule_ICMP, Protocol: mgmProto.FirewallRule_ICMP,
@ -343,6 +354,7 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) {
IP: ip, IP: ip,
Network: network, Network: network,
}).AnyTimes() }).AnyTimes()
ifaceMock.EXPECT().Address6().Return(nil).AnyTimes()
// we receive one rule from the management so for testing purposes ignore it // we receive one rule from the management so for testing purposes ignore it
fw, err := firewall.NewFirewall(context.Background(), ifaceMock) fw, err := firewall.NewFirewall(context.Background(), ifaceMock)

View File

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/netbirdio/netbird/client/internal/acl (interfaces: IFaceMapper) // Source: ./client/firewall/iface.go
// Package mocks is a generated GoMock package. // Package mocks is a generated GoMock package.
package mocks package mocks
@ -48,6 +48,20 @@ func (mr *MockIFaceMapperMockRecorder) Address() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Address", reflect.TypeOf((*MockIFaceMapper)(nil).Address)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Address", reflect.TypeOf((*MockIFaceMapper)(nil).Address))
} }
// Address6 mocks base method.
func (m *MockIFaceMapper) Address6() *iface.WGAddress {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Address6")
ret0, _ := ret[0].(*iface.WGAddress)
return ret0
}
// Address6 indicates an expected call of Address6.
func (mr *MockIFaceMapperMockRecorder) Address6() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Address6", reflect.TypeOf((*MockIFaceMapper)(nil).Address6))
}
// IsUserspaceBind mocks base method. // IsUserspaceBind mocks base method.
func (m *MockIFaceMapper) IsUserspaceBind() bool { func (m *MockIFaceMapper) IsUserspaceBind() bool {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -310,6 +310,7 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe
engineConf := &EngineConfig{ engineConf := &EngineConfig{
WgIfaceName: config.WgIface, WgIfaceName: config.WgIface,
WgAddr: peerConfig.Address, WgAddr: peerConfig.Address,
WgAddr6: peerConfig.Address6,
IFaceBlackList: config.IFaceBlackList, IFaceBlackList: config.IFaceBlackList,
DisableIPv6Discovery: config.DisableIPv6Discovery, DisableIPv6Discovery: config.DisableIPv6Discovery,
WgPrivateKey: key, WgPrivateKey: key,

View File

@ -485,7 +485,11 @@ func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord
} }
func getNSHostPort(ns nbdns.NameServer) string { func getNSHostPort(ns nbdns.NameServer) string {
return fmt.Sprintf("%s:%d", ns.IP.String(), ns.Port) if ns.IP.Is4() {
return fmt.Sprintf("%s:%d", ns.IP.String(), ns.Port)
} else {
return fmt.Sprintf("[%s]:%d", ns.IP.String(), ns.Port)
}
} }
// upstreamCallbacks returns two functions, the first one is used to deactivate // upstreamCallbacks returns two functions, the first one is used to deactivate

View File

@ -38,6 +38,13 @@ func (w *mocWGIface) Address() iface.WGAddress {
Network: network, Network: network,
} }
} }
func (w *mocWGIface) Address6() *iface.WGAddress {
ip, network, _ := net.ParseCIDR("fd00:1234:dead:beef::/64")
return &iface.WGAddress{
IP: ip,
Network: network,
}
}
func (w *mocWGIface) GetFilter() iface.PacketFilter { func (w *mocWGIface) GetFilter() iface.PacketFilter {
return w.filter return w.filter
@ -261,7 +268,7 @@ func TestUpdateDNSServer(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
wgIface, err := iface.NewWGIFace(fmt.Sprintf("utun230%d", n), fmt.Sprintf("100.66.100.%d/32", n+1), 33100, privKey.String(), iface.DefaultMTU, newNet, nil) wgIface, err := iface.NewWGIFace(fmt.Sprintf("utun230%d", n), fmt.Sprintf("100.66.100.%d/32", n+1), fmt.Sprintf("fd00:1234:dead:beef::%d/128", n+1), 33100, privKey.String(), iface.DefaultMTU, newNet, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -339,7 +346,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) {
} }
privKey, _ := wgtypes.GeneratePrivateKey() privKey, _ := wgtypes.GeneratePrivateKey()
wgIface, err := iface.NewWGIFace("utun2301", "100.66.100.1/32", 33100, privKey.String(), iface.DefaultMTU, newNet, nil) wgIface, err := iface.NewWGIFace("utun2301", "100.66.100.1/32", "", 33100, privKey.String(), iface.DefaultMTU, newNet, nil)
if err != nil { if err != nil {
t.Errorf("build interface wireguard: %v", err) t.Errorf("build interface wireguard: %v", err)
return return
@ -595,7 +602,7 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
} }
func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) { func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) {
wgIFace, err := createWgInterfaceWithBind(t) wgIFace, err := createWgInterfaceWithBind(t, false)
if err != nil { if err != nil {
t.Fatal("failed to initialize wg interface") t.Fatal("failed to initialize wg interface")
} }
@ -621,7 +628,7 @@ func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) {
} }
func TestDNSPermanent_updateUpstream(t *testing.T) { func TestDNSPermanent_updateUpstream(t *testing.T) {
wgIFace, err := createWgInterfaceWithBind(t) wgIFace, err := createWgInterfaceWithBind(t, false)
if err != nil { if err != nil {
t.Fatal("failed to initialize wg interface") t.Fatal("failed to initialize wg interface")
} }
@ -713,7 +720,7 @@ func TestDNSPermanent_updateUpstream(t *testing.T) {
} }
func TestDNSPermanent_matchOnly(t *testing.T) { func TestDNSPermanent_matchOnly(t *testing.T) {
wgIFace, err := createWgInterfaceWithBind(t) wgIFace, err := createWgInterfaceWithBind(t, false)
if err != nil { if err != nil {
t.Fatal("failed to initialize wg interface") t.Fatal("failed to initialize wg interface")
} }
@ -784,7 +791,7 @@ func TestDNSPermanent_matchOnly(t *testing.T) {
} }
} }
func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) { func createWgInterfaceWithBind(t *testing.T, enableV6 bool) (*iface.WGIface, error) {
t.Helper() t.Helper()
ov := os.Getenv("NB_WG_KERNEL_DISABLED") ov := os.Getenv("NB_WG_KERNEL_DISABLED")
defer t.Setenv("NB_WG_KERNEL_DISABLED", ov) defer t.Setenv("NB_WG_KERNEL_DISABLED", ov)
@ -797,7 +804,11 @@ func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) {
} }
privKey, _ := wgtypes.GeneratePrivateKey() privKey, _ := wgtypes.GeneratePrivateKey()
wgIface, err := iface.NewWGIFace("utun2301", "100.66.100.2/24", 33100, privKey.String(), iface.DefaultMTU, newNet, nil) v6Addr := ""
if enableV6 {
v6Addr = "fd00:1234:dead:beef::1/128"
}
wgIface, err := iface.NewWGIFace("utun2301", "100.66.100.2/24", v6Addr, 33100, privKey.String(), iface.DefaultMTU, newNet, nil)
if err != nil { if err != nil {
t.Fatalf("build interface wireguard: %v", err) t.Fatalf("build interface wireguard: %v", err)
return nil, err return nil, err

View File

@ -58,7 +58,8 @@ type EngineConfig struct {
WgIfaceName string WgIfaceName string
// WgAddr is a Wireguard local address (Netbird Network IP) // WgAddr is a Wireguard local address (Netbird Network IP)
WgAddr string WgAddr string
WgAddr6 string
// WgPrivateKey is a Wireguard private key of our peer (it MUST never leave the machine) // WgPrivateKey is a Wireguard private key of our peer (it MUST never leave the machine)
WgPrivateKey wgtypes.Key WgPrivateKey wgtypes.Key
@ -603,6 +604,32 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
log.Infof("updated peer address from %s to %s", oldAddr, conf.Address) log.Infof("updated peer address from %s to %s", oldAddr, conf.Address)
} }
if e.wgInterface.Address6() == nil && conf.Address6 != "" ||
e.wgInterface.Address6() != nil && e.wgInterface.Address6().String() != conf.Address6 {
oldAddr := "none"
if e.wgInterface.Address6() != nil {
oldAddr = e.wgInterface.Address6().String()
}
newAddr := "none"
if conf.Address6 != "" {
newAddr = conf.Address6
}
log.Debugf("updating peer IPv6 address from %s to %s", oldAddr, newAddr)
err := e.wgInterface.UpdateAddr6(conf.Address6)
if err != nil {
return err
}
e.config.WgAddr6 = conf.Address6
err = e.acl.ResetV6Acl()
if err != nil {
return err
}
e.routeManager.ResetV6Routes()
log.Infof("updated peer IPv6 address from %s to %s", oldAddr, conf.Address6)
}
if conf.GetSshConfig() != nil { if conf.GetSshConfig() != nil {
err := e.updateSSH(conf.GetSshConfig()) err := e.updateSSH(conf.GetSshConfig())
if err != nil { if err != nil {
@ -612,6 +639,7 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
e.statusRecorder.UpdateLocalPeerState(peer.LocalPeerState{ e.statusRecorder.UpdateLocalPeerState(peer.LocalPeerState{
IP: e.config.WgAddr, IP: e.config.WgAddr,
IP6: e.config.WgAddr6,
PubKey: e.config.WgPrivateKey.PublicKey().String(), PubKey: e.config.WgPrivateKey.PublicKey().String(),
KernelInterface: iface.WireGuardModuleIsLoaded(), KernelInterface: iface.WireGuardModuleIsLoaded(),
FQDN: conf.GetFqdn(), FQDN: conf.GetFqdn(),
@ -1238,7 +1266,7 @@ func (e *Engine) newWgIface() (*iface.WGIface, error) {
default: default:
} }
return iface.NewWGIFace(e.config.WgIfaceName, e.config.WgAddr, e.config.WgPort, e.config.WgPrivateKey.String(), iface.DefaultMTU, transportNet, mArgs) return iface.NewWGIFace(e.config.WgIfaceName, e.config.WgAddr, e.config.WgAddr6, e.config.WgPort, e.config.WgPrivateKey.String(), iface.DefaultMTU, transportNet, mArgs)
} }
func (e *Engine) wgInterfaceCreate() (err error) { func (e *Engine) wgInterfaceCreate() (err error) {

View File

@ -216,7 +216,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
engine.wgInterface, err = iface.NewWGIFace("utun102", "100.64.0.1/24", engine.config.WgPort, key.String(), iface.DefaultMTU, newNet, nil) engine.wgInterface, err = iface.NewWGIFace("utun102", "100.64.0.1/24", "", engine.config.WgPort, key.String(), iface.DefaultMTU, newNet, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -565,6 +565,7 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{ engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
WgIfaceName: wgIfaceName, WgIfaceName: wgIfaceName,
WgAddr: wgAddr, WgAddr: wgAddr,
WgAddr6: "",
WgPrivateKey: key, WgPrivateKey: key,
WgPort: 33100, WgPort: 33100,
}, MobileDependency{}, peer.NewRecorder("https://mgm")) }, MobileDependency{}, peer.NewRecorder("https://mgm"))
@ -573,7 +574,7 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, engine.config.WgPort, key.String(), iface.DefaultMTU, newNet, nil) engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, engine.config.WgAddr6, engine.config.WgPort, key.String(), iface.DefaultMTU, newNet, nil)
assert.NoError(t, err, "shouldn't return error") assert.NoError(t, err, "shouldn't return error")
input := struct { input := struct {
inputSerial uint64 inputSerial uint64
@ -735,6 +736,7 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{ engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
WgIfaceName: wgIfaceName, WgIfaceName: wgIfaceName,
WgAddr: wgAddr, WgAddr: wgAddr,
WgAddr6: "",
WgPrivateKey: key, WgPrivateKey: key,
WgPort: 33100, WgPort: 33100,
}, MobileDependency{}, peer.NewRecorder("https://mgm")) }, MobileDependency{}, peer.NewRecorder("https://mgm"))
@ -744,7 +746,7 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, 33100, key.String(), iface.DefaultMTU, newNet, nil) engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, engine.config.WgAddr6, 33100, key.String(), iface.DefaultMTU, newNet, nil)
assert.NoError(t, err, "shouldn't return error") assert.NoError(t, err, "shouldn't return error")
mockRouteManager := &routemanager.MockManager{ mockRouteManager := &routemanager.MockManager{

View File

@ -484,7 +484,7 @@ func (conn *Conn) configureConnection(remoteConn net.Conn, remoteWgPort int, rem
log.Warnf("unable to save peer's state, got error: %v", err) log.Warnf("unable to save peer's state, got error: %v", err)
} }
_, ipNet, err := net.ParseCIDR(conn.config.WgConfig.AllowedIps) _, ipNet, err := net.ParseCIDR(strings.Split(conn.config.WgConfig.AllowedIps, ",")[0])
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -68,6 +68,7 @@ func (s *State) GetRoutes() map[string]struct{} {
// LocalPeerState contains the latest state of the local peer // LocalPeerState contains the latest state of the local peer
type LocalPeerState struct { type LocalPeerState struct {
IP string IP string
IP6 string
PubKey string PubKey string
KernelInterface bool KernelInterface bool
FQDN string FQDN string

View File

@ -38,6 +38,7 @@ type clientNetwork struct {
peerStateUpdate chan struct{} peerStateUpdate chan struct{}
routePeersNotifiers map[string]chan struct{} routePeersNotifiers map[string]chan struct{}
chosenRoute *route.Route chosenRoute *route.Route
chosenIP *net.IP
network netip.Prefix network netip.Prefix
updateSerial uint64 updateSerial uint64
} }
@ -221,6 +222,7 @@ func (c *clientNetwork) removeRouteFromWireguardPeer(peerKey string) error {
func (c *clientNetwork) removeRouteFromPeerAndSystem() error { func (c *clientNetwork) removeRouteFromPeerAndSystem() error {
if c.chosenRoute != nil { if c.chosenRoute != nil {
// TODO IPv6 (pass wgInterface)
if err := removeVPNRoute(c.network, c.getAsInterface()); err != nil { if err := removeVPNRoute(c.network, c.getAsInterface()); err != nil {
return fmt.Errorf("remove route %s from system, err: %v", c.network, err) return fmt.Errorf("remove route %s from system, err: %v", c.network, err)
} }
@ -261,10 +263,20 @@ func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
return fmt.Errorf("remove route from peer: %v", err) return fmt.Errorf("remove route from peer: %v", err)
} }
} else { } else {
// TODO recheck IPv6
gwAddr := c.wgInterface.Address().IP
c.chosenIP = &gwAddr
if c.network.Addr().Is6() {
if c.wgInterface.Address6() == nil {
return fmt.Errorf("Could not assign IPv6 route %s for peer %s because no IPv6 address is assigned",
c.network.String(), c.wgInterface.Address().IP.String())
}
c.chosenIP = &c.wgInterface.Address6().IP
}
// otherwise add the route to the system // otherwise add the route to the system
if err := addVPNRoute(c.network, c.getAsInterface()); err != nil { if err := addVPNRoute(c.network, c.getAsInterface()); err != nil {
return fmt.Errorf("route %s couldn't be added for peer %s, err: %v", return fmt.Errorf("route %s couldn't be added for peer %s, err: %v",
c.network.String(), c.wgInterface.Address().IP.String(), err) c.network.String(), c.chosenIP.String(), err)
} }
} }

View File

@ -34,6 +34,7 @@ type Manager interface {
GetRouteSelector() *routeselector.RouteSelector GetRouteSelector() *routeselector.RouteSelector
SetRouteChangeListener(listener listener.NetworkChangeListener) SetRouteChangeListener(listener listener.NetworkChangeListener)
InitialRouteRange() []string InitialRouteRange() []string
ResetV6Routes()
EnableServerRouter(firewall firewall.Manager) error EnableServerRouter(firewall firewall.Manager) error
Stop() Stop()
} }
@ -148,6 +149,18 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro
} }
} }
// ResetV6Routes deletes all IPv6 routes (necessary if IPv6 address changes).
// It is expected that UpdateRoute is called afterwards to recreate the routing table.
func (m *DefaultManager) ResetV6Routes() {
for id, client := range m.clientNetworks {
if client.network.Addr().Is6() {
log.Debugf("stopping client network watcher due to IPv6 address change, %s", id)
client.stop()
delete(m.clientNetworks, id)
}
}
}
// SetRouteChangeListener set RouteListener for route change notifier // SetRouteChangeListener set RouteListener for route change notifier
func (m *DefaultManager) SetRouteChangeListener(listener listener.NetworkChangeListener) { func (m *DefaultManager) SetRouteChangeListener(listener listener.NetworkChangeListener) {
m.notifier.setListener(listener) m.notifier.setListener(listener)

View File

@ -36,6 +36,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
serverRoutesExpected int serverRoutesExpected int
clientNetworkWatchersExpected int clientNetworkWatchersExpected int
clientNetworkWatchersExpectedAllowed int clientNetworkWatchersExpectedAllowed int
isV6 bool
}{ }{
{ {
name: "Should create 2 client networks", name: "Should create 2 client networks",
@ -65,6 +66,35 @@ func TestManagerUpdateRoutes(t *testing.T) {
inputSerial: 1, inputSerial: 1,
clientNetworkWatchersExpected: 2, clientNetworkWatchersExpected: 2,
}, },
{
name: "Should create 2 client networks (IPv6)",
inputInitRoutes: []*route.Route{},
inputRoutes: []*route.Route{
{
ID: "a",
NetID: "routeA",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "b",
NetID: "routeB",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8::7890:abcd/128"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputSerial: 1,
clientNetworkWatchersExpected: 2,
isV6: true,
},
{ {
name: "Should Create 2 Server Routes", name: "Should Create 2 Server Routes",
inputRoutes: []*route.Route{ inputRoutes: []*route.Route{
@ -93,6 +123,34 @@ func TestManagerUpdateRoutes(t *testing.T) {
serverRoutesExpected: 2, serverRoutesExpected: 2,
clientNetworkWatchersExpected: 0, clientNetworkWatchersExpected: 0,
}, },
{
name: "Should Create 2 Server Routes (IPv6)",
inputRoutes: []*route.Route{
{
ID: "a",
NetID: "routeA",
Peer: localPeerKey,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "b",
NetID: "routeB",
Peer: localPeerKey,
Network: netip.MustParsePrefix("2001:db8::7890:abcd/128"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputSerial: 1,
serverRoutesExpected: 2,
clientNetworkWatchersExpected: 0,
},
{ {
name: "Should Create 1 Route For Client And Server", name: "Should Create 1 Route For Client And Server",
inputRoutes: []*route.Route{ inputRoutes: []*route.Route{
@ -121,6 +179,84 @@ func TestManagerUpdateRoutes(t *testing.T) {
serverRoutesExpected: 1, serverRoutesExpected: 1,
clientNetworkWatchersExpected: 1, clientNetworkWatchersExpected: 1,
}, },
{
name: "Should Create 1 Route For Client And Server (IPv6)",
inputRoutes: []*route.Route{
{
ID: "a",
NetID: "routeA",
Peer: localPeerKey,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "b",
NetID: "routeB",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8::7890:abcd/128"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputSerial: 1,
serverRoutesExpected: 1,
clientNetworkWatchersExpected: 1,
isV6: true,
},
{
name: "Should Create 1 Route For Client And Server for each IP version",
inputRoutes: []*route.Route{
{
ID: "a",
NetID: "routeA",
Peer: localPeerKey,
Network: netip.MustParsePrefix("100.64.30.250/30"),
NetworkType: route.IPv4Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "b",
NetID: "routeB",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("8.8.9.9/32"),
NetworkType: route.IPv4Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "a",
NetID: "routeA",
Peer: localPeerKey,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "b",
NetID: "routeB",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8::7890:abcd/128"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputSerial: 1,
serverRoutesExpected: 2,
clientNetworkWatchersExpected: 2,
isV6: true,
},
{ {
name: "Should Create 1 Route For Client and Skip Server Route On Empty Server Router", name: "Should Create 1 Route For Client and Skip Server Route On Empty Server Router",
inputRoutes: []*route.Route{ inputRoutes: []*route.Route{
@ -150,6 +286,36 @@ func TestManagerUpdateRoutes(t *testing.T) {
serverRoutesExpected: 0, serverRoutesExpected: 0,
clientNetworkWatchersExpected: 1, clientNetworkWatchersExpected: 1,
}, },
{
name: "Should Create 1 Route For Client and Skip Server Route On Empty Server Router (IPv6)",
inputRoutes: []*route.Route{
{
ID: "a",
NetID: "routeA",
Peer: localPeerKey,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "b",
NetID: "routeB",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8::7890:abcd/128"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputSerial: 1,
removeSrvRouter: true,
serverRoutesExpected: 0,
clientNetworkWatchersExpected: 1,
isV6: true,
},
{ {
name: "Should Create 1 HA Route and 1 Standalone", name: "Should Create 1 HA Route and 1 Standalone",
inputRoutes: []*route.Route{ inputRoutes: []*route.Route{
@ -187,6 +353,44 @@ func TestManagerUpdateRoutes(t *testing.T) {
inputSerial: 1, inputSerial: 1,
clientNetworkWatchersExpected: 2, clientNetworkWatchersExpected: 2,
}, },
{
name: "Should Create 1 HA Route and 1 Standalone (IPv6)",
inputRoutes: []*route.Route{
{
ID: "a",
NetID: "routeA",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "b",
NetID: "routeA",
Peer: remotePeerKey2,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "c",
NetID: "routeB",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8::7890:abcd/128"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputSerial: 1,
clientNetworkWatchersExpected: 2,
isV6: true,
},
{ {
name: "No Small Client Route Should Be Added", name: "No Small Client Route Should Be Added",
inputRoutes: []*route.Route{ inputRoutes: []*route.Route{
@ -205,6 +409,25 @@ func TestManagerUpdateRoutes(t *testing.T) {
clientNetworkWatchersExpected: 0, clientNetworkWatchersExpected: 0,
clientNetworkWatchersExpectedAllowed: 1, clientNetworkWatchersExpectedAllowed: 1,
}, },
{
name: "No Small Client Route Should Be Added (IPv6)",
inputRoutes: []*route.Route{
{
ID: "a",
NetID: "routeA",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("::/0"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputSerial: 1,
clientNetworkWatchersExpected: 0,
clientNetworkWatchersExpectedAllowed: 1,
isV6: true,
},
{ {
name: "Remove 1 Client Route", name: "Remove 1 Client Route",
inputInitRoutes: []*route.Route{ inputInitRoutes: []*route.Route{
@ -244,6 +467,46 @@ func TestManagerUpdateRoutes(t *testing.T) {
inputSerial: 1, inputSerial: 1,
clientNetworkWatchersExpected: 1, clientNetworkWatchersExpected: 1,
}, },
{
name: "Remove 1 Client Route (IPv6)",
inputInitRoutes: []*route.Route{
{
ID: "a",
NetID: "routeA",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "b",
NetID: "routeB",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8::abcd:7890/128"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputRoutes: []*route.Route{
{
ID: "a",
NetID: "routeA",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputSerial: 1,
clientNetworkWatchersExpected: 1,
isV6: true,
},
{ {
name: "Update Route to HA", name: "Update Route to HA",
inputInitRoutes: []*route.Route{ inputInitRoutes: []*route.Route{
@ -293,6 +556,56 @@ func TestManagerUpdateRoutes(t *testing.T) {
inputSerial: 1, inputSerial: 1,
clientNetworkWatchersExpected: 1, clientNetworkWatchersExpected: 1,
}, },
{
name: "Update Route to HA (IPv6)",
inputInitRoutes: []*route.Route{
{
ID: "a",
NetID: "routeA",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "b",
NetID: "routeB",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8::abcd:7890/128"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputRoutes: []*route.Route{
{
ID: "a",
NetID: "routeA",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "b",
NetID: "routeA",
Peer: remotePeerKey2,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputSerial: 1,
clientNetworkWatchersExpected: 1,
isV6: true,
},
{ {
name: "Remove Client Routes", name: "Remove Client Routes",
inputInitRoutes: []*route.Route{ inputInitRoutes: []*route.Route{
@ -321,6 +634,35 @@ func TestManagerUpdateRoutes(t *testing.T) {
inputSerial: 1, inputSerial: 1,
clientNetworkWatchersExpected: 0, clientNetworkWatchersExpected: 0,
}, },
{
name: "Remove Client Routes (IPv6)",
inputInitRoutes: []*route.Route{
{
ID: "a",
NetID: "routeA",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "b",
NetID: "routeB",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8::abcd:7890/128"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputRoutes: []*route.Route{},
inputSerial: 1,
clientNetworkWatchersExpected: 0,
isV6: true,
},
{ {
name: "Remove All Routes", name: "Remove All Routes",
inputInitRoutes: []*route.Route{ inputInitRoutes: []*route.Route{
@ -350,6 +692,36 @@ func TestManagerUpdateRoutes(t *testing.T) {
serverRoutesExpected: 0, serverRoutesExpected: 0,
clientNetworkWatchersExpected: 0, clientNetworkWatchersExpected: 0,
}, },
{
name: "Remove All Routes (IPv6)",
inputInitRoutes: []*route.Route{
{
ID: "a",
NetID: "routeA",
Peer: localPeerKey,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "b",
NetID: "routeB",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8::abcd:7890/128"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputRoutes: []*route.Route{},
inputSerial: 1,
serverRoutesExpected: 0,
clientNetworkWatchersExpected: 0,
isV6: true,
},
{ {
name: "HA server should not register routes from the same HA group", name: "HA server should not register routes from the same HA group",
inputRoutes: []*route.Route{ inputRoutes: []*route.Route{
@ -398,16 +770,74 @@ func TestManagerUpdateRoutes(t *testing.T) {
serverRoutesExpected: 2, serverRoutesExpected: 2,
clientNetworkWatchersExpected: 1, clientNetworkWatchersExpected: 1,
}, },
{
name: "HA server should not register routes from the same HA group (IPv6)",
inputRoutes: []*route.Route{
{
ID: "l1",
NetID: "routeA",
Peer: localPeerKey,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "l2",
NetID: "routeA",
Peer: localPeerKey,
Network: netip.MustParsePrefix("2001:db8::abcd:7890/128"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "r1",
NetID: "routeA",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
{
ID: "r2",
NetID: "routeC",
Peer: remotePeerKey1,
Network: netip.MustParsePrefix("2001:db8::abcd:789f/128"),
NetworkType: route.IPv6Network,
Metric: 9999,
Masquerade: false,
Enabled: true,
},
},
inputSerial: 1,
serverRoutesExpected: 2,
clientNetworkWatchersExpected: 1,
isV6: true,
},
} }
for n, testCase := range testCases { for n, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
v6Addr := ""
//goland:noinspection GoBoolExpressions
if !iface.SupportsIPv6() && testCase.isV6 {
t.Skip("Platform does not support IPv6, skipping IPv6 test...")
} else if testCase.isV6 {
v6Addr = "2001:db8::4242:4711/128"
}
peerPrivateKey, _ := wgtypes.GeneratePrivateKey() peerPrivateKey, _ := wgtypes.GeneratePrivateKey()
newNet, err := stdnet.NewNet() newNet, err := stdnet.NewNet()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
wgInterface, err := iface.NewWGIFace(fmt.Sprintf("utun43%d", n), "100.65.65.2/24", 33100, peerPrivateKey.String(), iface.DefaultMTU, newNet, nil) wgInterface, err := iface.NewWGIFace(fmt.Sprintf("utun43%d", n), "100.65.65.2/24", v6Addr, 33100, peerPrivateKey.String(), iface.DefaultMTU, newNet, nil)
require.NoError(t, err, "should create testing WGIface interface") require.NoError(t, err, "should create testing WGIface interface")
defer wgInterface.Close() defer wgInterface.Close()

View File

@ -64,6 +64,10 @@ func (m *MockManager) EnableServerRouter(firewall firewall.Manager) error {
panic("implement me") panic("implement me")
} }
func (m *MockManager) ResetV6Routes() {
panic("implement me")
}
// Stop mock implementation of Stop from Manager interface // Stop mock implementation of Stop from Manager interface
func (m *MockManager) Stop() { func (m *MockManager) Stop() {
if m.StopFunc != nil { if m.StopFunc != nil {

View File

@ -70,7 +70,7 @@ func (m *defaultServerRouter) updateRoutes(routesMap map[route.ID]*route.Route)
} }
if len(m.routes) > 0 { if len(m.routes) > 0 {
err := enableIPForwarding() err := enableIPForwarding(m.wgInterface.Address6() != nil)
if err != nil { if err != nil {
return err return err
} }
@ -79,7 +79,7 @@ func (m *defaultServerRouter) updateRoutes(routesMap map[route.ID]*route.Route)
return nil return nil
} }
func (m *defaultServerRouter) removeFromServerNetwork(route *route.Route) error { func (m *defaultServerRouter) removeFromServerNetwork(rt *route.Route) error {
select { select {
case <-m.ctx.Done(): case <-m.ctx.Done():
log.Infof("Not removing from server network because context is done") log.Infof("Not removing from server network because context is done")
@ -87,28 +87,32 @@ func (m *defaultServerRouter) removeFromServerNetwork(route *route.Route) error
default: default:
m.mux.Lock() m.mux.Lock()
defer m.mux.Unlock() defer m.mux.Unlock()
routingAddress := m.wgInterface.Address().Masked().String()
routerPair, err := routeToRouterPair(m.wgInterface.Address().Masked().String(), route) if rt.NetworkType == route.IPv6Network {
if m.wgInterface.Address6() == nil {
return fmt.Errorf("attempted to add route for IPv6 even though device has no v6 address")
}
routingAddress = m.wgInterface.Address6().Masked().String()
}
routerPair, err := routeToRouterPair(routingAddress, rt)
if err != nil { if err != nil {
return fmt.Errorf("parse prefix: %w", err) return fmt.Errorf("parse prefix: %w", err)
} }
err = m.firewall.RemoveRoutingRules(routerPair) err = m.firewall.RemoveRoutingRules(routerPair)
if err != nil { if err != nil {
return fmt.Errorf("remove routing rules: %w", err) return err
} }
delete(m.routes, route.ID) delete(m.routes, rt.ID)
state := m.statusRecorder.GetLocalPeerState() state := m.statusRecorder.GetLocalPeerState()
delete(state.Routes, route.Network.String()) delete(state.Routes, rt.Network.String())
m.statusRecorder.UpdateLocalPeerState(state) m.statusRecorder.UpdateLocalPeerState(state)
return nil return nil
} }
} }
func (m *defaultServerRouter) addToServerNetwork(route *route.Route) error { func (m *defaultServerRouter) addToServerNetwork(rt *route.Route) error {
select { select {
case <-m.ctx.Done(): case <-m.ctx.Done():
log.Infof("Not adding to server network because context is done") log.Infof("Not adding to server network because context is done")
@ -116,8 +120,15 @@ func (m *defaultServerRouter) addToServerNetwork(route *route.Route) error {
default: default:
m.mux.Lock() m.mux.Lock()
defer m.mux.Unlock() defer m.mux.Unlock()
routingAddress := m.wgInterface.Address().Masked().String()
if rt.NetworkType == route.IPv6Network {
if m.wgInterface.Address6() == nil {
return fmt.Errorf("attempted to add route for IPv6 even though device has no v6 address")
}
routingAddress = m.wgInterface.Address6().Masked().String()
}
routerPair, err := routeToRouterPair(m.wgInterface.Address().Masked().String(), route) routerPair, err := routeToRouterPair(routingAddress, rt)
if err != nil { if err != nil {
return fmt.Errorf("parse prefix: %w", err) return fmt.Errorf("parse prefix: %w", err)
} }
@ -127,13 +138,13 @@ func (m *defaultServerRouter) addToServerNetwork(route *route.Route) error {
return fmt.Errorf("insert routing rules: %w", err) return fmt.Errorf("insert routing rules: %w", err)
} }
m.routes[route.ID] = route m.routes[rt.ID] = rt
state := m.statusRecorder.GetLocalPeerState() state := m.statusRecorder.GetLocalPeerState()
if state.Routes == nil { if state.Routes == nil {
state.Routes = map[string]struct{}{} state.Routes = map[string]struct{}{}
} }
state.Routes[route.Network.String()] = struct{}{} state.Routes[rt.Network.String()] = struct{}{}
m.statusRecorder.UpdateLocalPeerState(state) m.statusRecorder.UpdateLocalPeerState(state)
return nil return nil
@ -144,10 +155,17 @@ func (m *defaultServerRouter) cleanUp() {
m.mux.Lock() m.mux.Lock()
defer m.mux.Unlock() defer m.mux.Unlock()
for _, r := range m.routes { for _, r := range m.routes {
routerPair, err := routeToRouterPair(m.wgInterface.Address().Masked().String(), r) routingAddress := m.wgInterface.Address().Masked().String()
if r.NetworkType == route.IPv6Network {
if m.wgInterface.Address6() == nil {
log.Errorf("attempted to remove route for IPv6 even though device has no v6 address")
continue
}
routingAddress = m.wgInterface.Address6().Masked().String()
}
routerPair, err := routeToRouterPair(routingAddress, r)
if err != nil { if err != nil {
log.Errorf("Failed to convert route to router pair: %v", err) log.Errorf("parse prefix: %v", err)
continue
} }
err = m.firewall.RemoveRoutingRules(routerPair) err = m.firewall.RemoveRoutingRules(routerPair)

View File

@ -122,6 +122,16 @@ func ipToAddr(ip net.IP, intf *net.Interface) (netip.Addr, error) {
} }
func existsInRouteTable(prefix netip.Prefix) (bool, error) { func existsInRouteTable(prefix netip.Prefix) (bool, error) {
linkLocalPrefix, err := netip.ParsePrefix("fe80::/10")
if err != nil {
return false, err
}
if prefix.Addr().Is6() && linkLocalPrefix.Contains(prefix.Addr()) {
// The link local prefix is not explicitly part of the routing table, but should be considered as such.
return true, nil
}
routes, err := getRoutesFromTable() routes, err := getRoutesFromTable()
if err != nil { if err != nil {
return false, fmt.Errorf("get routes from table: %w", err) return false, fmt.Errorf("get routes from table: %w", err)

View File

@ -19,7 +19,7 @@ func cleanupRouting() error {
return nil return nil
} }
func enableIPForwarding() error { func enableIPForwarding(includeV6 bool) error {
log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS) log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS)
return nil return nil
} }

View File

@ -184,12 +184,6 @@ func addVPNRoute(prefix netip.Prefix, intf *net.Interface) error {
// No need to check if routes exist as main table takes precedence over the VPN table via Rule 1 // No need to check if routes exist as main table takes precedence over the VPN table via Rule 1
// TODO remove this once we have ipv6 support
if prefix == defaultv4 {
if err := addUnreachableRoute(defaultv6, NetbirdVPNTableID); err != nil {
return fmt.Errorf("add blackhole: %w", err)
}
}
if err := addRoute(prefix, netip.Addr{}, intf, NetbirdVPNTableID); err != nil { if err := addRoute(prefix, netip.Addr{}, intf, NetbirdVPNTableID); err != nil {
return fmt.Errorf("add route: %w", err) return fmt.Errorf("add route: %w", err)
} }
@ -201,12 +195,6 @@ func removeVPNRoute(prefix netip.Prefix, intf *net.Interface) error {
return genericRemoveVPNRoute(prefix, intf) return genericRemoveVPNRoute(prefix, intf)
} }
// TODO remove this once we have ipv6 support
if prefix == defaultv4 {
if err := removeUnreachableRoute(defaultv6, NetbirdVPNTableID); err != nil {
return fmt.Errorf("remove unreachable route: %w", err)
}
}
if err := removeRoute(prefix, netip.Addr{}, intf, NetbirdVPNTableID); err != nil { if err := removeRoute(prefix, netip.Addr{}, intf, NetbirdVPNTableID); err != nil {
return fmt.Errorf("remove route: %w", err) return fmt.Errorf("remove route: %w", err)
} }
@ -282,6 +270,9 @@ func addRoute(prefix netip.Prefix, addr netip.Addr, intf *net.Interface, tableID
// addUnreachableRoute adds an unreachable route for the specified IP family and routing table. // addUnreachableRoute adds an unreachable route for the specified IP family and routing table.
// ipFamily should be netlink.FAMILY_V4 for IPv4 or netlink.FAMILY_V6 for IPv6. // ipFamily should be netlink.FAMILY_V4 for IPv4 or netlink.FAMILY_V6 for IPv6.
// tableID specifies the routing table to which the unreachable route will be added. // tableID specifies the routing table to which the unreachable route will be added.
// TODO should this be kept in for future use? If so, the linter needs to be told that this unreachable function should
//
// be kept
func addUnreachableRoute(prefix netip.Prefix, tableID int) error { func addUnreachableRoute(prefix netip.Prefix, tableID int) error {
_, ipNet, err := net.ParseCIDR(prefix.String()) _, ipNet, err := net.ParseCIDR(prefix.String())
if err != nil { if err != nil {
@ -302,6 +293,9 @@ func addUnreachableRoute(prefix netip.Prefix, tableID int) error {
return nil return nil
} }
// TODO should this be kept in for future use? If so, the linter needs to be told that this unreachable function should
//
// be kept
func removeUnreachableRoute(prefix netip.Prefix, tableID int) error { func removeUnreachableRoute(prefix netip.Prefix, tableID int) error {
_, ipNet, err := net.ParseCIDR(prefix.String()) _, ipNet, err := net.ParseCIDR(prefix.String())
if err != nil { if err != nil {
@ -376,8 +370,14 @@ func flushRoutes(tableID, family int) error {
return result.ErrorOrNil() return result.ErrorOrNil()
} }
func enableIPForwarding() error { func enableIPForwarding(includeV6 bool) error {
_, err := setSysctl(ipv4ForwardingPath, 1, false) _, err := setSysctl(ipv4ForwardingPath, 1, false)
if err != nil {
return err
}
if includeV6 {
_, err = setSysctl(ipv4ForwardingPath, 1, false)
}
return err return err
} }

View File

@ -10,7 +10,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func enableIPForwarding() error { func enableIPForwarding(includeV6 bool) error {
log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS) log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS)
return nil return nil
} }

View File

@ -6,6 +6,8 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"github.com/google/gopacket/routing"
"github.com/netbirdio/netbird/client/firewall"
"net" "net"
"net/netip" "net/netip"
"os" "os"
@ -46,18 +48,39 @@ func TestAddRemoveRoutes(t *testing.T) {
shouldRouteToWireguard: false, shouldRouteToWireguard: false,
shouldBeRemoved: false, shouldBeRemoved: false,
}, },
{
name: "Should Add And Remove Route 2001:db8:1234:5678::/64",
prefix: netip.MustParsePrefix("2001:db8:1234:5678::/64"),
shouldRouteToWireguard: true,
shouldBeRemoved: true,
},
{
name: "Should Not Add Or Remove Route ::1/128",
prefix: netip.MustParsePrefix("::1/128"),
shouldRouteToWireguard: false,
shouldBeRemoved: false,
},
} }
for n, testCase := range testCases { for n, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
t.Setenv("NB_DISABLE_ROUTE_CACHE", "true") t.Setenv("NB_DISABLE_ROUTE_CACHE", "true")
v6Addr := ""
hasV6DefaultRoute, err := EnvironmentHasIPv6DefaultRoute()
//goland:noinspection GoBoolExpressions
if (!iface.SupportsIPv6() || !firewall.SupportsIPv6() || !hasV6DefaultRoute || err != nil) && testCase.prefix.Addr().Is6() {
t.Skip("Platform does not support IPv6, skipping IPv6 test...")
} else if testCase.prefix.Addr().Is6() {
v6Addr = "2001:db8::4242:4711/128"
}
peerPrivateKey, _ := wgtypes.GeneratePrivateKey() peerPrivateKey, _ := wgtypes.GeneratePrivateKey()
newNet, err := stdnet.NewNet() newNet, err := stdnet.NewNet()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
wgInterface, err := iface.NewWGIFace(fmt.Sprintf("utun53%d", n), "100.65.75.2/24", 33100, peerPrivateKey.String(), iface.DefaultMTU, newNet, nil) wgInterface, err := iface.NewWGIFace(fmt.Sprintf("utun53%d", n), "100.65.75.2/24", v6Addr, 33100, peerPrivateKey.String(), iface.DefaultMTU, newNet, nil)
require.NoError(t, err, "should create testing WGIface interface") require.NoError(t, err, "should create testing WGIface interface")
defer wgInterface.Close() defer wgInterface.Close()
@ -92,6 +115,10 @@ func TestAddRemoveRoutes(t *testing.T) {
internetGateway, _, err := GetNextHop(netip.MustParseAddr("0.0.0.0")) internetGateway, _, err := GetNextHop(netip.MustParseAddr("0.0.0.0"))
require.NoError(t, err) require.NoError(t, err)
if testCase.prefix.Addr().Is6() {
internetGateway, _, err = GetNextHop(netip.MustParseAddr("::/0"))
}
require.NoError(t, err)
if testCase.shouldBeRemoved { if testCase.shouldBeRemoved {
require.Equal(t, internetGateway, prefixGateway, "route should be pointing to default internet gateway") require.Equal(t, internetGateway, prefixGateway, "route should be pointing to default internet gateway")
@ -151,6 +178,18 @@ func TestAddExistAndRemoveRoute(t *testing.T) {
if err != nil { if err != nil {
t.Fatal("shouldn't return error when fetching the gateway: ", err) t.Fatal("shouldn't return error when fetching the gateway: ", err)
} }
var defaultGateway6 *netip.Addr
hasV6DefaultRoute, err := EnvironmentHasIPv6DefaultRoute()
//goland:noinspection GoBoolExpressions
if iface.SupportsIPv6() && firewall.SupportsIPv6() && hasV6DefaultRoute && err == nil {
gw6, _, err := GetNextHop(netip.MustParseAddr("::"))
gw6 = gw6.WithZone("")
defaultGateway6 = &gw6
t.Log("defaultGateway6: ", defaultGateway6)
if err != nil {
t.Fatal("shouldn't return error when fetching the IPv6 gateway: ", err)
}
}
testCases := []struct { testCases := []struct {
name string name string
prefix netip.Prefix prefix netip.Prefix
@ -185,6 +224,43 @@ func TestAddExistAndRemoveRoute(t *testing.T) {
preExistingPrefix: netip.MustParsePrefix("100.100.0.0/16"), preExistingPrefix: netip.MustParsePrefix("100.100.0.0/16"),
shouldAddRoute: false, shouldAddRoute: false,
}, },
{
name: "Should Add And Remove random Route (IPv6)",
prefix: netip.MustParsePrefix("2001:db8::abcd/128"),
shouldAddRoute: true,
},
{
name: "Should Add Route if bigger network exists (IPv6)",
prefix: netip.MustParsePrefix("2001:db8:b14d:abcd:1234::/96"),
preExistingPrefix: netip.MustParsePrefix("2001:db8:b14d:abcd::/64"),
shouldAddRoute: true,
},
{
name: "Should Add Route if smaller network exists (IPv6)",
prefix: netip.MustParsePrefix("2001:db8:b14d::/48"),
preExistingPrefix: netip.MustParsePrefix("2001:db8:b14d:abcd::/64"),
shouldAddRoute: true,
},
{
name: "Should Not Add Route if same network exists (IPv6)",
prefix: netip.MustParsePrefix("2001:db8:b14d:abcd::/64"),
preExistingPrefix: netip.MustParsePrefix("2001:db8:b14d:abcd::/64"),
shouldAddRoute: false,
},
}
if defaultGateway6 != nil {
testCases = append(testCases, []struct {
name string
prefix netip.Prefix
preExistingPrefix netip.Prefix
shouldAddRoute bool
}{
{
name: "Should Not Add Route if overlaps with default gateway (IPv6)",
prefix: netip.MustParsePrefix(defaultGateway6.String() + "/127"),
shouldAddRoute: false,
},
}...)
} }
for n, testCase := range testCases { for n, testCase := range testCases {
@ -198,12 +274,19 @@ func TestAddExistAndRemoveRoute(t *testing.T) {
t.Setenv("NB_USE_LEGACY_ROUTING", "true") t.Setenv("NB_USE_LEGACY_ROUTING", "true")
t.Setenv("NB_DISABLE_ROUTE_CACHE", "true") t.Setenv("NB_DISABLE_ROUTE_CACHE", "true")
v6Addr := ""
if testCase.prefix.Addr().Is6() && defaultGateway6 == nil {
t.Skip("Platform does not support IPv6, skipping IPv6 test...")
} else if testCase.prefix.Addr().Is6() {
v6Addr = "2001:db8::4242:4711/128"
}
peerPrivateKey, _ := wgtypes.GeneratePrivateKey() peerPrivateKey, _ := wgtypes.GeneratePrivateKey()
newNet, err := stdnet.NewNet() newNet, err := stdnet.NewNet()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
wgInterface, err := iface.NewWGIFace(fmt.Sprintf("utun53%d", n), "100.65.75.2/24", 33100, peerPrivateKey.String(), iface.DefaultMTU, newNet, nil) wgInterface, err := iface.NewWGIFace(fmt.Sprintf("utun53%d", n), "100.65.75.2/24", v6Addr, 33100, peerPrivateKey.String(), iface.DefaultMTU, newNet, nil)
require.NoError(t, err, "should create testing WGIface interface") require.NoError(t, err, "should create testing WGIface interface")
defer wgInterface.Close() defer wgInterface.Close()
@ -249,6 +332,11 @@ func TestAddExistAndRemoveRoute(t *testing.T) {
} }
func TestIsSubRange(t *testing.T) { func TestIsSubRange(t *testing.T) {
// Note: This test may fail for IPv6 in some environments, where there actually exists another route that the
// determined prefix is a sub-range of.
hasV6DefaultRoute, err := EnvironmentHasIPv6DefaultRoute()
shouldIncludeV6Routes := iface.SupportsIPv6() && firewall.SupportsIPv6() && hasV6DefaultRoute && err == nil
addresses, err := net.InterfaceAddrs() addresses, err := net.InterfaceAddrs()
if err != nil { if err != nil {
t.Fatal("shouldn't return error when fetching interface addresses: ", err) t.Fatal("shouldn't return error when fetching interface addresses: ", err)
@ -258,7 +346,7 @@ func TestIsSubRange(t *testing.T) {
var nonSubRangeAddressPrefixes []netip.Prefix var nonSubRangeAddressPrefixes []netip.Prefix
for _, address := range addresses { for _, address := range addresses {
p := netip.MustParsePrefix(address.String()) p := netip.MustParsePrefix(address.String())
if !p.Addr().IsLoopback() && p.Addr().Is4() && p.Bits() < 32 { if !p.Addr().IsLoopback() && (p.Addr().Is4() && p.Bits() < 32) || (p.Addr().Is6() && shouldIncludeV6Routes && p.Bits() < 128) {
p2 := netip.PrefixFrom(p.Masked().Addr(), p.Bits()+1) p2 := netip.PrefixFrom(p.Masked().Addr(), p.Bits()+1)
subRangeAddressPrefixes = append(subRangeAddressPrefixes, p2) subRangeAddressPrefixes = append(subRangeAddressPrefixes, p2)
nonSubRangeAddressPrefixes = append(nonSubRangeAddressPrefixes, p.Masked()) nonSubRangeAddressPrefixes = append(nonSubRangeAddressPrefixes, p.Masked())
@ -286,16 +374,37 @@ func TestIsSubRange(t *testing.T) {
} }
} }
func EnvironmentHasIPv6DefaultRoute() (bool, error) {
//goland:noinspection GoBoolExpressions
if runtime.GOOS != "linux" {
// TODO when implementing IPv6 for other operating systems, this should be replaced with code that determines
// whether a default route for IPv6 exists (routing.Router panics on non-linux).
return false, nil
}
router, err := routing.New()
if err != nil {
return false, err
}
routeIface, _, _, err := router.Route(netip.MustParsePrefix("::/0").Addr().AsSlice())
if err != nil {
return false, err
}
return routeIface != nil, nil
}
func TestExistsInRouteTable(t *testing.T) { func TestExistsInRouteTable(t *testing.T) {
addresses, err := net.InterfaceAddrs() addresses, err := net.InterfaceAddrs()
if err != nil { if err != nil {
t.Fatal("shouldn't return error when fetching interface addresses: ", err) t.Fatal("shouldn't return error when fetching interface addresses: ", err)
} }
hasV6DefaultRoute, err := EnvironmentHasIPv6DefaultRoute()
shouldIncludeV6Routes := iface.SupportsIPv6() && firewall.SupportsIPv6() && hasV6DefaultRoute && err == nil
var addressPrefixes []netip.Prefix var addressPrefixes []netip.Prefix
for _, address := range addresses { for _, address := range addresses {
p := netip.MustParsePrefix(address.String()) p := netip.MustParsePrefix(address.String())
if p.Addr().Is6() { if p.Addr().Is6() && !shouldIncludeV6Routes {
continue continue
} }
// Windows sometimes has hidden interface link local addrs that don't turn up on any interface // Windows sometimes has hidden interface link local addrs that don't turn up on any interface
@ -321,7 +430,7 @@ func TestExistsInRouteTable(t *testing.T) {
} }
} }
func createWGInterface(t *testing.T, interfaceName, ipAddressCIDR string, listenPort int) *iface.WGIface { func createWGInterface(t *testing.T, interfaceName, ipAddressCIDR string, ipAddress6CIDR string, listenPort int) *iface.WGIface {
t.Helper() t.Helper()
peerPrivateKey, err := wgtypes.GeneratePrivateKey() peerPrivateKey, err := wgtypes.GeneratePrivateKey()
@ -330,7 +439,7 @@ func createWGInterface(t *testing.T, interfaceName, ipAddressCIDR string, listen
newNet, err := stdnet.NewNet() newNet, err := stdnet.NewNet()
require.NoError(t, err) require.NoError(t, err)
wgInterface, err := iface.NewWGIFace(interfaceName, ipAddressCIDR, listenPort, peerPrivateKey.String(), iface.DefaultMTU, newNet, nil) wgInterface, err := iface.NewWGIFace(interfaceName, ipAddressCIDR, ipAddress6CIDR, listenPort, peerPrivateKey.String(), iface.DefaultMTU, newNet, nil)
require.NoError(t, err, "should create testing WireGuard interface") require.NoError(t, err, "should create testing WireGuard interface")
err = wgInterface.Create() err = wgInterface.Create()
@ -348,12 +457,21 @@ func setupTestEnv(t *testing.T) {
setupDummyInterfacesAndRoutes(t) setupDummyInterfacesAndRoutes(t)
wgIface := createWGInterface(t, expectedVPNint, "100.64.0.1/24", 51820) v6Addr := ""
hasV6DefaultRoute, err := EnvironmentHasIPv6DefaultRoute()
//goland:noinspection GoBoolExpressions
if !iface.SupportsIPv6() || !firewall.SupportsIPv6() || !hasV6DefaultRoute || err != nil {
t.Skip("Platform does not support IPv6, skipping IPv6 test...")
} else {
v6Addr = "2001:db8::4242:4711/128"
}
wgIface := createWGInterface(t, expectedVPNint, "100.64.0.1/24", v6Addr, 51820)
t.Cleanup(func() { t.Cleanup(func() {
assert.NoError(t, wgIface.Close()) assert.NoError(t, wgIface.Close())
}) })
_, _, err := setupRouting(nil, wgIface) _, _, err = setupRouting(nil, wgIface)
require.NoError(t, err, "setupRouting should not return err") require.NoError(t, err, "setupRouting should not return err")
t.Cleanup(func() { t.Cleanup(func() {
assert.NoError(t, cleanupRouting()) assert.NoError(t, cleanupRouting())
@ -412,9 +530,15 @@ func assertWGOutInterface(t *testing.T, prefix netip.Prefix, wgIface *iface.WGIf
prefixGateway, _, err := GetNextHop(prefix.Addr()) prefixGateway, _, err := GetNextHop(prefix.Addr())
require.NoError(t, err, "GetNextHop should not return err") require.NoError(t, err, "GetNextHop should not return err")
nexthop := wgIface.Address().IP.String()
if prefix.Addr().Is6() {
nexthop = wgIface.Address6().IP.String()
}
if invert { if invert {
assert.NotEqual(t, wgIface.Address().IP.String(), prefixGateway.String(), "route should not point to wireguard interface IP") assert.NotEqual(t, nexthop, prefixGateway.String(), "route should not point to wireguard interface IP")
} else { } else {
assert.Equal(t, wgIface.Address().IP.String(), prefixGateway.String(), "route should point to wireguard interface IP") assert.Equal(t, nexthop, prefixGateway.String(), "route should point to wireguard interface IP")
} }
} }

View File

@ -51,6 +51,7 @@ type Info struct {
SystemProductName string SystemProductName string
SystemManufacturer string SystemManufacturer string
Environment Environment Environment Environment
Ipv6Supported bool
} }
// extractUserAgent extracts Netbird's agent (client) name and version from the outgoing context // extractUserAgent extracts Netbird's agent (client) name and version from the outgoing context

View File

@ -39,6 +39,7 @@ func GetInfo(ctx context.Context) *Info {
WiretrusteeVersion: version.NetbirdVersion(), WiretrusteeVersion: version.NetbirdVersion(),
UIVersion: extractUIVersion(ctx), UIVersion: extractUIVersion(ctx),
KernelVersion: kernelVersion, KernelVersion: kernelVersion,
Ipv6Supported: false,
} }
return gio return gio

View File

@ -61,6 +61,7 @@ func GetInfo(ctx context.Context) *Info {
SystemProductName: prodName, SystemProductName: prodName,
SystemManufacturer: manufacturer, SystemManufacturer: manufacturer,
Environment: env, Environment: env,
Ipv6Supported: false,
} }
systemHostname, _ := os.Hostname() systemHostname, _ := os.Hostname()

View File

@ -31,7 +31,7 @@ func GetInfo(ctx context.Context) *Info {
Platform: detect_platform.Detect(ctx), Platform: detect_platform.Detect(ctx),
} }
gio := &Info{Kernel: osInfo[0], Platform: runtime.GOARCH, OS: osInfo[2], GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: osInfo[1], Environment: env} gio := &Info{Kernel: osInfo[0], Platform: runtime.GOARCH, OS: osInfo[2], GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: osInfo[1], Environment: env, Ipv6Supported: false}
systemHostname, _ := os.Hostname() systemHostname, _ := os.Hostname()
gio.Hostname = extractDeviceName(ctx, systemHostname) gio.Hostname = extractDeviceName(ctx, systemHostname)

View File

@ -6,6 +6,8 @@ package system
import ( import (
"bytes" "bytes"
"context" "context"
"github.com/netbirdio/netbird/client/firewall"
"github.com/netbirdio/netbird/iface"
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
@ -84,6 +86,7 @@ func GetInfo(ctx context.Context) *Info {
SystemProductName: prodName, SystemProductName: prodName,
SystemManufacturer: manufacturer, SystemManufacturer: manufacturer,
Environment: env, Environment: env,
Ipv6Supported: _checkIPv6Support(),
} }
return gio return gio
@ -122,3 +125,8 @@ func sysInfo() (serialNumber string, productName string, manufacturer string) {
si.GetSysInfo() si.GetSysInfo()
return si.Chassis.Serial, si.Product.Name, si.Product.Vendor return si.Chassis.Serial, si.Product.Name, si.Product.Vendor
} }
func _checkIPv6Support() bool {
return firewall.SupportsIPv6() &&
iface.SupportsIPv6()
}

View File

@ -75,6 +75,7 @@ func GetInfo(ctx context.Context) *Info {
SystemProductName: prodName, SystemProductName: prodName,
SystemManufacturer: manufacturer, SystemManufacturer: manufacturer,
Environment: env, Environment: env,
Ipv6Supported: false,
} }
systemHostname, _ := os.Hostname() systemHostname, _ := os.Hostname()

View File

@ -48,6 +48,11 @@ func (w *WGIface) Address() WGAddress {
return w.tun.WgAddress() return w.tun.WgAddress()
} }
// Address6 returns the IPv6 interface address
func (w *WGIface) Address6() *WGAddress {
return w.tun.WgAddress6()
}
// Up configures a Wireguard interface // Up configures a Wireguard interface
// The interface must exist before calling this method (e.g. call interface.Create() before) // The interface must exist before calling this method (e.g. call interface.Create() before)
func (w *WGIface) Up() (*bind.UniversalUDPMuxDefault, error) { func (w *WGIface) Up() (*bind.UniversalUDPMuxDefault, error) {
@ -70,6 +75,23 @@ func (w *WGIface) UpdateAddr(newAddr string) error {
return w.tun.UpdateAddr(addr) return w.tun.UpdateAddr(addr)
} }
// UpdateAddr6 updates the IPv6 address of the interface
func (w *WGIface) UpdateAddr6(newAddr6 string) error {
w.mu.Lock()
defer w.mu.Unlock()
var addr *WGAddress
if newAddr6 != "" {
parsedAddr, err := parseWGAddress(newAddr6)
if err != nil {
return err
}
addr = &parsedAddr
}
return w.tun.UpdateAddr6(addr)
}
// UpdatePeer updates existing Wireguard Peer or creates a new one if doesn't exist // UpdatePeer updates existing Wireguard Peer or creates a new one if doesn't exist
// Endpoint is optional // Endpoint is optional
func (w *WGIface) UpdatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error { func (w *WGIface) UpdatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error {

View File

@ -3,11 +3,16 @@ package iface
import ( import (
"fmt" "fmt"
log "github.com/sirupsen/logrus"
"github.com/pion/transport/v3" "github.com/pion/transport/v3"
) )
// NewWGIFace Creates a new WireGuard interface instance // NewWGIFace Creates a new WireGuard interface instance
func NewWGIFace(iFaceName string, address string, wgPort int, wgPrivKey string, mtu int, transportNet transport.Net, args *MobileIFaceArguments) (*WGIface, error) { func NewWGIFace(iFaceName string, address string, address6 string, wgPort int, wgPrivKey string, mtu int, transportNet transport.Net, args *MobileIFaceArguments) (*WGIface, error) {
if address6 != "" {
log.Errorf("Attempted to configure IPv6 address %s on unsupported operating system", address6)
}
wgAddress, err := parseWGAddress(address) wgAddress, err := parseWGAddress(address)
if err != nil { if err != nil {
return nil, err return nil, err
@ -38,3 +43,7 @@ func (w *WGIface) CreateOnAndroid(routes []string, dns string, searchDomains []s
func (w *WGIface) Create() error { func (w *WGIface) Create() error {
return fmt.Errorf("this function has not implemented on this platform") return fmt.Errorf("this function has not implemented on this platform")
} }
func SupportsIPv6() bool {
return false
}

View File

@ -6,13 +6,18 @@ package iface
import ( import (
"fmt" "fmt"
log "github.com/sirupsen/logrus"
"github.com/pion/transport/v3" "github.com/pion/transport/v3"
"github.com/netbirdio/netbird/iface/netstack" "github.com/netbirdio/netbird/iface/netstack"
) )
// NewWGIFace Creates a new WireGuard interface instance // NewWGIFace Creates a new WireGuard interface instance
func NewWGIFace(iFaceName string, address string, wgPort int, wgPrivKey string, mtu int, transportNet transport.Net, args *MobileIFaceArguments) (*WGIface, error) { func NewWGIFace(iFaceName string, address string, address6 string, wgPort int, wgPrivKey string, mtu int, transportNet transport.Net, args *MobileIFaceArguments) (*WGIface, error) {
if address6 != "" {
log.Errorf("Attempted to configure IPv6 address %s on unsupported operating system", address6)
}
wgAddress, err := parseWGAddress(address) wgAddress, err := parseWGAddress(address)
if err != nil { if err != nil {
return nil, err return nil, err
@ -36,3 +41,7 @@ func NewWGIFace(iFaceName string, address string, wgPort int, wgPrivKey string,
func (w *WGIface) CreateOnAndroid([]string, string, []string) error { func (w *WGIface) CreateOnAndroid([]string, string, []string) error {
return fmt.Errorf("this function has not implemented on this platform") return fmt.Errorf("this function has not implemented on this platform")
} }
func SupportsIPv6() bool {
return false
}

View File

@ -5,12 +5,17 @@ package iface
import ( import (
"fmt" "fmt"
log "github.com/sirupsen/logrus"
"github.com/pion/transport/v3" "github.com/pion/transport/v3"
) )
// NewWGIFace Creates a new WireGuard interface instance // NewWGIFace Creates a new WireGuard interface instance
func NewWGIFace(iFaceName string, address string, wgPort int, wgPrivKey string, mtu int, transportNet transport.Net, args *MobileIFaceArguments) (*WGIface, error) { func NewWGIFace(iFaceName string, address string, address6 string, wgPort int, wgPrivKey string, mtu int, transportNet transport.Net, args *MobileIFaceArguments) (*WGIface, error) {
if address6 != "" {
log.Errorf("Attempted to configure IPv6 address %s on unsupported operating system", address6)
}
wgAddress, err := parseWGAddress(address) wgAddress, err := parseWGAddress(address)
if err != nil { if err != nil {
return nil, err return nil, err
@ -27,3 +32,7 @@ func NewWGIFace(iFaceName string, address string, wgPort int, wgPrivKey string,
func (w *WGIface) CreateOnAndroid([]string, string, []string) error { func (w *WGIface) CreateOnAndroid([]string, string, []string) error {
return fmt.Errorf("this function has not implemented on this platform") return fmt.Errorf("this function has not implemented on this platform")
} }
func SupportsIPv6() bool {
return false
}

View File

@ -5,14 +5,14 @@ package iface
import ( import (
"fmt" "fmt"
"github.com/pion/transport/v3"
"github.com/netbirdio/netbird/iface/netstack" "github.com/netbirdio/netbird/iface/netstack"
"github.com/pion/transport/v3"
log "github.com/sirupsen/logrus"
"golang.org/x/net/nettest"
) )
// NewWGIFace Creates a new WireGuard interface instance // NewWGIFace Creates a new WireGuard interface instance
func NewWGIFace(iFaceName string, address string, wgPort int, wgPrivKey string, mtu int, transportNet transport.Net, args *MobileIFaceArguments) (*WGIface, error) { func NewWGIFace(iFaceName string, address string, address6 string, wgPort int, wgPrivKey string, mtu int, transportNet transport.Net, args *MobileIFaceArguments) (*WGIface, error) {
wgAddress, err := parseWGAddress(address) wgAddress, err := parseWGAddress(address)
if err != nil { if err != nil {
return nil, err return nil, err
@ -20,6 +20,10 @@ func NewWGIFace(iFaceName string, address string, wgPort int, wgPrivKey string,
wgIFace := &WGIface{} wgIFace := &WGIface{}
if netstack.IsEnabled() || !WireGuardModuleIsLoaded() && address6 != "" {
log.Errorf("Attempted to configure IPv6 address %s on unsupported device implementation (netstack or tun).", address6)
}
// move the kernel/usp/netstack preference evaluation to upper layer // move the kernel/usp/netstack preference evaluation to upper layer
if netstack.IsEnabled() { if netstack.IsEnabled() {
wgIFace.tun = newTunNetstackDevice(iFaceName, wgAddress, wgPort, wgPrivKey, mtu, transportNet, netstack.ListenAddr()) wgIFace.tun = newTunNetstackDevice(iFaceName, wgAddress, wgPort, wgPrivKey, mtu, transportNet, netstack.ListenAddr())
@ -28,7 +32,16 @@ func NewWGIFace(iFaceName string, address string, wgPort int, wgPrivKey string,
} }
if WireGuardModuleIsLoaded() { if WireGuardModuleIsLoaded() {
wgIFace.tun = newTunDevice(iFaceName, wgAddress, wgPort, wgPrivKey, mtu, transportNet)
var wgAddress6 *WGAddress = nil
if address6 != "" {
tmpWgAddress6, err := parseWGAddress(address6)
wgAddress6 = &tmpWgAddress6
if err != nil {
return wgIFace, err
}
}
wgIFace.tun = newTunDevice(iFaceName, wgAddress, wgAddress6, wgPort, wgPrivKey, mtu, transportNet)
wgIFace.userspaceBind = false wgIFace.userspaceBind = false
return wgIFace, nil return wgIFace, nil
} }
@ -45,3 +58,7 @@ func NewWGIFace(iFaceName string, address string, wgPort int, wgPrivKey string,
func (w *WGIface) CreateOnAndroid([]string, string, []string) error { func (w *WGIface) CreateOnAndroid([]string, string, []string) error {
return fmt.Errorf("this function has not implemented on this platform") return fmt.Errorf("this function has not implemented on this platform")
} }
func SupportsIPv6() bool {
return nettest.SupportsIPv6() && WireGuardModuleIsLoaded() && !netstack.IsEnabled()
}

View File

@ -41,7 +41,7 @@ func TestWGIface_UpdateAddr(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
iface, err := NewWGIFace(ifaceName, addr, wgPort, key, DefaultMTU, newNet, nil) iface, err := NewWGIFace(ifaceName, addr, "", wgPort, key, DefaultMTU, newNet, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -95,6 +95,64 @@ func TestWGIface_UpdateAddr(t *testing.T) {
} }
} }
func TestWGIface_UpdateAddr6(t *testing.T) {
if !SupportsIPv6() {
t.Skip("Environment does not support IPv6, skipping IPv6 test...")
}
ifaceName := fmt.Sprintf("utun%d", WgIntNumber+4)
addr := "100.64.0.1/8"
addr6 := "2001:db8:1234:abcd::42/64"
wgPort := 33100
newNet, err := stdnet.NewNet()
if err != nil {
t.Fatal(err)
}
iface, err := NewWGIFace(ifaceName, addr, addr6, wgPort, key, DefaultMTU, newNet, nil)
if err != nil {
t.Fatal(err)
}
err = iface.Create()
if err != nil {
t.Fatal(err)
}
defer func() {
err = iface.Close()
if err != nil {
t.Error(err)
}
}()
_, err = iface.Up()
if err != nil {
t.Fatal(err)
}
addrs, err := getIfaceAddrs(ifaceName)
if err != nil {
t.Error(err)
}
assert.Equal(t, addr, addrs[0].String())
//update WireGuard address
addr = "100.64.0.2/8"
err = iface.UpdateAddr(addr)
if err != nil {
t.Fatal(err)
}
addrs, err = getIfaceAddrs(ifaceName)
if err != nil {
t.Error(err)
}
assert.Equal(t, addr6, addrs[1].String())
}
func getIfaceAddrs(ifaceName string) ([]net.Addr, error) { func getIfaceAddrs(ifaceName string) ([]net.Addr, error) {
ief, err := net.InterfaceByName(ifaceName) ief, err := net.InterfaceByName(ifaceName)
if err != nil { if err != nil {
@ -114,7 +172,45 @@ func Test_CreateInterface(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
iface, err := NewWGIFace(ifaceName, wgIP, 33100, key, DefaultMTU, newNet, nil) iface, err := NewWGIFace(ifaceName, wgIP, "", 33100, key, DefaultMTU, newNet, nil)
if err != nil {
t.Fatal(err)
}
err = iface.Create()
if err != nil {
t.Fatal(err)
}
defer func() {
err = iface.Close()
if err != nil {
t.Error(err)
}
}()
wg, err := wgctrl.New()
if err != nil {
t.Fatal(err)
}
defer func() {
err = wg.Close()
if err != nil {
t.Error(err)
}
}()
}
func Test_CreateInterface6(t *testing.T) {
if !SupportsIPv6() {
t.Skip("Environment does not support IPv6, skipping IPv6 test...")
}
ifaceName := fmt.Sprintf("utun%d", WgIntNumber+1)
wgIP := "10.99.99.1/32"
wgIP6 := "2001:db8:1234:abcd::43/64"
newNet, err := stdnet.NewNet()
if err != nil {
t.Fatal(err)
}
iface, err := NewWGIFace(ifaceName, wgIP, wgIP6, 33100, key, DefaultMTU, newNet, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -149,7 +245,46 @@ func Test_Close(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
iface, err := NewWGIFace(ifaceName, wgIP, wgPort, key, DefaultMTU, newNet, nil) iface, err := NewWGIFace(ifaceName, wgIP, "", wgPort, key, DefaultMTU, newNet, nil)
if err != nil {
t.Fatal(err)
}
err = iface.Create()
if err != nil {
t.Fatal(err)
}
wg, err := wgctrl.New()
if err != nil {
t.Fatal(err)
}
defer func() {
err = wg.Close()
if err != nil {
t.Error(err)
}
}()
err = iface.Close()
if err != nil {
t.Fatal(err)
}
}
func Test_Close6(t *testing.T) {
if !SupportsIPv6() {
t.Skip("Environment does not support IPv6, skipping IPv6 test...")
}
ifaceName := fmt.Sprintf("utun%d", WgIntNumber+2)
wgIP := "10.99.99.2/32"
wgIP6 := "2001:db8:1234:abcd::44/64"
wgPort := 33100
newNet, err := stdnet.NewNet()
if err != nil {
t.Fatal(err)
}
iface, err := NewWGIFace(ifaceName, wgIP, wgIP6, wgPort, key, DefaultMTU, newNet, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -182,7 +317,7 @@ func Test_ConfigureInterface(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
iface, err := NewWGIFace(ifaceName, wgIP, wgPort, key, DefaultMTU, newNet, nil) iface, err := NewWGIFace(ifaceName, wgIP, "", wgPort, key, DefaultMTU, newNet, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -230,7 +365,7 @@ func Test_UpdatePeer(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
iface, err := NewWGIFace(ifaceName, wgIP, 33100, key, DefaultMTU, newNet, nil) iface, err := NewWGIFace(ifaceName, wgIP, "", 33100, key, DefaultMTU, newNet, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -283,6 +418,73 @@ func Test_UpdatePeer(t *testing.T) {
} }
} }
func Test_UpdatePeer6(t *testing.T) {
if !SupportsIPv6() {
t.Skip("Environment does not support IPv6, skipping IPv6 test...")
}
ifaceName := fmt.Sprintf("utun%d", WgIntNumber+4)
wgIP := "10.99.99.9/30"
wgIP6 := "2001:db8:1234:abcd::45/64"
newNet, err := stdnet.NewNet()
if err != nil {
t.Fatal(err)
}
iface, err := NewWGIFace(ifaceName, wgIP, wgIP6, 33100, key, DefaultMTU, newNet, nil)
if err != nil {
t.Fatal(err)
}
err = iface.Create()
if err != nil {
t.Fatal(err)
}
defer func() {
err = iface.Close()
if err != nil {
t.Error(err)
}
}()
_, err = iface.Up()
if err != nil {
t.Fatal(err)
}
keepAlive := 15 * time.Second
allowedIP := "10.99.99.10/32"
allowedIP6 := "2001:db8:1234:abcd::46/128"
endpoint, err := net.ResolveUDPAddr("udp", "127.0.0.1:9900")
if err != nil {
t.Fatal(err)
}
err = iface.UpdatePeer(peerPubKey, allowedIP+","+allowedIP6, keepAlive, endpoint, nil)
if err != nil {
t.Fatal(err)
}
peer, err := getPeer(ifaceName, peerPubKey)
if err != nil {
t.Fatal(err)
}
if peer.PersistentKeepaliveInterval != keepAlive {
t.Fatal("configured peer with mismatched keepalive interval value")
}
if peer.Endpoint.String() != endpoint.String() {
t.Fatal("configured peer with mismatched endpoint")
}
var foundAllowedIP bool
for _, aip := range peer.AllowedIPs {
if aip.String() == allowedIP6 {
foundAllowedIP = true
break
}
}
if !foundAllowedIP {
t.Fatal("configured peer with mismatched Allowed IPs")
}
}
func Test_RemovePeer(t *testing.T) { func Test_RemovePeer(t *testing.T) {
ifaceName := fmt.Sprintf("utun%d", WgIntNumber+4) ifaceName := fmt.Sprintf("utun%d", WgIntNumber+4)
wgIP := "10.99.99.13/30" wgIP := "10.99.99.13/30"
@ -291,7 +493,7 @@ func Test_RemovePeer(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
iface, err := NewWGIFace(ifaceName, wgIP, 33100, key, DefaultMTU, newNet, nil) iface, err := NewWGIFace(ifaceName, wgIP, "", 33100, key, DefaultMTU, newNet, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -345,7 +547,7 @@ func Test_ConnectPeers(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
iface1, err := NewWGIFace(peer1ifaceName, peer1wgIP, peer1wgPort, peer1Key.String(), DefaultMTU, newNet, nil) iface1, err := NewWGIFace(peer1ifaceName, peer1wgIP, "", peer1wgPort, peer1Key.String(), DefaultMTU, newNet, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -368,7 +570,113 @@ func Test_ConnectPeers(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
iface2, err := NewWGIFace(peer2ifaceName, peer2wgIP, peer2wgPort, peer2Key.String(), DefaultMTU, newNet, nil) iface2, err := NewWGIFace(peer2ifaceName, peer2wgIP, "", peer2wgPort, peer2Key.String(), DefaultMTU, newNet, nil)
if err != nil {
t.Fatal(err)
}
err = iface2.Create()
if err != nil {
t.Fatal(err)
}
_, err = iface2.Up()
if err != nil {
t.Fatal(err)
}
peer2endpoint, err := net.ResolveUDPAddr("udp", fmt.Sprintf("127.0.0.1:%d", peer2wgPort))
if err != nil {
t.Fatal(err)
}
defer func() {
err = iface1.Close()
if err != nil {
t.Error(err)
}
err = iface2.Close()
if err != nil {
t.Error(err)
}
}()
err = iface1.UpdatePeer(peer2Key.PublicKey().String(), peer2wgIP, keepAlive, peer2endpoint, nil)
if err != nil {
t.Fatal(err)
}
err = iface2.UpdatePeer(peer1Key.PublicKey().String(), peer1wgIP, keepAlive, peer1endpoint, nil)
if err != nil {
t.Fatal(err)
}
// todo: investigate why in some tests execution we need 30s
timeout := 30 * time.Second
timeoutChannel := time.After(timeout)
for {
select {
case <-timeoutChannel:
t.Fatalf("waiting for peer handshake timeout after %s", timeout.String())
default:
}
peer, gpErr := getPeer(peer1ifaceName, peer2Key.PublicKey().String())
if gpErr != nil {
t.Fatal(gpErr)
}
if !peer.LastHandshakeTime.IsZero() {
t.Log("peers successfully handshake")
break
}
}
}
func Test_ConnectPeers6(t *testing.T) {
if !SupportsIPv6() {
t.Skip("Environment does not support IPv6, skipping IPv6 test...")
}
peer1ifaceName := fmt.Sprintf("utun%d", WgIntNumber+400)
peer1wgIP := "10.99.99.17/30"
peer1wgIP6 := "2001:db8:1234:abcd::47/64"
peer1Key, _ := wgtypes.GeneratePrivateKey()
peer1wgPort := 33100
peer2ifaceName := "utun500"
peer2wgIP := "10.99.99.18/30"
peer2wgIP6 := "2001:db8:1234:abcd::48/64"
peer2Key, _ := wgtypes.GeneratePrivateKey()
peer2wgPort := 33200
keepAlive := 1 * time.Second
newNet, err := stdnet.NewNet()
if err != nil {
t.Fatal(err)
}
iface1, err := NewWGIFace(peer1ifaceName, peer1wgIP, peer1wgIP6, peer1wgPort, peer1Key.String(), DefaultMTU, newNet, nil)
if err != nil {
t.Fatal(err)
}
err = iface1.Create()
if err != nil {
t.Fatal(err)
}
_, err = iface1.Up()
if err != nil {
t.Fatal(err)
}
peer1endpoint, err := net.ResolveUDPAddr("udp", fmt.Sprintf("127.0.0.1:%d", peer1wgPort))
if err != nil {
t.Fatal(err)
}
newNet, err = stdnet.NewNet()
if err != nil {
t.Fatal(err)
}
iface2, err := NewWGIFace(peer2ifaceName, peer2wgIP, peer2wgIP6, peer2wgPort, peer2Key.String(), DefaultMTU, newNet, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -3,13 +3,18 @@ package iface
import ( import (
"fmt" "fmt"
log "github.com/sirupsen/logrus"
"github.com/pion/transport/v3" "github.com/pion/transport/v3"
"github.com/netbirdio/netbird/iface/netstack" "github.com/netbirdio/netbird/iface/netstack"
) )
// NewWGIFace Creates a new WireGuard interface instance // NewWGIFace Creates a new WireGuard interface instance
func NewWGIFace(iFaceName string, address string, wgPort int, wgPrivKey string, mtu int, transportNet transport.Net, args *MobileIFaceArguments) (*WGIface, error) { func NewWGIFace(iFaceName string, address string, address6 string, wgPort int, wgPrivKey string, mtu int, transportNet transport.Net, args *MobileIFaceArguments) (*WGIface, error) {
if address6 != "" {
log.Errorf("Attempted to configure IPv6 address %s on unsupported operating system", address6)
}
wgAddress, err := parseWGAddress(address) wgAddress, err := parseWGAddress(address)
if err != nil { if err != nil {
return nil, err return nil, err
@ -37,3 +42,7 @@ func (w *WGIface) CreateOnAndroid([]string, string, []string) error {
func (w *WGIface) GetInterfaceGUIDString() (string, error) { func (w *WGIface) GetInterfaceGUIDString() (string, error) {
return w.tun.(*tunDevice).getInterfaceGUIDString() return w.tun.(*tunDevice).getInterfaceGUIDString()
} }
func SupportsIPv6() bool {
return false
}

View File

@ -12,6 +12,8 @@ type wgTunDevice interface {
Up() (*bind.UniversalUDPMuxDefault, error) Up() (*bind.UniversalUDPMuxDefault, error)
UpdateAddr(address WGAddress) error UpdateAddr(address WGAddress) error
WgAddress() WGAddress WgAddress() WGAddress
UpdateAddr6(addr6 *WGAddress) error
WgAddress6() *WGAddress
DeviceName() string DeviceName() string
Close() error Close() error
Wrapper() *DeviceWrapper // todo eliminate this function Wrapper() *DeviceWrapper // todo eliminate this function

View File

@ -4,6 +4,7 @@
package iface package iface
import ( import (
"fmt"
"strings" "strings"
"github.com/pion/transport/v3" "github.com/pion/transport/v3"
@ -98,6 +99,13 @@ func (t *wgTunDevice) UpdateAddr(addr WGAddress) error {
return nil return nil
} }
func (t *wgTunDevice) UpdateAddr6(addr6 *WGAddress) error {
if addr6 == nil {
return nil
}
return fmt.Errorf("IPv6 is not supported on this operating system")
}
func (t *wgTunDevice) Close() error { func (t *wgTunDevice) Close() error {
if t.configurer != nil { if t.configurer != nil {
t.configurer.close() t.configurer.close()
@ -127,6 +135,10 @@ func (t *wgTunDevice) WgAddress() WGAddress {
return t.address return t.address
} }
func (t *wgTunDevice) WgAddress6() *WGAddress {
return nil
}
func (t *wgTunDevice) Wrapper() *DeviceWrapper { func (t *wgTunDevice) Wrapper() *DeviceWrapper {
return t.wrapper return t.wrapper
} }

View File

@ -3,6 +3,7 @@
package iface package iface
import ( import (
"fmt"
"os/exec" "os/exec"
"github.com/pion/transport/v3" "github.com/pion/transport/v3"
@ -88,6 +89,13 @@ func (t *tunDevice) UpdateAddr(address WGAddress) error {
return t.assignAddr() return t.assignAddr()
} }
func (t *tunDevice) UpdateAddr6(address6 *WGAddress) error {
if address6 == nil {
return nil
}
return fmt.Errorf("IPv6 is not supported on this operating system")
}
func (t *tunDevice) Close() error { func (t *tunDevice) Close() error {
if t.configurer != nil { if t.configurer != nil {
t.configurer.close() t.configurer.close()
@ -108,6 +116,10 @@ func (t *tunDevice) WgAddress() WGAddress {
return t.address return t.address
} }
func (t *tunDevice) WgAddress6() *WGAddress {
return nil
}
func (t *tunDevice) DeviceName() string { func (t *tunDevice) DeviceName() string {
return t.name return t.name
} }

View File

@ -4,6 +4,7 @@
package iface package iface
import ( import (
"fmt"
"os" "os"
"github.com/pion/transport/v3" "github.com/pion/transport/v3"
@ -123,11 +124,22 @@ func (t *tunDevice) WgAddress() WGAddress {
return t.address return t.address
} }
func (t *tunDevice) WgAddress6() *WGAddress {
return nil
}
func (t *tunDevice) UpdateAddr(addr WGAddress) error { func (t *tunDevice) UpdateAddr(addr WGAddress) error {
// todo implement // todo implement
return nil return nil
} }
func (t *tunDevice) UpdateAddr6(address6 *WGAddress) error {
if address6 == nil {
return nil
}
return fmt.Errorf("IPv6 is not supported on this operating system")
}
func (t *tunDevice) Wrapper() *DeviceWrapper { func (t *tunDevice) Wrapper() *DeviceWrapper {
return t.wrapper return t.wrapper
} }

View File

@ -19,6 +19,7 @@ import (
type tunKernelDevice struct { type tunKernelDevice struct {
name string name string
address WGAddress address WGAddress
address6 *WGAddress
wgPort int wgPort int
key string key string
mtu int mtu int
@ -31,13 +32,14 @@ type tunKernelDevice struct {
udpMux *bind.UniversalUDPMuxDefault udpMux *bind.UniversalUDPMuxDefault
} }
func newTunDevice(name string, address WGAddress, wgPort int, key string, mtu int, transportNet transport.Net) wgTunDevice { func newTunDevice(name string, address WGAddress, address6 *WGAddress, wgPort int, key string, mtu int, transportNet transport.Net) wgTunDevice {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
return &tunKernelDevice{ return &tunKernelDevice{
ctx: ctx, ctx: ctx,
ctxCancel: cancel, ctxCancel: cancel,
name: name, name: name,
address: address, address: address,
address6: address6,
wgPort: wgPort, wgPort: wgPort,
key: key, key: key,
mtu: mtu, mtu: mtu,
@ -136,6 +138,11 @@ func (t *tunKernelDevice) UpdateAddr(address WGAddress) error {
return t.assignAddr() return t.assignAddr()
} }
func (t *tunKernelDevice) UpdateAddr6(address6 *WGAddress) error {
t.address6 = address6
return t.assignAddr()
}
func (t *tunKernelDevice) Close() error { func (t *tunKernelDevice) Close() error {
if t.link == nil { if t.link == nil {
return nil return nil
@ -168,6 +175,10 @@ func (t *tunKernelDevice) WgAddress() WGAddress {
return t.address return t.address
} }
func (t *tunKernelDevice) WgAddress6() *WGAddress {
return t.address6
}
func (t *tunKernelDevice) DeviceName() string { func (t *tunKernelDevice) DeviceName() string {
return t.name return t.name
} }
@ -203,6 +214,19 @@ func (t *tunKernelDevice) assignAddr() error {
} else if err != nil { } else if err != nil {
return err return err
} }
// Configure the optional additional IPv6 address if available.
if t.address6 != nil {
log.Debugf("adding IPv6 address %s to interface: %s", t.address6.String(), t.name)
addr6, _ := netlink.ParseAddr(t.address6.String())
err = netlink.AddrAdd(link, addr6)
if os.IsExist(err) {
log.Infof("interface %s already has the address: %s", t.name, t.address.String())
} else if err != nil {
return err
}
}
// On linux, the link must be brought up // On linux, the link must be brought up
err = netlink.LinkSetUp(link) err = netlink.LinkSetUp(link)
return err return err

View File

@ -91,6 +91,13 @@ func (t *tunNetstackDevice) UpdateAddr(WGAddress) error {
return nil return nil
} }
func (t *tunNetstackDevice) UpdateAddr6(address6 *WGAddress) error {
if address6 == nil {
return nil
}
return fmt.Errorf("IPv6 is not supported on this operating system")
}
func (t *tunNetstackDevice) Close() error { func (t *tunNetstackDevice) Close() error {
if t.configurer != nil { if t.configurer != nil {
t.configurer.close() t.configurer.close()
@ -110,6 +117,10 @@ func (t *tunNetstackDevice) WgAddress() WGAddress {
return t.address return t.address
} }
func (t *tunNetstackDevice) WgAddress6() *WGAddress {
return nil
}
func (t *tunNetstackDevice) DeviceName() string { func (t *tunNetstackDevice) DeviceName() string {
return t.name return t.name
} }

View File

@ -98,6 +98,13 @@ func (t *tunUSPDevice) UpdateAddr(address WGAddress) error {
return t.assignAddr() return t.assignAddr()
} }
func (t *tunUSPDevice) UpdateAddr6(address6 *WGAddress) error {
if address6 == nil {
return nil
}
return fmt.Errorf("IPv6 is not supported on this operating system")
}
func (t *tunUSPDevice) Close() error { func (t *tunUSPDevice) Close() error {
if t.configurer != nil { if t.configurer != nil {
t.configurer.close() t.configurer.close()
@ -117,6 +124,10 @@ func (t *tunUSPDevice) WgAddress() WGAddress {
return t.address return t.address
} }
func (t *tunUSPDevice) WgAddress6() *WGAddress {
return nil
}
func (t *tunUSPDevice) DeviceName() string { func (t *tunUSPDevice) DeviceName() string {
return t.name return t.name
} }

View File

@ -106,6 +106,13 @@ func (t *tunDevice) UpdateAddr(address WGAddress) error {
return t.assignAddr() return t.assignAddr()
} }
func (t *tunDevice) UpdateAddr6(address6 *WGAddress) error {
if address6 == nil {
return nil
}
return fmt.Errorf("IPv6 is not supported on this operating system")
}
func (t *tunDevice) Close() error { func (t *tunDevice) Close() error {
if t.configurer != nil { if t.configurer != nil {
t.configurer.close() t.configurer.close()
@ -126,6 +133,10 @@ func (t *tunDevice) WgAddress() WGAddress {
return t.address return t.address
} }
func (t *tunDevice) WgAddress6() *WGAddress {
return nil
}
func (t *tunDevice) DeviceName() string { func (t *tunDevice) DeviceName() string {
return t.name return t.name
} }

View File

@ -5,6 +5,7 @@ package iface
import ( import (
"fmt" "fmt"
"net" "net"
"strings"
"time" "time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -46,9 +47,13 @@ func (c *wgKernelConfigurer) configureInterface(privateKey string, port int) err
func (c *wgKernelConfigurer) updatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error { func (c *wgKernelConfigurer) updatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error {
// parse allowed ips // parse allowed ips
_, ipNet, err := net.ParseCIDR(allowedIps) var allowedIpNets []net.IPNet
if err != nil { for _, allowedIp := range strings.Split(allowedIps, ",") {
return err _, ipNet, err := net.ParseCIDR(allowedIp)
allowedIpNets = append(allowedIpNets, *ipNet)
if err != nil {
return err
}
} }
peerKeyParsed, err := wgtypes.ParseKey(peerKey) peerKeyParsed, err := wgtypes.ParseKey(peerKey)
@ -58,7 +63,7 @@ func (c *wgKernelConfigurer) updatePeer(peerKey string, allowedIps string, keepA
peer := wgtypes.PeerConfig{ peer := wgtypes.PeerConfig{
PublicKey: peerKeyParsed, PublicKey: peerKeyParsed,
ReplaceAllowedIPs: true, ReplaceAllowedIPs: true,
AllowedIPs: []net.IPNet{*ipNet}, AllowedIPs: allowedIpNets,
PersistentKeepaliveInterval: &keepAlive, PersistentKeepaliveInterval: &keepAlive,
Endpoint: endpoint, Endpoint: endpoint,
PresharedKey: preSharedKey, PresharedKey: preSharedKey,

View File

@ -379,6 +379,7 @@ func Test_SystemMetaDataFromClient(t *testing.T) {
SysProductName: info.SystemProductName, SysProductName: info.SystemProductName,
SysManufacturer: info.SystemManufacturer, SysManufacturer: info.SystemManufacturer,
Environment: &mgmtProto.Environment{Cloud: info.Environment.Cloud, Platform: info.Environment.Platform}, Environment: &mgmtProto.Environment{Cloud: info.Environment.Cloud, Platform: info.Environment.Platform},
Ipv6Supported: info.Ipv6Supported,
} }
assert.Equal(t, ValidKey, actualValidKey) assert.Equal(t, ValidKey, actualValidKey)

View File

@ -483,5 +483,6 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta {
Cloud: info.Environment.Cloud, Cloud: info.Environment.Cloud,
Platform: info.Environment.Platform, Platform: info.Environment.Platform,
}, },
Ipv6Supported: info.Ipv6Supported,
} }
} }

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.26.0 // protoc-gen-go v1.26.0
// protoc v4.24.3 // protoc v4.25.2
// source: management.proto // source: management.proto
package proto package proto
@ -668,6 +668,7 @@ type PeerSystemMeta struct {
SysProductName string `protobuf:"bytes,13,opt,name=sysProductName,proto3" json:"sysProductName,omitempty"` SysProductName string `protobuf:"bytes,13,opt,name=sysProductName,proto3" json:"sysProductName,omitempty"`
SysManufacturer string `protobuf:"bytes,14,opt,name=sysManufacturer,proto3" json:"sysManufacturer,omitempty"` SysManufacturer string `protobuf:"bytes,14,opt,name=sysManufacturer,proto3" json:"sysManufacturer,omitempty"`
Environment *Environment `protobuf:"bytes,15,opt,name=environment,proto3" json:"environment,omitempty"` Environment *Environment `protobuf:"bytes,15,opt,name=environment,proto3" json:"environment,omitempty"`
Ipv6Supported bool `protobuf:"varint,16,opt,name=ipv6Supported,proto3" json:"ipv6Supported,omitempty"`
} }
func (x *PeerSystemMeta) Reset() { func (x *PeerSystemMeta) Reset() {
@ -807,6 +808,13 @@ func (x *PeerSystemMeta) GetEnvironment() *Environment {
return nil return nil
} }
func (x *PeerSystemMeta) GetIpv6Supported() bool {
if x != nil {
return x.Ipv6Supported
}
return false
}
type LoginResponse struct { type LoginResponse struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@ -1172,6 +1180,8 @@ type PeerConfig struct {
SshConfig *SSHConfig `protobuf:"bytes,3,opt,name=sshConfig,proto3" json:"sshConfig,omitempty"` SshConfig *SSHConfig `protobuf:"bytes,3,opt,name=sshConfig,proto3" json:"sshConfig,omitempty"`
// Peer fully qualified domain name // Peer fully qualified domain name
Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"` Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"`
// Peer's virtual IPv6 address within the Wiretrustee VPN (a Wireguard address config)
Address6 string `protobuf:"bytes,5,opt,name=address6,proto3" json:"address6,omitempty"`
} }
func (x *PeerConfig) Reset() { func (x *PeerConfig) Reset() {
@ -1234,6 +1244,13 @@ func (x *PeerConfig) GetFqdn() string {
return "" return ""
} }
func (x *PeerConfig) GetAddress6() string {
if x != nil {
return x.Address6
}
return ""
}
// NetworkMap represents a network state of the peer with the corresponding configuration parameters to establish peer-to-peer connections // NetworkMap represents a network state of the peer with the corresponding configuration parameters to establish peer-to-peer connections
type NetworkMap struct { type NetworkMap struct {
state protoimpl.MessageState state protoimpl.MessageState
@ -2254,6 +2271,7 @@ type FirewallRule struct {
Action FirewallRuleAction `protobuf:"varint,3,opt,name=Action,proto3,enum=management.FirewallRuleAction" json:"Action,omitempty"` Action FirewallRuleAction `protobuf:"varint,3,opt,name=Action,proto3,enum=management.FirewallRuleAction" json:"Action,omitempty"`
Protocol FirewallRuleProtocol `protobuf:"varint,4,opt,name=Protocol,proto3,enum=management.FirewallRuleProtocol" json:"Protocol,omitempty"` Protocol FirewallRuleProtocol `protobuf:"varint,4,opt,name=Protocol,proto3,enum=management.FirewallRuleProtocol" json:"Protocol,omitempty"`
Port string `protobuf:"bytes,5,opt,name=Port,proto3" json:"Port,omitempty"` Port string `protobuf:"bytes,5,opt,name=Port,proto3" json:"Port,omitempty"`
PeerIP6 string `protobuf:"bytes,6,opt,name=PeerIP6,proto3" json:"PeerIP6,omitempty"`
} }
func (x *FirewallRule) Reset() { func (x *FirewallRule) Reset() {
@ -2323,6 +2341,13 @@ func (x *FirewallRule) GetPort() string {
return "" return ""
} }
func (x *FirewallRule) GetPeerIP6() string {
if x != nil {
return x.PeerIP6
}
return ""
}
type NetworkAddress struct { type NetworkAddress struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@ -2430,7 +2455,7 @@ var file_management_proto_rawDesc = []byte{
0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6c, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6c,
0x6f, 0x75, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x6f, 0x75, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x75, 0x64,
0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0xa9, 0x04, 0x0a, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x22, 0xcf, 0x04, 0x0a,
0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x12,
0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x67,
@ -2465,260 +2490,266 @@ var file_management_proto_rawDesc = []byte{
0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e, 0x76,
0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x94, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x70, 0x76, 0x36,
0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x11, 0x77, 0x69, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x52,
0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x0d, 0x69, 0x70, 0x76, 0x36, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x22, 0x94,
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x6e, 0x74, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x12, 0x4b, 0x0a, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43,
0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61,
0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75,
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x77, 0x69, 0x72, 0x65,
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a,
0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28,
0x79, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43,
0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b,
0x73, 0x41, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70,
0x70, 0x74, 0x79, 0x22, 0xa8, 0x01, 0x0a, 0x11, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xa8, 0x01, 0x0a, 0x11, 0x57, 0x69,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16,
0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73,
0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e,
0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16,
0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74,
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x22, 0x98, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a,
0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d,
0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63,
0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74,
0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03,
0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63,
0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, 0x08,
0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10,
0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54,
0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x03, 0x12,
0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f,
0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69,
0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x67, 0x6e, 0x61, 0x6c, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f,
0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x81, 0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e,
0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63,
0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07,
0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01,
0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54,
0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x54, 0x50, 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22,
0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74,
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0xe2, 0x03, 0x0a, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f,
0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e,
0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66,
0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12,
0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73,
0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03,
0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x9d,
0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a,
0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02,
0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68,
0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d,
0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e,
0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12,
0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71,
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x64, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x36, 0x18, 0x05,
0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x36, 0x22, 0xe2,
0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x03, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a,
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53,
0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e,
0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a,
0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03,
0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a,
0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d,
0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a,
0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e,
0x79, 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65,
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43,
0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61,
0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66,
0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a,
0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20,
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12,
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73,
0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65,
0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12,
0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73,
0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66,
0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d,
0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x70, 0x74, 0x79, 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65,
0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75,
0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49,
0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65,
0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64,
0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a,
0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73,
0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a,
0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73,
0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73,
0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69,
0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46,
0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44,
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69,
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64,
0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68,
0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72,
0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66,
0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f,
0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f,
0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c,
0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f,
0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15,
0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f,
0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65,
0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69,
0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69,
0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72,
0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08,
0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65,
0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c,
0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06,
0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f,
0x55, 0x52, 0x4c, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65,
0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65,
0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e,
0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65,
0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e,
0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18,
0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a,
0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08,
0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15,
0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x22, 0xb4, 0x01, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64,
0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74,
0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69,
0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52,
0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65,
0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65,
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44,
0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28,
0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65,
0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52,
0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04,
0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72,
0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03,
0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71,
0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61,
0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49,
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x22, 0xb4,
0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d,
0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20,
0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62,
0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d,
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54,
0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a,
0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44,
0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76,
0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53,
0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65,
0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53,
0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43,
0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b,
0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75,
0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d,
0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a,
0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20,
0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52,
0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d,
0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22,
0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12,
0x6f, 0x72, 0x74, 0x22, 0xf0, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e,
0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x40, 0x0a, 0x09, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73,
0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a,
0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12,
0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65,
0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d,
0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16,
0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65,
0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76,
0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02,
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a,
0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07,
0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18,
0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d,
0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x22, 0x1e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e,
0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18,
0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x22, 0x3c, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54,
0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70,
0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52,
0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x8a, 0x03, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61,
0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50,
0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x40,
0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46,
0x32, 0xd1, 0x03, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x64, 0x69, 0x72, 0x65,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x12, 0x37, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e,
0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69,
0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x08, 0x50, 0x72, 0x6f,
0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x6d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c,
0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08,
0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74,
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x18, 0x0a, 0x07,
0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x36, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x50,
0x65, 0x65, 0x72, 0x49, 0x50, 0x36, 0x22, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f,
0x55, 0x54, 0x10, 0x01, 0x22, 0x1e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a,
0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52,
0x4f, 0x50, 0x10, 0x01, 0x22, 0x3c, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a,
0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12,
0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50,
0x10, 0x04, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64,
0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61,
0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x32, 0xd1, 0x03, 0x0a,
0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e,
0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a,
0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63,
0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30,
0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65,
0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45,
0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74,
0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65,
0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d,
0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a,
0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f,
0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70,
0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e,
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65,
0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73,
0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43,
0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c,
0x67, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e,
0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00,
0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
} }
var ( var (

View File

@ -117,6 +117,7 @@ message PeerSystemMeta {
string sysProductName = 13; string sysProductName = 13;
string sysManufacturer = 14; string sysManufacturer = 14;
Environment environment = 15; Environment environment = 15;
bool ipv6Supported = 16;
} }
message LoginResponse { message LoginResponse {
@ -182,6 +183,8 @@ message PeerConfig {
SSHConfig sshConfig = 3; SSHConfig sshConfig = 3;
// Peer fully qualified domain name // Peer fully qualified domain name
string fqdn = 4; string fqdn = 4;
// Peer's virtual IPv6 address within the Wiretrustee VPN (a Wireguard address config)
string address6 = 5;
} }
// NetworkMap represents a network state of the peer with the corresponding configuration parameters to establish peer-to-peer connections // NetworkMap represents a network state of the peer with the corresponding configuration parameters to establish peer-to-peer connections
@ -349,6 +352,7 @@ message FirewallRule {
action Action = 3; action Action = 3;
protocol Protocol = 4; protocol Protocol = 4;
string Port = 5; string Port = 5;
string PeerIP6 = 6;
enum direction { enum direction {
IN = 0; IN = 0;

View File

@ -280,7 +280,8 @@ func (a *Account) getRoutesToSync(peerID string, aclPeers []*nbpeer.Peer) []*rou
groupListMap := a.getPeerGroups(peerID) groupListMap := a.getPeerGroups(peerID)
for _, peer := range aclPeers { for _, peer := range aclPeers {
activeRoutes, _ := a.getRoutingPeerRoutes(peer.ID) activeRoutes, _ := a.getRoutingPeerRoutes(peer.ID)
groupFilteredRoutes := a.filterRoutesByGroups(activeRoutes, groupListMap) addressFamilyFilteredRoutes := a.filterRoutesByIPv6Enabled(activeRoutes, a.GetPeer(peerID).IP6 != nil && peer.IP6 != nil)
groupFilteredRoutes := a.filterRoutesByGroups(addressFamilyFilteredRoutes, groupListMap)
filteredRoutes := a.filterRoutesFromPeersOfSameHAGroup(groupFilteredRoutes, peerRoutesMembership) filteredRoutes := a.filterRoutesFromPeersOfSameHAGroup(groupFilteredRoutes, peerRoutesMembership)
routes = append(routes, filteredRoutes...) routes = append(routes, filteredRoutes...)
} }
@ -315,6 +316,20 @@ func (a *Account) filterRoutesByGroups(routes []*route.Route, groupListMap looku
return filteredRoutes return filteredRoutes
} }
// Filters out IPv6 routes if the peer does not support them.
func (a *Account) filterRoutesByIPv6Enabled(routes []*route.Route, v6Supported bool) []*route.Route {
if v6Supported {
return routes
}
var filteredRoutes []*route.Route
for _, rt := range routes {
if rt.Network.Addr().Is4() {
filteredRoutes = append(filteredRoutes, rt)
}
}
return filteredRoutes
}
// getRoutingPeerRoutes returns the enabled and disabled lists of routes that the given routing peer serves // getRoutingPeerRoutes returns the enabled and disabled lists of routes that the given routing peer serves
// Please mind, that the returned route.Route objects will contain Peer.Key instead of Peer.ID. // Please mind, that the returned route.Route objects will contain Peer.Key instead of Peer.ID.
// If the given is not a routing peer, then the lists are empty. // If the given is not a routing peer, then the lists are empty.
@ -348,6 +363,10 @@ func (a *Account) getRoutingPeerRoutes(peerID string) (enabledRoutes []*route.Ro
} }
for _, r := range a.Routes { for _, r := range a.Routes {
// Skip IPv6 routes if IPv6 is currently not enabled.
if peer.IP6 == nil && r.NetworkType == route.IPv6Network {
continue
}
for _, groupID := range r.PeerGroups { for _, groupID := range r.PeerGroups {
group := a.GetGroup(groupID) group := a.GetGroup(groupID)
if group == nil { if group == nil {
@ -429,7 +448,7 @@ func (a *Account) GetPeerNetworkMap(peerID, dnsDomain string, validatedPeersMap
if dnsManagementStatus { if dnsManagementStatus {
var zones []nbdns.CustomZone var zones []nbdns.CustomZone
peersCustomZone := getPeersCustomZone(a, dnsDomain) peersCustomZone := getPeersCustomZone(a, dnsDomain, peer.IP6 != nil)
if peersCustomZone.Domain != "" { if peersCustomZone.Domain != "" {
zones = append(zones, peersCustomZone) zones = append(zones, peersCustomZone)
} }
@ -665,6 +684,17 @@ func (a *Account) getTakenIPs() []net.IP {
return takenIps return takenIps
} }
func (a *Account) getTakenIP6s() []net.IP {
var takenIps []net.IP
for _, existingPeer := range a.Peers {
if existingPeer.IP6 != nil {
takenIps = append(takenIps, *existingPeer.IP6)
}
}
return takenIps
}
func (a *Account) getPeerDNSLabels() lookupMap { func (a *Account) getPeerDNSLabels() lookupMap {
existingLabels := make(lookupMap) existingLabels := make(lookupMap)
for _, peer := range a.Peers { for _, peer := range a.Peers {
@ -1006,7 +1036,6 @@ func (am *DefaultAccountManager) UpdateAccountSettings(accountID, userID string,
} }
updatedAccount := account.UpdateSettings(newSettings) updatedAccount := account.UpdateSettings(newSettings)
err = am.Store.SaveAccount(account) err = am.Store.SaveAccount(account)
if err != nil { if err != nil {
return nil, err return nil, err
@ -2015,7 +2044,7 @@ func addAllGroup(account *Account) error {
func newAccountWithId(accountID, userID, domain string) *Account { func newAccountWithId(accountID, userID, domain string) *Account {
log.Debugf("creating new account") log.Debugf("creating new account")
network := NewNetwork() network := NewNetwork(true)
peers := make(map[string]*nbpeer.Peer) peers := make(map[string]*nbpeer.Peer)
users := make(map[string]*User) users := make(map[string]*User)
routes := make(map[route.ID]*route.Route) routes := make(map[route.ID]*route.Route)

View File

@ -69,14 +69,15 @@ func verifyCanAddPeerToAccount(t *testing.T, manager AccountManager, account *Ac
Key: "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8=", Key: "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8=",
Name: "test-host@netbird.io", Name: "test-host@netbird.io",
Meta: nbpeer.PeerSystemMeta{ Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host@netbird.io", Hostname: "test-host@netbird.io",
GoOS: "linux", GoOS: "linux",
Kernel: "Linux", Kernel: "Linux",
Core: "21.04", Core: "21.04",
Platform: "x86_64", Platform: "x86_64",
OS: "Ubuntu", OS: "Ubuntu",
WtVersion: "development", WtVersion: "development",
UIVersion: "development", UIVersion: "development",
Ipv6Supported: false,
}, },
} }
@ -964,78 +965,136 @@ func TestAccountManager_DeleteAccount(t *testing.T) {
} }
func TestAccountManager_AddPeer(t *testing.T) { func TestAccountManager_AddPeer(t *testing.T) {
manager, err := createManager(t)
if err != nil { testCases := []struct {
t.Fatal(err) name string
return peerIPv6Supported bool
allGroupIPv6Enabled bool
}{
{
name: "Peer and Group IPv6 enabled",
peerIPv6Supported: true,
allGroupIPv6Enabled: true,
},
{
name: "Peer IPv6 enabled, Group IPv6 disabled",
peerIPv6Supported: true,
allGroupIPv6Enabled: false,
},
{
name: "Peer IPv6 disabled, Group IPv6 enabled",
peerIPv6Supported: false,
allGroupIPv6Enabled: true,
},
{
name: "Peer and Group IPv6 disabled",
peerIPv6Supported: false,
allGroupIPv6Enabled: false,
},
} }
userID := "testingUser" for _, c := range testCases {
account, err := createAccount(manager, "test_account", userID, "netbird.cloud") t.Run(c.name, func(t *testing.T) {
if err != nil { manager, err := createManager(t)
t.Fatal(err) if err != nil {
t.Fatal(err)
return
}
userID := "testingUser"
account, err := createAccount(manager, "test_account", userID, "netbird.cloud")
if err != nil {
t.Fatal(err)
}
serial := account.Network.CurrentSerial() // should be 0
setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, userID, false)
if err != nil {
t.Fatal("error creating setup key")
return
}
if account.Network.Serial != 0 {
t.Errorf("expecting account network to have an initial Serial=0")
return
}
account, err = manager.Store.GetAccount(account.Id)
if err != nil {
t.Fatal(err)
return
}
if c.allGroupIPv6Enabled {
unlock := manager.Store.AcquireAccountWriteLock(account.Id)
groupAll, err := account.GetGroupAll()
if err != nil {
t.Fatal(err)
}
groupAll.IPv6Enabled = true
err = manager.Store.SaveAccount(account)
if err != nil {
t.Fatal(err)
}
unlock()
}
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
t.Fatal(err)
return
}
expectedPeerKey := key.PublicKey().String()
expectedSetupKey := setupKey.Key
peer, _, err := manager.AddPeer(setupKey.Key, "", &nbpeer.Peer{
Key: expectedPeerKey,
Meta: nbpeer.PeerSystemMeta{Hostname: expectedPeerKey, Ipv6Supported: c.peerIPv6Supported},
})
if err != nil {
t.Errorf("expecting peer to be added, got failure %v", err)
return
}
account, err = manager.Store.GetAccount(account.Id)
if err != nil {
t.Fatal(err)
return
}
if peer.Key != expectedPeerKey {
t.Errorf("expecting just added peer to have key = %s, got %s", expectedPeerKey, peer.Key)
}
if !account.Network.Net.Contains(peer.IP) {
t.Errorf("expecting just added peer's IP %s to be in a network range %s", peer.IP.String(), account.Network.Net.String())
}
if peer.SetupKey != expectedSetupKey {
t.Errorf("expecting just added peer to have SetupKey = %s, got %s", expectedSetupKey, peer.SetupKey)
}
if account.Network.CurrentSerial() != 1 {
t.Errorf("expecting Network Serial=%d to be incremented by 1 and be equal to %d when adding new peer to account", serial, account.Network.CurrentSerial())
}
ev := getEvent(t, account.Id, manager, activity.PeerAddedWithSetupKey)
assert.NotNil(t, ev)
assert.Equal(t, account.Id, ev.AccountID)
assert.Equal(t, peer.Name, ev.Meta["name"])
assert.Equal(t, peer.FQDN(account.Domain), ev.Meta["fqdn"])
assert.Equal(t, setupKey.Id, ev.InitiatorID)
assert.Equal(t, peer.ID, ev.TargetID)
assert.Equal(t, peer.IP.String(), fmt.Sprint(ev.Meta["ip"]))
assert.Equal(t, peer.V6Setting, nbpeer.V6Auto)
if c.peerIPv6Supported && c.allGroupIPv6Enabled {
assert.NotNil(t, peer.IP6)
} else {
assert.Nil(t, peer.IP6)
}
})
} }
serial := account.Network.CurrentSerial() // should be 0
setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, userID, false)
if err != nil {
t.Fatal("error creating setup key")
return
}
if account.Network.Serial != 0 {
t.Errorf("expecting account network to have an initial Serial=0")
return
}
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
t.Fatal(err)
return
}
expectedPeerKey := key.PublicKey().String()
expectedSetupKey := setupKey.Key
peer, _, err := manager.AddPeer(setupKey.Key, "", &nbpeer.Peer{
Key: expectedPeerKey,
Meta: nbpeer.PeerSystemMeta{Hostname: expectedPeerKey},
})
if err != nil {
t.Errorf("expecting peer to be added, got failure %v", err)
return
}
account, err = manager.Store.GetAccount(account.Id)
if err != nil {
t.Fatal(err)
return
}
if peer.Key != expectedPeerKey {
t.Errorf("expecting just added peer to have key = %s, got %s", expectedPeerKey, peer.Key)
}
if !account.Network.Net.Contains(peer.IP) {
t.Errorf("expecting just added peer's IP %s to be in a network range %s", peer.IP.String(), account.Network.Net.String())
}
if peer.SetupKey != expectedSetupKey {
t.Errorf("expecting just added peer to have SetupKey = %s, got %s", expectedSetupKey, peer.SetupKey)
}
if account.Network.CurrentSerial() != 1 {
t.Errorf("expecting Network Serial=%d to be incremented by 1 and be equal to %d when adding new peer to account", serial, account.Network.CurrentSerial())
}
ev := getEvent(t, account.Id, manager, activity.PeerAddedWithSetupKey)
assert.NotNil(t, ev)
assert.Equal(t, account.Id, ev.AccountID)
assert.Equal(t, peer.Name, ev.Meta["name"])
assert.Equal(t, peer.FQDN(account.Domain), ev.Meta["fqdn"])
assert.Equal(t, setupKey.Id, ev.InitiatorID)
assert.Equal(t, peer.ID, ev.TargetID)
assert.Equal(t, peer.IP.String(), fmt.Sprint(ev.Meta["ip"]))
} }
func TestAccountManager_AddPeerWithUserID(t *testing.T) { func TestAccountManager_AddPeerWithUserID(t *testing.T) {
@ -1455,9 +1514,22 @@ func TestAccount_GetRoutesToSync(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
_, prefix3, err := route.ParseNetwork("2001:db8:1234:5678::/64")
if err != nil {
t.Fatal(err)
}
_, prefix4, err := route.ParseNetwork("2001:db8:1234:6789::/64")
if err != nil {
t.Fatal(err)
}
peer2IP6 := net.ParseIP("2001:db8:abcd:1234::12")
account := &Account{ account := &Account{
Peers: map[string]*nbpeer.Peer{ Peers: map[string]*nbpeer.Peer{
"peer-1": {Key: "peer-1", Meta: nbpeer.PeerSystemMeta{GoOS: "linux"}}, "peer-2": {Key: "peer-2", Meta: nbpeer.PeerSystemMeta{GoOS: "linux"}}, "peer-3": {Key: "peer-1", Meta: nbpeer.PeerSystemMeta{GoOS: "linux"}}, "peer-1": {
ID: "peer-1", Key: "peer-1", Meta: nbpeer.PeerSystemMeta{GoOS: "linux"},
},
"peer-2": {ID: "peer-2", Key: "peer-2", Meta: nbpeer.PeerSystemMeta{GoOS: "linux", Ipv6Supported: true}, IP6: &peer2IP6},
"peer-3": {ID: "peer-3", Key: "peer-1", Meta: nbpeer.PeerSystemMeta{GoOS: "linux"}},
}, },
Groups: map[string]*group.Group{"group1": {ID: "group1", Peers: []string{"peer-1", "peer-2"}}}, Groups: map[string]*group.Group{"group1": {ID: "group1", Peers: []string{"peer-1", "peer-2"}}},
Routes: map[route.ID]*route.Route{ Routes: map[route.ID]*route.Route{
@ -1467,7 +1539,7 @@ func TestAccount_GetRoutesToSync(t *testing.T) {
NetID: "network-1", NetID: "network-1",
Description: "network-1", Description: "network-1",
Peer: "peer-1", Peer: "peer-1",
NetworkType: 0, NetworkType: route.IPv4Network,
Masquerade: false, Masquerade: false,
Metric: 999, Metric: 999,
Enabled: true, Enabled: true,
@ -1479,7 +1551,7 @@ func TestAccount_GetRoutesToSync(t *testing.T) {
NetID: "network-2", NetID: "network-2",
Description: "network-2", Description: "network-2",
Peer: "peer-2", Peer: "peer-2",
NetworkType: 0, NetworkType: route.IPv4Network,
Masquerade: false, Masquerade: false,
Metric: 999, Metric: 999,
Enabled: true, Enabled: true,
@ -1491,7 +1563,31 @@ func TestAccount_GetRoutesToSync(t *testing.T) {
NetID: "network-1", NetID: "network-1",
Description: "network-1", Description: "network-1",
Peer: "peer-2", Peer: "peer-2",
NetworkType: 0, NetworkType: route.IPv4Network,
Masquerade: false,
Metric: 999,
Enabled: true,
Groups: []string{"group1"},
},
"route-4": {
ID: "route-4",
Network: prefix3,
NetID: "network-3",
Description: "network-3",
Peer: "peer-1",
NetworkType: route.IPv6Network,
Masquerade: false,
Metric: 999,
Enabled: true,
Groups: []string{"group1"},
},
"route-5": {
ID: "route-5",
Network: prefix4,
NetID: "network-4",
Description: "network-4",
Peer: "peer-2",
NetworkType: route.IPv6Network,
Masquerade: false, Masquerade: false,
Metric: 999, Metric: 999,
Enabled: true, Enabled: true,
@ -1500,17 +1596,29 @@ func TestAccount_GetRoutesToSync(t *testing.T) {
}, },
} }
routes := account.getRoutesToSync("peer-2", []*nbpeer.Peer{{Key: "peer-1"}, {Key: "peer-3"}}) routes := account.getRoutesToSync("peer-1", []*nbpeer.Peer{{ID: "peer-2", Key: "peer-2"}, {ID: "peer-3", Key: "peer-3"}})
assert.Len(t, routes, 2) assert.Len(t, routes, 2)
routeIDs := make(map[route.ID]struct{}, 2) routeIDs := make(map[route.ID]struct{}, 2)
for _, r := range routes { for _, r := range routes {
routeIDs[r.ID] = struct{}{} routeIDs[r.ID] = struct{}{}
} }
assert.Contains(t, routeIDs, route.ID("route-1"))
assert.Contains(t, routeIDs, route.ID("route-2"))
routes = account.getRoutesToSync("peer-2", []*nbpeer.Peer{{ID: "peer-1", Key: "peer-1"}, {ID: "peer-3", Key: "peer-3"}})
assert.Len(t, routes, 3)
routeIDs = make(map[route.ID]struct{}, 2)
for _, r := range routes {
routeIDs[r.ID] = struct{}{}
}
assert.Contains(t, routeIDs, route.ID("route-2")) assert.Contains(t, routeIDs, route.ID("route-2"))
assert.Contains(t, routeIDs, route.ID("route-3")) assert.Contains(t, routeIDs, route.ID("route-3"))
assert.Contains(t, routeIDs, route.ID("route-5"))
emptyRoutes := account.getRoutesToSync("peer-3", []*nbpeer.Peer{{Key: "peer-1"}, {Key: "peer-2"}}) emptyRoutes := account.getRoutesToSync("peer-3", []*nbpeer.Peer{{ID: "peer-1", Key: "peer-1"}, {ID: "peer-2", Key: "peer-2"}})
assert.Len(t, emptyRoutes, 0) assert.Len(t, emptyRoutes, 0)
} }

View File

@ -139,6 +139,14 @@ const (
PostureCheckUpdated Activity = 61 PostureCheckUpdated Activity = 61
// PostureCheckDeleted indicates that the user deleted a posture check // PostureCheckDeleted indicates that the user deleted a posture check
PostureCheckDeleted Activity = 62 PostureCheckDeleted Activity = 62
// PeerIPv6Enabled indicates that a user enabled IPv6 for a peer
PeerIPv6Enabled Activity = 63
// PeerIPv6Disabled indicates that a user disabled IPv6 for a peer
PeerIPv6Disabled Activity = 64
// PeerIPv6InheritEnabled indicates that IPv6 was enabled for a peer due to a change in group memberships.
PeerIPv6InheritEnabled Activity = 65
// PeerIPv6InheritDisabled indicates that IPv6 was disabled for a peer due to a change in group memberships.
PeerIPv6InheritDisabled Activity = 66
) )
var activityMap = map[Activity]Code{ var activityMap = map[Activity]Code{
@ -205,6 +213,10 @@ var activityMap = map[Activity]Code{
PostureCheckCreated: {"Posture check created", "posture.check.created"}, PostureCheckCreated: {"Posture check created", "posture.check.created"},
PostureCheckUpdated: {"Posture check updated", "posture.check.updated"}, PostureCheckUpdated: {"Posture check updated", "posture.check.updated"},
PostureCheckDeleted: {"Posture check deleted", "posture.check.deleted"}, PostureCheckDeleted: {"Posture check deleted", "posture.check.deleted"},
PeerIPv6Enabled: {"Peer IPv6 enabled by user", "peer.ipv6.manual_enable"},
PeerIPv6Disabled: {"Peer IPv6 disabled by user", "peer.ipv6.manual_disable"},
PeerIPv6InheritDisabled: {"Peer IPv6 disabled due to change in group settings or membership", "peer.ipv6.inherit_disable"},
PeerIPv6InheritEnabled: {"Peer IPv6 enabled due to change in group settings or membership", "peer.ipv6.inherit_enable"},
} }
// StringCode returns a string code of the activity // StringCode returns a string code of the activity

View File

@ -149,7 +149,7 @@ func toProtocolDNSConfig(update nbdns.Config) *proto.DNSConfig {
return protoUpdate return protoUpdate
} }
func getPeersCustomZone(account *Account, dnsDomain string) nbdns.CustomZone { func getPeersCustomZone(account *Account, dnsDomain string, enableIPv6 bool) nbdns.CustomZone {
if dnsDomain == "" { if dnsDomain == "" {
log.Errorf("no dns domain is set, returning empty zone") log.Errorf("no dns domain is set, returning empty zone")
return nbdns.CustomZone{} return nbdns.CustomZone{}
@ -172,6 +172,16 @@ func getPeersCustomZone(account *Account, dnsDomain string) nbdns.CustomZone {
TTL: defaultTTL, TTL: defaultTTL,
RData: peer.IP.String(), RData: peer.IP.String(),
}) })
if peer.IP6 != nil && enableIPv6 {
customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
Name: dns.Fqdn(peer.DNSLabel + "." + dnsDomain),
Type: int(dns.TypeAAAA),
Class: nbdns.DefaultClass,
TTL: defaultTTL,
RData: peer.IP6.String(),
})
}
} }
return customZone return customZone
@ -179,6 +189,7 @@ func getPeersCustomZone(account *Account, dnsDomain string) nbdns.CustomZone {
func getPeerNSGroups(account *Account, peerID string) []*nbdns.NameServerGroup { func getPeerNSGroups(account *Account, peerID string) []*nbdns.NameServerGroup {
groupList := account.getPeerGroups(peerID) groupList := account.getPeerGroups(peerID)
peer := account.GetPeer(peerID)
var peerNSGroups []*nbdns.NameServerGroup var peerNSGroups []*nbdns.NameServerGroup
@ -189,8 +200,18 @@ func getPeerNSGroups(account *Account, peerID string) []*nbdns.NameServerGroup {
for _, gID := range nsGroup.Groups { for _, gID := range nsGroup.Groups {
_, found := groupList[gID] _, found := groupList[gID]
if found { if found {
if !peerIsNameserver(account.GetPeer(peerID), nsGroup) { if !peerIsNameserver(peer, nsGroup) {
peerNSGroups = append(peerNSGroups, nsGroup.Copy()) filteredNsGroup := nsGroup.Copy()
var newNameserverList []nbdns.NameServer
for _, nameserver := range filteredNsGroup.NameServers {
if nameserver.IP.Is4() || peer.IP6 != nil {
newNameserverList = append(newNameserverList, nameserver)
}
}
if len(newNameserverList) > 0 {
filteredNsGroup.NameServers = newNameserverList
peerNSGroups = append(peerNSGroups, filteredNsGroup)
}
break break
} }
} }

View File

@ -22,6 +22,7 @@ const (
dnsAdminUserID = "testingAdminUser" dnsAdminUserID = "testingAdminUser"
dnsRegularUserID = "testingRegularUser" dnsRegularUserID = "testingRegularUser"
dnsNSGroup1 = "ns1" dnsNSGroup1 = "ns1"
dnsNSGroup2 = "ns2"
) )
func TestGetDNSSettings(t *testing.T) { func TestGetDNSSettings(t *testing.T) {
@ -184,7 +185,7 @@ func TestGetNetworkMap_DNSConfigSync(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Len(t, peer2AccountDNSConfig.DNSConfig.CustomZones, 1, "DNS config should have one custom zone for peers not in the disabled group") require.Len(t, peer2AccountDNSConfig.DNSConfig.CustomZones, 1, "DNS config should have one custom zone for peers not in the disabled group")
require.True(t, peer2AccountDNSConfig.DNSConfig.ServiceEnable, "DNS config should have DNS service enabled for peers not in the disabled group") require.True(t, peer2AccountDNSConfig.DNSConfig.ServiceEnable, "DNS config should have DNS service enabled for peers not in the disabled group")
require.Len(t, peer2AccountDNSConfig.DNSConfig.NameServerGroups, 1, "updated DNS config should have 1 nameserver groups since peer 2 is part of the group All") require.Len(t, peer2AccountDNSConfig.DNSConfig.NameServerGroups, 2, "updated DNS config should have 2 nameserver groups since peer 2 is part of the group All and supports IPv6")
} }
func createDNSManager(t *testing.T) (*DefaultAccountManager, error) { func createDNSManager(t *testing.T) (*DefaultAccountManager, error) {
@ -215,14 +216,15 @@ func initTestDNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, erro
Key: dnsPeer1Key, Key: dnsPeer1Key,
Name: "test-host1@netbird.io", Name: "test-host1@netbird.io",
Meta: nbpeer.PeerSystemMeta{ Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host1@netbird.io", Hostname: "test-host1@netbird.io",
GoOS: "linux", GoOS: "linux",
Kernel: "Linux", Kernel: "Linux",
Core: "21.04", Core: "21.04",
Platform: "x86_64", Platform: "x86_64",
OS: "Ubuntu", OS: "Ubuntu",
WtVersion: "development", WtVersion: "development",
UIVersion: "development", UIVersion: "development",
Ipv6Supported: false,
}, },
DNSLabel: dnsPeer1Key, DNSLabel: dnsPeer1Key,
} }
@ -230,16 +232,18 @@ func initTestDNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, erro
Key: dnsPeer2Key, Key: dnsPeer2Key,
Name: "test-host2@netbird.io", Name: "test-host2@netbird.io",
Meta: nbpeer.PeerSystemMeta{ Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host2@netbird.io", Hostname: "test-host2@netbird.io",
GoOS: "linux", GoOS: "linux",
Kernel: "Linux", Kernel: "Linux",
Core: "21.04", Core: "21.04",
Platform: "x86_64", Platform: "x86_64",
OS: "Ubuntu", OS: "Ubuntu",
WtVersion: "development", WtVersion: "development",
UIVersion: "development", UIVersion: "development",
Ipv6Supported: true,
}, },
DNSLabel: dnsPeer2Key, V6Setting: nbpeer.V6Enabled,
DNSLabel: dnsPeer2Key,
} }
domain := "example.com" domain := "example.com"
@ -312,6 +316,20 @@ func initTestDNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, erro
Groups: []string{allGroup.ID}, Groups: []string{allGroup.ID},
} }
account.NameServerGroups[dnsNSGroup2] = &dns.NameServerGroup{
ID: dnsNSGroup2,
Name: "ns-group-2",
NameServers: []dns.NameServer{{
IP: netip.MustParseAddr("2001:4860:4860:0:0:0:0:8888"), // Google DNS
NSType: dns.UDPNameServerType,
Port: dns.DefaultDNSPort,
}},
Primary: false,
Domains: []string{"example.com"},
Enabled: true,
Groups: []string{allGroup.ID},
}
err = am.Store.SaveAccount(account) err = am.Store.SaveAccount(account)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -149,6 +149,35 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *n
oldGroup, exists := account.Groups[newGroup.ID] oldGroup, exists := account.Groups[newGroup.ID]
account.Groups[newGroup.ID] = newGroup account.Groups[newGroup.ID] = newGroup
// Determine peer difference for group.
addedPeers := make([]string, 0)
removedPeers := make([]string, 0)
if exists {
addedPeers = difference(newGroup.Peers, oldGroup.Peers)
removedPeers = difference(oldGroup.Peers, newGroup.Peers)
} else {
addedPeers = append(addedPeers, newGroup.Peers...)
}
// Need to check whether IPv6 status has changed for all potentially affected peers.
peersToUpdate := make([]string, 0)
// If group previously had IPv6 enabled, need to check all old peers for changes in IPv6 status.
if exists && oldGroup.IPv6Enabled {
peersToUpdate = removedPeers
}
// If group IPv6 status changed, need to check all current peers, if it did not, but IPv6 is enabled, only check
// added peers, otherwise check no peers (as group can not affect IPv6 state).
if exists && oldGroup.IPv6Enabled != newGroup.IPv6Enabled {
peersToUpdate = append(peersToUpdate, newGroup.Peers...)
} else if newGroup.IPv6Enabled {
peersToUpdate = append(peersToUpdate, addedPeers...)
}
_, err = am.updatePeerIPv6Status(account, userID, newGroup, peersToUpdate)
if err != nil {
return err
}
account.Network.IncSerial() account.Network.IncSerial()
if err = am.Store.SaveAccount(account); err != nil { if err = am.Store.SaveAccount(account); err != nil {
return err return err
@ -158,13 +187,7 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *n
// the following snippet tracks the activity and stores the group events in the event store. // the following snippet tracks the activity and stores the group events in the event store.
// It has to happen after all the operations have been successfully performed. // It has to happen after all the operations have been successfully performed.
addedPeers := make([]string, 0) if !exists {
removedPeers := make([]string, 0)
if exists {
addedPeers = difference(newGroup.Peers, oldGroup.Peers)
removedPeers = difference(oldGroup.Peers, newGroup.Peers)
} else {
addedPeers = append(addedPeers, newGroup.Peers...)
am.StoreEvent(userID, newGroup.ID, accountID, activity.GroupCreated, newGroup.EventMeta()) am.StoreEvent(userID, newGroup.ID, accountID, activity.GroupCreated, newGroup.EventMeta())
} }
@ -314,6 +337,14 @@ func (am *DefaultAccountManager) DeleteGroup(accountId, userId, groupID string)
delete(account.Groups, groupID) delete(account.Groups, groupID)
// Update IPv6 status of all group members if necessary.
if g.IPv6Enabled {
_, err = am.updatePeerIPv6Status(account, userId, g, g.Peers)
if err != nil {
return err
}
}
account.Network.IncSerial() account.Network.IncSerial()
if err = am.Store.SaveAccount(account); err != nil { if err = am.Store.SaveAccount(account); err != nil {
return err return err
@ -345,6 +376,7 @@ func (am *DefaultAccountManager) ListGroups(accountID string) ([]*nbgroup.Group,
} }
// GroupAddPeer appends peer to the group // GroupAddPeer appends peer to the group
// TODO Question for devs: Is this method dead code? I can't seem to find any usages outside of tests...
func (am *DefaultAccountManager) GroupAddPeer(accountID, groupID, peerID string) error { func (am *DefaultAccountManager) GroupAddPeer(accountID, groupID, peerID string) error {
unlock := am.Store.AcquireAccountWriteLock(accountID) unlock := am.Store.AcquireAccountWriteLock(accountID)
defer unlock() defer unlock()
@ -368,6 +400,14 @@ func (am *DefaultAccountManager) GroupAddPeer(accountID, groupID, peerID string)
} }
if add { if add {
group.Peers = append(group.Peers, peerID) group.Peers = append(group.Peers, peerID)
if group.IPv6Enabled {
// Update IPv6 status of added group member.
_, err = am.updatePeerIPv6Status(account, "", group, []string{peerID})
if err != nil {
return err
}
}
} }
account.Network.IncSerial() account.Network.IncSerial()
@ -381,6 +421,7 @@ func (am *DefaultAccountManager) GroupAddPeer(accountID, groupID, peerID string)
} }
// GroupDeletePeer removes peer from the group // GroupDeletePeer removes peer from the group
// TODO Question for devs: Same as above, this seems like dead code
func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerID string) error { func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerID string) error {
unlock := am.Store.AcquireAccountWriteLock(accountID) unlock := am.Store.AcquireAccountWriteLock(accountID)
defer unlock() defer unlock()
@ -399,6 +440,15 @@ func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerID stri
for i, itemID := range group.Peers { for i, itemID := range group.Peers {
if itemID == peerID { if itemID == peerID {
group.Peers = append(group.Peers[:i], group.Peers[i+1:]...) group.Peers = append(group.Peers[:i], group.Peers[i+1:]...)
if group.IPv6Enabled {
// Update IPv6 status of deleted group member.
_, err = am.updatePeerIPv6Status(account, "", group, []string{peerID})
if err != nil {
return err
}
}
if err := am.Store.SaveAccount(account); err != nil { if err := am.Store.SaveAccount(account); err != nil {
return err return err
} }
@ -409,3 +459,24 @@ func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerID stri
return nil return nil
} }
func (am *DefaultAccountManager) updatePeerIPv6Status(account *Account, userID string, group *nbgroup.Group, peersToUpdate []string) (bool, error) {
updated := false
for _, peer := range peersToUpdate {
peerObj := account.GetPeer(peer)
update, err := am.DeterminePeerV6(account, peerObj)
if err != nil {
return false, err
}
if update {
updated = true
account.UpdatePeer(peerObj)
if peerObj.IP6 != nil {
am.StoreEvent(userID, group.ID, account.Id, activity.PeerIPv6InheritEnabled, group.EventMeta())
} else {
am.StoreEvent(userID, group.ID, account.Id, activity.PeerIPv6InheritDisabled, group.EventMeta())
}
}
}
return updated, nil
}

View File

@ -25,6 +25,8 @@ type Group struct {
// Peers list of the group // Peers list of the group
Peers []string `gorm:"serializer:json"` Peers []string `gorm:"serializer:json"`
IPv6Enabled bool
IntegrationReference integration_reference.IntegrationReference `gorm:"embedded;embeddedPrefix:integration_ref_"` IntegrationReference integration_reference.IntegrationReference `gorm:"embedded;embeddedPrefix:integration_ref_"`
} }
@ -38,6 +40,7 @@ func (g *Group) Copy() *Group {
ID: g.ID, ID: g.ID,
Name: g.Name, Name: g.Name,
Issued: g.Issued, Issued: g.Issued,
IPv6Enabled: g.IPv6Enabled,
Peers: make([]string, len(g.Peers)), Peers: make([]string, len(g.Peers)),
IntegrationReference: g.IntegrationReference, IntegrationReference: g.IntegrationReference,
} }

View File

@ -2,6 +2,8 @@ package server
import ( import (
"errors" "errors"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/stretchr/testify/require"
"testing" "testing"
nbdns "github.com/netbirdio/netbird/dns" nbdns "github.com/netbirdio/netbird/dns"
@ -12,6 +14,8 @@ import (
const ( const (
groupAdminUserID = "testingAdminUser" groupAdminUserID = "testingAdminUser"
groupPeer1Key = "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8="
groupPeer2Key = "/yF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NyI="
) )
func TestDefaultAccountManager_CreateGroup(t *testing.T) { func TestDefaultAccountManager_CreateGroup(t *testing.T) {
@ -20,7 +24,7 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) {
t.Error("failed to create account manager") t.Error("failed to create account manager")
} }
account, err := initTestGroupAccount(am) _, account, err := initTestGroupAccount(am)
if err != nil { if err != nil {
t.Error("failed to init testing account") t.Error("failed to init testing account")
} }
@ -55,7 +59,7 @@ func TestDefaultAccountManager_DeleteGroup(t *testing.T) {
t.Error("failed to create account manager") t.Error("failed to create account manager")
} }
account, err := initTestGroupAccount(am) _, account, err := initTestGroupAccount(am)
if err != nil { if err != nil {
t.Error("failed to init testing account") t.Error("failed to init testing account")
} }
@ -131,10 +135,137 @@ func TestDefaultAccountManager_DeleteGroup(t *testing.T) {
} }
} }
func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) { func TestDefaultAccountManager_GroupIPv6Consistency(t *testing.T) {
am, err := createManager(t)
if err != nil {
t.Error("failed to create account manager")
}
peers, account, err := initTestGroupAccount(am)
peer1Id := peers[0]
peer2Id := peers[1]
if err != nil {
t.Error("failed to init testing account")
}
group := account.GetGroup("grp-for-ipv6")
// First, add one member to the IPv6 group before enabling IPv6.
group.Peers = append(group.Peers, peer1Id)
err = am.SaveGroup(account.Id, groupAdminUserID, group)
require.NoError(t, err, "unable to update group")
account, err = am.Store.GetAccount(account.Id)
require.NoError(t, err, "unable to update account")
group = account.GetGroup("grp-for-ipv6")
require.Nil(t, account.Peers[peer1Id].IP6, "peer1 should not have an IPv6 address if the group doesn't have it enabled.")
require.Nil(t, account.Peers[peer2Id].IP6, "peer2 should not have an IPv6 address.")
// Now, enable IPv6.
group.IPv6Enabled = true
err = am.SaveGroup(account.Id, groupAdminUserID, group)
require.NoError(t, err, "unable to update group")
account, err = am.Store.GetAccount(account.Id)
require.NoError(t, err, "unable to update account")
group = account.GetGroup("grp-for-ipv6")
require.NotNil(t, account.Peers[peer1Id].IP6, "peer1 should have an IPv6 address as it is a member of the IPv6-enabled group.")
require.Nil(t, account.Peers[peer2Id].IP6, "peer2 should not have an IPv6 address as it is not a member of the IPv6-enabled group.")
// Add the second peer.
group.Peers = append(group.Peers, peer2Id)
err = am.SaveGroup(account.Id, groupAdminUserID, group)
require.NoError(t, err, "unable to update group")
account, err = am.Store.GetAccount(account.Id)
require.NoError(t, err, "unable to update account")
group = account.GetGroup("grp-for-ipv6")
require.NotNil(t, account.Peers[peer1Id].IP6, "peer1 should have an IPv6 address as it is a member of the IPv6-enabled group.")
require.NotNil(t, account.Peers[peer2Id].IP6, "peer2 should have an IPv6 address as it is a member of the IPv6-enabled group.")
// Disable IPv6 and simultaneously delete the first peer.
group.IPv6Enabled = false
group.Peers = group.Peers[1:]
err = am.SaveGroup(account.Id, groupAdminUserID, group)
require.NoError(t, err, "unable to update group")
account, err = am.Store.GetAccount(account.Id)
require.NoError(t, err, "unable to update account")
group = account.GetGroup("grp-for-ipv6")
require.Nil(t, account.Peers[peer1Id].IP6, "peer1 should not have an IPv6 address as it is not a member of any IPv6-enabled group.")
require.Nil(t, account.Peers[peer2Id].IP6, "peer2 should not have an IPv6 address as the group has IPv6 disabled.")
// Enable IPv6 and simultaneously add the first peer again.
group.IPv6Enabled = true
group.Peers = append(group.Peers, peer1Id)
err = am.SaveGroup(account.Id, groupAdminUserID, group)
require.NoError(t, err, "unable to update group")
account, err = am.Store.GetAccount(account.Id)
require.NoError(t, err, "unable to update account")
require.NotNil(t, account.Peers[peer1Id].IP6, "peer1 should have an IPv6 address as it is a member of the IPv6-enabled group.")
require.NotNil(t, account.Peers[peer2Id].IP6, "peer2 should have an IPv6 address as it is a member of the IPv6-enabled group.")
// Force disable IPv6.
peer1 := account.GetPeer(peer1Id)
peer2 := account.GetPeer(peer2Id)
peer1.V6Setting = nbpeer.V6Disabled
peer2.V6Setting = nbpeer.V6Disabled
_, err = am.UpdatePeer(account.Id, groupAdminUserID, peer1)
require.NoError(t, err, "unable to update peer1")
_, err = am.UpdatePeer(account.Id, groupAdminUserID, peer2)
require.NoError(t, err, "unable to update peer2")
account, err = am.Store.GetAccount(account.Id)
require.NoError(t, err, "unable to fetch updated account")
group = account.GetGroup("grp-for-ipv6")
require.Nil(t, account.GetPeer(peer1Id).IP6, "peer1 should not have an IPv6 address as it is force disabled.")
require.Nil(t, account.GetPeer(peer2Id).IP6, "peer2 should not have an IPv6 address as it is force disabled.")
// Delete Group.
err = am.DeleteGroup(account.Id, groupAdminUserID, group.ID)
require.NoError(t, err, "unable to delete group")
account, err = am.Store.GetAccount(account.Id)
require.NoError(t, err, "unable to update account")
group = account.GetGroup("grp-for-ipv6")
require.Nil(t, group, "Group should no longer exist.")
require.Nil(t, account.Peers[peer1Id].IP6, "peer1 should not have an IPv6 address as the only IPv6-enabled group was deleted.")
require.Nil(t, account.Peers[peer2Id].IP6, "peer2 should not have an IPv6 address as the only IPv6-enabled group was deleted.")
}
func initTestGroupAccount(am *DefaultAccountManager) ([]string, *Account, error) {
accountID := "testingAcc" accountID := "testingAcc"
domain := "example.com" domain := "example.com"
peer1 := &nbpeer.Peer{
Key: peer1Key,
Name: "peer1",
Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host1@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Ipv6Supported: true,
},
V6Setting: nbpeer.V6Auto,
DNSLabel: groupPeer1Key,
}
peer2 := &nbpeer.Peer{
Key: peer2Key,
Name: "peer2",
Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host2@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Ipv6Supported: true,
},
V6Setting: nbpeer.V6Auto,
DNSLabel: groupPeer2Key,
}
groupForRoute := &nbgroup.Group{ groupForRoute := &nbgroup.Group{
ID: "grp-for-route", ID: "grp-for-route",
AccountID: "account-id", AccountID: "account-id",
@ -191,6 +322,14 @@ func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) {
Peers: make([]string, 0), Peers: make([]string, 0),
} }
groupForIPv6 := &nbgroup.Group{
ID: "grp-for-ipv6",
AccountID: "account-id",
Name: "Group for IPv6",
Issued: nbgroup.GroupIssuedAPI,
Peers: make([]string, 0),
}
routeResource := &route.Route{ routeResource := &route.Route{
ID: "example route", ID: "example route",
Groups: []string{groupForRoute.ID}, Groups: []string{groupForRoute.ID},
@ -235,7 +374,7 @@ func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) {
err := am.Store.SaveAccount(account) err := am.Store.SaveAccount(account)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
_ = am.SaveGroup(accountID, groupAdminUserID, groupForRoute) _ = am.SaveGroup(accountID, groupAdminUserID, groupForRoute)
@ -245,6 +384,11 @@ func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) {
_ = am.SaveGroup(accountID, groupAdminUserID, groupForSetupKeys) _ = am.SaveGroup(accountID, groupAdminUserID, groupForSetupKeys)
_ = am.SaveGroup(accountID, groupAdminUserID, groupForUsers) _ = am.SaveGroup(accountID, groupAdminUserID, groupForUsers)
_ = am.SaveGroup(accountID, groupAdminUserID, groupForIntegration) _ = am.SaveGroup(accountID, groupAdminUserID, groupForIntegration)
_ = am.SaveGroup(accountID, groupAdminUserID, groupForIPv6)
peer1, _, _ = am.AddPeer(setupKey.Key, user.Id, peer1)
peer2, _, _ = am.AddPeer(setupKey.Key, user.Id, peer2)
return am.Store.GetAccount(account.Id) account, err = am.Store.GetAccount(account.Id)
return []string{peer1.ID, peer2.ID}, account, err
} }

View File

@ -287,6 +287,7 @@ func extractPeerMeta(loginReq *proto.LoginRequest) nbpeer.PeerSystemMeta {
Cloud: loginReq.GetMeta().GetEnvironment().GetCloud(), Cloud: loginReq.GetMeta().GetEnvironment().GetCloud(),
Platform: loginReq.GetMeta().GetEnvironment().GetPlatform(), Platform: loginReq.GetMeta().GetEnvironment().GetPlatform(),
}, },
Ipv6Supported: loginReq.GetMeta().GetIpv6Supported(),
} }
} }
@ -455,21 +456,31 @@ func toWiretrusteeConfig(config *Config, turnCredentials *TURNCredentials) *prot
func toPeerConfig(peer *nbpeer.Peer, network *Network, dnsName string) *proto.PeerConfig { func toPeerConfig(peer *nbpeer.Peer, network *Network, dnsName string) *proto.PeerConfig {
netmask, _ := network.Net.Mask.Size() netmask, _ := network.Net.Mask.Size()
address6 := ""
if network.Net6 != nil && peer.IP6 != nil {
netmask6, _ := network.Net6.Mask.Size()
address6 = fmt.Sprintf("%s/%d", peer.IP6.String(), netmask6)
}
fqdn := peer.FQDN(dnsName) fqdn := peer.FQDN(dnsName)
return &proto.PeerConfig{ return &proto.PeerConfig{
Address: fmt.Sprintf("%s/%d", peer.IP.String(), netmask), // take it from the network Address: fmt.Sprintf("%s/%d", peer.IP.String(), netmask), // take it from the network
Address6: address6,
SshConfig: &proto.SSHConfig{SshEnabled: peer.SSHEnabled}, SshConfig: &proto.SSHConfig{SshEnabled: peer.SSHEnabled},
Fqdn: fqdn, Fqdn: fqdn,
} }
} }
func toRemotePeerConfig(peers []*nbpeer.Peer, dnsName string) []*proto.RemotePeerConfig { func toRemotePeerConfig(peers []*nbpeer.Peer, dnsName string, v6Enabled bool) []*proto.RemotePeerConfig {
remotePeers := []*proto.RemotePeerConfig{} remotePeers := []*proto.RemotePeerConfig{}
for _, rPeer := range peers { for _, rPeer := range peers {
fqdn := rPeer.FQDN(dnsName) fqdn := rPeer.FQDN(dnsName)
allowedIps := []string{fmt.Sprintf(AllowedIPsFormat, rPeer.IP)}
if rPeer.IP6 != nil && v6Enabled {
allowedIps = append(allowedIps, fmt.Sprintf(AllowedIP6sFormat, *rPeer.IP6))
}
remotePeers = append(remotePeers, &proto.RemotePeerConfig{ remotePeers = append(remotePeers, &proto.RemotePeerConfig{
WgPubKey: rPeer.Key, WgPubKey: rPeer.Key,
AllowedIps: []string{fmt.Sprintf(AllowedIPsFormat, rPeer.IP)}, AllowedIps: allowedIps,
SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)}, SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
Fqdn: fqdn, Fqdn: fqdn,
}) })
@ -482,13 +493,13 @@ func toSyncResponse(config *Config, peer *nbpeer.Peer, turnCredentials *TURNCred
pConfig := toPeerConfig(peer, networkMap.Network, dnsName) pConfig := toPeerConfig(peer, networkMap.Network, dnsName)
remotePeers := toRemotePeerConfig(networkMap.Peers, dnsName) remotePeers := toRemotePeerConfig(networkMap.Peers, dnsName, peer.IP6 != nil)
routesUpdate := toProtocolRoutes(networkMap.Routes) routesUpdate := toProtocolRoutes(networkMap.Routes)
dnsUpdate := toProtocolDNSConfig(networkMap.DNSConfig) dnsUpdate := toProtocolDNSConfig(networkMap.DNSConfig)
offlinePeers := toRemotePeerConfig(networkMap.OfflinePeers, dnsName) offlinePeers := toRemotePeerConfig(networkMap.OfflinePeers, dnsName, peer.IP6 != nil)
firewallRules := toProtocolFirewallRules(networkMap.FirewallRules) firewallRules := toProtocolFirewallRules(networkMap.FirewallRules)

View File

@ -62,7 +62,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
handler := initAccountsTestData(&server.Account{ handler := initAccountsTestData(&server.Account{
Id: accountID, Id: accountID,
Domain: "hotmail.com", Domain: "hotmail.com",
Network: server.NewNetwork(), Network: server.NewNetwork(true),
Users: map[string]*server.User{ Users: map[string]*server.User{
adminUser.Id: adminUser, adminUser.Id: adminUser,
}, },

View File

@ -247,10 +247,15 @@ components:
description: (Cloud only) Indicates whether peer needs approval description: (Cloud only) Indicates whether peer needs approval
type: boolean type: boolean
example: true example: true
ipv6_enabled:
type: string
enum: [enabled, disabled, auto]
example: auto
required: required:
- name - name
- ssh_enabled - ssh_enabled
- login_expiration_enabled - login_expiration_enabled
- ipv6_enabled
PeerBase: PeerBase:
allOf: allOf:
- $ref: '#/components/schemas/PeerMinimum' - $ref: '#/components/schemas/PeerMinimum'
@ -264,6 +269,10 @@ components:
description: Peer's public connection IP address description: Peer's public connection IP address
type: string type: string
example: 35.64.0.1 example: 35.64.0.1
ip6:
description: Peer's IPv6 address
type: string
example: 2001:db8::0123:4567:890a:bcde
connected: connected:
description: Peer to Management connection status description: Peer to Management connection status
type: boolean type: boolean
@ -310,6 +319,15 @@ components:
description: Peer's desktop UI version description: Peer's desktop UI version
type: string type: string
example: 0.14.0 example: 0.14.0
ipv6_supported:
description: Whether this peer supports IPv6
type: boolean
example: true
ipv6_enabled:
description: Whether IPv6 is enabled for this peer.
type: string
enum: [enabled, disabled, auto]
example: auto
dns_label: dns_label:
description: Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud description: Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
type: string type: string
@ -352,6 +370,8 @@ components:
- kernel_version - kernel_version
- last_login - last_login
- last_seen - last_seen
- ipv6_supported
- ipv6_enabled
- login_expiration_enabled - login_expiration_enabled
- login_expired - login_expired
- os - os
@ -627,8 +647,12 @@ components:
items: items:
type: string type: string
example: "ch8i4ug6lnn4g9hqv7m1" example: "ch8i4ug6lnn4g9hqv7m1"
ipv6_enabled:
description: Whether IPv6 should be enabled for all members with IPv6 set to "auto"
type: boolean
required: required:
- name - name
- ipv6_enabled
Group: Group:
allOf: allOf:
- $ref: '#/components/schemas/GroupMinimum' - $ref: '#/components/schemas/GroupMinimum'
@ -639,8 +663,12 @@ components:
type: array type: array
items: items:
$ref: '#/components/schemas/PeerMinimum' $ref: '#/components/schemas/PeerMinimum'
ipv6_enabled:
description: Whether IPv6 should be enabled for all members with IPv6 set to "auto"
type: boolean
required: required:
- peers - peers
- ipv6_enabled
PolicyRuleMinimum: PolicyRuleMinimum:
type: object type: object
properties: properties:

View File

@ -88,12 +88,40 @@ const (
NameserverNsTypeUdp NameserverNsType = "udp" NameserverNsTypeUdp NameserverNsType = "udp"
) )
// Defines values for PeerIpv6Enabled.
const (
PeerIpv6EnabledAuto PeerIpv6Enabled = "auto"
PeerIpv6EnabledDisabled PeerIpv6Enabled = "disabled"
PeerIpv6EnabledEnabled PeerIpv6Enabled = "enabled"
)
// Defines values for PeerBaseIpv6Enabled.
const (
PeerBaseIpv6EnabledAuto PeerBaseIpv6Enabled = "auto"
PeerBaseIpv6EnabledDisabled PeerBaseIpv6Enabled = "disabled"
PeerBaseIpv6EnabledEnabled PeerBaseIpv6Enabled = "enabled"
)
// Defines values for PeerBatchIpv6Enabled.
const (
PeerBatchIpv6EnabledAuto PeerBatchIpv6Enabled = "auto"
PeerBatchIpv6EnabledDisabled PeerBatchIpv6Enabled = "disabled"
PeerBatchIpv6EnabledEnabled PeerBatchIpv6Enabled = "enabled"
)
// Defines values for PeerNetworkRangeCheckAction. // Defines values for PeerNetworkRangeCheckAction.
const ( const (
PeerNetworkRangeCheckActionAllow PeerNetworkRangeCheckAction = "allow" PeerNetworkRangeCheckActionAllow PeerNetworkRangeCheckAction = "allow"
PeerNetworkRangeCheckActionDeny PeerNetworkRangeCheckAction = "deny" PeerNetworkRangeCheckActionDeny PeerNetworkRangeCheckAction = "deny"
) )
// Defines values for PeerRequestIpv6Enabled.
const (
PeerRequestIpv6EnabledAuto PeerRequestIpv6Enabled = "auto"
PeerRequestIpv6EnabledDisabled PeerRequestIpv6Enabled = "disabled"
PeerRequestIpv6EnabledEnabled PeerRequestIpv6Enabled = "enabled"
)
// Defines values for PolicyRuleAction. // Defines values for PolicyRuleAction.
const ( const (
PolicyRuleActionAccept PolicyRuleAction = "accept" PolicyRuleActionAccept PolicyRuleAction = "accept"
@ -307,6 +335,9 @@ type Group struct {
// Id Group ID // Id Group ID
Id string `json:"id"` Id string `json:"id"`
// Ipv6Enabled Whether IPv6 should be enabled for all members with IPv6 set to "auto"
Ipv6Enabled bool `json:"ipv6_enabled"`
// Issued How the group was issued (api, integration, jwt) // Issued How the group was issued (api, integration, jwt)
Issued *GroupIssued `json:"issued,omitempty"` Issued *GroupIssued `json:"issued,omitempty"`
@ -343,6 +374,9 @@ type GroupMinimumIssued string
// GroupRequest defines model for GroupRequest. // GroupRequest defines model for GroupRequest.
type GroupRequest struct { type GroupRequest struct {
// Ipv6Enabled Whether IPv6 should be enabled for all members with IPv6 set to "auto"
Ipv6Enabled bool `json:"ipv6_enabled"`
// Name Group name identifier // Name Group name identifier
Name string `json:"name"` Name string `json:"name"`
@ -502,6 +536,15 @@ type Peer struct {
// Ip Peer's IP address // Ip Peer's IP address
Ip string `json:"ip"` Ip string `json:"ip"`
// Ip6 Peer's IPv6 address
Ip6 *string `json:"ip6,omitempty"`
// Ipv6Enabled Whether IPv6 is enabled for this peer.
Ipv6Enabled PeerIpv6Enabled `json:"ipv6_enabled"`
// Ipv6Supported Whether this peer supports IPv6
Ipv6Supported bool `json:"ipv6_supported"`
// KernelVersion Peer's operating system kernel version // KernelVersion Peer's operating system kernel version
KernelVersion string `json:"kernel_version"` KernelVersion string `json:"kernel_version"`
@ -539,6 +582,9 @@ type Peer struct {
Version string `json:"version"` Version string `json:"version"`
} }
// PeerIpv6Enabled Whether IPv6 is enabled for this peer.
type PeerIpv6Enabled string
// PeerBase defines model for PeerBase. // PeerBase defines model for PeerBase.
type PeerBase struct { type PeerBase struct {
// ApprovalRequired (Cloud only) Indicates whether peer needs approval // ApprovalRequired (Cloud only) Indicates whether peer needs approval
@ -574,6 +620,15 @@ type PeerBase struct {
// Ip Peer's IP address // Ip Peer's IP address
Ip string `json:"ip"` Ip string `json:"ip"`
// Ip6 Peer's IPv6 address
Ip6 *string `json:"ip6,omitempty"`
// Ipv6Enabled Whether IPv6 is enabled for this peer.
Ipv6Enabled PeerBaseIpv6Enabled `json:"ipv6_enabled"`
// Ipv6Supported Whether this peer supports IPv6
Ipv6Supported bool `json:"ipv6_supported"`
// KernelVersion Peer's operating system kernel version // KernelVersion Peer's operating system kernel version
KernelVersion string `json:"kernel_version"` KernelVersion string `json:"kernel_version"`
@ -611,6 +666,9 @@ type PeerBase struct {
Version string `json:"version"` Version string `json:"version"`
} }
// PeerBaseIpv6Enabled Whether IPv6 is enabled for this peer.
type PeerBaseIpv6Enabled string
// PeerBatch defines model for PeerBatch. // PeerBatch defines model for PeerBatch.
type PeerBatch struct { type PeerBatch struct {
// AccessiblePeersCount Number of accessible peers // AccessiblePeersCount Number of accessible peers
@ -649,6 +707,15 @@ type PeerBatch struct {
// Ip Peer's IP address // Ip Peer's IP address
Ip string `json:"ip"` Ip string `json:"ip"`
// Ip6 Peer's IPv6 address
Ip6 *string `json:"ip6,omitempty"`
// Ipv6Enabled Whether IPv6 is enabled for this peer.
Ipv6Enabled PeerBatchIpv6Enabled `json:"ipv6_enabled"`
// Ipv6Supported Whether this peer supports IPv6
Ipv6Supported bool `json:"ipv6_supported"`
// KernelVersion Peer's operating system kernel version // KernelVersion Peer's operating system kernel version
KernelVersion string `json:"kernel_version"` KernelVersion string `json:"kernel_version"`
@ -686,6 +753,9 @@ type PeerBatch struct {
Version string `json:"version"` Version string `json:"version"`
} }
// PeerBatchIpv6Enabled Whether IPv6 is enabled for this peer.
type PeerBatchIpv6Enabled string
// PeerMinimum defines model for PeerMinimum. // PeerMinimum defines model for PeerMinimum.
type PeerMinimum struct { type PeerMinimum struct {
// Id Peer ID // Id Peer ID
@ -710,12 +780,16 @@ type PeerNetworkRangeCheckAction string
// PeerRequest defines model for PeerRequest. // PeerRequest defines model for PeerRequest.
type PeerRequest struct { type PeerRequest struct {
// ApprovalRequired (Cloud only) Indicates whether peer needs approval // ApprovalRequired (Cloud only) Indicates whether peer needs approval
ApprovalRequired *bool `json:"approval_required,omitempty"` ApprovalRequired *bool `json:"approval_required,omitempty"`
LoginExpirationEnabled bool `json:"login_expiration_enabled"` Ipv6Enabled PeerRequestIpv6Enabled `json:"ipv6_enabled"`
Name string `json:"name"` LoginExpirationEnabled bool `json:"login_expiration_enabled"`
SshEnabled bool `json:"ssh_enabled"` Name string `json:"name"`
SshEnabled bool `json:"ssh_enabled"`
} }
// PeerRequestIpv6Enabled defines model for PeerRequest.Ipv6Enabled.
type PeerRequestIpv6Enabled string
// PersonalAccessToken defines model for PersonalAccessToken. // PersonalAccessToken defines model for PersonalAccessToken.
type PersonalAccessToken struct { type PersonalAccessToken struct {
// CreatedAt Date the token was created // CreatedAt Date the token was created

View File

@ -3,6 +3,7 @@ package http
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"slices"
"github.com/gorilla/mux" "github.com/gorilla/mux"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -82,16 +83,6 @@ func (h *GroupsHandler) UpdateGroup(w http.ResponseWriter, r *http.Request) {
return return
} }
allGroup, err := account.GetGroupAll()
if err != nil {
util.WriteError(err, w)
return
}
if allGroup.ID == groupID {
util.WriteError(status.Errorf(status.InvalidArgument, "updating group ALL is not allowed"), w)
return
}
var req api.PutApiGroupsGroupIdJSONRequestBody var req api.PutApiGroupsGroupIdJSONRequestBody
err = json.NewDecoder(r.Body).Decode(&req) err = json.NewDecoder(r.Body).Decode(&req)
if err != nil { if err != nil {
@ -110,12 +101,42 @@ func (h *GroupsHandler) UpdateGroup(w http.ResponseWriter, r *http.Request) {
} else { } else {
peers = *req.Peers peers = *req.Peers
} }
allGroup, err := account.GetGroupAll()
if err != nil {
util.WriteError(err, w)
return
}
if allGroup.ID == groupID {
if len(peers) != len(allGroup.Peers) || req.Name != allGroup.Name {
util.WriteError(status.Errorf(status.InvalidArgument, "updating group ALL is not allowed"), w)
return
}
deduplicatedPeers := make(map[string]struct{})
for _, peer := range peers {
deduplicatedPeers[peer] = struct{}{}
}
if len(deduplicatedPeers) != len(peers) {
util.WriteError(status.Errorf(status.InvalidArgument, "updating group ALL is not allowed"), w)
return
}
for peer := range deduplicatedPeers {
if slices.Contains(allGroup.Peers, peer) {
continue
}
util.WriteError(status.Errorf(status.InvalidArgument, "updating group ALL is not allowed"), w)
return
}
}
group := nbgroup.Group{ group := nbgroup.Group{
ID: groupID, ID: groupID,
Name: req.Name, Name: req.Name,
Peers: peers, Peers: peers,
Issued: eg.Issued, Issued: eg.Issued,
IntegrationReference: eg.IntegrationReference, IntegrationReference: eg.IntegrationReference,
IPv6Enabled: req.Ipv6Enabled,
} }
if err := h.accountManager.SaveGroup(account.Id, user.Id, &group); err != nil { if err := h.accountManager.SaveGroup(account.Id, user.Id, &group); err != nil {
@ -246,6 +267,7 @@ func toGroupResponse(account *server.Account, group *nbgroup.Group) *api.Group {
Id: group.ID, Id: group.ID,
Name: group.Name, Name: group.Name,
Issued: (*api.GroupIssued)(&group.Issued), Issued: (*api.GroupIssued)(&group.Issued),
Ipv6Enabled: group.IPv6Enabled,
} }
for _, pid := range group.Peers { for _, pid := range group.Peers {

View File

@ -85,11 +85,17 @@ func (h *PeersHandler) updatePeer(account *server.Account, user *server.User, pe
return return
} }
v6Status := nbpeer.V6Auto
if req.Ipv6Enabled != api.PeerRequestIpv6EnabledAuto {
v6Status = nbpeer.V6Status(req.Ipv6Enabled)
}
update := &nbpeer.Peer{ update := &nbpeer.Peer{
ID: peerID, ID: peerID,
SSHEnabled: req.SshEnabled, SSHEnabled: req.SshEnabled,
Name: req.Name, Name: req.Name,
LoginExpirationEnabled: req.LoginExpirationEnabled, LoginExpirationEnabled: req.LoginExpirationEnabled,
V6Setting: v6Status,
} }
if req.ApprovalRequired != nil { if req.ApprovalRequired != nil {
@ -284,11 +290,22 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD
osVersion = peer.Meta.Core osVersion = peer.Meta.Core
} }
var ip6 *string
if peer.IP6 != nil {
ip6string := peer.IP6.String()
ip6 = &ip6string
}
v6Status := api.PeerIpv6EnabledAuto
if peer.V6Setting != nbpeer.V6Auto {
v6Status = api.PeerIpv6Enabled(peer.V6Setting)
}
return &api.Peer{ return &api.Peer{
Id: peer.ID, Id: peer.ID,
Name: peer.Name, Name: peer.Name,
Ip: peer.IP.String(), Ip: peer.IP.String(),
ConnectionIp: peer.Location.ConnectionIP.String(), ConnectionIp: peer.Location.ConnectionIP.String(),
Ip6: ip6,
Connected: peer.Status.Connected, Connected: peer.Status.Connected,
LastSeen: peer.Status.LastSeen, LastSeen: peer.Status.LastSeen,
Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion), Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion),
@ -300,6 +317,8 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD
Hostname: peer.Meta.Hostname, Hostname: peer.Meta.Hostname,
UserId: peer.UserID, UserId: peer.UserID,
UiVersion: peer.Meta.UIVersion, UiVersion: peer.Meta.UIVersion,
Ipv6Supported: peer.Meta.Ipv6Supported,
Ipv6Enabled: v6Status,
DnsLabel: fqdn(peer, dnsDomain), DnsLabel: fqdn(peer, dnsDomain),
LoginExpirationEnabled: peer.LoginExpirationEnabled, LoginExpirationEnabled: peer.LoginExpirationEnabled,
LastLogin: peer.LastLogin, LastLogin: peer.LastLogin,
@ -317,12 +336,22 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn
if osVersion == "" { if osVersion == "" {
osVersion = peer.Meta.Core osVersion = peer.Meta.Core
} }
var ip6 *string
if peer.IP6 != nil {
ip6string := peer.IP6.String()
ip6 = &ip6string
}
v6Status := api.PeerBatchIpv6EnabledAuto
if peer.V6Setting != nbpeer.V6Auto {
v6Status = api.PeerBatchIpv6Enabled(peer.V6Setting)
}
return &api.PeerBatch{ return &api.PeerBatch{
Id: peer.ID, Id: peer.ID,
Name: peer.Name, Name: peer.Name,
Ip: peer.IP.String(), Ip: peer.IP.String(),
ConnectionIp: peer.Location.ConnectionIP.String(), ConnectionIp: peer.Location.ConnectionIP.String(),
Ip6: ip6,
Connected: peer.Status.Connected, Connected: peer.Status.Connected,
LastSeen: peer.Status.LastSeen, LastSeen: peer.Status.LastSeen,
Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion), Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion),
@ -334,6 +363,8 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn
Hostname: peer.Meta.Hostname, Hostname: peer.Meta.Hostname,
UserId: peer.UserID, UserId: peer.UserID,
UiVersion: peer.Meta.UIVersion, UiVersion: peer.Meta.UIVersion,
Ipv6Supported: peer.Meta.Ipv6Supported,
Ipv6Enabled: v6Status,
DnsLabel: fqdn(peer, dnsDomain), DnsLabel: fqdn(peer, dnsDomain),
LoginExpirationEnabled: peer.LoginExpirationEnabled, LoginExpirationEnabled: peer.LoginExpirationEnabled,
LastLogin: peer.LastLogin, LastLogin: peer.LastLogin,

View File

@ -37,6 +37,12 @@ func initTestMetaData(peers ...*nbpeer.Peer) *PeersHandler {
break break
} }
} }
if p.V6Setting == nbpeer.V6Enabled && p.IP6 == nil {
ip6 := net.ParseIP("2001:db8::dead:beef")
p.IP6 = &ip6
} else {
p.IP6 = nil
}
p.SSHEnabled = update.SSHEnabled p.SSHEnabled = update.SSHEnabled
p.LoginExpirationEnabled = update.LoginExpirationEnabled p.LoginExpirationEnabled = update.LoginExpirationEnabled
p.Name = update.Name p.Name = update.Name

View File

@ -789,6 +789,7 @@ func initTestNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, error
OS: "Ubuntu", OS: "Ubuntu",
WtVersion: "development", WtVersion: "development",
UIVersion: "development", UIVersion: "development",
Ipv6Supported: false,
}, },
} }
peer2 := &nbpeer.Peer{ peer2 := &nbpeer.Peer{
@ -803,6 +804,7 @@ func initTestNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, error
OS: "Ubuntu", OS: "Ubuntu",
WtVersion: "development", WtVersion: "development",
UIVersion: "development", UIVersion: "development",
Ipv6Supported: false,
}, },
} }
existingNSGroup := nbdns.NameServerGroup{ existingNSGroup := nbdns.NameServerGroup{

View File

@ -1,13 +1,13 @@
package server package server
import ( import (
crand "crypto/rand"
"encoding/binary"
"github.com/c-robinson/iplib"
"github.com/rs/xid"
"math/rand" "math/rand"
"net" "net"
"sync" "sync"
"time"
"github.com/c-robinson/iplib"
"github.com/rs/xid"
nbdns "github.com/netbirdio/netbird/dns" nbdns "github.com/netbirdio/netbird/dns"
nbpeer "github.com/netbirdio/netbird/management/server/peer" nbpeer "github.com/netbirdio/netbird/management/server/peer"
@ -20,11 +20,21 @@ const (
SubnetSize = 16 SubnetSize = 16
// NetSize is a global network size 100.64.0.0/10 // NetSize is a global network size 100.64.0.0/10
NetSize = 10 NetSize = 10
// Subnet6Size is the size of an IPv6 subnet (in Bytes, not Bits)
Subnet6Size = 8
// AllowedIPsFormat generates Wireguard AllowedIPs format (e.g. 100.64.30.1/32) // AllowedIPsFormat generates Wireguard AllowedIPs format (e.g. 100.64.30.1/32)
AllowedIPsFormat = "%s/32" AllowedIPsFormat = "%s/32"
// AllowedIP6sFormat generates Wireguard AllowedIPs format (e.g. 2001:db8::dead:beef/128)
AllowedIP6sFormat = "%s/128"
) )
// Global random number generator for IP addresses
// Accesses to the RNG must always be protected using rngLock (RNG sources are not thread-safe)
var rng = initializeRng()
var rngLock = sync.Mutex{}
type NetworkMap struct { type NetworkMap struct {
Peers []*nbpeer.Peer Peers []*nbpeer.Peer
Network *Network Network *Network
@ -37,6 +47,7 @@ type NetworkMap struct {
type Network struct { type Network struct {
Identifier string `json:"id"` Identifier string `json:"id"`
Net net.IPNet `gorm:"serializer:json"` Net net.IPNet `gorm:"serializer:json"`
Net6 *net.IPNet `gorm:"serializer:json"` // Can't use gob serializer, as it cannot encode nil values.
Dns string Dns string
// Serial is an ID that increments by 1 when any change to the network happened (e.g. new peer has been added). // Serial is an ID that increments by 1 when any change to the network happened (e.g. new peer has been added).
// Used to synchronize state to the client apps. // Used to synchronize state to the client apps.
@ -45,24 +56,54 @@ type Network struct {
mu sync.Mutex `json:"-" gorm:"-"` mu sync.Mutex `json:"-" gorm:"-"`
} }
func initializeRng() *rand.Rand {
seed := make([]byte, 8)
_, err := crand.Read(seed)
if err != nil {
return nil
}
s := rand.NewSource(int64(binary.LittleEndian.Uint64(seed)))
return rand.New(s)
}
// NewNetwork creates a new Network initializing it with a Serial=0 // NewNetwork creates a new Network initializing it with a Serial=0
// It takes a random /16 subnet from 100.64.0.0/10 (64 different subnets) // It takes a random /16 subnet from 100.64.0.0/10 (64 different subnets)
func NewNetwork() *Network { func NewNetwork(enableV6 bool) *Network {
n := iplib.NewNet4(net.ParseIP("100.64.0.0"), NetSize) n := iplib.NewNet4(net.ParseIP("100.64.0.0"), NetSize)
sub, _ := n.Subnet(SubnetSize) sub, _ := n.Subnet(SubnetSize)
rngLock.Lock()
intn := rng.Intn(len(sub))
rngLock.Unlock()
s := rand.NewSource(time.Now().Unix()) var n6 *net.IPNet = nil
r := rand.New(s) if enableV6 {
intn := r.Intn(len(sub)) n6 = GenerateNetwork6()
}
return &Network{ return &Network{
Identifier: xid.New().String(), Identifier: xid.New().String(),
Net: sub[intn].IPNet, Net: sub[intn].IPNet,
Net6: n6,
Dns: "", Dns: "",
Serial: 0} Serial: 0}
} }
func GenerateNetwork6() *net.IPNet {
addrbuf := make([]byte, 16)
addrbuf[0] = 0xfd
addrbuf[1] = 0x00
addrbuf[2] = 0xb1
addrbuf[3] = 0x4d
rngLock.Lock()
_, _ = rng.Read(addrbuf[4:Subnet6Size])
rngLock.Unlock()
n6 := iplib.NewNet6(addrbuf, Subnet6Size*8, 0).IPNet
return &n6
}
// IncSerial increments Serial by 1 reflecting that the network state has been changed // IncSerial increments Serial by 1 reflecting that the network state has been changed
func (n *Network) IncSerial() { func (n *Network) IncSerial() {
n.mu.Lock() n.mu.Lock()
@ -81,6 +122,7 @@ func (n *Network) Copy() *Network {
return &Network{ return &Network{
Identifier: n.Identifier, Identifier: n.Identifier,
Net: n.Net, Net: n.Net,
Net6: n.Net6,
Dns: n.Dns, Dns: n.Dns,
Serial: n.Serial, Serial: n.Serial,
} }
@ -103,13 +145,38 @@ func AllocatePeerIP(ipNet net.IPNet, takenIps []net.IP) (net.IP, error) {
} }
// pick a random IP // pick a random IP
s := rand.NewSource(time.Now().Unix()) rngLock.Lock()
r := rand.New(s) intn := rng.Intn(len(ips))
intn := r.Intn(len(ips)) rngLock.Unlock()
return ips[intn], nil return ips[intn], nil
} }
// AllocatePeerIP6 pics an available IPv6 from an net.IPNet.
// This method considers already taken IPs and reuses IPs if there are gaps in takenIps.
func AllocatePeerIP6(ipNet net.IPNet, takenIps []net.IP) (net.IP, error) {
takenIPMap := make(map[string]struct{})
takenIPMap[ipNet.IP.String()] = struct{}{}
for _, ip := range takenIps {
takenIPMap[ip.String()] = struct{}{}
}
maskSize, _ := ipNet.Mask.Size()
// TODO for small subnet sizes, randomly generating values until we don't get a duplicate is inefficient and could
// lead to many loop iterations, using a method similar to IPv4 would be preferable here.
addrbuf := make(net.IP, 16)
copy(addrbuf, ipNet.IP.To16())
for duplicate := true; duplicate; _, duplicate = takenIPMap[addrbuf.String()] {
rngLock.Lock()
_, _ = rng.Read(addrbuf[(maskSize / 8):16])
rngLock.Unlock()
}
return addrbuf, nil
}
// generateIPs generates a list of all possible IPs of the given network excluding IPs specified in the exclusion list // generateIPs generates a list of all possible IPs of the given network excluding IPs specified in the exclusion list
func generateIPs(ipNet *net.IPNet, exclusions map[string]struct{}) ([]net.IP, int) { func generateIPs(ipNet *net.IPNet, exclusions map[string]struct{}) ([]net.IP, int) {

View File

@ -1,6 +1,7 @@
package server package server
import ( import (
"github.com/stretchr/testify/require"
"net" "net"
"testing" "testing"
@ -8,11 +9,19 @@ import (
) )
func TestNewNetwork(t *testing.T) { func TestNewNetwork(t *testing.T) {
network := NewNetwork() network := NewNetwork(true)
// generated net should be a subnet of a larger 100.64.0.0/10 net // generated net should be a subnet of a larger 100.64.0.0/10 net
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}} ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
assert.Equal(t, ipNet.Contains(network.Net.IP), true) assert.True(t, ipNet.Contains(network.Net.IP))
// generated IPv6 net should be a subnet of the fd00:b14d::/32 prefix.
_, ipNet6, err := net.ParseCIDR("fd00:b14d::/32")
require.NoError(t, err, "unable to parse IPv6 prefix")
assert.True(t, ipNet6.Contains(network.Net6.IP))
// IPv6 prefix should be of size /64
ones, _ := network.Net6.Mask.Size()
assert.Equal(t, ones, 64)
} }
func TestAllocatePeerIP(t *testing.T) { func TestAllocatePeerIP(t *testing.T) {
@ -38,6 +47,32 @@ func TestAllocatePeerIP(t *testing.T) {
} }
} }
func TestAllocatePeerIP6(t *testing.T) {
_, ipNet, err := net.ParseCIDR("2001:db8:abcd:1234::/64")
require.NoError(t, err, "unable to parse IPv6 prefix")
var ips []net.IP
// Yeah, we better not check all 2^64 possible addresses, just generating a bunch of addresses should hopefully
// reveal any possible bugs in the RNG.
for i := 0; i < 252; i++ {
ip, err := AllocatePeerIP6(*ipNet, ips)
if err != nil {
t.Fatal(err)
}
ips = append(ips, ip)
}
assert.Len(t, ips, 252)
uniq := make(map[string]struct{})
for _, ip := range ips {
if _, ok := uniq[ip.String()]; !ok {
uniq[ip.String()] = struct{}{}
} else {
t.Errorf("found duplicate IP %s", ip.String())
}
}
}
func TestGenerateIPs(t *testing.T) { func TestGenerateIPs(t *testing.T) {
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 255, 255, 0}} ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 255, 255, 0}}
ips, ipsLen := generateIPs(&ipNet, map[string]struct{}{"100.64.0.0": {}}) ips, ipsLen := generateIPs(&ipNet, map[string]struct{}{"100.64.0.0": {}})

View File

@ -3,6 +3,7 @@ package server
import ( import (
"fmt" "fmt"
"net" "net"
"slices"
"strings" "strings"
"time" "time"
@ -13,6 +14,7 @@ import (
"github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/activity"
nbpeer "github.com/netbirdio/netbird/management/server/peer" nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/status"
nbroute "github.com/netbirdio/netbird/route"
) )
// PeerSync used as a data object between the gRPC API and AccountManager on Sync request. // PeerSync used as a data object between the gRPC API and AccountManager on Sync request.
@ -140,7 +142,66 @@ func (am *DefaultAccountManager) MarkPeerConnected(peerPubKey string, connected
return nil return nil
} }
// UpdatePeer updates peer. Only Peer.Name, Peer.SSHEnabled, and Peer.LoginExpirationEnabled can be updated. // Determines the current IPv6 status of the peer (including checks for inheritance) and generates a new or removes an
// existing IPv6 address if necessary.
// Additionally, disables IPv6 routes if peer no longer has an IPv6 address.
// Note that this change does not get persisted here.
//
// Returns a boolean that indicates whether the peer and/or the account changed and needs to be updated in the data
// source.
func (am *DefaultAccountManager) DeterminePeerV6(account *Account, peer *nbpeer.Peer) (bool, error) {
v6Setting := peer.V6Setting
if peer.V6Setting == nbpeer.V6Auto {
if peer.Meta.Ipv6Supported {
for _, group := range account.Groups {
if group.IPv6Enabled && slices.Contains(group.Peers, peer.ID) {
v6Setting = nbpeer.V6Enabled
break
}
}
if v6Setting == nbpeer.V6Auto {
for _, route := range account.Routes {
if route.Peer == peer.ID && route.NetworkType == nbroute.IPv6Network {
v6Setting = nbpeer.V6Enabled
break
}
}
}
}
if v6Setting == nbpeer.V6Auto {
v6Setting = nbpeer.V6Disabled
}
}
if v6Setting == nbpeer.V6Enabled && peer.IP6 == nil {
if !peer.Meta.Ipv6Supported {
return false, status.Errorf(status.PreconditionFailed, "failed allocating new IPv6 for peer %s - peer does not support IPv6", peer.Name)
}
if account.Network.Net6 == nil {
account.Network.Net6 = GenerateNetwork6()
}
v6tmp, err := AllocatePeerIP6(*account.Network.Net6, account.getTakenIP6s())
if err != nil {
return false, err
}
peer.IP6 = &v6tmp
return true, nil
} else if v6Setting == nbpeer.V6Disabled && peer.IP6 != nil {
peer.IP6 = nil
for _, route := range account.Routes {
if route.NetworkType == nbroute.IPv6Network {
route.Enabled = false
account.Routes[route.ID] = route
}
}
return true, nil
}
return false, nil
}
// UpdatePeer updates peer. Only Peer.Name, Peer.SSHEnabled, Peer.V6Setting and Peer.LoginExpirationEnabled can be updated.
func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *nbpeer.Peer) (*nbpeer.Peer, error) { func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *nbpeer.Peer) (*nbpeer.Peer, error) {
unlock := am.Store.AcquireAccountWriteLock(accountID) unlock := am.Store.AcquireAccountWriteLock(accountID)
defer unlock() defer unlock()
@ -160,6 +221,20 @@ func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *nb
return nil, err return nil, err
} }
if peer.V6Setting != update.V6Setting {
peer.V6Setting = update.V6Setting
prevV6 := peer.IP6
v6StatusChanged, err := am.DeterminePeerV6(account, peer)
if err != nil {
return nil, err
}
if v6StatusChanged && peer.IP6 != nil {
am.StoreEvent(userID, peer.IP6.String(), account.Id, activity.PeerIPv6Enabled, peer.EventMeta(am.GetDNSDomain()))
} else if v6StatusChanged && peer.IP6 == nil {
am.StoreEvent(userID, prevV6.String(), account.Id, activity.PeerIPv6Disabled, peer.EventMeta(am.GetDNSDomain()))
}
}
if peer.SSHEnabled != update.SSHEnabled { if peer.SSHEnabled != update.SSHEnabled {
peer.SSHEnabled = update.SSHEnabled peer.SSHEnabled = update.SSHEnabled
event := activity.PeerSSHEnabled event := activity.PeerSSHEnabled
@ -431,6 +506,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *nbpeer.P
Key: peer.Key, Key: peer.Key,
SetupKey: upperKey, SetupKey: upperKey,
IP: nextIp, IP: nextIp,
IP6: nil,
Meta: peer.Meta, Meta: peer.Meta,
Name: peer.Meta.Hostname, Name: peer.Meta.Hostname,
DNSLabel: newLabel, DNSLabel: newLabel,
@ -443,6 +519,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *nbpeer.P
LoginExpirationEnabled: addedByUser, LoginExpirationEnabled: addedByUser,
Ephemeral: ephemeral, Ephemeral: ephemeral,
Location: peer.Location, Location: peer.Location,
V6Setting: peer.V6Setting, // empty string "" corresponds to "auto"
} }
// add peer to 'All' group // add peer to 'All' group
@ -475,6 +552,11 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *nbpeer.P
newPeer = am.integratedPeerValidator.PreparePeer(account.Id, newPeer, account.GetPeerGroupsList(newPeer.ID), account.Settings.Extra) newPeer = am.integratedPeerValidator.PreparePeer(account.Id, newPeer, account.GetPeerGroupsList(newPeer.ID), account.Settings.Extra)
_, err = am.DeterminePeerV6(account, newPeer)
if err != nil {
return nil, nil, err
}
if addedByUser { if addedByUser {
user, err := account.FindUser(userID) user, err := account.FindUser(userID)
if err != nil { if err != nil {
@ -676,6 +758,14 @@ func (am *DefaultAccountManager) LoginPeer(login PeerLogin) (*nbpeer.Peer, *Netw
shouldStoreAccount = true shouldStoreAccount = true
} }
updated, err = am.DeterminePeerV6(account, peer)
if err != nil {
return nil, nil, err
}
if updated {
shouldStoreAccount = true
}
peer, err = am.checkAndUpdatePeerSSHKey(peer, account, login.SSHKey) peer, err = am.checkAndUpdatePeerSSHKey(peer, account, login.SSHKey)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

View File

@ -20,6 +20,8 @@ type Peer struct {
SetupKey string SetupKey string
// IP address of the Peer // IP address of the Peer
IP net.IP `gorm:"serializer:json"` IP net.IP `gorm:"serializer:json"`
// IPv6 address of the Peer
IP6 *net.IP `gorm:"uniqueIndex:idx_peers_account_id_ip6"`
// Meta is a Peer system meta data // Meta is a Peer system meta data
Meta PeerSystemMeta `gorm:"embedded;embeddedPrefix:meta_"` Meta PeerSystemMeta `gorm:"embedded;embeddedPrefix:meta_"`
// Name is peer's name (machine name) // Name is peer's name (machine name)
@ -44,10 +46,23 @@ type Peer struct {
CreatedAt time.Time CreatedAt time.Time
// Indicate ephemeral peer attribute // Indicate ephemeral peer attribute
Ephemeral bool Ephemeral bool
// Geo location based on connection IP // Geolocation based on connection IP
Location Location `gorm:"embedded;embeddedPrefix:location_"` Location Location `gorm:"embedded;embeddedPrefix:location_"`
// Whether IPv6 should be enabled or not.
V6Setting V6Status
} }
type V6Status string
const (
// Inherit IPv6 settings from groups (=> if one group the peer is a member of has IPv6 enabled, it will be enabled).
V6Auto V6Status = ""
// Enable IPv6 regardless of group settings, as long as it is supported.
V6Enabled V6Status = "enabled"
// Disable IPv6 regardless of group settings.
V6Disabled V6Status = "disabled"
)
type PeerStatus struct { //nolint:revive type PeerStatus struct { //nolint:revive
// LastSeen is the last time peer was connected to the management service // LastSeen is the last time peer was connected to the management service
LastSeen time.Time LastSeen time.Time
@ -96,6 +111,7 @@ type PeerSystemMeta struct { //nolint:revive
SystemProductName string SystemProductName string
SystemManufacturer string SystemManufacturer string
Environment Environment `gorm:"serializer:json"` Environment Environment `gorm:"serializer:json"`
Ipv6Supported bool
} }
func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool { func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool {
@ -130,7 +146,8 @@ func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool {
p.SystemProductName == other.SystemProductName && p.SystemProductName == other.SystemProductName &&
p.SystemManufacturer == other.SystemManufacturer && p.SystemManufacturer == other.SystemManufacturer &&
p.Environment.Cloud == other.Environment.Cloud && p.Environment.Cloud == other.Environment.Cloud &&
p.Environment.Platform == other.Environment.Platform p.Environment.Platform == other.Environment.Platform &&
p.Ipv6Supported == other.Ipv6Supported
} }
// AddedWithSSOLogin indicates whether this peer has been added with an SSO login by a user. // AddedWithSSOLogin indicates whether this peer has been added with an SSO login by a user.
@ -150,6 +167,7 @@ func (p *Peer) Copy() *Peer {
Key: p.Key, Key: p.Key,
SetupKey: p.SetupKey, SetupKey: p.SetupKey,
IP: p.IP, IP: p.IP,
IP6: p.IP6,
Meta: p.Meta, Meta: p.Meta,
Name: p.Name, Name: p.Name,
DNSLabel: p.DNSLabel, DNSLabel: p.DNSLabel,
@ -162,6 +180,7 @@ func (p *Peer) Copy() *Peer {
CreatedAt: p.CreatedAt, CreatedAt: p.CreatedAt,
Ephemeral: p.Ephemeral, Ephemeral: p.Ephemeral,
Location: p.Location, Location: p.Location,
V6Setting: p.V6Setting,
} }
} }

View File

@ -1,6 +1,7 @@
package server package server
import ( import (
"github.com/stretchr/testify/require"
"testing" "testing"
"time" "time"
@ -136,6 +137,112 @@ func TestAccountManager_GetNetworkMap(t *testing.T) {
} }
} }
func TestDefaultAccountManager_DeterminePeerV6(t *testing.T) {
manager, err := createManager(t)
if err != nil {
t.Fatal(err)
return
}
expectedId := "test_account"
userId := "account_creator"
account, err := createAccount(manager, expectedId, userId, "")
if err != nil {
t.Fatal(err)
}
setupKey, err := manager.CreateSetupKey(account.Id, "test-key", SetupKeyReusable, time.Hour, nil, 999, userId, false)
if err != nil {
t.Fatal("error creating setup key")
return
}
peerKey1, err := wgtypes.GeneratePrivateKey()
if err != nil {
t.Fatal(err)
return
}
peer1, _, err := manager.AddPeer(setupKey.Key, "", &nbpeer.Peer{
Key: peerKey1.PublicKey().String(),
Meta: nbpeer.PeerSystemMeta{
Hostname: "test-peer-1",
Ipv6Supported: true,
},
})
if err != nil {
t.Errorf("expecting peer to be added, got failure %v", err)
return
}
peerKey2, err := wgtypes.GeneratePrivateKey()
if err != nil {
t.Fatal(err)
return
}
peer2, _, err := manager.AddPeer(setupKey.Key, "", &nbpeer.Peer{
Key: peerKey2.PublicKey().String(),
Meta: nbpeer.PeerSystemMeta{
Hostname: "test-peer-2",
Ipv6Supported: false,
},
})
if err != nil {
t.Errorf("expecting peer to be added, got failure %v", err)
return
}
account, err = manager.Store.GetAccount(account.Id)
require.NoError(t, err, "unable to fetch updated account")
// Check if automatic setting defaults to "false".
// (Other tests for interactions between the automatic setting and group/route memberships are already covered in
// group_test.go and route_test.go)
_, err = manager.DeterminePeerV6(account, peer1)
require.NoError(t, err, "unable to determine effective peer IPv6 status")
_, err = manager.DeterminePeerV6(account, peer2)
require.NoError(t, err, "unable to determine effective peer IPv6 status")
require.Nil(t, peer1.IP6, "peer1 IPv6 address did not default to nil.")
require.Nil(t, peer1.IP6, "peer2 IPv6 address did not default to nil.")
peer1.V6Setting = nbpeer.V6Disabled
peer2.V6Setting = nbpeer.V6Disabled
_, err = manager.DeterminePeerV6(account, peer1)
require.NoError(t, err, "unable to determine effective peer IPv6 status")
_, err = manager.DeterminePeerV6(account, peer2)
require.NoError(t, err, "unable to determine effective peer IPv6 status")
require.Nil(t, peer1.IP6, "peer1 IPv6 address is not nil even though it is force disabled.")
require.Nil(t, peer2.IP6, "peer2 IPv6 address is not nil even though it is force disabled and unsupported.")
peer1.V6Setting = nbpeer.V6Enabled
peer2.V6Setting = nbpeer.V6Enabled
_, err = manager.DeterminePeerV6(account, peer1)
require.NoError(t, err, "unable to determine effective peer IPv6 status")
_, err = manager.DeterminePeerV6(account, peer2)
require.Error(t, err, "determining peer2 IPv6 address should fail as it is force enabled, but unsupported.")
require.NotNil(t, peer1.IP6, "peer1 IPv6 address is nil even though it is force enabled.")
require.Nil(t, peer2.IP6, "peer2 IPv6 address is not nil even though it is unsupported.")
// Test whether disabling IPv6 will disable IPv6 routes.
allGroup, err := account.GetGroupAll()
require.NoError(t, err, "unable to retrieve all group")
route, err := manager.CreateRoute(account.Id, "2001:db8:2345:6789::/64", peer1.ID, make([]string, 0), "testroute", "testnet", false, 9999, []string{allGroup.ID}, true, userID)
require.NoError(t, err, "unable to create test IPv6 route")
require.True(t, route.Enabled, "created IPv6 test route should be enabled")
peer1.V6Setting = nbpeer.V6Disabled
_, err = manager.UpdatePeer(account.Id, userID, peer1)
require.NoError(t, err, "unable to update peer")
account, err = manager.Store.GetAccount(account.Id)
require.NoError(t, err, "unable to fetch updated account")
route = account.Routes[route.ID]
require.False(t, route.Enabled, "disabling IPv6 for a peer should disable all of its IPv6 routes.")
}
func TestAccountManager_GetNetworkMapWithPolicy(t *testing.T) { func TestAccountManager_GetNetworkMapWithPolicy(t *testing.T) {
// TODO: disable until we start use policy again // TODO: disable until we start use policy again
t.Skip() t.Skip()

View File

@ -195,6 +195,8 @@ type FirewallRule struct {
// PeerIP of the peer // PeerIP of the peer
PeerIP string PeerIP string
PeerIP6 string
// Direction of the traffic // Direction of the traffic
Direction int Direction int
@ -278,8 +280,14 @@ func (a *Account) connResourcesGenerator() (func(*PolicyRule, []*nbpeer.Peer, in
peersExists[peer.ID] = struct{}{} peersExists[peer.ID] = struct{}{}
} }
ip6 := ""
if peer.IP6 != nil {
ip6 = peer.IP6.String()
}
fr := FirewallRule{ fr := FirewallRule{
PeerIP: peer.IP.String(), PeerIP: peer.IP.String(),
PeerIP6: ip6,
Direction: direction, Direction: direction,
Action: string(rule.Action), Action: string(rule.Action),
Protocol: string(rule.Protocol), Protocol: string(rule.Protocol),
@ -287,6 +295,7 @@ func (a *Account) connResourcesGenerator() (func(*PolicyRule, []*nbpeer.Peer, in
if isAll { if isAll {
fr.PeerIP = "0.0.0.0" fr.PeerIP = "0.0.0.0"
fr.PeerIP6 = "::"
} }
ruleID := (rule.ID + fr.PeerIP + strconv.Itoa(direction) + ruleID := (rule.ID + fr.PeerIP + strconv.Itoa(direction) +
@ -474,6 +483,7 @@ func toProtocolFirewallRules(update []*FirewallRule) []*proto.FirewallRule {
result[i] = &proto.FirewallRule{ result[i] = &proto.FirewallRule{
PeerIP: update[i].PeerIP, PeerIP: update[i].PeerIP,
PeerIP6: update[i].PeerIP6,
Direction: direction, Direction: direction,
Action: action, Action: action,
Protocol: protocol, Protocol: protocol,

View File

@ -14,16 +14,20 @@ import (
) )
func TestAccount_getPeersByPolicy(t *testing.T) { func TestAccount_getPeersByPolicy(t *testing.T) {
peerAIP6 := net.ParseIP("2001:db8:abcd:1234::2")
peerBIP6 := net.ParseIP("2001:db8:abcd:1234::3")
account := &Account{ account := &Account{
Peers: map[string]*nbpeer.Peer{ Peers: map[string]*nbpeer.Peer{
"peerA": { "peerA": {
ID: "peerA", ID: "peerA",
IP: net.ParseIP("100.65.14.88"), IP: net.ParseIP("100.65.14.88"),
IP6: &peerAIP6,
Status: &nbpeer.PeerStatus{}, Status: &nbpeer.PeerStatus{},
}, },
"peerB": { "peerB": {
ID: "peerB", ID: "peerB",
IP: net.ParseIP("100.65.80.39"), IP: net.ParseIP("100.65.80.39"),
IP6: &peerBIP6,
Status: &nbpeer.PeerStatus{}, Status: &nbpeer.PeerStatus{},
}, },
"peerC": { "peerC": {
@ -161,6 +165,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
epectedFirewallRules := []*FirewallRule{ epectedFirewallRules := []*FirewallRule{
{ {
PeerIP: "0.0.0.0", PeerIP: "0.0.0.0",
PeerIP6: "::",
Direction: firewallRuleDirectionIN, Direction: firewallRuleDirectionIN,
Action: "accept", Action: "accept",
Protocol: "all", Protocol: "all",
@ -168,6 +173,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
}, },
{ {
PeerIP: "0.0.0.0", PeerIP: "0.0.0.0",
PeerIP6: "::",
Direction: firewallRuleDirectionOUT, Direction: firewallRuleDirectionOUT,
Action: "accept", Action: "accept",
Protocol: "all", Protocol: "all",
@ -175,6 +181,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
}, },
{ {
PeerIP: "100.65.14.88", PeerIP: "100.65.14.88",
PeerIP6: "2001:db8:abcd:1234::2",
Direction: firewallRuleDirectionIN, Direction: firewallRuleDirectionIN,
Action: "accept", Action: "accept",
Protocol: "all", Protocol: "all",
@ -182,6 +189,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
}, },
{ {
PeerIP: "100.65.14.88", PeerIP: "100.65.14.88",
PeerIP6: "2001:db8:abcd:1234::2",
Direction: firewallRuleDirectionOUT, Direction: firewallRuleDirectionOUT,
Action: "accept", Action: "accept",
Protocol: "all", Protocol: "all",
@ -678,6 +686,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
expectedFirewallRules := []*FirewallRule{ expectedFirewallRules := []*FirewallRule{
{ {
PeerIP: "0.0.0.0", PeerIP: "0.0.0.0",
PeerIP6: "::",
Direction: firewallRuleDirectionOUT, Direction: firewallRuleDirectionOUT,
Action: "accept", Action: "accept",
Protocol: "tcp", Protocol: "tcp",

View File

@ -1,6 +1,7 @@
package server package server
import ( import (
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"net/netip" "net/netip"
"unicode/utf8" "unicode/utf8"
@ -180,6 +181,25 @@ func (am *DefaultAccountManager) CreateRoute(accountID, network, peerID string,
account.Routes[newRoute.ID] = &newRoute account.Routes[newRoute.ID] = &newRoute
// IPv6 route must only be created with IPv6 enabled peers, creating an IPv6 enabled route may enable IPv6 for
// peers with V6Setting = Auto.
if peerID != "" && prefixType == route.IPv6Network && newRoute.Enabled {
peer := account.GetPeer(peerID)
if peer.V6Setting == nbpeer.V6Disabled || !peer.Meta.Ipv6Supported {
return nil, status.Errorf(
status.InvalidArgument,
"IPv6 must be enabled for peer %s to be used in route %s",
peer.Name, newPrefix.String())
} else if peer.IP6 == nil {
_, err = am.DeterminePeerV6(account, peer)
if err != nil {
return nil, err
}
account.UpdatePeer(peer)
}
}
account.Network.IncSerial() account.Network.IncSerial()
if err = am.Store.SaveAccount(account); err != nil { if err = am.Store.SaveAccount(account); err != nil {
return nil, err return nil, err
@ -222,6 +242,16 @@ func (am *DefaultAccountManager) SaveRoute(accountID, userID string, routeToSave
return status.Errorf(status.InvalidArgument, "peer with ID and peer groups should not be provided at the same time") return status.Errorf(status.InvalidArgument, "peer with ID and peer groups should not be provided at the same time")
} }
if routeToSave.Peer != "" {
peer := account.GetPeer(routeToSave.Peer)
if peer == nil {
return status.Errorf(status.InvalidArgument, "provided peer does not exist")
}
if routeToSave.NetworkType == route.IPv6Network && routeToSave.Enabled && (!peer.Meta.Ipv6Supported || peer.V6Setting == nbpeer.V6Disabled) {
return status.Errorf(status.InvalidArgument, "peer with IPv6 disabled can't be used for IPv6 route")
}
}
if len(routeToSave.PeerGroups) > 0 { if len(routeToSave.PeerGroups) > 0 {
err = validateGroups(routeToSave.PeerGroups, account.Groups) err = validateGroups(routeToSave.PeerGroups, account.Groups)
if err != nil { if err != nil {
@ -239,8 +269,49 @@ func (am *DefaultAccountManager) SaveRoute(accountID, userID string, routeToSave
return err return err
} }
oldRoute := account.Routes[routeToSave.ID]
account.Routes[routeToSave.ID] = routeToSave account.Routes[routeToSave.ID] = routeToSave
// Check if old peer's IPv6 status needs to be recalculated.
// Must happen if route is an IPv6 route, and either:
// - The routing peer has changed
// - The route has been disabled
// - (the route has been enabled) => caught in the next if-block
if oldRoute.Peer != "" && routeToSave.NetworkType == route.IPv6Network && ((oldRoute.Enabled && !routeToSave.Enabled) || oldRoute.Peer != routeToSave.Peer) {
oldPeer := account.GetPeer(oldRoute.Peer)
if oldPeer.V6Setting == nbpeer.V6Auto {
changed, err := am.DeterminePeerV6(account, oldPeer)
if err != nil {
return err
}
if changed {
account.UpdatePeer(oldPeer)
}
}
}
// Check if new peer's IPv6 status needs to be recalculated.
// Must happen if route is an IPv6 route, and either:
// - The routing peer has changed
// - The route has been enabled
// - (The route has been disabled) => caught in previous if-block
if oldRoute.Peer != "" && routeToSave.NetworkType == route.IPv6Network && routeToSave.Enabled && (!oldRoute.Enabled || oldRoute.Peer != routeToSave.Peer) {
newPeer := account.GetPeer(routeToSave.Peer)
if newPeer.V6Setting == nbpeer.V6Disabled || !newPeer.Meta.Ipv6Supported {
return status.Errorf(
status.InvalidArgument,
"IPv6 must be enabled for peer %s to be used in route %s",
newPeer.Name, routeToSave.Network.String())
} else if newPeer.IP6 == nil {
_, err = am.DeterminePeerV6(account, newPeer)
if err != nil {
return err
}
account.UpdatePeer(newPeer)
}
}
account.Network.IncSerial() account.Network.IncSerial()
if err = am.Store.SaveAccount(account); err != nil { if err = am.Store.SaveAccount(account); err != nil {
return err return err
@ -269,6 +340,21 @@ func (am *DefaultAccountManager) DeleteRoute(accountID string, routeID route.ID,
} }
delete(account.Routes, routeID) delete(account.Routes, routeID)
// If the route was an IPv6 route, deleting it may update the automatic IPv6 enablement status of its routing peers,
// check if this is the case and update accordingly.
if routy.Peer != "" && routy.Enabled && routy.NetworkType == route.IPv6Network {
oldPeer := account.GetPeer(routy.Peer)
if oldPeer.V6Setting == nbpeer.V6Auto {
changed, err := am.DeterminePeerV6(account, oldPeer)
if err != nil {
return err
}
if changed {
account.UpdatePeer(oldPeer)
}
}
}
account.Network.IncSerial() account.Network.IncSerial()
if err = am.Store.SaveAccount(account); err != nil { if err = am.Store.SaveAccount(account); err != nil {
return err return err

View File

@ -319,6 +319,88 @@ func TestCreateRoute(t *testing.T) {
errFunc: require.Error, errFunc: require.Error,
shouldCreate: false, shouldCreate: false,
}, },
{
name: "IPv6 route on peer with disabled IPv6 should fail",
inputArgs: input{
network: "2001:db8:7654:3210::/64",
netID: "NewId",
peerKey: peer4ID,
description: "",
masquerade: false,
metric: 9999,
enabled: true,
groups: []string{routeGroup1},
},
errFunc: require.Error,
shouldCreate: false,
},
{
name: "IPv6 route on peer with unsupported IPv6 should fail",
inputArgs: input{
network: "2001:db8:7654:3210::/64",
netID: "NewId",
peerKey: peer5ID,
description: "",
masquerade: false,
metric: 9999,
enabled: true,
groups: []string{routeGroup1},
},
errFunc: require.Error,
shouldCreate: false,
},
{
name: "IPv6 route on peer with automatic IPv6 setting should succeed",
inputArgs: input{
network: "2001:db8:7654:3210::/64",
netID: "NewId",
peerKey: peer1ID,
description: "",
masquerade: false,
metric: 9999,
enabled: true,
groups: []string{routeGroup1},
},
errFunc: require.NoError,
shouldCreate: true,
expectedRoute: &route.Route{
Network: netip.MustParsePrefix("2001:db8:7654:3210::/64"),
NetworkType: route.IPv6Network,
NetID: "NewId",
Peer: peer1ID,
Description: "",
Masquerade: false,
Metric: 9999,
Enabled: true,
Groups: []string{routeGroup1},
},
},
{
name: "IPv6 route on peer with force enabled IPv6 setting should succeed",
inputArgs: input{
network: "2001:db8:7654:3211::/64",
netID: "NewId",
peerKey: peer2ID,
description: "",
masquerade: false,
metric: 9999,
enabled: true,
groups: []string{routeGroup1},
},
errFunc: require.NoError,
shouldCreate: true,
expectedRoute: &route.Route{
Network: netip.MustParsePrefix("2001:db8:7654:3211::/64"),
NetworkType: route.IPv6Network,
NetID: "NewId",
Peer: peer2ID,
Description: "",
Masquerade: false,
Metric: 9999,
Enabled: true,
Groups: []string{routeGroup1},
},
},
} }
for _, testCase := range testCases { for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
@ -377,6 +459,8 @@ func TestCreateRoute(t *testing.T) {
func TestSaveRoute(t *testing.T) { func TestSaveRoute(t *testing.T) {
validPeer := peer2ID validPeer := peer2ID
validUsedPeer := peer5ID validUsedPeer := peer5ID
ipv6DisabledPeer := peer4ID
ipv6UnsupportedPeer := peer5ID
invalidPeer := "nonExisting" invalidPeer := "nonExisting"
validPrefix := netip.MustParsePrefix("192.168.0.0/24") validPrefix := netip.MustParsePrefix("192.168.0.0/24")
invalidPrefix, _ := netip.ParsePrefix("192.168.0.0/34") invalidPrefix, _ := netip.ParsePrefix("192.168.0.0/34")
@ -467,7 +551,7 @@ func TestSaveRoute(t *testing.T) {
}, },
}, },
{ {
name: "Both peer and peers_roup Provided Should Fail", name: "Both peer and peers_group Provided Should Fail",
existingRoute: &route.Route{ existingRoute: &route.Route{
ID: "testingRoute", ID: "testingRoute",
Network: netip.MustParsePrefix("192.168.0.0/16"), Network: netip.MustParsePrefix("192.168.0.0/16"),
@ -517,6 +601,38 @@ func TestSaveRoute(t *testing.T) {
newPeer: &invalidPeer, newPeer: &invalidPeer,
errFunc: require.Error, errFunc: require.Error,
}, },
{
name: "IPv6 disabled host should not be allowed as peer for IPv6 route",
existingRoute: &route.Route{
ID: "testingRoute",
Network: netip.MustParsePrefix("2001:db8:4321:5678::/64"),
NetID: validNetID,
NetworkType: route.IPv6Network,
Description: "super",
Masquerade: false,
Metric: 9999,
Enabled: true,
Groups: []string{routeGroup1},
},
newPeer: &ipv6DisabledPeer,
errFunc: require.Error,
},
{
name: "IPv6 unsupported host should not be allowed as peer for IPv6 route",
existingRoute: &route.Route{
ID: "testingRoute",
Network: netip.MustParsePrefix("2001:db8:4321:5678::/64"),
NetID: validNetID,
NetworkType: route.IPv6Network,
Description: "super",
Masquerade: false,
Metric: 9999,
Enabled: true,
Groups: []string{routeGroup1},
},
newPeer: &ipv6UnsupportedPeer,
errFunc: require.Error,
},
{ {
name: "Invalid Metric Should Fail", name: "Invalid Metric Should Fail",
existingRoute: &route.Route{ existingRoute: &route.Route{
@ -772,7 +888,7 @@ func TestDeleteRoute(t *testing.T) {
ID: "testingRoute", ID: "testingRoute",
Network: netip.MustParsePrefix("192.168.0.0/16"), Network: netip.MustParsePrefix("192.168.0.0/16"),
NetworkType: route.IPv4Network, NetworkType: route.IPv4Network,
Peer: peer1Key, Peer: peer1ID,
Description: "super", Description: "super",
Masquerade: false, Masquerade: false,
Metric: 9999, Metric: 9999,
@ -812,6 +928,77 @@ func TestDeleteRoute(t *testing.T) {
} }
} }
func TestRouteIPv6Consistency(t *testing.T) {
testingRoute := &route.Route{
ID: "testingRoute",
Network: netip.MustParsePrefix("2001:db8:0987:6543::/64"),
NetworkType: route.IPv6Network,
NetID: existingRouteID,
Peer: peer1ID,
Description: "super",
Masquerade: false,
Metric: 9999,
Enabled: false,
Groups: []string{routeGroup1},
}
am, err := createRouterManager(t)
if err != nil {
t.Error("failed to create account manager")
}
account, err := initTestRouteAccount(t, am)
if err != nil {
t.Error("failed to init testing account")
}
account.Routes[testingRoute.ID] = testingRoute
err = am.Store.SaveAccount(account)
if err != nil {
t.Error("failed to save account")
}
savedAccount, err := am.Store.GetAccount(account.Id)
if err != nil {
t.Error("failed to retrieve saved account with error: ", err)
}
testingRoute = savedAccount.Routes[testingRoute.ID]
testingRoute.Enabled = true
err = am.SaveRoute(account.Id, userID, testingRoute)
if err != nil {
t.Error("failed to save route")
}
savedAccount, err = am.Store.GetAccount(account.Id)
if err != nil {
t.Error("failed to retrieve saved account with error: ", err)
}
peer := savedAccount.GetPeer(peer1ID)
require.NotNil(t, peer.IP6, "peer with enabled IPv6 route in automatic setting must have IPv6 active.")
err = am.DeleteRoute(account.Id, testingRoute.ID, userID)
if err != nil {
t.Error("deleting route failed with error: ", err)
}
savedAccount, err = am.Store.GetAccount(account.Id)
if err != nil {
t.Error("failed to retrieve saved account with error: ", err)
}
_, found := savedAccount.Routes[testingRoute.ID]
if found {
t.Error("route shouldn't be found after delete")
}
peer = savedAccount.GetPeer(peer1ID)
require.Nil(t, peer.IP6, "disabling the only IPv6 route for a peer with automatic IPv6 setting should disable IPv6")
}
func TestGetNetworkMap_RouteSyncPeerGroups(t *testing.T) { func TestGetNetworkMap_RouteSyncPeerGroups(t *testing.T) {
baseRoute := &route.Route{ baseRoute := &route.Route{
Network: netip.MustParsePrefix("192.168.0.0/16"), Network: netip.MustParsePrefix("192.168.0.0/16"),
@ -1055,14 +1242,15 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
Name: "test-host1@netbird.io", Name: "test-host1@netbird.io",
UserID: userID, UserID: userID,
Meta: nbpeer.PeerSystemMeta{ Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host1@netbird.io", Hostname: "test-host1@netbird.io",
GoOS: "linux", GoOS: "linux",
Kernel: "Linux", Kernel: "Linux",
Core: "21.04", Core: "21.04",
Platform: "x86_64", Platform: "x86_64",
OS: "Ubuntu", OS: "Ubuntu",
WtVersion: "development", WtVersion: "development",
UIVersion: "development", UIVersion: "development",
Ipv6Supported: true,
}, },
Status: &nbpeer.PeerStatus{}, Status: &nbpeer.PeerStatus{},
} }
@ -1073,24 +1261,32 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
if err != nil { if err != nil {
return nil, err return nil, err
} }
ips = account.getTakenIP6s()
peer2IP6, err := AllocatePeerIP6(account.Network.Net, ips)
if err != nil {
return nil, err
}
peer2 := &nbpeer.Peer{ peer2 := &nbpeer.Peer{
IP: peer2IP, IP: peer2IP,
IP6: &peer2IP6,
ID: peer2ID, ID: peer2ID,
Key: peer2Key, Key: peer2Key,
Name: "test-host2@netbird.io", Name: "test-host2@netbird.io",
UserID: userID, UserID: userID,
Meta: nbpeer.PeerSystemMeta{ Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host2@netbird.io", Hostname: "test-host2@netbird.io",
GoOS: "linux", GoOS: "linux",
Kernel: "Linux", Kernel: "Linux",
Core: "21.04", Core: "21.04",
Platform: "x86_64", Platform: "x86_64",
OS: "Ubuntu", OS: "Ubuntu",
WtVersion: "development", WtVersion: "development",
UIVersion: "development", UIVersion: "development",
Ipv6Supported: true,
}, },
Status: &nbpeer.PeerStatus{}, V6Setting: nbpeer.V6Enabled,
Status: &nbpeer.PeerStatus{},
} }
account.Peers[peer2.ID] = peer2 account.Peers[peer2.ID] = peer2
@ -1099,24 +1295,32 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
if err != nil { if err != nil {
return nil, err return nil, err
} }
ips = account.getTakenIP6s()
peer3IP6, err := AllocatePeerIP6(account.Network.Net, ips)
if err != nil {
return nil, err
}
peer3 := &nbpeer.Peer{ peer3 := &nbpeer.Peer{
IP: peer3IP, IP: peer3IP,
IP6: &peer3IP6,
ID: peer3ID, ID: peer3ID,
Key: peer3Key, Key: peer3Key,
Name: "test-host3@netbird.io", Name: "test-host3@netbird.io",
UserID: userID, UserID: userID,
Meta: nbpeer.PeerSystemMeta{ Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host3@netbird.io", Hostname: "test-host3@netbird.io",
GoOS: "darwin", GoOS: "darwin",
Kernel: "Darwin", Kernel: "Darwin",
Core: "13.4.1", Core: "13.4.1",
Platform: "arm64", Platform: "arm64",
OS: "darwin", OS: "darwin",
WtVersion: "development", WtVersion: "development",
UIVersion: "development", UIVersion: "development",
Ipv6Supported: true,
}, },
Status: &nbpeer.PeerStatus{}, V6Setting: nbpeer.V6Enabled,
Status: &nbpeer.PeerStatus{},
} }
account.Peers[peer3.ID] = peer3 account.Peers[peer3.ID] = peer3
@ -1133,14 +1337,15 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
Name: "test-host4@netbird.io", Name: "test-host4@netbird.io",
UserID: userID, UserID: userID,
Meta: nbpeer.PeerSystemMeta{ Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host4@netbird.io", Hostname: "test-host4@netbird.io",
GoOS: "linux", GoOS: "linux",
Kernel: "Linux", Kernel: "Linux",
Core: "21.04", Core: "21.04",
Platform: "x86_64", Platform: "x86_64",
OS: "Ubuntu", OS: "Ubuntu",
WtVersion: "development", WtVersion: "development",
UIVersion: "development", UIVersion: "development",
Ipv6Supported: false,
}, },
Status: &nbpeer.PeerStatus{}, Status: &nbpeer.PeerStatus{},
} }
@ -1159,16 +1364,18 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
Name: "test-host4@netbird.io", Name: "test-host4@netbird.io",
UserID: userID, UserID: userID,
Meta: nbpeer.PeerSystemMeta{ Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host4@netbird.io", Hostname: "test-host4@netbird.io",
GoOS: "linux", GoOS: "linux",
Kernel: "Linux", Kernel: "Linux",
Core: "21.04", Core: "21.04",
Platform: "x86_64", Platform: "x86_64",
OS: "Ubuntu", OS: "Ubuntu",
WtVersion: "development", WtVersion: "development",
UIVersion: "development", UIVersion: "development",
Ipv6Supported: true,
}, },
Status: &nbpeer.PeerStatus{}, Status: &nbpeer.PeerStatus{},
V6Setting: nbpeer.V6Disabled,
} }
account.Peers[peer5.ID] = peer5 account.Peers[peer5.ID] = peer5