inject proxy policies on nmdata path

This commit is contained in:
pascal
2026-08-19 15:47:59 +02:00
parent fa286ac778
commit 2e2d0d54dc
21 changed files with 594 additions and 212 deletions

View File

@@ -131,12 +131,12 @@ func (c *Controller) OnPeerDisconnected(ctx context.Context, accountID string, p
// injectAllProxyPolicies prepares an account for the per-peer network-map
// computation. It prepends the in-memory agent-network services synthesised
// from the account's current provider/policy state to account.Services so
// the existing InjectProxyPolicies + injectPrivateServicePolicies walks pick
// them up alongside persisted reverse-proxy services. Synthesised services
// are never persisted; the account is loaded fresh per cycle so re-prepending
// is safe and idempotent. Accounts without agent-network providers get an
// empty synth slice — no behaviour change.
// from the account's current provider/policy state to account.Services, so the
// twin store built from the account carries them alongside the persisted
// reverse-proxy services and synthesises their ACLs. Synthesised services are
// never persisted; the account is loaded fresh per cycle so re-prepending is
// safe and idempotent. Accounts without agent-network providers get an empty
// synth slice — no behaviour change.
func (c *Controller) injectAllProxyPolicies(ctx context.Context, account *types.Account) {
synth, err := c.repo.SynthesizeAgentNetworkServices(ctx, account.Id)
if err != nil {
@@ -144,7 +144,26 @@ func (c *Controller) injectAllProxyPolicies(ctx context.Context, account *types.
} else if len(synth) > 0 {
account.Services = append(synth, account.Services...)
}
account.InjectProxyPolicies(ctx)
}
// proxyServicesFromRepo is the store-path counterpart of
// injectAllProxyPolicies: the network-map store reads the policies table, which
// never holds the proxy ACLs, so the twin gets the services they are
// synthesised from — the synthesised agent-network ones first, exactly as the
// account path orders them.
func (c *Controller) proxyServicesFromRepo(ctx context.Context, accountID string) []*nmdata.Service {
persisted, err := c.repo.GetAccountServices(ctx, accountID)
if err != nil {
log.WithContext(ctx).Errorf("failed to get services for account %s: %v", accountID, err)
return nil
}
synth, err := c.repo.SynthesizeAgentNetworkServices(ctx, accountID)
if err != nil {
log.WithContext(ctx).Warnf("synthesise agent-network services for account %s: %v", accountID, err)
}
return types.TwinServices(append(synth, persisted...))
}
func (c *Controller) CountStreams() int {
@@ -467,6 +486,8 @@ func (c *Controller) getNetworkMapData(ctx context.Context, accountID string) *n
return nil
}
nmData.Services = c.proxyServicesFromRepo(ctx, accountID)
return nmData
}

View File

@@ -3,9 +3,9 @@ package controller
import (
"context"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
"github.com/netbirdio/netbird/management/internals/modules/zones"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
"github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types"
@@ -22,6 +22,7 @@ type Repository interface {
// services synthesised from the account's agent-network provider/policy
// state. Empty for accounts without agent-network providers.
SynthesizeAgentNetworkServices(ctx context.Context, accountID string) ([]*service.Service, error)
GetAccountServices(ctx context.Context, accountID string) ([]*service.Service, error)
}
type repository struct {
@@ -60,6 +61,10 @@ func (r *repository) SynthesizeAgentNetworkServices(ctx context.Context, account
return agentnetwork.SynthesizeServices(ctx, r.store, accountID)
}
func (r *repository) GetAccountServices(ctx context.Context, accountID string) ([]*service.Service, error) {
return r.store.GetAccountServices(ctx, store.LockingStrengthNone, accountID)
}
func (r *repository) GetAccountZones(ctx context.Context, accountID string) ([]*zones.Zone, error) {
return r.store.GetAccountZones(ctx, store.LockingStrengthNone, accountID)
}

View File

@@ -0,0 +1,7 @@
{
"description": "A reverse-proxy service targeting a domain network resource. The synthesised proxy-access ACL is a resource policy too: on the account path the resource-policy map was built after injection, so the routing peer must carry a route firewall rule sourced from the proxy peer for the resource's domain. The store reads the policies table and ResourcePolicies never holds it, so only the synthesis puts it there.",
"peers": [
"router-peer",
"proxy-peer"
]
}

View File

@@ -0,0 +1,44 @@
{
"Network": {"Serial": 32},
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
"Peers": {
"router-peer": {"IP": "100.64.0.9", "Meta": {"WtVersion": "0.60.0"}},
"proxy-peer": {
"IP": "100.64.0.99",
"Meta": {"WtVersion": "0.60.0"},
"ProxyMeta": {"Embedded": true, "Cluster": "eu.proxy.netbird.io"}
}
},
"NetworkResources": [
{
"ID": "res-domain",
"NetworkID": "net-1",
"Name": "app-domain",
"Type": "domain",
"Domain": "app.internal",
"Enabled": true
}
],
"Routers": {
"net-1": {
"router-peer": {"PublicID": "router-direct", "Masquerade": true, "Metric": 9999, "Enabled": true}
}
},
"ProxyTargetedDomainResourceIDs": {"res-domain": {}},
"Services": [
{
"ID": "svc-1",
"Enabled": true,
"Mode": "http",
"ProxyCluster": "eu.proxy.netbird.io",
"Targets": [
{
"Enabled": true,
"Protocol": "https",
"TargetID": "res-domain",
"TargetType": "domain"
}
]
}
]
}

View File

@@ -0,0 +1,7 @@
{
"description": "A reverse-proxy service targeting a peer. The proxy-access ACL is synthesised from Services, never loaded from the policies table, and is what lets the cluster's embedded proxy peer reach the target on the target's port: proxy-peer gets an OUT rule to app-peer on TCP 8080 and app-peer the matching IN rule. Without the synthesis both maps are empty of each other.",
"peers": [
"proxy-peer",
"app-peer"
]
}

View File

@@ -0,0 +1,63 @@
{
"Serial": "30",
"peerConfig": {
"address": "100.64.0.10/10",
"sshConfig": {},
"fqdn": "app-peer.netbird.test",
"RoutingPeerDnsResolutionEnabled": true,
"autoUpdate": {}
},
"remotePeers": [
{
"wgPubKey": "MgrwmZOHFZ+i0SXrfbBOcATxBAQsWKllrGL/32GvlxY=",
"allowedIps": [
"100.64.0.99/32"
],
"sshConfig": {},
"fqdn": "proxy-peer.netbird.test",
"agentVersion": "0.60.0"
}
],
"DNSConfig": {
"ServiceEnable": true,
"CustomZones": [
{
"Domain": "netbird.test.",
"Records": [
{
"Name": "app-peer.netbird.test",
"Type": "1",
"Class": "IN",
"TTL": "300",
"RData": "100.64.0.10"
},
{
"Name": "proxy-peer.netbird.test",
"Type": "1",
"Class": "IN",
"TTL": "300",
"RData": "100.64.0.99"
}
]
}
],
"ForwarderPort": "22054"
},
"FirewallRules": [
{
"PeerIP": "100.64.0.99",
"Protocol": "TCP",
"PortInfo": {
"range": {
"start": 8080,
"end": 8080
}
},
"PolicyID": "cHJveHktYWNjZXNzLXN2Yy0xLXByb3h5LXBlZXIt"
}
],
"routesFirewallRulesIsEmpty": true,
"sshAuth": {
"UserIDClaim": "sub"
}
}

View File

@@ -0,0 +1,64 @@
{
"Serial": "30",
"peerConfig": {
"address": "100.64.0.99/10",
"sshConfig": {},
"fqdn": "proxy-peer.netbird.test",
"RoutingPeerDnsResolutionEnabled": true,
"autoUpdate": {}
},
"remotePeers": [
{
"wgPubKey": "/wFxrqMtMwWNZak/f0UDddUkCZMTmxNuiuk4/RGGNcY=",
"allowedIps": [
"100.64.0.10/32"
],
"sshConfig": {},
"fqdn": "app-peer.netbird.test",
"agentVersion": "0.60.0"
}
],
"DNSConfig": {
"ServiceEnable": true,
"CustomZones": [
{
"Domain": "netbird.test.",
"Records": [
{
"Name": "app-peer.netbird.test",
"Type": "1",
"Class": "IN",
"TTL": "300",
"RData": "100.64.0.10"
},
{
"Name": "proxy-peer.netbird.test",
"Type": "1",
"Class": "IN",
"TTL": "300",
"RData": "100.64.0.99"
}
]
}
],
"ForwarderPort": "22054"
},
"FirewallRules": [
{
"PeerIP": "100.64.0.10",
"Direction": "OUT",
"Protocol": "TCP",
"PortInfo": {
"range": {
"start": 8080,
"end": 8080
}
},
"PolicyID": "cHJveHktYWNjZXNzLXN2Yy0xLXByb3h5LXBlZXIt"
}
],
"routesFirewallRulesIsEmpty": true,
"sshAuth": {
"UserIDClaim": "sub"
}
}

View File

@@ -0,0 +1,29 @@
{
"Network": {"Serial": 30},
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
"Peers": {
"proxy-peer": {
"IP": "100.64.0.99",
"Meta": {"WtVersion": "0.60.0"},
"ProxyMeta": {"Embedded": true, "Cluster": "eu.proxy.netbird.io"}
},
"app-peer": {"IP": "100.64.0.10", "Meta": {"WtVersion": "0.60.0"}}
},
"Services": [
{
"ID": "svc-1",
"Enabled": true,
"Mode": "http",
"ProxyCluster": "eu.proxy.netbird.io",
"Targets": [
{
"Enabled": true,
"Port": 8080,
"Protocol": "http",
"TargetID": "app-peer",
"TargetType": "peer"
}
]
}
]
}

View File

@@ -0,0 +1,7 @@
{
"description": "A private reverse-proxy service. The private-access ACL is synthesised from Services, never loaded from the policies table, and is what lets the service's AccessGroups reach the cluster's embedded proxy peer on TCP 80 and 443: user-peer gets OUT rules on both ports and proxy-peer the matching IN rules. Without the synthesis both maps are empty of each other.",
"peers": [
"user-peer",
"proxy-peer"
]
}

View File

@@ -0,0 +1,26 @@
{
"Network": {"Serial": 31},
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
"Peers": {
"user-peer": {"IP": "100.64.0.10", "Meta": {"WtVersion": "0.60.0"}},
"other-peer": {"IP": "100.64.0.11", "Meta": {"WtVersion": "0.60.0"}},
"proxy-peer": {
"IP": "100.64.0.99",
"Meta": {"WtVersion": "0.60.0"},
"ProxyMeta": {"Embedded": true, "Cluster": "eu.proxy.netbird.io"}
}
},
"Groups": {
"grp-admins": {"Peers": ["user-peer"]}
},
"Services": [
{
"ID": "svc-1",
"Enabled": true,
"Private": true,
"Mode": "http",
"ProxyCluster": "eu.proxy.netbird.io",
"AccessGroups": ["grp-admins", "grp-deleted"]
}
]
}