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

@@ -17,6 +17,8 @@ type sshRequirements struct {
// exactly, operating on nmdata twins throughout — no Account reference and no
// twin↔real conversion, since the produced components hold twins.
func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCustomZone nmdata.CustomZone) *types.NetworkMapComponents {
nmd.InjectProxyPolicies()
forceRoutingPeerDNS := nmd.forcesRoutingPeerDNSResolution(peerID)
peer := nmd.Peers[peerID]

View File

@@ -48,8 +48,16 @@ type NetworkMapData struct { //nolint:revive // established name across the code
AppliedZoneCandidates []AppliedZoneCandidate
PrivateServiceCandidates []PrivateServiceCandidate
// Services are the account's reverse-proxy services, persisted ones and
// the in-memory ones synthesised from agent-network state. They are the
// source of the proxy ACLs injectProxyPolicies synthesises, which no
// builder can load because they are never written to the database.
Services []*nmdata.Service
peerGroupsOnce sync.Once
peerGroupsIdx map[string]map[string]struct{}
proxyPoliciesOnce sync.Once
}
// AppliedZoneCandidate is an account-level custom DNS zone reduced to the

View File

@@ -36,6 +36,7 @@ type Peer struct {
// ProxyMeta is the slim twin of peer.ProxyMeta.
type ProxyMeta struct {
Embedded bool
Cluster string
}
// PeerSystemMeta is the slim twin of peer.PeerSystemMeta.

View File

@@ -0,0 +1,25 @@
package nmdata
// Service is the slim twin of the reverse-proxy service.Service. It carries
// only the state proxy-policy injection reads: the persisted reverse-proxy
// services and the in-memory ones synthesised from agent-network state, which
// are never written to the database.
type Service struct {
ID string
Enabled bool
Private bool
Mode string
ProxyCluster string
AccessGroups []string
Targets []*ServiceTarget
}
// ServiceTarget is the slim twin of service.Target.
type ServiceTarget struct {
Enabled bool
Path string
Port uint16
Protocol string
TargetID string
TargetType string
}

View File

@@ -0,0 +1,203 @@
package networkmap
import (
"fmt"
"slices"
"strings"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/types"
)
const (
serviceModeUDP = "udp"
privateServicePortHTTP = 80
privateServicePortHTTPS = 443
)
// InjectProxyPolicies synthesises the in-memory ACLs that carry reverse-proxy
// traffic and appends them to the twin's policies. They are never persisted,
// so no builder can load them: a proxy-access policy lets a cluster's proxy
// peers reach each enabled target of a service, and a private-access policy
// lets a private service's AccessGroups reach those proxy peers on HTTP(S).
//
// GetPeerNetworkMapComponents calls it, so every caller of the twin gets the
// same policy set no matter which builder produced it. It runs at most once
// per twin, and is safe to call again to force the synthesis early.
func (nmd *NetworkMapData) InjectProxyPolicies() {
nmd.proxyPoliciesOnce.Do(nmd.injectProxyPolicies)
}
func (nmd *NetworkMapData) injectProxyPolicies() {
if len(nmd.Services) == 0 {
return
}
proxyPeersByCluster := nmd.proxyPeersByCluster()
if len(proxyPeersByCluster) == 0 {
return
}
for _, svc := range nmd.Services {
if svc == nil || !svc.Enabled {
continue
}
proxyPeers := proxyPeersByCluster[svc.ProxyCluster]
for _, target := range svc.Targets {
if target == nil || !target.Enabled {
continue
}
port, ok := resolveTargetPort(target)
if !ok {
continue
}
for _, proxyPeer := range proxyPeers {
nmd.addInjectedPolicy(proxyAccessPolicy(svc, target, proxyPeer, port))
}
}
nmd.injectPrivateServicePolicies(svc, proxyPeers)
}
}
// injectPrivateServicePolicies synthesises AccessGroups → cluster proxy peers on TCP 80/443.
func (nmd *NetworkMapData) injectPrivateServicePolicies(svc *nmdata.Service, proxyPeers []*nmdata.Peer) {
if !svc.Private || len(svc.AccessGroups) == 0 || len(proxyPeers) == 0 {
return
}
// A service's AccessGroups can name groups that no longer exist — persisted
// services and the agent-network synthesiser both carry the ids verbatim from
// their own state. An unresolvable source authorises nothing, so drop it here
// rather than let the network-map assembly resolve it to a nil group.
sources := nmd.existingGroupIDs(svc.AccessGroups)
if len(sources) == 0 {
return
}
for _, proxyPeer := range proxyPeers {
nmd.addInjectedPolicy(privateAccessPolicy(svc, proxyPeer, sources))
}
}
// addInjectedPolicy appends the policy to the twin's policy set, and to the
// policies of the network resource it targets — mirroring the account path,
// where the resource-policy map was built after injection.
func (nmd *NetworkMapData) addInjectedPolicy(policy *nmdata.Policy) {
nmd.Policies = append(nmd.Policies, policy)
resourceID := policy.Rules[0].DestinationResource.ID
if resourceID == "" {
return
}
for _, resource := range nmd.NetworkResources {
if resource == nil || !resource.Enabled || resource.ID != resourceID {
continue
}
if nmd.ResourcePolicies == nil {
nmd.ResourcePolicies = make(map[string][]*nmdata.Policy)
}
nmd.ResourcePolicies[resourceID] = append(nmd.ResourcePolicies[resourceID], policy)
return
}
}
func proxyAccessPolicy(svc *nmdata.Service, target *nmdata.ServiceTarget, proxyPeer *nmdata.Peer, port uint16) *nmdata.Policy {
policyID := fmt.Sprintf("proxy-access-%s-%s-%s", svc.ID, proxyPeer.ID, target.Path)
protocol := types.PolicyRuleProtocolTCP
if svc.Mode == serviceModeUDP {
protocol = types.PolicyRuleProtocolUDP
}
return &nmdata.Policy{
ID: policyID,
Enabled: true,
Rules: []*nmdata.PolicyRule{
{
ID: policyID,
PolicyID: policyID,
Enabled: true,
SourceResource: nmdata.Resource{ID: proxyPeer.ID, Type: string(types.ResourceTypePeer)},
DestinationResource: nmdata.Resource{ID: target.TargetID, Type: target.TargetType},
Bidirectional: false,
Protocol: string(protocol),
Action: string(types.PolicyTrafficActionAccept),
PortRanges: []nmdata.RulePortRange{{Start: port, End: port}},
},
},
}
}
func privateAccessPolicy(svc *nmdata.Service, proxyPeer *nmdata.Peer, accessGroups []string) *nmdata.Policy {
policyID := fmt.Sprintf("private-access-%s-%s", svc.ID, proxyPeer.ID)
return &nmdata.Policy{
ID: policyID,
Enabled: true,
Rules: []*nmdata.PolicyRule{
{
ID: policyID,
PolicyID: policyID,
Enabled: true,
Sources: slices.Clone(accessGroups),
DestinationResource: nmdata.Resource{ID: proxyPeer.ID, Type: string(types.ResourceTypePeer)},
Bidirectional: false,
Protocol: string(types.PolicyRuleProtocolTCP),
Action: string(types.PolicyTrafficActionAccept),
PortRanges: []nmdata.RulePortRange{
{Start: privateServicePortHTTP, End: privateServicePortHTTP},
{Start: privateServicePortHTTPS, End: privateServicePortHTTPS},
},
},
},
}
}
func resolveTargetPort(target *nmdata.ServiceTarget) (uint16, bool) {
if target.Port != 0 {
return target.Port, true
}
switch target.Protocol {
case "https", "tls":
return privateServicePortHTTPS, true
case "http":
return privateServicePortHTTP, true
default:
return 0, false
}
}
// proxyPeersByCluster groups the account's embedded proxy peers by the cluster
// they serve. Sorted by peer ID so the synthesised policy order is stable.
func (nmd *NetworkMapData) proxyPeersByCluster() map[string][]*nmdata.Peer {
var proxyPeers map[string][]*nmdata.Peer
for _, peer := range nmd.Peers {
if peer == nil || !peer.ProxyMeta.Embedded {
continue
}
if proxyPeers == nil {
proxyPeers = make(map[string][]*nmdata.Peer)
}
proxyPeers[peer.ProxyMeta.Cluster] = append(proxyPeers[peer.ProxyMeta.Cluster], peer)
}
for _, peers := range proxyPeers {
slices.SortFunc(peers, func(a, b *nmdata.Peer) int { return strings.Compare(a.ID, b.ID) })
}
return proxyPeers
}
// existingGroupIDs returns the subset of groupIDs that resolve to a group,
// preserving the input order.
func (nmd *NetworkMapData) existingGroupIDs(groupIDs []string) []string {
out := make([]string, 0, len(groupIDs))
for _, groupID := range groupIDs {
if _, ok := nmd.Groups[groupID]; ok {
out = append(out, groupID)
}
}
return out
}