mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-26 20:26:39 +00:00
[management] Add IPv6 overlay addressing and capability gating (#5698)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user