mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-24 07:39:07 +02: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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()},
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 }(),
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user