[management] validate the proxy cluster an agent network bootstraps onto

The agent network gateway service is synthesised as private: agents reach
it over the WireGuard tunnel and are authorised by ValidateTunnelPeer
against the enabled policies' source groups, and its only target is the
cluster itself with DirectUpstream. Only a proxy running embedded in a
netbird client can serve that, which management already reports per
cluster as the `private` capability.

CreateSettings accepted any hostname as proxy_address, so a labeled
bootstrap could pin the account to a cluster that cannot serve its
gateway — another account's BYOP cluster, or one whose proxies are all
centralised. The endpoint assigned at bootstrap is immutable, so the
account is then stuck with a dead gateway until someone deletes and
re-bootstraps the settings row.

Validate the cluster before allocating an endpoint beneath it: a cluster
whose live proxies have reported their capabilities must be one the
account may route through and must be private-capable. A cluster nothing
is connected to is left alone, so claiming an address ahead of the
proxy's first connection keeps working — the same address-first order the
dedicated (self-addressed) path documents, and the one the e2e suite and
self-hosted setups follow.

The e2e coverage drives the real thing: one combined server and two
proxies in the same cluster — a centralised one that makes the cluster
live but unusable, then an embedded one that makes it usable — so both
the refusal and the acceptance are exercised against the same account and
cluster address, with the domains endpoint (the list the dashboard picks
from) as the barrier between starting a proxy and asserting on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-09-01 22:56:07 +00:00
parent 11733fd718
commit 083906b84d
6 changed files with 382 additions and 0 deletions
@@ -13,6 +13,7 @@ import (
networkmap "github.com/netbirdio/netbird/management/internals/controllers/network_map"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
agenttypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
rpproxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/store"
@@ -92,6 +93,7 @@ func TestAgentNetwork_ProviderCRUD_FansOutToProxyAndClientPeers(t *testing.T) {
// UpdateAccountPeers, which is the path under test.
agentMgr := agentnetwork.NewManager(am.Store, permissions.NewManager(am.Store), am, nil)
seedEmbeddedProxyCluster(t, am.Store, clusterAddr)
_, err = agentMgr.CreateSettings(ctx, adminUserID, agenttypes.DefaultSettings(accountID), clusterAddr, "")
require.NoError(t, err, "CreateSettings must bootstrap the endpoint")
// The bootstrap itself reconciles and queues updates on both channels;
@@ -218,3 +220,22 @@ func synthZoneRData(sync *nbproto.SyncResponse, clusterAddr, fqdn string) string
}
return ""
}
// seedEmbeddedProxyCluster registers a connected proxy running embedded in a
// netbird client for clusterAddr, matching what a real deployment looks like
// when the account bootstraps: the agent-network gateway service is always
// private, so its cluster has to be one that can serve private services.
func seedEmbeddedProxyCluster(t *testing.T, st store.Store, clusterAddr string) {
t.Helper()
private := true
now := time.Now().UTC()
require.NoError(t, st.SaveProxy(context.Background(), &rpproxy.Proxy{
ID: "agent-net-proxy-" + clusterAddr,
SessionID: "agent-net-session",
ClusterAddress: clusterAddr,
LastSeen: now,
ConnectedAt: &now,
Status: rpproxy.StatusConnected,
Capabilities: rpproxy.Capabilities{Private: &private},
}), "seeding the proxy cluster must succeed")
}