mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-19 22:21:29 +02:00
Compare commits
167 Commits
fix/androi
...
revert/com
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9324608d4e | ||
|
|
93a0b914fa | ||
|
|
2e2d0d54dc | ||
|
|
fa286ac778 | ||
|
|
1e673eb727 | ||
|
|
d3c9053b2f | ||
|
|
9efa3c6579 | ||
|
|
32deb605f9 | ||
|
|
8ed9f8c93d | ||
|
|
70c75bb94c | ||
|
|
4036121b0d | ||
|
|
a803ad398d | ||
|
|
6ee0987cba | ||
|
|
6fdde1385f | ||
|
|
aafc233310 | ||
|
|
7bc38d9b85 | ||
|
|
84e4b08056 | ||
|
|
14f976bade | ||
|
|
fdb956a974 | ||
|
|
c6db3ad575 | ||
|
|
cb4460d1d9 | ||
|
|
c004d09d77 | ||
|
|
9fed4ce414 | ||
|
|
4aa123b6ad | ||
|
|
931598e593 | ||
|
|
9bb1db28c3 | ||
|
|
4952f2e8cf | ||
|
|
32c7918c80 | ||
|
|
56d1607f72 | ||
|
|
460778abb9 | ||
|
|
4be6603815 | ||
|
|
05511cc13d | ||
|
|
9427fa87e1 | ||
|
|
fe5e5c08eb | ||
|
|
cb858b335d | ||
|
|
8cbbea4536 | ||
|
|
cd6f63272b | ||
|
|
859af13c12 | ||
|
|
4375fdc7b6 | ||
|
|
1caa0ddf27 | ||
|
|
cd2a91ddd8 | ||
|
|
38c5932375 | ||
|
|
fe9ea43198 | ||
|
|
fad568fdb0 | ||
|
|
3da27221ac | ||
|
|
d954a2dc3e | ||
|
|
433a4f12bf | ||
|
|
3834e67a51 | ||
|
|
78947c1611 | ||
|
|
763b8f6933 | ||
|
|
e530c25812 | ||
|
|
166b3d7739 | ||
|
|
20f18d1479 | ||
|
|
5594289924 | ||
|
|
a03635680b | ||
|
|
905e1c14ba | ||
|
|
5e4d6a5e44 | ||
|
|
65f184141b | ||
|
|
c54c35beed | ||
|
|
935d1c5863 | ||
|
|
ac3b713dda | ||
|
|
4d4de15f48 | ||
|
|
28074405e1 | ||
|
|
fd23461b03 | ||
|
|
b19cf7405d | ||
|
|
90e0c5bd0c | ||
|
|
6ccb2bb239 | ||
|
|
b160cb43b1 | ||
|
|
1902777509 | ||
|
|
cff46eee67 | ||
|
|
0513109c35 | ||
|
|
6b8393c6b0 | ||
|
|
316e82337f | ||
|
|
bacacb8f77 | ||
|
|
994e84a686 | ||
|
|
07809fe923 | ||
|
|
ec3e37456e | ||
|
|
c81103dfa6 | ||
|
|
ba574dc739 | ||
|
|
067982f772 | ||
|
|
ce4f0a7821 | ||
|
|
3218e2f744 | ||
|
|
7e24c27ef1 | ||
|
|
06817271bf | ||
|
|
1530739b54 | ||
|
|
e745aedc92 | ||
|
|
4cf3903c83 | ||
|
|
1e14b554a6 | ||
|
|
11a3627899 | ||
|
|
2442bd6198 | ||
|
|
006cee000f | ||
|
|
c93aa03c0e | ||
|
|
efe2eaeb09 | ||
|
|
1926e983fb | ||
|
|
e9b8175915 | ||
|
|
88f1930450 | ||
|
|
1774a3d9dd | ||
|
|
795e06ce83 | ||
|
|
748f6b3fbb | ||
|
|
d5adfc799f | ||
|
|
60fe56e442 | ||
|
|
327fa4c0d6 | ||
|
|
ed015a7972 | ||
|
|
b43708e31b | ||
|
|
ce55b8406b | ||
|
|
0456b64ea1 | ||
|
|
942ee81ec0 | ||
|
|
33a0e1bc2b | ||
|
|
dbfdd04c7b | ||
|
|
4d010f60ce | ||
|
|
3272058e56 | ||
|
|
70c3feb05b | ||
|
|
a0fe80cd99 | ||
|
|
6fadf8f24a | ||
|
|
ef0032685b | ||
|
|
5ed38569f7 | ||
|
|
d4e1c8978e | ||
|
|
d5ac70d806 | ||
|
|
23579e4dd4 | ||
|
|
a00c5164a8 | ||
|
|
02138dffdd | ||
|
|
02ec1f5dcb | ||
|
|
07d0440e34 | ||
|
|
2c85d94c6c | ||
|
|
1f190a50cf | ||
|
|
52fdfd5bdc | ||
|
|
e35a0f3318 | ||
|
|
56c411babd | ||
|
|
164b2baa4d | ||
|
|
0b29c6ed1a | ||
|
|
724b61440b | ||
|
|
22590ad66e | ||
|
|
b8e004ea89 | ||
|
|
e2797360f4 | ||
|
|
2f399f1e6e | ||
|
|
82c1e18264 | ||
|
|
ce9023bd27 | ||
|
|
7d33356776 | ||
|
|
993291149c | ||
|
|
98f7ea40a1 | ||
|
|
76db9ab94f | ||
|
|
9653b15d78 | ||
|
|
206bb1676b | ||
|
|
600b0c752b | ||
|
|
b02736adc3 | ||
|
|
5d6117d2c0 | ||
|
|
c2f8360b00 | ||
|
|
ea1b4d56e8 | ||
|
|
acbe22b831 | ||
|
|
3b5c8e2298 | ||
|
|
2baeb4bc0d | ||
|
|
4bf75fdc97 | ||
|
|
799f3a3c62 | ||
|
|
129736ad61 | ||
|
|
f7be9c4347 | ||
|
|
e05cb5264d | ||
|
|
5a10561ca1 | ||
|
|
42ce83a8f3 | ||
|
|
e620c86cd4 | ||
|
|
9dee2d60b9 | ||
|
|
4525014632 | ||
|
|
23a5c0de4b | ||
|
|
25e882004f | ||
|
|
15003258d2 | ||
|
|
2af3a5fba5 | ||
|
|
a6603a2e0a | ||
|
|
7ed3737cda |
9
.github/workflows/golang-test-linux.yml
vendored
9
.github/workflows/golang-test-linux.yml
vendored
@@ -730,6 +730,11 @@ jobs:
|
||||
- name: Install modules
|
||||
run: go mod tidy
|
||||
|
||||
- name: Run Mage
|
||||
uses: magefile/mage-action@a662bd8c29d8106879588cfff83b2faf6e6f59db # v4.0.0
|
||||
with:
|
||||
install-only: true
|
||||
|
||||
- name: check git status
|
||||
run: git --no-pager diff --exit-code
|
||||
|
||||
@@ -738,9 +743,7 @@ jobs:
|
||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
||||
CI=true \
|
||||
go test -tags=integration -coverprofile=coverage.txt \
|
||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
|
||||
-timeout 20m ./management/server/http/...
|
||||
mage integrationtest:all -gotestflags="-coverprofile=coverage.txt"
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: matrix.arch == 'amd64'
|
||||
|
||||
@@ -26,7 +26,8 @@ import (
|
||||
"github.com/netbirdio/netbird/client/internal/routemanager"
|
||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||
"github.com/netbirdio/netbird/client/net"
|
||||
"github.com/netbirdio/netbird/client/netevents"
|
||||
"github.com/netbirdio/netbird/client/netstate"
|
||||
"github.com/netbirdio/netbird/client/netsweep"
|
||||
"github.com/netbirdio/netbird/client/system"
|
||||
"github.com/netbirdio/netbird/formatter"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
@@ -81,10 +82,13 @@ type Client struct {
|
||||
deviceName string
|
||||
uiVersion string
|
||||
networkChangeListener listener.NetworkChangeListener
|
||||
// netMgr outlives engine restarts: it mirrors the OS connectivity, not
|
||||
// the engine lifecycle. Run and RunWithoutLogin inject its state and
|
||||
// sweeper into each new ConnectClient.
|
||||
netMgr *netevents.Manager
|
||||
// netState outlives engine restarts: it mirrors the OS connectivity, not
|
||||
// the engine lifecycle. Run and RunWithoutLogin inject it into each new
|
||||
// ConnectClient, which distributes it to every reconnection loop.
|
||||
netState *netstate.State
|
||||
|
||||
// sweeper also outlives engine restarts; NotifyNetworkChange sweeps it.
|
||||
sweeper *netsweep.Sweeper
|
||||
|
||||
stateMu sync.RWMutex
|
||||
connectClient *internal.ConnectClient
|
||||
@@ -149,16 +153,16 @@ func NewClient(androidSDKVersion int, deviceName string, uiVersion string, tunAd
|
||||
|
||||
net.SetAndroidProtectSocketFn(tunAdapter.ProtectSocket)
|
||||
system.SetIFaceDiscover(iFaceDiscover)
|
||||
recorder := peer.NewRecorder("")
|
||||
return &Client{
|
||||
deviceName: deviceName,
|
||||
uiVersion: uiVersion,
|
||||
tunAdapter: tunAdapter,
|
||||
iFaceDiscover: iFaceDiscover,
|
||||
recorder: recorder,
|
||||
recorder: peer.NewRecorder(""),
|
||||
ctxCancelLock: &sync.Mutex{},
|
||||
networkChangeListener: networkChangeListener,
|
||||
netMgr: netevents.NewManager(recorder),
|
||||
netState: netstate.New(),
|
||||
sweeper: netsweep.New(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,9 +203,8 @@ func (c *Client) Run(platformFiles PlatformFiles, urlOpener URLOpener, isAndroid
|
||||
}
|
||||
// todo do not throw error in case of cancelled context
|
||||
ctx = internal.CtxInitState(ctx)
|
||||
|
||||
connectClient := internal.NewConnectClient(ctx, cfg, c.recorder,
|
||||
internal.WithNetEvents(c.netMgr))
|
||||
internal.WithNetworkState(c.netState), internal.WithSweeper(c.sweeper))
|
||||
c.setState(cfg, cacheDir, cfgFile, connectClient)
|
||||
// This path runs the interactive SSO flow, so reaching here means the peer
|
||||
// is authenticated again — release the latch Status() reports from. Clear
|
||||
@@ -243,7 +246,7 @@ func (c *Client) RunWithoutLogin(platformFiles PlatformFiles, dns *DNSList, dnsR
|
||||
// todo do not throw error in case of cancelled context
|
||||
ctx = internal.CtxInitState(ctx)
|
||||
connectClient := internal.NewConnectClient(ctx, cfg, c.recorder,
|
||||
internal.WithNetEvents(c.netMgr))
|
||||
internal.WithNetworkState(c.netState), internal.WithSweeper(c.sweeper))
|
||||
c.setState(cfg, cacheDir, cfgFile, connectClient)
|
||||
return connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, stateFile, cacheDir)
|
||||
}
|
||||
@@ -295,12 +298,9 @@ func (c *Client) GetTunSettings() (*TunSettings, error) {
|
||||
// While unavailable, the internal reconnect loops suspend their attempts and
|
||||
// the connection listener reports NoNetwork instead of Connecting; when
|
||||
// availability returns, the loops resume immediately with a fresh backoff.
|
||||
// Losing the last network also sweeps the registered connections: nothing can
|
||||
// redial while offline, so the stale sockets would otherwise stay silently
|
||||
// "connected" until their own timeouts and the client would keep reporting
|
||||
// Connected with no network at all.
|
||||
func (c *Client) SetNetworkAvailable(available bool) {
|
||||
c.netMgr.SetNetworkAvailable(available)
|
||||
c.netState.Set(available)
|
||||
c.recorder.SetNetworkAvailable(available)
|
||||
}
|
||||
|
||||
// NotifyNetworkChange marks the management, signal and relay connections
|
||||
@@ -308,7 +308,8 @@ func (c *Client) SetNetworkAvailable(available bool) {
|
||||
// whatever has not redialed on the new network by then. The engine and the
|
||||
// TUN device stay untouched.
|
||||
func (c *Client) NotifyNetworkChange() {
|
||||
c.netMgr.NotifyNetworkChange()
|
||||
c.sweeper.MarkNetworkChange()
|
||||
log.Infof("network change: connections marked stale")
|
||||
}
|
||||
|
||||
// DebugBundle generates a debug bundle, uploads it, and returns the upload key.
|
||||
|
||||
@@ -124,7 +124,7 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
|
||||
|
||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
requestBuffer := mgmt.NewAccountRequestBuffer(ctx, store)
|
||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersmanager), config)
|
||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersmanager), config, nil)
|
||||
|
||||
accountManager, err := mgmt.BuildManager(ctx, config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false, cacheStore)
|
||||
if err != nil {
|
||||
|
||||
@@ -146,7 +146,7 @@ func startManagement(t *testing.T, signalAddr string) string {
|
||||
|
||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
requestBuffer := mgmt.NewAccountRequestBuffer(context.Background(), testStore)
|
||||
networkMapController := controller.NewController(context.Background(), testStore, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(testStore, peersManager), cfg)
|
||||
networkMapController := controller.NewController(context.Background(), testStore, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(testStore, peersManager), cfg, nil)
|
||||
accountManager, err := mgmt.BuildManager(context.Background(), cfg, testStore, networkMapController, jobManager, nil, "", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -16,14 +16,9 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
nbnet "github.com/netbirdio/netbird/client/net"
|
||||
"github.com/netbirdio/netbird/client/netevents/sweep"
|
||||
"github.com/netbirdio/netbird/client/netsweep"
|
||||
)
|
||||
|
||||
// Sweeper registers in-flight dials for the network change sweep.
|
||||
type Sweeper interface {
|
||||
StartDial(ctx context.Context) *sweep.Dial
|
||||
}
|
||||
|
||||
func WithCustomDialer(_ bool, _ string) grpc.DialOption {
|
||||
return grpc.WithContextDialer(dialContext)
|
||||
}
|
||||
@@ -31,7 +26,7 @@ func WithCustomDialer(_ bool, _ string) grpc.DialOption {
|
||||
// WithSweeper dials like WithCustomDialer but registers connections and
|
||||
// dials with the sweeper. Append it after WithCustomDialer: gRPC applies
|
||||
// dial options in order, so the later context dialer wins.
|
||||
func WithSweeper(sweeper Sweeper) grpc.DialOption {
|
||||
func WithSweeper(sweeper *netsweep.Sweeper) grpc.DialOption {
|
||||
return grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
dial := sweeper.StartDial(ctx)
|
||||
defer dial.Release()
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/netbirdio/netbird/client/netevents/sweep"
|
||||
"github.com/netbirdio/netbird/client/netsweep"
|
||||
"github.com/netbirdio/netbird/util/wsproxy/client"
|
||||
)
|
||||
|
||||
// Sweeper registers in-flight dials for the network change sweep.
|
||||
type Sweeper interface {
|
||||
StartDial(ctx context.Context) *sweep.Dial
|
||||
}
|
||||
|
||||
// WithCustomDialer returns a gRPC dial option that uses WebSocket transport for WASM/JS environments.
|
||||
// The component parameter specifies the WebSocket proxy component path (e.g., "/management", "/signal").
|
||||
func WithCustomDialer(tlsEnabled bool, component string) grpc.DialOption {
|
||||
@@ -21,6 +14,6 @@ func WithCustomDialer(tlsEnabled bool, component string) grpc.DialOption {
|
||||
}
|
||||
|
||||
// WithSweeper is a no-op on WASM/JS: there is no network change signal.
|
||||
func WithSweeper(_ Sweeper) grpc.DialOption {
|
||||
func WithSweeper(_ *netsweep.Sweeper) grpc.DialOption {
|
||||
return grpc.EmptyDialOption{}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,16 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
)
|
||||
|
||||
// ChangeWatcher exposes OS network availability transitions.
|
||||
type ChangeWatcher interface {
|
||||
Changed() <-chan struct{}
|
||||
}
|
||||
"github.com/netbirdio/netbird/client/netstate"
|
||||
)
|
||||
|
||||
// Retry mirrors backoff.Retry, but the sleep between attempts also wakes on
|
||||
// OS network availability transitions: an operation cut down by a network
|
||||
// change retries the moment the network settles instead of sleeping through
|
||||
// the recovery. A nil watcher never fires, leaving plain backoff.Retry
|
||||
// the recovery. A nil netState never fires, leaving plain backoff.Retry
|
||||
// behavior.
|
||||
func Retry(ctx context.Context, operation backoff.Operation, bo backoff.BackOff, watcher ChangeWatcher) error {
|
||||
func Retry(ctx context.Context, operation backoff.Operation, bo backoff.BackOff, netState *netstate.State) error {
|
||||
bo.Reset()
|
||||
for {
|
||||
err := operation()
|
||||
@@ -39,14 +36,10 @@ func Retry(ctx context.Context, operation backoff.Operation, bo backoff.BackOff,
|
||||
return err
|
||||
}
|
||||
|
||||
var changed <-chan struct{}
|
||||
if watcher != nil {
|
||||
changed = watcher.Changed()
|
||||
}
|
||||
timer := time.NewTimer(next)
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-changed:
|
||||
case <-netState.Changed():
|
||||
timer.Stop()
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/client/netevents/netstate"
|
||||
"github.com/netbirdio/netbird/client/netstate"
|
||||
)
|
||||
|
||||
func TestRetryWakesOnNetworkChange(t *testing.T) {
|
||||
|
||||
@@ -38,7 +38,8 @@ import (
|
||||
"github.com/netbirdio/netbird/client/internal/updater"
|
||||
"github.com/netbirdio/netbird/client/internal/updater/installer"
|
||||
nbnet "github.com/netbirdio/netbird/client/net"
|
||||
"github.com/netbirdio/netbird/client/netevents"
|
||||
"github.com/netbirdio/netbird/client/netstate"
|
||||
"github.com/netbirdio/netbird/client/netsweep"
|
||||
cProto "github.com/netbirdio/netbird/client/proto"
|
||||
"github.com/netbirdio/netbird/client/ssh"
|
||||
sshconfig "github.com/netbirdio/netbird/client/ssh/config"
|
||||
@@ -72,17 +73,28 @@ type ConnectClient struct {
|
||||
|
||||
persistSyncResponse bool
|
||||
|
||||
// netEvents gates every reconnection loop on OS-reported network
|
||||
// availability and sweeps connections on network change.
|
||||
netEvents *netevents.Manager
|
||||
// netState gates every reconnection loop on OS-reported network
|
||||
// availability. Nil (the default) disables gating; mobile platforms
|
||||
// inject it via WithNetworkState.
|
||||
netState *netstate.State
|
||||
|
||||
// sweeper cuts the management, signal and relay connections on network
|
||||
// change; nil disables it.
|
||||
sweeper *netsweep.Sweeper
|
||||
}
|
||||
|
||||
// ConnectClientOption configures optional ConnectClient behavior.
|
||||
type ConnectClientOption func(*ConnectClient)
|
||||
|
||||
// WithNetEvents injects the OS network event handling.
|
||||
func WithNetEvents(events *netevents.Manager) ConnectClientOption {
|
||||
return func(c *ConnectClient) { c.netEvents = events }
|
||||
// WithNetworkState injects the OS network availability state that gates every
|
||||
// reconnection loop; without it gating is disabled.
|
||||
func WithNetworkState(netState *netstate.State) ConnectClientOption {
|
||||
return func(c *ConnectClient) { c.netState = netState }
|
||||
}
|
||||
|
||||
// WithSweeper injects the network change sweeper.
|
||||
func WithSweeper(sweeper *netsweep.Sweeper) ConnectClientOption {
|
||||
return func(c *ConnectClient) { c.sweeper = sweeper }
|
||||
}
|
||||
|
||||
func NewConnectClient(
|
||||
@@ -293,7 +305,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
||||
}
|
||||
|
||||
// suspend connection attempts while the OS reports no usable network
|
||||
if waited, err := c.netEvents.Wait(c.ctx); err != nil {
|
||||
if waited, err := c.netState.Wait(c.ctx); err != nil {
|
||||
return nil
|
||||
} else if waited {
|
||||
backOff.Reset()
|
||||
@@ -311,7 +323,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
||||
|
||||
log.Debugf("connecting to the Management service %s", c.config.ManagementURL.Host)
|
||||
mgmClient, err := mgm.NewClient(engineCtx, c.config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled,
|
||||
mgm.WithNetEvents(c.netEvents))
|
||||
mgm.WithNetworkState(c.netState), mgm.WithSweeper(c.sweeper))
|
||||
if err != nil {
|
||||
// On daemon shutdown / Down() the parent context is cancelled
|
||||
// and the dial fails with "context canceled". Wrapping that
|
||||
@@ -386,7 +398,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
||||
}()
|
||||
|
||||
// with the global Netbird config in hand connect (just a connection, no stream yet) Signal
|
||||
signalClient, err := connectToSignal(engineCtx, loginResp.GetNetbirdConfig(), myPrivateKey, c.netEvents)
|
||||
signalClient, err := connectToSignal(engineCtx, loginResp.GetNetbirdConfig(), myPrivateKey, c.netState, c.sweeper)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return wrapErr(err)
|
||||
@@ -423,7 +435,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
||||
}
|
||||
|
||||
relayManager := relayClient.NewManager(engineCtx, relayURLs, myPrivateKey.PublicKey().String(), engineConfig.MTU,
|
||||
relayClient.WithNetEvents(c.netEvents))
|
||||
relayClient.WithNetworkState(c.netState), relayClient.WithSweeper(c.sweeper))
|
||||
c.statusRecorder.SetRelayMgr(relayManager)
|
||||
if len(relayURLs) > 0 {
|
||||
if token != nil {
|
||||
@@ -451,7 +463,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
|
||||
UpdateManager: c.updateManager,
|
||||
ClientMetrics: c.clientMetrics,
|
||||
MetricsCtx: c.ctx,
|
||||
NetState: c.netEvents,
|
||||
NetState: c.netState,
|
||||
}, mobileDependency)
|
||||
engine.SetSyncResponsePersistence(c.persistSyncResponse)
|
||||
c.engine = engine
|
||||
@@ -711,7 +723,7 @@ func selectMTU(localMTU uint16, peerMTU int32) uint16 {
|
||||
}
|
||||
|
||||
// connectToSignal creates Signal Service client and established a connection
|
||||
func connectToSignal(ctx context.Context, wtConfig *mgmProto.NetbirdConfig, ourPrivateKey wgtypes.Key, netEvents *netevents.Manager) (*signal.GrpcClient, error) {
|
||||
func connectToSignal(ctx context.Context, wtConfig *mgmProto.NetbirdConfig, ourPrivateKey wgtypes.Key, netState *netstate.State, sweeper *netsweep.Sweeper) (*signal.GrpcClient, error) {
|
||||
var sigTLSEnabled bool
|
||||
if wtConfig.Signal.Protocol == mgmProto.HostConfig_HTTPS {
|
||||
sigTLSEnabled = true
|
||||
@@ -720,7 +732,7 @@ func connectToSignal(ctx context.Context, wtConfig *mgmProto.NetbirdConfig, ourP
|
||||
}
|
||||
|
||||
signalClient, err := signal.NewClient(ctx, wtConfig.Signal.Uri, ourPrivateKey, sigTLSEnabled,
|
||||
signal.WithNetEvents(netEvents))
|
||||
signal.WithNetworkState(netState), signal.WithSweeper(sweeper))
|
||||
if err != nil {
|
||||
log.Errorf("error while connecting to the Signal Exchange Service %s: %s", wtConfig.Signal.Uri, err)
|
||||
return nil, gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Signal Service : %s", err)
|
||||
|
||||
@@ -59,7 +59,7 @@ import (
|
||||
"github.com/netbirdio/netbird/client/internal/syncstore"
|
||||
"github.com/netbirdio/netbird/client/internal/updater"
|
||||
"github.com/netbirdio/netbird/client/jobexec"
|
||||
"github.com/netbirdio/netbird/client/netevents"
|
||||
"github.com/netbirdio/netbird/client/netstate"
|
||||
cProto "github.com/netbirdio/netbird/client/proto"
|
||||
"github.com/netbirdio/netbird/client/system"
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
@@ -184,7 +184,7 @@ type EngineServices struct {
|
||||
MetricsCtx context.Context
|
||||
// NetState gates the reconnection loops on OS-reported network
|
||||
// availability; nil disables gating.
|
||||
NetState *netevents.Manager
|
||||
NetState *netstate.State
|
||||
}
|
||||
|
||||
// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
|
||||
@@ -210,7 +210,7 @@ type Engine struct {
|
||||
|
||||
// netState gates the peer reconnection guards on OS-reported network
|
||||
// availability; nil disables gating.
|
||||
netState *netevents.Manager
|
||||
netState *netstate.State
|
||||
|
||||
// STUNs is a list of STUN servers used by ICE
|
||||
STUNs []*stun.URI
|
||||
|
||||
@@ -519,7 +519,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
|
||||
|
||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
requestBuffer := server.NewAccountRequestBuffer(context.Background(), store)
|
||||
networkMapController := controller.NewController(context.Background(), store, metrics, updateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersManager), config)
|
||||
networkMapController := controller.NewController(context.Background(), store, metrics, updateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersManager), config, nil)
|
||||
accountManager, err := server.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"github.com/netbirdio/netbird/client/internal/portforward"
|
||||
"github.com/netbirdio/netbird/client/internal/rosenpass"
|
||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||
"github.com/netbirdio/netbird/client/netevents"
|
||||
"github.com/netbirdio/netbird/client/netstate"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||
)
|
||||
@@ -97,7 +97,7 @@ type ConnConfig struct {
|
||||
|
||||
// NetworkState gates the reconnection guard on OS-reported network
|
||||
// availability; nil disables gating.
|
||||
NetworkState *netevents.Manager
|
||||
NetworkState *netstate.State
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/netstate"
|
||||
)
|
||||
|
||||
// ConnStatus represents the connection state as seen by the guard.
|
||||
@@ -22,12 +24,6 @@ const (
|
||||
|
||||
type connStatusFunc func() ConnStatus
|
||||
|
||||
// NetworkWatcher is the availability view the guard gates reconnects on.
|
||||
type NetworkWatcher interface {
|
||||
IsOnline() bool
|
||||
Changed() <-chan struct{}
|
||||
}
|
||||
|
||||
// Guard is responsible for the reconnection logic.
|
||||
// It will trigger to send an offer to the peer then has connection issues.
|
||||
// Watch these events:
|
||||
@@ -43,14 +39,14 @@ type Guard struct {
|
||||
srWatcher *SRWatcher
|
||||
// netState gates reconnect attempts on OS-reported network availability;
|
||||
// nil disables gating.
|
||||
netState NetworkWatcher
|
||||
netState *netstate.State
|
||||
relayedConnDisconnected chan struct{}
|
||||
iCEConnDisconnected chan struct{}
|
||||
}
|
||||
|
||||
// NewGuard creates a reconnection guard for a peer connection. A nil netState
|
||||
// disables network availability gating.
|
||||
func NewGuard(log *log.Entry, isConnectedFn connStatusFunc, timeout time.Duration, srWatcher *SRWatcher, netState NetworkWatcher) *Guard {
|
||||
func NewGuard(log *log.Entry, isConnectedFn connStatusFunc, timeout time.Duration, srWatcher *SRWatcher, netState *netstate.State) *Guard {
|
||||
return &Guard{
|
||||
log: log,
|
||||
isConnectedOnAllWay: isConnectedFn,
|
||||
@@ -108,17 +104,14 @@ func (g *Guard) reconnectLoopWithRetry(ctx context.Context, callback func()) {
|
||||
iceState := &iceRetryState{log: g.log}
|
||||
defer iceState.reset()
|
||||
|
||||
var netChanged <-chan struct{}
|
||||
if g.netState != nil {
|
||||
netChanged = g.netState.Changed()
|
||||
}
|
||||
netChanged := g.netState.Changed()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-tickerChannel:
|
||||
// skip attempts while the OS reports no usable network; the
|
||||
// netChanged case below resumes the loop once it returns
|
||||
if g.netState != nil && !g.netState.IsOnline() {
|
||||
if !g.netState.IsOnline() {
|
||||
continue
|
||||
}
|
||||
switch g.isConnectedOnAllWay() {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/peer/ice"
|
||||
"github.com/netbirdio/netbird/client/netevents/netstate"
|
||||
"github.com/netbirdio/netbird/client/netstate"
|
||||
)
|
||||
|
||||
// newTestGuardWithNetState builds a guard with a realistic MaxInterval: the
|
||||
|
||||
@@ -307,6 +307,16 @@ func startUIInSession(uiPath string, sessionID uint32) error {
|
||||
}
|
||||
}()
|
||||
|
||||
var env *uint16
|
||||
if err := windows.CreateEnvironmentBlock(&env, primaryToken, false); err != nil {
|
||||
return fmt.Errorf("create environment block: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := windows.DestroyEnvironmentBlock(env); err != nil {
|
||||
log.Warnf("failed to destroy environment block: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Prepare startup info
|
||||
var si windows.StartupInfo
|
||||
si.Cb = uint32(unsafe.Sizeof(si))
|
||||
@@ -329,7 +339,7 @@ func startUIInSession(uiPath string, sessionID uint32) error {
|
||||
nil,
|
||||
false,
|
||||
creationFlags,
|
||||
nil,
|
||||
env,
|
||||
nil,
|
||||
&si,
|
||||
&pi,
|
||||
|
||||
@@ -22,7 +22,8 @@ import (
|
||||
"github.com/netbirdio/netbird/client/internal/listener"
|
||||
"github.com/netbirdio/netbird/client/internal/peer"
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
"github.com/netbirdio/netbird/client/netevents"
|
||||
"github.com/netbirdio/netbird/client/netstate"
|
||||
"github.com/netbirdio/netbird/client/netsweep"
|
||||
"github.com/netbirdio/netbird/client/system"
|
||||
"github.com/netbirdio/netbird/formatter"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
@@ -83,10 +84,12 @@ type Client struct {
|
||||
onHostDnsFn func([]string)
|
||||
dnsManager dns.IosDnsManager
|
||||
loginComplete bool
|
||||
// netMgr outlives engine restarts: it mirrors the OS connectivity, not
|
||||
// the engine lifecycle. Run injects its state and sweeper into each new
|
||||
// ConnectClient.
|
||||
netMgr *netevents.Manager
|
||||
// netState outlives engine restarts: it mirrors the OS connectivity, not
|
||||
// the engine lifecycle. Run injects it into each new ConnectClient, which
|
||||
// distributes it to every reconnection loop.
|
||||
netState *netstate.State
|
||||
// sweeper also outlives engine restarts; NotifyNetworkChange sweeps it.
|
||||
sweeper *netsweep.Sweeper
|
||||
// preloadedConfig holds config loaded from JSON (used on tvOS where file writes are blocked)
|
||||
preloadedConfig *profilemanager.Config
|
||||
|
||||
@@ -97,7 +100,6 @@ type Client struct {
|
||||
|
||||
// NewClient instantiate a new Client
|
||||
func NewClient(cfgFile, stateFile, cacheDir, logFilePath, deviceName string, osVersion string, osName string, networkChangeListener NetworkChangeListener, dnsManager DnsManager) *Client {
|
||||
recorder := peer.NewRecorder("")
|
||||
return &Client{
|
||||
cfgFile: cfgFile,
|
||||
stateFile: stateFile,
|
||||
@@ -106,11 +108,12 @@ func NewClient(cfgFile, stateFile, cacheDir, logFilePath, deviceName string, osV
|
||||
deviceName: deviceName,
|
||||
osName: osName,
|
||||
osVersion: osVersion,
|
||||
recorder: recorder,
|
||||
recorder: peer.NewRecorder(""),
|
||||
ctxCancelLock: &sync.Mutex{},
|
||||
networkChangeListener: networkChangeListener,
|
||||
dnsManager: dnsManager,
|
||||
netMgr: netevents.NewManager(recorder),
|
||||
netState: netstate.New(),
|
||||
sweeper: netsweep.New(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +190,7 @@ func (c *Client) Run(fd int32, interfaceName string, envList *EnvList) error {
|
||||
cfg.WgIface = interfaceName
|
||||
|
||||
connectClient := internal.NewConnectClient(ctx, cfg, c.recorder,
|
||||
internal.WithNetEvents(c.netMgr))
|
||||
internal.WithNetworkState(c.netState), internal.WithSweeper(c.sweeper))
|
||||
c.setState(cfg, connectClient)
|
||||
// Persist the latest sync response so DebugBundle can include the network
|
||||
// map. On iOS this is backed by disk to keep it out of the constrained
|
||||
@@ -200,11 +203,10 @@ func (c *Client) Run(fd int32, interfaceName string, envList *EnvList) error {
|
||||
// (e.g. from NWPathMonitor). While unavailable, the internal reconnect loops
|
||||
// suspend their attempts and the connection listener reports NoNetwork
|
||||
// instead of Connecting; when availability returns, the loops resume
|
||||
// immediately with a fresh backoff. Losing the last network also sweeps the
|
||||
// registered connections, so the client does not keep reporting Connected
|
||||
// over stale sockets with no network at all.
|
||||
// immediately with a fresh backoff.
|
||||
func (c *Client) SetNetworkAvailable(available bool) {
|
||||
c.netMgr.SetNetworkAvailable(available)
|
||||
c.netState.Set(available)
|
||||
c.recorder.SetNetworkAvailable(available)
|
||||
}
|
||||
|
||||
// NotifyNetworkChange marks the management, signal and relay connections
|
||||
@@ -212,7 +214,8 @@ func (c *Client) SetNetworkAvailable(available bool) {
|
||||
// whatever has not redialed on the new network by then. The engine and the
|
||||
// TUN device stay untouched.
|
||||
func (c *Client) NotifyNetworkChange() {
|
||||
c.netMgr.NotifyNetworkChange()
|
||||
c.sweeper.MarkNetworkChange()
|
||||
log.Infof("network change: connections marked stale")
|
||||
}
|
||||
|
||||
// Stop the internal client and free the resources
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
// Package netevents owns the OS network event handling shared by the mobile
|
||||
// bindings: availability changes park or wake the reconnection loops and drive
|
||||
// the NoNetwork listener state, and both losing the last network and switching
|
||||
// networks sweep the stale connections so their owners redial immediately.
|
||||
package netevents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/netevents/netstate"
|
||||
"github.com/netbirdio/netbird/client/netevents/sweep"
|
||||
)
|
||||
|
||||
// Recorder receives the availability changes for listener state reporting.
|
||||
type Recorder interface {
|
||||
SetNetworkAvailable(available bool)
|
||||
}
|
||||
|
||||
// Manager ties the network availability state, the connection sweeper and the
|
||||
// status recorder together; it outlives engine restarts. A nil *Manager is
|
||||
// the valid no-events value: the read methods report always-online and never
|
||||
// sweep.
|
||||
type Manager struct {
|
||||
netState *netstate.State
|
||||
sweeper *sweep.Sweeper
|
||||
recorder Recorder
|
||||
}
|
||||
|
||||
// NewManager creates a Manager reporting into recorder, starting online.
|
||||
func NewManager(recorder Recorder) *Manager {
|
||||
return &Manager{
|
||||
netState: netstate.New(),
|
||||
sweeper: sweep.New(),
|
||||
recorder: recorder,
|
||||
}
|
||||
}
|
||||
|
||||
// SetNetworkAvailable records OS-reported network availability. While
|
||||
// unavailable, the reconnection loops suspend their attempts and the
|
||||
// connection listener reports NoNetwork instead of Connecting; when
|
||||
// availability returns, the loops resume immediately with a fresh backoff.
|
||||
// Losing the last network also sweeps the registered connections: nothing can
|
||||
// redial while offline, so the stale sockets would otherwise stay silently
|
||||
// "connected" until their own timeouts and the client would keep reporting
|
||||
// Connected with no network at all.
|
||||
func (m *Manager) SetNetworkAvailable(available bool) {
|
||||
if !available && m.netState.IsOnline() {
|
||||
m.sweeper.MarkNetworkChange()
|
||||
}
|
||||
m.netState.Set(available)
|
||||
m.recorder.SetNetworkAvailable(available)
|
||||
}
|
||||
|
||||
// NotifyNetworkChange marks the management, signal and relay connections
|
||||
// stale after the OS switched networks and schedules a sweep that cuts
|
||||
// whatever has not redialed on the new network by then. The engine and the
|
||||
// TUN device stay untouched.
|
||||
func (m *Manager) NotifyNetworkChange() {
|
||||
m.sweeper.MarkNetworkChange()
|
||||
log.Infof("network change: connections marked stale")
|
||||
}
|
||||
|
||||
// IsOnline reports whether the OS reports at least one usable network.
|
||||
func (m *Manager) IsOnline() bool {
|
||||
if m == nil {
|
||||
return true
|
||||
}
|
||||
return m.netState.IsOnline()
|
||||
}
|
||||
|
||||
// Changed returns a channel closed on the next availability transition.
|
||||
func (m *Manager) Changed() <-chan struct{} {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return m.netState.Changed()
|
||||
}
|
||||
|
||||
// Wait blocks while the network is offline; see netstate.State.Wait.
|
||||
func (m *Manager) Wait(ctx context.Context) (bool, error) {
|
||||
if m == nil {
|
||||
return false, nil
|
||||
}
|
||||
return m.netState.Wait(ctx)
|
||||
}
|
||||
|
||||
// WaitSettled waits until an online verdict holds for a full settleWindow, or
|
||||
// while offline until the budget runs out. Returns false when ctx is
|
||||
// cancelled. The settle window exists because a disconnect often precedes the
|
||||
// OS offline flag by a few milliseconds, so a fresh online verdict cannot be
|
||||
// trusted immediately. A nil Manager has no events to watch: it degrades to a
|
||||
// fixed budget-long sleep.
|
||||
func (m *Manager) WaitSettled(ctx context.Context, budget, settleWindow time.Duration) bool {
|
||||
if m == nil {
|
||||
select {
|
||||
case <-time.After(budget):
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
budgetTimer := time.NewTimer(budget)
|
||||
defer budgetTimer.Stop()
|
||||
|
||||
settle := time.NewTimer(settleWindow)
|
||||
defer settle.Stop()
|
||||
|
||||
for {
|
||||
// Channel first, flag second: a flip in between still fires the channel.
|
||||
changedCh := m.netState.Changed()
|
||||
if m.netState.IsOnline() {
|
||||
select {
|
||||
case <-settle.C:
|
||||
return true
|
||||
case <-changedCh:
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
select {
|
||||
case <-budgetTimer.C:
|
||||
return true
|
||||
case <-changedCh:
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !settle.Stop() {
|
||||
select {
|
||||
case <-settle.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
settle.Reset(settleWindow)
|
||||
}
|
||||
}
|
||||
|
||||
// StartDial registers an in-flight dial with the sweeper; see sweep.Sweeper.StartDial.
|
||||
func (m *Manager) StartDial(ctx context.Context) *sweep.Dial {
|
||||
if m == nil {
|
||||
return (*sweep.Sweeper)(nil).StartDial(ctx)
|
||||
}
|
||||
return m.sweeper.StartDial(ctx)
|
||||
}
|
||||
|
||||
// QuickRetryBackoff wraps bo for a quick retry after a network change; see
|
||||
// sweep.Sweeper.QuickRetryBackoff.
|
||||
func (m *Manager) QuickRetryBackoff(ctx context.Context, bo backoff.BackOff) backoff.BackOff {
|
||||
if m == nil {
|
||||
return bo
|
||||
}
|
||||
return m.sweeper.QuickRetryBackoff(ctx, bo, m.netState)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package netevents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type recorderStub struct{}
|
||||
|
||||
func (recorderStub) SetNetworkAvailable(bool) {}
|
||||
|
||||
func TestWaitSettledAfterOutage(t *testing.T) {
|
||||
const budget = 1500 * time.Millisecond
|
||||
const settleWindow = 200 * time.Millisecond
|
||||
const outage = 2 * settleWindow
|
||||
|
||||
m := NewManager(recorderStub{})
|
||||
m.SetNetworkAvailable(false)
|
||||
|
||||
start := time.Now()
|
||||
go func() {
|
||||
time.Sleep(outage)
|
||||
m.SetNetworkAvailable(true)
|
||||
}()
|
||||
|
||||
ok := m.WaitSettled(context.Background(), budget, settleWindow)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
assert.True(t, ok, "recovered network must let the caller proceed")
|
||||
assert.GreaterOrEqual(t, elapsed, outage+settleWindow, "an online verdict must hold a full settle window before it is trusted")
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
// Package sweep cuts network-bound activity when the OS switches networks:
|
||||
// Package netsweep cuts network-bound activity when the OS switches networks:
|
||||
// a sweep closes the registered connections and aborts the in-flight dials, so
|
||||
// their owners redial immediately instead of waiting for the old sockets to
|
||||
// time out.
|
||||
//
|
||||
// A nil *Sweeper disables everything: all methods are nil-safe no-ops.
|
||||
package sweep
|
||||
package netsweep
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/netevents/netstate"
|
||||
"github.com/netbirdio/netbird/client/netstate"
|
||||
)
|
||||
|
||||
// DefaultSweepDelay absorbs network flapping while the OS settles on a
|
||||
@@ -34,7 +34,7 @@ type Config struct {
|
||||
// ErrSwept reports that a dial finished after a network change swept its
|
||||
// registration. The connection is already closed; the caller must treat it
|
||||
// as a failed dial and redial on the new network.
|
||||
var ErrSwept = errors.New("sweep: connection swept by network change")
|
||||
var ErrSwept = errors.New("netsweep: connection swept by network change")
|
||||
|
||||
// sweepID identifies one registration in a sweeper. Connections and dials
|
||||
// draw from the same counter, so an id is unique across both registries.
|
||||
@@ -1,4 +1,4 @@
|
||||
package sweep
|
||||
package netsweep
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,11 +1,11 @@
|
||||
package sweep
|
||||
package netsweep
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
|
||||
"github.com/netbirdio/netbird/client/netevents/netstate"
|
||||
"github.com/netbirdio/netbird/client/netstate"
|
||||
)
|
||||
|
||||
const quickRetryDelay = 200 * time.Millisecond
|
||||
@@ -1,4 +1,4 @@
|
||||
package sweep
|
||||
package netsweep
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -232,4 +232,3 @@ func toNetIDs(routes []string) []route.NetID {
|
||||
}
|
||||
return netIDs
|
||||
}
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve
|
||||
|
||||
requestBuffer := server.NewAccountRequestBuffer(context.Background(), store)
|
||||
peersUpdateManager := update_channel.NewPeersUpdateManager(metrics)
|
||||
networkMapController := controller.NewController(context.Background(), store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersManager), config)
|
||||
networkMapController := controller.NewController(context.Background(), store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersManager), config, nil)
|
||||
accountManager, err := server.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false, cacheStore)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
|
||||
9
go.mod
9
go.mod
@@ -72,17 +72,18 @@ require (
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/jackc/pgx/v5 v5.5.5
|
||||
github.com/jackc/pgx/v5 v5.10.0
|
||||
github.com/libdns/route53 v1.5.0
|
||||
github.com/libp2p/go-nat v0.2.0
|
||||
github.com/libp2p/go-netroute v0.4.0
|
||||
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81
|
||||
github.com/magefile/mage v1.17.2
|
||||
github.com/mdlayher/socket v0.5.1
|
||||
github.com/mdp/qrterminal/v3 v3.2.1
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/moby/moby/api v1.54.1
|
||||
github.com/netbirdio/management-integrations/integrations v0.0.0-20260416123949-2355d972be42
|
||||
github.com/netbirdio/management-integrations/integrations v0.0.0-20260803100840-78e79ba20f87
|
||||
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45
|
||||
github.com/oapi-codegen/runtime v1.1.2
|
||||
github.com/okta/okta-sdk-golang/v2 v2.18.0
|
||||
@@ -236,8 +237,8 @@ require (
|
||||
github.com/huin/goupnp v1.2.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
|
||||
18
go.sum
18
go.sum
@@ -341,12 +341,12 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
||||
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
@@ -415,6 +415,8 @@ github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81 h1:J56rFEfUTFT9j9
|
||||
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81/go.mod h1:RD8ML/YdXctQ7qbcizZkw5mZ6l8Ogrl1dodBzVJduwI=
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
|
||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/magefile/mage v1.17.2 h1:fyXVu1eadI8Ap1HCCNgEhJ5McIWiYhLR8uol64ZZc40=
|
||||
github.com/magefile/mage v1.17.2/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
@@ -482,8 +484,8 @@ github.com/netbirdio/easyjson v0.9.0 h1:6Nw2lghSVuy8RSkAYDhDv1thBVEmfVbKZnV7T7Z6
|
||||
github.com/netbirdio/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51 h1:Ov4qdafATOgGMB1wbSuh+0aAHcwz9hdvB6VZjh1mVMI=
|
||||
github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51/go.mod h1:ZSIbPdBn5hePO8CpF1PekH2SfpTxg1PDhEwtbqZS7R8=
|
||||
github.com/netbirdio/management-integrations/integrations v0.0.0-20260416123949-2355d972be42 h1:F3zS5fT9xzD1OFLfcdAE+3FfyiwjGukF1hvj0jErgs8=
|
||||
github.com/netbirdio/management-integrations/integrations v0.0.0-20260416123949-2355d972be42/go.mod h1:n47r67ZSPgwSmT/Z1o48JjZQW9YJ6m/6Bd/uAXkL3Pg=
|
||||
github.com/netbirdio/management-integrations/integrations v0.0.0-20260803100840-78e79ba20f87 h1:iJeUvSMC0BTpkw7u4JyWcY4/3dl7fEL9DR/TpKf2+1w=
|
||||
github.com/netbirdio/management-integrations/integrations v0.0.0-20260803100840-78e79ba20f87/go.mod h1:pmsCPx1S0nuZRxCextGpc9AV4hLgGSuTsc4NMuwGeCo=
|
||||
github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8=
|
||||
github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
||||
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 h1:ujgviVYmx243Ksy7NdSwrdGPSRNE3pb8kEDSpH0QuAQ=
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetAccountSettings(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
execQuery(t, ctx,
|
||||
`insert into accounts (id, settings_peer_login_expiration_enabled, settings_peer_login_expiration, settings_peer_inactivity_expiration_enabled,
|
||||
settings_peer_inactivity_expiration, settings_dns_domain, settings_ipv6_enabled_groups, settings_routing_peer_dns_resolution_enabled,
|
||||
settings_lazy_connection_enabled, settings_auto_update_version, settings_auto_update_always, settings_metrics_push_enabled)
|
||||
values('account-3',null,null,null,null,null,null,null,null,null,null,null)`)
|
||||
|
||||
accountSettings, err := conn(t, ctx).GetAccountSettings(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, accountSettings, nmdata.AccountSettingsInfo{
|
||||
PeerLoginExpirationEnabled: true,
|
||||
PeerLoginExpiration: 86400000000000 * time.Nanosecond,
|
||||
PeerInactivityExpirationEnabled: false,
|
||||
PeerInactivityExpiration: 86400000000000 * time.Nanosecond,
|
||||
DNSDomain: "",
|
||||
IPv6EnabledGroups: []string{"group-one-resource-id"},
|
||||
RoutingPeerDNSResolutionEnabled: false,
|
||||
LazyConnectionEnabled: false,
|
||||
AutoUpdateVersion: "disabled",
|
||||
AutoUpdateAlways: false,
|
||||
MetricsPushEnabled: false,
|
||||
})
|
||||
|
||||
accountSettings, err = conn(t, ctx).GetAccountSettings(ctx, "account-2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, accountSettings, nmdata.AccountSettingsInfo{
|
||||
PeerLoginExpirationEnabled: true,
|
||||
PeerLoginExpiration: 86400000000000 * time.Nanosecond,
|
||||
PeerInactivityExpirationEnabled: false,
|
||||
PeerInactivityExpiration: 86400000000000 * time.Nanosecond,
|
||||
DNSDomain: "",
|
||||
IPv6EnabledGroups: []string{"group-two-resources-id"},
|
||||
RoutingPeerDNSResolutionEnabled: false,
|
||||
LazyConnectionEnabled: false,
|
||||
AutoUpdateVersion: "disabled",
|
||||
AutoUpdateAlways: false,
|
||||
MetricsPushEnabled: false,
|
||||
})
|
||||
|
||||
accountSettings, err = conn(t, ctx).GetAccountSettings(ctx, "account-3")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, accountSettings, nmdata.AccountSettingsInfo{})
|
||||
}
|
||||
53
integration_tests/management/network_map_db/base_data.sql
Normal file
53
integration_tests/management/network_map_db/base_data.sql
Normal file
@@ -0,0 +1,53 @@
|
||||
insert into accounts (id, network_identifier, network_net, network_net_v6, network_dns, network_serial,dns_settings_disabled_management_groups,
|
||||
settings_peer_login_expiration_enabled, settings_peer_login_expiration, settings_peer_inactivity_expiration_enabled,
|
||||
settings_peer_inactivity_expiration, settings_dns_domain, settings_ipv6_enabled_groups, settings_routing_peer_dns_resolution_enabled,
|
||||
settings_lazy_connection_enabled, settings_auto_update_version, settings_auto_update_always, settings_metrics_push_enabled)
|
||||
VALUES('account-1','network-1','{"IP":"100.103.0.0","Mask":"//8AAA=="}','{"IP":"fdde:e995:fd38:a465::","Mask":"//////////8AAAAAAAAAAA=="}','',1,'["disabled-group-1","disabled-group-2"]',
|
||||
true, 86400000000000, false,
|
||||
86400000000000, null, '["group-one-resource-id"]', false,
|
||||
false, 'disabled', false, false);
|
||||
insert into accounts (id, network_identifier, network_net, network_net_v6, network_dns, network_serial,dns_settings_disabled_management_groups,
|
||||
settings_peer_login_expiration_enabled, settings_peer_login_expiration, settings_peer_inactivity_expiration_enabled,
|
||||
settings_peer_inactivity_expiration, settings_dns_domain, settings_ipv6_enabled_groups, settings_routing_peer_dns_resolution_enabled,
|
||||
settings_lazy_connection_enabled, settings_auto_update_version, settings_auto_update_always, settings_metrics_push_enabled)
|
||||
VALUES('account-2','network-2','{"IP":"110.0.0.0","Mask":"//8AAA=="}','{"IP":"fddf:e995:fd38:a465::","Mask":"//////////8AAAAAAAAAAA=="}','',2,null,
|
||||
true, 86400000000000, false,
|
||||
86400000000000, null, '["group-two-resources-id"]', false,
|
||||
false, 'disabled', false, false);
|
||||
insert into groups (id, account_id, name, resources, public_id) VALUES('group-one-resource-id','account-1','group-1-name', '[{"ID":"host-id-1","Type":"host"}]','group-one-resource-id-public');
|
||||
insert into groups (id, account_id, name, resources, public_id) VALUES('group-two-resources-id','account-1','group-2-name', '[{"ID":"subnet-id-1","Type":"subnet"}, {"ID":"host-id-2","Type":"host"}]','group-two-resources-id-public');
|
||||
insert into groups (id, account_id, name, resources, public_id) VALUES('group-no-resources-id','account-1','group-3-name', null,'group-no-resources-id-public');
|
||||
insert into group_peers (account_id, peer_id, group_id) VALUES('account-1','peer-id-1','group-one-resource-id');
|
||||
insert into group_peers (account_id, peer_id, group_id) VALUES('account-1','peer-id-2','group-two-resources-id');
|
||||
insert into group_peers (account_id, peer_id, group_id) VALUES('account-1','peer-id-3','group-two-resources-id');
|
||||
insert into peers (id, account_id, "key", ssh_key, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
|
||||
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, proxy_meta_cluster,
|
||||
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files,
|
||||
meta_capabilities, meta_flags, meta_sync_message_version,
|
||||
location_country_code, location_city_name, location_connection_ip)
|
||||
values('peer-id-1','account-1','key-1','ssh-key-1','peer-1','["extra-peer-1"]','user-id-1',true,true,'2026-08-06 13:25:59.12999','"10.10.10.1"','"fdf4:ba80:6aa5:89f1:44d7:8701:8699:4940"',
|
||||
false,true,true,'cluster-1.netbird.services',
|
||||
'0.76.0','linux','26.4.1','6.8.0-134-generic','[{"NetIP":"fe80::8b4c:973f:a76b:3771/64","Mac":"00:15:5d:24:0c:ac"},{"NetIP":"192.168.16.1/20","Mac":"00:15:5d:24:0c:ac"}]','[{"Path":"/usr/bin/netbird","Exist":false,"ProcessIsRunning":false}]',
|
||||
'[1,2]','{"RosenpassEnabled":false,"RosenpassPermissive":false,"ServerSSHAllowed":true,"DisableClientRoutes":false,"DisableServerRoutes":false,"DisableDNS":false,"DisableFirewall":false,"BlockLANAccess":false,"BlockInbound":false,"DisableIPv6":false,"LazyConnectionEnabled":false}',1,
|
||||
'DE','Berlin','"46.201.148.187"');
|
||||
insert into peers (id,account_id,"key", ssh_key, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
|
||||
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, proxy_meta_cluster,
|
||||
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files,
|
||||
meta_capabilities, meta_flags, meta_sync_message_version,
|
||||
location_country_code, location_city_name, location_connection_ip)
|
||||
values('peer-id-2','account-1','key-2','ssh-key-2','peer-2','["extra-peer-2"]','user-id-2',true,true,'2026-08-06 14:25:59.12999','"10.10.100.1"','"fdf5:ba80:6aa5:89f1:44d7:8701:8699:4940"',
|
||||
false,true,true,'cluster-2.netbird.services',
|
||||
'0.76.1','linux','26.4.2','6.8.0-135-generic','[{"NetIP":"fe81::8b4c:973f:a76b:3771/64","Mac":"00:15:5d:24:0c:ad"},{"NetIP":"192.168.17.1/20","Mac":"00:15:5d:24:0c:ad"}]','[{"Path":"/usr/bin/netbird","Exist":false,"ProcessIsRunning":false}]',
|
||||
'[1,2]','{"RosenpassEnabled":false,"RosenpassPermissive":false,"ServerSSHAllowed":true,"DisableClientRoutes":false,"DisableServerRoutes":false,"DisableDNS":false,"DisableFirewall":false,"BlockLANAccess":false,"BlockInbound":false,"DisableIPv6":false,"LazyConnectionEnabled":false}',0,
|
||||
'DE','Berlin','"46.201.149.187"');
|
||||
insert into peers (id,account_id,"key", ssh_key, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
|
||||
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, proxy_meta_cluster,
|
||||
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files,
|
||||
meta_capabilities, meta_flags, meta_sync_message_version,
|
||||
location_country_code, location_city_name, location_connection_ip)
|
||||
values('peer-id-3','account-1','key-3','ssh-key-3','peer-3','["extra-peer-3"]','user-id-3',true,true,'2026-08-06 12:25:59.12999','"10.10.200.1"','"fdf6:ba80:6aa5:89f1:44d7:8701:8699:4940"',
|
||||
false,true,true,'cluster-3.netbird.services',
|
||||
'0.76.2','linux','26.4.3','6.8.0-136-generic','[{"NetIP":"fe82::8b4c:973f:a76b:3771/64","Mac":"00:15:5d:24:0c:ae"},{"NetIP":"192.168.18.1/20","Mac":"00:15:5d:24:0c:ae"}]','[{"Path":"/usr/bin/netbird","Exist":false,"ProcessIsRunning":false}]',
|
||||
'[1,2]','{"RosenpassEnabled":false,"RosenpassPermissive":false,"ServerSSHAllowed":true,"DisableClientRoutes":false,"DisableServerRoutes":false,"DisableDNS":false,"DisableFirewall":false,"BlockLANAccess":false,"BlockInbound":false,"DisableIPv6":false,"LazyConnectionEnabled":false}',1,
|
||||
'DE','Berlin','"46.201.150.187"');
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetDnsSettings(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
settings, err := conn(t, ctx).GetDnsSettings(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, settings, nmdata.DNSSettings{
|
||||
DisabledManagementGroups: []string{"disabled-group-1", "disabled-group-2"},
|
||||
})
|
||||
|
||||
settings, err = conn(t, ctx).GetDnsSettings(ctx, "account-2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, settings, nmdata.DNSSettings{})
|
||||
}
|
||||
62
integration_tests/management/network_map_db/dns_test.go
Normal file
62
integration_tests/management/network_map_db/dns_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetAppliedZoneCandidatesViaPgxConnection(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
execQuery(t, ctx,
|
||||
`insert into zones (id, account_id, domain, enable_search_domain, distribution_groups)
|
||||
VALUES('zone-1','account-1','test-1.com',true,'["group-one-resource-id"]')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into zones (id, account_id, domain, enable_search_domain, distribution_groups)
|
||||
VALUES('zone-2','account-1','test-2.com',false,'["group-two-resources-id"]')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into records (id, account_id, zone_id, name, type, ttl, content)
|
||||
VALUES('record-1','account-1','zone-1','test.test-1.com','A',1800,'1.1.1.1')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into records (id, account_id, zone_id, name, type, ttl, content)
|
||||
VALUES('record-2','account-1','zone-1','test2.test-1.com','A',1800,'1.1.1.2')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into records (id, account_id, zone_id, name, type, ttl, content)
|
||||
VALUES('record-3','account-1','zone-1','test3.test-1.com','CNAME',1800,'test4.test-1.com')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into records (id, account_id, zone_id, name, type, ttl, content)
|
||||
VALUES('record-4','account-1','zone-2','test2.test-2.com','CNAME',1800,'test3.test-2.com')`)
|
||||
|
||||
zoneCandidates, err := conn(t, ctx).GetAppliedZoneCandidates(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Contains(t, zoneCandidates, networkmap.AppliedZoneCandidate{
|
||||
DistributionGroups: []string{"group-one-resource-id"},
|
||||
Zone: nmdata.CustomZone{
|
||||
Domain: "test-1.com",
|
||||
SearchDomainDisabled: false,
|
||||
Records: []nmdata.SimpleRecord{
|
||||
{Name: "test.test-1.com", Type: int(dns.TypeA), Class: "IN", TTL: 1800, RData: "1.1.1.1"},
|
||||
{Name: "test2.test-1.com", Type: int(dns.TypeA), Class: "IN", TTL: 1800, RData: "1.1.1.2"},
|
||||
{Name: "test3.test-1.com", Type: int(dns.TypeCNAME), Class: "IN", TTL: 1800, RData: "test4.test-1.com."},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Contains(t, zoneCandidates, networkmap.AppliedZoneCandidate{
|
||||
DistributionGroups: []string{"group-two-resources-id"},
|
||||
Zone: nmdata.CustomZone{
|
||||
Domain: "test-2.com",
|
||||
SearchDomainDisabled: true,
|
||||
Records: []nmdata.SimpleRecord{
|
||||
{Name: "test2.test-2.com", Type: int(dns.TypeCNAME), Class: "IN", TTL: 1800, RData: "test3.test-2.com."},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
39
integration_tests/management/network_map_db/domain_test.go
Normal file
39
integration_tests/management/network_map_db/domain_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetDomains(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
execQuery(t, ctx,
|
||||
`insert into domains (id, account_id, domain, target_cluster)
|
||||
VALUES('domain-1','account-1','test-1.com','target-1.cluster.local')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into domains (id, account_id, domain, target_cluster)
|
||||
VALUES('domain-2','account-1','test-2.com','target-2.cluster.local')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into domains (id, account_id, domain, target_cluster)
|
||||
VALUES('domain-3','account-1',null,null)`)
|
||||
|
||||
domains, err := conn(t, ctx).GetDomains(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, domains, 2)
|
||||
|
||||
assert.Contains(t, domains, networkmapdb.Domain{
|
||||
Domain: sql.NullString{String: "test-1.com", Valid: true},
|
||||
TargetCluster: sql.NullString{String: "target-1.cluster.local", Valid: true},
|
||||
})
|
||||
assert.Contains(t, domains, networkmapdb.Domain{
|
||||
Domain: sql.NullString{String: "test-2.com", Valid: true},
|
||||
TargetCluster: sql.NullString{String: "target-2.cluster.local", Valid: true},
|
||||
})
|
||||
}
|
||||
54
integration_tests/management/network_map_db/group_test.go
Normal file
54
integration_tests/management/network_map_db/group_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetGroups(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
groups, resourceToGroupIdx, err := conn(t, ctx).GetGroups(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t,
|
||||
groups,
|
||||
nmdata.Group{ID: "group-one-resource-id", Name: "group-1-name", PublicID: "group-one-resource-id-public", Resources: []nmdata.Resource{{ID: "host-id-1", Type: "host"}}, Peers: []string{"peer-id-1"}},
|
||||
)
|
||||
assert.NotNil(t, resourceToGroupIdx["host-id-1"]["group-one-resource-id"])
|
||||
assert.Contains(t,
|
||||
groups,
|
||||
nmdata.Group{ID: "group-two-resources-id", Name: "group-2-name", PublicID: "group-two-resources-id-public",
|
||||
Resources: []nmdata.Resource{{ID: "subnet-id-1", Type: "subnet"}, {ID: "host-id-2", Type: "host"}},
|
||||
Peers: []string{"peer-id-2", "peer-id-3"}},
|
||||
)
|
||||
assert.NotNil(t, resourceToGroupIdx["host-id-2"]["group-two-resources-id"])
|
||||
assert.NotNil(t, resourceToGroupIdx["subnet-id-1"]["group-two-resources-id"])
|
||||
assert.Contains(t,
|
||||
groups,
|
||||
nmdata.Group{ID: "group-no-resources-id", Name: "group-3-name", PublicID: "group-no-resources-id-public"})
|
||||
}
|
||||
|
||||
// Verify handling of empty fields in groups table
|
||||
// Verify that group's PublicID gets populated on retrieval
|
||||
// TODO (dmitri) PublicID should not be populated with delta updates,
|
||||
// which require stable PublicIDs
|
||||
func TestGetGroupsWithoutExpectedFields(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
execQuery(t, ctx,
|
||||
"insert into accounts (id) VALUES('random-id')")
|
||||
|
||||
execQuery(t, ctx,
|
||||
"insert into groups (id, account_id) VALUES('g2-test-group-id-1','random-id')")
|
||||
|
||||
groups, _, err := conn(t, ctx).GetGroups(ctx, "random-id")
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, groups, 1)
|
||||
assert.NotEmpty(t, groups[0].PublicID)
|
||||
}
|
||||
99
integration_tests/management/network_map_db/main_test.go
Normal file
99
integration_tests/management/network_map_db/main_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db"
|
||||
networkmap_pgsql "github.com/netbirdio/netbird/management/internals/network_map_db/pgsql"
|
||||
networkmap_sqlite "github.com/netbirdio/netbird/management/internals/network_map_db/sqlite"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
|
||||
//go:embed base_data.sql
|
||||
var baseData string
|
||||
|
||||
var (
|
||||
pgstore *networkmap_pgsql.PgStore
|
||||
sqlitestore *networkmap_sqlite.SqliteStore
|
||||
engine string
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var cleanup func()
|
||||
kind, _ := os.LookupEnv("NETBIRD_STORE_ENGINE")
|
||||
switch kind {
|
||||
case string(types.PostgresStoreEngine):
|
||||
engine = string(types.PostgresStoreEngine)
|
||||
pgstore, cleanup = createPGTestStore(baseData)
|
||||
pgstore.UsingTimeZone(time.UTC)
|
||||
case "", string(types.SqliteStoreEngine):
|
||||
engine = string(types.SqliteStoreEngine)
|
||||
sqlitestore, cleanup = createSqliteTestStore(baseData)
|
||||
default:
|
||||
log.Fatalf("unsupported db '%s' in NETBIRD_STORE_ENGINE env var", kind)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
cleanup()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func conn(t *testing.T, ctx context.Context) networkmapdb.NetworkMapDBStoreConn {
|
||||
t.Helper()
|
||||
switch engine {
|
||||
case string(types.PostgresStoreEngine):
|
||||
c, err := pgstore.Pool.Acquire(ctx)
|
||||
assert.NoError(t, err)
|
||||
return pgstore.UsingConnection(c.Conn())
|
||||
case string(types.SqliteStoreEngine):
|
||||
return sqlitestore.UsingConn()
|
||||
}
|
||||
log.Fatalf("unknown db engine kind %s", engine)
|
||||
return nil
|
||||
}
|
||||
|
||||
func store(t *testing.T) networkmapdb.NetworkMapDBStore {
|
||||
t.Helper()
|
||||
switch engine {
|
||||
case string(types.PostgresStoreEngine):
|
||||
return pgstore
|
||||
case string(types.SqliteStoreEngine):
|
||||
return sqlitestore
|
||||
}
|
||||
log.Fatalf("unknown db engine kind %s", engine)
|
||||
return nil
|
||||
}
|
||||
|
||||
func execQuery(t *testing.T, ctx context.Context, q string) {
|
||||
t.Helper()
|
||||
switch engine {
|
||||
case string(types.PostgresStoreEngine):
|
||||
_, err := pgstore.Pool.Exec(ctx, q)
|
||||
assert.NoError(t, err)
|
||||
case string(types.SqliteStoreEngine):
|
||||
_, err := sqlitestore.Db.ExecContext(ctx, q)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// use to parse time in time.RFC3339Nano format
|
||||
// returns the time in the UTC time zone
|
||||
func mustParseTime(t string) *time.Time {
|
||||
tt, err := time.Parse(time.RFC3339Nano, t)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
utc := tt.UTC()
|
||||
return &utc
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetNameServerGroups(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
execQuery(t, ctx,
|
||||
`insert into name_server_groups (id, public_id, name, description, name_servers, groups, domains, enabled, search_domains_enabled, "primary", account_id)
|
||||
VALUES('nsgroup-1','nsgroup-1-public','nsgroup-1','nsgroup-1','[{"IP":"192.168.31.2","NSType":1,"Port":53}]','["group-one-resource-id"]','["test-1.com"]',TRUE,FALSE,TRUE,'account-1')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into name_server_groups (id, public_id, name, description, name_servers, groups, domains, enabled, search_domains_enabled,"primary",account_id)
|
||||
VALUES('nsgroup-2','nsgroup-2-public','nsgroup-2','nsgroup-2','[{"IP":"192.168.32.3","NSType":1,"Port":53}]','["group-one-resource-id","group-no-resources-id"]','["test-1.com","test-2.com"]',TRUE,FALSE,TRUE,'account-1')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into name_server_groups (id, public_id, name, description, name_servers, groups, domains, enabled, search_domains_enabled,"primary",account_id)
|
||||
VALUES('nsgroup-3','nsgroup-3-public',null,null,null,null,null,TRUE,FALSE,FALSE,'account-1')`)
|
||||
|
||||
nsgroups, err := conn(t, ctx).GetNameServerGroups(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Contains(t, nsgroups, nmdata.NameServerGroup{
|
||||
ID: "nsgroup-1",
|
||||
PublicID: "nsgroup-1-public",
|
||||
Name: "nsgroup-1",
|
||||
Description: "nsgroup-1",
|
||||
NameServers: []nmdata.NameServer{{IP: netip.MustParseAddr("192.168.31.2"), NSType: 1, Port: 53}},
|
||||
Groups: []string{"group-one-resource-id"},
|
||||
Domains: []string{"test-1.com"},
|
||||
Primary: true,
|
||||
SearchDomainsEnabled: false,
|
||||
Enabled: true,
|
||||
})
|
||||
assert.Contains(t, nsgroups, nmdata.NameServerGroup{
|
||||
ID: "nsgroup-2",
|
||||
PublicID: "nsgroup-2-public",
|
||||
Name: "nsgroup-2",
|
||||
Description: "nsgroup-2",
|
||||
NameServers: []nmdata.NameServer{{IP: netip.MustParseAddr("192.168.32.3"), NSType: 1, Port: 53}},
|
||||
Groups: []string{"group-one-resource-id", "group-no-resources-id"},
|
||||
Domains: []string{"test-1.com", "test-2.com"},
|
||||
Primary: true,
|
||||
SearchDomainsEnabled: false,
|
||||
Enabled: true,
|
||||
})
|
||||
assert.Contains(t, nsgroups, nmdata.NameServerGroup{
|
||||
ID: "nsgroup-3",
|
||||
PublicID: "nsgroup-3-public",
|
||||
Primary: false,
|
||||
SearchDomainsEnabled: false,
|
||||
Enabled: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
insert into accounts (id, network_identifier, network_net, network_net_v6, network_dns, network_serial,dns_settings_disabled_management_groups,
|
||||
settings_peer_login_expiration_enabled, settings_peer_login_expiration, settings_peer_inactivity_expiration_enabled,
|
||||
settings_peer_inactivity_expiration, settings_dns_domain, settings_ipv6_enabled_groups, settings_routing_peer_dns_resolution_enabled,
|
||||
settings_lazy_connection_enabled, settings_auto_update_version, settings_auto_update_always, settings_metrics_push_enabled)
|
||||
VALUES('account-33','network-331','{"IP":"100.103.0.0","Mask":"//8AAA=="}','{"IP":"fdde:e995:fd38:a465::","Mask":"//////////8AAAAAAAAAAA=="}','',1,'["disabled-group-1","disabled-group-2"]',
|
||||
true, 86400000000000, false,
|
||||
86400000000000, null, '["33-group-one-resource-id"]', false,
|
||||
false, 'disabled', false, false);
|
||||
insert into groups (id, account_id, name, resources, public_id) VALUES('33-group-one-resource-id','account-33','group-1-name', '[{"ID":"host-id-1","Type":"host"}]','group-one-resource-id-public');
|
||||
insert into groups (id, account_id, name, resources, public_id) VALUES('33-group-two-resources-id','account-33','group-2-name', '[{"ID":"subnet-id-1","Type":"subnet"}, {"ID":"host-id-2","Type":"host"}]','33-group-two-resources-id-public');
|
||||
insert into groups (id, account_id, name, resources, public_id) VALUES('33-group-no-resources-id','account-33','group-3-name', null,'33-group-no-resources-id-public');
|
||||
insert into group_peers (account_id, peer_id, group_id) VALUES('account-33','peer-id-331','33-group-one-resource-id');
|
||||
insert into group_peers (account_id, peer_id, group_id) VALUES('account-33','peer-id-332','33-group-two-resources-id');
|
||||
insert into group_peers (account_id, peer_id, group_id) VALUES('account-33','peer-id-333','33-group-two-resources-id');
|
||||
insert into peers (id, account_id, "key", ssh_key, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
|
||||
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, proxy_meta_cluster,
|
||||
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files,
|
||||
meta_capabilities, meta_flags, meta_sync_message_version,
|
||||
location_country_code, location_city_name, location_connection_ip)
|
||||
values('peer-id-331','account-33','key-331','ssh-key-1','peer-1','["extra-peer-1"]','user-id-1',true,true,'2026-08-06 13:25:59.12999','"10.10.10.1"','"fdf4:ba80:6aa5:89f1:44d7:8701:8699:4940"',
|
||||
false,true,true,'cluster-1.netbird.services',
|
||||
'0.76.0','linux','26.4.1','6.8.0-134-generic','[{"NetIP":"fe80::8b4c:973f:a76b:3771/64","Mac":"00:15:5d:24:0c:ac"},{"NetIP":"192.168.16.1/20","Mac":"00:15:5d:24:0c:ac"}]','[{"Path":"/usr/bin/netbird","Exist":false,"ProcessIsRunning":false}]',
|
||||
'[1,2]','{"RosenpassEnabled":false,"RosenpassPermissive":false,"ServerSSHAllowed":true,"DisableClientRoutes":false,"DisableServerRoutes":false,"DisableDNS":false,"DisableFirewall":false,"BlockLANAccess":false,"BlockInbound":false,"DisableIPv6":false,"LazyConnectionEnabled":false}',1,
|
||||
'DE','Berlin','"46.201.148.187"');
|
||||
insert into peers (id,account_id,"key", ssh_key, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
|
||||
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, proxy_meta_cluster,
|
||||
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files,
|
||||
meta_capabilities, meta_flags, meta_sync_message_version,
|
||||
location_country_code, location_city_name, location_connection_ip)
|
||||
values('peer-id-332','account-33','key-332','ssh-key-2','peer-2','["extra-peer-2"]','user-id-2',true,true,'2026-08-06 14:25:59.12999','"10.10.100.1"','"fdf5:ba80:6aa5:89f1:44d7:8701:8699:4940"',
|
||||
false,true,true,'cluster-2.netbird.services',
|
||||
'0.76.1','linux','26.4.2','6.8.0-135-generic','[{"NetIP":"fe81::8b4c:973f:a76b:3771/64","Mac":"00:15:5d:24:0c:ad"},{"NetIP":"192.168.17.1/20","Mac":"00:15:5d:24:0c:ad"}]','[{"Path":"/usr/bin/netbird","Exist":false,"ProcessIsRunning":false}]',
|
||||
'[1,2]','{"RosenpassEnabled":false,"RosenpassPermissive":false,"ServerSSHAllowed":true,"DisableClientRoutes":false,"DisableServerRoutes":false,"DisableDNS":false,"DisableFirewall":false,"BlockLANAccess":false,"BlockInbound":false,"DisableIPv6":false,"LazyConnectionEnabled":false}',0,
|
||||
'DE','Berlin','"46.201.149.187"');
|
||||
insert into peers (id,account_id,"key", ssh_key, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
|
||||
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, proxy_meta_cluster,
|
||||
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files,
|
||||
meta_capabilities, meta_flags, meta_sync_message_version,
|
||||
location_country_code, location_city_name, location_connection_ip)
|
||||
values('peer-id-333','account-33','key-333','ssh-key-3','peer-3','["extra-peer-3"]','user-id-3',true,true,'2026-08-06 12:25:59.12999','"10.10.200.1"','"fdf6:ba80:6aa5:89f1:44d7:8701:8699:4940"',
|
||||
false,true,true,'cluster-3.netbird.services',
|
||||
'0.76.2','linux','26.4.3','6.8.0-136-generic','[{"NetIP":"fe82::8b4c:973f:a76b:3771/64","Mac":"00:15:5d:24:0c:ae"},{"NetIP":"192.168.18.1/20","Mac":"00:15:5d:24:0c:ae"}]','[{"Path":"/usr/bin/netbird","Exist":false,"ProcessIsRunning":false}]',
|
||||
'[1,2]','{"RosenpassEnabled":false,"RosenpassPermissive":false,"ServerSSHAllowed":true,"DisableClientRoutes":false,"DisableServerRoutes":false,"DisableDNS":false,"DisableFirewall":false,"BlockLANAccess":false,"BlockInbound":false,"DisableIPv6":false,"LazyConnectionEnabled":false}',1,
|
||||
'DE','Berlin','"46.201.150.187"');
|
||||
|
||||
insert into zones (id, account_id, domain, enable_search_domain, distribution_groups)
|
||||
VALUES('zone-331','account-33','test-331.com',true,'["33-group-one-resource-id"]');
|
||||
insert into records (id, account_id, zone_id, name, type, ttl, content)
|
||||
VALUES('record-331','account-33','zone-331','test.test-331.com','A',1800,'1.1.1.1');
|
||||
insert into records (id, account_id, zone_id, name, type, ttl, content)
|
||||
VALUES('record-332','account-33','zone-331','test2.test-331.com','A',1800,'1.1.1.2');
|
||||
|
||||
insert into domains (id, account_id, domain, target_cluster)
|
||||
VALUES('domain-331','account-33','test-331.com','target-1.cluster.local');
|
||||
|
||||
insert into name_server_groups (id, public_id, name, description, name_servers, groups, domains, enabled, search_domains_enabled, "primary", account_id)
|
||||
VALUES('nsgroup-331','nsgroup-1-public','nsgroup-1','nsgroup-1','[{"IP":"192.168.31.2","NSType":1,"Port":53}]','["33-group-one-resource-id"]','["test-1.com"]',TRUE,FALSE,TRUE,'account-33');
|
||||
insert into name_server_groups (id, public_id, name, description, name_servers, groups, domains, enabled, search_domains_enabled,"primary",account_id)
|
||||
VALUES('nsgroup-332','nsgroup-2-public','nsgroup-2','nsgroup-2','[{"IP":"192.168.32.3","NSType":1,"Port":53}]','["33-group-one-resource-id","33-group-no-resources-id"]','["test-1.com","test-2.com"]',TRUE,FALSE,TRUE,'account-33');
|
||||
|
||||
insert into network_resources (id, account_id, network_id, public_id, name, description, type, domain, prefix, enabled)
|
||||
VALUES('net-resource-331','account-33','network-331','net-resource-public-1','network-resource-1','network-resource-1','subnet','','"10.0.0.0/16"',TRUE);
|
||||
insert into network_resources (id, account_id, network_id, public_id, name, description, type, domain, prefix, enabled)
|
||||
VALUES('net-resource-332','account-33','network-332','net-resource-public-2','network-resource-2','network-resource-2','domain','test.com','',TRUE);
|
||||
|
||||
insert into network_routers (id, account_id, public_id, peer, network_id, masquerade, metric, enabled, peer_groups)
|
||||
VALUES('test-nr-id-331','account-33','public-id-1','peer-id-331','network-id-1',TRUE,999,TRUE,'["33-group-one-resource-id"]');
|
||||
insert into network_routers (id, account_id, public_id, peer, network_id, masquerade, metric, enabled, peer_groups)
|
||||
VALUES('test-nr-id-332','account-33','public-id-2','','network-id-2',TRUE,333,TRUE,'["33-group-two-resources-id","33-group-no-resources-id"]');
|
||||
|
||||
insert into networks (id, account_id, public_id) VALUES('network-331','account-33','network-1-public');
|
||||
insert into networks (id, account_id, public_id) VALUES('network-332','account-33','network-2-public');
|
||||
|
||||
insert into policies (id, public_id, account_id, enabled, source_posture_checks)
|
||||
values('policy-331','policy-1-public','account-33',true,'["posture-checks-1","posture-checks-2"]');
|
||||
insert into policy_rules (id, policy_id, enabled, action, protocol, bidirectional, sources, destinations,
|
||||
source_resource, destination_resource, ports, port_ranges,
|
||||
authorized_groups, authorized_user)
|
||||
values('policy-331-rule-1','policy-331',true,'accept','tcp',true,'["33-group-one-resource-id","33-group-two-resources-id"]','["33-group-one-resource-id","33-group-two-resources-id"]',
|
||||
'{"ID":"host-id-1","Type":"host"}','{"ID":"domain-331","Type":"domain"}','["8080","8443"]', '[{"Start":8080,"End":8090}]',
|
||||
'{"33-group-one-resource-id":["user-1", "user-2"]}','user-3');
|
||||
|
||||
insert into posture_checks (id, account_id, public_id, checks)
|
||||
VALUES('posturecheck-331','account-33','posturecheck-1-public',
|
||||
'{"NBVersionCheck":{"MinVersion":"0.25.0"},
|
||||
"OSVersionCheck":{"Darwin":{"MinVersion":"12.0"}},
|
||||
"GeoLocationCheck":{"Locations":[{"CountryCode":"FI","CityName":""}],"Action":"allow"},
|
||||
"PeerNetworkRangeCheck":{"Action":"deny","Ranges":["192.168.0.1/24"]}}');
|
||||
|
||||
insert into routes (id, account_id, public_id, network, domains, keep_route, net_id, description,
|
||||
peer, peer_groups, network_type, masquerade, metric, enabled,
|
||||
groups, access_control_groups, skip_auto_apply)
|
||||
VALUES('route-331','account-33','route-1-public','"172.0.0.0/16"','["test-1.com"]',true,'route-331-net-id','route-1',
|
||||
'peer-id-331','["33-group-one-resource-id"]',1,true,9999,true,
|
||||
'["33-group-one-resource-id"]','["33-group-one-resource-id"]',false);
|
||||
|
||||
insert into services (id, account_id, enabled, private, access_groups, proxy_cluster, domain)
|
||||
values('service-331','account-33',true,true,'["33-group-one-resource-id"]','test-1.com','test-332.com');
|
||||
@@ -0,0 +1,516 @@
|
||||
{
|
||||
"Peers": {
|
||||
"peer-id-331": {
|
||||
"ID": "peer-id-331",
|
||||
"Key": "key-331",
|
||||
"SSHKey": "ssh-key-1",
|
||||
"DNSLabel": "peer-1",
|
||||
"UserID": "user-id-1",
|
||||
"SSHEnabled": true,
|
||||
"LoginExpirationEnabled": true,
|
||||
"LastLogin": "2026-08-06T13:25:59.12999Z",
|
||||
"IP": "10.10.10.1",
|
||||
"IPv6": "fdf4:ba80:6aa5:89f1:44d7:8701:8699:4940",
|
||||
"RequiresApproval": false,
|
||||
"ExtraDNSLabels": [
|
||||
"extra-peer-1"
|
||||
],
|
||||
"Meta": {
|
||||
"WtVersion": "0.76.0",
|
||||
"GoOS": "linux",
|
||||
"OSVersion": "26.4.1",
|
||||
"KernelVersion": "6.8.0-134-generic",
|
||||
"NetworkAddresses": [
|
||||
{
|
||||
"NetIP": "fe80::8b4c:973f:a76b:3771/64"
|
||||
},
|
||||
{
|
||||
"NetIP": "192.168.16.1/20"
|
||||
}
|
||||
],
|
||||
"Files": [
|
||||
{
|
||||
"Path": "/usr/bin/netbird",
|
||||
"ProcessIsRunning": false
|
||||
}
|
||||
],
|
||||
"Capabilities": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"Flags": {
|
||||
"ServerSSHAllowed": true,
|
||||
"DisableIPv6": false
|
||||
},
|
||||
"SyncMessageVersion": 1
|
||||
},
|
||||
"ProxyMeta": {
|
||||
"Embedded": true
|
||||
},
|
||||
"Location": {
|
||||
"CountryCode": "DE",
|
||||
"CityName": "Berlin",
|
||||
"ConnectionIP": "46.201.148.187"
|
||||
}
|
||||
},
|
||||
"peer-id-332": {
|
||||
"ID": "peer-id-332",
|
||||
"Key": "key-332",
|
||||
"SSHKey": "ssh-key-2",
|
||||
"DNSLabel": "peer-2",
|
||||
"UserID": "user-id-2",
|
||||
"SSHEnabled": true,
|
||||
"LoginExpirationEnabled": true,
|
||||
"LastLogin": "2026-08-06T14:25:59.12999Z",
|
||||
"IP": "10.10.100.1",
|
||||
"IPv6": "fdf5:ba80:6aa5:89f1:44d7:8701:8699:4940",
|
||||
"RequiresApproval": false,
|
||||
"ExtraDNSLabels": [
|
||||
"extra-peer-2"
|
||||
],
|
||||
"Meta": {
|
||||
"WtVersion": "0.76.1",
|
||||
"GoOS": "linux",
|
||||
"OSVersion": "26.4.2",
|
||||
"KernelVersion": "6.8.0-135-generic",
|
||||
"NetworkAddresses": [
|
||||
{
|
||||
"NetIP": "fe81::8b4c:973f:a76b:3771/64"
|
||||
},
|
||||
{
|
||||
"NetIP": "192.168.17.1/20"
|
||||
}
|
||||
],
|
||||
"Files": [
|
||||
{
|
||||
"Path": "/usr/bin/netbird",
|
||||
"ProcessIsRunning": false
|
||||
}
|
||||
],
|
||||
"Capabilities": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"Flags": {
|
||||
"ServerSSHAllowed": true,
|
||||
"DisableIPv6": false
|
||||
},
|
||||
"SyncMessageVersion": 0
|
||||
},
|
||||
"ProxyMeta": {
|
||||
"Embedded": true
|
||||
},
|
||||
"Location": {
|
||||
"CountryCode": "DE",
|
||||
"CityName": "Berlin",
|
||||
"ConnectionIP": "46.201.149.187"
|
||||
}
|
||||
},
|
||||
"peer-id-333": {
|
||||
"ID": "peer-id-333",
|
||||
"Key": "key-333",
|
||||
"SSHKey": "ssh-key-3",
|
||||
"DNSLabel": "peer-3",
|
||||
"UserID": "user-id-3",
|
||||
"SSHEnabled": true,
|
||||
"LoginExpirationEnabled": true,
|
||||
"LastLogin": "2026-08-06T12:25:59.12999Z",
|
||||
"IP": "10.10.200.1",
|
||||
"IPv6": "fdf6:ba80:6aa5:89f1:44d7:8701:8699:4940",
|
||||
"RequiresApproval": false,
|
||||
"ExtraDNSLabels": [
|
||||
"extra-peer-3"
|
||||
],
|
||||
"Meta": {
|
||||
"WtVersion": "0.76.2",
|
||||
"GoOS": "linux",
|
||||
"OSVersion": "26.4.3",
|
||||
"KernelVersion": "6.8.0-136-generic",
|
||||
"NetworkAddresses": [
|
||||
{
|
||||
"NetIP": "fe82::8b4c:973f:a76b:3771/64"
|
||||
},
|
||||
{
|
||||
"NetIP": "192.168.18.1/20"
|
||||
}
|
||||
],
|
||||
"Files": [
|
||||
{
|
||||
"Path": "/usr/bin/netbird",
|
||||
"ProcessIsRunning": false
|
||||
}
|
||||
],
|
||||
"Capabilities": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"Flags": {
|
||||
"ServerSSHAllowed": true,
|
||||
"DisableIPv6": false
|
||||
},
|
||||
"SyncMessageVersion": 1
|
||||
},
|
||||
"ProxyMeta": {
|
||||
"Embedded": true
|
||||
},
|
||||
"Location": {
|
||||
"CountryCode": "DE",
|
||||
"CityName": "Berlin",
|
||||
"ConnectionIP": "46.201.150.187"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Groups": {
|
||||
"33-group-no-resources-id": {
|
||||
"ID": "33-group-no-resources-id",
|
||||
"Name": "group-3-name",
|
||||
"PublicID": "33-group-no-resources-id-public",
|
||||
"Peers": null,
|
||||
"Resources": null
|
||||
},
|
||||
"33-group-one-resource-id": {
|
||||
"ID": "33-group-one-resource-id",
|
||||
"Name": "group-1-name",
|
||||
"PublicID": "group-one-resource-id-public",
|
||||
"Peers": [
|
||||
"peer-id-331"
|
||||
],
|
||||
"Resources": [
|
||||
{
|
||||
"ID": "host-id-1",
|
||||
"Type": "host"
|
||||
}
|
||||
]
|
||||
},
|
||||
"33-group-two-resources-id": {
|
||||
"ID": "33-group-two-resources-id",
|
||||
"Name": "group-2-name",
|
||||
"PublicID": "33-group-two-resources-id-public",
|
||||
"Peers": [
|
||||
"peer-id-332",
|
||||
"peer-id-333"
|
||||
],
|
||||
"Resources": [
|
||||
{
|
||||
"ID": "subnet-id-1",
|
||||
"Type": "subnet"
|
||||
},
|
||||
{
|
||||
"ID": "host-id-2",
|
||||
"Type": "host"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "policy-331",
|
||||
"PublicID": "policy-1-public",
|
||||
"Enabled": true,
|
||||
"SourcePostureChecks": [
|
||||
"posture-checks-1",
|
||||
"posture-checks-2"
|
||||
],
|
||||
"Rules": [
|
||||
{
|
||||
"ID": "policy-331",
|
||||
"PolicyID": "policy-331",
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Bidirectional": true,
|
||||
"Sources": [
|
||||
"33-group-one-resource-id",
|
||||
"33-group-two-resources-id"
|
||||
],
|
||||
"Destinations": [
|
||||
"33-group-one-resource-id",
|
||||
"33-group-two-resources-id"
|
||||
],
|
||||
"SourceResource": {
|
||||
"ID": "host-id-1",
|
||||
"Type": "host"
|
||||
},
|
||||
"DestinationResource": {
|
||||
"ID": "domain-331",
|
||||
"Type": "domain"
|
||||
},
|
||||
"Ports": [
|
||||
"8080",
|
||||
"8443"
|
||||
],
|
||||
"PortRanges": [
|
||||
{
|
||||
"Start": 8080,
|
||||
"End": 8090
|
||||
}
|
||||
],
|
||||
"AuthorizedGroups": {
|
||||
"33-group-one-resource-id": [
|
||||
"user-1",
|
||||
"user-2"
|
||||
]
|
||||
},
|
||||
"AuthorizedUser": "user-3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "route-331",
|
||||
"AccountID": "account-33",
|
||||
"PublicID": "route-1-public",
|
||||
"Network": "172.0.0.0/16",
|
||||
"Domains": [
|
||||
"test-1.com"
|
||||
],
|
||||
"KeepRoute": true,
|
||||
"NetID": "route-331-net-id",
|
||||
"Description": "route-1",
|
||||
"Peer": "peer-id-331",
|
||||
"PeerID": "peer-id-331",
|
||||
"PeerGroups": [
|
||||
"33-group-one-resource-id"
|
||||
],
|
||||
"NetworkType": 1,
|
||||
"Masquerade": true,
|
||||
"Metric": 9999,
|
||||
"Enabled": true,
|
||||
"Groups": [
|
||||
"33-group-one-resource-id"
|
||||
],
|
||||
"AccessControlGroups": [
|
||||
"33-group-one-resource-id"
|
||||
],
|
||||
"SkipAutoApply": false
|
||||
}
|
||||
],
|
||||
"NameServerGroups": [
|
||||
{
|
||||
"ID": "nsgroup-331",
|
||||
"PublicID": "nsgroup-1-public",
|
||||
"Name": "nsgroup-1",
|
||||
"Description": "nsgroup-1",
|
||||
"NameServers": [
|
||||
{
|
||||
"IP": "192.168.31.2",
|
||||
"NSType": 1,
|
||||
"Port": 53
|
||||
}
|
||||
],
|
||||
"Groups": [
|
||||
"33-group-one-resource-id"
|
||||
],
|
||||
"Primary": true,
|
||||
"Domains": [
|
||||
"test-1.com"
|
||||
],
|
||||
"Enabled": true,
|
||||
"SearchDomainsEnabled": false
|
||||
},
|
||||
{
|
||||
"ID": "nsgroup-332",
|
||||
"PublicID": "nsgroup-2-public",
|
||||
"Name": "nsgroup-2",
|
||||
"Description": "nsgroup-2",
|
||||
"NameServers": [
|
||||
{
|
||||
"IP": "192.168.32.3",
|
||||
"NSType": 1,
|
||||
"Port": 53
|
||||
}
|
||||
],
|
||||
"Groups": [
|
||||
"33-group-one-resource-id",
|
||||
"33-group-no-resources-id"
|
||||
],
|
||||
"Primary": true,
|
||||
"Domains": [
|
||||
"test-1.com",
|
||||
"test-2.com"
|
||||
],
|
||||
"Enabled": true,
|
||||
"SearchDomainsEnabled": false
|
||||
}
|
||||
],
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "net-resource-331",
|
||||
"NetworkID": "network-331",
|
||||
"AccountID": "account-33",
|
||||
"PublicID": "net-resource-public-1",
|
||||
"Name": "network-resource-1",
|
||||
"Description": "network-resource-1",
|
||||
"Type": "subnet",
|
||||
"Address": "",
|
||||
"Domain": "",
|
||||
"Prefix": "10.0.0.0/16",
|
||||
"Enabled": true
|
||||
},
|
||||
{
|
||||
"ID": "net-resource-332",
|
||||
"NetworkID": "network-332",
|
||||
"AccountID": "account-33",
|
||||
"PublicID": "net-resource-public-2",
|
||||
"Name": "network-resource-2",
|
||||
"Description": "network-resource-2",
|
||||
"Type": "domain",
|
||||
"Address": "",
|
||||
"Domain": "test.com",
|
||||
"Prefix": "",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Network": {
|
||||
"Identifier": "network-331",
|
||||
"Net": {
|
||||
"IP": "100.103.0.0",
|
||||
"Mask": "//8AAA=="
|
||||
},
|
||||
"NetV6": {
|
||||
"IP": "fdde:e995:fd38:a465::",
|
||||
"Mask": "//////////8AAAAAAAAAAA=="
|
||||
},
|
||||
"Dns": "",
|
||||
"Serial": 1
|
||||
},
|
||||
"DNSSettings": {
|
||||
"DisabledManagementGroups": [
|
||||
"disabled-group-1",
|
||||
"disabled-group-2"
|
||||
]
|
||||
},
|
||||
"AccountSettings": {
|
||||
"PeerLoginExpirationEnabled": true,
|
||||
"PeerLoginExpiration": 86400000000000,
|
||||
"PeerInactivityExpirationEnabled": false,
|
||||
"PeerInactivityExpiration": 86400000000000,
|
||||
"DNSDomain": "",
|
||||
"IPv6EnabledGroups": [
|
||||
"33-group-one-resource-id"
|
||||
],
|
||||
"RoutingPeerDNSResolutionEnabled": false,
|
||||
"LazyConnectionEnabled": false,
|
||||
"AutoUpdateVersion": "disabled",
|
||||
"AutoUpdateAlways": false,
|
||||
"MetricsPushEnabled": false
|
||||
},
|
||||
"PostureChecks": {
|
||||
"posturecheck-331": {
|
||||
"ID": "posturecheck-331",
|
||||
"Checks": {
|
||||
"NBVersionCheck": {
|
||||
"MinVersion": "0.25.0"
|
||||
},
|
||||
"OSVersionCheck": {
|
||||
"Android": null,
|
||||
"Darwin": {
|
||||
"MinVersion": "12.0"
|
||||
},
|
||||
"Ios": null,
|
||||
"Linux": null,
|
||||
"Windows": null
|
||||
},
|
||||
"GeoLocationCheck": {
|
||||
"Locations": [
|
||||
{
|
||||
"CountryCode": "FI",
|
||||
"CityName": ""
|
||||
}
|
||||
],
|
||||
"Action": "allow"
|
||||
},
|
||||
"PeerNetworkRangeCheck": {
|
||||
"Action": "deny",
|
||||
"Ranges": [
|
||||
"192.168.0.1/24"
|
||||
]
|
||||
},
|
||||
"ProcessCheck": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"PostureValidation": null,
|
||||
"AllowedUserIDs": {},
|
||||
"NetworkXIDToPublicID": {
|
||||
"network-331": "network-1-public",
|
||||
"network-332": "network-2-public"
|
||||
},
|
||||
"PostureCheckXIDToPublicID": {
|
||||
"posturecheck-331": "posturecheck-1-public"
|
||||
},
|
||||
"ValidatedPeers": {
|
||||
"peer-id-1": {},
|
||||
"peer-id-2": {},
|
||||
"peer-id-3": {}
|
||||
},
|
||||
"ResourcePolicies": {},
|
||||
"Routers": {
|
||||
"network-id-1": {
|
||||
"peer-id-331": {
|
||||
"PublicID": "public-id-1",
|
||||
"PeerGroups": [
|
||||
"33-group-one-resource-id"
|
||||
],
|
||||
"Masquerade": true,
|
||||
"Metric": 999,
|
||||
"Enabled": true
|
||||
}
|
||||
},
|
||||
"network-id-2": {
|
||||
"peer-id-332": {
|
||||
"PublicID": "public-id-2",
|
||||
"PeerGroups": [
|
||||
"33-group-two-resources-id",
|
||||
"33-group-no-resources-id"
|
||||
],
|
||||
"Masquerade": true,
|
||||
"Metric": 333,
|
||||
"Enabled": true
|
||||
},
|
||||
"peer-id-333": {
|
||||
"PublicID": "public-id-2",
|
||||
"PeerGroups": [
|
||||
"33-group-two-resources-id",
|
||||
"33-group-no-resources-id"
|
||||
],
|
||||
"Masquerade": true,
|
||||
"Metric": 333,
|
||||
"Enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"GroupIDToUserIDs": {},
|
||||
"DNSDomain": "",
|
||||
"ProxyTargetedDomainResourceIDs": {},
|
||||
"AppliedZoneCandidates": [
|
||||
{
|
||||
"DistributionGroups": [
|
||||
"33-group-one-resource-id"
|
||||
],
|
||||
"Zone": {
|
||||
"Domain": "test-331.com",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "test.test-331.com",
|
||||
"Type": 1,
|
||||
"Class": "IN",
|
||||
"TTL": 1800,
|
||||
"RData": "1.1.1.1"
|
||||
},
|
||||
{
|
||||
"Name": "test2.test-331.com",
|
||||
"Type": 1,
|
||||
"Class": "IN",
|
||||
"TTL": 1800,
|
||||
"RData": "1.1.1.2"
|
||||
}
|
||||
],
|
||||
"SearchDomainDisabled": false,
|
||||
"NonAuthoritative": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"PrivateServiceCandidates": null
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db"
|
||||
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator"
|
||||
"github.com/netbirdio/netbird/management/server/settings"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
//go:embed network_map_data.sql
|
||||
var nmapData string
|
||||
|
||||
//go:embed network_map_data_golden.json
|
||||
var goldenNMap string
|
||||
|
||||
const EnvUpdateGoldenData = "NMAP_UPDATE_GOLDEN_DATA"
|
||||
|
||||
func TestGetNetworkMapData(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
extraSettingsManager := settings.NewMockManager(ctrl)
|
||||
extraSettingsManager.EXPECT().GetExtraSettings(gomock.Any(), gomock.Any()).Return(&types.ExtraSettings{}, nil)
|
||||
|
||||
peerValidators := integrated_validator.NewMockIntegratedValidator(ctrl)
|
||||
peerValidators.EXPECT().GetValidatedPeers(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(
|
||||
map[string]struct{}{
|
||||
"peer-id-1": {},
|
||||
"peer-id-2": {},
|
||||
"peer-id-3": {},
|
||||
}, nil)
|
||||
|
||||
storeImpl := networkmapdb.NetworkMapDBStoreImpl{
|
||||
Store: store(t),
|
||||
ExtraSettingsManager: extraSettingsManager,
|
||||
IntegratedPeerValidator: peerValidators,
|
||||
}
|
||||
|
||||
for _, query := range strings.Split(nmapData, ";") {
|
||||
if err := store(t).Exec(ctx, query); err != nil {
|
||||
log.Fatalf("error initializing nmap test: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
nmap, err := storeImpl.GetNetworkMapData(ctx, "account-33")
|
||||
assert.NoError(t, err)
|
||||
|
||||
serializedNMap, err := json.MarshalIndent(nmap, "", " ")
|
||||
assert.NoError(t, err)
|
||||
|
||||
if _, ok := os.LookupEnv(EnvUpdateGoldenData); ok {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
tosavepath := filepath.Join(filepath.Dir(filename), "network_map_data_golden.json")
|
||||
err = os.WriteFile(tosavepath, serializedNMap, 0644)
|
||||
assert.NoError(t, err)
|
||||
goldenNMap = string(serializedNMap)
|
||||
}
|
||||
assert.Equal(t, goldenNMap, string(serializedNMap))
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetNetworkResources(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
execQuery(t, ctx,
|
||||
`insert into network_resources (id, account_id, network_id, public_id, name, description, type, domain, prefix, enabled)
|
||||
VALUES('net-resource-1','account-1','network-1','net-resource-public-1','network-resource-1','network-resource-1','subnet','','"10.0.0.0/16"',TRUE)`)
|
||||
execQuery(t, ctx,
|
||||
`insert into network_resources (id, account_id, network_id, public_id, name, description, type, domain, prefix, enabled)
|
||||
VALUES('net-resource-2','account-1','network-2','net-resource-public-2','network-resource-2','network-resource-2','domain','test.com','',TRUE)`)
|
||||
execQuery(t, ctx,
|
||||
`insert into network_resources (id, account_id, network_id, public_id, name, description, type, domain, prefix, enabled)
|
||||
VALUES('net-resource-3','account-1','network-3','net-resource-public-3','network-resource-3','network-resource-3','host','','"10.0.0.1/32"',TRUE)`)
|
||||
|
||||
resources, err := conn(t, ctx).GetNetworkResources(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Contains(t, resources, nmdata.NetworkResource{
|
||||
ID: "net-resource-1",
|
||||
AccountID: "account-1",
|
||||
NetworkID: "network-1",
|
||||
PublicID: "net-resource-public-1",
|
||||
Name: "network-resource-1",
|
||||
Description: "network-resource-1",
|
||||
Type: "subnet",
|
||||
Domain: "",
|
||||
Prefix: netip.MustParsePrefix("10.0.0.0/16"),
|
||||
Enabled: true,
|
||||
})
|
||||
assert.Contains(t, resources, nmdata.NetworkResource{
|
||||
ID: "net-resource-2",
|
||||
AccountID: "account-1",
|
||||
NetworkID: "network-2",
|
||||
PublicID: "net-resource-public-2",
|
||||
Name: "network-resource-2",
|
||||
Description: "network-resource-2",
|
||||
Type: "domain",
|
||||
Domain: "test.com",
|
||||
Enabled: true,
|
||||
})
|
||||
assert.Contains(t, resources, nmdata.NetworkResource{
|
||||
ID: "net-resource-3",
|
||||
AccountID: "account-1",
|
||||
NetworkID: "network-3",
|
||||
PublicID: "net-resource-public-3",
|
||||
Name: "network-resource-3",
|
||||
Description: "network-resource-3",
|
||||
Type: "host",
|
||||
Domain: "",
|
||||
Prefix: netip.MustParsePrefix("10.0.0.1/32"),
|
||||
Enabled: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetNetworkRouters(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
execQuery(t, ctx,
|
||||
`insert into network_routers (id, account_id, public_id, peer, network_id, masquerade, metric, enabled, peer_groups)
|
||||
VALUES('test-nr-id-1','account-1','public-id-1','peer-id-1','network-id-1',TRUE,999,TRUE,'["group-one-resource-id"]')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into network_routers (id, account_id, public_id, peer, network_id, masquerade, metric, enabled, peer_groups)
|
||||
VALUES('test-nr-id-2','account-1','public-id-2','','network-id-2',TRUE,333,TRUE,'["group-two-resources-id","group-no-resources-id"]')`)
|
||||
|
||||
routers, err := conn(t, ctx).GetNetworkRouters(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, routers)
|
||||
|
||||
assert.Equal(t, routers["network-id-1"],
|
||||
map[string]*nmdata.NetworkRouter{"peer-id-1": {PublicID: "public-id-1", Masquerade: true, Metric: 999, Enabled: true, PeerGroups: []string{"group-one-resource-id"}}})
|
||||
assert.Equal(t, routers["network-id-2"],
|
||||
map[string]*nmdata.NetworkRouter{
|
||||
"peer-id-2": {PublicID: "public-id-2", Masquerade: true, Metric: 333, Enabled: true, PeerGroups: []string{"group-two-resources-id", "group-no-resources-id"}},
|
||||
"peer-id-3": {PublicID: "public-id-2", Masquerade: true, Metric: 333, Enabled: true, PeerGroups: []string{"group-two-resources-id", "group-no-resources-id"}}})
|
||||
}
|
||||
56
integration_tests/management/network_map_db/network_test.go
Normal file
56
integration_tests/management/network_map_db/network_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetNetwork(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
network, err := conn(t, ctx).GetNetwork(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, network, nmdata.Network{
|
||||
Identifier: "network-1",
|
||||
Net: mustParseCIDR("100.103.0.0/16"),
|
||||
NetV6: mustParseCIDR("fdde:e995:fd38:a465::/64"),
|
||||
Serial: 1,
|
||||
})
|
||||
|
||||
network, err = conn(t, ctx).GetNetwork(ctx, "account-2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, network, nmdata.Network{
|
||||
Identifier: "network-2",
|
||||
Net: mustParseCIDR("110.0.0.0/16"),
|
||||
NetV6: mustParseCIDR("fddf:e995:fd38:a465::/64"),
|
||||
Serial: 2,
|
||||
})
|
||||
}
|
||||
|
||||
func mustParseCIDR(s string) net.IPNet {
|
||||
var toret net.IPNet
|
||||
|
||||
_, net, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
jn, err := json.Marshal(net)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(jn, &toret)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return toret
|
||||
}
|
||||
26
integration_tests/management/network_map_db/networks_test.go
Normal file
26
integration_tests/management/network_map_db/networks_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetNetworks(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
execQuery(t, ctx,
|
||||
`insert into networks (id, account_id, public_id) VALUES('network-1','account-1','network-1-public')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into networks (id, account_id, public_id) VALUES('network-2','account-1','network-2-public')`)
|
||||
|
||||
networksIdx, err := conn(t, ctx).GetNetworkXIDToPublicIdMap(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, networksIdx, map[string]string{
|
||||
"network-1": "network-1-public",
|
||||
"network-2": "network-2-public",
|
||||
})
|
||||
}
|
||||
163
integration_tests/management/network_map_db/peer_test.go
Normal file
163
integration_tests/management/network_map_db/peer_test.go
Normal file
@@ -0,0 +1,163 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetPeers(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
peers, clusterToPeersIdx, err := conn(t, ctx).GetPeers(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// shouldn't be returned in the index, as it's not connected
|
||||
execQuery(t, ctx,
|
||||
`insert into peers (id,account_id,"key",ssh_key,proxy_meta_embedded,peer_status_connected)
|
||||
values('peer-4','account-1','key-4','ssh-key-4',true,false)`)
|
||||
// shouldn't be returned in the index as it doesn't have cluster set
|
||||
execQuery(t, ctx,
|
||||
`insert into peers (id,account_id,"key",ssh_key,proxy_meta_embedded,peer_status_connected)
|
||||
values('peer-5','account-1','key-5','ssh-key-5',false,true)`)
|
||||
|
||||
peer1 := nmdata.Peer{
|
||||
ID: "peer-id-1",
|
||||
Key: "key-1",
|
||||
SSHKey: "ssh-key-1",
|
||||
DNSLabel: "peer-1",
|
||||
ExtraDNSLabels: []string{"extra-peer-1"},
|
||||
UserID: "user-id-1",
|
||||
SSHEnabled: true,
|
||||
LoginExpirationEnabled: true,
|
||||
LastLogin: mustParseTime("2026-08-06T13:25:59.12999+00:00"),
|
||||
IP: netip.MustParseAddr("10.10.10.1"),
|
||||
IPv6: netip.MustParseAddr("fdf4:ba80:6aa5:89f1:44d7:8701:8699:4940"),
|
||||
RequiresApproval: false,
|
||||
Meta: nmdata.PeerSystemMeta{
|
||||
WtVersion: "0.76.0",
|
||||
GoOS: "linux",
|
||||
OSVersion: "26.4.1",
|
||||
KernelVersion: "6.8.0-134-generic",
|
||||
NetworkAddresses: []nmdata.NetworkAddress{
|
||||
{NetIP: netip.MustParsePrefix("fe80::8b4c:973f:a76b:3771/64")},
|
||||
{NetIP: netip.MustParsePrefix("192.168.16.1/20")},
|
||||
},
|
||||
Files: []nmdata.File{
|
||||
{Path: "/usr/bin/netbird", ProcessIsRunning: false},
|
||||
},
|
||||
Capabilities: []int32{1, 2},
|
||||
Flags: nmdata.Flags{
|
||||
ServerSSHAllowed: true,
|
||||
DisableIPv6: false,
|
||||
},
|
||||
SyncMessageVersion: 1,
|
||||
},
|
||||
ProxyMeta: nmdata.ProxyMeta{
|
||||
Embedded: true,
|
||||
},
|
||||
Location: nmdata.PeerLocation{
|
||||
CountryCode: "DE",
|
||||
CityName: "Berlin",
|
||||
ConnectionIP: net.ParseIP("46.201.148.187"),
|
||||
},
|
||||
}
|
||||
peer2 := nmdata.Peer{
|
||||
ID: "peer-id-2",
|
||||
Key: "key-2",
|
||||
SSHKey: "ssh-key-2",
|
||||
DNSLabel: "peer-2",
|
||||
ExtraDNSLabels: []string{"extra-peer-2"},
|
||||
UserID: "user-id-2",
|
||||
SSHEnabled: true,
|
||||
LoginExpirationEnabled: true,
|
||||
LastLogin: mustParseTime("2026-08-06T14:25:59.12999+00:00"),
|
||||
IP: netip.MustParseAddr("10.10.100.1"),
|
||||
IPv6: netip.MustParseAddr("fdf5:ba80:6aa5:89f1:44d7:8701:8699:4940"),
|
||||
RequiresApproval: false,
|
||||
Meta: nmdata.PeerSystemMeta{
|
||||
WtVersion: "0.76.1",
|
||||
GoOS: "linux",
|
||||
OSVersion: "26.4.2",
|
||||
KernelVersion: "6.8.0-135-generic",
|
||||
NetworkAddresses: []nmdata.NetworkAddress{
|
||||
{NetIP: netip.MustParsePrefix("fe81::8b4c:973f:a76b:3771/64")},
|
||||
{NetIP: netip.MustParsePrefix("192.168.17.1/20")},
|
||||
},
|
||||
Files: []nmdata.File{
|
||||
{Path: "/usr/bin/netbird", ProcessIsRunning: false},
|
||||
},
|
||||
Capabilities: []int32{1, 2},
|
||||
Flags: nmdata.Flags{
|
||||
ServerSSHAllowed: true,
|
||||
DisableIPv6: false,
|
||||
},
|
||||
SyncMessageVersion: 0,
|
||||
},
|
||||
ProxyMeta: nmdata.ProxyMeta{
|
||||
Embedded: true,
|
||||
},
|
||||
Location: nmdata.PeerLocation{
|
||||
CountryCode: "DE",
|
||||
CityName: "Berlin",
|
||||
ConnectionIP: net.ParseIP("46.201.149.187"),
|
||||
},
|
||||
}
|
||||
peer3 := nmdata.Peer{
|
||||
ID: "peer-id-3",
|
||||
Key: "key-3",
|
||||
SSHKey: "ssh-key-3",
|
||||
DNSLabel: "peer-3",
|
||||
ExtraDNSLabels: []string{"extra-peer-3"},
|
||||
UserID: "user-id-3",
|
||||
SSHEnabled: true,
|
||||
LoginExpirationEnabled: true,
|
||||
LastLogin: mustParseTime("2026-08-06T12:25:59.12999+00:00"),
|
||||
IP: netip.MustParseAddr("10.10.200.1"),
|
||||
IPv6: netip.MustParseAddr("fdf6:ba80:6aa5:89f1:44d7:8701:8699:4940"),
|
||||
RequiresApproval: false,
|
||||
Meta: nmdata.PeerSystemMeta{
|
||||
WtVersion: "0.76.2",
|
||||
GoOS: "linux",
|
||||
OSVersion: "26.4.3",
|
||||
KernelVersion: "6.8.0-136-generic",
|
||||
NetworkAddresses: []nmdata.NetworkAddress{
|
||||
{NetIP: netip.MustParsePrefix("fe82::8b4c:973f:a76b:3771/64")},
|
||||
{NetIP: netip.MustParsePrefix("192.168.18.1/20")},
|
||||
},
|
||||
Files: []nmdata.File{
|
||||
{Path: "/usr/bin/netbird", ProcessIsRunning: false},
|
||||
},
|
||||
Capabilities: []int32{1, 2},
|
||||
Flags: nmdata.Flags{
|
||||
ServerSSHAllowed: true,
|
||||
DisableIPv6: false,
|
||||
},
|
||||
SyncMessageVersion: 1,
|
||||
},
|
||||
ProxyMeta: nmdata.ProxyMeta{
|
||||
Embedded: true,
|
||||
},
|
||||
Location: nmdata.PeerLocation{
|
||||
CountryCode: "DE",
|
||||
CityName: "Berlin",
|
||||
ConnectionIP: net.ParseIP("46.201.150.187"),
|
||||
},
|
||||
}
|
||||
|
||||
assert.Contains(t, peers, peer1)
|
||||
assert.Contains(t, peers, peer2)
|
||||
assert.Contains(t, peers, peer3)
|
||||
|
||||
assert.Equal(t, clusterToPeersIdx, map[string][]*nmdata.Peer{
|
||||
"cluster-1.netbird.services": {&peer1},
|
||||
"cluster-2.netbird.services": {&peer2},
|
||||
"cluster-3.netbird.services": {&peer3},
|
||||
})
|
||||
}
|
||||
121
integration_tests/management/network_map_db/pg_test_store.go
Normal file
121
integration_tests/management/network_map_db/pg_test_store.go
Normal file
@@ -0,0 +1,121 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/google/uuid"
|
||||
networkmap_pgsql "github.com/netbirdio/netbird/management/internals/network_map_db/pgsql"
|
||||
gormstore "github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/testutil"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func createPGTestStore(baseData string) (*networkmap_pgsql.PgStore, func()) {
|
||||
_, tmpdsn, err := testutil.CreatePostgresTestContainer()
|
||||
if err != nil {
|
||||
log.Fatalf("error starting postres container %v", err)
|
||||
}
|
||||
|
||||
var db *gorm.DB
|
||||
for i := range 5 {
|
||||
db, err = gorm.Open(postgres.Open(tmpdsn), &gorm.Config{})
|
||||
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if i < 5 {
|
||||
waitTime := time.Duration(100*(i+1)) * time.Millisecond
|
||||
time.Sleep(waitTime)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Fatalf("error connecting to postres db %v", err)
|
||||
}
|
||||
|
||||
var cleanup func()
|
||||
dsn, cleanup, err := createRandomDB(tmpdsn, db)
|
||||
sqlDB, _ := db.DB()
|
||||
if sqlDB != nil {
|
||||
sqlDB.Close()
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("error creating postres db %v", err)
|
||||
}
|
||||
|
||||
_, err = gormstore.NewPostgresqlStoreForTests(context.TODO(), dsn, nil, false)
|
||||
if err != nil {
|
||||
log.Fatalf("error running migrations %v", err)
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
pgstore, err := networkmap_pgsql.NewPostgresqlStore(ctx, dsn)
|
||||
if err != nil {
|
||||
log.Fatal("error creating postgres store %w", err)
|
||||
}
|
||||
|
||||
for _, query := range strings.Split(baseData, ";") {
|
||||
if _, err := pgstore.Pool.Exec(ctx, query); err != nil {
|
||||
log.Fatalf("error initializing db: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return pgstore, cleanup
|
||||
}
|
||||
|
||||
func createRandomDB(dsn string, db *gorm.DB) (string, func(), error) {
|
||||
dbName := fmt.Sprintf("test_db_%s", strings.ReplaceAll(uuid.New().String(), "-", "_"))
|
||||
|
||||
if err := db.Exec(fmt.Sprintf("CREATE DATABASE %s", dbName)).Error; err != nil {
|
||||
return "", nil, fmt.Errorf("failed to create database: %v", err)
|
||||
}
|
||||
|
||||
originalDSN := dsn
|
||||
|
||||
cleanup := func() {
|
||||
var dropDB *gorm.DB
|
||||
var err error
|
||||
|
||||
dropDB, err = gorm.Open(postgres.Open(originalDSN), &gorm.Config{
|
||||
SkipDefaultTransaction: true,
|
||||
PrepareStmt: false,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("failed to connect for dropping database %s: %v", dbName, err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if sqlDB, _ := dropDB.DB(); sqlDB != nil {
|
||||
sqlDB.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if sqlDB, _ := dropDB.DB(); sqlDB != nil {
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
sqlDB.SetMaxIdleConns(0)
|
||||
sqlDB.SetConnMaxLifetime(time.Second)
|
||||
}
|
||||
|
||||
err = dropDB.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s WITH (FORCE)", dbName)).Error
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to drop database %s: %v", dbName, err)
|
||||
}
|
||||
}
|
||||
|
||||
return replaceDBName(dsn, dbName), cleanup, nil
|
||||
}
|
||||
|
||||
func replaceDBName(dsn, newDBName string) string {
|
||||
re := regexp.MustCompile(`(?P<pre>[:/@])(?P<dbname>[^/?]+)(?P<post>\?|$)`)
|
||||
return re.ReplaceAllString(dsn, `${pre}`+newDBName+`${post}`)
|
||||
}
|
||||
146
integration_tests/management/network_map_db/policy_test.go
Normal file
146
integration_tests/management/network_map_db/policy_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetPolicies(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
execQuery(t, ctx,
|
||||
`insert into policies (id, public_id, account_id, enabled, source_posture_checks)
|
||||
values('policy-1','policy-1-public','account-1',true,'["posture-checks-1","posture-checks-2"]')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into policy_rules (id, policy_id, enabled, action, protocol, bidirectional, sources, destinations,
|
||||
source_resource, destination_resource, ports, port_ranges,
|
||||
authorized_groups, authorized_user)
|
||||
values('policy-1-rule-1','policy-1',true,'accept','tcp',true,'["group-one-resource-id","group-two-resources-id"]','["group-one-resource-id","group-two-resources-id"]',
|
||||
'{"ID":"host-id-1","Type":"host"}','{"ID":"domain-1","Type":"domain"}','["8080","8443"]', '[{"Start":8080,"End":8090}]',
|
||||
'{"group-one-resource-id":["user-1", "user-2"]}','user-3')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into policies (id, public_id, account_id, enabled, source_posture_checks)
|
||||
values('policy-2','policy-2-public','account-1',true,'["posture-checks-3","posture-checks-4"]')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into policy_rules (id, policy_id, enabled, action, protocol, bidirectional, sources, destinations,
|
||||
source_resource, destination_resource, ports, port_ranges,
|
||||
authorized_groups, authorized_user)
|
||||
values('policy-2-rule-1','policy-2',true,'accept','tcp',true,'["group-one-resource-id"]','["group-two-resources-id"]',
|
||||
'{"ID":"host-id-3","Type":"host"}','{"ID":"domain-3","Type":"domain"}','["8080","8443"]', '[{"Start":8080,"End":8090}]',
|
||||
'{"group-one-resource-id":["user-6", "user-7"]}','user-8')`)
|
||||
// policy with a rule with null fields
|
||||
execQuery(t, ctx,
|
||||
`insert into policies (id, public_id, account_id, enabled, source_posture_checks)
|
||||
values('policy-3','policy-3-public','account-1',true,null)`)
|
||||
execQuery(t, ctx,
|
||||
`insert into policy_rules (id, policy_id, enabled, action, protocol, bidirectional, sources, destinations,
|
||||
source_resource, destination_resource, ports, port_ranges,
|
||||
authorized_groups, authorized_user)
|
||||
values('policy-3-rule-1','policy-3',true,null,null,null,null,null,null,null,null,null,null,null)`)
|
||||
// policy with a disabled rule, destination resource and groups should not be in indexes
|
||||
execQuery(t, ctx,
|
||||
`insert into policies (id, public_id, account_id, enabled, source_posture_checks)
|
||||
values('policy-4','policy-4-public','account-1',true,null)`)
|
||||
execQuery(t, ctx,
|
||||
`insert into policy_rules (id, policy_id, enabled, action, protocol, bidirectional, sources, destinations,
|
||||
source_resource, destination_resource, ports, port_ranges,
|
||||
authorized_groups, authorized_user)
|
||||
values('policy-4-rule-1','policy-4',false,null,null,null,null,'["group-two-resources-id"]',
|
||||
null,'{"ID":"domain-3","Type":"domain"}',null,null,null,null)`)
|
||||
|
||||
policies, policyToDestinationResourceIdx, policyToDestinationGroupIdx, err := conn(t, ctx).GetPolicies(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Contains(t, policies, nmdata.Policy{
|
||||
ID: "policy-1",
|
||||
PublicID: "policy-1-public",
|
||||
Enabled: true,
|
||||
SourcePostureChecks: []string{"posture-checks-1", "posture-checks-2"},
|
||||
Rules: []*nmdata.PolicyRule{
|
||||
{
|
||||
ID: "policy-1",
|
||||
PolicyID: "policy-1",
|
||||
Enabled: true,
|
||||
Action: "accept",
|
||||
Protocol: "tcp",
|
||||
Bidirectional: true,
|
||||
Sources: []string{"group-one-resource-id", "group-two-resources-id"},
|
||||
Destinations: []string{"group-one-resource-id", "group-two-resources-id"},
|
||||
SourceResource: nmdata.Resource{ID: "host-id-1", Type: "host"},
|
||||
DestinationResource: nmdata.Resource{ID: "domain-1", Type: "domain"},
|
||||
Ports: []string{"8080", "8443"},
|
||||
PortRanges: []nmdata.RulePortRange{{Start: 8080, End: 8090}},
|
||||
AuthorizedGroups: map[string][]string{"group-one-resource-id": {"user-1", "user-2"}},
|
||||
AuthorizedUser: "user-3",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.Contains(t, policies, nmdata.Policy{
|
||||
ID: "policy-2",
|
||||
PublicID: "policy-2-public",
|
||||
Enabled: true,
|
||||
SourcePostureChecks: []string{"posture-checks-3", "posture-checks-4"},
|
||||
Rules: []*nmdata.PolicyRule{
|
||||
{
|
||||
ID: "policy-2",
|
||||
PolicyID: "policy-2",
|
||||
Enabled: true,
|
||||
Action: "accept",
|
||||
Protocol: "tcp",
|
||||
Bidirectional: true,
|
||||
Sources: []string{"group-one-resource-id"},
|
||||
Destinations: []string{"group-two-resources-id"},
|
||||
SourceResource: nmdata.Resource{ID: "host-id-3", Type: "host"},
|
||||
DestinationResource: nmdata.Resource{ID: "domain-3", Type: "domain"},
|
||||
Ports: []string{"8080", "8443"},
|
||||
PortRanges: []nmdata.RulePortRange{{Start: 8080, End: 8090}},
|
||||
AuthorizedGroups: map[string][]string{"group-one-resource-id": {"user-6", "user-7"}},
|
||||
AuthorizedUser: "user-8",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.Contains(t, policies, nmdata.Policy{
|
||||
ID: "policy-3",
|
||||
PublicID: "policy-3-public",
|
||||
Enabled: true,
|
||||
SourcePostureChecks: nil,
|
||||
Rules: []*nmdata.PolicyRule{
|
||||
{
|
||||
ID: "policy-3",
|
||||
PolicyID: "policy-3",
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Contains(t, policies, nmdata.Policy{
|
||||
ID: "policy-4",
|
||||
PublicID: "policy-4-public",
|
||||
Enabled: true,
|
||||
SourcePostureChecks: nil,
|
||||
Rules: []*nmdata.PolicyRule{
|
||||
{
|
||||
ID: "policy-4",
|
||||
PolicyID: "policy-4",
|
||||
Enabled: false,
|
||||
Destinations: []string{"group-two-resources-id"},
|
||||
DestinationResource: nmdata.Resource{ID: "domain-3", Type: "domain"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.Equal(t, policyToDestinationGroupIdx, map[string]map[string]any{
|
||||
"policy-1": {"group-one-resource-id": struct{}{}, "group-two-resources-id": struct{}{}},
|
||||
"policy-2": {"group-two-resources-id": struct{}{}},
|
||||
})
|
||||
assert.Equal(t, policyToDestinationResourceIdx, map[string]map[string]any{
|
||||
"policy-1": {"domain-1": struct{}{}},
|
||||
"policy-2": {"domain-3": struct{}{}},
|
||||
})
|
||||
}
|
||||
61
integration_tests/management/network_map_db/posture_test.go
Normal file
61
integration_tests/management/network_map_db/posture_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetPostureChecks(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
execQuery(t, ctx,
|
||||
`insert into posture_checks (id, account_id, public_id, checks)
|
||||
VALUES('posturecheck-1','account-1','posturecheck-1-public',
|
||||
'{"NBVersionCheck":{"MinVersion":"0.25.0"},
|
||||
"OSVersionCheck":{"Darwin":{"MinVersion":"12.0"}},
|
||||
"GeoLocationCheck":{"Locations":[{"CountryCode":"FI","CityName":""}],"Action":"allow"},
|
||||
"PeerNetworkRangeCheck":{"Action":"deny","Ranges":["192.168.0.1/24"]}}')`)
|
||||
|
||||
execQuery(t, ctx,
|
||||
`insert into posture_checks (id, account_id, public_id, checks)
|
||||
VALUES('posturecheck-2','account-1','posturecheck-2-public',
|
||||
'{"NBVersionCheck":{"MinVersion":"0.25.0"},
|
||||
"OSVersionCheck":{"Android":{"MinVersion":"0"}},
|
||||
"GeoLocationCheck":{"Locations":[{"CountryCode":"US","CityName":"Harker Heights"}],"Action":"allow"},
|
||||
"PeerNetworkRangeCheck":{"Action":"allow","Ranges":["0.0.0.0/0"]}}')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into posture_checks (id, account_id, public_id, checks)
|
||||
VALUES('posturecheck-3','account-1','posturecheck-3-public', null)`)
|
||||
|
||||
postureChecks, idToPublicIDIdx, err := conn(t, ctx).GetPostureChecks(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, idToPublicIDIdx, map[string]string{
|
||||
"posturecheck-1": "posturecheck-1-public",
|
||||
"posturecheck-2": "posturecheck-2-public",
|
||||
"posturecheck-3": "posturecheck-3-public",
|
||||
})
|
||||
assert.Contains(t, postureChecks, nmdata.PostureChecks{
|
||||
ID: "posturecheck-1",
|
||||
Checks: nmdata.ChecksDefinition{
|
||||
NBVersionCheck: &nmdata.NBVersionCheck{MinVersion: "0.25.0"},
|
||||
OSVersionCheck: &nmdata.OSVersionCheck{Darwin: &nmdata.MinVersionCheck{MinVersion: "12.0"}},
|
||||
GeoLocationCheck: &nmdata.GeoLocationCheck{Locations: []nmdata.GeoLocation{{CountryCode: "FI"}}, Action: "allow"},
|
||||
PeerNetworkRangeCheck: &nmdata.PeerNetworkRangeCheck{Action: "deny", Ranges: []netip.Prefix{netip.MustParsePrefix("192.168.0.1/24")}},
|
||||
}})
|
||||
assert.Contains(t, postureChecks, nmdata.PostureChecks{
|
||||
ID: "posturecheck-2",
|
||||
Checks: nmdata.ChecksDefinition{
|
||||
NBVersionCheck: &nmdata.NBVersionCheck{MinVersion: "0.25.0"},
|
||||
OSVersionCheck: &nmdata.OSVersionCheck{Android: &nmdata.MinVersionCheck{MinVersion: "0"}},
|
||||
GeoLocationCheck: &nmdata.GeoLocationCheck{Locations: []nmdata.GeoLocation{{CountryCode: "US", CityName: "Harker Heights"}}, Action: "allow"},
|
||||
PeerNetworkRangeCheck: &nmdata.PeerNetworkRangeCheck{Action: "allow", Ranges: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")}},
|
||||
}})
|
||||
assert.Contains(t, postureChecks, nmdata.PostureChecks{
|
||||
ID: "posturecheck-3"})
|
||||
}
|
||||
87
integration_tests/management/network_map_db/route_test.go
Normal file
87
integration_tests/management/network_map_db/route_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetRoutes(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
execQuery(t, ctx,
|
||||
`insert into routes (id, account_id, public_id, network, domains, keep_route, net_id, description,
|
||||
peer, peer_groups, network_type, masquerade, metric, enabled,
|
||||
groups, access_control_groups, skip_auto_apply)
|
||||
VALUES('route-1','account-1','route-1-public','"172.0.0.0/16"','["test-1.com"]',true,'route-1-net-id','route-1',
|
||||
'peer-id-1','["group-one-resource-id"]',1,true,9999,true,
|
||||
'["group-one-resource-id"]','["group-one-resource-id"]',false)`)
|
||||
execQuery(t, ctx,
|
||||
`insert into routes (id, account_id, public_id, network, domains, keep_route, net_id, description,
|
||||
peer, peer_groups, network_type, masquerade, metric, enabled,
|
||||
groups, access_control_groups, skip_auto_apply)
|
||||
VALUES('route-2','account-1','route-2-public','"172.10.0.0/16"','["test-1.com","test-2.com"]',true,'route-2-net-id','route-2',
|
||||
'peer-id-2','["group-two-resources-id"]',1,true,9999,true,
|
||||
'["group-two-resources-id"]','["group-two-resources-id"]',false)`)
|
||||
execQuery(t, ctx,
|
||||
`insert into routes (id, account_id, public_id, network, domains, keep_route, net_id, description,
|
||||
peer, peer_groups, network_type, masquerade, metric, enabled,
|
||||
groups, access_control_groups, skip_auto_apply)
|
||||
VALUES('route-3','account-1','route-3-public',null,null,null,null,'route-3',
|
||||
null,null,null,null,null,null,null,null,null)`)
|
||||
|
||||
routes, err := conn(t, ctx).GetRoutes(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, routes, nmdata.Route{
|
||||
ID: "route-1",
|
||||
AccountID: "account-1",
|
||||
PublicID: "route-1-public",
|
||||
Network: netip.MustParsePrefix("172.0.0.0/16"),
|
||||
Domains: domain.List{"test-1.com"},
|
||||
KeepRoute: true,
|
||||
NetID: "route-1-net-id",
|
||||
Description: "route-1",
|
||||
Peer: "peer-id-1",
|
||||
PeerID: "peer-id-1",
|
||||
PeerGroups: []string{"group-one-resource-id"},
|
||||
NetworkType: 1,
|
||||
Masquerade: true,
|
||||
Metric: 9999,
|
||||
Enabled: true,
|
||||
Groups: []string{"group-one-resource-id"},
|
||||
AccessControlGroups: []string{"group-one-resource-id"},
|
||||
SkipAutoApply: false,
|
||||
})
|
||||
assert.Contains(t, routes, nmdata.Route{
|
||||
ID: "route-2",
|
||||
AccountID: "account-1",
|
||||
PublicID: "route-2-public",
|
||||
Network: netip.MustParsePrefix("172.10.0.0/16"),
|
||||
Domains: domain.List{"test-1.com", "test-2.com"},
|
||||
KeepRoute: true,
|
||||
NetID: "route-2-net-id",
|
||||
Description: "route-2",
|
||||
Peer: "peer-id-2",
|
||||
PeerID: "peer-id-2",
|
||||
PeerGroups: []string{"group-two-resources-id"},
|
||||
NetworkType: 1,
|
||||
Masquerade: true,
|
||||
Metric: 9999,
|
||||
Enabled: true,
|
||||
Groups: []string{"group-two-resources-id"},
|
||||
AccessControlGroups: []string{"group-two-resources-id"},
|
||||
SkipAutoApply: false,
|
||||
})
|
||||
assert.Contains(t, routes, nmdata.Route{
|
||||
ID: "route-3",
|
||||
AccountID: "account-1",
|
||||
PublicID: "route-3-public",
|
||||
Description: "route-3",
|
||||
})
|
||||
}
|
||||
109
integration_tests/management/network_map_db/service_test.go
Normal file
109
integration_tests/management/network_map_db/service_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db"
|
||||
)
|
||||
|
||||
func TestGetPrivateServices(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
execQuery(t, ctx,
|
||||
`insert into services (id, account_id, enabled, private, access_groups, proxy_cluster, domain)
|
||||
values('service-1','account-1',true,true,'["group-one-resource-id"]','test-1.com','test-2.com')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into services (id, account_id, enabled, private, access_groups, proxy_cluster, domain)
|
||||
values('service-2','account-1',true,true,'["group-one-resource-id","group-two-resources-id"]','test-3.com','test-4.com')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into services (id, account_id, enabled, private, access_groups, proxy_cluster, domain)
|
||||
values('service-3','account-1',null,null,null,null,null)`)
|
||||
|
||||
services, err := conn(t, ctx).GetPrivateServices(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, services, networkmapdb.Service{
|
||||
Enabled: sql.NullBool{Bool: true, Valid: true},
|
||||
Private: sql.NullBool{Bool: true, Valid: true},
|
||||
AccessGroups: []string{"group-one-resource-id"},
|
||||
ProxyCluster: sql.NullString{String: "test-1.com", Valid: true},
|
||||
Domain: sql.NullString{String: "test-2.com", Valid: true},
|
||||
})
|
||||
assert.Contains(t, services, networkmapdb.Service{
|
||||
Enabled: sql.NullBool{Bool: true, Valid: true},
|
||||
Private: sql.NullBool{Bool: true, Valid: true},
|
||||
AccessGroups: []string{"group-one-resource-id", "group-two-resources-id"},
|
||||
ProxyCluster: sql.NullString{String: "test-3.com", Valid: true},
|
||||
Domain: sql.NullString{String: "test-4.com", Valid: true},
|
||||
})
|
||||
assert.Contains(t, services, networkmapdb.Service{
|
||||
Enabled: sql.NullBool{Bool: false, Valid: false},
|
||||
Private: sql.NullBool{Bool: false, Valid: false},
|
||||
AccessGroups: []string{},
|
||||
ProxyCluster: sql.NullString{String: "", Valid: false},
|
||||
Domain: sql.NullString{String: "", Valid: false},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetProxyTargetedDomainResourceIDs(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
execQuery(t, ctx,
|
||||
`insert into services (id, account_id, enabled, terminated)
|
||||
values('service-4','account-1',true,false)`)
|
||||
execQuery(t, ctx,
|
||||
`insert into targets (target_id, account_id, service_id, enabled, target_type)
|
||||
values('target-1','account-1','service-4',true,'domain')`)
|
||||
// id shouldn't be returned as the taget_type is not "domain"
|
||||
execQuery(t, ctx,
|
||||
`insert into targets (target_id, account_id, service_id, enabled, target_type)
|
||||
values('target-2','account-1','service-4',true,'cluster')`)
|
||||
// id shouldn't be included as the target is disabled
|
||||
execQuery(t, ctx,
|
||||
`insert into targets (target_id, account_id, service_id, enabled, target_type)
|
||||
values('target-3','account-1','service-4',false,'domain')`)
|
||||
// id shouldn't be included as the service is disabled
|
||||
execQuery(t, ctx,
|
||||
`insert into services (id, account_id, enabled, terminated)
|
||||
values('service-5','account-1',false,false)`)
|
||||
execQuery(t, ctx,
|
||||
`insert into targets (target_id, account_id, service_id, enabled, target_type)
|
||||
values('target-4','account-1','service-5',false,'domain')`)
|
||||
// id shouldn't be included as the service is terminated (explicitly)
|
||||
execQuery(t, ctx,
|
||||
`insert into services (id, account_id, enabled, terminated)
|
||||
values('service-6','account-1',true,true)`)
|
||||
execQuery(t, ctx,
|
||||
`insert into targets (target_id, account_id, service_id, enabled, target_type)
|
||||
values('target-5','account-1','service-6',true,'domain')`)
|
||||
// id shouldn't be included as the service is terminated (implicitly)
|
||||
execQuery(t, ctx,
|
||||
`insert into services (id, account_id, enabled, terminated)
|
||||
values('service-7','account-1',true,null)`)
|
||||
execQuery(t, ctx,
|
||||
`insert into targets (target_id, account_id, service_id, enabled, target_type)
|
||||
values('target-6','account-1','service-7',true,'domain')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into services (id, account_id, enabled, terminated)
|
||||
values('service-8','account-1',true,false)`)
|
||||
execQuery(t, ctx,
|
||||
`insert into targets (target_id, account_id, service_id, enabled, target_type)
|
||||
values('target-7','account-1','service-8',true,'domain')`)
|
||||
// id shouldn't be returned as the taget_id is null
|
||||
execQuery(t, ctx,
|
||||
`insert into targets (target_id, account_id, service_id, enabled, target_type)
|
||||
values(null,'account-1','service-4',true,'cluster')`)
|
||||
|
||||
servtargetedDomains, err := conn(t, ctx).GetProxyTargetedDomainResourceIDs(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, servtargetedDomains, map[string]struct{}{
|
||||
"target-1": {},
|
||||
"target-6": {},
|
||||
"target-7": {},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
networkmap_sqlite "github.com/netbirdio/netbird/management/internals/network_map_db/sqlite"
|
||||
gormstore "github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func createSqliteTestStore(baseData string) (*networkmap_sqlite.SqliteStore, func()) {
|
||||
storeSqliteFileName := ":memory:"
|
||||
storeStr := fmt.Sprintf("%s?cache=shared", storeSqliteFileName)
|
||||
if runtime.GOOS == "windows" {
|
||||
// Vo avoid `The process cannot access the file because it is being used by another process` on Windows
|
||||
storeStr = storeSqliteFileName
|
||||
}
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(storeStr), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatalf("error initializing db: %s", err.Error())
|
||||
}
|
||||
_, err = gormstore.NewSqlStore(context.TODO(), db, types.SqliteStoreEngine, nil, false)
|
||||
if err != nil {
|
||||
log.Fatalf("error initializing db: %s", err.Error())
|
||||
}
|
||||
|
||||
sqldb, err := db.DB()
|
||||
if err != nil {
|
||||
log.Fatalf("error initializing db: %s", err.Error())
|
||||
|
||||
}
|
||||
for _, query := range strings.Split(baseData, ";") {
|
||||
if _, err := sqldb.Exec(query); err != nil {
|
||||
log.Fatalf("error initializing db: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return &networkmap_sqlite.SqliteStore{Db: sqldb}, func() {}
|
||||
}
|
||||
57
integration_tests/management/network_map_db/user_test.go
Normal file
57
integration_tests/management/network_map_db/user_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
//go:build integration
|
||||
|
||||
package networkmap_pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetAllowedUsers(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
execQuery(t, ctx,
|
||||
`insert into users (id, name, account_id, auto_groups, blocked, is_service_user)
|
||||
VALUES('user-1','user-1','account-1','["group-one-resource-id"]',false,false)`)
|
||||
execQuery(t, ctx,
|
||||
`insert into users (id, name, account_id, auto_groups, blocked, is_service_user)
|
||||
VALUES('user-2','user-2','account-1','["group-one-resource-id","group-two-resources-id"]',false,false)`)
|
||||
execQuery(t, ctx,
|
||||
`insert into users (id, name, account_id, auto_groups, blocked, is_service_user)
|
||||
VALUES('user-3','user-3','account-1','["group-two-resources-id"]',false,false)`)
|
||||
// shouldn't be included as it's blocked
|
||||
execQuery(t, ctx,
|
||||
`insert into users (id, name, account_id, auto_groups, blocked, is_service_user)
|
||||
VALUES('user-4','user-4','account-1','["group-two-resources-id"]',true,false)`)
|
||||
// shouldn't be included as it's a service_user
|
||||
execQuery(t, ctx,
|
||||
`insert into users (id, name, account_id, auto_groups, blocked, is_service_user)
|
||||
VALUES('user-5','user-5','account-1','["group-two-resources-id"]',false,true)`)
|
||||
execQuery(t, ctx,
|
||||
`insert into groups (id, name, account_id)
|
||||
VALUES('all-group-1','All','account-1')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into groups (id, name, account_id)
|
||||
VALUES('all-group-2','All','account-1')`)
|
||||
execQuery(t, ctx,
|
||||
`insert into groups (id, name, account_id)
|
||||
VALUES('all-group-3','All','account-1')`)
|
||||
|
||||
userIdx, groupIdToUserIds, err := conn(t, ctx).GetAllowedUsers(ctx, "account-1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, userIdx, map[string]struct{}{
|
||||
"user-1": {},
|
||||
"user-2": {},
|
||||
"user-3": {},
|
||||
})
|
||||
assert.Equal(t, groupIdToUserIds, map[string][]string{
|
||||
"group-one-resource-id": {"user-1", "user-2"},
|
||||
"group-two-resources-id": {"user-2", "user-3"},
|
||||
"all-group-1": {"user-1", "user-2", "user-3"},
|
||||
"all-group-2": {"user-1", "user-2", "user-3"},
|
||||
"all-group-3": {"user-1", "user-2", "user-3"},
|
||||
})
|
||||
}
|
||||
10
magefiles/magefile.go
Normal file
10
magefiles/magefile.go
Normal file
@@ -0,0 +1,10 @@
|
||||
//mage:multiline
|
||||
|
||||
// Set the general description you want to have displayed with mage -l here.
|
||||
package main
|
||||
|
||||
// mg contains helpful utility functions, like Deps
|
||||
|
||||
// Default target to run when none is specified
|
||||
// If not set, running mage will list available targets
|
||||
//var Default = Integrationtest.All
|
||||
74
magefiles/test.go
Normal file
74
magefiles/test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/magefile/mage/mg"
|
||||
"github.com/magefile/mage/sh"
|
||||
)
|
||||
|
||||
var defaultcli = []string{"test", "-tags=integration", "-timeout=20m"}
|
||||
|
||||
type Integrationtest mg.Namespace
|
||||
|
||||
func (i Integrationtest) All(gotestflags *string) error {
|
||||
var errs []error
|
||||
if err := i.Api(gotestflags); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := i.NmapDb(gotestflags); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Integrationtest) NmapDb(gotestflags *string) error {
|
||||
cli := defaultcli
|
||||
if gotestflags != nil {
|
||||
cli = append(cli, strings.Split(*gotestflags, " ")...)
|
||||
}
|
||||
cli = append(cli, "./integration_tests/management/network_map_db/...")
|
||||
|
||||
return sh.RunV("go", cli...)
|
||||
}
|
||||
|
||||
func (Integrationtest) NmapDbPostgres(gotestflags *string) error {
|
||||
cli := defaultcli
|
||||
if gotestflags != nil {
|
||||
cli = append(cli, strings.Split(*gotestflags, " ")...)
|
||||
}
|
||||
cli = append(cli, "./integration_tests/management/network_map_db/...")
|
||||
|
||||
return sh.RunWithV(map[string]string{"NETBIRD_STORE_ENGINE": "postgres"}, "go", cli...)
|
||||
}
|
||||
|
||||
func (Integrationtest) NmapDbSqlite(gotestflags *string) error {
|
||||
cli := defaultcli
|
||||
if gotestflags != nil {
|
||||
cli = append(cli, strings.Split(*gotestflags, " ")...)
|
||||
}
|
||||
cli = append(cli, "./integration_tests/management/network_map_db/...")
|
||||
return sh.RunWithV(map[string]string{"NETBIRD_STORE_ENGINE": "sqlite"}, "go", cli...)
|
||||
}
|
||||
|
||||
func (Integrationtest) RegenerateNmapGoldenData(gotestflags *string) error {
|
||||
cli := defaultcli
|
||||
if gotestflags != nil {
|
||||
cli = append(cli, strings.Split(*gotestflags, " ")...)
|
||||
}
|
||||
cli = append(cli, "./integration_tests/management/network_map_db/...")
|
||||
return sh.RunWithV(map[string]string{"NMAP_UPDATE_GOLDEN_DATA": "true", "NETBIRD_STORE_ENGINE": "sqlite"}, "go", cli...)
|
||||
}
|
||||
|
||||
func (Integrationtest) Api(gotestflags *string) error {
|
||||
cli := defaultcli
|
||||
if gotestflags != nil {
|
||||
cli = append(cli, strings.Split(*gotestflags, " ")...)
|
||||
}
|
||||
cli = append(cli, "./management/server/http/...")
|
||||
return sh.RunV("go", cli...)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/peers/ephemeral"
|
||||
networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db"
|
||||
"github.com/netbirdio/netbird/management/internals/server/config"
|
||||
"github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||
"github.com/netbirdio/netbird/management/server/account"
|
||||
@@ -30,6 +31,8 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
sharedgrpc "github.com/netbirdio/netbird/shared/management/grpc"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
"github.com/netbirdio/netbird/shared/management/status"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
@@ -61,6 +64,8 @@ type Controller struct {
|
||||
serverSupportedSyncMessageVersion sharedgrpc.SyncMessageVersion
|
||||
|
||||
perAccountServerSupportedSyncMessageVersions map[string]sharedgrpc.SyncMessageVersion
|
||||
|
||||
nmdataStore *networkmapdb.NetworkMapDBStoreImpl
|
||||
}
|
||||
|
||||
type bufferUpdate struct {
|
||||
@@ -78,7 +83,7 @@ type bufferAffectedUpdate struct {
|
||||
|
||||
var _ network_map.Controller = (*Controller)(nil)
|
||||
|
||||
func NewController(ctx context.Context, store store.Store, metrics telemetry.AppMetrics, peersUpdateManager network_map.PeersUpdateManager, requestBuffer account.RequestBuffer, integratedPeerValidator integrated_validator.IntegratedValidator, settingsManager settings.Manager, dnsDomain string, proxyController port_forwarding.Controller, ephemeralPeersManager ephemeral.Manager, config *config.Config) *Controller {
|
||||
func NewController(ctx context.Context, store store.Store, metrics telemetry.AppMetrics, peersUpdateManager network_map.PeersUpdateManager, requestBuffer account.RequestBuffer, integratedPeerValidator integrated_validator.IntegratedValidator, settingsManager settings.Manager, dnsDomain string, proxyController port_forwarding.Controller, ephemeralPeersManager ephemeral.Manager, config *config.Config, nmdataStore *networkmapdb.NetworkMapDBStoreImpl) *Controller {
|
||||
nMetrics, err := newMetrics(metrics.UpdateChannelMetrics())
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Errorf("error creating metrics: %w", err))
|
||||
@@ -99,6 +104,7 @@ func NewController(ctx context.Context, store store.Store, metrics telemetry.App
|
||||
EphemeralPeersManager: ephemeralPeersManager,
|
||||
serverSupportedSyncMessageVersion: sharedgrpc.SyncMessageVersionFromConfig(config.HighestSupportedSyncMessageVersion),
|
||||
perAccountServerSupportedSyncMessageVersions: sharedgrpc.SyncMessageVersionsFromMap(config.PerAccountHighestSupportedSyncMessageVersion),
|
||||
nmdataStore: nmdataStore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,12 +131,12 @@ func (c *Controller) OnPeerDisconnected(ctx context.Context, accountID string, p
|
||||
|
||||
// injectAllProxyPolicies prepares an account for the per-peer network-map
|
||||
// computation. It prepends the in-memory agent-network services synthesised
|
||||
// from the account's current provider/policy state to account.Services so
|
||||
// the existing InjectProxyPolicies + injectPrivateServicePolicies walks pick
|
||||
// them up alongside persisted reverse-proxy services. Synthesised services
|
||||
// are never persisted; the account is loaded fresh per cycle so re-prepending
|
||||
// is safe and idempotent. Accounts without agent-network providers get an
|
||||
// empty synth slice — no behaviour change.
|
||||
// from the account's current provider/policy state to account.Services, so the
|
||||
// twin store built from the account carries them alongside the persisted
|
||||
// reverse-proxy services and synthesises their ACLs. Synthesised services are
|
||||
// never persisted; the account is loaded fresh per cycle so re-prepending is
|
||||
// safe and idempotent. Accounts without agent-network providers get an empty
|
||||
// synth slice — no behaviour change.
|
||||
func (c *Controller) injectAllProxyPolicies(ctx context.Context, account *types.Account) {
|
||||
synth, err := c.repo.SynthesizeAgentNetworkServices(ctx, account.Id)
|
||||
if err != nil {
|
||||
@@ -138,7 +144,26 @@ func (c *Controller) injectAllProxyPolicies(ctx context.Context, account *types.
|
||||
} else if len(synth) > 0 {
|
||||
account.Services = append(synth, account.Services...)
|
||||
}
|
||||
account.InjectProxyPolicies(ctx)
|
||||
}
|
||||
|
||||
// proxyServicesFromRepo is the store-path counterpart of
|
||||
// injectAllProxyPolicies: the network-map store reads the policies table, which
|
||||
// never holds the proxy ACLs, so the twin gets the services they are
|
||||
// synthesised from — the synthesised agent-network ones first, exactly as the
|
||||
// account path orders them.
|
||||
func (c *Controller) proxyServicesFromRepo(ctx context.Context, accountID string) []*nmdata.Service {
|
||||
persisted, err := c.repo.GetAccountServices(ctx, accountID)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to get services for account %s: %v", accountID, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
synth, err := c.repo.SynthesizeAgentNetworkServices(ctx, accountID)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Warnf("synthesise agent-network services for account %s: %v", accountID, err)
|
||||
}
|
||||
|
||||
return types.TwinServices(append(synth, persisted...))
|
||||
}
|
||||
|
||||
func (c *Controller) CountStreams() int {
|
||||
@@ -147,6 +172,11 @@ func (c *Controller) CountStreams() int {
|
||||
|
||||
func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error {
|
||||
log.WithContext(ctx).Tracef("updating peers for account %s from %s", accountID, util.GetCallerName())
|
||||
|
||||
if nmData := c.getNetworkMapData(ctx, accountID); nmData != nil {
|
||||
return c.sendUpdateAccountPeersFromData(ctx, accountID, reason, nmData)
|
||||
}
|
||||
|
||||
account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get account: %v", err)
|
||||
@@ -167,7 +197,7 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
|
||||
return nil
|
||||
}
|
||||
|
||||
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
||||
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, types.TwinGroups(maps.Values(account.Groups)), types.TwinPeers(maps.Values(account.Peers)), account.Settings.Extra)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get validate peers: %v", err)
|
||||
}
|
||||
@@ -255,7 +285,7 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
|
||||
// proxyNetworkMap rides the envelope as a ProxyPatch sidecar;
|
||||
// the client merges it into Calculate()'s output the same
|
||||
// way the legacy server did via NetworkMap.Merge.
|
||||
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(p), nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, types.TwinAccountSettings(account.Settings), extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||
c.metrics.CountToComponentSyncResponseDuration(time.Since(start))
|
||||
|
||||
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
||||
@@ -276,7 +306,7 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
|
||||
}
|
||||
|
||||
start = time.Now()
|
||||
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(p), nil, nil, nmap, dnsDomain, postureChecks, dnsCache, types.TwinAccountSettings(account.Settings), extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||
c.metrics.CountToSyncResponseDuration(time.Since(start))
|
||||
|
||||
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
||||
@@ -294,6 +324,263 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendUpdateAccountPeersFromData is the account-free variant of
|
||||
// sendUpdateAccountPeers: everything is computed from the network-map DB
|
||||
// store's twin data; only extra settings and validated peers are resolved at
|
||||
// runtime. Proxy network maps and policy injection, private-service zones,
|
||||
// group-to-user SSH mappings and forced routing-peer DNS resolution have no
|
||||
// DB-backed source yet and are omitted.
|
||||
func (c *Controller) sendUpdateAccountPeersFromData(ctx context.Context, accountID string, reason types.UpdateReason, nmData *networkmap.NetworkMapData) error {
|
||||
peersToUpdate := c.connectedPeersFromData(nmData, nil)
|
||||
if len(peersToUpdate) == 0 {
|
||||
return nil
|
||||
}
|
||||
return c.sendUpdatesFromData(ctx, accountID, nmData, peersToUpdate, &reason)
|
||||
}
|
||||
|
||||
// sendUpdateForAffectedPeersFromData is the account-free variant of
|
||||
// sendUpdateForAffectedPeers.
|
||||
func (c *Controller) sendUpdateForAffectedPeersFromData(ctx context.Context, accountID string, peerIDs []string, nmData *networkmap.NetworkMapData) error {
|
||||
if len(peerIDs) == 0 {
|
||||
log.WithContext(ctx).Tracef("sendUpdateForAffectedPeersFromData: no affected peers")
|
||||
return nil
|
||||
}
|
||||
|
||||
peersToUpdate := c.connectedPeersFromData(nmData, peerIDs)
|
||||
if len(peersToUpdate) == 0 {
|
||||
log.WithContext(ctx).Tracef("sendUpdateForAffectedPeersFromData: no peers to update (affected peers not found in data or no channels)")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.WithContext(ctx).Tracef("sendUpdateForAffectedPeersFromData: sending network map to %d connected peers", len(peersToUpdate))
|
||||
|
||||
return c.sendUpdatesFromData(ctx, accountID, nmData, peersToUpdate, nil)
|
||||
}
|
||||
|
||||
// connectedPeersFromData returns the peers with an open update channel. An
|
||||
// empty affected list means all peers; a non-empty list restricts the result
|
||||
// to those peer IDs.
|
||||
func (c *Controller) connectedPeersFromData(nmData *networkmap.NetworkMapData, affected []string) []*nmdata.Peer {
|
||||
if len(affected) == 0 {
|
||||
result := make([]*nmdata.Peer, 0, len(nmData.Peers))
|
||||
for _, peer := range nmData.Peers {
|
||||
if c.peersUpdateManager.HasChannel(peer.ID) {
|
||||
result = append(result, peer)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
result := make([]*nmdata.Peer, 0, len(affected))
|
||||
for _, peerID := range affected {
|
||||
peer := nmData.Peers[peerID]
|
||||
if peer == nil {
|
||||
continue
|
||||
}
|
||||
if c.peersUpdateManager.HasChannel(peerID) {
|
||||
result = append(result, peer)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *Controller) sendUpdatesFromData(ctx context.Context, accountID string, nmData *networkmap.NetworkMapData, peersToUpdate []*nmdata.Peer, reason *types.UpdateReason) error {
|
||||
globalStart := time.Now()
|
||||
|
||||
extraSettings, err := c.settingsManager.GetExtraSettings(ctx, accountID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get flow enabled status: %v", err)
|
||||
}
|
||||
|
||||
nmData.PrecomputePostureValidation()
|
||||
|
||||
dnsCache := &cache.DNSConfigCache{}
|
||||
dnsDomain := c.getDNSDomainFromData(nmData.AccountSettings)
|
||||
peersCustomZone := networkmap.PeersCustomZone(ctx, accountID, dnsDomain, nmData.Peers, IPv6AllowedPeersFromData(nmData))
|
||||
|
||||
dnsFwdPort := ComputeForwarderPortFromData(nmData.Peers, network_map.DnsForwarderPortMinVersion)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
semaphore := make(chan struct{}, 10)
|
||||
|
||||
for _, peer := range peersToUpdate {
|
||||
if reason != nil && c.accountManagerMetrics != nil {
|
||||
c.accountManagerMetrics.CountNmapTriggered(string(reason.Resource), string(reason.Operation))
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
semaphore <- struct{}{}
|
||||
go func(p *nmdata.Peer) {
|
||||
defer wg.Done()
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
postureChecks := peerPostureChecksFromData(nmData, p.ID)
|
||||
|
||||
c.metrics.CountCalcPostureChecksDuration(time.Since(start))
|
||||
start = time.Now()
|
||||
|
||||
peerGroups := maps.Keys(nmData.GetPeerGroups(p.ID))
|
||||
var update *proto.SyncResponse
|
||||
|
||||
commonSyncMessageVersion := sharedgrpc.HighestCommonSyncMessageVersion(
|
||||
c.perAccountOrGlobalSupportedSyncMessageVersions(accountID),
|
||||
sharedgrpc.SyncMessageVersionFromConfig(&p.Meta.SyncMessageVersion))
|
||||
|
||||
log.WithContext(ctx).
|
||||
WithFields(log.Fields{
|
||||
"sync_message_version": commonSyncMessageVersion,
|
||||
"server_sync_message_version": c.perAccountOrGlobalSupportedSyncMessageVersions(accountID),
|
||||
"peer_sync_message_version": sharedgrpc.SyncMessageVersionFromConfig(&p.Meta.SyncMessageVersion),
|
||||
}).Debug("common highest sync message version")
|
||||
|
||||
if commonSyncMessageVersion == sharedgrpc.ComponentNetworkMap {
|
||||
components := nmData.GetPeerNetworkMapComponents(p.ID, peersCustomZone)
|
||||
|
||||
c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start))
|
||||
|
||||
start = time.Now()
|
||||
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, components, nil, dnsDomain, postureChecks, nmData.AccountSettings, extraSettings, peerGroups, dnsFwdPort)
|
||||
c.metrics.CountToComponentSyncResponseDuration(time.Since(start))
|
||||
|
||||
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
||||
Update: update,
|
||||
MessageType: network_map.MessageTypeNetworkMap,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
nmap := NetworkMapFromData(ctx, nmData, p.ID, peersCustomZone)
|
||||
|
||||
c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start))
|
||||
|
||||
start = time.Now()
|
||||
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, nmData.AccountSettings, extraSettings, peerGroups, dnsFwdPort)
|
||||
c.metrics.CountToSyncResponseDuration(time.Since(start))
|
||||
|
||||
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
||||
Update: update,
|
||||
MessageType: network_map.MessageTypeNetworkMap,
|
||||
})
|
||||
}(peer)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
if c.accountManagerMetrics != nil {
|
||||
c.accountManagerMetrics.CountUpdateAccountPeersDuration(time.Since(globalStart))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) getNetworkMapData(ctx context.Context, accountID string) *networkmap.NetworkMapData {
|
||||
if c.nmdataStore == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
nmData, err := c.nmdataStore.GetNetworkMapData(ctx, accountID)
|
||||
if err != nil {
|
||||
log.WithContext(ctx).Errorf("failed to get network map data for account %s, falling back to account-based computation: %v", accountID, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
nmData.Services = c.proxyServicesFromRepo(ctx, accountID)
|
||||
|
||||
return nmData
|
||||
}
|
||||
|
||||
func (c *Controller) getDNSDomainFromData(settings *nmdata.AccountSettingsInfo) string {
|
||||
if settings == nil || settings.DNSDomain == "" {
|
||||
return c.dnsDomain
|
||||
}
|
||||
return settings.DNSDomain
|
||||
}
|
||||
|
||||
func IPv6AllowedPeersFromData(nmData *networkmap.NetworkMapData) map[string]struct{} {
|
||||
result := make(map[string]struct{})
|
||||
if nmData.AccountSettings != nil {
|
||||
for _, groupID := range nmData.AccountSettings.IPv6EnabledGroups {
|
||||
group := nmData.Groups[groupID]
|
||||
if group == nil {
|
||||
continue
|
||||
}
|
||||
for _, peerID := range group.Peers {
|
||||
result[peerID] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
for id, p := range nmData.Peers {
|
||||
if p != nil && p.ProxyMeta.Embedded {
|
||||
result[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func NetworkMapFromData(ctx context.Context, nmData *networkmap.NetworkMapData, peerID string, peersCustomZone nmdata.CustomZone) *types.NetworkMap {
|
||||
components := nmData.GetPeerNetworkMapComponents(peerID, peersCustomZone)
|
||||
if components.IsEmpty() {
|
||||
return &types.NetworkMap{Network: components.Network}
|
||||
}
|
||||
return types.CalculateNetworkMapFromComponents(ctx, components)
|
||||
}
|
||||
|
||||
// peerPostureChecksFromData mirrors getPeerPostureChecks on the twin store. The
|
||||
// sync response only encodes process-check file paths, so only ProcessCheck is
|
||||
// converted back to the server posture type.
|
||||
func peerPostureChecksFromData(nmData *networkmap.NetworkMapData, peerID string) []*posture.Checks {
|
||||
if len(nmData.PostureChecks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
peerPostureChecks := make(map[string]*posture.Checks)
|
||||
for _, policy := range nmData.Policies {
|
||||
if policy == nil || !policy.Enabled || len(policy.SourcePostureChecks) == 0 {
|
||||
continue
|
||||
}
|
||||
if !isPeerInPolicySourceGroupsFromData(nmData, peerID, policy) {
|
||||
continue
|
||||
}
|
||||
for _, checkID := range policy.SourcePostureChecks {
|
||||
twin := nmData.PostureChecks[checkID]
|
||||
if twin == nil {
|
||||
continue
|
||||
}
|
||||
peerPostureChecks[checkID] = postureChecksFromTwin(twin)
|
||||
}
|
||||
}
|
||||
|
||||
return maps.Values(peerPostureChecks)
|
||||
}
|
||||
|
||||
func isPeerInPolicySourceGroupsFromData(nmData *networkmap.NetworkMapData, peerID string, policy *nmdata.Policy) bool {
|
||||
for _, rule := range policy.Rules {
|
||||
if rule == nil || !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
for _, groupID := range rule.Sources {
|
||||
if group := nmData.Groups[groupID]; group != nil && slices.Contains(group.Peers, peerID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func postureChecksFromTwin(twin *nmdata.PostureChecks) *posture.Checks {
|
||||
checks := &posture.Checks{ID: twin.ID}
|
||||
if twin.Checks.ProcessCheck != nil {
|
||||
processes := make([]posture.Process, 0, len(twin.Checks.ProcessCheck.Processes))
|
||||
for _, p := range twin.Checks.ProcessCheck.Processes {
|
||||
processes = append(processes, posture.Process{LinuxPath: p.LinuxPath, MacPath: p.MacPath, WindowsPath: p.WindowsPath})
|
||||
}
|
||||
checks.Checks.ProcessCheck = &posture.ProcessCheck{Processes: processes}
|
||||
}
|
||||
return checks
|
||||
}
|
||||
|
||||
func (c *Controller) perAccountOrGlobalSupportedSyncMessageVersions(accountId string) sharedgrpc.SyncMessageVersion {
|
||||
if perAccount, ok := c.perAccountServerSupportedSyncMessageVersions[accountId]; ok {
|
||||
return perAccount
|
||||
@@ -326,6 +613,10 @@ func (c *Controller) sendUpdateForAffectedPeers(ctx context.Context, accountID s
|
||||
return nil
|
||||
}
|
||||
|
||||
if nmData := c.getNetworkMapData(ctx, accountID); nmData != nil {
|
||||
return c.sendUpdateForAffectedPeersFromData(ctx, accountID, peerIDs, nmData)
|
||||
}
|
||||
|
||||
account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get account: %v", err)
|
||||
@@ -341,7 +632,7 @@ func (c *Controller) sendUpdateForAffectedPeers(ctx context.Context, accountID s
|
||||
|
||||
log.WithContext(ctx).Tracef("sendUpdateForAffectedPeers: sending network map to %d connected peers", len(peersToUpdate))
|
||||
|
||||
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
||||
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, types.TwinGroups(maps.Values(account.Groups)), types.TwinPeers(maps.Values(account.Peers)), account.Settings.Extra)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get validate peers: %v", err)
|
||||
}
|
||||
@@ -428,7 +719,7 @@ func (c *Controller) sendUpdateForAffectedPeers(ctx context.Context, accountID s
|
||||
// proxyNetworkMap rides the envelope as a ProxyPatch sidecar;
|
||||
// the client merges it into Calculate()'s output the same
|
||||
// way the legacy server did via NetworkMap.Merge.
|
||||
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(p), nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, types.TwinAccountSettings(account.Settings), extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||
c.metrics.CountToComponentSyncResponseDuration(time.Since(start))
|
||||
|
||||
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
||||
@@ -449,7 +740,7 @@ func (c *Controller) sendUpdateForAffectedPeers(ctx context.Context, accountID s
|
||||
}
|
||||
|
||||
start = time.Now()
|
||||
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(p), nil, nil, nmap, dnsDomain, postureChecks, dnsCache, types.TwinAccountSettings(account.Settings), extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||
c.metrics.CountToSyncResponseDuration(time.Since(start))
|
||||
|
||||
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
||||
@@ -506,7 +797,7 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe
|
||||
return fmt.Errorf("peer %s doesn't exists in account %s", peerId, accountId)
|
||||
}
|
||||
|
||||
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
||||
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, types.TwinGroups(maps.Values(account.Groups)), types.TwinPeers(maps.Values(account.Peers)), account.Settings.Extra)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get validated peers: %v", err)
|
||||
}
|
||||
@@ -566,7 +857,7 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe
|
||||
// proxyNetworkMap rides the envelope as a ProxyPatch sidecar;
|
||||
// the client merges it into Calculate()'s output the same
|
||||
// way the legacy server did via NetworkMap.Merge.
|
||||
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, peer, nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSettings, maps.Keys(peerGroups), dnsFwdPort)
|
||||
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(peer), nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, types.TwinAccountSettings(account.Settings), extraSettings, maps.Keys(peerGroups), dnsFwdPort)
|
||||
|
||||
c.peersUpdateManager.SendUpdate(ctx, peer.ID, &network_map.UpdateMessage{
|
||||
Update: update,
|
||||
@@ -583,7 +874,7 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe
|
||||
nmap.Merge(proxyNetworkMap)
|
||||
}
|
||||
|
||||
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, peer, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSettings, maps.Keys(peerGroups), dnsFwdPort)
|
||||
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(peer), nil, nil, nmap, dnsDomain, postureChecks, dnsCache, types.TwinAccountSettings(account.Settings), extraSettings, maps.Keys(peerGroups), dnsFwdPort)
|
||||
|
||||
c.peersUpdateManager.SendUpdate(ctx, peer.ID, &network_map.UpdateMessage{
|
||||
Update: update,
|
||||
@@ -643,7 +934,11 @@ func (c *Controller) GetValidatedPeerWithComponents(ctx context.Context, isRequi
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, 0, err
|
||||
}
|
||||
return peer, &types.NetworkMapComponents{Network: network.Copy()}, nil, nil, 0, nil
|
||||
return peer, &types.NetworkMapComponents{Network: types.TwinNetwork(network)}, nil, nil, 0, nil
|
||||
}
|
||||
|
||||
if nmData := c.getNetworkMapData(ctx, accountID); nmData != nil {
|
||||
return c.getValidatedPeerWithComponentsFromData(ctx, accountID, peer, nmData)
|
||||
}
|
||||
|
||||
account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
|
||||
@@ -653,7 +948,7 @@ func (c *Controller) GetValidatedPeerWithComponents(ctx context.Context, isRequi
|
||||
|
||||
c.injectAllProxyPolicies(ctx, account)
|
||||
|
||||
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
||||
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, types.TwinGroups(maps.Values(account.Groups)), types.TwinPeers(maps.Values(account.Peers)), account.Settings.Extra)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, 0, err
|
||||
}
|
||||
@@ -690,6 +985,21 @@ func (c *Controller) GetValidatedPeerWithComponents(ctx context.Context, isRequi
|
||||
return peer, components, proxyNetworkMaps[peer.ID], postureChecks, dnsFwdPort, nil
|
||||
}
|
||||
|
||||
// getValidatedPeerWithComponentsFromData is the account-free variant of
|
||||
// GetValidatedPeerWithComponents. The proxy network map fragment is omitted
|
||||
// like on the other nmdata paths.
|
||||
func (c *Controller) getValidatedPeerWithComponentsFromData(ctx context.Context, accountID string, peer *nbpeer.Peer, nmData *networkmap.NetworkMapData) (*nbpeer.Peer, *types.NetworkMapComponents, *types.NetworkMap, []*posture.Checks, int64, error) {
|
||||
postureChecks := peerPostureChecksFromData(nmData, peer.ID)
|
||||
|
||||
dnsDomain := c.getDNSDomainFromData(nmData.AccountSettings)
|
||||
peersCustomZone := networkmap.PeersCustomZone(ctx, accountID, dnsDomain, nmData.Peers, IPv6AllowedPeersFromData(nmData))
|
||||
|
||||
components := nmData.GetPeerNetworkMapComponents(peer.ID, peersCustomZone)
|
||||
dnsFwdPort := ComputeForwarderPortFromData(nmData.Peers, network_map.DnsForwarderPortMinVersion)
|
||||
|
||||
return peer, components, nil, postureChecks, dnsFwdPort, nil
|
||||
}
|
||||
|
||||
// BufferUpdateAffectedPeers accumulates peer IDs and flushes them after the buffer interval.
|
||||
func (c *Controller) BufferUpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string, reason types.UpdateReason) error {
|
||||
if len(peerIDs) == 0 {
|
||||
@@ -796,11 +1106,15 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr
|
||||
}
|
||||
|
||||
emptyMap := &types.NetworkMap{
|
||||
Network: network.Copy(),
|
||||
Network: types.TwinNetwork(network),
|
||||
}
|
||||
return emptyMap, nil, 0, nil
|
||||
}
|
||||
|
||||
if nmData := c.getNetworkMapData(ctx, accountID); nmData != nil {
|
||||
return c.getValidatedPeerWithMapFromData(ctx, accountID, peerID, nmData)
|
||||
}
|
||||
|
||||
account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
@@ -808,7 +1122,7 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr
|
||||
|
||||
c.injectAllProxyPolicies(ctx, account)
|
||||
|
||||
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
||||
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, types.TwinGroups(maps.Values(account.Groups)), types.TwinPeers(maps.Values(account.Peers)), account.Settings.Extra)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
@@ -848,6 +1162,21 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr
|
||||
return networkMap, postureChecks, dnsFwdPort, nil
|
||||
}
|
||||
|
||||
// getValidatedPeerWithMapFromData is the account-free variant of
|
||||
// GetValidatedPeerWithMap. The proxy network map fragment is omitted like on
|
||||
// the other nmdata paths.
|
||||
func (c *Controller) getValidatedPeerWithMapFromData(ctx context.Context, accountID string, peerID string, nmData *networkmap.NetworkMapData) (*types.NetworkMap, []*posture.Checks, int64, error) {
|
||||
postureChecks := peerPostureChecksFromData(nmData, peerID)
|
||||
|
||||
dnsDomain := c.getDNSDomainFromData(nmData.AccountSettings)
|
||||
peersCustomZone := networkmap.PeersCustomZone(ctx, accountID, dnsDomain, nmData.Peers, IPv6AllowedPeersFromData(nmData))
|
||||
|
||||
networkMap := NetworkMapFromData(ctx, nmData, peerID, peersCustomZone)
|
||||
dnsFwdPort := ComputeForwarderPortFromData(nmData.Peers, network_map.DnsForwarderPortMinVersion)
|
||||
|
||||
return networkMap, postureChecks, dnsFwdPort, nil
|
||||
}
|
||||
|
||||
// GetDNSDomain returns the configured dnsDomain
|
||||
func (c *Controller) GetDNSDomain(settings *types.Settings) string {
|
||||
if settings == nil {
|
||||
@@ -910,20 +1239,36 @@ func (c *Controller) StartWarmup(ctx context.Context) {
|
||||
// computeForwarderPort checks if all peers in the account have updated to a specific version or newer.
|
||||
// If all peers have the required version, it returns the new well-known port (22054), otherwise returns 0.
|
||||
func computeForwarderPort(peers []*nbpeer.Peer, requiredVersion string) int64 {
|
||||
if len(peers) == 0 {
|
||||
versions := make([]string, 0, len(peers))
|
||||
for _, peer := range peers {
|
||||
versions = append(versions, peer.Meta.WtVersion)
|
||||
}
|
||||
return computeForwarderPortFromVersions(versions, requiredVersion)
|
||||
}
|
||||
|
||||
func ComputeForwarderPortFromData(peers map[string]*nmdata.Peer, requiredVersion string) int64 {
|
||||
versions := make([]string, 0, len(peers))
|
||||
for _, peer := range peers {
|
||||
versions = append(versions, peer.Meta.WtVersion)
|
||||
}
|
||||
return computeForwarderPortFromVersions(versions, requiredVersion)
|
||||
}
|
||||
|
||||
func computeForwarderPortFromVersions(wtVersions []string, requiredVersion string) int64 {
|
||||
if len(wtVersions) == 0 {
|
||||
return int64(network_map.OldForwarderPort)
|
||||
}
|
||||
|
||||
reqVer := semver.Canonical(requiredVersion)
|
||||
|
||||
// Check if all peers have the required version or newer
|
||||
for _, peer := range peers {
|
||||
for _, wtVersion := range wtVersions {
|
||||
|
||||
// Development version is always supported
|
||||
if version.IsDevelopmentVersion(peer.Meta.WtVersion) {
|
||||
if version.IsDevelopmentVersion(wtVersion) {
|
||||
continue
|
||||
}
|
||||
peerVersion := semver.Canonical("v" + peer.Meta.WtVersion)
|
||||
peerVersion := semver.Canonical("v" + wtVersion)
|
||||
if peerVersion == "" {
|
||||
// If any peer doesn't have version info, return 0
|
||||
return int64(network_map.OldForwarderPort)
|
||||
@@ -1057,7 +1402,7 @@ func (c *Controller) GetNetworkMap(ctx context.Context, peerID string) (*types.N
|
||||
groups[groupID] = group.Peers
|
||||
}
|
||||
|
||||
validatedPeers, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
||||
validatedPeers, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, types.TwinGroups(maps.Values(account.Groups)), types.TwinPeers(maps.Values(account.Peers)), account.Settings.Extra)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package controller
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
|
||||
"github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
@@ -22,6 +22,7 @@ type Repository interface {
|
||||
// services synthesised from the account's agent-network provider/policy
|
||||
// state. Empty for accounts without agent-network providers.
|
||||
SynthesizeAgentNetworkServices(ctx context.Context, accountID string) ([]*service.Service, error)
|
||||
GetAccountServices(ctx context.Context, accountID string) ([]*service.Service, error)
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
@@ -60,6 +61,10 @@ func (r *repository) SynthesizeAgentNetworkServices(ctx context.Context, account
|
||||
return agentnetwork.SynthesizeServices(ctx, r.store, accountID)
|
||||
}
|
||||
|
||||
func (r *repository) GetAccountServices(ctx context.Context, accountID string) ([]*service.Service, error) {
|
||||
return r.store.GetAccountServices(ctx, store.LockingStrengthNone, accountID)
|
||||
}
|
||||
|
||||
func (r *repository) GetAccountZones(ctx context.Context, accountID string) ([]*zones.Zone, error) {
|
||||
return r.store.GetAccountZones(ctx, store.LockingStrengthNone, accountID)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
package nmaptest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
// normalizeIDSpace replaces policy and route identifiers with positional
|
||||
// placeholders so a comparison can reach everything else.
|
||||
//
|
||||
// This exists only because the envelope round-trip currently substitutes each
|
||||
// internal xid with the object's public id, which is a tracked defect and not a
|
||||
// licence to differ: those identifiers reach the server again inside flow
|
||||
// events, which resolve them by internal id, so the substitution silently
|
||||
// breaks flow attribution for component-format peers. TestIDSpaceMatches
|
||||
// asserts the equality that must eventually hold; this erasure keeps the other
|
||||
// 40-odd cases reporting on semantics meanwhile. When the id space is unified,
|
||||
// delete this and the calls to it — every case should still pass.
|
||||
//
|
||||
// Cardinality and cross-references survive the erasure: two rules under one
|
||||
// policy still share a token and a route firewall rule still points at its
|
||||
// route, so a path that drops a policy, merges two policies, or misattributes a
|
||||
// rule to the wrong route still fails.
|
||||
func normalizeIDSpace(nm *proto.NetworkMap) {
|
||||
if nm == nil {
|
||||
return
|
||||
}
|
||||
policies := newTokenizer("policy")
|
||||
routes := newTokenizer("route")
|
||||
|
||||
for _, i := range orderBy(nm.Routes, routeKeyWithoutID) {
|
||||
nm.Routes[i].ID = routes.get(nm.Routes[i].ID)
|
||||
}
|
||||
for _, i := range orderBy(nm.FirewallRules, firewallKeyWithoutPolicy) {
|
||||
r := nm.FirewallRules[i]
|
||||
if len(r.PolicyID) > 0 {
|
||||
r.PolicyID = []byte(policies.get(string(r.PolicyID)))
|
||||
}
|
||||
}
|
||||
for _, i := range orderBy(nm.RoutesFirewallRules, routeFirewallKeyWithoutIDs) {
|
||||
r := nm.RoutesFirewallRules[i]
|
||||
if len(r.PolicyID) > 0 {
|
||||
r.PolicyID = []byte(policies.get(string(r.PolicyID)))
|
||||
}
|
||||
r.RouteID = routes.get(r.RouteID)
|
||||
}
|
||||
}
|
||||
|
||||
// tokenizer maps identifiers to positional placeholders in order of first use.
|
||||
type tokenizer struct {
|
||||
prefix string
|
||||
seen map[string]string
|
||||
}
|
||||
|
||||
func newTokenizer(prefix string) *tokenizer {
|
||||
return &tokenizer{prefix: prefix, seen: make(map[string]string)}
|
||||
}
|
||||
|
||||
func (t *tokenizer) get(id string) string {
|
||||
if id == "" {
|
||||
return ""
|
||||
}
|
||||
if tok, ok := t.seen[id]; ok {
|
||||
return tok
|
||||
}
|
||||
tok := fmt.Sprintf("%s#%d", t.prefix, len(t.seen))
|
||||
t.seen[id] = tok
|
||||
return tok
|
||||
}
|
||||
|
||||
// orderBy returns indices sorted by key, so placeholder numbering does not
|
||||
// depend on the identifiers being erased.
|
||||
func orderBy[T any](items []T, key func(T) string) []int {
|
||||
idx := make([]int, len(items))
|
||||
for i := range idx {
|
||||
idx[i] = i
|
||||
}
|
||||
sort.SliceStable(idx, func(a, b int) bool { return key(items[idx[a]]) < key(items[idx[b]]) })
|
||||
return idx
|
||||
}
|
||||
|
||||
func routeKeyWithoutID(r *proto.Route) string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s|%s|%s|%d|%d|%t|%t|%v",
|
||||
r.Network, r.NetID, r.Peer, r.Metric, r.NetworkType, r.Masquerade, r.KeepRoute, r.Domains)
|
||||
}
|
||||
|
||||
func firewallKeyWithoutPolicy(r *proto.FirewallRule) string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s|%d|%d|%d|%s|%s|%v",
|
||||
r.PeerIP, r.Direction, r.Action, r.Protocol, r.Port, portInfoKey(r.PortInfo), r.SourcePrefixes) //nolint:staticcheck
|
||||
}
|
||||
|
||||
func routeFirewallKeyWithoutIDs(r *proto.RouteFirewallRule) string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s|%d|%d|%s|%v|%v|%t|%d",
|
||||
r.Destination, r.Protocol, r.Action, portInfoKey(r.PortInfo), r.Domains, r.SourceRanges, r.IsDynamic, r.CustomProtocol)
|
||||
}
|
||||
|
||||
// canonicalize sorts every repeated field of the NetworkMap by a stable key.
|
||||
// The producing paths iterate Go maps while building these slices, so order
|
||||
// can differ between runs even when the content is identical; comparing
|
||||
// without this 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 ""
|
||||
}
|
||||
218
management/internals/controllers/network_map/nmaptest/fixture.go
Normal file
218
management/internals/controllers/network_map/nmaptest/fixture.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package nmaptest
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
// LoadNetworkMapData reads a fixture holding the NetworkMapData the store
|
||||
// would return for one account. Unknown fields are rejected so fixture typos
|
||||
// fail loudly instead of silently testing a default.
|
||||
func LoadNetworkMapData(path string) (*networkmap.NetworkMapData, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open fixture: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
dec := json.NewDecoder(f)
|
||||
dec.DisallowUnknownFields()
|
||||
var nmData networkmap.NetworkMapData
|
||||
if err := dec.Decode(&nmData); err != nil {
|
||||
return nil, fmt.Errorf("decode fixture %s: %w", path, err)
|
||||
}
|
||||
return &nmData, nil
|
||||
}
|
||||
|
||||
var defaultNetworkNet = func() net.IPNet {
|
||||
_, ipnet, err := net.ParseCIDR("100.64.0.0/10")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return *ipnet
|
||||
}()
|
||||
|
||||
// applyFixtureDefaults fills the boilerplate a fixture may omit. Map-keyed
|
||||
// objects inherit their key as ID, peers get a deterministic WG-shaped key
|
||||
// and their ID as DNS label, PublicIDs default to the internal ID (the
|
||||
// envelope encoder puts public IDs on the wire and silently degrades on
|
||||
// empty ones), and a nil ValidatedPeers validates every peer — production
|
||||
// fills it through the integrated validator, not the store.
|
||||
func applyFixtureDefaults(nmData *networkmap.NetworkMapData) {
|
||||
if nmData.Network == nil {
|
||||
nmData.Network = &nmdata.Network{}
|
||||
}
|
||||
if nmData.Network.Identifier == "" {
|
||||
nmData.Network.Identifier = "network"
|
||||
}
|
||||
if nmData.Network.Net.IP == nil {
|
||||
nmData.Network.Net = defaultNetworkNet
|
||||
}
|
||||
if nmData.AccountSettings == nil {
|
||||
nmData.AccountSettings = &nmdata.AccountSettingsInfo{}
|
||||
}
|
||||
if nmData.DNSSettings == nil {
|
||||
nmData.DNSSettings = &nmdata.DNSSettings{}
|
||||
}
|
||||
|
||||
for id, p := range nmData.Peers {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
if p.ID == "" {
|
||||
p.ID = id
|
||||
}
|
||||
if p.Key == "" {
|
||||
p.Key = derivedWgKey(p.ID)
|
||||
}
|
||||
if p.DNSLabel == "" {
|
||||
p.DNSLabel = p.ID
|
||||
}
|
||||
}
|
||||
|
||||
for id, g := range nmData.Groups {
|
||||
if g == nil {
|
||||
continue
|
||||
}
|
||||
if g.ID == "" {
|
||||
g.ID = id
|
||||
}
|
||||
if g.Name == "" {
|
||||
g.Name = g.ID
|
||||
}
|
||||
if g.PublicID == "" {
|
||||
g.PublicID = g.ID
|
||||
}
|
||||
}
|
||||
|
||||
for _, policy := range nmData.Policies {
|
||||
defaultPolicyIDs(policy)
|
||||
}
|
||||
resolveResourcePolicyRefs(nmData)
|
||||
|
||||
for _, r := range nmData.Routes {
|
||||
if r != nil && r.PublicID == "" {
|
||||
r.PublicID = r.ID
|
||||
}
|
||||
}
|
||||
for _, nsg := range nmData.NameServerGroups {
|
||||
if nsg != nil && nsg.PublicID == "" {
|
||||
nsg.PublicID = nsg.ID
|
||||
}
|
||||
}
|
||||
for _, res := range nmData.NetworkResources {
|
||||
if res == nil {
|
||||
continue
|
||||
}
|
||||
if res.PublicID == "" {
|
||||
res.PublicID = res.ID
|
||||
}
|
||||
defaultXIDMapping(&nmData.NetworkXIDToPublicID, res.NetworkID)
|
||||
}
|
||||
for networkID, routers := range nmData.Routers {
|
||||
defaultXIDMapping(&nmData.NetworkXIDToPublicID, networkID)
|
||||
for _, router := range routers {
|
||||
if router != nil && router.PublicID == "" {
|
||||
router.PublicID = networkID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id, pc := range nmData.PostureChecks {
|
||||
if pc == nil {
|
||||
continue
|
||||
}
|
||||
if pc.ID == "" {
|
||||
pc.ID = id
|
||||
}
|
||||
defaultXIDMapping(&nmData.PostureCheckXIDToPublicID, pc.ID)
|
||||
}
|
||||
|
||||
if nmData.ValidatedPeers == nil {
|
||||
nmData.ValidatedPeers = make(map[string]struct{}, len(nmData.Peers))
|
||||
for id := range nmData.Peers {
|
||||
nmData.ValidatedPeers[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resolveResourcePolicyRefs lets a fixture name an account policy by ID in
|
||||
// ResourcePolicies — {"ID": "pol-x"} with no rules — instead of repeating it.
|
||||
// The real store puts the same policy pointer in both places, which is what
|
||||
// resolving the reference reproduces.
|
||||
func resolveResourcePolicyRefs(nmData *networkmap.NetworkMapData) {
|
||||
byID := make(map[string]*nmdata.Policy, len(nmData.Policies))
|
||||
for _, policy := range nmData.Policies {
|
||||
if policy != nil && policy.ID != "" {
|
||||
byID[policy.ID] = policy
|
||||
}
|
||||
}
|
||||
|
||||
for _, policies := range nmData.ResourcePolicies {
|
||||
for i, policy := range policies {
|
||||
if policy == nil {
|
||||
continue
|
||||
}
|
||||
if len(policy.Rules) == 0 {
|
||||
if full, ok := byID[policy.ID]; ok {
|
||||
policies[i] = full
|
||||
continue
|
||||
}
|
||||
}
|
||||
defaultPolicyIDs(policy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func defaultPolicyIDs(policy *nmdata.Policy) {
|
||||
if policy == nil {
|
||||
return
|
||||
}
|
||||
if policy.PublicID == "" {
|
||||
policy.PublicID = policy.ID
|
||||
}
|
||||
for i, rule := range policy.Rules {
|
||||
if rule == nil {
|
||||
continue
|
||||
}
|
||||
if rule.PolicyID == "" {
|
||||
rule.PolicyID = policy.ID
|
||||
}
|
||||
if rule.ID == "" {
|
||||
// Production gives a rule its policy's id (management/server/policy.go:205,
|
||||
// "when policy can contain multiple rules, need refactor"), so a
|
||||
// single-rule policy — the only shape the product can create today —
|
||||
// must be modelled that way or the wire ids come out unrealistic.
|
||||
rule.ID = policy.ID
|
||||
if len(policy.Rules) > 1 {
|
||||
rule.ID = fmt.Sprintf("%s-rule-%d", policy.ID, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func defaultXIDMapping(m *map[string]string, id string) {
|
||||
if id == "" {
|
||||
return
|
||||
}
|
||||
if *m == nil {
|
||||
*m = make(map[string]string)
|
||||
}
|
||||
if _, ok := (*m)[id]; !ok {
|
||||
(*m)[id] = id
|
||||
}
|
||||
}
|
||||
|
||||
// derivedWgKey returns a deterministic base64 key of 32 bytes, valid for the
|
||||
// envelope decoder's WG-key identity.
|
||||
func derivedWgKey(peerID string) string {
|
||||
sum := sha256.Sum256([]byte(peerID))
|
||||
return base64.StdEncoding.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package nmaptest_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/nmaptest"
|
||||
)
|
||||
|
||||
func TestNetworkMapGolden(t *testing.T) {
|
||||
nmaptest.RunGoldenDir(t, filepath.Join("testdata", "cases"))
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
package nmaptest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
||||
"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/management/server/posture"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/management/server/types/legacynmap"
|
||||
nbroute "github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
sharedtypes "github.com/netbirdio/netbird/shared/management/types"
|
||||
)
|
||||
|
||||
// legacyInput is the account and the four derived arguments main's computation
|
||||
// took alongside it. The controller resolved them from the account before
|
||||
// calling; the twin carries them as fields, so the fixture is the source for
|
||||
// both halves.
|
||||
type legacyInput struct {
|
||||
account *types.Account
|
||||
accountZones []*zones.Zone
|
||||
validatedPeers map[string]struct{}
|
||||
resourcePolicies map[string][]*types.Policy
|
||||
routers map[string]map[string]*routerTypes.NetworkRouter
|
||||
groupIDToUserIDs map[string][]string
|
||||
}
|
||||
|
||||
// legacyInputFromData rebuilds the Account the fixture stands for. A fixture is
|
||||
// the value the store returns, and the store's twins carry exactly the state
|
||||
// the computation reads, so inverting them reproduces the account main would
|
||||
// have loaded — which is what lets one expectation measure all three paths.
|
||||
//
|
||||
// The inverse is only defined for what a twin carries: fields the builders drop
|
||||
// (peer names, policy descriptions, user records behind AllowedUserIDs) come
|
||||
// back as the zero value or a minimal stand-in, because no path reads them.
|
||||
func legacyInputFromData(accountID string, nmData *networkmap.NetworkMapData) legacyInput {
|
||||
account := &types.Account{
|
||||
Id: accountID,
|
||||
Network: accountNetwork(nmData.Network),
|
||||
Settings: accountSettings(nmData.AccountSettings),
|
||||
DNSSettings: types.DNSSettings{DisabledManagementGroups: nmData.DNSSettings.DisabledManagementGroups},
|
||||
Peers: make(map[string]*nbpeer.Peer, len(nmData.Peers)),
|
||||
Groups: make(map[string]*types.Group, len(nmData.Groups)),
|
||||
Policies: make([]*types.Policy, 0, len(nmData.Policies)),
|
||||
Routes: make(map[nbroute.ID]*nbroute.Route, len(nmData.Routes)),
|
||||
NameServerGroups: make(map[string]*nbdns.NameServerGroup, len(nmData.NameServerGroups)),
|
||||
NetworkResources: make([]*resourceTypes.NetworkResource, 0, len(nmData.NetworkResources)),
|
||||
PostureChecks: make([]*posture.Checks, 0, len(nmData.PostureChecks)),
|
||||
Users: make(map[string]*types.User, len(nmData.AllowedUserIDs)),
|
||||
Services: accountServices(nmData.Services),
|
||||
}
|
||||
|
||||
for id, p := range nmData.Peers {
|
||||
account.Peers[id] = accountPeer(id, p)
|
||||
}
|
||||
for id, g := range nmData.Groups {
|
||||
account.Groups[id] = accountGroup(id, g)
|
||||
}
|
||||
|
||||
policiesByID := make(map[string]*types.Policy, len(nmData.Policies))
|
||||
for _, p := range nmData.Policies {
|
||||
policy := accountPolicy(p)
|
||||
if policy == nil {
|
||||
continue
|
||||
}
|
||||
account.Policies = append(account.Policies, policy)
|
||||
policiesByID[policy.ID] = policy
|
||||
}
|
||||
|
||||
for _, r := range nmData.Routes {
|
||||
route := accountRoute(r)
|
||||
if route != nil {
|
||||
account.Routes[route.ID] = route
|
||||
}
|
||||
}
|
||||
for _, nsg := range nmData.NameServerGroups {
|
||||
group := accountNSG(nsg)
|
||||
if group != nil {
|
||||
account.NameServerGroups[group.ID] = group
|
||||
}
|
||||
}
|
||||
for _, res := range nmData.NetworkResources {
|
||||
if resource := accountNetworkResource(res); resource != nil {
|
||||
account.NetworkResources = append(account.NetworkResources, resource)
|
||||
}
|
||||
}
|
||||
for id, pc := range nmData.PostureChecks {
|
||||
if check := accountPostureChecks(id, pc, nmData.PostureCheckXIDToPublicID[id]); check != nil {
|
||||
account.PostureChecks = append(account.PostureChecks, check)
|
||||
}
|
||||
}
|
||||
for xid, publicID := range nmData.NetworkXIDToPublicID {
|
||||
account.Networks = append(account.Networks, &networkTypes.Network{ID: xid, PublicID: publicID})
|
||||
}
|
||||
// The twin keeps only the ids of the users a peer may be shared with; the
|
||||
// legacy side derives the same set from the account's user records, so a
|
||||
// bare non-blocked regular user per id is enough.
|
||||
for userID := range nmData.AllowedUserIDs {
|
||||
account.Users[userID] = &types.User{Id: userID}
|
||||
}
|
||||
|
||||
// Main's network-map controller synthesised the reverse-proxy ACLs onto the
|
||||
// account and only then derived the resource-policy map, so the frozen copy
|
||||
// has to be fed in that order to stand for what main produced.
|
||||
account.Policies = append(account.Policies, legacynmap.SynthesizeProxyPolicies(account)...)
|
||||
|
||||
return legacyInput{
|
||||
account: account,
|
||||
accountZones: accountZones(nmData.AppliedZoneCandidates),
|
||||
validatedPeers: nmData.ValidatedPeers,
|
||||
resourcePolicies: account.GetResourcePoliciesMap(),
|
||||
routers: accountRouters(nmData.Routers),
|
||||
groupIDToUserIDs: nmData.GroupIDToUserIDs,
|
||||
}
|
||||
}
|
||||
|
||||
// computeLegacy runs the fixture through main's frozen path and its own proto
|
||||
// encoder, the one comparison surface the three modes share.
|
||||
func computeLegacy(t *testing.T, ctx context.Context, legacy legacyInput, peerID string, zone nmdata.CustomZone, dnsDomain string, dnsFwdPort int64) *proto.NetworkMap {
|
||||
t.Helper()
|
||||
|
||||
require.NotNil(t, legacy.account, "legacy mode needs an account rebuilt from the fixture")
|
||||
peer := legacy.account.Peers[peerID]
|
||||
require.NotNil(t, peer, "target peer %q not in rebuilt account", peerID)
|
||||
|
||||
nm := legacynmap.GetPeerNetworkMapFromComponents(
|
||||
legacy.account, ctx, peerID, legacyCustomZone(zone), legacy.accountZones, legacy.validatedPeers,
|
||||
legacy.resourcePolicies, legacy.routers, nil, legacy.groupIDToUserIDs,
|
||||
)
|
||||
require.NotNil(t, nm, "legacy path returned no network map for peer %q", peerID)
|
||||
|
||||
return legacynmap.ToProtoNetworkMap(
|
||||
ctx, peer, nm, dnsDomain, legacy.account.Settings, nil, &cache.DNSConfigCache{}, dnsFwdPort,
|
||||
)
|
||||
}
|
||||
|
||||
// legacyCustomZone converts the peers custom zone the runner computes once for
|
||||
// every mode into the shape main's path took.
|
||||
func legacyCustomZone(z nmdata.CustomZone) nbdns.CustomZone {
|
||||
zoneRecords := make([]nbdns.SimpleRecord, 0, len(z.Records))
|
||||
for _, r := range z.Records {
|
||||
zoneRecords = append(zoneRecords, nbdns.SimpleRecord{
|
||||
Name: r.Name,
|
||||
Type: r.Type,
|
||||
Class: r.Class,
|
||||
TTL: r.TTL,
|
||||
RData: r.RData,
|
||||
})
|
||||
}
|
||||
return nbdns.CustomZone{
|
||||
Domain: z.Domain,
|
||||
Records: zoneRecords,
|
||||
SearchDomainDisabled: z.SearchDomainDisabled,
|
||||
NonAuthoritative: z.NonAuthoritative,
|
||||
}
|
||||
}
|
||||
|
||||
func accountNetwork(n *nmdata.Network) *types.Network {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.Network{
|
||||
Identifier: n.Identifier,
|
||||
Net: n.Net,
|
||||
NetV6: n.NetV6,
|
||||
Dns: n.Dns,
|
||||
Serial: uint64(n.Serial),
|
||||
}
|
||||
}
|
||||
|
||||
func accountSettings(s *nmdata.AccountSettingsInfo) *types.Settings {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.Settings{
|
||||
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 accountPeer(id string, p *nmdata.Peer) *nbpeer.Peer {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
networkAddresses := make([]nbpeer.NetworkAddress, 0, len(p.Meta.NetworkAddresses))
|
||||
for _, na := range p.Meta.NetworkAddresses {
|
||||
networkAddresses = append(networkAddresses, nbpeer.NetworkAddress{NetIP: na.NetIP})
|
||||
}
|
||||
files := make([]nbpeer.File, 0, len(p.Meta.Files))
|
||||
for _, f := range p.Meta.Files {
|
||||
files = append(files, nbpeer.File{Path: f.Path, ProcessIsRunning: f.ProcessIsRunning})
|
||||
}
|
||||
return &nbpeer.Peer{
|
||||
ID: 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,
|
||||
ExtraDNSLabels: p.ExtraDNSLabels,
|
||||
ProxyMeta: nbpeer.ProxyMeta{Embedded: p.ProxyMeta.Embedded, Cluster: p.ProxyMeta.Cluster},
|
||||
// Connected is what SynthesizePrivateServiceZones gates its records on,
|
||||
// and a fixture peer stands for a peer the store returned, so it is one
|
||||
// the account would have reported connected.
|
||||
Status: &nbpeer.PeerStatus{RequiresApproval: p.RequiresApproval, Connected: true},
|
||||
Meta: nbpeer.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: nbpeer.Flags{
|
||||
ServerSSHAllowed: p.Meta.Flags.ServerSSHAllowed,
|
||||
DisableIPv6: p.Meta.Flags.DisableIPv6,
|
||||
},
|
||||
},
|
||||
Location: nbpeer.Location{
|
||||
CountryCode: p.Location.CountryCode,
|
||||
CityName: p.Location.CityName,
|
||||
ConnectionIP: p.Location.ConnectionIP,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func accountGroup(id string, g *nmdata.Group) *types.Group {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.Group{
|
||||
ID: id,
|
||||
Name: g.Name,
|
||||
PublicID: g.PublicID,
|
||||
Peers: g.Peers,
|
||||
}
|
||||
}
|
||||
|
||||
func accountPolicy(p *nmdata.Policy) *types.Policy {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
rules := make([]*types.PolicyRule, 0, len(p.Rules))
|
||||
for _, r := range p.Rules {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
var portRanges []sharedtypes.RulePortRange
|
||||
if r.PortRanges != nil {
|
||||
portRanges = make([]sharedtypes.RulePortRange, len(r.PortRanges))
|
||||
for i, pr := range r.PortRanges {
|
||||
portRanges[i] = sharedtypes.RulePortRange{Start: pr.Start, End: pr.End}
|
||||
}
|
||||
}
|
||||
rules = append(rules, &types.PolicyRule{
|
||||
ID: r.ID,
|
||||
PolicyID: r.PolicyID,
|
||||
Enabled: r.Enabled,
|
||||
Action: sharedtypes.PolicyTrafficActionType(r.Action),
|
||||
Protocol: sharedtypes.PolicyRuleProtocolType(r.Protocol),
|
||||
Bidirectional: r.Bidirectional,
|
||||
Sources: r.Sources,
|
||||
Destinations: r.Destinations,
|
||||
SourceResource: types.Resource{ID: r.SourceResource.ID, Type: sharedtypes.ResourceType(r.SourceResource.Type)},
|
||||
DestinationResource: types.Resource{ID: r.DestinationResource.ID, Type: sharedtypes.ResourceType(r.DestinationResource.Type)},
|
||||
Ports: r.Ports,
|
||||
PortRanges: portRanges,
|
||||
AuthorizedGroups: r.AuthorizedGroups,
|
||||
AuthorizedUser: r.AuthorizedUser,
|
||||
})
|
||||
}
|
||||
return &types.Policy{
|
||||
ID: p.ID,
|
||||
PublicID: p.PublicID,
|
||||
Enabled: p.Enabled,
|
||||
SourcePostureChecks: p.SourcePostureChecks,
|
||||
Rules: rules,
|
||||
}
|
||||
}
|
||||
|
||||
func accountRoute(r *nmdata.Route) *nbroute.Route {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &nbroute.Route{
|
||||
ID: nbroute.ID(r.ID),
|
||||
AccountID: r.AccountID,
|
||||
PublicID: r.PublicID,
|
||||
Network: r.Network,
|
||||
Domains: r.Domains,
|
||||
KeepRoute: r.KeepRoute,
|
||||
NetID: nbroute.NetID(r.NetID),
|
||||
Description: r.Description,
|
||||
Peer: r.Peer,
|
||||
PeerID: r.PeerID,
|
||||
PeerGroups: r.PeerGroups,
|
||||
NetworkType: nbroute.NetworkType(r.NetworkType),
|
||||
Masquerade: r.Masquerade,
|
||||
Metric: r.Metric,
|
||||
Enabled: r.Enabled,
|
||||
Groups: r.Groups,
|
||||
AccessControlGroups: r.AccessControlGroups,
|
||||
SkipAutoApply: r.SkipAutoApply,
|
||||
}
|
||||
}
|
||||
|
||||
func accountNSG(n *nmdata.NameServerGroup) *nbdns.NameServerGroup {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
nameServers := make([]nbdns.NameServer, 0, len(n.NameServers))
|
||||
for _, ns := range n.NameServers {
|
||||
nameServers = append(nameServers, nbdns.NameServer{
|
||||
IP: ns.IP,
|
||||
NSType: nbdns.NameServerType(ns.NSType),
|
||||
Port: ns.Port,
|
||||
})
|
||||
}
|
||||
return &nbdns.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,
|
||||
}
|
||||
}
|
||||
|
||||
func accountNetworkResource(r *nmdata.NetworkResource) *resourceTypes.NetworkResource {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &resourceTypes.NetworkResource{
|
||||
ID: r.ID,
|
||||
NetworkID: r.NetworkID,
|
||||
AccountID: r.AccountID,
|
||||
PublicID: r.PublicID,
|
||||
Name: r.Name,
|
||||
Description: r.Description,
|
||||
Type: resourceTypes.NetworkResourceType(r.Type),
|
||||
Address: r.Address,
|
||||
Domain: r.Domain,
|
||||
Prefix: r.Prefix,
|
||||
Enabled: r.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func accountPostureChecks(id string, pc *nmdata.PostureChecks, publicID string) *posture.Checks {
|
||||
if pc == nil {
|
||||
return nil
|
||||
}
|
||||
out := &posture.Checks{ID: id, PublicID: publicID}
|
||||
def := pc.Checks
|
||||
if def.NBVersionCheck != nil {
|
||||
out.Checks.NBVersionCheck = &posture.NBVersionCheck{MinVersion: def.NBVersionCheck.MinVersion}
|
||||
}
|
||||
if def.OSVersionCheck != nil {
|
||||
oc := &posture.OSVersionCheck{}
|
||||
if def.OSVersionCheck.Android != nil {
|
||||
oc.Android = &posture.MinVersionCheck{MinVersion: def.OSVersionCheck.Android.MinVersion}
|
||||
}
|
||||
if def.OSVersionCheck.Darwin != nil {
|
||||
oc.Darwin = &posture.MinVersionCheck{MinVersion: def.OSVersionCheck.Darwin.MinVersion}
|
||||
}
|
||||
if def.OSVersionCheck.Ios != nil {
|
||||
oc.Ios = &posture.MinVersionCheck{MinVersion: def.OSVersionCheck.Ios.MinVersion}
|
||||
}
|
||||
if def.OSVersionCheck.Linux != nil {
|
||||
oc.Linux = &posture.MinKernelVersionCheck{MinKernelVersion: def.OSVersionCheck.Linux.MinKernelVersion}
|
||||
}
|
||||
if def.OSVersionCheck.Windows != nil {
|
||||
oc.Windows = &posture.MinKernelVersionCheck{MinKernelVersion: def.OSVersionCheck.Windows.MinKernelVersion}
|
||||
}
|
||||
out.Checks.OSVersionCheck = oc
|
||||
}
|
||||
if def.GeoLocationCheck != nil {
|
||||
gc := &posture.GeoLocationCheck{Action: def.GeoLocationCheck.Action}
|
||||
for _, loc := range def.GeoLocationCheck.Locations {
|
||||
gc.Locations = append(gc.Locations, posture.Location{CountryCode: loc.CountryCode, CityName: loc.CityName})
|
||||
}
|
||||
out.Checks.GeoLocationCheck = gc
|
||||
}
|
||||
if def.PeerNetworkRangeCheck != nil {
|
||||
out.Checks.PeerNetworkRangeCheck = &posture.PeerNetworkRangeCheck{
|
||||
Action: def.PeerNetworkRangeCheck.Action,
|
||||
Ranges: def.PeerNetworkRangeCheck.Ranges,
|
||||
}
|
||||
}
|
||||
if def.ProcessCheck != nil {
|
||||
procs := make([]posture.Process, 0, len(def.ProcessCheck.Processes))
|
||||
for _, p := range def.ProcessCheck.Processes {
|
||||
procs = append(procs, posture.Process{LinuxPath: p.LinuxPath, MacPath: p.MacPath, WindowsPath: p.WindowsPath})
|
||||
}
|
||||
out.Checks.ProcessCheck = &posture.ProcessCheck{Processes: procs}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func accountServices(services []*nmdata.Service) []*service.Service {
|
||||
if len(services) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]*service.Service, 0, len(services))
|
||||
for _, svc := range services {
|
||||
if svc == nil {
|
||||
continue
|
||||
}
|
||||
targets := make([]*service.Target, 0, len(svc.Targets))
|
||||
for _, t := range svc.Targets {
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
target := &service.Target{
|
||||
Enabled: t.Enabled,
|
||||
Port: t.Port,
|
||||
Protocol: t.Protocol,
|
||||
TargetId: t.TargetID,
|
||||
TargetType: service.TargetType(t.TargetType),
|
||||
}
|
||||
if t.Path != "" {
|
||||
path := t.Path
|
||||
target.Path = &path
|
||||
}
|
||||
targets = append(targets, target)
|
||||
}
|
||||
out = append(out, &service.Service{
|
||||
ID: svc.ID,
|
||||
Enabled: svc.Enabled,
|
||||
Private: svc.Private,
|
||||
Mode: svc.Mode,
|
||||
ProxyCluster: svc.ProxyCluster,
|
||||
AccessGroups: svc.AccessGroups,
|
||||
Targets: targets,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// accountZones inverts buildAppliedZoneCandidates. Records come back with the
|
||||
// record type the builder mapped them from; a candidate only ever carries the
|
||||
// three types it converts.
|
||||
func accountZones(candidates []networkmap.AppliedZoneCandidate) []*zones.Zone {
|
||||
if len(candidates) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]*zones.Zone, 0, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
zoneRecords := make([]*records.Record, 0, len(candidate.Zone.Records))
|
||||
for _, r := range candidate.Zone.Records {
|
||||
recordType, ok := zoneRecordType(r.Type)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
zoneRecords = append(zoneRecords, &records.Record{
|
||||
Name: strings.TrimSuffix(r.Name, "."),
|
||||
Type: recordType,
|
||||
Content: r.RData,
|
||||
TTL: r.TTL,
|
||||
})
|
||||
}
|
||||
out = append(out, &zones.Zone{
|
||||
ID: candidate.Zone.Domain,
|
||||
Domain: strings.TrimSuffix(candidate.Zone.Domain, "."),
|
||||
Enabled: true,
|
||||
EnableSearchDomain: !candidate.Zone.SearchDomainDisabled,
|
||||
DistributionGroups: candidate.DistributionGroups,
|
||||
Records: zoneRecords,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func zoneRecordType(recordType int) (records.RecordType, bool) {
|
||||
switch uint16(recordType) {
|
||||
case dns.TypeA:
|
||||
return records.RecordTypeA, true
|
||||
case dns.TypeAAAA:
|
||||
return records.RecordTypeAAAA, true
|
||||
case dns.TypeCNAME:
|
||||
return records.RecordTypeCNAME, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func accountRouters(routers map[string]map[string]*nmdata.NetworkRouter) map[string]map[string]*routerTypes.NetworkRouter {
|
||||
if len(routers) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]map[string]*routerTypes.NetworkRouter, len(routers))
|
||||
for networkID, inner := range routers {
|
||||
converted := make(map[string]*routerTypes.NetworkRouter, len(inner))
|
||||
for peerID, router := range inner {
|
||||
if router == nil {
|
||||
continue
|
||||
}
|
||||
converted[peerID] = &routerTypes.NetworkRouter{
|
||||
NetworkID: networkID,
|
||||
PublicID: router.PublicID,
|
||||
Peer: peerID,
|
||||
PeerGroups: router.PeerGroups,
|
||||
Masquerade: router.Masquerade,
|
||||
Metric: router.Metric,
|
||||
Enabled: router.Enabled,
|
||||
}
|
||||
}
|
||||
out[networkID] = converted
|
||||
}
|
||||
return out
|
||||
}
|
||||
332
management/internals/controllers/network_map/nmaptest/runner.go
Normal file
332
management/internals/controllers/network_map/nmaptest/runner.go
Normal file
@@ -0,0 +1,332 @@
|
||||
// Package nmaptest measures network map generation on the dedicated store
|
||||
// path against committed expectations. A case stands in for the store load
|
||||
// with a NetworkMapData fixture — the value NetworkMapDBStoreImpl returns for
|
||||
// one account — then runs the production per-peer pipeline the controller
|
||||
// uses, PeersCustomZone → GetPeerNetworkMapComponents → proto conversion, in
|
||||
// both wire shapes: the full map (grpc.ToSyncResponse) and the component
|
||||
// envelope expanded client-side (grpc.ToComponentSyncResponse →
|
||||
// networkmap.EnvelopeToNetworkMap). A third mode inverts the fixture back into
|
||||
// the Account it stands for and runs main's frozen path over it (legacynmap),
|
||||
// so every case is pinned to what main shipped as well.
|
||||
//
|
||||
// The expectation files are the point of the framework. They state what the
|
||||
// output should be, so a failing case means the code disagrees with the
|
||||
// expectation and the answer is normally to fix the code; an expectation
|
||||
// changes only through a deliberate reviewed edit. Nothing in this package
|
||||
// writes to testdata — there is no flag that records current behaviour into an
|
||||
// expectation, because that is how a defect becomes the baseline. Cases whose
|
||||
// expectation encodes correct behaviour the code does not yet deliver stay red
|
||||
// on purpose.
|
||||
//
|
||||
// A case lives in testdata/cases/<name>/ as case.json (manifest: description,
|
||||
// peers, optional accountID, dnsDomain, modes), nmdata.json (the fixture the
|
||||
// mocked store returns, using Go field names; zero values may be omitted and
|
||||
// applyFixtureDefaults fills the boilerplate) and golden/<peerID>.json.
|
||||
//
|
||||
// There is ONE expectation per peer, shared by every mode, because all three
|
||||
// must arrive at the same client-facing map. Full and envelope are not even
|
||||
// different computations — CalculateNetworkMapFromComponents is
|
||||
// components.Calculate and both assemble the proto with the same encode
|
||||
// helpers — so the only variable between them is what the envelope round-trip
|
||||
// did in transit, and a difference there is a round-trip fidelity defect.
|
||||
// Legacy is a different computation, main's, reached from a rebuilt account;
|
||||
// a difference there is this tree having drifted from what main shipped.
|
||||
// Results are canonicalized before comparison, since repeated proto fields
|
||||
// come from map iteration.
|
||||
package nmaptest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/maps"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
||||
mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
// Mode selects the wire shape a case is verified through. Both end in a
|
||||
// *proto.NetworkMap, the one comparison surface shared by every path.
|
||||
type Mode string
|
||||
|
||||
const (
|
||||
// ModeFull is the legacy wire shape: the server runs Calculate and sends
|
||||
// the expanded map (grpc.ToSyncResponse).
|
||||
ModeFull Mode = "full"
|
||||
// ModeEnvelope is the component wire shape: the server encodes components
|
||||
// into a NetworkMapEnvelope (grpc.ToComponentSyncResponse) and the map is
|
||||
// expanded the way the client engine does (networkmap.EnvelopeToNetworkMap).
|
||||
ModeEnvelope Mode = "envelope"
|
||||
// ModeLegacy is main's frozen path: the fixture is inverted back into the
|
||||
// Account it stands for and run through legacynmap, the copy of what main
|
||||
// shipped. It is the outside measurement — the other two modes share this
|
||||
// tree's computation, so only this one can catch the whole tree drifting.
|
||||
ModeLegacy Mode = "legacy"
|
||||
|
||||
defaultAccountID = "account"
|
||||
defaultDNSDomain = "netbird.test"
|
||||
)
|
||||
|
||||
var defaultModes = []Mode{ModeFull, ModeEnvelope, ModeLegacy}
|
||||
|
||||
// Case is one nmap-generation scenario: store data for a single account, the
|
||||
// peers whose network maps are computed, and the directory holding one expected
|
||||
// *proto.NetworkMap per peer — shared by every mode.
|
||||
type Case struct {
|
||||
Name string
|
||||
AccountID string
|
||||
DNSDomain string
|
||||
Peers []string
|
||||
Modes []Mode
|
||||
Data *networkmap.NetworkMapData
|
||||
GoldenDir string
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
Description string
|
||||
AccountID string
|
||||
DNSDomain string
|
||||
Peers []string
|
||||
Modes []Mode
|
||||
}
|
||||
|
||||
// RunGoldenDir discovers and runs every fixture case under dir. A case is a
|
||||
// directory containing case.json (manifest), nmdata.json (store fixture) and
|
||||
// golden/<peerID>.json (expected proto.NetworkMap, protojson).
|
||||
func RunGoldenDir(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
require.NoError(t, err, "read cases dir")
|
||||
|
||||
ran := 0
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
caseDir := filepath.Join(dir, entry.Name())
|
||||
c, err := loadCase(caseDir)
|
||||
require.NoError(t, err, "load case %s", entry.Name())
|
||||
ran++
|
||||
t.Run(entry.Name(), func(t *testing.T) {
|
||||
RunCase(t, c)
|
||||
})
|
||||
}
|
||||
require.NotZero(t, ran, "no cases found under %s", dir)
|
||||
}
|
||||
|
||||
func loadCase(caseDir string) (Case, error) {
|
||||
raw, err := os.ReadFile(filepath.Join(caseDir, "case.json"))
|
||||
if err != nil {
|
||||
return Case{}, fmt.Errorf("read manifest: %w", err)
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
var m manifest
|
||||
if err := dec.Decode(&m); err != nil {
|
||||
return Case{}, fmt.Errorf("decode manifest: %w", err)
|
||||
}
|
||||
|
||||
data, err := LoadNetworkMapData(filepath.Join(caseDir, "nmdata.json"))
|
||||
if err != nil {
|
||||
return Case{}, err
|
||||
}
|
||||
|
||||
return Case{
|
||||
Name: filepath.Base(caseDir),
|
||||
AccountID: m.AccountID,
|
||||
DNSDomain: m.DNSDomain,
|
||||
Peers: m.Peers,
|
||||
Modes: m.Modes,
|
||||
Data: data,
|
||||
GoldenDir: filepath.Join(caseDir, "golden"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RunCase computes each target peer's network map through every enabled mode
|
||||
// and compares the canonicalized result against the peer's expectation file.
|
||||
// It mirrors the controller's store path: fill fixture defaults, precompute
|
||||
// posture validation once, then run the per-peer pipeline.
|
||||
func RunCase(t *testing.T, c Case) {
|
||||
t.Helper()
|
||||
|
||||
require.NotNil(t, c.Data, "case %s: Data is required", c.Name)
|
||||
require.NotEmpty(t, c.Peers, "case %s: Peers is required", c.Name)
|
||||
require.NotEmpty(t, c.GoldenDir, "case %s: GoldenDir is required", c.Name)
|
||||
if c.AccountID == "" {
|
||||
c.AccountID = defaultAccountID
|
||||
}
|
||||
if c.DNSDomain == "" {
|
||||
c.DNSDomain = defaultDNSDomain
|
||||
}
|
||||
if len(c.Modes) == 0 {
|
||||
c.Modes = defaultModes
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
nmData := c.Data
|
||||
applyFixtureDefaults(nmData)
|
||||
nmData.PrecomputePostureValidation()
|
||||
|
||||
dnsDomain := c.DNSDomain
|
||||
if nmData.AccountSettings.DNSDomain != "" {
|
||||
dnsDomain = nmData.AccountSettings.DNSDomain
|
||||
}
|
||||
|
||||
zone := networkmap.PeersCustomZone(ctx, c.AccountID, dnsDomain, nmData.Peers, controller.IPv6AllowedPeersFromData(nmData))
|
||||
dnsFwdPort := controller.ComputeForwarderPortFromData(nmData.Peers, network_map.DnsForwarderPortMinVersion)
|
||||
|
||||
for _, mode := range c.Modes {
|
||||
if mode == ModeEnvelope {
|
||||
requireEnvelopeSafeKeys(t, nmData, c.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Built before any mode runs: the first per-peer computation injects the
|
||||
// synthesised proxy ACLs into the twin's policies, and the legacy side
|
||||
// synthesises its own, so inverting a twin that already carries them would
|
||||
// hand the legacy path each ACL twice.
|
||||
var legacy legacyInput
|
||||
if slices.Contains(c.Modes, ModeLegacy) {
|
||||
legacy = legacyInputFromData(c.AccountID, nmData)
|
||||
}
|
||||
|
||||
for _, peerID := range c.Peers {
|
||||
peer := nmData.Peers[peerID]
|
||||
require.NotNil(t, peer, "case %s: target peer %q not in fixture", c.Name, peerID)
|
||||
|
||||
for _, mode := range c.Modes {
|
||||
t.Run(peerID+"/"+string(mode), func(t *testing.T) {
|
||||
got := computeMode(t, ctx, mode, nmData, peerID, zone, dnsDomain, dnsFwdPort, legacy)
|
||||
canonicalize(got)
|
||||
compareGolden(t, filepath.Join(c.GoldenDir, peerID+".json"), got, mode)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// computeMode produces the peer's proto.NetworkMap the way the controller does
|
||||
// for that wire shape.
|
||||
func computeMode(t *testing.T, ctx context.Context, mode Mode, nmData *networkmap.NetworkMapData,
|
||||
peerID string, zone nmdata.CustomZone, dnsDomain string, dnsFwdPort int64, legacy legacyInput) *proto.NetworkMap {
|
||||
t.Helper()
|
||||
|
||||
peer := nmData.Peers[peerID]
|
||||
require.NotNil(t, peer, "target peer %q not in fixture", peerID)
|
||||
|
||||
switch mode {
|
||||
case ModeLegacy:
|
||||
return computeLegacy(t, ctx, legacy, peerID, zone, dnsDomain, dnsFwdPort)
|
||||
case ModeFull:
|
||||
nmap := controller.NetworkMapFromData(ctx, nmData, peerID, zone)
|
||||
return mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, nmap, dnsDomain, nil,
|
||||
&cache.DNSConfigCache{}, nmData.AccountSettings, nil, nil, dnsFwdPort).NetworkMap
|
||||
case ModeEnvelope:
|
||||
components := nmData.GetPeerNetworkMapComponents(peerID, zone)
|
||||
peerGroups := maps.Keys(nmData.GetPeerGroups(peerID))
|
||||
resp := mgmtgrpc.ToComponentSyncResponse(ctx, nil, nil, nil, peer, nil, nil, components, nil,
|
||||
dnsDomain, nil, nmData.AccountSettings, nil, peerGroups, dnsFwdPort)
|
||||
res, err := networkmap.EnvelopeToNetworkMap(ctx, resp.NetworkMapEnvelope, peer.Key, dnsDomain)
|
||||
require.NoError(t, err, "expand envelope")
|
||||
return res.NetworkMap
|
||||
default:
|
||||
t.Fatalf("unknown mode %q", mode)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// requireEnvelopeSafeKeys fails fast on peer keys the envelope decoder would
|
||||
// silently drop: it re-keys peers by base64 of the raw 32-byte WG public key.
|
||||
func requireEnvelopeSafeKeys(t *testing.T, nmData *networkmap.NetworkMapData, caseName string) {
|
||||
t.Helper()
|
||||
for id, p := range nmData.Peers {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
raw, err := base64.StdEncoding.DecodeString(p.Key)
|
||||
if err != nil || len(raw) != 32 {
|
||||
t.Fatalf("case %s: peer %q Key must be base64 of 32 bytes for mode %q (the envelope decoder drops it otherwise); use a real WireGuard public key or restrict the case to mode %q",
|
||||
caseName, id, ModeEnvelope, ModeFull)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// compareGolden measures got against the committed expectation file. One
|
||||
// expectation serves every mode, because the modes run the same computation and
|
||||
// must therefore agree. The expectation is the authority: a mismatch means the
|
||||
// code does not produce what this case says it should, so it is reported as a
|
||||
// failure and not quietly absorbed.
|
||||
//
|
||||
// The full and legacy modes are compared verbatim, identifiers included, so the
|
||||
// expectation pins real ids and stays readable. The envelope mode has
|
||||
// identifiers erased on both sides first, because it currently rewrites them —
|
||||
// a tracked defect that TestIDSpaceMatches asserts against on its own, so it
|
||||
// does not have to drown out every other case here.
|
||||
// Nothing here writes to testdata. Expectation files are authored by hand and
|
||||
// only ever change through a reviewed edit, so there is no mode in which a run
|
||||
// can create or replace one. When a file is missing the computed map is printed
|
||||
// for the author to read and, if it is genuinely correct, save deliberately.
|
||||
func compareGolden(t *testing.T, path string, got *proto.NetworkMap, mode Mode) {
|
||||
t.Helper()
|
||||
|
||||
if mode == ModeEnvelope {
|
||||
normalizeIDSpace(got)
|
||||
canonicalize(got)
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
rendered, mErr := renderNetworkMap(got)
|
||||
require.NoError(t, mErr)
|
||||
t.Fatalf("no expectation file %s: %v\nThis case has nothing to measure against — write the "+
|
||||
"proto.NetworkMap this peer should receive. Mode %s currently produces:\n%s\nRead it before "+
|
||||
"saving any of it: if the code is wrong, so is this.", path, err, mode, rendered)
|
||||
}
|
||||
want := &proto.NetworkMap{}
|
||||
require.NoError(t, protojson.Unmarshal(raw, want), "parse expectation %s", path)
|
||||
canonicalize(want)
|
||||
if mode == ModeEnvelope {
|
||||
normalizeIDSpace(want)
|
||||
canonicalize(want)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
|
||||
t.Errorf("mode %s does not produce what %s expects (-want +got):\n%s\n"+
|
||||
"Every mode has to deliver the same client-facing map for the same account state. "+
|
||||
"The expectation file is the committed statement of correct output — fix the code, or change the "+
|
||||
"expectation deliberately if the intended behaviour really moved.", mode, path, diff)
|
||||
}
|
||||
}
|
||||
|
||||
// renderNetworkMap renders stable protojson: protojson output whitespace is
|
||||
// deliberately unstable, so it is reformatted through json.Indent.
|
||||
func renderNetworkMap(nm *proto.NetworkMap) ([]byte, error) {
|
||||
raw, err := protojson.Marshal(nm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := json.Indent(&buf, raw, "", " "); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf.WriteByte('\n')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
7
management/internals/controllers/network_map/nmaptest/testdata/cases/basic-policy/case.json
vendored
Normal file
7
management/internals/controllers/network_map/nmaptest/testdata/cases/basic-policy/case.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Two groups joined by one allow-all policy; peer-c has SSH enabled so the legacy-SSH path fills SshAuth from AllowedUserIDs.",
|
||||
"peers": [
|
||||
"peer-a",
|
||||
"peer-c"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"Serial": "5",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "4deEImv8zGvsyBmmfC2G0eQkbyMzyGuz/YK7pcYETwM=",
|
||||
"allowedIps": [
|
||||
"100.64.0.3/32"
|
||||
],
|
||||
"sshConfig": {
|
||||
"sshPubKey": "c3NoLXBlZXItYw=="
|
||||
},
|
||||
"fqdn": "peer-c.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-c.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.3",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLWFsbA=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.3",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLWFsbA=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
102
management/internals/controllers/network_map/nmaptest/testdata/cases/basic-policy/golden/peer-c.json
vendored
Normal file
102
management/internals/controllers/network_map/nmaptest/testdata/cases/basic-policy/golden/peer-c.json
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"Serial": "5",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.3/10",
|
||||
"sshConfig": {
|
||||
"sshEnabled": true
|
||||
},
|
||||
"fqdn": "peer-c.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "AvldyrZ12Pf90jzf3AXmhPwg3UcI+jtJHfbpBlupvko=",
|
||||
"allowedIps": [
|
||||
"100.64.0.2/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-b.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
},
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-b.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.2"
|
||||
},
|
||||
{
|
||||
"Name": "peer-c.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLWFsbA=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLWFsbA=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.2",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLWFsbA=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.2",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLWFsbA=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub",
|
||||
"AuthorizedUsers": [
|
||||
"u9dHvAXZJKiXITuwP9jD/A=="
|
||||
],
|
||||
"machineUsers": {
|
||||
"*": {
|
||||
"indexes": [
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
management/internals/controllers/network_map/nmaptest/testdata/cases/basic-policy/nmdata.json
vendored
Normal file
31
management/internals/controllers/network_map/nmaptest/testdata/cases/basic-policy/nmdata.json
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"Network": {"Serial": 5},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-b": {"IP": "100.64.0.2", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-c": {"IP": "100.64.0.3", "SSHEnabled": true, "SSHKey": "ssh-peer-c", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a", "peer-b"]},
|
||||
"grp-ops": {"Peers": ["peer-c"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-all",
|
||||
"PublicID": "pol-all-pub",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "all",
|
||||
"Bidirectional": true,
|
||||
"Sources": ["grp-dev"],
|
||||
"Destinations": ["grp-ops"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"AllowedUserIDs": {"user-ops": {}}
|
||||
}
|
||||
7
management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/case.json
vendored
Normal file
7
management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/case.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Nameserver group and applied custom zone distributed to grp-dev; peer-a (with an extra DNS label) receives them, peer-c outside the group receives neither.",
|
||||
"peers": [
|
||||
"peer-a",
|
||||
"peer-c"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"Serial": "8",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "AvldyrZ12Pf90jzf3AXmhPwg3UcI+jtJHfbpBlupvko=",
|
||||
"allowedIps": [
|
||||
"100.64.0.2/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-b.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"NameServerGroups": [
|
||||
{
|
||||
"NameServers": [
|
||||
{
|
||||
"IP": "8.8.8.8",
|
||||
"Port": "53"
|
||||
}
|
||||
],
|
||||
"Primary": true
|
||||
}
|
||||
],
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "corp.internal.",
|
||||
"NonAuthoritative": true,
|
||||
"Records": [
|
||||
{
|
||||
"Name": "db.corp.internal.",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "10.10.0.5"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-b.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.2"
|
||||
},
|
||||
{
|
||||
"Name": "www.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.2",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLW1lc2g="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.2",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLW1lc2g="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "8",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.3/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-c.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-c.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
52
management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/nmdata.json
vendored
Normal file
52
management/internals/controllers/network_map/nmaptest/testdata/cases/dns-config/nmdata.json
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"Network": {"Serial": 8},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "ExtraDNSLabels": ["www"], "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-b": {"IP": "100.64.0.2", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-c": {"IP": "100.64.0.3", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a", "peer-b"]},
|
||||
"grp-ops": {"Peers": ["peer-c"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-mesh",
|
||||
"PublicID": "pol-mesh-pub",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "all",
|
||||
"Bidirectional": true,
|
||||
"Sources": ["grp-dev"],
|
||||
"Destinations": ["grp-dev"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"NameServerGroups": [
|
||||
{
|
||||
"ID": "nsg-1",
|
||||
"Name": "dns-primary",
|
||||
"NameServers": [{"IP": "8.8.8.8", "Port": 53}],
|
||||
"Groups": ["grp-dev"],
|
||||
"Primary": true,
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"AppliedZoneCandidates": [
|
||||
{
|
||||
"DistributionGroups": ["grp-dev"],
|
||||
"Zone": {
|
||||
"Domain": "corp.internal.",
|
||||
"NonAuthoritative": true,
|
||||
"Records": [
|
||||
{"Name": "db.corp.internal.", "Type": 1, "Class": "IN", "TTL": 300, "RData": "10.10.0.5"}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "Domain network resource: the route carries the domain list and the 192.0.2.0/32 placeholder network with NetworkType 3 (dynamic), and peer-r's route firewall rules must be marked dynamic and repeat the domain. Two ports on the policy must produce one rule per port. A domain resource contributes no DNS custom zone of its own — resolution happens through the routing peer's forwarder.",
|
||||
"peers": ["peer-a", "peer-r"]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"Serial": "22",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"allowedIps": [
|
||||
"100.64.0.9/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-domain:peer-r",
|
||||
"Network": "192.0.2.0/32",
|
||||
"NetworkType": "3",
|
||||
"Peer": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "app-domain",
|
||||
"Domains": [
|
||||
"app.internal"
|
||||
],
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"Serial": "22",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.9/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-domain:peer-r",
|
||||
"Network": "192.0.2.0/32",
|
||||
"NetworkType": "3",
|
||||
"Peer": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "app-domain",
|
||||
"Domains": [
|
||||
"app.internal"
|
||||
],
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.9"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRules": [
|
||||
{
|
||||
"sourceRanges": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"destination": "192.0.2.0/32",
|
||||
"protocol": "TCP",
|
||||
"portInfo": {
|
||||
"port": 443
|
||||
},
|
||||
"isDynamic": true,
|
||||
"domains": [
|
||||
"app.internal"
|
||||
],
|
||||
"PolicyID": "cG9sLWFwcA==",
|
||||
"RouteID": "res-domain:peer-r"
|
||||
},
|
||||
{
|
||||
"sourceRanges": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"destination": "192.0.2.0/32",
|
||||
"protocol": "TCP",
|
||||
"portInfo": {
|
||||
"port": 80
|
||||
},
|
||||
"isDynamic": true,
|
||||
"domains": [
|
||||
"app.internal"
|
||||
],
|
||||
"PolicyID": "cG9sLWFwcA==",
|
||||
"RouteID": "res-domain:peer-r"
|
||||
}
|
||||
],
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Network": {"Serial": 22},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r": {"IP": "100.64.0.9", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-app",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["80", "443"],
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "res-domain", "Type": "domain"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ResourcePolicies": {"res-domain": [{"ID": "pol-app"}]},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-domain",
|
||||
"NetworkID": "net-1",
|
||||
"Name": "app-domain",
|
||||
"Type": "domain",
|
||||
"Domain": "app.internal",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-1": {
|
||||
"peer-r": {"PublicID": "router-direct", "Masquerade": true, "Metric": 9999, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "Host network resource (single /32) behind one directly-assigned router. peer-a is in the resource policy's source group and must receive one route to 10.10.0.7/32 via peer-r with KeepRoute set and NetID taken from the resource name; peer-r as the router must receive the same route plus a route firewall rule whose SourceRanges are the policy's source peers. A client never gets route firewall rules.",
|
||||
"peers": ["peer-a", "peer-r"]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"Serial": "20",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"allowedIps": [
|
||||
"100.64.0.9/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-host:peer-r",
|
||||
"Network": "10.10.0.7/32",
|
||||
"NetworkType": "1",
|
||||
"Peer": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "web-host",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"Serial": "20",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.9/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-host:peer-r",
|
||||
"Network": "10.10.0.7/32",
|
||||
"NetworkType": "1",
|
||||
"Peer": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "web-host",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.9"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRules": [
|
||||
{
|
||||
"sourceRanges": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"destination": "10.10.0.7/32",
|
||||
"protocol": "TCP",
|
||||
"portInfo": {
|
||||
"port": 443
|
||||
},
|
||||
"PolicyID": "cG9sLXdlYg==",
|
||||
"RouteID": "res-host:peer-r"
|
||||
}
|
||||
],
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Network": {"Serial": 20},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r": {"IP": "100.64.0.9", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-web",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["443"],
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "res-host", "Type": "host"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ResourcePolicies": {"res-host": [{"ID": "pol-web"}]},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-host",
|
||||
"NetworkID": "net-1",
|
||||
"Name": "web-host",
|
||||
"Type": "host",
|
||||
"Prefix": "10.10.0.7/32",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-1": {
|
||||
"peer-r": {"PublicID": "router-direct", "Masquerade": true, "Metric": 9999, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "A disabled resource with a valid policy and router must leave no trace: no routes and no route firewall rules for either the client or the router. Disabling a resource is the switch that revokes access without deleting the policy.",
|
||||
"peers": ["peer-a", "peer-r"]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "25",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "25",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.9/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.9"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Network": {"Serial": 25},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r": {"IP": "100.64.0.9", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-off-resource",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["443"],
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "res-disabled", "Type": "subnet"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ResourcePolicies": {"res-disabled": [{"ID": "pol-off-resource"}]},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-disabled",
|
||||
"NetworkID": "net-1",
|
||||
"Name": "disabled-subnet",
|
||||
"Type": "subnet",
|
||||
"Prefix": "10.50.0.0/24"
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-1": {
|
||||
"peer-r": {"PublicID": "router-direct", "Masquerade": true, "Metric": 9999, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "An enabled resource with a healthy router but no policy granting access to it must produce nothing anywhere: no route for the client and none for the router either, since access to a resource is only ever created by a policy. The router also gets no route firewall rules despite being a routing peer for the network.",
|
||||
"peers": ["peer-a", "peer-r"]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "24",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "24",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.9/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.9"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"Network": {"Serial": 24},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r": {"IP": "100.64.0.9", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]}
|
||||
},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-orphan",
|
||||
"NetworkID": "net-1",
|
||||
"Name": "orphan-subnet",
|
||||
"Type": "subnet",
|
||||
"Prefix": "10.40.0.0/24",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-1": {
|
||||
"peer-r": {"PublicID": "router-direct", "Masquerade": true, "Metric": 9999, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "A DISABLED policy granting access to a network resource must grant nothing: no route to 10.90.0.0/24 for peer-a and none for the router either, exactly as if the policy were absent. THE FULL EXPECTATION CURRENTLY FAILS, and should: resource-policy selection never checks policy.Enabled (networkmapcompute.go and networkmap_components.go both test only nil/len(Rules)/Rules[0]), so the legacy path still hands out the route — access survives disabling the policy. The envelope path happens to be correct because the encoder drops disabled policies from the wire. Fix the compute path, do not weaken this expectation.",
|
||||
"peers": ["peer-a", "peer-r"]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "39",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "39",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.9/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.9"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Network": {"Serial": 39},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r": {"IP": "100.64.0.9", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-revoked",
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["5432"],
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "res-db", "Type": "subnet"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ResourcePolicies": {"res-db": [{"ID": "pol-revoked"}]},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-db",
|
||||
"NetworkID": "net-1",
|
||||
"Name": "db-subnet",
|
||||
"Type": "subnet",
|
||||
"Prefix": "10.90.0.0/24",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-1": {
|
||||
"peer-r": {"PublicID": "router-direct", "Masquerade": true, "Metric": 9999, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "The routing peer for the resource is not in ValidatedPeers — an unapproved peer, which the integrated validator withholds. peer-a must therefore receive no route through it and must not see it as a peer at all: traffic may not be routed through a peer the account has not approved. THE ENVELOPE EXPECTATION CURRENTLY FAILS, and should: component selection puts every routing peer into RouterPeers without checking validation, the encoder indexes them into the envelope's peer table, and the client decoder puts every peer it finds back into its peer map, so the unapproved router reappears client-side with a working route. The full path drops it correctly. Fix the component/encoder path, do not weaken this expectation.",
|
||||
"peers": ["peer-a"]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "40",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"Network": {"Serial": 40},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r": {"IP": "100.64.0.9", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"ValidatedPeers": {"peer-a": {}},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-db",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["5432"],
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "res-db", "Type": "subnet"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ResourcePolicies": {"res-db": [{"ID": "pol-db"}]},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-db",
|
||||
"NetworkID": "net-1",
|
||||
"Name": "db-subnet",
|
||||
"Type": "subnet",
|
||||
"Prefix": "10.100.0.0/24",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-1": {
|
||||
"peer-r": {"PublicID": "router-direct", "Masquerade": true, "Metric": 9999, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "Routing peer group: one router record assigned to a peer group, which the store expands into one entry per member peer sharing the router's settings. peer-a must receive one route per routing peer — same NetID and destination, different route ID and peer — which is what gives the client an HA pair to choose between. Each router must receive only its own route, never its sibling's, plus its own route firewall rule.",
|
||||
"peers": ["peer-a", "peer-r1", "peer-r2"]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"Serial": "23",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "Iu6Lj1HqDXfgDh5RppHCRI6RO3lMlZBCndRNrdS7QUI=",
|
||||
"allowedIps": [
|
||||
"100.64.0.11/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r1.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
},
|
||||
{
|
||||
"wgPubKey": "YC6sbWtvpB2d6W/XqsNIV9crsrXBoVDdwmhCoszjCps=",
|
||||
"allowedIps": [
|
||||
"100.64.0.12/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r2.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-ha:peer-r1",
|
||||
"Network": "10.30.0.0/24",
|
||||
"NetworkType": "1",
|
||||
"Peer": "Iu6Lj1HqDXfgDh5RppHCRI6RO3lMlZBCndRNrdS7QUI=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "ha-subnet",
|
||||
"keepRoute": true
|
||||
},
|
||||
{
|
||||
"ID": "res-ha:peer-r2",
|
||||
"Network": "10.30.0.0/24",
|
||||
"NetworkType": "1",
|
||||
"Peer": "YC6sbWtvpB2d6W/XqsNIV9crsrXBoVDdwmhCoszjCps=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "ha-subnet",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"Serial": "23",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.11/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r1.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-ha:peer-r1",
|
||||
"Network": "10.30.0.0/24",
|
||||
"NetworkType": "1",
|
||||
"Peer": "Iu6Lj1HqDXfgDh5RppHCRI6RO3lMlZBCndRNrdS7QUI=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "ha-subnet",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r1.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.11"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRules": [
|
||||
{
|
||||
"sourceRanges": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"destination": "10.30.0.0/24",
|
||||
"protocol": "TCP",
|
||||
"portInfo": {
|
||||
"port": 5432
|
||||
},
|
||||
"PolicyID": "cG9sLWhh",
|
||||
"RouteID": "res-ha:peer-r1"
|
||||
}
|
||||
],
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"Serial": "23",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.12/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r2.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-ha:peer-r2",
|
||||
"Network": "10.30.0.0/24",
|
||||
"NetworkType": "1",
|
||||
"Peer": "YC6sbWtvpB2d6W/XqsNIV9crsrXBoVDdwmhCoszjCps=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "ha-subnet",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r2.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.12"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRules": [
|
||||
{
|
||||
"sourceRanges": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"destination": "10.30.0.0/24",
|
||||
"protocol": "TCP",
|
||||
"portInfo": {
|
||||
"port": 5432
|
||||
},
|
||||
"PolicyID": "cG9sLWhh",
|
||||
"RouteID": "res-ha:peer-r2"
|
||||
}
|
||||
],
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"Network": {"Serial": 23},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r1": {"IP": "100.64.0.11", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r2": {"IP": "100.64.0.12", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]},
|
||||
"grp-routers": {"Peers": ["peer-r1", "peer-r2"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-ha",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["5432"],
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "res-ha", "Type": "subnet"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ResourcePolicies": {"res-ha": [{"ID": "pol-ha"}]},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-ha",
|
||||
"NetworkID": "net-ha",
|
||||
"Name": "ha-subnet",
|
||||
"Type": "subnet",
|
||||
"Prefix": "10.30.0.0/24",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-ha": {
|
||||
"peer-r1": {"PublicID": "router-ha", "PeerGroups": ["grp-routers"], "Masquerade": true, "Metric": 9999, "Enabled": true},
|
||||
"peer-r2": {"PublicID": "router-ha", "PeerGroups": ["grp-routers"], "Masquerade": true, "Metric": 9999, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "Subnet network resource behind one directly-assigned router, with masquerade off and a non-default metric so both reach the wire verbatim, and an all-protocol policy from a two-peer source group. peer-r's route firewall rule must list both source peers; peer-b confirms a second client in the same group gets its own identical route.",
|
||||
"peers": ["peer-a", "peer-b", "peer-r"]
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"Serial": "21",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"allowedIps": [
|
||||
"100.64.0.9/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-subnet:peer-r",
|
||||
"Network": "10.20.0.0/24",
|
||||
"NetworkType": "1",
|
||||
"Peer": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"Metric": "500",
|
||||
"NetID": "office-subnet",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"Serial": "21",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.2/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-b.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"allowedIps": [
|
||||
"100.64.0.9/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-subnet:peer-r",
|
||||
"Network": "10.20.0.0/24",
|
||||
"NetworkType": "1",
|
||||
"Peer": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"Metric": "500",
|
||||
"NetID": "office-subnet",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-b.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"Serial": "21",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.9/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "AvldyrZ12Pf90jzf3AXmhPwg3UcI+jtJHfbpBlupvko=",
|
||||
"allowedIps": [
|
||||
"100.64.0.2/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-b.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
},
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-subnet:peer-r",
|
||||
"Network": "10.20.0.0/24",
|
||||
"NetworkType": "1",
|
||||
"Peer": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"Metric": "500",
|
||||
"NetID": "office-subnet",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.9"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRules": [
|
||||
{
|
||||
"sourceRanges": [
|
||||
"100.64.0.1/32",
|
||||
"100.64.0.2/32"
|
||||
],
|
||||
"destination": "10.20.0.0/24",
|
||||
"protocol": "ALL",
|
||||
"portInfo": {},
|
||||
"PolicyID": "cG9sLXN1Ym5ldA==",
|
||||
"RouteID": "res-subnet:peer-r"
|
||||
}
|
||||
],
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user