mirror of
https://github.com/netbirdio/netbird.git
synced 2026-06-24 08:49:55 +00:00
Compare commits
8 Commits
profile-bi
...
add-trace-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f24dc5fa7 | ||
|
|
ecd133ca70 | ||
|
|
3d4a70deeb | ||
|
|
330a03ce75 | ||
|
|
4b89f3be8a | ||
|
|
b4c1db17e4 | ||
|
|
58cd0eae4e | ||
|
|
49c8d571b2 |
@@ -1,294 +0,0 @@
|
||||
//go:build ios
|
||||
|
||||
package NetBirdSDK
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
)
|
||||
|
||||
// iOS profile storage layout (mirrors the Android layout so the shared
|
||||
// profilemanager.ServiceManager behaves identically on both platforms):
|
||||
//
|
||||
// <container>/ ← configDir parameter (App Group root)
|
||||
// ├── netbird.cfg ← default profile config
|
||||
// ├── state.json ← default profile state
|
||||
// ├── active_profile.json ← active profile tracker {"name": <id>, "username": "ios"}
|
||||
// └── profiles/ ← non-default profiles
|
||||
// ├── <id>.json ← profile config (holds the display "Name")
|
||||
// └── <id>.state.json ← profile state
|
||||
//
|
||||
// The ProfileLayoutMigration in NetbirdKit moves the legacy directory-per-name
|
||||
// layout into this shape before NewProfileManager ever runs.
|
||||
|
||||
const (
|
||||
// iosDefaultConfigFilename is the default profile config name. Must match
|
||||
// GlobalConstants.configFileName on the Swift side ("netbird.cfg").
|
||||
iosDefaultConfigFilename = "netbird.cfg"
|
||||
// iosDefaultStateFilename is the default profile state name. Must match
|
||||
// GlobalConstants.stateFileName on the Swift side ("state.json").
|
||||
iosDefaultStateFilename = "state.json"
|
||||
// iosProfilesSubdir holds non-default profile files.
|
||||
iosProfilesSubdir = "profiles"
|
||||
// iosUsername is the single user context the app runs under. The value is
|
||||
// written into active_profile.json's "username" field and is required to be
|
||||
// non-empty for non-default profiles by ServiceManager.SetActiveProfileState.
|
||||
// Must match the value the migration writes ("ios").
|
||||
iosUsername = "ios"
|
||||
)
|
||||
|
||||
// Profile represents a profile for gomobile. gomobile exposes the exported
|
||||
// fields as id_/name/isActive on the Swift side.
|
||||
type Profile struct {
|
||||
ID string
|
||||
Name string
|
||||
IsActive bool
|
||||
}
|
||||
|
||||
// ProfileArray wraps a profile slice for gomobile (which cannot bind Go slices
|
||||
// directly; callers iterate with Length()/Get()).
|
||||
type ProfileArray struct {
|
||||
items []*Profile
|
||||
}
|
||||
|
||||
// Length returns the number of profiles.
|
||||
func (p *ProfileArray) Length() int {
|
||||
return len(p.items)
|
||||
}
|
||||
|
||||
// Get returns the profile at index i, or nil if i is out of range.
|
||||
func (p *ProfileArray) Get(i int) *Profile {
|
||||
if i < 0 || i >= len(p.items) {
|
||||
return nil
|
||||
}
|
||||
return p.items[i]
|
||||
}
|
||||
|
||||
// ProfileManager manages profiles for iOS. It wraps the internal
|
||||
// profilemanager.ServiceManager, which owns all profile identity (the on-disk
|
||||
// filename is the ID, the display name lives inside the config JSON).
|
||||
type ProfileManager struct {
|
||||
configDir string
|
||||
serviceMgr *profilemanager.ServiceManager
|
||||
}
|
||||
|
||||
// NewProfileManager creates a profile manager rooted at configDir (the App
|
||||
// Group shared container). gomobile maps this to a nullable Swift initializer.
|
||||
func NewProfileManager(configDir string) *ProfileManager {
|
||||
defaultConfigPath := filepath.Join(configDir, iosDefaultConfigFilename)
|
||||
|
||||
// Point the package-level paths at the iOS container. The default profile
|
||||
// lives in the root configDir (not under profiles/).
|
||||
profilemanager.DefaultConfigPathDir = configDir
|
||||
profilemanager.DefaultConfigPath = defaultConfigPath
|
||||
profilemanager.ActiveProfileStatePath = filepath.Join(configDir, "active_profile.json")
|
||||
|
||||
// A fixed profiles directory avoids mutating the global ConfigDirOverride;
|
||||
// the ServiceManager then ignores the username when resolving the directory.
|
||||
profilesDir := filepath.Join(configDir, iosProfilesSubdir)
|
||||
serviceMgr := profilemanager.NewServiceManagerWithProfilesDir(defaultConfigPath, profilesDir)
|
||||
|
||||
return &ProfileManager{
|
||||
configDir: configDir,
|
||||
serviceMgr: serviceMgr,
|
||||
}
|
||||
}
|
||||
|
||||
// ListProfiles returns all available profiles, including the default, with
|
||||
// their active status and resolved display names.
|
||||
func (pm *ProfileManager) ListProfiles() (*ProfileArray, error) {
|
||||
internalProfiles, err := pm.serviceMgr.ListProfiles(iosUsername)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list profiles: %w", err)
|
||||
}
|
||||
|
||||
var profiles []*Profile
|
||||
for _, p := range internalProfiles {
|
||||
profiles = append(profiles, &Profile{
|
||||
ID: p.ID.String(),
|
||||
Name: p.Name,
|
||||
IsActive: p.IsActive,
|
||||
})
|
||||
}
|
||||
|
||||
return &ProfileArray{items: profiles}, nil
|
||||
}
|
||||
|
||||
// GetActiveProfile returns the currently active profile with its display name
|
||||
// resolved. ActiveProfileState only records the ID, so the ID is resolved to a
|
||||
// full profile to recover the Name.
|
||||
func (pm *ProfileManager) GetActiveProfile() (*Profile, error) {
|
||||
activeState, err := pm.serviceMgr.GetActiveProfileState()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get active profile: %w", err)
|
||||
}
|
||||
|
||||
prof, err := pm.serviceMgr.ResolveProfile(activeState.ID.String(), iosUsername)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve active profile %q: %w", activeState.ID, err)
|
||||
}
|
||||
|
||||
return &Profile{ID: prof.ID.String(), Name: prof.Name, IsActive: true}, nil
|
||||
}
|
||||
|
||||
// AddProfile creates a new profile with displayName and returns it. The
|
||||
// returned profile carries the freshly generated ID, which callers must use
|
||||
// for all follow-up operations (the ID is NOT the display name).
|
||||
func (pm *ProfileManager) AddProfile(displayName string) (*Profile, error) {
|
||||
prof, err := pm.serviceMgr.AddProfile(displayName, iosUsername)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add profile: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("created new profile: %s", prof.ID)
|
||||
return &Profile{ID: prof.ID.String(), Name: prof.Name, IsActive: false}, nil
|
||||
}
|
||||
|
||||
// SwitchProfile records the given profile ID as the active profile. Callers
|
||||
// must stop the VPN before switching.
|
||||
func (pm *ProfileManager) SwitchProfile(id string) error {
|
||||
if err := pm.serviceMgr.SetActiveProfileState(&profilemanager.ActiveProfileState{
|
||||
ID: profilemanager.ID(id),
|
||||
Username: iosUsername,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to switch profile: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("switched to profile: %s", id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenameProfile changes a profile's display name. The on-disk ID (filename) is
|
||||
// unchanged. There is no ServiceManager rename, so this edits the Name field of
|
||||
// the config JSON in place.
|
||||
func (pm *ProfileManager) RenameProfile(id, newName string) error {
|
||||
if id == profilemanager.DefaultProfileName {
|
||||
return fmt.Errorf("cannot rename the default profile")
|
||||
}
|
||||
if !profilemanager.IsValidProfileFilenameStem(profilemanager.ID(id)) {
|
||||
return fmt.Errorf("invalid profile ID: %q", id)
|
||||
}
|
||||
|
||||
newName = strings.TrimSpace(newName)
|
||||
if newName == "" {
|
||||
return fmt.Errorf("profile name must not be empty")
|
||||
}
|
||||
if newName == profilemanager.DefaultProfileName {
|
||||
return fmt.Errorf("cannot use reserved profile name: %s", profilemanager.DefaultProfileName)
|
||||
}
|
||||
|
||||
configPath, err := pm.getProfileConfigPath(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("profile %q does not exist", id)
|
||||
}
|
||||
|
||||
config, err := profilemanager.ReadConfig(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read profile config: %w", err)
|
||||
}
|
||||
|
||||
config.Name = newName
|
||||
|
||||
if err := profilemanager.WriteOutConfig(configPath, config); err != nil {
|
||||
return fmt.Errorf("failed to write profile config: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("renamed profile %q to %q", id, newName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveProfile deletes a profile. The default and the active profile cannot be
|
||||
// removed.
|
||||
func (pm *ProfileManager) RemoveProfile(id string) error {
|
||||
if err := pm.serviceMgr.RemoveProfile(profilemanager.ID(id), iosUsername); err != nil {
|
||||
return fmt.Errorf("failed to remove profile: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("removed profile: %s", id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogoutProfile clears a profile's authentication (private key and SSH key),
|
||||
// forcing re-login. The management URL is preserved in the config.
|
||||
func (pm *ProfileManager) LogoutProfile(id string) error {
|
||||
if !profilemanager.IsValidProfileFilenameStem(profilemanager.ID(id)) {
|
||||
return fmt.Errorf("invalid profile ID: %q", id)
|
||||
}
|
||||
|
||||
configPath, err := pm.getProfileConfigPath(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("profile %q does not exist", id)
|
||||
}
|
||||
|
||||
config, err := profilemanager.ReadConfig(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read profile config: %w", err)
|
||||
}
|
||||
|
||||
config.PrivateKey = ""
|
||||
config.SSHKey = ""
|
||||
|
||||
if err := profilemanager.WriteOutConfig(configPath, config); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("logged out from profile: %s", id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfigPath returns the config file path for a given profile ID.
|
||||
func (pm *ProfileManager) GetConfigPath(id string) (string, error) {
|
||||
return pm.getProfileConfigPath(id)
|
||||
}
|
||||
|
||||
// GetStateFilePath returns the state file path for a given profile ID.
|
||||
func (pm *ProfileManager) GetStateFilePath(id string) (string, error) {
|
||||
if id == "" || id == profilemanager.DefaultProfileName {
|
||||
return filepath.Join(pm.configDir, iosDefaultStateFilename), nil
|
||||
}
|
||||
|
||||
profilesDir := filepath.Join(pm.configDir, iosProfilesSubdir)
|
||||
return filepath.Join(profilesDir, id+".state.json"), nil
|
||||
}
|
||||
|
||||
// GetActiveConfigPath returns the config file path for the active profile.
|
||||
func (pm *ProfileManager) GetActiveConfigPath() (string, error) {
|
||||
activeProfile, err := pm.GetActiveProfile()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get active profile: %w", err)
|
||||
}
|
||||
return pm.GetConfigPath(activeProfile.ID)
|
||||
}
|
||||
|
||||
// GetActiveStateFilePath returns the state file path for the active profile.
|
||||
func (pm *ProfileManager) GetActiveStateFilePath() (string, error) {
|
||||
activeProfile, err := pm.GetActiveProfile()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get active profile: %w", err)
|
||||
}
|
||||
return pm.GetStateFilePath(activeProfile.ID)
|
||||
}
|
||||
|
||||
// getProfileConfigPath returns the config file path for a profile ID. The
|
||||
// default profile lives in the root configDir as netbird.cfg; everything else
|
||||
// lives under profiles/ as <id>.json.
|
||||
func (pm *ProfileManager) getProfileConfigPath(id string) (string, error) {
|
||||
if id == "" || id == profilemanager.DefaultProfileName {
|
||||
return filepath.Join(pm.configDir, iosDefaultConfigFilename), nil
|
||||
}
|
||||
|
||||
profilesDir := filepath.Join(pm.configDir, iosProfilesSubdir)
|
||||
return filepath.Join(profilesDir, id+".json"), nil
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
@@ -177,6 +178,10 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
|
||||
return fmt.Errorf("failed to get account zones: %v", err)
|
||||
}
|
||||
|
||||
if reason.Operation == types.UpdateOperationUpdate && reason.Resource == types.UpdateResourceUser {
|
||||
log.WithContext(ctx).Tracef("got an user update, stack: %s", debug.Stack())
|
||||
}
|
||||
|
||||
for _, peer := range account.Peers {
|
||||
if !c.peersUpdateManager.HasChannel(peer.ID) {
|
||||
log.WithContext(ctx).Tracef("peer %s doesn't have a channel, skipping network map update", peer.ID)
|
||||
@@ -244,6 +249,7 @@ func (c *Controller) UpdateAccountPeers(ctx context.Context, accountID string, r
|
||||
|
||||
// UpdateAffectedPeers updates only the specified peers that belong to an account.
|
||||
func (c *Controller) UpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string) error {
|
||||
log.WithContext(ctx).Tracef("UpdateAccountPeers: account %s, %d affected peers (caller: %s)", accountID, len(peerIDs), util.GetCallerName())
|
||||
if len(peerIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -251,7 +257,7 @@ func (c *Controller) UpdateAffectedPeers(ctx context.Context, accountID string,
|
||||
}
|
||||
|
||||
func (c *Controller) sendUpdateForAffectedPeers(ctx context.Context, accountID string, peerIDs []string) error {
|
||||
log.WithContext(ctx).Tracef("sendUpdateForAffectedPeers: account %s, %d affected peers: %v (caller: %s)", accountID, len(peerIDs), peerIDs, util.GetCallerName())
|
||||
log.WithContext(ctx).Tracef("sendUpdateForAffectedPeers: account %s, %d affected peers (caller: %s)", accountID, len(peerIDs), util.GetCallerName())
|
||||
|
||||
if !c.hasConnectedPeers(peerIDs) {
|
||||
log.WithContext(ctx).Tracef("sendUpdateForAffectedPeers: no connected peers among %v, skipping", peerIDs)
|
||||
@@ -497,7 +503,11 @@ func (c *Controller) BufferUpdateAffectedPeers(ctx context.Context, accountID st
|
||||
c.accountManagerMetrics.CountUpdateAccountPeersTriggered(string(reason.Resource), string(reason.Operation))
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Tracef("buffer updating %d affected peers for account %s from %s", len(peerIDs), accountID, util.GetCallerName())
|
||||
log.WithContext(ctx).Tracef("buffer updating %d affected peers for account %s from %s with reason %s/%s", len(peerIDs), accountID, util.GetCallerName(), reason.Operation, reason.Resource)
|
||||
|
||||
if reason.Operation == types.UpdateOperationUpdate && reason.Resource == types.UpdateResourceUser {
|
||||
log.WithContext(ctx).Tracef("got an user update, stack: %s", debug.Stack())
|
||||
}
|
||||
|
||||
bufUpd, _ := c.affectedPeerUpdateLocks.LoadOrStore(accountID, &bufferAffectedUpdate{
|
||||
peerIDs: make(map[string]struct{}),
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/util"
|
||||
"github.com/rs/xid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/maps"
|
||||
@@ -200,14 +201,14 @@ func (am *DefaultAccountManager) resolvePeerLocation(ctx context.Context, peer *
|
||||
if am.geo == nil || realIP == nil {
|
||||
return nil
|
||||
}
|
||||
if peer.Location.ConnectionIP != nil && peer.Location.ConnectionIP.Equal(realIP) {
|
||||
return nil
|
||||
}
|
||||
location, err := am.geo.Lookup(realIP)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Warnf("failed to get location for peer %s realip: [%s]: %v", peer.ID, realIP.String(), err)
|
||||
return nil
|
||||
}
|
||||
if peer.Location.ConnectionIP != nil && peer.Location.ConnectionIP.Equal(realIP) && peer.Location.GeoNameID == location.City.GeonameID {
|
||||
return nil
|
||||
}
|
||||
return &nbpeer.Location{
|
||||
ConnectionIP: realIP,
|
||||
CountryCode: location.Country.ISOCode,
|
||||
@@ -1042,8 +1043,8 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSy
|
||||
return nil, nil, nil, 0, err
|
||||
}
|
||||
|
||||
metaDiffAffectsPosture := posture.AffectsPosture(&metaDiff, resPostureChecks)
|
||||
if isStatusChanged || sync.UpdateAccountPeers || ipv6CapabilityChanged || metaDiffAffectsPosture || metaDiff.VersionChanged || metaDiff.Hostname {
|
||||
metaDiffAffectsPosture := posture.AffectsPosture(ctx, &metaDiff, resPostureChecks)
|
||||
if requiresPeerUpdate(ctx, isStatusChanged, sync.UpdateAccountPeers, ipv6CapabilityChanged, metaDiffAffectsPosture, metaDiff.VersionChanged, metaDiff.Hostname) {
|
||||
changedPeerIDs := []string{peer.ID}
|
||||
affectedPeerIDs := am.syncPeerAffectedPeers(ctx, accountID, peer.ID, nmap, peerNotValid, metaDiffAffectsPosture)
|
||||
if err = am.networkMapController.OnPeersUpdated(ctx, accountID, changedPeerIDs, affectedPeerIDs); err != nil {
|
||||
@@ -1054,6 +1055,29 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSy
|
||||
return peer, nmap, resPostureChecks, dnsFwdPort, nil
|
||||
}
|
||||
|
||||
func requiresPeerUpdate(ctx context.Context, isStatusChanged, updateAccountPeers, ipv6CapabilityChanged, metaDiffAffectsPosture, versionChanged, hostname bool) bool {
|
||||
reason := ""
|
||||
switch {
|
||||
case isStatusChanged:
|
||||
reason = "status changed"
|
||||
case updateAccountPeers:
|
||||
reason = "update account peers"
|
||||
case ipv6CapabilityChanged:
|
||||
reason = "ipv6 capability changed"
|
||||
case metaDiffAffectsPosture:
|
||||
reason = "meta diff affects posture"
|
||||
case versionChanged:
|
||||
reason = "version changed"
|
||||
case hostname:
|
||||
reason = "hostname changed"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Tracef("peer update required: %s", reason)
|
||||
return true
|
||||
}
|
||||
|
||||
// syncPeerAffectedPeers resolves the peers affected by a SyncPeer change. The
|
||||
// peer's own validated network map is bidirectional for policy and routing
|
||||
// reachability, so when the peer stays valid and no source-posture gate is in
|
||||
@@ -1477,6 +1501,7 @@ func (am *DefaultAccountManager) GetPeer(ctx context.Context, accountID, peerID,
|
||||
// UpdateAccountPeers updates all peers that belong to an account.
|
||||
// Should be called when changes have to be synced to peers.
|
||||
func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) {
|
||||
log.WithContext(ctx).Tracef("update account peers for account %s from caller: %s with reason %s/%s", accountID, util.GetCallerName(), reason.Operation, reason.Resource)
|
||||
_ = am.networkMapController.UpdateAccountPeers(ctx, accountID, reason)
|
||||
}
|
||||
|
||||
@@ -1575,6 +1600,7 @@ func (am *DefaultAccountManager) resolveAffectedPeersForPeerChanges(ctx context.
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) BufferUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) {
|
||||
log.WithContext(ctx).Tracef("buffering update account peers for account %s from caller: %s with reason %s/%s", accountID, util.GetCallerName(), reason.Operation, reason.Resource)
|
||||
_ = am.networkMapController.BufferUpdateAccountPeers(ctx, accountID, reason)
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import (
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/management/server/geolocation"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/posture"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
@@ -2893,3 +2894,141 @@ func TestUpdatePeer_DnsLabelUniqueName(t *testing.T) {
|
||||
require.NoError(t, err, "renaming to unique FQDN should succeed")
|
||||
assert.Equal(t, "api-server", updated.DNSLabel, "DNS label should be first label of FQDN")
|
||||
}
|
||||
|
||||
// fakeGeo is a configurable geolocation.Geolocation implementation for tests. It
|
||||
// returns a record built from the configured city geoname id, or an error when set.
|
||||
type fakeGeo struct {
|
||||
geoNameID uint
|
||||
isoCode string
|
||||
cityName string
|
||||
err error
|
||||
}
|
||||
|
||||
func (g *fakeGeo) Lookup(net.IP) (*geolocation.Record, error) {
|
||||
if g.err != nil {
|
||||
return nil, g.err
|
||||
}
|
||||
record := &geolocation.Record{}
|
||||
record.City.GeonameID = g.geoNameID
|
||||
record.City.Names.En = g.cityName
|
||||
record.Country.ISOCode = g.isoCode
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (g *fakeGeo) GetAllCountries() ([]geolocation.Country, error) { return nil, nil }
|
||||
|
||||
func (g *fakeGeo) GetCitiesByCountry(string) ([]geolocation.City, error) { return nil, nil }
|
||||
|
||||
func (g *fakeGeo) Stop() error { return nil }
|
||||
|
||||
func TestResolvePeerLocation(t *testing.T) {
|
||||
realIP := net.ParseIP("203.0.113.10")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
geo geolocation.Geolocation
|
||||
peer *nbpeer.Peer
|
||||
realIP net.IP
|
||||
want *nbpeer.Location
|
||||
wantNil bool
|
||||
}{
|
||||
{
|
||||
name: "no geo configured returns nil",
|
||||
geo: nil,
|
||||
peer: &nbpeer.Peer{ID: "p1"},
|
||||
realIP: realIP,
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "nil real IP returns nil",
|
||||
geo: &fakeGeo{geoNameID: 100},
|
||||
peer: &nbpeer.Peer{ID: "p1"},
|
||||
realIP: nil,
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "lookup error returns nil",
|
||||
geo: &fakeGeo{err: fmt.Errorf("lookup boom")},
|
||||
peer: &nbpeer.Peer{ID: "p1"},
|
||||
realIP: realIP,
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "same IP and same geoname returns nil",
|
||||
geo: &fakeGeo{geoNameID: 100, isoCode: "US", cityName: "City A"},
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "p1",
|
||||
Location: nbpeer.Location{
|
||||
ConnectionIP: realIP,
|
||||
GeoNameID: 100,
|
||||
},
|
||||
},
|
||||
realIP: realIP,
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "same IP but changed geoname returns location",
|
||||
geo: &fakeGeo{geoNameID: 200, isoCode: "US", cityName: "City B"},
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "p1",
|
||||
Location: nbpeer.Location{
|
||||
ConnectionIP: realIP,
|
||||
GeoNameID: 100,
|
||||
},
|
||||
},
|
||||
realIP: realIP,
|
||||
want: &nbpeer.Location{
|
||||
ConnectionIP: realIP,
|
||||
CountryCode: "US",
|
||||
CityName: "City B",
|
||||
GeoNameID: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "different IP returns location",
|
||||
geo: &fakeGeo{geoNameID: 100, isoCode: "US", cityName: "City A"},
|
||||
peer: &nbpeer.Peer{
|
||||
ID: "p1",
|
||||
Location: nbpeer.Location{
|
||||
ConnectionIP: net.ParseIP("198.51.100.7"),
|
||||
GeoNameID: 100,
|
||||
},
|
||||
},
|
||||
realIP: realIP,
|
||||
want: &nbpeer.Location{
|
||||
ConnectionIP: realIP,
|
||||
CountryCode: "US",
|
||||
CityName: "City A",
|
||||
GeoNameID: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no prior location returns location",
|
||||
geo: &fakeGeo{geoNameID: 100, isoCode: "US", cityName: "City A"},
|
||||
peer: &nbpeer.Peer{ID: "p1"},
|
||||
realIP: realIP,
|
||||
want: &nbpeer.Location{
|
||||
ConnectionIP: realIP,
|
||||
CountryCode: "US",
|
||||
CityName: "City A",
|
||||
GeoNameID: 100,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
am := &DefaultAccountManager{geo: tt.geo}
|
||||
got := am.resolvePeerLocation(context.Background(), tt.peer, tt.realIP)
|
||||
if tt.wantNil {
|
||||
assert.Nil(t, got, "resolved location should be nil")
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got, "resolved location should not be nil")
|
||||
assert.True(t, tt.want.ConnectionIP.Equal(got.ConnectionIP), "connection IP should match")
|
||||
assert.Equal(t, tt.want.CountryCode, got.CountryCode, "country code should match")
|
||||
assert.Equal(t, tt.want.CityName, got.CityName, "city name should match")
|
||||
assert.Equal(t, tt.want.GeoNameID, got.GeoNameID, "geoname id should match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
130
management/server/posture/affects_test.go
Normal file
130
management/server/posture/affects_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package posture
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
)
|
||||
|
||||
func TestAffectsPosture(t *testing.T) {
|
||||
processCheck := &Checks{Checks: ChecksDefinition{ProcessCheck: &ProcessCheck{}}}
|
||||
osCheck := &Checks{Checks: ChecksDefinition{OSVersionCheck: &OSVersionCheck{}}}
|
||||
nbCheck := &Checks{Checks: ChecksDefinition{NBVersionCheck: &NBVersionCheck{}}}
|
||||
geoCheck := &Checks{Checks: ChecksDefinition{GeoLocationCheck: &GeoLocationCheck{}}}
|
||||
|
||||
privateRangeCheck := &Checks{Checks: ChecksDefinition{
|
||||
PeerNetworkRangeCheck: &PeerNetworkRangeCheck{
|
||||
Ranges: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")},
|
||||
},
|
||||
}}
|
||||
publicRangeCheck := &Checks{Checks: ChecksDefinition{
|
||||
PeerNetworkRangeCheck: &PeerNetworkRangeCheck{
|
||||
Ranges: []netip.Prefix{netip.MustParsePrefix("203.0.113.0/24")},
|
||||
},
|
||||
}}
|
||||
mixedRangeCheck := &Checks{Checks: ChecksDefinition{
|
||||
PeerNetworkRangeCheck: &PeerNetworkRangeCheck{
|
||||
Ranges: []netip.Prefix{
|
||||
netip.MustParsePrefix("203.0.113.0/24"),
|
||||
netip.MustParsePrefix("192.168.0.0/16"),
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
diff *nbpeer.MetaDiff
|
||||
checks []*Checks
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil diff never affects posture",
|
||||
diff: nil,
|
||||
checks: []*Checks{processCheck},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "process check affected by files change",
|
||||
diff: &nbpeer.MetaDiff{Files: true},
|
||||
checks: []*Checks{processCheck},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "process check ignores unrelated change",
|
||||
diff: &nbpeer.MetaDiff{Hostname: true},
|
||||
checks: []*Checks{processCheck},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "os check affected by os version change",
|
||||
diff: &nbpeer.MetaDiff{OSVersion: true},
|
||||
checks: []*Checks{osCheck},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "nb check affected by wt version change",
|
||||
diff: &nbpeer.MetaDiff{WtVersion: true},
|
||||
checks: []*Checks{nbCheck},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "geo check affected by location change",
|
||||
diff: &nbpeer.MetaDiff{LocationChanged: true},
|
||||
checks: []*Checks{geoCheck},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "network range check not affected without network address or location change",
|
||||
diff: &nbpeer.MetaDiff{Hostname: true},
|
||||
checks: []*Checks{privateRangeCheck},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "private range check affected by network address change",
|
||||
diff: &nbpeer.MetaDiff{NetworkAddresses: true},
|
||||
checks: []*Checks{privateRangeCheck},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "public range check not affected by network address change alone",
|
||||
diff: &nbpeer.MetaDiff{NetworkAddresses: true},
|
||||
checks: []*Checks{publicRangeCheck},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "public range check affected by location change alone",
|
||||
diff: &nbpeer.MetaDiff{LocationChanged: true},
|
||||
checks: []*Checks{publicRangeCheck},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "private range check affected by location change alone",
|
||||
diff: &nbpeer.MetaDiff{LocationChanged: true},
|
||||
checks: []*Checks{privateRangeCheck},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "public range check affected when location also changed",
|
||||
diff: &nbpeer.MetaDiff{NetworkAddresses: true, LocationChanged: true},
|
||||
checks: []*Checks{publicRangeCheck},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "mixed ranges affected by network address change due to private range",
|
||||
diff: &nbpeer.MetaDiff{NetworkAddresses: true},
|
||||
checks: []*Checks{mixedRangeCheck},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := AffectsPosture(context.Background(), tt.diff, tt.checks)
|
||||
assert.Equal(t, tt.want, got, "AffectsPosture result should match expectation")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"regexp"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
@@ -56,25 +57,38 @@ type Checks struct {
|
||||
// alter the outcome of any of the given posture checks. It maps each check kind to
|
||||
// the metadata fields it inspects, so an unrelated change (e.g. a hostname update)
|
||||
// does not force a posture re-evaluation.
|
||||
func AffectsPosture(diff *nbpeer.MetaDiff, checks []*Checks) bool {
|
||||
func AffectsPosture(ctx context.Context, diff *nbpeer.MetaDiff, checks []*Checks) bool {
|
||||
if diff == nil {
|
||||
return false
|
||||
}
|
||||
for _, c := range checks {
|
||||
if c.Checks.ProcessCheck != nil && diff.Files {
|
||||
log.WithContext(ctx).Tracef("posture check %s is affected by files change", c.Name)
|
||||
return true
|
||||
}
|
||||
if c.Checks.OSVersionCheck != nil && (diff.OSVersion || diff.OS || diff.KernelVersion) {
|
||||
log.WithContext(ctx).Tracef("posture check %s is affected by OS version check", c.Name)
|
||||
return true
|
||||
}
|
||||
if c.Checks.NBVersionCheck != nil && diff.WtVersion {
|
||||
log.WithContext(ctx).Tracef("posture check %s is affected by NB version change", c.Name)
|
||||
return true
|
||||
}
|
||||
if c.Checks.GeoLocationCheck != nil && diff.LocationChanged {
|
||||
log.WithContext(ctx).Tracef("posture check %s is affected by location change", c.Name)
|
||||
return true
|
||||
}
|
||||
if c.Checks.PeerNetworkRangeCheck != nil && diff.NetworkAddresses {
|
||||
return true
|
||||
if c.Checks.PeerNetworkRangeCheck != nil && (diff.NetworkAddresses || diff.LocationChanged) {
|
||||
if diff.LocationChanged {
|
||||
log.WithContext(ctx).Tracef("posture check %s is affected by location change", c.Name)
|
||||
return true
|
||||
}
|
||||
for _, r := range c.Checks.PeerNetworkRangeCheck.Ranges {
|
||||
if r.Addr().IsPrivate() {
|
||||
log.WithContext(ctx).Tracef("posture check %s is affected by network address change", c.Name)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user