mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-16 19:59:07 +02:00
inject proxy policies on nmdata path
This commit is contained in:
@@ -131,12 +131,12 @@ func (c *Controller) OnPeerDisconnected(ctx context.Context, accountID string, p
|
|||||||
|
|
||||||
// injectAllProxyPolicies prepares an account for the per-peer network-map
|
// injectAllProxyPolicies prepares an account for the per-peer network-map
|
||||||
// computation. It prepends the in-memory agent-network services synthesised
|
// computation. It prepends the in-memory agent-network services synthesised
|
||||||
// from the account's current provider/policy state to account.Services so
|
// from the account's current provider/policy state to account.Services, so the
|
||||||
// the existing InjectProxyPolicies + injectPrivateServicePolicies walks pick
|
// twin store built from the account carries them alongside the persisted
|
||||||
// them up alongside persisted reverse-proxy services. Synthesised services
|
// reverse-proxy services and synthesises their ACLs. Synthesised services are
|
||||||
// are never persisted; the account is loaded fresh per cycle so re-prepending
|
// never persisted; the account is loaded fresh per cycle so re-prepending is
|
||||||
// is safe and idempotent. Accounts without agent-network providers get an
|
// safe and idempotent. Accounts without agent-network providers get an empty
|
||||||
// empty synth slice — no behaviour change.
|
// synth slice — no behaviour change.
|
||||||
func (c *Controller) injectAllProxyPolicies(ctx context.Context, account *types.Account) {
|
func (c *Controller) injectAllProxyPolicies(ctx context.Context, account *types.Account) {
|
||||||
synth, err := c.repo.SynthesizeAgentNetworkServices(ctx, account.Id)
|
synth, err := c.repo.SynthesizeAgentNetworkServices(ctx, account.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -144,7 +144,26 @@ func (c *Controller) injectAllProxyPolicies(ctx context.Context, account *types.
|
|||||||
} else if len(synth) > 0 {
|
} else if len(synth) > 0 {
|
||||||
account.Services = append(synth, account.Services...)
|
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 {
|
func (c *Controller) CountStreams() int {
|
||||||
@@ -467,6 +486,8 @@ func (c *Controller) getNetworkMapData(ctx context.Context, accountID string) *n
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nmData.Services = c.proxyServicesFromRepo(ctx, accountID)
|
||||||
|
|
||||||
return nmData
|
return nmData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"context"
|
"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/reverseproxy/service"
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
"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/peer"
|
||||||
"github.com/netbirdio/netbird/management/server/store"
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
@@ -22,6 +22,7 @@ type Repository interface {
|
|||||||
// services synthesised from the account's agent-network provider/policy
|
// services synthesised from the account's agent-network provider/policy
|
||||||
// state. Empty for accounts without agent-network providers.
|
// state. Empty for accounts without agent-network providers.
|
||||||
SynthesizeAgentNetworkServices(ctx context.Context, accountID string) ([]*service.Service, error)
|
SynthesizeAgentNetworkServices(ctx context.Context, accountID string) ([]*service.Service, error)
|
||||||
|
GetAccountServices(ctx context.Context, accountID string) ([]*service.Service, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type repository struct {
|
type repository struct {
|
||||||
@@ -60,6 +61,10 @@ func (r *repository) SynthesizeAgentNetworkServices(ctx context.Context, account
|
|||||||
return agentnetwork.SynthesizeServices(ctx, r.store, accountID)
|
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) {
|
func (r *repository) GetAccountZones(ctx context.Context, accountID string) ([]*zones.Zone, error) {
|
||||||
return r.store.GetAccountZones(ctx, store.LockingStrengthNone, accountID)
|
return r.store.GetAccountZones(ctx, store.LockingStrengthNone, accountID)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
+44
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+7
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
+63
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+7
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
+26
@@ -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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -294,6 +294,7 @@ func ConvertToNmdataPeers(peers []Peer) ([]nmdata.Peer, map[string][]*nmdata.Pee
|
|||||||
if p.ProxyMetaEmbedded.Valid {
|
if p.ProxyMetaEmbedded.Valid {
|
||||||
dp.ProxyMeta.Embedded = p.ProxyMetaEmbedded.Bool
|
dp.ProxyMeta.Embedded = p.ProxyMetaEmbedded.Bool
|
||||||
}
|
}
|
||||||
|
dp.ProxyMeta.Cluster = p.ProxyMetaCluster.String
|
||||||
// This is only used to build private service candidates, not connected peers are skipped
|
// This is only used to build private service candidates, not connected peers are skipped
|
||||||
if dp.ProxyMeta.Embedded && p.PeerStatusConnected.Bool {
|
if dp.ProxyMeta.Embedded && p.PeerStatusConnected.Bool {
|
||||||
clusterToPeerIdx[p.ProxyMetaCluster.String] = append(clusterToPeerIdx[p.ProxyMetaCluster.String], &dp)
|
clusterToPeerIdx[p.ProxyMetaCluster.String] = append(clusterToPeerIdx[p.ProxyMetaCluster.String], &dp)
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ func allPeerMaps(t *testing.T, manager *DefaultAccountManager, accountID string)
|
|||||||
account, err := manager.Store.GetAccount(ctx, accountID)
|
account, err := manager.Store.GetAccount(ctx, accountID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
account.InjectProxyPolicies(ctx)
|
|
||||||
|
|
||||||
validated := make(map[string]struct{}, len(account.Peers))
|
validated := make(map[string]struct{}, len(account.Peers))
|
||||||
for id := range account.Peers {
|
for id := range account.Peers {
|
||||||
validated[id] = struct{}{}
|
validated[id] = struct{}{}
|
||||||
|
|||||||
@@ -1554,176 +1554,6 @@ func (a *Account) GetProxyPeers() map[string][]*nbpeer.Peer {
|
|||||||
return proxyPeers
|
return proxyPeers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Account) InjectProxyPolicies(ctx context.Context) {
|
|
||||||
if len(a.Services) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyPeersByCluster := a.GetProxyPeers()
|
|
||||||
if len(proxyPeersByCluster) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, service := range a.Services {
|
|
||||||
if !service.Enabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
a.injectServiceProxyPolicies(ctx, service, proxyPeersByCluster)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) injectServiceProxyPolicies(ctx context.Context, service *service.Service, proxyPeersByCluster map[string][]*nbpeer.Peer) {
|
|
||||||
proxyPeers := proxyPeersByCluster[service.ProxyCluster]
|
|
||||||
for _, target := range service.Targets {
|
|
||||||
if !target.Enabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
a.injectTargetProxyPolicies(ctx, service, target, proxyPeers)
|
|
||||||
}
|
|
||||||
|
|
||||||
a.injectPrivateServicePolicies(service, proxyPeers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// injectPrivateServicePolicies synthesises an in-memory ACL: AccessGroups → cluster proxy peers on TCP 80/443.
|
|
||||||
func (a *Account) injectPrivateServicePolicies(svc *service.Service, proxyPeers []*nbpeer.Peer) {
|
|
||||||
if !svc.Private {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(svc.AccessGroups) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if 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 := a.existingGroupIDs(svc.AccessGroups)
|
|
||||||
if len(sources) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, proxyPeer := range proxyPeers {
|
|
||||||
a.Policies = append(a.Policies, a.createPrivateServicePolicy(svc, proxyPeer, sources))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// existingGroupIDs returns the subset of groupIDs that resolve to a group in the account,
|
|
||||||
// preserving the input order.
|
|
||||||
func (a *Account) existingGroupIDs(groupIDs []string) []string {
|
|
||||||
out := make([]string, 0, len(groupIDs))
|
|
||||||
for _, groupID := range groupIDs {
|
|
||||||
if _, ok := a.Groups[groupID]; ok {
|
|
||||||
out = append(out, groupID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) createPrivateServicePolicy(svc *service.Service, proxyPeer *nbpeer.Peer, accessGroups []string) *Policy {
|
|
||||||
policyID := fmt.Sprintf("private-access-%s-%s", svc.ID, proxyPeer.ID)
|
|
||||||
sources := append([]string(nil), accessGroups...)
|
|
||||||
return &Policy{
|
|
||||||
ID: policyID,
|
|
||||||
Name: fmt.Sprintf("Private Access to %s", svc.Name),
|
|
||||||
Enabled: true,
|
|
||||||
Rules: []*PolicyRule{
|
|
||||||
{
|
|
||||||
ID: policyID,
|
|
||||||
PolicyID: policyID,
|
|
||||||
Name: fmt.Sprintf("Allow access groups to reach %s", svc.Name),
|
|
||||||
Enabled: true,
|
|
||||||
Sources: sources,
|
|
||||||
DestinationResource: Resource{
|
|
||||||
ID: proxyPeer.ID,
|
|
||||||
Type: ResourceTypePeer,
|
|
||||||
},
|
|
||||||
Bidirectional: false,
|
|
||||||
Protocol: PolicyRuleProtocolTCP,
|
|
||||||
Action: PolicyTrafficActionAccept,
|
|
||||||
PortRanges: []RulePortRange{
|
|
||||||
{Start: 80, End: 80},
|
|
||||||
{Start: 443, End: 443},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) injectTargetProxyPolicies(ctx context.Context, service *service.Service, target *service.Target, proxyPeers []*nbpeer.Peer) {
|
|
||||||
port, ok := a.resolveTargetPort(ctx, target)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
path := ""
|
|
||||||
if target.Path != nil {
|
|
||||||
path = *target.Path
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, proxyPeer := range proxyPeers {
|
|
||||||
policy := a.createProxyPolicy(service, target, proxyPeer, port, path)
|
|
||||||
a.Policies = append(a.Policies, policy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) resolveTargetPort(ctx context.Context, target *service.Target) (uint16, bool) {
|
|
||||||
if target.Port != 0 {
|
|
||||||
return target.Port, true
|
|
||||||
}
|
|
||||||
|
|
||||||
switch target.Protocol {
|
|
||||||
case "https", "tls":
|
|
||||||
return 443, true
|
|
||||||
case "http":
|
|
||||||
return 80, true
|
|
||||||
default:
|
|
||||||
log.WithContext(ctx).Warnf("unsupported protocol %s for proxy target %s, skipping policy injection", target.Protocol, target.TargetId)
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) createProxyPolicy(svc *service.Service, target *service.Target, proxyPeer *nbpeer.Peer, port uint16, path string) *Policy {
|
|
||||||
policyID := fmt.Sprintf("proxy-access-%s-%s-%s", svc.ID, proxyPeer.ID, path)
|
|
||||||
|
|
||||||
protocol := PolicyRuleProtocolTCP
|
|
||||||
if svc.Mode == service.ModeUDP {
|
|
||||||
protocol = PolicyRuleProtocolUDP
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Policy{
|
|
||||||
ID: policyID,
|
|
||||||
Name: fmt.Sprintf("Proxy Access to %s", svc.Name),
|
|
||||||
Enabled: true,
|
|
||||||
Rules: []*PolicyRule{
|
|
||||||
{
|
|
||||||
ID: policyID,
|
|
||||||
PolicyID: policyID,
|
|
||||||
Name: fmt.Sprintf("Allow access to %s", svc.Name),
|
|
||||||
Enabled: true,
|
|
||||||
SourceResource: Resource{
|
|
||||||
ID: proxyPeer.ID,
|
|
||||||
Type: ResourceTypePeer,
|
|
||||||
},
|
|
||||||
DestinationResource: Resource{
|
|
||||||
ID: target.TargetId,
|
|
||||||
Type: ResourceType(target.TargetType),
|
|
||||||
},
|
|
||||||
Bidirectional: false,
|
|
||||||
Protocol: protocol,
|
|
||||||
Action: PolicyTrafficActionAccept,
|
|
||||||
PortRanges: []RulePortRange{
|
|
||||||
{
|
|
||||||
Start: port,
|
|
||||||
End: port,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterZoneRecordsForPeers filters DNS records to only include peers to connect.
|
// filterZoneRecordsForPeers filters DNS records to only include peers to connect.
|
||||||
// AAAA records are excluded when the requesting peer lacks IPv6 capability.
|
// AAAA records are excluded when the requesting peer lacks IPv6 capability.
|
||||||
func filterZoneRecordsForPeers(peer *nbpeer.Peer, customZone nbdns.CustomZone, peersToConnect, expiredPeers []*nbpeer.Peer) []nbdns.SimpleRecord {
|
func filterZoneRecordsForPeers(peer *nbpeer.Peer, customZone nbdns.CustomZone, peersToConnect, expiredPeers []*nbpeer.Peer) []nbdns.SimpleRecord {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/zones/records"
|
"github.com/netbirdio/netbird/management/internals/modules/zones/records"
|
||||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||||
@@ -112,10 +113,54 @@ func (a *Account) toNetworkMapData(
|
|||||||
nmd.ProxyTargetedDomainResourceIDs = a.proxyTargetedDomainResourceIDs()
|
nmd.ProxyTargetedDomainResourceIDs = a.proxyTargetedDomainResourceIDs()
|
||||||
nmd.AppliedZoneCandidates = buildAppliedZoneCandidates(accountZones)
|
nmd.AppliedZoneCandidates = buildAppliedZoneCandidates(accountZones)
|
||||||
nmd.PrivateServiceCandidates = a.buildPrivateServiceCandidates()
|
nmd.PrivateServiceCandidates = a.buildPrivateServiceCandidates()
|
||||||
|
nmd.Services = TwinServices(a.Services)
|
||||||
|
|
||||||
return nmd
|
return nmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TwinServices converts reverse-proxy services to their slim nmdata twins.
|
||||||
|
// Exported for the network-map controller, which hands the store-backed twin
|
||||||
|
// the same services the account carries.
|
||||||
|
func TwinServices(services []*service.Service) []*nmdata.Service {
|
||||||
|
if len(services) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*nmdata.Service, 0, len(services))
|
||||||
|
for _, svc := range services {
|
||||||
|
if svc == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targets := make([]*nmdata.ServiceTarget, 0, len(svc.Targets))
|
||||||
|
for _, t := range svc.Targets {
|
||||||
|
if t == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := ""
|
||||||
|
if t.Path != nil {
|
||||||
|
path = *t.Path
|
||||||
|
}
|
||||||
|
targets = append(targets, &nmdata.ServiceTarget{
|
||||||
|
Enabled: t.Enabled,
|
||||||
|
Path: path,
|
||||||
|
Port: t.Port,
|
||||||
|
Protocol: t.Protocol,
|
||||||
|
TargetID: t.TargetId,
|
||||||
|
TargetType: string(t.TargetType),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
out = append(out, &nmdata.Service{
|
||||||
|
ID: svc.ID,
|
||||||
|
Enabled: svc.Enabled,
|
||||||
|
Private: svc.Private,
|
||||||
|
Mode: svc.Mode,
|
||||||
|
ProxyCluster: svc.ProxyCluster,
|
||||||
|
AccessGroups: svc.AccessGroups,
|
||||||
|
Targets: targets,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func twinPeer(p *nbpeer.Peer) *nmdata.Peer {
|
func twinPeer(p *nbpeer.Peer) *nmdata.Peer {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -141,7 +186,7 @@ func twinPeer(p *nbpeer.Peer) *nmdata.Peer {
|
|||||||
IPv6: p.IPv6,
|
IPv6: p.IPv6,
|
||||||
RequiresApproval: p.Status != nil && p.Status.RequiresApproval,
|
RequiresApproval: p.Status != nil && p.Status.RequiresApproval,
|
||||||
ExtraDNSLabels: p.ExtraDNSLabels,
|
ExtraDNSLabels: p.ExtraDNSLabels,
|
||||||
ProxyMeta: nmdata.ProxyMeta{Embedded: p.ProxyMeta.Embedded},
|
ProxyMeta: nmdata.ProxyMeta{Embedded: p.ProxyMeta.Embedded, Cluster: p.ProxyMeta.Cluster},
|
||||||
Meta: nmdata.PeerSystemMeta{
|
Meta: nmdata.PeerSystemMeta{
|
||||||
WtVersion: p.Meta.WtVersion,
|
WtVersion: p.Meta.WtVersion,
|
||||||
GoOS: p.Meta.GoOS,
|
GoOS: p.Meta.GoOS,
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ func TestPrivateService_NetworkMap_UserPeer_AndProxyPeer(t *testing.T) {
|
|||||||
account.Peers["proxy-peer"].Meta.WtVersion = "0.50.0"
|
account.Peers["proxy-peer"].Meta.WtVersion = "0.50.0"
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
account.InjectProxyPolicies(ctx)
|
|
||||||
|
|
||||||
validated := map[string]struct{}{
|
validated := map[string]struct{}{
|
||||||
"user-peer": {},
|
"user-peer": {},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
@@ -1051,6 +1052,7 @@ func TestInjectPrivateServicePolicies_ProxyPeerGetsInboundRule(t *testing.T) {
|
|||||||
Identifier: "net-1",
|
Identifier: "net-1",
|
||||||
Net: net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.CIDRMask(10, 32)},
|
Net: net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.CIDRMask(10, 32)},
|
||||||
},
|
},
|
||||||
|
Settings: &Settings{},
|
||||||
Peers: map[string]*nbpeer.Peer{
|
Peers: map[string]*nbpeer.Peer{
|
||||||
"user-peer": {
|
"user-peer": {
|
||||||
ID: "user-peer",
|
ID: "user-peer",
|
||||||
@@ -1101,41 +1103,25 @@ func TestInjectPrivateServicePolicies_ProxyPeerGetsInboundRule(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
account.InjectProxyPolicies(ctx)
|
found := findPolicy(injectedPolicies(account), "private-access-svc-1-proxy-peer")
|
||||||
|
require.NotNil(t, found, "expected synthesised private-access policy in the twin store")
|
||||||
var found *Policy
|
|
||||||
for _, p := range account.Policies {
|
|
||||||
if p != nil && p.ID == "private-access-svc-1-proxy-peer" {
|
|
||||||
found = p
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
require.NotNil(t, found, "expected synthesised private-access policy in account.Policies")
|
|
||||||
require.Len(t, found.Rules, 1, "policy should have exactly one rule")
|
require.Len(t, found.Rules, 1, "policy should have exactly one rule")
|
||||||
rule := found.Rules[0]
|
rule := found.Rules[0]
|
||||||
assert.Equal(t, []string{"grp-admins"}, rule.Sources, "sources should be group IDs verbatim")
|
assert.Equal(t, []string{"grp-admins"}, rule.Sources, "sources should be group IDs verbatim")
|
||||||
assert.Equal(t, "proxy-peer", rule.DestinationResource.ID, "destination resource should be the proxy peer ID")
|
assert.Equal(t, "proxy-peer", rule.DestinationResource.ID, "destination resource should be the proxy peer ID")
|
||||||
assert.Equal(t, ResourceTypePeer, rule.DestinationResource.Type, "destination resource type should be peer")
|
assert.Equal(t, string(ResourceTypePeer), rule.DestinationResource.Type, "destination resource type should be peer")
|
||||||
|
|
||||||
validatedPeersMap := map[string]struct{}{
|
validatedPeersMap := map[string]struct{}{
|
||||||
"user-peer": {},
|
"user-peer": {},
|
||||||
"proxy-peer": {},
|
"proxy-peer": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyPeer := account.Peers["proxy-peer"]
|
nm := account.GetPeerNetworkMapFromComponents(ctx, "proxy-peer", nbdns.CustomZone{}, nil, validatedPeersMap, nil, nil, nil, nil)
|
||||||
aclPeers, firewallRules, _, _ := account.GetPeerConnectionResources(ctx, proxyPeer, validatedPeersMap, nil)
|
|
||||||
|
|
||||||
var sawUserAsAclPeer bool
|
assert.Contains(t, netmapPeerIDs(nm.Peers), "user-peer", "proxy peer should see the user peer as an ACL peer")
|
||||||
for _, p := range aclPeers {
|
|
||||||
if p.ID == "user-peer" {
|
|
||||||
sawUserAsAclPeer = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.True(t, sawUserAsAclPeer, "proxy peer should see the user peer as an ACL peer")
|
|
||||||
|
|
||||||
var inboundRules []*FirewallRule
|
var inboundRules []*FirewallRule
|
||||||
for _, r := range firewallRules {
|
for _, r := range nm.FirewallRules {
|
||||||
if r.Direction == FirewallRuleDirectionIN && r.PeerIP == userPeerIP.String() {
|
if r.Direction == FirewallRuleDirectionIN && r.PeerIP == userPeerIP.String() {
|
||||||
inboundRules = append(inboundRules, r)
|
inboundRules = append(inboundRules, r)
|
||||||
}
|
}
|
||||||
@@ -1144,29 +1130,23 @@ func TestInjectPrivateServicePolicies_ProxyPeerGetsInboundRule(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInjectPrivateServicePolicies_NotPrivate_NoPolicy(t *testing.T) {
|
func TestInjectPrivateServicePolicies_NotPrivate_NoPolicy(t *testing.T) {
|
||||||
ctx := context.Background()
|
|
||||||
account := privateServiceTestAccount(t)
|
account := privateServiceTestAccount(t)
|
||||||
account.Services[0].Private = false
|
account.Services[0].Private = false
|
||||||
|
|
||||||
account.InjectProxyPolicies(ctx)
|
|
||||||
assert.False(t, hasPrivateAccessPolicy(account, "svc-1"), "non-private service must not synthesise an access policy")
|
assert.False(t, hasPrivateAccessPolicy(account, "svc-1"), "non-private service must not synthesise an access policy")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInjectPrivateServicePolicies_EmptyAccessGroups_NoPolicy(t *testing.T) {
|
func TestInjectPrivateServicePolicies_EmptyAccessGroups_NoPolicy(t *testing.T) {
|
||||||
ctx := context.Background()
|
|
||||||
account := privateServiceTestAccount(t)
|
account := privateServiceTestAccount(t)
|
||||||
account.Services[0].AccessGroups = nil
|
account.Services[0].AccessGroups = nil
|
||||||
|
|
||||||
account.InjectProxyPolicies(ctx)
|
|
||||||
assert.False(t, hasPrivateAccessPolicy(account, "svc-1"), "private service with no access groups must not synthesise a policy")
|
assert.False(t, hasPrivateAccessPolicy(account, "svc-1"), "private service with no access groups must not synthesise a policy")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInjectPrivateServicePolicies_NoProxyPeers_NoPolicy(t *testing.T) {
|
func TestInjectPrivateServicePolicies_NoProxyPeers_NoPolicy(t *testing.T) {
|
||||||
ctx := context.Background()
|
|
||||||
account := privateServiceTestAccount(t)
|
account := privateServiceTestAccount(t)
|
||||||
delete(account.Peers, "proxy-peer")
|
delete(account.Peers, "proxy-peer")
|
||||||
|
|
||||||
account.InjectProxyPolicies(ctx)
|
|
||||||
assert.False(t, hasPrivateAccessPolicy(account, "svc-1"), "policy must not synthesise when the cluster has no proxy peers")
|
assert.False(t, hasPrivateAccessPolicy(account, "svc-1"), "policy must not synthesise when the cluster has no proxy peers")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1229,10 +1209,27 @@ func privateServiceTestAccount(t *testing.T) *Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// injectedPolicies returns the twin's policies with the synthesised proxy ACLs
|
||||||
|
// already in place, the way the per-peer computation sees them.
|
||||||
|
func injectedPolicies(account *Account) []*nmdata.Policy {
|
||||||
|
nmd := account.toNetworkMapData(nil, nil, nil, nil, nil)
|
||||||
|
nmd.InjectProxyPolicies()
|
||||||
|
return nmd.Policies
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPolicy(policies []*nmdata.Policy, id string) *nmdata.Policy {
|
||||||
|
for _, p := range policies {
|
||||||
|
if p != nil && p.ID == id {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func hasPrivateAccessPolicy(account *Account, serviceID string) bool {
|
func hasPrivateAccessPolicy(account *Account, serviceID string) bool {
|
||||||
prefix := "private-access-" + serviceID + "-"
|
prefix := "private-access-" + serviceID + "-"
|
||||||
for _, p := range account.Policies {
|
for _, p := range injectedPolicies(account) {
|
||||||
if p != nil && len(p.ID) > len(prefix) && p.ID[:len(prefix)] == prefix {
|
if p != nil && strings.HasPrefix(p.ID, prefix) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ type sshRequirements struct {
|
|||||||
// exactly, operating on nmdata twins throughout — no Account reference and no
|
// exactly, operating on nmdata twins throughout — no Account reference and no
|
||||||
// twin↔real conversion, since the produced components hold twins.
|
// twin↔real conversion, since the produced components hold twins.
|
||||||
func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCustomZone nmdata.CustomZone) *types.NetworkMapComponents {
|
func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCustomZone nmdata.CustomZone) *types.NetworkMapComponents {
|
||||||
|
nmd.InjectProxyPolicies()
|
||||||
|
|
||||||
forceRoutingPeerDNS := nmd.forcesRoutingPeerDNSResolution(peerID)
|
forceRoutingPeerDNS := nmd.forcesRoutingPeerDNSResolution(peerID)
|
||||||
|
|
||||||
peer := nmd.Peers[peerID]
|
peer := nmd.Peers[peerID]
|
||||||
|
|||||||
@@ -48,8 +48,16 @@ type NetworkMapData struct { //nolint:revive // established name across the code
|
|||||||
AppliedZoneCandidates []AppliedZoneCandidate
|
AppliedZoneCandidates []AppliedZoneCandidate
|
||||||
PrivateServiceCandidates []PrivateServiceCandidate
|
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
|
peerGroupsOnce sync.Once
|
||||||
peerGroupsIdx map[string]map[string]struct{}
|
peerGroupsIdx map[string]map[string]struct{}
|
||||||
|
|
||||||
|
proxyPoliciesOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppliedZoneCandidate is an account-level custom DNS zone reduced to the
|
// AppliedZoneCandidate is an account-level custom DNS zone reduced to the
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type Peer struct {
|
|||||||
// ProxyMeta is the slim twin of peer.ProxyMeta.
|
// ProxyMeta is the slim twin of peer.ProxyMeta.
|
||||||
type ProxyMeta struct {
|
type ProxyMeta struct {
|
||||||
Embedded bool
|
Embedded bool
|
||||||
|
Cluster string
|
||||||
}
|
}
|
||||||
|
|
||||||
// PeerSystemMeta is the slim twin of peer.PeerSystemMeta.
|
// PeerSystemMeta is the slim twin of peer.PeerSystemMeta.
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user