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
85 changed files with 4311 additions and 795 deletions
+33 -4
View File
@@ -280,7 +280,8 @@ func (a *Account) getRoutesToSync(peerID string, aclPeers []*nbpeer.Peer) []*rou
groupListMap := a.getPeerGroups(peerID)
for _, peer := range aclPeers {
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)
routes = append(routes, filteredRoutes...)
}
@@ -315,6 +316,20 @@ func (a *Account) filterRoutesByGroups(routes []*route.Route, groupListMap looku
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
// 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.
@@ -348,6 +363,10 @@ func (a *Account) getRoutingPeerRoutes(peerID string) (enabledRoutes []*route.Ro
}
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 {
group := a.GetGroup(groupID)
if group == nil {
@@ -429,7 +448,7 @@ func (a *Account) GetPeerNetworkMap(peerID, dnsDomain string, validatedPeersMap
if dnsManagementStatus {
var zones []nbdns.CustomZone
peersCustomZone := getPeersCustomZone(a, dnsDomain)
peersCustomZone := getPeersCustomZone(a, dnsDomain, peer.IP6 != nil)
if peersCustomZone.Domain != "" {
zones = append(zones, peersCustomZone)
}
@@ -665,6 +684,17 @@ func (a *Account) getTakenIPs() []net.IP {
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 {
existingLabels := make(lookupMap)
for _, peer := range a.Peers {
@@ -1006,7 +1036,6 @@ func (am *DefaultAccountManager) UpdateAccountSettings(accountID, userID string,
}
updatedAccount := account.UpdateSettings(newSettings)
err = am.Store.SaveAccount(account)
if err != nil {
return nil, err
@@ -2015,7 +2044,7 @@ func addAllGroup(account *Account) error {
func newAccountWithId(accountID, userID, domain string) *Account {
log.Debugf("creating new account")
network := NewNetwork()
network := NewNetwork(true)
peers := make(map[string]*nbpeer.Peer)
users := make(map[string]*User)
routes := make(map[route.ID]*route.Route)
+191 -83
View File
@@ -69,14 +69,15 @@ func verifyCanAddPeerToAccount(t *testing.T, manager AccountManager, account *Ac
Key: "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8=",
Name: "test-host@netbird.io",
Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Hostname: "test-host@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Ipv6Supported: false,
},
}
@@ -964,78 +965,136 @@ func TestAccountManager_DeleteAccount(t *testing.T) {
}
func TestAccountManager_AddPeer(t *testing.T) {
manager, err := createManager(t)
if err != nil {
t.Fatal(err)
return
testCases := []struct {
name string
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"
account, err := createAccount(manager, "test_account", userID, "netbird.cloud")
if err != nil {
t.Fatal(err)
for _, c := range testCases {
t.Run(c.name, func(t *testing.T) {
manager, err := createManager(t)
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) {
@@ -1455,9 +1514,22 @@ func TestAccount_GetRoutesToSync(t *testing.T) {
if err != nil {
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{
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"}}},
Routes: map[route.ID]*route.Route{
@@ -1467,7 +1539,7 @@ func TestAccount_GetRoutesToSync(t *testing.T) {
NetID: "network-1",
Description: "network-1",
Peer: "peer-1",
NetworkType: 0,
NetworkType: route.IPv4Network,
Masquerade: false,
Metric: 999,
Enabled: true,
@@ -1479,7 +1551,7 @@ func TestAccount_GetRoutesToSync(t *testing.T) {
NetID: "network-2",
Description: "network-2",
Peer: "peer-2",
NetworkType: 0,
NetworkType: route.IPv4Network,
Masquerade: false,
Metric: 999,
Enabled: true,
@@ -1491,7 +1563,31 @@ func TestAccount_GetRoutesToSync(t *testing.T) {
NetID: "network-1",
Description: "network-1",
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,
Metric: 999,
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)
routeIDs := make(map[route.ID]struct{}, 2)
for _, r := range routes {
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-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)
}
+12
View File
@@ -139,6 +139,14 @@ const (
PostureCheckUpdated Activity = 61
// PostureCheckDeleted indicates that the user deleted a posture check
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{
@@ -205,6 +213,10 @@ var activityMap = map[Activity]Code{
PostureCheckCreated: {"Posture check created", "posture.check.created"},
PostureCheckUpdated: {"Posture check updated", "posture.check.updated"},
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
+24 -3
View File
@@ -149,7 +149,7 @@ func toProtocolDNSConfig(update nbdns.Config) *proto.DNSConfig {
return protoUpdate
}
func getPeersCustomZone(account *Account, dnsDomain string) nbdns.CustomZone {
func getPeersCustomZone(account *Account, dnsDomain string, enableIPv6 bool) nbdns.CustomZone {
if dnsDomain == "" {
log.Errorf("no dns domain is set, returning empty zone")
return nbdns.CustomZone{}
@@ -172,6 +172,16 @@ func getPeersCustomZone(account *Account, dnsDomain string) nbdns.CustomZone {
TTL: defaultTTL,
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
@@ -179,6 +189,7 @@ func getPeersCustomZone(account *Account, dnsDomain string) nbdns.CustomZone {
func getPeerNSGroups(account *Account, peerID string) []*nbdns.NameServerGroup {
groupList := account.getPeerGroups(peerID)
peer := account.GetPeer(peerID)
var peerNSGroups []*nbdns.NameServerGroup
@@ -189,8 +200,18 @@ func getPeerNSGroups(account *Account, peerID string) []*nbdns.NameServerGroup {
for _, gID := range nsGroup.Groups {
_, found := groupList[gID]
if found {
if !peerIsNameserver(account.GetPeer(peerID), nsGroup) {
peerNSGroups = append(peerNSGroups, nsGroup.Copy())
if !peerIsNameserver(peer, nsGroup) {
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
}
}
+36 -18
View File
@@ -22,6 +22,7 @@ const (
dnsAdminUserID = "testingAdminUser"
dnsRegularUserID = "testingRegularUser"
dnsNSGroup1 = "ns1"
dnsNSGroup2 = "ns2"
)
func TestGetDNSSettings(t *testing.T) {
@@ -184,7 +185,7 @@ func TestGetNetworkMap_DNSConfigSync(t *testing.T) {
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.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) {
@@ -215,14 +216,15 @@ func initTestDNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, erro
Key: dnsPeer1Key,
Name: "test-host1@netbird.io",
Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host1@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Hostname: "test-host1@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Ipv6Supported: false,
},
DNSLabel: dnsPeer1Key,
}
@@ -230,16 +232,18 @@ func initTestDNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, erro
Key: dnsPeer2Key,
Name: "test-host2@netbird.io",
Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host2@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Hostname: "test-host2@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Ipv6Supported: true,
},
DNSLabel: dnsPeer2Key,
V6Setting: nbpeer.V6Enabled,
DNSLabel: dnsPeer2Key,
}
domain := "example.com"
@@ -312,6 +316,20 @@ func initTestDNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, erro
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)
if err != nil {
return nil, err
+78 -7
View File
@@ -149,6 +149,35 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *n
oldGroup, exists := account.Groups[newGroup.ID]
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()
if err = am.Store.SaveAccount(account); err != nil {
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.
// It has to happen after all the operations have been successfully performed.
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...)
if !exists {
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)
// 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()
if err = am.Store.SaveAccount(account); err != nil {
return err
@@ -345,6 +376,7 @@ func (am *DefaultAccountManager) ListGroups(accountID string) ([]*nbgroup.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 {
unlock := am.Store.AcquireAccountWriteLock(accountID)
defer unlock()
@@ -368,6 +400,14 @@ func (am *DefaultAccountManager) GroupAddPeer(accountID, groupID, peerID string)
}
if add {
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()
@@ -381,6 +421,7 @@ func (am *DefaultAccountManager) GroupAddPeer(accountID, groupID, peerID string)
}
// 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 {
unlock := am.Store.AcquireAccountWriteLock(accountID)
defer unlock()
@@ -399,6 +440,15 @@ func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerID stri
for i, itemID := range group.Peers {
if itemID == peerID {
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 {
return err
}
@@ -409,3 +459,24 @@ func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerID stri
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
}
+3
View File
@@ -25,6 +25,8 @@ type Group struct {
// Peers list of the group
Peers []string `gorm:"serializer:json"`
IPv6Enabled bool
IntegrationReference integration_reference.IntegrationReference `gorm:"embedded;embeddedPrefix:integration_ref_"`
}
@@ -38,6 +40,7 @@ func (g *Group) Copy() *Group {
ID: g.ID,
Name: g.Name,
Issued: g.Issued,
IPv6Enabled: g.IPv6Enabled,
Peers: make([]string, len(g.Peers)),
IntegrationReference: g.IntegrationReference,
}
+149 -5
View File
@@ -2,6 +2,8 @@ package server
import (
"errors"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/stretchr/testify/require"
"testing"
nbdns "github.com/netbirdio/netbird/dns"
@@ -12,6 +14,8 @@ import (
const (
groupAdminUserID = "testingAdminUser"
groupPeer1Key = "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8="
groupPeer2Key = "/yF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NyI="
)
func TestDefaultAccountManager_CreateGroup(t *testing.T) {
@@ -20,7 +24,7 @@ func TestDefaultAccountManager_CreateGroup(t *testing.T) {
t.Error("failed to create account manager")
}
account, err := initTestGroupAccount(am)
_, account, err := initTestGroupAccount(am)
if err != nil {
t.Error("failed to init testing account")
}
@@ -55,7 +59,7 @@ func TestDefaultAccountManager_DeleteGroup(t *testing.T) {
t.Error("failed to create account manager")
}
account, err := initTestGroupAccount(am)
_, account, err := initTestGroupAccount(am)
if err != nil {
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"
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{
ID: "grp-for-route",
AccountID: "account-id",
@@ -191,6 +322,14 @@ func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) {
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{
ID: "example route",
Groups: []string{groupForRoute.ID},
@@ -235,7 +374,7 @@ func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) {
err := am.Store.SaveAccount(account)
if err != nil {
return nil, err
return nil, nil, err
}
_ = am.SaveGroup(accountID, groupAdminUserID, groupForRoute)
@@ -245,6 +384,11 @@ func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) {
_ = am.SaveGroup(accountID, groupAdminUserID, groupForSetupKeys)
_ = am.SaveGroup(accountID, groupAdminUserID, groupForUsers)
_ = 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
}
+15 -4
View File
@@ -287,6 +287,7 @@ func extractPeerMeta(loginReq *proto.LoginRequest) nbpeer.PeerSystemMeta {
Cloud: loginReq.GetMeta().GetEnvironment().GetCloud(),
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 {
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)
return &proto.PeerConfig{
Address: fmt.Sprintf("%s/%d", peer.IP.String(), netmask), // take it from the network
Address6: address6,
SshConfig: &proto.SSHConfig{SshEnabled: peer.SSHEnabled},
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{}
for _, rPeer := range peers {
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{
WgPubKey: rPeer.Key,
AllowedIps: []string{fmt.Sprintf(AllowedIPsFormat, rPeer.IP)},
AllowedIps: allowedIps,
SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
Fqdn: fqdn,
})
@@ -482,13 +493,13 @@ func toSyncResponse(config *Config, peer *nbpeer.Peer, turnCredentials *TURNCred
pConfig := toPeerConfig(peer, networkMap.Network, dnsName)
remotePeers := toRemotePeerConfig(networkMap.Peers, dnsName)
remotePeers := toRemotePeerConfig(networkMap.Peers, dnsName, peer.IP6 != nil)
routesUpdate := toProtocolRoutes(networkMap.Routes)
dnsUpdate := toProtocolDNSConfig(networkMap.DNSConfig)
offlinePeers := toRemotePeerConfig(networkMap.OfflinePeers, dnsName)
offlinePeers := toRemotePeerConfig(networkMap.OfflinePeers, dnsName, peer.IP6 != nil)
firewallRules := toProtocolFirewallRules(networkMap.FirewallRules)
@@ -62,7 +62,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
handler := initAccountsTestData(&server.Account{
Id: accountID,
Domain: "hotmail.com",
Network: server.NewNetwork(),
Network: server.NewNetwork(true),
Users: map[string]*server.User{
adminUser.Id: adminUser,
},
+28
View File
@@ -247,10 +247,15 @@ components:
description: (Cloud only) Indicates whether peer needs approval
type: boolean
example: true
ipv6_enabled:
type: string
enum: [enabled, disabled, auto]
example: auto
required:
- name
- ssh_enabled
- login_expiration_enabled
- ipv6_enabled
PeerBase:
allOf:
- $ref: '#/components/schemas/PeerMinimum'
@@ -264,6 +269,10 @@ components:
description: Peer's public connection IP address
type: string
example: 35.64.0.1
ip6:
description: Peer's IPv6 address
type: string
example: 2001:db8::0123:4567:890a:bcde
connected:
description: Peer to Management connection status
type: boolean
@@ -310,6 +319,15 @@ components:
description: Peer's desktop UI version
type: string
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:
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
@@ -352,6 +370,8 @@ components:
- kernel_version
- last_login
- last_seen
- ipv6_supported
- ipv6_enabled
- login_expiration_enabled
- login_expired
- os
@@ -627,8 +647,12 @@ components:
items:
type: string
example: "ch8i4ug6lnn4g9hqv7m1"
ipv6_enabled:
description: Whether IPv6 should be enabled for all members with IPv6 set to "auto"
type: boolean
required:
- name
- ipv6_enabled
Group:
allOf:
- $ref: '#/components/schemas/GroupMinimum'
@@ -639,8 +663,12 @@ components:
type: array
items:
$ref: '#/components/schemas/PeerMinimum'
ipv6_enabled:
description: Whether IPv6 should be enabled for all members with IPv6 set to "auto"
type: boolean
required:
- peers
- ipv6_enabled
PolicyRuleMinimum:
type: object
properties:
+78 -4
View File
@@ -88,12 +88,40 @@ const (
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.
const (
PeerNetworkRangeCheckActionAllow PeerNetworkRangeCheckAction = "allow"
PeerNetworkRangeCheckActionDeny PeerNetworkRangeCheckAction = "deny"
)
// Defines values for PeerRequestIpv6Enabled.
const (
PeerRequestIpv6EnabledAuto PeerRequestIpv6Enabled = "auto"
PeerRequestIpv6EnabledDisabled PeerRequestIpv6Enabled = "disabled"
PeerRequestIpv6EnabledEnabled PeerRequestIpv6Enabled = "enabled"
)
// Defines values for PolicyRuleAction.
const (
PolicyRuleActionAccept PolicyRuleAction = "accept"
@@ -307,6 +335,9 @@ type Group struct {
// Id Group 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 *GroupIssued `json:"issued,omitempty"`
@@ -343,6 +374,9 @@ type GroupMinimumIssued string
// GroupRequest defines model for GroupRequest.
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 string `json:"name"`
@@ -502,6 +536,15 @@ type Peer struct {
// Ip Peer's IP address
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 string `json:"kernel_version"`
@@ -539,6 +582,9 @@ type Peer struct {
Version string `json:"version"`
}
// PeerIpv6Enabled Whether IPv6 is enabled for this peer.
type PeerIpv6Enabled string
// PeerBase defines model for PeerBase.
type PeerBase struct {
// ApprovalRequired (Cloud only) Indicates whether peer needs approval
@@ -574,6 +620,15 @@ type PeerBase struct {
// Ip Peer's IP address
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 string `json:"kernel_version"`
@@ -611,6 +666,9 @@ type PeerBase struct {
Version string `json:"version"`
}
// PeerBaseIpv6Enabled Whether IPv6 is enabled for this peer.
type PeerBaseIpv6Enabled string
// PeerBatch defines model for PeerBatch.
type PeerBatch struct {
// AccessiblePeersCount Number of accessible peers
@@ -649,6 +707,15 @@ type PeerBatch struct {
// Ip Peer's IP address
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 string `json:"kernel_version"`
@@ -686,6 +753,9 @@ type PeerBatch struct {
Version string `json:"version"`
}
// PeerBatchIpv6Enabled Whether IPv6 is enabled for this peer.
type PeerBatchIpv6Enabled string
// PeerMinimum defines model for PeerMinimum.
type PeerMinimum struct {
// Id Peer ID
@@ -710,12 +780,16 @@ type PeerNetworkRangeCheckAction string
// PeerRequest defines model for PeerRequest.
type PeerRequest struct {
// ApprovalRequired (Cloud only) Indicates whether peer needs approval
ApprovalRequired *bool `json:"approval_required,omitempty"`
LoginExpirationEnabled bool `json:"login_expiration_enabled"`
Name string `json:"name"`
SshEnabled bool `json:"ssh_enabled"`
ApprovalRequired *bool `json:"approval_required,omitempty"`
Ipv6Enabled PeerRequestIpv6Enabled `json:"ipv6_enabled"`
LoginExpirationEnabled bool `json:"login_expiration_enabled"`
Name string `json:"name"`
SshEnabled bool `json:"ssh_enabled"`
}
// PeerRequestIpv6Enabled defines model for PeerRequest.Ipv6Enabled.
type PeerRequestIpv6Enabled string
// PersonalAccessToken defines model for PersonalAccessToken.
type PersonalAccessToken struct {
// CreatedAt Date the token was created
+32 -10
View File
@@ -3,6 +3,7 @@ package http
import (
"encoding/json"
"net/http"
"slices"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
@@ -82,16 +83,6 @@ func (h *GroupsHandler) UpdateGroup(w http.ResponseWriter, r *http.Request) {
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
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
@@ -110,12 +101,42 @@ func (h *GroupsHandler) UpdateGroup(w http.ResponseWriter, r *http.Request) {
} else {
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{
ID: groupID,
Name: req.Name,
Peers: peers,
Issued: eg.Issued,
IntegrationReference: eg.IntegrationReference,
IPv6Enabled: req.Ipv6Enabled,
}
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,
Name: group.Name,
Issued: (*api.GroupIssued)(&group.Issued),
Ipv6Enabled: group.IPv6Enabled,
}
for _, pid := range group.Peers {
+31
View File
@@ -85,11 +85,17 @@ func (h *PeersHandler) updatePeer(account *server.Account, user *server.User, pe
return
}
v6Status := nbpeer.V6Auto
if req.Ipv6Enabled != api.PeerRequestIpv6EnabledAuto {
v6Status = nbpeer.V6Status(req.Ipv6Enabled)
}
update := &nbpeer.Peer{
ID: peerID,
SSHEnabled: req.SshEnabled,
Name: req.Name,
LoginExpirationEnabled: req.LoginExpirationEnabled,
V6Setting: v6Status,
}
if req.ApprovalRequired != nil {
@@ -284,11 +290,22 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD
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{
Id: peer.ID,
Name: peer.Name,
Ip: peer.IP.String(),
ConnectionIp: peer.Location.ConnectionIP.String(),
Ip6: ip6,
Connected: peer.Status.Connected,
LastSeen: peer.Status.LastSeen,
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,
UserId: peer.UserID,
UiVersion: peer.Meta.UIVersion,
Ipv6Supported: peer.Meta.Ipv6Supported,
Ipv6Enabled: v6Status,
DnsLabel: fqdn(peer, dnsDomain),
LoginExpirationEnabled: peer.LoginExpirationEnabled,
LastLogin: peer.LastLogin,
@@ -317,12 +336,22 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn
if osVersion == "" {
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{
Id: peer.ID,
Name: peer.Name,
Ip: peer.IP.String(),
ConnectionIp: peer.Location.ConnectionIP.String(),
Ip6: ip6,
Connected: peer.Status.Connected,
LastSeen: peer.Status.LastSeen,
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,
UserId: peer.UserID,
UiVersion: peer.Meta.UIVersion,
Ipv6Supported: peer.Meta.Ipv6Supported,
Ipv6Enabled: v6Status,
DnsLabel: fqdn(peer, dnsDomain),
LoginExpirationEnabled: peer.LoginExpirationEnabled,
LastLogin: peer.LastLogin,
@@ -37,6 +37,12 @@ func initTestMetaData(peers ...*nbpeer.Peer) *PeersHandler {
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.LoginExpirationEnabled = update.LoginExpirationEnabled
p.Name = update.Name
+2
View File
@@ -789,6 +789,7 @@ func initTestNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, error
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Ipv6Supported: false,
},
}
peer2 := &nbpeer.Peer{
@@ -803,6 +804,7 @@ func initTestNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, error
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Ipv6Supported: false,
},
}
existingNSGroup := nbdns.NameServerGroup{
+78 -11
View File
@@ -1,13 +1,13 @@
package server
import (
crand "crypto/rand"
"encoding/binary"
"github.com/c-robinson/iplib"
"github.com/rs/xid"
"math/rand"
"net"
"sync"
"time"
"github.com/c-robinson/iplib"
"github.com/rs/xid"
nbdns "github.com/netbirdio/netbird/dns"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
@@ -20,11 +20,21 @@ const (
SubnetSize = 16
// NetSize is a global network size 100.64.0.0/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 = "%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 {
Peers []*nbpeer.Peer
Network *Network
@@ -37,6 +47,7 @@ type NetworkMap struct {
type Network struct {
Identifier string `json:"id"`
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
// 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.
@@ -45,24 +56,54 @@ type Network struct {
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
// 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)
sub, _ := n.Subnet(SubnetSize)
rngLock.Lock()
intn := rng.Intn(len(sub))
rngLock.Unlock()
s := rand.NewSource(time.Now().Unix())
r := rand.New(s)
intn := r.Intn(len(sub))
var n6 *net.IPNet = nil
if enableV6 {
n6 = GenerateNetwork6()
}
return &Network{
Identifier: xid.New().String(),
Net: sub[intn].IPNet,
Net6: n6,
Dns: "",
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
func (n *Network) IncSerial() {
n.mu.Lock()
@@ -81,6 +122,7 @@ func (n *Network) Copy() *Network {
return &Network{
Identifier: n.Identifier,
Net: n.Net,
Net6: n.Net6,
Dns: n.Dns,
Serial: n.Serial,
}
@@ -103,13 +145,38 @@ func AllocatePeerIP(ipNet net.IPNet, takenIps []net.IP) (net.IP, error) {
}
// pick a random IP
s := rand.NewSource(time.Now().Unix())
r := rand.New(s)
intn := r.Intn(len(ips))
rngLock.Lock()
intn := rng.Intn(len(ips))
rngLock.Unlock()
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
func generateIPs(ipNet *net.IPNet, exclusions map[string]struct{}) ([]net.IP, int) {
+37 -2
View File
@@ -1,6 +1,7 @@
package server
import (
"github.com/stretchr/testify/require"
"net"
"testing"
@@ -8,11 +9,19 @@ import (
)
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
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) {
@@ -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) {
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": {}})
+91 -1
View File
@@ -3,6 +3,7 @@ package server
import (
"fmt"
"net"
"slices"
"strings"
"time"
@@ -13,6 +14,7 @@ import (
"github.com/netbirdio/netbird/management/server/activity"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"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.
@@ -140,7 +142,66 @@ func (am *DefaultAccountManager) MarkPeerConnected(peerPubKey string, connected
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) {
unlock := am.Store.AcquireAccountWriteLock(accountID)
defer unlock()
@@ -160,6 +221,20 @@ func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *nb
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 {
peer.SSHEnabled = update.SSHEnabled
event := activity.PeerSSHEnabled
@@ -431,6 +506,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *nbpeer.P
Key: peer.Key,
SetupKey: upperKey,
IP: nextIp,
IP6: nil,
Meta: peer.Meta,
Name: peer.Meta.Hostname,
DNSLabel: newLabel,
@@ -443,6 +519,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *nbpeer.P
LoginExpirationEnabled: addedByUser,
Ephemeral: ephemeral,
Location: peer.Location,
V6Setting: peer.V6Setting, // empty string "" corresponds to "auto"
}
// 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)
_, err = am.DeterminePeerV6(account, newPeer)
if err != nil {
return nil, nil, err
}
if addedByUser {
user, err := account.FindUser(userID)
if err != nil {
@@ -676,6 +758,14 @@ func (am *DefaultAccountManager) LoginPeer(login PeerLogin) (*nbpeer.Peer, *Netw
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)
if err != nil {
return nil, nil, err
+21 -2
View File
@@ -20,6 +20,8 @@ type Peer struct {
SetupKey string
// IP address of the Peer
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 PeerSystemMeta `gorm:"embedded;embeddedPrefix:meta_"`
// Name is peer's name (machine name)
@@ -44,10 +46,23 @@ type Peer struct {
CreatedAt time.Time
// Indicate ephemeral peer attribute
Ephemeral bool
// Geo location based on connection IP
// Geolocation based on connection IP
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
// LastSeen is the last time peer was connected to the management service
LastSeen time.Time
@@ -96,6 +111,7 @@ type PeerSystemMeta struct { //nolint:revive
SystemProductName string
SystemManufacturer string
Environment Environment `gorm:"serializer:json"`
Ipv6Supported bool
}
func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool {
@@ -130,7 +146,8 @@ func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool {
p.SystemProductName == other.SystemProductName &&
p.SystemManufacturer == other.SystemManufacturer &&
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.
@@ -150,6 +167,7 @@ func (p *Peer) Copy() *Peer {
Key: p.Key,
SetupKey: p.SetupKey,
IP: p.IP,
IP6: p.IP6,
Meta: p.Meta,
Name: p.Name,
DNSLabel: p.DNSLabel,
@@ -162,6 +180,7 @@ func (p *Peer) Copy() *Peer {
CreatedAt: p.CreatedAt,
Ephemeral: p.Ephemeral,
Location: p.Location,
V6Setting: p.V6Setting,
}
}
+107
View File
@@ -1,6 +1,7 @@
package server
import (
"github.com/stretchr/testify/require"
"testing"
"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) {
// TODO: disable until we start use policy again
t.Skip()
+10
View File
@@ -195,6 +195,8 @@ type FirewallRule struct {
// PeerIP of the peer
PeerIP string
PeerIP6 string
// Direction of the traffic
Direction int
@@ -278,8 +280,14 @@ func (a *Account) connResourcesGenerator() (func(*PolicyRule, []*nbpeer.Peer, in
peersExists[peer.ID] = struct{}{}
}
ip6 := ""
if peer.IP6 != nil {
ip6 = peer.IP6.String()
}
fr := FirewallRule{
PeerIP: peer.IP.String(),
PeerIP6: ip6,
Direction: direction,
Action: string(rule.Action),
Protocol: string(rule.Protocol),
@@ -287,6 +295,7 @@ func (a *Account) connResourcesGenerator() (func(*PolicyRule, []*nbpeer.Peer, in
if isAll {
fr.PeerIP = "0.0.0.0"
fr.PeerIP6 = "::"
}
ruleID := (rule.ID + fr.PeerIP + strconv.Itoa(direction) +
@@ -474,6 +483,7 @@ func toProtocolFirewallRules(update []*FirewallRule) []*proto.FirewallRule {
result[i] = &proto.FirewallRule{
PeerIP: update[i].PeerIP,
PeerIP6: update[i].PeerIP6,
Direction: direction,
Action: action,
Protocol: protocol,
+9
View File
@@ -14,16 +14,20 @@ import (
)
func TestAccount_getPeersByPolicy(t *testing.T) {
peerAIP6 := net.ParseIP("2001:db8:abcd:1234::2")
peerBIP6 := net.ParseIP("2001:db8:abcd:1234::3")
account := &Account{
Peers: map[string]*nbpeer.Peer{
"peerA": {
ID: "peerA",
IP: net.ParseIP("100.65.14.88"),
IP6: &peerAIP6,
Status: &nbpeer.PeerStatus{},
},
"peerB": {
ID: "peerB",
IP: net.ParseIP("100.65.80.39"),
IP6: &peerBIP6,
Status: &nbpeer.PeerStatus{},
},
"peerC": {
@@ -161,6 +165,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
epectedFirewallRules := []*FirewallRule{
{
PeerIP: "0.0.0.0",
PeerIP6: "::",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "all",
@@ -168,6 +173,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
},
{
PeerIP: "0.0.0.0",
PeerIP6: "::",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "all",
@@ -175,6 +181,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
},
{
PeerIP: "100.65.14.88",
PeerIP6: "2001:db8:abcd:1234::2",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "all",
@@ -182,6 +189,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
},
{
PeerIP: "100.65.14.88",
PeerIP6: "2001:db8:abcd:1234::2",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "all",
@@ -678,6 +686,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
expectedFirewallRules := []*FirewallRule{
{
PeerIP: "0.0.0.0",
PeerIP6: "::",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "tcp",
+86
View File
@@ -1,6 +1,7 @@
package server
import (
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"net/netip"
"unicode/utf8"
@@ -180,6 +181,25 @@ func (am *DefaultAccountManager) CreateRoute(accountID, network, peerID string,
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()
if err = am.Store.SaveAccount(account); err != nil {
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")
}
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 {
err = validateGroups(routeToSave.PeerGroups, account.Groups)
if err != nil {
@@ -239,8 +269,49 @@ func (am *DefaultAccountManager) SaveRoute(accountID, userID string, routeToSave
return err
}
oldRoute := account.Routes[routeToSave.ID]
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()
if err = am.Store.SaveAccount(account); err != nil {
return err
@@ -269,6 +340,21 @@ func (am *DefaultAccountManager) DeleteRoute(accountID string, routeID route.ID,
}
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()
if err = am.Store.SaveAccount(account); err != nil {
return err
+252 -45
View File
@@ -319,6 +319,88 @@ func TestCreateRoute(t *testing.T) {
errFunc: require.Error,
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 {
t.Run(testCase.name, func(t *testing.T) {
@@ -377,6 +459,8 @@ func TestCreateRoute(t *testing.T) {
func TestSaveRoute(t *testing.T) {
validPeer := peer2ID
validUsedPeer := peer5ID
ipv6DisabledPeer := peer4ID
ipv6UnsupportedPeer := peer5ID
invalidPeer := "nonExisting"
validPrefix := netip.MustParsePrefix("192.168.0.0/24")
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{
ID: "testingRoute",
Network: netip.MustParsePrefix("192.168.0.0/16"),
@@ -517,6 +601,38 @@ func TestSaveRoute(t *testing.T) {
newPeer: &invalidPeer,
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",
existingRoute: &route.Route{
@@ -772,7 +888,7 @@ func TestDeleteRoute(t *testing.T) {
ID: "testingRoute",
Network: netip.MustParsePrefix("192.168.0.0/16"),
NetworkType: route.IPv4Network,
Peer: peer1Key,
Peer: peer1ID,
Description: "super",
Masquerade: false,
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) {
baseRoute := &route.Route{
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",
UserID: userID,
Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host1@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Hostname: "test-host1@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Ipv6Supported: true,
},
Status: &nbpeer.PeerStatus{},
}
@@ -1073,24 +1261,32 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
if err != nil {
return nil, err
}
ips = account.getTakenIP6s()
peer2IP6, err := AllocatePeerIP6(account.Network.Net, ips)
if err != nil {
return nil, err
}
peer2 := &nbpeer.Peer{
IP: peer2IP,
IP6: &peer2IP6,
ID: peer2ID,
Key: peer2Key,
Name: "test-host2@netbird.io",
UserID: userID,
Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host2@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Hostname: "test-host2@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Ipv6Supported: true,
},
Status: &nbpeer.PeerStatus{},
V6Setting: nbpeer.V6Enabled,
Status: &nbpeer.PeerStatus{},
}
account.Peers[peer2.ID] = peer2
@@ -1099,24 +1295,32 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
if err != nil {
return nil, err
}
ips = account.getTakenIP6s()
peer3IP6, err := AllocatePeerIP6(account.Network.Net, ips)
if err != nil {
return nil, err
}
peer3 := &nbpeer.Peer{
IP: peer3IP,
IP6: &peer3IP6,
ID: peer3ID,
Key: peer3Key,
Name: "test-host3@netbird.io",
UserID: userID,
Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host3@netbird.io",
GoOS: "darwin",
Kernel: "Darwin",
Core: "13.4.1",
Platform: "arm64",
OS: "darwin",
WtVersion: "development",
UIVersion: "development",
Hostname: "test-host3@netbird.io",
GoOS: "darwin",
Kernel: "Darwin",
Core: "13.4.1",
Platform: "arm64",
OS: "darwin",
WtVersion: "development",
UIVersion: "development",
Ipv6Supported: true,
},
Status: &nbpeer.PeerStatus{},
V6Setting: nbpeer.V6Enabled,
Status: &nbpeer.PeerStatus{},
}
account.Peers[peer3.ID] = peer3
@@ -1133,14 +1337,15 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
Name: "test-host4@netbird.io",
UserID: userID,
Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host4@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Hostname: "test-host4@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Ipv6Supported: false,
},
Status: &nbpeer.PeerStatus{},
}
@@ -1159,16 +1364,18 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
Name: "test-host4@netbird.io",
UserID: userID,
Meta: nbpeer.PeerSystemMeta{
Hostname: "test-host4@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Hostname: "test-host4@netbird.io",
GoOS: "linux",
Kernel: "Linux",
Core: "21.04",
Platform: "x86_64",
OS: "Ubuntu",
WtVersion: "development",
UIVersion: "development",
Ipv6Supported: true,
},
Status: &nbpeer.PeerStatus{},
Status: &nbpeer.PeerStatus{},
V6Setting: nbpeer.V6Disabled,
}
account.Peers[peer5.ID] = peer5