diff --git a/management/internals/modules/reverseproxy/reverseproxy.go b/management/internals/modules/reverseproxy/reverseproxy.go index 62451c288..a013b0c0d 100644 --- a/management/internals/modules/reverseproxy/reverseproxy.go +++ b/management/internals/modules/reverseproxy/reverseproxy.go @@ -46,6 +46,9 @@ type Target struct { TargetId string `json:"target_id"` TargetType string `json:"target_type"` Enabled bool `json:"enabled"` + // AccessLocal indicates the resource is served locally on the router peer, + // requiring additional peer-level firewall rules for the proxy to access the router directly. + AccessLocal bool `json:"access_local"` } type PasswordAuthConfig struct { @@ -149,13 +152,14 @@ func (r *ReverseProxy) ToAPIResponse() *api.ReverseProxy { apiTargets := make([]api.ReverseProxyTarget, 0, len(r.Targets)) for _, target := range r.Targets { apiTargets = append(apiTargets, api.ReverseProxyTarget{ - Path: target.Path, - Host: target.Host, - Port: target.Port, - Protocol: api.ReverseProxyTargetProtocol(target.Protocol), - TargetId: target.TargetId, - TargetType: api.ReverseProxyTargetTargetType(target.TargetType), - Enabled: target.Enabled, + Path: target.Path, + Host: target.Host, + Port: target.Port, + Protocol: api.ReverseProxyTargetProtocol(target.Protocol), + TargetId: target.TargetId, + TargetType: api.ReverseProxyTargetTargetType(target.TargetType), + Enabled: target.Enabled, + AccessLocal: &target.AccessLocal, }) } @@ -263,14 +267,16 @@ func (r *ReverseProxy) FromAPIRequest(req *api.ReverseProxyRequest, accountID st targets := make([]Target, 0, len(req.Targets)) for _, apiTarget := range req.Targets { + accessLocal := apiTarget.AccessLocal != nil && *apiTarget.AccessLocal targets = append(targets, Target{ - Path: apiTarget.Path, - Host: apiTarget.Host, - Port: apiTarget.Port, - Protocol: string(apiTarget.Protocol), - TargetId: apiTarget.TargetId, - TargetType: string(apiTarget.TargetType), - Enabled: apiTarget.Enabled, + Path: apiTarget.Path, + Host: apiTarget.Host, + Port: apiTarget.Port, + Protocol: string(apiTarget.Protocol), + TargetId: apiTarget.TargetId, + TargetType: string(apiTarget.TargetType), + Enabled: apiTarget.Enabled, + AccessLocal: accessLocal, }) } r.Targets = targets diff --git a/management/server/types/account.go b/management/server/types/account.go index 53d9ad51f..6554acb5f 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -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) diff --git a/management/server/types/networkmap_golden_test.go b/management/server/types/networkmap_golden_test.go index ef6c51779..36f536a89 100644 --- a/management/server/types/networkmap_golden_test.go +++ b/management/server/types/networkmap_golden_test.go @@ -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) } } }) diff --git a/management/server/types/proxy_firewall_rules_test.go b/management/server/types/proxy_firewall_rules_test.go new file mode 100644 index 000000000..e09f761cc --- /dev/null +++ b/management/server/types/proxy_firewall_rules_test.go @@ -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) +} diff --git a/proxy/internal/proxy/servicemapping.go b/proxy/internal/proxy/servicemapping.go index bfe755269..9b45a3c3d 100644 --- a/proxy/internal/proxy/servicemapping.go +++ b/proxy/internal/proxy/servicemapping.go @@ -38,6 +38,7 @@ func (p *ReverseProxy) findTargetForRequest(req *http.Request) (targetResult, bo p.logger.Debugf("looking for mapping for host: %s, path: %s", host, req.URL.Path) m, exists := p.mappings[host] if !exists { + p.logger.Debugf("no mapping found for host: %s", host) return targetResult{}, false } @@ -52,14 +53,17 @@ func (p *ReverseProxy) findTargetForRequest(req *http.Request) (targetResult, bo for _, path := range paths { if strings.HasPrefix(req.URL.Path, path) { + target := m.Paths[path] + p.logger.Debugf("matched host: %s, path: %s -> %s", host, path, target) return targetResult{ - url: m.Paths[path], + url: target, serviceID: m.ID, accountID: m.AccountID, passHostHeader: m.PassHostHeader, }, true } } + p.logger.Debugf("no path match for host: %s, path: %s", host, req.URL.Path) return targetResult{}, false } diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index 3fa8560a1..be1cf9b76 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -2954,6 +2954,9 @@ components: enabled: type: boolean description: Whether this target is enabled + access_local: + type: boolean + description: Whether the resource is served locally on the router peer, requiring direct peer-level access required: - target_id - target_type diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index 1f503b85b..89a61ad74 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -2068,6 +2068,9 @@ type ReverseProxyRequest struct { // ReverseProxyTarget defines model for ReverseProxyTarget. type ReverseProxyTarget struct { + // AccessLocal Whether the resource is served locally on the router peer, requiring direct peer-level access + AccessLocal *bool `json:"access_local,omitempty"` + // Enabled Whether this target is enabled Enabled bool `json:"enabled"`