Add peer firewall to the receiving peer

This commit is contained in:
Viktor Liu
2026-02-08 16:19:35 +08:00
parent 07e59b2708
commit 7c647dd160
7 changed files with 475 additions and 39 deletions

View File

@@ -310,10 +310,10 @@ func (a *Account) GetPeerNetworkMap(
var authorizedUsers map[string]map[string]struct{}
var enableSSH bool
if peer.ProxyEmbedded {
aclPeers, firewallRules = a.GetProxyConnectionResources(ctx, exposedServices)
aclPeers = a.GetProxyConnectionResources(ctx, exposedServices)
} else {
aclPeers, firewallRules, authorizedUsers, enableSSH = a.GetPeerConnectionResources(ctx, peer, validatedPeersMap, groupIDToUserIDs)
proxyAclPeers, proxyFirewallRules := a.GetPeerProxyResources(exposedServices[peerID], proxyPeers)
proxyAclPeers, proxyFirewallRules := a.GetPeerProxyResources(peerID, exposedServices[peerID], proxyPeers)
aclPeers = append(aclPeers, proxyAclPeers...)
firewallRules = append(firewallRules, proxyFirewallRules...)
}
@@ -408,9 +408,11 @@ func (a *Account) GetPeerNetworkMap(
return nm
}
func (a *Account) GetProxyConnectionResources(ctx context.Context, exposedServices map[string][]*reverseproxy.ReverseProxy) ([]*nbpeer.Peer, []*FirewallRule) {
// GetProxyConnectionResources returns ACL peers for the proxy-embedded peer based on exposed services.
// No firewall rules are generated here; the proxy peer is always a new on-demand client with a stateful
// firewall, so OUT rules are unnecessary. Inbound rules are handled on the target/router peer side.
func (a *Account) GetProxyConnectionResources(ctx context.Context, exposedServices map[string][]*reverseproxy.ReverseProxy) []*nbpeer.Peer {
var aclPeers []*nbpeer.Peer
var firewallRules []*FirewallRule
for _, peerServices := range exposedServices {
for _, service := range peerServices {
@@ -427,23 +429,18 @@ func (a *Account) GetProxyConnectionResources(ctx context.Context, exposedServic
continue
}
aclPeers = append(aclPeers, tpeer)
firewallRules = append(firewallRules, &FirewallRule{
PolicyID: "proxy-" + service.ID,
PeerIP: tpeer.IP.String(),
Direction: FirewallRuleDirectionOUT,
Action: "allow",
Protocol: string(PolicyRuleProtocolTCP),
PortRange: RulePortRange{Start: uint16(target.Port), End: uint16(target.Port)},
})
}
}
}
}
return aclPeers, firewallRules
return aclPeers
}
func (a *Account) GetPeerProxyResources(services []*reverseproxy.ReverseProxy, proxyPeers []*nbpeer.Peer) ([]*nbpeer.Peer, []*FirewallRule) {
// GetPeerProxyResources returns ACL peers and inbound firewall rules for a peer that is targeted by reverse proxy services.
// Only IN rules are generated; OUT rules are omitted since proxy peers are always new clients with stateful firewalls.
// Rules use PortRange only (not the legacy Port field) as this feature only targets current peer versions.
func (a *Account) GetPeerProxyResources(peerID string, services []*reverseproxy.ReverseProxy, proxyPeers []*nbpeer.Peer) ([]*nbpeer.Peer, []*FirewallRule) {
var aclPeers []*nbpeer.Peer
var firewallRules []*FirewallRule
@@ -455,7 +452,24 @@ func (a *Account) GetPeerProxyResources(services []*reverseproxy.ReverseProxy, p
if !target.Enabled {
continue
}
aclPeers = proxyPeers
needsPeerRules := (target.TargetType == reverseproxy.TargetTypePeer && target.TargetId == peerID) ||
(target.TargetType == reverseproxy.TargetTypeResource && target.AccessLocal)
if needsPeerRules {
for _, proxyPeer := range proxyPeers {
firewallRules = append(firewallRules, &FirewallRule{
PolicyID: "proxy-" + service.ID,
PeerIP: proxyPeer.IP.String(),
Direction: FirewallRuleDirectionIN,
Action: "allow",
Protocol: string(PolicyRuleProtocolTCP),
PortRange: RulePortRange{Start: uint16(target.Port), End: uint16(target.Port)},
})
}
}
}
}
@@ -1914,7 +1928,7 @@ func (a *Account) GetPeerProxyRoutes(ctx context.Context, peer *nbpeer.Peer, pro
for _, proxyPerResource := range proxies {
for _, proxy := range proxyPerResource {
for _, target := range proxy.Targets {
if target.TargetType == reverseproxy.TargetTypeResource {
if target.TargetType == reverseproxy.TargetTypeResource && !target.AccessLocal {
resource, ok := resourcesMap[target.TargetId]
if !ok {
log.WithContext(ctx).Warnf("proxy target %s not found in resources map", target.TargetId)

View File

@@ -70,7 +70,7 @@ func TestGetPeerNetworkMap_Golden(t *testing.T) {
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
legacyNetworkMap := account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, resourcePolicies, routers, nil, account.GetActiveGroupUsers())
legacyNetworkMap := account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, resourcePolicies, routers, nil, nil, account.GetActiveGroupUsers(), nil, nil)
normalizeAndSortNetworkMap(legacyNetworkMap)
legacyJSON, err := json.MarshalIndent(toNetworkMapJSON(legacyNetworkMap), "", " ")
require.NoError(t, err, "error marshaling legacy network map to JSON")
@@ -115,7 +115,7 @@ func BenchmarkGetPeerNetworkMap(b *testing.B) {
b.Run("old builder", func(b *testing.B) {
for range b.N {
for _, peerID := range peerIDs {
_ = account.GetPeerNetworkMap(ctx, peerID, dns.CustomZone{}, nil, validatedPeersMap, nil, nil, nil, account.GetActiveGroupUsers())
_ = account.GetPeerNetworkMap(ctx, peerID, dns.CustomZone{}, nil, validatedPeersMap, nil, nil, nil, nil, account.GetActiveGroupUsers(), nil, nil)
}
}
})
@@ -177,7 +177,7 @@ func TestGetPeerNetworkMap_Golden_WithNewPeer(t *testing.T) {
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
legacyNetworkMap := account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, resourcePolicies, routers, nil, account.GetActiveGroupUsers())
legacyNetworkMap := account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, resourcePolicies, routers, nil, nil, account.GetActiveGroupUsers(), nil, nil)
normalizeAndSortNetworkMap(legacyNetworkMap)
legacyJSON, err := json.MarshalIndent(toNetworkMapJSON(legacyNetworkMap), "", " ")
require.NoError(t, err, "error marshaling legacy network map to JSON")
@@ -240,7 +240,7 @@ func BenchmarkGetPeerNetworkMap_AfterPeerAdded(b *testing.B) {
b.Run("old builder after add", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, testingPeerID := range peerIDs {
_ = account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, nil, nil, nil, account.GetActiveGroupUsers())
_ = account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, nil, nil, nil, nil, account.GetActiveGroupUsers(), nil, nil)
}
}
})
@@ -317,7 +317,7 @@ func TestGetPeerNetworkMap_Golden_WithNewRoutingPeer(t *testing.T) {
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
legacyNetworkMap := account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, resourcePolicies, routers, nil, account.GetActiveGroupUsers())
legacyNetworkMap := account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, resourcePolicies, routers, nil, nil, account.GetActiveGroupUsers(), nil, nil)
normalizeAndSortNetworkMap(legacyNetworkMap)
legacyJSON, err := json.MarshalIndent(toNetworkMapJSON(legacyNetworkMap), "", " ")
require.NoError(t, err, "error marshaling legacy network map to JSON")
@@ -402,7 +402,7 @@ func BenchmarkGetPeerNetworkMap_AfterRouterPeerAdded(b *testing.B) {
b.Run("old builder after add", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, testingPeerID := range peerIDs {
_ = account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, nil, nil, nil, account.GetActiveGroupUsers())
_ = account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, nil, nil, nil, nil, account.GetActiveGroupUsers(), nil, nil)
}
}
})
@@ -458,7 +458,7 @@ func TestGetPeerNetworkMap_Golden_WithDeletedPeer(t *testing.T) {
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
legacyNetworkMap := account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, resourcePolicies, routers, nil, account.GetActiveGroupUsers())
legacyNetworkMap := account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, resourcePolicies, routers, nil, nil, account.GetActiveGroupUsers(), nil, nil)
normalizeAndSortNetworkMap(legacyNetworkMap)
legacyJSON, err := json.MarshalIndent(toNetworkMapJSON(legacyNetworkMap), "", " ")
require.NoError(t, err, "error marshaling legacy network map to JSON")
@@ -537,7 +537,7 @@ func TestGetPeerNetworkMap_Golden_WithDeletedRouterPeer(t *testing.T) {
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
legacyNetworkMap := account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, resourcePolicies, routers, nil, account.GetActiveGroupUsers())
legacyNetworkMap := account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, resourcePolicies, routers, nil, nil, account.GetActiveGroupUsers(), nil, nil)
normalizeAndSortNetworkMap(legacyNetworkMap)
legacyJSON, err := json.MarshalIndent(toNetworkMapJSON(legacyNetworkMap), "", " ")
require.NoError(t, err, "error marshaling legacy network map to JSON")
@@ -597,7 +597,7 @@ func BenchmarkGetPeerNetworkMap_AfterPeerDeleted(b *testing.B) {
b.Run("old builder after delete", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, testingPeerID := range peerIDs {
_ = account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, nil, nil, nil, account.GetActiveGroupUsers())
_ = account.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, nil, nil, nil, nil, account.GetActiveGroupUsers(), nil, nil)
}
}
})

View File

@@ -0,0 +1,406 @@
package types
import (
"context"
"net"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
)
func TestGetProxyConnectionResources_PeerTarget(t *testing.T) {
account := &Account{
Peers: map[string]*nbpeer.Peer{
"target-peer": {ID: "target-peer", IP: net.ParseIP("100.64.0.1")},
},
}
exposedServices := map[string][]*reverseproxy.ReverseProxy{
"target-peer": {
{
ID: "proxy-1",
Enabled: true,
Targets: []reverseproxy.Target{
{
TargetType: reverseproxy.TargetTypePeer,
TargetId: "target-peer",
Port: 8080,
Enabled: true,
},
},
},
},
}
aclPeers := account.GetProxyConnectionResources(context.Background(), exposedServices)
require.Len(t, aclPeers, 1)
assert.Equal(t, "target-peer", aclPeers[0].ID)
}
func TestGetProxyConnectionResources_DisabledService(t *testing.T) {
account := &Account{
Peers: map[string]*nbpeer.Peer{
"target-peer": {ID: "target-peer", IP: net.ParseIP("100.64.0.1")},
},
}
exposedServices := map[string][]*reverseproxy.ReverseProxy{
"target-peer": {
{
ID: "proxy-1",
Enabled: false,
Targets: []reverseproxy.Target{
{
TargetType: reverseproxy.TargetTypePeer,
TargetId: "target-peer",
Port: 8080,
Enabled: true,
},
},
},
},
}
aclPeers := account.GetProxyConnectionResources(context.Background(), exposedServices)
assert.Empty(t, aclPeers)
}
func TestGetProxyConnectionResources_ResourceTargetSkipped(t *testing.T) {
account := &Account{
Peers: map[string]*nbpeer.Peer{
"router-peer": {ID: "router-peer", IP: net.ParseIP("100.64.0.2")},
},
}
exposedServices := map[string][]*reverseproxy.ReverseProxy{
"router-peer": {
{
ID: "proxy-1",
Enabled: true,
Targets: []reverseproxy.Target{
{
TargetType: reverseproxy.TargetTypeResource,
TargetId: "resource-1",
Port: 443,
Enabled: true,
},
},
},
},
}
aclPeers := account.GetProxyConnectionResources(context.Background(), exposedServices)
assert.Empty(t, aclPeers, "resource targets should not add ACL peers via GetProxyConnectionResources")
}
func TestGetPeerProxyResources_PeerTarget(t *testing.T) {
proxyPeers := []*nbpeer.Peer{
{ID: "proxy-peer-1", IP: net.ParseIP("100.64.0.10")},
{ID: "proxy-peer-2", IP: net.ParseIP("100.64.0.11")},
}
services := []*reverseproxy.ReverseProxy{
{
ID: "proxy-1",
Enabled: true,
Targets: []reverseproxy.Target{
{
TargetType: reverseproxy.TargetTypePeer,
TargetId: "target-peer",
Port: 8080,
Enabled: true,
},
},
},
}
account := &Account{}
aclPeers, fwRules := account.GetPeerProxyResources("target-peer", services, proxyPeers)
require.Len(t, aclPeers, 2, "should include all proxy peers")
require.Len(t, fwRules, 2, "should have one IN rule per proxy peer")
for i, rule := range fwRules {
assert.Equal(t, "proxy-proxy-1", rule.PolicyID)
assert.Equal(t, proxyPeers[i].IP.String(), rule.PeerIP)
assert.Equal(t, FirewallRuleDirectionIN, rule.Direction)
assert.Equal(t, "allow", rule.Action)
assert.Equal(t, string(PolicyRuleProtocolTCP), rule.Protocol)
assert.Equal(t, uint16(8080), rule.PortRange.Start)
assert.Equal(t, uint16(8080), rule.PortRange.End)
}
}
func TestGetPeerProxyResources_PeerTargetMismatch(t *testing.T) {
proxyPeers := []*nbpeer.Peer{
{ID: "proxy-peer-1", IP: net.ParseIP("100.64.0.10")},
}
services := []*reverseproxy.ReverseProxy{
{
ID: "proxy-1",
Enabled: true,
Targets: []reverseproxy.Target{
{
TargetType: reverseproxy.TargetTypePeer,
TargetId: "other-peer",
Port: 8080,
Enabled: true,
},
},
},
}
account := &Account{}
aclPeers, fwRules := account.GetPeerProxyResources("target-peer", services, proxyPeers)
require.Len(t, aclPeers, 1, "should still add proxy peers to ACL")
assert.Empty(t, fwRules, "should not generate rules when target doesn't match this peer")
}
func TestGetPeerProxyResources_ResourceAccessLocal(t *testing.T) {
proxyPeers := []*nbpeer.Peer{
{ID: "proxy-peer-1", IP: net.ParseIP("100.64.0.10")},
}
services := []*reverseproxy.ReverseProxy{
{
ID: "proxy-1",
Enabled: true,
Targets: []reverseproxy.Target{
{
TargetType: reverseproxy.TargetTypeResource,
TargetId: "resource-1",
Port: 443,
Enabled: true,
AccessLocal: true,
},
},
},
}
account := &Account{}
aclPeers, fwRules := account.GetPeerProxyResources("router-peer", services, proxyPeers)
require.Len(t, aclPeers, 1, "should include proxy peers in ACL")
require.Len(t, fwRules, 1, "should generate IN rule for AccessLocal resource")
rule := fwRules[0]
assert.Equal(t, "proxy-proxy-1", rule.PolicyID)
assert.Equal(t, "100.64.0.10", rule.PeerIP)
assert.Equal(t, FirewallRuleDirectionIN, rule.Direction)
assert.Equal(t, uint16(443), rule.PortRange.Start)
}
func TestGetPeerProxyResources_ResourceWithoutAccessLocal(t *testing.T) {
proxyPeers := []*nbpeer.Peer{
{ID: "proxy-peer-1", IP: net.ParseIP("100.64.0.10")},
}
services := []*reverseproxy.ReverseProxy{
{
ID: "proxy-1",
Enabled: true,
Targets: []reverseproxy.Target{
{
TargetType: reverseproxy.TargetTypeResource,
TargetId: "resource-1",
Port: 443,
Enabled: true,
AccessLocal: false,
},
},
},
}
account := &Account{}
aclPeers, fwRules := account.GetPeerProxyResources("router-peer", services, proxyPeers)
require.Len(t, aclPeers, 1, "should still include proxy peers in ACL")
assert.Empty(t, fwRules, "should not generate peer rules when AccessLocal is false")
}
func TestGetPeerProxyResources_MixedTargets(t *testing.T) {
proxyPeers := []*nbpeer.Peer{
{ID: "proxy-peer-1", IP: net.ParseIP("100.64.0.10")},
}
services := []*reverseproxy.ReverseProxy{
{
ID: "proxy-1",
Enabled: true,
Targets: []reverseproxy.Target{
{
TargetType: reverseproxy.TargetTypePeer,
TargetId: "target-peer",
Port: 8080,
Enabled: true,
},
{
TargetType: reverseproxy.TargetTypeResource,
TargetId: "resource-1",
Port: 443,
Enabled: true,
AccessLocal: true,
},
{
TargetType: reverseproxy.TargetTypeResource,
TargetId: "resource-2",
Port: 8443,
Enabled: true,
AccessLocal: false,
},
},
},
}
account := &Account{}
aclPeers, fwRules := account.GetPeerProxyResources("target-peer", services, proxyPeers)
require.Len(t, aclPeers, 1)
require.Len(t, fwRules, 2, "should have rules for peer target + AccessLocal resource")
ports := []uint16{fwRules[0].PortRange.Start, fwRules[1].PortRange.Start}
assert.Contains(t, ports, uint16(8080), "should include peer target port")
assert.Contains(t, ports, uint16(443), "should include AccessLocal resource port")
}
func newProxyRoutesTestAccount() *Account {
return &Account{
Peers: map[string]*nbpeer.Peer{
"router-peer": {ID: "router-peer", Key: "router-key", IP: net.ParseIP("100.64.0.2")},
"proxy-peer": {ID: "proxy-peer", Key: "proxy-key", IP: net.ParseIP("100.64.0.10")},
},
}
}
func TestGetPeerProxyRoutes_ResourceWithoutAccessLocal(t *testing.T) {
account := newProxyRoutesTestAccount()
proxyPeers := []*nbpeer.Peer{account.Peers["proxy-peer"]}
resourcesMap := map[string]*resourceTypes.NetworkResource{
"resource-1": {
ID: "resource-1",
AccountID: "accountID",
NetworkID: "net-1",
Name: "web-service",
Type: resourceTypes.Host,
Prefix: netip.MustParsePrefix("192.168.1.100/32"),
Enabled: true,
},
}
routers := map[string]map[string]*routerTypes.NetworkRouter{
"net-1": {
"router-peer": {ID: "router-1", NetworkID: "net-1", Peer: "router-peer", Masquerade: true, Metric: 100},
},
}
exposedServices := map[string][]*reverseproxy.ReverseProxy{
"router-peer": {
{
ID: "proxy-1",
Enabled: true,
Targets: []reverseproxy.Target{
{
TargetType: reverseproxy.TargetTypeResource,
TargetId: "resource-1",
Port: 443,
Enabled: true,
AccessLocal: false,
},
},
},
},
}
routes, routeFwRules, aclPeers := account.GetPeerProxyRoutes(context.Background(), account.Peers["proxy-peer"], exposedServices, resourcesMap, routers, proxyPeers)
require.NotEmpty(t, routes, "should generate routes for non-AccessLocal resource")
require.NotEmpty(t, routeFwRules, "should generate route firewall rules for non-AccessLocal resource")
require.NotEmpty(t, aclPeers, "should include router peer in ACL")
assert.Equal(t, uint16(443), routeFwRules[0].PortRange.Start)
assert.Equal(t, "192.168.1.100/32", routeFwRules[0].Destination)
}
func TestGetPeerProxyRoutes_ResourceWithAccessLocal(t *testing.T) {
account := newProxyRoutesTestAccount()
proxyPeers := []*nbpeer.Peer{account.Peers["proxy-peer"]}
resourcesMap := map[string]*resourceTypes.NetworkResource{
"resource-1": {
ID: "resource-1",
AccountID: "accountID",
NetworkID: "net-1",
Name: "local-service",
Type: resourceTypes.Host,
Prefix: netip.MustParsePrefix("192.168.1.100/32"),
Enabled: true,
},
}
routers := map[string]map[string]*routerTypes.NetworkRouter{
"net-1": {
"router-peer": {ID: "router-1", NetworkID: "net-1", Peer: "router-peer", Masquerade: true, Metric: 100},
},
}
exposedServices := map[string][]*reverseproxy.ReverseProxy{
"router-peer": {
{
ID: "proxy-1",
Enabled: true,
Targets: []reverseproxy.Target{
{
TargetType: reverseproxy.TargetTypeResource,
TargetId: "resource-1",
Port: 443,
Enabled: true,
AccessLocal: true,
},
},
},
},
}
routes, routeFwRules, aclPeers := account.GetPeerProxyRoutes(context.Background(), account.Peers["proxy-peer"], exposedServices, resourcesMap, routers, proxyPeers)
assert.Empty(t, routes, "should NOT generate routes for AccessLocal resource")
assert.Empty(t, routeFwRules, "should NOT generate route firewall rules for AccessLocal resource")
assert.Empty(t, aclPeers, "should NOT include router peer from route path for AccessLocal resource")
}
func TestGetPeerProxyRoutes_PeerTargetSkipped(t *testing.T) {
account := newProxyRoutesTestAccount()
proxyPeers := []*nbpeer.Peer{account.Peers["proxy-peer"]}
exposedServices := map[string][]*reverseproxy.ReverseProxy{
"router-peer": {
{
ID: "proxy-1",
Enabled: true,
Targets: []reverseproxy.Target{
{
TargetType: reverseproxy.TargetTypePeer,
TargetId: "target-peer",
Port: 8080,
Enabled: true,
},
},
},
},
}
routes, routeFwRules, aclPeers := account.GetPeerProxyRoutes(context.Background(), account.Peers["proxy-peer"], exposedServices, nil, nil, proxyPeers)
assert.Empty(t, routes, "should NOT generate routes for peer targets")
assert.Empty(t, routeFwRules, "should NOT generate route firewall rules for peer targets")
assert.Empty(t, aclPeers)
}