Compare commits

..

1 Commits

Author SHA1 Message Date
Zoltán Papp
6c93581e56 Dummy file 2026-08-12 14:55:40 +02:00
359 changed files with 3863 additions and 23724 deletions

View File

@@ -3,7 +3,7 @@
[branches]
main = "main"
perennials = []
perennial-regex = "^release-"
perennial-regex = ""
[create]
new-branch-type = "feature"

View File

@@ -2,7 +2,7 @@ name: Check License Dependencies
on:
push:
branches: [main, "release-*"]
branches: [main]
paths:
- "go.mod"
- "go.sum"

View File

@@ -10,7 +10,6 @@ on:
push:
branches:
- main
- "release-*"
paths:
- "client/ui/frontend/**"
- "client/ui/i18n/**"

View File

@@ -4,7 +4,6 @@ on:
push:
branches:
- main
- "release-*"
pull_request:
concurrency:

View File

@@ -4,7 +4,6 @@ on:
push:
branches:
- main
- "release-*"
pull_request:
concurrency:

View File

@@ -4,7 +4,6 @@ on:
push:
branches:
- main
- "release-*"
pull_request:
concurrency:
@@ -730,11 +729,6 @@ 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
@@ -743,7 +737,9 @@ jobs:
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
CI=true \
mage integrationtest:all -gotestflags="-coverprofile=coverage.txt"
go test -tags=integration -coverprofile=coverage.txt \
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
-timeout 20m ./management/server/http/...
- name: Upload coverage reports to Codecov
if: matrix.arch == 'amd64'

View File

@@ -4,7 +4,6 @@ on:
push:
branches:
- main
- "release-*"
pull_request:
env:

View File

@@ -4,7 +4,6 @@ on:
push:
branches:
- main
- "release-*"
pull_request:
paths:
- "release_files/install.sh"

View File

@@ -4,7 +4,6 @@ on:
push:
branches:
- main
- "release-*"
pull_request:
concurrency:

View File

@@ -6,7 +6,6 @@ on:
- "v*"
branches:
- main
- "release-*"
pull_request:
env:
@@ -255,23 +254,15 @@ jobs:
id: tag_and_push_images
if: |
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release-')))
(github.event_name == 'push' && github.ref == 'refs/heads/main')
run: |
set -euo pipefail
# $GITHUB_REF / $GITHUB_EVENT_NAME are read from the runner
# environment rather than substituted into this script with the
# workflow expression syntax: branch names may legally contain
# $(…), and interpolating github.ref would execute it.
resolve_tags() {
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "pr-${{ github.event.pull_request.number }}"
elif [[ "$GITHUB_REF" == "refs/heads/main" ]]; then
echo "main sha-$(git rev-parse --short HEAD)"
else
# Release branches get an immutable sha-* tag only — the floating
# "main" tag must never move from a release branch.
echo "sha-$(git rev-parse --short HEAD)"
echo "main sha-$(git rev-parse --short HEAD)"
fi
}

View File

@@ -9,9 +9,21 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
# The receiving bump-netbird workflows expect the short tag form
# (e.g. v0.30.0), not refs/tags/v0.30.0 — github.ref_name, not github.ref.
# Receiving workflows (cloud sync-tag, mobile bump-netbird) expect the short
# tag form (e.g. v0.30.0), not refs/tags/v0.30.0 — github.ref_name, not github.ref.
jobs:
trigger_sync_tag:
runs-on: ubuntu-latest
steps:
- name: Trigger release tag sync
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
with:
workflow: sync-tag.yml
ref: main
repo: ${{ secrets.UPSTREAM_REPO }}
token: ${{ secrets.NC_GITHUB_TOKEN }}
inputs: '{ "tag": "${{ github.ref_name }}" }'
trigger_android_bump:
runs-on: ubuntu-latest
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')

View File

@@ -4,7 +4,6 @@ on:
push:
branches:
- main
- "release-*"
pull_request:
paths:
- "infrastructure_files/**"

View File

@@ -4,7 +4,6 @@ on:
push:
branches:
- main
- "release-*"
pull_request:
concurrency:

View File

@@ -468,13 +468,6 @@ checksum:
- glob: ./infrastructure_files/migrate-to-enterprise.sh
release:
# The signing pipeline (netbirdio/sign-pipelines, dispatched by
# trigger_signer) marks the release latest once the Windows and macOS
# artifacts are signed. Without this override goreleaser marks it latest
# at publish time, while those artifacts are still unsigned.
make_latest: false
# Mark x.y.z-rc.* and other prerelease tags as prereleases on GitHub.
prerelease: auto
extra_files:
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
- glob: ./release_files/install.sh

View File

@@ -144,11 +144,3 @@ uploads:
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
username: dev@wiretrustee.com
method: PUT
release:
# Uploads into the release created by the main .goreleaser.yaml run.
# make_latest stays false everywhere: the signing pipeline
# (netbirdio/sign-pipelines) marks the release latest after the Windows
# and macOS artifacts are signed.
make_latest: false
prerelease: auto

View File

@@ -43,11 +43,3 @@ checksum:
name_template: "{{ .ProjectName }}_darwin_checksums.txt"
changelog:
disable: true
release:
# Uploads into the release created by the main .goreleaser.yaml run.
# make_latest stays false everywhere: the signing pipeline
# (netbirdio/sign-pipelines) marks the release latest after the Windows
# and macOS artifacts are signed.
make_latest: false
prerelease: auto

View File

@@ -134,11 +134,3 @@ uploads:
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
username: dev@wiretrustee.com
method: PUT
release:
# Uploads into the release created by the main .goreleaser.yaml run.
# make_latest stays false everywhere: the signing pipeline
# (netbirdio/sign-pipelines) marks the release latest after the Windows
# and macOS artifacts are signed.
make_latest: false
prerelease: auto

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"os/user"
"runtime"
"strings"
log "github.com/sirupsen/logrus"
@@ -120,7 +121,7 @@ func doDaemonLogin(ctx context.Context, cmd *cobra.Command, providedSetupKey str
loginRequest := proto.LoginRequest{
SetupKey: providedSetupKey,
ManagementUrl: managementURL,
IsUnixDesktopClient: util.HasGraphicalSession(),
IsUnixDesktopClient: isUnixRunningDesktop(),
Hostname: hostName,
DnsLabels: dnsLabelsReq,
ProfileName: &handle,
@@ -188,8 +189,7 @@ func doExtendSession(ctx context.Context, cmd *cobra.Command) error {
client := proto.NewDaemonServiceClient(conn)
// the CLI runs in the user's session, the daemon does not: tell it what we can see
req := &proto.RequestExtendAuthSessionRequest{HasGraphicalSession: util.HasGraphicalSession()}
req := &proto.RequestExtendAuthSessionRequest{}
// Pre-fill the IdP login hint from the active profile so the user
// doesn't have to retype their email. Best-effort: we still proceed
// without a hint if the lookup fails.
@@ -408,7 +408,7 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *pro
hint = profileState.Email
}
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, util.HasGraphicalSession(), false, hint)
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isUnixRunningDesktop(), false, hint)
if err != nil {
return nil, err
}
@@ -458,6 +458,14 @@ func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBro
}
}
// isUnixRunningDesktop checks if a Linux OS is running desktop environment
func isUnixRunningDesktop() bool {
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
return false
}
return os.Getenv("DESKTOP_SESSION") != "" || os.Getenv("XDG_CURRENT_DESKTOP") != ""
}
func setEnvAndFlags(cmd *cobra.Command) error {
SetFlagsFromEnvVars(rootCmd)

View File

@@ -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, nil)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersmanager), config)
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 {

View File

@@ -21,8 +21,8 @@ import (
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
nbnet "github.com/netbirdio/netbird/client/net"
"github.com/netbirdio/netbird/client/proto"
nbnet "github.com/netbirdio/netbird/client/net"
"github.com/netbirdio/netbird/client/server"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/shared/management/domain"
@@ -626,7 +626,7 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte
NatExternalIPs: natExternalIPs,
CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0,
CustomDNSAddress: customDNSAddressConverted,
IsUnixDesktopClient: util.HasGraphicalSession(),
IsUnixDesktopClient: isUnixRunningDesktop(),
Hostname: hostName,
ExtraIFaceBlacklist: extraIFaceBlackList,
DnsLabels: dnsLabels,

View File

@@ -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, nil)
networkMapController := controller.NewController(context.Background(), testStore, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(testStore, peersManager), cfg)
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)

View File

@@ -1,240 +0,0 @@
//go:build privileged
package iptables
import (
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
fw "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/iface/wgaddr"
)
func iptRefcountIfaceV4() *iFaceMock {
return &iFaceMock{
NameFunc: func() string { return "wt-refcount" },
AddressFunc: func() wgaddr.Address {
return wgaddr.Address{
IP: netip.MustParseAddr("10.20.0.1"),
Network: netip.MustParsePrefix("10.20.0.0/24"),
}
},
}
}
func iptRefcountIfaceDual() *iFaceMock {
return &iFaceMock{
NameFunc: func() string { return "wt-refcount" },
AddressFunc: func() wgaddr.Address {
return wgaddr.Address{
IP: netip.MustParseAddr("10.20.0.1"),
Network: netip.MustParsePrefix("10.20.0.0/24"),
IPv6: netip.MustParseAddr("fd00::1"),
IPv6Net: netip.MustParsePrefix("fd00::/64"),
}
},
}
}
func newIptRefcountManager(t *testing.T, dual bool) *Manager {
t.Helper()
var ifMock *iFaceMock
if dual {
ifMock = iptRefcountIfaceDual()
} else {
ifMock = iptRefcountIfaceV4()
}
m, err := Create(ifMock, iface.DefaultMTU)
require.NoError(t, err, "create manager")
require.NoError(t, m.Init(nil), "init manager")
t.Cleanup(func() {
require.NoError(t, m.Close(nil), "close manager")
})
return m
}
func iptDnatV4(port uint16) fw.ForwardRule {
return fw.ForwardRule{
Protocol: fw.ProtocolTCP,
DestinationPort: fw.Port{Values: []uint16{port}},
TranslatedAddress: netip.MustParseAddr("10.20.0.2"),
TranslatedPort: fw.Port{Values: []uint16{80}},
}
}
func iptDnatV6(port uint16) fw.ForwardRule {
return fw.ForwardRule{
Protocol: fw.ProtocolTCP,
DestinationPort: fw.Port{Values: []uint16{port}},
TranslatedAddress: netip.MustParseAddr("fd00::2"),
TranslatedPort: fw.Port{Values: []uint16{80}},
}
}
// TestIptablesRouting_RepeatedEnableSingleReference verifies that EnableRouting
// (called on every network-map update) holds at most one reference per family
// and a single DisableRouting drops both back to zero.
func TestIptablesRouting_RepeatedEnableSingleReference(t *testing.T) {
m := newIptRefcountManager(t, true)
state := m.router.ipFwdState
require.NoError(t, m.EnableRouting(), "first enable")
require.NoError(t, m.EnableRouting(), "second enable")
require.NoError(t, m.EnableRouting(), "third enable")
v4, v6 := state.Counts()
assert.Equal(t, 1, v4, "repeated enable holds a single v4 reference")
assert.Equal(t, 1, v6, "repeated enable holds a single v6 reference")
require.NoError(t, m.DisableRouting(), "disable")
v4, v6 = state.Counts()
assert.Equal(t, 0, v4, "single disable releases the v4 reference")
assert.Equal(t, 0, v6, "single disable releases the v6 reference")
}
// TestIptablesRouting_DisableKeepsDNATReference verifies that an unpaired
// DisableRouting does not release references held by active DNAT rules.
func TestIptablesRouting_DisableKeepsDNATReference(t *testing.T) {
m := newIptRefcountManager(t, true)
state := m.router.ipFwdState
r1, err := m.AddDNATRule(iptDnatV6(9095))
require.NoError(t, err, "add v6 dnat")
require.NoError(t, m.DisableRouting(), "unpaired disable")
_, v6 := state.Counts()
assert.Equal(t, 1, v6, "DNAT-held reference survives unpaired DisableRouting")
require.NoError(t, m.DeleteDNATRule(r1), "delete v6 dnat")
_, v6 = state.Counts()
assert.Equal(t, 0, v6, "delete releases the DNAT reference")
}
// TestIptablesDNAT_RefcountBalancedV4 covers a Balanced Add/Delete pair on v4.
func TestIptablesDNAT_RefcountBalancedV4(t *testing.T) {
m := newIptRefcountManager(t, false)
state := m.router.ipFwdState
r1, err := m.AddDNATRule(iptDnatV4(7081))
require.NoError(t, err, "add v4 dnat 1")
v4, v6 := state.Counts()
assert.Equal(t, 1, v4, "v4 refcount after first add")
assert.Equal(t, 0, v6, "v6 refcount unchanged")
r2, err := m.AddDNATRule(iptDnatV4(7082))
require.NoError(t, err, "add v4 dnat 2")
v4, v6 = state.Counts()
assert.Equal(t, 2, v4, "v4 refcount after second add")
assert.Equal(t, 0, v6, "v6 refcount unchanged")
require.NoError(t, m.DeleteDNATRule(r1))
v4, v6 = state.Counts()
assert.Equal(t, 1, v4, "v4 refcount after first delete")
assert.Equal(t, 0, v6, "v6 refcount unchanged")
require.NoError(t, m.DeleteDNATRule(r2))
v4, v6 = state.Counts()
assert.Equal(t, 0, v4, "v4 refcount after second delete")
assert.Equal(t, 0, v6, "v6 refcount unchanged")
}
// TestIptablesDNAT_RefcountBalancedV6 checks the v6 path increments v6 only and
// decrements back to zero.
func TestIptablesDNAT_RefcountBalancedV6(t *testing.T) {
m := newIptRefcountManager(t, true)
require.NotNil(t, m.router6, "v6 router")
require.Same(t, m.router.ipFwdState, m.router6.ipFwdState, "shared state")
state := m.router.ipFwdState
r1, err := m.AddDNATRule(iptDnatV6(9081))
require.NoError(t, err, "add v6 dnat 1")
v4, v6 := state.Counts()
assert.Equal(t, 0, v4)
assert.Equal(t, 1, v6, "v6 refcount after first add")
r2, err := m.AddDNATRule(iptDnatV6(9082))
require.NoError(t, err, "add v6 dnat 2")
v4, v6 = state.Counts()
assert.Equal(t, 0, v4, "v4 refcount unchanged")
assert.Equal(t, 2, v6, "v6 refcount after second add")
require.NoError(t, m.DeleteDNATRule(r1))
v4, v6 = state.Counts()
assert.Equal(t, 0, v4, "v4 refcount unchanged")
assert.Equal(t, 1, v6, "v6 refcount after first delete")
require.NoError(t, m.DeleteDNATRule(r2))
v4, v6 = state.Counts()
assert.Equal(t, 0, v4)
assert.Equal(t, 0, v6, "v6 refcount after second delete")
}
// TestIptablesDNAT_DuplicateAddNoLeak verifies the duplicate-rule path returns
// without bumping the refcount.
func TestIptablesDNAT_DuplicateAddNoLeak(t *testing.T) {
m := newIptRefcountManager(t, true)
state := m.router.ipFwdState
rule := iptDnatV4(7083)
r1, err := m.AddDNATRule(rule)
require.NoError(t, err)
v4, _ := state.Counts()
assert.Equal(t, 1, v4)
_, err = m.AddDNATRule(rule)
require.NoError(t, err, "duplicate add")
v4, _ = state.Counts()
assert.Equal(t, 1, v4, "duplicate add must not increment")
require.NoError(t, m.DeleteDNATRule(r1))
v4, _ = state.Counts()
assert.Equal(t, 0, v4, "single delete must drop to zero")
}
// TestIptablesDNAT_DeleteMissingNoUnderflow verifies Delete on an unknown rule
// neither errors nor releases the refcount.
func TestIptablesDNAT_DeleteMissingNoUnderflow(t *testing.T) {
m := newIptRefcountManager(t, true)
state := m.router.ipFwdState
phantom := iptDnatV4(7099)
require.NoError(t, m.DeleteDNATRule(&phantom), "delete missing v4")
v4, v6 := state.Counts()
assert.Equal(t, 0, v4)
assert.Equal(t, 0, v6)
phantom6 := iptDnatV6(9099)
require.NoError(t, m.DeleteDNATRule(&phantom6), "delete missing v6")
v4, v6 = state.Counts()
assert.Equal(t, 0, v4)
assert.Equal(t, 0, v6)
r1, err := m.AddDNATRule(iptDnatV4(7100))
require.NoError(t, err)
v4, _ = state.Counts()
assert.Equal(t, 1, v4, "real add still increments after phantom delete")
require.NoError(t, m.DeleteDNATRule(r1))
}
// TestIptablesDNAT_DoubleDeleteNoUnderflow verifies a second Delete on the same
// rule is a no-op.
func TestIptablesDNAT_DoubleDeleteNoUnderflow(t *testing.T) {
m := newIptRefcountManager(t, true)
state := m.router.ipFwdState
r1, err := m.AddDNATRule(iptDnatV6(9083))
require.NoError(t, err)
_, v6 := state.Counts()
assert.Equal(t, 1, v6)
require.NoError(t, m.DeleteDNATRule(r1), "first delete")
_, v6 = state.Counts()
assert.Equal(t, 0, v6)
require.NoError(t, m.DeleteDNATRule(r1), "second delete must be no-op")
_, v6 = state.Counts()
assert.Equal(t, 0, v6, "double delete must not underflow")
}

View File

@@ -89,7 +89,7 @@ func (m *Manager) createIPv6Components(wgIface iFaceMapper, mtu uint16) error {
}
// Share the same IP forwarding state with the v4 router, since
// Forwarding refcounter is per-family but shared between v4 and v6 routers.
// EnableIPForwarding controls both v4 and v6 sysctls.
m.router6.ipFwdState = m.router.ipFwdState
m.aclMgr6, err = newAclManager(ip6Client, wgIface)
@@ -402,12 +402,17 @@ func (m *Manager) SetLogLevel(log.Level) {
}
func (m *Manager) EnableRouting() error {
// v6 only when the overlay actually has v6.
return m.router.ipFwdState.RequestRouting(m.router6 != nil)
if err := m.router.ipFwdState.RequestForwarding(); err != nil {
return fmt.Errorf("enable IP forwarding: %w", err)
}
return nil
}
func (m *Manager) DisableRouting() error {
return m.router.ipFwdState.ReleaseRouting()
if err := m.router.ipFwdState.ReleaseForwarding(); err != nil {
return fmt.Errorf("disable IP forwarding: %w", err)
}
return nil
}
// AddDNATRule adds a DNAT rule

View File

@@ -102,7 +102,7 @@ func newRouter(iptablesClient *iptables.IPTables, wgIface iFaceMapper, mtu uint1
wgIface: wgIface,
mtu: mtu,
v6: iptablesClient.Proto() == iptables.ProtocolIPv6,
ipFwdState: ipfwdstate.NewIPForwardingState(wgIface.Name()),
ipFwdState: ipfwdstate.NewIPForwardingState(),
}
r.ipsetCounter = refcounter.New(
@@ -770,6 +770,10 @@ func (r *router) updateState() {
}
func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
if err := r.ipFwdState.RequestForwarding(); err != nil {
return nil, err
}
ruleKey := rule.ID()
if _, exists := r.rules[ruleKey+dnatSuffix]; exists {
return rule, nil
@@ -836,34 +840,18 @@ func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
for key, ruleInfo := range rules {
if err := r.iptablesClient.Append(ruleInfo.table, ruleInfo.chain, ruleInfo.rule...); err != nil {
r.cleanupFailedDNATAdd(rules)
if rollbackErr := r.rollbackRules(rules); rollbackErr != nil {
log.Errorf("rollback failed: %v", rollbackErr)
}
return nil, fmt.Errorf("add rule %s: %w", key, err)
}
r.rules[key] = ruleInfo.rule
}
if err := r.ipFwdState.RequestForwarding(r.v6); err != nil {
r.cleanupFailedDNATAdd(rules)
return nil, fmt.Errorf("enable forwarding: %w", err)
}
r.updateState()
return rule, nil
}
// cleanupFailedDNATAdd removes the bookkeeping written by a partially applied
// AddDNATRule before rolling back the kernel rules, so no entries remain that
// never got a forwarding refcount. rollbackRules re-adds entries it failed to
// remove from the kernel.
func (r *router) cleanupFailedDNATAdd(rules map[string]ruleInfo) {
for key := range rules {
delete(r.rules, key)
}
if err := r.rollbackRules(rules); err != nil {
log.Errorf("rollback failed: %v", err)
}
}
func (r *router) rollbackRules(rules map[string]ruleInfo) error {
var merr *multierror.Error
for key, ruleInfo := range rules {
@@ -880,47 +868,32 @@ func (r *router) rollbackRules(rules map[string]ruleInfo) error {
}
func (r *router) DeleteDNATRule(rule firewall.Rule) error {
ruleKey := rule.ID()
_, hadDNAT := r.rules[ruleKey+dnatSuffix]
_, hadSNAT := r.rules[ruleKey+snatSuffix]
_, hadFWD := r.rules[ruleKey+fwdSuffix]
if !hadDNAT && !hadSNAT && !hadFWD {
return nil
if err := r.ipFwdState.ReleaseForwarding(); err != nil {
log.Errorf("%v", err)
}
ruleKey := rule.ID()
var merr *multierror.Error
if dnatRule, exists := r.rules[ruleKey+dnatSuffix]; exists {
if err := r.iptablesClient.Delete(tableNat, chainRTRDR, dnatRule...); err != nil {
merr = multierror.Append(merr, fmt.Errorf("delete DNAT rule: %w", err))
} else {
delete(r.rules, ruleKey+dnatSuffix)
}
delete(r.rules, ruleKey+dnatSuffix)
}
if snatRule, exists := r.rules[ruleKey+snatSuffix]; exists {
if err := r.iptablesClient.Delete(tableNat, chainRTNAT, snatRule...); err != nil {
merr = multierror.Append(merr, fmt.Errorf("delete SNAT rule: %w", err))
} else {
delete(r.rules, ruleKey+snatSuffix)
}
delete(r.rules, ruleKey+snatSuffix)
}
if fwdRule, exists := r.rules[ruleKey+fwdSuffix]; exists {
if err := r.iptablesClient.Delete(tableFilter, chainRTFWDOUT, fwdRule...); err != nil {
merr = multierror.Append(merr, fmt.Errorf("delete forward rule: %w", err))
} else {
delete(r.rules, ruleKey+fwdSuffix)
}
}
// Release the refcount only once all rules are gone from the kernel. On
// partial failure the failed entries stay in r.rules so a retry can remove
// them and release then.
if merr == nil {
if err := r.ipFwdState.ReleaseForwarding(r.v6); err != nil {
log.Errorf("%v", err)
}
delete(r.rules, ruleKey+fwdSuffix)
}
r.updateState()

View File

@@ -1,249 +0,0 @@
//go:build privileged
package nftables
import (
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
fw "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/iface/wgaddr"
)
func nftRefcountIfaceV4() *iFaceMock {
return &iFaceMock{
NameFunc: func() string { return "wt-refcount" },
AddressFunc: func() wgaddr.Address {
return wgaddr.Address{
IP: netip.MustParseAddr("100.96.0.1"),
Network: netip.MustParsePrefix("100.96.0.0/16"),
}
},
}
}
func nftRefcountIfaceDual() *iFaceMock {
return &iFaceMock{
NameFunc: func() string { return "wt-refcount" },
AddressFunc: func() wgaddr.Address {
return wgaddr.Address{
IP: netip.MustParseAddr("100.96.0.1"),
Network: netip.MustParsePrefix("100.96.0.0/16"),
IPv6: netip.MustParseAddr("fd00::1"),
IPv6Net: netip.MustParsePrefix("fd00::/64"),
}
},
}
}
func newNftRefcountManager(t *testing.T, dual bool) *Manager {
t.Helper()
if check() != NFTABLES {
t.Skip("nftables not supported on this system")
}
var ifMock *iFaceMock
if dual {
ifMock = nftRefcountIfaceDual()
} else {
ifMock = nftRefcountIfaceV4()
}
m, err := Create(ifMock, iface.DefaultMTU)
require.NoError(t, err, "create manager")
require.NoError(t, m.Init(nil), "init manager")
t.Cleanup(func() {
require.NoError(t, m.Close(nil), "close manager")
})
return m
}
func dnatV4(port uint16) fw.ForwardRule {
return fw.ForwardRule{
Protocol: fw.ProtocolTCP,
DestinationPort: fw.Port{Values: []uint16{port}},
TranslatedAddress: netip.MustParseAddr("100.96.0.2"),
TranslatedPort: fw.Port{Values: []uint16{80}},
}
}
func dnatV6(port uint16) fw.ForwardRule {
return fw.ForwardRule{
Protocol: fw.ProtocolTCP,
DestinationPort: fw.Port{Values: []uint16{port}},
TranslatedAddress: netip.MustParseAddr("fd00::2"),
TranslatedPort: fw.Port{Values: []uint16{80}},
}
}
// TestNftablesDNAT_RefcountBalancedV4 verifies that Add/Delete pairs leave the
// v4 refcount at zero.
func TestNftablesDNAT_RefcountBalancedV4(t *testing.T) {
m := newNftRefcountManager(t, false)
state := m.router.ipFwdState
r1, err := m.AddDNATRule(dnatV4(8081))
require.NoError(t, err, "add v4 dnat 1")
v4, v6 := state.Counts()
assert.Equal(t, 1, v4, "v4 refcount after first add")
assert.Equal(t, 0, v6, "v6 refcount unchanged")
r2, err := m.AddDNATRule(dnatV4(8082))
require.NoError(t, err, "add v4 dnat 2")
v4, v6 = state.Counts()
assert.Equal(t, 2, v4, "v4 refcount after second add")
assert.Equal(t, 0, v6, "v6 refcount unchanged")
require.NoError(t, m.DeleteDNATRule(r1), "delete v4 dnat 1")
v4, v6 = state.Counts()
assert.Equal(t, 1, v4, "v4 refcount after first delete")
assert.Equal(t, 0, v6, "v6 refcount unchanged")
require.NoError(t, m.DeleteDNATRule(r2), "delete v4 dnat 2")
v4, v6 = state.Counts()
assert.Equal(t, 0, v4, "v4 refcount after second delete")
assert.Equal(t, 0, v6, "v6 refcount unchanged")
}
// TestNftablesDNAT_RefcountBalancedV6 verifies the v6 path increments v6 only
// and decrements back to zero on Delete.
func TestNftablesDNAT_RefcountBalancedV6(t *testing.T) {
m := newNftRefcountManager(t, true)
require.NotNil(t, m.router6, "v6 router")
require.Same(t, m.router.ipFwdState, m.router6.ipFwdState, "shared state")
state := m.router.ipFwdState
r1, err := m.AddDNATRule(dnatV6(9091))
require.NoError(t, err, "add v6 dnat 1")
v4, v6 := state.Counts()
assert.Equal(t, 0, v4, "v4 refcount unchanged")
assert.Equal(t, 1, v6, "v6 refcount after first add")
r2, err := m.AddDNATRule(dnatV6(9092))
require.NoError(t, err, "add v6 dnat 2")
v4, v6 = state.Counts()
assert.Equal(t, 0, v4)
assert.Equal(t, 2, v6, "v6 refcount after second add")
require.NoError(t, m.DeleteDNATRule(r1), "delete v6 dnat 1")
v4, v6 = state.Counts()
assert.Equal(t, 0, v4, "v4 refcount unchanged")
assert.Equal(t, 1, v6, "v6 refcount after first delete")
require.NoError(t, m.DeleteDNATRule(r2), "delete v6 dnat 2")
v4, v6 = state.Counts()
assert.Equal(t, 0, v4)
assert.Equal(t, 0, v6, "v6 refcount after second delete")
}
// TestNftablesDNAT_DuplicateAddNoLeak verifies that a duplicate Add (same
// ForwardRule) does not double-increment the refcount.
func TestNftablesDNAT_DuplicateAddNoLeak(t *testing.T) {
m := newNftRefcountManager(t, true)
state := m.router.ipFwdState
rule := dnatV4(8083)
r1, err := m.AddDNATRule(rule)
require.NoError(t, err, "add v4 dnat")
v4, _ := state.Counts()
assert.Equal(t, 1, v4)
// duplicate add: same rule ID, must be a no-op for the refcount.
_, err = m.AddDNATRule(rule)
require.NoError(t, err, "duplicate add")
v4, _ = state.Counts()
assert.Equal(t, 1, v4, "duplicate add must not increment")
require.NoError(t, m.DeleteDNATRule(r1), "delete v4 dnat")
v4, _ = state.Counts()
assert.Equal(t, 0, v4, "single delete must drop to zero")
}
// TestNftablesDNAT_DeleteMissingNoUnderflow verifies deleting a rule that was
// never added does not underflow the refcount.
func TestNftablesDNAT_DeleteMissingNoUnderflow(t *testing.T) {
m := newNftRefcountManager(t, true)
state := m.router.ipFwdState
// Construct a Rule reference for something never added. The router stores
// rules by ID(), and DeleteDNATRule looks them up in r.rules; a missing
// entry must be a no-op rather than calling Release.
phantom := dnatV4(8099)
require.NoError(t, m.DeleteDNATRule(&phantom), "delete missing v4 dnat")
v4, v6 := state.Counts()
assert.Equal(t, 0, v4, "v4 refcount unaffected by missing delete")
assert.Equal(t, 0, v6, "v6 refcount unaffected")
phantom6 := dnatV6(9099)
require.NoError(t, m.DeleteDNATRule(&phantom6), "delete missing v6 dnat")
v4, v6 = state.Counts()
assert.Equal(t, 0, v4)
assert.Equal(t, 0, v6, "v6 refcount unaffected by missing delete")
// And after a phantom delete, a real add still results in count=1.
r1, err := m.AddDNATRule(dnatV4(8100))
require.NoError(t, err, "add v4 dnat after phantom delete")
v4, _ = state.Counts()
assert.Equal(t, 1, v4, "real add still increments after phantom delete")
require.NoError(t, m.DeleteDNATRule(r1))
}
// TestNftablesRouting_RepeatedEnableSingleReference verifies that EnableRouting
// (called on every network-map update) holds at most one reference per family
// and a single DisableRouting drops both back to zero.
func TestNftablesRouting_RepeatedEnableSingleReference(t *testing.T) {
m := newNftRefcountManager(t, true)
state := m.router.ipFwdState
require.NoError(t, m.EnableRouting(), "first enable")
require.NoError(t, m.EnableRouting(), "second enable")
require.NoError(t, m.EnableRouting(), "third enable")
v4, v6 := state.Counts()
assert.Equal(t, 1, v4, "repeated enable holds a single v4 reference")
assert.Equal(t, 1, v6, "repeated enable holds a single v6 reference")
require.NoError(t, m.DisableRouting(), "disable")
v4, v6 = state.Counts()
assert.Equal(t, 0, v4, "single disable releases the v4 reference")
assert.Equal(t, 0, v6, "single disable releases the v6 reference")
}
// TestNftablesRouting_DisableKeepsDNATReference verifies that an unpaired
// DisableRouting does not release references held by active DNAT rules.
func TestNftablesRouting_DisableKeepsDNATReference(t *testing.T) {
m := newNftRefcountManager(t, true)
state := m.router.ipFwdState
r1, err := m.AddDNATRule(dnatV6(9095))
require.NoError(t, err, "add v6 dnat")
require.NoError(t, m.DisableRouting(), "unpaired disable")
_, v6 := state.Counts()
assert.Equal(t, 1, v6, "DNAT-held reference survives unpaired DisableRouting")
require.NoError(t, m.DeleteDNATRule(r1), "delete v6 dnat")
_, v6 = state.Counts()
assert.Equal(t, 0, v6, "delete releases the DNAT reference")
}
// TestNftablesDNAT_DoubleDeleteNoUnderflow verifies that deleting the same rule
// twice does not underflow the refcount (the second delete is a no-op).
func TestNftablesDNAT_DoubleDeleteNoUnderflow(t *testing.T) {
m := newNftRefcountManager(t, true)
state := m.router.ipFwdState
r1, err := m.AddDNATRule(dnatV6(9093))
require.NoError(t, err)
_, v6 := state.Counts()
assert.Equal(t, 1, v6)
require.NoError(t, m.DeleteDNATRule(r1), "first delete")
_, v6 = state.Counts()
assert.Equal(t, 0, v6)
require.NoError(t, m.DeleteDNATRule(r1), "second delete must be no-op")
_, v6 = state.Counts()
assert.Equal(t, 0, v6, "double delete must not underflow")
}

View File

@@ -105,8 +105,8 @@ func (m *Manager) createIPv6Components(tableName string, wgIface iFaceMapper, mt
return fmt.Errorf("create v6 router: %w", err)
}
// Share the per-family forwarding refcounter with the v4 router so a v4
// rule and a v6 rule against the same state machine cooperate cleanly.
// Share the same IP forwarding state with the v4 router, since
// EnableIPForwarding controls both v4 and v6 sysctls.
m.router6.ipFwdState = m.router.ipFwdState
m.aclManager6, err = newAclManager(workTable6, wgIface, chainNameRoutingFw)
@@ -530,12 +530,17 @@ func (m *Manager) SetLogLevel(log.Level) {
}
func (m *Manager) EnableRouting() error {
// v6 only when the overlay actually has v6.
return m.router.ipFwdState.RequestRouting(m.router6 != nil)
if err := m.router.ipFwdState.RequestForwarding(); err != nil {
return fmt.Errorf("enable IP forwarding: %w", err)
}
return nil
}
func (m *Manager) DisableRouting() error {
return m.router.ipFwdState.ReleaseRouting()
if err := m.router.ipFwdState.ReleaseForwarding(); err != nil {
return fmt.Errorf("disable IP forwarding: %w", err)
}
return nil
}
// Flush rule/chain/set operations from the buffer

View File

@@ -93,7 +93,7 @@ func newRouter(workTable *nftables.Table, wgIface iFaceMapper, mtu uint16) (*rou
rules: make(map[string]*nftables.Rule),
af: familyForAddr(workTable.Family == nftables.TableFamilyIPv4),
wgIface: wgIface,
ipFwdState: ipfwdstate.NewIPForwardingState(wgIface.Name()),
ipFwdState: ipfwdstate.NewIPForwardingState(),
mtu: mtu,
}
@@ -1553,6 +1553,10 @@ func (r *router) refreshRulesMap() error {
}
func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
if err := r.ipFwdState.RequestForwarding(); err != nil {
return nil, err
}
ruleKey := rule.ID()
if _, exists := r.rules[ruleKey+dnatSuffix]; exists {
return rule, nil
@@ -1563,18 +1567,7 @@ func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
return nil, fmt.Errorf("convert protocol to number: %w", err)
}
// Request forwarding before queueing rules: addDnatRedirect/addDnatMasq
// buffer netlink messages on r.conn that the next caller's Flush would
// commit if we returned without flushing them ourselves.
v6 := r.af.tableFamily == nftables.TableFamilyIPv6
if err := r.ipFwdState.RequestForwarding(v6); err != nil {
return nil, fmt.Errorf("enable forwarding: %w", err)
}
if err := r.addDnatRedirect(rule, protoNum, ruleKey); err != nil {
if rerr := r.ipFwdState.ReleaseForwarding(v6); rerr != nil {
log.Warnf("rollback forwarding refcount: %v", rerr)
}
return nil, err
}
@@ -1586,11 +1579,6 @@ func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
// TODO: find chains with drop policies and add rules there
if err := r.conn.Flush(); err != nil {
if rerr := r.ipFwdState.ReleaseForwarding(v6); rerr != nil {
log.Warnf("rollback forwarding refcount: %v", rerr)
}
delete(r.rules, ruleKey+dnatSuffix)
delete(r.rules, ruleKey+snatSuffix)
return nil, fmt.Errorf("flush rules: %w", err)
}
@@ -1793,18 +1781,16 @@ func (r *router) addDnatMasq(rule firewall.ForwardRule, protoNum uint8, ruleKey
}
func (r *router) DeleteDNATRule(rule firewall.Rule) error {
if err := r.ipFwdState.ReleaseForwarding(); err != nil {
log.Errorf("%v", err)
}
ruleKey := rule.ID()
if err := r.refreshRulesMap(); err != nil {
return fmt.Errorf(refreshRulesMapError, err)
}
_, hadDNAT := r.rules[ruleKey+dnatSuffix]
_, hadSNAT := r.rules[ruleKey+snatSuffix]
if !hadDNAT && !hadSNAT {
return nil
}
var merr *multierror.Error
var needsFlush bool
@@ -1836,16 +1822,9 @@ func (r *router) DeleteDNATRule(rule firewall.Rule) error {
}
}
// Release the refcount only once the rules are gone from the kernel. On
// failure (including the refreshRulesMap error above) the rules and their
// map entries remain, keeping forwarding on until a retry removes them.
if merr == nil {
delete(r.rules, ruleKey+dnatSuffix)
delete(r.rules, ruleKey+snatSuffix)
if err := r.ipFwdState.ReleaseForwarding(r.af.tableFamily == nftables.TableFamilyIPv6); err != nil {
log.Errorf("%v", err)
}
}
return nberrors.FormatErrorOrNil(merr)

View File

@@ -22,6 +22,8 @@
!define UI_REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${UI_APP_EXE}"
!define UI_UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UI_APP_NAME}"
!define AUTOSTART_REG_KEY "Software\Microsoft\Windows\CurrentVersion\Run"
!define NETBIRD_DATA_DIR "$COMMONPROGRAMDATA\Netbird"
Unicode True
@@ -226,6 +228,13 @@ WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}"
WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}"
; Autostart is owned by the UI's per-user setting (HKCU\...\Run via Wails),
; not the installer. Drop the machine-wide entry older installers wrote so the
; toggle is the single source of truth. HKCU is left untouched -- it may hold
; the user's own toggle state, which must survive upgrades.
DetailPrint "Removing installer-managed autostart registry entry if present..."
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
EnVar::SetHKLM
EnVar::AddValueEx "path" "$INSTDIR"
@@ -290,6 +299,15 @@ ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
DetailPrint "Terminating Netbird UI process..."
ExecWait `taskkill /im ${UI_APP_EXE}.exe /f`
; Remove autostart registry entries
DetailPrint "Removing autostart registry entries if they exist..."
; Legacy machine-wide entry written by older installers.
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
; Per-user entry the UI toggle writes via Wails (value name is the lowercase
; app-name slug). Uninstall removes the app, so drop it too.
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "netbird"
; Handle data deletion based on checkbox
DetailPrint "Checking if user requested data deletion..."
${If} $DeleteDataEnabled == "1"

View File

@@ -844,10 +844,6 @@ func collectSysctls() string {
[]string{"net.ipv4.conf.all.src_valid_mark", "net.ipv4.conf.default.src_valid_mark"},
listInterfaceSysctls("ipv4", "src_valid_mark")...,
))
writeSysctlGroup(&builder, "accept_ra", append(
[]string{"net.ipv6.conf.all.accept_ra", "net.ipv6.conf.default.accept_ra"},
listInterfaceSysctls("ipv6", "accept_ra")...,
))
writeSysctlGroup(&builder, "conntrack", []string{
"net.netfilter.nf_conntrack_acct",
"net.netfilter.nf_conntrack_tcp_loose",

View File

@@ -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, nil)
networkMapController := controller.NewController(context.Background(), store, metrics, updateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersManager), config)
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

View File

@@ -2,183 +2,54 @@ package ipfwdstate
import (
"fmt"
"sync"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
)
// IPForwardingState tracks v4 and v6 IP-forwarding sysctl enables with
// independent refcounts so a v4-only routing setup doesn't flip v6 sysctls.
// IPForwardingState is a struct that keeps track of the IP forwarding state.
// todo: read initial state of the IP forwarding from the system and reset the state based on it.
// todo: separate v4/v6 forwarding state, since the sysctls are independent
// (net.ipv4.ip_forward vs net.ipv6.conf.all.forwarding). Currently the nftables
// manager shares one instance between both routers, which works only because
// EnableIPForwarding enables both sysctls in a single call.
type IPForwardingState struct {
mu sync.Mutex
v4Count int
v6Count int
// routingV4/routingV6 track whether the routing path currently holds a
// reference, so repeated EnableRouting calls (one per network-map update)
// hold at most one reference per family and an unpaired DisableRouting
// can't release references held by DNAT rules.
routingV4 bool
routingV6 bool
wgIfaceName string
v6Saved map[string]int
enabledCounter int
}
// NewIPForwardingState returns a state tracker for the IP-forwarding sysctls.
// wgIfaceName is excluded from the per-interface accept_ra handling.
func NewIPForwardingState(wgIfaceName string) *IPForwardingState {
return &IPForwardingState{wgIfaceName: wgIfaceName}
func NewIPForwardingState() *IPForwardingState {
return &IPForwardingState{}
}
// Counts returns the current v4 and v6 refcounts. Intended for diagnostics
// and tests.
func (f *IPForwardingState) Counts() (v4, v6 int) {
f.mu.Lock()
defer f.mu.Unlock()
return f.v4Count, f.v6Count
}
// RequestRouting takes the forwarding references for the routing path. It is
// idempotent: while routing already holds a reference, further calls don't
// increment the refcounts, and a v4-only request releases a previously held v6
// reference. A v6 sysctl failure is logged and not returned so it can't take
// down v4 routing (the sysctl may be unwritable, e.g. read-only /proc/sys or
// IPv6 disabled on the kernel command line); v6 is retried on the next call.
func (f *IPForwardingState) RequestRouting(v6 bool) error {
f.mu.Lock()
defer f.mu.Unlock()
if !f.routingV4 {
if err := f.requestV4(); err != nil {
return err
}
f.routingV4 = true
}
if !v6 {
if !f.routingV6 {
return nil
}
f.routingV6 = false
return f.releaseV6()
}
if f.routingV6 {
return nil
}
if err := f.requestV6(); err != nil {
log.Warnf("enable IPv6 forwarding for routing: %v", err)
return nil
}
f.routingV6 = true
return nil
}
// ReleaseRouting releases the references RequestRouting holds. Calls without a
// held reference are no-ops.
func (f *IPForwardingState) ReleaseRouting() error {
f.mu.Lock()
defer f.mu.Unlock()
if f.routingV4 {
f.routingV4 = false
f.releaseV4()
}
if f.routingV6 {
f.routingV6 = false
return f.releaseV6()
}
return nil
}
// RequestForwarding enables the family's forwarding sysctl on first request.
func (f *IPForwardingState) RequestForwarding(v6 bool) error {
f.mu.Lock()
defer f.mu.Unlock()
if v6 {
return f.requestV6()
}
return f.requestV4()
}
// ReleaseForwarding decrements the family counter. The last v6 release restores
// what enable captured. v4 stays on: net.ipv4.ip_forward is co-owned by other
// tooling (docker, k8s, libvirt).
func (f *IPForwardingState) ReleaseForwarding(v6 bool) error {
f.mu.Lock()
defer f.mu.Unlock()
if v6 {
return f.releaseV6()
}
f.releaseV4()
return nil
}
func (f *IPForwardingState) requestV4() error {
if f.v4Count == 0 {
if err := systemops.EnableV4IPForwarding(); err != nil {
return fmt.Errorf("enable IPv4 forwarding: %w", err)
}
log.Info("IPv4 forwarding enabled")
}
f.v4Count++
return nil
}
func (f *IPForwardingState) releaseV4() {
if f.v4Count > 0 {
f.v4Count--
}
}
func (f *IPForwardingState) requestV6() error {
if f.v6Count == 0 {
saved, err := systemops.EnableV6IPForwarding(f.wgIfaceName)
if err != nil {
if rerr := systemops.DisableV6IPForwarding(saved); rerr != nil {
log.Warnf("rollback partial v6 sysctls: %v", rerr)
}
return fmt.Errorf("enable IPv6 forwarding: %w", err)
}
// A failed restore on a previous release keeps its saved values; those
// are the true originals, so keep them over what this enable captured.
if f.v6Saved == nil {
f.v6Saved = saved
} else {
for k, v := range saved {
if _, ok := f.v6Saved[k]; !ok {
f.v6Saved[k] = v
}
}
}
log.Info("IPv6 forwarding enabled")
}
f.v6Count++
return nil
}
func (f *IPForwardingState) releaseV6() error {
if f.v6Count == 0 {
return nil
}
f.v6Count--
if f.v6Count > 0 {
func (f *IPForwardingState) RequestForwarding() error {
if f.enabledCounter != 0 {
f.enabledCounter++
return nil
}
// Keep the saved values on failure so a later release or enable/release
// cycle can still restore them; re-restoring an already-restored key is a
// no-op since the sysctl already holds the desired value.
if err := systemops.DisableV6IPForwarding(f.v6Saved); err != nil {
return fmt.Errorf("disable IPv6 forwarding: %w", err)
if err := systemops.EnableIPForwarding(); err != nil {
return fmt.Errorf("failed to enable IP forwarding with sysctl: %w", err)
}
f.v6Saved = nil
log.Info("IPv6 forwarding disabled")
f.enabledCounter = 1
log.Info("IP forwarding enabled")
return nil
}
func (f *IPForwardingState) ReleaseForwarding() error {
if f.enabledCounter == 0 {
return nil
}
if f.enabledCounter > 1 {
f.enabledCounter--
return nil
}
// if failed to disable IP forwarding we anyway decrement the counter
f.enabledCounter = 0
// todo call systemops.DisableIPForwarding()
return nil
}

View File

@@ -1,39 +0,0 @@
//go:build privileged
package ipfwdstate
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestRequestRoutingV6ToV4Transition verifies that a v4-only routing request
// releases a previously held routing-owned v6 reference without touching
// references held by DNAT rules.
func TestRequestRoutingV6ToV4Transition(t *testing.T) {
f := NewIPForwardingState("wt-fwd-test")
require.NoError(t, f.RequestRouting(true), "request routing with v6")
v4, v6 := f.Counts()
assert.Equal(t, 1, v4, "v4 reference held")
assert.Equal(t, 1, v6, "v6 reference held")
require.NoError(t, f.RequestRouting(false), "request routing v4-only")
v4, v6 = f.Counts()
assert.Equal(t, 1, v4, "v4 reference kept")
assert.Equal(t, 0, v6, "routing-owned v6 reference released")
// A DNAT-held reference survives a v4-only routing request.
require.NoError(t, f.RequestForwarding(true), "dnat v6 reference")
require.NoError(t, f.RequestRouting(false), "repeat v4-only request")
_, v6 = f.Counts()
assert.Equal(t, 1, v6, "dnat-held v6 reference survives")
require.NoError(t, f.ReleaseForwarding(true), "release dnat v6 reference")
require.NoError(t, f.ReleaseRouting(), "release routing")
v4, v6 = f.Counts()
assert.Equal(t, 0, v4, "all v4 references released")
assert.Equal(t, 0, v6, "all v6 references released")
}

View File

@@ -58,7 +58,11 @@ func Setup(wgIface iface) (map[string]int, error) {
continue
}
i := fmt.Sprintf(rpFilterInterfacePath, EscapeInterfaceName(intf.Name))
// Escape '%' and '.' so they survive the dot-to-slash conversion in Set()
safeName := strings.ReplaceAll(intf.Name, "%", percentEscape)
safeName = strings.ReplaceAll(safeName, ".", dotEscape)
i := fmt.Sprintf(rpFilterInterfacePath, safeName)
oldVal, err := Set(i, 2, true)
if err != nil {
result = multierror.Append(result, err)
@@ -70,13 +74,6 @@ func Setup(wgIface iface) (map[string]int, error) {
return keys, nberrors.FormatErrorOrNil(result)
}
// EscapeInterfaceName escapes '%' and '.' in an interface name (e.g. VLANs
// like eth0.100) so the name survives the dot-to-slash conversion in Set.
func EscapeInterfaceName(name string) string {
safe := strings.ReplaceAll(name, "%", percentEscape)
return strings.ReplaceAll(safe, ".", dotEscape)
}
// Set sets a sysctl configuration, if onlyIfOne is true it will only set the new value if it's set to 1
func Set(key string, desiredValue int, onlyIfOne bool) (int, error) {
path := strings.ReplaceAll(key, ".", "/")

View File

@@ -32,17 +32,8 @@ func (r *SysOps) removeFromRouteTable(netip.Prefix, Nexthop) error {
return nil
}
func EnableV4IPForwarding() error {
log.Infof("Enable IPv4 forwarding is not implemented on %s", runtime.GOOS)
return nil
}
func EnableV6IPForwarding(string) (map[string]int, error) {
log.Infof("Enable IPv6 forwarding is not implemented on %s", runtime.GOOS)
return map[string]int{}, nil
}
func DisableV6IPForwarding(map[string]int) error {
func EnableIPForwarding() error {
log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS)
return nil
}

View File

@@ -58,17 +58,8 @@ func (r *SysOps) removeFromRouteTable(netip.Prefix, Nexthop) error {
return nil
}
func EnableV4IPForwarding() error {
log.Infof("Enable IPv4 forwarding is not implemented on %s", runtime.GOOS)
return nil
}
func EnableV6IPForwarding(string) (map[string]int, error) {
log.Infof("Enable IPv6 forwarding is not implemented on %s", runtime.GOOS)
return map[string]int{}, nil
}
func DisableV6IPForwarding(map[string]int) error {
func EnableIPForwarding() error {
log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS)
return nil
}

View File

@@ -763,10 +763,13 @@ func flushRoutes(tableID, family int) error {
return nberrors.FormatErrorOrNil(result)
}
func EnableV4IPForwarding() error {
func EnableIPForwarding() error {
if _, err := sysctl.Set(ipv4ForwardingPath, 1, false); err != nil {
return err
}
if _, err := sysctl.Set(ipv6ForwardingPath, 1, false); err != nil {
log.Warnf("failed to enable IPv6 forwarding: %v", err)
}
return nil
}

View File

@@ -43,17 +43,8 @@ func (r *SysOps) RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error
return r.genericRemoveVPNRoute(prefix, intf)
}
func EnableV4IPForwarding() error {
log.Infof("Enable IPv4 forwarding is not implemented on %s", runtime.GOOS)
return nil
}
func EnableV6IPForwarding(string) (map[string]int, error) {
log.Infof("Enable IPv6 forwarding is not implemented on %s", runtime.GOOS)
return map[string]int{}, nil
}
func DisableV6IPForwarding(map[string]int) error {
func EnableIPForwarding() error {
log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS)
return nil
}

View File

@@ -1,92 +0,0 @@
//go:build !android
package systemops
import (
"fmt"
"net"
"os"
"github.com/hashicorp/go-multierror"
log "github.com/sirupsen/logrus"
nberrors "github.com/netbirdio/netbird/client/errors"
"github.com/netbirdio/netbird/client/internal/routemanager/sysctl"
)
const (
// 1 (default) accepts RAs only while forwarding is off; 2 keeps RA
// acceptance on regardless, so RA-installed host defaults survive our
// v6 forwarding flip.
acceptRAInterfacePath = "net.ipv6.conf.%s.accept_ra"
acceptRADefaultPath = "net.ipv6.conf.default.accept_ra"
acceptRAProcPathFormat = "/proc/sys/net/ipv6/conf/%s/accept_ra"
)
// EnableV6IPForwarding bumps accept_ra=2 on host v6 interfaces before flipping
// forwarding=1, so RA-installed host defaults survive. Returns the prior values
// of sysctls we actually changed; entries already at the target are omitted.
func EnableV6IPForwarding(wgIfaceName string) (map[string]int, error) {
saved := map[string]int{}
bumpAcceptRA(saved, wgIfaceName)
oldVal, err := sysctl.Set(ipv6ForwardingPath, 1, false)
if err != nil {
return saved, err
}
if oldVal != 1 {
saved[ipv6ForwardingPath] = oldVal
}
return saved, nil
}
// DisableV6IPForwarding restores what EnableV6IPForwarding captured.
func DisableV6IPForwarding(saved map[string]int) error {
var result *multierror.Error
for key, value := range saved {
if _, err := sysctl.Set(key, value, false); err != nil {
result = multierror.Append(result, fmt.Errorf("restore %s: %w", key, err))
}
}
return nberrors.FormatErrorOrNil(result)
}
func bumpAcceptRA(saved map[string]int, wgIfaceName string) {
// Also bump conf.default so interfaces created while forwarding is on
// (hotplug, new Wi-Fi/dock) inherit accept_ra=2 and keep accepting RAs.
bumpAcceptRAKey(saved, acceptRADefaultPath)
interfaces, err := net.Interfaces()
if err != nil {
log.Warnf("list interfaces for accept_ra: %v", err)
return
}
for _, intf := range interfaces {
if intf.Name == "lo" || intf.Name == wgIfaceName {
continue
}
bumpAcceptRAForInterface(saved, intf.Name)
}
}
func bumpAcceptRAForInterface(saved map[string]int, name string) {
// Build procfs path from name, not the dotted key: VLAN names like eth0.100.
if _, err := os.Stat(fmt.Sprintf(acceptRAProcPathFormat, name)); err != nil {
return
}
bumpAcceptRAKey(saved, fmt.Sprintf(acceptRAInterfacePath, sysctl.EscapeInterfaceName(name)))
}
func bumpAcceptRAKey(saved map[string]int, key string) {
// onlyIfOne=true: leave admin overrides (0, 2) alone.
oldVal, err := sysctl.Set(key, 2, true)
if err != nil {
log.Warnf("bump %s: %v", key, err)
return
}
// With onlyIfOne, a write only happened when the old value was 1; values
// left untouched (0, 2) must not be recorded for restore.
if oldVal == 1 {
saved[key] = oldVal
}
}

View File

@@ -5628,13 +5628,9 @@ func (x *GetPeerSSHHostKeyResponse) GetFound() bool {
type RequestJWTAuthRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// hint for OIDC login_hint parameter (typically email address)
Hint *string `protobuf:"bytes,1,opt,name=hint,proto3,oneof" json:"hint,omitempty"`
// hasGraphicalSession tells the daemon that the caller has a graphical session,
// which decides whether PKCE or the device code flow is preferred. The daemon
// cannot detect this itself: it does not inherit the session environment.
HasGraphicalSession bool `protobuf:"varint,2,opt,name=hasGraphicalSession,proto3" json:"hasGraphicalSession,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
Hint *string `protobuf:"bytes,1,opt,name=hint,proto3,oneof" json:"hint,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RequestJWTAuthRequest) Reset() {
@@ -5674,13 +5670,6 @@ func (x *RequestJWTAuthRequest) GetHint() string {
return ""
}
func (x *RequestJWTAuthRequest) GetHasGraphicalSession() bool {
if x != nil {
return x.HasGraphicalSession
}
return false
}
// RequestJWTAuthResponse contains authentication flow information
type RequestJWTAuthResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -5905,13 +5894,9 @@ type RequestExtendAuthSessionRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Optional OIDC login_hint (typically the user's email) to pre-fill the
// IdP login form.
Hint *string `protobuf:"bytes,1,opt,name=hint,proto3,oneof" json:"hint,omitempty"`
// hasGraphicalSession tells the daemon that the caller has a graphical session,
// which decides whether PKCE or the device code flow is preferred. The daemon
// cannot detect this itself: it does not inherit the session environment.
HasGraphicalSession bool `protobuf:"varint,2,opt,name=hasGraphicalSession,proto3" json:"hasGraphicalSession,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
Hint *string `protobuf:"bytes,1,opt,name=hint,proto3,oneof" json:"hint,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RequestExtendAuthSessionRequest) Reset() {
@@ -5951,13 +5936,6 @@ func (x *RequestExtendAuthSessionRequest) GetHint() string {
return ""
}
func (x *RequestExtendAuthSessionRequest) GetHasGraphicalSession() bool {
if x != nil {
return x.HasGraphicalSession
}
return false
}
// RequestExtendAuthSessionResponse carries the verification URI the UI
// should open in a browser. The daemon retains the flow state and resolves
// it via WaitExtendAuthSession.
@@ -7525,10 +7503,9 @@ const file_daemon_proto_rawDesc = "" +
"sshHostKey\x12\x16\n" +
"\x06peerIP\x18\x02 \x01(\tR\x06peerIP\x12\x1a\n" +
"\bpeerFQDN\x18\x03 \x01(\tR\bpeerFQDN\x12\x14\n" +
"\x05found\x18\x04 \x01(\bR\x05found\"k\n" +
"\x05found\x18\x04 \x01(\bR\x05found\"9\n" +
"\x15RequestJWTAuthRequest\x12\x17\n" +
"\x04hint\x18\x01 \x01(\tH\x00R\x04hint\x88\x01\x01\x120\n" +
"\x13hasGraphicalSession\x18\x02 \x01(\bR\x13hasGraphicalSessionB\a\n" +
"\x04hint\x18\x01 \x01(\tH\x00R\x04hint\x88\x01\x01B\a\n" +
"\x05_hint\"\x9a\x02\n" +
"\x16RequestJWTAuthResponse\x12(\n" +
"\x0fverificationURI\x18\x01 \x01(\tR\x0fverificationURI\x128\n" +
@@ -7548,10 +7525,9 @@ const file_daemon_proto_rawDesc = "" +
"\x14WaitJWTTokenResponse\x12\x14\n" +
"\x05token\x18\x01 \x01(\tR\x05token\x12\x1c\n" +
"\ttokenType\x18\x02 \x01(\tR\ttokenType\x12\x1c\n" +
"\texpiresIn\x18\x03 \x01(\x03R\texpiresIn\"u\n" +
"\texpiresIn\x18\x03 \x01(\x03R\texpiresIn\"C\n" +
"\x1fRequestExtendAuthSessionRequest\x12\x17\n" +
"\x04hint\x18\x01 \x01(\tH\x00R\x04hint\x88\x01\x01\x120\n" +
"\x13hasGraphicalSession\x18\x02 \x01(\bR\x13hasGraphicalSessionB\a\n" +
"\x04hint\x18\x01 \x01(\tH\x00R\x04hint\x88\x01\x01B\a\n" +
"\x05_hint\"\xe0\x01\n" +
" RequestExtendAuthSessionResponse\x12(\n" +
"\x0fverificationURI\x18\x01 \x01(\tR\x0fverificationURI\x128\n" +

View File

@@ -894,10 +894,6 @@ message GetPeerSSHHostKeyResponse {
message RequestJWTAuthRequest {
// hint for OIDC login_hint parameter (typically email address)
optional string hint = 1;
// hasGraphicalSession tells the daemon that the caller has a graphical session,
// which decides whether PKCE or the device code flow is preferred. The daemon
// cannot detect this itself: it does not inherit the session environment.
bool hasGraphicalSession = 2;
}
// RequestJWTAuthResponse contains authentication flow information
@@ -941,10 +937,6 @@ message RequestExtendAuthSessionRequest {
// Optional OIDC login_hint (typically the user's email) to pre-fill the
// IdP login form.
optional string hint = 1;
// hasGraphicalSession tells the daemon that the caller has a graphical session,
// which decides whether PKCE or the device code flow is preferred. The daemon
// cannot detect this itself: it does not inherit the session environment.
bool hasGraphicalSession = 2;
}
// RequestExtendAuthSessionResponse carries the verification URI the UI

View File

@@ -232,3 +232,4 @@ func toNetIDs(routes []string) []route.NetID {
}
return netIDs
}

View File

@@ -1723,8 +1723,8 @@ func (s *Server) RequestJWTAuth(
hint = profilemanager.GetLoginHint()
}
// the daemon has no graphical session of its own, only the caller can answer this
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, msg.GetHasGraphicalSession(), false, hint)
isDesktop := isUnixRunningDesktop()
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isDesktop, false, hint)
if err != nil {
return nil, gstatus.Errorf(codes.Internal, "failed to create OAuth flow: %v", err)
}
@@ -1827,8 +1827,8 @@ func (s *Server) RequestExtendAuthSession(
hint = profilemanager.GetLoginHint()
}
// the daemon has no graphical session of its own, only the caller can answer this
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, msg.GetHasGraphicalSession(), false, hint)
isDesktop := isUnixRunningDesktop()
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isDesktop, false, hint)
if err != nil {
return nil, gstatus.Errorf(codes.Internal, "failed to create OAuth flow: %v", err)
}
@@ -2000,6 +2000,13 @@ func (s *Server) ExposeService(req *proto.ExposeServiceRequest, srv proto.Daemon
return nil
}
func isUnixRunningDesktop() bool {
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
return false
}
return os.Getenv("DESKTOP_SESSION") != "" || os.Getenv("XDG_CURRENT_DESKTOP") != ""
}
func (s *Server) runProbes(ctx context.Context, waitForProbeResult bool) {
if s.connectClient == nil {
return

View File

@@ -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, nil)
networkMapController := controller.NewController(context.Background(), store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), manager.NewEphemeralManager(store, peersManager), config)
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

View File

@@ -13,7 +13,6 @@ import (
"golang.org/x/crypto/ssh"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/util"
)
const (
@@ -93,8 +92,7 @@ func printAuthInstructions(stderr io.Writer, authResponse *proto.RequestJWTAuthR
// RequestJWTToken requests or retrieves a JWT token for SSH authentication
func RequestJWTToken(ctx context.Context, client proto.DaemonServiceClient, stdout, stderr io.Writer, useCache bool, hint string, openBrowser func(string) error) (string, error) {
// the ssh client runs in the user's session, the daemon does not: tell it what we can see
req := &proto.RequestJWTAuthRequest{HasGraphicalSession: util.HasGraphicalSession()}
req := &proto.RequestJWTAuthRequest{}
if hint != "" {
req.Hint = &hint
}
@@ -195,3 +193,4 @@ func buildAddressList(hostname string, remote net.Addr) []string {
}
return addresses
}

View File

@@ -58,8 +58,7 @@ func (s *Session) RequestExtend(ctx context.Context, p ExtendStartParams) (Exten
return ExtendStartResult{}, err
}
// a request from the UI implies a graphical session, which the daemon cannot detect itself
req := &proto.RequestExtendAuthSessionRequest{HasGraphicalSession: true}
req := &proto.RequestExtendAuthSessionRequest{}
if p.Hint != "" {
h := p.Hint
req.Hint = &h

View File

@@ -108,11 +108,10 @@ func (s *Connection) Login(ctx context.Context, p LoginParams) (LoginResult, err
}
req := &proto.LoginRequest{
ManagementUrl: p.ManagementURL,
SetupKey: p.SetupKey,
Hostname: p.Hostname,
// a login driven by the UI always has a graphical session available
IsUnixDesktopClient: true,
ManagementUrl: p.ManagementURL,
SetupKey: p.SetupKey,
Hostname: p.Hostname,
IsUnixDesktopClient: runtime.GOOS == "linux",
}
if profileName != "" {
req.ProfileName = ptrStr(profileName)

0
dummy.txt Normal file
View File

9
go.mod
View File

@@ -71,18 +71,17 @@ 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.10.0
github.com/jackc/pgx/v5 v5.5.5
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-20260803100840-78e79ba20f87
github.com/netbirdio/management-integrations/integrations v0.0.0-20260416123949-2355d972be42
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
@@ -237,8 +236,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-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // 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
View File

@@ -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-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/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/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,8 +415,6 @@ 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=
@@ -484,8 +482,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-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/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/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=

View File

@@ -1,58 +0,0 @@
//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{})
}

View File

@@ -1,22 +0,0 @@
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');

View File

@@ -1,25 +0,0 @@
//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{})
}

View File

@@ -1,62 +0,0 @@
//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."},
},
},
})
}

View File

@@ -1,39 +0,0 @@
//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},
})
}

View File

@@ -1,58 +0,0 @@
//go:build integration
package networkmap_pgsql
import (
"context"
"testing"
"github.com/netbirdio/netbird/management/server/types"
"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) {
if engine == string(types.SqliteStoreEngine) {
t.Skip()
}
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)
}

View File

@@ -1,105 +0,0 @@
//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
//go:embed pg_data.sql
var pgData string
//go:embed sqlite_data.sql
var sqliteData 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, pgData)
pgstore.UsingTimeZone(time.UTC)
case "", string(types.SqliteStoreEngine):
engine = string(types.SqliteStoreEngine)
sqlitestore, cleanup = createSqliteTestStore(baseData, sqliteData)
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
}

View File

@@ -1,61 +0,0 @@
//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,
})
}

View File

@@ -1,372 +0,0 @@
{
"Peers": {
"peer-id-1": {
"ID": "peer-id-1",
"Key": "key-1",
"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-2": {
"ID": "peer-id-2",
"Key": "key-2",
"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-3": {
"ID": "peer-id-3",
"Key": "key-3",
"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": {
"group-no-resources-id": {
"ID": "group-no-resources-id",
"Name": "group-3-name",
"PublicID": "group-no-resources-id-public",
"Peers": null,
"Resources": null
},
"group-one-resource-id": {
"ID": "group-one-resource-id",
"Name": "group-1-name",
"PublicID": "group-one-resource-id-public",
"Peers": [
"peer-id-1"
],
"Resources": [
{
"ID": "host-id-1",
"Type": "host"
}
]
},
"group-two-resources-id": {
"ID": "group-two-resources-id",
"Name": "group-2-name",
"PublicID": "group-two-resources-id-public",
"Peers": [
"peer-id-2",
"peer-id-3"
],
"Resources": [
{
"ID": "subnet-id-1",
"Type": "subnet"
},
{
"ID": "host-id-2",
"Type": "host"
}
]
}
},
"Policies": [],
"Routes": [],
"NameServerGroups": [
{
"ID": "nsgroup-1",
"PublicID": "nsgroup-1-public",
"Name": "nsgroup-1",
"Description": "nsgroup-1",
"NameServers": [
{
"IP": "192.168.31.2",
"NSType": 1,
"Port": 53
}
],
"Groups": [
"group-one-resource-id"
],
"Primary": true,
"Domains": [
"test-1.com"
],
"Enabled": true,
"SearchDomainsEnabled": false
},
{
"ID": "nsgroup-2",
"PublicID": "nsgroup-2-public",
"Name": "nsgroup-2",
"Description": "nsgroup-2",
"NameServers": [
{
"IP": "192.168.32.3",
"NSType": 1,
"Port": 53
}
],
"Groups": [
"group-one-resource-id",
"group-no-resources-id"
],
"Primary": true,
"Domains": [
"test-1.com",
"test-2.com"
],
"Enabled": true,
"SearchDomainsEnabled": false
},
{
"ID": "nsgroup-3",
"PublicID": "nsgroup-3-public",
"Name": "",
"Description": "",
"NameServers": null,
"Groups": null,
"Primary": false,
"Domains": null,
"Enabled": true,
"SearchDomainsEnabled": false
}
],
"NetworkResources": [],
"Network": {
"Identifier": "network-1",
"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": [
"group-one-resource-id"
],
"RoutingPeerDNSResolutionEnabled": false,
"LazyConnectionEnabled": false,
"AutoUpdateVersion": "disabled",
"AutoUpdateAlways": false,
"MetricsPushEnabled": false
},
"PostureChecks": {},
"PostureValidation": null,
"AllowedUserIDs": {},
"NetworkXIDToPublicID": {},
"PostureCheckXIDToPublicID": {},
"ValidatedPeers": {
"peer-id-1": {},
"peer-id-2": {},
"peer-id-3": {}
},
"ResourcePolicies": {},
"Routers": {},
"GroupIDToUserIDs": {},
"DNSDomain": "",
"ProxyTargetedDomainResourceIDs": {},
"AppliedZoneCandidates": [
{
"DistributionGroups": [
"group-one-resource-id"
],
"Zone": {
"Domain": "test-1.com",
"Records": [
{
"Name": "test.test-1.com",
"Type": 1,
"Class": "IN",
"TTL": 1800,
"RData": "1.1.1.1"
},
{
"Name": "test2.test-1.com",
"Type": 1,
"Class": "IN",
"TTL": 1800,
"RData": "1.1.1.2"
},
{
"Name": "test3.test-1.com",
"Type": 5,
"Class": "IN",
"TTL": 1800,
"RData": "test4.test-1.com."
}
],
"SearchDomainDisabled": false,
"NonAuthoritative": false
}
},
{
"DistributionGroups": [
"group-two-resources-id"
],
"Zone": {
"Domain": "test-2.com",
"Records": [
{
"Name": "test2.test-2.com",
"Type": 5,
"Class": "IN",
"TTL": 1800,
"RData": "test3.test-2.com."
}
],
"SearchDomainDisabled": true,
"NonAuthoritative": false
}
}
],
"PrivateServiceCandidates": null
}

View File

@@ -1,103 +0,0 @@
//go:build integration
package networkmap_pgsql
import (
"context"
_ "embed"
"encoding/json"
"net/netip"
"os"
"path/filepath"
"runtime"
"testing"
networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db"
"github.com/netbirdio/netbird/management/server/integrations/extra_settings"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/proto"
"github.com/stretchr/testify/assert"
)
//go:embed network_map_data_golden.json
var goldenNMap string
const EnvUpdateGoldenData = "NMAP_UPDATE_GOLDEN_DATA"
func TestGetNetworkMapData(t *testing.T) {
ctx := context.TODO()
storeImpl := networkmapdb.NetworkMapDBStoreImpl{
Store: store(t),
ExtraSettingsManager: &extraSettingsManagerForTesting{},
IntegratedPeerValidator: &peerValidatorForTesting{},
}
nmap, err := storeImpl.GetNetworkMapData(ctx, "account-1")
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))
}
// need these calls for the test
func (*extraSettingsManagerForTesting) GetExtraSettings(ctx context.Context, accountID string) (*types.ExtraSettings, error) {
return &types.ExtraSettings{}, nil
}
func (*peerValidatorForTesting) GetValidatedPeers(ctx context.Context, accountID string, groups []*nmdata.Group, peers []*nmdata.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) {
return map[string]struct{}{
"peer-id-1": {},
"peer-id-2": {},
"peer-id-3": {},
}, nil
}
type extraSettingsManagerForTesting struct{}
func (*extraSettingsManagerForTesting) GetExtraSettingsManager() extra_settings.Manager { return nil }
func (*extraSettingsManagerForTesting) GetSettings(ctx context.Context, accountID string, userID string) (*types.Settings, error) {
return nil, nil
}
func (*extraSettingsManagerForTesting) UpdateExtraSettings(ctx context.Context, accountID, userID string, extraSettings *types.ExtraSettings) (bool, error) {
return false, nil
}
func (*extraSettingsManagerForTesting) GetEffectiveNetworkRanges(ctx context.Context, accountID string) (v4, v6 netip.Prefix, err error) {
return netip.Prefix{}, netip.Prefix{}, nil
}
type peerValidatorForTesting struct{}
func (*peerValidatorForTesting) ValidateExtraSettings(ctx context.Context, newExtraSettings *types.ExtraSettings, oldExtraSettings *types.ExtraSettings, userID string, accountID string) error {
return nil
}
func (*peerValidatorForTesting) ValidatePeer(ctx context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *types.ExtraSettings) (*nbpeer.Peer, bool, error) {
return nil, false, nil
}
func (*peerValidatorForTesting) PreparePeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings, temporary bool) *nbpeer.Peer {
return nil
}
func (*peerValidatorForTesting) IsNotValidPeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) (bool, bool, error) {
return false, false, nil
}
func (*peerValidatorForTesting) GetInvalidPeers(ctx context.Context, accountID string, extraSettings *types.ExtraSettings) (map[string]string, error) {
return nil, nil
}
func (*peerValidatorForTesting) PeerDeleted(ctx context.Context, accountID, peerID string, extraSettings *types.ExtraSettings) error {
return nil
}
func (*peerValidatorForTesting) SetPeerInvalidationListener(fn func(accountID string, peerIDs []string)) {
}
func (*peerValidatorForTesting) Stop(ctx context.Context) {}
func (*peerValidatorForTesting) ValidateFlowResponse(ctx context.Context, peerKey string, flowResponse *proto.PKCEAuthorizationFlow) *proto.PKCEAuthorizationFlow {
return nil
}

View File

@@ -1,65 +0,0 @@
//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,
})
}

View File

@@ -1,33 +0,0 @@
//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"}}})
}

View File

@@ -1,56 +0,0 @@
//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
}

View File

@@ -1,26 +0,0 @@
//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",
})
}

View File

@@ -1,163 +0,0 @@
//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},
})
}

View File

@@ -1,30 +0,0 @@
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+00','"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+00','"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+00','"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"');

View File

@@ -1,122 +0,0 @@
//go:build integration
package networkmap_pgsql
import (
"context"
"fmt"
"regexp"
"slices"
"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, pgData 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 slices.Concat(strings.Split(baseData, ";"), strings.Split(pgData, ";")) {
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}`)
}

View File

@@ -1,146 +0,0 @@
//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{}{}},
})
}

View File

@@ -1,61 +0,0 @@
//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"})
}

View File

@@ -1,87 +0,0 @@
//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",
})
}

View File

@@ -1,109 +0,0 @@
//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": {},
})
}

View File

@@ -1,30 +0,0 @@
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"');

View File

@@ -1,49 +0,0 @@
//go:build integration
package networkmap_pgsql
import (
"context"
"fmt"
"runtime"
"slices"
"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, sqliteData 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 slices.Concat(strings.Split(baseData, ";"), strings.Split(sqliteData, ";")) {
if _, err := sqldb.Exec(query); err != nil {
log.Fatalf("error initializing db: %s", err.Error())
}
}
return &networkmap_sqlite.SqliteStore{Db: sqldb}, func() {}
}

View File

@@ -1,57 +0,0 @@
//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"},
})
}

View File

@@ -1,10 +0,0 @@
//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

View File

@@ -1,74 +0,0 @@
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...)
}

View File

@@ -18,7 +18,6 @@ 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"
@@ -31,8 +30,6 @@ 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"
@@ -64,8 +61,6 @@ type Controller struct {
serverSupportedSyncMessageVersion sharedgrpc.SyncMessageVersion
perAccountServerSupportedSyncMessageVersions map[string]sharedgrpc.SyncMessageVersion
nmdataStore *networkmapdb.NetworkMapDBStoreImpl
}
type bufferUpdate struct {
@@ -83,7 +78,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, nmdataStore *networkmapdb.NetworkMapDBStoreImpl) *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) *Controller {
nMetrics, err := newMetrics(metrics.UpdateChannelMetrics())
if err != nil {
log.Fatal(fmt.Errorf("error creating metrics: %w", err))
@@ -104,7 +99,6 @@ 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,
}
}
@@ -153,11 +147,6 @@ 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)
@@ -178,7 +167,7 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
return nil
}
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, types.TwinGroups(maps.Values(account.Groups)), types.TwinPeers(maps.Values(account.Peers)), account.Settings.Extra)
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
if err != nil {
return fmt.Errorf("failed to get validate peers: %v", err)
}
@@ -266,7 +255,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, types.TwinPeer(p), nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, types.TwinAccountSettings(account.Settings), extraSetting, maps.Keys(peerGroups), dnsFwdPort)
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)
c.metrics.CountToComponentSyncResponseDuration(time.Since(start))
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
@@ -287,7 +276,7 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
}
start = time.Now()
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)
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)
c.metrics.CountToSyncResponseDuration(time.Since(start))
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
@@ -305,261 +294,6 @@ 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
}
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
@@ -592,10 +326,6 @@ 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)
@@ -611,7 +341,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, types.TwinGroups(maps.Values(account.Groups)), types.TwinPeers(maps.Values(account.Peers)), account.Settings.Extra)
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
if err != nil {
return fmt.Errorf("failed to get validate peers: %v", err)
}
@@ -698,7 +428,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, types.TwinPeer(p), nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, types.TwinAccountSettings(account.Settings), extraSetting, maps.Keys(peerGroups), dnsFwdPort)
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)
c.metrics.CountToComponentSyncResponseDuration(time.Since(start))
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
@@ -719,7 +449,7 @@ func (c *Controller) sendUpdateForAffectedPeers(ctx context.Context, accountID s
}
start = time.Now()
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)
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)
c.metrics.CountToSyncResponseDuration(time.Since(start))
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
@@ -776,7 +506,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, types.TwinGroups(maps.Values(account.Groups)), types.TwinPeers(maps.Values(account.Peers)), account.Settings.Extra)
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
if err != nil {
return fmt.Errorf("failed to get validated peers: %v", err)
}
@@ -836,7 +566,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, types.TwinPeer(peer), nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, types.TwinAccountSettings(account.Settings), extraSettings, maps.Keys(peerGroups), dnsFwdPort)
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)
c.peersUpdateManager.SendUpdate(ctx, peer.ID, &network_map.UpdateMessage{
Update: update,
@@ -853,7 +583,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, types.TwinPeer(peer), nil, nil, nmap, dnsDomain, postureChecks, dnsCache, types.TwinAccountSettings(account.Settings), extraSettings, maps.Keys(peerGroups), dnsFwdPort)
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)
c.peersUpdateManager.SendUpdate(ctx, peer.ID, &network_map.UpdateMessage{
Update: update,
@@ -913,11 +643,7 @@ func (c *Controller) GetValidatedPeerWithComponents(ctx context.Context, isRequi
if err != nil {
return nil, nil, nil, nil, 0, err
}
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)
return peer, &types.NetworkMapComponents{Network: network.Copy()}, nil, nil, 0, nil
}
account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
@@ -927,7 +653,7 @@ func (c *Controller) GetValidatedPeerWithComponents(ctx context.Context, isRequi
c.injectAllProxyPolicies(ctx, account)
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, types.TwinGroups(maps.Values(account.Groups)), types.TwinPeers(maps.Values(account.Peers)), account.Settings.Extra)
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
if err != nil {
return nil, nil, nil, nil, 0, err
}
@@ -964,21 +690,6 @@ 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 {
@@ -1085,15 +796,11 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr
}
emptyMap := &types.NetworkMap{
Network: types.TwinNetwork(network),
Network: network.Copy(),
}
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
@@ -1101,7 +808,7 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr
c.injectAllProxyPolicies(ctx, account)
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, types.TwinGroups(maps.Values(account.Groups)), types.TwinPeers(maps.Values(account.Peers)), account.Settings.Extra)
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
if err != nil {
return nil, nil, 0, err
}
@@ -1141,21 +848,6 @@ 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 {
@@ -1218,36 +910,20 @@ 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 {
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 {
if len(peers) == 0 {
return int64(network_map.OldForwarderPort)
}
reqVer := semver.Canonical(requiredVersion)
// Check if all peers have the required version or newer
for _, wtVersion := range wtVersions {
for _, peer := range peers {
// Development version is always supported
if version.IsDevelopmentVersion(wtVersion) {
if version.IsDevelopmentVersion(peer.Meta.WtVersion) {
continue
}
peerVersion := semver.Canonical("v" + wtVersion)
peerVersion := semver.Canonical("v" + peer.Meta.WtVersion)
if peerVersion == "" {
// If any peer doesn't have version info, return 0
return int64(network_map.OldForwarderPort)
@@ -1381,7 +1057,7 @@ func (c *Controller) GetNetworkMap(ctx context.Context, peerID string) (*types.N
groups[groupID] = group.Peers
}
validatedPeers, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, types.TwinGroups(maps.Values(account.Groups)), types.TwinPeers(maps.Values(account.Peers)), account.Settings.Extra)
validatedPeers, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
if err != nil {
return nil, err
}

View File

@@ -1,380 +0,0 @@
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 ""
}

View File

@@ -1,218 +0,0 @@
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[:])
}

View File

@@ -1,12 +0,0 @@
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"))
}

View File

@@ -1,311 +0,0 @@
// 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 legacy full map (grpc.ToSyncResponse) and the
// component envelope expanded client-side (grpc.ToComponentSyncResponse →
// networkmap.EnvelopeToNetworkMap).
//
// 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. The modes are not
// different computations: CalculateNetworkMapFromComponents is
// components.Calculate, and both sides assemble the proto with the same
// encode helpers, so the only variable is what the envelope round-trip did to
// the components in transit. Any difference between modes is therefore a
// round-trip fidelity defect, and a shared expectation is what exposes it.
// 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"
"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"
defaultAccountID = "account"
defaultDNSDomain = "netbird.test"
)
var defaultModes = []Mode{ModeFull, ModeEnvelope}
// 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
}
}
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)
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) *proto.NetworkMap {
t.Helper()
peer := nmData.Peers[peerID]
require.NotNil(t, peer, "target peer %q not in fixture", peerID)
switch mode {
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 mode is compared verbatim, identifiers included, so the expectation
// pins real ids and stays readable. Other modes have identifiers erased on both
// sides first, because the envelope 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 != ModeFull {
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 != ModeFull {
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"+
"Both modes run the same computation on the same components, so they must produce the same map. "+
"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
}

View File

@@ -1,7 +0,0 @@
{
"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"
]
}

View File

@@ -1,65 +0,0 @@
{
"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"
}
}

View File

@@ -1,102 +0,0 @@
{
"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
]
}
}
}
}

View File

@@ -1,31 +0,0 @@
{
"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": {}}
}

View File

@@ -1,7 +0,0 @@
{
"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"
]
}

View File

@@ -1,93 +0,0 @@
{
"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.",
"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"
}
}

View File

@@ -1,34 +0,0 @@
{
"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"
}
}

View File

@@ -1,51 +0,0 @@
{
"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.",
"Records": [
{"Name": "db.corp.internal.", "Type": 1, "Class": "IN", "TTL": 300, "RData": "10.10.0.5"}
]
}
}
]
}

View File

@@ -1,4 +0,0 @@
{
"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"]
}

View File

@@ -1,59 +0,0 @@
{
"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"
}
}

View File

@@ -1,92 +0,0 @@
{
"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"
}
}

View File

@@ -1,43 +0,0 @@
{
"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}
}
}
}

View File

@@ -1,4 +0,0 @@
{
"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"]
}

View File

@@ -1,56 +0,0 @@
{
"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"
}
}

View File

@@ -1,69 +0,0 @@
{
"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"
}
}

View File

@@ -1,43 +0,0 @@
{
"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}
}
}
}

View File

@@ -1,4 +0,0 @@
{
"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"]
}

View File

@@ -1,34 +0,0 @@
{
"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"
}
}

View File

@@ -1,34 +0,0 @@
{
"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"
}
}

View File

@@ -1,42 +0,0 @@
{
"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}
}
}
}

Some files were not shown because too many files have changed in this diff Show More