[management] Add IPv6 overlay addressing and capability gating (#5698)

This commit is contained in:
Viktor Liu
2026-04-08 16:40:51 +02:00
committed by GitHub
parent 86f1b53bd4
commit a1e7db2713
51 changed files with 2622 additions and 394 deletions
+307 -5
View File
@@ -332,6 +332,13 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
updateAccountPeers = true
}
if ipv6SettingsChanged(oldSettings, newSettings) {
if err = am.updatePeerIPv6Addresses(ctx, transaction, accountID, newSettings); err != nil {
return err
}
updateAccountPeers = true
}
if oldSettings.RoutingPeerDNSResolutionEnabled != newSettings.RoutingPeerDNSResolutionEnabled ||
oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled ||
oldSettings.DNSDomain != newSettings.DNSDomain ||
@@ -396,6 +403,22 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
}
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountNetworkRangeUpdated, eventMeta)
}
oldIPv6On := len(oldSettings.IPv6EnabledGroups) > 0
newIPv6On := len(newSettings.IPv6EnabledGroups) > 0
if oldIPv6On != newIPv6On {
if newIPv6On {
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountIPv6Enabled, nil)
} else {
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountIPv6Disabled, nil)
}
}
if oldSettings.NetworkRangeV6 != newSettings.NetworkRangeV6 {
eventMeta := map[string]any{
"old_network_range_v6": oldSettings.NetworkRangeV6.String(),
"new_network_range_v6": newSettings.NetworkRangeV6.String(),
}
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountNetworkRangeUpdated, eventMeta)
}
if reloadReverseProxy {
if err = am.serviceManager.ReloadAllServicesForAccount(ctx, accountID); err != nil {
log.WithContext(ctx).Warnf("failed to reload all services for account %s: %v", accountID, err)
@@ -409,6 +432,17 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
return newSettings, nil
}
func ipv6SettingsChanged(old, updated *types.Settings) bool {
if old.NetworkRangeV6 != updated.NetworkRangeV6 {
return true
}
oldGroups := slices.Clone(old.IPv6EnabledGroups)
newGroups := slices.Clone(updated.IPv6EnabledGroups)
slices.Sort(oldGroups)
slices.Sort(newGroups)
return !slices.Equal(oldGroups, newGroups)
}
func (am *DefaultAccountManager) validateSettingsUpdate(ctx context.Context, transaction store.Store, newSettings, oldSettings *types.Settings, userID, accountID string) error {
halfYearLimit := 180 * 24 * time.Hour
if newSettings.PeerLoginExpiration > halfYearLimit {
@@ -435,9 +469,38 @@ func (am *DefaultAccountManager) validateSettingsUpdate(ctx context.Context, tra
}
}
if err := validateIPv6EnabledGroups(ctx, transaction, accountID, newSettings.IPv6EnabledGroups); err != nil {
return err
}
return am.integratedPeerValidator.ValidateExtraSettings(ctx, newSettings.Extra, oldSettings.Extra, userID, accountID)
}
// validateIPv6EnabledGroups checks that all referenced IPv6-enabled group IDs exist in the account.
func validateIPv6EnabledGroups(ctx context.Context, transaction store.Store, accountID string, groupIDs []string) error {
if len(groupIDs) == 0 {
return nil
}
groups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthNone, accountID)
if err != nil {
return fmt.Errorf("get groups for IPv6 validation: %w", err)
}
existing := make(map[string]struct{}, len(groups))
for _, g := range groups {
existing[g.ID] = struct{}{}
}
for _, gid := range groupIDs {
if _, ok := existing[gid]; !ok {
return status.Errorf(status.InvalidArgument, "IPv6 enabled group %s does not exist", gid)
}
}
return nil
}
func (am *DefaultAccountManager) handleRoutingPeerDNSResolutionSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) {
if oldSettings.RoutingPeerDNSResolutionEnabled != newSettings.RoutingPeerDNSResolutionEnabled {
if newSettings.RoutingPeerDNSResolutionEnabled {
@@ -1921,6 +1984,11 @@ func newAccountWithId(ctx context.Context, accountID, userID, domain, email, nam
if err := acc.AddAllGroup(disableDefaultPolicy); err != nil {
log.WithContext(ctx).Errorf("error adding all group to account %s: %v", acc.Id, err)
}
if allGroup, err := acc.GetGroupAll(); err == nil {
acc.Settings.IPv6EnabledGroups = []string{allGroup.ID}
}
return acc
}
@@ -2027,6 +2095,10 @@ func (am *DefaultAccountManager) GetOrCreateAccountByPrivateDomain(ctx context.C
return nil, false, status.Errorf(status.Internal, "failed to add all group to new account by private domain")
}
if allGroup, err := newAccount.GetGroupAll(); err == nil {
newAccount.Settings.IPv6EnabledGroups = []string{allGroup.ID}
}
if err := am.Store.SaveAccount(ctx, newAccount); err != nil {
log.WithContext(ctx).WithFields(log.Fields{
"accountId": newAccount.Id,
@@ -2164,10 +2236,10 @@ func (am *DefaultAccountManager) reallocateAccountPeerIPs(ctx context.Context, t
return err
}
var takenIPs []net.IP
var takenIPs []netip.Addr
for _, peer := range peers {
newIP, err := types.AllocatePeerIP(newIPNet, takenIPs)
newIP, err := types.AllocatePeerIP(newNetworkRange, takenIPs)
if err != nil {
return status.Errorf(status.Internal, "allocate IP for peer %s: %v", peer.ID, err)
}
@@ -2191,13 +2263,165 @@ func (am *DefaultAccountManager) reallocateAccountPeerIPs(ctx context.Context, t
return nil
}
// updatePeerIPv6Addresses assigns or removes IPv6 addresses for all peers
// based on the current IPv6 settings. When IPv6 is enabled, peers without a
// v6 address get one allocated. When disabled, all v6 addresses are cleared.
// When the v6 range changes, all v6 addresses are reallocated.
func (am *DefaultAccountManager) checkIPv6Collision(ctx context.Context, transaction store.Store, accountID, peerID string, newIPv6 netip.Addr) error {
peers, err := transaction.GetAccountPeers(ctx, store.LockingStrengthShare, accountID, "", "")
if err != nil {
return fmt.Errorf("get peers: %w", err)
}
for _, p := range peers {
if p.ID != peerID && p.IPv6.IsValid() && p.IPv6 == newIPv6 {
return status.Errorf(status.InvalidArgument, "IPv6 %s is already assigned to peer %s", newIPv6, p.Name)
}
}
return nil
}
func (am *DefaultAccountManager) updatePeerIPv6Addresses(ctx context.Context, transaction store.Store, accountID string, settings *types.Settings) error {
peers, err := transaction.GetAccountPeers(ctx, store.LockingStrengthUpdate, accountID, "", "")
if err != nil {
return fmt.Errorf("get peers: %w", err)
}
network, err := transaction.GetAccountNetwork(ctx, store.LockingStrengthUpdate, accountID)
if err != nil {
return fmt.Errorf("get network: %w", err)
}
if err := am.ensureIPv6Subnet(ctx, transaction, accountID, settings, network); err != nil {
return err
}
allowedPeers, err := am.buildIPv6AllowedPeers(ctx, transaction, accountID, settings)
if err != nil {
return err
}
v6Prefix, err := netip.ParsePrefix(network.NetV6.String())
if err != nil {
return fmt.Errorf("parse IPv6 prefix: %w", err)
}
if err := am.assignPeerIPv6Addresses(ctx, transaction, accountID, peers, network, allowedPeers, v6Prefix); err != nil {
return err
}
log.WithContext(ctx).Infof("updated IPv6 addresses for %d peers in account %s (groups=%d)",
len(peers), accountID, len(settings.IPv6EnabledGroups))
return nil
}
func (am *DefaultAccountManager) ensureIPv6Subnet(ctx context.Context, transaction store.Store, accountID string, settings *types.Settings, network *types.Network) error {
if settings.NetworkRangeV6.IsValid() {
network.NetV6 = net.IPNet{
IP: settings.NetworkRangeV6.Masked().Addr().AsSlice(),
Mask: net.CIDRMask(settings.NetworkRangeV6.Bits(), 128),
}
return transaction.UpdateAccountNetworkV6(ctx, accountID, network.NetV6)
}
if network.NetV6.IP == nil {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
network.NetV6 = types.AllocateIPv6Subnet(r)
// Sync settings to match the allocated subnet so SaveAccountSettings persists it.
ones, _ := network.NetV6.Mask.Size()
addr, _ := netip.AddrFromSlice(network.NetV6.IP)
settings.NetworkRangeV6 = netip.PrefixFrom(addr.Unmap(), ones)
return transaction.UpdateAccountNetworkV6(ctx, accountID, network.NetV6)
}
return nil
}
func (am *DefaultAccountManager) assignPeerIPv6Addresses(
ctx context.Context, transaction store.Store, accountID string,
peers []*nbpeer.Peer, network *types.Network,
allowedPeers map[string]struct{}, v6Prefix netip.Prefix,
) error {
takenV6 := make(map[netip.Addr]struct{})
for _, peer := range peers {
if _, ok := allowedPeers[peer.ID]; ok && peer.IPv6.IsValid() && network.NetV6.Contains(peer.IPv6.AsSlice()) {
takenV6[peer.IPv6] = struct{}{}
}
}
for _, peer := range peers {
_, allowed := allowedPeers[peer.ID]
oldIPv6 := peer.IPv6
if !allowed {
peer.IPv6 = netip.Addr{}
} else if !peer.IPv6.IsValid() || !network.NetV6.Contains(peer.IPv6.AsSlice()) {
newIP, err := allocateIPv6WithRetry(v6Prefix, takenV6, peer.ID)
if err != nil {
return err
}
peer.IPv6 = newIP
}
if peer.IPv6 == oldIPv6 {
continue
}
if err := transaction.SavePeer(ctx, accountID, peer); err != nil {
return fmt.Errorf("save peer %s: %w", peer.ID, err)
}
}
return nil
}
func allocateIPv6WithRetry(prefix netip.Prefix, taken map[netip.Addr]struct{}, peerID string) (netip.Addr, error) {
for attempts := 0; attempts < 10; attempts++ {
newIP, err := types.AllocateRandomPeerIPv6(prefix)
if err != nil {
return netip.Addr{}, fmt.Errorf("allocate v6 for peer %s: %w", peerID, err)
}
if _, ok := taken[newIP]; !ok {
taken[newIP] = struct{}{}
return newIP, nil
}
}
return netip.Addr{}, fmt.Errorf("allocate v6 for peer %s: exhausted 10 attempts", peerID)
}
func (am *DefaultAccountManager) buildIPv6AllowedPeers(ctx context.Context, transaction store.Store, accountID string, settings *types.Settings) (map[string]struct{}, error) {
if len(settings.IPv6EnabledGroups) == 0 {
return make(map[string]struct{}), nil
}
groups, err := transaction.GetAccountGroups(ctx, store.LockingStrengthNone, accountID)
if err != nil {
return nil, fmt.Errorf("get groups: %w", err)
}
enabledSet := make(map[string]struct{}, len(settings.IPv6EnabledGroups))
for _, gid := range settings.IPv6EnabledGroups {
enabledSet[gid] = struct{}{}
}
allowedPeers := make(map[string]struct{})
for _, group := range groups {
if _, ok := enabledSet[group.ID]; !ok {
continue
}
for _, peerID := range group.Peers {
allowedPeers[peerID] = struct{}{}
}
}
return allowedPeers, nil
}
func (am *DefaultAccountManager) validateIPForUpdate(account *types.Account, peers []*nbpeer.Peer, peerID string, newIP netip.Addr) error {
if !account.Network.Net.Contains(newIP.AsSlice()) {
return status.Errorf(status.InvalidArgument, "IP %s is not within the account network range %s", newIP.String(), account.Network.Net.String())
}
for _, peer := range peers {
if peer.ID != peerID && peer.IP.Equal(newIP.AsSlice()) {
if peer.ID != peerID && peer.IP == newIP {
return status.Errorf(status.InvalidArgument, "IP %s is already assigned to peer %s", newIP.String(), peer.ID)
}
}
@@ -2244,7 +2468,7 @@ func (am *DefaultAccountManager) updatePeerIPInTransaction(ctx context.Context,
return fmt.Errorf("get peer: %w", err)
}
if existingPeer.IP.Equal(newIP.AsSlice()) {
if existingPeer.IP == newIP {
return nil
}
@@ -2279,7 +2503,7 @@ func (am *DefaultAccountManager) savePeerIPUpdate(ctx context.Context, transacti
eventMeta := peer.EventMeta(dnsDomain)
oldIP := peer.IP.String()
peer.IP = newIP.AsSlice()
peer.IP = newIP
err = transaction.SavePeer(ctx, accountID, peer)
if err != nil {
return fmt.Errorf("save peer: %w", err)
@@ -2292,6 +2516,84 @@ func (am *DefaultAccountManager) savePeerIPUpdate(ctx context.Context, transacti
return nil
}
// UpdatePeerIPv6 updates the IPv6 overlay address of a peer, validating it's
// within the account's v6 network range and not already taken.
func (am *DefaultAccountManager) UpdatePeerIPv6(ctx context.Context, accountID, userID, peerID string, newIPv6 netip.Addr) error {
allowed, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.Peers, operations.Update)
if err != nil {
return fmt.Errorf("validate user permissions: %w", err)
}
if !allowed {
return status.NewPermissionDeniedError()
}
var updateNetworkMap bool
err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
var txErr error
updateNetworkMap, txErr = am.updatePeerIPv6InTransaction(ctx, transaction, accountID, peerID, newIPv6)
return txErr
})
if err != nil {
return err
}
if updateNetworkMap {
if err := am.networkMapController.OnPeersUpdated(ctx, accountID, []string{peerID}); err != nil {
return fmt.Errorf("notify network map controller: %w", err)
}
}
return nil
}
// updatePeerIPv6InTransaction validates and applies an IPv6 address change within a store transaction.
func (am *DefaultAccountManager) updatePeerIPv6InTransaction(ctx context.Context, transaction store.Store, accountID, peerID string, newIPv6 netip.Addr) (bool, error) {
network, err := transaction.GetAccountNetwork(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return false, fmt.Errorf("get network: %w", err)
}
if network.NetV6.IP == nil {
return false, status.Errorf(status.PreconditionFailed, "IPv6 is not configured for this account")
}
if !network.NetV6.Contains(newIPv6.AsSlice()) {
return false, status.Errorf(status.InvalidArgument, "IP %s is not within the account IPv6 range %s", newIPv6, network.NetV6.String())
}
settings, err := transaction.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
if err != nil {
return false, fmt.Errorf("get settings: %w", err)
}
allowedPeers, err := am.buildIPv6AllowedPeers(ctx, transaction, accountID, settings)
if err != nil {
return false, err
}
if _, ok := allowedPeers[peerID]; !ok {
return false, status.Errorf(status.PreconditionFailed, "peer is not in any IPv6-enabled group")
}
peer, err := transaction.GetPeerByID(ctx, store.LockingStrengthUpdate, accountID, peerID)
if err != nil {
return false, fmt.Errorf("get peer: %w", err)
}
if peer.IPv6.IsValid() && peer.IPv6 == newIPv6 {
return false, nil
}
if err := am.checkIPv6Collision(ctx, transaction, accountID, peerID, newIPv6); err != nil {
return false, err
}
peer.IPv6 = newIPv6
if err := transaction.SavePeer(ctx, accountID, peer); err != nil {
return false, fmt.Errorf("save peer: %w", err)
}
return true, nil
}
func (am *DefaultAccountManager) GetUserIDByPeerKey(ctx context.Context, peerKey string) (string, error) {
return am.Store.GetUserIDByPeerKey(ctx, store.LockingStrengthNone, peerKey)
}
+1
View File
@@ -65,6 +65,7 @@ type Manager interface {
DeletePeer(ctx context.Context, accountID, peerID, userID string) error
UpdatePeer(ctx context.Context, accountID, userID string, p *nbpeer.Peer) (*nbpeer.Peer, error)
UpdatePeerIP(ctx context.Context, accountID, userID, peerID string, newIP netip.Addr) error
UpdatePeerIPv6(ctx context.Context, accountID, userID, peerID string, newIPv6 netip.Addr) error
GetNetworkMap(ctx context.Context, peerID string) (*types.NetworkMap, error)
GetPeerNetwork(ctx context.Context, peerID string) (*types.Network, error)
AddPeer(ctx context.Context, accountID, setupKey, userID string, p *nbpeer.Peer, temporary bool) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error)
+12
View File
@@ -1709,6 +1709,18 @@ func (mr *MockManagerMockRecorder) UpdatePeerIP(ctx, accountID, userID, peerID,
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePeerIP", reflect.TypeOf((*MockManager)(nil).UpdatePeerIP), ctx, accountID, userID, peerID, newIP)
}
func (m *MockManager) UpdatePeerIPv6(ctx context.Context, accountID, userID, peerID string, newIPv6 netip.Addr) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdatePeerIPv6", ctx, accountID, userID, peerID, newIPv6)
ret0, _ := ret[0].(error)
return ret0
}
func (mr *MockManagerMockRecorder) UpdatePeerIPv6(ctx, accountID, userID, peerID, newIPv6 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePeerIPv6", reflect.TypeOf((*MockManager)(nil).UpdatePeerIPv6), ctx, accountID, userID, peerID, newIPv6)
}
// UpdateToPrimaryAccount mocks base method.
func (m *MockManager) UpdateToPrimaryAccount(ctx context.Context, accountId string) error {
m.ctrl.T.Helper()
+182 -25
View File
@@ -158,7 +158,8 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
"peer-1": {
ID: peerID1,
Key: "peer-1-key",
IP: net.IP{100, 64, 0, 1},
IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
IPv6: netip.MustParseAddr("fd00::6440:1"),
Name: peerID1,
DNSLabel: peerID1,
Status: &nbpeer.PeerStatus{
@@ -172,7 +173,8 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
"peer-2": {
ID: peerID2,
Key: "peer-2-key",
IP: net.IP{100, 64, 0, 1},
IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
IPv6: netip.MustParseAddr("fd00::6440:1"),
Name: peerID2,
DNSLabel: peerID2,
Status: &nbpeer.PeerStatus{
@@ -196,7 +198,8 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
"peer-1": {
ID: peerID1,
Key: "peer-1-key",
IP: net.IP{100, 64, 0, 1},
IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
IPv6: netip.MustParseAddr("fd00::6440:1"),
Name: peerID1,
DNSLabel: peerID1,
Status: &nbpeer.PeerStatus{
@@ -211,7 +214,8 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
"peer-2": {
ID: peerID2,
Key: "peer-2-key",
IP: net.IP{100, 64, 0, 1},
IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
IPv6: netip.MustParseAddr("fd00::6440:1"),
Name: peerID2,
DNSLabel: peerID2,
Status: &nbpeer.PeerStatus{
@@ -235,7 +239,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
// "peer-1": {
// ID: peerID1,
// Key: "peer-1-key",
// IP: net.IP{100, 64, 0, 1},
// IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
// Name: peerID1,
// DNSLabel: peerID1,
// Status: &PeerStatus{
@@ -249,7 +253,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
// "peer-2": {
// ID: peerID2,
// Key: "peer-2-key",
// IP: net.IP{100, 64, 0, 1},
// IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
// Name: peerID2,
// DNSLabel: peerID2,
// Status: &PeerStatus{
@@ -263,7 +267,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
// "peer-3": {
// ID: peerID3,
// Key: "peer-3-key",
// IP: net.IP{100, 64, 0, 1},
// IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
// Name: peerID3,
// DNSLabel: peerID3,
// Status: &PeerStatus{
@@ -286,7 +290,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
// "peer-1": {
// ID: peerID1,
// Key: "peer-1-key",
// IP: net.IP{100, 64, 0, 1},
// IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
// Name: peerID1,
// DNSLabel: peerID1,
// Status: &PeerStatus{
@@ -300,7 +304,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
// "peer-2": {
// ID: peerID2,
// Key: "peer-2-key",
// IP: net.IP{100, 64, 0, 1},
// IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
// Name: peerID2,
// DNSLabel: peerID2,
// Status: &PeerStatus{
@@ -314,7 +318,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
// "peer-3": {
// ID: peerID3,
// Key: "peer-3-key",
// IP: net.IP{100, 64, 0, 1},
// IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
// Name: peerID3,
// DNSLabel: peerID3,
// Status: &PeerStatus{
@@ -337,7 +341,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
// "peer-1": {
// ID: peerID1,
// Key: "peer-1-key",
// IP: net.IP{100, 64, 0, 1},
// IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
// Name: peerID1,
// DNSLabel: peerID1,
// Status: &PeerStatus{
@@ -351,7 +355,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
// "peer-2": {
// ID: peerID2,
// Key: "peer-2-key",
// IP: net.IP{100, 64, 0, 1},
// IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
// Name: peerID2,
// DNSLabel: peerID2,
// Status: &PeerStatus{
@@ -365,7 +369,7 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) {
// "peer-3": {
// ID: peerID3,
// Key: "peer-3-key",
// IP: net.IP{100, 64, 0, 1},
// IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
// Name: peerID3,
// DNSLabel: peerID3,
// Status: &PeerStatus{
@@ -1082,7 +1086,7 @@ func TestAccountManager_AddPeer(t *testing.T) {
t.Errorf("expecting just added peer to have key = %s, got %s", expectedPeerKey, peer.Key)
}
if !account.Network.Net.Contains(peer.IP) {
if !account.Network.Net.Contains(peer.IP.AsSlice()) {
t.Errorf("expecting just added peer's IP %s to be in a network range %s", peer.IP.String(), account.Network.Net.String())
}
@@ -1146,7 +1150,7 @@ func TestAccountManager_AddPeerWithUserID(t *testing.T) {
t.Errorf("expecting just added peer to have key = %s, got %s", expectedPeerKey, peer.Key)
}
if !account.Network.Net.Contains(peer.IP) {
if !account.Network.Net.Contains(peer.IP.AsSlice()) {
t.Errorf("expecting just added peer's IP %s to be in a network range %s", peer.IP.String(), account.Network.Net.String())
}
@@ -2852,11 +2856,46 @@ func TestAccount_SetJWTGroups(t *testing.T) {
account := &types.Account{
Id: "accountID",
Peers: map[string]*nbpeer.Peer{
"peer1": {ID: "peer1", Key: "key1", UserID: "user1", IP: net.IP{1, 1, 1, 1}, DNSLabel: "peer1.domain.test"},
"peer2": {ID: "peer2", Key: "key2", UserID: "user1", IP: net.IP{2, 2, 2, 2}, DNSLabel: "peer2.domain.test"},
"peer3": {ID: "peer3", Key: "key3", UserID: "user1", IP: net.IP{3, 3, 3, 3}, DNSLabel: "peer3.domain.test"},
"peer4": {ID: "peer4", Key: "key4", UserID: "user2", IP: net.IP{4, 4, 4, 4}, DNSLabel: "peer4.domain.test"},
"peer5": {ID: "peer5", Key: "key5", UserID: "user2", IP: net.IP{5, 5, 5, 5}, DNSLabel: "peer5.domain.test"},
"peer1": {
ID: "peer1",
Key: "key1",
UserID: "user1",
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
IPv6: netip.MustParseAddr("fd00::1"),
DNSLabel: "peer1.domain.test",
},
"peer2": {
ID: "peer2",
Key: "key2",
UserID: "user1",
IP: netip.AddrFrom4([4]byte{2, 2, 2, 2}),
IPv6: netip.MustParseAddr("fd00::2"),
DNSLabel: "peer2.domain.test",
},
"peer3": {
ID: "peer3",
Key: "key3",
UserID: "user1",
IP: netip.AddrFrom4([4]byte{3, 3, 3, 3}),
IPv6: netip.MustParseAddr("fd00::3"),
DNSLabel: "peer3.domain.test",
},
"peer4": {
ID: "peer4",
Key: "key4",
UserID: "user2",
IP: netip.AddrFrom4([4]byte{4, 4, 4, 4}),
IPv6: netip.MustParseAddr("fd00::4"),
DNSLabel: "peer4.domain.test",
},
"peer5": {
ID: "peer5",
Key: "key5",
UserID: "user2",
IP: netip.AddrFrom4([4]byte{5, 5, 5, 5}),
IPv6: netip.MustParseAddr("fd00::5"),
DNSLabel: "peer5.domain.test",
},
},
Groups: map[string]*types.Group{
"group1": {ID: "group1", Name: "group1", Issued: types.GroupIssuedAPI, Peers: []string{}},
@@ -3601,11 +3640,27 @@ func TestPropagateUserGroupMemberships(t *testing.T) {
account, err := manager.GetOrCreateAccountByUser(ctx, auth.UserAuth{UserId: initiatorId, Domain: domain})
require.NoError(t, err)
peer1 := &nbpeer.Peer{ID: "peer1", AccountID: account.Id, Key: "key1", UserID: initiatorId, IP: net.IP{1, 1, 1, 1}, DNSLabel: "peer1.domain.test"}
peer1 := &nbpeer.Peer{
ID: "peer1",
AccountID: account.Id,
Key: "key1",
UserID: initiatorId,
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
IPv6: netip.MustParseAddr("fd00::1"),
DNSLabel: "peer1.domain.test",
}
err = manager.Store.AddPeerToAccount(ctx, peer1)
require.NoError(t, err)
peer2 := &nbpeer.Peer{ID: "peer2", AccountID: account.Id, Key: "key2", UserID: initiatorId, IP: net.IP{2, 2, 2, 2}, DNSLabel: "peer2.domain.test"}
peer2 := &nbpeer.Peer{
ID: "peer2",
AccountID: account.Id,
Key: "key2",
UserID: initiatorId,
IP: netip.AddrFrom4([4]byte{2, 2, 2, 2}),
IPv6: netip.MustParseAddr("fd00::2"),
DNSLabel: "peer2.domain.test",
}
err = manager.Store.AddPeerToAccount(ctx, peer2)
require.NoError(t, err)
@@ -3806,11 +3861,10 @@ func TestDefaultAccountManager_UpdatePeerIP(t *testing.T) {
account, err := manager.Store.GetAccount(context.Background(), accountID)
require.NoError(t, err, "unable to get account")
newIP, err := types.AllocatePeerIP(account.Network.Net, []net.IP{peer1.IP, peer2.IP})
newIP, err := types.AllocatePeerIP(netip.MustParsePrefix(account.Network.Net.String()), []netip.Addr{peer1.IP, peer2.IP})
require.NoError(t, err, "unable to allocate new IP")
newAddr := netip.MustParseAddr(newIP.String())
err = manager.UpdatePeerIP(context.Background(), accountID, userID, peer1.ID, newAddr)
err = manager.UpdatePeerIP(context.Background(), accountID, userID, peer1.ID, newIP)
require.NoError(t, err, "unable to update peer IP")
updatedPeer, err := manager.GetPeer(context.Background(), accountID, peer1.ID, userID)
@@ -3968,6 +4022,109 @@ func TestDefaultAccountManager_UpdateAccountSettings_NetworkRangeChange(t *testi
}
}
func TestDefaultAccountManager_UpdateAccountSettings_IPv6EnabledGroups(t *testing.T) {
manager, _, account, peer1, peer2, peer3 := setupNetworkMapTest(t)
ctx := context.Background()
accountID := account.Id
// New accounts default to All group in IPv6EnabledGroups, so all 3 peers should have IPv6.
settings, err := manager.Store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID)
require.NoError(t, err)
require.NotEmpty(t, settings.IPv6EnabledGroups, "new account should have IPv6 enabled for All group")
peers, err := manager.Store.GetAccountPeers(ctx, store.LockingStrengthNone, accountID, "", "")
require.NoError(t, err)
for _, p := range peers {
assert.True(t, p.IPv6.IsValid(), "peer %s should have IPv6 with All group enabled", p.ID)
}
// Create a group with only peer1 and peer2.
partialGroup := &types.Group{
ID: "ipv6-partial-group",
AccountID: accountID,
Name: "IPv6Partial",
}
err = manager.Store.CreateGroup(ctx, partialGroup)
require.NoError(t, err)
require.NoError(t, manager.Store.AddPeerToGroup(ctx, accountID, peer1.ID, partialGroup.ID))
require.NoError(t, manager.Store.AddPeerToGroup(ctx, accountID, peer2.ID, partialGroup.ID))
// Switch IPv6EnabledGroups to only the partial group.
updatedSettings, err := manager.UpdateAccountSettings(ctx, accountID, userID, &types.Settings{
PeerLoginExpiration: types.DefaultPeerLoginExpiration,
PeerLoginExpirationEnabled: true,
IPv6EnabledGroups: []string{partialGroup.ID},
Extra: &types.ExtraSettings{},
})
require.NoError(t, err)
assert.Equal(t, []string{partialGroup.ID}, updatedSettings.IPv6EnabledGroups)
// peer1 and peer2 should have IPv6; peer3 should not.
peers, err = manager.Store.GetAccountPeers(ctx, store.LockingStrengthNone, accountID, "", "")
require.NoError(t, err)
peerMap := make(map[string]*nbpeer.Peer, len(peers))
for _, p := range peers {
peerMap[p.ID] = p
}
assert.True(t, peerMap[peer1.ID].IPv6.IsValid(), "peer1 in partial group should keep IPv6")
assert.True(t, peerMap[peer2.ID].IPv6.IsValid(), "peer2 in partial group should keep IPv6")
assert.False(t, peerMap[peer3.ID].IPv6.IsValid(), "peer3 not in partial group should lose IPv6")
// Clearing all groups disables IPv6 for everyone.
updatedSettings, err = manager.UpdateAccountSettings(ctx, accountID, userID, &types.Settings{
PeerLoginExpiration: types.DefaultPeerLoginExpiration,
PeerLoginExpirationEnabled: true,
IPv6EnabledGroups: []string{},
Extra: &types.ExtraSettings{},
})
require.NoError(t, err)
assert.Empty(t, updatedSettings.IPv6EnabledGroups)
peers, err = manager.Store.GetAccountPeers(ctx, store.LockingStrengthNone, accountID, "", "")
require.NoError(t, err)
for _, p := range peers {
assert.False(t, p.IPv6.IsValid(), "peer %s should have no IPv6 when groups cleared", p.ID)
}
// Re-enabling with the partial group should allocate IPv6 only for peer1 and peer2.
_, err = manager.UpdateAccountSettings(ctx, accountID, userID, &types.Settings{
PeerLoginExpiration: types.DefaultPeerLoginExpiration,
PeerLoginExpirationEnabled: true,
IPv6EnabledGroups: []string{partialGroup.ID},
Extra: &types.ExtraSettings{},
})
require.NoError(t, err)
peers, err = manager.Store.GetAccountPeers(ctx, store.LockingStrengthNone, accountID, "", "")
require.NoError(t, err)
peerMap = make(map[string]*nbpeer.Peer, len(peers))
for _, p := range peers {
peerMap[p.ID] = p
}
assert.True(t, peerMap[peer1.ID].IPv6.IsValid(), "peer1 should get IPv6 back")
assert.True(t, peerMap[peer2.ID].IPv6.IsValid(), "peer2 should get IPv6 back")
assert.False(t, peerMap[peer3.ID].IPv6.IsValid(), "peer3 still excluded")
// No-op update with the same groups should not cause errors.
_, err = manager.UpdateAccountSettings(ctx, accountID, userID, &types.Settings{
PeerLoginExpiration: types.DefaultPeerLoginExpiration,
PeerLoginExpirationEnabled: true,
IPv6EnabledGroups: []string{partialGroup.ID},
Extra: &types.ExtraSettings{},
})
require.NoError(t, err)
// Setting a nonexistent group ID should fail.
_, err = manager.UpdateAccountSettings(ctx, accountID, userID, &types.Settings{
PeerLoginExpiration: types.DefaultPeerLoginExpiration,
PeerLoginExpirationEnabled: true,
IPv6EnabledGroups: []string{"nonexistent-group-id"},
Extra: &types.ExtraSettings{},
})
require.Error(t, err)
assert.Contains(t, err.Error(), "does not exist")
}
func TestUpdateUserAuthWithSingleMode(t *testing.T) {
t.Run("sets defaults and overrides domain from store", func(t *testing.T) {
ctrl := gomock.NewController(t)
+7
View File
@@ -231,6 +231,10 @@ const (
DomainDeleted Activity = 119
// DomainValidated indicates that a custom domain was validated
DomainValidated Activity = 120
// AccountIPv6Enabled indicates that a user enabled IPv6 overlay for the account
AccountIPv6Enabled Activity = 121
// AccountIPv6Disabled indicates that a user disabled IPv6 overlay for the account
AccountIPv6Disabled Activity = 122
AccountDeleted Activity = 99999
)
@@ -347,6 +351,9 @@ var activityMap = map[Activity]Code{
AccountAutoUpdateAlwaysEnabled: {"Account auto-update always enabled", "account.setting.auto.update.always.enable"},
AccountAutoUpdateAlwaysDisabled: {"Account auto-update always disabled", "account.setting.auto.update.always.disable"},
AccountIPv6Enabled: {"Account IPv6 overlay enabled", "account.setting.ipv6.enable"},
AccountIPv6Disabled: {"Account IPv6 overlay disabled", "account.setting.ipv6.disable"},
IdentityProviderCreated: {"Identity provider created", "identityprovider.create"},
IdentityProviderUpdated: {"Identity provider updated", "identityprovider.update"},
IdentityProviderDeleted: {"Identity provider deleted", "identityprovider.delete"},
+4 -5
View File
@@ -5,7 +5,6 @@ import (
"encoding/binary"
"errors"
"fmt"
"net"
"net/netip"
"strconv"
"sync"
@@ -999,10 +998,10 @@ func Test_AddPeerAndAddToAll(t *testing.T) {
assert.Equal(t, totalPeers, len(account.Peers), "Expected %d peers in account %s, got %d", totalPeers, accountID, len(account.Peers))
}
func uint32ToIP(n uint32) net.IP {
ip := make(net.IP, 4)
binary.BigEndian.PutUint32(ip, n)
return ip
func uint32ToIP(n uint32) netip.Addr {
var b [4]byte
binary.BigEndian.PutUint32(b[:], n)
return netip.AddrFrom4(b)
}
func Test_IncrementNetworkSerial(t *testing.T) {
@@ -4,10 +4,13 @@ import (
"context"
"encoding/json"
"fmt"
"math"
"net/http"
"net/netip"
"time"
log "github.com/sirupsen/logrus"
"github.com/gorilla/mux"
goversion "github.com/hashicorp/go-version"
@@ -29,7 +32,9 @@ const (
// MinNetworkBits is the minimum prefix length for IPv4 network ranges (e.g., /29 gives 8 addresses, /28 gives 16)
MinNetworkBitsIPv4 = 28
// MinNetworkBitsIPv6 is the minimum prefix length for IPv6 network ranges
MinNetworkBitsIPv6 = 120
MinNetworkBitsIPv6 = 120
// MaxNetworkSizeIPv6 is the largest allowed IPv6 prefix (smallest number)
MaxNetworkSizeIPv6 = 48
disableAutoUpdate = "disabled"
autoUpdateLatestVersion = "latest"
)
@@ -76,12 +81,35 @@ func validateMinimumSize(prefix netip.Prefix) error {
if addr.Is4() && prefix.Bits() > MinNetworkBitsIPv4 {
return status.Errorf(status.InvalidArgument, "network range too small: minimum size is /%d for IPv4", MinNetworkBitsIPv4)
}
if addr.Is6() && prefix.Bits() > MinNetworkBitsIPv6 {
return status.Errorf(status.InvalidArgument, "network range too small: minimum size is /%d for IPv6", MinNetworkBitsIPv6)
if addr.Is6() {
if prefix.Bits() > MinNetworkBitsIPv6 {
return status.Errorf(status.InvalidArgument, "network range too small: minimum size is /%d for IPv6", MinNetworkBitsIPv6)
}
if prefix.Bits() < MaxNetworkSizeIPv6 {
return status.Errorf(status.InvalidArgument, "network range too large: maximum size is /%d for IPv6", MaxNetworkSizeIPv6)
}
}
return nil
}
func (h *handler) parseAndValidateNetworkRange(ctx context.Context, accountID, userID, rangeStr string, requireV6 bool) (netip.Prefix, error) {
prefix, err := netip.ParsePrefix(rangeStr)
if err != nil {
return netip.Prefix{}, status.Errorf(status.InvalidArgument, "invalid CIDR format: %v", err)
}
prefix = prefix.Masked()
if requireV6 && !prefix.Addr().Is6() {
return netip.Prefix{}, status.Errorf(status.InvalidArgument, "network range must be an IPv6 address")
}
if !requireV6 && prefix.Addr().Is6() {
return netip.Prefix{}, status.Errorf(status.InvalidArgument, "network range must be an IPv4 address")
}
if err := h.validateNetworkRange(ctx, accountID, userID, prefix); err != nil {
return netip.Prefix{}, err
}
return prefix, nil
}
func (h *handler) validateNetworkRange(ctx context.Context, accountID, userID string, networkRange netip.Prefix) error {
if !networkRange.IsValid() {
return nil
@@ -117,9 +145,12 @@ func (h *handler) validateCapacity(ctx context.Context, accountID, userID string
}
func calculateMaxHosts(prefix netip.Prefix) int64 {
availableAddresses := prefix.Addr().BitLen() - prefix.Bits()
maxHosts := int64(1) << availableAddresses
hostBits := prefix.Addr().BitLen() - prefix.Bits()
if hostBits >= 63 {
return math.MaxInt64
}
maxHosts := int64(1) << hostBits
if prefix.Addr().Is4() {
maxHosts -= 2 // network and broadcast addresses
}
@@ -164,6 +195,24 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) {
}
resp := toAccountResponse(accountID, settings, meta, onboarding)
// Populate effective network ranges when settings don't have explicit overrides.
if resp.Settings.NetworkRange == nil || resp.Settings.NetworkRangeV6 == nil {
v4, v6, err := h.settingsManager.GetEffectiveNetworkRanges(r.Context(), accountID)
if err != nil {
log.WithContext(r.Context()).Warnf("get effective network ranges: %v", err)
} else {
if resp.Settings.NetworkRange == nil && v4.IsValid() {
s := v4.String()
resp.Settings.NetworkRange = &s
}
if resp.Settings.NetworkRangeV6 == nil && v6.IsValid() {
s := v6.String()
resp.Settings.NetworkRangeV6 = &s
}
}
}
util.WriteJSONObject(r.Context(), w, []*api.Account{resp})
}
@@ -228,6 +277,9 @@ func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJS
if req.Settings.AutoUpdateAlways != nil {
returnSettings.AutoUpdateAlways = *req.Settings.AutoUpdateAlways
}
if req.Settings.Ipv6EnabledGroups != nil {
returnSettings.IPv6EnabledGroups = *req.Settings.Ipv6EnabledGroups
}
return returnSettings, nil
}
@@ -262,18 +314,23 @@ func (h *handler) updateAccount(w http.ResponseWriter, r *http.Request) {
return
}
if req.Settings.NetworkRange != nil && *req.Settings.NetworkRange != "" {
prefix, err := netip.ParsePrefix(*req.Settings.NetworkRange)
prefix, err := h.parseAndValidateNetworkRange(r.Context(), accountID, userID, *req.Settings.NetworkRange, false)
if err != nil {
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid CIDR format: %v", err), w)
return
}
if err := h.validateNetworkRange(r.Context(), accountID, userID, prefix); err != nil {
util.WriteError(r.Context(), err, w)
return
}
settings.NetworkRange = prefix
}
if req.Settings.NetworkRangeV6 != nil && *req.Settings.NetworkRangeV6 != "" {
prefix, err := h.parseAndValidateNetworkRange(r.Context(), accountID, userID, *req.Settings.NetworkRangeV6, true)
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
settings.NetworkRangeV6 = prefix
}
var onboarding *types.AccountOnboarding
if req.Onboarding != nil {
onboarding = &types.AccountOnboarding{
@@ -352,6 +409,7 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A
DnsDomain: &settings.DNSDomain,
AutoUpdateVersion: &settings.AutoUpdateVersion,
AutoUpdateAlways: &settings.AutoUpdateAlways,
Ipv6EnabledGroups: &settings.IPv6EnabledGroups,
EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled,
LocalAuthDisabled: &settings.LocalAuthDisabled,
}
@@ -360,6 +418,10 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A
networkRangeStr := settings.NetworkRange.String()
apiSettings.NetworkRange = &networkRangeStr
}
if settings.NetworkRangeV6.IsValid() {
networkRangeV6Str := settings.NetworkRangeV6.String()
apiSettings.NetworkRangeV6 = &networkRangeV6Str
}
apiOnboarding := api.AccountOnboarding{
OnboardingFlowPending: onboarding.OnboardingFlowPending,
@@ -5,8 +5,10 @@ import (
"context"
"encoding/json"
"io"
"math"
"net/http"
"net/http/httptest"
"net/netip"
"testing"
"time"
@@ -31,6 +33,10 @@ func initAccountsTestData(t *testing.T, account *types.Account) *handler {
GetSettings(gomock.Any(), account.Id, "test_user").
Return(account.Settings, nil).
AnyTimes()
settingsMockManager.EXPECT().
GetEffectiveNetworkRanges(gomock.Any(), account.Id).
Return(netip.Prefix{}, netip.Prefix{}, nil).
AnyTimes()
return &handler{
accountManager: &mock_server.MockAccountManager{
@@ -336,3 +342,27 @@ func TestAccounts_AccountsHandler(t *testing.T) {
})
}
}
func TestCalculateMaxHosts(t *testing.T) {
tests := []struct {
name string
prefix string
min int64
}{
{"v4 /24", "100.64.0.0/24", 254},
{"v4 /16", "100.64.0.0/16", 65534},
{"v4 /28", "100.64.0.0/28", 14},
{"v6 /64", "fd00::/64", math.MaxInt64},
{"v6 /120", "fd00::/120", 256},
{"v6 /112", "fd00::/112", 65536},
{"v6 /48", "fd00::/48", math.MaxInt64},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prefix := netip.MustParsePrefix(tt.prefix)
got := calculateMaxHosts(prefix)
assert.Equal(t, tt.min, got)
})
}
}
@@ -7,8 +7,8 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/http/httptest"
"strings"
"testing"
@@ -29,8 +29,8 @@ import (
)
var TestPeers = map[string]*nbpeer.Peer{
"A": {Key: "A", ID: "peer-A-ID", IP: net.ParseIP("100.100.100.100")},
"B": {Key: "B", ID: "peer-B-ID", IP: net.ParseIP("200.200.200.200")},
"A": {Key: "A", ID: "peer-A-ID", IP: netip.MustParseAddr("100.100.100.100")},
"B": {Key: "B", ID: "peer-B-ID", IP: netip.MustParseAddr("200.200.200.200")},
}
func initGroupTestData(initGroups ...*types.Group) *handler {
@@ -220,6 +220,18 @@ func (h *Handler) updatePeer(ctx context.Context, accountID, userID, peerID stri
}
}
if req.Ipv6 != nil {
v6Addr, err := parseIPv6(req.Ipv6)
if err != nil {
util.WriteError(ctx, status.Errorf(status.InvalidArgument, "%v", err), w)
return
}
if err = h.accountManager.UpdatePeerIPv6(ctx, accountID, userID, peerID, v6Addr); err != nil {
util.WriteError(ctx, err, w)
return
}
}
peer, err := h.accountManager.UpdatePeer(ctx, accountID, userID, update)
if err != nil {
util.WriteError(ctx, err, w)
@@ -355,6 +367,21 @@ func (h *Handler) setApprovalRequiredFlag(respBody []*api.PeerBatch, validPeersM
}
}
func parseIPv6(s *string) (netip.Addr, error) {
if s == nil {
return netip.Addr{}, fmt.Errorf("IPv6 address is nil")
}
addr, err := netip.ParseAddr(*s)
if err != nil {
return netip.Addr{}, fmt.Errorf("invalid IPv6 address %s: %w", *s, err)
}
addr = addr.Unmap()
if !addr.Is6() {
return netip.Addr{}, fmt.Errorf("address %s is not IPv6", *s)
}
return addr, nil
}
// GetAccessiblePeers returns a list of all peers that the specified peer can connect to within the network.
func (h *Handler) GetAccessiblePeers(w http.ResponseWriter, r *http.Request) {
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
@@ -529,6 +556,7 @@ func peerToAccessiblePeer(peer *nbpeer.Peer, dnsDomain string) api.AccessiblePee
GeonameId: int(peer.Location.GeoNameID),
Id: peer.ID,
Ip: peer.IP.String(),
Ipv6: peerIPv6String(peer),
LastSeen: peer.Status.LastSeen,
Name: peer.Name,
Os: peer.Meta.OS,
@@ -547,6 +575,7 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD
Id: peer.ID,
Name: peer.Name,
Ip: peer.IP.String(),
Ipv6: peerIPv6String(peer),
ConnectionIp: peer.Location.ConnectionIP.String(),
Connected: peer.Status.Connected,
LastSeen: peer.Status.LastSeen,
@@ -601,6 +630,7 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn
Id: peer.ID,
Name: peer.Name,
Ip: peer.IP.String(),
Ipv6: peerIPv6String(peer),
ConnectionIp: peer.Location.ConnectionIP.String(),
Connected: peer.Status.Connected,
LastSeen: peer.Status.LastSeen,
@@ -677,3 +707,11 @@ func fqdnList(extraLabels []string, dnsDomain string) []string {
}
return fqdnList
}
func peerIPv6String(peer *nbpeer.Peer) *string {
if !peer.IPv6.IsValid() {
return nil
}
s := peer.IPv6.String()
return &s
}
@@ -146,7 +146,7 @@ func initTestMetaData(t *testing.T, peers ...*nbpeer.Peer) *Handler {
UpdatePeerIPFunc: func(_ context.Context, accountID, userID, peerID string, newIP netip.Addr) error {
for _, peer := range peers {
if peer.ID == peerID {
peer.IP = net.IP(newIP.AsSlice())
peer.IP = newIP
return nil
}
}
@@ -228,7 +228,8 @@ func TestGetPeers(t *testing.T) {
peer := &nbpeer.Peer{
ID: testPeerID,
Key: "key",
IP: net.ParseIP("100.64.0.1"),
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
Status: &nbpeer.PeerStatus{Connected: true},
Name: "PeerName",
LoginExpirationEnabled: false,
@@ -368,7 +369,8 @@ func TestGetAccessiblePeers(t *testing.T) {
peer1 := &nbpeer.Peer{
ID: "peer1",
Key: "key1",
IP: net.ParseIP("100.64.0.1"),
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00:1234::1"),
Status: &nbpeer.PeerStatus{Connected: true},
Name: "peer1",
LoginExpirationEnabled: false,
@@ -378,7 +380,8 @@ func TestGetAccessiblePeers(t *testing.T) {
peer2 := &nbpeer.Peer{
ID: "peer2",
Key: "key2",
IP: net.ParseIP("100.64.0.2"),
IP: netip.MustParseAddr("100.64.0.2"),
IPv6: netip.MustParseAddr("fd00:1234::2"),
Status: &nbpeer.PeerStatus{Connected: true},
Name: "peer2",
LoginExpirationEnabled: false,
@@ -388,7 +391,8 @@ func TestGetAccessiblePeers(t *testing.T) {
peer3 := &nbpeer.Peer{
ID: "peer3",
Key: "key3",
IP: net.ParseIP("100.64.0.3"),
IP: netip.MustParseAddr("100.64.0.3"),
IPv6: netip.MustParseAddr("fd00:1234::3"),
Status: &nbpeer.PeerStatus{Connected: true},
Name: "peer3",
LoginExpirationEnabled: false,
@@ -532,7 +536,8 @@ func TestPeersHandlerUpdatePeerIP(t *testing.T) {
testPeer := &nbpeer.Peer{
ID: testPeerID,
Key: "key",
IP: net.ParseIP("100.64.0.1"),
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()},
Name: "test-host@netbird.io",
LoginExpirationEnabled: false,
@@ -5,9 +5,9 @@ import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/netip"
"os"
"strconv"
"testing"
@@ -133,7 +133,7 @@ func PopulateTestData(b *testing.B, am account.Manager, peers, groups, users, se
ID: fmt.Sprintf("oldpeer-%d", i),
DNSLabel: fmt.Sprintf("oldpeer-%d", i),
Key: peerKey.PublicKey().String(),
IP: net.ParseIP(fmt.Sprintf("100.64.%d.%d", i/256, i%256)),
IP: netip.MustParseAddr(fmt.Sprintf("100.64.%d.%d", i/256, i%256)),
Status: &nbpeer.PeerStatus{LastSeen: time.Now().UTC(), Connected: true},
UserID: TestUserId,
}
@@ -63,6 +63,7 @@ type MockAccountManager struct {
UpdatePeerMetaFunc func(ctx context.Context, peerID string, meta nbpeer.PeerSystemMeta) error
UpdatePeerFunc func(ctx context.Context, accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error)
UpdatePeerIPFunc func(ctx context.Context, accountID, userID, peerID string, newIP netip.Addr) error
UpdatePeerIPv6Func func(ctx context.Context, accountID, userID, peerID string, newIPv6 netip.Addr) error
CreateRouteFunc func(ctx context.Context, accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peer string, peerGroups []string, description string, netID route.NetID, masquerade bool, metric int, groups, accessControlGroupIDs []string, enabled bool, userID string, keepRoute bool, isSelected bool) (*route.Route, error)
GetRouteFunc func(ctx context.Context, accountID string, routeID route.ID, userID string) (*route.Route, error)
SaveRouteFunc func(ctx context.Context, accountID string, userID string, route *route.Route) error
@@ -539,6 +540,13 @@ func (am *MockAccountManager) UpdatePeerIP(ctx context.Context, accountID, userI
return status.Errorf(codes.Unimplemented, "method UpdatePeerIP is not implemented")
}
func (am *MockAccountManager) UpdatePeerIPv6(ctx context.Context, accountID, userID, peerID string, newIPv6 netip.Addr) error {
if am.UpdatePeerIPv6Func != nil {
return am.UpdatePeerIPv6Func(ctx, accountID, userID, peerID, newIPv6)
}
return status.Errorf(codes.Unimplemented, "method UpdatePeerIPv6 is not implemented")
}
// CreateRoute mock implementation of CreateRoute from server.AccountManager interface
func (am *MockAccountManager) CreateRoute(ctx context.Context, accountID string, prefix netip.Prefix, networkType route.NetworkType, domains domain.List, peerID string, peerGroupIDs []string, description string, netID route.NetID, masquerade bool, metric int, groups, accessControlGroupID []string, enabled bool, userID string, keepRoute bool, isSelected bool) (*route.Route, error) {
if am.CreateRouteFunc != nil {
+55 -9
View File
@@ -6,6 +6,7 @@ import (
b64 "encoding/base64"
"fmt"
"net"
"net/netip"
"slices"
"strings"
"time"
@@ -561,6 +562,27 @@ func (am *DefaultAccountManager) GetPeerNetwork(ctx context.Context, peerID stri
return account.Network.Copy(), err
}
// peerWillHaveIPv6 checks whether the peer's future group memberships
// (auto-groups + allGroupID) overlap with IPv6EnabledGroups.
func peerWillHaveIPv6(settings *types.Settings, groupsToAdd []string, allGroupID string) bool {
enabledSet := make(map[string]struct{}, len(settings.IPv6EnabledGroups))
for _, gid := range settings.IPv6EnabledGroups {
enabledSet[gid] = struct{}{}
}
if allGroupID != "" {
if _, ok := enabledSet[allGroupID]; ok {
return true
}
}
for _, gid := range groupsToAdd {
if _, ok := enabledSet[gid]; ok {
return true
}
}
return false
}
type peerAddAuthConfig struct {
AccountID string
SetupKeyID string
@@ -755,8 +777,11 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
maxAttempts := 10
for attempt := 1; attempt <= maxAttempts; attempt++ {
var freeIP net.IP
freeIP, err = types.AllocateRandomPeerIP(network.Net)
netPrefix, err := netip.ParsePrefix(network.Net.String())
if err != nil {
return nil, nil, nil, fmt.Errorf("parse network prefix: %w", err)
}
freeIP, err := types.AllocateRandomPeerIP(netPrefix)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to get free IP: %w", err)
}
@@ -776,6 +801,28 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
newPeer.DNSLabel = freeLabel
newPeer.IP = freeIP
if len(settings.IPv6EnabledGroups) > 0 && network.NetV6.IP != nil {
var allGroupID string
if !peer.ProxyMeta.Embedded {
allGroup, err := am.Store.GetGroupByName(ctx, store.LockingStrengthNone, accountID, "All")
if err != nil {
return nil, nil, nil, fmt.Errorf("get All group: %w", err)
}
allGroupID = allGroup.ID
}
if peerWillHaveIPv6(settings, peerAddConfig.GroupsToAdd, allGroupID) {
v6Prefix, err := netip.ParsePrefix(network.NetV6.String())
if err != nil {
return nil, nil, nil, fmt.Errorf("parse IPv6 prefix: %w", err)
}
freeIPv6, err := types.AllocateRandomPeerIPv6(v6Prefix)
if err != nil {
return nil, nil, nil, fmt.Errorf("allocate peer IPv6: %w", err)
}
newPeer.IPv6 = freeIPv6
}
}
err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
err = transaction.AddPeerToAccount(ctx, newPeer)
if err != nil {
@@ -845,10 +892,6 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
return nil, nil, nil, fmt.Errorf("failed to add peer to database: %w", err)
}
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to add peer to database after %d attempts: %w", maxAttempts, err)
}
if newPeer == nil {
return nil, nil, nil, fmt.Errorf("new peer is nil")
}
@@ -871,15 +914,18 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
return p, nmap, pc, err
}
func getPeerIPDNSLabel(ip net.IP, peerHostName string) (string, error) {
ip = ip.To4()
func getPeerIPDNSLabel(ip netip.Addr, peerHostName string) (string, error) {
if !ip.Is4() {
return "", fmt.Errorf("DNS label generation requires an IPv4 address, got %s", ip)
}
b := ip.As4()
dnsName, err := nbdns.GetParsedDomainLabel(peerHostName)
if err != nil {
return "", fmt.Errorf("failed to parse peer host name %s: %w", peerHostName, err)
}
return fmt.Sprintf("%s-%d-%d", dnsName, ip[2], ip[3]), nil
return fmt.Sprintf("%s-%d-%d", dnsName, b[2], b[3]), nil
}
// SyncPeer checks whether peer is eligible for receiving NetworkMap (authenticated) and returns its NetworkMap if eligible
+52 -4
View File
@@ -11,6 +11,12 @@ import (
"github.com/netbirdio/netbird/shared/management/http/api"
)
// Peer capability constants mirror the proto enum values.
const (
PeerCapabilitySourcePrefixes int32 = 1
PeerCapabilityIPv6Overlay int32 = 2
)
// Peer represents a machine connected to the network.
// The Peer is a WireGuard peer identified by a public key
type Peer struct {
@@ -21,7 +27,9 @@ type Peer struct {
// WireGuard public key
Key string // uniqueness index (check migrations)
// IP address of the Peer
IP net.IP `gorm:"serializer:json"` // uniqueness index per accountID (check migrations)
IP netip.Addr `gorm:"serializer:json"` // uniqueness index per accountID (check migrations)
// IPv6 overlay address of the Peer, zero value if IPv6 is not enabled for the account.
IPv6 netip.Addr `gorm:"serializer:json"`
// Meta is a Peer system meta data
Meta PeerSystemMeta `gorm:"embedded;embeddedPrefix:meta_"`
// ProxyMeta is metadata related to proxy peers
@@ -115,6 +123,7 @@ type Flags struct {
DisableFirewall bool
BlockLANAccess bool
BlockInbound bool
DisableIPv6 bool
LazyConnectionEnabled bool
}
@@ -138,6 +147,7 @@ type PeerSystemMeta struct { //nolint:revive
Environment Environment `gorm:"serializer:json"`
Flags Flags `gorm:"serializer:json"`
Files []File `gorm:"serializer:json"`
Capabilities []int32 `gorm:"serializer:json"`
}
func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool {
@@ -182,7 +192,8 @@ func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool {
p.SystemManufacturer == other.SystemManufacturer &&
p.Environment.Cloud == other.Environment.Cloud &&
p.Environment.Platform == other.Environment.Platform &&
p.Flags.isEqual(other.Flags)
p.Flags.isEqual(other.Flags) &&
capabilitiesEqual(p.Capabilities, other.Capabilities)
}
func (p PeerSystemMeta) isEmpty() bool {
@@ -210,6 +221,37 @@ func (p *Peer) AddedWithSSOLogin() bool {
return p.UserID != ""
}
// HasCapability reports whether the peer has the given capability.
func (p *Peer) HasCapability(capability int32) bool {
return slices.Contains(p.Meta.Capabilities, capability)
}
// SupportsIPv6 reports whether the peer supports IPv6 overlay.
func (p *Peer) SupportsIPv6() bool {
return !p.Meta.Flags.DisableIPv6 && p.HasCapability(PeerCapabilityIPv6Overlay)
}
// SupportsSourcePrefixes reports whether the peer reads SourcePrefixes.
func (p *Peer) SupportsSourcePrefixes() bool {
return p.HasCapability(PeerCapabilitySourcePrefixes)
}
func capabilitiesEqual(a, b []int32) bool {
if len(a) != len(b) {
return false
}
set := make(map[int32]struct{}, len(a))
for _, c := range a {
set[c] = struct{}{}
}
for _, c := range b {
if _, ok := set[c]; !ok {
return false
}
}
return true
}
// Copy copies Peer object
func (p *Peer) Copy() *Peer {
peerStatus := p.Status
@@ -221,6 +263,7 @@ func (p *Peer) Copy() *Peer {
AccountID: p.AccountID,
Key: p.Key,
IP: p.IP,
IPv6: p.IPv6,
Meta: p.Meta,
Name: p.Name,
DNSLabel: p.DNSLabel,
@@ -323,9 +366,13 @@ func (p *Peer) FQDN(dnsDomain string) string {
// EventMeta returns activity event meta related to the peer
func (p *Peer) EventMeta(dnsDomain string) map[string]any {
return map[string]any{"name": p.Name, "fqdn": p.FQDN(dnsDomain), "ip": p.IP, "created_at": p.CreatedAt,
meta := map[string]any{"name": p.Name, "fqdn": p.FQDN(dnsDomain), "ip": p.IP, "created_at": p.CreatedAt,
"location_city_name": p.Location.CityName, "location_country_code": p.Location.CountryCode,
"location_geo_name_id": p.Location.GeoNameID, "location_connection_ip": p.Location.ConnectionIP}
if p.IPv6.IsValid() {
meta["ipv6"] = p.IPv6.String()
}
return meta
}
// Copy PeerStatus
@@ -369,5 +416,6 @@ func (f Flags) isEqual(other Flags) bool {
f.DisableFirewall == other.DisableFirewall &&
f.BlockLANAccess == other.BlockLANAccess &&
f.BlockInbound == other.BlockInbound &&
f.LazyConnectionEnabled == other.LazyConnectionEnabled
f.LazyConnectionEnabled == other.LazyConnectionEnabled &&
f.DisableIPv6 == other.DisableIPv6
}
+23
View File
@@ -5,6 +5,7 @@ import (
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -141,3 +142,25 @@ func TestFlags_IsEqual(t *testing.T) {
})
}
}
func TestPeerCapabilities(t *testing.T) {
tests := []struct {
name string
capabilities []int32
ipv6 bool
srcPrefixes bool
}{
{"no capabilities", nil, false, false},
{"only source prefixes", []int32{PeerCapabilitySourcePrefixes}, false, true},
{"only ipv6", []int32{PeerCapabilityIPv6Overlay}, true, false},
{"both", []int32{PeerCapabilitySourcePrefixes, PeerCapabilityIPv6Overlay}, true, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Peer{Meta: PeerSystemMeta{Capabilities: tt.capabilities}}
assert.Equal(t, tt.ipv6, p.SupportsIPv6())
assert.Equal(t, tt.srcPrefixes, p.SupportsSourcePrefixes())
})
}
}
+55 -12
View File
@@ -774,7 +774,8 @@ func setupTestAccountManager(b testing.TB, peers int, groups int) (*DefaultAccou
ID: fmt.Sprintf("peer-%d", i),
DNSLabel: fmt.Sprintf("peer-%d", i),
Key: peerKey.PublicKey().String(),
IP: net.ParseIP(fmt.Sprintf("100.64.%d.%d", i/256, i%256)),
IP: netip.MustParseAddr(fmt.Sprintf("100.64.%d.%d", i/256, i%256)),
IPv6: netip.MustParseAddr(fmt.Sprintf("fd00::%d", i+1)),
Status: &nbpeer.PeerStatus{LastSeen: time.Now().UTC(), Connected: true},
UserID: regularUser,
}
@@ -803,7 +804,15 @@ func setupTestAccountManager(b testing.TB, peers int, groups int) (*DefaultAccou
account.Networks = append(account.Networks, network)
ips := account.GetTakenIPs()
peerIP, err := types.AllocatePeerIP(account.Network.Net, ips)
peerIP, err := types.AllocatePeerIP(netip.MustParsePrefix(account.Network.Net.String()), ips)
if err != nil {
return nil, nil, "", "", err
}
v6Prefix, err := netip.ParsePrefix(account.Network.NetV6.String())
if err != nil {
return nil, nil, "", "", err
}
peerIPv6, err := types.AllocateRandomPeerIPv6(v6Prefix)
if err != nil {
return nil, nil, "", "", err
}
@@ -814,6 +823,7 @@ func setupTestAccountManager(b testing.TB, peers int, groups int) (*DefaultAccou
DNSLabel: fmt.Sprintf("peer-nr-%d", len(account.Peers)+1),
Key: peerKey.PublicKey().String(),
IP: peerIP,
IPv6: peerIPv6,
Status: &nbpeer.PeerStatus{LastSeen: time.Now().UTC(), Connected: true},
UserID: regularUser,
Meta: nbpeer.PeerSystemMeta{
@@ -1093,7 +1103,8 @@ func TestToSyncResponse(t *testing.T) {
},
}
peer := &nbpeer.Peer{
IP: net.ParseIP("192.168.1.1"),
IP: netip.MustParseAddr("192.168.1.1"),
IPv6: netip.MustParseAddr("fd00::1"),
SSHEnabled: true,
Key: "peer-key",
DNSLabel: "peer1",
@@ -1104,9 +1115,21 @@ func TestToSyncResponse(t *testing.T) {
Signature: "turn-pass",
}
networkMap := &types.NetworkMap{
Network: &types.Network{Net: *ipnet, Serial: 1000},
Peers: []*nbpeer.Peer{{IP: net.ParseIP("192.168.1.2"), Key: "peer2-key", DNSLabel: "peer2", SSHEnabled: true, SSHKey: "peer2-ssh-key"}},
OfflinePeers: []*nbpeer.Peer{{IP: net.ParseIP("192.168.1.3"), Key: "peer3-key", DNSLabel: "peer3", SSHEnabled: true, SSHKey: "peer3-ssh-key"}},
Network: &types.Network{Net: *ipnet, Serial: 1000},
Peers: []*nbpeer.Peer{{
IP: netip.MustParseAddr("192.168.1.2"),
IPv6: netip.MustParseAddr("fd00::2"),
Key: "peer2-key",
DNSLabel: "peer2",
SSHEnabled: true,
SSHKey: "peer2-ssh-key"}},
OfflinePeers: []*nbpeer.Peer{{
IP: netip.MustParseAddr("192.168.1.3"),
IPv6: netip.MustParseAddr("fd00::3"),
Key: "peer3-key",
DNSLabel: "peer3",
SSHEnabled: true,
SSHKey: "peer3-ssh-key"}},
Routes: []*nbroute.Route{
{
ID: "route1",
@@ -1312,7 +1335,8 @@ func Test_RegisterPeerByUser(t *testing.T) {
ID: xid.New().String(),
AccountID: existingAccountID,
Key: "newPeerKey",
IP: net.IP{123, 123, 123, 123},
IP: netip.AddrFrom4([4]byte{123, 123, 123, 123}),
IPv6: netip.MustParseAddr("fd00::7b:7b:7b:7b"),
Meta: nbpeer.PeerSystemMeta{
Hostname: "newPeer",
GoOS: "linux",
@@ -1396,7 +1420,8 @@ func Test_RegisterPeerBySetupKey(t *testing.T) {
newPeerTemplate := &nbpeer.Peer{
AccountID: existingAccountID,
UserID: "",
IP: net.IP{123, 123, 123, 123},
IP: netip.AddrFrom4([4]byte{123, 123, 123, 123}),
IPv6: netip.MustParseAddr("fd00::7b:7b:7b:7b"),
Meta: nbpeer.PeerSystemMeta{
Hostname: "newPeer",
GoOS: "linux",
@@ -1553,7 +1578,8 @@ func Test_RegisterPeerRollbackOnFailure(t *testing.T) {
AccountID: existingAccountID,
Key: "newPeerKey",
UserID: "",
IP: net.IP{123, 123, 123, 123},
IP: netip.AddrFrom4([4]byte{123, 123, 123, 123}),
IPv6: netip.MustParseAddr("fd00::7b:7b:7b:7b"),
Meta: nbpeer.PeerSystemMeta{
Hostname: "newPeer",
GoOS: "linux",
@@ -1635,7 +1661,8 @@ func Test_LoginPeer(t *testing.T) {
newPeerTemplate := &nbpeer.Peer{
AccountID: existingAccountID,
UserID: "",
IP: net.IP{123, 123, 123, 123},
IP: netip.AddrFrom4([4]byte{123, 123, 123, 123}),
IPv6: netip.MustParseAddr("fd00::7b:7b:7b:7b"),
Meta: nbpeer.PeerSystemMeta{
Hostname: "newPeer",
GoOS: "linux",
@@ -2137,14 +2164,16 @@ func Test_DeletePeer(t *testing.T) {
ID: "peer1",
AccountID: accountID,
Key: "key1",
IP: net.IP{1, 1, 1, 1},
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
IPv6: netip.MustParseAddr("fd00::1"),
DNSLabel: "peer1.test",
},
"peer2": {
ID: "peer2",
AccountID: accountID,
Key: "key2",
IP: net.IP{2, 2, 2, 2},
IP: netip.AddrFrom4([4]byte{2, 2, 2, 2}),
IPv6: netip.MustParseAddr("fd00::2"),
DNSLabel: "peer2.test",
},
}
@@ -2741,6 +2770,20 @@ func TestProcessPeerAddAuth(t *testing.T) {
})
}
func TestPeerWillHaveIPv6(t *testing.T) {
settings := &types.Settings{
IPv6EnabledGroups: []string{"all-group-id", "group-a"},
}
assert.True(t, peerWillHaveIPv6(settings, nil, "all-group-id"), "peer in All group should get IPv6")
assert.True(t, peerWillHaveIPv6(settings, []string{"group-a"}, ""), "peer with matching auto-group should get IPv6")
assert.False(t, peerWillHaveIPv6(settings, []string{"group-b"}, "other-all"), "peer with no matching groups should not get IPv6")
assert.False(t, peerWillHaveIPv6(settings, nil, ""), "embedded peer with no groups should not get IPv6")
emptySettings := &types.Settings{IPv6EnabledGroups: []string{}}
assert.False(t, peerWillHaveIPv6(emptySettings, []string{"group-a"}, "all-group-id"), "no IPv6 groups means no IPv6")
}
func TestUpdatePeer_DnsLabelCollisionWithFQDN(t *testing.T) {
manager, _, err := createManager(t)
require.NoError(t, err, "unable to create account manager")
+23 -23
View File
@@ -3,7 +3,7 @@ package server
import (
"context"
"fmt"
"net"
"net/netip"
"testing"
"time"
@@ -20,53 +20,53 @@ func TestAccount_getPeersByPolicy(t *testing.T) {
Peers: map[string]*nbpeer.Peer{
"peerA": {
ID: "peerA",
IP: net.ParseIP("100.65.14.88"),
IP: netip.MustParseAddr("100.65.14.88"),
Status: &nbpeer.PeerStatus{},
},
"peerB": {
ID: "peerB",
IP: net.ParseIP("100.65.80.39"),
IP: netip.MustParseAddr("100.65.80.39"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.48.0"},
},
"peerC": {
ID: "peerC",
IP: net.ParseIP("100.65.254.139"),
IP: netip.MustParseAddr("100.65.254.139"),
Status: &nbpeer.PeerStatus{},
},
"peerD": {
ID: "peerD",
IP: net.ParseIP("100.65.62.5"),
IP: netip.MustParseAddr("100.65.62.5"),
Status: &nbpeer.PeerStatus{},
},
"peerE": {
ID: "peerE",
IP: net.ParseIP("100.65.32.206"),
IP: netip.MustParseAddr("100.65.32.206"),
Status: &nbpeer.PeerStatus{},
},
"peerF": {
ID: "peerF",
IP: net.ParseIP("100.65.250.202"),
IP: netip.MustParseAddr("100.65.250.202"),
Status: &nbpeer.PeerStatus{},
},
"peerG": {
ID: "peerG",
IP: net.ParseIP("100.65.13.186"),
IP: netip.MustParseAddr("100.65.13.186"),
Status: &nbpeer.PeerStatus{},
},
"peerH": {
ID: "peerH",
IP: net.ParseIP("100.65.29.55"),
IP: netip.MustParseAddr("100.65.29.55"),
Status: &nbpeer.PeerStatus{},
},
"peerI": {
ID: "peerI",
IP: net.ParseIP("100.65.31.2"),
IP: netip.MustParseAddr("100.65.31.2"),
Status: &nbpeer.PeerStatus{},
},
"peerK": {
ID: "peerK",
IP: net.ParseIP("100.32.80.1"),
IP: netip.MustParseAddr("100.32.80.1"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.30.0"},
},
@@ -540,17 +540,17 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) {
Peers: map[string]*nbpeer.Peer{
"peerA": {
ID: "peerA",
IP: net.ParseIP("100.65.14.88"),
IP: netip.MustParseAddr("100.65.14.88"),
Status: &nbpeer.PeerStatus{},
},
"peerB": {
ID: "peerB",
IP: net.ParseIP("100.65.80.39"),
IP: netip.MustParseAddr("100.65.80.39"),
Status: &nbpeer.PeerStatus{},
},
"peerC": {
ID: "peerC",
IP: net.ParseIP("100.65.254.139"),
IP: netip.MustParseAddr("100.65.254.139"),
Status: &nbpeer.PeerStatus{},
},
},
@@ -746,7 +746,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
Peers: map[string]*nbpeer.Peer{
"peerA": {
ID: "peerA",
IP: net.ParseIP("100.65.14.88"),
IP: netip.MustParseAddr("100.65.14.88"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
@@ -756,7 +756,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
},
"peerB": {
ID: "peerB",
IP: net.ParseIP("100.65.80.39"),
IP: netip.MustParseAddr("100.65.80.39"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
@@ -766,7 +766,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
},
"peerC": {
ID: "peerC",
IP: net.ParseIP("100.65.254.139"),
IP: netip.MustParseAddr("100.65.254.139"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
@@ -776,7 +776,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
},
"peerD": {
ID: "peerD",
IP: net.ParseIP("100.65.62.5"),
IP: netip.MustParseAddr("100.65.62.5"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
@@ -786,7 +786,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
},
"peerE": {
ID: "peerE",
IP: net.ParseIP("100.65.32.206"),
IP: netip.MustParseAddr("100.65.32.206"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
@@ -796,7 +796,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
},
"peerF": {
ID: "peerF",
IP: net.ParseIP("100.65.250.202"),
IP: netip.MustParseAddr("100.65.250.202"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
@@ -806,7 +806,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
},
"peerG": {
ID: "peerG",
IP: net.ParseIP("100.65.13.186"),
IP: netip.MustParseAddr("100.65.13.186"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
@@ -816,7 +816,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
},
"peerH": {
ID: "peerH",
IP: net.ParseIP("100.65.29.55"),
IP: netip.MustParseAddr("100.65.29.55"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
@@ -826,7 +826,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
},
"peerI": {
ID: "peerI",
IP: net.ParseIP("100.65.21.56"),
IP: netip.MustParseAddr("100.65.21.56"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "windows",
+118 -41
View File
@@ -3,7 +3,6 @@ package server
import (
"context"
"fmt"
"net"
"net/netip"
"sort"
"testing"
@@ -1328,14 +1327,24 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*types.Accou
return nil, err
}
v6Prefix, err := netip.ParsePrefix(account.Network.NetV6.String())
if err != nil {
return nil, err
}
ips := account.GetTakenIPs()
peer1IP, err := types.AllocatePeerIP(account.Network.Net, ips)
peer1IP, err := types.AllocatePeerIP(netip.MustParsePrefix(account.Network.Net.String()), ips)
if err != nil {
return nil, err
}
peer1IPv6, err := types.AllocateRandomPeerIPv6(v6Prefix)
if err != nil {
return nil, err
}
peer1 := &nbpeer.Peer{
IP: peer1IP,
IPv6: peer1IPv6,
ID: peer1ID,
Key: peer1Key,
Name: "test-host1@netbird.io",
@@ -1356,13 +1365,18 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*types.Accou
account.Peers[peer1.ID] = peer1
ips = account.GetTakenIPs()
peer2IP, err := types.AllocatePeerIP(account.Network.Net, ips)
peer2IP, err := types.AllocatePeerIP(netip.MustParsePrefix(account.Network.Net.String()), ips)
if err != nil {
return nil, err
}
peer2IPv6, err := types.AllocateRandomPeerIPv6(v6Prefix)
if err != nil {
return nil, err
}
peer2 := &nbpeer.Peer{
IP: peer2IP,
IPv6: peer2IPv6,
ID: peer2ID,
Key: peer2Key,
Name: "test-host2@netbird.io",
@@ -1383,13 +1397,18 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*types.Accou
account.Peers[peer2.ID] = peer2
ips = account.GetTakenIPs()
peer3IP, err := types.AllocatePeerIP(account.Network.Net, ips)
peer3IP, err := types.AllocatePeerIP(netip.MustParsePrefix(account.Network.Net.String()), ips)
if err != nil {
return nil, err
}
peer3IPv6, err := types.AllocateRandomPeerIPv6(v6Prefix)
if err != nil {
return nil, err
}
peer3 := &nbpeer.Peer{
IP: peer3IP,
IPv6: peer3IPv6,
ID: peer3ID,
Key: peer3Key,
Name: "test-host3@netbird.io",
@@ -1410,13 +1429,18 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*types.Accou
account.Peers[peer3.ID] = peer3
ips = account.GetTakenIPs()
peer4IP, err := types.AllocatePeerIP(account.Network.Net, ips)
peer4IP, err := types.AllocatePeerIP(netip.MustParsePrefix(account.Network.Net.String()), ips)
if err != nil {
return nil, err
}
peer4IPv6, err := types.AllocateRandomPeerIPv6(v6Prefix)
if err != nil {
return nil, err
}
peer4 := &nbpeer.Peer{
IP: peer4IP,
IPv6: peer4IPv6,
ID: peer4ID,
Key: peer4Key,
Name: "test-host4@netbird.io",
@@ -1437,13 +1461,18 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*types.Accou
account.Peers[peer4.ID] = peer4
ips = account.GetTakenIPs()
peer5IP, err := types.AllocatePeerIP(account.Network.Net, ips)
peer5IP, err := types.AllocatePeerIP(netip.MustParsePrefix(account.Network.Net.String()), ips)
if err != nil {
return nil, err
}
peer5IPv6, err := types.AllocateRandomPeerIPv6(v6Prefix)
if err != nil {
return nil, err
}
peer5 := &nbpeer.Peer{
IP: peer5IP,
IPv6: peer5IPv6,
ID: peer5ID,
Key: peer5Key,
Name: "test-host5@netbird.io",
@@ -1544,7 +1573,8 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
Peers: map[string]*nbpeer.Peer{
"peerA": {
ID: "peerA",
IP: net.ParseIP("100.65.14.88"),
IP: netip.MustParseAddr("100.65.14.88"),
IPv6: netip.MustParseAddr("fd00::1"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
@@ -1552,18 +1582,21 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
},
"peerB": {
ID: "peerB",
IP: net.ParseIP(peerBIp),
IP: netip.MustParseAddr(peerBIp),
IPv6: netip.MustParseAddr("fd00::2"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{},
},
"peerC": {
ID: "peerC",
IP: net.ParseIP(peerCIp),
IP: netip.MustParseAddr(peerCIp),
IPv6: netip.MustParseAddr("fd00::3"),
Status: &nbpeer.PeerStatus{},
},
"peerD": {
ID: "peerD",
IP: net.ParseIP("100.65.62.5"),
IP: netip.MustParseAddr("100.65.62.5"),
IPv6: netip.MustParseAddr("fd00::4"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
@@ -1571,7 +1604,8 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
},
"peerE": {
ID: "peerE",
IP: net.ParseIP("100.65.32.206"),
IP: netip.MustParseAddr("100.65.32.206"),
IPv6: netip.MustParseAddr("fd00::5"),
Key: peer1Key,
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
@@ -1580,27 +1614,32 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
},
"peerF": {
ID: "peerF",
IP: net.ParseIP("100.65.250.202"),
IP: netip.MustParseAddr("100.65.250.202"),
IPv6: netip.MustParseAddr("fd00::6"),
Status: &nbpeer.PeerStatus{},
},
"peerG": {
ID: "peerG",
IP: net.ParseIP("100.65.13.186"),
IP: netip.MustParseAddr("100.65.13.186"),
IPv6: netip.MustParseAddr("fd00::7"),
Status: &nbpeer.PeerStatus{},
},
"peerH": {
ID: "peerH",
IP: net.ParseIP(peerHIp),
IP: netip.MustParseAddr(peerHIp),
IPv6: netip.MustParseAddr("fd00::8"),
Status: &nbpeer.PeerStatus{},
},
"peerJ": {
ID: "peerJ",
IP: net.ParseIP(peerJIp),
IP: netip.MustParseAddr(peerJIp),
IPv6: netip.MustParseAddr("fd00::a"),
Status: &nbpeer.PeerStatus{},
},
"peerK": {
ID: "peerK",
IP: net.ParseIP(peerKIp),
IP: netip.MustParseAddr(peerKIp),
IPv6: netip.MustParseAddr("fd00::b"),
Status: &nbpeer.PeerStatus{},
},
},
@@ -1853,7 +1892,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
})
t.Run("check peer routes firewall rules", func(t *testing.T) {
routesFirewallRules := account.GetPeerRoutesFirewallRules(context.Background(), "peerA", validatedPeers)
routesFirewallRules := account.GetPeerRoutesFirewallRules(context.Background(), "peerA", validatedPeers, true)
assert.Len(t, routesFirewallRules, 4)
expectedRoutesFirewallRules := []*types.RouteFirewallRule{
@@ -1907,7 +1946,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
assert.ElementsMatch(t, orderRuleSourceRanges(routesFirewallRules), orderRuleSourceRanges(append(expectedRoutesFirewallRules, additionalFirewallRule...)))
// peerD is also the routing peer for route1, should contain same routes firewall rules as peerA
routesFirewallRules = account.GetPeerRoutesFirewallRules(context.Background(), "peerD", validatedPeers)
routesFirewallRules = account.GetPeerRoutesFirewallRules(context.Background(), "peerD", validatedPeers, true)
assert.Len(t, routesFirewallRules, 2)
for _, rule := range expectedRoutesFirewallRules {
rule.RouteID = "route1:peerD"
@@ -1915,7 +1954,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
assert.ElementsMatch(t, orderRuleSourceRanges(routesFirewallRules), orderRuleSourceRanges(expectedRoutesFirewallRules))
// peerE is a single routing peer for route 2 and route 3
routesFirewallRules = account.GetPeerRoutesFirewallRules(context.Background(), "peerE", validatedPeers)
routesFirewallRules = account.GetPeerRoutesFirewallRules(context.Background(), "peerE", validatedPeers, true)
assert.Len(t, routesFirewallRules, 3)
expectedRoutesFirewallRules = []*types.RouteFirewallRule{
@@ -1949,7 +1988,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) {
assert.ElementsMatch(t, orderRuleSourceRanges(routesFirewallRules), orderRuleSourceRanges(expectedRoutesFirewallRules))
// peerC is part of route1 distribution groups but should not receive the routes firewall rules
routesFirewallRules = account.GetPeerRoutesFirewallRules(context.Background(), "peerC", validatedPeers)
routesFirewallRules = account.GetPeerRoutesFirewallRules(context.Background(), "peerC", validatedPeers, true)
assert.Len(t, routesFirewallRules, 0)
})
@@ -2239,84 +2278,101 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
Peers: map[string]*nbpeer.Peer{
"peerA": {
ID: "peerA",
IP: net.ParseIP("100.65.14.88"),
IP: netip.MustParseAddr("100.65.14.88"),
IPv6: netip.MustParseAddr("fd00::1"),
Key: "peerA",
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
GoOS: "linux",
Capabilities: []int32{nbpeer.PeerCapabilityIPv6Overlay},
},
},
"peerB": {
ID: "peerB",
IP: net.ParseIP(peerBIp),
IP: netip.MustParseAddr(peerBIp),
IPv6: netip.MustParseAddr("fd00::2"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{},
},
"peerC": {
ID: "peerC",
IP: net.ParseIP(peerCIp),
IP: netip.MustParseAddr(peerCIp),
IPv6: netip.MustParseAddr("fd00::3"),
Status: &nbpeer.PeerStatus{},
},
"peerD": {
ID: "peerD",
IP: net.ParseIP("100.65.62.5"),
IP: netip.MustParseAddr("100.65.62.5"),
IPv6: netip.MustParseAddr("fd00::4"),
Key: "peerD",
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
GoOS: "linux",
Capabilities: []int32{nbpeer.PeerCapabilityIPv6Overlay},
},
},
"peerE": {
ID: "peerE",
IP: net.ParseIP("100.65.32.206"),
IP: netip.MustParseAddr("100.65.32.206"),
IPv6: netip.MustParseAddr("fd00::5"),
Key: "peerE",
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
GoOS: "linux",
Capabilities: []int32{nbpeer.PeerCapabilityIPv6Overlay},
},
},
"peerF": {
ID: "peerF",
IP: net.ParseIP("100.65.250.202"),
IP: netip.MustParseAddr("100.65.250.202"),
IPv6: netip.MustParseAddr("fd00::6"),
Status: &nbpeer.PeerStatus{},
},
"peerG": {
ID: "peerG",
IP: net.ParseIP("100.65.13.186"),
IP: netip.MustParseAddr("100.65.13.186"),
IPv6: netip.MustParseAddr("fd00::7"),
Status: &nbpeer.PeerStatus{},
},
"peerH": {
ID: "peerH",
IP: net.ParseIP(peerHIp),
IP: netip.MustParseAddr(peerHIp),
IPv6: netip.MustParseAddr("fd00::8"),
Status: &nbpeer.PeerStatus{},
},
"peerJ": {
ID: "peerJ",
IP: net.ParseIP(peerJIp),
IP: netip.MustParseAddr(peerJIp),
IPv6: netip.MustParseAddr("fd00::a"),
Status: &nbpeer.PeerStatus{},
},
"peerK": {
ID: "peerK",
IP: net.ParseIP(peerKIp),
IP: netip.MustParseAddr(peerKIp),
IPv6: netip.MustParseAddr("fd00::b"),
Status: &nbpeer.PeerStatus{},
},
"peerL": {
ID: "peerL",
IP: net.ParseIP("100.65.19.186"),
IP: netip.MustParseAddr("100.65.19.186"),
IPv6: netip.MustParseAddr("fd00::d"),
Key: "peerL",
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
GoOS: "linux",
Capabilities: []int32{nbpeer.PeerCapabilityIPv6Overlay},
},
},
"peerM": {
ID: "peerM",
IP: net.ParseIP(peerMIp),
IP: netip.MustParseAddr(peerMIp),
IPv6: netip.MustParseAddr("fd00::e"),
Status: &nbpeer.PeerStatus{},
},
"peerN": {
ID: "peerN",
IP: net.ParseIP("100.65.20.18"),
IP: netip.MustParseAddr("100.65.20.18"),
IPv6: netip.MustParseAddr("fd00::f"),
Key: "peerN",
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
@@ -2325,7 +2381,8 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
},
"peerO": {
ID: "peerO",
IP: net.ParseIP(peerOIp),
IP: netip.MustParseAddr(peerOIp),
IPv6: netip.MustParseAddr("fd00::10"),
Status: &nbpeer.PeerStatus{},
},
},
@@ -2692,7 +2749,7 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
resourceRoutersMap := account.GetResourceRoutersMap()
_, routes, sourcePeers := account.GetNetworkResourcesRoutesToSync(context.Background(), "peerA", resourcePoliciesMap, resourceRoutersMap)
firewallRules := account.GetPeerNetworkResourceFirewallRules(context.Background(), account.Peers["peerA"], validatedPeers, routes, resourcePoliciesMap)
assert.Len(t, firewallRules, 4)
assert.Len(t, firewallRules, 6)
assert.Len(t, sourcePeers, 5)
expectedFirewallRules := []*types.RouteFirewallRule{
@@ -2746,6 +2803,25 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
IsDynamic: true,
RouteID: "resource4:peerA",
},
{
SourceRanges: []string{"fd00::a/128"},
Action: "accept",
Destination: "192.0.2.0/32",
Protocol: "tcp",
Port: 80,
Domains: domain.List{"example.com"},
IsDynamic: true,
RouteID: "resource4:peerA",
},
{
SourceRanges: []string{"fd00::b/128"},
Action: "accept",
Destination: "192.0.2.0/32",
Protocol: "all",
Domains: domain.List{"example.com"},
IsDynamic: true,
RouteID: "resource4:peerA",
},
}
assert.ElementsMatch(t, orderRuleSourceRanges(firewallRules), orderRuleSourceRanges(append(expectedFirewallRules, additionalFirewallRules...)))
@@ -2778,8 +2854,9 @@ func TestAccount_GetPeerNetworkResourceFirewallRules(t *testing.T) {
}
assert.ElementsMatch(t, orderRuleSourceRanges(firewallRules), orderRuleSourceRanges(expectedFirewallRules))
// peerC is part of distribution groups for resource2 but should not receive the firewall rules
firewallRules = account.GetPeerRoutesFirewallRules(context.Background(), "peerC", validatedPeers)
// peerC is in a distribution group for resource2 but is not a routing peer, so it should not receive firewall rules
_, peerCRoutes, _ := account.GetNetworkResourcesRoutesToSync(context.Background(), "peerC", resourcePoliciesMap, resourceRoutersMap)
firewallRules = account.GetPeerNetworkResourceFirewallRules(context.Background(), account.Peers["peerC"], validatedPeers, peerCRoutes, resourcePoliciesMap)
assert.Len(t, firewallRules, 0)
// peerL is the single routing peer for resource5
+29
View File
@@ -5,6 +5,7 @@ package settings
import (
"context"
"fmt"
"net/netip"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/management/server/integrations/extra_settings"
@@ -22,6 +23,9 @@ type Manager interface {
GetSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error)
GetExtraSettings(ctx context.Context, accountID string) (*types.ExtraSettings, error)
UpdateExtraSettings(ctx context.Context, accountID, userID string, extraSettings *types.ExtraSettings) (bool, error)
// GetEffectiveNetworkRanges returns the actual allocated network ranges (v4 and v6).
// This includes auto-allocated ranges even when no custom override was set.
GetEffectiveNetworkRanges(ctx context.Context, accountID string) (v4, v6 netip.Prefix, err error)
}
// IdpConfig holds IdP-related configuration that is set at runtime
@@ -115,3 +119,28 @@ func (m *managerImpl) GetExtraSettings(ctx context.Context, accountID string) (*
func (m *managerImpl) UpdateExtraSettings(ctx context.Context, accountID, userID string, extraSettings *types.ExtraSettings) (bool, error) {
return m.extraSettingsManager.UpdateExtraSettings(ctx, accountID, userID, extraSettings)
}
// GetEffectiveNetworkRanges returns the actual allocated network ranges from the account's network object.
func (m *managerImpl) GetEffectiveNetworkRanges(ctx context.Context, accountID string) (netip.Prefix, netip.Prefix, error) {
network, err := m.store.GetAccountNetwork(ctx, store.LockingStrengthNone, accountID)
if err != nil {
return netip.Prefix{}, netip.Prefix{}, fmt.Errorf("get account network: %w", err)
}
var v4, v6 netip.Prefix
if network.Net.IP != nil {
addr, ok := netip.AddrFromSlice(network.Net.IP)
if ok {
ones, _ := network.Net.Mask.Size()
v4 = netip.PrefixFrom(addr.Unmap(), ones)
}
}
if network.NetV6.IP != nil {
addr, ok := netip.AddrFromSlice(network.NetV6.IP)
if ok {
ones, _ := network.NetV6.Mask.Size()
v6 = netip.PrefixFrom(addr.Unmap(), ones)
}
}
return v4, v6, nil
}
@@ -6,6 +6,7 @@ package settings
import (
context "context"
netip "net/netip"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
@@ -94,3 +95,19 @@ func (mr *MockManagerMockRecorder) UpdateExtraSettings(ctx, accountID, userID, e
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateExtraSettings", reflect.TypeOf((*MockManager)(nil).UpdateExtraSettings), ctx, accountID, userID, extraSettings)
}
// GetEffectiveNetworkRanges mocks base method.
func (m *MockManager) GetEffectiveNetworkRanges(ctx context.Context, accountID string) (netip.Prefix, netip.Prefix, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetEffectiveNetworkRanges", ctx, accountID)
ret0, _ := ret[0].(netip.Prefix)
ret1, _ := ret[1].(netip.Prefix)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// GetEffectiveNetworkRanges indicates an expected call of GetEffectiveNetworkRanges.
func (mr *MockManagerMockRecorder) GetEffectiveNetworkRanges(ctx, accountID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEffectiveNetworkRanges", reflect.TypeOf((*MockManager)(nil).GetEffectiveNetworkRanges), ctx, accountID)
}
+53 -18
View File
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"net"
"net/netip"
"os"
"path/filepath"
"runtime"
@@ -1499,7 +1500,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
SELECT
id, created_by, created_at, domain, domain_category, is_domain_primary_account,
-- Embedded Network
network_identifier, network_net, network_dns, network_serial,
network_identifier, network_net, network_net_v6, network_dns, network_serial,
-- Embedded DNSSettings
dns_settings_disabled_management_groups,
-- Embedded Settings
@@ -1508,7 +1509,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
settings_regular_users_view_blocked, settings_groups_propagation_enabled,
settings_jwt_groups_enabled, settings_jwt_groups_claim_name, settings_jwt_allow_groups,
settings_routing_peer_dns_resolution_enabled, settings_dns_domain, settings_network_range,
settings_lazy_connection_enabled,
settings_network_range_v6, settings_ipv6_enabled_groups, settings_lazy_connection_enabled,
-- Embedded ExtraSettings
settings_extra_peer_approval_enabled, settings_extra_user_approval_required,
settings_extra_integrated_validator, settings_extra_integrated_validator_groups
@@ -1527,12 +1528,15 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
sRoutingPeerDNSResolutionEnabled sql.NullBool
sDNSDomain sql.NullString
sNetworkRange sql.NullString
sNetworkRangeV6 sql.NullString
sIPv6EnabledGroups sql.NullString
sLazyConnectionEnabled sql.NullBool
sExtraPeerApprovalEnabled sql.NullBool
sExtraUserApprovalRequired sql.NullBool
sExtraIntegratedValidator sql.NullString
sExtraIntegratedValidatorGroups sql.NullString
networkNet sql.NullString
networkNetV6 sql.NullString
dnsSettingsDisabledGroups sql.NullString
networkIdentifier sql.NullString
networkDns sql.NullString
@@ -1541,14 +1545,14 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
)
err := s.pool.QueryRow(ctx, accountQuery, accountID).Scan(
&account.Id, &account.CreatedBy, &createdAt, &account.Domain, &account.DomainCategory, &account.IsDomainPrimaryAccount,
&networkIdentifier, &networkNet, &networkDns, &networkSerial,
&networkIdentifier, &networkNet, &networkNetV6, &networkDns, &networkSerial,
&dnsSettingsDisabledGroups,
&sPeerLoginExpirationEnabled, &sPeerLoginExpiration,
&sPeerInactivityExpirationEnabled, &sPeerInactivityExpiration,
&sRegularUsersViewBlocked, &sGroupsPropagationEnabled,
&sJWTGroupsEnabled, &sJWTGroupsClaimName, &sJWTAllowGroups,
&sRoutingPeerDNSResolutionEnabled, &sDNSDomain, &sNetworkRange,
&sLazyConnectionEnabled,
&sNetworkRangeV6, &sIPv6EnabledGroups, &sLazyConnectionEnabled,
&sExtraPeerApprovalEnabled, &sExtraUserApprovalRequired,
&sExtraIntegratedValidator, &sExtraIntegratedValidatorGroups,
)
@@ -1617,6 +1621,15 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
if sNetworkRange.Valid {
_ = json.Unmarshal([]byte(sNetworkRange.String), &account.Settings.NetworkRange)
}
if networkNetV6.Valid {
_ = json.Unmarshal([]byte(networkNetV6.String), &account.Network.NetV6)
}
if sNetworkRangeV6.Valid {
_ = json.Unmarshal([]byte(sNetworkRangeV6.String), &account.Settings.NetworkRangeV6)
}
if sIPv6EnabledGroups.Valid {
_ = json.Unmarshal([]byte(sIPv6EnabledGroups.String), &account.Settings.IPv6EnabledGroups)
}
if sExtraPeerApprovalEnabled.Valid {
account.Settings.Extra.PeerApprovalEnabled = sExtraPeerApprovalEnabled.Bool
@@ -1699,12 +1712,12 @@ func (s *SqlStore) getSetupKeys(ctx context.Context, accountID string) ([]types.
func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Peer, error) {
const query = `SELECT id, account_id, key, ip, name, dns_label, user_id, ssh_key, ssh_enabled, login_expiration_enabled,
inactivity_expiration_enabled, last_login, created_at, ephemeral, extra_dns_labels, allow_extra_dns_labels, meta_hostname,
meta_go_os, meta_kernel, meta_core, meta_platform, meta_os, meta_os_version, meta_wt_version, meta_ui_version,
inactivity_expiration_enabled, last_login, created_at, ephemeral, extra_dns_labels, allow_extra_dns_labels, meta_hostname,
meta_go_os, meta_kernel, meta_core, meta_platform, meta_os, meta_os_version, meta_wt_version, meta_ui_version,
meta_kernel_version, meta_network_addresses, meta_system_serial_number, meta_system_product_name, meta_system_manufacturer,
meta_environment, meta_flags, meta_files, peer_status_last_seen, peer_status_connected, peer_status_login_expired,
peer_status_requires_approval, location_connection_ip, location_country_code, location_city_name,
location_geo_name_id, proxy_meta_embedded, proxy_meta_cluster FROM peers WHERE account_id = $1`
meta_environment, meta_flags, meta_files, peer_status_last_seen, peer_status_connected, peer_status_login_expired,
peer_status_requires_approval, location_connection_ip, location_country_code, location_city_name,
location_geo_name_id, proxy_meta_embedded, proxy_meta_cluster, ipv6 FROM peers WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
if err != nil {
return nil, err
@@ -1718,7 +1731,7 @@ func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Pee
sshEnabled, loginExpirationEnabled, inactivityExpirationEnabled, ephemeral, allowExtraDNSLabels sql.NullBool
peerStatusLastSeen sql.NullTime
peerStatusConnected, peerStatusLoginExpired, peerStatusRequiresApproval, proxyEmbedded sql.NullBool
ip, extraDNS, netAddr, env, flags, files, connIP []byte
ip, extraDNS, netAddr, env, flags, files, connIP, ipv6 []byte
metaHostname, metaGoOS, metaKernel, metaCore, metaPlatform sql.NullString
metaOS, metaOSVersion, metaWtVersion, metaUIVersion, metaKernelVersion sql.NullString
metaSystemSerialNumber, metaSystemProductName, metaSystemManufacturer sql.NullString
@@ -1732,7 +1745,7 @@ func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Pee
&metaOS, &metaOSVersion, &metaWtVersion, &metaUIVersion, &metaKernelVersion, &netAddr,
&metaSystemSerialNumber, &metaSystemProductName, &metaSystemManufacturer, &env, &flags, &files,
&peerStatusLastSeen, &peerStatusConnected, &peerStatusLoginExpired, &peerStatusRequiresApproval, &connIP,
&locationCountryCode, &locationCityName, &locationGeoNameID, &proxyEmbedded, &proxyCluster)
&locationCountryCode, &locationCityName, &locationGeoNameID, &proxyEmbedded, &proxyCluster, &ipv6)
if err == nil {
if lastLogin.Valid {
@@ -1825,6 +1838,9 @@ func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Pee
if ip != nil {
_ = json.Unmarshal(ip, &p.IP)
}
if ipv6 != nil {
_ = json.Unmarshal(ipv6, &p.IPv6)
}
if extraDNS != nil {
_ = json.Unmarshal(extraDNS, &p.ExtraDNSLabels)
}
@@ -2573,7 +2589,7 @@ func (s *SqlStore) GetAccountIDBySetupKey(ctx context.Context, setupKey string)
return accountID, nil
}
func (s *SqlStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength, accountID string) ([]net.IP, error) {
func (s *SqlStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength, accountID string) ([]netip.Addr, error) {
tx := s.db
if lockStrength != LockingStrengthNone {
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
@@ -2581,7 +2597,6 @@ func (s *SqlStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength
var ipJSONStrings []string
// Fetch the IP addresses as JSON strings
result := tx.Model(&nbpeer.Peer{}).
Where("account_id = ?", accountID).
Pluck("ip", &ipJSONStrings)
@@ -2592,14 +2607,13 @@ func (s *SqlStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength
return nil, status.Errorf(status.Internal, "issue getting IPs from store: %s", result.Error)
}
// Convert the JSON strings to net.IP objects
ips := make([]net.IP, len(ipJSONStrings))
ips := make([]netip.Addr, len(ipJSONStrings))
for i, ipJSON := range ipJSONStrings {
var ip net.IP
var ip netip.Addr
if err := json.Unmarshal([]byte(ipJSON), &ip); err != nil {
return nil, status.Errorf(status.Internal, "issue parsing IP JSON from store")
}
ips[i] = ip
ips[i] = ip.Unmap()
}
return ips, nil
@@ -3201,7 +3215,7 @@ func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStre
query = query.Where("name LIKE ?", "%"+nameFilter+"%")
}
if ipFilter != "" {
query = query.Where("ip LIKE ?", "%"+ipFilter+"%")
query = query.Where("ip LIKE ? OR ipv6 LIKE ?", "%"+ipFilter+"%", "%"+ipFilter+"%")
}
if err := query.Find(&peers).Error; err != nil {
@@ -4631,6 +4645,27 @@ func (s *SqlStore) UpdateAccountNetwork(ctx context.Context, accountID string, i
return nil
}
// UpdateAccountNetworkV6 updates the IPv6 network range for the account.
func (s *SqlStore) UpdateAccountNetworkV6(ctx context.Context, accountID string, ipNet net.IPNet) error {
patch := accountNetworkPatch{
Network: &types.Network{NetV6: ipNet},
}
result := s.db.
Model(&types.Account{}).
Where(idQueryCondition, accountID).
Updates(&patch)
if result.Error != nil {
log.WithContext(ctx).Errorf("failed to update account network v6: %v", result.Error)
return status.Errorf(status.Internal, "update account network v6")
}
if result.RowsAffected == 0 {
return status.NewAccountNotFoundError(accountID)
}
return nil
}
func (s *SqlStore) GetPeersByGroupIDs(ctx context.Context, accountID string, groupIDs []string) ([]*nbpeer.Peer, error) {
if len(groupIDs) == 0 {
return []*nbpeer.Peer{}, nil
@@ -148,7 +148,8 @@ func TestGetAccount_ComprehensiveFieldValidation(t *testing.T) {
AccountID: accountID,
Key: "peer-key-1-AAAA",
Name: "Peer 1",
IP: net.ParseIP("100.64.0.1"),
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
Meta: nbpeer.PeerSystemMeta{
Hostname: "peer1.example.com",
GoOS: "linux",
@@ -195,7 +196,8 @@ func TestGetAccount_ComprehensiveFieldValidation(t *testing.T) {
AccountID: accountID,
Key: "peer-key-2-BBBB",
Name: "Peer 2",
IP: net.ParseIP("100.64.0.2"),
IP: netip.MustParseAddr("100.64.0.2"),
IPv6: netip.MustParseAddr("fd00::2"),
Meta: nbpeer.PeerSystemMeta{
Hostname: "peer2.example.com",
GoOS: "darwin",
@@ -232,7 +234,8 @@ func TestGetAccount_ComprehensiveFieldValidation(t *testing.T) {
AccountID: accountID,
Key: "peer-key-3-CCCC",
Name: "Peer 3 (Ephemeral)",
IP: net.ParseIP("100.64.0.3"),
IP: netip.MustParseAddr("100.64.0.3"),
IPv6: netip.MustParseAddr("fd00::3"),
Meta: nbpeer.PeerSystemMeta{
Hostname: "peer3.example.com",
GoOS: "windows",
@@ -710,7 +713,7 @@ func TestGetAccount_ComprehensiveFieldValidation(t *testing.T) {
require.True(t, exists, "Peer 1 should exist")
assert.Equal(t, "Peer 1", p1.Name, "Peer 1 name mismatch")
assert.Equal(t, "peer-key-1-AAAA", p1.Key, "Peer 1 key mismatch")
assert.True(t, p1.IP.Equal(net.ParseIP("100.64.0.1")), "Peer 1 IP mismatch")
assert.Equal(t, netip.MustParseAddr("100.64.0.1"), p1.IP, "Peer 1 IP mismatch")
assert.Equal(t, userID1, p1.UserID, "Peer 1 user ID mismatch")
assert.True(t, p1.SSHEnabled, "Peer 1 SSH should be enabled")
assert.Equal(t, "ssh-rsa AAAAB3NzaC1...", p1.SSHKey, "Peer 1 SSH key mismatch")
+53 -31
View File
@@ -92,11 +92,12 @@ func runLargeTest(t *testing.T, store Store) {
for n := 0; n < numPerAccount; n++ {
netIP := randomIPv4()
peerID := fmt.Sprintf("%s-peer-%d", account.Id, n)
addr, _ := netip.AddrFromSlice(netIP)
peer := &nbpeer.Peer{
ID: peerID,
Key: peerID,
IP: netIP,
IP: addr.Unmap(),
Name: peerID,
DNSLabel: peerID,
UserID: "testuser",
@@ -233,7 +234,8 @@ func Test_SaveAccount(t *testing.T) {
account.SetupKeys[setupKey.Key] = setupKey
account.Peers["testpeer"] = &nbpeer.Peer{
Key: "peerkey",
IP: net.IP{127, 0, 0, 1},
IP: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
IPv6: netip.MustParseAddr("fd00::1"),
Meta: nbpeer.PeerSystemMeta{},
Name: "peer name",
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
@@ -247,7 +249,8 @@ func Test_SaveAccount(t *testing.T) {
account2.SetupKeys[setupKey.Key] = setupKey
account2.Peers["testpeer2"] = &nbpeer.Peer{
Key: "peerkey2",
IP: net.IP{127, 0, 0, 2},
IP: netip.AddrFrom4([4]byte{127, 0, 0, 2}),
IPv6: netip.MustParseAddr("fd00::2"),
Meta: nbpeer.PeerSystemMeta{},
Name: "peer name 2",
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
@@ -314,7 +317,8 @@ func TestSqlite_DeleteAccount(t *testing.T) {
account.SetupKeys[setupKey.Key] = setupKey
account.Peers["testpeer"] = &nbpeer.Peer{
Key: "peerkey",
IP: net.IP{127, 0, 0, 1},
IP: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
IPv6: netip.MustParseAddr("fd00::1"),
Meta: nbpeer.PeerSystemMeta{},
Name: "peer name",
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
@@ -454,7 +458,8 @@ func TestSqlStore_SavePeer(t *testing.T) {
peer := &nbpeer.Peer{
Key: "peerkey",
ID: "testpeer",
IP: net.IP{127, 0, 0, 1},
IP: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
IPv6: netip.MustParseAddr("fd00::1"),
Meta: nbpeer.PeerSystemMeta{Hostname: "testingpeer"},
Name: "peer name",
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
@@ -511,7 +516,8 @@ func TestSqlStore_SavePeerStatus(t *testing.T) {
account.Peers["testpeer"] = &nbpeer.Peer{
Key: "peerkey",
ID: "testpeer",
IP: net.IP{127, 0, 0, 1},
IP: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
IPv6: netip.MustParseAddr("fd00::1"),
Meta: nbpeer.PeerSystemMeta{},
Name: "peer name",
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
@@ -739,7 +745,8 @@ func newAccount(store Store, id int) error {
account.SetupKeys[setupKey.Key] = setupKey
account.Peers["p"+str] = &nbpeer.Peer{
Key: "peerkey" + str,
IP: net.IP{127, 0, 0, 1},
IP: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
IPv6: netip.MustParseAddr("fd00::1"),
Meta: nbpeer.PeerSystemMeta{},
Name: "peer name",
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
@@ -778,7 +785,8 @@ func TestPostgresql_SaveAccount(t *testing.T) {
account.SetupKeys[setupKey.Key] = setupKey
account.Peers["testpeer"] = &nbpeer.Peer{
Key: "peerkey",
IP: net.IP{127, 0, 0, 1},
IP: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
IPv6: netip.MustParseAddr("fd00::1"),
Meta: nbpeer.PeerSystemMeta{},
Name: "peer name",
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
@@ -792,7 +800,8 @@ func TestPostgresql_SaveAccount(t *testing.T) {
account2.SetupKeys[setupKey.Key] = setupKey
account2.Peers["testpeer2"] = &nbpeer.Peer{
Key: "peerkey2",
IP: net.IP{127, 0, 0, 2},
IP: netip.AddrFrom4([4]byte{127, 0, 0, 2}),
IPv6: netip.MustParseAddr("fd00::2"),
Meta: nbpeer.PeerSystemMeta{},
Name: "peer name 2",
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
@@ -858,7 +867,8 @@ func TestPostgresql_DeleteAccount(t *testing.T) {
account.SetupKeys[setupKey.Key] = setupKey
account.Peers["testpeer"] = &nbpeer.Peer{
Key: "peerkey",
IP: net.IP{127, 0, 0, 1},
IP: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
IPv6: netip.MustParseAddr("fd00::1"),
Meta: nbpeer.PeerSystemMeta{},
Name: "peer name",
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
@@ -965,37 +975,39 @@ func TestSqlite_GetTakenIPs(t *testing.T) {
takenIPs, err := store.GetTakenIPs(context.Background(), LockingStrengthNone, existingAccountID)
require.NoError(t, err)
assert.Equal(t, []net.IP{}, takenIPs)
assert.Equal(t, []netip.Addr{}, takenIPs)
peer1 := &nbpeer.Peer{
ID: "peer1",
AccountID: existingAccountID,
Key: "key1",
DNSLabel: "peer1",
IP: net.IP{1, 1, 1, 1},
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
IPv6: netip.MustParseAddr("fd00::1:1:1:1"),
}
err = store.AddPeerToAccount(context.Background(), peer1)
require.NoError(t, err)
takenIPs, err = store.GetTakenIPs(context.Background(), LockingStrengthNone, existingAccountID)
require.NoError(t, err)
ip1 := net.IP{1, 1, 1, 1}.To16()
assert.Equal(t, []net.IP{ip1}, takenIPs)
ip1 := netip.AddrFrom4([4]byte{1, 1, 1, 1})
assert.Equal(t, []netip.Addr{ip1}, takenIPs)
peer2 := &nbpeer.Peer{
ID: "peer1second",
AccountID: existingAccountID,
Key: "key2",
DNSLabel: "peer1-1",
IP: net.IP{2, 2, 2, 2},
IP: netip.AddrFrom4([4]byte{2, 2, 2, 2}),
IPv6: netip.MustParseAddr("fd00::2:2:2:2"),
}
err = store.AddPeerToAccount(context.Background(), peer2)
require.NoError(t, err)
takenIPs, err = store.GetTakenIPs(context.Background(), LockingStrengthNone, existingAccountID)
require.NoError(t, err)
ip2 := net.IP{2, 2, 2, 2}.To16()
assert.Equal(t, []net.IP{ip1, ip2}, takenIPs)
ip2 := netip.AddrFrom4([4]byte{2, 2, 2, 2})
assert.Equal(t, []netip.Addr{ip1, ip2}, takenIPs)
}
func TestSqlite_GetPeerLabelsInAccount(t *testing.T) {
@@ -1015,7 +1027,8 @@ func TestSqlite_GetPeerLabelsInAccount(t *testing.T) {
AccountID: existingAccountID,
Key: "key1",
DNSLabel: "peer1",
IP: net.IP{1, 1, 1, 1},
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
IPv6: netip.MustParseAddr("fd00::1:1:1:1"),
}
err = store.AddPeerToAccount(context.Background(), peer1)
require.NoError(t, err)
@@ -1029,7 +1042,8 @@ func TestSqlite_GetPeerLabelsInAccount(t *testing.T) {
AccountID: existingAccountID,
Key: "key2",
DNSLabel: "peer1-1",
IP: net.IP{2, 2, 2, 2},
IP: netip.AddrFrom4([4]byte{2, 2, 2, 2}),
IPv6: netip.MustParseAddr("fd00::2:2:2:2"),
}
err = store.AddPeerToAccount(context.Background(), peer2)
require.NoError(t, err)
@@ -1082,7 +1096,8 @@ func Test_AddPeerWithSameIP(t *testing.T) {
ID: "peer1",
AccountID: existingAccountID,
Key: "key1",
IP: net.IP{1, 1, 1, 1},
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
IPv6: netip.MustParseAddr("fd00::1:1:1:1"),
}
err = store.AddPeerToAccount(context.Background(), peer1)
require.NoError(t, err)
@@ -1091,7 +1106,8 @@ func Test_AddPeerWithSameIP(t *testing.T) {
ID: "peer1second",
AccountID: existingAccountID,
Key: "key2",
IP: net.IP{1, 1, 1, 1},
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
IPv6: netip.MustParseAddr("fd00::2:2:2:2"),
}
err = store.AddPeerToAccount(context.Background(), peer2)
require.Error(t, err)
@@ -2595,7 +2611,8 @@ func TestSqlStore_AddPeerToAccount(t *testing.T) {
ID: "peer1",
AccountID: accountID,
Key: "key",
IP: net.IP{1, 1, 1, 1},
IP: netip.AddrFrom4([4]byte{1, 1, 1, 1}),
IPv6: netip.MustParseAddr("fd00::1:1:1:1"),
Meta: nbpeer.PeerSystemMeta{
Hostname: "hostname",
GoOS: "linux",
@@ -3748,10 +3765,10 @@ func BenchmarkGetAccountPeers(b *testing.B) {
}
}
func intToIPv4(n uint32) net.IP {
ip := make(net.IP, 4)
binary.BigEndian.PutUint32(ip, n)
return ip
func intToIPv4(n uint32) netip.Addr {
var b [4]byte
binary.BigEndian.PutUint32(b[:], n)
return netip.AddrFrom4(b)
}
func TestSqlStore_GetPeersByGroupIDs(t *testing.T) {
@@ -3878,7 +3895,8 @@ func TestSqlStore_GetUserIDByPeerKey(t *testing.T) {
Key: peerKey,
AccountID: existingAccountID,
UserID: userID,
IP: net.IP{10, 0, 0, 1},
IP: netip.AddrFrom4([4]byte{10, 0, 0, 1}),
IPv6: netip.MustParseAddr("fd00::a00:1"),
DNSLabel: "test-peer-1",
}
@@ -3915,7 +3933,8 @@ func TestSqlStore_GetUserIDByPeerKey_NoUserID(t *testing.T) {
Key: peerKey,
AccountID: existingAccountID,
UserID: "",
IP: net.IP{10, 0, 0, 1},
IP: netip.AddrFrom4([4]byte{10, 0, 0, 1}),
IPv6: netip.MustParseAddr("fd00::a00:1"),
DNSLabel: "test-peer-1",
}
@@ -3942,7 +3961,8 @@ func TestSqlStore_ApproveAccountPeers(t *testing.T) {
AccountID: accountID,
DNSLabel: "peer1.netbird.cloud",
Key: "peer1-key",
IP: net.ParseIP("100.64.0.1"),
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
Status: &nbpeer.PeerStatus{
RequiresApproval: true,
LastSeen: time.Now().UTC(),
@@ -3953,7 +3973,8 @@ func TestSqlStore_ApproveAccountPeers(t *testing.T) {
AccountID: accountID,
DNSLabel: "peer2.netbird.cloud",
Key: "peer2-key",
IP: net.ParseIP("100.64.0.2"),
IP: netip.MustParseAddr("100.64.0.2"),
IPv6: netip.MustParseAddr("fd00::2"),
Status: &nbpeer.PeerStatus{
RequiresApproval: true,
LastSeen: time.Now().UTC(),
@@ -3964,7 +3985,8 @@ func TestSqlStore_ApproveAccountPeers(t *testing.T) {
AccountID: accountID,
DNSLabel: "peer3.netbird.cloud",
Key: "peer3-key",
IP: net.ParseIP("100.64.0.3"),
IP: netip.MustParseAddr("100.64.0.3"),
IPv6: netip.MustParseAddr("fd00::3"),
Status: &nbpeer.PeerStatus{
RequiresApproval: false,
LastSeen: time.Now().UTC(),
@@ -342,7 +342,8 @@ func setupBenchmarkDB(b testing.TB) (*SqlStore, func(), string) {
ID: fmt.Sprintf("peer-%d", i),
AccountID: accountID,
Key: fmt.Sprintf("peerkey-%d", i),
IP: net.ParseIP(fmt.Sprintf("100.64.0.%d", i+1)),
IP: netip.MustParseAddr(fmt.Sprintf("100.64.0.%d", i+1)),
IPv6: netip.MustParseAddr(fmt.Sprintf("fd00::%d", i+1)),
Name: fmt.Sprintf("peer-name-%d", i),
Status: &nbpeer.PeerStatus{Connected: i%2 == 0, LastSeen: time.Now()},
})
+2 -1
View File
@@ -185,7 +185,7 @@ type Store interface {
SaveNameServerGroup(ctx context.Context, nameServerGroup *dns.NameServerGroup) error
DeleteNameServerGroup(ctx context.Context, accountID, nameServerGroupID string) error
GetTakenIPs(ctx context.Context, lockStrength LockingStrength, accountId string) ([]net.IP, error)
GetTakenIPs(ctx context.Context, lockStrength LockingStrength, accountId string) ([]netip.Addr, error)
IncrementNetworkSerial(ctx context.Context, accountId string) error
GetAccountNetwork(ctx context.Context, lockStrength LockingStrength, accountId string) (*types.Network, error)
@@ -225,6 +225,7 @@ type Store interface {
IsPrimaryAccount(ctx context.Context, accountID string) (bool, string, error)
MarkAccountPrimary(ctx context.Context, accountID string) error
UpdateAccountNetwork(ctx context.Context, accountID string, ipNet net.IPNet) error
UpdateAccountNetworkV6(ctx context.Context, accountID string, ipNet net.IPNet) error
GetPolicyRulesByResourceID(ctx context.Context, lockStrength LockingStrength, accountID string, peerID string) ([]*types.PolicyRule, error)
// SetFieldEncrypt sets the field encryptor for encrypting sensitive user data.
+17 -2
View File
@@ -7,6 +7,7 @@ package store
import (
context "context"
net "net"
netip "net/netip"
reflect "reflect"
time "time"
@@ -2124,10 +2125,10 @@ func (mr *MockStoreMockRecorder) GetStoreEngine() *gomock.Call {
}
// GetTakenIPs mocks base method.
func (m *MockStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength, accountId string) ([]net.IP, error) {
func (m *MockStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength, accountId string) ([]netip.Addr, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTakenIPs", ctx, lockStrength, accountId)
ret0, _ := ret[0].([]net.IP)
ret0, _ := ret[0].([]netip.Addr)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -2924,6 +2925,20 @@ func (mr *MockStoreMockRecorder) UpdateAccountNetwork(ctx, accountID, ipNet inte
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountNetwork", reflect.TypeOf((*MockStore)(nil).UpdateAccountNetwork), ctx, accountID, ipNet)
}
// UpdateAccountNetworkV6 mocks base method.
func (m *MockStore) UpdateAccountNetworkV6(ctx context.Context, accountID string, ipNet net.IPNet) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateAccountNetworkV6", ctx, accountID, ipNet)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateAccountNetworkV6 indicates an expected call of UpdateAccountNetworkV6.
func (mr *MockStoreMockRecorder) UpdateAccountNetworkV6(ctx, accountID, ipNet interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountNetworkV6", reflect.TypeOf((*MockStore)(nil).UpdateAccountNetworkV6), ctx, accountID, ipNet)
}
// UpdateCustomDomain mocks base method.
func (m *MockStore) UpdateCustomDomain(ctx context.Context, accountID string, d *domain.Domain) (*domain.Domain, error) {
m.ctrl.T.Helper()
+114 -32
View File
@@ -3,7 +3,6 @@ package types
import (
"context"
"fmt"
"net"
"net/netip"
"slices"
"strconv"
@@ -315,8 +314,9 @@ func (a *Account) GetPeerNetworkMap(
peersToConnect = append(peersToConnect, p)
}
routesUpdate := a.GetRoutesToSync(ctx, peerID, peersToConnect, peerGroups)
routesFirewallRules := a.GetPeerRoutesFirewallRules(ctx, peerID, validatedPeersMap)
includeIPv6 := peer.SupportsIPv6() && peer.IPv6.IsValid()
routesUpdate := filterAndExpandRoutes(a.GetRoutesToSync(ctx, peerID, peersToConnect, peerGroups), includeIPv6)
routesFirewallRules := a.GetPeerRoutesFirewallRules(ctx, peerID, validatedPeersMap, includeIPv6)
isRouter, networkResourcesRoutes, sourcePeers := a.GetNetworkResourcesRoutesToSync(ctx, peerID, resourcePolicies, routers)
var networkResourcesFirewallRules []*RouteFirewallRule
if isRouter {
@@ -350,7 +350,7 @@ func (a *Account) GetPeerNetworkMap(
nm := &NetworkMap{
Peers: peersToConnectIncludingRouters,
Network: a.Network.Copy(),
Routes: slices.Concat(networkResourcesRoutes, routesUpdate),
Routes: slices.Concat(filterAndExpandRoutes(networkResourcesRoutes, includeIPv6), routesUpdate),
DNSConfig: dnsUpdate,
OfflinePeers: expiredPeers,
FirewallRules: firewallRules,
@@ -445,7 +445,7 @@ func getPeerNSGroups(account *Account, peerID string) []*nbdns.NameServerGroup {
// peerIsNameserver returns true if the peer is a nameserver for a nsGroup
func peerIsNameserver(peer *nbpeer.Peer, nsGroup *nbdns.NameServerGroup) bool {
for _, ns := range nsGroup.NameServers {
if peer.IP.Equal(ns.IP.AsSlice()) {
if peer.IP == ns.IP {
return true
}
}
@@ -512,6 +512,8 @@ func (a *Account) GetPeersCustomZone(ctx context.Context, dnsDomain string) nbdn
domainSuffix := "." + dnsDomain
ipv6AllowedPeers := a.peerIPv6AllowedSet()
var sb strings.Builder
for _, peer := range a.Peers {
if peer.DNSLabel == "" {
@@ -523,13 +525,32 @@ func (a *Account) GetPeersCustomZone(ctx context.Context, dnsDomain string) nbdn
sb.WriteString(peer.DNSLabel)
sb.WriteString(domainSuffix)
fqdn := sb.String()
customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
Name: sb.String(),
Name: fqdn,
Type: int(dns.TypeA),
Class: nbdns.DefaultClass,
TTL: defaultTTL,
RData: peer.IP.String(),
})
// Only advertise AAAA for peers that have a valid IPv6, whose client supports it,
// and that belong to an IPv6-enabled group. Old clients don't configure v6 on their
// WireGuard interface, so resolving their AAAA causes connections to hang.
// Edge case: toggling --disable-ipv6 on a peer without a version change does not
// propagate to other peers, so AAAA records can be stale until the next full sync.
// This is accepted because v4 connectivity is unaffected. Can be fixed by adding
// capability-change detection to the SyncPeer propagation condition.
_, peerAllowed := ipv6AllowedPeers[peer.ID]
hasIPv6 := peer.IPv6.IsValid() && peer.SupportsIPv6() && peerAllowed
if hasIPv6 {
customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
Name: fqdn,
Type: int(dns.TypeAAAA),
Class: nbdns.DefaultClass,
TTL: defaultTTL,
RData: peer.IPv6.String(),
})
}
sb.Reset()
for _, extraLabel := range peer.ExtraDNSLabels {
@@ -537,13 +558,23 @@ func (a *Account) GetPeersCustomZone(ctx context.Context, dnsDomain string) nbdn
sb.WriteString(extraLabel)
sb.WriteString(domainSuffix)
extraFqdn := sb.String()
customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
Name: sb.String(),
Name: extraFqdn,
Type: int(dns.TypeA),
Class: nbdns.DefaultClass,
TTL: defaultTTL,
RData: peer.IP.String(),
})
if hasIPv6 {
customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
Name: extraFqdn,
Type: int(dns.TypeAAAA),
Class: nbdns.DefaultClass,
TTL: defaultTTL,
RData: peer.IPv6.String(),
})
}
sb.Reset()
}
@@ -824,8 +855,43 @@ func (a *Account) GetPeerGroups(peerID string) LookupMap {
return groupList
}
func (a *Account) GetTakenIPs() []net.IP {
var takenIps []net.IP
// PeerIPv6Allowed reports whether the given peer is in any of the account's IPv6 enabled groups.
// Returns false if IPv6 is disabled or no groups are configured.
func (a *Account) PeerIPv6Allowed(peerID string) bool {
if len(a.Settings.IPv6EnabledGroups) == 0 {
return false
}
for _, groupID := range a.Settings.IPv6EnabledGroups {
group, ok := a.Groups[groupID]
if !ok {
continue
}
if slices.Contains(group.Peers, peerID) {
return true
}
}
return false
}
// peerIPv6AllowedSet returns a set of peer IDs that belong to any IPv6-enabled group.
func (a *Account) peerIPv6AllowedSet() map[string]struct{} {
result := make(map[string]struct{})
for _, groupID := range a.Settings.IPv6EnabledGroups {
group, ok := a.Groups[groupID]
if !ok {
continue
}
for _, peerID := range group.Peers {
result[peerID] = struct{}{}
}
}
return result
}
// GetTakenIPs returns all peer IP addresses currently allocated in the account.
func (a *Account) GetTakenIPs() []netip.Addr {
takenIps := make([]netip.Addr, 0, len(a.Peers))
for _, existingPeer := range a.Peers {
takenIps = append(takenIps, existingPeer.IP)
}
@@ -1178,10 +1244,17 @@ func (a *Account) connResourcesGenerator(ctx context.Context, targetPeer *nbpeer
if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 {
rules = append(rules, &fr)
continue
} else {
rules = append(rules, expandPortsAndRanges(fr, rule, targetPeer)...)
}
rules = append(rules, expandPortsAndRanges(fr, rule, targetPeer)...)
rules = appendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, firewallRuleContext{
direction: direction,
dirStr: strconv.Itoa(direction),
protocolStr: string(protocol),
actionStr: string(rule.Action),
portsJoined: strings.Join(rule.Ports, ","),
})
}
}, func() ([]*nbpeer.Peer, []*FirewallRule) {
return peers, rules
@@ -1297,14 +1370,14 @@ func (a *Account) GetPostureChecks(postureChecksID string) *posture.Checks {
}
// GetPeerRoutesFirewallRules gets the routes firewall rules associated with a routing peer ID for the account.
func (a *Account) GetPeerRoutesFirewallRules(ctx context.Context, peerID string, validatedPeersMap map[string]struct{}) []*RouteFirewallRule {
func (a *Account) GetPeerRoutesFirewallRules(ctx context.Context, peerID string, validatedPeersMap map[string]struct{}, includeIPv6 bool) []*RouteFirewallRule {
routesFirewallRules := make([]*RouteFirewallRule, 0, len(a.Routes))
enabledRoutes, _ := a.getRoutingPeerRoutes(ctx, peerID)
for _, route := range enabledRoutes {
// If no access control groups are specified, accept all traffic.
if len(route.AccessControlGroups) == 0 {
defaultPermit := getDefaultPermit(route)
defaultPermit := getDefaultPermit(route, includeIPv6)
routesFirewallRules = append(routesFirewallRules, defaultPermit...)
continue
}
@@ -1313,7 +1386,7 @@ func (a *Account) GetPeerRoutesFirewallRules(ctx context.Context, peerID string,
for _, accessGroup := range route.AccessControlGroups {
policies := GetAllRoutePoliciesFromGroups(a, []string{accessGroup})
rules := a.getRouteFirewallRules(ctx, peerID, policies, route, validatedPeersMap, distributionPeers)
rules := a.getRouteFirewallRules(ctx, peerID, policies, route, validatedPeersMap, distributionPeers, includeIPv6)
routesFirewallRules = append(routesFirewallRules, rules...)
}
}
@@ -1321,7 +1394,7 @@ func (a *Account) GetPeerRoutesFirewallRules(ctx context.Context, peerID string,
return routesFirewallRules
}
func (a *Account) getRouteFirewallRules(ctx context.Context, peerID string, policies []*Policy, route *route.Route, validatedPeersMap map[string]struct{}, distributionPeers map[string]struct{}) []*RouteFirewallRule {
func (a *Account) getRouteFirewallRules(ctx context.Context, peerID string, policies []*Policy, route *route.Route, validatedPeersMap map[string]struct{}, distributionPeers map[string]struct{}, includeIPv6 bool) []*RouteFirewallRule {
var fwRules []*RouteFirewallRule
for _, policy := range policies {
if !policy.Enabled {
@@ -1334,7 +1407,7 @@ func (a *Account) getRouteFirewallRules(ctx context.Context, peerID string, poli
}
rulePeers := a.getRulePeers(rule, policy.SourcePostureChecks, peerID, distributionPeers, validatedPeersMap)
rules := generateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN)
rules := generateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN, includeIPv6)
fwRules = append(fwRules, rules...)
}
}
@@ -1394,8 +1467,10 @@ func (a *Account) getDistributionGroupsPeers(route *route.Route) map[string]stru
return distPeers
}
func getDefaultPermit(route *route.Route) []*RouteFirewallRule {
var rules []*RouteFirewallRule
func getDefaultPermit(route *route.Route, includeIPv6 bool) []*RouteFirewallRule {
if route.Network.Addr().Is6() && !includeIPv6 {
return nil
}
sources := []string{"0.0.0.0/0"}
if route.Network.Addr().Is6() {
@@ -1411,10 +1486,9 @@ func getDefaultPermit(route *route.Route) []*RouteFirewallRule {
RouteID: route.ID,
}
rules = append(rules, &rule)
rules := []*RouteFirewallRule{&rule}
// dynamic routes always contain an IPv4 placeholder as destination, hence we must add IPv6 rules additionally
if route.IsDynamic() {
if includeIPv6 && route.IsDynamic() {
ruleV6 := rule
ruleV6.SourceRanges = []string{"::/0"}
rules = append(rules, &ruleV6)
@@ -1460,7 +1534,7 @@ func (a *Account) GetPeerNetworkResourceFirewallRules(ctx context.Context, peer
resourceAppliedPolicies := resourcePolicies[string(route.GetResourceID())]
distributionPeers := getPoliciesSourcePeers(resourceAppliedPolicies, a.Groups)
rules := a.getRouteFirewallRules(ctx, peer.ID, resourceAppliedPolicies, route, validatedPeersMap, distributionPeers)
rules := a.getRouteFirewallRules(ctx, peer.ID, resourceAppliedPolicies, route, validatedPeersMap, distributionPeers, peer.SupportsIPv6() && peer.IPv6.IsValid())
for _, rule := range rules {
if len(rule.SourceRanges) > 0 {
routesFirewallRules = append(routesFirewallRules, rule)
@@ -1990,24 +2064,32 @@ func peerSupportedFirewallFeatures(peerVer string) supportedFeatures {
}
// filterZoneRecordsForPeers filters DNS records to only include peers to connect.
// AAAA records are excluded when the requesting peer lacks IPv6 capability.
func filterZoneRecordsForPeers(peer *nbpeer.Peer, customZone nbdns.CustomZone, peersToConnect, expiredPeers []*nbpeer.Peer) []nbdns.SimpleRecord {
filteredRecords := make([]nbdns.SimpleRecord, 0, len(customZone.Records))
peerIPs := make(map[string]struct{})
peerIPs := make(map[netip.Addr]struct{}, len(peersToConnect)+len(expiredPeers)+2)
includeIPv6 := peer.SupportsIPv6() && peer.IPv6.IsValid()
// Add peer's own IP to include its own DNS records
peerIPs[peer.IP.String()] = struct{}{}
for _, peerToConnect := range peersToConnect {
peerIPs[peerToConnect.IP.String()] = struct{}{}
addPeerIPs := func(p *nbpeer.Peer) {
peerIPs[p.IP] = struct{}{}
if includeIPv6 && p.IPv6.IsValid() {
peerIPs[p.IPv6] = struct{}{}
}
}
for _, expiredPeer := range expiredPeers {
peerIPs[expiredPeer.IP.String()] = struct{}{}
addPeerIPs(peer)
for _, p := range peersToConnect {
addPeerIPs(p)
}
for _, p := range expiredPeers {
addPeerIPs(p)
}
for _, record := range customZone.Records {
if _, exists := peerIPs[record.RData]; exists {
filteredRecords = append(filteredRecords, record)
if addr, err := netip.ParseAddr(record.RData); err == nil {
if _, exists := peerIPs[addr.Unmap()]; exists {
filteredRecords = append(filteredRecords, record)
}
}
}
@@ -544,10 +544,15 @@ func filterDNSRecordsByPeers(records []nbdns.SimpleRecord, peers map[string]*nbp
return nil
}
peerIPs := make(map[string]struct{}, len(peers))
// Include both v4 and v6 addresses so AAAA records (whose RData is an IPv6
// address) are not filtered out when peers have IPv6 assigned.
peerIPs := make(map[string]struct{}, len(peers)*2)
for _, peer := range peers {
if peer != nil {
peerIPs[peer.IP.String()] = struct{}{}
if peer.IPv6.IsValid() {
peerIPs[peer.IPv6.String()] = struct{}{}
}
}
}
+57 -15
View File
@@ -3,7 +3,6 @@ package types
import (
"context"
"fmt"
"net"
"net/netip"
"slices"
"testing"
@@ -466,9 +465,9 @@ const (
)
var (
accNetResourcePeer1IP = net.IP{192, 168, 1, 1}
accNetResourcePeer2IP = net.IP{192, 168, 1, 2}
accNetResourceRouter1IP = net.IP{192, 168, 1, 3}
accNetResourcePeer1IP = netip.AddrFrom4([4]byte{192, 168, 1, 1})
accNetResourcePeer2IP = netip.AddrFrom4([4]byte{192, 168, 1, 2})
accNetResourceRouter1IP = netip.AddrFrom4([4]byte{192, 168, 1, 3})
accNetResourceValidPeers = map[string]struct{}{accNetResourcePeer1ID: {}, accNetResourcePeer2ID: {}}
)
@@ -832,7 +831,13 @@ func Test_NetworksNetMapGenWithTwoPostureChecks(t *testing.T) {
func Test_NetworksNetMapGenShouldExcludeOtherRouters(t *testing.T) {
account := getBasicAccountsWithResource()
account.Peers["router2Id"] = &nbpeer.Peer{Key: "router2Key", ID: "router2Id", AccountID: accID, IP: net.IP{192, 168, 1, 4}}
account.Peers["router2Id"] = &nbpeer.Peer{
Key: "router2Key",
ID: "router2Id",
AccountID: accID,
IP: netip.AddrFrom4([4]byte{192, 168, 1, 4}),
IPv6: netip.MustParseAddr("fd00::c0a8:104"),
}
account.NetworkRouters = append(account.NetworkRouters, &routerTypes.NetworkRouter{
ID: "router2Id",
NetworkID: network1ID,
@@ -1320,7 +1325,11 @@ func Test_FilterZoneRecordsForPeers(t *testing.T) {
},
peersToConnect: []*nbpeer.Peer{},
expiredPeers: []*nbpeer.Peer{},
peer: &nbpeer.Peer{ID: "router", IP: net.ParseIP("10.0.0.100")},
peer: &nbpeer.Peer{
ID: "router",
IP: netip.MustParseAddr("10.0.0.100"),
IPv6: netip.MustParseAddr("fd00::a00:64"),
},
expectedRecords: []nbdns.SimpleRecord{
{Name: "router.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.100"},
},
@@ -1347,14 +1356,19 @@ func Test_FilterZoneRecordsForPeers(t *testing.T) {
var peers []*nbpeer.Peer
for _, i := range []int{1, 5, 10, 25, 50, 75, 100} {
peers = append(peers, &nbpeer.Peer{
ID: fmt.Sprintf("peer%d", i),
IP: net.ParseIP(fmt.Sprintf("10.0.%d.%d", i/256, i%256)),
ID: fmt.Sprintf("peer%d", i),
IP: netip.MustParseAddr(fmt.Sprintf("10.0.%d.%d", i/256, i%256)),
IPv6: netip.MustParseAddr(fmt.Sprintf("fd00::%d", i)),
})
}
return peers
}(),
expiredPeers: []*nbpeer.Peer{},
peer: &nbpeer.Peer{ID: "router", IP: net.ParseIP("10.0.0.100")},
peer: &nbpeer.Peer{
ID: "router",
IP: netip.MustParseAddr("10.0.0.100"),
IPv6: netip.MustParseAddr("fd00::a00:64"),
},
expectedRecords: func() []nbdns.SimpleRecord {
var records []nbdns.SimpleRecord
for _, i := range []int{1, 5, 10, 25, 50, 75, 100} {
@@ -1385,11 +1399,27 @@ func Test_FilterZoneRecordsForPeers(t *testing.T) {
},
},
peersToConnect: []*nbpeer.Peer{
{ID: "peer1", IP: net.ParseIP("10.0.0.1"), DNSLabel: "peer1", ExtraDNSLabels: []string{"peer1-alt", "peer1-backup"}},
{ID: "peer2", IP: net.ParseIP("10.0.0.2"), DNSLabel: "peer2", ExtraDNSLabels: []string{"peer2-service"}},
{
ID: "peer1",
IP: netip.MustParseAddr("10.0.0.1"),
IPv6: netip.MustParseAddr("fd00::a00:1"),
DNSLabel: "peer1",
ExtraDNSLabels: []string{"peer1-alt", "peer1-backup"},
},
{
ID: "peer2",
IP: netip.MustParseAddr("10.0.0.2"),
IPv6: netip.MustParseAddr("fd00::a00:2"),
DNSLabel: "peer2",
ExtraDNSLabels: []string{"peer2-service"},
},
},
expiredPeers: []*nbpeer.Peer{},
peer: &nbpeer.Peer{ID: "router", IP: net.ParseIP("10.0.0.100")},
peer: &nbpeer.Peer{
ID: "router",
IP: netip.MustParseAddr("10.0.0.100"),
IPv6: netip.MustParseAddr("fd00::a00:64"),
},
expectedRecords: []nbdns.SimpleRecord{
{Name: "peer1.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
{Name: "peer1-alt.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
@@ -1411,12 +1441,24 @@ func Test_FilterZoneRecordsForPeers(t *testing.T) {
},
},
peersToConnect: []*nbpeer.Peer{
{ID: "peer1", IP: net.ParseIP("10.0.0.1")},
{
ID: "peer1",
IP: netip.MustParseAddr("10.0.0.1"),
IPv6: netip.MustParseAddr("fd00::a00:1"),
},
},
expiredPeers: []*nbpeer.Peer{
{ID: "expired-peer", IP: net.ParseIP("10.0.0.99")},
{
ID: "expired-peer",
IP: netip.MustParseAddr("10.0.0.99"),
IPv6: netip.MustParseAddr("fd00::a00:63"),
},
},
peer: &nbpeer.Peer{
ID: "router",
IP: netip.MustParseAddr("10.0.0.100"),
IPv6: netip.MustParseAddr("fd00::a00:64"),
},
peer: &nbpeer.Peer{ID: "router", IP: net.ParseIP("10.0.0.100")},
expectedRecords: []nbdns.SimpleRecord{
{Name: "peer1.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"},
{Name: "expired-peer.netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.99"},
+52 -9
View File
@@ -48,16 +48,30 @@ func (r *FirewallRule) Equal(other *FirewallRule) bool {
}
// generateRouteFirewallRules generates a list of firewall rules for a given route.
func generateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int) []*RouteFirewallRule {
// For static routes, source ranges match the destination family (v4 or v6).
// For dynamic routes (domain-based), separate v4 and v6 rules are generated
// so the routing peer's forwarding chain allows both address families.
func generateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int, includeIPv6 bool) []*RouteFirewallRule {
rulesExists := make(map[string]struct{})
rules := make([]*RouteFirewallRule, 0)
sourceRanges := make([]string, 0, len(groupPeers))
for _, peer := range groupPeers {
if peer == nil {
continue
}
sourceRanges = append(sourceRanges, fmt.Sprintf(AllowedIPsFormat, peer.IP))
v4Sources, v6Sources := splitPeerSourcesByFamily(groupPeers)
isV6Route := route.Network.Addr().Is6()
// Skip v6 destination routes entirely for peers without IPv6 support
if isV6Route && !includeIPv6 {
return rules
}
// Pick sources matching the destination family
sourceRanges := v4Sources
if isV6Route {
sourceRanges = v6Sources
}
if len(sourceRanges) == 0 {
return rules
}
baseRule := RouteFirewallRule{
@@ -71,18 +85,47 @@ func generateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule
IsDynamic: route.IsDynamic(),
}
// generate rule for port range
if len(rule.Ports) == 0 {
rules = append(rules, generateRulesWithPortRanges(baseRule, rule, rulesExists)...)
} else {
rules = append(rules, generateRulesWithPorts(ctx, baseRule, rule, rulesExists)...)
}
// TODO: generate IPv6 rules for dynamic routes
// Generate v6 counterpart for dynamic routes and 0.0.0.0/0 exit node routes.
isDefaultV4 := !isV6Route && route.Network.Bits() == 0
if includeIPv6 && (route.IsDynamic() || isDefaultV4) && len(v6Sources) > 0 {
v6Rule := baseRule
v6Rule.SourceRanges = v6Sources
if isDefaultV4 {
v6Rule.Destination = "::/0"
v6Rule.RouteID = route.ID + "-v6-default"
}
if len(rule.Ports) == 0 {
rules = append(rules, generateRulesWithPortRanges(v6Rule, rule, rulesExists)...)
} else {
rules = append(rules, generateRulesWithPorts(ctx, v6Rule, rule, rulesExists)...)
}
}
return rules
}
// splitPeerSourcesByFamily separates peer IPs into v4 (/32) and v6 (/128) source ranges.
func splitPeerSourcesByFamily(groupPeers []*nbpeer.Peer) (v4, v6 []string) {
v4 = make([]string, 0, len(groupPeers))
v6 = make([]string, 0, len(groupPeers))
for _, peer := range groupPeers {
if peer == nil {
continue
}
v4 = append(v4, fmt.Sprintf(AllowedIPsFormat, peer.IP))
if peer.IPv6.IsValid() {
v6 = append(v6, fmt.Sprintf(AllowedIPsV6Format, peer.IPv6))
}
}
return
}
// generateRulesForPeer generates rules for a given peer based on ports and port ranges.
func generateRulesWithPortRanges(baseRule RouteFirewallRule, rule *PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule {
rules := make([]*RouteFirewallRule, 0)
@@ -0,0 +1,197 @@
package types
import (
"context"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/domain"
)
func TestSplitPeerSourcesByFamily(t *testing.T) {
peers := []*nbpeer.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
},
{
IP: netip.MustParseAddr("100.64.0.2"),
},
{
IP: netip.MustParseAddr("100.64.0.3"),
IPv6: netip.MustParseAddr("fd00::3"),
},
nil,
}
v4, v6 := splitPeerSourcesByFamily(peers)
assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32", "100.64.0.3/32"}, v4)
assert.Equal(t, []string{"fd00::1/128", "fd00::3/128"}, v6)
}
func TestGenerateRouteFirewallRules_V4Route(t *testing.T) {
peers := []*nbpeer.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
},
{
IP: netip.MustParseAddr("100.64.0.2"),
},
}
r := &route.Route{
ID: "route1",
Network: netip.MustParsePrefix("10.0.0.0/24"),
}
rule := &PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
}
rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
require.Len(t, rules, 1)
assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges, "v4 route should only have v4 sources")
assert.Equal(t, "10.0.0.0/24", rules[0].Destination)
}
func TestGenerateRouteFirewallRules_V6Route(t *testing.T) {
peers := []*nbpeer.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
},
{
IP: netip.MustParseAddr("100.64.0.2"),
},
}
r := &route.Route{
ID: "route1",
Network: netip.MustParsePrefix("2001:db8::/32"),
}
rule := &PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
}
rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
require.Len(t, rules, 1)
assert.Equal(t, []string{"fd00::1/128"}, rules[0].SourceRanges, "v6 route should only have v6 sources")
}
func TestGenerateRouteFirewallRules_DynamicRoute_DualStack(t *testing.T) {
peers := []*nbpeer.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
},
{
IP: netip.MustParseAddr("100.64.0.2"),
},
}
r := &route.Route{
ID: "route1",
NetworkType: route.DomainNetwork,
Domains: domain.List{"example.com"},
}
rule := &PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
}
rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
require.Len(t, rules, 2, "dynamic route should produce both v4 and v6 rules")
assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges)
assert.Equal(t, []string{"fd00::1/128"}, rules[1].SourceRanges)
assert.Equal(t, rules[0].Domains, rules[1].Domains)
assert.True(t, rules[0].IsDynamic)
assert.True(t, rules[1].IsDynamic)
}
func TestGenerateRouteFirewallRules_DynamicRoute_NoV6Peers(t *testing.T) {
peers := []*nbpeer.Peer{
{IP: netip.MustParseAddr("100.64.0.1")},
{IP: netip.MustParseAddr("100.64.0.2")},
}
r := &route.Route{
ID: "route1",
NetworkType: route.DomainNetwork,
Domains: domain.List{"example.com"},
}
rule := &PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
}
rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
require.Len(t, rules, 1, "no v6 peers means only v4 rule")
assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges)
}
func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) {
peers := []*nbpeer.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
},
{
IP: netip.MustParseAddr("100.64.0.2"),
IPv6: netip.MustParseAddr("fd00::2"),
},
}
t.Run("v6 route excluded", func(t *testing.T) {
r := &route.Route{
ID: "route1",
Network: netip.MustParsePrefix("2001:db8::/32"),
}
rule := &PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
}
rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false)
assert.Empty(t, rules, "v6 route should produce no rules when includeIPv6 is false")
})
t.Run("dynamic route only v4", func(t *testing.T) {
r := &route.Route{
ID: "route1",
NetworkType: route.DomainNetwork,
Domains: domain.List{"example.com"},
}
rule := &PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
}
rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false)
require.Len(t, rules, 1, "dynamic route with includeIPv6=false should produce only v4 rule")
assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges)
})
}
+234
View File
@@ -0,0 +1,234 @@
package types
import (
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
)
func TestPeerIPv6Allowed(t *testing.T) {
account := &Account{
Groups: map[string]*Group{
"group-all": {ID: "group-all", Name: "All", Peers: []string{"peer1", "peer2", "peer3"}},
"group-devs": {ID: "group-devs", Name: "Devs", Peers: []string{"peer1", "peer2"}},
"group-infra": {ID: "group-infra", Name: "Infra", Peers: []string{"peer2", "peer3"}},
"group-empty": {ID: "group-empty", Name: "Empty", Peers: []string{}},
},
Settings: &Settings{},
}
tests := []struct {
name string
enabledGroups []string
peerID string
expected bool
}{
{
name: "empty groups list disables IPv6 for all",
enabledGroups: []string{},
peerID: "peer1",
expected: false,
},
{
name: "All group enables IPv6 for everyone",
enabledGroups: []string{"group-all"},
peerID: "peer1",
expected: true,
},
{
name: "peer in enabled group gets IPv6",
enabledGroups: []string{"group-devs"},
peerID: "peer1",
expected: true,
},
{
name: "peer not in any enabled group denied IPv6",
enabledGroups: []string{"group-devs"},
peerID: "peer3",
expected: false,
},
{
name: "peer in multiple groups, one enabled",
enabledGroups: []string{"group-infra"},
peerID: "peer2",
expected: true,
},
{
name: "peer in multiple groups, other one enabled",
enabledGroups: []string{"group-devs"},
peerID: "peer2",
expected: true,
},
{
name: "multiple enabled groups, peer in one",
enabledGroups: []string{"group-devs", "group-infra"},
peerID: "peer1",
expected: true,
},
{
name: "multiple enabled groups, peer in both",
enabledGroups: []string{"group-devs", "group-infra"},
peerID: "peer2",
expected: true,
},
{
name: "nonexistent group ID in enabled list",
enabledGroups: []string{"group-deleted"},
peerID: "peer1",
expected: false,
},
{
name: "empty group in enabled list",
enabledGroups: []string{"group-empty"},
peerID: "peer1",
expected: false,
},
{
name: "unknown peer ID",
enabledGroups: []string{"group-all"},
peerID: "peer-unknown",
expected: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
account.Settings.IPv6EnabledGroups = tc.enabledGroups
result := account.PeerIPv6Allowed(tc.peerID)
assert.Equal(t, tc.expected, result)
})
}
}
func TestIPv6RecalculationOnGroupChange(t *testing.T) {
peerWithV6 := func(id string, v6 string) *nbpeer.Peer {
p := &nbpeer.Peer{
ID: id,
IP: netip.MustParseAddr("100.64.0.1"),
}
if v6 != "" {
p.IPv6 = netip.MustParseAddr(v6)
}
return p
}
t.Run("peer loses IPv6 when removed from enabled groups", func(t *testing.T) {
peer := peerWithV6("peer1", "fd00::1")
account := &Account{
Peers: map[string]*nbpeer.Peer{"peer1": peer},
Groups: map[string]*Group{
"group-a": {ID: "group-a", Peers: []string{"peer1"}},
"group-b": {ID: "group-b", Peers: []string{}},
},
Settings: &Settings{
IPv6EnabledGroups: []string{"group-a"},
},
}
assert.True(t, account.PeerIPv6Allowed("peer1"), "peer should be allowed before change")
// Move peer out of enabled group
account.Groups["group-a"].Peers = []string{}
account.Groups["group-b"].Peers = []string{"peer1"}
assert.False(t, account.PeerIPv6Allowed("peer1"), "peer should be denied after group change")
})
t.Run("peer gains IPv6 when added to enabled group", func(t *testing.T) {
peer := peerWithV6("peer1", "")
account := &Account{
Peers: map[string]*nbpeer.Peer{"peer1": peer},
Groups: map[string]*Group{
"group-a": {ID: "group-a", Peers: []string{}},
"group-b": {ID: "group-b", Peers: []string{"peer1"}},
},
Settings: &Settings{
IPv6EnabledGroups: []string{"group-a"},
},
}
assert.False(t, account.PeerIPv6Allowed("peer1"), "peer should be denied before change")
// Add peer to enabled group
account.Groups["group-a"].Peers = []string{"peer1"}
assert.True(t, account.PeerIPv6Allowed("peer1"), "peer should be allowed after joining enabled group")
})
t.Run("peer in two groups, one leaves enabled list", func(t *testing.T) {
peer := peerWithV6("peer1", "fd00::1")
account := &Account{
Peers: map[string]*nbpeer.Peer{"peer1": peer},
Groups: map[string]*Group{
"group-a": {ID: "group-a", Peers: []string{"peer1"}},
"group-b": {ID: "group-b", Peers: []string{"peer1"}},
},
Settings: &Settings{
IPv6EnabledGroups: []string{"group-a", "group-b"},
},
}
assert.True(t, account.PeerIPv6Allowed("peer1"))
// Remove group-a from enabled list, peer still in group-b
account.Settings.IPv6EnabledGroups = []string{"group-b"}
assert.True(t, account.PeerIPv6Allowed("peer1"), "peer should still be allowed via group-b")
})
t.Run("peer in two groups, both leave enabled list", func(t *testing.T) {
peer := peerWithV6("peer1", "fd00::1")
account := &Account{
Peers: map[string]*nbpeer.Peer{"peer1": peer},
Groups: map[string]*Group{
"group-a": {ID: "group-a", Peers: []string{"peer1"}},
"group-b": {ID: "group-b", Peers: []string{"peer1"}},
},
Settings: &Settings{
IPv6EnabledGroups: []string{"group-a", "group-b"},
},
}
assert.True(t, account.PeerIPv6Allowed("peer1"))
// Clear all enabled groups
account.Settings.IPv6EnabledGroups = []string{}
assert.False(t, account.PeerIPv6Allowed("peer1"), "peer should be denied when no groups enabled")
})
t.Run("enabling a group gives only its peers IPv6", func(t *testing.T) {
account := &Account{
Peers: map[string]*nbpeer.Peer{
"peer1": peerWithV6("peer1", ""),
"peer2": peerWithV6("peer2", ""),
"peer3": peerWithV6("peer3", ""),
},
Groups: map[string]*Group{
"group-devs": {ID: "group-devs", Peers: []string{"peer1", "peer2"}},
"group-infra": {ID: "group-infra", Peers: []string{"peer2", "peer3"}},
},
Settings: &Settings{
IPv6EnabledGroups: []string{"group-devs"},
},
}
assert.True(t, account.PeerIPv6Allowed("peer1"), "peer1 in devs")
assert.True(t, account.PeerIPv6Allowed("peer2"), "peer2 in devs")
assert.False(t, account.PeerIPv6Allowed("peer3"), "peer3 not in devs")
// Add infra group
account.Settings.IPv6EnabledGroups = []string{"group-devs", "group-infra"}
assert.True(t, account.PeerIPv6Allowed("peer1"), "peer1 still in devs")
assert.True(t, account.PeerIPv6Allowed("peer2"), "peer2 in both")
assert.True(t, account.PeerIPv6Allowed("peer3"), "peer3 now in infra")
})
}
+122 -29
View File
@@ -2,8 +2,11 @@ package types
import (
"encoding/binary"
"fmt"
"math/rand"
"net"
"net/netip"
"slices"
"sync"
"time"
@@ -27,6 +30,12 @@ const (
// AllowedIPsFormat generates Wireguard AllowedIPs format (e.g. 100.64.30.1/32)
AllowedIPsFormat = "%s/32"
// AllowedIPsV6Format generates AllowedIPs format for v6 (e.g. fd12:3456:7890::1/128)
AllowedIPsV6Format = "%s/128"
// IPv6SubnetSize is the prefix length of per-account IPv6 subnets.
// Each account gets a /64 from its unique /48 ULA prefix.
IPv6SubnetSize = 64
)
type NetworkMap struct {
@@ -111,7 +120,9 @@ func ipToBytes(ip net.IP) []byte {
type Network struct {
Identifier string `json:"id"`
Net net.IPNet `gorm:"serializer:json"`
Dns string
// NetV6 is the IPv6 ULA subnet for this account's overlay. Empty if not yet allocated.
NetV6 net.IPNet `gorm:"serializer:json"`
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.
Serial uint64
@@ -121,20 +132,45 @@ type Network struct {
// 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)
// and a random /64 subnet from fd00:4e42::/32 for IPv6.
func NewNetwork() *Network {
n := iplib.NewNet4(net.ParseIP("100.64.0.0"), NetSize)
sub, _ := n.Subnet(SubnetSize)
s := rand.NewSource(time.Now().Unix())
s := rand.NewSource(time.Now().UnixNano())
r := rand.New(s)
intn := r.Intn(len(sub))
return &Network{
Identifier: xid.New().String(),
Net: sub[intn].IPNet,
NetV6: AllocateIPv6Subnet(r),
Dns: "",
Serial: 0}
Serial: 0,
}
}
// AllocateIPv6Subnet generates a random RFC 4193 ULA /64 prefix.
// The format follows RFC 4193 section 3.1: fd + 40-bit Global ID + 16-bit Subnet ID.
// The Global ID and Subnet ID are randomized (simplified from the SHA-1 algorithm
// in section 3.2.2), giving 2^56 possible /64 subnets across all accounts.
func AllocateIPv6Subnet(r *rand.Rand) net.IPNet {
ip := make(net.IP, 16)
ip[0] = 0xfd
// Bytes 1-5: 40-bit random Global ID
ip[1] = byte(r.Intn(256))
ip[2] = byte(r.Intn(256))
ip[3] = byte(r.Intn(256))
ip[4] = byte(r.Intn(256))
ip[5] = byte(r.Intn(256))
// Bytes 6-7: 16-bit random Subnet ID
ip[6] = byte(r.Intn(256))
ip[7] = byte(r.Intn(256))
return net.IPNet{
IP: ip,
Mask: net.CIDRMask(IPv6SubnetSize, 128),
}
}
// IncSerial increments Serial by 1 reflecting that the network state has been changed
@@ -157,19 +193,19 @@ func (n *Network) Copy() *Network {
return &Network{
Identifier: n.Identifier,
Net: n.Net,
NetV6: n.NetV6,
Dns: n.Dns,
Serial: n.Serial,
}
}
// AllocatePeerIP pics an available IP from an net.IPNet.
// This method considers already taken IPs and reuses IPs if there are gaps in takenIps
// E.g. if ipNet=100.30.0.0/16 and takenIps=[100.30.0.1, 100.30.0.4] then the result would be 100.30.0.2 or 100.30.0.3
func AllocatePeerIP(ipNet net.IPNet, takenIps []net.IP) (net.IP, error) {
baseIP := ipToUint32(ipNet.IP.Mask(ipNet.Mask))
ones, bits := ipNet.Mask.Size()
hostBits := bits - ones
// AllocatePeerIP picks an available IP from a netip.Prefix.
// This method considers already taken IPs and reuses IPs if there are gaps in takenIps.
// E.g. if prefix=100.30.0.0/16 and takenIps=[100.30.0.1, 100.30.0.4] then the result would be 100.30.0.2 or 100.30.0.3.
func AllocatePeerIP(prefix netip.Prefix, takenIps []netip.Addr) (netip.Addr, error) {
b := prefix.Masked().Addr().As4()
baseIP := binary.BigEndian.Uint32(b[:])
hostBits := 32 - prefix.Bits()
totalIPs := uint32(1 << hostBits)
taken := make(map[uint32]struct{}, len(takenIps)+1)
@@ -177,7 +213,8 @@ func AllocatePeerIP(ipNet net.IPNet, takenIps []net.IP) (net.IP, error) {
taken[baseIP+totalIPs-1] = struct{}{} // reserve broadcast IP
for _, ip := range takenIps {
taken[ipToUint32(ip)] = struct{}{}
ab := ip.As4()
taken[binary.BigEndian.Uint32(ab[:])] = struct{}{}
}
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
@@ -198,15 +235,14 @@ func AllocatePeerIP(ipNet net.IPNet, takenIps []net.IP) (net.IP, error) {
}
}
return nil, status.Errorf(status.PreconditionFailed, "network %s is out of IPs", ipNet.String())
return netip.Addr{}, status.Errorf(status.PreconditionFailed, "network %s is out of IPs", prefix.String())
}
func AllocateRandomPeerIP(ipNet net.IPNet) (net.IP, error) {
baseIP := ipToUint32(ipNet.IP.Mask(ipNet.Mask))
ones, bits := ipNet.Mask.Size()
hostBits := bits - ones
// AllocateRandomPeerIP picks a random available IP from a netip.Prefix.
func AllocateRandomPeerIP(prefix netip.Prefix) (netip.Addr, error) {
b := prefix.Masked().Addr().As4()
baseIP := binary.BigEndian.Uint32(b[:])
hostBits := 32 - prefix.Bits()
totalIPs := uint32(1 << hostBits)
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
@@ -216,18 +252,75 @@ func AllocateRandomPeerIP(ipNet net.IPNet) (net.IP, error) {
return uint32ToIP(candidate), nil
}
func ipToUint32(ip net.IP) uint32 {
ip = ip.To4()
if len(ip) < 4 {
return 0
// AllocateRandomPeerIPv6 picks a random host address within the given IPv6 prefix.
// Only the host bits (after the prefix length) are randomized.
func AllocateRandomPeerIPv6(prefix netip.Prefix) (netip.Addr, error) {
ones := prefix.Bits()
if ones == 0 || ones > 126 || !prefix.Addr().Is6() {
return netip.Addr{}, fmt.Errorf("invalid IPv6 subnet: %s", prefix.String())
}
return binary.BigEndian.Uint32(ip)
ip := prefix.Addr().As16()
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
// Determine which byte the host bits start in
firstHostByte := ones / 8
// If the prefix doesn't end on a byte boundary, handle the partial byte
partialBits := ones % 8
if partialBits > 0 {
// Keep the network bits in the partial byte, randomize the rest
hostMask := byte(0xff >> partialBits)
ip[firstHostByte] = (ip[firstHostByte] & ^hostMask) | (byte(rng.Intn(256)) & hostMask)
firstHostByte++
}
// Randomize remaining full host bytes
for i := firstHostByte; i < 16; i++ {
ip[i] = byte(rng.Intn(256))
}
// Avoid all-zeros and all-ones host parts by checking only host bits.
if isHostAllZeroOrOnes(ip[:], ones) {
ip = prefix.Masked().Addr().As16()
ip[15] |= 0x01
}
return netip.AddrFrom16(ip).Unmap(), nil
}
func uint32ToIP(n uint32) net.IP {
ip := make(net.IP, 4)
binary.BigEndian.PutUint32(ip, n)
return ip
// isHostAllZeroOrOnes checks whether all host bits (after prefixLen) are zero or all ones.
func isHostAllZeroOrOnes(ip []byte, prefixLen int) bool {
hostStart := prefixLen / 8
partialBits := prefixLen % 8
hostSlice := slices.Clone(ip[hostStart:])
if partialBits > 0 {
hostSlice[0] &= 0xff >> partialBits
}
allZero := !slices.ContainsFunc(hostSlice, func(v byte) bool { return v != 0 })
if allZero {
return true
}
// Build the all-ones mask for host bits
onesMask := make([]byte, len(hostSlice))
for i := range onesMask {
onesMask[i] = 0xff
}
if partialBits > 0 {
onesMask[0] = 0xff >> partialBits
}
return slices.Equal(hostSlice, onesMask)
}
func uint32ToIP(n uint32) netip.Addr {
var b [4]byte
binary.BigEndian.PutUint32(b[:], n)
return netip.AddrFrom4(b)
}
// generateIPs generates a list of all possible IPs of the given network excluding IPs specified in the exclusion list
+131 -20
View File
@@ -1,7 +1,9 @@
package types
import (
"encoding/binary"
"net"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
@@ -17,10 +19,10 @@ func TestNewNetwork(t *testing.T) {
}
func TestAllocatePeerIP(t *testing.T) {
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 255, 255, 0}}
var ips []net.IP
prefix := netip.MustParsePrefix("100.64.0.0/24")
var ips []netip.Addr
for i := 0; i < 252; i++ {
ip, err := AllocatePeerIP(ipNet, ips)
ip, err := AllocatePeerIP(prefix, ips)
if err != nil {
t.Fatal(err)
}
@@ -41,19 +43,19 @@ func TestAllocatePeerIP(t *testing.T) {
func TestAllocatePeerIPSmallSubnet(t *testing.T) {
// Test /27 network (10.0.0.0/27) - should only have 30 usable IPs (10.0.0.1 to 10.0.0.30)
ipNet := net.IPNet{IP: net.ParseIP("10.0.0.0"), Mask: net.IPMask{255, 255, 255, 224}}
var ips []net.IP
prefix := netip.MustParsePrefix("10.0.0.0/27")
var ips []netip.Addr
// Allocate all available IPs in the /27 network
for i := 0; i < 30; i++ {
ip, err := AllocatePeerIP(ipNet, ips)
ip, err := AllocatePeerIP(prefix, ips)
if err != nil {
t.Fatal(err)
}
// Verify IP is within the correct range
if !ipNet.Contains(ip) {
t.Errorf("allocated IP %s is not within network %s", ip.String(), ipNet.String())
if !prefix.Contains(ip) {
t.Errorf("allocated IP %s is not within network %s", ip.String(), prefix.String())
}
ips = append(ips, ip)
@@ -72,7 +74,7 @@ func TestAllocatePeerIPSmallSubnet(t *testing.T) {
}
// Try to allocate one more IP - should fail as network is full
_, err := AllocatePeerIP(ipNet, ips)
_, err := AllocatePeerIP(prefix, ips)
if err == nil {
t.Error("expected error when network is full, but got none")
}
@@ -95,10 +97,11 @@ func TestAllocatePeerIPVariousCIDRs(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, ipNet, err := net.ParseCIDR(tc.cidr)
prefix, err := netip.ParsePrefix(tc.cidr)
require.NoError(t, err)
prefix = prefix.Masked()
var ips []net.IP
var ips []netip.Addr
// For larger networks, test only a subset to avoid long test runs
testCount := tc.expectedUsable
@@ -108,21 +111,21 @@ func TestAllocatePeerIPVariousCIDRs(t *testing.T) {
// Allocate IPs and verify they're within the correct range
for i := 0; i < testCount; i++ {
ip, err := AllocatePeerIP(*ipNet, ips)
ip, err := AllocatePeerIP(prefix, ips)
require.NoError(t, err, "failed to allocate IP %d", i)
// Verify IP is within the correct range
assert.True(t, ipNet.Contains(ip), "allocated IP %s is not within network %s", ip.String(), ipNet.String())
assert.True(t, prefix.Contains(ip), "allocated IP %s is not within network %s", ip.String(), prefix.String())
// Verify IP is not network or broadcast address
networkIP := ipNet.IP.Mask(ipNet.Mask)
ones, bits := ipNet.Mask.Size()
hostBits := bits - ones
broadcastInt := uint32(ipToUint32(networkIP)) + (1 << hostBits) - 1
broadcastIP := uint32ToIP(broadcastInt)
networkAddr := prefix.Masked().Addr()
hostBits := 32 - prefix.Bits()
b := networkAddr.As4()
baseIP := binary.BigEndian.Uint32(b[:])
broadcastIP := uint32ToIP(baseIP + (1 << hostBits) - 1)
assert.False(t, ip.Equal(networkIP), "allocated network address %s", ip.String())
assert.False(t, ip.Equal(broadcastIP), "allocated broadcast address %s", ip.String())
assert.NotEqual(t, networkAddr, ip, "allocated network address %s", ip.String())
assert.NotEqual(t, broadcastIP, ip, "allocated broadcast address %s", ip.String())
ips = append(ips, ip)
}
@@ -151,3 +154,111 @@ func TestGenerateIPs(t *testing.T) {
t.Errorf("expected last ip to be: 100.64.0.253, got %s", ips[len(ips)-1].String())
}
}
func TestNewNetworkHasIPv6(t *testing.T) {
network := NewNetwork()
assert.NotNil(t, network.NetV6.IP, "v6 subnet should be allocated")
assert.True(t, network.NetV6.IP.To4() == nil, "v6 subnet should be IPv6")
assert.Equal(t, byte(0xfd), network.NetV6.IP[0], "v6 subnet should be ULA (fd prefix)")
ones, bits := network.NetV6.Mask.Size()
assert.Equal(t, 64, ones, "v6 subnet should be /64")
assert.Equal(t, 128, bits)
}
func TestAllocateIPv6SubnetUniqueness(t *testing.T) {
seen := make(map[string]struct{})
for i := 0; i < 100; i++ {
network := NewNetwork()
key := network.NetV6.IP.String()
_, duplicate := seen[key]
assert.False(t, duplicate, "duplicate v6 subnet: %s", key)
seen[key] = struct{}{}
}
}
func TestAllocateRandomPeerIPv6(t *testing.T) {
prefix := netip.MustParsePrefix("fd12:3456:7890:abcd::/64")
ip, err := AllocateRandomPeerIPv6(prefix)
require.NoError(t, err)
assert.True(t, ip.Is6(), "should be IPv6")
assert.True(t, prefix.Contains(ip), "should be within subnet")
// First 8 bytes (network prefix) should match
b := ip.As16()
prefixBytes := prefix.Addr().As16()
assert.Equal(t, prefixBytes[:8], b[:8], "prefix should match")
// Interface ID should not be all zeros
allZero := true
for _, v := range b[8:] {
if v != 0 {
allZero = false
break
}
}
assert.False(t, allZero, "interface ID should not be all zeros")
}
func TestAllocateRandomPeerIPv6_VariousPrefixes(t *testing.T) {
tests := []struct {
name string
cidr string
prefix int
}{
{"standard /64", "fd00:1234:5678:abcd::/64", 64},
{"small /112", "fd00:1234:5678:abcd::/112", 112},
{"large /48", "fd00:1234::/48", 48},
{"non-boundary /60", "fd00:1234:5670::/60", 60},
{"non-boundary /52", "fd00:1230::/52", 52},
{"minimum /120", "fd00:1234:5678:abcd::100/120", 120},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prefix, err := netip.ParsePrefix(tt.cidr)
require.NoError(t, err)
prefix = prefix.Masked()
assert.Equal(t, tt.prefix, prefix.Bits())
for i := 0; i < 50; i++ {
ip, err := AllocateRandomPeerIPv6(prefix)
require.NoError(t, err)
assert.True(t, prefix.Contains(ip), "IP %s should be within %s", ip, prefix)
}
})
}
}
func TestAllocateRandomPeerIPv6_PreservesNetworkBits(t *testing.T) {
// For a /112, bytes 0-13 should be preserved, only bytes 14-15 should vary
prefix := netip.MustParsePrefix("fd00:1234:5678:abcd:ef01:2345:6789:0/112")
prefixBytes := prefix.Addr().As16()
for i := 0; i < 20; i++ {
ip, err := AllocateRandomPeerIPv6(prefix)
require.NoError(t, err)
// First 14 bytes (112 bits = 14 bytes) must match the network
b := ip.As16()
assert.Equal(t, prefixBytes[:14], b[:14], "network bytes should be preserved for /112")
}
}
func TestAllocateRandomPeerIPv6_NonByteBoundary(t *testing.T) {
// For a /60, the first 7.5 bytes are network, so byte 7 is partial
prefix := netip.MustParsePrefix("fd00:1234:5678:abc0::/60")
prefixBytes := prefix.Addr().As16()
for i := 0; i < 50; i++ {
ip, err := AllocateRandomPeerIPv6(prefix)
require.NoError(t, err)
b := ip.As16()
assert.True(t, prefix.Contains(ip), "IP %s should be within %s", ip, prefix)
// First 7 bytes must match exactly
assert.Equal(t, prefixBytes[:7], b[:7], "full network bytes should match for /60")
// Byte 7: top 4 bits (0xc = 1100) must be preserved
assert.Equal(t, prefixBytes[7]&0xf0, b[7]&0xf0, "partial byte network bits should be preserved for /60")
}
}
@@ -322,7 +322,7 @@ func createTestAccount() *Account {
for i := range numPeers {
peerID := fmt.Sprintf("peer-%d", i)
ip := net.IP{100, 64, 0, byte(i + 1)}
ip := netip.AddrFrom4([4]byte{100, 64, 0, byte(i + 1)})
wtVersion := "0.25.0"
if i%2 == 0 {
wtVersion = "0.40.0"
@@ -3,7 +3,6 @@ package types
import (
"context"
"maps"
"net"
"net/netip"
"slices"
"strconv"
@@ -116,13 +115,17 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap {
peersToConnect, expiredPeers := c.filterPeersByLoginExpiration(aclPeers)
routesUpdate := c.getRoutesToSync(targetPeerID, peersToConnect, peerGroups)
routesFirewallRules := c.getPeerRoutesFirewallRules(ctx, targetPeerID)
includeIPv6 := false
if p := c.Peers[targetPeerID]; p != nil {
includeIPv6 = p.SupportsIPv6() && p.IPv6.IsValid()
}
routesUpdate := filterAndExpandRoutes(c.getRoutesToSync(targetPeerID, peersToConnect, peerGroups), includeIPv6)
routesFirewallRules := c.getPeerRoutesFirewallRules(ctx, targetPeerID, includeIPv6)
isRouter, networkResourcesRoutes, sourcePeers := c.getNetworkResourcesRoutesToSync(targetPeerID)
var networkResourcesFirewallRules []*RouteFirewallRule
if isRouter {
networkResourcesFirewallRules = c.getPeerNetworkResourceFirewallRules(ctx, targetPeerID, networkResourcesRoutes)
networkResourcesFirewallRules = c.getPeerNetworkResourceFirewallRules(ctx, targetPeerID, networkResourcesRoutes, includeIPv6)
}
peersToConnectIncludingRouters := c.addNetworksRoutingPeers(
@@ -158,7 +161,7 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap {
return &NetworkMap{
Peers: peersToConnectIncludingRouters,
Network: c.Network.Copy(),
Routes: append(networkResourcesRoutes, routesUpdate...),
Routes: append(filterAndExpandRoutes(networkResourcesRoutes, includeIPv6), routesUpdate...),
DNSConfig: dnsUpdate,
OfflinePeers: expiredPeers,
FirewallRules: firewallRules,
@@ -298,7 +301,7 @@ func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *nbpeer.Peer) (
peersExists[peer.ID] = struct{}{}
}
peerIP := net.IP(peer.IP).String()
peerIP := peer.IP.String()
fr := FirewallRule{
PolicyID: rule.ID,
@@ -317,10 +320,17 @@ func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *nbpeer.Peer) (
if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 {
rules = append(rules, &fr)
continue
} else {
rules = append(rules, expandPortsAndRanges(fr, rule, targetPeer)...)
}
rules = append(rules, expandPortsAndRanges(fr, rule, targetPeer)...)
rules = appendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, firewallRuleContext{
direction: direction,
dirStr: dirStr,
protocolStr: protocolStr,
actionStr: actionStr,
portsJoined: portsJoined,
})
}
}, func() ([]*nbpeer.Peer, []*FirewallRule) {
return peers, rules
@@ -456,6 +466,29 @@ func (c *NetworkMapComponents) peerIsNameserver(peerIPStr string, nsGroup *nbdns
return false
}
// filterAndExpandRoutes drops v6 routes for non-capable peers and duplicates
// the default v4 route (0.0.0.0/0) as ::/0 for v6-capable peers.
// TODO: the "-v6" suffix on IDs could collide with user-supplied route IDs.
func filterAndExpandRoutes(routes []*route.Route, includeIPv6 bool) []*route.Route {
filtered := make([]*route.Route, 0, len(routes))
for _, r := range routes {
if !includeIPv6 && r.Network.Addr().Is6() {
continue
}
filtered = append(filtered, r)
if includeIPv6 && r.Network.Bits() == 0 && r.Network.Addr().Is4() {
v6 := r.Copy()
v6.ID = r.ID + "-v6-default"
v6.NetID = r.NetID + "-v6"
v6.Network = netip.MustParsePrefix("::/0")
v6.NetworkType = route.IPv6Network
filtered = append(filtered, v6)
}
}
return filtered
}
func (c *NetworkMapComponents) getRoutesToSync(peerID string, aclPeers []*nbpeer.Peer, peerGroups LookupMap) []*route.Route {
routes, peerDisabledRoutes := c.getRoutingPeerRoutes(peerID)
peerRoutesMembership := make(LookupMap)
@@ -526,7 +559,6 @@ func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoute
return enabledRoutes, disabledRoutes
}
func (c *NetworkMapComponents) filterRoutesByGroups(routes []*route.Route, groupListMap LookupMap) []*route.Route {
var filteredRoutes []*route.Route
for _, r := range routes {
@@ -552,13 +584,13 @@ func (c *NetworkMapComponents) filterRoutesFromPeersOfSameHAGroup(routes []*rout
return filteredRoutes
}
func (c *NetworkMapComponents) getPeerRoutesFirewallRules(ctx context.Context, peerID string) []*RouteFirewallRule {
func (c *NetworkMapComponents) getPeerRoutesFirewallRules(ctx context.Context, peerID string, includeIPv6 bool) []*RouteFirewallRule {
routesFirewallRules := make([]*RouteFirewallRule, 0)
enabledRoutes, _ := c.getRoutingPeerRoutes(peerID)
for _, r := range enabledRoutes {
if len(r.AccessControlGroups) == 0 {
defaultPermit := c.getDefaultPermit(r)
defaultPermit := c.getDefaultPermit(r, includeIPv6)
routesFirewallRules = append(routesFirewallRules, defaultPermit...)
continue
}
@@ -567,7 +599,7 @@ func (c *NetworkMapComponents) getPeerRoutesFirewallRules(ctx context.Context, p
for _, accessGroup := range r.AccessControlGroups {
policies := c.getAllRoutePoliciesFromGroups([]string{accessGroup})
rules := c.getRouteFirewallRules(ctx, peerID, policies, r, distributionPeers)
rules := c.getRouteFirewallRules(ctx, peerID, policies, r, distributionPeers, includeIPv6)
routesFirewallRules = append(routesFirewallRules, rules...)
}
}
@@ -575,8 +607,10 @@ func (c *NetworkMapComponents) getPeerRoutesFirewallRules(ctx context.Context, p
return routesFirewallRules
}
func (c *NetworkMapComponents) getDefaultPermit(r *route.Route) []*RouteFirewallRule {
var rules []*RouteFirewallRule
func (c *NetworkMapComponents) getDefaultPermit(r *route.Route, includeIPv6 bool) []*RouteFirewallRule {
if r.Network.Addr().Is6() && !includeIPv6 {
return nil
}
sources := []string{"0.0.0.0/0"}
if r.Network.Addr().Is6() {
@@ -593,9 +627,9 @@ func (c *NetworkMapComponents) getDefaultPermit(r *route.Route) []*RouteFirewall
RouteID: r.ID,
}
rules = append(rules, &rule)
rules := []*RouteFirewallRule{&rule}
if r.IsDynamic() {
if includeIPv6 && r.IsDynamic() {
ruleV6 := rule
ruleV6.SourceRanges = []string{"::/0"}
rules = append(rules, &ruleV6)
@@ -634,7 +668,7 @@ func (c *NetworkMapComponents) getAllRoutePoliciesFromGroups(accessControlGroups
return routePolicies
}
func (c *NetworkMapComponents) getRouteFirewallRules(ctx context.Context, peerID string, policies []*Policy, route *route.Route, distributionPeers map[string]struct{}) []*RouteFirewallRule {
func (c *NetworkMapComponents) getRouteFirewallRules(ctx context.Context, peerID string, policies []*Policy, route *route.Route, distributionPeers map[string]struct{}, includeIPv6 bool) []*RouteFirewallRule {
var fwRules []*RouteFirewallRule
for _, policy := range policies {
if !policy.Enabled {
@@ -647,7 +681,7 @@ func (c *NetworkMapComponents) getRouteFirewallRules(ctx context.Context, peerID
}
rulePeers := c.getRulePeers(rule, policy.SourcePostureChecks, peerID, distributionPeers)
rules := generateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN)
rules := generateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN, includeIPv6)
fwRules = append(fwRules, rules...)
}
}
@@ -798,7 +832,7 @@ func (c *NetworkMapComponents) getPostureValidPeers(inputPeers []string, posture
return dest
}
func (c *NetworkMapComponents) getPeerNetworkResourceFirewallRules(ctx context.Context, peerID string, routes []*route.Route) []*RouteFirewallRule {
func (c *NetworkMapComponents) getPeerNetworkResourceFirewallRules(ctx context.Context, peerID string, routes []*route.Route, includeIPv6 bool) []*RouteFirewallRule {
routesFirewallRules := make([]*RouteFirewallRule, 0)
peerInfo := c.GetPeerInfo(peerID)
@@ -815,7 +849,7 @@ func (c *NetworkMapComponents) getPeerNetworkResourceFirewallRules(ctx context.C
resourcePolicies := c.ResourcePoliciesMap[resourceID]
distributionPeers := c.getPoliciesSourcePeers(resourcePolicies)
rules := c.getRouteFirewallRules(ctx, peerID, resourcePolicies, r, distributionPeers)
rules := c.getRouteFirewallRules(ctx, peerID, resourcePolicies, r, distributionPeers, includeIPv6)
for _, rule := range rules {
if len(rule.SourceRanges) > 0 {
routesFirewallRules = append(routesFirewallRules, rule)
@@ -899,3 +933,36 @@ func (c *NetworkMapComponents) addNetworksRoutingPeers(
return peersToConnect
}
type firewallRuleContext struct {
direction int
dirStr string
protocolStr string
actionStr string
portsJoined string
}
func appendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *nbpeer.Peer, rule *PolicyRule, rc firewallRuleContext) []*FirewallRule {
if !peer.IPv6.IsValid() || !targetPeer.SupportsIPv6() || !targetPeer.IPv6.IsValid() {
return rules
}
v6IP := peer.IPv6.String()
v6RuleID := rule.ID + v6IP + rc.dirStr + rc.protocolStr + rc.actionStr + rc.portsJoined
if _, ok := rulesExists[v6RuleID]; ok {
return rules
}
rulesExists[v6RuleID] = struct{}{}
v6fr := FirewallRule{
PolicyID: rule.ID,
PeerIP: v6IP,
Direction: rc.direction,
Action: rc.actionStr,
Protocol: rc.protocolStr,
}
if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 {
return append(rules, &v6fr)
}
return append(rules, expandPortsAndRanges(v6fr, rule, targetPeer)...)
}
@@ -147,15 +147,16 @@ func TestGetPeerNetworkMap_Golden_WithNewPeer(t *testing.T) {
builder := types.NewNetworkMapBuilder(account, validatedPeersMap)
newPeerID := "peer-new-101"
newPeerIP := net.IP{100, 64, 1, 1}
newPeerIP := netip.MustParseAddr("100.64.1.1")
newPeer := &nbpeer.Peer{
ID: newPeerID,
IP: newPeerIP,
IPv6: netip.MustParseAddr("fd00:1234:5678::101"),
Key: fmt.Sprintf("key-%s", newPeerID),
DNSLabel: "peernew101",
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now()},
UserID: "user-admin",
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.26.0", GoOS: "linux"},
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0", GoOS: "linux"},
LastLogin: func() *time.Time { t := time.Now(); return &t }(),
}
@@ -224,12 +225,13 @@ func BenchmarkGetPeerNetworkMap_AfterPeerAdded(b *testing.B) {
newPeerID := "peer-new-101"
newPeer := &nbpeer.Peer{
ID: newPeerID,
IP: net.IP{100, 64, 1, 1},
IP: netip.MustParseAddr("100.64.1.1"),
IPv6: netip.MustParseAddr("fd00:1234:5678::101"),
Key: fmt.Sprintf("key-%s", newPeerID),
DNSLabel: "peernew101",
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now()},
UserID: "user-admin",
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.26.0", GoOS: "linux"},
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0", GoOS: "linux"},
}
account.Peers[newPeerID] = newPeer
@@ -273,15 +275,16 @@ func TestGetPeerNetworkMap_Golden_WithNewRoutingPeer(t *testing.T) {
builder := types.NewNetworkMapBuilder(account, validatedPeersMap)
newRouterID := "peer-new-router-102"
newRouterIP := net.IP{100, 64, 1, 2}
newRouterIP := netip.MustParseAddr("100.64.1.2")
newRouter := &nbpeer.Peer{
ID: newRouterID,
IP: newRouterIP,
IPv6: netip.MustParseAddr("fd00:1234:5678::102"),
Key: fmt.Sprintf("key-%s", newRouterID),
DNSLabel: "newrouter102",
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now()},
UserID: "user-admin",
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.26.0", GoOS: "linux"},
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0", GoOS: "linux"},
LastLogin: func() *time.Time { t := time.Now(); return &t }(),
}
@@ -362,15 +365,16 @@ func BenchmarkGetPeerNetworkMap_AfterRouterPeerAdded(b *testing.B) {
}
builder := types.NewNetworkMapBuilder(account, validatedPeersMap)
newRouterID := "peer-new-router-102"
newRouterIP := net.IP{100, 64, 1, 2}
newRouterIP := netip.MustParseAddr("100.64.1.2")
newRouter := &nbpeer.Peer{
ID: newRouterID,
IP: newRouterIP,
IPv6: netip.MustParseAddr("fd00:1234:5678::102"),
Key: fmt.Sprintf("key-%s", newRouterID),
DNSLabel: "newrouter102",
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now()},
UserID: "user-admin",
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.26.0", GoOS: "linux"},
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0", GoOS: "linux"},
LastLogin: func() *time.Time { t := time.Now(); return &t }(),
}
@@ -729,16 +733,21 @@ func createTestAccountWithEntities() *types.Account {
for i := range numPeers {
peerID := fmt.Sprintf("peer-%d", i)
ip := net.IP{100, 64, 0, byte(i + 1)}
ip := netip.MustParseAddr(fmt.Sprintf("100.64.0.%d", i+1))
ipv6 := netip.MustParseAddr(fmt.Sprintf("fd00:1234:5678::%d", i+1))
wtVersion := "0.25.0"
if i%2 == 0 {
wtVersion = "0.40.0"
}
p := &nbpeer.Peer{
ID: peerID, IP: ip, Key: fmt.Sprintf("key-%s", peerID), DNSLabel: fmt.Sprintf("peer%d", i+1),
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now()},
UserID: "user-admin", Meta: nbpeer.PeerSystemMeta{WtVersion: wtVersion, GoOS: "linux"},
ID: peerID,
IP: ip,
IPv6: ipv6,
Key: fmt.Sprintf("key-%s", peerID),
DNSLabel: fmt.Sprintf("peer%d", i+1),
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now()},
UserID: "user-admin", Meta: nbpeer.PeerSystemMeta{WtVersion: wtVersion, GoOS: "linux"},
}
if peerID == expiredPeerID {
@@ -850,7 +859,10 @@ func createTestAccountWithEntities() *types.Account {
Id: testAccountID, Peers: peers, Groups: groups, Policies: policies, Routes: routes,
Users: users,
Network: &types.Network{
Identifier: "net-golden-test", Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(16, 32)}, Serial: 1,
Identifier: "net-golden-test",
Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(16, 32)},
NetV6: net.IPNet{IP: net.ParseIP("fd00:1234:5678::"), Mask: net.CIDRMask(64, 128)},
Serial: 1,
},
DNSSettings: types.DNSSettings{DisabledManagementGroups: []string{opsGroupID}},
NameServerGroups: map[string]*dns.NameServerGroup{
@@ -871,7 +883,7 @@ func createTestAccountWithEntities() *types.Account {
NetworkRouters: []*routerTypes.NetworkRouter{
{ID: networkRouterID, NetworkID: networkID, Peer: routingPeerID, Enabled: true, AccountID: testAccountID},
},
Settings: &types.Settings{PeerLoginExpirationEnabled: true, PeerLoginExpiration: 1 * time.Hour},
Settings: &types.Settings{PeerLoginExpirationEnabled: true, PeerLoginExpiration: 1 * time.Hour, IPv6EnabledGroups: []string{allGroupID}},
}
for _, p := range account.Policies {
@@ -900,15 +912,16 @@ func TestGetPeerNetworkMap_Golden_New_WithOnPeerAddedRouter_Batched(t *testing.T
builder := types.NewNetworkMapBuilder(account, validatedPeersMap)
newRouterID := "peer-new-router-102"
newRouterIP := net.IP{100, 64, 1, 2}
newRouterIP := netip.MustParseAddr("100.64.1.2")
newRouter := &nbpeer.Peer{
ID: newRouterID,
IP: newRouterIP,
IPv6: netip.MustParseAddr("fd00:1234:5678::102"),
Key: fmt.Sprintf("key-%s", newRouterID),
DNSLabel: "newrouter102",
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now()},
UserID: "user-admin",
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.26.0", GoOS: "linux"},
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0", GoOS: "linux"},
LastLogin: func() *time.Time { t := time.Now(); return &t }(),
}
+88 -24
View File
@@ -521,10 +521,17 @@ func (b *NetworkMapBuilder) generateResourcescached(
if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 {
*rules = append(*rules, &fr)
continue
} else {
*rules = append(*rules, expandPortsAndRanges(fr, rule, targetPeer)...)
}
*rules = append(*rules, expandPortsAndRanges(fr, rule, targetPeer)...)
*rules = appendIPv6FirewallRule(*rules, rulesExists, peer, targetPeer, rule, firewallRuleContext{
direction: direction,
dirStr: strconv.Itoa(direction),
protocolStr: firewallRuleProtocol(rule.Protocol),
actionStr: string(rule.Action),
portsJoined: strings.Join(rule.Ports, ","),
})
}
}
@@ -720,7 +727,7 @@ func (b *NetworkMapBuilder) buildPeerRoutesView(account *Account, peerID string)
allRoutes := slices.Concat(enabledRoutes, networkResourcesRoutes)
b.updateACGIndexForPeer(peerID, allRoutes)
routeFirewallRules := b.getPeerRoutesFirewallRules(account, peerID, b.validatedPeers)
routeFirewallRules := b.getPeerRoutesFirewallRules(account, peerID, b.validatedPeers, peer.SupportsIPv6() && peer.IPv6.IsValid())
for _, rule := range routeFirewallRules {
ruleID := b.generateRouteFirewallRuleID(rule)
view.RouteFirewallRuleIDs = append(view.RouteFirewallRuleIDs, ruleID)
@@ -823,13 +830,13 @@ func (b *NetworkMapBuilder) getRoutingPeerRoutes(peerID string) (enabledRoutes [
return enabledRoutes, disabledRoutes
}
func (b *NetworkMapBuilder) getPeerRoutesFirewallRules(account *Account, peerID string, validatedPeersMap map[string]struct{}) []*RouteFirewallRule {
func (b *NetworkMapBuilder) getPeerRoutesFirewallRules(account *Account, peerID string, validatedPeersMap map[string]struct{}, includeIPv6 bool) []*RouteFirewallRule {
routesFirewallRules := make([]*RouteFirewallRule, 0)
enabledRoutes, _ := b.getRoutingPeerRoutes(peerID)
for _, route := range enabledRoutes {
if len(route.AccessControlGroups) == 0 {
defaultPermit := getDefaultPermit(route)
defaultPermit := getDefaultPermit(route, includeIPv6)
routesFirewallRules = append(routesFirewallRules, defaultPermit...)
continue
}
@@ -839,7 +846,7 @@ func (b *NetworkMapBuilder) getPeerRoutesFirewallRules(account *Account, peerID
for _, accessGroup := range route.AccessControlGroups {
policies := b.getAllRoutePoliciesFromGroups([]string{accessGroup})
rules := b.getRouteFirewallRules(peerID, policies, route, validatedPeersMap, distributionPeers, account)
rules := b.getRouteFirewallRules(peerID, policies, route, validatedPeersMap, distributionPeers, account, includeIPv6)
routesFirewallRules = append(routesFirewallRules, rules...)
}
}
@@ -887,7 +894,7 @@ func (b *NetworkMapBuilder) getAllRoutePoliciesFromGroups(accessControlGroups []
func (b *NetworkMapBuilder) getRouteFirewallRules(
peerID string, policies []*Policy, route *route.Route, validatedPeersMap map[string]struct{},
distributionPeers map[string]struct{}, account *Account,
distributionPeers map[string]struct{}, account *Account, includeIPv6 bool,
) []*RouteFirewallRule {
ctx := context.Background()
var fwRules []*RouteFirewallRule
@@ -903,7 +910,7 @@ func (b *NetworkMapBuilder) getRouteFirewallRules(
rulePeers := b.getRulePeers(rule, policy.SourcePostureChecks, peerID, distributionPeers, validatedPeersMap, account)
rules := generateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN)
rules := generateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN, includeIPv6)
fwRules = append(fwRules, rules...)
}
}
@@ -1100,14 +1107,17 @@ func (b *NetworkMapBuilder) assembleNetworkMap(
}
}
var routes []*route.Route
includeIPv6 := peer.SupportsIPv6() && peer.IPv6.IsValid()
var rawRoutes []*route.Route
allRouteIDs := slices.Concat(routesView.OwnRouteIDs, routesView.NetworkResourceIDs, routesView.InheritedRouteIDs)
for _, routeID := range allRouteIDs {
if route := b.cache.globalRoutes[routeID]; route != nil {
routes = append(routes, route)
rawRoutes = append(rawRoutes, route)
}
}
routes := filterAndExpandRoutes(rawRoutes, includeIPv6)
var firewallRules []*FirewallRule
for _, ruleID := range aclView.FirewallRuleIDs {
@@ -1654,12 +1664,20 @@ func (b *NetworkMapBuilder) calculateRouteFirewallUpdates(
) {
processedPeerRoutes := make(map[string]map[route.ID]struct{})
peerV6 := ""
if newPeer.IPv6.IsValid() {
peerV6 = newPeer.IPv6.String()
}
for routeID, info := range b.cache.noACGRoutes {
if info.PeerID == newPeerID {
continue
}
b.addRouteFirewallUpdate(updates, info.PeerID, string(routeID), newPeer.IP.String())
if peerV6 != "" {
b.addRouteFirewallUpdate(updates, info.PeerID, string(routeID), peerV6)
}
if processedPeerRoutes[info.PeerID] == nil {
processedPeerRoutes[info.PeerID] = make(map[route.ID]struct{})
@@ -1685,6 +1703,9 @@ func (b *NetworkMapBuilder) calculateRouteFirewallUpdates(
}
b.addRouteFirewallUpdate(updates, info.PeerID, string(routeID), newPeer.IP.String())
if peerV6 != "" {
b.addRouteFirewallUpdate(updates, info.PeerID, string(routeID), peerV6)
}
if processedPeerRoutes[info.PeerID] == nil {
processedPeerRoutes[info.PeerID] = make(map[route.ID]struct{})
@@ -1875,6 +1896,18 @@ func (b *NetworkMapBuilder) addUpdateForPeersInGroups(
Action: string(rule.Action),
Protocol: firewallRuleProtocol(rule.Protocol),
}
var v6fr *FirewallRule
if newPeer.IPv6.IsValid() {
v6fr = &FirewallRule{
PolicyID: rule.ID,
PeerIP: newPeer.IPv6.String(),
Direction: direction,
Action: string(rule.Action),
Protocol: firewallRuleProtocol(rule.Protocol),
}
}
for _, peerID := range peers {
if peerID == newPeerID {
continue
@@ -1893,6 +1926,14 @@ func (b *NetworkMapBuilder) addUpdateForPeersInGroups(
}
b.addOrUpdateFirewallRuleInDelta(updates, peerID, newPeerID, rule, direction, fr, peerIPForRule, targetPeer)
if v6fr != nil && targetPeer.SupportsIPv6() && targetPeer.IPv6.IsValid() {
v6PeerIP := v6fr.PeerIP
if all {
v6PeerIP = "::"
}
b.addOrUpdateFirewallRuleInDelta(updates, peerID, newPeerID, rule, direction, v6fr, v6PeerIP, targetPeer)
}
}
}
}
@@ -1928,6 +1969,17 @@ func (b *NetworkMapBuilder) addUpdateForDirectPeerResource(
}
b.addOrUpdateFirewallRuleInDelta(updates, targetPeerID, newPeerID, rule, direction, fr, fr.PeerIP, targetPeer)
if newPeer.IPv6.IsValid() && targetPeer.SupportsIPv6() && targetPeer.IPv6.IsValid() {
v6fr := &FirewallRule{
PolicyID: rule.ID,
PeerIP: newPeer.IPv6.String(),
Direction: direction,
Action: string(rule.Action),
Protocol: firewallRuleProtocol(rule.Protocol),
}
b.addOrUpdateFirewallRuleInDelta(updates, targetPeerID, newPeerID, rule, direction, v6fr, v6fr.PeerIP, targetPeer)
}
}
func (b *NetworkMapBuilder) addOrUpdateFirewallRuleInDelta(
@@ -2002,34 +2054,46 @@ func (b *NetworkMapBuilder) applyDeltaToPeer(account *Account, peerID string, de
func (b *NetworkMapBuilder) updateRouteFirewallRules(routesView *PeerRoutesView, updates []*RouteFirewallRuleUpdate) {
for _, update := range updates {
isV6Source := strings.Contains(update.AddSourceIP, ":")
for _, ruleID := range routesView.RouteFirewallRuleIDs {
rule := b.cache.globalRouteRules[ruleID]
if rule == nil {
continue
}
if string(rule.RouteID) == update.RuleID {
if hasWildcard := slices.Contains(rule.SourceRanges, allWildcard) || slices.Contains(rule.SourceRanges, v6AllWildcard); hasWildcard {
break
}
if string(rule.RouteID) != update.RuleID {
continue
}
sourceIP := update.AddSourceIP
// Dynamic routes share the same RouteID for v4 and v6 rules.
// Match the source IP family to the rule's destination family.
isV6Rule := strings.Contains(rule.Destination, ":")
if isV6Source != isV6Rule {
continue
}
if strings.Contains(sourceIP, ":") {
sourceIP += "/128" // IPv6
} else {
sourceIP += "/32" // IPv4
}
if !slices.Contains(rule.SourceRanges, sourceIP) {
rule.SourceRanges = append(rule.SourceRanges, sourceIP)
}
if slices.Contains(rule.SourceRanges, allWildcard) || slices.Contains(rule.SourceRanges, v6AllWildcard) {
break
}
sourceIP := update.AddSourceIP
if isV6Source {
sourceIP += "/128"
} else {
sourceIP += "/32"
}
if !slices.Contains(rule.SourceRanges, sourceIP) {
rule.SourceRanges = append(rule.SourceRanges, sourceIP)
}
break
}
}
}
func (b *NetworkMapBuilder) OnPeerDeleted(acc *Account, peerID string) error {
b.cache.mu.Lock()
defer b.cache.mu.Unlock()
@@ -0,0 +1,142 @@
package types
import (
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/route"
)
func newTestBuilder() *NetworkMapBuilder {
return &NetworkMapBuilder{
cache: &NetworkMapCache{
globalRouteRules: make(map[string]*RouteFirewallRule),
noACGRoutes: make(map[route.ID]*RouteOwnerInfo),
acgToRoutes: make(map[string]map[route.ID]*RouteOwnerInfo),
peerRoutes: make(map[string]*PeerRoutesView),
},
}
}
func TestUpdateRouteFirewallRules_FamilyMatching(t *testing.T) {
b := newTestBuilder()
// Simulate a dynamic route with both v4 and v6 rules sharing the same RouteID.
b.cache.globalRouteRules["rule-v4"] = &RouteFirewallRule{
RouteID: "route-dynamic",
SourceRanges: []string{"100.64.0.1/32"},
Destination: "0.0.0.0/0",
}
b.cache.globalRouteRules["rule-v6"] = &RouteFirewallRule{
RouteID: "route-dynamic",
SourceRanges: []string{"fd00::1/128"},
Destination: "::/0",
}
view := &PeerRoutesView{
RouteFirewallRuleIDs: []string{"rule-v4", "rule-v6"},
}
// Add a v4 source: should only go to the v4 rule.
b.updateRouteFirewallRules(view, []*RouteFirewallRuleUpdate{
{RuleID: "route-dynamic", AddSourceIP: "100.64.0.2"},
})
assert.Contains(t, b.cache.globalRouteRules["rule-v4"].SourceRanges, "100.64.0.2/32")
assert.NotContains(t, b.cache.globalRouteRules["rule-v6"].SourceRanges, "100.64.0.2/32",
"v4 source should not leak into v6 rule")
// Add a v6 source: should only go to the v6 rule.
b.updateRouteFirewallRules(view, []*RouteFirewallRuleUpdate{
{RuleID: "route-dynamic", AddSourceIP: "fd00::2"},
})
assert.Contains(t, b.cache.globalRouteRules["rule-v6"].SourceRanges, "fd00::2/128")
assert.NotContains(t, b.cache.globalRouteRules["rule-v4"].SourceRanges, "fd00::2/128",
"v6 source should not leak into v4 rule")
}
func TestUpdateRouteFirewallRules_WildcardSkip(t *testing.T) {
b := newTestBuilder()
b.cache.globalRouteRules["rule-wildcard"] = &RouteFirewallRule{
RouteID: "route-1",
SourceRanges: []string{"0.0.0.0/0"},
Destination: "10.0.0.0/8",
}
view := &PeerRoutesView{
RouteFirewallRuleIDs: []string{"rule-wildcard"},
}
b.updateRouteFirewallRules(view, []*RouteFirewallRuleUpdate{
{RuleID: "route-1", AddSourceIP: "100.64.0.5"},
})
assert.Equal(t, []string{"0.0.0.0/0"}, b.cache.globalRouteRules["rule-wildcard"].SourceRanges,
"wildcard rule should not get individual sources appended")
}
func TestCalculateRouteFirewallUpdates_DualStack(t *testing.T) {
b := newTestBuilder()
// Routing peer "router-1" owns a no-ACG route.
b.cache.noACGRoutes["route-exit"] = &RouteOwnerInfo{
PeerID: "router-1",
RouteID: "route-exit",
}
b.cache.peerRoutes["router-1"] = &PeerRoutesView{}
newPeer := &nbpeer.Peer{
ID: "new-peer",
IP: netip.MustParseAddr("100.64.0.5"),
IPv6: netip.MustParseAddr("fd00::5"),
}
updates := make(map[string]*PeerUpdateDelta)
b.calculateRouteFirewallUpdates("new-peer", newPeer, nil, updates)
require.Contains(t, updates, "router-1")
delta := updates["router-1"]
var v4Found, v6Found bool
for _, u := range delta.UpdateRouteFirewallRules {
if u.RuleID == "route-exit" && u.AddSourceIP == "100.64.0.5" {
v4Found = true
}
if u.RuleID == "route-exit" && u.AddSourceIP == "fd00::5" {
v6Found = true
}
}
assert.True(t, v4Found, "v4 source should be enqueued")
assert.True(t, v6Found, "v6 source should be enqueued")
}
func TestCalculateRouteFirewallUpdates_V4Only(t *testing.T) {
b := newTestBuilder()
b.cache.noACGRoutes["route-1"] = &RouteOwnerInfo{
PeerID: "router-1",
RouteID: "route-1",
}
b.cache.peerRoutes["router-1"] = &PeerRoutesView{}
// Peer without IPv6.
newPeer := &nbpeer.Peer{
ID: "new-peer",
IP: netip.MustParseAddr("100.64.0.5"),
}
updates := make(map[string]*PeerUpdateDelta)
b.calculateRouteFirewallUpdates("new-peer", newPeer, nil, updates)
require.Contains(t, updates, "router-1")
delta := updates["router-1"]
require.Len(t, delta.UpdateRouteFirewallRules, 1)
assert.Equal(t, "100.64.0.5", delta.UpdateRouteFirewallRules[0].AddSourceIP)
}
+10
View File
@@ -46,6 +46,8 @@ type Settings struct {
// NetworkRange is the custom network range for that account
NetworkRange netip.Prefix `gorm:"serializer:json"`
// NetworkRangeV6 is the custom IPv6 network range for that account
NetworkRangeV6 netip.Prefix `gorm:"serializer:json"`
// PeerExposeEnabled enables or disables peer-initiated service expose
PeerExposeEnabled bool
@@ -65,6 +67,12 @@ type Settings struct {
// when false, updates require user interaction from the UI
AutoUpdateAlways bool `gorm:"default:false"`
// IPv6EnabledGroups is the list of group IDs whose peers receive IPv6 overlay addresses.
// Peers not in any of these groups will not be allocated an IPv6 address.
// Empty list means IPv6 is disabled for the account.
// For new accounts this defaults to the All group.
IPv6EnabledGroups []string `gorm:"serializer:json"`
// EmbeddedIdpEnabled indicates if the embedded identity provider is enabled.
// This is a runtime-only field, not stored in the database.
EmbeddedIdpEnabled bool `gorm:"-"`
@@ -94,8 +102,10 @@ func (s *Settings) Copy() *Settings {
LazyConnectionEnabled: s.LazyConnectionEnabled,
DNSDomain: s.DNSDomain,
NetworkRange: s.NetworkRange,
NetworkRangeV6: s.NetworkRangeV6,
AutoUpdateVersion: s.AutoUpdateVersion,
AutoUpdateAlways: s.AutoUpdateAlways,
IPv6EnabledGroups: slices.Clone(s.IPv6EnabledGroups),
EmbeddedIdpEnabled: s.EmbeddedIdpEnabled,
LocalAuthDisabled: s.LocalAuthDisabled,
}