mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-23 23:29:08 +02:00
[management] Add IPv6 overlay addressing and capability gating (#5698)
This commit is contained in:
@@ -33,3 +33,4 @@ infrastructure_files/setup-*.env
|
||||
vendor/
|
||||
/netbird
|
||||
client/netbird-electron/
|
||||
management/server/types/testdata/
|
||||
|
||||
@@ -2,7 +2,7 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -56,7 +56,8 @@ func setupL4Test(t *testing.T, customPortsSupported *bool) (*Manager, store.Stor
|
||||
Key: "test-key",
|
||||
DNSLabel: "test-peer",
|
||||
Name: "test-peer",
|
||||
IP: net.ParseIP("100.64.0.1"),
|
||||
IP: netip.MustParseAddr("100.64.0.1"),
|
||||
IPv6: netip.MustParseAddr("fd00::1"),
|
||||
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now()},
|
||||
Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer"},
|
||||
},
|
||||
|
||||
@@ -1271,7 +1271,7 @@ func addPeerInfoToEventMeta(meta map[string]any, peer *nbpeer.Peer) map[string]a
|
||||
return meta
|
||||
}
|
||||
meta["peer_name"] = peer.Name
|
||||
if peer.IP != nil {
|
||||
if peer.IP.IsValid() {
|
||||
meta["peer_ip"] = peer.IP.String()
|
||||
}
|
||||
return meta
|
||||
|
||||
@@ -3,7 +3,7 @@ package manager
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -396,7 +396,8 @@ func TestDeletePeerService_SourcePeerValidation(t *testing.T) {
|
||||
testPeer := &nbpeer.Peer{
|
||||
ID: ownerPeerID,
|
||||
Name: "test-peer",
|
||||
IP: net.ParseIP("100.64.0.1"),
|
||||
IP: netip.MustParseAddr("100.64.0.1"),
|
||||
IPv6: netip.MustParseAddr("fd00::1"),
|
||||
}
|
||||
|
||||
newEphemeralService := func() *rpservice.Service {
|
||||
@@ -675,7 +676,8 @@ func setupIntegrationTest(t *testing.T) (*Manager, store.Store) {
|
||||
Key: "test-key",
|
||||
DNSLabel: "test-peer",
|
||||
Name: "test-peer",
|
||||
IP: net.ParseIP("100.64.0.1"),
|
||||
IP: netip.MustParseAddr("100.64.0.1"),
|
||||
IPv6: netip.MustParseAddr("fd00::1"),
|
||||
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now()},
|
||||
Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer"},
|
||||
},
|
||||
@@ -746,7 +748,8 @@ func Test_validateExposePermission(t *testing.T) {
|
||||
Key: "other-key",
|
||||
DNSLabel: "other-peer",
|
||||
Name: "other-peer",
|
||||
IP: net.ParseIP("100.64.0.2"),
|
||||
IP: netip.MustParseAddr("100.64.0.2"),
|
||||
IPv6: netip.MustParseAddr("fd00::2"),
|
||||
Status: &nbpeer.PeerStatus{LastSeen: time.Now()},
|
||||
Meta: nbpeer.PeerSystemMeta{Hostname: "other-peer"},
|
||||
})
|
||||
|
||||
@@ -3,12 +3,15 @@ package grpc
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
goproto "google.golang.org/protobuf/proto"
|
||||
|
||||
integrationsConfig "github.com/netbirdio/management-integrations/integrations/config"
|
||||
|
||||
"github.com/netbirdio/netbird/client/ssh/auth"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
@@ -17,8 +20,9 @@ import (
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/posture"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
nbroute "github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
"github.com/netbirdio/netbird/shared/netiputil"
|
||||
"github.com/netbirdio/netbird/shared/sshauth"
|
||||
)
|
||||
|
||||
@@ -100,7 +104,7 @@ func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, set
|
||||
sshConfig.JwtConfig = buildJWTConfig(httpConfig, deviceFlowConfig)
|
||||
}
|
||||
|
||||
return &proto.PeerConfig{
|
||||
peerConfig := &proto.PeerConfig{
|
||||
Address: fmt.Sprintf("%s/%d", peer.IP.String(), netmask),
|
||||
SshConfig: sshConfig,
|
||||
Fqdn: fqdn,
|
||||
@@ -111,9 +115,23 @@ func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, set
|
||||
AlwaysUpdate: settings.AutoUpdateAlways,
|
||||
},
|
||||
}
|
||||
|
||||
if peer.SupportsIPv6() && peer.IPv6.IsValid() && network.NetV6.IP != nil {
|
||||
ones, _ := network.NetV6.Mask.Size()
|
||||
v6Prefix := netip.PrefixFrom(peer.IPv6.Unmap(), ones)
|
||||
peerConfig.AddressV6 = netiputil.EncodePrefix(v6Prefix)
|
||||
}
|
||||
|
||||
return peerConfig
|
||||
}
|
||||
|
||||
func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *cache.DNSConfigCache, settings *types.Settings, extraSettings *types.ExtraSettings, peerGroups []string, dnsFwdPort int64) *proto.SyncResponse {
|
||||
// IPv6 data in AllowedIPs and SourcePrefixes wildcard expansion depends on
|
||||
// whether the target peer supports IPv6. Routes and firewall rules are already
|
||||
// filtered at the source (network map builder).
|
||||
includeIPv6 := peer.SupportsIPv6() && peer.IPv6.IsValid()
|
||||
useSourcePrefixes := peer.SupportsSourcePrefixes()
|
||||
|
||||
response := &proto.SyncResponse{
|
||||
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH),
|
||||
NetworkMap: &proto.NetworkMap{
|
||||
@@ -132,15 +150,15 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb
|
||||
response.NetworkMap.PeerConfig = response.PeerConfig
|
||||
|
||||
remotePeers := make([]*proto.RemotePeerConfig, 0, len(networkMap.Peers)+len(networkMap.OfflinePeers))
|
||||
remotePeers = appendRemotePeerConfig(remotePeers, networkMap.Peers, dnsName)
|
||||
remotePeers = appendRemotePeerConfig(remotePeers, networkMap.Peers, dnsName, includeIPv6)
|
||||
response.RemotePeers = remotePeers
|
||||
response.NetworkMap.RemotePeers = remotePeers
|
||||
response.RemotePeersIsEmpty = len(remotePeers) == 0
|
||||
response.NetworkMap.RemotePeersIsEmpty = response.RemotePeersIsEmpty
|
||||
|
||||
response.NetworkMap.OfflinePeers = appendRemotePeerConfig(nil, networkMap.OfflinePeers, dnsName)
|
||||
response.NetworkMap.OfflinePeers = appendRemotePeerConfig(nil, networkMap.OfflinePeers, dnsName, includeIPv6)
|
||||
|
||||
firewallRules := toProtocolFirewallRules(networkMap.FirewallRules)
|
||||
firewallRules := toProtocolFirewallRules(networkMap.FirewallRules, includeIPv6, useSourcePrefixes)
|
||||
response.NetworkMap.FirewallRules = firewallRules
|
||||
response.NetworkMap.FirewallRulesIsEmpty = len(firewallRules) == 0
|
||||
|
||||
@@ -195,11 +213,15 @@ func buildAuthorizedUsersProto(ctx context.Context, authorizedUsers map[string]m
|
||||
return hashedUsers, machineUsers
|
||||
}
|
||||
|
||||
func appendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nbpeer.Peer, dnsName string) []*proto.RemotePeerConfig {
|
||||
func appendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nbpeer.Peer, dnsName string, includeIPv6 bool) []*proto.RemotePeerConfig {
|
||||
for _, rPeer := range peers {
|
||||
allowedIPs := []string{rPeer.IP.String() + "/32"}
|
||||
if includeIPv6 && rPeer.IPv6.IsValid() {
|
||||
allowedIPs = append(allowedIPs, rPeer.IPv6.String()+"/128")
|
||||
}
|
||||
dst = append(dst, &proto.RemotePeerConfig{
|
||||
WgPubKey: rPeer.Key,
|
||||
AllowedIps: []string{rPeer.IP.String() + "/32"},
|
||||
AllowedIps: allowedIPs,
|
||||
SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
|
||||
Fqdn: rPeer.FQDN(dnsName),
|
||||
AgentVersion: rPeer.Meta.WtVersion,
|
||||
@@ -253,7 +275,7 @@ func ToResponseProto(configProto nbconfig.Protocol) proto.HostConfig_Protocol {
|
||||
}
|
||||
}
|
||||
|
||||
func toProtocolRoutes(routes []*route.Route) []*proto.Route {
|
||||
func toProtocolRoutes(routes []*nbroute.Route) []*proto.Route {
|
||||
protoRoutes := make([]*proto.Route, 0, len(routes))
|
||||
for _, r := range routes {
|
||||
protoRoutes = append(protoRoutes, toProtocolRoute(r))
|
||||
@@ -261,7 +283,7 @@ func toProtocolRoutes(routes []*route.Route) []*proto.Route {
|
||||
return protoRoutes
|
||||
}
|
||||
|
||||
func toProtocolRoute(route *route.Route) *proto.Route {
|
||||
func toProtocolRoute(route *nbroute.Route) *proto.Route {
|
||||
return &proto.Route{
|
||||
ID: string(route.ID),
|
||||
NetID: string(route.NetID),
|
||||
@@ -277,30 +299,70 @@ func toProtocolRoute(route *route.Route) *proto.Route {
|
||||
}
|
||||
|
||||
// toProtocolFirewallRules converts the firewall rules to the protocol firewall rules.
|
||||
func toProtocolFirewallRules(rules []*types.FirewallRule) []*proto.FirewallRule {
|
||||
result := make([]*proto.FirewallRule, len(rules))
|
||||
// When useSourcePrefixes is true, the compact SourcePrefixes field is populated
|
||||
// alongside the deprecated PeerIP for forward compatibility.
|
||||
// Wildcard rules ("0.0.0.0") are expanded into separate v4 and v6 SourcePrefixes
|
||||
// when includeIPv6 is true.
|
||||
func toProtocolFirewallRules(rules []*types.FirewallRule, includeIPv6, useSourcePrefixes bool) []*proto.FirewallRule {
|
||||
result := make([]*proto.FirewallRule, 0, len(rules))
|
||||
for i := range rules {
|
||||
rule := rules[i]
|
||||
|
||||
fwRule := &proto.FirewallRule{
|
||||
PolicyID: []byte(rule.PolicyID),
|
||||
PeerIP: rule.PeerIP, //nolint:staticcheck // populated for backward compatibility
|
||||
|
||||
Direction: getProtoDirection(rule.Direction),
|
||||
Action: getProtoAction(rule.Action),
|
||||
Protocol: getProtoProtocol(rule.Protocol),
|
||||
Port: rule.Port,
|
||||
}
|
||||
|
||||
if useSourcePrefixes && rule.PeerIP != "" {
|
||||
result = append(result, populateSourcePrefixes(fwRule, rule, includeIPv6)...)
|
||||
}
|
||||
|
||||
if shouldUsePortRange(fwRule) {
|
||||
fwRule.PortInfo = rule.PortRange.ToProto()
|
||||
}
|
||||
|
||||
result[i] = fwRule
|
||||
result = append(result, fwRule)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
// populateSourcePrefixes sets SourcePrefixes on fwRule and returns any
|
||||
// additional rules needed (e.g. a v6 wildcard clone when the peer IP is unspecified).
|
||||
func populateSourcePrefixes(fwRule *proto.FirewallRule, rule *types.FirewallRule, includeIPv6 bool) []*proto.FirewallRule {
|
||||
addr, err := netip.ParseAddr(rule.PeerIP)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !addr.IsUnspecified() {
|
||||
fwRule.SourcePrefixes = [][]byte{netiputil.EncodeAddr(addr.Unmap())}
|
||||
return nil
|
||||
}
|
||||
|
||||
fwRule.SourcePrefixes = [][]byte{
|
||||
netiputil.EncodePrefix(netip.PrefixFrom(netip.IPv4Unspecified(), 0)),
|
||||
}
|
||||
|
||||
if !includeIPv6 {
|
||||
return nil
|
||||
}
|
||||
|
||||
v6Rule := goproto.Clone(fwRule).(*proto.FirewallRule)
|
||||
v6Rule.PeerIP = "::" //nolint:staticcheck // populated for backward compatibility
|
||||
v6Rule.SourcePrefixes = [][]byte{
|
||||
netiputil.EncodePrefix(netip.PrefixFrom(netip.IPv6Unspecified(), 0)),
|
||||
}
|
||||
if shouldUsePortRange(v6Rule) {
|
||||
v6Rule.PortInfo = rule.PortRange.ToProto()
|
||||
}
|
||||
return []*proto.FirewallRule{v6Rule}
|
||||
}
|
||||
|
||||
// getProtoDirection converts the direction to proto.RuleDirection.
|
||||
func getProtoDirection(direction int) proto.RuleDirection {
|
||||
if direction == types.FirewallRuleDirectionOUT {
|
||||
|
||||
@@ -672,11 +672,21 @@ func extractPeerMeta(ctx context.Context, meta *proto.PeerSystemMeta) nbpeer.Pee
|
||||
BlockLANAccess: meta.GetFlags().GetBlockLANAccess(),
|
||||
BlockInbound: meta.GetFlags().GetBlockInbound(),
|
||||
LazyConnectionEnabled: meta.GetFlags().GetLazyConnectionEnabled(),
|
||||
DisableIPv6: meta.GetFlags().GetDisableIPv6(),
|
||||
},
|
||||
Files: files,
|
||||
Files: files,
|
||||
Capabilities: capabilitiesToInt32(meta.GetCapabilities()),
|
||||
}
|
||||
}
|
||||
|
||||
func capabilitiesToInt32(caps []proto.PeerCapability) []int32 {
|
||||
result := make([]int32, len(caps))
|
||||
for i, c := range caps {
|
||||
result[i] = int32(c)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Server) parseRequest(ctx context.Context, req *proto.EncryptedMessage, parsed pb.Message) (wgtypes.Key, error) {
|
||||
peerKey, err := wgtypes.ParseKey(req.GetWgPubKey())
|
||||
if err != nil {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -341,7 +341,11 @@ components:
|
||||
description: Allows to define a custom network range for the account in CIDR format
|
||||
type: string
|
||||
format: cidr
|
||||
example: 100.64.0.0/16
|
||||
network_range_v6:
|
||||
description: Allows to define a custom IPv6 network range for the account in CIDR format.
|
||||
type: string
|
||||
format: cidr
|
||||
example: fd00:1234:5678::/64
|
||||
peer_expose_enabled:
|
||||
description: Enables or disables peer expose. If enabled, peers can expose local services through the reverse proxy using the CLI.
|
||||
type: boolean
|
||||
@@ -377,6 +381,12 @@ components:
|
||||
type: boolean
|
||||
readOnly: true
|
||||
example: false
|
||||
ipv6_enabled_groups:
|
||||
description: List of group IDs whose peers receive IPv6 overlay addresses. Peers not in any of these groups will not be allocated an IPv6 address. New accounts default to the All group.
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: ["ch8i4ug6lnn4g9hqv7m0"]
|
||||
required:
|
||||
- peer_login_expiration_enabled
|
||||
- peer_login_expiration
|
||||
@@ -776,6 +786,11 @@ components:
|
||||
type: string
|
||||
format: ipv4
|
||||
example: 100.64.0.15
|
||||
ipv6:
|
||||
description: Peer's IPv6 overlay address. Omitted if IPv6 is not enabled for the account.
|
||||
type: string
|
||||
format: ipv6
|
||||
example: "fd00:4e42:ab12::1"
|
||||
required:
|
||||
- name
|
||||
- ssh_enabled
|
||||
@@ -795,6 +810,11 @@ components:
|
||||
description: Peer's IP address
|
||||
type: string
|
||||
example: 10.64.0.1
|
||||
ipv6:
|
||||
description: Peer's IPv6 overlay address
|
||||
type: string
|
||||
format: ipv6
|
||||
example: "fd00:4e42:ab12::1"
|
||||
connection_ip:
|
||||
description: Peer's public connection IP address
|
||||
type: string
|
||||
@@ -1013,6 +1033,10 @@ components:
|
||||
description: Peer's IP address
|
||||
type: string
|
||||
example: 10.64.0.1
|
||||
ipv6:
|
||||
description: Peer's IPv6 overlay address
|
||||
type: string
|
||||
example: "fd00:4e42:ab12::1"
|
||||
dns_label:
|
||||
description: Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud
|
||||
type: string
|
||||
|
||||
@@ -1351,6 +1351,9 @@ type AccessiblePeer struct {
|
||||
// Ip Peer's IP address
|
||||
Ip string `json:"ip"`
|
||||
|
||||
// Ipv6 Peer's IPv6 overlay address
|
||||
Ipv6 *string `json:"ipv6,omitempty"`
|
||||
|
||||
// LastSeen Last time peer connected to Netbird's management service
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
|
||||
@@ -1435,6 +1438,9 @@ type AccountSettings struct {
|
||||
// GroupsPropagationEnabled Allows propagate the new user auto groups to peers that belongs to the user
|
||||
GroupsPropagationEnabled *bool `json:"groups_propagation_enabled,omitempty"`
|
||||
|
||||
// Ipv6EnabledGroups List of group IDs whose peers receive IPv6 overlay addresses. Peers not in any of these groups will not be allocated an IPv6 address. New accounts default to the All group.
|
||||
Ipv6EnabledGroups *[]string `json:"ipv6_enabled_groups,omitempty"`
|
||||
|
||||
// JwtAllowGroups List of groups to which users are allowed access
|
||||
JwtAllowGroups *[]string `json:"jwt_allow_groups,omitempty"`
|
||||
|
||||
@@ -1453,6 +1459,9 @@ type AccountSettings struct {
|
||||
// NetworkRange Allows to define a custom network range for the account in CIDR format
|
||||
NetworkRange *string `json:"network_range,omitempty"`
|
||||
|
||||
// NetworkRangeV6 Allows to define a custom IPv6 network range for the account in CIDR format.
|
||||
NetworkRangeV6 *string `json:"network_range_v6,omitempty"`
|
||||
|
||||
// PeerExposeEnabled Enables or disables peer expose. If enabled, peers can expose local services through the reverse proxy using the CLI.
|
||||
PeerExposeEnabled bool `json:"peer_expose_enabled"`
|
||||
|
||||
@@ -3111,6 +3120,9 @@ type Peer struct {
|
||||
// Ip Peer's IP address
|
||||
Ip string `json:"ip"`
|
||||
|
||||
// Ipv6 Peer's IPv6 overlay address
|
||||
Ipv6 *string `json:"ipv6,omitempty"`
|
||||
|
||||
// KernelVersion Peer's operating system kernel version
|
||||
KernelVersion string `json:"kernel_version"`
|
||||
|
||||
@@ -3202,6 +3214,9 @@ type PeerBatch struct {
|
||||
// Ip Peer's IP address
|
||||
Ip string `json:"ip"`
|
||||
|
||||
// Ipv6 Peer's IPv6 overlay address
|
||||
Ipv6 *string `json:"ipv6,omitempty"`
|
||||
|
||||
// KernelVersion Peer's operating system kernel version
|
||||
KernelVersion string `json:"kernel_version"`
|
||||
|
||||
@@ -3301,7 +3316,10 @@ type PeerRequest struct {
|
||||
InactivityExpirationEnabled bool `json:"inactivity_expiration_enabled"`
|
||||
|
||||
// Ip Peer's IP address
|
||||
Ip *string `json:"ip,omitempty"`
|
||||
Ip *string `json:"ip,omitempty"`
|
||||
|
||||
// Ipv6 Peer's IPv6 overlay address. Omitted if IPv6 is not enabled for the account.
|
||||
Ipv6 *string `json:"ipv6,omitempty"`
|
||||
LoginExpirationEnabled bool `json:"login_expiration_enabled"`
|
||||
Name string `json:"name"`
|
||||
SshEnabled bool `json:"ssh_enabled"`
|
||||
|
||||
@@ -14,10 +14,15 @@ import (
|
||||
)
|
||||
|
||||
// EncodePrefix encodes a netip.Prefix into compact bytes.
|
||||
// The address is always unmapped before encoding.
|
||||
// The address is always unmapped before encoding. If unmapping produces a v4
|
||||
// address, the prefix length is clamped to 32.
|
||||
func EncodePrefix(p netip.Prefix) []byte {
|
||||
addr := p.Addr().Unmap()
|
||||
return append(addr.AsSlice(), byte(p.Bits()))
|
||||
bits := p.Bits()
|
||||
if addr.Is4() && bits > 32 {
|
||||
bits = 32
|
||||
}
|
||||
return append(addr.AsSlice(), byte(bits))
|
||||
}
|
||||
|
||||
// DecodePrefix decodes compact bytes into a netip.Prefix.
|
||||
|
||||
@@ -80,6 +80,26 @@ func TestEncodePrefixUnmaps(t *testing.T) {
|
||||
assert.Equal(t, netip.MustParsePrefix("10.1.2.3/32"), decoded)
|
||||
}
|
||||
|
||||
func TestEncodePrefixUnmapsClampsBits(t *testing.T) {
|
||||
// v4-mapped v6 with bits > 32 should clamp to /32
|
||||
mapped := netip.MustParsePrefix("::ffff:10.1.2.3/128")
|
||||
b := EncodePrefix(mapped)
|
||||
assert.Equal(t, 5, len(b), "v4-mapped should encode as 5 bytes")
|
||||
|
||||
decoded, err := DecodePrefix(b)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, netip.MustParsePrefix("10.1.2.3/32"), decoded)
|
||||
|
||||
// v4-mapped v6 with bits=96 should also clamp to /32
|
||||
mapped96 := netip.MustParsePrefix("::ffff:10.0.0.0/96")
|
||||
b96 := EncodePrefix(mapped96)
|
||||
assert.Equal(t, 5, len(b96))
|
||||
|
||||
decoded96, err := DecodePrefix(b96)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 32, decoded96.Bits())
|
||||
}
|
||||
|
||||
func TestDecodeAddr(t *testing.T) {
|
||||
v4 := netip.MustParseAddr("100.64.0.5")
|
||||
b := EncodeAddr(v4)
|
||||
|
||||
Reference in New Issue
Block a user