From 2a8aacc5c917a517e0077d49f9ad0c8745cc3834 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:08:21 +0200 Subject: [PATCH] [management] allow local routing peer resource (#5814) --- .../server/networks/resources/manager.go | 2 +- .../networks/resources/types/resource.go | 89 ++++++++++--------- management/server/store/sql_store.go | 8 +- management/server/store/sql_store_test.go | 2 +- .../server/types/networkmap_components.go | 70 +++++++++++++-- shared/management/http/api/openapi.yml | 4 + shared/management/http/api/types.gen.go | 9 ++ 7 files changed, 134 insertions(+), 50 deletions(-) diff --git a/management/server/networks/resources/manager.go b/management/server/networks/resources/manager.go index 86f9b6579..ec20f2963 100644 --- a/management/server/networks/resources/manager.go +++ b/management/server/networks/resources/manager.go @@ -108,7 +108,7 @@ func (m *managerImpl) CreateResource(ctx context.Context, userID string, resourc return nil, status.NewPermissionDeniedError() } - resource, err = types.NewNetworkResource(resource.AccountID, resource.NetworkID, resource.Name, resource.Description, resource.Address, resource.GroupIDs, resource.Enabled) + resource, err = types.NewNetworkResource(resource.AccountID, resource.NetworkID, resource.Name, resource.Description, resource.Address, resource.GroupIDs, resource.OnRoutingPeer, resource.Enabled) if err != nil { return nil, fmt.Errorf("failed to create new network resource: %w", err) } diff --git a/management/server/networks/resources/types/resource.go b/management/server/networks/resources/types/resource.go index 1fa908393..3a25c4f03 100644 --- a/management/server/networks/resources/types/resource.go +++ b/management/server/networks/resources/types/resource.go @@ -29,37 +29,39 @@ func (p NetworkResourceType) String() string { } type NetworkResource struct { - ID string `gorm:"primaryKey"` - NetworkID string `gorm:"index"` - AccountID string `gorm:"index"` - Name string - Description string - Type NetworkResourceType - Address string `gorm:"-"` - GroupIDs []string `gorm:"-"` - Domain string - Prefix netip.Prefix `gorm:"serializer:json"` - Enabled bool + ID string `gorm:"primaryKey"` + NetworkID string `gorm:"index"` + AccountID string `gorm:"index"` + Name string + Description string + Type NetworkResourceType + Address string `gorm:"-"` + GroupIDs []string `gorm:"-"` + Domain string + Prefix netip.Prefix `gorm:"serializer:json"` + Enabled bool + OnRoutingPeer bool } -func NewNetworkResource(accountID, networkID, name, description, address string, groupIDs []string, enabled bool) (*NetworkResource, error) { +func NewNetworkResource(accountID, networkID, name, description, address string, groupIDs []string, onRoutingPeer, enabled bool) (*NetworkResource, error) { resourceType, domain, prefix, err := GetResourceType(address) if err != nil { return nil, fmt.Errorf("invalid address: %w", err) } return &NetworkResource{ - ID: xid.New().String(), - AccountID: accountID, - NetworkID: networkID, - Name: name, - Description: description, - Type: resourceType, - Address: address, - Domain: domain, - Prefix: prefix, - GroupIDs: groupIDs, - Enabled: enabled, + ID: xid.New().String(), + AccountID: accountID, + NetworkID: networkID, + Name: name, + Description: description, + Type: resourceType, + Address: address, + Domain: domain, + Prefix: prefix, + GroupIDs: groupIDs, + Enabled: enabled, + OnRoutingPeer: onRoutingPeer, }, nil } @@ -70,13 +72,14 @@ func (n *NetworkResource) ToAPIResponse(groups []api.GroupMinimum) *api.NetworkR } return &api.NetworkResource{ - Id: n.ID, - Name: n.Name, - Description: &n.Description, - Type: api.NetworkResourceType(n.Type.String()), - Address: addr, - Groups: groups, - Enabled: n.Enabled, + Id: n.ID, + Name: n.Name, + Description: &n.Description, + Type: api.NetworkResourceType(n.Type.String()), + Address: addr, + Groups: groups, + Enabled: n.Enabled, + OnRoutingPeer: &n.OnRoutingPeer, } } @@ -86,6 +89,9 @@ func (n *NetworkResource) FromAPIRequest(req *api.NetworkResourceRequest) { if req.Description != nil { n.Description = *req.Description } + if req.OnRoutingPeer != nil { + n.OnRoutingPeer = *req.OnRoutingPeer + } n.Address = req.Address n.GroupIDs = req.Groups n.Enabled = req.Enabled @@ -93,17 +99,18 @@ func (n *NetworkResource) FromAPIRequest(req *api.NetworkResourceRequest) { func (n *NetworkResource) Copy() *NetworkResource { return &NetworkResource{ - ID: n.ID, - AccountID: n.AccountID, - NetworkID: n.NetworkID, - Name: n.Name, - Description: n.Description, - Type: n.Type, - Address: n.Address, - Domain: n.Domain, - Prefix: n.Prefix, - GroupIDs: n.GroupIDs, - Enabled: n.Enabled, + ID: n.ID, + AccountID: n.AccountID, + NetworkID: n.NetworkID, + Name: n.Name, + Description: n.Description, + Type: n.Type, + Address: n.Address, + Domain: n.Domain, + Prefix: n.Prefix, + GroupIDs: n.GroupIDs, + Enabled: n.Enabled, + OnRoutingPeer: n.OnRoutingPeer, } } diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 0b463a724..6f177f80e 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -2291,7 +2291,7 @@ func (s *SqlStore) getNetworkRouters(ctx context.Context, accountID string) ([]* } func (s *SqlStore) getNetworkResources(ctx context.Context, accountID string) ([]*resourceTypes.NetworkResource, error) { - const query = `SELECT id, network_id, account_id, name, description, type, domain, prefix, enabled FROM network_resources WHERE account_id = $1` + const query = `SELECT id, network_id, account_id, name, description, type, domain, prefix, enabled, on_routing_peer FROM network_resources WHERE account_id = $1` rows, err := s.pool.Query(ctx, query, accountID) if err != nil { return nil, err @@ -2300,11 +2300,15 @@ func (s *SqlStore) getNetworkResources(ctx context.Context, accountID string) ([ var r resourceTypes.NetworkResource var prefix []byte var enabled sql.NullBool - err := row.Scan(&r.ID, &r.NetworkID, &r.AccountID, &r.Name, &r.Description, &r.Type, &r.Domain, &prefix, &enabled) + var onRoutingPeer sql.NullBool + err := row.Scan(&r.ID, &r.NetworkID, &r.AccountID, &r.Name, &r.Description, &r.Type, &r.Domain, &prefix, &enabled, &onRoutingPeer) if err == nil { if enabled.Valid { r.Enabled = enabled.Bool } + if onRoutingPeer.Valid { + r.OnRoutingPeer = onRoutingPeer.Bool + } if prefix != nil { _ = json.Unmarshal(prefix, &r.Prefix) } diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 8ea6c2ae5..da3be2803 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -2508,7 +2508,7 @@ func TestSqlStore_SaveNetworkResource(t *testing.T) { accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" networkID := "ct286bi7qv930dsrrug0" - netResource, err := resourceTypes.NewNetworkResource(accountID, networkID, "resource-name", "", "example.com", []string{}, true) + netResource, err := resourceTypes.NewNetworkResource(accountID, networkID, "resource-name", "", "example.com", []string{}, false, true) require.NoError(t, err) err = store.SaveNetworkResource(context.Background(), netResource) diff --git a/management/server/types/networkmap_components.go b/management/server/types/networkmap_components.go index 23d84a994..9f8f822c9 100644 --- a/management/server/types/networkmap_components.go +++ b/management/server/types/networkmap_components.go @@ -2,7 +2,6 @@ package types import ( "context" - "maps" "net" "net/netip" "slices" @@ -17,6 +16,7 @@ import ( nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/shared/management/domain" + "golang.org/x/exp/maps" ) const EnvNewNetworkMapCompacted = "NB_NETWORK_MAP_COMPACTED" @@ -119,9 +119,10 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap { routesUpdate := c.getRoutesToSync(targetPeerID, peersToConnect, peerGroups) routesFirewallRules := c.getPeerRoutesFirewallRules(ctx, targetPeerID) - isRouter, networkResourcesRoutes, sourcePeers := c.getNetworkResourcesRoutesToSync(targetPeerID) + isRouter, networkResourcesRoutes, sourcePeers, peerFirewallRules := c.getNetworkResourcesRoutesToSync(targetPeerID) var networkResourcesFirewallRules []*RouteFirewallRule if isRouter { + firewallRules = append(firewallRules, peerFirewallRules...) networkResourcesFirewallRules = c.getPeerNetworkResourceFirewallRules(ctx, targetPeerID, networkResourcesRoutes) } @@ -526,7 +527,6 @@ func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoute return enabledRoutes, disabledRoutes } - func (c *NetworkMapComponents) filterRoutesByGroups(routes []*route.Route, groupListMap LookupMap) []*route.Route { var filteredRoutes []*route.Route for _, r := range routes { @@ -692,10 +692,11 @@ func (c *NetworkMapComponents) getRulePeers(rule *PolicyRule, postureChecks []st return distributionGroupPeers } -func (c *NetworkMapComponents) getNetworkResourcesRoutesToSync(peerID string) (bool, []*route.Route, map[string]struct{}) { +func (c *NetworkMapComponents) getNetworkResourcesRoutesToSync(peerID string) (bool, []*route.Route, map[string]struct{}, []*FirewallRule) { var isRoutingPeer bool var routes []*route.Route allSourcePeers := make(map[string]struct{}) + localResourceFwRule := make([]*FirewallRule, 0) for _, resource := range c.NetworkResources { if !resource.Enabled { @@ -714,6 +715,9 @@ func (c *NetworkMapComponents) getNetworkResourcesRoutesToSync(peerID string) (b addedResourceRoute := false for _, policy := range c.ResourcePoliciesMap[resource.ID] { + if isRoutingPeer && resource.OnRoutingPeer { + localResourceFwRule = append(localResourceFwRule, c.getLocalResourceFirewallRules(policy)...) + } var peers []string if policy.Rules[0].SourceResource.Type == ResourceTypePeer && policy.Rules[0].SourceResource.ID != "" { peers = []string{policy.Rules[0].SourceResource.ID} @@ -736,7 +740,63 @@ func (c *NetworkMapComponents) getNetworkResourcesRoutesToSync(peerID string) (b } } - return isRoutingPeer, routes, allSourcePeers + return isRoutingPeer, routes, allSourcePeers, localResourceFwRule +} + +func (c *NetworkMapComponents) getLocalResourceFirewallRules(policy *Policy) []*FirewallRule { + sourcePeerIDs := c.getPoliciesSourcePeers([]*Policy{policy}) + postureValidatedPeerIDs := c.getPostureValidPeers(maps.Keys(sourcePeerIDs), policy.SourcePostureChecks) + + rules := make([]*FirewallRule, 0) + for _, rule := range policy.Rules { + if !rule.Enabled { + continue + } + + protocol := rule.Protocol + if protocol == PolicyRuleProtocolNetbirdSSH { + continue + } + + for _, peerID := range postureValidatedPeerIDs { + peer := c.GetPeerInfo(peerID) + if peer == nil { + continue + } + peerIP := peer.IP.String() + + fr := FirewallRule{ + PolicyID: rule.ID, + PeerIP: peerIP, + Direction: FirewallRuleDirectionIN, + Action: string(rule.Action), + Protocol: string(protocol), + } + + if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 { + rules = append(rules, &fr) + continue + } + + for _, port := range rule.Ports { + portRule := fr + portRule.Port = port + rules = append(rules, &portRule) + } + + for _, portRange := range rule.PortRanges { + if len(rule.Ports) > 0 { + break + } + rangeRule := fr + rangeRule.PortRange = portRange + rules = append(rules, &rangeRule) + } + + } + } + + return rules } func (c *NetworkMapComponents) getNetworkResourcesRoutes(resource *resourceTypes.NetworkResource, peerID string, router *routerTypes.NetworkRouter) []*route.Route { diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index 766fdf0de..d471ba376 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -1980,6 +1980,10 @@ components: description: Network resource status type: boolean example: true + on_routing_peer: + description: Indicate if the resource is on a routing peer or not. It is needed if the resource is targeting the IP of the routing peer itself + type: boolean + example: true required: - name - address diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index 14bb6ee03..03febb193 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -2728,6 +2728,9 @@ type NetworkResource struct { // Name Network resource name Name string `json:"name"` + // OnRoutingPeer Indicate if the resource is on a routing peer or not. It is needed if the resource is targeting the IP of the routing peer itself + OnRoutingPeer *bool `json:"on_routing_peer,omitempty"` + // Type Network resource type based of the address Type NetworkResourceType `json:"type"` } @@ -2745,6 +2748,9 @@ type NetworkResourceMinimum struct { // Name Network resource name Name string `json:"name"` + + // OnRoutingPeer Indicate if the resource is on a routing peer or not. It is needed if the resource is targeting the IP of the routing peer itself + OnRoutingPeer *bool `json:"on_routing_peer,omitempty"` } // NetworkResourceRequest defines model for NetworkResourceRequest. @@ -2763,6 +2769,9 @@ type NetworkResourceRequest struct { // Name Network resource name Name string `json:"name"` + + // OnRoutingPeer Indicate if the resource is on a routing peer or not. It is needed if the resource is targeting the IP of the routing peer itself + OnRoutingPeer *bool `json:"on_routing_peer,omitempty"` } // NetworkResourceType Network resource type based of the address