mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-23 23:29:08 +02:00
Merge branch 'main' into feature/policy-by-public-id
This commit is contained in:
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -52,6 +51,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/util"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
nbdomain "github.com/netbirdio/netbird/shared/management/domain"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
)
|
||||
|
||||
@@ -64,7 +64,7 @@ const (
|
||||
type userLoggedInOnce bool
|
||||
|
||||
func cacheEntryExpiration() time.Duration {
|
||||
r := rand.Intn(int(nbcache.DefaultIDPCacheExpirationMax.Milliseconds()-nbcache.DefaultIDPCacheExpirationMin.Milliseconds())) + int(nbcache.DefaultIDPCacheExpirationMin.Milliseconds())
|
||||
r := util.RandIntn(int(nbcache.DefaultIDPCacheExpirationMax.Milliseconds()-nbcache.DefaultIDPCacheExpirationMin.Milliseconds())) + int(nbcache.DefaultIDPCacheExpirationMin.Milliseconds())
|
||||
return time.Duration(r) * time.Millisecond
|
||||
}
|
||||
|
||||
@@ -1920,7 +1920,7 @@ func domainIsUpToDate(domain string, domainCategory string, userAuth auth.UserAu
|
||||
// derived from syncTime (the moment the gRPC stream opened). Any
|
||||
// concurrent stream that started earlier loses the optimistic-lock race
|
||||
// in MarkPeerConnected and bails without writing.
|
||||
func (am *DefaultAccountManager) SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP, syncTime time.Time) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error) {
|
||||
func (am *DefaultAccountManager) SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP, syncTime time.Time) (*nbpeer.Peer, *types.NetworkMap, []*nmdata.PostureChecks, int64, error) {
|
||||
peer, netMap, postureChecks, dnsfwdPort, err := am.SyncPeer(ctx, types.PeerSync{WireGuardPubKey: peerPubKey, Meta: meta, RealIP: realIP}, accountID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, fmt.Errorf("error syncing peer: %w", err)
|
||||
@@ -2469,8 +2469,7 @@ func (am *DefaultAccountManager) ensureIPv6Subnet(ctx context.Context, transacti
|
||||
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)
|
||||
network.NetV6 = types.AllocateIPv6Subnet()
|
||||
|
||||
// Sync settings to match the allocated subnet so SaveAccountSettings persists it.
|
||||
ones, _ := network.NetV6.Mask.Size()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package account
|
||||
|
||||
//go:generate go run github.com/golang/mock/mockgen -package account -destination=manager_mock.go -source=./manager.go -build_flags=-mod=mod
|
||||
//go:generate go tool mockgen -package account -destination=manager_mock.go -source=./manager.go -build_flags=-mod=mod
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/users"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
type ExternalCacheManager nbcache.UserDataCache
|
||||
@@ -70,7 +71,7 @@ type Manager interface {
|
||||
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.Network, []*posture.Checks, bool, error)
|
||||
AddPeer(ctx context.Context, accountID, setupKey, userID string, p *nbpeer.Peer, temporary bool) (*nbpeer.Peer, *types.Network, []*nmdata.PostureChecks, bool, error)
|
||||
CreatePAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenName string, expiresIn int) (*types.PersonalAccessTokenGenerated, error)
|
||||
DeletePAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) error
|
||||
GetPAT(ctx context.Context, accountID string, initiatorUserID string, targetUserID string, tokenID string) (*types.PersonalAccessToken, error)
|
||||
@@ -109,9 +110,9 @@ type Manager interface {
|
||||
GetPeer(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error)
|
||||
UpdateAccountSettings(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error)
|
||||
UpdateAccountOnboarding(ctx context.Context, accountID, userID string, newOnboarding *types.AccountOnboarding) (*types.AccountOnboarding, error)
|
||||
LoginPeer(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.Network, []*posture.Checks, bool, error) // used by peer gRPC API
|
||||
ExtendPeerSession(ctx context.Context, peerPubKey, userID string) (time.Time, error) // used by peer gRPC API for ExtendAuthSession
|
||||
SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error) // used by peer gRPC API
|
||||
LoginPeer(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.Network, []*nmdata.PostureChecks, bool, error) // used by peer gRPC API
|
||||
ExtendPeerSession(ctx context.Context, peerPubKey, userID string) (time.Time, error) // used by peer gRPC API for ExtendAuthSession
|
||||
SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*nmdata.PostureChecks, int64, error) // used by peer gRPC API
|
||||
GetExternalCacheManager() ExternalCacheManager
|
||||
GetPostureChecks(ctx context.Context, accountID, postureChecksID, userID string) (*posture.Checks, error)
|
||||
SavePostureChecks(ctx context.Context, accountID, userID string, postureChecks *posture.Checks, create bool) (*posture.Checks, error)
|
||||
@@ -121,7 +122,7 @@ type Manager interface {
|
||||
UpdateIntegratedValidator(ctx context.Context, accountID, userID, validator string, groups []string) error
|
||||
GroupValidation(ctx context.Context, accountId string, groups []string) (bool, error)
|
||||
GetValidatedPeers(ctx context.Context, accountID string) (map[string]struct{}, map[string]string, error)
|
||||
SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP, syncTime time.Time) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error)
|
||||
SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP, syncTime time.Time) (*nbpeer.Peer, *types.NetworkMap, []*nmdata.PostureChecks, int64, error)
|
||||
OnPeerDisconnected(ctx context.Context, accountID string, peerPubKey string, streamStartTime time.Time) error
|
||||
SyncPeerMeta(ctx context.Context, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) error
|
||||
FindExistingPostureCheck(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,8 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
|
||||
//go:generate go tool mockgen -package=account -source=./request_buffer.go -destination=request_buffer_mock.go
|
||||
|
||||
type RequestBuffer interface {
|
||||
GetAccountWithBackpressure(ctx context.Context, accountID string) (*types.Account, error)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./request_buffer.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package=account -source=./request_buffer.go -destination=request_buffer_mock.go
|
||||
//
|
||||
|
||||
// Package account is a generated GoMock package.
|
||||
package account
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
types "github.com/netbirdio/netbird/management/server/types"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockRequestBuffer is a mock of RequestBuffer interface.
|
||||
type MockRequestBuffer struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockRequestBufferMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockRequestBufferMockRecorder is the mock recorder for MockRequestBuffer.
|
||||
type MockRequestBufferMockRecorder struct {
|
||||
mock *MockRequestBuffer
|
||||
}
|
||||
|
||||
// NewMockRequestBuffer creates a new mock instance.
|
||||
func NewMockRequestBuffer(ctrl *gomock.Controller) *MockRequestBuffer {
|
||||
mock := &MockRequestBuffer{ctrl: ctrl}
|
||||
mock.recorder = &MockRequestBufferMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockRequestBuffer) EXPECT() *MockRequestBufferMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetAccountWithBackpressure mocks base method.
|
||||
func (m *MockRequestBuffer) GetAccountWithBackpressure(ctx context.Context, accountID string) (*types.Account, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAccountWithBackpressure", ctx, accountID)
|
||||
ret0, _ := ret[0].(*types.Account)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAccountWithBackpressure indicates an expected call of GetAccountWithBackpressure.
|
||||
func (mr *MockRequestBufferMockRecorder) GetAccountWithBackpressure(ctx, accountID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountWithBackpressure", reflect.TypeOf((*MockRequestBuffer)(nil).GetAccountWithBackpressure), ctx, accountID)
|
||||
}
|
||||
@@ -2,117 +2,38 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/shared/requestbuffer"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
|
||||
// AccountRequest holds the result channel to return the requested account.
|
||||
type AccountRequest struct {
|
||||
AccountID string
|
||||
ResultChan chan *AccountResult
|
||||
}
|
||||
|
||||
// AccountResult holds the account data or an error.
|
||||
type AccountResult struct {
|
||||
Account *types.Account
|
||||
Err error
|
||||
}
|
||||
const defaultAccountBufferInterval = 100 * time.Millisecond
|
||||
|
||||
type AccountRequestBuffer struct {
|
||||
store store.Store
|
||||
getAccountRequests map[string][]*AccountRequest
|
||||
mu sync.Mutex
|
||||
getAccountRequestCh chan *AccountRequest
|
||||
bufferInterval time.Duration
|
||||
buffer *requestbuffer.Buffer[*types.Account]
|
||||
}
|
||||
|
||||
func NewAccountRequestBuffer(ctx context.Context, store store.Store) *AccountRequestBuffer {
|
||||
bufferIntervalStr := os.Getenv("NB_GET_ACCOUNT_BUFFER_INTERVAL")
|
||||
bufferInterval, err := time.ParseDuration(bufferIntervalStr)
|
||||
if err != nil {
|
||||
if bufferIntervalStr != "" {
|
||||
log.WithContext(ctx).Warnf("failed to parse account request buffer interval: %s", err)
|
||||
}
|
||||
bufferInterval = 100 * time.Millisecond
|
||||
interval := requestbuffer.Interval(ctx, "NB_GET_ACCOUNT_BUFFER_INTERVAL", defaultAccountBufferInterval)
|
||||
log.WithContext(ctx).Infof("set account request buffer interval to %s", interval)
|
||||
|
||||
return &AccountRequestBuffer{
|
||||
buffer: requestbuffer.New(ctx, "account request buffer", interval, store.GetAccount),
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Infof("set account request buffer interval to %s", bufferInterval)
|
||||
|
||||
ac := AccountRequestBuffer{
|
||||
store: store,
|
||||
getAccountRequests: make(map[string][]*AccountRequest),
|
||||
getAccountRequestCh: make(chan *AccountRequest),
|
||||
bufferInterval: bufferInterval,
|
||||
}
|
||||
|
||||
go ac.processGetAccountRequests(ctx)
|
||||
|
||||
return &ac
|
||||
}
|
||||
|
||||
func (ac *AccountRequestBuffer) GetAccountWithBackpressure(ctx context.Context, accountID string) (*types.Account, error) {
|
||||
req := &AccountRequest{
|
||||
AccountID: accountID,
|
||||
ResultChan: make(chan *AccountResult, 1),
|
||||
account, err := ac.buffer.Get(ctx, accountID)
|
||||
if err != nil || account == nil {
|
||||
return account, err
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Tracef("requesting account %s with backpressure", accountID)
|
||||
startTime := time.Now()
|
||||
ac.getAccountRequestCh <- req
|
||||
|
||||
result := <-req.ResultChan
|
||||
log.WithContext(ctx).Tracef("got account with backpressure after %s", time.Since(startTime))
|
||||
return result.Account, result.Err
|
||||
}
|
||||
|
||||
func (ac *AccountRequestBuffer) processGetAccountBatch(ctx context.Context, accountID string) {
|
||||
ac.mu.Lock()
|
||||
requests := ac.getAccountRequests[accountID]
|
||||
delete(ac.getAccountRequests, accountID)
|
||||
ac.mu.Unlock()
|
||||
|
||||
if len(requests) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
account, err := ac.store.GetAccount(ctx, accountID)
|
||||
log.WithContext(ctx).Tracef("getting account %s in batch took %s", accountID, time.Since(startTime))
|
||||
result := &AccountResult{Account: account, Err: err}
|
||||
|
||||
for _, req := range requests {
|
||||
if account != nil {
|
||||
// Shallow copy the account so each goroutine gets its own struct value.
|
||||
// This prevents data races when callers mutate fields like Policies.
|
||||
accountCopy := *account
|
||||
req.ResultChan <- &AccountResult{Account: &accountCopy, Err: err}
|
||||
} else {
|
||||
req.ResultChan <- result
|
||||
}
|
||||
close(req.ResultChan)
|
||||
}
|
||||
}
|
||||
|
||||
func (ac *AccountRequestBuffer) processGetAccountRequests(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case req := <-ac.getAccountRequestCh:
|
||||
ac.mu.Lock()
|
||||
ac.getAccountRequests[req.AccountID] = append(ac.getAccountRequests[req.AccountID], req)
|
||||
if len(ac.getAccountRequests[req.AccountID]) == 1 {
|
||||
go func(ctx context.Context, accountID string) {
|
||||
time.Sleep(ac.bufferInterval)
|
||||
ac.processGetAccountBatch(ctx, accountID)
|
||||
}(ctx, req.AccountID)
|
||||
}
|
||||
ac.mu.Unlock()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
// Shallow copy the account so each caller gets its own struct value.
|
||||
// This prevents data races when callers mutate fields like Policies.
|
||||
accountCopy := *account
|
||||
return &accountCopy, nil
|
||||
}
|
||||
|
||||
@@ -10,16 +10,17 @@ import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/prometheus/client_golang/prometheus/push"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel/metric/noop"
|
||||
"go.uber.org/mock/gomock"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
|
||||
@@ -37,6 +38,8 @@ import (
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
reverseproxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service/manager"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
||||
networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db"
|
||||
networkmapdbfactory "github.com/netbirdio/netbird/management/internals/network_map_db/factory"
|
||||
"github.com/netbirdio/netbird/management/internals/server/config"
|
||||
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||
nbAccount "github.com/netbirdio/netbird/management/server/account"
|
||||
@@ -3293,13 +3296,33 @@ func createManager(t testing.TB) (*DefaultAccountManager, *update_channel.PeersU
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
eventStore := &activity.InMemoryEventStore{}
|
||||
return buildTestManager(t, store, nil)
|
||||
}
|
||||
|
||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
// createManagerWithNetworkMapStore builds a manager whose network map controller
|
||||
// reads the twin (nmdata) store, the production path on sqlite and postgres.
|
||||
func createManagerWithNetworkMapStore(t testing.TB) (*DefaultAccountManager, *update_channel.PeersUpdateManager) {
|
||||
t.Helper()
|
||||
|
||||
if engine := os.Getenv("NETBIRD_STORE_ENGINE"); engine != "" && !strings.EqualFold(engine, string(types.SqliteStoreEngine)) {
|
||||
t.Skipf("network map store test needs the sqlite engine, got %s", engine)
|
||||
}
|
||||
|
||||
dataDir := t.TempDir()
|
||||
store, err := createStoreAt(t, dataDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
nmdataStore, err := networkmapdbfactory.NewNetworkMapDBStore(context.Background(), types.SqliteStoreEngine, dataDir, MockIntegratedValidator{}, newSettingsMockManager(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
manager, updateManager, err := buildTestManager(t, store, nmdataStore)
|
||||
require.NoError(t, err)
|
||||
return manager, updateManager
|
||||
}
|
||||
|
||||
func newSettingsMockManager(t testing.TB) *settings.MockManager {
|
||||
t.Helper()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
t.Cleanup(ctrl.Finish)
|
||||
|
||||
@@ -3312,6 +3335,23 @@ func createManager(t testing.TB) (*DefaultAccountManager, *update_channel.PeersU
|
||||
UpdateExtraSettings(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(false, nil).
|
||||
AnyTimes()
|
||||
return settingsMockManager
|
||||
}
|
||||
|
||||
func buildTestManager(t testing.TB, store store.Store, nmdataStore *networkmapdb.NetworkMapDBStoreImpl) (*DefaultAccountManager, *update_channel.PeersUpdateManager, error) {
|
||||
t.Helper()
|
||||
|
||||
eventStore := &activity.InMemoryEventStore{}
|
||||
|
||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
t.Cleanup(ctrl.Finish)
|
||||
|
||||
settingsMockManager := newSettingsMockManager(t)
|
||||
|
||||
permissionsManager := permissions.NewManager(store)
|
||||
peersManager := peers.NewManager(store, permissionsManager)
|
||||
@@ -3331,7 +3371,7 @@ func createManager(t testing.TB) (*DefaultAccountManager, *update_channel.PeersU
|
||||
|
||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
requestBuffer := NewAccountRequestBuffer(ctx, store)
|
||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{})
|
||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}, nmdataStore)
|
||||
manager, err := BuildManager(ctx, &config.Config{}, store, networkMapController, job.NewJobManager(nil, store, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -3349,7 +3389,11 @@ func createManager(t testing.TB) (*DefaultAccountManager, *update_channel.PeersU
|
||||
|
||||
func createStore(t testing.TB) (store.Store, error) {
|
||||
t.Helper()
|
||||
dataDir := t.TempDir()
|
||||
return createStoreAt(t, t.TempDir())
|
||||
}
|
||||
|
||||
func createStoreAt(t testing.TB, dataDir string) (store.Store, error) {
|
||||
t.Helper()
|
||||
store, cleanUp, err := store.NewTestStoreFromSQL(context.Background(), "", dataDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -27,8 +27,6 @@ func allPeerMaps(t *testing.T, manager *DefaultAccountManager, accountID string)
|
||||
account, err := manager.Store.GetAccount(ctx, accountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
account.InjectProxyPolicies(ctx)
|
||||
|
||||
validated := make(map[string]struct{}, len(account.Peers))
|
||||
for id := range account.Peers {
|
||||
validated[id] = struct{}{}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||
"github.com/netbirdio/netbird/management/server/posture"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
|
||||
@@ -145,7 +146,7 @@ func TestAffectedPeers_GroupAddResource_RefreshesRoutingPeer(t *testing.T) {
|
||||
assert.NotContains(t, affected, s.unrelatedPeerID, "unrelated peer must not be affected")
|
||||
}
|
||||
|
||||
func (s *routerScenario) createPostureCheckGatedPolicy(t *testing.T, ctx context.Context) string {
|
||||
func (s *routerScenario) createPostureCheckGatedPolicy(t *testing.T, ctx context.Context, policy *types.Policy) string {
|
||||
t.Helper()
|
||||
|
||||
check, err := s.manager.SavePostureChecks(ctx, s.accountID, userID, &posture.Checks{
|
||||
@@ -156,7 +157,6 @@ func (s *routerScenario) createPostureCheckGatedPolicy(t *testing.T, ctx context
|
||||
}, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
policy := peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID)
|
||||
policy.SourcePostureChecks = []string{check.ID}
|
||||
_, err = s.manager.SavePolicy(ctx, s.accountID, userID, policy, true)
|
||||
require.NoError(t, err)
|
||||
@@ -168,7 +168,7 @@ func TestAffectedPeers_E2E_SavePostureCheck_RefreshesRoutingPeer(t *testing.T) {
|
||||
s := setupRouterScenario(t, true)
|
||||
ctx := context.Background()
|
||||
|
||||
checkID := s.createPostureCheckGatedPolicy(t, ctx)
|
||||
checkID := s.createPostureCheckGatedPolicy(t, ctx, peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID))
|
||||
|
||||
srcCh := s.updateManager.CreateChannel(ctx, s.sourcePeerID)
|
||||
routerCh := s.updateManager.CreateChannel(ctx, s.routerPeerID)
|
||||
@@ -338,3 +338,61 @@ func TestAffectedPeers_PeerChange_RouterInOtherNetworkNotAffected(t *testing.T)
|
||||
assert.NotContains(t, affected, second.routerPeerID,
|
||||
"a router in an unrelated network must not be affected by a source-peer change for another resource")
|
||||
}
|
||||
|
||||
// TestAffectedPeers_E2E_PostureFlip_RefreshesRoutingPeer drives the customer path
|
||||
// on the twin store: the source peer's metadata flips a posture verdict on sync,
|
||||
// and the routing peer serving the gated resource must be refreshed in both
|
||||
// directions. Without the flip detection the deny direction takes the nmap
|
||||
// shortcut (the denied peer's map holds no router) and the allow direction
|
||||
// depends on which meta field moved, leaving the routers with a stale map.
|
||||
func TestAffectedPeers_E2E_PostureFlip_RefreshesRoutingPeer(t *testing.T) {
|
||||
runPostureFlipRefreshesRoutingPeer(t, func(s *routerScenario) *types.Policy {
|
||||
return peerToResourcePolicyByGroup(s.sourceGroupID, s.resourceGroupID)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAffectedPeers_E2E_PostureFlip_DirectSourcePeer_RefreshesRoutingPeer is the same
|
||||
// scenario with the source peer named directly in the rule: it must receive its posture
|
||||
// checks and have its flips detected exactly like a group member.
|
||||
func TestAffectedPeers_E2E_PostureFlip_DirectSourcePeer_RefreshesRoutingPeer(t *testing.T) {
|
||||
runPostureFlipRefreshesRoutingPeer(t, func(s *routerScenario) *types.Policy {
|
||||
return peerToResourcePolicyByPeer(s.sourcePeerID, s.resourceGroupID)
|
||||
})
|
||||
}
|
||||
|
||||
func runPostureFlipRefreshesRoutingPeer(t *testing.T, policyFor func(s *routerScenario) *types.Policy) {
|
||||
t.Helper()
|
||||
|
||||
manager, updateManager := createManagerWithNetworkMapStore(t)
|
||||
s := buildRouterScenario(t, manager, updateManager, true)
|
||||
ctx := context.Background()
|
||||
|
||||
s.createPostureCheckGatedPolicy(t, ctx, policyFor(s))
|
||||
|
||||
source, err := s.manager.Store.GetPeerByID(ctx, store.LockingStrengthNone, s.accountID, s.sourcePeerID)
|
||||
require.NoError(t, err)
|
||||
|
||||
syncWithVersion := func(version string) {
|
||||
meta := source.Meta
|
||||
meta.WtVersion = version
|
||||
_, _, _, _, err := s.manager.SyncPeer(ctx, types.PeerSync{WireGuardPubKey: source.Key, Meta: meta}, s.accountID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
syncWithVersion("0.31.0")
|
||||
|
||||
routerCh := s.updateManager.CreateChannel(ctx, s.routerPeerID)
|
||||
unrelatedCh := s.updateManager.CreateChannel(ctx, s.unrelatedPeerID)
|
||||
t.Cleanup(func() {
|
||||
s.updateManager.CloseChannel(ctx, s.routerPeerID)
|
||||
s.updateManager.CloseChannel(ctx, s.unrelatedPeerID)
|
||||
})
|
||||
settleAffectedUpdates(routerCh, unrelatedCh)
|
||||
|
||||
syncWithVersion("0.29.0")
|
||||
peerShouldReceiveUpdate(t, routerCh)
|
||||
peerShouldNotReceiveUpdate(t, unrelatedCh)
|
||||
|
||||
syncWithVersion("0.31.0")
|
||||
peerShouldReceiveUpdate(t, routerCh)
|
||||
peerShouldNotReceiveUpdate(t, unrelatedCh)
|
||||
}
|
||||
|
||||
@@ -60,6 +60,12 @@ func setupRouterScenario(t *testing.T, directRouterPeer bool) *routerScenario {
|
||||
manager, updateManager, err := createManager(t)
|
||||
require.NoError(t, err)
|
||||
|
||||
return buildRouterScenario(t, manager, updateManager, directRouterPeer)
|
||||
}
|
||||
|
||||
func buildRouterScenario(t *testing.T, manager *DefaultAccountManager, updateManager *update_channel.PeersUpdateManager, directRouterPeer bool) *routerScenario {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
account, err := createAccount(manager, "router_scenario", userID, "")
|
||||
@@ -167,6 +173,23 @@ func peerToResourcePolicyByGroup(sourceGroupID, resourceGroupID string) *types.P
|
||||
}
|
||||
}
|
||||
|
||||
// peerToResourcePolicyByPeer builds a policy naming the source peer directly via
|
||||
// SourceResource rather than through a group.
|
||||
func peerToResourcePolicyByPeer(sourcePeerID, resourceGroupID string) *types.Policy {
|
||||
return &types.Policy{
|
||||
Enabled: true,
|
||||
Name: "peer-to-resource-by-peer",
|
||||
Rules: []*types.PolicyRule{
|
||||
{
|
||||
Enabled: true,
|
||||
SourceResource: types.Resource{ID: sourcePeerID, Type: types.ResourceTypePeer},
|
||||
Destinations: []string{resourceGroupID},
|
||||
Action: types.PolicyTrafficActionAccept,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// peerToResourcePolicyByResource builds a policy referencing the resource
|
||||
// directly via DestinationResource rather than its group.
|
||||
func peerToResourcePolicyByResource(sourceGroupID, resourceID string) *types.Policy {
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"go.uber.org/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
@@ -234,7 +234,7 @@ func createDNSManager(t *testing.T) (*DefaultAccountManager, error) {
|
||||
|
||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
requestBuffer := NewAccountRequestBuffer(ctx, store)
|
||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.test", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{})
|
||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.test", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}, nil)
|
||||
|
||||
return BuildManager(context.Background(), nil, store, networkMapController, job.NewJobManager(nil, store, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package geolocation
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
@@ -21,6 +22,8 @@ const (
|
||||
geoLiteCitySha256ZipURL = "https://pkgs.netbird.io/geolocation-dbs/GeoLite2-City-CSV/download?suffix=zip.sha256"
|
||||
geoLiteCityMMDB = "GeoLite2-City.mmdb"
|
||||
geoLiteCityCSV = "GeoLite2-City-Locations-en.csv"
|
||||
|
||||
geonamesCsvFields = 14
|
||||
)
|
||||
|
||||
// loadGeolocationDatabases loads the MaxMind databases.
|
||||
@@ -160,6 +163,10 @@ func loadGeonamesCsv(filepath string) ([]GeoNames, error) {
|
||||
if index == 0 {
|
||||
continue
|
||||
}
|
||||
if len(record) < geonamesCsvFields {
|
||||
return nil, fmt.Errorf("geonames csv record %d has %d fields, want at least %d", index, len(record), geonamesCsvFields)
|
||||
}
|
||||
|
||||
geoNameID, err := strconv.Atoi(record[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -242,7 +242,11 @@ func getDatabaseFilename(ctx context.Context, databaseURL string, filenamePatter
|
||||
// strip suffixes that may be nested, such as .tar.gz
|
||||
basename := strings.SplitN(filename, ".", 2)[0]
|
||||
// get date version from basename
|
||||
date := strings.SplitN(basename, "_", 2)[1]
|
||||
parts := strings.SplitN(basename, "_", 2)
|
||||
if len(parts) < 2 || parts[1] == "" {
|
||||
return "", fmt.Errorf("unexpected database filename %q: missing date suffix", filename)
|
||||
}
|
||||
date := parts[1]
|
||||
// format db as "GeoLite2-Cities-{maxmind|geonames}_{DATE}.{mmdb|db}"
|
||||
databaseFilename := filepath.Base(strings.Replace(filenamePattern, "*", date, 1))
|
||||
|
||||
|
||||
@@ -184,7 +184,12 @@ func getFilenameFromURL(url string) (string, error) {
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
_, params, err := mime.ParseMediaType(resp.Header["Content-Disposition"][0])
|
||||
contentDisposition := resp.Header.Get("Content-Disposition")
|
||||
if contentDisposition == "" {
|
||||
return "", fmt.Errorf("no Content-Disposition header in response from %s", url)
|
||||
}
|
||||
|
||||
_, params, err := mime.ParseMediaType(contentDisposition)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -28,7 +27,7 @@ func TestGroupIPv6Assignment(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Allocate IPv6 subnet for the account
|
||||
account.Network.NetV6 = types.AllocateIPv6Subnet(rand.New(rand.NewSource(time.Now().UnixNano())))
|
||||
account.Network.NetV6 = types.AllocateIPv6Subnet()
|
||||
require.NoError(t, am.Store.SaveAccount(ctx, account))
|
||||
|
||||
// Create setup key
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"go.uber.org/mock/gomock"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/account"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
"github.com/netbirdio/netbird/management/server/permissions"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||
@@ -31,10 +30,6 @@ type managerImpl struct {
|
||||
accountManager account.Manager
|
||||
}
|
||||
|
||||
func eventMetaResource(group *types.Group, resource *resourceTypes.NetworkResource) map[string]any {
|
||||
return map[string]any{"name": group.Name, "id": group.ID, "resource_name": resource.Name, "resource_id": resource.ID, "resource_type": resource.Type}
|
||||
}
|
||||
|
||||
type mockManager struct {
|
||||
}
|
||||
|
||||
@@ -114,7 +109,7 @@ func (m *managerImpl) AddResourceToGroupInTransaction(ctx context.Context, trans
|
||||
}
|
||||
|
||||
event := func() {
|
||||
m.accountManager.StoreEvent(ctx, userID, groupID, accountID, activity.ResourceAddedToGroup, eventMetaResource(group, networkResource))
|
||||
m.accountManager.StoreEvent(ctx, userID, groupID, accountID, activity.ResourceAddedToGroup, group.EventMetaResource(types.TwinNetworkResource(networkResource)))
|
||||
}
|
||||
|
||||
return event, nil
|
||||
@@ -138,7 +133,7 @@ func (m *managerImpl) RemoveResourceFromGroupInTransaction(ctx context.Context,
|
||||
}
|
||||
|
||||
event := func() {
|
||||
m.accountManager.StoreEvent(ctx, userID, groupID, accountID, activity.ResourceRemovedFromGroup, eventMetaResource(group, networkResource))
|
||||
m.accountManager.StoreEvent(ctx, userID, groupID, accountID, activity.ResourceRemovedFromGroup, group.EventMetaResource(types.TwinNetworkResource(networkResource)))
|
||||
}
|
||||
|
||||
return event, nil
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"go.uber.org/mock/gomock"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"net/mail"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"go.uber.org/mock/gomock"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -446,7 +446,7 @@ func (h *Handler) GetAccessiblePeers(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
netMap := account.GetPeerNetworkMapFromComponents(ctx, peerID, dns.CustomZone{}, nil, validPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil, account.GetActiveGroupUsers())
|
||||
|
||||
util.WriteJSONObject(ctx, w, toAccessiblePeers(netMap, account.Peers, dnsDomain))
|
||||
util.WriteJSONObject(ctx, w, toAccessiblePeers(account.Peers, netMap, dnsDomain))
|
||||
}
|
||||
|
||||
func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -534,20 +534,22 @@ func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request)
|
||||
util.WriteJSONObject(r.Context(), w, resp)
|
||||
}
|
||||
|
||||
// toAccessiblePeers rehydrates the calculated map's component peers into the
|
||||
// account's full peer objects, which carry the location/status/meta fields
|
||||
// the API response needs.
|
||||
func toAccessiblePeers(netMap *types.NetworkMap, accountPeers map[string]*nbpeer.Peer, dnsDomain string) []api.AccessiblePeer {
|
||||
// toAccessiblePeers resolves the twin peers in netMap back to the full account
|
||||
// peers (by ID) so the API response keeps Status/Name/OS/GeoNameID, which the
|
||||
// slim netmap twins intentionally don't carry.
|
||||
func toAccessiblePeers(accountPeers map[string]*nbpeer.Peer, netMap *types.NetworkMap, dnsDomain string) []api.AccessiblePeer {
|
||||
accessiblePeers := make([]api.AccessiblePeer, 0, len(netMap.Peers)+len(netMap.OfflinePeers))
|
||||
add := func(peers []*types.ComponentPeer) {
|
||||
for _, p := range peers {
|
||||
if peer := accountPeers[p.ID]; peer != nil {
|
||||
accessiblePeers = append(accessiblePeers, peerToAccessiblePeer(peer, dnsDomain))
|
||||
}
|
||||
appendByID := func(id string) {
|
||||
if p, ok := accountPeers[id]; ok && p != nil {
|
||||
accessiblePeers = append(accessiblePeers, peerToAccessiblePeer(p, dnsDomain))
|
||||
}
|
||||
}
|
||||
add(netMap.Peers)
|
||||
add(netMap.OfflinePeers)
|
||||
for _, p := range netMap.Peers {
|
||||
appendByID(p.ID)
|
||||
}
|
||||
for _, p := range netMap.OfflinePeers {
|
||||
appendByID(p.ID)
|
||||
}
|
||||
|
||||
return accessiblePeers
|
||||
}
|
||||
@@ -615,6 +617,7 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD
|
||||
RosenpassEnabled: &peer.Meta.Flags.RosenpassEnabled,
|
||||
RosenpassPermissive: &peer.Meta.Flags.RosenpassPermissive,
|
||||
ServerSshAllowed: &peer.Meta.Flags.ServerSSHAllowed,
|
||||
RemoteJobsAllowed: &peer.Meta.Flags.RemoteJobsAllowed,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -670,6 +673,7 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn
|
||||
RosenpassEnabled: &peer.Meta.Flags.RosenpassEnabled,
|
||||
RosenpassPermissive: &peer.Meta.Flags.RosenpassPermissive,
|
||||
ServerSshAllowed: &peer.Meta.Flags.ServerSSHAllowed,
|
||||
RemoteJobsAllowed: &peer.Meta.Flags.RemoteJobsAllowed,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/gorilla/mux"
|
||||
ugomock "go.uber.org/mock/gomock"
|
||||
"go.uber.org/mock/gomock"
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||
@@ -106,7 +105,7 @@ func initTestMetaData(t *testing.T, peers ...*nbpeer.Peer) *Handler {
|
||||
},
|
||||
}
|
||||
|
||||
ctrl := ugomock.NewController(t)
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
networkMapController := network_map.NewMockController(ctrl)
|
||||
networkMapController.EXPECT().
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"go.uber.org/mock/gomock"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
|
||||
@@ -64,6 +64,19 @@ func (h *handler) createSetupKey(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// A one-off key can be used once, and GenerateSetupKey pins its usage limit
|
||||
// at 1 whatever the request says. Silently overriding a caller that asked
|
||||
// for a different number leaves them holding a key that does not do what
|
||||
// they configured, and no way to find out except by using it. Only values
|
||||
// above 1 are refused: usage_limit is a required field with no null, so 0
|
||||
// cannot be told apart from a caller that has nothing to say about it.
|
||||
if types.SetupKeyType(req.Type) == types.SetupKeyOneOff && req.UsageLimit > 1 {
|
||||
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument,
|
||||
"usage_limit %d is not valid for a one-off setup key, which can be used once; use type reusable for a key that can be used more than once",
|
||||
req.UsageLimit), w)
|
||||
return
|
||||
}
|
||||
|
||||
expiresIn := time.Duration(req.ExpiresIn) * time.Second
|
||||
|
||||
if expiresIn < 0 {
|
||||
|
||||
@@ -134,6 +134,40 @@ func TestSetupKeysHandlers(t *testing.T) {
|
||||
expectedBody: true,
|
||||
expectedSetupKey: expectedNewKey,
|
||||
},
|
||||
{
|
||||
// A one-off key is used once. Asking for more used to be accepted
|
||||
// and then quietly reduced to 1.
|
||||
name: "Create One-Off Setup Key With Conflicting Usage Limit",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/setup-keys",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(fmt.Sprintf("{\"name\":\"%s\",\"type\":\"one-off\",\"expires_in\":86400,\"usage_limit\":5}", newSetupKeyName))),
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
// 0 is what a caller sends when it has nothing to say about the
|
||||
// usage limit, since the field is required and has no null, so it
|
||||
// has to keep working.
|
||||
name: "Create One-Off Setup Key Without Usage Limit",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/setup-keys",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(fmt.Sprintf("{\"name\":\"%s\",\"type\":\"one-off\",\"expires_in\":86400,\"usage_limit\":0}", newSetupKeyName))),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
// Only one-off keys are constrained; a reusable key means what it
|
||||
// says.
|
||||
name: "Create Reusable Setup Key With Usage Limit",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/setup-keys",
|
||||
requestBody: bytes.NewBuffer(
|
||||
[]byte(fmt.Sprintf("{\"name\":\"%s\",\"type\":\"reusable\",\"expires_in\":86400,\"usage_limit\":5}", newSetupKeyName))),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: false,
|
||||
},
|
||||
{
|
||||
name: "Update Setup Key",
|
||||
requestType: http.MethodPut,
|
||||
|
||||
@@ -136,7 +136,10 @@ func Test_SetupKeys_Create(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create Setup Key as on-off with more than one usage",
|
||||
// The key used to be created anyway, with its usage limit quietly
|
||||
// reduced to 1, so the caller was told a key they had not asked for
|
||||
// was what they asked for.
|
||||
name: "Create Setup Key as one-off with more than one usage",
|
||||
requestType: http.MethodPost,
|
||||
requestPath: "/api/setup-keys",
|
||||
requestBody: &api.CreateSetupKeyRequest{
|
||||
@@ -146,23 +149,7 @@ func Test_SetupKeys_Create(t *testing.T) {
|
||||
Type: "one-off",
|
||||
UsageLimit: 3,
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedResponse: &api.SetupKey{
|
||||
AutoGroups: []string{},
|
||||
Ephemeral: false,
|
||||
Expires: time.Time{},
|
||||
Id: "",
|
||||
Key: "",
|
||||
LastUsed: time.Time{},
|
||||
Name: testing_tools.NewKeyName,
|
||||
Revoked: false,
|
||||
State: "valid",
|
||||
Type: "one-off",
|
||||
UpdatedAt: time.Now(),
|
||||
UsageLimit: 1,
|
||||
UsedTimes: 0,
|
||||
Valid: true,
|
||||
},
|
||||
expectedStatus: http.StatusUnprocessableEntity,
|
||||
},
|
||||
{
|
||||
name: "Create Setup Key with expiration in the past",
|
||||
|
||||
@@ -59,13 +59,21 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||
// Bound the background loops these managers start (account request buffer,
|
||||
// telemetry P95 flushers, PAT usage tracker, API rate limiter, proxy service
|
||||
// cleanup, cache janitors, DB connection pools) to the test's lifetime. On
|
||||
// context.Background() they never stop and accumulate across the package,
|
||||
// exhausting DB connections until the suite hits the 20m test timeout.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
metrics, err := telemetry.NewDefaultAppMetrics(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create metrics: %v", err)
|
||||
}
|
||||
|
||||
peersUpdateManager := update_channel.NewPeersUpdateManager(nil)
|
||||
updMsg := peersUpdateManager.CreateChannel(context.Background(), testing_tools.TestPeerId)
|
||||
updMsg := peersUpdateManager.CreateChannel(ctx, testing_tools.TestPeerId)
|
||||
done := make(chan struct{})
|
||||
if validateUpdate {
|
||||
go func() {
|
||||
@@ -88,15 +96,13 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
|
||||
|
||||
jobManager := job.NewJobManager(nil, store, peersManager)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
cacheStore, err := nbcache.NewStore(ctx, 100*time.Millisecond, 300*time.Millisecond, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create cache store: %v", err)
|
||||
}
|
||||
|
||||
requestBuffer := server.NewAccountRequestBuffer(ctx, store)
|
||||
networkMapController := controller.NewController(ctx, store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsManager, "", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{})
|
||||
networkMapController := controller.NewController(ctx, store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsManager, "", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{}, nil)
|
||||
am, err := server.BuildManager(ctx, nil, store, networkMapController, jobManager, nil, "", &activity.InMemoryEventStore{}, geoMock, false, validatorMock, metrics, proxyController, settingsManager, permissionsManager, false, cacheStore)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create manager: %v", err)
|
||||
@@ -111,6 +117,10 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
|
||||
t.Fatalf("Failed to create proxy manager: %v", err)
|
||||
}
|
||||
proxyServiceServer := nbgrpc.NewProxyServiceServer(accessLogsManager, proxyTokenStore, pkceverifierStore, nbgrpc.ProxyOIDCConfig{}, peersManager, userManager, nil, proxyMgr, nil)
|
||||
// NewProxyServiceServer starts cleanupStaleProxies on a context it derives
|
||||
// from context.Background(), independent of the cancellable ctx above;
|
||||
// Close() cancels it so the goroutine does not outlive the test.
|
||||
t.Cleanup(proxyServiceServer.Close)
|
||||
domainManager := manager.NewManager(store, proxyMgr, permissionsManager, am)
|
||||
serviceProxyController, err := proxymanager.NewGRPCController(proxyServiceServer, noopMeter)
|
||||
if err != nil {
|
||||
@@ -137,7 +147,7 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
|
||||
zoneRecordsManager := recordsManager.NewManager(store, am, permissionsManager)
|
||||
|
||||
apiRouter := mux.NewRouter().PathPrefix("/api").Subrouter()
|
||||
apiHandler, err := http2.NewAPIHandler(context.Background(), apiRouter, am, networksManager, resourcesManager, routersManager, groupsManager, geoMock, authManagerMock, metrics, permissionsManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil, nil, nil, nil)
|
||||
apiHandler, err := http2.NewAPIHandler(ctx, apiRouter, am, networksManager, resourcesManager, routersManager, groupsManager, geoMock, authManagerMock, metrics, permissionsManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create API handler: %v", err)
|
||||
}
|
||||
@@ -200,13 +210,21 @@ func BuildApiBlackBoxWithDBStateAndPeerChannel(t testing_tools.TB, sqlFile strin
|
||||
}
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||
// Bound the background loops these managers start (account request buffer,
|
||||
// telemetry P95 flushers, PAT usage tracker, API rate limiter, proxy service
|
||||
// cleanup, cache janitors, DB connection pools) to the test's lifetime. On
|
||||
// context.Background() they never stop and accumulate across the package,
|
||||
// exhausting DB connections until the suite hits the 20m test timeout.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
metrics, err := telemetry.NewDefaultAppMetrics(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create metrics: %v", err)
|
||||
}
|
||||
|
||||
peersUpdateManager := update_channel.NewPeersUpdateManager(nil)
|
||||
updMsg := peersUpdateManager.CreateChannel(context.Background(), testing_tools.TestPeerId)
|
||||
updMsg := peersUpdateManager.CreateChannel(ctx, testing_tools.TestPeerId)
|
||||
|
||||
geoMock := &geolocation.Mock{}
|
||||
validatorMock := server.MockIntegratedValidator{}
|
||||
@@ -218,15 +236,13 @@ func BuildApiBlackBoxWithDBStateAndPeerChannel(t testing_tools.TB, sqlFile strin
|
||||
|
||||
jobManager := job.NewJobManager(nil, store, peersManager)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
cacheStore, err := nbcache.NewStore(ctx, 100*time.Millisecond, 300*time.Millisecond, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create cache store: %v", err)
|
||||
}
|
||||
|
||||
requestBuffer := server.NewAccountRequestBuffer(ctx, store)
|
||||
networkMapController := controller.NewController(ctx, store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsManager, "", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{})
|
||||
networkMapController := controller.NewController(ctx, store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsManager, "", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{}, nil)
|
||||
am, err := server.BuildManager(ctx, nil, store, networkMapController, jobManager, nil, "", &activity.InMemoryEventStore{}, geoMock, false, validatorMock, metrics, proxyController, settingsManager, permissionsManager, false, cacheStore)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create manager: %v", err)
|
||||
@@ -241,6 +257,10 @@ func BuildApiBlackBoxWithDBStateAndPeerChannel(t testing_tools.TB, sqlFile strin
|
||||
t.Fatalf("Failed to create proxy manager: %v", err)
|
||||
}
|
||||
proxyServiceServer := nbgrpc.NewProxyServiceServer(accessLogsManager, proxyTokenStore, pkceverifierStore, nbgrpc.ProxyOIDCConfig{}, peersManager, userManager, nil, proxyMgr, nil)
|
||||
// NewProxyServiceServer starts cleanupStaleProxies on a context it derives
|
||||
// from context.Background(), independent of the cancellable ctx above;
|
||||
// Close() cancels it so the goroutine does not outlive the test.
|
||||
t.Cleanup(proxyServiceServer.Close)
|
||||
domainManager := manager.NewManager(store, proxyMgr, permissionsManager, am)
|
||||
serviceProxyController, err := proxymanager.NewGRPCController(proxyServiceServer, noopMeter)
|
||||
if err != nil {
|
||||
@@ -267,7 +287,7 @@ func BuildApiBlackBoxWithDBStateAndPeerChannel(t testing_tools.TB, sqlFile strin
|
||||
zoneRecordsManager := recordsManager.NewManager(store, am, permissionsManager)
|
||||
|
||||
apiRouter := mux.NewRouter().PathPrefix("/api").Subrouter()
|
||||
apiHandler, err := http2.NewAPIHandler(context.Background(), apiRouter, am, networksManager, resourcesManager, routersManager, groupsManager, geoMock, authManagerMock, metrics, permissionsManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil, nil, nil, nil)
|
||||
apiHandler, err := http2.NewAPIHandler(ctx, apiRouter, am, networksManager, resourcesManager, routersManager, groupsManager, geoMock, authManagerMock, metrics, permissionsManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create API handler: %v", err)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"go.uber.org/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -92,7 +92,7 @@ func createManagerWithEmbeddedIdP(t testing.TB) (*DefaultAccountManager, *update
|
||||
|
||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
requestBuffer := NewAccountRequestBuffer(ctx, testStore)
|
||||
networkMapController := controller.NewController(ctx, testStore, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(testStore, peersManager), &config.Config{})
|
||||
networkMapController := controller.NewController(ctx, testStore, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(testStore, peersManager), &config.Config{}, nil)
|
||||
manager, err := BuildManager(ctx, &config.Config{}, testStore, networkMapController, job.NewJobManager(nil, testStore, peersManager), idpManager, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
@@ -2,11 +2,12 @@ package idp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/util"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -33,31 +34,32 @@ func GeneratePassword(passwordLength, minSpecialChar, minNum, minUpperCase int)
|
||||
|
||||
//Set special character
|
||||
for i := 0; i < minSpecialChar; i++ {
|
||||
random := rand.Intn(len(specialCharSet))
|
||||
random := util.RandIntn(len(specialCharSet))
|
||||
password.WriteString(string(specialCharSet[random]))
|
||||
}
|
||||
|
||||
//Set numeric
|
||||
for i := 0; i < minNum; i++ {
|
||||
random := rand.Intn(len(numberSet))
|
||||
random := util.RandIntn(len(numberSet))
|
||||
password.WriteString(string(numberSet[random]))
|
||||
}
|
||||
|
||||
//Set uppercase
|
||||
for i := 0; i < minUpperCase; i++ {
|
||||
random := rand.Intn(len(upperCharSet))
|
||||
random := util.RandIntn(len(upperCharSet))
|
||||
password.WriteString(string(upperCharSet[random]))
|
||||
}
|
||||
|
||||
remainingLength := passwordLength - minSpecialChar - minNum - minUpperCase
|
||||
for i := 0; i < remainingLength; i++ {
|
||||
random := rand.Intn(len(allCharSet))
|
||||
random := util.RandIntn(len(allCharSet))
|
||||
password.WriteString(string(allCharSet[random]))
|
||||
}
|
||||
inRune := []rune(password.String())
|
||||
rand.Shuffle(len(inRune), func(i, j int) {
|
||||
for i := len(inRune) - 1; i > 0; i-- {
|
||||
j := util.RandIntn(i + 1)
|
||||
inRune[i], inRune[j] = inRune[j], inRune[i]
|
||||
})
|
||||
}
|
||||
return string(inRune)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"go.uber.org/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
// UpdateIntegratedValidator updates the integrated validator groups for a specified account.
|
||||
@@ -109,7 +110,7 @@ func (am *DefaultAccountManager) GetValidatedPeers(ctx context.Context, accountI
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
validPeers, err := am.integratedPeerValidator.GetValidatedPeers(ctx, accountID, groups, peers, settings.Extra)
|
||||
validPeers, err := am.integratedPeerValidator.GetValidatedPeers(ctx, accountID, types.TwinGroups(groups), types.TwinPeers(peers), settings.Extra)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -138,7 +139,7 @@ func (a MockIntegratedValidator) ValidatePeer(_ context.Context, update *nbpeer.
|
||||
return update, false, nil
|
||||
}
|
||||
|
||||
func (a MockIntegratedValidator) GetValidatedPeers(_ context.Context, accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) {
|
||||
func (a MockIntegratedValidator) GetValidatedPeers(_ context.Context, accountID string, groups []*nmdata.Group, peers []*nmdata.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) {
|
||||
validatedPeers := make(map[string]struct{})
|
||||
for _, peer := range peers {
|
||||
validatedPeers[peer.ID] = struct{}{}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package integration_reference
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IntegrationReference holds the reference to a particular integration
|
||||
type IntegrationReference struct {
|
||||
ID int
|
||||
IntegrationType string
|
||||
}
|
||||
|
||||
func (ir IntegrationReference) String() string {
|
||||
return fmt.Sprintf("%s:%d", ir.IntegrationType, ir.ID)
|
||||
}
|
||||
|
||||
func (ir IntegrationReference) CacheKey(path ...string) string {
|
||||
if len(path) == 0 {
|
||||
return ir.String()
|
||||
}
|
||||
return fmt.Sprintf("%s:%s", ir.String(), strings.Join(path, ":"))
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./interface.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package integrated_validator -destination=integrated_validator_mock.go -source=./interface.go -build_flags=-mod=mod
|
||||
//
|
||||
|
||||
// Package integrated_validator is a generated GoMock package.
|
||||
package integrated_validator
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
peer "github.com/netbirdio/netbird/management/server/peer"
|
||||
types "github.com/netbirdio/netbird/management/server/types"
|
||||
nmdata "github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
proto "github.com/netbirdio/netbird/shared/management/proto"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockIntegratedValidator is a mock of IntegratedValidator interface.
|
||||
type MockIntegratedValidator struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockIntegratedValidatorMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockIntegratedValidatorMockRecorder is the mock recorder for MockIntegratedValidator.
|
||||
type MockIntegratedValidatorMockRecorder struct {
|
||||
mock *MockIntegratedValidator
|
||||
}
|
||||
|
||||
// NewMockIntegratedValidator creates a new mock instance.
|
||||
func NewMockIntegratedValidator(ctrl *gomock.Controller) *MockIntegratedValidator {
|
||||
mock := &MockIntegratedValidator{ctrl: ctrl}
|
||||
mock.recorder = &MockIntegratedValidatorMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockIntegratedValidator) EXPECT() *MockIntegratedValidatorMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetInvalidPeers mocks base method.
|
||||
func (m *MockIntegratedValidator) GetInvalidPeers(ctx context.Context, accountID string, extraSettings *types.ExtraSettings) (map[string]string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetInvalidPeers", ctx, accountID, extraSettings)
|
||||
ret0, _ := ret[0].(map[string]string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetInvalidPeers indicates an expected call of GetInvalidPeers.
|
||||
func (mr *MockIntegratedValidatorMockRecorder) GetInvalidPeers(ctx, accountID, extraSettings any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInvalidPeers", reflect.TypeOf((*MockIntegratedValidator)(nil).GetInvalidPeers), ctx, accountID, extraSettings)
|
||||
}
|
||||
|
||||
// GetValidatedPeers mocks base method.
|
||||
func (m *MockIntegratedValidator) GetValidatedPeers(ctx context.Context, accountID string, groups []*nmdata.Group, peers []*nmdata.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetValidatedPeers", ctx, accountID, groups, peers, extraSettings)
|
||||
ret0, _ := ret[0].(map[string]struct{})
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetValidatedPeers indicates an expected call of GetValidatedPeers.
|
||||
func (mr *MockIntegratedValidatorMockRecorder) GetValidatedPeers(ctx, accountID, groups, peers, extraSettings any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatedPeers", reflect.TypeOf((*MockIntegratedValidator)(nil).GetValidatedPeers), ctx, accountID, groups, peers, extraSettings)
|
||||
}
|
||||
|
||||
// IsNotValidPeer mocks base method.
|
||||
func (m *MockIntegratedValidator) IsNotValidPeer(ctx context.Context, accountID string, arg2 *peer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) (bool, bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsNotValidPeer", ctx, accountID, arg2, peersGroup, extraSettings)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(bool)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// IsNotValidPeer indicates an expected call of IsNotValidPeer.
|
||||
func (mr *MockIntegratedValidatorMockRecorder) IsNotValidPeer(ctx, accountID, arg2, peersGroup, extraSettings any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsNotValidPeer", reflect.TypeOf((*MockIntegratedValidator)(nil).IsNotValidPeer), ctx, accountID, arg2, peersGroup, extraSettings)
|
||||
}
|
||||
|
||||
// PeerDeleted mocks base method.
|
||||
func (m *MockIntegratedValidator) PeerDeleted(ctx context.Context, accountID, peerID string, extraSettings *types.ExtraSettings) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PeerDeleted", ctx, accountID, peerID, extraSettings)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// PeerDeleted indicates an expected call of PeerDeleted.
|
||||
func (mr *MockIntegratedValidatorMockRecorder) PeerDeleted(ctx, accountID, peerID, extraSettings any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PeerDeleted", reflect.TypeOf((*MockIntegratedValidator)(nil).PeerDeleted), ctx, accountID, peerID, extraSettings)
|
||||
}
|
||||
|
||||
// PreparePeer mocks base method.
|
||||
func (m *MockIntegratedValidator) PreparePeer(ctx context.Context, accountID string, p *peer.Peer, peersGroup []string, extraSettings *types.ExtraSettings, temporary bool) *peer.Peer {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PreparePeer", ctx, accountID, p, peersGroup, extraSettings, temporary)
|
||||
ret0, _ := ret[0].(*peer.Peer)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// PreparePeer indicates an expected call of PreparePeer.
|
||||
func (mr *MockIntegratedValidatorMockRecorder) PreparePeer(ctx, accountID, p, peersGroup, extraSettings, temporary any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreparePeer", reflect.TypeOf((*MockIntegratedValidator)(nil).PreparePeer), ctx, accountID, p, peersGroup, extraSettings, temporary)
|
||||
}
|
||||
|
||||
// SetPeerInvalidationListener mocks base method.
|
||||
func (m *MockIntegratedValidator) SetPeerInvalidationListener(fn func(string, []string)) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "SetPeerInvalidationListener", fn)
|
||||
}
|
||||
|
||||
// SetPeerInvalidationListener indicates an expected call of SetPeerInvalidationListener.
|
||||
func (mr *MockIntegratedValidatorMockRecorder) SetPeerInvalidationListener(fn any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPeerInvalidationListener", reflect.TypeOf((*MockIntegratedValidator)(nil).SetPeerInvalidationListener), fn)
|
||||
}
|
||||
|
||||
// Stop mocks base method.
|
||||
func (m *MockIntegratedValidator) Stop(ctx context.Context) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Stop", ctx)
|
||||
}
|
||||
|
||||
// Stop indicates an expected call of Stop.
|
||||
func (mr *MockIntegratedValidatorMockRecorder) Stop(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockIntegratedValidator)(nil).Stop), ctx)
|
||||
}
|
||||
|
||||
// ValidateExtraSettings mocks base method.
|
||||
func (m *MockIntegratedValidator) ValidateExtraSettings(ctx context.Context, newExtraSettings, oldExtraSettings *types.ExtraSettings, userID, accountID string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ValidateExtraSettings", ctx, newExtraSettings, oldExtraSettings, userID, accountID)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ValidateExtraSettings indicates an expected call of ValidateExtraSettings.
|
||||
func (mr *MockIntegratedValidatorMockRecorder) ValidateExtraSettings(ctx, newExtraSettings, oldExtraSettings, userID, accountID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateExtraSettings", reflect.TypeOf((*MockIntegratedValidator)(nil).ValidateExtraSettings), ctx, newExtraSettings, oldExtraSettings, userID, accountID)
|
||||
}
|
||||
|
||||
// ValidateFlowResponse mocks base method.
|
||||
func (m *MockIntegratedValidator) ValidateFlowResponse(ctx context.Context, peerKey string, flowResponse *proto.PKCEAuthorizationFlow) *proto.PKCEAuthorizationFlow {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ValidateFlowResponse", ctx, peerKey, flowResponse)
|
||||
ret0, _ := ret[0].(*proto.PKCEAuthorizationFlow)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ValidateFlowResponse indicates an expected call of ValidateFlowResponse.
|
||||
func (mr *MockIntegratedValidatorMockRecorder) ValidateFlowResponse(ctx, peerKey, flowResponse any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateFlowResponse", reflect.TypeOf((*MockIntegratedValidator)(nil).ValidateFlowResponse), ctx, peerKey, flowResponse)
|
||||
}
|
||||
|
||||
// ValidatePeer mocks base method.
|
||||
func (m *MockIntegratedValidator) ValidatePeer(ctx context.Context, update, p *peer.Peer, userID, accountID, dnsDomain string, peersGroup []string, extraSettings *types.ExtraSettings) (*peer.Peer, bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ValidatePeer", ctx, update, p, userID, accountID, dnsDomain, peersGroup, extraSettings)
|
||||
ret0, _ := ret[0].(*peer.Peer)
|
||||
ret1, _ := ret[1].(bool)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// ValidatePeer indicates an expected call of ValidatePeer.
|
||||
func (mr *MockIntegratedValidatorMockRecorder) ValidatePeer(ctx, update, p, userID, accountID, dnsDomain, peersGroup, extraSettings any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidatePeer", reflect.TypeOf((*MockIntegratedValidator)(nil).ValidatePeer), ctx, update, p, userID, accountID, dnsDomain, peersGroup, extraSettings)
|
||||
}
|
||||
@@ -5,16 +5,19 @@ import (
|
||||
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
//go:generate go tool mockgen -package integrated_validator -destination=integrated_validator_mock.go -source=./interface.go -build_flags=-mod=mod
|
||||
|
||||
// IntegratedValidator interface exists to avoid the circle dependencies
|
||||
type IntegratedValidator interface {
|
||||
ValidateExtraSettings(ctx context.Context, newExtraSettings *types.ExtraSettings, oldExtraSettings *types.ExtraSettings, userID string, accountID string) error
|
||||
ValidatePeer(ctx context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *types.ExtraSettings) (*nbpeer.Peer, bool, error)
|
||||
PreparePeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings, temporary bool) *nbpeer.Peer
|
||||
ValidatePeer(ctx context.Context, update *nbpeer.Peer, p *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *types.ExtraSettings) (*nbpeer.Peer, bool, error)
|
||||
PreparePeer(ctx context.Context, accountID string, p *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings, temporary bool) *nbpeer.Peer
|
||||
IsNotValidPeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) (bool, bool, error)
|
||||
GetValidatedPeers(ctx context.Context, accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error)
|
||||
GetValidatedPeers(ctx context.Context, accountID string, groups []*nmdata.Group, peers []*nmdata.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error)
|
||||
GetInvalidPeers(ctx context.Context, accountID string, extraSettings *types.ExtraSettings) (map[string]string, error)
|
||||
PeerDeleted(ctx context.Context, accountID, peerID string, extraSettings *types.ExtraSettings) error
|
||||
SetPeerInvalidationListener(fn func(accountID string, peerIDs []string))
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/settings"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
@@ -35,7 +36,7 @@ func (v *IntegratedValidatorImpl) IsNotValidPeer(_ context.Context, _ string, _
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
func (v *IntegratedValidatorImpl) GetValidatedPeers(_ context.Context, _ string, _ []*types.Group, peers []*nbpeer.Peer, _ *types.ExtraSettings) (map[string]struct{}, error) {
|
||||
func (v *IntegratedValidatorImpl) GetValidatedPeers(_ context.Context, _ string, _ []*nmdata.Group, peers []*nmdata.Peer, _ *types.ExtraSettings) (map[string]struct{}, error) {
|
||||
validatedPeers := make(map[string]struct{})
|
||||
for _, p := range peers {
|
||||
validatedPeers[p.ID] = struct{}{}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"go.uber.org/mock/gomock"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
@@ -376,7 +376,7 @@ func startManagementForTest(t *testing.T, testFile string, config *config.Config
|
||||
return nil, nil, "", cleanup, err
|
||||
}
|
||||
|
||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeralMgr, config)
|
||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeralMgr, config, nil)
|
||||
accountManager, err := BuildManager(ctx, nil, store, networkMapController, jobManager, nil, "",
|
||||
eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"go.uber.org/mock/gomock"
|
||||
pb "github.com/golang/protobuf/proto" //nolint
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -216,7 +216,7 @@ func startServer(
|
||||
|
||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
requestBuffer := server.NewAccountRequestBuffer(ctx, str)
|
||||
networkMapController := controller.NewController(ctx, str, metrics, updateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(str, peers.NewManager(str, permissionsManager)), config)
|
||||
networkMapController := controller.NewController(ctx, str, metrics, updateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(str, peers.NewManager(str, permissionsManager)), config, nil)
|
||||
|
||||
accountManager, err := server.BuildManager(
|
||||
context.Background(),
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/users"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
var _ account.Manager = (*MockAccountManager)(nil)
|
||||
@@ -41,11 +42,11 @@ type MockAccountManager struct {
|
||||
GetPeersFunc func(ctx context.Context, accountID, userID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error)
|
||||
MarkPeerConnectedFunc func(ctx context.Context, peerKey string, accountID string, sessionStartedAt int64, nmap *types.NetworkMap) error
|
||||
MarkPeerDisconnectedFunc func(ctx context.Context, peerKey string, accountID string, sessionStartedAt int64) error
|
||||
SyncAndMarkPeerFunc func(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP, syncTime time.Time) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error)
|
||||
SyncAndMarkPeerFunc func(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP, syncTime time.Time) (*nbpeer.Peer, *types.NetworkMap, []*nmdata.PostureChecks, int64, error)
|
||||
DeletePeerFunc func(ctx context.Context, accountID, peerKey, userID string) error
|
||||
GetNetworkMapFunc func(ctx context.Context, peerKey string) (*types.NetworkMap, error)
|
||||
GetPeerNetworkFunc func(ctx context.Context, peerKey string) (*types.Network, error)
|
||||
AddPeerFunc func(ctx context.Context, accountID string, setupKey string, userId string, peer *nbpeer.Peer, temporary bool) (*nbpeer.Peer, *types.Network, []*posture.Checks, bool, error)
|
||||
AddPeerFunc func(ctx context.Context, accountID string, setupKey string, userId string, peer *nbpeer.Peer, temporary bool) (*nbpeer.Peer, *types.Network, []*nmdata.PostureChecks, bool, error)
|
||||
GetGroupFunc func(ctx context.Context, accountID, groupID, userID string) (*types.Group, error)
|
||||
GetAllGroupsFunc func(ctx context.Context, accountID, userID string) ([]*types.Group, error)
|
||||
GetGroupByNameFunc func(ctx context.Context, groupName, accountID, userID string) (*types.Group, error)
|
||||
@@ -98,9 +99,9 @@ type MockAccountManager struct {
|
||||
SaveDNSSettingsFunc func(ctx context.Context, accountID, userID string, dnsSettingsToSave *types.DNSSettings) error
|
||||
GetPeerFunc func(ctx context.Context, accountID, peerID, userID string) (*nbpeer.Peer, error)
|
||||
UpdateAccountSettingsFunc func(ctx context.Context, accountID, userID string, newSettings *types.Settings) (*types.Settings, error)
|
||||
LoginPeerFunc func(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.Network, []*posture.Checks, bool, error)
|
||||
LoginPeerFunc func(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.Network, []*nmdata.PostureChecks, bool, error)
|
||||
ExtendPeerSessionFunc func(ctx context.Context, peerPubKey, userID string) (time.Time, error)
|
||||
SyncPeerFunc func(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error)
|
||||
SyncPeerFunc func(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*nmdata.PostureChecks, int64, error)
|
||||
InviteUserFunc func(ctx context.Context, accountID string, initiatorUserID string, targetUserEmail string) error
|
||||
ApproveUserFunc func(ctx context.Context, accountID, initiatorUserID, targetUserID string) (*types.UserInfo, error)
|
||||
RejectUserFunc func(ctx context.Context, accountID, initiatorUserID, targetUserID string) error
|
||||
@@ -230,7 +231,7 @@ func (am *MockAccountManager) DeleteSetupKey(ctx context.Context, accountID, use
|
||||
return status.Errorf(codes.Unimplemented, "method DeleteSetupKey is not implemented")
|
||||
}
|
||||
|
||||
func (am *MockAccountManager) SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP, syncTime time.Time) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error) {
|
||||
func (am *MockAccountManager) SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP, syncTime time.Time) (*nbpeer.Peer, *types.NetworkMap, []*nmdata.PostureChecks, int64, error) {
|
||||
if am.SyncAndMarkPeerFunc != nil {
|
||||
return am.SyncAndMarkPeerFunc(ctx, accountID, peerPubKey, meta, realIP, syncTime)
|
||||
}
|
||||
@@ -424,7 +425,7 @@ func (am *MockAccountManager) AddPeer(
|
||||
userId string,
|
||||
peer *nbpeer.Peer,
|
||||
temporary bool,
|
||||
) (*nbpeer.Peer, *types.Network, []*posture.Checks, bool, error) {
|
||||
) (*nbpeer.Peer, *types.Network, []*nmdata.PostureChecks, bool, error) {
|
||||
if am.AddPeerFunc != nil {
|
||||
return am.AddPeerFunc(ctx, accountID, setupKey, userId, peer, temporary)
|
||||
}
|
||||
@@ -862,7 +863,7 @@ func (am *MockAccountManager) UpdateAccountSettings(ctx context.Context, account
|
||||
}
|
||||
|
||||
// LoginPeer mocks LoginPeer of the AccountManager interface
|
||||
func (am *MockAccountManager) LoginPeer(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.Network, []*posture.Checks, bool, error) {
|
||||
func (am *MockAccountManager) LoginPeer(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.Network, []*nmdata.PostureChecks, bool, error) {
|
||||
if am.LoginPeerFunc != nil {
|
||||
return am.LoginPeerFunc(ctx, login)
|
||||
}
|
||||
@@ -878,7 +879,7 @@ func (am *MockAccountManager) ExtendPeerSession(ctx context.Context, peerPubKey,
|
||||
}
|
||||
|
||||
// SyncPeer mocks SyncPeer of the AccountManager interface
|
||||
func (am *MockAccountManager) SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error) {
|
||||
func (am *MockAccountManager) SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*nmdata.PostureChecks, int64, error) {
|
||||
if am.SyncPeerFunc != nil {
|
||||
return am.SyncPeerFunc(ctx, sync, accountID)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"go.uber.org/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -803,7 +803,7 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) {
|
||||
|
||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
requestBuffer := NewAccountRequestBuffer(ctx, store)
|
||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{})
|
||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}, nil)
|
||||
|
||||
return BuildManager(context.Background(), nil, store, networkMapController, job.NewJobManager(nil, store, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"go.uber.org/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
reverseproxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
nbDomain "github.com/netbirdio/netbird/shared/management/domain"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
sharedTypes "github.com/netbirdio/netbird/shared/management/types"
|
||||
)
|
||||
|
||||
type NetworkResourceType string
|
||||
@@ -65,27 +64,6 @@ func NewNetworkResource(accountID, networkID, name, description, address string,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ToComponent converts the resource to its self-contained components
|
||||
// representation. Returns nil for a nil resource.
|
||||
func (n *NetworkResource) ToComponent() *sharedTypes.ComponentResource {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
return &sharedTypes.ComponentResource{
|
||||
ID: n.ID,
|
||||
PublicID: n.PublicID,
|
||||
NetworkID: n.NetworkID,
|
||||
AccountID: n.AccountID,
|
||||
Name: n.Name,
|
||||
Description: n.Description,
|
||||
Type: sharedTypes.ComponentResourceType(n.Type),
|
||||
Address: n.Address,
|
||||
Domain: n.Domain,
|
||||
Prefix: n.Prefix,
|
||||
Enabled: n.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NetworkResource) ToAPIResponse(groups []api.GroupMinimum) *api.NetworkResource {
|
||||
addr := n.Prefix.String()
|
||||
if n.Type == Domain {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/networks/types"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
sharedTypes "github.com/netbirdio/netbird/shared/management/types"
|
||||
)
|
||||
|
||||
type NetworkRouter struct {
|
||||
@@ -22,36 +21,6 @@ type NetworkRouter struct {
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// ToComponent converts the router to its self-contained components
|
||||
// representation. Returns nil for a nil router.
|
||||
func (n *NetworkRouter) ToComponent() *sharedTypes.ComponentRouter {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
return &sharedTypes.ComponentRouter{
|
||||
NetworkID: n.NetworkID,
|
||||
PublicID: n.PublicID,
|
||||
Peer: n.Peer,
|
||||
PeerGroups: n.PeerGroups,
|
||||
Masquerade: n.Masquerade,
|
||||
Metric: n.Metric,
|
||||
Enabled: n.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
// ToComponentMap converts a peer-keyed router map to its components
|
||||
// representation.
|
||||
func ToComponentMap(routers map[string]*NetworkRouter) map[string]*sharedTypes.ComponentRouter {
|
||||
if routers == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]*sharedTypes.ComponentRouter, len(routers))
|
||||
for id, r := range routers {
|
||||
out[id] = r.ToComponent()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func NewNetworkRouter(accountID string, networkID string, peer string, peerGroups []string, masquerade bool, metric int, enabled bool) (*NetworkRouter, error) {
|
||||
r := &NetworkRouter{
|
||||
ID: xid.New().String(),
|
||||
|
||||
+26
-14
@@ -21,8 +21,8 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/posture"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
|
||||
@@ -740,7 +740,7 @@ func (am *DefaultAccountManager) handleSetupKeyAddedPeer(ctx context.Context, en
|
||||
// to it. We also add the User ID to the peer metadata to identify registrant. If no userID provided, then fail with status.PermissionDenied
|
||||
// Each new Peer will be assigned a new next net.IP from the Account.Network and Account.Network.LastIP will be updated (IP's are not reused).
|
||||
// The peer property is just a placeholder for the Peer properties to pass further
|
||||
func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKey, userID string, peer *nbpeer.Peer, temporary bool) (*nbpeer.Peer, *types.Network, []*posture.Checks, bool, error) {
|
||||
func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKey, userID string, peer *nbpeer.Peer, temporary bool) (*nbpeer.Peer, *types.Network, []*nmdata.PostureChecks, bool, error) {
|
||||
if setupKey == "" && userID == "" && !peer.ProxyMeta.Embedded {
|
||||
// no auth method provided => reject access
|
||||
return nil, nil, nil, false, status.ErrNoAuthMethodProvided
|
||||
@@ -1000,7 +1000,7 @@ func getPeerIPDNSLabel(ip netip.Addr, peerHostName string) (string, error) {
|
||||
}
|
||||
|
||||
// SyncPeer checks whether peer is eligible for receiving NetworkMap (authenticated) and returns its NetworkMap if eligible
|
||||
func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error) {
|
||||
func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSync, accountID string) (*nbpeer.Peer, *types.NetworkMap, []*nmdata.PostureChecks, int64, error) {
|
||||
var peer *nbpeer.Peer
|
||||
var ipv6CapabilityChanged bool
|
||||
var metaDiff nbpeer.MetaDiff
|
||||
@@ -1064,7 +1064,7 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSy
|
||||
return nil, nil, nil, 0, err
|
||||
}
|
||||
|
||||
metaDiffAffectsPosture := posture.AffectsPosture(ctx, &metaDiff, resPostureChecks)
|
||||
metaDiffAffectsPosture := metaDiffAffectsPosture(&metaDiff, resPostureChecks)
|
||||
if requiresPeerUpdate(ctx, isStatusChanged, sync.UpdateAccountPeers, ipv6CapabilityChanged, metaDiffAffectsPosture, metaDiff.VersionChanged(), metaDiff.HostnameChanged()) {
|
||||
changedPeerIDs := []string{peer.ID}
|
||||
affectedPeerIDs := am.syncPeerAffectedPeers(ctx, accountID, peer.ID, nmap, peerNotValid, metaDiffAffectsPosture)
|
||||
@@ -1076,6 +1076,14 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync types.PeerSy
|
||||
return peer, nmap, resPostureChecks, dnsFwdPort, nil
|
||||
}
|
||||
|
||||
// metaDiffAffectsPosture reports whether the meta change flips the verdict of any of
|
||||
// the peer's posture checks, replaying them against the old and new state.
|
||||
func metaDiffAffectsPosture(diff *nbpeer.MetaDiff, checks []*nmdata.PostureChecks) bool {
|
||||
oldPeer := types.TwinPeer(&nbpeer.Peer{Meta: diff.OldMeta, Location: diff.OldLocation})
|
||||
newPeer := types.TwinPeer(&nbpeer.Peer{Meta: diff.NewMeta, Location: diff.NewLocation})
|
||||
return nmdata.PostureVerdictChanged(checks, oldPeer, newPeer)
|
||||
}
|
||||
|
||||
func requiresPeerUpdate(ctx context.Context, isStatusChanged, updateAccountPeers, ipv6CapabilityChanged, metaDiffAffectsPosture, versionChanged, hostname bool) bool {
|
||||
var reason string
|
||||
switch {
|
||||
@@ -1127,7 +1135,7 @@ func (am *DefaultAccountManager) markConnectedAffectedPeers(ctx context.Context,
|
||||
return affectedPeerIDsFromNetworkMap(nmap, peerID)
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) handlePeerLoginNotFound(ctx context.Context, login types.PeerLogin, err error) (*nbpeer.Peer, *types.Network, []*posture.Checks, bool, error) {
|
||||
func (am *DefaultAccountManager) handlePeerLoginNotFound(ctx context.Context, login types.PeerLogin, err error) (*nbpeer.Peer, *types.Network, []*nmdata.PostureChecks, bool, error) {
|
||||
if errStatus, ok := status.FromError(err); ok && errStatus.Type() == status.NotFound {
|
||||
// we couldn't find this peer by its public key which can mean that peer hasn't been registered yet.
|
||||
// Try registering it.
|
||||
@@ -1148,7 +1156,7 @@ func (am *DefaultAccountManager) handlePeerLoginNotFound(ctx context.Context, lo
|
||||
|
||||
// LoginPeer logs in or registers a peer.
|
||||
// If peer doesn't exist the function checks whether a setup key or a user is present and registers a new peer if so.
|
||||
func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.Network, []*posture.Checks, bool, error) {
|
||||
func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.PeerLogin) (*nbpeer.Peer, *types.Network, []*nmdata.PostureChecks, bool, error) {
|
||||
accountID, err := am.Store.GetAccountIDByPeerPubKey(ctx, login.WireGuardPubKey)
|
||||
if err != nil {
|
||||
return am.handlePeerLoginNotFound(ctx, login, err)
|
||||
@@ -1321,7 +1329,7 @@ func (am *DefaultAccountManager) ExtendPeerSession(ctx context.Context, peerPubK
|
||||
|
||||
// getPeerLoginInfo computes the login/register response data (network, posture
|
||||
// checks, SSH) from the store without building the peer's full network map.
|
||||
func getPeerLoginInfo(ctx context.Context, transaction store.Store, accountID string, peer *nbpeer.Peer, isValid bool) (*types.Network, []*posture.Checks, bool, error) {
|
||||
func getPeerLoginInfo(ctx context.Context, transaction store.Store, accountID string, peer *nbpeer.Peer, isValid bool) (*types.Network, []*nmdata.PostureChecks, bool, error) {
|
||||
network, err := transaction.GetAccountNetwork(ctx, store.LockingStrengthNone, accountID)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("get account network: %w", err)
|
||||
@@ -1341,7 +1349,7 @@ func getPeerLoginInfo(ctx context.Context, transaction store.Store, accountID st
|
||||
return nil, nil, false, err
|
||||
}
|
||||
|
||||
postureChecks, err := getPeerPostureChecks(ctx, transaction, accountID, peerGroupIDs, policies)
|
||||
postureChecks, err := getPeerPostureChecks(ctx, transaction, accountID, peer.ID, peerGroupIDs, policies)
|
||||
if err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
@@ -1363,7 +1371,7 @@ func isPeerSSHEnabled(ctx context.Context, peer *nbpeer.Peer, policies []*types.
|
||||
}
|
||||
|
||||
// getPeerPostureChecks returns the posture checks for the peer.
|
||||
func getPeerPostureChecks(ctx context.Context, transaction store.Store, accountID string, peerGroupIDs []string, policies []*types.Policy) ([]*posture.Checks, error) {
|
||||
func getPeerPostureChecks(ctx context.Context, transaction store.Store, accountID, peerID string, peerGroupIDs []string, policies []*types.Policy) ([]*nmdata.PostureChecks, error) {
|
||||
if len(policies) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1375,7 +1383,7 @@ func getPeerPostureChecks(ctx context.Context, transaction store.Store, accountI
|
||||
continue
|
||||
}
|
||||
|
||||
postureChecksIDs := processPeerPostureChecks(policy, peerGroupIDs)
|
||||
postureChecksIDs := processPeerPostureChecks(policy, peerID, peerGroupIDs)
|
||||
peerPostureChecksIDs = append(peerPostureChecksIDs, postureChecksIDs...)
|
||||
}
|
||||
|
||||
@@ -1384,16 +1392,20 @@ func getPeerPostureChecks(ctx context.Context, transaction store.Store, accountI
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return maps.Values(peerPostureChecks), nil
|
||||
return types.TwinPostureChecksList(maps.Values(peerPostureChecks)), nil
|
||||
}
|
||||
|
||||
// processPeerPostureChecks checks if the peer is in the source group of the policy and returns the posture checks.
|
||||
func processPeerPostureChecks(policy *types.Policy, peerGroupIDs []string) []string {
|
||||
// processPeerPostureChecks returns the policy's posture checks when the peer is a source of the policy, directly or through a source group.
|
||||
func processPeerPostureChecks(policy *types.Policy, peerID string, peerGroupIDs []string) []string {
|
||||
for _, rule := range policy.Rules {
|
||||
if !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if rule.SourceResource.Type == types.ResourceTypePeer && rule.SourceResource.ID == peerID {
|
||||
return policy.SourcePostureChecks
|
||||
}
|
||||
|
||||
for _, sourceGroup := range rule.Sources {
|
||||
if slices.Contains(peerGroupIDs, sourceGroup) {
|
||||
return policy.SourcePostureChecks
|
||||
@@ -1588,7 +1600,7 @@ func affectedPeerIDsFromNetworkMap(nmap *types.NetworkMap, selfPeerID string) []
|
||||
}
|
||||
seen := make(map[string]struct{}, len(nmap.Peers)+len(nmap.OfflinePeers))
|
||||
ids := make([]string, 0, len(nmap.Peers)+len(nmap.OfflinePeers))
|
||||
add := func(peers []*types.ComponentPeer) {
|
||||
add := func(peers []*nmdata.Peer) {
|
||||
for _, p := range peers {
|
||||
if p == nil || p.ID == "" || p.ID == selfPeerID {
|
||||
continue
|
||||
|
||||
@@ -13,14 +13,14 @@ import (
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/util"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
sharedTypes "github.com/netbirdio/netbird/shared/management/types"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
// Peer capability constants mirror the proto enum values.
|
||||
const (
|
||||
PeerCapabilitySourcePrefixes int32 = 1
|
||||
PeerCapabilityIPv6Overlay int32 = 2
|
||||
PeerCapabilityComponentNetworkMap int32 = 3
|
||||
PeerCapabilitySourcePrefixes = nmdata.PeerCapabilitySourcePrefixes
|
||||
PeerCapabilityIPv6Overlay = nmdata.PeerCapabilityIPv6Overlay
|
||||
PeerCapabilityComponentNetworkMap = nmdata.PeerCapabilityComponentNetworkMap
|
||||
)
|
||||
|
||||
// Peer represents a machine connected to the network.
|
||||
@@ -142,6 +142,7 @@ type Flags struct {
|
||||
RosenpassEnabled bool
|
||||
RosenpassPermissive bool
|
||||
ServerSSHAllowed bool
|
||||
RemoteJobsAllowed bool
|
||||
|
||||
DisableClientRoutes bool
|
||||
DisableServerRoutes bool
|
||||
@@ -206,35 +207,6 @@ func (p *Peer) AddedWithSSOLogin() bool {
|
||||
return p.UserID != ""
|
||||
}
|
||||
|
||||
// ToComponent converts the peer to its self-contained components
|
||||
// representation, carrying exactly the subset of peer data that crosses the
|
||||
// components wire format. Returns nil for a nil peer so callers can convert
|
||||
// possibly-missing peers without guarding.
|
||||
func (p *Peer) ToComponent() *sharedTypes.ComponentPeer {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
cp := &sharedTypes.ComponentPeer{
|
||||
ID: p.ID,
|
||||
Key: p.Key,
|
||||
IP: p.IP,
|
||||
IPv6: p.IPv6,
|
||||
DNSLabel: p.DNSLabel,
|
||||
SSHKey: p.SSHKey,
|
||||
SSHEnabled: p.SSHEnabled,
|
||||
ServerSSHAllowed: p.Meta.Flags.ServerSSHAllowed,
|
||||
AgentVersion: p.Meta.WtVersion,
|
||||
SupportsSourcePrefixes: p.SupportsSourcePrefixes(),
|
||||
SupportsIPv6: p.SupportsIPv6(),
|
||||
LoginExpirationEnabled: p.LoginExpirationEnabled,
|
||||
AddedWithSSOLogin: p.AddedWithSSOLogin(),
|
||||
}
|
||||
if p.LastLogin != nil {
|
||||
cp.LastLogin = *p.LastLogin
|
||||
}
|
||||
return cp
|
||||
}
|
||||
|
||||
// HasCapability reports whether the peer has the given capability.
|
||||
func (p *Peer) HasCapability(capability int32) bool {
|
||||
return slices.Contains(p.Meta.Capabilities, capability)
|
||||
@@ -602,6 +574,7 @@ func (f Flags) isEqual(other Flags) bool {
|
||||
return f.RosenpassEnabled == other.RosenpassEnabled &&
|
||||
f.RosenpassPermissive == other.RosenpassPermissive &&
|
||||
f.ServerSSHAllowed == other.ServerSSHAllowed &&
|
||||
f.RemoteJobsAllowed == other.RemoteJobsAllowed &&
|
||||
f.DisableClientRoutes == other.DisableClientRoutes &&
|
||||
f.DisableServerRoutes == other.DisableServerRoutes &&
|
||||
f.DisableDNS == other.DisableDNS &&
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
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/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
func diffFrom(oldMeta, newMeta nbpeer.PeerSystemMeta, oldLoc, newLoc nbpeer.Location) *nbpeer.MetaDiff {
|
||||
return &nbpeer.MetaDiff{
|
||||
OldMeta: oldMeta,
|
||||
NewMeta: newMeta,
|
||||
OldLocation: oldLoc,
|
||||
NewLocation: newLoc,
|
||||
}
|
||||
}
|
||||
|
||||
func postureBundle(def nmdata.ChecksDefinition) []*nmdata.PostureChecks {
|
||||
return []*nmdata.PostureChecks{{Checks: def}}
|
||||
}
|
||||
|
||||
func TestMetaDiffAffectsPosture_NBVersion(t *testing.T) {
|
||||
c := postureBundle(nmdata.ChecksDefinition{NBVersionCheck: &nmdata.NBVersionCheck{MinVersion: "1.2.0"}})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
oldVer, newVer string
|
||||
want bool
|
||||
}{
|
||||
{"both above min, no flip", "1.3.0", "1.4.0", false},
|
||||
{"both below min, no flip", "1.0.0", "1.1.0", false},
|
||||
{"crosses up below->above", "1.1.0", "1.3.0", true},
|
||||
{"crosses down above->below", "1.3.0", "1.1.0", true},
|
||||
{"unparsable old only -> flip", "garbage", "1.3.0", true},
|
||||
{"unparsable both -> no flip", "garbage", "junk", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
diff := diffFrom(
|
||||
nbpeer.PeerSystemMeta{WtVersion: tt.oldVer},
|
||||
nbpeer.PeerSystemMeta{WtVersion: tt.newVer},
|
||||
nbpeer.Location{}, nbpeer.Location{},
|
||||
)
|
||||
assert.Equal(t, tt.want, metaDiffAffectsPosture(diff, c))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaDiffAffectsPosture_OSVersion_KernelBumpWithinMin(t *testing.T) {
|
||||
c := postureBundle(nmdata.ChecksDefinition{OSVersionCheck: &nmdata.OSVersionCheck{
|
||||
Linux: &nmdata.MinKernelVersionCheck{MinKernelVersion: "5.0.0"},
|
||||
}})
|
||||
|
||||
withinMin := diffFrom(
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "5.10.0-arch1"},
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "5.15.0-arch2"},
|
||||
nbpeer.Location{}, nbpeer.Location{},
|
||||
)
|
||||
assert.False(t, metaDiffAffectsPosture(withinMin, c))
|
||||
|
||||
crossesDown := diffFrom(
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "5.10.0-arch1"},
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "4.19.0-arch1"},
|
||||
nbpeer.Location{}, nbpeer.Location{},
|
||||
)
|
||||
assert.True(t, metaDiffAffectsPosture(crossesDown, c))
|
||||
}
|
||||
|
||||
func TestMetaDiffAffectsPosture_OSVersion_GoOSSwitchFlipsVerdict(t *testing.T) {
|
||||
c := postureBundle(nmdata.ChecksDefinition{OSVersionCheck: &nmdata.OSVersionCheck{
|
||||
Linux: &nmdata.MinKernelVersionCheck{MinKernelVersion: "6.0.0"},
|
||||
}})
|
||||
|
||||
diff := diffFrom(
|
||||
nbpeer.PeerSystemMeta{GoOS: "freebsd"},
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "4.19.0"},
|
||||
nbpeer.Location{}, nbpeer.Location{},
|
||||
)
|
||||
assert.True(t, metaDiffAffectsPosture(diff, c))
|
||||
}
|
||||
|
||||
func TestMetaDiffAffectsPosture_Process_GoOSSwitchFlipsVerdict(t *testing.T) {
|
||||
c := postureBundle(nmdata.ChecksDefinition{ProcessCheck: &nmdata.ProcessCheck{
|
||||
Processes: []nmdata.Process{{LinuxPath: "/usr/bin/foo"}},
|
||||
}})
|
||||
|
||||
files := []nbpeer.File{{Path: "/usr/bin/foo", ProcessIsRunning: true}}
|
||||
diff := diffFrom(
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", Files: files},
|
||||
nbpeer.PeerSystemMeta{GoOS: "windows", Files: files},
|
||||
nbpeer.Location{}, nbpeer.Location{},
|
||||
)
|
||||
assert.True(t, metaDiffAffectsPosture(diff, c))
|
||||
}
|
||||
|
||||
func TestMetaDiffAffectsPosture_Process_UnrelatedFileChange(t *testing.T) {
|
||||
c := postureBundle(nmdata.ChecksDefinition{ProcessCheck: &nmdata.ProcessCheck{
|
||||
Processes: []nmdata.Process{{LinuxPath: "/usr/bin/foo"}},
|
||||
}})
|
||||
|
||||
diff := diffFrom(
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", Files: []nbpeer.File{
|
||||
{Path: "/usr/bin/foo", ProcessIsRunning: true},
|
||||
}},
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", Files: []nbpeer.File{
|
||||
{Path: "/usr/bin/foo", ProcessIsRunning: true},
|
||||
{Path: "/usr/bin/bar", ProcessIsRunning: true},
|
||||
}},
|
||||
nbpeer.Location{}, nbpeer.Location{},
|
||||
)
|
||||
assert.False(t, metaDiffAffectsPosture(diff, c))
|
||||
}
|
||||
|
||||
func TestMetaDiffAffectsPosture_GeoLocation(t *testing.T) {
|
||||
c := postureBundle(nmdata.ChecksDefinition{GeoLocationCheck: &nmdata.GeoLocationCheck{
|
||||
Action: posture.CheckActionAllow,
|
||||
Locations: []nmdata.GeoLocation{{CountryCode: "DE"}},
|
||||
}})
|
||||
|
||||
stayAllowed := diffFrom(
|
||||
nbpeer.PeerSystemMeta{}, nbpeer.PeerSystemMeta{},
|
||||
nbpeer.Location{CountryCode: "DE", CityName: "Berlin"},
|
||||
nbpeer.Location{CountryCode: "DE", CityName: "Munich"},
|
||||
)
|
||||
assert.False(t, metaDiffAffectsPosture(stayAllowed, c))
|
||||
|
||||
moveOut := diffFrom(
|
||||
nbpeer.PeerSystemMeta{}, nbpeer.PeerSystemMeta{},
|
||||
nbpeer.Location{CountryCode: "DE"},
|
||||
nbpeer.Location{CountryCode: "FR"},
|
||||
)
|
||||
assert.True(t, metaDiffAffectsPosture(moveOut, c))
|
||||
}
|
||||
|
||||
func TestMetaDiffAffectsPosture_PeerNetworkRange_ConnectionIP(t *testing.T) {
|
||||
c := postureBundle(nmdata.ChecksDefinition{PeerNetworkRangeCheck: &nmdata.PeerNetworkRangeCheck{
|
||||
Action: posture.CheckActionAllow,
|
||||
Ranges: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")},
|
||||
}})
|
||||
|
||||
movesOutOfRange := diffFrom(
|
||||
nbpeer.PeerSystemMeta{}, nbpeer.PeerSystemMeta{},
|
||||
nbpeer.Location{ConnectionIP: net.ParseIP("10.1.2.3")},
|
||||
nbpeer.Location{ConnectionIP: net.ParseIP("8.8.8.8")},
|
||||
)
|
||||
assert.True(t, metaDiffAffectsPosture(movesOutOfRange, c))
|
||||
|
||||
staysInRange := diffFrom(
|
||||
nbpeer.PeerSystemMeta{}, nbpeer.PeerSystemMeta{},
|
||||
nbpeer.Location{ConnectionIP: net.ParseIP("10.1.2.3")},
|
||||
nbpeer.Location{ConnectionIP: net.ParseIP("10.9.9.9")},
|
||||
)
|
||||
assert.False(t, metaDiffAffectsPosture(staysInRange, c))
|
||||
}
|
||||
|
||||
func TestMetaDiffAffectsPosture_IrrelevantFieldChange(t *testing.T) {
|
||||
c := postureBundle(nmdata.ChecksDefinition{
|
||||
NBVersionCheck: &nmdata.NBVersionCheck{MinVersion: "1.0.0"},
|
||||
GeoLocationCheck: &nmdata.GeoLocationCheck{Action: posture.CheckActionAllow, Locations: []nmdata.GeoLocation{{CountryCode: "DE"}}},
|
||||
})
|
||||
|
||||
diff := diffFrom(
|
||||
nbpeer.PeerSystemMeta{Hostname: "old", WtVersion: "1.5.0"},
|
||||
nbpeer.PeerSystemMeta{Hostname: "new", WtVersion: "1.5.0"},
|
||||
nbpeer.Location{CountryCode: "DE"}, nbpeer.Location{CountryCode: "DE"},
|
||||
)
|
||||
assert.False(t, metaDiffAffectsPosture(diff, c))
|
||||
}
|
||||
|
||||
func TestMetaDiffAffectsPosture_NoChecks(t *testing.T) {
|
||||
diff := diffFrom(
|
||||
nbpeer.PeerSystemMeta{WtVersion: "1.0.0"},
|
||||
nbpeer.PeerSystemMeta{WtVersion: "2.0.0"},
|
||||
nbpeer.Location{}, nbpeer.Location{},
|
||||
)
|
||||
assert.False(t, metaDiffAffectsPosture(diff, nil))
|
||||
}
|
||||
|
||||
func TestProcessPeerPostureChecks(t *testing.T) {
|
||||
policy := &types.Policy{
|
||||
Enabled: true,
|
||||
SourcePostureChecks: []string{"pc1"},
|
||||
Rules: []*types.PolicyRule{
|
||||
{Enabled: false, Sources: []string{"g-disabled"}, SourceResource: types.Resource{ID: "peer-disabled", Type: types.ResourceTypePeer}},
|
||||
{Enabled: true, Sources: []string{"g-src"}, Destinations: []string{"g-dst"}},
|
||||
{Enabled: true, SourceResource: types.Resource{ID: "peer-direct", Type: types.ResourceTypePeer}, Destinations: []string{"g-dst"}},
|
||||
{Enabled: true, SourceResource: types.Resource{ID: "peer-as-host", Type: types.ResourceTypeHost}, Destinations: []string{"g-dst"}},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"pc1"}, processPeerPostureChecks(policy, "peer-in-group", []string{"g-src"}), "source group member")
|
||||
assert.Equal(t, []string{"pc1"}, processPeerPostureChecks(policy, "peer-direct", nil), "direct source peer")
|
||||
assert.Empty(t, processPeerPostureChecks(policy, "peer-elsewhere", []string{"g-dst"}), "destination-only peer")
|
||||
assert.Empty(t, processPeerPostureChecks(policy, "peer-disabled", []string{"g-disabled"}), "disabled rule")
|
||||
assert.Empty(t, processPeerPostureChecks(policy, "peer-as-host", nil), "source resource of a non-peer type")
|
||||
}
|
||||
@@ -16,11 +16,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/rs/xid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
|
||||
@@ -57,6 +57,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
nbroute "github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
@@ -1091,22 +1092,22 @@ func TestToSyncResponse(t *testing.T) {
|
||||
Signature: "turn-pass",
|
||||
}
|
||||
networkMap := &types.NetworkMap{
|
||||
Network: &types.Network{Net: *ipnet, Serial: 1000},
|
||||
Peers: []*types.ComponentPeer{{
|
||||
Network: &nmdata.Network{Net: *ipnet, Serial: 1000},
|
||||
Peers: []*nmdata.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: []*types.ComponentPeer{{
|
||||
OfflinePeers: []*nmdata.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{
|
||||
Routes: []*nmdata.Route{
|
||||
{
|
||||
ID: "route1",
|
||||
Network: netip.MustParsePrefix("10.0.0.0/24"),
|
||||
@@ -1169,18 +1170,18 @@ func TestToSyncResponse(t *testing.T) {
|
||||
},
|
||||
}
|
||||
dnsName := "example.com"
|
||||
checks := []*posture.Checks{
|
||||
checks := []*nmdata.PostureChecks{
|
||||
{
|
||||
Checks: posture.ChecksDefinition{
|
||||
ProcessCheck: &posture.ProcessCheck{
|
||||
Processes: []posture.Process{{LinuxPath: "/usr/bin/netbird"}},
|
||||
Checks: nmdata.ChecksDefinition{
|
||||
ProcessCheck: &nmdata.ProcessCheck{
|
||||
Processes: []nmdata.Process{{LinuxPath: "/usr/bin/netbird"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
dnsCache := &cache.DNSConfigCache{}
|
||||
accountSettings := &types.Settings{RoutingPeerDNSResolutionEnabled: true}
|
||||
response := grpc.ToSyncResponse(context.Background(), config, config.HttpConfig, config.DeviceAuthorizationFlow, peer, turnRelayToken, turnRelayToken, networkMap, dnsName, checks, dnsCache, accountSettings, nil, []string{}, int64(dnsForwarderPort))
|
||||
response := grpc.ToSyncResponse(context.Background(), config, config.HttpConfig, config.DeviceAuthorizationFlow, types.TwinPeer(peer), turnRelayToken, turnRelayToken, networkMap, dnsName, checks, dnsCache, types.TwinAccountSettings(accountSettings), nil, []string{}, int64(dnsForwarderPort))
|
||||
|
||||
assert.NotNil(t, response)
|
||||
// assert peer config
|
||||
@@ -1300,7 +1301,7 @@ func Test_RegisterPeerByUser(t *testing.T) {
|
||||
|
||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
requestBuffer := NewAccountRequestBuffer(ctx, s)
|
||||
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{})
|
||||
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}, nil)
|
||||
|
||||
am, err := BuildManager(context.Background(), nil, s, networkMapController, job.NewJobManager(nil, s, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
||||
assert.NoError(t, err)
|
||||
@@ -1391,7 +1392,7 @@ func Test_RegisterPeerBySetupKey(t *testing.T) {
|
||||
|
||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
requestBuffer := NewAccountRequestBuffer(ctx, s)
|
||||
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{})
|
||||
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}, nil)
|
||||
|
||||
am, err := BuildManager(context.Background(), nil, s, networkMapController, job.NewJobManager(nil, s, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
||||
assert.NoError(t, err)
|
||||
@@ -1550,7 +1551,7 @@ func Test_RegisterPeerRollbackOnFailure(t *testing.T) {
|
||||
|
||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
requestBuffer := NewAccountRequestBuffer(ctx, s)
|
||||
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{})
|
||||
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}, nil)
|
||||
|
||||
am, err := BuildManager(context.Background(), nil, s, networkMapController, job.NewJobManager(nil, s, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
||||
assert.NoError(t, err)
|
||||
@@ -1635,7 +1636,7 @@ func Test_LoginPeer(t *testing.T) {
|
||||
|
||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
requestBuffer := NewAccountRequestBuffer(ctx, s)
|
||||
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{})
|
||||
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}, nil)
|
||||
|
||||
am, err := BuildManager(context.Background(), nil, s, networkMapController, job.NewJobManager(nil, s, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package permissions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/roles"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
|
||||
var allOps = []operations.Operation{operations.Read, operations.Create, operations.Update, operations.Delete}
|
||||
|
||||
// TestAgentNetworkAdminRole pins the delegated-admin contract: full control
|
||||
// over the whole agent_network area (parent grant cascades to every
|
||||
// submodule), read-only on the account objects needed to build policies,
|
||||
// and nothing else in the account.
|
||||
func TestAgentNetworkAdminRole(t *testing.T) {
|
||||
manager := NewManager(nil)
|
||||
ctx := context.Background()
|
||||
|
||||
role, ok := roles.RolesMap[types.UserRoleAgentNetworkAdmin]
|
||||
require.True(t, ok, "agent_network_admin must exist in RolesMap")
|
||||
|
||||
agentNetworkModules := []modules.Module{
|
||||
modules.AgentNetwork,
|
||||
modules.AgentNetworkProviders,
|
||||
modules.AgentNetworkPolicies,
|
||||
modules.AgentNetworkGuardrails,
|
||||
modules.AgentNetworkBudgets,
|
||||
modules.AgentNetworkUsage,
|
||||
modules.AgentNetworkLogs,
|
||||
modules.AgentNetworkSettings,
|
||||
}
|
||||
for _, m := range agentNetworkModules {
|
||||
for _, op := range allOps {
|
||||
assert.True(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, op),
|
||||
"agent_network_admin must have %s on %s", op, m)
|
||||
}
|
||||
}
|
||||
|
||||
// Settings read rides along because GET /api/accounts (which the
|
||||
// dashboard needs to boot) validates it, like network_admin.
|
||||
for _, m := range []modules.Module{modules.Users, modules.Groups, modules.Peers, modules.Accounts, modules.Settings} {
|
||||
assert.True(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, operations.Read),
|
||||
"agent_network_admin must read %s to build policies and load the dashboard", m)
|
||||
for _, op := range []operations.Operation{operations.Create, operations.Update, operations.Delete} {
|
||||
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, op),
|
||||
"agent_network_admin must not have %s on %s", op, m)
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range []modules.Module{modules.Networks, modules.Dns, modules.SetupKeys, modules.Routes} {
|
||||
for _, op := range allOps {
|
||||
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, op),
|
||||
"agent_network_admin must not have %s on %s", op, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestUsageViewerRole pins the least-privilege cost role: read on the
|
||||
// aggregated usage overview plus read-only on the resources its filters
|
||||
// and display columns resolve against (users, groups, peers, the provider
|
||||
// list) — no policies, no request-level logs (which can contain captured
|
||||
// prompts), nothing else in the account.
|
||||
func TestUsageViewerRole(t *testing.T) {
|
||||
manager := NewManager(nil)
|
||||
ctx := context.Background()
|
||||
|
||||
role, ok := roles.RolesMap[types.UserRoleUsageViewer]
|
||||
require.True(t, ok, "usage_viewer must exist in RolesMap")
|
||||
|
||||
readOnly := []modules.Module{
|
||||
modules.AgentNetworkUsage,
|
||||
modules.AgentNetworkProviders,
|
||||
modules.Users,
|
||||
modules.Groups,
|
||||
modules.Peers,
|
||||
}
|
||||
for _, m := range readOnly {
|
||||
assert.True(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, operations.Read),
|
||||
"usage_viewer must read %s for the usage view and its filters", m)
|
||||
for _, op := range []operations.Operation{operations.Create, operations.Update, operations.Delete} {
|
||||
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, op),
|
||||
"usage_viewer must not have %s on %s", op, m)
|
||||
}
|
||||
}
|
||||
|
||||
denied := []modules.Module{
|
||||
modules.AgentNetwork,
|
||||
modules.AgentNetworkPolicies,
|
||||
modules.AgentNetworkGuardrails,
|
||||
modules.AgentNetworkBudgets,
|
||||
modules.AgentNetworkLogs,
|
||||
modules.AgentNetworkSettings,
|
||||
modules.Networks,
|
||||
modules.SetupKeys,
|
||||
}
|
||||
for _, m := range denied {
|
||||
for _, op := range allOps {
|
||||
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, op),
|
||||
"usage_viewer must not have %s on %s", op, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBillingAdminRoleResolves pins that billing_admin has a proper entry
|
||||
// in the permission map. Its plan/seat/invoice permissions are enforced
|
||||
// outside this map; management-side it carries the regular User baseline
|
||||
// instead of failing role resolution.
|
||||
func TestBillingAdminRoleResolves(t *testing.T) {
|
||||
manager := NewManager(nil)
|
||||
ctx := context.Background()
|
||||
|
||||
role, ok := roles.RolesMap[types.UserRoleBillingAdmin]
|
||||
require.True(t, ok, "billing_admin must exist in RolesMap")
|
||||
|
||||
permissions, err := manager.GetPermissionsByRole(ctx, types.UserRoleBillingAdmin)
|
||||
require.NoError(t, err, "billing_admin role must resolve")
|
||||
require.NotEmpty(t, permissions)
|
||||
|
||||
for _, m := range []modules.Module{modules.AgentNetwork, modules.Networks, modules.Users, modules.Peers} {
|
||||
for _, op := range allOps {
|
||||
assert.False(t, manager.ValidateRoleModuleAccess(ctx, "account", role, m, op),
|
||||
"billing_admin must not have %s on %s", op, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewRolesParse pins the API role strings, which are permanent once
|
||||
// released.
|
||||
func TestNewRolesParse(t *testing.T) {
|
||||
assert.Equal(t, types.UserRoleAgentNetworkAdmin, types.StrRoleToUserRole("agent_network_admin"))
|
||||
assert.Equal(t, types.UserRoleUsageViewer, types.StrRoleToUserRole("usage_viewer"))
|
||||
assert.Equal(t, types.UserRoleBillingAdmin, types.StrRoleToUserRole("billing_admin"))
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package permissions
|
||||
|
||||
//go:generate go run github.com/golang/mock/mockgen -package permissions -destination=manager_mock.go -source=./manager.go -build_flags=-mod=mod
|
||||
//go:generate go tool mockgen -package permissions -destination=manager_mock.go -source=./manager.go -build_flags=-mod=mod
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./manager.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package permissions -destination=manager_mock.go -source=./manager.go -build_flags=-mod=mod
|
||||
//
|
||||
|
||||
// Package permissions is a generated GoMock package.
|
||||
package permissions
|
||||
@@ -8,18 +13,19 @@ import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
account "github.com/netbirdio/netbird/management/server/account"
|
||||
modules "github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||
operations "github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||
roles "github.com/netbirdio/netbird/management/server/permissions/roles"
|
||||
types "github.com/netbirdio/netbird/management/server/types"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockManager is a mock of Manager interface.
|
||||
type MockManager struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockManagerMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockManagerMockRecorder is the mock recorder for MockManager.
|
||||
@@ -49,7 +55,7 @@ func (m *MockManager) GetPermissionsByRole(ctx context.Context, role types.UserR
|
||||
}
|
||||
|
||||
// GetPermissionsByRole indicates an expected call of GetPermissionsByRole.
|
||||
func (mr *MockManagerMockRecorder) GetPermissionsByRole(ctx, role interface{}) *gomock.Call {
|
||||
func (mr *MockManagerMockRecorder) GetPermissionsByRole(ctx, role any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPermissionsByRole", reflect.TypeOf((*MockManager)(nil).GetPermissionsByRole), ctx, role)
|
||||
}
|
||||
@@ -61,7 +67,7 @@ func (m *MockManager) SetAccountManager(accountManager account.Manager) {
|
||||
}
|
||||
|
||||
// SetAccountManager indicates an expected call of SetAccountManager.
|
||||
func (mr *MockManagerMockRecorder) SetAccountManager(accountManager interface{}) *gomock.Call {
|
||||
func (mr *MockManagerMockRecorder) SetAccountManager(accountManager any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAccountManager", reflect.TypeOf((*MockManager)(nil).SetAccountManager), accountManager)
|
||||
}
|
||||
@@ -76,7 +82,7 @@ func (m *MockManager) ValidateAccountAccess(ctx context.Context, accountID strin
|
||||
}
|
||||
|
||||
// ValidateAccountAccess indicates an expected call of ValidateAccountAccess.
|
||||
func (mr *MockManagerMockRecorder) ValidateAccountAccess(ctx, accountID, user, allowOwnerAndAdmin interface{}) *gomock.Call {
|
||||
func (mr *MockManagerMockRecorder) ValidateAccountAccess(ctx, accountID, user, allowOwnerAndAdmin any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateAccountAccess", reflect.TypeOf((*MockManager)(nil).ValidateAccountAccess), ctx, accountID, user, allowOwnerAndAdmin)
|
||||
}
|
||||
@@ -90,7 +96,7 @@ func (m *MockManager) ValidateRoleModuleAccess(ctx context.Context, accountID st
|
||||
}
|
||||
|
||||
// ValidateRoleModuleAccess indicates an expected call of ValidateRoleModuleAccess.
|
||||
func (mr *MockManagerMockRecorder) ValidateRoleModuleAccess(ctx, accountID, role, module, operation interface{}) *gomock.Call {
|
||||
func (mr *MockManagerMockRecorder) ValidateRoleModuleAccess(ctx, accountID, role, module, operation any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateRoleModuleAccess", reflect.TypeOf((*MockManager)(nil).ValidateRoleModuleAccess), ctx, accountID, role, module, operation)
|
||||
}
|
||||
@@ -106,7 +112,7 @@ func (m *MockManager) ValidateUserPermissions(ctx context.Context, accountID, us
|
||||
}
|
||||
|
||||
// ValidateUserPermissions indicates an expected call of ValidateUserPermissions.
|
||||
func (mr *MockManagerMockRecorder) ValidateUserPermissions(ctx, accountID, userID, module, operation interface{}) *gomock.Call {
|
||||
func (mr *MockManagerMockRecorder) ValidateUserPermissions(ctx, accountID, userID, module, operation any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateUserPermissions", reflect.TypeOf((*MockManager)(nil).ValidateUserPermissions), ctx, accountID, userID, module, operation)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package roles
|
||||
|
||||
import (
|
||||
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
|
||||
// AgentNetworkAdmin is the delegated administrator for the Agent Network
|
||||
// area: full control over providers, policies, guardrails, budgets, usage,
|
||||
// logs, and its settings, plus read-only visibility into the account
|
||||
// objects needed to build policies (users, groups, peers) and the account
|
||||
// settings/meta read the dashboard needs to boot (GET /api/accounts
|
||||
// validates Settings read, same as network_admin). Nothing else in the
|
||||
// account is visible.
|
||||
var AgentNetworkAdmin = RolePermissions{
|
||||
Role: types.UserRoleAgentNetworkAdmin,
|
||||
AutoAllowNew: map[operations.Operation]bool{
|
||||
operations.Read: false,
|
||||
operations.Create: false,
|
||||
operations.Update: false,
|
||||
operations.Delete: false,
|
||||
},
|
||||
Permissions: Permissions{
|
||||
modules.AgentNetwork: {
|
||||
operations.Read: true,
|
||||
operations.Create: true,
|
||||
operations.Update: true,
|
||||
operations.Delete: true,
|
||||
},
|
||||
modules.Users: {
|
||||
operations.Read: true,
|
||||
operations.Create: false,
|
||||
operations.Update: false,
|
||||
operations.Delete: false,
|
||||
},
|
||||
modules.Groups: {
|
||||
operations.Read: true,
|
||||
operations.Create: false,
|
||||
operations.Update: false,
|
||||
operations.Delete: false,
|
||||
},
|
||||
modules.Peers: {
|
||||
operations.Read: true,
|
||||
operations.Create: false,
|
||||
operations.Update: false,
|
||||
operations.Delete: false,
|
||||
},
|
||||
modules.Accounts: {
|
||||
operations.Read: true,
|
||||
operations.Create: false,
|
||||
operations.Update: false,
|
||||
operations.Delete: false,
|
||||
},
|
||||
modules.Settings: {
|
||||
operations.Read: true,
|
||||
operations.Create: false,
|
||||
operations.Update: false,
|
||||
operations.Delete: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package roles
|
||||
|
||||
import (
|
||||
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
|
||||
// BillingAdmin manages plans, seats, and invoices, which are enforced
|
||||
// outside this permission map (NetBird Cloud). Management-side it carries
|
||||
// the regular User baseline; the explicit entry keeps role resolution from
|
||||
// failing with a role-not-found error.
|
||||
var BillingAdmin = RolePermissions{
|
||||
Role: types.UserRoleBillingAdmin,
|
||||
AutoAllowNew: map[operations.Operation]bool{
|
||||
operations.Read: false,
|
||||
operations.Create: false,
|
||||
operations.Update: false,
|
||||
operations.Delete: false,
|
||||
},
|
||||
}
|
||||
@@ -15,9 +15,12 @@ type RolePermissions struct {
|
||||
type Permissions map[modules.Module]map[operations.Operation]bool
|
||||
|
||||
var RolesMap = map[types.UserRole]RolePermissions{
|
||||
types.UserRoleOwner: Owner,
|
||||
types.UserRoleAdmin: Admin,
|
||||
types.UserRoleUser: User,
|
||||
types.UserRoleAuditor: Auditor,
|
||||
types.UserRoleNetworkAdmin: NetworkAdmin,
|
||||
types.UserRoleOwner: Owner,
|
||||
types.UserRoleAdmin: Admin,
|
||||
types.UserRoleUser: User,
|
||||
types.UserRoleAuditor: Auditor,
|
||||
types.UserRoleNetworkAdmin: NetworkAdmin,
|
||||
types.UserRoleAgentNetworkAdmin: AgentNetworkAdmin,
|
||||
types.UserRoleUsageViewer: UsageViewer,
|
||||
types.UserRoleBillingAdmin: BillingAdmin,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package roles
|
||||
|
||||
import (
|
||||
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
||||
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
|
||||
// UsageViewer is the regular User baseline plus read access to the
|
||||
// aggregated Agent Network usage and cost overview, and read-only access
|
||||
// to the resources the usage filters and display columns resolve against:
|
||||
// users and groups (identity filters and name resolution), peers (agent
|
||||
// principals in the caller column), and the provider list (provider and
|
||||
// model filter options — the manager redacts connection config such as
|
||||
// upstream URLs and operator-supplied header values for callers holding
|
||||
// read without update). It sees no policies and no account-wide
|
||||
// request-level access logs (which can contain captured prompts); its own
|
||||
// requests remain readable through the self-scoped endpoints, like any
|
||||
// caller's.
|
||||
var UsageViewer = RolePermissions{
|
||||
Role: types.UserRoleUsageViewer,
|
||||
AutoAllowNew: map[operations.Operation]bool{
|
||||
operations.Read: false,
|
||||
operations.Create: false,
|
||||
operations.Update: false,
|
||||
operations.Delete: false,
|
||||
},
|
||||
Permissions: Permissions{
|
||||
modules.AgentNetworkUsage: {
|
||||
operations.Read: true,
|
||||
operations.Create: false,
|
||||
operations.Update: false,
|
||||
operations.Delete: false,
|
||||
},
|
||||
modules.AgentNetworkProviders: {
|
||||
operations.Read: true,
|
||||
operations.Create: false,
|
||||
operations.Update: false,
|
||||
operations.Delete: false,
|
||||
},
|
||||
modules.Users: {
|
||||
operations.Read: true,
|
||||
operations.Create: false,
|
||||
operations.Update: false,
|
||||
operations.Delete: false,
|
||||
},
|
||||
modules.Groups: {
|
||||
operations.Read: true,
|
||||
operations.Create: false,
|
||||
operations.Update: false,
|
||||
operations.Delete: false,
|
||||
},
|
||||
modules.Peers: {
|
||||
operations.Read: true,
|
||||
operations.Create: false,
|
||||
operations.Update: false,
|
||||
operations.Delete: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
package posture
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
)
|
||||
|
||||
// diffFrom builds a MetaDiff from the old/new snapshots AffectsPosture replays against.
|
||||
func diffFrom(oldMeta, newMeta nbpeer.PeerSystemMeta, oldLoc, newLoc nbpeer.Location) *nbpeer.MetaDiff {
|
||||
return &nbpeer.MetaDiff{
|
||||
OldMeta: oldMeta,
|
||||
NewMeta: newMeta,
|
||||
OldLocation: oldLoc,
|
||||
NewLocation: newLoc,
|
||||
}
|
||||
}
|
||||
|
||||
func checks(def ChecksDefinition) []*Checks {
|
||||
return []*Checks{{Checks: def}}
|
||||
}
|
||||
|
||||
func TestAffectsPosture_NilDiff(t *testing.T) {
|
||||
assert.False(t, AffectsPosture(context.Background(), nil, checks(ChecksDefinition{
|
||||
NBVersionCheck: &NBVersionCheck{MinVersion: "1.0.0"},
|
||||
})))
|
||||
}
|
||||
|
||||
func TestAffectsPosture_NBVersion(t *testing.T) {
|
||||
c := checks(ChecksDefinition{NBVersionCheck: &NBVersionCheck{MinVersion: "1.2.0"}})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
oldVer, newVer string
|
||||
want bool
|
||||
}{
|
||||
{"both above min, no flip", "1.3.0", "1.4.0", false},
|
||||
{"both below min, no flip", "1.0.0", "1.1.0", false},
|
||||
{"crosses up below->above", "1.1.0", "1.3.0", true},
|
||||
{"crosses down above->below", "1.3.0", "1.1.0", true},
|
||||
{"unparsable old only -> flip", "garbage", "1.3.0", true},
|
||||
{"unparsable both -> no flip", "garbage", "junk", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
diff := diffFrom(
|
||||
nbpeer.PeerSystemMeta{WtVersion: tt.oldVer},
|
||||
nbpeer.PeerSystemMeta{WtVersion: tt.newVer},
|
||||
nbpeer.Location{}, nbpeer.Location{},
|
||||
)
|
||||
assert.Equal(t, tt.want, AffectsPosture(context.Background(), diff, c))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAffectsPosture_OSVersion_KernelBumpWithinMin(t *testing.T) {
|
||||
c := checks(ChecksDefinition{OSVersionCheck: &OSVersionCheck{
|
||||
Linux: &MinKernelVersionCheck{MinKernelVersion: "5.0.0"},
|
||||
}})
|
||||
|
||||
// Kernel moves but stays above the minimum: verdict stays pass -> not affected.
|
||||
withinMin := diffFrom(
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "5.10.0-arch1"},
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "5.15.0-arch2"},
|
||||
nbpeer.Location{}, nbpeer.Location{},
|
||||
)
|
||||
assert.False(t, AffectsPosture(context.Background(), withinMin, c))
|
||||
|
||||
// Kernel drops below the minimum: verdict flips pass -> fail -> affected.
|
||||
crossesDown := diffFrom(
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "5.10.0-arch1"},
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "4.19.0-arch1"},
|
||||
nbpeer.Location{}, nbpeer.Location{},
|
||||
)
|
||||
assert.True(t, AffectsPosture(context.Background(), crossesDown, c))
|
||||
}
|
||||
|
||||
func TestAffectsPosture_OSVersion_GoOSSwitchFlipsVerdict(t *testing.T) {
|
||||
// Only Linux is constrained. An OS outside the switch (freebsd) passes; switching to a
|
||||
// failing linux kernel flips the verdict pass -> fail.
|
||||
c := checks(ChecksDefinition{OSVersionCheck: &OSVersionCheck{
|
||||
Linux: &MinKernelVersionCheck{MinKernelVersion: "6.0.0"},
|
||||
}})
|
||||
|
||||
diff := diffFrom(
|
||||
nbpeer.PeerSystemMeta{GoOS: "freebsd"},
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", KernelVersion: "4.19.0"},
|
||||
nbpeer.Location{}, nbpeer.Location{},
|
||||
)
|
||||
assert.True(t, AffectsPosture(context.Background(), diff, c))
|
||||
}
|
||||
|
||||
func TestAffectsPosture_Process_GoOSSwitchFlipsVerdict(t *testing.T) {
|
||||
// Process runs at a linux path. Switching GoOS to windows (no WindowsPath configured)
|
||||
// flips the verdict.
|
||||
c := checks(ChecksDefinition{ProcessCheck: &ProcessCheck{
|
||||
Processes: []Process{{LinuxPath: "/usr/bin/foo"}},
|
||||
}})
|
||||
|
||||
files := []nbpeer.File{{Path: "/usr/bin/foo", ProcessIsRunning: true}}
|
||||
diff := diffFrom(
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", Files: files},
|
||||
nbpeer.PeerSystemMeta{GoOS: "windows", Files: files},
|
||||
nbpeer.Location{}, nbpeer.Location{},
|
||||
)
|
||||
assert.True(t, AffectsPosture(context.Background(), diff, c))
|
||||
}
|
||||
|
||||
func TestAffectsPosture_Process_UnrelatedFileChange(t *testing.T) {
|
||||
// A tracked process stays running while an unrelated file is added: the verdict does
|
||||
// not move, so posture is not affected.
|
||||
c := checks(ChecksDefinition{ProcessCheck: &ProcessCheck{
|
||||
Processes: []Process{{LinuxPath: "/usr/bin/foo"}},
|
||||
}})
|
||||
|
||||
diff := diffFrom(
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", Files: []nbpeer.File{
|
||||
{Path: "/usr/bin/foo", ProcessIsRunning: true},
|
||||
}},
|
||||
nbpeer.PeerSystemMeta{GoOS: "linux", Files: []nbpeer.File{
|
||||
{Path: "/usr/bin/foo", ProcessIsRunning: true},
|
||||
{Path: "/usr/bin/bar", ProcessIsRunning: true},
|
||||
}},
|
||||
nbpeer.Location{}, nbpeer.Location{},
|
||||
)
|
||||
assert.False(t, AffectsPosture(context.Background(), diff, c))
|
||||
}
|
||||
|
||||
func TestAffectsPosture_GeoLocation(t *testing.T) {
|
||||
c := checks(ChecksDefinition{GeoLocationCheck: &GeoLocationCheck{
|
||||
Action: CheckActionAllow,
|
||||
Locations: []Location{{CountryCode: "DE"}},
|
||||
}})
|
||||
|
||||
// Moving within allowed countries keeps the verdict; moving out flips it.
|
||||
stayAllowed := diffFrom(
|
||||
nbpeer.PeerSystemMeta{}, nbpeer.PeerSystemMeta{},
|
||||
nbpeer.Location{CountryCode: "DE", CityName: "Berlin"},
|
||||
nbpeer.Location{CountryCode: "DE", CityName: "Munich"},
|
||||
)
|
||||
assert.False(t, AffectsPosture(context.Background(), stayAllowed, c))
|
||||
|
||||
moveOut := diffFrom(
|
||||
nbpeer.PeerSystemMeta{}, nbpeer.PeerSystemMeta{},
|
||||
nbpeer.Location{CountryCode: "DE"},
|
||||
nbpeer.Location{CountryCode: "FR"},
|
||||
)
|
||||
assert.True(t, AffectsPosture(context.Background(), moveOut, c))
|
||||
}
|
||||
|
||||
func TestAffectsPosture_PeerNetworkRange_ConnectionIP(t *testing.T) {
|
||||
// The check reads the connection IP. Moving out of the allowed range flips the verdict;
|
||||
// moving within it does not.
|
||||
_, allowed, _ := net.ParseCIDR("10.0.0.0/8")
|
||||
c := checks(ChecksDefinition{PeerNetworkRangeCheck: &PeerNetworkRangeCheck{
|
||||
Action: CheckActionAllow,
|
||||
Ranges: []netip.Prefix{netip.MustParsePrefix(allowed.String())},
|
||||
}})
|
||||
|
||||
movesOutOfRange := diffFrom(
|
||||
nbpeer.PeerSystemMeta{}, nbpeer.PeerSystemMeta{},
|
||||
nbpeer.Location{ConnectionIP: net.ParseIP("10.1.2.3")},
|
||||
nbpeer.Location{ConnectionIP: net.ParseIP("8.8.8.8")},
|
||||
)
|
||||
assert.True(t, AffectsPosture(context.Background(), movesOutOfRange, c))
|
||||
|
||||
staysInRange := diffFrom(
|
||||
nbpeer.PeerSystemMeta{}, nbpeer.PeerSystemMeta{},
|
||||
nbpeer.Location{ConnectionIP: net.ParseIP("10.1.2.3")},
|
||||
nbpeer.Location{ConnectionIP: net.ParseIP("10.9.9.9")},
|
||||
)
|
||||
assert.False(t, AffectsPosture(context.Background(), staysInRange, c))
|
||||
}
|
||||
|
||||
func TestAffectsPosture_IrrelevantFieldChange(t *testing.T) {
|
||||
// Hostname changes but no check reads it: not affected even with checks present.
|
||||
c := checks(ChecksDefinition{
|
||||
NBVersionCheck: &NBVersionCheck{MinVersion: "1.0.0"},
|
||||
GeoLocationCheck: &GeoLocationCheck{Action: CheckActionAllow, Locations: []Location{{CountryCode: "DE"}}},
|
||||
})
|
||||
|
||||
diff := diffFrom(
|
||||
nbpeer.PeerSystemMeta{Hostname: "old", WtVersion: "1.5.0"},
|
||||
nbpeer.PeerSystemMeta{Hostname: "new", WtVersion: "1.5.0"},
|
||||
nbpeer.Location{CountryCode: "DE"}, nbpeer.Location{CountryCode: "DE"},
|
||||
)
|
||||
assert.False(t, AffectsPosture(context.Background(), diff, c))
|
||||
}
|
||||
|
||||
func TestAffectsPosture_NoChecks(t *testing.T) {
|
||||
diff := diffFrom(
|
||||
nbpeer.PeerSystemMeta{WtVersion: "1.0.0"},
|
||||
nbpeer.PeerSystemMeta{WtVersion: "2.0.0"},
|
||||
nbpeer.Location{}, nbpeer.Location{},
|
||||
)
|
||||
assert.False(t, AffectsPosture(context.Background(), diff, nil))
|
||||
}
|
||||
@@ -7,7 +7,6 @@ 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"
|
||||
@@ -55,46 +54,6 @@ type Checks struct {
|
||||
Checks ChecksDefinition `gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
// AffectsPosture reports whether the change in diff flips the verdict of any check. It
|
||||
// replays each check against the peer's old and new state and compares verdicts, so a
|
||||
// change that moves a field but stays the right side of a threshold (e.g. a kernel bump
|
||||
// still above the minimum) does not force a re-evaluation. See verdictChanged for how an
|
||||
// evaluation error counts.
|
||||
func AffectsPosture(ctx context.Context, diff *nbpeer.MetaDiff, checks []*Checks) bool {
|
||||
if diff == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
oldPeer := nbpeer.Peer{Meta: diff.OldMeta, Location: diff.OldLocation}
|
||||
newPeer := nbpeer.Peer{Meta: diff.NewMeta, Location: diff.NewLocation}
|
||||
|
||||
for _, c := range checks {
|
||||
for _, check := range c.GetChecks() {
|
||||
if verdictChanged(ctx, check, oldPeer, newPeer) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// verdictChanged replays check against old and new state and reports whether the verdict
|
||||
// differs. Like callers, it treats an evaluation error as deny: two errors are the same
|
||||
// verdict (no change), an error on one side only is a flip.
|
||||
func verdictChanged(ctx context.Context, check Check, oldPeer, newPeer nbpeer.Peer) bool {
|
||||
oldPass, oldErr := check.Check(ctx, oldPeer)
|
||||
newPass, newErr := check.Check(ctx, newPeer)
|
||||
|
||||
oldVerdict := oldPass && (oldErr == nil)
|
||||
newVerdict := newPass && (newErr == nil)
|
||||
changed := oldVerdict != newVerdict
|
||||
|
||||
log.WithContext(ctx).Tracef("posture check %s replay: verdict %t -> %t (changed=%t), errs: %v -> %v",
|
||||
check.Name(), oldVerdict, newVerdict, changed, oldErr, newErr)
|
||||
|
||||
return changed
|
||||
}
|
||||
|
||||
// ChecksDefinition contains definition of actual check
|
||||
type ChecksDefinition struct {
|
||||
NBVersionCheck *NBVersionCheck `json:",omitempty"`
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"go.uber.org/mock/gomock"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -1201,7 +1201,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) {
|
||||
peer1Routes, err := am.GetNetworkMap(context.Background(), peer1ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, peer1Routes.Routes, 1, "we should receive one route for peer1")
|
||||
require.True(t, expectedRoute.Equal(peer1Routes.Routes[0]), "received route should be equal")
|
||||
require.True(t, types.TwinRoute(expectedRoute).Equal(peer1Routes.Routes[0]), "received route should be equal")
|
||||
|
||||
peer2Routes, err := am.GetNetworkMap(context.Background(), peer2ID)
|
||||
require.NoError(t, err)
|
||||
@@ -1299,7 +1299,7 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, *update_channel.
|
||||
|
||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
requestBuffer := NewAccountRequestBuffer(ctx, store)
|
||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{})
|
||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}, nil)
|
||||
|
||||
am, err := BuildManager(context.Background(), nil, store, networkMapController, job.NewJobManager(nil, store, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package settings
|
||||
|
||||
//go:generate go run github.com/golang/mock/mockgen -package settings -destination=manager_mock.go -source=./manager.go -build_flags=-mod=mod
|
||||
//go:generate go tool mockgen -package settings -destination=manager_mock.go -source=./manager.go -build_flags=-mod=mod
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./manager.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package settings -destination=manager_mock.go -source=./manager.go -build_flags=-mod=mod
|
||||
//
|
||||
|
||||
// Package settings is a generated GoMock package.
|
||||
package settings
|
||||
@@ -9,15 +14,16 @@ import (
|
||||
netip "net/netip"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
extra_settings "github.com/netbirdio/netbird/management/server/integrations/extra_settings"
|
||||
types "github.com/netbirdio/netbird/management/server/types"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockManager is a mock of Manager interface.
|
||||
type MockManager struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockManagerMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockManagerMockRecorder is the mock recorder for MockManager.
|
||||
@@ -37,6 +43,22 @@ func (m *MockManager) EXPECT() *MockManagerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// 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 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEffectiveNetworkRanges", reflect.TypeOf((*MockManager)(nil).GetEffectiveNetworkRanges), ctx, accountID)
|
||||
}
|
||||
|
||||
// GetExtraSettings mocks base method.
|
||||
func (m *MockManager) GetExtraSettings(ctx context.Context, accountID string) (*types.ExtraSettings, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -47,7 +69,7 @@ func (m *MockManager) GetExtraSettings(ctx context.Context, accountID string) (*
|
||||
}
|
||||
|
||||
// GetExtraSettings indicates an expected call of GetExtraSettings.
|
||||
func (mr *MockManagerMockRecorder) GetExtraSettings(ctx, accountID interface{}) *gomock.Call {
|
||||
func (mr *MockManagerMockRecorder) GetExtraSettings(ctx, accountID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExtraSettings", reflect.TypeOf((*MockManager)(nil).GetExtraSettings), ctx, accountID)
|
||||
}
|
||||
@@ -76,7 +98,7 @@ func (m *MockManager) GetSettings(ctx context.Context, accountID, userID string)
|
||||
}
|
||||
|
||||
// GetSettings indicates an expected call of GetSettings.
|
||||
func (mr *MockManagerMockRecorder) GetSettings(ctx, accountID, userID interface{}) *gomock.Call {
|
||||
func (mr *MockManagerMockRecorder) GetSettings(ctx, accountID, userID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSettings", reflect.TypeOf((*MockManager)(nil).GetSettings), ctx, accountID, userID)
|
||||
}
|
||||
@@ -91,23 +113,7 @@ func (m *MockManager) UpdateExtraSettings(ctx context.Context, accountID, userID
|
||||
}
|
||||
|
||||
// UpdateExtraSettings indicates an expected call of UpdateExtraSettings.
|
||||
func (mr *MockManagerMockRecorder) UpdateExtraSettings(ctx, accountID, userID, extraSettings interface{}) *gomock.Call {
|
||||
func (mr *MockManagerMockRecorder) UpdateExtraSettings(ctx, accountID, userID, extraSettings any) *gomock.Call {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -3167,9 +3167,9 @@ func getGormConfig() *gorm.Config {
|
||||
|
||||
// newPostgresStore initializes a new Postgres store.
|
||||
func newPostgresStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {
|
||||
dsn, ok := lookupDSNEnv(postgresDsnEnv, postgresDsnEnvLegacy)
|
||||
dsn, ok := lookupDSNEnv(PostgresDsnEnv, PostgresDsnEnvLegacy)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s is not set", postgresDsnEnv)
|
||||
return nil, fmt.Errorf("%s is not set", PostgresDsnEnv)
|
||||
}
|
||||
return NewPostgresqlStore(ctx, dsn, metrics, skipMigration)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/management/server/integration_reference"
|
||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
|
||||
@@ -21,6 +20,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/posture"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/integration_reference"
|
||||
)
|
||||
|
||||
// TestGetAccount_LoadsCustomDomains verifies GetAccount populates account.Domains.
|
||||
|
||||
@@ -573,7 +573,7 @@ func TestSqlStore_SavePeer(t *testing.T) {
|
||||
|
||||
numOfFields, err := populateFields.PopulateAll(reflectedMetadata)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 32, numOfFields)
|
||||
assert.Equal(t, 33, numOfFields)
|
||||
|
||||
// save status of non-existing peer
|
||||
peer := &nbpeer.Peer{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package store
|
||||
|
||||
//go:generate go run github.com/golang/mock/mockgen -package store -destination=store_mock.go -source=./store.go -build_flags=-mod=mod
|
||||
//go:generate go tool mockgen -package store -destination=store_mock.go -source=./store.go -build_flags=-mod=mod
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -439,8 +439,8 @@ type AgentNetworkMetrics struct {
|
||||
}
|
||||
|
||||
const (
|
||||
postgresDsnEnv = "NB_STORE_ENGINE_POSTGRES_DSN"
|
||||
postgresDsnEnvLegacy = "NETBIRD_STORE_ENGINE_POSTGRES_DSN"
|
||||
PostgresDsnEnv = "NB_STORE_ENGINE_POSTGRES_DSN"
|
||||
PostgresDsnEnvLegacy = "NETBIRD_STORE_ENGINE_POSTGRES_DSN"
|
||||
mysqlDsnEnv = "NB_STORE_ENGINE_MYSQL_DSN"
|
||||
mysqlDsnEnvLegacy = "NETBIRD_STORE_ENGINE_MYSQL_DSN"
|
||||
)
|
||||
@@ -784,7 +784,7 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind types.Engine)
|
||||
}
|
||||
|
||||
func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind types.Engine) (*SqlStore, func(), error) {
|
||||
dsn, ok := lookupDSNEnv(postgresDsnEnv, postgresDsnEnvLegacy)
|
||||
dsn, ok := lookupDSNEnv(PostgresDsnEnv, PostgresDsnEnvLegacy)
|
||||
if !ok || dsn == "" {
|
||||
var err error
|
||||
_, dsn, err = testutil.CreatePostgresTestContainer()
|
||||
@@ -794,7 +794,7 @@ func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind types.Eng
|
||||
}
|
||||
|
||||
if dsn == "" {
|
||||
return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv)
|
||||
return nil, nil, fmt.Errorf("%s is not set", PostgresDsnEnv)
|
||||
}
|
||||
|
||||
db, err := openDBWithRetry(dsn, kind, 5)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/rs/xid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -18,8 +17,6 @@ import (
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
proxydomain "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones/records"
|
||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
|
||||
@@ -28,11 +25,12 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/util"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTTL = 300
|
||||
// privateServiceDNSRecordTTL is short so proxy-peer changes propagate quickly to clients.
|
||||
privateServiceDNSRecordTTL = 5
|
||||
DefaultPeerLoginExpiration = 24 * time.Hour
|
||||
@@ -384,94 +382,11 @@ func peerInDistributionGroups(peerGroups LookupMap, distributionGroups []string)
|
||||
}
|
||||
|
||||
func (a *Account) GetPeersCustomZone(ctx context.Context, dnsDomain string) nbdns.CustomZone {
|
||||
var merr *multierror.Error
|
||||
|
||||
if dnsDomain == "" {
|
||||
log.WithContext(ctx).Error("no dns domain is set, returning empty zone")
|
||||
return nbdns.CustomZone{}
|
||||
twins := make(map[string]*nmdata.Peer, len(a.Peers))
|
||||
for id, p := range a.Peers {
|
||||
twins[id] = twinPeer(p)
|
||||
}
|
||||
|
||||
customZone := nbdns.CustomZone{
|
||||
Domain: dns.Fqdn(dnsDomain),
|
||||
Records: make([]nbdns.SimpleRecord, 0, len(a.Peers)),
|
||||
}
|
||||
|
||||
domainSuffix := "." + dnsDomain
|
||||
|
||||
ipv6AllowedPeers := a.peerIPv6AllowedSet()
|
||||
|
||||
var sb strings.Builder
|
||||
for _, peer := range a.Peers {
|
||||
if peer.DNSLabel == "" {
|
||||
merr = multierror.Append(merr, fmt.Errorf("peer %s has an empty DNS label", peer.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
sb.Grow(len(peer.DNSLabel) + len(domainSuffix))
|
||||
sb.WriteString(peer.DNSLabel)
|
||||
sb.WriteString(domainSuffix)
|
||||
|
||||
fqdn := sb.String()
|
||||
customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
|
||||
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.
|
||||
// Capability changes (client upgrade/downgrade, --disable-ipv6 toggle) propagate
|
||||
// to other peers via SyncPeer/LoginPeer regardless of version change, so AAAA
|
||||
// records refresh when a peer first reports the IPv6 overlay capability.
|
||||
_, 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 {
|
||||
sb.Grow(len(extraLabel) + len(domainSuffix))
|
||||
sb.WriteString(extraLabel)
|
||||
sb.WriteString(domainSuffix)
|
||||
|
||||
extraFqdn := sb.String()
|
||||
customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
go func() {
|
||||
if merr != nil {
|
||||
log.WithContext(ctx).Errorf("error generating custom zone for account %s: %v", a.Id, merr)
|
||||
}
|
||||
}()
|
||||
|
||||
return customZone
|
||||
return fromTwinCustomZone(networkmap.PeersCustomZone(ctx, a.Id, dnsDomain, twins, a.peerIPv6AllowedSet()))
|
||||
}
|
||||
|
||||
// GetExpiredPeers returns peers that have been expired
|
||||
@@ -994,13 +909,13 @@ func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.P
|
||||
var peerInSources, peerInDestinations bool
|
||||
|
||||
if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" {
|
||||
sourcePeers, peerInSources = a.getPeerFromResource(rule.SourceResource, peer.ID)
|
||||
sourcePeers, peerInSources = a.getPeerFromResource(ctx, rule.SourceResource, peer.ID, policy.SourcePostureChecks, validatedPeersMap)
|
||||
} else {
|
||||
sourcePeers, peerInSources = a.getAllPeersFromGroups(ctx, rule.Sources, peer.ID, policy.SourcePostureChecks, validatedPeersMap)
|
||||
}
|
||||
|
||||
if rule.DestinationResource.Type == ResourceTypePeer && rule.DestinationResource.ID != "" {
|
||||
destinationPeers, peerInDestinations = a.getPeerFromResource(rule.DestinationResource, peer.ID)
|
||||
destinationPeers, peerInDestinations = a.getPeerFromResource(ctx, rule.DestinationResource, peer.ID, nil, validatedPeersMap)
|
||||
} else {
|
||||
destinationPeers, peerInDestinations = a.getAllPeersFromGroups(ctx, rule.Destinations, peer.ID, nil, validatedPeersMap)
|
||||
}
|
||||
@@ -1065,6 +980,26 @@ func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.P
|
||||
return peers, fwRules, authorizedUsers, sshEnabled
|
||||
}
|
||||
|
||||
// proxyTargetedDomainResourceIDs returns the set of domain network resource IDs
|
||||
// targeted by an enabled, non-terminated reverse-proxy service.
|
||||
func (a *Account) proxyTargetedDomainResourceIDs() map[string]struct{} {
|
||||
ids := make(map[string]struct{})
|
||||
for _, svc := range a.Services {
|
||||
if svc == nil || !svc.Enabled || svc.Terminated {
|
||||
continue
|
||||
}
|
||||
for _, target := range svc.Targets {
|
||||
if target == nil || !target.Enabled {
|
||||
continue
|
||||
}
|
||||
if target.TargetType == service.TargetTypeDomain {
|
||||
ids[target.TargetId] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (a *Account) getAllowedUserIDs() map[string]struct{} {
|
||||
users := make(map[string]struct{})
|
||||
for _, nbUser := range a.Users {
|
||||
@@ -1085,7 +1020,6 @@ func (a *Account) connResourcesGenerator(ctx context.Context, targetPeer *nbpeer
|
||||
peersExists := make(map[string]struct{})
|
||||
rules := make([]*FirewallRule, 0)
|
||||
peers := make([]*nbpeer.Peer, 0)
|
||||
targetComponent := targetPeer.ToComponent()
|
||||
|
||||
return func(rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int) {
|
||||
for _, peer := range groupPeers {
|
||||
@@ -1121,10 +1055,10 @@ func (a *Account) connResourcesGenerator(ctx context.Context, targetPeer *nbpeer
|
||||
if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 {
|
||||
rules = append(rules, &fr)
|
||||
} else {
|
||||
rules = append(rules, ExpandPortsAndRanges(fr, rule, targetComponent)...)
|
||||
rules = append(rules, ExpandPortsAndRanges(fr, rule, targetPeer)...)
|
||||
}
|
||||
|
||||
rules = AppendIPv6FirewallRule(rules, rulesExists, peer.ToComponent(), targetComponent, rule, FirewallRuleContext{
|
||||
rules = AppendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, FirewallRuleContext{
|
||||
Direction: direction,
|
||||
DirStr: strconv.Itoa(direction),
|
||||
ProtocolStr: string(protocol),
|
||||
@@ -1186,8 +1120,17 @@ func ruleHasDestination(rule *PolicyRule, peerID string, peerGroupIDs map[string
|
||||
// Important: Posture checks are applicable only to source group peers,
|
||||
// for destination group peers, call this method with an empty list of sourcePostureChecksIDs
|
||||
func (a *Account) getAllPeersFromGroups(ctx context.Context, groups []string, peerID string, sourcePostureChecksIDs []string, validatedPeersMap map[string]struct{}) ([]*nbpeer.Peer, bool) {
|
||||
return a.filterPolicyPeers(ctx, a.getUniquePeerIDsFromGroupsIDs(ctx, groups), peerID, sourcePostureChecksIDs, validatedPeersMap)
|
||||
}
|
||||
|
||||
// getPeerFromResource resolves a rule side that names a peer directly, admitting it
|
||||
// like a member of a group holding only that peer.
|
||||
func (a *Account) getPeerFromResource(ctx context.Context, resource Resource, peerID string, sourcePostureChecksIDs []string, validatedPeersMap map[string]struct{}) ([]*nbpeer.Peer, bool) {
|
||||
return a.filterPolicyPeers(ctx, []string{resource.ID}, peerID, sourcePostureChecksIDs, validatedPeersMap)
|
||||
}
|
||||
|
||||
func (a *Account) filterPolicyPeers(ctx context.Context, uniquePeerIDs []string, peerID string, sourcePostureChecksIDs []string, validatedPeersMap map[string]struct{}) ([]*nbpeer.Peer, bool) {
|
||||
peerInGroups := false
|
||||
uniquePeerIDs := a.getUniquePeerIDsFromGroupsIDs(ctx, groups)
|
||||
filteredPeers := make([]*nbpeer.Peer, 0, len(uniquePeerIDs))
|
||||
for _, p := range uniquePeerIDs {
|
||||
peer, ok := a.Peers[p]
|
||||
@@ -1216,19 +1159,6 @@ func (a *Account) getAllPeersFromGroups(ctx context.Context, groups []string, pe
|
||||
return filteredPeers, peerInGroups
|
||||
}
|
||||
|
||||
func (a *Account) getPeerFromResource(resource Resource, peerID string) ([]*nbpeer.Peer, bool) {
|
||||
peer := a.GetPeer(resource.ID)
|
||||
if peer == nil {
|
||||
return []*nbpeer.Peer{}, false
|
||||
}
|
||||
|
||||
if peer.ID == peerID {
|
||||
return []*nbpeer.Peer{}, true
|
||||
}
|
||||
|
||||
return []*nbpeer.Peer{peer}, false
|
||||
}
|
||||
|
||||
// validatePostureChecksOnPeer validates the posture checks on a peer
|
||||
func (a *Account) validatePostureChecksOnPeer(ctx context.Context, sourcePostureChecksID []string, peerID string) bool {
|
||||
peer, ok := a.Peers[peerID]
|
||||
@@ -1284,7 +1214,7 @@ func (a *Account) getRouteFirewallRules(ctx context.Context, peerID string, poli
|
||||
return fwRules
|
||||
}
|
||||
|
||||
func (a *Account) getRulePeers(rule *PolicyRule, postureChecks []string, peerID string, distributionPeers map[string]struct{}, validatedPeersMap map[string]struct{}) []*ComponentPeer {
|
||||
func (a *Account) getRulePeers(rule *PolicyRule, postureChecks []string, peerID string, distributionPeers map[string]struct{}, validatedPeersMap map[string]struct{}) []*nbpeer.Peer {
|
||||
distPeersWithPolicy := make(map[string]struct{})
|
||||
for _, id := range rule.Sources {
|
||||
group := a.Groups[id]
|
||||
@@ -1311,13 +1241,13 @@ func (a *Account) getRulePeers(rule *PolicyRule, postureChecks []string, peerID
|
||||
}
|
||||
}
|
||||
|
||||
distributionGroupPeers := make([]*ComponentPeer, 0, len(distPeersWithPolicy))
|
||||
distributionGroupPeers := make([]*nbpeer.Peer, 0, len(distPeersWithPolicy))
|
||||
for pID := range distPeersWithPolicy {
|
||||
peer := a.Peers[pID]
|
||||
if peer == nil {
|
||||
continue
|
||||
}
|
||||
distributionGroupPeers = append(distributionGroupPeers, peer.ToComponent())
|
||||
distributionGroupPeers = append(distributionGroupPeers, peer)
|
||||
}
|
||||
return distributionGroupPeers
|
||||
}
|
||||
@@ -1520,54 +1450,6 @@ func (a *Account) GetResourceRoutersMap() map[string]map[string]*routerTypes.Net
|
||||
return routers
|
||||
}
|
||||
|
||||
// forcesRoutingPeerDNSResolution reports whether the given peer must run
|
||||
// routing-peer DNS resolution regardless of the account-global
|
||||
// RoutingPeerDNSResolutionEnabled setting. It returns true when the peer is a
|
||||
// router for a domain network resource that is targeted by an enabled
|
||||
// reverse-proxy service, so the peer's DNS forwarder starts and can resolve
|
||||
// the target for the embedded proxy peers. Embedded proxy peers themselves are
|
||||
// handled at PeerConfig build time.
|
||||
func (a *Account) forcesRoutingPeerDNSResolution(peerID string, routers map[string]map[string]*routerTypes.NetworkRouter) bool {
|
||||
targeted := a.proxyTargetedDomainResourceIDs()
|
||||
if len(targeted) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, resource := range a.NetworkResources {
|
||||
if resource == nil || !resource.Enabled || resource.Type != resourceTypes.Domain {
|
||||
continue
|
||||
}
|
||||
if _, ok := targeted[resource.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, isRouter := routers[resource.NetworkID][peerID]; isRouter {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// proxyTargetedDomainResourceIDs returns the set of domain network resource IDs
|
||||
// targeted by an enabled, non-terminated reverse-proxy service.
|
||||
func (a *Account) proxyTargetedDomainResourceIDs() map[string]struct{} {
|
||||
ids := make(map[string]struct{})
|
||||
for _, svc := range a.Services {
|
||||
if svc == nil || !svc.Enabled || svc.Terminated {
|
||||
continue
|
||||
}
|
||||
for _, target := range svc.Targets {
|
||||
if target == nil || !target.Enabled {
|
||||
continue
|
||||
}
|
||||
if target.TargetType == service.TargetTypeDomain {
|
||||
ids[target.TargetId] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// getPoliciesSourcePeers collects all unique peers from the source groups defined in the given policies.
|
||||
func getPoliciesSourcePeers(policies []*Policy, groups map[string]*Group) map[string]struct{} {
|
||||
sourcePeers := make(map[string]struct{})
|
||||
@@ -1668,176 +1550,6 @@ func (a *Account) GetProxyPeers() map[string][]*nbpeer.Peer {
|
||||
return proxyPeers
|
||||
}
|
||||
|
||||
func (a *Account) InjectProxyPolicies(ctx context.Context) {
|
||||
if len(a.Services) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
proxyPeersByCluster := a.GetProxyPeers()
|
||||
if len(proxyPeersByCluster) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, service := range a.Services {
|
||||
if !service.Enabled {
|
||||
continue
|
||||
}
|
||||
a.injectServiceProxyPolicies(ctx, service, proxyPeersByCluster)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (a *Account) injectServiceProxyPolicies(ctx context.Context, service *service.Service, proxyPeersByCluster map[string][]*nbpeer.Peer) {
|
||||
proxyPeers := proxyPeersByCluster[service.ProxyCluster]
|
||||
for _, target := range service.Targets {
|
||||
if !target.Enabled {
|
||||
continue
|
||||
}
|
||||
a.injectTargetProxyPolicies(ctx, service, target, proxyPeers)
|
||||
}
|
||||
|
||||
a.injectPrivateServicePolicies(service, proxyPeers)
|
||||
}
|
||||
|
||||
// injectPrivateServicePolicies synthesises an in-memory ACL: AccessGroups → cluster proxy peers on TCP 80/443.
|
||||
func (a *Account) injectPrivateServicePolicies(svc *service.Service, proxyPeers []*nbpeer.Peer) {
|
||||
if !svc.Private {
|
||||
return
|
||||
}
|
||||
if len(svc.AccessGroups) == 0 {
|
||||
return
|
||||
}
|
||||
if len(proxyPeers) == 0 {
|
||||
return
|
||||
}
|
||||
// A service's AccessGroups can name groups that no longer exist — persisted
|
||||
// services and the agent-network synthesiser both carry the ids verbatim from
|
||||
// their own state. An unresolvable source authorises nothing, so drop it here
|
||||
// rather than let the network-map assembly resolve it to a nil group.
|
||||
sources := a.existingGroupIDs(svc.AccessGroups)
|
||||
if len(sources) == 0 {
|
||||
return
|
||||
}
|
||||
for _, proxyPeer := range proxyPeers {
|
||||
a.Policies = append(a.Policies, a.createPrivateServicePolicy(svc, proxyPeer, sources))
|
||||
}
|
||||
}
|
||||
|
||||
// existingGroupIDs returns the subset of groupIDs that resolve to a group in the account,
|
||||
// preserving the input order.
|
||||
func (a *Account) existingGroupIDs(groupIDs []string) []string {
|
||||
out := make([]string, 0, len(groupIDs))
|
||||
for _, groupID := range groupIDs {
|
||||
if _, ok := a.Groups[groupID]; ok {
|
||||
out = append(out, groupID)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *Account) createPrivateServicePolicy(svc *service.Service, proxyPeer *nbpeer.Peer, accessGroups []string) *Policy {
|
||||
policyID := fmt.Sprintf("private-access-%s-%s", svc.ID, proxyPeer.ID)
|
||||
sources := append([]string(nil), accessGroups...)
|
||||
return &Policy{
|
||||
ID: policyID,
|
||||
Name: fmt.Sprintf("Private Access to %s", svc.Name),
|
||||
Enabled: true,
|
||||
Rules: []*PolicyRule{
|
||||
{
|
||||
ID: policyID,
|
||||
PolicyID: policyID,
|
||||
Name: fmt.Sprintf("Allow access groups to reach %s", svc.Name),
|
||||
Enabled: true,
|
||||
Sources: sources,
|
||||
DestinationResource: Resource{
|
||||
ID: proxyPeer.ID,
|
||||
Type: ResourceTypePeer,
|
||||
},
|
||||
Bidirectional: false,
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
Action: PolicyTrafficActionAccept,
|
||||
PortRanges: []RulePortRange{
|
||||
{Start: 80, End: 80},
|
||||
{Start: 443, End: 443},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Account) injectTargetProxyPolicies(ctx context.Context, service *service.Service, target *service.Target, proxyPeers []*nbpeer.Peer) {
|
||||
port, ok := a.resolveTargetPort(ctx, target)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
path := ""
|
||||
if target.Path != nil {
|
||||
path = *target.Path
|
||||
}
|
||||
|
||||
for _, proxyPeer := range proxyPeers {
|
||||
policy := a.createProxyPolicy(service, target, proxyPeer, port, path)
|
||||
a.Policies = append(a.Policies, policy)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Account) resolveTargetPort(ctx context.Context, target *service.Target) (uint16, bool) {
|
||||
if target.Port != 0 {
|
||||
return target.Port, true
|
||||
}
|
||||
|
||||
switch target.Protocol {
|
||||
case "https", "tls":
|
||||
return 443, true
|
||||
case "http":
|
||||
return 80, true
|
||||
default:
|
||||
log.WithContext(ctx).Warnf("unsupported protocol %s for proxy target %s, skipping policy injection", target.Protocol, target.TargetId)
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Account) createProxyPolicy(svc *service.Service, target *service.Target, proxyPeer *nbpeer.Peer, port uint16, path string) *Policy {
|
||||
policyID := fmt.Sprintf("proxy-access-%s-%s-%s", svc.ID, proxyPeer.ID, path)
|
||||
|
||||
protocol := PolicyRuleProtocolTCP
|
||||
if svc.Mode == service.ModeUDP {
|
||||
protocol = PolicyRuleProtocolUDP
|
||||
}
|
||||
|
||||
return &Policy{
|
||||
ID: policyID,
|
||||
Name: fmt.Sprintf("Proxy Access to %s", svc.Name),
|
||||
Enabled: true,
|
||||
Rules: []*PolicyRule{
|
||||
{
|
||||
ID: policyID,
|
||||
PolicyID: policyID,
|
||||
Name: fmt.Sprintf("Allow access to %s", svc.Name),
|
||||
Enabled: true,
|
||||
SourceResource: Resource{
|
||||
ID: proxyPeer.ID,
|
||||
Type: ResourceTypePeer,
|
||||
},
|
||||
DestinationResource: Resource{
|
||||
ID: target.TargetId,
|
||||
Type: ResourceType(target.TargetType),
|
||||
},
|
||||
Bidirectional: false,
|
||||
Protocol: protocol,
|
||||
Action: PolicyTrafficActionAccept,
|
||||
PortRanges: []RulePortRange{
|
||||
{
|
||||
Start: port,
|
||||
End: port,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -1870,66 +1582,3 @@ func filterZoneRecordsForPeers(peer *nbpeer.Peer, customZone nbdns.CustomZone, p
|
||||
|
||||
return filteredRecords
|
||||
}
|
||||
|
||||
// filterPeerAppliedZones filters account zones based on the peer's group membership
|
||||
func filterPeerAppliedZones(ctx context.Context, accountZones []*zones.Zone, peerGroups LookupMap) []nbdns.CustomZone {
|
||||
var customZones []nbdns.CustomZone
|
||||
|
||||
if len(peerGroups) == 0 {
|
||||
return customZones
|
||||
}
|
||||
|
||||
for _, zone := range accountZones {
|
||||
if !zone.Enabled || len(zone.Records) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
hasAccess := false
|
||||
for _, distGroupID := range zone.DistributionGroups {
|
||||
if _, found := peerGroups[distGroupID]; found {
|
||||
hasAccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasAccess {
|
||||
continue
|
||||
}
|
||||
|
||||
simpleRecords := make([]nbdns.SimpleRecord, 0, len(zone.Records))
|
||||
for _, record := range zone.Records {
|
||||
var recordType int
|
||||
rData := record.Content
|
||||
|
||||
switch record.Type {
|
||||
case records.RecordTypeA:
|
||||
recordType = int(dns.TypeA)
|
||||
case records.RecordTypeAAAA:
|
||||
recordType = int(dns.TypeAAAA)
|
||||
case records.RecordTypeCNAME:
|
||||
recordType = int(dns.TypeCNAME)
|
||||
rData = dns.Fqdn(record.Content)
|
||||
default:
|
||||
log.WithContext(ctx).Warnf("unknown DNS record type %s for record %s", record.Type, record.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
simpleRecords = append(simpleRecords, nbdns.SimpleRecord{
|
||||
Name: dns.Fqdn(record.Name),
|
||||
Type: recordType,
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: record.TTL,
|
||||
RData: rData,
|
||||
})
|
||||
}
|
||||
|
||||
customZones = append(customZones, nbdns.CustomZone{
|
||||
Domain: dns.Fqdn(zone.Domain),
|
||||
Records: simpleRecords,
|
||||
SearchDomainDisabled: !zone.EnableSearchDomain,
|
||||
NonAuthoritative: true,
|
||||
})
|
||||
}
|
||||
|
||||
return customZones
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -10,10 +9,7 @@ import (
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
||||
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/posture"
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
)
|
||||
|
||||
// GetPeerNetworkMapResult dispatches to either the legacy-NetworkMap path or
|
||||
@@ -94,6 +90,9 @@ func (a *Account) GetPeerNetworkMapFromComponents(
|
||||
return nm
|
||||
}
|
||||
|
||||
// GetPeerNetworkMapComponents builds the account's slim twin store and computes
|
||||
// the peer's components on it. The calculation itself lives on
|
||||
// networkmap.NetworkMapData and never touches the Account.
|
||||
func (a *Account) GetPeerNetworkMapComponents(
|
||||
ctx context.Context,
|
||||
peerID string,
|
||||
@@ -104,724 +103,19 @@ func (a *Account) GetPeerNetworkMapComponents(
|
||||
routers map[string]map[string]*routerTypes.NetworkRouter,
|
||||
groupIDToUserIDs map[string][]string,
|
||||
) *NetworkMapComponents {
|
||||
peer := a.Peers[peerID]
|
||||
// this can never happen, things are very wrong if it did
|
||||
// TODO (dmitri) maybe consider using invariants?
|
||||
if peer == nil {
|
||||
log.WithField("peer id", peerID).Error("NetworkMapComponents are computed for a peer missing from the account")
|
||||
return EmptyNetworkMapComponents(&NetworkMapComponents{
|
||||
PeerID: peerID,
|
||||
Network: a.Network.Copy(),
|
||||
// must include the target peer as it's required on the client
|
||||
Peers: map[string]*ComponentPeer{peerID: peer.ToComponent()},
|
||||
})
|
||||
}
|
||||
|
||||
if _, ok := validatedPeersMap[peerID]; !ok {
|
||||
// Mirror legacy graceful-degrade: GetPeerNetworkMapFromComponents
|
||||
// returns &NetworkMap{Network: a.Network.Copy()} when components is
|
||||
// nil. Match that floor so the receiving client always sees the
|
||||
// account Network identifier, not a fully-empty envelope.
|
||||
return EmptyNetworkMapComponents(&NetworkMapComponents{
|
||||
PeerID: peerID,
|
||||
Network: a.Network.Copy(),
|
||||
// must include the target peer as it's required on the client
|
||||
Peers: map[string]*ComponentPeer{peerID: peer.ToComponent()},
|
||||
})
|
||||
}
|
||||
|
||||
components := &NetworkMapComponents{
|
||||
PeerID: peerID,
|
||||
Network: a.Network.Copy(),
|
||||
NameServerGroups: make([]*nbdns.NameServerGroup, 0),
|
||||
CustomZoneDomain: peersCustomZone.Domain,
|
||||
ResourcePoliciesMap: make(map[string][]*Policy),
|
||||
RoutersMap: make(map[string]map[string]*ComponentRouter),
|
||||
NetworkResources: make([]*ComponentResource, 0),
|
||||
PostureFailedPeers: make(map[string]map[string]struct{}, len(a.PostureChecks)),
|
||||
RouterPeers: make(map[string]*ComponentPeer),
|
||||
NetworkXIDToPublicID: make(map[string]string, len(a.Networks)),
|
||||
PostureCheckXIDToPublicID: make(map[string]string, len(a.PostureChecks)),
|
||||
|
||||
ForceRoutingPeerDNSResolution: a.forcesRoutingPeerDNSResolution(peerID, routers),
|
||||
}
|
||||
for _, n := range a.Networks {
|
||||
if n != nil {
|
||||
components.NetworkXIDToPublicID[n.ID] = n.PublicID
|
||||
}
|
||||
}
|
||||
for _, pc := range a.PostureChecks {
|
||||
if pc != nil {
|
||||
components.PostureCheckXIDToPublicID[pc.ID] = pc.PublicID
|
||||
}
|
||||
}
|
||||
|
||||
components.AccountSettings = &AccountSettingsInfo{
|
||||
PeerLoginExpirationEnabled: a.Settings.PeerLoginExpirationEnabled,
|
||||
PeerLoginExpiration: a.Settings.PeerLoginExpiration,
|
||||
PeerInactivityExpirationEnabled: a.Settings.PeerInactivityExpirationEnabled,
|
||||
PeerInactivityExpiration: a.Settings.PeerInactivityExpiration,
|
||||
}
|
||||
|
||||
components.DNSSettings = &a.DNSSettings
|
||||
|
||||
// relevantPeers always contains the target peer (peerID)
|
||||
relevantPeers, relevantGroups, relevantPolicies, relevantRoutes, sshReqs := a.getPeersGroupsPoliciesRoutes(ctx, peerID, peer.SSHEnabled, validatedPeersMap, &components.PostureFailedPeers)
|
||||
|
||||
if len(sshReqs.neededGroupIDs) > 0 {
|
||||
components.GroupIDToUserIDs = filterGroupIDToUserIDs(groupIDToUserIDs, sshReqs.neededGroupIDs)
|
||||
}
|
||||
if sshReqs.needAllowedUserIDs {
|
||||
components.AllowedUserIDs = a.getAllowedUserIDs()
|
||||
}
|
||||
|
||||
components.Peers = relevantPeers
|
||||
components.Groups = GroupsToComponent(relevantGroups)
|
||||
components.Policies = relevantPolicies
|
||||
components.Routes = relevantRoutes
|
||||
components.AllDNSRecords = filterDNSRecordsByPeers(peersCustomZone.Records, relevantPeers, peer.SupportsIPv6() && peer.IPv6.IsValid())
|
||||
|
||||
peerGroups := a.GetPeerGroups(peerID)
|
||||
components.AccountZones = filterPeerAppliedZones(ctx, accountZones, peerGroups)
|
||||
components.AccountZones = append(components.AccountZones, a.SynthesizePrivateServiceZones(peerID)...)
|
||||
|
||||
for _, nsGroup := range a.NameServerGroups {
|
||||
if nsGroup.Enabled {
|
||||
for _, gID := range nsGroup.Groups {
|
||||
if _, found := relevantGroups[gID]; found {
|
||||
components.NameServerGroups = append(components.NameServerGroups, nsGroup)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, resource := range a.NetworkResources {
|
||||
if !resource.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
policies, exists := resourcePolicies[resource.ID]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
addSourcePeers := false
|
||||
|
||||
networkRoutingPeers, routerExists := routers[resource.NetworkID]
|
||||
if routerExists {
|
||||
if _, ok := networkRoutingPeers[peerID]; ok {
|
||||
addSourcePeers = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, policy := range policies {
|
||||
if addSourcePeers {
|
||||
var peers []string
|
||||
if policy.Rules[0].SourceResource.Type == ResourceTypePeer && policy.Rules[0].SourceResource.ID != "" {
|
||||
peers = []string{policy.Rules[0].SourceResource.ID}
|
||||
} else {
|
||||
peers = a.getUniquePeerIDsFromGroupsIDs(ctx, policy.SourceGroups())
|
||||
}
|
||||
for _, pID := range a.getPostureValidPeersSaveFailed(peers, policy.SourcePostureChecks, validatedPeersMap, &components.PostureFailedPeers) {
|
||||
if _, exists := components.Peers[pID]; !exists {
|
||||
components.Peers[pID] = a.GetPeer(pID).ToComponent()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
peerInSources := false
|
||||
if policy.Rules[0].SourceResource.Type == ResourceTypePeer && policy.Rules[0].SourceResource.ID != "" {
|
||||
peerInSources = policy.Rules[0].SourceResource.ID == peerID
|
||||
} else {
|
||||
for _, groupID := range policy.SourceGroups() {
|
||||
if group := a.GetGroup(groupID); group != nil && slices.Contains(group.Peers, peerID) {
|
||||
peerInSources = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !peerInSources {
|
||||
continue
|
||||
}
|
||||
isValid, pname := a.validatePostureChecksOnPeerGetFailed(ctx, policy.SourcePostureChecks, peerID)
|
||||
if !isValid && len(pname) > 0 {
|
||||
if _, ok := components.PostureFailedPeers[pname]; !ok {
|
||||
components.PostureFailedPeers[pname] = make(map[string]struct{})
|
||||
}
|
||||
components.PostureFailedPeers[pname][peer.ID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
addSourcePeers = true
|
||||
}
|
||||
|
||||
for _, rule := range policy.Rules {
|
||||
for _, srcGroupID := range rule.Sources {
|
||||
if g := a.Groups[srcGroupID]; g != nil {
|
||||
if _, exists := components.Groups[srcGroupID]; !exists {
|
||||
components.Groups[srcGroupID] = g.ToComponent()
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, dstGroupID := range rule.Destinations {
|
||||
if g := a.Groups[dstGroupID]; g != nil {
|
||||
if _, exists := components.Groups[dstGroupID]; !exists {
|
||||
components.Groups[dstGroupID] = g.ToComponent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
components.ResourcePoliciesMap[resource.ID] = policies
|
||||
}
|
||||
|
||||
// Only expose router peers and the per-network routers_map when this
|
||||
// target peer actually has access to the resource (either as a router
|
||||
// itself or via a policy that includes it as a source). Without this
|
||||
// gate, every peer's envelope was leaking router peers of every
|
||||
// network in the account — accounts with many tenants/networks
|
||||
// shipped tens of unrelated peers in `peers[]` and `routers_map`.
|
||||
if addSourcePeers {
|
||||
components.RoutersMap[resource.NetworkID] = routerTypes.ToComponentMap(networkRoutingPeers)
|
||||
for peerIDKey := range networkRoutingPeers {
|
||||
if p := a.Peers[peerIDKey]; p != nil {
|
||||
cp := components.RouterPeers[peerIDKey]
|
||||
if cp == nil {
|
||||
cp = p.ToComponent()
|
||||
components.RouterPeers[peerIDKey] = cp
|
||||
}
|
||||
if _, exists := components.Peers[peerIDKey]; !exists {
|
||||
if _, validated := validatedPeersMap[peerIDKey]; validated {
|
||||
components.Peers[peerIDKey] = cp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
components.NetworkResources = append(components.NetworkResources, resource.ToComponent())
|
||||
}
|
||||
}
|
||||
|
||||
filterGroupPeers(&components.Groups, components.Peers)
|
||||
filterPostureFailedPeers(&components.PostureFailedPeers, components.Policies, components.ResourcePoliciesMap, components.Peers)
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
type sshRequirements struct {
|
||||
neededGroupIDs map[string]struct{}
|
||||
needAllowedUserIDs bool
|
||||
}
|
||||
|
||||
func (a *Account) getPeersGroupsPoliciesRoutes(
|
||||
ctx context.Context,
|
||||
peerID string,
|
||||
peerSSHEnabled bool,
|
||||
validatedPeersMap map[string]struct{},
|
||||
postureFailedPeers *map[string]map[string]struct{},
|
||||
) (map[string]*ComponentPeer, map[string]*Group, []*Policy, []*route.Route, sshRequirements) {
|
||||
relevantPeerIDs := make(map[string]*ComponentPeer, len(a.Peers)/4)
|
||||
relevantGroupIDs := make(map[string]*Group, len(a.Groups)/4)
|
||||
relevantPolicies := make([]*Policy, 0, len(a.Policies))
|
||||
relevantRoutes := make([]*route.Route, 0, len(a.Routes))
|
||||
sshReqs := sshRequirements{neededGroupIDs: make(map[string]struct{})}
|
||||
|
||||
relevantPeerIDs[peerID] = a.GetPeer(peerID).ToComponent()
|
||||
|
||||
peerGroupSet := make(map[string]struct{}, 8)
|
||||
for groupID, group := range a.Groups {
|
||||
if slices.Contains(group.Peers, peerID) {
|
||||
relevantGroupIDs[groupID] = a.GetGroup(groupID)
|
||||
peerGroupSet[groupID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
routeAccessControlGroups := make(map[string]struct{})
|
||||
for _, r := range a.Routes {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
relevant := r.Peer == peerID
|
||||
if !relevant {
|
||||
for _, groupID := range r.PeerGroups {
|
||||
if _, ok := peerGroupSet[groupID]; ok {
|
||||
relevant = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !relevant && r.Enabled {
|
||||
for _, groupID := range r.Groups {
|
||||
if _, ok := peerGroupSet[groupID]; ok {
|
||||
relevant = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !relevant {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, groupID := range r.PeerGroups {
|
||||
relevantGroupIDs[groupID] = a.GetGroup(groupID)
|
||||
}
|
||||
for _, groupID := range r.Groups {
|
||||
relevantGroupIDs[groupID] = a.GetGroup(groupID)
|
||||
}
|
||||
if r.Enabled {
|
||||
for _, groupID := range r.AccessControlGroups {
|
||||
relevantGroupIDs[groupID] = a.GetGroup(groupID)
|
||||
routeAccessControlGroups[groupID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Include route advertisers in relevantPeerIDs. The envelope
|
||||
// encoder writes route.peer_index by looking up r.Peer in the
|
||||
// shipped peers list; if the advertiser is policy-isolated from
|
||||
// the target peer (no rule edge between them), it would otherwise
|
||||
// be omitted and the decoder would fail to resolve r.Peer, leaving
|
||||
// the client without a WG tunnel target for this route. Legacy
|
||||
// NetworkMap.Routes shipped the WG public key inline, so the
|
||||
// equivalence path doesn't surface this — but the dependency is
|
||||
// real once a client actually tries to use the route.
|
||||
// Gate by validatedPeersMap so non-validated advertisers stay out
|
||||
// (matches the network-resource router behaviour at the bottom of
|
||||
// this loop, and the legacy invariant that only validated peers
|
||||
// reach a client's view).
|
||||
if r.Peer != "" {
|
||||
if _, ok := validatedPeersMap[r.Peer]; ok {
|
||||
if p := a.GetPeer(r.Peer); p != nil {
|
||||
relevantPeerIDs[r.Peer] = p.ToComponent()
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, groupID := range r.PeerGroups {
|
||||
g := a.GetGroup(groupID)
|
||||
if g == nil {
|
||||
continue
|
||||
}
|
||||
for _, pid := range g.Peers {
|
||||
if _, exists := relevantPeerIDs[pid]; exists {
|
||||
continue
|
||||
}
|
||||
if _, ok := validatedPeersMap[pid]; !ok {
|
||||
continue
|
||||
}
|
||||
if p := a.GetPeer(pid); p != nil {
|
||||
relevantPeerIDs[pid] = p.ToComponent()
|
||||
}
|
||||
}
|
||||
}
|
||||
relevantRoutes = append(relevantRoutes, r)
|
||||
}
|
||||
|
||||
for _, policy := range a.Policies {
|
||||
if !policy.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
policyRelevant := false
|
||||
for _, rule := range policy.Rules {
|
||||
if !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(routeAccessControlGroups) > 0 {
|
||||
for _, destGroupID := range rule.Destinations {
|
||||
if _, needed := routeAccessControlGroups[destGroupID]; needed {
|
||||
policyRelevant = true
|
||||
for _, srcGroupID := range rule.Sources {
|
||||
relevantGroupIDs[srcGroupID] = a.GetGroup(srcGroupID)
|
||||
}
|
||||
for _, dstGroupID := range rule.Destinations {
|
||||
relevantGroupIDs[dstGroupID] = a.GetGroup(dstGroupID)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sourcePeers, destinationPeers []string
|
||||
var peerInSources, peerInDestinations bool
|
||||
|
||||
if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" {
|
||||
sourcePeers = []string{rule.SourceResource.ID}
|
||||
if rule.SourceResource.ID == peerID {
|
||||
peerInSources = true
|
||||
}
|
||||
} else {
|
||||
sourcePeers, peerInSources = a.getPeersFromGroups(ctx, rule.Sources, peerID, policy.SourcePostureChecks, validatedPeersMap, postureFailedPeers)
|
||||
}
|
||||
|
||||
if rule.DestinationResource.Type == ResourceTypePeer && rule.DestinationResource.ID != "" {
|
||||
destinationPeers = []string{rule.DestinationResource.ID}
|
||||
if rule.DestinationResource.ID == peerID {
|
||||
peerInDestinations = true
|
||||
}
|
||||
} else {
|
||||
destinationPeers, peerInDestinations = a.getPeersFromGroups(ctx, rule.Destinations, peerID, nil, validatedPeersMap, postureFailedPeers)
|
||||
}
|
||||
|
||||
if peerInSources {
|
||||
policyRelevant = true
|
||||
for _, pid := range destinationPeers {
|
||||
if _, exists := relevantPeerIDs[pid]; !exists {
|
||||
relevantPeerIDs[pid] = a.GetPeer(pid).ToComponent()
|
||||
}
|
||||
}
|
||||
for _, dstGroupID := range rule.Destinations {
|
||||
relevantGroupIDs[dstGroupID] = a.GetGroup(dstGroupID)
|
||||
}
|
||||
}
|
||||
|
||||
if peerInDestinations {
|
||||
policyRelevant = true
|
||||
for _, pid := range sourcePeers {
|
||||
if _, exists := relevantPeerIDs[pid]; !exists {
|
||||
relevantPeerIDs[pid] = a.GetPeer(pid).ToComponent()
|
||||
}
|
||||
}
|
||||
for _, srcGroupID := range rule.Sources {
|
||||
relevantGroupIDs[srcGroupID] = a.GetGroup(srcGroupID)
|
||||
}
|
||||
|
||||
if rule.Protocol == PolicyRuleProtocolNetbirdSSH {
|
||||
switch {
|
||||
case len(rule.AuthorizedGroups) > 0:
|
||||
for groupID := range rule.AuthorizedGroups {
|
||||
sshReqs.neededGroupIDs[groupID] = struct{}{}
|
||||
}
|
||||
case rule.AuthorizedUser != "":
|
||||
default:
|
||||
sshReqs.needAllowedUserIDs = true
|
||||
}
|
||||
} else if PolicyRuleImpliesLegacySSH(rule) && peerSSHEnabled {
|
||||
sshReqs.needAllowedUserIDs = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if policyRelevant {
|
||||
relevantPolicies = append(relevantPolicies, policy)
|
||||
}
|
||||
}
|
||||
|
||||
return relevantPeerIDs, relevantGroupIDs, relevantPolicies, relevantRoutes, sshReqs
|
||||
}
|
||||
|
||||
func (a *Account) getPeersFromGroups(ctx context.Context, groups []string, peerID string, sourcePostureChecksIDs []string,
|
||||
validatedPeersMap map[string]struct{}, postureFailedPeers *map[string]map[string]struct{}) ([]string, bool) {
|
||||
peerInGroups := false
|
||||
var filteredPeerIDs []string
|
||||
var seenPeerIds map[string]struct{}
|
||||
|
||||
for _, gid := range groups {
|
||||
group := a.GetGroup(gid)
|
||||
if group == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if group.IsGroupAll() || len(groups) == 1 {
|
||||
filteredPeerIDs = make([]string, 0, len(group.Peers))
|
||||
peerInGroups = false
|
||||
for _, pid := range group.Peers {
|
||||
peer, ok := a.Peers[pid]
|
||||
if !ok || peer == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := validatedPeersMap[peer.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
isValid, pname := a.validatePostureChecksOnPeerGetFailed(ctx, sourcePostureChecksIDs, peer.ID)
|
||||
if !isValid && len(pname) > 0 {
|
||||
if _, ok := (*postureFailedPeers)[pname]; !ok {
|
||||
(*postureFailedPeers)[pname] = make(map[string]struct{})
|
||||
}
|
||||
(*postureFailedPeers)[pname][peer.ID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
if peer.ID == peerID {
|
||||
peerInGroups = true
|
||||
continue
|
||||
}
|
||||
|
||||
filteredPeerIDs = append(filteredPeerIDs, peer.ID)
|
||||
}
|
||||
return filteredPeerIDs, peerInGroups
|
||||
}
|
||||
|
||||
if seenPeerIds == nil {
|
||||
totalGroupPeers := 0
|
||||
for _, g := range groups {
|
||||
if grp := a.GetGroup(g); grp != nil {
|
||||
totalGroupPeers += len(grp.Peers)
|
||||
}
|
||||
}
|
||||
filteredPeerIDs = make([]string, 0, totalGroupPeers)
|
||||
seenPeerIds = make(map[string]struct{}, totalGroupPeers)
|
||||
}
|
||||
|
||||
for _, pid := range group.Peers {
|
||||
if _, seen := seenPeerIds[pid]; seen {
|
||||
continue
|
||||
}
|
||||
seenPeerIds[pid] = struct{}{}
|
||||
peer, ok := a.Peers[pid]
|
||||
if !ok || peer == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := validatedPeersMap[peer.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
isValid, pname := a.validatePostureChecksOnPeerGetFailed(ctx, sourcePostureChecksIDs, peer.ID)
|
||||
if !isValid && len(pname) > 0 {
|
||||
if _, ok := (*postureFailedPeers)[pname]; !ok {
|
||||
(*postureFailedPeers)[pname] = make(map[string]struct{})
|
||||
}
|
||||
(*postureFailedPeers)[pname][peer.ID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
if peer.ID == peerID {
|
||||
peerInGroups = true
|
||||
continue
|
||||
}
|
||||
|
||||
filteredPeerIDs = append(filteredPeerIDs, peer.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredPeerIDs, peerInGroups
|
||||
}
|
||||
|
||||
func (a *Account) validatePostureChecksOnPeerGetFailed(ctx context.Context, sourcePostureChecksID []string, peerID string) (bool, string) {
|
||||
peer, ok := a.Peers[peerID]
|
||||
if !ok || peer == nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
for _, postureChecksID := range sourcePostureChecksID {
|
||||
if valid, cached := a.cachedPostureCheckResult(postureChecksID, peerID); cached {
|
||||
if !valid {
|
||||
return false, postureChecksID
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
postureChecks := a.GetPostureChecks(postureChecksID)
|
||||
if postureChecks == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !peerPassesPostureChecks(ctx, postureChecks.GetChecks(), peer) {
|
||||
return false, postureChecksID
|
||||
}
|
||||
}
|
||||
return true, ""
|
||||
nmd := a.toNetworkMapData(accountZones, validatedPeersMap, resourcePolicies, routers, groupIDToUserIDs)
|
||||
return nmd.GetPeerNetworkMapComponents(peerID, TwinCustomZone(peersCustomZone))
|
||||
}
|
||||
|
||||
// PrecomputePostureValidation evaluates every posture check referenced by an enabled
|
||||
// policy once against the peers of that policy's source groups and stores the results,
|
||||
// so the per-peer network map calculations that follow look them up instead of
|
||||
// re-evaluating checks for every peer pair. It must be called before the account is
|
||||
// shared across goroutines; lookups not covered by the precomputed results fall back
|
||||
// to direct evaluation.
|
||||
// policy once and stores the results on the account, so the per-peer components
|
||||
// calculations that follow look them up instead of re-evaluating checks for every
|
||||
// peer pair. The evaluation itself runs on the twin store; every twin built from
|
||||
// this account afterwards inherits the results. It must be called before the
|
||||
// account is shared across goroutines.
|
||||
func (a *Account) PrecomputePostureValidation(ctx context.Context) {
|
||||
if len(a.PostureChecks) == 0 {
|
||||
a.PostureValidation = nil
|
||||
return
|
||||
}
|
||||
|
||||
checkPeerIDs := make(map[string]map[string]struct{})
|
||||
for _, policy := range a.Policies {
|
||||
if !policy.Enabled || len(policy.SourcePostureChecks) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
peerIDs := a.getUniquePeerIDsFromGroupsIDs(ctx, policy.SourceGroups())
|
||||
for _, rule := range policy.Rules {
|
||||
if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" {
|
||||
peerIDs = append(peerIDs, rule.SourceResource.ID)
|
||||
}
|
||||
}
|
||||
|
||||
for _, postureChecksID := range policy.SourcePostureChecks {
|
||||
set := checkPeerIDs[postureChecksID]
|
||||
if set == nil {
|
||||
set = make(map[string]struct{}, len(peerIDs))
|
||||
checkPeerIDs[postureChecksID] = set
|
||||
}
|
||||
for _, pid := range peerIDs {
|
||||
set[pid] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results := make(map[string]map[string]bool, len(checkPeerIDs))
|
||||
for postureChecksID, peerIDs := range checkPeerIDs {
|
||||
results[postureChecksID] = a.evaluatePostureChecksForPeers(ctx, postureChecksID, peerIDs)
|
||||
}
|
||||
a.PostureValidation = results
|
||||
}
|
||||
|
||||
func (a *Account) evaluatePostureChecksForPeers(ctx context.Context, postureChecksID string, peerIDs map[string]struct{}) map[string]bool {
|
||||
postureChecks := a.GetPostureChecks(postureChecksID)
|
||||
if postureChecks == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
checks := postureChecks.GetChecks()
|
||||
results := make(map[string]bool, len(peerIDs))
|
||||
for peerID := range peerIDs {
|
||||
peer, ok := a.Peers[peerID]
|
||||
if !ok || peer == nil {
|
||||
continue
|
||||
}
|
||||
results[peerID] = peerPassesPostureChecks(ctx, checks, peer)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func (a *Account) cachedPostureCheckResult(postureChecksID, peerID string) (bool, bool) {
|
||||
results, ok := a.PostureValidation[postureChecksID]
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
if results == nil {
|
||||
return true, true
|
||||
}
|
||||
valid, found := results[peerID]
|
||||
return valid, found
|
||||
}
|
||||
|
||||
func peerPassesPostureChecks(ctx context.Context, checks []posture.Check, peer *nbpeer.Peer) bool {
|
||||
for _, check := range checks {
|
||||
isValid, _ := check.Check(ctx, *peer)
|
||||
if !isValid {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *Account) getPostureValidPeersSaveFailed(inputPeers []string, postureChecksIDs []string, validatedPeersMap map[string]struct{}, postureFailedPeers *map[string]map[string]struct{}) []string {
|
||||
var dest []string
|
||||
for _, peerID := range inputPeers {
|
||||
if _, validated := validatedPeersMap[peerID]; !validated {
|
||||
continue
|
||||
}
|
||||
valid, pname := a.validatePostureChecksOnPeerGetFailed(context.Background(), postureChecksIDs, peerID)
|
||||
if valid {
|
||||
dest = append(dest, peerID)
|
||||
continue
|
||||
}
|
||||
if _, ok := (*postureFailedPeers)[pname]; !ok {
|
||||
(*postureFailedPeers)[pname] = make(map[string]struct{})
|
||||
}
|
||||
(*postureFailedPeers)[pname][peerID] = struct{}{}
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
// filterGroupPeers trims each group's Peers slice to only those peers that
|
||||
// also appear in `peers`. Groups whose filtered list is empty are NOT
|
||||
// deleted from the map — they're kept so the components wire encoder can
|
||||
// still resolve seq references from routes/policies/access-control groups
|
||||
// that name them. Calculate() tolerates groups with empty Peers (the inner
|
||||
// loops simply iterate zero times), so retaining them is behaviourally a
|
||||
// no-op for the legacy path that consumes the same NetworkMapComponents.
|
||||
func filterGroupPeers(groups *map[string]*ComponentGroup, peers map[string]*ComponentPeer) {
|
||||
for groupID, groupInfo := range *groups {
|
||||
filteredPeers := make([]string, 0, len(groupInfo.Peers))
|
||||
for _, pid := range groupInfo.Peers {
|
||||
if _, exists := peers[pid]; exists {
|
||||
filteredPeers = append(filteredPeers, pid)
|
||||
}
|
||||
}
|
||||
|
||||
if len(filteredPeers) != len(groupInfo.Peers) {
|
||||
ng := *groupInfo
|
||||
ng.Peers = filteredPeers
|
||||
(*groups)[groupID] = &ng
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func filterPostureFailedPeers(postureFailedPeers *map[string]map[string]struct{}, policies []*Policy, resourcePoliciesMap map[string][]*Policy, peers map[string]*ComponentPeer) {
|
||||
if len(*postureFailedPeers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
referencedPostureChecks := make(map[string]struct{})
|
||||
for _, policy := range policies {
|
||||
for _, checkID := range policy.SourcePostureChecks {
|
||||
referencedPostureChecks[checkID] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, resPolicies := range resourcePoliciesMap {
|
||||
for _, policy := range resPolicies {
|
||||
for _, checkID := range policy.SourcePostureChecks {
|
||||
referencedPostureChecks[checkID] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for checkID, failedPeers := range *postureFailedPeers {
|
||||
if _, referenced := referencedPostureChecks[checkID]; !referenced {
|
||||
delete(*postureFailedPeers, checkID)
|
||||
continue
|
||||
}
|
||||
for peerID := range failedPeers {
|
||||
if _, exists := peers[peerID]; !exists {
|
||||
delete(failedPeers, peerID)
|
||||
}
|
||||
}
|
||||
if len(failedPeers) == 0 {
|
||||
delete(*postureFailedPeers, checkID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func filterDNSRecordsByPeers(records []nbdns.SimpleRecord, peers map[string]*ComponentPeer, includeIPv6 bool) []nbdns.SimpleRecord {
|
||||
if len(records) == 0 || len(peers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Include both v4 and v6 addresses so AAAA records (whose RData is an IPv6
|
||||
// address) are not filtered out when peers have IPv6 assigned. When the
|
||||
// requesting peer doesn't have IPv6, omit v6 IPs so AAAA records get dropped.
|
||||
peerIPs := make(map[string]struct{}, len(peers)*2)
|
||||
for _, peer := range peers {
|
||||
if peer == nil {
|
||||
continue
|
||||
}
|
||||
peerIPs[peer.IP.String()] = struct{}{}
|
||||
if includeIPv6 && peer.IPv6.IsValid() {
|
||||
peerIPs[peer.IPv6.String()] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
filteredRecords := make([]nbdns.SimpleRecord, 0, len(records))
|
||||
for _, record := range records {
|
||||
if _, exists := peerIPs[record.RData]; exists {
|
||||
filteredRecords = append(filteredRecords, record)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredRecords
|
||||
}
|
||||
|
||||
func filterGroupIDToUserIDs(fullMap map[string][]string, neededGroupIDs map[string]struct{}) map[string][]string {
|
||||
if len(neededGroupIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
filtered := make(map[string][]string, len(neededGroupIDs))
|
||||
for groupID := range neededGroupIDs {
|
||||
if users, ok := fullMap[groupID]; ok {
|
||||
filtered[groupID] = users
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
nmd := a.toNetworkMapData(nil, nil, nil, nil, nil)
|
||||
nmd.PrecomputePostureValidation()
|
||||
a.PostureValidation = nmd.PostureValidation
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetPeerNetworkMapComponents_PeerMissingFromAcount(t *testing.T) {
|
||||
account := Account{Network: NewNetwork()}
|
||||
nmapcomponets := account.GetPeerNetworkMapComponents(context.TODO(), "missing-peer", dns.CustomZone{}, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Equal(t, EmptyNetworkMapComponents(&types.NetworkMapComponents{
|
||||
PeerID: "missing-peer",
|
||||
Network: TwinNetwork(account.Network),
|
||||
Peers: map[string]*nmdata.Peer{"missing-peer": nil},
|
||||
ForceRoutingPeerDNSResolution: false,
|
||||
}), nmapcomponets)
|
||||
}
|
||||
@@ -0,0 +1,643 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/miekg/dns"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
proxydomain "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones/records"
|
||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/posture"
|
||||
nbroute "github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
// toNetworkMapData builds the slim twin store from the account once per
|
||||
// account. The per-peer components calculation then runs on the twin.
|
||||
func (a *Account) toNetworkMapData(
|
||||
accountZones []*zones.Zone,
|
||||
validatedPeersMap map[string]struct{},
|
||||
resourcePolicies map[string][]*Policy,
|
||||
routers map[string]map[string]*routerTypes.NetworkRouter,
|
||||
groupIDToUserIDs map[string][]string,
|
||||
) *networkmap.NetworkMapData {
|
||||
nmd := &networkmap.NetworkMapData{
|
||||
Peers: make(map[string]*nmdata.Peer, len(a.Peers)),
|
||||
Groups: make(map[string]*nmdata.Group, len(a.Groups)),
|
||||
Policies: make([]*nmdata.Policy, 0, len(a.Policies)),
|
||||
Routes: make([]*nmdata.Route, 0, len(a.Routes)),
|
||||
NameServerGroups: make([]*nmdata.NameServerGroup, 0, len(a.NameServerGroups)),
|
||||
NetworkResources: make([]*nmdata.NetworkResource, 0, len(a.NetworkResources)),
|
||||
PostureChecks: make(map[string]*nmdata.PostureChecks, len(a.PostureChecks)),
|
||||
ResourcePolicies: make(map[string][]*nmdata.Policy, len(resourcePolicies)),
|
||||
Routers: make(map[string]map[string]*nmdata.NetworkRouter, len(routers)),
|
||||
ValidatedPeers: validatedPeersMap,
|
||||
GroupIDToUserIDs: groupIDToUserIDs,
|
||||
PostureValidation: a.PostureValidation,
|
||||
AllowedUserIDs: a.getAllowedUserIDs(),
|
||||
NetworkXIDToPublicID: make(map[string]string, len(a.Networks)),
|
||||
PostureCheckXIDToPublicID: make(map[string]string, len(a.PostureChecks)),
|
||||
}
|
||||
|
||||
if a.Network != nil {
|
||||
nmd.Network = TwinNetwork(a.Network)
|
||||
}
|
||||
nmd.DNSSettings = &nmdata.DNSSettings{DisabledManagementGroups: a.DNSSettings.DisabledManagementGroups}
|
||||
nmd.AccountSettings = TwinAccountSettings(a.Settings)
|
||||
|
||||
for id, p := range a.Peers {
|
||||
nmd.Peers[id] = twinPeer(p)
|
||||
}
|
||||
for id, g := range a.Groups {
|
||||
nmd.Groups[id] = twinGroup(g)
|
||||
}
|
||||
|
||||
policyCache := make(map[string]*nmdata.Policy, len(a.Policies))
|
||||
twinPol := func(p *Policy) *nmdata.Policy {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
if tp, ok := policyCache[p.ID]; ok {
|
||||
return tp
|
||||
}
|
||||
tp := twinPolicy(p)
|
||||
policyCache[p.ID] = tp
|
||||
return tp
|
||||
}
|
||||
for _, p := range a.Policies {
|
||||
nmd.Policies = append(nmd.Policies, twinPol(p))
|
||||
}
|
||||
for resID, pols := range resourcePolicies {
|
||||
twinPols := make([]*nmdata.Policy, 0, len(pols))
|
||||
for _, p := range pols {
|
||||
twinPols = append(twinPols, twinPol(p))
|
||||
}
|
||||
nmd.ResourcePolicies[resID] = twinPols
|
||||
}
|
||||
|
||||
for _, r := range a.Routes {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
nmd.Routes = append(nmd.Routes, twinRoute(r))
|
||||
}
|
||||
for _, nsg := range a.NameServerGroups {
|
||||
nmd.NameServerGroups = append(nmd.NameServerGroups, twinNSG(nsg))
|
||||
}
|
||||
for _, res := range a.NetworkResources {
|
||||
nmd.NetworkResources = append(nmd.NetworkResources, TwinNetworkResource(res))
|
||||
}
|
||||
for _, pc := range a.PostureChecks {
|
||||
if pc != nil {
|
||||
nmd.PostureChecks[pc.ID] = TwinPostureChecks(pc)
|
||||
nmd.PostureCheckXIDToPublicID[pc.ID] = pc.PublicID
|
||||
}
|
||||
}
|
||||
for _, n := range a.Networks {
|
||||
if n != nil {
|
||||
nmd.NetworkXIDToPublicID[n.ID] = n.PublicID
|
||||
}
|
||||
}
|
||||
for networkID, inner := range routers {
|
||||
twinInner := make(map[string]*nmdata.NetworkRouter, len(inner))
|
||||
for peerID, router := range inner {
|
||||
twinInner[peerID] = twinRouter(router)
|
||||
}
|
||||
nmd.Routers[networkID] = twinInner
|
||||
}
|
||||
|
||||
nmd.ProxyTargetedDomainResourceIDs = a.proxyTargetedDomainResourceIDs()
|
||||
nmd.AppliedZoneCandidates = buildAppliedZoneCandidates(accountZones)
|
||||
nmd.PrivateServiceCandidates = a.buildPrivateServiceCandidates()
|
||||
nmd.Services = TwinServices(a.Services)
|
||||
nmd.Domains = twinProxyDomains(a.Domains)
|
||||
|
||||
return nmd
|
||||
}
|
||||
|
||||
// TwinServices converts reverse-proxy services to their slim nmdata twins.
|
||||
// Exported for the network-map controller, which hands the store-backed twin
|
||||
// the same services the account carries.
|
||||
func TwinServices(services []*service.Service) []*nmdata.Service {
|
||||
if len(services) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]*nmdata.Service, 0, len(services))
|
||||
for _, svc := range services {
|
||||
if svc == nil {
|
||||
continue
|
||||
}
|
||||
targets := make([]*nmdata.ServiceTarget, 0, len(svc.Targets))
|
||||
for _, t := range svc.Targets {
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
path := ""
|
||||
if t.Path != nil {
|
||||
path = *t.Path
|
||||
}
|
||||
targets = append(targets, &nmdata.ServiceTarget{
|
||||
Enabled: t.Enabled,
|
||||
Path: path,
|
||||
Port: t.Port,
|
||||
Protocol: t.Protocol,
|
||||
TargetID: t.TargetId,
|
||||
TargetType: string(t.TargetType),
|
||||
})
|
||||
}
|
||||
out = append(out, &nmdata.Service{
|
||||
ID: svc.ID,
|
||||
Enabled: svc.Enabled,
|
||||
Private: svc.Private,
|
||||
Mode: svc.Mode,
|
||||
Domain: svc.Domain,
|
||||
ProxyCluster: svc.ProxyCluster,
|
||||
AccessGroups: svc.AccessGroups,
|
||||
Targets: targets,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func twinPeer(p *nbpeer.Peer) *nmdata.Peer {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
networkAddresses := make([]nmdata.NetworkAddress, 0, len(p.Meta.NetworkAddresses))
|
||||
for _, na := range p.Meta.NetworkAddresses {
|
||||
networkAddresses = append(networkAddresses, nmdata.NetworkAddress{NetIP: na.NetIP})
|
||||
}
|
||||
files := make([]nmdata.File, 0, len(p.Meta.Files))
|
||||
for _, f := range p.Meta.Files {
|
||||
files = append(files, nmdata.File{Path: f.Path, ProcessIsRunning: f.ProcessIsRunning})
|
||||
}
|
||||
return &nmdata.Peer{
|
||||
ID: p.ID,
|
||||
Key: p.Key,
|
||||
SSHKey: p.SSHKey,
|
||||
DNSLabel: p.DNSLabel,
|
||||
UserID: p.UserID,
|
||||
SSHEnabled: p.SSHEnabled,
|
||||
LoginExpirationEnabled: p.LoginExpirationEnabled,
|
||||
LastLogin: p.LastLogin,
|
||||
IP: p.IP,
|
||||
IPv6: p.IPv6,
|
||||
RequiresApproval: p.Status != nil && p.Status.RequiresApproval,
|
||||
Connected: p.Status != nil && p.Status.Connected,
|
||||
ExtraDNSLabels: p.ExtraDNSLabels,
|
||||
ProxyMeta: nmdata.ProxyMeta{Embedded: p.ProxyMeta.Embedded, Cluster: p.ProxyMeta.Cluster},
|
||||
Meta: nmdata.PeerSystemMeta{
|
||||
WtVersion: p.Meta.WtVersion,
|
||||
GoOS: p.Meta.GoOS,
|
||||
OSVersion: p.Meta.OSVersion,
|
||||
KernelVersion: p.Meta.KernelVersion,
|
||||
NetworkAddresses: networkAddresses,
|
||||
Files: files,
|
||||
Capabilities: p.Meta.Capabilities,
|
||||
SyncMessageVersion: p.Meta.SyncMessageVersion,
|
||||
Flags: nmdata.Flags{
|
||||
ServerSSHAllowed: p.Meta.Flags.ServerSSHAllowed,
|
||||
DisableIPv6: p.Meta.Flags.DisableIPv6,
|
||||
},
|
||||
},
|
||||
Location: nmdata.PeerLocation{
|
||||
CountryCode: p.Location.CountryCode,
|
||||
CityName: p.Location.CityName,
|
||||
ConnectionIP: p.Location.ConnectionIP,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TwinPeer converts a real peer to its slim nmdata twin. Exported for the
|
||||
// port-forwarding integration, which builds proxy NetworkMaps holding twins.
|
||||
func TwinPeer(p *nbpeer.Peer) *nmdata.Peer {
|
||||
return twinPeer(p)
|
||||
}
|
||||
|
||||
// TwinPeers converts real peers to their slim nmdata twins.
|
||||
func TwinPeers(peers []*nbpeer.Peer) []*nmdata.Peer {
|
||||
out := make([]*nmdata.Peer, len(peers))
|
||||
for i, p := range peers {
|
||||
out[i] = twinPeer(p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TwinGroups converts real groups to their slim nmdata twins.
|
||||
func TwinGroups(groups []*Group) []*nmdata.Group {
|
||||
out := make([]*nmdata.Group, len(groups))
|
||||
for i, g := range groups {
|
||||
out[i] = twinGroup(g)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func twinGroup(g *Group) *nmdata.Group {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
return &nmdata.Group{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
PublicID: g.PublicID,
|
||||
Peers: g.Peers,
|
||||
}
|
||||
}
|
||||
|
||||
func twinPolicy(p *Policy) *nmdata.Policy {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
rules := make([]*nmdata.PolicyRule, 0, len(p.Rules))
|
||||
for _, r := range p.Rules {
|
||||
rules = append(rules, twinRule(r))
|
||||
}
|
||||
return &nmdata.Policy{
|
||||
ID: p.ID,
|
||||
PublicID: p.PublicID,
|
||||
Enabled: p.Enabled,
|
||||
SourcePostureChecks: p.SourcePostureChecks,
|
||||
Rules: rules,
|
||||
}
|
||||
}
|
||||
|
||||
func twinRule(r *PolicyRule) *nmdata.PolicyRule {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
var portRanges []nmdata.RulePortRange
|
||||
if r.PortRanges != nil {
|
||||
portRanges = make([]nmdata.RulePortRange, len(r.PortRanges))
|
||||
for i, pr := range r.PortRanges {
|
||||
portRanges[i] = nmdata.RulePortRange{Start: pr.Start, End: pr.End}
|
||||
}
|
||||
}
|
||||
return &nmdata.PolicyRule{
|
||||
ID: r.ID,
|
||||
PolicyID: r.PolicyID,
|
||||
Enabled: r.Enabled,
|
||||
Action: string(r.Action),
|
||||
Protocol: string(r.Protocol),
|
||||
Bidirectional: r.Bidirectional,
|
||||
Sources: r.Sources,
|
||||
Destinations: r.Destinations,
|
||||
SourceResource: nmdata.Resource{ID: r.SourceResource.ID, Type: string(r.SourceResource.Type)},
|
||||
DestinationResource: nmdata.Resource{ID: r.DestinationResource.ID, Type: string(r.DestinationResource.Type)},
|
||||
Ports: r.Ports,
|
||||
PortRanges: portRanges,
|
||||
AuthorizedGroups: r.AuthorizedGroups,
|
||||
AuthorizedUser: r.AuthorizedUser,
|
||||
}
|
||||
}
|
||||
|
||||
func twinRoute(r *nbroute.Route) *nmdata.Route {
|
||||
return &nmdata.Route{
|
||||
ID: string(r.ID),
|
||||
AccountID: r.AccountID,
|
||||
PublicID: r.PublicID,
|
||||
Network: r.Network,
|
||||
Domains: r.Domains,
|
||||
KeepRoute: r.KeepRoute,
|
||||
NetID: string(r.NetID),
|
||||
Description: r.Description,
|
||||
Peer: r.Peer,
|
||||
PeerID: r.PeerID,
|
||||
PeerGroups: r.PeerGroups,
|
||||
NetworkType: int(r.NetworkType),
|
||||
Masquerade: r.Masquerade,
|
||||
Metric: r.Metric,
|
||||
Enabled: r.Enabled,
|
||||
Groups: r.Groups,
|
||||
AccessControlGroups: r.AccessControlGroups,
|
||||
SkipAutoApply: r.SkipAutoApply,
|
||||
}
|
||||
}
|
||||
|
||||
// TwinRoute converts a real *route.Route to its slim nmdata twin. Exported for
|
||||
// tests that assert against twin routes returned in a NetworkMap.
|
||||
func TwinRoute(r *nbroute.Route) *nmdata.Route {
|
||||
return twinRoute(r)
|
||||
}
|
||||
|
||||
func TwinNetworkResource(r *resourceTypes.NetworkResource) *nmdata.NetworkResource {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &nmdata.NetworkResource{
|
||||
ID: r.ID,
|
||||
NetworkID: r.NetworkID,
|
||||
AccountID: r.AccountID,
|
||||
PublicID: r.PublicID,
|
||||
Name: r.Name,
|
||||
Description: r.Description,
|
||||
Type: string(r.Type),
|
||||
Address: r.Address,
|
||||
Domain: r.Domain,
|
||||
Prefix: r.Prefix,
|
||||
Enabled: r.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func twinRouter(r *routerTypes.NetworkRouter) *nmdata.NetworkRouter {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &nmdata.NetworkRouter{
|
||||
PublicID: r.PublicID,
|
||||
PeerGroups: r.PeerGroups,
|
||||
Masquerade: r.Masquerade,
|
||||
Metric: r.Metric,
|
||||
Enabled: r.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func twinNSG(n *nbdns.NameServerGroup) *nmdata.NameServerGroup {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
nameServers := make([]nmdata.NameServer, 0, len(n.NameServers))
|
||||
for _, ns := range n.NameServers {
|
||||
nameServers = append(nameServers, nmdata.NameServer{
|
||||
IP: ns.IP,
|
||||
NSType: int(ns.NSType),
|
||||
Port: ns.Port,
|
||||
})
|
||||
}
|
||||
return &nmdata.NameServerGroup{
|
||||
ID: n.ID,
|
||||
PublicID: n.PublicID,
|
||||
Name: n.Name,
|
||||
Description: n.Description,
|
||||
NameServers: nameServers,
|
||||
Groups: n.Groups,
|
||||
Primary: n.Primary,
|
||||
Domains: n.Domains,
|
||||
Enabled: n.Enabled,
|
||||
SearchDomainsEnabled: n.SearchDomainsEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
// TwinNetwork converts a real *Network to its slim twin. Exported for the
|
||||
// graceful-degrade path that builds a minimal NetworkMapComponents directly.
|
||||
func TwinNetwork(n *Network) *nmdata.Network {
|
||||
nc := n.Copy()
|
||||
return &nmdata.Network{
|
||||
Identifier: nc.Identifier,
|
||||
Net: nc.Net,
|
||||
NetV6: nc.NetV6,
|
||||
Dns: nc.Dns,
|
||||
Serial: int64(nc.Serial),
|
||||
}
|
||||
}
|
||||
|
||||
// TwinPostureChecksList converts posture checks to their slim nmdata twins.
|
||||
func TwinPostureChecksList(checks []*posture.Checks) []*nmdata.PostureChecks {
|
||||
out := make([]*nmdata.PostureChecks, 0, len(checks))
|
||||
for _, pc := range checks {
|
||||
out = append(out, TwinPostureChecks(pc))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TwinPostureChecks converts posture checks to their slim nmdata twin.
|
||||
func TwinPostureChecks(pc *posture.Checks) *nmdata.PostureChecks {
|
||||
if pc == nil {
|
||||
return nil
|
||||
}
|
||||
out := &nmdata.PostureChecks{ID: pc.ID}
|
||||
def := pc.Checks
|
||||
if def.NBVersionCheck != nil {
|
||||
out.Checks.NBVersionCheck = &nmdata.NBVersionCheck{MinVersion: def.NBVersionCheck.MinVersion}
|
||||
}
|
||||
if def.OSVersionCheck != nil {
|
||||
oc := &nmdata.OSVersionCheck{}
|
||||
if def.OSVersionCheck.Android != nil {
|
||||
oc.Android = &nmdata.MinVersionCheck{MinVersion: def.OSVersionCheck.Android.MinVersion}
|
||||
}
|
||||
if def.OSVersionCheck.Darwin != nil {
|
||||
oc.Darwin = &nmdata.MinVersionCheck{MinVersion: def.OSVersionCheck.Darwin.MinVersion}
|
||||
}
|
||||
if def.OSVersionCheck.Ios != nil {
|
||||
oc.Ios = &nmdata.MinVersionCheck{MinVersion: def.OSVersionCheck.Ios.MinVersion}
|
||||
}
|
||||
if def.OSVersionCheck.Linux != nil {
|
||||
oc.Linux = &nmdata.MinKernelVersionCheck{MinKernelVersion: def.OSVersionCheck.Linux.MinKernelVersion}
|
||||
}
|
||||
if def.OSVersionCheck.Windows != nil {
|
||||
oc.Windows = &nmdata.MinKernelVersionCheck{MinKernelVersion: def.OSVersionCheck.Windows.MinKernelVersion}
|
||||
}
|
||||
out.Checks.OSVersionCheck = oc
|
||||
}
|
||||
if def.GeoLocationCheck != nil {
|
||||
gc := &nmdata.GeoLocationCheck{Action: def.GeoLocationCheck.Action}
|
||||
for _, loc := range def.GeoLocationCheck.Locations {
|
||||
gc.Locations = append(gc.Locations, nmdata.GeoLocation{CountryCode: loc.CountryCode, CityName: loc.CityName})
|
||||
}
|
||||
out.Checks.GeoLocationCheck = gc
|
||||
}
|
||||
if def.PeerNetworkRangeCheck != nil {
|
||||
out.Checks.PeerNetworkRangeCheck = &nmdata.PeerNetworkRangeCheck{
|
||||
Action: def.PeerNetworkRangeCheck.Action,
|
||||
Ranges: def.PeerNetworkRangeCheck.Ranges,
|
||||
}
|
||||
}
|
||||
if def.ProcessCheck != nil {
|
||||
procs := make([]nmdata.Process, 0, len(def.ProcessCheck.Processes))
|
||||
for _, p := range def.ProcessCheck.Processes {
|
||||
procs = append(procs, nmdata.Process{LinuxPath: p.LinuxPath, MacPath: p.MacPath, WindowsPath: p.WindowsPath})
|
||||
}
|
||||
out.Checks.ProcessCheck = &nmdata.ProcessCheck{Processes: procs}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// buildAppliedZoneCandidates precomputes the account-level custom DNS zones
|
||||
// (record conversion) once; the per-peer distribution-group gate runs in the
|
||||
// components calc. Mirrors the account-level half of filterPeerAppliedZones.
|
||||
func buildAppliedZoneCandidates(accountZones []*zones.Zone) []networkmap.AppliedZoneCandidate {
|
||||
var out []networkmap.AppliedZoneCandidate
|
||||
for _, zone := range accountZones {
|
||||
if !zone.Enabled || len(zone.Records) == 0 {
|
||||
continue
|
||||
}
|
||||
simpleRecords := make([]nmdata.SimpleRecord, 0, len(zone.Records))
|
||||
for _, record := range zone.Records {
|
||||
var recordType int
|
||||
rData := record.Content
|
||||
switch record.Type {
|
||||
case records.RecordTypeA:
|
||||
recordType = int(dns.TypeA)
|
||||
case records.RecordTypeAAAA:
|
||||
recordType = int(dns.TypeAAAA)
|
||||
case records.RecordTypeCNAME:
|
||||
recordType = int(dns.TypeCNAME)
|
||||
rData = dns.Fqdn(record.Content)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
simpleRecords = append(simpleRecords, nmdata.SimpleRecord{
|
||||
Name: dns.Fqdn(record.Name),
|
||||
Type: recordType,
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: record.TTL,
|
||||
RData: rData,
|
||||
})
|
||||
}
|
||||
out = append(out, networkmap.AppliedZoneCandidate{
|
||||
DistributionGroups: zone.DistributionGroups,
|
||||
Zone: nmdata.CustomZone{
|
||||
Domain: dns.Fqdn(zone.Domain),
|
||||
Records: simpleRecords,
|
||||
SearchDomainDisabled: !zone.EnableSearchDomain,
|
||||
NonAuthoritative: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// buildPrivateServiceCandidates precomputes the connected-proxy A records per
|
||||
// private service (account-level); the per-peer access-group gate + apex merge
|
||||
// run in the components calc. Mirrors the account-level half of
|
||||
// SynthesizePrivateServiceZones.
|
||||
func (a *Account) buildPrivateServiceCandidates() []networkmap.PrivateServiceCandidate {
|
||||
if len(a.Services) == 0 {
|
||||
return nil
|
||||
}
|
||||
proxyPeersByCluster := a.GetProxyPeers()
|
||||
if len(proxyPeersByCluster) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var out []networkmap.PrivateServiceCandidate
|
||||
for _, svc := range a.Services {
|
||||
if svc == nil || !svc.Enabled || !svc.Private {
|
||||
continue
|
||||
}
|
||||
if len(svc.AccessGroups) == 0 {
|
||||
continue
|
||||
}
|
||||
proxyPeers := proxyPeersByCluster[svc.ProxyCluster]
|
||||
if len(proxyPeers) == 0 {
|
||||
continue
|
||||
}
|
||||
apex := a.privateServiceDomainZone(svc)
|
||||
if apex == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var recs []nmdata.SimpleRecord
|
||||
for _, p := range proxyPeers {
|
||||
if p == nil || !p.IP.IsValid() {
|
||||
continue
|
||||
}
|
||||
if p.Status == nil || !p.Status.Connected {
|
||||
continue
|
||||
}
|
||||
recs = append(recs, nmdata.SimpleRecord{
|
||||
Name: dns.Fqdn(svc.Domain),
|
||||
Type: int(dns.TypeA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: privateServiceDNSRecordTTL,
|
||||
RData: p.IP.String(),
|
||||
})
|
||||
}
|
||||
if len(recs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, networkmap.PrivateServiceCandidate{
|
||||
AccessGroups: svc.AccessGroups,
|
||||
Zone: nmdata.CustomZone{
|
||||
Domain: dns.Fqdn(apex),
|
||||
Records: recs,
|
||||
NonAuthoritative: true,
|
||||
SearchDomainDisabled: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TwinAccountSettings converts real account settings to the slim nmdata twin.
|
||||
// Exported for callers of the twin-based sync response builders.
|
||||
func TwinAccountSettings(s *Settings) *nmdata.AccountSettingsInfo {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return &nmdata.AccountSettingsInfo{
|
||||
PeerLoginExpirationEnabled: s.PeerLoginExpirationEnabled,
|
||||
PeerLoginExpiration: s.PeerLoginExpiration,
|
||||
PeerInactivityExpirationEnabled: s.PeerInactivityExpirationEnabled,
|
||||
PeerInactivityExpiration: s.PeerInactivityExpiration,
|
||||
DNSDomain: s.DNSDomain,
|
||||
IPv6EnabledGroups: s.IPv6EnabledGroups,
|
||||
RoutingPeerDNSResolutionEnabled: s.RoutingPeerDNSResolutionEnabled,
|
||||
LazyConnectionEnabled: s.LazyConnectionEnabled,
|
||||
AutoUpdateVersion: s.AutoUpdateVersion,
|
||||
AutoUpdateAlways: s.AutoUpdateAlways,
|
||||
MetricsPushEnabled: s.MetricsPushEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
func fromTwinCustomZone(z nmdata.CustomZone) nbdns.CustomZone {
|
||||
records := make([]nbdns.SimpleRecord, 0, len(z.Records))
|
||||
for _, r := range z.Records {
|
||||
records = append(records, nbdns.SimpleRecord{
|
||||
Name: r.Name,
|
||||
Type: r.Type,
|
||||
Class: r.Class,
|
||||
TTL: r.TTL,
|
||||
RData: r.RData,
|
||||
})
|
||||
}
|
||||
return nbdns.CustomZone{
|
||||
Domain: z.Domain,
|
||||
Records: records,
|
||||
SearchDomainDisabled: z.SearchDomainDisabled,
|
||||
NonAuthoritative: z.NonAuthoritative,
|
||||
}
|
||||
}
|
||||
|
||||
// TwinCustomZone converts a real DNS custom zone to its slim nmdata twin.
|
||||
// Exported for the network-map controller's DB-store path, which feeds real
|
||||
// zones into the twin-based components calculation.
|
||||
func TwinCustomZone(z nbdns.CustomZone) nmdata.CustomZone {
|
||||
records := make([]nmdata.SimpleRecord, 0, len(z.Records))
|
||||
for _, r := range z.Records {
|
||||
records = append(records, nmdata.SimpleRecord{
|
||||
Name: r.Name,
|
||||
Type: r.Type,
|
||||
Class: r.Class,
|
||||
TTL: r.TTL,
|
||||
RData: r.RData,
|
||||
})
|
||||
}
|
||||
return nmdata.CustomZone{
|
||||
Domain: z.Domain,
|
||||
Records: records,
|
||||
SearchDomainDisabled: z.SearchDomainDisabled,
|
||||
NonAuthoritative: z.NonAuthoritative,
|
||||
}
|
||||
}
|
||||
|
||||
// twinProxyDomains converts the account's registered reverse-proxy domains to
|
||||
// their slim twins, so private-service zone apex resolution runs on the twin.
|
||||
func twinProxyDomains(domains []*proxydomain.Domain) []nmdata.ProxyDomain {
|
||||
if len(domains) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]nmdata.ProxyDomain, 0, len(domains))
|
||||
for _, d := range domains {
|
||||
if d == nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, nmdata.ProxyDomain{Domain: d.Domain, TargetCluster: d.TargetCluster})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
func TestPrivateService_NetworkMap_UserPeer_AndProxyPeer(t *testing.T) {
|
||||
@@ -17,7 +18,6 @@ func TestPrivateService_NetworkMap_UserPeer_AndProxyPeer(t *testing.T) {
|
||||
account.Peers["proxy-peer"].Meta.WtVersion = "0.50.0"
|
||||
|
||||
ctx := context.Background()
|
||||
account.InjectProxyPolicies(ctx)
|
||||
|
||||
validated := map[string]struct{}{
|
||||
"user-peer": {},
|
||||
@@ -48,7 +48,7 @@ func TestPrivateService_NetworkMap_UserPeer_AndProxyPeer(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func netmapPeerIDs(peers []*ComponentPeer) []string {
|
||||
func netmapPeerIDs(peers []*nmdata.Peer) []string {
|
||||
ids := make([]string, 0, len(peers))
|
||||
for _, p := range peers {
|
||||
if p == nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
@@ -13,13 +14,12 @@ import (
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones/records"
|
||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
func setupTestAccount() *Account {
|
||||
@@ -666,7 +666,7 @@ func Test_ExpandPortsAndRanges_SSHRuleExpansion(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ExpandPortsAndRanges(tt.base, tt.rule, tt.peer.ToComponent())
|
||||
result := ExpandPortsAndRanges(tt.base, tt.rule, tt.peer)
|
||||
|
||||
var ports []string
|
||||
for _, fr := range result {
|
||||
@@ -1040,518 +1040,6 @@ func Test_FilterZoneRecordsForPeers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_filterPeerAppliedZones(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
accountZones []*zones.Zone
|
||||
peerGroups LookupMap
|
||||
expected []nbdns.CustomZone
|
||||
}{
|
||||
{
|
||||
name: "empty peer groups returns empty custom zones",
|
||||
accountZones: []*zones.Zone{},
|
||||
peerGroups: LookupMap{},
|
||||
expected: []nbdns.CustomZone{},
|
||||
},
|
||||
{
|
||||
name: "peer has access to zone with A record",
|
||||
accountZones: []*zones.Zone{
|
||||
{
|
||||
ID: "zone1",
|
||||
Domain: "example.com",
|
||||
Enabled: true,
|
||||
EnableSearchDomain: false,
|
||||
DistributionGroups: []string{"group1"},
|
||||
Records: []*records.Record{
|
||||
{
|
||||
ID: "record1",
|
||||
Name: "www.example.com",
|
||||
Type: records.RecordTypeA,
|
||||
Content: "192.168.1.1",
|
||||
TTL: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
peerGroups: LookupMap{"group1": struct{}{}},
|
||||
expected: []nbdns.CustomZone{
|
||||
{
|
||||
Domain: "example.com.",
|
||||
Records: []nbdns.SimpleRecord{
|
||||
{
|
||||
Name: "www.example.com.",
|
||||
Type: int(dns.TypeA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: 300,
|
||||
RData: "192.168.1.1",
|
||||
},
|
||||
},
|
||||
SearchDomainDisabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "peer has access to zone with search domain enabled",
|
||||
accountZones: []*zones.Zone{
|
||||
{
|
||||
ID: "zone1",
|
||||
Domain: "internal.local",
|
||||
Enabled: true,
|
||||
EnableSearchDomain: true,
|
||||
DistributionGroups: []string{"group1"},
|
||||
Records: []*records.Record{
|
||||
{
|
||||
ID: "record1",
|
||||
Name: "api.internal.local",
|
||||
Type: records.RecordTypeA,
|
||||
Content: "10.0.0.1",
|
||||
TTL: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
peerGroups: LookupMap{"group1": struct{}{}},
|
||||
expected: []nbdns.CustomZone{
|
||||
{
|
||||
Domain: "internal.local.",
|
||||
Records: []nbdns.SimpleRecord{
|
||||
{
|
||||
Name: "api.internal.local.",
|
||||
Type: int(dns.TypeA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: 600,
|
||||
RData: "10.0.0.1",
|
||||
},
|
||||
},
|
||||
SearchDomainDisabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "peer has no access to zone",
|
||||
accountZones: []*zones.Zone{
|
||||
{
|
||||
ID: "zone1",
|
||||
Domain: "private.com",
|
||||
Enabled: true,
|
||||
EnableSearchDomain: false,
|
||||
DistributionGroups: []string{"group2"},
|
||||
Records: []*records.Record{
|
||||
{
|
||||
ID: "record1",
|
||||
Name: "secret.private.com",
|
||||
Type: records.RecordTypeA,
|
||||
Content: "192.168.1.1",
|
||||
TTL: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
peerGroups: LookupMap{"group1": struct{}{}},
|
||||
expected: []nbdns.CustomZone{},
|
||||
},
|
||||
{
|
||||
name: "disabled zone is filtered out",
|
||||
accountZones: []*zones.Zone{
|
||||
{
|
||||
ID: "zone1",
|
||||
Domain: "disabled.com",
|
||||
Enabled: false,
|
||||
EnableSearchDomain: false,
|
||||
DistributionGroups: []string{"group1"},
|
||||
Records: []*records.Record{
|
||||
{
|
||||
ID: "record1",
|
||||
Name: "www.disabled.com",
|
||||
Type: records.RecordTypeA,
|
||||
Content: "192.168.1.1",
|
||||
TTL: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
peerGroups: LookupMap{"group1": struct{}{}},
|
||||
expected: []nbdns.CustomZone{},
|
||||
},
|
||||
{
|
||||
name: "zone with no records is filtered out",
|
||||
accountZones: []*zones.Zone{
|
||||
{
|
||||
ID: "zone1",
|
||||
Domain: "empty.com",
|
||||
Enabled: true,
|
||||
EnableSearchDomain: false,
|
||||
DistributionGroups: []string{"group1"},
|
||||
Records: []*records.Record{},
|
||||
},
|
||||
},
|
||||
peerGroups: LookupMap{"group1": struct{}{}},
|
||||
expected: []nbdns.CustomZone{},
|
||||
},
|
||||
{
|
||||
name: "peer has access via multiple groups",
|
||||
accountZones: []*zones.Zone{
|
||||
{
|
||||
ID: "zone1",
|
||||
Domain: "multi.com",
|
||||
Enabled: true,
|
||||
EnableSearchDomain: false,
|
||||
DistributionGroups: []string{"group1", "group2", "group3"},
|
||||
Records: []*records.Record{
|
||||
{
|
||||
ID: "record1",
|
||||
Name: "www.multi.com",
|
||||
Type: records.RecordTypeA,
|
||||
Content: "192.168.1.1",
|
||||
TTL: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
peerGroups: LookupMap{"group2": struct{}{}},
|
||||
expected: []nbdns.CustomZone{
|
||||
{
|
||||
Domain: "multi.com.",
|
||||
Records: []nbdns.SimpleRecord{
|
||||
{
|
||||
Name: "www.multi.com.",
|
||||
Type: int(dns.TypeA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: 300,
|
||||
RData: "192.168.1.1",
|
||||
},
|
||||
},
|
||||
SearchDomainDisabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple zones with mixed access",
|
||||
accountZones: []*zones.Zone{
|
||||
{
|
||||
ID: "zone1",
|
||||
Domain: "allowed.com",
|
||||
Enabled: true,
|
||||
EnableSearchDomain: false,
|
||||
DistributionGroups: []string{"group1"},
|
||||
Records: []*records.Record{
|
||||
{
|
||||
ID: "record1",
|
||||
Name: "www.allowed.com",
|
||||
Type: records.RecordTypeA,
|
||||
Content: "192.168.1.1",
|
||||
TTL: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "zone2",
|
||||
Domain: "denied.com",
|
||||
Enabled: true,
|
||||
EnableSearchDomain: false,
|
||||
DistributionGroups: []string{"group2"},
|
||||
Records: []*records.Record{
|
||||
{
|
||||
ID: "record2",
|
||||
Name: "www.denied.com",
|
||||
Type: records.RecordTypeA,
|
||||
Content: "192.168.1.2",
|
||||
TTL: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
peerGroups: LookupMap{"group1": struct{}{}},
|
||||
expected: []nbdns.CustomZone{
|
||||
{
|
||||
Domain: "allowed.com.",
|
||||
Records: []nbdns.SimpleRecord{
|
||||
{
|
||||
Name: "www.allowed.com.",
|
||||
Type: int(dns.TypeA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: 300,
|
||||
RData: "192.168.1.1",
|
||||
},
|
||||
},
|
||||
SearchDomainDisabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "zone with multiple record types",
|
||||
accountZones: []*zones.Zone{
|
||||
{
|
||||
ID: "zone1",
|
||||
Domain: "mixed.com",
|
||||
Enabled: true,
|
||||
EnableSearchDomain: false,
|
||||
DistributionGroups: []string{"group1"},
|
||||
Records: []*records.Record{
|
||||
{
|
||||
ID: "record1",
|
||||
Name: "www.mixed.com",
|
||||
Type: records.RecordTypeA,
|
||||
Content: "192.168.1.1",
|
||||
TTL: 300,
|
||||
},
|
||||
{
|
||||
ID: "record2",
|
||||
Name: "ipv6.mixed.com",
|
||||
Type: records.RecordTypeAAAA,
|
||||
Content: "2001:db8::1",
|
||||
TTL: 600,
|
||||
},
|
||||
{
|
||||
ID: "record3",
|
||||
Name: "alias.mixed.com",
|
||||
Type: records.RecordTypeCNAME,
|
||||
Content: "www.mixed.com",
|
||||
TTL: 900,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
peerGroups: LookupMap{"group1": struct{}{}},
|
||||
expected: []nbdns.CustomZone{
|
||||
{
|
||||
Domain: "mixed.com.",
|
||||
Records: []nbdns.SimpleRecord{
|
||||
{
|
||||
Name: "www.mixed.com.",
|
||||
Type: int(dns.TypeA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: 300,
|
||||
RData: "192.168.1.1",
|
||||
},
|
||||
{
|
||||
Name: "ipv6.mixed.com.",
|
||||
Type: int(dns.TypeAAAA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: 600,
|
||||
RData: "2001:db8::1",
|
||||
},
|
||||
{
|
||||
Name: "alias.mixed.com.",
|
||||
Type: int(dns.TypeCNAME),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: 900,
|
||||
RData: "www.mixed.com.",
|
||||
},
|
||||
},
|
||||
SearchDomainDisabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple zones both accessible",
|
||||
accountZones: []*zones.Zone{
|
||||
{
|
||||
ID: "zone1",
|
||||
Domain: "first.com",
|
||||
Enabled: true,
|
||||
EnableSearchDomain: true,
|
||||
DistributionGroups: []string{"group1"},
|
||||
Records: []*records.Record{
|
||||
{
|
||||
ID: "record1",
|
||||
Name: "www.first.com",
|
||||
Type: records.RecordTypeA,
|
||||
Content: "192.168.1.1",
|
||||
TTL: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "zone2",
|
||||
Domain: "second.com",
|
||||
Enabled: true,
|
||||
EnableSearchDomain: false,
|
||||
DistributionGroups: []string{"group1"},
|
||||
Records: []*records.Record{
|
||||
{
|
||||
ID: "record2",
|
||||
Name: "www.second.com",
|
||||
Type: records.RecordTypeA,
|
||||
Content: "192.168.1.2",
|
||||
TTL: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
peerGroups: LookupMap{"group1": struct{}{}},
|
||||
expected: []nbdns.CustomZone{
|
||||
{
|
||||
Domain: "first.com.",
|
||||
Records: []nbdns.SimpleRecord{
|
||||
{
|
||||
Name: "www.first.com.",
|
||||
Type: int(dns.TypeA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: 300,
|
||||
RData: "192.168.1.1",
|
||||
},
|
||||
},
|
||||
SearchDomainDisabled: false,
|
||||
},
|
||||
{
|
||||
Domain: "second.com.",
|
||||
Records: []nbdns.SimpleRecord{
|
||||
{
|
||||
Name: "www.second.com.",
|
||||
Type: int(dns.TypeA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: 600,
|
||||
RData: "192.168.1.2",
|
||||
},
|
||||
},
|
||||
SearchDomainDisabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "zone with multiple records of same type",
|
||||
accountZones: []*zones.Zone{
|
||||
{
|
||||
ID: "zone1",
|
||||
Domain: "multi-a.com",
|
||||
Enabled: true,
|
||||
EnableSearchDomain: false,
|
||||
DistributionGroups: []string{"group1"},
|
||||
Records: []*records.Record{
|
||||
{
|
||||
ID: "record1",
|
||||
Name: "www.multi-a.com",
|
||||
Type: records.RecordTypeA,
|
||||
Content: "192.168.1.1",
|
||||
TTL: 300,
|
||||
},
|
||||
{
|
||||
ID: "record2",
|
||||
Name: "www.multi-a.com",
|
||||
Type: records.RecordTypeA,
|
||||
Content: "192.168.1.2",
|
||||
TTL: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
peerGroups: LookupMap{"group1": struct{}{}},
|
||||
expected: []nbdns.CustomZone{
|
||||
{
|
||||
Domain: "multi-a.com.",
|
||||
Records: []nbdns.SimpleRecord{
|
||||
{
|
||||
Name: "www.multi-a.com.",
|
||||
Type: int(dns.TypeA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: 300,
|
||||
RData: "192.168.1.1",
|
||||
},
|
||||
{
|
||||
Name: "www.multi-a.com.",
|
||||
Type: int(dns.TypeA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: 300,
|
||||
RData: "192.168.1.2",
|
||||
},
|
||||
},
|
||||
SearchDomainDisabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "peer in multiple groups accessing different zones",
|
||||
accountZones: []*zones.Zone{
|
||||
{
|
||||
ID: "zone1",
|
||||
Domain: "zone1.com",
|
||||
Enabled: true,
|
||||
EnableSearchDomain: false,
|
||||
DistributionGroups: []string{"group1"},
|
||||
Records: []*records.Record{
|
||||
{
|
||||
ID: "record1",
|
||||
Name: "www.zone1.com",
|
||||
Type: records.RecordTypeA,
|
||||
Content: "192.168.1.1",
|
||||
TTL: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "zone2",
|
||||
Domain: "zone2.com",
|
||||
Enabled: true,
|
||||
EnableSearchDomain: false,
|
||||
DistributionGroups: []string{"group2"},
|
||||
Records: []*records.Record{
|
||||
{
|
||||
ID: "record2",
|
||||
Name: "www.zone2.com",
|
||||
Type: records.RecordTypeA,
|
||||
Content: "192.168.1.2",
|
||||
TTL: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
peerGroups: LookupMap{"group1": struct{}{}, "group2": struct{}{}},
|
||||
expected: []nbdns.CustomZone{
|
||||
{
|
||||
Domain: "zone1.com.",
|
||||
Records: []nbdns.SimpleRecord{
|
||||
{
|
||||
Name: "www.zone1.com.",
|
||||
Type: int(dns.TypeA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: 300,
|
||||
RData: "192.168.1.1",
|
||||
},
|
||||
},
|
||||
SearchDomainDisabled: true,
|
||||
},
|
||||
{
|
||||
Domain: "zone2.com.",
|
||||
Records: []nbdns.SimpleRecord{
|
||||
{
|
||||
Name: "www.zone2.com.",
|
||||
Type: int(dns.TypeA),
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: 300,
|
||||
RData: "192.168.1.2",
|
||||
},
|
||||
},
|
||||
SearchDomainDisabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := filterPeerAppliedZones(ctx, tt.accountZones, tt.peerGroups)
|
||||
require.Equal(t, len(tt.expected), len(result), "number of custom zones should match")
|
||||
|
||||
for i, expectedZone := range tt.expected {
|
||||
assert.Equal(t, expectedZone.Domain, result[i].Domain, "domain should match")
|
||||
assert.Equal(t, expectedZone.SearchDomainDisabled, result[i].SearchDomainDisabled, "search domain disabled flag should match")
|
||||
assert.Equal(t, len(expectedZone.Records), len(result[i].Records), "number of records should match")
|
||||
|
||||
for j, expectedRecord := range expectedZone.Records {
|
||||
assert.Equal(t, expectedRecord.Name, result[i].Records[j].Name, "record name should match")
|
||||
assert.Equal(t, expectedRecord.Type, result[i].Records[j].Type, "record type should match")
|
||||
assert.Equal(t, expectedRecord.Class, result[i].Records[j].Class, "record class should match")
|
||||
assert.Equal(t, expectedRecord.TTL, result[i].Records[j].TTL, "record TTL should match")
|
||||
assert.Equal(t, expectedRecord.RData, result[i].Records[j].RData, "record RData should match")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectPrivateServicePolicies_ProxyPeerGetsInboundRule(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -1564,6 +1052,7 @@ func TestInjectPrivateServicePolicies_ProxyPeerGetsInboundRule(t *testing.T) {
|
||||
Identifier: "net-1",
|
||||
Net: net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.CIDRMask(10, 32)},
|
||||
},
|
||||
Settings: &Settings{},
|
||||
Peers: map[string]*nbpeer.Peer{
|
||||
"user-peer": {
|
||||
ID: "user-peer",
|
||||
@@ -1614,41 +1103,25 @@ func TestInjectPrivateServicePolicies_ProxyPeerGetsInboundRule(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
account.InjectProxyPolicies(ctx)
|
||||
|
||||
var found *Policy
|
||||
for _, p := range account.Policies {
|
||||
if p != nil && p.ID == "private-access-svc-1-proxy-peer" {
|
||||
found = p
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, found, "expected synthesised private-access policy in account.Policies")
|
||||
found := findPolicy(injectedPolicies(account), "private-access-svc-1-proxy-peer")
|
||||
require.NotNil(t, found, "expected synthesised private-access policy in the twin store")
|
||||
require.Len(t, found.Rules, 1, "policy should have exactly one rule")
|
||||
rule := found.Rules[0]
|
||||
assert.Equal(t, []string{"grp-admins"}, rule.Sources, "sources should be group IDs verbatim")
|
||||
assert.Equal(t, "proxy-peer", rule.DestinationResource.ID, "destination resource should be the proxy peer ID")
|
||||
assert.Equal(t, ResourceTypePeer, rule.DestinationResource.Type, "destination resource type should be peer")
|
||||
assert.Equal(t, string(ResourceTypePeer), rule.DestinationResource.Type, "destination resource type should be peer")
|
||||
|
||||
validatedPeersMap := map[string]struct{}{
|
||||
"user-peer": {},
|
||||
"proxy-peer": {},
|
||||
}
|
||||
|
||||
proxyPeer := account.Peers["proxy-peer"]
|
||||
aclPeers, firewallRules, _, _ := account.GetPeerConnectionResources(ctx, proxyPeer, validatedPeersMap, nil)
|
||||
nm := account.GetPeerNetworkMapFromComponents(ctx, "proxy-peer", nbdns.CustomZone{}, nil, validatedPeersMap, nil, nil, nil, nil)
|
||||
|
||||
var sawUserAsAclPeer bool
|
||||
for _, p := range aclPeers {
|
||||
if p.ID == "user-peer" {
|
||||
sawUserAsAclPeer = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, sawUserAsAclPeer, "proxy peer should see the user peer as an ACL peer")
|
||||
assert.Contains(t, netmapPeerIDs(nm.Peers), "user-peer", "proxy peer should see the user peer as an ACL peer")
|
||||
|
||||
var inboundRules []*FirewallRule
|
||||
for _, r := range firewallRules {
|
||||
for _, r := range nm.FirewallRules {
|
||||
if r.Direction == FirewallRuleDirectionIN && r.PeerIP == userPeerIP.String() {
|
||||
inboundRules = append(inboundRules, r)
|
||||
}
|
||||
@@ -1657,29 +1130,23 @@ func TestInjectPrivateServicePolicies_ProxyPeerGetsInboundRule(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInjectPrivateServicePolicies_NotPrivate_NoPolicy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
account := privateServiceTestAccount(t)
|
||||
account.Services[0].Private = false
|
||||
|
||||
account.InjectProxyPolicies(ctx)
|
||||
assert.False(t, hasPrivateAccessPolicy(account, "svc-1"), "non-private service must not synthesise an access policy")
|
||||
}
|
||||
|
||||
func TestInjectPrivateServicePolicies_EmptyAccessGroups_NoPolicy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
account := privateServiceTestAccount(t)
|
||||
account.Services[0].AccessGroups = nil
|
||||
|
||||
account.InjectProxyPolicies(ctx)
|
||||
assert.False(t, hasPrivateAccessPolicy(account, "svc-1"), "private service with no access groups must not synthesise a policy")
|
||||
}
|
||||
|
||||
func TestInjectPrivateServicePolicies_NoProxyPeers_NoPolicy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
account := privateServiceTestAccount(t)
|
||||
delete(account.Peers, "proxy-peer")
|
||||
|
||||
account.InjectProxyPolicies(ctx)
|
||||
assert.False(t, hasPrivateAccessPolicy(account, "svc-1"), "policy must not synthesise when the cluster has no proxy peers")
|
||||
}
|
||||
|
||||
@@ -1742,10 +1209,27 @@ func privateServiceTestAccount(t *testing.T) *Account {
|
||||
}
|
||||
}
|
||||
|
||||
// injectedPolicies returns the twin's policies with the synthesised proxy ACLs
|
||||
// already in place, the way the per-peer computation sees them.
|
||||
func injectedPolicies(account *Account) []*nmdata.Policy {
|
||||
nmd := account.toNetworkMapData(nil, nil, nil, nil, nil)
|
||||
nmd.InjectProxyPolicies()
|
||||
return nmd.Policies
|
||||
}
|
||||
|
||||
func findPolicy(policies []*nmdata.Policy, id string) *nmdata.Policy {
|
||||
for _, p := range policies {
|
||||
if p != nil && p.ID == id {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasPrivateAccessPolicy(account *Account, serviceID string) bool {
|
||||
prefix := "private-access-" + serviceID + "-"
|
||||
for _, p := range account.Policies {
|
||||
if p != nil && len(p.ID) > len(prefix) && p.ID[:len(prefix)] == prefix {
|
||||
for _, p := range injectedPolicies(account) {
|
||||
if p != nil && strings.HasPrefix(p.ID, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1781,41 +1265,45 @@ func TestForcesRoutingPeerDNSResolution(t *testing.T) {
|
||||
return buildAccountRes(serviceEnabled, targetEnabled, resourceEnabled, targetType, resourceTypes.Domain)
|
||||
}
|
||||
|
||||
forced := func(account *Account, peerID string) bool {
|
||||
nmd := account.toNetworkMapData(nil, nil, nil, account.GetResourceRoutersMap(), nil)
|
||||
return nmd.GetPeerNetworkMapComponents(peerID, nmdata.CustomZone{}).ForceRoutingPeerDNSResolution
|
||||
}
|
||||
|
||||
t.Run("router peer for RP-targeted domain resource is forced", func(t *testing.T) {
|
||||
account := buildAccount(true, true, true, service.TargetTypeDomain)
|
||||
routers := account.GetResourceRoutersMap()
|
||||
assert.True(t, account.forcesRoutingPeerDNSResolution("router-peer", routers), "direct router peer should be forced")
|
||||
assert.True(t, account.forcesRoutingPeerDNSResolution("router-peer-grp", routers), "group-member router peer should be forced")
|
||||
assert.True(t, forced(account, "router-peer"), "direct router peer should be forced")
|
||||
assert.True(t, forced(account, "router-peer-grp"), "group-member router peer should be forced")
|
||||
})
|
||||
|
||||
t.Run("non-router peer is not forced", func(t *testing.T) {
|
||||
account := buildAccount(true, true, true, service.TargetTypeDomain)
|
||||
assert.False(t, account.forcesRoutingPeerDNSResolution("other-peer", account.GetResourceRoutersMap()))
|
||||
assert.False(t, forced(account, "other-peer"))
|
||||
})
|
||||
|
||||
t.Run("not forced when service disabled", func(t *testing.T) {
|
||||
account := buildAccount(false, true, true, service.TargetTypeDomain)
|
||||
assert.False(t, account.forcesRoutingPeerDNSResolution("router-peer", account.GetResourceRoutersMap()))
|
||||
assert.False(t, forced(account, "router-peer"))
|
||||
})
|
||||
|
||||
t.Run("not forced when target disabled", func(t *testing.T) {
|
||||
account := buildAccount(true, false, true, service.TargetTypeDomain)
|
||||
assert.False(t, account.forcesRoutingPeerDNSResolution("router-peer", account.GetResourceRoutersMap()))
|
||||
assert.False(t, forced(account, "router-peer"))
|
||||
})
|
||||
|
||||
t.Run("not forced when resource disabled", func(t *testing.T) {
|
||||
account := buildAccount(true, true, false, service.TargetTypeDomain)
|
||||
assert.False(t, account.forcesRoutingPeerDNSResolution("router-peer", account.GetResourceRoutersMap()))
|
||||
assert.False(t, forced(account, "router-peer"))
|
||||
})
|
||||
|
||||
t.Run("not forced for non-domain target type", func(t *testing.T) {
|
||||
account := buildAccount(true, true, true, service.TargetTypePeer)
|
||||
assert.False(t, account.forcesRoutingPeerDNSResolution("router-peer", account.GetResourceRoutersMap()))
|
||||
assert.False(t, forced(account, "router-peer"))
|
||||
})
|
||||
|
||||
t.Run("not forced when targeted resource is not a domain", func(t *testing.T) {
|
||||
account := buildAccountRes(true, true, true, service.TargetTypeDomain, resourceTypes.Host)
|
||||
assert.False(t, account.forcesRoutingPeerDNSResolution("router-peer", account.GetResourceRoutersMap()),
|
||||
assert.False(t, forced(account, "router-peer"),
|
||||
"a domain target pointing at a non-domain resource must not force resolution")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,54 +2,31 @@ package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
nbroute "github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
sharedtypes "github.com/netbirdio/netbird/shared/management/types"
|
||||
)
|
||||
|
||||
// Type aliases for types relocated to shared/management/types so that the
|
||||
// client-side compute path can depend on them
|
||||
|
||||
type DNSSettings = sharedtypes.DNSSettings
|
||||
|
||||
type FirewallRule = sharedtypes.FirewallRule
|
||||
|
||||
type Network = sharedtypes.Network
|
||||
type NetworkMap = sharedtypes.NetworkMap
|
||||
type ForwardingRule = sharedtypes.ForwardingRule
|
||||
|
||||
type Policy = sharedtypes.Policy
|
||||
type PolicyUpdateOperation = sharedtypes.PolicyUpdateOperation
|
||||
|
||||
type PolicyRule = sharedtypes.PolicyRule
|
||||
type PolicyUpdateOperationType = sharedtypes.PolicyUpdateOperationType
|
||||
type PolicyTrafficActionType = sharedtypes.PolicyTrafficActionType
|
||||
type PolicyRuleProtocolType = sharedtypes.PolicyRuleProtocolType
|
||||
type PolicyRuleDirection = sharedtypes.PolicyRuleDirection
|
||||
type RulePortRange = sharedtypes.RulePortRange
|
||||
|
||||
type Resource = sharedtypes.Resource
|
||||
type ResourceType = sharedtypes.ResourceType
|
||||
|
||||
type RouteFirewallRule = sharedtypes.RouteFirewallRule
|
||||
|
||||
type NetworkMapComponents = sharedtypes.NetworkMapComponents
|
||||
|
||||
type ComponentPeer = sharedtypes.ComponentPeer
|
||||
type ComponentGroup = sharedtypes.ComponentGroup
|
||||
type ComponentRouter = sharedtypes.ComponentRouter
|
||||
type ComponentResource = sharedtypes.ComponentResource
|
||||
type ComponentResourceType = sharedtypes.ComponentResourceType
|
||||
|
||||
const (
|
||||
ComponentResourceHost = sharedtypes.ComponentResourceHost
|
||||
ComponentResourceSubnet = sharedtypes.ComponentResourceSubnet
|
||||
ComponentResourceDomain = sharedtypes.ComponentResourceDomain
|
||||
)
|
||||
|
||||
var EmptyNetworkMapComponents = sharedtypes.EmptyNetworkMapComponents
|
||||
|
||||
type AccountSettingsInfo = sharedtypes.AccountSettingsInfo
|
||||
@@ -60,54 +37,36 @@ type NetworkMapComponentsCompact = sharedtypes.NetworkMapComponentsCompact
|
||||
type LookupMap = sharedtypes.LookupMap
|
||||
type FirewallRuleContext = sharedtypes.FirewallRuleContext
|
||||
|
||||
const GroupAllName = sharedtypes.GroupAllName
|
||||
|
||||
// Function forwarders preserve types.X(...) call sites that previously
|
||||
// resolved to package-local funcs. Plain forwarders (not var aliases) keep
|
||||
// the symbol immutable and allow the inliner to flatten the call.
|
||||
|
||||
func ParseRuleString(rule string) (PolicyRuleProtocolType, RulePortRange, error) {
|
||||
return sharedtypes.ParseRuleString(rule)
|
||||
}
|
||||
|
||||
func PolicyRuleImpliesLegacySSH(rule *PolicyRule) bool {
|
||||
return sharedtypes.PolicyRuleImpliesLegacySSH(rule)
|
||||
return nmdata.PolicyRuleImpliesLegacySSH(twinRule(rule))
|
||||
}
|
||||
|
||||
func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *ComponentPeer) []*FirewallRule {
|
||||
return sharedtypes.ExpandPortsAndRanges(base, rule, peer)
|
||||
// ExpandPortsAndRanges / AppendIPv6FirewallRule / GenerateRouteFirewallRules
|
||||
// forward to the shared twin-typed helpers, converting the real types the
|
||||
// legacy Account calc still uses to nmdata twins at this boundary.
|
||||
|
||||
func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer) []*FirewallRule {
|
||||
return sharedtypes.ExpandPortsAndRanges(base, twinRule(rule), twinPeer(peer))
|
||||
}
|
||||
|
||||
func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *ComponentPeer, rule *PolicyRule, rc FirewallRuleContext) []*FirewallRule {
|
||||
return sharedtypes.AppendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, rc)
|
||||
func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *nbpeer.Peer, rule *PolicyRule, rc FirewallRuleContext) []*FirewallRule {
|
||||
return sharedtypes.AppendIPv6FirewallRule(rules, rulesExists, twinPeer(peer), twinPeer(targetPeer), twinRule(rule), rc)
|
||||
}
|
||||
|
||||
func CalculateNetworkMapFromComponents(ctx context.Context, components *NetworkMapComponents) *NetworkMap {
|
||||
return sharedtypes.CalculateNetworkMapFromComponents(ctx, components)
|
||||
}
|
||||
|
||||
func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*ComponentPeer, direction int, includeIPv6 bool) []*RouteFirewallRule {
|
||||
return sharedtypes.GenerateRouteFirewallRules(ctx, route, rule, groupPeers, direction, includeIPv6)
|
||||
}
|
||||
|
||||
func AllocateIPv6Subnet(r *rand.Rand) net.IPNet {
|
||||
return sharedtypes.AllocateIPv6Subnet(r)
|
||||
}
|
||||
|
||||
func NewNetwork() *Network {
|
||||
return sharedtypes.NewNetwork()
|
||||
}
|
||||
|
||||
func AllocatePeerIP(prefix netip.Prefix, takenIps []netip.Addr) (netip.Addr, error) {
|
||||
return sharedtypes.AllocatePeerIP(prefix, takenIps)
|
||||
}
|
||||
|
||||
func AllocateRandomPeerIP(prefix netip.Prefix) (netip.Addr, error) {
|
||||
return sharedtypes.AllocateRandomPeerIP(prefix)
|
||||
}
|
||||
|
||||
func AllocateRandomPeerIPv6(prefix netip.Prefix) (netip.Addr, error) {
|
||||
return sharedtypes.AllocateRandomPeerIPv6(prefix)
|
||||
}
|
||||
|
||||
func ParseRuleString(rule string) (PolicyRuleProtocolType, RulePortRange, error) {
|
||||
return sharedtypes.ParseRuleString(rule)
|
||||
func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int, includeIPv6 bool) []*RouteFirewallRule {
|
||||
return sharedtypes.GenerateRouteFirewallRules(ctx, twinRoute(route), twinRule(rule), TwinPeers(groupPeers), direction, includeIPv6)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -115,6 +74,11 @@ const (
|
||||
FirewallRuleDirectionOUT = sharedtypes.FirewallRuleDirectionOUT
|
||||
)
|
||||
|
||||
const (
|
||||
AllowedIPsFormat = sharedtypes.AllowedIPsFormat
|
||||
AllowedIPsV6Format = sharedtypes.AllowedIPsV6Format
|
||||
)
|
||||
|
||||
const (
|
||||
ResourceTypePeer = sharedtypes.ResourceTypePeer
|
||||
ResourceTypeDomain = sharedtypes.ResourceTypeDomain
|
||||
@@ -134,15 +98,3 @@ const (
|
||||
PolicyRuleProtocolICMP = sharedtypes.PolicyRuleProtocolICMP
|
||||
PolicyRuleProtocolNetbirdSSH = sharedtypes.PolicyRuleProtocolNetbirdSSH
|
||||
)
|
||||
|
||||
const (
|
||||
PolicyRuleFlowDirect = sharedtypes.PolicyRuleFlowDirect
|
||||
PolicyRuleFlowBidirect = sharedtypes.PolicyRuleFlowBidirect
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultRuleName = sharedtypes.DefaultRuleName
|
||||
DefaultRuleDescription = sharedtypes.DefaultRuleDescription
|
||||
DefaultPolicyName = sharedtypes.DefaultPolicyName
|
||||
DefaultPolicyDescription = sharedtypes.DefaultPolicyDescription
|
||||
)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package types
|
||||
|
||||
// DNSSettings defines dns settings at the account level
|
||||
type DNSSettings struct {
|
||||
// DisabledManagementGroups groups whose DNS management is disabled
|
||||
DisabledManagementGroups []string `gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
// Copy returns a copy of the DNS settings
|
||||
func (d DNSSettings) Copy() DNSSettings {
|
||||
settings := DNSSettings{
|
||||
DisabledManagementGroups: make([]string, len(d.DisabledManagementGroups)),
|
||||
}
|
||||
copy(settings.DisabledManagementGroups, d.DisabledManagementGroups)
|
||||
return settings
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/netbirdio/netbird/management/server/integration_reference"
|
||||
"github.com/netbirdio/netbird/shared/management/integration_reference"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -67,6 +68,10 @@ func (g *Group) EventMeta() map[string]any {
|
||||
return map[string]any{"name": g.Name}
|
||||
}
|
||||
|
||||
func (g *Group) EventMetaResource(resource *nmdata.NetworkResource) map[string]any {
|
||||
return map[string]any{"name": g.Name, "id": g.ID, "resource_name": resource.Name, "resource_id": resource.ID, "resource_type": resource.Type}
|
||||
}
|
||||
|
||||
func (g *Group) Copy() *Group {
|
||||
group := &Group{
|
||||
ID: g.ID,
|
||||
@@ -90,39 +95,14 @@ func (g *Group) HasPeers() bool {
|
||||
return len(g.Peers) > 0
|
||||
}
|
||||
|
||||
// GroupAllName is the reserved name of the default group that contains every peer in an account.
|
||||
const GroupAllName = "All"
|
||||
|
||||
// IsGroupAll checks if the group is a default "All" group.
|
||||
func (g *Group) IsGroupAll() bool {
|
||||
return g.Name == GroupAllName
|
||||
}
|
||||
|
||||
// ToComponent converts the group to its self-contained components
|
||||
// representation. The Peers slice is shared, not copied — components are
|
||||
// treated as immutable snapshots. Returns nil for a nil group.
|
||||
func (g *Group) ToComponent() *ComponentGroup {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
return &ComponentGroup{
|
||||
ID: g.ID,
|
||||
PublicID: g.PublicID,
|
||||
Name: g.Name,
|
||||
Peers: g.Peers,
|
||||
}
|
||||
}
|
||||
|
||||
// GroupsToComponent converts an id-keyed group map to its components
|
||||
// representation, preserving nil entries.
|
||||
func GroupsToComponent(groups map[string]*Group) map[string]*ComponentGroup {
|
||||
if groups == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]*ComponentGroup, len(groups))
|
||||
for id, g := range groups {
|
||||
out[id] = g.ToComponent()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// AddPeer adds peerID to Peers if not present, returning true if added.
|
||||
func (g *Group) AddPeer(peerID string) bool {
|
||||
if peerID == "" {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
func TestNetworkMapComponents_IPv6EndToEnd(t *testing.T) {
|
||||
@@ -105,7 +105,7 @@ func TestNetworkMapComponents_RemotePeerWithoutCapability(t *testing.T) {
|
||||
require.NotNil(t, nm)
|
||||
|
||||
t.Run("AllowedIPs include remote v6", func(t *testing.T) {
|
||||
var dst *types.ComponentPeer
|
||||
var dst *nmdata.Peer
|
||||
for _, p := range nm.Peers {
|
||||
if p.ID == "peer-dst-1" {
|
||||
dst = p
|
||||
|
||||
@@ -3,10 +3,12 @@ package types
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/netbirdio/netbird/client/anonymize"
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
@@ -150,6 +152,21 @@ func validateAndBuildBundleParams(req api.WorkloadRequest, workload *Workload) e
|
||||
if bundle.Parameters.LogFileCount < 1 || bundle.Parameters.LogFileCount > 1000 {
|
||||
return fmt.Errorf("log-file-count must be between 1 and 1000, got %d", bundle.Parameters.LogFileCount)
|
||||
}
|
||||
// validate anonymize_level: omitted or empty defaults on the client;
|
||||
// otherwise it must name a known level. An unknown value is rejected here
|
||||
// rather than silently escalated, so a typo surfaces at job creation. The
|
||||
// normalized (trimmed, lowercased) value is persisted so it matches what
|
||||
// the client parses — the client only lowercases, so a stored " default "
|
||||
// would otherwise resolve to strict.
|
||||
if lvl := bundle.Parameters.AnonymizeLevel; lvl != nil {
|
||||
normalized := strings.ToLower(strings.TrimSpace(*lvl))
|
||||
switch normalized {
|
||||
case "", anonymize.LevelDefaultString, anonymize.LevelStrictString:
|
||||
default:
|
||||
return fmt.Errorf("anonymize_level must be %q or %q, got %q", anonymize.LevelDefaultString, anonymize.LevelStrictString, *lvl)
|
||||
}
|
||||
bundle.Parameters.AnonymizeLevel = &normalized
|
||||
}
|
||||
|
||||
workload.Parameters, err = json.Marshal(bundle.Parameters)
|
||||
if err != nil {
|
||||
@@ -209,6 +226,17 @@ func (j *Job) ToStreamJobRequest() (*proto.JobRequest, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// derefString returns the pointed-to string, or "" when the pointer is nil.
|
||||
// The bundle parameters carry anonymize_level and upload_url as optional
|
||||
// fields; an absent value maps to the empty proto string, which the client
|
||||
// resolves to its default.
|
||||
func derefString(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func (j *Job) buildStreamBundleResponse() (*proto.JobRequest, error) {
|
||||
var p api.BundleParameters
|
||||
if err := json.Unmarshal(j.Workload.Parameters, &p); err != nil {
|
||||
@@ -218,10 +246,12 @@ func (j *Job) buildStreamBundleResponse() (*proto.JobRequest, error) {
|
||||
ID: []byte(j.ID),
|
||||
WorkloadParameters: &proto.JobRequest_Bundle{
|
||||
Bundle: &proto.BundleParameters{
|
||||
BundleFor: p.BundleFor,
|
||||
BundleForTime: int64(p.BundleForTime),
|
||||
LogFileCount: int32(p.LogFileCount),
|
||||
Anonymize: p.Anonymize,
|
||||
BundleFor: p.BundleFor,
|
||||
BundleForTime: int64(p.BundleForTime),
|
||||
LogFileCount: int32(p.LogFileCount),
|
||||
Anonymize: p.Anonymize,
|
||||
AnonymizeLevel: derefString(p.AnonymizeLevel),
|
||||
UploadUrl: derefString(p.UploadUrl),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
func strPtr(s string) *string { return &s }
|
||||
|
||||
// bundleJobFromParams builds a bundle Job whose stored workload parameters are
|
||||
// the marshalled REST BundleParameters, mirroring what NewJob persists.
|
||||
func bundleJobFromParams(t *testing.T, p api.BundleParameters) *Job {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(p)
|
||||
require.NoError(t, err, "marshal bundle parameters")
|
||||
return &Job{
|
||||
ID: "job-1",
|
||||
Workload: Workload{
|
||||
Type: JobTypeBundle,
|
||||
Parameters: raw,
|
||||
Result: []byte("{}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildStreamBundleResponse_CarriesIdentityAndUploadFields verifies the
|
||||
// anonymize_level and upload_url REST fields are mapped onto the proto request
|
||||
// the client receives.
|
||||
func TestBuildStreamBundleResponse_CarriesIdentityAndUploadFields(t *testing.T) {
|
||||
job := bundleJobFromParams(t, api.BundleParameters{
|
||||
BundleFor: true,
|
||||
BundleForTime: 2,
|
||||
LogFileCount: 100,
|
||||
Anonymize: true,
|
||||
AnonymizeLevel: strPtr("strict"),
|
||||
UploadUrl: strPtr("https://upload.example.com"),
|
||||
})
|
||||
|
||||
req, err := job.ToStreamJobRequest()
|
||||
require.NoError(t, err, "ToStreamJobRequest must succeed")
|
||||
|
||||
bundle := req.GetBundle()
|
||||
require.NotNil(t, bundle, "the request must carry bundle parameters")
|
||||
assert.Equal(t, "strict", bundle.GetAnonymizeLevel(), "anonymize_level must reach the client")
|
||||
assert.Equal(t, "https://upload.example.com", bundle.GetUploadUrl(), "upload_url must reach the client")
|
||||
assert.True(t, bundle.GetAnonymize(), "existing fields must still map")
|
||||
assert.Equal(t, int32(100), bundle.GetLogFileCount(), "existing fields must still map")
|
||||
}
|
||||
|
||||
// newBundleJobRequest builds an api.JobRequest carrying a bundle workload with
|
||||
// the given parameters, mirroring what the REST handler decodes.
|
||||
func newBundleJobRequest(t *testing.T, p api.BundleParameters) *api.JobRequest {
|
||||
t.Helper()
|
||||
var wr api.WorkloadRequest
|
||||
require.NoError(t, wr.FromBundleWorkloadRequest(api.BundleWorkloadRequest{
|
||||
Type: api.WorkloadTypeBundle,
|
||||
Parameters: p,
|
||||
}), "build bundle workload request")
|
||||
return &api.JobRequest{Workload: wr}
|
||||
}
|
||||
|
||||
// TestNewJob_AnonymizeLevelValidation verifies the management API accepts only
|
||||
// known anonymization levels (empty defaults on the client) and rejects an
|
||||
// unknown value instead of silently escalating it.
|
||||
func TestNewJob_AnonymizeLevelValidation(t *testing.T) {
|
||||
base := api.BundleParameters{BundleFor: false, LogFileCount: 100, Anonymize: true}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
level *string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "omitted", level: nil},
|
||||
{name: "empty", level: strPtr("")},
|
||||
{name: "default", level: strPtr("default")},
|
||||
{name: "strict", level: strPtr("strict")},
|
||||
{name: "mixed case", level: strPtr("Strict")},
|
||||
{name: "padded", level: strPtr(" default ")},
|
||||
{name: "unknown", level: strPtr("verbose"), wantErr: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := base
|
||||
p.AnonymizeLevel = tc.level
|
||||
_, err := NewJob("user-1", "acc-1", "peer-1", newBundleJobRequest(t, p))
|
||||
if tc.wantErr {
|
||||
require.Error(t, err, "an unknown anonymize_level must be rejected")
|
||||
assert.Contains(t, err.Error(), "anonymize_level", "the error must name the offending field")
|
||||
return
|
||||
}
|
||||
require.NoError(t, err, "a known anonymize_level must be accepted")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewJob_AnonymizeLevelNormalized verifies an accepted level is persisted
|
||||
// trimmed and lowercased, so it reaches the client as a value the client's
|
||||
// lowercase-only parser resolves correctly rather than escalating to strict.
|
||||
func TestNewJob_AnonymizeLevelNormalized(t *testing.T) {
|
||||
job, err := NewJob("user-1", "acc-1", "peer-1", newBundleJobRequest(t, api.BundleParameters{
|
||||
BundleFor: false,
|
||||
LogFileCount: 100,
|
||||
Anonymize: true,
|
||||
AnonymizeLevel: strPtr(" Default "),
|
||||
}))
|
||||
require.NoError(t, err, "a padded known level must be accepted")
|
||||
|
||||
req, err := job.ToStreamJobRequest()
|
||||
require.NoError(t, err, "ToStreamJobRequest must succeed")
|
||||
assert.Equal(t, "default", req.GetBundle().GetAnonymizeLevel(),
|
||||
"the persisted level must be normalized so the client does not resolve it to strict")
|
||||
}
|
||||
|
||||
// TestBuildStreamBundleResponse_OmittedFieldsMapToEmpty verifies that omitted
|
||||
// optional fields map to the empty proto string, which the client resolves to
|
||||
// its defaults (default anonymization level, default upload server).
|
||||
func TestBuildStreamBundleResponse_OmittedFieldsMapToEmpty(t *testing.T) {
|
||||
job := bundleJobFromParams(t, api.BundleParameters{
|
||||
BundleFor: false,
|
||||
BundleForTime: 1,
|
||||
LogFileCount: 50,
|
||||
Anonymize: false,
|
||||
// AnonymizeLevel and UploadUrl intentionally nil.
|
||||
})
|
||||
|
||||
req, err := job.ToStreamJobRequest()
|
||||
require.NoError(t, err, "ToStreamJobRequest must succeed")
|
||||
|
||||
bundle := req.GetBundle()
|
||||
require.NotNil(t, bundle, "the request must carry bundle parameters")
|
||||
assert.Empty(t, bundle.GetAnonymizeLevel(), "an omitted anonymize_level must map to empty so the client defaults it")
|
||||
assert.Empty(t, bundle.GetUploadUrl(), "an omitted upload_url must map to empty so the client defaults it")
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
package legacynmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
||||
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
)
|
||||
|
||||
// GetPeerNetworkMapResult dispatches to either the legacy-NetworkMap path or
|
||||
// the components path based on the peer's capability and the kill switch.
|
||||
// Capable peers (PeerCapabilityComponentNetworkMap) get the raw components
|
||||
// shape — the server skips Calculate() entirely for them, saving CPU
|
||||
// proportional to the number of capable peers in the account. Legacy peers
|
||||
// (or any peer when componentsDisabled is true) get the fully-expanded
|
||||
// NetworkMap as before.
|
||||
|
||||
func GetPeerNetworkMapFromComponents(a *Account,
|
||||
ctx context.Context,
|
||||
peerID string,
|
||||
peersCustomZone nbdns.CustomZone,
|
||||
accountZones []*zones.Zone,
|
||||
validatedPeersMap map[string]struct{},
|
||||
resourcePolicies map[string][]*Policy,
|
||||
routers map[string]map[string]*routerTypes.NetworkRouter,
|
||||
metrics *telemetry.AccountManagerMetrics,
|
||||
groupIDToUserIDs map[string][]string,
|
||||
) *NetworkMap {
|
||||
start := time.Now()
|
||||
|
||||
components := GetPeerNetworkMapComponents(a,
|
||||
ctx,
|
||||
peerID,
|
||||
peersCustomZone,
|
||||
accountZones,
|
||||
validatedPeersMap,
|
||||
resourcePolicies,
|
||||
routers,
|
||||
groupIDToUserIDs,
|
||||
)
|
||||
|
||||
if components.IsEmpty() {
|
||||
return &NetworkMap{Network: components.Network}
|
||||
}
|
||||
|
||||
nm := CalculateNetworkMapFromComponents(ctx, components)
|
||||
|
||||
if metrics != nil {
|
||||
objectCount := int64(len(nm.Peers) + len(nm.OfflinePeers) + len(nm.Routes) + len(nm.FirewallRules) + len(nm.RoutesFirewallRules))
|
||||
metrics.CountNetworkMapObjects(objectCount)
|
||||
metrics.CountGetPeerNetworkMapDuration(time.Since(start))
|
||||
|
||||
if objectCount > 5000 {
|
||||
log.WithContext(ctx).Tracef("account: %s has a total resource count of %d objects from components, "+
|
||||
"peers: %d, offline peers: %d, routes: %d, firewall rules: %d, route firewall rules: %d",
|
||||
a.Id, objectCount, len(nm.Peers), len(nm.OfflinePeers), len(nm.Routes), len(nm.FirewallRules), len(nm.RoutesFirewallRules))
|
||||
}
|
||||
}
|
||||
|
||||
return nm
|
||||
}
|
||||
|
||||
func GetPeerNetworkMapComponents(a *Account,
|
||||
ctx context.Context,
|
||||
peerID string,
|
||||
peersCustomZone nbdns.CustomZone,
|
||||
accountZones []*zones.Zone,
|
||||
validatedPeersMap map[string]struct{},
|
||||
resourcePolicies map[string][]*Policy,
|
||||
routers map[string]map[string]*routerTypes.NetworkRouter,
|
||||
groupIDToUserIDs map[string][]string,
|
||||
) *NetworkMapComponents {
|
||||
peer := a.Peers[peerID]
|
||||
// this can never happen, things are very wrong if it did
|
||||
// TODO (dmitri) maybe consider using invariants?
|
||||
if peer == nil {
|
||||
log.WithField("peer id", peerID).Error("NetworkMapComponents are computed for a peer missing from the account")
|
||||
return EmptyNetworkMapComponents(&NetworkMapComponents{
|
||||
PeerID: peerID,
|
||||
Network: a.Network.Copy(),
|
||||
// must include the target peer as it's required on the client
|
||||
Peers: map[string]*ComponentPeer{peerID: peerToComponent(peer)},
|
||||
})
|
||||
}
|
||||
|
||||
if _, ok := validatedPeersMap[peerID]; !ok {
|
||||
// Mirror legacy graceful-degrade: GetPeerNetworkMapFromComponents
|
||||
// returns &NetworkMap{Network: a.Network.Copy()} when components is
|
||||
// nil. Match that floor so the receiving client always sees the
|
||||
// account Network identifier, not a fully-empty envelope.
|
||||
return EmptyNetworkMapComponents(&NetworkMapComponents{
|
||||
PeerID: peerID,
|
||||
Network: a.Network.Copy(),
|
||||
// must include the target peer as it's required on the client
|
||||
Peers: map[string]*ComponentPeer{peerID: peerToComponent(peer)},
|
||||
})
|
||||
}
|
||||
|
||||
components := &NetworkMapComponents{
|
||||
PeerID: peerID,
|
||||
Network: a.Network.Copy(),
|
||||
NameServerGroups: make([]*nbdns.NameServerGroup, 0),
|
||||
CustomZoneDomain: peersCustomZone.Domain,
|
||||
ResourcePoliciesMap: make(map[string][]*Policy),
|
||||
RoutersMap: make(map[string]map[string]*ComponentRouter),
|
||||
NetworkResources: make([]*ComponentResource, 0),
|
||||
PostureFailedPeers: make(map[string]map[string]struct{}, len(a.PostureChecks)),
|
||||
RouterPeers: make(map[string]*ComponentPeer),
|
||||
NetworkXIDToPublicID: make(map[string]string, len(a.Networks)),
|
||||
PostureCheckXIDToPublicID: make(map[string]string, len(a.PostureChecks)),
|
||||
|
||||
ForceRoutingPeerDNSResolution: forcesRoutingPeerDNSResolution(a, peerID, routers),
|
||||
}
|
||||
for _, n := range a.Networks {
|
||||
if n != nil {
|
||||
components.NetworkXIDToPublicID[n.ID] = n.PublicID
|
||||
}
|
||||
}
|
||||
for _, pc := range a.PostureChecks {
|
||||
if pc != nil {
|
||||
components.PostureCheckXIDToPublicID[pc.ID] = pc.PublicID
|
||||
}
|
||||
}
|
||||
|
||||
components.AccountSettings = &AccountSettingsInfo{
|
||||
PeerLoginExpirationEnabled: a.Settings.PeerLoginExpirationEnabled,
|
||||
PeerLoginExpiration: a.Settings.PeerLoginExpiration,
|
||||
PeerInactivityExpirationEnabled: a.Settings.PeerInactivityExpirationEnabled,
|
||||
PeerInactivityExpiration: a.Settings.PeerInactivityExpiration,
|
||||
}
|
||||
|
||||
components.DNSSettings = &a.DNSSettings
|
||||
|
||||
// relevantPeers always contains the target peer (peerID)
|
||||
relevantPeers, relevantGroups, relevantPolicies, relevantRoutes, sshReqs := getPeersGroupsPoliciesRoutes(a, ctx, peerID, peer.SSHEnabled, validatedPeersMap, &components.PostureFailedPeers)
|
||||
|
||||
if len(sshReqs.neededGroupIDs) > 0 {
|
||||
components.GroupIDToUserIDs = filterGroupIDToUserIDs(groupIDToUserIDs, sshReqs.neededGroupIDs)
|
||||
}
|
||||
if sshReqs.needAllowedUserIDs {
|
||||
components.AllowedUserIDs = getAllowedUserIDs(a)
|
||||
}
|
||||
|
||||
components.Peers = relevantPeers
|
||||
components.Groups = groupsToComponent(relevantGroups)
|
||||
components.Policies = relevantPolicies
|
||||
components.Routes = relevantRoutes
|
||||
components.AllDNSRecords = filterDNSRecordsByPeers(peersCustomZone.Records, relevantPeers, peer.SupportsIPv6() && peer.IPv6.IsValid())
|
||||
|
||||
peerGroups := a.GetPeerGroups(peerID)
|
||||
components.AccountZones = filterPeerAppliedZones(ctx, accountZones, LookupMap(peerGroups))
|
||||
components.AccountZones = append(components.AccountZones, a.SynthesizePrivateServiceZones(peerID)...)
|
||||
|
||||
for _, nsGroup := range a.NameServerGroups {
|
||||
if nsGroup.Enabled {
|
||||
for _, gID := range nsGroup.Groups {
|
||||
if _, found := relevantGroups[gID]; found {
|
||||
components.NameServerGroups = append(components.NameServerGroups, nsGroup)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, resource := range a.NetworkResources {
|
||||
if !resource.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
policies, exists := resourcePolicies[resource.ID]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
addSourcePeers := false
|
||||
|
||||
networkRoutingPeers, routerExists := routers[resource.NetworkID]
|
||||
if routerExists {
|
||||
if _, ok := networkRoutingPeers[peerID]; ok {
|
||||
addSourcePeers = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, policy := range policies {
|
||||
if addSourcePeers {
|
||||
var peers []string
|
||||
if policy.Rules[0].SourceResource.Type == ResourceTypePeer && policy.Rules[0].SourceResource.ID != "" {
|
||||
peers = []string{policy.Rules[0].SourceResource.ID}
|
||||
} else {
|
||||
peers = getUniquePeerIDsFromGroupsIDs(a, ctx, policy.SourceGroups())
|
||||
}
|
||||
for _, pID := range getPostureValidPeersSaveFailed(a, peers, policy.SourcePostureChecks, validatedPeersMap, &components.PostureFailedPeers) {
|
||||
if _, exists := components.Peers[pID]; !exists {
|
||||
components.Peers[pID] = peerToComponent(a.GetPeer(pID))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
peerInSources := false
|
||||
if policy.Rules[0].SourceResource.Type == ResourceTypePeer && policy.Rules[0].SourceResource.ID != "" {
|
||||
peerInSources = policy.Rules[0].SourceResource.ID == peerID
|
||||
} else {
|
||||
for _, groupID := range policy.SourceGroups() {
|
||||
if group := a.GetGroup(groupID); group != nil && slices.Contains(group.Peers, peerID) {
|
||||
peerInSources = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !peerInSources {
|
||||
continue
|
||||
}
|
||||
isValid, pname := validatePostureChecksOnPeerGetFailed(a, ctx, policy.SourcePostureChecks, peerID)
|
||||
if !isValid && len(pname) > 0 {
|
||||
if _, ok := components.PostureFailedPeers[pname]; !ok {
|
||||
components.PostureFailedPeers[pname] = make(map[string]struct{})
|
||||
}
|
||||
components.PostureFailedPeers[pname][peer.ID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
addSourcePeers = true
|
||||
}
|
||||
|
||||
for _, rule := range policy.Rules {
|
||||
for _, srcGroupID := range rule.Sources {
|
||||
if g := a.Groups[srcGroupID]; g != nil {
|
||||
if _, exists := components.Groups[srcGroupID]; !exists {
|
||||
components.Groups[srcGroupID] = groupToComponent(g)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, dstGroupID := range rule.Destinations {
|
||||
if g := a.Groups[dstGroupID]; g != nil {
|
||||
if _, exists := components.Groups[dstGroupID]; !exists {
|
||||
components.Groups[dstGroupID] = groupToComponent(g)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
components.ResourcePoliciesMap[resource.ID] = policies
|
||||
}
|
||||
|
||||
// Only expose router peers and the per-network routers_map when this
|
||||
// target peer actually has access to the resource (either as a router
|
||||
// itself or via a policy that includes it as a source). Without this
|
||||
// gate, every peer's envelope was leaking router peers of every
|
||||
// network in the account — accounts with many tenants/networks
|
||||
// shipped tens of unrelated peers in `peers[]` and `routers_map`.
|
||||
if addSourcePeers {
|
||||
components.RoutersMap[resource.NetworkID] = routersToComponentMap(networkRoutingPeers)
|
||||
for peerIDKey := range networkRoutingPeers {
|
||||
if p := a.Peers[peerIDKey]; p != nil {
|
||||
cp := components.RouterPeers[peerIDKey]
|
||||
if cp == nil {
|
||||
cp = peerToComponent(p)
|
||||
components.RouterPeers[peerIDKey] = cp
|
||||
}
|
||||
if _, exists := components.Peers[peerIDKey]; !exists {
|
||||
if _, validated := validatedPeersMap[peerIDKey]; validated {
|
||||
components.Peers[peerIDKey] = cp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
components.NetworkResources = append(components.NetworkResources, resourceToComponent(resource))
|
||||
}
|
||||
}
|
||||
|
||||
filterGroupPeers(&components.Groups, components.Peers)
|
||||
filterPostureFailedPeers(&components.PostureFailedPeers, components.Policies, components.ResourcePoliciesMap, components.Peers)
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
type sshRequirements struct {
|
||||
neededGroupIDs map[string]struct{}
|
||||
needAllowedUserIDs bool
|
||||
}
|
||||
|
||||
func getPeersGroupsPoliciesRoutes(a *Account,
|
||||
ctx context.Context,
|
||||
peerID string,
|
||||
peerSSHEnabled bool,
|
||||
validatedPeersMap map[string]struct{},
|
||||
postureFailedPeers *map[string]map[string]struct{},
|
||||
) (map[string]*ComponentPeer, map[string]*Group, []*Policy, []*route.Route, sshRequirements) {
|
||||
relevantPeerIDs := make(map[string]*ComponentPeer, len(a.Peers)/4)
|
||||
relevantGroupIDs := make(map[string]*Group, len(a.Groups)/4)
|
||||
relevantPolicies := make([]*Policy, 0, len(a.Policies))
|
||||
relevantRoutes := make([]*route.Route, 0, len(a.Routes))
|
||||
sshReqs := sshRequirements{neededGroupIDs: make(map[string]struct{})}
|
||||
|
||||
relevantPeerIDs[peerID] = peerToComponent(a.GetPeer(peerID))
|
||||
|
||||
peerGroupSet := make(map[string]struct{}, 8)
|
||||
for groupID, group := range a.Groups {
|
||||
if slices.Contains(group.Peers, peerID) {
|
||||
relevantGroupIDs[groupID] = a.GetGroup(groupID)
|
||||
peerGroupSet[groupID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
routeAccessControlGroups := make(map[string]struct{})
|
||||
for _, r := range a.Routes {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
relevant := r.Peer == peerID
|
||||
if !relevant {
|
||||
for _, groupID := range r.PeerGroups {
|
||||
if _, ok := peerGroupSet[groupID]; ok {
|
||||
relevant = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !relevant && r.Enabled {
|
||||
for _, groupID := range r.Groups {
|
||||
if _, ok := peerGroupSet[groupID]; ok {
|
||||
relevant = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !relevant {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, groupID := range r.PeerGroups {
|
||||
relevantGroupIDs[groupID] = a.GetGroup(groupID)
|
||||
}
|
||||
for _, groupID := range r.Groups {
|
||||
relevantGroupIDs[groupID] = a.GetGroup(groupID)
|
||||
}
|
||||
if r.Enabled {
|
||||
for _, groupID := range r.AccessControlGroups {
|
||||
relevantGroupIDs[groupID] = a.GetGroup(groupID)
|
||||
routeAccessControlGroups[groupID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Include route advertisers in relevantPeerIDs. The envelope
|
||||
// encoder writes route.peer_index by looking up r.Peer in the
|
||||
// shipped peers list; if the advertiser is policy-isolated from
|
||||
// the target peer (no rule edge between them), it would otherwise
|
||||
// be omitted and the decoder would fail to resolve r.Peer, leaving
|
||||
// the client without a WG tunnel target for this route. Legacy
|
||||
// NetworkMap.Routes shipped the WG public key inline, so the
|
||||
// equivalence path doesn't surface this — but the dependency is
|
||||
// real once a client actually tries to use the route.
|
||||
// Gate by validatedPeersMap so non-validated advertisers stay out
|
||||
// (matches the network-resource router behaviour at the bottom of
|
||||
// this loop, and the legacy invariant that only validated peers
|
||||
// reach a client's view).
|
||||
if r.Peer != "" {
|
||||
if _, ok := validatedPeersMap[r.Peer]; ok {
|
||||
if p := a.GetPeer(r.Peer); p != nil {
|
||||
relevantPeerIDs[r.Peer] = peerToComponent(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, groupID := range r.PeerGroups {
|
||||
g := a.GetGroup(groupID)
|
||||
if g == nil {
|
||||
continue
|
||||
}
|
||||
for _, pid := range g.Peers {
|
||||
if _, exists := relevantPeerIDs[pid]; exists {
|
||||
continue
|
||||
}
|
||||
if _, ok := validatedPeersMap[pid]; !ok {
|
||||
continue
|
||||
}
|
||||
if p := a.GetPeer(pid); p != nil {
|
||||
relevantPeerIDs[pid] = peerToComponent(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
relevantRoutes = append(relevantRoutes, r)
|
||||
}
|
||||
|
||||
for _, policy := range a.Policies {
|
||||
if !policy.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
policyRelevant := false
|
||||
for _, rule := range policy.Rules {
|
||||
if !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(routeAccessControlGroups) > 0 {
|
||||
for _, destGroupID := range rule.Destinations {
|
||||
if _, needed := routeAccessControlGroups[destGroupID]; needed {
|
||||
policyRelevant = true
|
||||
for _, srcGroupID := range rule.Sources {
|
||||
relevantGroupIDs[srcGroupID] = a.GetGroup(srcGroupID)
|
||||
}
|
||||
for _, dstGroupID := range rule.Destinations {
|
||||
relevantGroupIDs[dstGroupID] = a.GetGroup(dstGroupID)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sourcePeers, destinationPeers []string
|
||||
var peerInSources, peerInDestinations bool
|
||||
|
||||
if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" {
|
||||
sourcePeers = []string{rule.SourceResource.ID}
|
||||
if rule.SourceResource.ID == peerID {
|
||||
peerInSources = true
|
||||
}
|
||||
} else {
|
||||
sourcePeers, peerInSources = getPeersFromGroups(a, ctx, rule.Sources, peerID, policy.SourcePostureChecks, validatedPeersMap, postureFailedPeers)
|
||||
}
|
||||
|
||||
if rule.DestinationResource.Type == ResourceTypePeer && rule.DestinationResource.ID != "" {
|
||||
destinationPeers = []string{rule.DestinationResource.ID}
|
||||
if rule.DestinationResource.ID == peerID {
|
||||
peerInDestinations = true
|
||||
}
|
||||
} else {
|
||||
destinationPeers, peerInDestinations = getPeersFromGroups(a, ctx, rule.Destinations, peerID, nil, validatedPeersMap, postureFailedPeers)
|
||||
}
|
||||
|
||||
if peerInSources {
|
||||
policyRelevant = true
|
||||
for _, pid := range destinationPeers {
|
||||
if _, exists := relevantPeerIDs[pid]; !exists {
|
||||
relevantPeerIDs[pid] = peerToComponent(a.GetPeer(pid))
|
||||
}
|
||||
}
|
||||
for _, dstGroupID := range rule.Destinations {
|
||||
relevantGroupIDs[dstGroupID] = a.GetGroup(dstGroupID)
|
||||
}
|
||||
}
|
||||
|
||||
if peerInDestinations {
|
||||
policyRelevant = true
|
||||
for _, pid := range sourcePeers {
|
||||
if _, exists := relevantPeerIDs[pid]; !exists {
|
||||
relevantPeerIDs[pid] = peerToComponent(a.GetPeer(pid))
|
||||
}
|
||||
}
|
||||
for _, srcGroupID := range rule.Sources {
|
||||
relevantGroupIDs[srcGroupID] = a.GetGroup(srcGroupID)
|
||||
}
|
||||
|
||||
if rule.Protocol == PolicyRuleProtocolNetbirdSSH {
|
||||
switch {
|
||||
case len(rule.AuthorizedGroups) > 0:
|
||||
for groupID := range rule.AuthorizedGroups {
|
||||
sshReqs.neededGroupIDs[groupID] = struct{}{}
|
||||
}
|
||||
case rule.AuthorizedUser != "":
|
||||
default:
|
||||
sshReqs.needAllowedUserIDs = true
|
||||
}
|
||||
} else if PolicyRuleImpliesLegacySSH(rule) && peerSSHEnabled {
|
||||
sshReqs.needAllowedUserIDs = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if policyRelevant {
|
||||
relevantPolicies = append(relevantPolicies, policy)
|
||||
}
|
||||
}
|
||||
|
||||
return relevantPeerIDs, relevantGroupIDs, relevantPolicies, relevantRoutes, sshReqs
|
||||
}
|
||||
|
||||
func getPeersFromGroups(a *Account, ctx context.Context, groups []string, peerID string, sourcePostureChecksIDs []string,
|
||||
validatedPeersMap map[string]struct{}, postureFailedPeers *map[string]map[string]struct{}) ([]string, bool) {
|
||||
peerInGroups := false
|
||||
filteredPeerIDs := make([]string, 0, len(groups))
|
||||
seenPeerIds := make(map[string]struct{}, len(groups))
|
||||
|
||||
for _, gid := range groups {
|
||||
group := a.GetGroup(gid)
|
||||
if group == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if group.IsGroupAll() || len(groups) == 1 {
|
||||
filteredPeerIDs = make([]string, 0, len(group.Peers))
|
||||
peerInGroups = false
|
||||
for _, pid := range group.Peers {
|
||||
peer, ok := a.Peers[pid]
|
||||
if !ok || peer == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := validatedPeersMap[peer.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
isValid, pname := validatePostureChecksOnPeerGetFailed(a, ctx, sourcePostureChecksIDs, peer.ID)
|
||||
if !isValid && len(pname) > 0 {
|
||||
if _, ok := (*postureFailedPeers)[pname]; !ok {
|
||||
(*postureFailedPeers)[pname] = make(map[string]struct{})
|
||||
}
|
||||
(*postureFailedPeers)[pname][peer.ID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
if peer.ID == peerID {
|
||||
peerInGroups = true
|
||||
continue
|
||||
}
|
||||
|
||||
filteredPeerIDs = append(filteredPeerIDs, peer.ID)
|
||||
}
|
||||
return filteredPeerIDs, peerInGroups
|
||||
}
|
||||
|
||||
for _, pid := range group.Peers {
|
||||
if _, seen := seenPeerIds[pid]; seen {
|
||||
continue
|
||||
}
|
||||
seenPeerIds[pid] = struct{}{}
|
||||
peer, ok := a.Peers[pid]
|
||||
if !ok || peer == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := validatedPeersMap[peer.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
isValid, pname := validatePostureChecksOnPeerGetFailed(a, ctx, sourcePostureChecksIDs, peer.ID)
|
||||
if !isValid && len(pname) > 0 {
|
||||
if _, ok := (*postureFailedPeers)[pname]; !ok {
|
||||
(*postureFailedPeers)[pname] = make(map[string]struct{})
|
||||
}
|
||||
(*postureFailedPeers)[pname][peer.ID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
if peer.ID == peerID {
|
||||
peerInGroups = true
|
||||
continue
|
||||
}
|
||||
|
||||
filteredPeerIDs = append(filteredPeerIDs, peer.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredPeerIDs, peerInGroups
|
||||
}
|
||||
|
||||
func validatePostureChecksOnPeerGetFailed(a *Account, ctx context.Context, sourcePostureChecksID []string, peerID string) (bool, string) {
|
||||
peer, ok := a.Peers[peerID]
|
||||
if !ok || peer == nil {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
for _, postureChecksID := range sourcePostureChecksID {
|
||||
postureChecks := a.GetPostureChecks(postureChecksID)
|
||||
if postureChecks == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, check := range postureChecks.GetChecks() {
|
||||
isValid, _ := check.Check(ctx, *peer)
|
||||
if !isValid {
|
||||
return false, postureChecksID
|
||||
}
|
||||
}
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func getPostureValidPeersSaveFailed(a *Account, inputPeers []string, postureChecksIDs []string, validatedPeersMap map[string]struct{}, postureFailedPeers *map[string]map[string]struct{}) []string {
|
||||
var dest []string
|
||||
for _, peerID := range inputPeers {
|
||||
if _, validated := validatedPeersMap[peerID]; !validated {
|
||||
continue
|
||||
}
|
||||
valid, pname := validatePostureChecksOnPeerGetFailed(a, context.Background(), postureChecksIDs, peerID)
|
||||
if valid {
|
||||
dest = append(dest, peerID)
|
||||
continue
|
||||
}
|
||||
if _, ok := (*postureFailedPeers)[pname]; !ok {
|
||||
(*postureFailedPeers)[pname] = make(map[string]struct{})
|
||||
}
|
||||
(*postureFailedPeers)[pname][peerID] = struct{}{}
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
// filterGroupPeers trims each group's Peers slice to only those peers that
|
||||
// also appear in `peers`. Groups whose filtered list is empty are NOT
|
||||
// deleted from the map — they're kept so the components wire encoder can
|
||||
// still resolve seq references from routes/policies/access-control groups
|
||||
// that name them. Calculate() tolerates groups with empty Peers (the inner
|
||||
// loops simply iterate zero times), so retaining them is behaviourally a
|
||||
// no-op for the legacy path that consumes the same NetworkMapComponents.
|
||||
func filterGroupPeers(groups *map[string]*ComponentGroup, peers map[string]*ComponentPeer) {
|
||||
for groupID, groupInfo := range *groups {
|
||||
filteredPeers := make([]string, 0, len(groupInfo.Peers))
|
||||
for _, pid := range groupInfo.Peers {
|
||||
if _, exists := peers[pid]; exists {
|
||||
filteredPeers = append(filteredPeers, pid)
|
||||
}
|
||||
}
|
||||
|
||||
if len(filteredPeers) != len(groupInfo.Peers) {
|
||||
ng := *groupInfo
|
||||
ng.Peers = filteredPeers
|
||||
(*groups)[groupID] = &ng
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func filterPostureFailedPeers(postureFailedPeers *map[string]map[string]struct{}, policies []*Policy, resourcePoliciesMap map[string][]*Policy, peers map[string]*ComponentPeer) {
|
||||
if len(*postureFailedPeers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
referencedPostureChecks := make(map[string]struct{})
|
||||
for _, policy := range policies {
|
||||
for _, checkID := range policy.SourcePostureChecks {
|
||||
referencedPostureChecks[checkID] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, resPolicies := range resourcePoliciesMap {
|
||||
for _, policy := range resPolicies {
|
||||
for _, checkID := range policy.SourcePostureChecks {
|
||||
referencedPostureChecks[checkID] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for checkID, failedPeers := range *postureFailedPeers {
|
||||
if _, referenced := referencedPostureChecks[checkID]; !referenced {
|
||||
delete(*postureFailedPeers, checkID)
|
||||
continue
|
||||
}
|
||||
for peerID := range failedPeers {
|
||||
if _, exists := peers[peerID]; !exists {
|
||||
delete(failedPeers, peerID)
|
||||
}
|
||||
}
|
||||
if len(failedPeers) == 0 {
|
||||
delete(*postureFailedPeers, checkID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func filterDNSRecordsByPeers(records []nbdns.SimpleRecord, peers map[string]*ComponentPeer, includeIPv6 bool) []nbdns.SimpleRecord {
|
||||
if len(records) == 0 || len(peers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Include both v4 and v6 addresses so AAAA records (whose RData is an IPv6
|
||||
// address) are not filtered out when peers have IPv6 assigned. When the
|
||||
// requesting peer doesn't have IPv6, omit v6 IPs so AAAA records get dropped.
|
||||
peerIPs := make(map[string]struct{}, len(peers)*2)
|
||||
for _, peer := range peers {
|
||||
if peer == nil {
|
||||
continue
|
||||
}
|
||||
peerIPs[peer.IP.String()] = struct{}{}
|
||||
if includeIPv6 && peer.IPv6.IsValid() {
|
||||
peerIPs[peer.IPv6.String()] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
filteredRecords := make([]nbdns.SimpleRecord, 0, len(records))
|
||||
for _, record := range records {
|
||||
if _, exists := peerIPs[record.RData]; exists {
|
||||
filteredRecords = append(filteredRecords, record)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredRecords
|
||||
}
|
||||
|
||||
func filterGroupIDToUserIDs(fullMap map[string][]string, neededGroupIDs map[string]struct{}) map[string][]string {
|
||||
if len(neededGroupIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
filtered := make(map[string][]string, len(neededGroupIDs))
|
||||
for groupID := range neededGroupIDs {
|
||||
if users, ok := fullMap[groupID]; ok {
|
||||
filtered[groupID] = users
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package legacynmap
|
||||
|
||||
import (
|
||||
types "github.com/netbirdio/netbird/management/server/types"
|
||||
sharedtypes "github.com/netbirdio/netbird/shared/management/types"
|
||||
)
|
||||
|
||||
type (
|
||||
Account = types.Account
|
||||
|
||||
DNSSettings = types.DNSSettings
|
||||
FirewallRule = sharedtypes.FirewallRule
|
||||
ForwardingRule = sharedtypes.ForwardingRule
|
||||
Group = types.Group
|
||||
Network = types.Network
|
||||
Policy = types.Policy
|
||||
PolicyRule = types.PolicyRule
|
||||
Resource = types.Resource
|
||||
RulePortRange = sharedtypes.RulePortRange
|
||||
RouteFirewallRule = sharedtypes.RouteFirewallRule
|
||||
)
|
||||
|
||||
const (
|
||||
FirewallRuleDirectionIN = sharedtypes.FirewallRuleDirectionIN
|
||||
FirewallRuleDirectionOUT = sharedtypes.FirewallRuleDirectionOUT
|
||||
|
||||
PolicyRuleProtocolALL = sharedtypes.PolicyRuleProtocolALL
|
||||
PolicyRuleProtocolTCP = sharedtypes.PolicyRuleProtocolTCP
|
||||
PolicyRuleProtocolNetbirdSSH = sharedtypes.PolicyRuleProtocolNetbirdSSH
|
||||
PolicyTrafficActionAccept = sharedtypes.PolicyTrafficActionAccept
|
||||
ResourceTypePeer = sharedtypes.ResourceTypePeer
|
||||
|
||||
AllowedIPsFormat = sharedtypes.AllowedIPsFormat
|
||||
AllowedIPsV6Format = sharedtypes.AllowedIPsV6Format
|
||||
)
|
||||
@@ -0,0 +1,350 @@
|
||||
//go:build nmapequiv
|
||||
|
||||
// Account-load benchmark: the legacy store.GetAccount hydration (pgx fast
|
||||
// path, as in production) vs the nmdata store's GetNetworkMapData, against the
|
||||
// same Postgres copy as the equivalence test.
|
||||
//
|
||||
// NETBIRD_STORE_ENGINE_POSTGRES_DSN='...' go test -tags nmapequiv \
|
||||
// -run '^$' -bench . -benchtime 5x -timeout 60m \
|
||||
// ./management/server/types/legacynmap/
|
||||
//
|
||||
// NETMAP_ACCOUNTS selects the accounts (comma-separated); by default the ten
|
||||
// accounts with the most peers are used. Each account is a sub-benchmark, so
|
||||
// the two paths can be compared per account. One warmup call runs untimed
|
||||
// before each measurement so Postgres buffer-cache state is comparable.
|
||||
//
|
||||
// Reported metrics beyond ns/op and allocs:
|
||||
//
|
||||
// - queries/op round trips, counted client-side via a pgx tracer
|
||||
// (GetNetworkMapData only — the legacy store's pool is internal)
|
||||
// - xact/op committed transactions from pg_stat_database; the legacy
|
||||
// pgx path runs autocommit statements, so this approximates its round
|
||||
// trips, while GetNetworkMapData runs a single transaction
|
||||
// - tup_returned/op, tup_fetched/op rows scanned/fetched server-side
|
||||
// - blks_read/op, blks_hit/op buffer cache misses/hits
|
||||
//
|
||||
// The pg_stat_database numbers are database-global: run without concurrent
|
||||
// load. The two stat snapshots per sub-benchmark add a small constant
|
||||
// overhead to the server-side deltas.
|
||||
package legacynmap_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
||||
networkmap_pgsql "github.com/netbirdio/netbird/management/internals/network_map_db/pgsql"
|
||||
mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
func BenchmarkGetAccount(b *testing.B) {
|
||||
dsn := equivDSN()
|
||||
if dsn == "" {
|
||||
b.Skip("NETBIRD_STORE_ENGINE_POSTGRES_DSN not set")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
statsConn, err := pgx.Connect(ctx, dsn)
|
||||
require.NoError(b, err, "connect stats connection")
|
||||
b.Cleanup(func() { statsConn.Close(ctx) })
|
||||
|
||||
testStore, err := store.NewPostgresqlStore(ctx, dsn, nil, true)
|
||||
require.NoError(b, err, "connect to postgres")
|
||||
b.Cleanup(func() { testStore.Close(ctx) })
|
||||
|
||||
for _, accountID := range benchAccountIDs(b, ctx, statsConn) {
|
||||
b.Run(accountID, func(b *testing.B) {
|
||||
logAccountShape(b, ctx, statsConn, accountID)
|
||||
benchDBLoad(b, ctx, statsConn, nil, func() error {
|
||||
_, err := testStore.GetAccount(ctx, accountID)
|
||||
return err
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetNetworkMapData(b *testing.B) {
|
||||
dsn := equivDSN()
|
||||
if dsn == "" {
|
||||
b.Skip("NETBIRD_STORE_ENGINE_POSTGRES_DSN not set")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
statsConn, err := pgx.Connect(ctx, dsn)
|
||||
require.NoError(b, err, "connect stats connection")
|
||||
b.Cleanup(func() { statsConn.Close(ctx) })
|
||||
|
||||
tracer := &queryCountTracer{}
|
||||
cfg, err := pgxpool.ParseConfig(dsn)
|
||||
require.NoError(b, err, "parse dsn")
|
||||
cfg.ConnConfig.Tracer = tracer
|
||||
pool, err := pgxpool.NewWithConfig(ctx, cfg)
|
||||
require.NoError(b, err, "connect nmdata store")
|
||||
b.Cleanup(pool.Close)
|
||||
nmStore := nmDataStore(b, &networkmap_pgsql.PgStore{Pool: pool})
|
||||
|
||||
for _, accountID := range benchAccountIDs(b, ctx, statsConn) {
|
||||
b.Run(accountID, func(b *testing.B) {
|
||||
logAccountShape(b, ctx, statsConn, accountID)
|
||||
benchDBLoad(b, ctx, statsConn, tracer, func() error {
|
||||
_, err := nmStore.GetNetworkMapData(ctx, accountID)
|
||||
return err
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkAccountFullRound measures store load plus the full per-peer fan-out
|
||||
// to *proto.SyncResponse for every peer of the account, the way the production
|
||||
// account path runs it: index maps and per-peer twin building happen after
|
||||
// GetAccount and are part of the measured op. BenchmarkNetworkMapDataFullRound
|
||||
// is the equivalent for the nmdata path, whose index building happens inside
|
||||
// GetNetworkMapData. Select both with -bench FullRound.
|
||||
func BenchmarkAccountFullRound(b *testing.B) {
|
||||
dsn := equivDSN()
|
||||
if dsn == "" {
|
||||
b.Skip("NETBIRD_STORE_ENGINE_POSTGRES_DSN not set")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
statsConn, err := pgx.Connect(ctx, dsn)
|
||||
require.NoError(b, err, "connect stats connection")
|
||||
b.Cleanup(func() { statsConn.Close(ctx) })
|
||||
|
||||
testStore, err := store.NewPostgresqlStore(ctx, dsn, nil, true)
|
||||
require.NoError(b, err, "connect to postgres")
|
||||
b.Cleanup(func() { testStore.Close(ctx) })
|
||||
|
||||
for _, accountID := range benchAccountIDs(b, ctx, statsConn) {
|
||||
b.Run(accountID, func(b *testing.B) {
|
||||
logAccountShape(b, ctx, statsConn, accountID)
|
||||
benchDBLoad(b, ctx, statsConn, nil, func() error {
|
||||
account, err := testStore.GetAccount(ctx, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buildAccountSyncResponses(ctx, account)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNetworkMapDataFullRound(b *testing.B) {
|
||||
dsn := equivDSN()
|
||||
if dsn == "" {
|
||||
b.Skip("NETBIRD_STORE_ENGINE_POSTGRES_DSN not set")
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
statsConn, err := pgx.Connect(ctx, dsn)
|
||||
require.NoError(b, err, "connect stats connection")
|
||||
b.Cleanup(func() { statsConn.Close(ctx) })
|
||||
|
||||
tracer := &queryCountTracer{}
|
||||
cfg, err := pgxpool.ParseConfig(dsn)
|
||||
require.NoError(b, err, "parse dsn")
|
||||
cfg.ConnConfig.Tracer = tracer
|
||||
pool, err := pgxpool.NewWithConfig(ctx, cfg)
|
||||
require.NoError(b, err, "connect nmdata store")
|
||||
b.Cleanup(pool.Close)
|
||||
nmStore := nmDataStore(b, &networkmap_pgsql.PgStore{Pool: pool})
|
||||
|
||||
for _, accountID := range benchAccountIDs(b, ctx, statsConn) {
|
||||
b.Run(accountID, func(b *testing.B) {
|
||||
logAccountShape(b, ctx, statsConn, accountID)
|
||||
benchDBLoad(b, ctx, statsConn, tracer, func() error {
|
||||
nmData, err := nmStore.GetNetworkMapData(ctx, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buildDataSyncResponses(ctx, nmData)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// buildAccountSyncResponses fans out to every peer like the controller's
|
||||
// account path: index maps once, twin conversion and network-map computation
|
||||
// per peer.
|
||||
func buildAccountSyncResponses(ctx context.Context, account *types.Account) {
|
||||
validated := make(map[string]struct{}, len(account.Peers))
|
||||
for peerID := range account.Peers {
|
||||
validated[peerID] = struct{}{}
|
||||
}
|
||||
resourcePolicies := account.GetResourcePoliciesMap()
|
||||
routers := account.GetResourceRoutersMap()
|
||||
groupUsers := account.GetActiveGroupUsers()
|
||||
settings := account.Settings
|
||||
if settings == nil {
|
||||
settings = &types.Settings{}
|
||||
}
|
||||
dnsCache := &cache.DNSConfigCache{}
|
||||
|
||||
for peerID, peer := range account.Peers {
|
||||
nm := account.GetPeerNetworkMapFromComponents(
|
||||
ctx, peerID, nbdns.CustomZone{}, nil, validated, resourcePolicies, routers, nil, groupUsers,
|
||||
)
|
||||
mgmtgrpc.ToSyncResponse(
|
||||
ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, nm, equivDNSName, nil,
|
||||
dnsCache, types.TwinAccountSettings(settings), settings.Extra, nil, 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// buildDataSyncResponses is the nmdata-path equivalent of
|
||||
// buildAccountSyncResponses.
|
||||
func buildDataSyncResponses(ctx context.Context, nmData *networkmap.NetworkMapData) {
|
||||
validated := make(map[string]struct{}, len(nmData.Peers))
|
||||
for peerID := range nmData.Peers {
|
||||
validated[peerID] = struct{}{}
|
||||
}
|
||||
nmData.ValidatedPeers = validated
|
||||
dnsCache := &cache.DNSConfigCache{}
|
||||
|
||||
for peerID, peer := range nmData.Peers {
|
||||
components := nmData.GetPeerNetworkMapComponents(peerID, nmdata.CustomZone{})
|
||||
nm := &types.NetworkMap{Network: components.Network}
|
||||
if !components.IsEmpty() {
|
||||
nm = types.CalculateNetworkMapFromComponents(ctx, components)
|
||||
}
|
||||
mgmtgrpc.ToSyncResponse(
|
||||
ctx, nil, nil, nil, peer, nil, nil, nm, equivDNSName, nil,
|
||||
dnsCache, nmData.AccountSettings, nil, nil, 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// benchDBLoad runs op b.N times and reports server-side pg_stat_database
|
||||
// deltas per op. A non-nil tracer additionally reports exact client round
|
||||
// trips per op.
|
||||
//
|
||||
// Backends flush cumulative stats at most once per second and only while
|
||||
// processing commands, so around each snapshot the load settles: sleep past
|
||||
// the flush interval, then run one extra untimed op whose command end flushes
|
||||
// everything pending. The trailing extra op lands inside the measured window,
|
||||
// hence the b.N+1 denominator for the server-side metrics.
|
||||
func benchDBLoad(b *testing.B, ctx context.Context, statsConn *pgx.Conn, tracer *queryCountTracer, op func() error) {
|
||||
b.Helper()
|
||||
|
||||
require.NoError(b, op(), "warmup")
|
||||
settleDBStats(b, op)
|
||||
|
||||
before, err := snapshotDBStats(ctx, statsConn)
|
||||
require.NoError(b, err, "stats snapshot")
|
||||
var queriesBefore int64
|
||||
if tracer != nil {
|
||||
queriesBefore = tracer.queries.Load()
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := op(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
|
||||
settleDBStats(b, op)
|
||||
after, err := snapshotDBStats(ctx, statsConn)
|
||||
require.NoError(b, err, "stats snapshot")
|
||||
|
||||
ops := float64(b.N + 1)
|
||||
if tracer != nil {
|
||||
b.ReportMetric(float64(tracer.queries.Load()-queriesBefore)/ops, "queries/op")
|
||||
}
|
||||
b.ReportMetric(float64(after.xactCommit-before.xactCommit)/ops, "xact/op")
|
||||
b.ReportMetric(float64(after.tupReturned-before.tupReturned)/ops, "tup_returned/op")
|
||||
b.ReportMetric(float64(after.tupFetched-before.tupFetched)/ops, "tup_fetched/op")
|
||||
b.ReportMetric(float64(after.blksRead-before.blksRead)/ops, "blks_read/op")
|
||||
b.ReportMetric(float64(after.blksHit-before.blksHit)/ops, "blks_hit/op")
|
||||
}
|
||||
|
||||
func settleDBStats(b *testing.B, op func() error) {
|
||||
b.Helper()
|
||||
time.Sleep(1100 * time.Millisecond)
|
||||
require.NoError(b, op(), "stats flush op")
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func benchAccountIDs(b *testing.B, ctx context.Context, conn *pgx.Conn) []string {
|
||||
b.Helper()
|
||||
|
||||
if ids := strings.TrimSpace(os.Getenv("NETMAP_ACCOUNTS")); ids != "" {
|
||||
var out []string
|
||||
for _, id := range strings.Split(ids, ",") {
|
||||
if id = strings.TrimSpace(id); id != "" {
|
||||
out = append(out, id)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
rows, err := conn.Query(ctx,
|
||||
"select account_id from peers group by account_id order by count(*) desc, account_id limit 10")
|
||||
require.NoError(b, err, "list benchmark accounts")
|
||||
ids, err := pgx.CollectRows(rows, pgx.RowTo[string])
|
||||
require.NoError(b, err, "collect benchmark accounts")
|
||||
require.NotEmpty(b, ids, "no accounts found")
|
||||
return ids
|
||||
}
|
||||
|
||||
func logAccountShape(b *testing.B, ctx context.Context, conn *pgx.Conn, accountID string) {
|
||||
b.Helper()
|
||||
|
||||
var peers, groups, users, policies, routes, resources, nsGroups int
|
||||
err := conn.QueryRow(ctx, `select
|
||||
(select count(*) from peers where account_id=$1),
|
||||
(select count(*) from groups where account_id=$1),
|
||||
(select count(*) from users where account_id=$1),
|
||||
(select count(*) from policies where account_id=$1),
|
||||
(select count(*) from routes where account_id=$1),
|
||||
(select count(*) from network_resources where account_id=$1),
|
||||
(select count(*) from name_server_groups where account_id=$1)`, accountID).
|
||||
Scan(&peers, &groups, &users, &policies, &routes, &resources, &nsGroups)
|
||||
require.NoError(b, err, "account shape")
|
||||
b.Logf("account=%s peers=%d groups=%d users=%d policies=%d routes=%d resources=%d nsgroups=%d",
|
||||
accountID, peers, groups, users, policies, routes, resources, nsGroups)
|
||||
}
|
||||
|
||||
type dbStats struct {
|
||||
xactCommit int64
|
||||
tupReturned int64
|
||||
tupFetched int64
|
||||
blksRead int64
|
||||
blksHit int64
|
||||
}
|
||||
|
||||
func snapshotDBStats(ctx context.Context, conn *pgx.Conn) (dbStats, error) {
|
||||
var s dbStats
|
||||
err := conn.QueryRow(ctx, `select xact_commit, tup_returned, tup_fetched, blks_read, blks_hit
|
||||
from pg_stat_database where datname = current_database()`).
|
||||
Scan(&s.xactCommit, &s.tupReturned, &s.tupFetched, &s.blksRead, &s.blksHit)
|
||||
return s, err
|
||||
}
|
||||
|
||||
type queryCountTracer struct {
|
||||
queries atomic.Int64
|
||||
}
|
||||
|
||||
func (t *queryCountTracer) TraceQueryStart(ctx context.Context, _ *pgx.Conn, _ pgx.TraceQueryStartData) context.Context {
|
||||
t.queries.Add(1)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (t *queryCountTracer) TraceQueryEnd(context.Context, *pgx.Conn, pgx.TraceQueryEndData) {}
|
||||
@@ -0,0 +1,106 @@
|
||||
package legacynmap
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ComponentPeer is the self-contained peer representation used by
|
||||
// NetworkMapComponents and the calculated NetworkMap. It carries exactly the
|
||||
// subset of peer data that crosses the components wire format, so the shared
|
||||
// calculation layer stays independent of the management server's domain
|
||||
// types.
|
||||
type ComponentPeer struct {
|
||||
ID string
|
||||
Key string
|
||||
IP netip.Addr
|
||||
IPv6 netip.Addr
|
||||
DNSLabel string
|
||||
SSHKey string
|
||||
SSHEnabled bool
|
||||
ServerSSHAllowed bool
|
||||
AgentVersion string
|
||||
SupportsSourcePrefixes bool
|
||||
SupportsIPv6 bool
|
||||
LoginExpirationEnabled bool
|
||||
AddedWithSSOLogin bool
|
||||
LastLogin time.Time
|
||||
// ProxyEmbedded marks an ephemeral embedded proxy peer. Connections
|
||||
// involving such a peer on either endpoint default to lazy.
|
||||
ProxyEmbedded bool
|
||||
}
|
||||
|
||||
// FQDN returns the peer's FQDN combined of the peer's DNS label and the system's DNS domain.
|
||||
func (p *ComponentPeer) FQDN(dnsDomain string) string {
|
||||
if dnsDomain == "" {
|
||||
return ""
|
||||
}
|
||||
return p.DNSLabel + "." + dnsDomain
|
||||
}
|
||||
|
||||
// LoginExpired indicates whether the peer's login has expired, mirroring the
|
||||
// server-side peer semantics: only SSO-added peers with login expiration
|
||||
// enabled can expire.
|
||||
func (p *ComponentPeer) LoginExpired(expiresIn time.Duration) (bool, time.Duration) {
|
||||
if !p.AddedWithSSOLogin || !p.LoginExpirationEnabled {
|
||||
return false, 0
|
||||
}
|
||||
timeLeft := time.Until(p.LastLogin.Add(expiresIn))
|
||||
return timeLeft <= 0, timeLeft
|
||||
}
|
||||
|
||||
// GroupAllName is the reserved name of the default group that contains every peer in an account.
|
||||
const GroupAllName = "All"
|
||||
|
||||
// ComponentGroup is the self-contained group representation used by
|
||||
// NetworkMapComponents: just the membership view the network-map calculation
|
||||
// needs, without the server's storage fields.
|
||||
type ComponentGroup struct {
|
||||
ID string
|
||||
PublicID string
|
||||
Name string
|
||||
Peers []string
|
||||
}
|
||||
|
||||
// IsGroupAll checks if the group is a default "All" group.
|
||||
func (g *ComponentGroup) IsGroupAll() bool {
|
||||
return g.Name == GroupAllName
|
||||
}
|
||||
|
||||
// ComponentRouter is the self-contained network-router representation used by
|
||||
// NetworkMapComponents.
|
||||
type ComponentRouter struct {
|
||||
NetworkID string
|
||||
PublicID string
|
||||
Peer string
|
||||
PeerGroups []string
|
||||
Masquerade bool
|
||||
Metric int
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// ComponentResourceType mirrors the network-resource type enum on the
|
||||
// components wire format.
|
||||
type ComponentResourceType string
|
||||
|
||||
const (
|
||||
ComponentResourceHost ComponentResourceType = "host"
|
||||
ComponentResourceSubnet ComponentResourceType = "subnet"
|
||||
ComponentResourceDomain ComponentResourceType = "domain"
|
||||
)
|
||||
|
||||
// ComponentResource is the self-contained network-resource representation
|
||||
// used by NetworkMapComponents.
|
||||
type ComponentResource struct {
|
||||
ID string
|
||||
PublicID string
|
||||
NetworkID string
|
||||
AccountID string
|
||||
Name string
|
||||
Description string
|
||||
Type ComponentResourceType
|
||||
Address string
|
||||
Domain string
|
||||
Prefix netip.Prefix
|
||||
Enabled bool
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package legacynmap
|
||||
|
||||
import (
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
)
|
||||
|
||||
// NetworkMap is main's shape. It is copied rather than aliased because this
|
||||
// branch's NetworkMap dropped ForceRoutingPeerDNSResolution, which main threads
|
||||
// into PeerConfig.RoutingPeerDnsResolutionEnabled.
|
||||
type NetworkMap struct {
|
||||
Peers []*ComponentPeer
|
||||
Network *Network
|
||||
Routes []*route.Route
|
||||
DNSConfig nbdns.Config
|
||||
OfflinePeers []*ComponentPeer
|
||||
FirewallRules []*FirewallRule
|
||||
RoutesFirewallRules []*RouteFirewallRule
|
||||
ForwardingRules []*ForwardingRule
|
||||
AuthorizedUsers map[string]map[string]struct{}
|
||||
EnableSSH bool
|
||||
// ForceRoutingPeerDNSResolution forces the peer to run/use routing-peer DNS
|
||||
// resolution regardless of the account-global setting, for reverse-proxy
|
||||
// domain targets.
|
||||
ForceRoutingPeerDNSResolution bool
|
||||
}
|
||||
|
||||
// The ToComponent converters below are main's methods, re-expressed as free
|
||||
// functions because their receivers live in packages this one cannot extend.
|
||||
// Bodies are otherwise unchanged.
|
||||
|
||||
func peerToComponent(p *nbpeer.Peer) *ComponentPeer {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
cp := &ComponentPeer{
|
||||
ID: p.ID,
|
||||
Key: p.Key,
|
||||
IP: p.IP,
|
||||
IPv6: p.IPv6,
|
||||
DNSLabel: p.DNSLabel,
|
||||
SSHKey: p.SSHKey,
|
||||
SSHEnabled: p.SSHEnabled,
|
||||
ServerSSHAllowed: p.Meta.Flags.ServerSSHAllowed,
|
||||
AgentVersion: p.Meta.WtVersion,
|
||||
SupportsSourcePrefixes: p.SupportsSourcePrefixes(),
|
||||
SupportsIPv6: p.SupportsIPv6(),
|
||||
LoginExpirationEnabled: p.LoginExpirationEnabled,
|
||||
AddedWithSSOLogin: p.AddedWithSSOLogin(),
|
||||
ProxyEmbedded: p.ProxyMeta.Embedded,
|
||||
}
|
||||
if p.LastLogin != nil {
|
||||
cp.LastLogin = *p.LastLogin
|
||||
}
|
||||
return cp
|
||||
}
|
||||
|
||||
func groupToComponent(g *Group) *ComponentGroup {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
return &ComponentGroup{
|
||||
ID: g.ID,
|
||||
PublicID: g.PublicID,
|
||||
Name: g.Name,
|
||||
Peers: g.Peers,
|
||||
}
|
||||
}
|
||||
|
||||
func groupsToComponent(groups map[string]*Group) map[string]*ComponentGroup {
|
||||
if groups == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]*ComponentGroup, len(groups))
|
||||
for id, g := range groups {
|
||||
out[id] = groupToComponent(g)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func routerToComponent(n *routerTypes.NetworkRouter) *ComponentRouter {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
return &ComponentRouter{
|
||||
NetworkID: n.NetworkID,
|
||||
PublicID: n.PublicID,
|
||||
Peer: n.Peer,
|
||||
PeerGroups: n.PeerGroups,
|
||||
Masquerade: n.Masquerade,
|
||||
Metric: n.Metric,
|
||||
Enabled: n.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func routersToComponentMap(routers map[string]*routerTypes.NetworkRouter) map[string]*ComponentRouter {
|
||||
if routers == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]*ComponentRouter, len(routers))
|
||||
for id, r := range routers {
|
||||
out[id] = routerToComponent(r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func resourceToComponent(n *resourceTypes.NetworkResource) *ComponentResource {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
return &ComponentResource{
|
||||
ID: n.ID,
|
||||
PublicID: n.PublicID,
|
||||
NetworkID: n.NetworkID,
|
||||
AccountID: n.AccountID,
|
||||
Name: n.Name,
|
||||
Description: n.Description,
|
||||
Type: ComponentResourceType(n.Type),
|
||||
Address: n.Address,
|
||||
Domain: n.Domain,
|
||||
Prefix: n.Prefix,
|
||||
Enabled: n.Enabled,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package legacynmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones/records"
|
||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||
nbroute "github.com/netbirdio/netbird/route"
|
||||
)
|
||||
|
||||
func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*ComponentPeer, direction int, includeIPv6 bool) []*RouteFirewallRule {
|
||||
rulesExists := make(map[string]struct{})
|
||||
rules := make([]*RouteFirewallRule, 0)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
baseRule := RouteFirewallRule{
|
||||
PolicyID: rule.PolicyID,
|
||||
RouteID: route.ID,
|
||||
SourceRanges: sourceRanges,
|
||||
Action: string(rule.Action),
|
||||
Destination: route.Network.String(),
|
||||
Protocol: string(rule.Protocol),
|
||||
Domains: route.Domains,
|
||||
IsDynamic: route.IsDynamic(),
|
||||
}
|
||||
|
||||
if len(rule.Ports) == 0 {
|
||||
rules = append(rules, generateRulesWithPortRanges(baseRule, rule, rulesExists)...)
|
||||
} else {
|
||||
rules = append(rules, generateRulesWithPorts(ctx, baseRule, rule, rulesExists)...)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func filterPeerAppliedZones(ctx context.Context, accountZones []*zones.Zone, peerGroups LookupMap) []nbdns.CustomZone {
|
||||
var customZones []nbdns.CustomZone
|
||||
|
||||
if len(peerGroups) == 0 {
|
||||
return customZones
|
||||
}
|
||||
|
||||
for _, zone := range accountZones {
|
||||
if !zone.Enabled || len(zone.Records) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
hasAccess := false
|
||||
for _, distGroupID := range zone.DistributionGroups {
|
||||
if _, found := peerGroups[distGroupID]; found {
|
||||
hasAccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasAccess {
|
||||
continue
|
||||
}
|
||||
|
||||
simpleRecords := make([]nbdns.SimpleRecord, 0, len(zone.Records))
|
||||
for _, record := range zone.Records {
|
||||
var recordType int
|
||||
rData := record.Content
|
||||
|
||||
switch record.Type {
|
||||
case records.RecordTypeA:
|
||||
recordType = int(dns.TypeA)
|
||||
case records.RecordTypeAAAA:
|
||||
recordType = int(dns.TypeAAAA)
|
||||
case records.RecordTypeCNAME:
|
||||
recordType = int(dns.TypeCNAME)
|
||||
rData = dns.Fqdn(record.Content)
|
||||
default:
|
||||
log.WithContext(ctx).Warnf("unknown DNS record type %s for record %s", record.Type, record.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
simpleRecords = append(simpleRecords, nbdns.SimpleRecord{
|
||||
Name: dns.Fqdn(record.Name),
|
||||
Type: recordType,
|
||||
Class: nbdns.DefaultClass,
|
||||
TTL: record.TTL,
|
||||
RData: rData,
|
||||
})
|
||||
}
|
||||
|
||||
customZones = append(customZones, nbdns.CustomZone{
|
||||
Domain: dns.Fqdn(zone.Domain),
|
||||
Records: simpleRecords,
|
||||
SearchDomainDisabled: !zone.EnableSearchDomain,
|
||||
NonAuthoritative: true,
|
||||
})
|
||||
}
|
||||
|
||||
return customZones
|
||||
}
|
||||
|
||||
func getAllowedUserIDs(a *Account) map[string]struct{} {
|
||||
users := make(map[string]struct{})
|
||||
for _, nbUser := range a.Users {
|
||||
if !nbUser.IsBlocked() && !nbUser.IsServiceUser {
|
||||
users[nbUser.Id] = struct{}{}
|
||||
}
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
func getUniquePeerIDsFromGroupsIDs(a *Account, ctx context.Context, groups []string) []string {
|
||||
peerIDs := make(map[string]struct{}, len(groups)) // we expect at least one peer per group as initial capacity
|
||||
for _, groupID := range groups {
|
||||
group := a.GetGroup(groupID)
|
||||
if group == nil {
|
||||
log.WithContext(ctx).Warnf("group %s doesn't exist under account %s, will continue map generation without it", groupID, a.Id)
|
||||
continue
|
||||
}
|
||||
|
||||
if group.IsGroupAll() || len(groups) == 1 {
|
||||
return group.Peers
|
||||
}
|
||||
|
||||
for _, peerID := range group.Peers {
|
||||
peerIDs[peerID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
ids := make([]string, 0, len(peerIDs))
|
||||
for peerID := range peerIDs {
|
||||
ids = append(ids, peerID)
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
func forcesRoutingPeerDNSResolution(a *Account, peerID string, routers map[string]map[string]*routerTypes.NetworkRouter) bool {
|
||||
targeted := proxyTargetedDomainResourceIDs(a)
|
||||
if len(targeted) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, resource := range a.NetworkResources {
|
||||
if resource == nil || !resource.Enabled || resource.Type != resourceTypes.Domain {
|
||||
continue
|
||||
}
|
||||
if _, ok := targeted[resource.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, isRouter := routers[resource.NetworkID][peerID]; isRouter {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func proxyTargetedDomainResourceIDs(a *Account) map[string]struct{} {
|
||||
ids := make(map[string]struct{})
|
||||
for _, svc := range a.Services {
|
||||
if svc == nil || !svc.Enabled || svc.Terminated {
|
||||
continue
|
||||
}
|
||||
for _, target := range svc.Targets {
|
||||
if target == nil || !target.Enabled {
|
||||
continue
|
||||
}
|
||||
if target.TargetType == service.TargetTypeDomain {
|
||||
ids[target.TargetId] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func splitPeerSourcesByFamily(groupPeers []*ComponentPeer) (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
|
||||
}
|
||||
|
||||
func generateRulesWithPortRanges(baseRule RouteFirewallRule, rule *PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule {
|
||||
rules := make([]*RouteFirewallRule, 0)
|
||||
|
||||
ruleIDBase := generateRuleIDBase(rule, baseRule)
|
||||
if len(rule.Ports) == 0 {
|
||||
if len(rule.PortRanges) == 0 {
|
||||
if _, ok := rulesExists[ruleIDBase]; !ok {
|
||||
rulesExists[ruleIDBase] = struct{}{}
|
||||
rules = append(rules, &baseRule)
|
||||
}
|
||||
} else {
|
||||
for _, portRange := range rule.PortRanges {
|
||||
ruleID := fmt.Sprintf("%s%d-%d", ruleIDBase, portRange.Start, portRange.End)
|
||||
if _, ok := rulesExists[ruleID]; !ok {
|
||||
rulesExists[ruleID] = struct{}{}
|
||||
pr := baseRule
|
||||
pr.PortRange = portRange
|
||||
rules = append(rules, &pr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
func generateRulesWithPorts(ctx context.Context, baseRule RouteFirewallRule, rule *PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule {
|
||||
rules := make([]*RouteFirewallRule, 0)
|
||||
ruleIDBase := generateRuleIDBase(rule, baseRule)
|
||||
|
||||
for _, port := range rule.Ports {
|
||||
ruleID := ruleIDBase + port
|
||||
if _, ok := rulesExists[ruleID]; ok {
|
||||
continue
|
||||
}
|
||||
rulesExists[ruleID] = struct{}{}
|
||||
|
||||
pr := baseRule
|
||||
p, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to parse port %s for rule: %s", port, rule.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
pr.Port = uint16(p)
|
||||
rules = append(rules, &pr)
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
func generateRuleIDBase(rule *PolicyRule, baseRule RouteFirewallRule) string {
|
||||
return rule.ID + strings.Join(baseRule.SourceRanges, ",") + strconv.Itoa(FirewallRuleDirectionIN) + baseRule.Protocol + baseRule.Action
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Package legacynmap is a frozen copy of main's Account → NetworkMapComponents
|
||||
// → NetworkMap → proto path. It exists only to measure this tree against main:
|
||||
// the proto-equivalence test runs it over a production database copy, and the
|
||||
// nmaptest golden suite runs it as a third mode so every case pins all three
|
||||
// shapes to one expectation.
|
||||
//
|
||||
// It lives in its own package so it cannot reach this tree's unexported
|
||||
// helpers — a divergence can therefore never be hidden by the two sides
|
||||
// sharing code. Nothing in production imports it.
|
||||
//
|
||||
// Types are aliased rather than copied where they are byte-identical between
|
||||
// main and this branch. Anything that drifted is copied instead; see
|
||||
// converters.go and copied_funcs.go.
|
||||
//
|
||||
// Delete this package once the nmdata refactor is validated.
|
||||
package legacynmap
|
||||
@@ -0,0 +1,680 @@
|
||||
//go:build nmapequiv
|
||||
|
||||
// Main-vs-branch equivalence check. For every peer of every account in a real
|
||||
// Postgres copy it computes the client-facing proto.NetworkMap twice:
|
||||
//
|
||||
// - legacy path: main's Account → NetworkMapComponents → Calculate → proto
|
||||
// (the frozen copy in this package)
|
||||
// - store path: the pgsql nmdata store's NetworkMapData → components →
|
||||
// Calculate → ToSyncResponse → proto (no Account involved)
|
||||
// - account path: Account → toNetworkMapData twins → components → Calculate
|
||||
// → ToSyncResponse → proto (the in-memory builder, no store queries)
|
||||
//
|
||||
// Both new paths are checked against the legacy proto.
|
||||
//
|
||||
// proto.NetworkMap is generated code identical in both trees, which is what
|
||||
// makes it the one usable comparison surface — the intermediate Go types differ
|
||||
// by design. proto.Equal would trip over repeated-field ordering, so both sides
|
||||
// are canonicalized first.
|
||||
//
|
||||
// NETBIRD_STORE_ENGINE_POSTGRES_DSN='...' go test -tags nmapequiv \
|
||||
// -run TestNetworkMapProtoEquivalence -count=1 -timeout 60m \
|
||||
// ./management/server/types/legacynmap/
|
||||
//
|
||||
// Accounts are loaded one at a time and released between iterations, so peak
|
||||
// memory tracks the largest single account rather than the whole database.
|
||||
//
|
||||
// Env knobs: NETMAP_ACCOUNTS (comma-separated ids, skips discovery),
|
||||
// NETMAP_MAX_ACCOUNTS (0 = all), NETMAP_MAX_PEERS (0 = all). Fails at the
|
||||
// first divergence.
|
||||
package legacynmap_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"google.golang.org/protobuf/encoding/prototext"
|
||||
goproto "google.golang.org/protobuf/proto"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
gormlogger "gorm.io/gorm/logger"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
||||
networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db"
|
||||
networkmap_pgsql "github.com/netbirdio/netbird/management/internals/network_map_db/pgsql"
|
||||
mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator"
|
||||
"github.com/netbirdio/netbird/management/server/settings"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/management/server/types/legacynmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
equivDNSName = "netbird.cloud"
|
||||
progressEvery = 5000
|
||||
)
|
||||
|
||||
type equivStats struct {
|
||||
accounts int
|
||||
peersChecked int
|
||||
}
|
||||
|
||||
func TestNetworkMapProtoEquivalence(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("prod-db equivalence test, skipped in short mode")
|
||||
}
|
||||
dsn := equivDSN()
|
||||
if dsn == "" {
|
||||
t.Skip("NETBIRD_STORE_ENGINE_POSTGRES_DSN not set")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
// skipMigration=true: this reads a restored production copy and must not
|
||||
// alter its schema. Flip to false only if reads fail on an older dump.
|
||||
testStore, err := store.NewPostgresqlStore(ctx, dsn, nil, true)
|
||||
require.NoError(t, err, "connect to postgres")
|
||||
t.Cleanup(func() { testStore.Close(ctx) })
|
||||
|
||||
pgStore, err := networkmap_pgsql.NewPostgresqlStore(ctx, dsn)
|
||||
require.NoError(t, err, "connect nmdata store")
|
||||
t.Cleanup(func() { pgStore.Pool.Close() })
|
||||
nmStore := nmDataStore(t, pgStore)
|
||||
|
||||
accountIDs := equivAccountIDs(t, dsn)
|
||||
require.NotEmpty(t, accountIDs, "no accounts selected")
|
||||
|
||||
stats := &equivStats{accounts: len(accountIDs)}
|
||||
maxPeers := envInt("NETMAP_MAX_PEERS", 0)
|
||||
|
||||
for i, accountID := range accountIDs {
|
||||
account, err := testStore.GetAccount(ctx, accountID)
|
||||
if err != nil {
|
||||
t.Logf("account %s: load failed, skipping: %v", accountID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
checkAccount(ctx, t, testStore, nmStore, account, maxPeers, stats)
|
||||
|
||||
account = nil
|
||||
debug.FreeOSMemory()
|
||||
|
||||
if i%progressEvery == 0 {
|
||||
var ms runtime.MemStats
|
||||
runtime.ReadMemStats(&ms)
|
||||
t.Logf("progress: accounts=%d/%d peers_checked=%d heap=%dMiB", i, len(accountIDs), stats.peersChecked, ms.HeapAlloc>>20)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("equivalence: accounts=%d peers_checked=%d — no divergence",
|
||||
stats.accounts, stats.peersChecked)
|
||||
}
|
||||
|
||||
// checkAccount compares both paths for every peer of one account. Nothing is
|
||||
// retained across peers, so memory stays flat within an account.
|
||||
func checkAccount(ctx context.Context, t *testing.T, accountStore store.Store, nmStore *networkmapdb.NetworkMapDBStoreImpl, account *types.Account, maxPeers int, stats *equivStats) {
|
||||
t.Helper()
|
||||
|
||||
if len(account.Peers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
nmData, err := nmStore.GetNetworkMapData(ctx, account.Id)
|
||||
require.NoError(t, err, "account %s: nmdata store load", account.Id)
|
||||
|
||||
validated := make(map[string]struct{}, len(account.Peers))
|
||||
peerIDs := make([]string, 0, len(account.Peers))
|
||||
for peerID := range account.Peers {
|
||||
validated[peerID] = struct{}{}
|
||||
peerIDs = append(peerIDs, peerID)
|
||||
}
|
||||
sort.Strings(peerIDs)
|
||||
if maxPeers > 0 && len(peerIDs) > maxPeers {
|
||||
peerIDs = peerIDs[:maxPeers]
|
||||
}
|
||||
|
||||
// Production fills ValidatedPeers via the integrated-validator wrapper; here
|
||||
// every peer counts as validated, matching the legacy side's map.
|
||||
nmData.ValidatedPeers = validated
|
||||
|
||||
// Custom DNS zones are built twice from the same rows — the account side
|
||||
// from the zones manager, the store side in SQL — so both are fed in and
|
||||
// compared rather than dropped. The same goes for the peers zone below:
|
||||
// each side computes it with its own helper, which is where an AAAA gate
|
||||
// that disagrees between the two would show up.
|
||||
accountZones, err := accountStore.GetAccountZones(ctx, store.LockingStrengthNone, account.Id)
|
||||
require.NoError(t, err, "account %s: load account zones", account.Id)
|
||||
|
||||
resourcePolicies := account.GetResourcePoliciesMap()
|
||||
routers := account.GetResourceRoutersMap()
|
||||
groupUsers := account.GetActiveGroupUsers()
|
||||
|
||||
// The reverse-proxy ACLs are synthesised, never persisted. Both new paths
|
||||
// derive them inside the twin; main derived them in the controller, onto
|
||||
// the account, before the resource-policy map. The legacy side therefore
|
||||
// runs on its own view of the policies — a shallow copy so the account the
|
||||
// other two paths read stays untouched and cannot double-count them.
|
||||
legacyAccount := *account
|
||||
if synth := legacynmap.SynthesizeProxyPolicies(account); len(synth) > 0 {
|
||||
legacyAccount.Policies = append(slices.Clone(account.Policies), synth...)
|
||||
}
|
||||
legacyResourcePolicies := legacyAccount.GetResourcePoliciesMap()
|
||||
|
||||
settings := account.Settings
|
||||
if settings == nil {
|
||||
settings = &types.Settings{}
|
||||
}
|
||||
|
||||
accountPeersZone := account.GetPeersCustomZone(ctx, equivDNSName)
|
||||
storePeersZone := networkmap.PeersCustomZone(ctx, account.Id, equivDNSName, nmData.Peers, controller.IPv6AllowedPeersFromData(nmData))
|
||||
|
||||
for _, peerID := range peerIDs {
|
||||
peer := account.Peers[peerID]
|
||||
if peer == nil {
|
||||
continue
|
||||
}
|
||||
dataPeer := nmData.Peers[peerID]
|
||||
if dataPeer == nil {
|
||||
t.Fatalf("after %d peers: account=%s peer=%s present in account store, missing in nmdata store", stats.peersChecked, account.Id, peerID)
|
||||
}
|
||||
|
||||
// STORE PATH — nmdata store through the production computation, mirroring
|
||||
// the controller's networkMapFromData.
|
||||
components := nmData.GetPeerNetworkMapComponents(peerID, storePeersZone)
|
||||
storeNM := &types.NetworkMap{Network: components.Network}
|
||||
if !components.IsEmpty() {
|
||||
storeNM = types.CalculateNetworkMapFromComponents(ctx, components)
|
||||
}
|
||||
// A separate cache per side: sharing one would let the first path
|
||||
// populate entries the second then reuses, which can mask a real diff.
|
||||
storeProto := mgmtgrpc.ToSyncResponse(
|
||||
ctx, nil, nil, nil, dataPeer, nil, nil, storeNM, equivDNSName, nil,
|
||||
&cache.DNSConfigCache{}, nmData.AccountSettings, settings.Extra, nil, 0,
|
||||
).NetworkMap
|
||||
|
||||
// ACCOUNT PATH — Account → toNetworkMapData twins → components.
|
||||
acctNM := account.GetPeerNetworkMapFromComponents(
|
||||
ctx, peerID, accountPeersZone, accountZones, validated, resourcePolicies, routers, nil, groupUsers,
|
||||
)
|
||||
acctProto := mgmtgrpc.ToSyncResponse(
|
||||
ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, acctNM, equivDNSName, nil,
|
||||
&cache.DNSConfigCache{}, types.TwinAccountSettings(settings), settings.Extra, nil, 0,
|
||||
).NetworkMap
|
||||
|
||||
// LEGACY PATH — main's frozen copy.
|
||||
legacyNM := legacynmap.GetPeerNetworkMapFromComponents(
|
||||
&legacyAccount, ctx, peerID, accountPeersZone, accountZones, validated, legacyResourcePolicies, routers, nil, groupUsers,
|
||||
)
|
||||
if legacyNM == nil {
|
||||
t.Fatalf("after %d peers: account=%s peer=%s legacy NetworkMap nil, new non-nil", stats.peersChecked, account.Id, peerID)
|
||||
}
|
||||
legacyProto := legacynmap.ToProtoNetworkMap(
|
||||
ctx, peer, legacyNM, equivDNSName, settings, nil, &cache.DNSConfigCache{}, 0,
|
||||
)
|
||||
|
||||
canonicalize(legacyProto)
|
||||
canonicalize(storeProto)
|
||||
canonicalize(acctProto)
|
||||
stats.peersChecked++
|
||||
|
||||
if !goproto.Equal(legacyProto, storeProto) {
|
||||
t.Fatalf("after %d peers: store path: %s", stats.peersChecked, describeDivergence(legacyProto, storeProto, account.Id, peerID))
|
||||
}
|
||||
if !goproto.Equal(legacyProto, acctProto) {
|
||||
t.Fatalf("after %d peers: account path: %s", stats.peersChecked, describeDivergence(legacyProto, acctProto, account.Id, peerID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nmDataStore wraps a raw connection store the way production's factory does.
|
||||
// The validator marks every peer validated and the extra settings are empty:
|
||||
// checkAccount overwrites ValidatedPeers anyway, and neither reaches the
|
||||
// compared network map.
|
||||
func nmDataStore(tb testing.TB, s networkmapdb.NetworkMapDBStore) *networkmapdb.NetworkMapDBStoreImpl {
|
||||
tb.Helper()
|
||||
|
||||
extraSettings := settings.NewMockManager(gomock.NewController(tb))
|
||||
extraSettings.EXPECT().GetExtraSettings(gomock.Any(), gomock.Any()).Return(&types.ExtraSettings{}, nil).AnyTimes()
|
||||
|
||||
return &networkmapdb.NetworkMapDBStoreImpl{
|
||||
Store: s,
|
||||
IntegratedPeerValidator: &validator.IntegratedValidatorImpl{},
|
||||
ExtraSettingsManager: extraSettings,
|
||||
}
|
||||
}
|
||||
|
||||
func equivDSN() string {
|
||||
if dsn := os.Getenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN"); dsn != "" {
|
||||
return dsn
|
||||
}
|
||||
return os.Getenv("NB_STORE_ENGINE_POSTGRES_DSN")
|
||||
}
|
||||
|
||||
// equivAccountIDs lists account ids with an id-only query. store.GetAllAccounts
|
||||
// would hydrate every account in the database before the first comparison runs.
|
||||
// Sorting happens in Go so the order does not depend on database collation.
|
||||
func equivAccountIDs(t *testing.T, dsn string) []string {
|
||||
t.Helper()
|
||||
|
||||
if ids := strings.TrimSpace(os.Getenv("NETMAP_ACCOUNTS")); ids != "" {
|
||||
var out []string
|
||||
for _, id := range strings.Split(ids, ",") {
|
||||
if id = strings.TrimSpace(id); id != "" {
|
||||
out = append(out, id)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: gormlogger.Discard})
|
||||
require.NoError(t, err, "open id-listing connection")
|
||||
defer func() {
|
||||
if sqlDB, err := db.DB(); err == nil {
|
||||
sqlDB.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var ids []string
|
||||
require.NoError(t, db.Model(&types.Account{}).Pluck("id", &ids).Error)
|
||||
sort.Strings(ids)
|
||||
|
||||
if max := envInt("NETMAP_MAX_ACCOUNTS", 0); max > 0 && len(ids) > max {
|
||||
ids = ids[:max]
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func envInt(name string, def int) int {
|
||||
if v := os.Getenv(name); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// canonicalize sorts every repeated field by a stable key. Both paths iterate Go
|
||||
// maps while building these slices, so order can differ even when the content is
|
||||
// identical; without this proto.Equal reports noise.
|
||||
func canonicalize(nm *proto.NetworkMap) {
|
||||
if nm == nil {
|
||||
return
|
||||
}
|
||||
slices.SortFunc(nm.RemotePeers, cmpRemotePeer)
|
||||
slices.SortFunc(nm.OfflinePeers, cmpRemotePeer)
|
||||
slices.SortFunc(nm.Routes, cmpRoute)
|
||||
slices.SortFunc(nm.FirewallRules, cmpFirewallRule)
|
||||
slices.SortFunc(nm.RoutesFirewallRules, cmpRouteFirewallRule)
|
||||
slices.SortFunc(nm.ForwardingRules, cmpForwardingRule)
|
||||
|
||||
for _, r := range nm.FirewallRules {
|
||||
slices.SortFunc(r.SourcePrefixes, bytes.Compare)
|
||||
}
|
||||
for _, r := range nm.RoutesFirewallRules {
|
||||
slices.Sort(r.SourceRanges)
|
||||
}
|
||||
canonicalizeDNSConfig(nm.DNSConfig)
|
||||
canonicalizeSSHAuth(nm.SshAuth)
|
||||
}
|
||||
|
||||
func canonicalizeDNSConfig(d *proto.DNSConfig) {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
for _, g := range d.NameServerGroups {
|
||||
if g == nil {
|
||||
continue
|
||||
}
|
||||
slices.Sort(g.Domains)
|
||||
slices.SortFunc(g.NameServers, func(a, b *proto.NameServer) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
if c := cmp.Compare(a.IP, b.IP); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Port, b.Port); c != 0 {
|
||||
return c
|
||||
}
|
||||
return cmp.Compare(a.NSType, b.NSType)
|
||||
})
|
||||
}
|
||||
slices.SortFunc(d.NameServerGroups, func(a, b *proto.NameServerGroup) int {
|
||||
return cmp.Compare(nsgKey(a), nsgKey(b))
|
||||
})
|
||||
for _, z := range d.CustomZones {
|
||||
if z == nil {
|
||||
continue
|
||||
}
|
||||
slices.SortFunc(z.Records, cmpSimpleRecord)
|
||||
}
|
||||
slices.SortFunc(d.CustomZones, func(a, b *proto.CustomZone) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
return cmp.Compare(a.Domain, b.Domain)
|
||||
})
|
||||
}
|
||||
|
||||
// canonicalizeSSHAuth sorts AuthorizedUsers and re-keys MachineUsers.Indexes
|
||||
// against the new ordering, preserving which machine user maps to which hashes.
|
||||
func canonicalizeSSHAuth(s *proto.SSHAuth) {
|
||||
if s == nil || len(s.AuthorizedUsers) == 0 {
|
||||
return
|
||||
}
|
||||
type hashed struct {
|
||||
bytes []byte
|
||||
old uint32
|
||||
}
|
||||
entries := make([]hashed, len(s.AuthorizedUsers))
|
||||
for i, b := range s.AuthorizedUsers {
|
||||
entries[i] = hashed{bytes: b, old: uint32(i)}
|
||||
}
|
||||
slices.SortFunc(entries, func(a, b hashed) int { return bytes.Compare(a.bytes, b.bytes) })
|
||||
|
||||
remap := make(map[uint32]uint32, len(entries))
|
||||
sorted := make([][]byte, len(entries))
|
||||
for newIdx, e := range entries {
|
||||
remap[e.old] = uint32(newIdx)
|
||||
sorted[newIdx] = e.bytes
|
||||
}
|
||||
s.AuthorizedUsers = sorted
|
||||
|
||||
for _, mu := range s.MachineUsers {
|
||||
if mu == nil {
|
||||
continue
|
||||
}
|
||||
for i, oldIdx := range mu.Indexes {
|
||||
if newIdx, ok := remap[oldIdx]; ok {
|
||||
mu.Indexes[i] = newIdx
|
||||
}
|
||||
}
|
||||
slices.Sort(mu.Indexes)
|
||||
}
|
||||
}
|
||||
|
||||
func boolCmp(a, b bool) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
if a {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func nsgKey(g *proto.NameServerGroup) string {
|
||||
if g == nil {
|
||||
return ""
|
||||
}
|
||||
var parts []string
|
||||
for _, ns := range g.NameServers {
|
||||
if ns == nil {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, ns.IP+":"+strconv.FormatInt(ns.Port, 10)+":"+strconv.FormatInt(ns.NSType, 10))
|
||||
}
|
||||
slices.Sort(parts)
|
||||
key := strings.Join(parts, ",")
|
||||
domains := append([]string(nil), g.Domains...)
|
||||
slices.Sort(domains)
|
||||
key += "|" + strings.Join(domains, "|")
|
||||
if g.Primary {
|
||||
key += "|P"
|
||||
}
|
||||
if g.SearchDomainsEnabled {
|
||||
key += "|S"
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func cmpSimpleRecord(a, b *proto.SimpleRecord) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
if c := cmp.Compare(a.Name, b.Name); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Type, b.Type); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Class, b.Class); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.RData, b.RData); c != 0 {
|
||||
return c
|
||||
}
|
||||
return cmp.Compare(a.TTL, b.TTL)
|
||||
}
|
||||
|
||||
func cmpRemotePeer(a, b *proto.RemotePeerConfig) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
return cmp.Compare(a.WgPubKey, b.WgPubKey)
|
||||
}
|
||||
|
||||
func cmpRoute(a, b *proto.Route) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
if c := cmp.Compare(a.ID, b.ID); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.NetID, b.NetID); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Network, b.Network); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Peer, b.Peer); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Metric, b.Metric); c != 0 {
|
||||
return c
|
||||
}
|
||||
return slices.Compare(a.Domains, b.Domains)
|
||||
}
|
||||
|
||||
func cmpFirewallRule(a, b *proto.FirewallRule) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
if c := bytes.Compare(a.PolicyID, b.PolicyID); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.PeerIP, b.PeerIP); c != 0 { //nolint:staticcheck
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(int32(a.Direction), int32(b.Direction)); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(int32(a.Action), int32(b.Action)); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(int32(a.Protocol), int32(b.Protocol)); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Port, b.Port); c != 0 {
|
||||
return c
|
||||
}
|
||||
return cmp.Compare(portInfoKey(a.PortInfo), portInfoKey(b.PortInfo))
|
||||
}
|
||||
|
||||
func cmpRouteFirewallRule(a, b *proto.RouteFirewallRule) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
if c := bytes.Compare(a.PolicyID, b.PolicyID); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.RouteID, b.RouteID); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Destination, b.Destination); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(int32(a.Protocol), int32(b.Protocol)); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(portInfoKey(a.PortInfo), portInfoKey(b.PortInfo)); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(int32(a.Action), int32(b.Action)); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := slices.Compare(a.Domains, b.Domains); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := slices.Compare(a.SourceRanges, b.SourceRanges); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.CustomProtocol, b.CustomProtocol); c != 0 {
|
||||
return c
|
||||
}
|
||||
return boolCmp(a.IsDynamic, b.IsDynamic)
|
||||
}
|
||||
|
||||
func cmpForwardingRule(a, b *proto.ForwardingRule) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
if c := cmp.Compare(int32(a.Protocol), int32(b.Protocol)); c != 0 {
|
||||
return c
|
||||
}
|
||||
return bytes.Compare(a.TranslatedAddress, b.TranslatedAddress)
|
||||
}
|
||||
|
||||
func portInfoKey(pi *proto.PortInfo) string {
|
||||
if pi == nil {
|
||||
return ""
|
||||
}
|
||||
switch sel := pi.PortSelection.(type) {
|
||||
case *proto.PortInfo_Port:
|
||||
return "P" + strconv.FormatUint(uint64(sel.Port), 10)
|
||||
case *proto.PortInfo_Range_:
|
||||
if sel.Range == nil {
|
||||
return "R"
|
||||
}
|
||||
return "R" + strconv.FormatUint(uint64(sel.Range.Start), 10) + "-" + strconv.FormatUint(uint64(sel.Range.End), 10)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// describeDivergence names the first differing field so a failure is actionable
|
||||
// without re-running against the database.
|
||||
func describeDivergence(legacy, updated *proto.NetworkMap, accountID, peerID string) string {
|
||||
prefix := "account=" + accountID + " peer=" + peerID
|
||||
|
||||
lens := []struct {
|
||||
field string
|
||||
a, b int
|
||||
diff func() string
|
||||
}{
|
||||
{"RemotePeers", len(legacy.RemotePeers), len(updated.RemotePeers), func() string { return diffLists(legacy.RemotePeers, updated.RemotePeers) }},
|
||||
{"OfflinePeers", len(legacy.OfflinePeers), len(updated.OfflinePeers), func() string { return diffLists(legacy.OfflinePeers, updated.OfflinePeers) }},
|
||||
{"Routes", len(legacy.Routes), len(updated.Routes), func() string { return diffLists(legacy.Routes, updated.Routes) }},
|
||||
{"FirewallRules", len(legacy.FirewallRules), len(updated.FirewallRules), func() string { return diffLists(legacy.FirewallRules, updated.FirewallRules) }},
|
||||
{"RoutesFirewallRules", len(legacy.RoutesFirewallRules), len(updated.RoutesFirewallRules), func() string { return diffLists(legacy.RoutesFirewallRules, updated.RoutesFirewallRules) }},
|
||||
{"ForwardingRules", len(legacy.ForwardingRules), len(updated.ForwardingRules), func() string { return diffLists(legacy.ForwardingRules, updated.ForwardingRules) }},
|
||||
}
|
||||
for _, l := range lens {
|
||||
if l.a != l.b {
|
||||
return prefix + " field=" + l.field + " legacy_len=" + strconv.Itoa(l.a) + " new_len=" + strconv.Itoa(l.b) + l.diff()
|
||||
}
|
||||
}
|
||||
|
||||
for i := range legacy.RemotePeers {
|
||||
if !goproto.Equal(legacy.RemotePeers[i], updated.RemotePeers[i]) {
|
||||
return prefix + " field=RemotePeers[" + strconv.Itoa(i) + "] legacy=" + protoStr(legacy.RemotePeers[i]) + " new=" + protoStr(updated.RemotePeers[i])
|
||||
}
|
||||
}
|
||||
for i := range legacy.Routes {
|
||||
if !goproto.Equal(legacy.Routes[i], updated.Routes[i]) {
|
||||
return prefix + " field=Routes[" + strconv.Itoa(i) + "] legacy=" + protoStr(legacy.Routes[i]) + " new=" + protoStr(updated.Routes[i])
|
||||
}
|
||||
}
|
||||
for i := range legacy.FirewallRules {
|
||||
if !goproto.Equal(legacy.FirewallRules[i], updated.FirewallRules[i]) {
|
||||
return prefix + " field=FirewallRules[" + strconv.Itoa(i) + "] legacy=" + protoStr(legacy.FirewallRules[i]) + " new=" + protoStr(updated.FirewallRules[i])
|
||||
}
|
||||
}
|
||||
for i := range legacy.RoutesFirewallRules {
|
||||
if !goproto.Equal(legacy.RoutesFirewallRules[i], updated.RoutesFirewallRules[i]) {
|
||||
return prefix + " field=RoutesFirewallRules[" + strconv.Itoa(i) + "] legacy=" + protoStr(legacy.RoutesFirewallRules[i]) + " new=" + protoStr(updated.RoutesFirewallRules[i])
|
||||
}
|
||||
}
|
||||
if !goproto.Equal(legacy.PeerConfig, updated.PeerConfig) {
|
||||
return prefix + " field=PeerConfig legacy=" + protoStr(legacy.PeerConfig) + " new=" + protoStr(updated.PeerConfig)
|
||||
}
|
||||
if !goproto.Equal(legacy.DNSConfig, updated.DNSConfig) {
|
||||
return prefix + " field=DNSConfig legacy=" + protoStr(legacy.DNSConfig) + " new=" + protoStr(updated.DNSConfig)
|
||||
}
|
||||
if !goproto.Equal(legacy.SshAuth, updated.SshAuth) {
|
||||
return prefix + " field=SshAuth legacy=" + protoStr(legacy.SshAuth) + " new=" + protoStr(updated.SshAuth)
|
||||
}
|
||||
if legacy.Serial != updated.Serial {
|
||||
return prefix + " field=Serial legacy=" + strconv.FormatUint(legacy.Serial, 10) + " new=" + strconv.FormatUint(updated.Serial, 10)
|
||||
}
|
||||
return prefix + " (repeated fields equal element-wise — scalar/oneof mismatch)"
|
||||
}
|
||||
|
||||
// diffLists reports the multiset difference of two repeated proto fields, so a
|
||||
// length mismatch shows which elements each side is missing.
|
||||
func diffLists[M goproto.Message](legacy, updated []M) string {
|
||||
counts := make(map[string]int)
|
||||
for _, m := range legacy {
|
||||
counts[prototext.MarshalOptions{}.Format(m)]++
|
||||
}
|
||||
for _, m := range updated {
|
||||
counts[prototext.MarshalOptions{}.Format(m)]--
|
||||
}
|
||||
|
||||
var onlyLegacy, onlyNew []string
|
||||
for k, c := range counts {
|
||||
for ; c > 0; c-- {
|
||||
onlyLegacy = append(onlyLegacy, k)
|
||||
}
|
||||
for ; c < 0; c++ {
|
||||
onlyNew = append(onlyNew, k)
|
||||
}
|
||||
}
|
||||
slices.Sort(onlyLegacy)
|
||||
slices.Sort(onlyNew)
|
||||
|
||||
var b strings.Builder
|
||||
for _, k := range onlyLegacy {
|
||||
b.WriteString("\n only_legacy: " + k)
|
||||
}
|
||||
for _, k := range onlyNew {
|
||||
b.WriteString("\n only_new: " + k)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func protoStr(m goproto.Message) string {
|
||||
if m == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
s := prototext.Format(m)
|
||||
const maxLen = 800
|
||||
if len(s) > maxLen {
|
||||
return s[:maxLen] + "...(truncated)"
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package legacynmap
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
v "github.com/hashicorp/go-version"
|
||||
|
||||
"github.com/netbirdio/netbird/version"
|
||||
)
|
||||
|
||||
const (
|
||||
firewallRuleMinPortRangesVer = "0.48.0"
|
||||
firewallRuleMinNativeSSHVer = "0.60.0"
|
||||
|
||||
nativeSSHPortString = "22022"
|
||||
nativeSSHPortNumber = 22022
|
||||
defaultSSHPortString = "22"
|
||||
defaultSSHPortNumber = 22
|
||||
)
|
||||
|
||||
type supportedFeatures struct {
|
||||
nativeSSH bool
|
||||
portRanges bool
|
||||
}
|
||||
|
||||
type LookupMap map[string]struct{}
|
||||
|
||||
func PolicyRuleImpliesLegacySSH(rule *PolicyRule) bool {
|
||||
return rule.Protocol == PolicyRuleProtocolALL || (rule.Protocol == PolicyRuleProtocolTCP && (portsIncludesSSH(rule.Ports) || portRangeIncludesSSH(rule.PortRanges)))
|
||||
}
|
||||
|
||||
func portRangeIncludesSSH(portRanges []RulePortRange) bool {
|
||||
for _, pr := range portRanges {
|
||||
if (pr.Start <= defaultSSHPortNumber && pr.End >= defaultSSHPortNumber) || (pr.Start <= nativeSSHPortNumber && pr.End >= nativeSSHPortNumber) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func portsIncludesSSH(ports []string) bool {
|
||||
for _, port := range ports {
|
||||
if port == defaultSSHPortString || port == nativeSSHPortString {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ExpandPortsAndRanges expands Ports and PortRanges of a rule into individual firewall rules.
|
||||
func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *ComponentPeer) []*FirewallRule {
|
||||
features := peerSupportedFirewallFeatures(peer.AgentVersion)
|
||||
|
||||
var expanded []*FirewallRule
|
||||
|
||||
for _, port := range rule.Ports {
|
||||
fr := base
|
||||
fr.Port = port
|
||||
expanded = append(expanded, &fr)
|
||||
}
|
||||
|
||||
for _, portRange := range rule.PortRanges {
|
||||
if len(rule.Ports) > 0 {
|
||||
break
|
||||
}
|
||||
fr := base
|
||||
|
||||
if features.portRanges {
|
||||
fr.PortRange = portRange
|
||||
} else {
|
||||
if portRange.Start != portRange.End {
|
||||
continue
|
||||
}
|
||||
fr.Port = strconv.FormatUint(uint64(portRange.Start), 10)
|
||||
}
|
||||
expanded = append(expanded, &fr)
|
||||
}
|
||||
|
||||
if shouldCheckRulesForNativeSSH(features.nativeSSH, rule, peer) || rule.Protocol == PolicyRuleProtocolNetbirdSSH {
|
||||
expanded = addNativeSSHRule(base, expanded)
|
||||
}
|
||||
|
||||
return expanded
|
||||
}
|
||||
|
||||
func addNativeSSHRule(base FirewallRule, expanded []*FirewallRule) []*FirewallRule {
|
||||
shouldAdd := false
|
||||
for _, fr := range expanded {
|
||||
if isPortInRule(nativeSSHPortString, 22022, fr) {
|
||||
return expanded
|
||||
}
|
||||
if isPortInRule(defaultSSHPortString, 22, fr) {
|
||||
shouldAdd = true
|
||||
}
|
||||
}
|
||||
if !shouldAdd {
|
||||
return expanded
|
||||
}
|
||||
|
||||
fr := base
|
||||
fr.Port = nativeSSHPortString
|
||||
return append(expanded, &fr)
|
||||
}
|
||||
|
||||
func isPortInRule(portString string, portInt uint16, rule *FirewallRule) bool {
|
||||
return rule.Port == portString || (rule.PortRange.Start <= portInt && portInt <= rule.PortRange.End)
|
||||
}
|
||||
|
||||
func shouldCheckRulesForNativeSSH(supportsNative bool, rule *PolicyRule, peer *ComponentPeer) bool {
|
||||
return supportsNative && peer.SSHEnabled && peer.ServerSSHAllowed && rule.Protocol == PolicyRuleProtocolTCP
|
||||
}
|
||||
|
||||
func peerSupportedFirewallFeatures(peerVer string) supportedFeatures {
|
||||
if version.IsDevelopmentVersion(peerVer) {
|
||||
return supportedFeatures{true, true}
|
||||
}
|
||||
|
||||
var features supportedFeatures
|
||||
|
||||
meetMinVer, err := meetsMinVersion(firewallRuleMinNativeSSHVer, peerVer)
|
||||
features.nativeSSH = err == nil && meetMinVer
|
||||
|
||||
if features.nativeSSH {
|
||||
features.portRanges = true
|
||||
} else {
|
||||
meetMinVer, err = meetsMinVersion(firewallRuleMinPortRangesVer, peerVer)
|
||||
features.portRanges = err == nil && meetMinVer
|
||||
}
|
||||
|
||||
return features
|
||||
}
|
||||
|
||||
// meetsMinVersion is main's version.MeetsMinVersion, which does not exist at HEAD.
|
||||
func meetsMinVersion(minVer, peerVer string) (bool, error) {
|
||||
peerVer = sanitizeVersion(peerVer)
|
||||
minVer = sanitizeVersion(minVer)
|
||||
|
||||
peerNBVer, err := v.NewVersion(peerVer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
constraints, err := v.NewConstraint(">= " + minVer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return constraints.Check(peerNBVer), nil
|
||||
}
|
||||
|
||||
func sanitizeVersion(version string) string {
|
||||
parts := strings.Split(version, "-")
|
||||
return parts[0]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,220 @@
|
||||
package legacynmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/netbirdio/netbird/client/ssh/auth"
|
||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
nbroute "github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
"github.com/netbirdio/netbird/shared/netiputil"
|
||||
)
|
||||
|
||||
func ToProtocolRoutes(routes []*nbroute.Route) []*proto.Route {
|
||||
protoRoutes := make([]*proto.Route, 0, len(routes))
|
||||
for _, r := range routes {
|
||||
protoRoutes = append(protoRoutes, ToProtocolRoute(r))
|
||||
}
|
||||
return protoRoutes
|
||||
}
|
||||
|
||||
func ToProtocolRoute(route *nbroute.Route) *proto.Route {
|
||||
return &proto.Route{
|
||||
ID: string(route.ID),
|
||||
NetID: string(route.NetID),
|
||||
Network: route.Network.String(),
|
||||
Domains: route.Domains.ToPunycodeList(),
|
||||
NetworkType: int64(route.NetworkType),
|
||||
Peer: route.Peer,
|
||||
Metric: int64(route.Metric),
|
||||
Masquerade: route.Masquerade,
|
||||
KeepRoute: route.KeepRoute,
|
||||
SkipAutoApply: route.SkipAutoApply,
|
||||
}
|
||||
}
|
||||
|
||||
func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*ComponentPeer, dnsName string, includeIPv6 bool, localIsProxy 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: allowedIPs,
|
||||
SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
|
||||
Fqdn: rPeer.FQDN(dnsName),
|
||||
AgentVersion: rPeer.AgentVersion,
|
||||
LazyState: lazyStateFor(localIsProxy, rPeer),
|
||||
})
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// lazyStateFor returns the per-peer lazy override for a remote peer. Connections
|
||||
// involving an ephemeral proxy peer on either endpoint default to lazy so shared
|
||||
// proxy infrastructure is not kept permanently connected to every peer. All
|
||||
// other peers follow the account-wide flag. A future admin-facing per-peer
|
||||
// setting can return LazyStateEager here to force a peer always-active.
|
||||
func lazyStateFor(localIsProxy bool, rPeer *ComponentPeer) proto.LazyState {
|
||||
if localIsProxy || rPeer.ProxyEmbedded {
|
||||
return proto.LazyState_LazyStateLazy
|
||||
}
|
||||
return proto.LazyState_LazyStateDefault
|
||||
}
|
||||
|
||||
func buildJWTConfig(config *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow) *proto.JWTConfig {
|
||||
if config == nil || config.AuthAudience == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
issuer := strings.TrimSpace(config.AuthIssuer)
|
||||
if issuer == "" && deviceFlowConfig != nil {
|
||||
if d := deriveIssuerFromTokenEndpoint(deviceFlowConfig.ProviderConfig.TokenEndpoint); d != "" {
|
||||
issuer = d
|
||||
}
|
||||
}
|
||||
if issuer == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
keysLocation := strings.TrimSpace(config.AuthKeysLocation)
|
||||
if keysLocation == "" {
|
||||
keysLocation = strings.TrimSuffix(issuer, "/") + "/.well-known/jwks.json"
|
||||
}
|
||||
|
||||
audience := config.AuthAudience
|
||||
if config.CLIAuthAudience != "" {
|
||||
audience = config.CLIAuthAudience
|
||||
}
|
||||
|
||||
audiences := []string{config.AuthAudience}
|
||||
if config.CLIAuthAudience != "" && config.CLIAuthAudience != config.AuthAudience {
|
||||
audiences = append(audiences, config.CLIAuthAudience)
|
||||
}
|
||||
|
||||
return &proto.JWTConfig{
|
||||
Issuer: issuer,
|
||||
Audience: audience, //nolint:staticcheck
|
||||
Audiences: audiences,
|
||||
KeysLocation: keysLocation,
|
||||
}
|
||||
}
|
||||
|
||||
func toPeerConfig(peer *nbpeer.Peer, network *Network, dnsName string, settings *types.Settings, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, enableSSH bool, forceRoutingPeerDNS bool) *proto.PeerConfig {
|
||||
netmask, _ := network.Net.Mask.Size()
|
||||
fqdn := peer.FQDN(dnsName)
|
||||
|
||||
sshConfig := &proto.SSHConfig{
|
||||
SshEnabled: peer.SSHEnabled || enableSSH,
|
||||
}
|
||||
|
||||
if sshConfig.SshEnabled {
|
||||
sshConfig.JwtConfig = buildJWTConfig(httpConfig, deviceFlowConfig)
|
||||
}
|
||||
|
||||
peerConfig := &proto.PeerConfig{
|
||||
Address: fmt.Sprintf("%s/%d", peer.IP.String(), netmask),
|
||||
SshConfig: sshConfig,
|
||||
Fqdn: fqdn,
|
||||
RoutingPeerDnsResolutionEnabled: settings.RoutingPeerDNSResolutionEnabled || peer.ProxyMeta.Embedded || forceRoutingPeerDNS,
|
||||
LazyConnectionEnabled: settings.LazyConnectionEnabled,
|
||||
AutoUpdate: &proto.AutoUpdateSettings{
|
||||
Version: settings.AutoUpdateVersion,
|
||||
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)
|
||||
if b, err := netiputil.EncodePrefix(v6Prefix); err == nil {
|
||||
peerConfig.AddressV6 = b
|
||||
}
|
||||
}
|
||||
|
||||
return peerConfig
|
||||
}
|
||||
|
||||
// ToProtoNetworkMap mirrors main's ToSyncResponse, restricted to the
|
||||
// proto.NetworkMap it produces. SyncResponse-level fields (NetbirdConfig,
|
||||
// Checks, the deprecated top-level RemotePeers) are omitted — they are not part
|
||||
// of the equivalence surface. PeerConfig is included because proto.NetworkMap
|
||||
// carries it, and it is where main's ForceRoutingPeerDNSResolution surfaces.
|
||||
func ToProtoNetworkMap(
|
||||
ctx context.Context,
|
||||
peer *nbpeer.Peer,
|
||||
nm *NetworkMap,
|
||||
dnsName string,
|
||||
settings *types.Settings,
|
||||
httpConfig *nbconfig.HttpServerConfig,
|
||||
dnsCache networkmap.DNSConfigCache,
|
||||
dnsFwdPort int64,
|
||||
) *proto.NetworkMap {
|
||||
includeIPv6 := peer.SupportsIPv6() && peer.IPv6.IsValid()
|
||||
useSourcePrefixes := peer.SupportsSourcePrefixes()
|
||||
localIsProxy := peer.ProxyMeta.Embedded
|
||||
|
||||
peerConfig := toPeerConfig(peer, nm.Network, dnsName, settings, httpConfig, nil, nm.EnableSSH, nm.ForceRoutingPeerDNSResolution)
|
||||
|
||||
pm := &proto.NetworkMap{
|
||||
Serial: nm.Network.CurrentSerial(),
|
||||
Routes: ToProtocolRoutes(nm.Routes),
|
||||
DNSConfig: networkmap.ToProtocolDNSConfig(nm.DNSConfig, dnsCache, dnsFwdPort),
|
||||
PeerConfig: peerConfig,
|
||||
}
|
||||
|
||||
remotePeers := make([]*proto.RemotePeerConfig, 0, len(nm.Peers)+len(nm.OfflinePeers))
|
||||
remotePeers = AppendRemotePeerConfig(remotePeers, nm.Peers, dnsName, includeIPv6, localIsProxy)
|
||||
pm.RemotePeers = remotePeers
|
||||
pm.RemotePeersIsEmpty = len(remotePeers) == 0
|
||||
|
||||
pm.OfflinePeers = AppendRemotePeerConfig(nil, nm.OfflinePeers, dnsName, includeIPv6, localIsProxy)
|
||||
|
||||
firewallRules := networkmap.ToProtocolFirewallRules(nm.FirewallRules, includeIPv6, useSourcePrefixes)
|
||||
pm.FirewallRules = firewallRules
|
||||
pm.FirewallRulesIsEmpty = len(firewallRules) == 0
|
||||
|
||||
routesFirewallRules := networkmap.ToProtocolRoutesFirewallRules(nm.RoutesFirewallRules)
|
||||
pm.RoutesFirewallRules = routesFirewallRules
|
||||
pm.RoutesFirewallRulesIsEmpty = len(routesFirewallRules) == 0
|
||||
|
||||
if nm.ForwardingRules != nil {
|
||||
forwardingRules := make([]*proto.ForwardingRule, 0, len(nm.ForwardingRules))
|
||||
for _, rule := range nm.ForwardingRules {
|
||||
forwardingRules = append(forwardingRules, rule.ToProto())
|
||||
}
|
||||
pm.ForwardingRules = forwardingRules
|
||||
}
|
||||
|
||||
if nm.AuthorizedUsers != nil {
|
||||
hashedUsers, machineUsers := networkmap.BuildAuthorizedUsersProto(ctx, nm.AuthorizedUsers)
|
||||
userIDClaim := auth.DefaultUserIDClaim
|
||||
if httpConfig != nil && httpConfig.AuthUserIDClaim != "" {
|
||||
userIDClaim = httpConfig.AuthUserIDClaim
|
||||
}
|
||||
pm.SshAuth = &proto.SSHAuth{AuthorizedUsers: hashedUsers, MachineUsers: machineUsers, UserIDClaim: userIDClaim}
|
||||
}
|
||||
|
||||
return pm
|
||||
}
|
||||
|
||||
func deriveIssuerFromTokenEndpoint(tokenEndpoint string) string {
|
||||
if tokenEndpoint == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
u, err := url.Parse(tokenEndpoint)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s://%s/", u.Scheme, u.Host)
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package legacynmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
sharedtypes "github.com/netbirdio/netbird/shared/management/types"
|
||||
)
|
||||
|
||||
// SynthesizeProxyPolicies is main's Account.InjectProxyPolicies, frozen. On
|
||||
// main the network-map controller called it on the account before computing,
|
||||
// so a comparison that starts from the account has to apply it too. It returns
|
||||
// the policies instead of appending them, so the caller can measure the legacy
|
||||
// path without mutating the account the other paths share.
|
||||
func SynthesizeProxyPolicies(a *Account) []*Policy {
|
||||
if len(a.Services) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
proxyPeersByCluster := a.GetProxyPeers()
|
||||
if len(proxyPeersByCluster) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var out []*Policy
|
||||
for _, svc := range a.Services {
|
||||
if svc == nil || !svc.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
proxyPeers := proxyPeersByCluster[svc.ProxyCluster]
|
||||
for _, target := range svc.Targets {
|
||||
if target == nil || !target.Enabled {
|
||||
continue
|
||||
}
|
||||
port, ok := legacyTargetPort(target)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
path := ""
|
||||
if target.Path != nil {
|
||||
path = *target.Path
|
||||
}
|
||||
for _, proxyPeer := range proxyPeers {
|
||||
out = append(out, legacyProxyPolicy(svc, target, proxyPeer, port, path))
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, legacyPrivateServicePolicies(a, svc, proxyPeers)...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func legacyPrivateServicePolicies(a *Account, svc *service.Service, proxyPeers []*nbpeer.Peer) []*Policy {
|
||||
if !svc.Private || len(svc.AccessGroups) == 0 || len(proxyPeers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sources := make([]string, 0, len(svc.AccessGroups))
|
||||
for _, groupID := range svc.AccessGroups {
|
||||
if _, ok := a.Groups[groupID]; ok {
|
||||
sources = append(sources, groupID)
|
||||
}
|
||||
}
|
||||
if len(sources) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]*Policy, 0, len(proxyPeers))
|
||||
for _, proxyPeer := range proxyPeers {
|
||||
policyID := fmt.Sprintf("private-access-%s-%s", svc.ID, proxyPeer.ID)
|
||||
out = append(out, &Policy{
|
||||
ID: policyID,
|
||||
Name: fmt.Sprintf("Private Access to %s", svc.Name),
|
||||
Enabled: true,
|
||||
Rules: []*PolicyRule{
|
||||
{
|
||||
ID: policyID,
|
||||
PolicyID: policyID,
|
||||
Name: fmt.Sprintf("Allow access groups to reach %s", svc.Name),
|
||||
Enabled: true,
|
||||
Sources: append([]string(nil), sources...),
|
||||
DestinationResource: Resource{
|
||||
ID: proxyPeer.ID,
|
||||
Type: ResourceTypePeer,
|
||||
},
|
||||
Bidirectional: false,
|
||||
Protocol: PolicyRuleProtocolTCP,
|
||||
Action: PolicyTrafficActionAccept,
|
||||
PortRanges: []RulePortRange{
|
||||
{Start: 80, End: 80},
|
||||
{Start: 443, End: 443},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func legacyProxyPolicy(svc *service.Service, target *service.Target, proxyPeer *nbpeer.Peer, port uint16, path string) *Policy {
|
||||
policyID := fmt.Sprintf("proxy-access-%s-%s-%s", svc.ID, proxyPeer.ID, path)
|
||||
|
||||
protocol := PolicyRuleProtocolTCP
|
||||
if svc.Mode == service.ModeUDP {
|
||||
protocol = sharedtypes.PolicyRuleProtocolUDP
|
||||
}
|
||||
|
||||
return &Policy{
|
||||
ID: policyID,
|
||||
Name: fmt.Sprintf("Proxy Access to %s", svc.Name),
|
||||
Enabled: true,
|
||||
Rules: []*PolicyRule{
|
||||
{
|
||||
ID: policyID,
|
||||
PolicyID: policyID,
|
||||
Name: fmt.Sprintf("Allow access to %s", svc.Name),
|
||||
Enabled: true,
|
||||
SourceResource: Resource{
|
||||
ID: proxyPeer.ID,
|
||||
Type: ResourceTypePeer,
|
||||
},
|
||||
DestinationResource: Resource{
|
||||
ID: target.TargetId,
|
||||
Type: sharedtypes.ResourceType(target.TargetType),
|
||||
},
|
||||
Bidirectional: false,
|
||||
Protocol: protocol,
|
||||
Action: PolicyTrafficActionAccept,
|
||||
PortRanges: []RulePortRange{{Start: port, End: port}},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func legacyTargetPort(target *service.Target) (uint16, bool) {
|
||||
if target.Port != 0 {
|
||||
return target.Port, true
|
||||
}
|
||||
|
||||
switch target.Protocol {
|
||||
case "https", "tls":
|
||||
return 443, true
|
||||
case "http":
|
||||
return 80, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/c-robinson/iplib"
|
||||
"github.com/rs/xid"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/util"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
)
|
||||
|
||||
const (
|
||||
// SubnetSize is a size of the subnet of the global network, e.g. 100.77.0.0/16
|
||||
SubnetSize = 16
|
||||
// NetSize is a global network size 100.64.0.0/10
|
||||
NetSize = 10
|
||||
|
||||
// IPv6SubnetSize is the prefix length of per-account IPv6 subnets.
|
||||
// Each account gets a /64 from its unique /48 ULA prefix.
|
||||
IPv6SubnetSize = 64
|
||||
)
|
||||
|
||||
type Network struct {
|
||||
Identifier string `json:"id"`
|
||||
Net net.IPNet `gorm:"serializer:json"`
|
||||
// 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
|
||||
|
||||
Mu sync.Mutex `json:"-" gorm:"-"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
intn := util.RandIntn(len(sub))
|
||||
|
||||
return &Network{
|
||||
Identifier: xid.New().String(),
|
||||
Net: sub[intn].IPNet,
|
||||
NetV6: AllocateIPv6Subnet(),
|
||||
Dns: "",
|
||||
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() net.IPNet {
|
||||
ip := make(net.IP, 16)
|
||||
ip[0] = 0xfd
|
||||
// Bytes 1-5: 40-bit random Global ID, bytes 6-7: 16-bit random Subnet ID
|
||||
if _, err := rand.Read(ip[1:8]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return net.IPNet{
|
||||
IP: ip,
|
||||
Mask: net.CIDRMask(IPv6SubnetSize, 128),
|
||||
}
|
||||
}
|
||||
|
||||
// IncSerial increments Serial by 1 reflecting that the network state has been changed
|
||||
func (n *Network) IncSerial() {
|
||||
n.Mu.Lock()
|
||||
defer n.Mu.Unlock()
|
||||
n.Serial++
|
||||
}
|
||||
|
||||
// CurrentSerial returns the Network.Serial of the network (latest state id)
|
||||
func (n *Network) CurrentSerial() uint64 {
|
||||
n.Mu.Lock()
|
||||
defer n.Mu.Unlock()
|
||||
return n.Serial
|
||||
}
|
||||
|
||||
func (n *Network) Copy() *Network {
|
||||
n.Mu.Lock()
|
||||
defer n.Mu.Unlock()
|
||||
return &Network{
|
||||
Identifier: n.Identifier,
|
||||
Net: n.Net,
|
||||
NetV6: n.NetV6,
|
||||
Dns: n.Dns,
|
||||
Serial: n.Serial,
|
||||
}
|
||||
}
|
||||
|
||||
// validateIPv4Prefix ensures the prefix is an IPv4 network with assignable host addresses.
|
||||
func validateIPv4Prefix(prefix netip.Prefix) error {
|
||||
if !prefix.IsValid() || !prefix.Addr().Is4() || prefix.Bits() < 1 || prefix.Bits() >= 31 {
|
||||
return fmt.Errorf("invalid IPv4 subnet: %s", prefix.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if err := validateIPv4Prefix(prefix); err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
|
||||
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)
|
||||
taken[baseIP] = struct{}{} // reserve network IP
|
||||
taken[baseIP+totalIPs-1] = struct{}{} // reserve broadcast IP
|
||||
|
||||
for _, ip := range takenIps {
|
||||
if !ip.Is4() {
|
||||
continue
|
||||
}
|
||||
ab := ip.As4()
|
||||
taken[binary.BigEndian.Uint32(ab[:])] = struct{}{}
|
||||
}
|
||||
|
||||
maxAttempts := (int(totalIPs) - len(taken)) / 100
|
||||
|
||||
for i := 0; i < maxAttempts; i++ {
|
||||
offset := uint32(util.RandIntn(int(totalIPs-2))) + 1
|
||||
candidate := baseIP + offset
|
||||
if _, exists := taken[candidate]; !exists {
|
||||
return uint32ToIP(candidate), nil
|
||||
}
|
||||
}
|
||||
|
||||
for offset := uint32(1); offset < totalIPs-1; offset++ {
|
||||
candidate := baseIP + offset
|
||||
if _, exists := taken[candidate]; !exists {
|
||||
return uint32ToIP(candidate), nil
|
||||
}
|
||||
}
|
||||
|
||||
return netip.Addr{}, status.Errorf(status.PreconditionFailed, "network %s is out of IPs", prefix.String())
|
||||
}
|
||||
|
||||
// AllocateRandomPeerIP picks a random available IP from a netip.Prefix.
|
||||
func AllocateRandomPeerIP(prefix netip.Prefix) (netip.Addr, error) {
|
||||
if err := validateIPv4Prefix(prefix); err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
|
||||
b := prefix.Masked().Addr().As4()
|
||||
baseIP := binary.BigEndian.Uint32(b[:])
|
||||
hostBits := 32 - prefix.Bits()
|
||||
totalIPs := uint32(1 << hostBits)
|
||||
|
||||
offset := uint32(util.RandIntn(int(totalIPs-2))) + 1
|
||||
|
||||
candidate := baseIP + offset
|
||||
return uint32ToIP(candidate), nil
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
ip := prefix.Addr().As16()
|
||||
|
||||
// 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
|
||||
|
||||
var rnd [16]byte
|
||||
if _, err := rand.Read(rnd[firstHostByte:]); err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
|
||||
if partialBits > 0 {
|
||||
// Keep the network bits in the partial byte, randomize the rest
|
||||
hostMask := byte(0xff >> partialBits)
|
||||
ip[firstHostByte] = (ip[firstHostByte] & ^hostMask) | (rnd[firstHostByte] & hostMask)
|
||||
firstHostByte++
|
||||
}
|
||||
|
||||
// Randomize remaining full host bytes
|
||||
for i := firstHostByte; i < 16; i++ {
|
||||
ip[i] = rnd[i]
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
func generateIPs(ipNet *net.IPNet, exclusions map[string]struct{}) ([]net.IP, int) {
|
||||
|
||||
var ips []net.IP
|
||||
for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); incIP(ip) {
|
||||
if _, ok := exclusions[ip.String()]; !ok && ip[3] != 0 {
|
||||
ips = append(ips, copyIP(ip))
|
||||
}
|
||||
}
|
||||
|
||||
// remove network address, broadcast and Fake DNS resolver address
|
||||
lenIPs := len(ips)
|
||||
switch {
|
||||
case lenIPs < 2:
|
||||
return ips, lenIPs
|
||||
case lenIPs < 3:
|
||||
return ips[1 : len(ips)-1], lenIPs - 2
|
||||
default:
|
||||
return ips[1 : len(ips)-2], lenIPs - 3
|
||||
}
|
||||
}
|
||||
|
||||
func copyIP(ip net.IP) net.IP {
|
||||
dup := make(net.IP, len(ip))
|
||||
copy(dup, ip)
|
||||
return dup
|
||||
}
|
||||
|
||||
func incIP(ip net.IP) {
|
||||
for j := len(ip) - 1; j >= 0; j-- {
|
||||
ip[j]++
|
||||
if ip[j] > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewNetwork(t *testing.T) {
|
||||
network := NewNetwork()
|
||||
|
||||
// generated net should be a subnet of a larger 100.64.0.0/10 net
|
||||
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
|
||||
assert.Equal(t, ipNet.Contains(network.Net.IP), true)
|
||||
}
|
||||
|
||||
func TestAllocatePeerIP(t *testing.T) {
|
||||
prefix := netip.MustParsePrefix("100.64.0.0/24")
|
||||
var ips []netip.Addr
|
||||
for i := 0; i < 252; i++ {
|
||||
ip, err := AllocatePeerIP(prefix, ips)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
|
||||
assert.Len(t, ips, 252)
|
||||
|
||||
uniq := make(map[string]struct{})
|
||||
for _, ip := range ips {
|
||||
if _, ok := uniq[ip.String()]; !ok {
|
||||
uniq[ip.String()] = struct{}{}
|
||||
} else {
|
||||
t.Errorf("found duplicate IP %s", ip.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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(prefix, ips)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify IP is within the correct range
|
||||
if !prefix.Contains(ip) {
|
||||
t.Errorf("allocated IP %s is not within network %s", ip.String(), prefix.String())
|
||||
}
|
||||
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
|
||||
assert.Len(t, ips, 30)
|
||||
|
||||
// Verify all IPs are unique
|
||||
uniq := make(map[string]struct{})
|
||||
for _, ip := range ips {
|
||||
if _, ok := uniq[ip.String()]; !ok {
|
||||
uniq[ip.String()] = struct{}{}
|
||||
} else {
|
||||
t.Errorf("found duplicate IP %s", ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Try to allocate one more IP - should fail as network is full
|
||||
_, err := AllocatePeerIP(prefix, ips)
|
||||
if err == nil {
|
||||
t.Error("expected error when network is full, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllocatePeerIPVariousCIDRs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
cidr string
|
||||
expectedUsable int
|
||||
}{
|
||||
{"/30 network", "192.168.1.0/30", 2}, // 4 total - 2 reserved = 2 usable
|
||||
{"/29 network", "192.168.1.0/29", 6}, // 8 total - 2 reserved = 6 usable
|
||||
{"/28 network", "192.168.1.0/28", 14}, // 16 total - 2 reserved = 14 usable
|
||||
{"/27 network", "192.168.1.0/27", 30}, // 32 total - 2 reserved = 30 usable
|
||||
{"/26 network", "192.168.1.0/26", 62}, // 64 total - 2 reserved = 62 usable
|
||||
{"/25 network", "192.168.1.0/25", 126}, // 128 total - 2 reserved = 126 usable
|
||||
{"/16 network", "10.0.0.0/16", 65534}, // 65536 total - 2 reserved = 65534 usable
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
prefix, err := netip.ParsePrefix(tc.cidr)
|
||||
require.NoError(t, err)
|
||||
prefix = prefix.Masked()
|
||||
|
||||
var ips []netip.Addr
|
||||
|
||||
// For larger networks, test only a subset to avoid long test runs
|
||||
testCount := tc.expectedUsable
|
||||
if testCount > 1000 {
|
||||
testCount = 1000
|
||||
}
|
||||
|
||||
// Allocate IPs and verify they're within the correct range
|
||||
for i := 0; i < testCount; i++ {
|
||||
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, prefix.Contains(ip), "allocated IP %s is not within network %s", ip.String(), prefix.String())
|
||||
|
||||
// Verify IP is not network or broadcast address
|
||||
networkAddr := prefix.Masked().Addr()
|
||||
hostBits := 32 - prefix.Bits()
|
||||
b := networkAddr.As4()
|
||||
baseIP := binary.BigEndian.Uint32(b[:])
|
||||
broadcastIP := uint32ToIP(baseIP + (1 << hostBits) - 1)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
assert.Len(t, ips, testCount)
|
||||
|
||||
// Verify all IPs are unique
|
||||
uniq := make(map[string]struct{})
|
||||
for _, ip := range ips {
|
||||
ipStr := ip.String()
|
||||
assert.NotContains(t, uniq, ipStr, "found duplicate IP %s", ipStr)
|
||||
uniq[ipStr] = struct{}{}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllocateIPv4InvalidPrefixes(t *testing.T) {
|
||||
prefixes := []netip.Prefix{
|
||||
{},
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
netip.MustParsePrefix("192.168.1.0/31"),
|
||||
netip.MustParsePrefix("192.168.1.1/32"),
|
||||
netip.MustParsePrefix("fd12:3456:7890:abcd::/64"),
|
||||
}
|
||||
|
||||
for _, prefix := range prefixes {
|
||||
t.Run(prefix.String(), func(t *testing.T) {
|
||||
_, err := AllocatePeerIP(prefix, nil)
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = AllocateRandomPeerIP(prefix)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllocatePeerIPIgnoresNonIPv4TakenIPs(t *testing.T) {
|
||||
prefix := netip.MustParsePrefix("192.168.1.0/29")
|
||||
|
||||
ip, err := AllocatePeerIP(prefix, []netip.Addr{netip.MustParseAddr("fd12:3456:7890:abcd::1")})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, prefix.Contains(ip))
|
||||
}
|
||||
|
||||
func TestGenerateIPs(t *testing.T) {
|
||||
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 255, 255, 0}}
|
||||
ips, ipsLen := generateIPs(&ipNet, map[string]struct{}{"100.64.0.0": {}})
|
||||
if ipsLen != 252 {
|
||||
t.Errorf("expected 252 ips, got %d", len(ips))
|
||||
return
|
||||
}
|
||||
if ips[len(ips)-1].String() != "100.64.0.253" {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -388,7 +388,7 @@ func TestComponents_NetworkSerial(t *testing.T) {
|
||||
account.Network.Serial = 42
|
||||
nm := componentsNetworkMap(account, "peer-0", validatedPeers)
|
||||
require.NotNil(t, nm)
|
||||
assert.Equal(t, uint64(42), nm.Network.Serial, "network serial should match")
|
||||
assert.Equal(t, uint64(42), nm.Network.CurrentSerial(), "network serial should match")
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -812,7 +812,7 @@ func TestComponents_AllPeersGetValidMaps(t *testing.T) {
|
||||
}
|
||||
nm := componentsNetworkMap(account, peerID, validatedPeers)
|
||||
require.NotNil(t, nm, "network map should not be nil for %s", peerID)
|
||||
assert.Equal(t, account.Network.Serial, nm.Network.Serial, "serial mismatch for %s", peerID)
|
||||
assert.Equal(t, account.Network.Serial, nm.Network.CurrentSerial(), "serial mismatch for %s", peerID)
|
||||
assert.NotEmpty(t, nm.Peers, "validated peer %s should see other peers", peerID)
|
||||
}
|
||||
}
|
||||
@@ -833,7 +833,7 @@ func TestComponents_LargeScaleMapGeneration(t *testing.T) {
|
||||
require.NotNil(t, nm, "network map should not be nil for %s", peerID)
|
||||
assert.NotEmpty(t, nm.Peers, "peer %s should see other peers at scale", peerID)
|
||||
assert.NotEmpty(t, nm.Routes, "peer %s should have routes at scale", peerID)
|
||||
assert.Equal(t, account.Network.Serial, nm.Network.Serial, "serial mismatch for %s", peerID)
|
||||
assert.Equal(t, account.Network.Serial, nm.Network.CurrentSerial(), "serial mismatch for %s", peerID)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -875,6 +875,89 @@ func TestComponents_PeerAsSourceResource(t *testing.T) {
|
||||
assert.True(t, has443, "peer-0 as source resource should have port 443 rule")
|
||||
}
|
||||
|
||||
func hasFirewallRuleTo(nm *types.NetworkMap, peerIP, port string) bool {
|
||||
for _, rule := range nm.FirewallRules {
|
||||
if rule.PeerIP == peerIP && rule.Port == port {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TestComponents_PeerAsSourceResource_PostureChecks verifies that a directly referenced
|
||||
// source peer is gated by the policy's posture checks like a member of a group holding only
|
||||
// that peer: peer-1 (0.25.0) fails the 0.26.0 minimum, peer-2 (0.40.0) passes.
|
||||
func TestComponents_PeerAsSourceResource_PostureChecks(t *testing.T) {
|
||||
account, validatedPeers := scalableTestAccountWithoutDefaultPolicy(20, 2)
|
||||
|
||||
for _, sourcePeerID := range []string{"peer-1", "peer-2"} {
|
||||
account.Policies = append(account.Policies, &types.Policy{
|
||||
ID: "policy-peer-src-" + sourcePeerID, Name: "Peer Source " + sourcePeerID, Enabled: true, AccountID: "test-account",
|
||||
SourcePostureChecks: []string{"posture-check-ver"},
|
||||
Rules: []*types.PolicyRule{{
|
||||
ID: "rule-peer-src-" + sourcePeerID, Enabled: true,
|
||||
Action: types.PolicyTrafficActionAccept,
|
||||
Protocol: types.PolicyRuleProtocolTCP,
|
||||
Bidirectional: true,
|
||||
Ports: []string{"9443"},
|
||||
SourceResource: types.Resource{ID: sourcePeerID, Type: types.ResourceTypePeer},
|
||||
Destinations: []string{"group-0"},
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
nm0 := componentsNetworkMap(account, "peer-0", validatedPeers)
|
||||
require.NotNil(t, nm0)
|
||||
assert.False(t, hasFirewallRuleTo(nm0, "100.64.0.1", "9443"), "destination must not see the direct source peer failing the posture check")
|
||||
assert.True(t, hasFirewallRuleTo(nm0, "100.64.0.2", "9443"), "destination must see the direct source peer passing the posture check")
|
||||
|
||||
nm1 := componentsNetworkMap(account, "peer-1", validatedPeers)
|
||||
require.NotNil(t, nm1)
|
||||
assert.False(t, hasFirewallRuleTo(nm1, "100.64.0.0", "9443"), "a direct source peer failing the posture check gets no policy connectivity")
|
||||
|
||||
nm2 := componentsNetworkMap(account, "peer-2", validatedPeers)
|
||||
require.NotNil(t, nm2)
|
||||
assert.True(t, hasFirewallRuleTo(nm2, "100.64.0.0", "9443"), "a direct source peer passing the posture check gets policy connectivity")
|
||||
}
|
||||
|
||||
// TestComponents_PeerAsResource_Unvalidated verifies that a directly referenced peer is
|
||||
// subject to approval like a group member, whether it is the rule's source or destination.
|
||||
func TestComponents_PeerAsResource_Unvalidated(t *testing.T) {
|
||||
account, validatedPeers := scalableTestAccountWithoutDefaultPolicy(20, 2)
|
||||
delete(validatedPeers, "peer-2")
|
||||
|
||||
account.Policies = append(account.Policies,
|
||||
&types.Policy{
|
||||
ID: "policy-unval-src", Name: "Unvalidated Source", Enabled: true, AccountID: "test-account",
|
||||
Rules: []*types.PolicyRule{{
|
||||
ID: "rule-unval-src", Enabled: true,
|
||||
Action: types.PolicyTrafficActionAccept, Protocol: types.PolicyRuleProtocolTCP, Bidirectional: true,
|
||||
Ports: []string{"9443"},
|
||||
SourceResource: types.Resource{ID: "peer-2", Type: types.ResourceTypePeer},
|
||||
Destinations: []string{"group-0"},
|
||||
}},
|
||||
},
|
||||
&types.Policy{
|
||||
ID: "policy-unval-dst", Name: "Unvalidated Destination", Enabled: true, AccountID: "test-account",
|
||||
Rules: []*types.PolicyRule{{
|
||||
ID: "rule-unval-dst", Enabled: true,
|
||||
Action: types.PolicyTrafficActionAccept, Protocol: types.PolicyRuleProtocolTCP, Bidirectional: true,
|
||||
Ports: []string{"9444"},
|
||||
Sources: []string{"group-0"},
|
||||
DestinationResource: types.Resource{ID: "peer-2", Type: types.ResourceTypePeer},
|
||||
}},
|
||||
},
|
||||
)
|
||||
|
||||
nm0 := componentsNetworkMap(account, "peer-0", validatedPeers)
|
||||
require.NotNil(t, nm0)
|
||||
assert.False(t, hasFirewallRuleTo(nm0, "100.64.0.2", "9443"), "an unvalidated direct source peer must not be admitted")
|
||||
assert.False(t, hasFirewallRuleTo(nm0, "100.64.0.2", "9444"), "an unvalidated direct destination peer must not be admitted")
|
||||
for _, p := range nm0.Peers {
|
||||
assert.NotEqual(t, "peer-2", p.ID, "an unvalidated direct peer must not be shipped as a remote peer")
|
||||
}
|
||||
}
|
||||
|
||||
// TestComponents_PeerAsDestinationResource verifies that a policy with DestinationResource.Type=Peer
|
||||
// targets only that specific peer as the destination.
|
||||
func TestComponents_PeerAsDestinationResource(t *testing.T) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/posture"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
func networkMapFromComponents(t *testing.T, account *types.Account, peerID string, validatedPeers map[string]struct{}) *types.NetworkMap {
|
||||
@@ -49,7 +50,7 @@ func allPeersValidated(account *types.Account, excludePeerIDs ...string) map[str
|
||||
return validated
|
||||
}
|
||||
|
||||
func peerIDs(peers []*types.ComponentPeer) []string {
|
||||
func peerIDs(peers []*nmdata.Peer) []string {
|
||||
ids := make([]string, len(peers))
|
||||
for i, p := range peers {
|
||||
ids[i] = p.ID
|
||||
@@ -625,7 +626,7 @@ func TestNetworkMapComponents_DomainNetworkResource(t *testing.T) {
|
||||
|
||||
var hasDomainRoute bool
|
||||
for _, r := range nm.Routes {
|
||||
if r.NetworkType == route.DomainNetwork && len(r.Domains) > 0 && r.Domains[0].SafeString() == "api.example.com" {
|
||||
if r.NetworkType == int(route.DomainNetwork) && len(r.Domains) > 0 && r.Domains[0].SafeString() == "api.example.com" {
|
||||
hasDomainRoute = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func BenchmarkNetworkMapWireEncode(b *testing.B) {
|
||||
|
||||
// Pre-encode once so the size metric is identical for every run inside
|
||||
// the same scale; the b.Loop call only re-runs encode + Marshal.
|
||||
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0)
|
||||
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, networkMap, "netbird.cloud", nil, dnsCache, types.TwinAccountSettings(settings), nil, nil, 0)
|
||||
legacyBytes, err := goproto.Marshal(legacyResp.NetworkMap)
|
||||
if err != nil {
|
||||
b.Fatalf("marshal legacy networkmap: %v", err)
|
||||
@@ -88,7 +88,7 @@ func BenchmarkNetworkMapWireEncode(b *testing.B) {
|
||||
b.ReportMetric(float64(len(legacyBytes)), "bytes/msg")
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
resp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0)
|
||||
resp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, networkMap, "netbird.cloud", nil, dnsCache, types.TwinAccountSettings(settings), nil, nil, 0)
|
||||
if _, err := goproto.Marshal(resp.NetworkMap); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -135,7 +135,7 @@ func BenchmarkNetworkMapWireSize(b *testing.B) {
|
||||
dnsCache := &cache.DNSConfigCache{}
|
||||
settings := &types.Settings{}
|
||||
|
||||
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0)
|
||||
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, networkMap, "netbird.cloud", nil, dnsCache, types.TwinAccountSettings(settings), nil, nil, 0)
|
||||
legacyBytes, err := goproto.Marshal(legacyResp.NetworkMap)
|
||||
if err != nil {
|
||||
b.Fatalf("marshal legacy networkmap: %v", err)
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestNetworkMapWireBreakdown(t *testing.T) {
|
||||
dnsCache := &cache.DNSConfigCache{}
|
||||
settings := &types.Settings{}
|
||||
|
||||
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0)
|
||||
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, networkMap, "netbird.cloud", nil, dnsCache, types.TwinAccountSettings(settings), nil, nil, 0)
|
||||
legacyTotal := mustMarshalSize(t, legacyResp.NetworkMap)
|
||||
|
||||
envelope := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package types
|
||||
|
||||
const (
|
||||
// PolicyRuleFlowDirect allows traffic from source to destination
|
||||
PolicyRuleFlowDirect = PolicyRuleDirection("direct")
|
||||
// PolicyRuleFlowBidirect allows traffic to both directions
|
||||
PolicyRuleFlowBidirect = PolicyRuleDirection("bidirect")
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultRuleName is a name for the Default rule that is created for every account
|
||||
DefaultRuleName = "Default"
|
||||
// DefaultRuleDescription is a description for the Default rule that is created for every account
|
||||
DefaultRuleDescription = "This is a default rule that allows connections between all the resources"
|
||||
// DefaultPolicyName is a name for the Default policy that is created for every account
|
||||
DefaultPolicyName = "Default"
|
||||
// DefaultPolicyDescription is a description for the Default policy that is created for every account
|
||||
DefaultPolicyDescription = "This is a default policy that allows connections between all the resources"
|
||||
)
|
||||
|
||||
// PolicyUpdateOperation operation object with type and values to be applied
|
||||
type PolicyUpdateOperation struct {
|
||||
Type PolicyUpdateOperationType
|
||||
Values []string
|
||||
}
|
||||
|
||||
// Policy of the Rego query
|
||||
type Policy struct {
|
||||
// ID of the policy'
|
||||
ID string `gorm:"primaryKey"`
|
||||
|
||||
PublicID string `json:"-" gorm:"index"`
|
||||
|
||||
// AccountID is a reference to Account that this object belongs
|
||||
AccountID string `json:"-" gorm:"index"`
|
||||
|
||||
// Name of the Policy
|
||||
Name string
|
||||
|
||||
// Description of the policy visible in the UI
|
||||
Description string
|
||||
|
||||
// Enabled status of the policy
|
||||
Enabled bool
|
||||
|
||||
// Rules of the policy
|
||||
Rules []*PolicyRule `gorm:"foreignKey:PolicyID;references:id;constraint:OnDelete:CASCADE;"`
|
||||
|
||||
// SourcePostureChecks are ID references to Posture checks for policy source groups
|
||||
SourcePostureChecks []string `gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
// Copy returns a copy of the policy.
|
||||
func (p *Policy) Copy() *Policy {
|
||||
c := &Policy{
|
||||
ID: p.ID,
|
||||
AccountID: p.AccountID,
|
||||
PublicID: p.PublicID,
|
||||
Name: p.Name,
|
||||
Description: p.Description,
|
||||
Enabled: p.Enabled,
|
||||
Rules: make([]*PolicyRule, len(p.Rules)),
|
||||
SourcePostureChecks: make([]string, len(p.SourcePostureChecks)),
|
||||
}
|
||||
for i, r := range p.Rules {
|
||||
c.Rules[i] = r.Copy()
|
||||
}
|
||||
copy(c.SourcePostureChecks, p.SourcePostureChecks)
|
||||
return c
|
||||
}
|
||||
|
||||
func (p *Policy) Equal(other *Policy) bool {
|
||||
if p == nil || other == nil {
|
||||
return p == other
|
||||
}
|
||||
|
||||
if p.ID != other.ID ||
|
||||
p.AccountID != other.AccountID ||
|
||||
p.Name != other.Name ||
|
||||
p.Description != other.Description ||
|
||||
p.Enabled != other.Enabled {
|
||||
return false
|
||||
}
|
||||
|
||||
if !stringSlicesEqualUnordered(p.SourcePostureChecks, other.SourcePostureChecks) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(p.Rules) != len(other.Rules) {
|
||||
return false
|
||||
}
|
||||
|
||||
otherRules := make(map[string]*PolicyRule, len(other.Rules))
|
||||
for _, r := range other.Rules {
|
||||
otherRules[r.ID] = r
|
||||
}
|
||||
for _, r := range p.Rules {
|
||||
otherRule, ok := otherRules[r.ID]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !r.Equal(otherRule) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// EventMeta returns activity event meta related to this policy
|
||||
func (p *Policy) EventMeta() map[string]any {
|
||||
return map[string]any{"name": p.Name}
|
||||
}
|
||||
|
||||
// UpgradeAndFix different version of policies to latest version
|
||||
func (p *Policy) UpgradeAndFix() {
|
||||
for _, r := range p.Rules {
|
||||
// start migrate from version v0.20.3
|
||||
if r.Protocol == "" {
|
||||
r.Protocol = PolicyRuleProtocolALL
|
||||
}
|
||||
if r.Protocol == PolicyRuleProtocolALL && !r.Bidirectional {
|
||||
r.Bidirectional = true
|
||||
}
|
||||
// -- v0.20.4
|
||||
}
|
||||
}
|
||||
|
||||
// RuleGroups returns a list of all groups referenced in the policy's rules,
|
||||
// including sources and destinations.
|
||||
func (p *Policy) RuleGroups() []string {
|
||||
groups := make([]string, 0)
|
||||
for _, rule := range p.Rules {
|
||||
groups = append(groups, rule.Sources...)
|
||||
groups = append(groups, rule.Destinations...)
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
// SourceGroups returns a slice of all unique source groups referenced in the policy's rules.
|
||||
func (p *Policy) SourceGroups() []string {
|
||||
if len(p.Rules) == 1 {
|
||||
return p.Rules[0].Sources
|
||||
}
|
||||
groups := make(map[string]struct{}, len(p.Rules))
|
||||
for _, rule := range p.Rules {
|
||||
for _, source := range rule.Sources {
|
||||
groups[source] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
groupIDs := make([]string, 0, len(groups))
|
||||
for groupID := range groups {
|
||||
groupIDs = append(groupIDs, groupID)
|
||||
}
|
||||
|
||||
return groupIDs
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"slices"
|
||||
)
|
||||
|
||||
// PolicyUpdateOperationType operation type
|
||||
type PolicyUpdateOperationType int
|
||||
|
||||
// PolicyRuleDirection direction of traffic
|
||||
type PolicyRuleDirection string
|
||||
|
||||
// PolicyRule is the metadata of the policy
|
||||
type PolicyRule struct {
|
||||
// ID of the policy rule
|
||||
ID string `gorm:"primaryKey"`
|
||||
|
||||
// PolicyID is a reference to Policy that this object belongs
|
||||
PolicyID string `json:"-" gorm:"index"`
|
||||
|
||||
// Name of the rule visible in the UI
|
||||
Name string
|
||||
|
||||
// Description of the rule visible in the UI
|
||||
Description string
|
||||
|
||||
// Enabled status of rule in the system
|
||||
Enabled bool
|
||||
|
||||
// Action policy accept or drops packets
|
||||
Action PolicyTrafficActionType
|
||||
|
||||
// Destinations policy destination groups
|
||||
Destinations []string `gorm:"serializer:json"`
|
||||
|
||||
// DestinationResource policy destination resource that the rule is applied to
|
||||
DestinationResource Resource `gorm:"serializer:json"`
|
||||
|
||||
// Sources policy source groups
|
||||
Sources []string `gorm:"serializer:json"`
|
||||
|
||||
// SourceResource policy source resource that the rule is applied to
|
||||
SourceResource Resource `gorm:"serializer:json"`
|
||||
|
||||
// Bidirectional define if the rule is applicable in both directions, sources, and destinations
|
||||
Bidirectional bool
|
||||
|
||||
// Protocol type of the traffic
|
||||
Protocol PolicyRuleProtocolType
|
||||
|
||||
// Ports or it ranges list
|
||||
Ports []string `gorm:"serializer:json"`
|
||||
|
||||
// PortRanges a list of port ranges.
|
||||
PortRanges []RulePortRange `gorm:"serializer:json"`
|
||||
|
||||
// AuthorizedGroups is a map of groupIDs and their respective access to local users via ssh
|
||||
AuthorizedGroups map[string][]string `gorm:"serializer:json"`
|
||||
|
||||
// AuthorizedUser is a list of userIDs that are authorized to access local resources via ssh
|
||||
AuthorizedUser string
|
||||
}
|
||||
|
||||
// Copy returns a copy of a policy rule
|
||||
func (pm *PolicyRule) Copy() *PolicyRule {
|
||||
rule := &PolicyRule{
|
||||
ID: pm.ID,
|
||||
PolicyID: pm.PolicyID,
|
||||
Name: pm.Name,
|
||||
Description: pm.Description,
|
||||
Enabled: pm.Enabled,
|
||||
Action: pm.Action,
|
||||
Destinations: make([]string, len(pm.Destinations)),
|
||||
DestinationResource: pm.DestinationResource,
|
||||
Sources: make([]string, len(pm.Sources)),
|
||||
SourceResource: pm.SourceResource,
|
||||
Bidirectional: pm.Bidirectional,
|
||||
Protocol: pm.Protocol,
|
||||
Ports: make([]string, len(pm.Ports)),
|
||||
PortRanges: make([]RulePortRange, len(pm.PortRanges)),
|
||||
AuthorizedGroups: make(map[string][]string, len(pm.AuthorizedGroups)),
|
||||
AuthorizedUser: pm.AuthorizedUser,
|
||||
}
|
||||
copy(rule.Destinations, pm.Destinations)
|
||||
copy(rule.Sources, pm.Sources)
|
||||
copy(rule.Ports, pm.Ports)
|
||||
copy(rule.PortRanges, pm.PortRanges)
|
||||
for k, v := range pm.AuthorizedGroups {
|
||||
rule.AuthorizedGroups[k] = make([]string, len(v))
|
||||
copy(rule.AuthorizedGroups[k], v)
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
func (pm *PolicyRule) Equal(other *PolicyRule) bool {
|
||||
if pm == nil || other == nil {
|
||||
return pm == other
|
||||
}
|
||||
|
||||
if pm.ID != other.ID ||
|
||||
pm.PolicyID != other.PolicyID ||
|
||||
pm.Name != other.Name ||
|
||||
pm.Description != other.Description ||
|
||||
pm.Enabled != other.Enabled ||
|
||||
pm.Action != other.Action ||
|
||||
pm.Bidirectional != other.Bidirectional ||
|
||||
pm.Protocol != other.Protocol ||
|
||||
pm.SourceResource != other.SourceResource ||
|
||||
pm.DestinationResource != other.DestinationResource ||
|
||||
pm.AuthorizedUser != other.AuthorizedUser {
|
||||
return false
|
||||
}
|
||||
|
||||
if !stringSlicesEqualUnordered(pm.Sources, other.Sources) {
|
||||
return false
|
||||
}
|
||||
if !stringSlicesEqualUnordered(pm.Destinations, other.Destinations) {
|
||||
return false
|
||||
}
|
||||
if !stringSlicesEqualUnordered(pm.Ports, other.Ports) {
|
||||
return false
|
||||
}
|
||||
if !portRangeSlicesEqualUnordered(pm.PortRanges, other.PortRanges) {
|
||||
return false
|
||||
}
|
||||
if !authorizedGroupsEqual(pm.AuthorizedGroups, other.AuthorizedGroups) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func stringSlicesEqualUnordered(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
if len(a) == 0 {
|
||||
return true
|
||||
}
|
||||
sorted1 := make([]string, len(a))
|
||||
sorted2 := make([]string, len(b))
|
||||
copy(sorted1, a)
|
||||
copy(sorted2, b)
|
||||
slices.Sort(sorted1)
|
||||
slices.Sort(sorted2)
|
||||
return slices.Equal(sorted1, sorted2)
|
||||
}
|
||||
|
||||
func portRangeSlicesEqualUnordered(a, b []RulePortRange) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
if len(a) == 0 {
|
||||
return true
|
||||
}
|
||||
cmp := func(x, y RulePortRange) int {
|
||||
if x.Start != y.Start {
|
||||
if x.Start < y.Start {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
if x.End != y.End {
|
||||
if x.End < y.End {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
sorted1 := make([]RulePortRange, len(a))
|
||||
sorted2 := make([]RulePortRange, len(b))
|
||||
copy(sorted1, a)
|
||||
copy(sorted2, b)
|
||||
slices.SortFunc(sorted1, cmp)
|
||||
slices.SortFunc(sorted2, cmp)
|
||||
return slices.EqualFunc(sorted1, sorted2, func(x, y RulePortRange) bool {
|
||||
return x.Start == y.Start && x.End == y.End
|
||||
})
|
||||
}
|
||||
|
||||
func authorizedGroupsEqual(a, b map[string][]string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for k, va := range a {
|
||||
vb, ok := b[k]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !stringSlicesEqualUnordered(va, vb) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
)
|
||||
|
||||
type Resource struct {
|
||||
ID string
|
||||
Type ResourceType
|
||||
}
|
||||
|
||||
func (r *Resource) ToAPIResponse() *api.Resource {
|
||||
if r.ID == "" && r.Type == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &api.Resource{
|
||||
Id: r.ID,
|
||||
Type: api.ResourceType(r.Type),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resource) FromAPIRequest(req *api.Resource) {
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.ID = req.Id
|
||||
r.Type = ResourceType(req.Type)
|
||||
}
|
||||
@@ -6,18 +6,20 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
"github.com/netbirdio/netbird/management/server/integration_reference"
|
||||
"github.com/netbirdio/netbird/shared/management/integration_reference"
|
||||
"github.com/netbirdio/netbird/util/crypt"
|
||||
)
|
||||
|
||||
const (
|
||||
UserRoleOwner UserRole = "owner"
|
||||
UserRoleAdmin UserRole = "admin"
|
||||
UserRoleUser UserRole = "user"
|
||||
UserRoleUnknown UserRole = "unknown"
|
||||
UserRoleBillingAdmin UserRole = "billing_admin"
|
||||
UserRoleAuditor UserRole = "auditor"
|
||||
UserRoleNetworkAdmin UserRole = "network_admin"
|
||||
UserRoleOwner UserRole = "owner"
|
||||
UserRoleAdmin UserRole = "admin"
|
||||
UserRoleUser UserRole = "user"
|
||||
UserRoleUnknown UserRole = "unknown"
|
||||
UserRoleBillingAdmin UserRole = "billing_admin"
|
||||
UserRoleAuditor UserRole = "auditor"
|
||||
UserRoleNetworkAdmin UserRole = "network_admin"
|
||||
UserRoleAgentNetworkAdmin UserRole = "agent_network_admin"
|
||||
UserRoleUsageViewer UserRole = "usage_viewer"
|
||||
|
||||
UserStatusActive UserStatus = "active"
|
||||
UserStatusDisabled UserStatus = "disabled"
|
||||
@@ -42,6 +44,10 @@ func StrRoleToUserRole(strRole string) UserRole {
|
||||
return UserRoleAuditor
|
||||
case "network_admin":
|
||||
return UserRoleNetworkAdmin
|
||||
case "agent_network_admin":
|
||||
return UserRoleAgentNetworkAdmin
|
||||
case "usage_viewer":
|
||||
return UserRoleUsageViewer
|
||||
default:
|
||||
return UserRoleUnknown
|
||||
}
|
||||
@@ -140,7 +146,7 @@ func (u *User) IsRegularUser() bool {
|
||||
|
||||
// IsRestrictable checks whether a user is in a restrictable role.
|
||||
func (u *User) IsRestrictable() bool {
|
||||
return u.Role == UserRoleUser || u.Role == UserRoleBillingAdmin
|
||||
return u.Role == UserRoleUser || u.Role == UserRoleBillingAdmin || u.Role == UserRoleUsageViewer
|
||||
}
|
||||
|
||||
// ToUserInfo converts a User object to a UserInfo object.
|
||||
|
||||
@@ -33,7 +33,7 @@ import (
|
||||
"github.com/netbirdio/netbird/idp/dex"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
"github.com/netbirdio/netbird/management/server/integration_reference"
|
||||
"github.com/netbirdio/netbird/shared/management/integration_reference"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// RandIntn returns a uniformly distributed int in [0, n) sourced from
|
||||
// crypto/rand. It panics if n <= 0 or the platform randomness source fails.
|
||||
func RandIntn(n int) int {
|
||||
v, err := rand.Int(rand.Reader, big.NewInt(int64(n)))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return int(v.Int64())
|
||||
}
|
||||
|
||||
// Difference returns the elements in `a` that aren't in `b`.
|
||||
func Difference(a, b []string) []string {
|
||||
mb := make(map[string]struct{}, len(b))
|
||||
|
||||
Reference in New Issue
Block a user