Merge branch 'main' into file-share

# Conflicts:
#	client/ios/NetBirdSDK/client.go
This commit is contained in:
Zoltán Papp
2026-09-01 18:06:26 +02:00
485 changed files with 36221 additions and 5501 deletions
+47 -19
View File
@@ -289,36 +289,64 @@ func getPublicKey(token *jwt.Token, jwks *Jwks) (interface{}, error) {
return nil, errKeyNotFound
}
func getPublicKeyFromECDSA(jwk JSONWebKey) (publicKey *ecdsa.PublicKey, err error) {
func curveFromName(crv string) (elliptic.Curve, error) {
switch crv {
case p256:
return elliptic.P256(), nil
case p384:
return elliptic.P384(), nil
case p521:
return elliptic.P521(), nil
default:
return nil, fmt.Errorf("unsupported elliptic curve %q", crv)
}
}
func getPublicKeyFromECDSA(jwk JSONWebKey) (*ecdsa.PublicKey, error) {
if jwk.X == "" || jwk.Y == "" || jwk.Crv == "" {
return nil, fmt.Errorf("ecdsa key incomplete")
}
var xCoordinate []byte
if xCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.X); err != nil {
curve, err := curveFromName(jwk.Crv)
if err != nil {
return nil, err
}
var yCoordinate []byte
if yCoordinate, err = base64.RawURLEncoding.DecodeString(jwk.Y); err != nil {
return nil, err
xCoordinate, err := base64.RawURLEncoding.DecodeString(jwk.X)
if err != nil {
return nil, fmt.Errorf("decode ecdsa x coordinate: %w", err)
}
publicKey = &ecdsa.PublicKey{}
var curve elliptic.Curve
switch jwk.Crv {
case p256:
curve = elliptic.P256()
case p384:
curve = elliptic.P384()
case p521:
curve = elliptic.P521()
yCoordinate, err := base64.RawURLEncoding.DecodeString(jwk.Y)
if err != nil {
return nil, fmt.Errorf("decode ecdsa y coordinate: %w", err)
}
publicKey.Curve = curve
publicKey.X = big.NewInt(0).SetBytes(xCoordinate)
publicKey.Y = big.NewInt(0).SetBytes(yCoordinate)
var x, y big.Int
x.SetBytes(xCoordinate)
y.SetBytes(yCoordinate)
bits := curve.Params().BitSize
if x.BitLen() > bits {
return nil, fmt.Errorf("ecdsa x coordinate is %d bits, exceeds curve %s field size of %d bits", x.BitLen(), jwk.Crv, bits)
}
if y.BitLen() > bits {
return nil, fmt.Errorf("ecdsa y coordinate is %d bits, exceeds curve %s field size of %d bits", y.BitLen(), jwk.Crv, bits)
}
// Round up: P-521's field is 521 bits, so a coordinate needs 66 bytes, not 65.
size := (bits + 7) / 8
// Assemble the SEC 1 uncompressed point (0x04 || X || Y)
point := make([]byte, 1+2*size)
point[0] = 4
x.FillBytes(point[1 : 1+size])
y.FillBytes(point[1+size:])
publicKey, err := ecdsa.ParseUncompressedPublicKey(curve, point)
if err != nil {
return nil, fmt.Errorf("parse ecdsa public key: %w", err)
}
return publicKey, nil
}
+214
View File
@@ -0,0 +1,214 @@
package jwt
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ecdsaJWK builds a JWK for pub using uncompressed-point encoding
func ecdsaJWK(t *testing.T, kid string, pub *ecdsa.PublicKey, crv string, size int) JSONWebKey {
t.Helper()
point, err := pub.Bytes()
require.NoError(t, err)
require.Len(t, point, 1+2*size)
require.Equal(t, byte(4), point[0], "expected uncompressed point")
return JSONWebKey{
Kty: "EC",
Kid: kid,
Use: "sig",
Crv: crv,
X: base64.RawURLEncoding.EncodeToString(point[1 : 1+size]),
Y: base64.RawURLEncoding.EncodeToString(point[1+size:]),
}
}
func TestGetPublicKeyFromECDSA_RoundTrip(t *testing.T) {
tests := []struct {
crv string
curve elliptic.Curve
size int
}{
{p256, elliptic.P256(), 32},
{p384, elliptic.P384(), 48},
{p521, elliptic.P521(), 66},
}
for _, tc := range tests {
t.Run(tc.crv, func(t *testing.T) {
priv, err := ecdsa.GenerateKey(tc.curve, rand.Reader)
require.NoError(t, err)
got, err := getPublicKeyFromECDSA(ecdsaJWK(t, "kid", &priv.PublicKey, tc.crv, tc.size))
require.NoError(t, err)
assert.True(t, priv.PublicKey.Equal(got), "parsed key differs from the original")
})
}
}
// TestGetPublicKeyFromECDSA_ShortCoordinate covers IdPs that strip leading zero
// bytes from a coordinate instead of padding to the curve's field size.
func TestGetPublicKeyFromECDSA_ShortCoordinate(t *testing.T) {
var (
priv *ecdsa.PrivateKey
point []byte
)
for i := 0; i < 10000; i++ {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
p, err := key.PublicKey.Bytes()
require.NoError(t, err)
if p[1] == 0 || p[33] == 0 {
priv, point = key, p
break
}
}
require.NotNil(t, priv, "no key with a leading zero coordinate byte was generated")
jwk := JSONWebKey{
Kty: "EC",
Kid: "kid",
Crv: p256,
X: base64.RawURLEncoding.EncodeToString(bytes.TrimLeft(point[1:33], "\x00")),
Y: base64.RawURLEncoding.EncodeToString(bytes.TrimLeft(point[33:], "\x00")),
}
got, err := getPublicKeyFromECDSA(jwk)
require.NoError(t, err)
assert.True(t, priv.PublicKey.Equal(got))
}
func TestGetPublicKeyFromECDSA_Invalid(t *testing.T) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
valid := ecdsaJWK(t, "kid", &priv.PublicKey, p256, 32)
offCurve := valid
x, err := base64.RawURLEncoding.DecodeString(valid.X)
require.NoError(t, err)
x[31] ^= 0xff
offCurve.X = base64.RawURLEncoding.EncodeToString(x)
// 33 non-zero bytes is 264 bits, past P-256's 256-bit field.
oversized := valid
oversized.X = base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{0xff}, 33))
// P-521 coordinates occupy 66 bytes but only 521 bits, so a full 66-byte
// 0xff value (528 bits) is over the field size without being over the byte
// length. Only a bit-length bound catches this.
overP521 := JSONWebKey{
Kty: "EC",
Crv: p521,
X: base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{0xff}, 66)),
Y: base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{0xff}, 66)),
}
zeroPoint := valid
zeroPoint.X = base64.RawURLEncoding.EncodeToString(make([]byte, 32))
zeroPoint.Y = base64.RawURLEncoding.EncodeToString(make([]byte, 32))
tests := []struct {
name string
jwk JSONWebKey
errContains string
}{
{name: "missing crv", jwk: JSONWebKey{Kty: "EC", X: valid.X, Y: valid.Y}},
{name: "missing x", jwk: JSONWebKey{Kty: "EC", Crv: p256, Y: valid.Y}},
{name: "unsupported curve", jwk: JSONWebKey{Kty: "EC", Crv: "P-224", X: valid.X, Y: valid.Y}, errContains: "unsupported elliptic curve"},
{name: "undecodable x", jwk: JSONWebKey{Kty: "EC", Crv: p256, X: "!!not base64!!!", Y: valid.Y}, errContains: "decode ecdsa x coordinate"},
{name: "coordinate over field size", jwk: oversized, errContains: "exceeds curve P-256 field size of 256 bits"},
{name: "p521 coordinate over field size", jwk: overP521, errContains: "exceeds curve P-521 field size of 521 bits"},
{name: "off-curve point", jwk: offCurve, errContains: "parse ecdsa public key"},
{name: "point at infinity", jwk: zeroPoint, errContains: "parse ecdsa public key"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
key, err := getPublicKeyFromECDSA(tc.jwk)
require.Error(t, err)
assert.Nil(t, key)
if tc.errContains != "" {
assert.ErrorContains(t, err, tc.errContains)
}
})
}
}
// TestValidateAndParse_ECDSA verifies an ES256-signed token end to end, proving
// the parsed key actually validates signatures.
func TestValidateAndParse_ECDSA(t *testing.T) {
const (
kid = "es256-kid"
issuer = "https://issuer.example.com/"
audience = "netbird"
)
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
jwks, err := json.Marshal(Jwks{Keys: []JSONWebKey{ecdsaJWK(t, kid, &priv.PublicKey, p256, 32)}})
require.NoError(t, err)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jwks)
}))
defer srv.Close()
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
"iss": issuer,
"aud": audience,
"sub": "user-1",
"iat": time.Now().Add(-time.Minute).Unix(),
"exp": time.Now().Add(time.Hour).Unix(),
})
token.Header["kid"] = kid
signed, err := token.SignedString(priv)
require.NoError(t, err)
v := NewValidator(issuer, []string{audience}, srv.URL, false)
parsed, err := v.ValidateAndParse(context.Background(), signed)
require.NoError(t, err)
require.True(t, parsed.Valid)
claims, ok := parsed.Claims.(jwt.MapClaims)
require.True(t, ok)
assert.Equal(t, "user-1", claims["sub"])
// A token signed by a different key of the same curve must be rejected.
other, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
forged := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
"iss": issuer,
"aud": audience,
"sub": "user-1",
"iat": time.Now().Add(-time.Minute).Unix(),
"exp": time.Now().Add(time.Hour).Unix(),
})
forged.Header["kid"] = kid
forgedSigned, err := forged.SignedString(other)
require.NoError(t, err)
_, err = v.ValidateAndParse(context.Background(), forgedSigned)
require.Error(t, err)
}
+1 -1
View File
@@ -126,7 +126,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := mgmt.NewAccountRequestBuffer(ctx, store)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManger), config)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManger), config, nil)
accountManager, err := mgmt.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false, cacheStore)
if err != nil {
t.Fatal(err)
+1
View File
@@ -1039,6 +1039,7 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta {
RosenpassEnabled: info.RosenpassEnabled,
RosenpassPermissive: info.RosenpassPermissive,
ServerSSHAllowed: info.ServerSSHAllowed,
RemoteJobsAllowed: info.RemoteJobsAllowed,
DisableClientRoutes: info.DisableClientRoutes,
DisableServerRoutes: info.DisableServerRoutes,
+85 -3
View File
@@ -154,6 +154,14 @@ components:
type: boolean
description: Whether sensitive data should be anonymized in the bundle.
example: false
anonymize_level:
type: string
description: How much the anonymizer redacts. "default" (or empty) keeps internal IP ranges, "strict" also anonymizes them.
example: strict
upload_url:
type: string
description: Service URL the client requests an upload URL from before uploading the bundle. Empty selects the default upload server.
example: https://upload.debug.netbird.io
required:
- bundle_for
- bundle_for_time
@@ -976,6 +984,10 @@ components:
description: Indicates whether SSH access this peer is allowed or not
type: boolean
example: true
remote_jobs_allowed:
description: Indicates whether the peer has opted into management-requested remote jobs (e.g. debug bundles)
type: boolean
example: true
disable_client_routes:
description: Indicates whether client routes are disabled on this peer or not
type: boolean
@@ -5807,6 +5819,57 @@ components:
required:
- name
- checks
AgentNetworkAgentConfig:
type: object
description: The caller-scoped Agent Network connection config backing the self-service "Connect your agent" view. Available to every authenticated user; the providers are computed from the caller's own groups and the answer carries display metadata only.
properties:
configured:
type: boolean
description: False only when the account has no Agent Network set up. A caller that no policy covers yet still reads as configured, with an empty providers list.
endpoint:
type: string
description: The account's Agent Network base URL, reachable over the NetBird tunnel only. Returned to every member of a configured account - it authorizes nothing on its own, since the gateway still refuses every request no policy permits. Empty when configured is false.
example: https://calm-otter.proxy.example.com
providers:
type: array
description: The providers at least one of the caller's policies authorizes, in creation order. Empty when no policy covers the caller.
items:
$ref: '#/components/schemas/AgentNetworkAgentConfigProvider'
required:
- configured
- endpoint
- providers
AgentNetworkAgentConfigProvider:
type: object
description: One provider the caller may use, reduced to what a local tool needs for configuration.
properties:
name:
type: string
description: Operator-assigned provider label.
example: Bedrock prod
catalog_id:
type: string
description: Catalog entry id naming the provider type.
example: bedrock_api
api_flavor:
type: string
description: Request-body shape the provider speaks ("anthropic", "openai"). Empty when the gateway dispatches it by URL path instead.
example: anthropic
all_models_allowed:
type: boolean
description: True when no model allowlist restricts this provider for the caller; models then lists the declared or catalog models as a courtesy.
models:
type: array
description: The effective model allowlist for the caller (or the declared/catalog models when all_models_allowed is true).
items:
type: string
example: [ "anthropic.claude-sonnet-4-5" ]
required:
- name
- catalog_id
- api_flavor
- all_models_allowed
- models
AgentNetworkConsumption:
type: object
description: One per-(dimension, window) consumption counter row. The proxy ticks one row per dimension on every served LLM request; the dashboard reads this listing to surface live counter growth.
@@ -13471,7 +13534,7 @@ paths:
/api/agent-network/access-logs:
get:
summary: List Agent Network access logs
description: Returns a paginated, server-side-filtered list of agent-network (LLM) access log entries. Available only when the account has log collection enabled; otherwise entries are not retained.
description: Returns a paginated, server-side-filtered list of agent-network (LLM) access log entries. Available only when the account has log collection enabled; otherwise entries are not retained. Callers without the account-wide grant are not denied - the response is scoped to their own requests (any user_id or group_id filter is overridden).
tags: [ Agent Network ]
security:
- BearerAuth: [ ]
@@ -13586,7 +13649,7 @@ paths:
/api/agent-network/access-log-sessions:
get:
summary: List Agent Network access logs grouped by session
description: Returns a paginated, server-side-filtered list of agent-network (LLM) access logs grouped by session. The page unit is a session (total_records counts sessions); each session carries an aggregate summary and its ordered entries. Requests the client sent no session id for each form their own singleton group. Accepts the same filters as the flat access-logs endpoint. Available only when the account has log collection enabled.
description: Returns a paginated, server-side-filtered list of agent-network (LLM) access logs grouped by session. The page unit is a session (total_records counts sessions); each session carries an aggregate summary and its ordered entries. Requests the client sent no session id for each form their own singleton group. Accepts the same filters as the flat access-logs endpoint. Available only when the account has log collection enabled. Callers without the account-wide grant are not denied - the response is scoped to their own requests (any user_id or group_id filter is overridden).
tags: [ Agent Network ]
security:
- BearerAuth: [ ]
@@ -13701,7 +13764,7 @@ paths:
/api/agent-network/usage/overview:
get:
summary: Agent Network usage overview
description: Returns agent-network token and cost usage aggregated into time buckets, server-side filtered. Usage is always collected (independent of log collection).
description: Returns agent-network token and cost usage aggregated into time buckets, server-side filtered. Usage is always collected (independent of log collection). Callers without the account-wide grant are not denied - the response is scoped to their own usage (any user_id or group_id filter is overridden).
tags: [ Agent Network ]
security:
- BearerAuth: [ ]
@@ -13801,6 +13864,25 @@ paths:
"$ref": "#/components/responses/forbidden"
'500':
"$ref": "#/components/responses/internal_error"
/api/agent-network/agent-config:
get:
summary: Retrieve the caller's Agent Network agent config
description: Returns everything the caller needs to configure a local AI tool and nothing more - the account's Agent Network endpoint plus the providers and models the caller's own policies allow. Available to every authenticated user regardless of role; the response never contains provider credentials, policy or guardrail configuration, or providers the caller cannot reach.
tags: [ Agent Network ]
security:
- BearerAuth: [ ]
- TokenAuth: [ ]
responses:
'200':
description: The caller-scoped Agent Network agent config
content:
application/json:
schema:
$ref: '#/components/schemas/AgentNetworkAgentConfig'
'401':
"$ref": "#/components/responses/requires_authentication"
'500':
"$ref": "#/components/responses/internal_error"
/api/agent-network/settings:
get:
summary: Retrieve Agent Network settings
+39
View File
@@ -1931,6 +1931,36 @@ type AgentNetworkAccessLogsResponse struct {
TotalRecords int `json:"total_records"`
}
// AgentNetworkAgentConfig The caller-scoped Agent Network connection config backing the self-service "Connect your agent" view. Available to every authenticated user; the providers are computed from the caller's own groups and the answer carries display metadata only.
type AgentNetworkAgentConfig struct {
// Configured False only when the account has no Agent Network set up. A caller that no policy covers yet still reads as configured, with an empty providers list.
Configured bool `json:"configured"`
// Endpoint The account's Agent Network base URL, reachable over the NetBird tunnel only. Returned to every member of a configured account - it authorizes nothing on its own, since the gateway still refuses every request no policy permits. Empty when configured is false.
Endpoint string `json:"endpoint"`
// Providers The providers at least one of the caller's policies authorizes, in creation order. Empty when no policy covers the caller.
Providers []AgentNetworkAgentConfigProvider `json:"providers"`
}
// AgentNetworkAgentConfigProvider One provider the caller may use, reduced to what a local tool needs for configuration.
type AgentNetworkAgentConfigProvider struct {
// AllModelsAllowed True when no model allowlist restricts this provider for the caller; models then lists the declared or catalog models as a courtesy.
AllModelsAllowed bool `json:"all_models_allowed"`
// ApiFlavor Request-body shape the provider speaks ("anthropic", "openai"). Empty when the gateway dispatches it by URL path instead.
ApiFlavor string `json:"api_flavor"`
// CatalogId Catalog entry id naming the provider type.
CatalogId string `json:"catalog_id"`
// Models The effective model allowlist for the caller (or the declared/catalog models when all_models_allowed is true).
Models []string `json:"models"`
// Name Operator-assigned provider label.
Name string `json:"name"`
}
// AgentNetworkBudgetRule Account-level budget rule. A limit-only rule bound to groups and/or users that applies across all policies as a min-wins ceiling. Empty targets means it applies to every caller.
type AgentNetworkBudgetRule struct {
CreatedAt *time.Time `json:"created_at,omitempty"`
@@ -2575,6 +2605,9 @@ type BundleParameters struct {
// Anonymize Whether sensitive data should be anonymized in the bundle.
Anonymize bool `json:"anonymize"`
// AnonymizeLevel How much the anonymizer redacts. "default" (or empty) keeps internal IP ranges, "strict" also anonymizes them.
AnonymizeLevel *string `json:"anonymize_level,omitempty"`
// BundleFor Whether to generate a bundle for the given timeframe.
BundleFor bool `json:"bundle_for"`
@@ -2583,6 +2616,9 @@ type BundleParameters struct {
// LogFileCount Maximum number of log files to include in the bundle.
LogFileCount int `json:"log_file_count"`
// UploadUrl Service URL the client requests an upload URL from before uploading the bundle. Empty selects the default upload server.
UploadUrl *string `json:"upload_url,omitempty"`
}
// BundleResult defines model for BundleResult.
@@ -4327,6 +4363,9 @@ type PeerLocalFlags struct {
// LazyConnectionEnabled Indicates whether lazy connection is enabled on this peer
LazyConnectionEnabled *bool `json:"lazy_connection_enabled,omitempty"`
// RemoteJobsAllowed Indicates whether the peer has opted into management-requested remote jobs (e.g. debug bundles)
RemoteJobsAllowed *bool `json:"remote_jobs_allowed,omitempty"`
// RosenpassEnabled Indicates whether Rosenpass is enabled on this peer
RosenpassEnabled *bool `json:"rosenpass_enabled,omitempty"`
@@ -0,0 +1,23 @@
package integration_reference
import (
"fmt"
"strings"
)
// IntegrationReference holds the reference to a particular integration
type IntegrationReference struct {
ID int
IntegrationType string
}
func (ir IntegrationReference) String() string {
return fmt.Sprintf("%s:%d", ir.IntegrationType, ir.ID)
}
func (ir IntegrationReference) CacheKey(path ...string) string {
if len(path) == 0 {
return ir.String()
}
return fmt.Sprintf("%s:%s", ir.String(), strings.Join(path, ":"))
}
+158 -75
View File
@@ -1,18 +1,19 @@
package networkmap
import (
"context"
"encoding/base64"
"fmt"
"net"
"net/netip"
"slices"
"strconv"
"time"
log "github.com/sirupsen/logrus"
nbdns "github.com/netbirdio/netbird/dns"
nbroute "github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/proto"
"github.com/netbirdio/netbird/shared/management/types"
)
@@ -24,7 +25,7 @@ import (
// ID scheme on the client side:
//
// Peers base64(wg_pub_key) // stable across snapshots
func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, error) {
func DecodeEnvelope(ctx context.Context, env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, error) {
full := env.GetFull()
if full == nil {
return nil, fmt.Errorf("envelope has no Full payload")
@@ -35,28 +36,28 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
Network: decodeAccountNetwork(full.Network),
AccountSettings: decodeAccountSettings(full.AccountSettings),
CustomZoneDomain: full.CustomZoneDomain,
Peers: make(map[string]*types.ComponentPeer, len(full.Peers)),
Groups: make(map[string]*types.ComponentGroup, len(full.Groups)),
Policies: make([]*types.Policy, 0, len(full.Policies)),
Routes: make([]*nbroute.Route, 0, len(full.Routes)),
NameServerGroups: make([]*nbdns.NameServerGroup, 0, len(full.NameserverGroups)),
Peers: make(map[string]*nmdata.Peer, len(full.Peers)),
Groups: make(map[string]*nmdata.Group, len(full.Groups)),
Policies: make([]*nmdata.Policy, 0, len(full.Policies)),
Routes: make([]*nmdata.Route, 0, len(full.Routes)),
NameServerGroups: make([]*nmdata.NameServerGroup, 0, len(full.NameserverGroups)),
AllDNSRecords: decodeSimpleRecords(full.AllDnsRecords),
AccountZones: decodeCustomZones(full.AccountZones),
ResourcePoliciesMap: make(map[string][]*types.Policy),
RoutersMap: make(map[string]map[string]*types.ComponentRouter),
NetworkResources: make([]*types.ComponentResource, 0, len(full.NetworkResources)),
RouterPeers: make(map[string]*types.ComponentPeer),
ResourcePoliciesMap: make(map[string][]*nmdata.Policy),
RoutersMap: make(map[string]map[string]*nmdata.NetworkRouter),
NetworkResources: make([]*nmdata.NetworkResource, 0, len(full.NetworkResources)),
RouterPeers: make(map[string]*nmdata.Peer),
AllowedUserIDs: stringSliceToSet(full.AllowedUserIds),
PostureFailedPeers: make(map[string]map[string]struct{}, len(full.PostureFailedPeers)),
GroupIDToUserIDs: make(map[string][]string, len(full.GroupIdToUserIds)),
}
if full.DnsSettings != nil {
c.DNSSettings = &types.DNSSettings{
c.DNSSettings = &nmdata.DNSSettings{
DisabledManagementGroups: full.DnsSettings.DisabledManagementGroupIds,
}
} else {
c.DNSSettings = &types.DNSSettings{}
c.DNSSettings = &nmdata.DNSSettings{}
}
// Phase 1: peers. The envelope's peers slice is index-addressed on the
@@ -98,20 +99,36 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
log.WithField("peer idx", idx).Error("unrecognized peer idx during decoding")
}
}
group := &types.ComponentGroup{
ID: groupID,
PublicID: gc.Id,
Peers: peerIDs,
fromCompactResources := func() []nmdata.Resource {
var toret []nmdata.Resource
for _, r := range gc.Resources {
res := resourceFromProto(r, peerIDByIndex)
if res == (nmdata.Resource{}) {
log.WithContext(ctx).Warnf("skipping invalid resource in group compact: %s", r.String())
continue
}
toret = append(toret, res)
}
return toret
}
group := &nmdata.Group{
PublicID: gc.Id,
Peers: peerIDs,
Resources: fromCompactResources(),
}
if gc.IsAll {
group.Name = types.GroupAllName
group.Name = nmdata.GroupAllName
}
c.Groups[groupID] = group
}
// Phase 3: policies (PolicyCompact = one rule per entry; current data
// model is 1 rule per policy).
policyByID := make(map[string]*types.Policy, len(full.Policies))
policyByID := make(map[string]*nmdata.Policy, len(full.Policies))
for i, pc := range full.Policies {
if pc == nil {
return nil, fmt.Errorf("invalid envelope: policies[%d] is nil", i)
@@ -148,7 +165,7 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
// Phase 7: routers_map (outer key = network seq id, inner key = peer-id
// reconstructed from peer_index). Synthesized network id is "net_<seq>".
for networkID, list := range full.RoutersMap {
inner := make(map[string]*types.ComponentRouter, len(list.Entries))
inner := make(map[string]*nmdata.NetworkRouter, len(list.Entries))
for _, entry := range list.Entries {
if !entry.PeerIndexSet {
continue
@@ -158,10 +175,8 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
continue
}
peerID := peerIDByIndex[entry.PeerIndex]
inner[peerID] = &types.ComponentRouter{
NetworkID: networkID,
inner[peerID] = &nmdata.NetworkRouter{
PublicID: entry.Id,
Peer: peerID,
PeerGroups: entry.PeerGroupIds,
Masquerade: entry.Masquerade,
Metric: int(entry.Metric),
@@ -180,7 +195,7 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
if len(ids.Ids) == 0 {
continue
}
policies := make([]*types.Policy, 0, len(ids.Ids))
policies := make([]*nmdata.Policy, 0, len(ids.Ids))
for _, id := range ids.Ids {
if p, ok := policyByID[id]; ok {
policies = append(policies, p)
@@ -193,6 +208,15 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
}
}
// Phase 8: rebuild resource_policies_map
for _, r := range c.NetworkResources {
policies := policiesForNetworkResource(r.ID, c.Policies, c.Groups)
if len(policies) == 0 {
continue
}
c.ResourcePoliciesMap[r.ID] = policies
}
// Phase 9: group_id_to_user_ids — wire keys are seq ids, synth to strings.
for groupId, list := range full.GroupIdToUserIds {
c.GroupIDToUserIDs[groupId] = append([]string(nil), list.UserIds...)
@@ -228,17 +252,54 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
return c, nil
}
func networkResourceGroups(resourceId string, groups map[string]*nmdata.Group) []string {
var toret []string
for _, group := range groups {
for _, resource := range group.Resources {
if resource.ID == resourceId {
toret = append(toret, group.PublicID)
}
}
}
return toret
}
func policiesForNetworkResource(resourceId string, allPolicies []*nmdata.Policy, groups map[string]*nmdata.Group) []*nmdata.Policy {
var toret []*nmdata.Policy
networkResourceGroups := networkResourceGroups(resourceId, groups)
for _, p := range allPolicies {
if p == nil || !p.Enabled || len(p.Rules) == 0 {
continue
}
// there's always only one rule in each policy
if p.Rules[0].DestinationResource.ID == resourceId {
toret = append(toret, p)
continue
}
for _, groupId := range networkResourceGroups {
if slices.Contains(p.Rules[0].Destinations, groupId) {
toret = append(toret, p)
break
}
}
}
return toret
}
// decodeAccountNetwork never returns nil — Calculate() dereferences
// c.Network unconditionally, and servers that predate the fix omit the field
// entirely from the empty-components envelope.
func decodeAccountNetwork(an *proto.AccountNetwork) *types.Network {
n := &types.Network{}
func decodeAccountNetwork(an *proto.AccountNetwork) *nmdata.Network {
n := &nmdata.Network{}
if an == nil {
return n
}
n.Identifier = an.Identifier
n.Dns = an.Dns
n.Serial = an.Serial
n.Serial = int64(an.Serial)
if an.NetCidr != "" {
if _, ipnet, err := net.ParseCIDR(an.NetCidr); err == nil && ipnet != nil {
n.Net = *ipnet
@@ -252,33 +313,51 @@ func decodeAccountNetwork(an *proto.AccountNetwork) *types.Network {
return n
}
func decodeAccountSettings(as *proto.AccountSettingsCompact) *types.AccountSettingsInfo {
func decodeAccountSettings(as *proto.AccountSettingsCompact) *nmdata.AccountSettingsInfo {
if as == nil {
return &types.AccountSettingsInfo{}
return &nmdata.AccountSettingsInfo{}
}
return &types.AccountSettingsInfo{
return &nmdata.AccountSettingsInfo{
PeerLoginExpirationEnabled: as.PeerLoginExpirationEnabled,
PeerLoginExpiration: time.Duration(as.PeerLoginExpirationNs),
}
}
func decodePeerCompact(pc *proto.PeerCompact, peerID string) *types.ComponentPeer {
peer := &types.ComponentPeer{
func decodePeerCompact(pc *proto.PeerCompact, peerID string) *nmdata.Peer {
var caps []int32
if pc.SupportsSourcePrefixes {
caps = append(caps, nmdata.PeerCapabilitySourcePrefixes)
}
if pc.SupportsIpv6 {
caps = append(caps, nmdata.PeerCapabilityIPv6Overlay)
}
peer := &nmdata.Peer{
ID: peerID,
Key: peerID,
SSHKey: string(pc.SshPubKey),
SSHEnabled: pc.SshEnabled,
DNSLabel: pc.DnsLabel,
LoginExpirationEnabled: pc.LoginExpirationEnabled,
AgentVersion: pc.AgentVersion,
SupportsSourcePrefixes: pc.SupportsSourcePrefixes,
SupportsIPv6: pc.SupportsIpv6,
ServerSSHAllowed: pc.ServerSshAllowed,
AddedWithSSOLogin: pc.AddedWithSsoLogin,
ProxyEmbedded: pc.ProxyEmbedded,
ProxyMeta: nmdata.ProxyMeta{Embedded: pc.ProxyEmbedded},
Meta: nmdata.PeerSystemMeta{
WtVersion: pc.AgentVersion,
Capabilities: caps,
Flags: nmdata.Flags{
ServerSSHAllowed: pc.ServerSshAllowed,
},
},
}
if pc.AddedWithSsoLogin {
// Set a non-empty UserID so (*Peer).AddedWithSSOLogin() returns true.
// The original UserID isn't on the wire; the value is intentionally
// visibly synthetic so any future consumer that mistakes UserID for a
// real account user xid won't silently match (or worse, write the
// sentinel into a downstream record).
peer.UserID = "<env-sso>"
}
if pc.LastLoginUnixNano != 0 {
peer.LastLogin = time.Unix(0, pc.LastLoginUnixNano)
t := time.Unix(0, pc.LastLoginUnixNano)
peer.LastLogin = &t
}
switch len(pc.Ip) {
case 4:
@@ -296,13 +375,13 @@ func decodePeerCompact(pc *proto.PeerCompact, peerID string) *types.ComponentPee
return peer
}
func decodePolicyCompact(pc *proto.PolicyCompact, policyID string, peerIDByIndex []string) *types.Policy {
rule := &types.PolicyRule{
func decodePolicyCompact(pc *proto.PolicyCompact, policyID string, peerIDByIndex []string) *nmdata.Policy {
rule := &nmdata.PolicyRule{
ID: policyID, // 1 rule per policy → reuse synthesized id
PolicyID: policyID,
Enabled: true,
Action: actionFromProto(pc.Action),
Protocol: protocolFromProto(pc.Protocol),
Action: string(actionFromProto(pc.Action)),
Protocol: string(protocolFromProto(pc.Protocol)),
Bidirectional: pc.Bidirectional,
Ports: uint32SliceToStrings(pc.Ports),
PortRanges: portRangesFromProto(pc.PortRanges),
@@ -313,11 +392,11 @@ func decodePolicyCompact(pc *proto.PolicyCompact, policyID string, peerIDByIndex
SourceResource: resourceFromProto(pc.SourceResource, peerIDByIndex),
DestinationResource: resourceFromProto(pc.DestinationResource, peerIDByIndex),
}
return &types.Policy{
return &nmdata.Policy{
ID: policyID,
PublicID: pc.Id,
Enabled: true,
Rules: []*types.PolicyRule{rule},
Rules: []*nmdata.PolicyRule{rule},
SourcePostureChecks: pc.SourcePostureCheckIds,
}
}
@@ -325,15 +404,19 @@ func decodePolicyCompact(pc *proto.PolicyCompact, policyID string, peerIDByIndex
// resourceFromProto rebuilds types.Resource. For peer-typed resources the
// peer reference is reconstructed from the envelope's peer index — wire
// format ships no xid for peers, so we use the synthesized peer id.
func resourceFromProto(r *proto.ResourceCompact, peerIDByIndex []string) types.Resource {
if r == nil {
return types.Resource{}
func resourceFromProto(r *proto.ResourceCompact, peerIDByIndex []string) nmdata.Resource {
if r == nil || !types.ResourceType(r.Type).Valid() {
return nmdata.Resource{}
}
out := types.Resource{Type: types.ResourceType(r.Type)}
if r.PeerIndexSet && int(r.PeerIndex) < len(peerIDByIndex) {
out.ID = peerIDByIndex[r.PeerIndex]
if r.Type == string(types.ResourceTypePeer) {
if !r.PeerIndexSet || int(r.PeerIndex) >= len(peerIDByIndex) {
return nmdata.Resource{}
}
return nmdata.Resource{Type: r.Type, ID: peerIDByIndex[int(r.PeerIndex)]}
}
return out
return nmdata.Resource{Type: r.Type, ID: r.Id}
}
// authorizedGroupsFromProto inverts encodeAuthorizedGroups: the wire form
@@ -354,15 +437,15 @@ func authorizedGroupsFromProto(m map[string]*proto.UserNameList) map[string][]st
return out
}
func decodeRouteRaw(rr *proto.RouteRaw, peerIDByIndex []string) *nbroute.Route {
r := &nbroute.Route{
ID: nbroute.ID(rr.Id),
func decodeRouteRaw(rr *proto.RouteRaw, peerIDByIndex []string) *nmdata.Route {
r := &nmdata.Route{
ID: rr.Id,
PublicID: rr.Id,
NetID: nbroute.NetID(rr.NetId),
NetID: rr.NetId,
Description: rr.Description,
Domains: domainsFromPunycode(rr.Domains),
KeepRoute: rr.KeepRoute,
NetworkType: nbroute.NetworkType(rr.NetworkType),
NetworkType: int(rr.NetworkType),
Masquerade: rr.Masquerade,
Metric: int(rr.Metric),
Enabled: rr.Enabled,
@@ -382,8 +465,8 @@ func decodeRouteRaw(rr *proto.RouteRaw, peerIDByIndex []string) *nbroute.Route {
return r
}
func decodeNameServerGroupRaw(nsg *proto.NameServerGroupRaw) *nbdns.NameServerGroup {
out := &nbdns.NameServerGroup{
func decodeNameServerGroupRaw(nsg *proto.NameServerGroupRaw) *nmdata.NameServerGroup {
out := &nmdata.NameServerGroup{
ID: nsg.Id,
PublicID: nsg.Id,
Groups: nsg.GroupIds,
@@ -391,13 +474,13 @@ func decodeNameServerGroupRaw(nsg *proto.NameServerGroupRaw) *nbdns.NameServerGr
Domains: nsg.Domains,
Enabled: nsg.Enabled,
SearchDomainsEnabled: nsg.SearchDomainsEnabled,
NameServers: make([]nbdns.NameServer, 0, len(nsg.Nameservers)),
NameServers: make([]nmdata.NameServer, 0, len(nsg.Nameservers)),
}
for _, ns := range nsg.Nameservers {
if addr, err := netip.ParseAddr(ns.IP); err == nil {
out.NameServers = append(out.NameServers, nbdns.NameServer{
out.NameServers = append(out.NameServers, nmdata.NameServer{
IP: addr,
NSType: nbdns.NameServerType(ns.NSType),
NSType: int(ns.NSType),
Port: int(ns.Port),
})
}
@@ -405,14 +488,14 @@ func decodeNameServerGroupRaw(nsg *proto.NameServerGroupRaw) *nbdns.NameServerGr
return out
}
func decodeNetworkResource(nr *proto.NetworkResourceRaw) *types.ComponentResource {
out := &types.ComponentResource{
func decodeNetworkResource(nr *proto.NetworkResourceRaw) *nmdata.NetworkResource {
out := &nmdata.NetworkResource{
ID: nr.Id,
PublicID: nr.Id,
NetworkID: nr.NetworkSeq,
Name: nr.Name,
Description: nr.Description,
Type: types.ComponentResourceType(nr.Type),
Type: nr.Type,
Address: nr.Address,
Domain: nr.DomainValue,
Enabled: nr.Enabled,
@@ -425,10 +508,10 @@ func decodeNetworkResource(nr *proto.NetworkResourceRaw) *types.ComponentResourc
return out
}
func decodeSimpleRecords(records []*proto.SimpleRecord) []nbdns.SimpleRecord {
out := make([]nbdns.SimpleRecord, 0, len(records))
func decodeSimpleRecords(records []*proto.SimpleRecord) []nmdata.SimpleRecord {
out := make([]nmdata.SimpleRecord, 0, len(records))
for _, r := range records {
out = append(out, nbdns.SimpleRecord{
out = append(out, nmdata.SimpleRecord{
Name: r.Name,
Type: int(r.Type),
Class: r.Class,
@@ -439,10 +522,10 @@ func decodeSimpleRecords(records []*proto.SimpleRecord) []nbdns.SimpleRecord {
return out
}
func decodeCustomZones(zones []*proto.CustomZone) []nbdns.CustomZone {
out := make([]nbdns.CustomZone, 0, len(zones))
func decodeCustomZones(zones []*proto.CustomZone) []nmdata.CustomZone {
out := make([]nmdata.CustomZone, 0, len(zones))
for _, z := range zones {
out = append(out, nbdns.CustomZone{
out = append(out, nmdata.CustomZone{
Domain: z.Domain,
Records: decodeSimpleRecords(z.Records),
SearchDomainDisabled: z.SearchDomainDisabled,
@@ -463,16 +546,16 @@ func uint32SliceToStrings(ports []uint32) []string {
return out
}
func portRangesFromProto(ranges []*proto.PortInfo_Range) []types.RulePortRange {
func portRangesFromProto(ranges []*proto.PortInfo_Range) []nmdata.RulePortRange {
if len(ranges) == 0 {
return nil
}
out := make([]types.RulePortRange, 0, len(ranges))
out := make([]nmdata.RulePortRange, 0, len(ranges))
for _, r := range ranges {
if r == nil || r.Start > 65535 || r.End > 65535 {
continue
}
out = append(out, types.RulePortRange{
out = append(out, nmdata.RulePortRange{
Start: uint16(r.Start),
End: uint16(r.End),
})
@@ -0,0 +1,61 @@
package networkmap
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
protobuf "google.golang.org/protobuf/proto"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/proto"
)
func TestDecodePolicy(t *testing.T) {
assert.Equal(t,
nmdata.Resource{Type: "peer", ID: "valid-id"},
resourceFromProto(
&proto.ResourceCompact{Type: "peer", PeerIndexSet: true, PeerIndex: uint32(1)},
[]string{"invalid-id-0", "valid-id", "invalid-id-2"}))
// check invalid peer index returns an empty resource
assert.Equal(t,
nmdata.Resource{},
resourceFromProto(
&proto.ResourceCompact{Type: "peer", PeerIndexSet: true, PeerIndex: uint32(100)},
[]string{"invalid-id-0", "valid-id", "invalid-id-2"}))
assert.Equal(t,
nmdata.Resource{Type: "domain", ID: "domain"},
resourceFromProto(
&proto.ResourceCompact{Type: "domain", Id: "domain"}, []string{}))
assert.Equal(t,
nmdata.Resource{Type: "host", ID: "host"},
resourceFromProto(
&proto.ResourceCompact{Type: "host", Id: "host"}, []string{}))
assert.Equal(t,
nmdata.Resource{Type: "subnet", ID: "subnet"},
resourceFromProto(
&proto.ResourceCompact{Type: "subnet", Id: "subnet"}, []string{}))
// an unknown resource type return an empty resource
assert.Equal(t,
nmdata.Resource{},
resourceFromProto(
&proto.ResourceCompact{Type: "boom", Id: "boom"}, []string{}))
}
// ResourceCompact fields 1-3 are the v0.77 wire contract. Retyping any of them
// makes peers on either side of the change silently drop policy resources, so
// the encoding is pinned here as raw bytes: field 1 "peer" (bytes), field 2
// true (varint), field 3 7 (varint).
func TestResourceCompactLegacyWireFormat(t *testing.T) {
legacy := []byte{0x0a, 0x04, 'p', 'e', 'e', 'r', 0x10, 0x01, 0x18, 0x07}
var decoded proto.ResourceCompact
require.NoError(t, protobuf.Unmarshal(legacy, &decoded))
assert.Equal(t, "peer", decoded.Type)
assert.True(t, decoded.PeerIndexSet)
assert.Equal(t, uint32(7), decoded.PeerIndex)
encoded, err := protobuf.Marshal(&proto.ResourceCompact{Type: "peer", PeerIndexSet: true, PeerIndex: 7})
require.NoError(t, err)
assert.Equal(t, legacy, encoded)
}
+9 -8
View File
@@ -17,10 +17,11 @@ import (
log "github.com/sirupsen/logrus"
goproto "google.golang.org/protobuf/proto"
nbdns "github.com/netbirdio/netbird/dns"
"net/netip"
nbroute "github.com/netbirdio/netbird/route"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/proto"
"github.com/netbirdio/netbird/shared/management/types"
"github.com/netbirdio/netbird/shared/netiputil"
@@ -28,7 +29,7 @@ import (
)
// ToProtocolRoutes converts a slice of typed routes to their proto form.
func ToProtocolRoutes(routes []*nbroute.Route) []*proto.Route {
func ToProtocolRoutes(routes []*nmdata.Route) []*proto.Route {
protoRoutes := make([]*proto.Route, 0, len(routes))
for _, r := range routes {
protoRoutes = append(protoRoutes, ToProtocolRoute(r))
@@ -37,7 +38,7 @@ func ToProtocolRoutes(routes []*nbroute.Route) []*proto.Route {
}
// ToProtocolRoute converts one typed route to its proto form.
func ToProtocolRoute(route *nbroute.Route) *proto.Route {
func ToProtocolRoute(route *nmdata.Route) *proto.Route {
return &proto.Route{
ID: string(route.ID),
NetID: string(route.NetID),
@@ -274,7 +275,7 @@ func ToProtocolDNSConfig(update nbdns.Config, cache DNSConfigCache, forwardPort
// AppendRemotePeerConfig appends typed peers as proto.RemotePeerConfig
// entries to dst and returns the result. localIsProxy reports whether the peer
// receiving this config is itself an embedded proxy.
func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*types.ComponentPeer, dnsName string, includeIPv6 bool, localIsProxy bool) []*proto.RemotePeerConfig {
func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nmdata.Peer, dnsName string, includeIPv6 bool, localIsProxy bool) []*proto.RemotePeerConfig {
for _, rPeer := range peers {
allowedIPs := []string{rPeer.IP.String() + "/32"}
if includeIPv6 && rPeer.IPv6.IsValid() {
@@ -285,7 +286,7 @@ func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*types.Compon
AllowedIps: allowedIPs,
SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
Fqdn: rPeer.FQDN(dnsName),
AgentVersion: rPeer.AgentVersion,
AgentVersion: rPeer.Meta.WtVersion,
LazyState: lazyStateFor(localIsProxy, rPeer),
})
}
@@ -297,8 +298,8 @@ func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*types.Compon
// proxy infrastructure is not kept permanently connected to every peer. All
// other peers follow the account-wide flag. A future admin-facing per-peer
// setting can return LazyStateEager here to force a peer always-active.
func lazyStateFor(localIsProxy bool, rPeer *types.ComponentPeer) proto.LazyState {
if localIsProxy || rPeer.ProxyEmbedded {
func lazyStateFor(localIsProxy bool, rPeer *nmdata.Peer) proto.LazyState {
if localIsProxy || rPeer.ProxyMeta.Embedded {
return proto.LazyState_LazyStateLazy
}
return proto.LazyState_LazyStateDefault
+5 -5
View File
@@ -36,7 +36,7 @@ type EnvelopeResult struct {
// dnsName is the account's DNS domain ("netbird.cloud" etc.); used when
// rebuilding the per-peer FQDNs that proto.RemotePeerConfig carries.
func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, localPeerKey, dnsName string) (*EnvelopeResult, error) {
components, err := DecodeEnvelope(env)
components, err := DecodeEnvelope(ctx, env)
if err != nil {
return nil, fmt.Errorf("decode envelope: %w", err)
}
@@ -54,8 +54,8 @@ func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, lo
}
components.PeerID = canonicalKey
includeIPv6 := localPeer.SupportsIPv6 && localPeer.IPv6.IsValid()
useSourcePrefixes := localPeer.SupportsSourcePrefixes
includeIPv6 := localPeer.SupportsIPv6() && localPeer.IPv6.IsValid()
useSourcePrefixes := localPeer.SupportsSourcePrefixes()
typedNM := components.Calculate(ctx)
@@ -74,11 +74,11 @@ func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, lo
protoNM.Routes = ToProtocolRoutes(typedNM.Routes)
protoNM.DNSConfig = ToProtocolDNSConfig(typedNM.DNSConfig, nil, dnsFwdPort)
remotePeers := AppendRemotePeerConfig(nil, typedNM.Peers, dnsName, includeIPv6, localPeer.ProxyEmbedded)
remotePeers := AppendRemotePeerConfig(nil, typedNM.Peers, dnsName, includeIPv6, localPeer.ProxyMeta.Embedded)
protoNM.RemotePeers = remotePeers
protoNM.RemotePeersIsEmpty = len(remotePeers) == 0
protoNM.OfflinePeers = AppendRemotePeerConfig(nil, typedNM.OfflinePeers, dnsName, includeIPv6, localPeer.ProxyEmbedded)
protoNM.OfflinePeers = AppendRemotePeerConfig(nil, typedNM.OfflinePeers, dnsName, includeIPv6, localPeer.ProxyMeta.Embedded)
firewallRules := ToProtocolFirewallRules(typedNM.FirewallRules, includeIPv6, useSourcePrefixes)
protoNM.FirewallRules = firewallRules
+49 -48
View File
@@ -15,6 +15,7 @@ import (
mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
"github.com/netbirdio/netbird/management/server/types"
nbnetworkmap "github.com/netbirdio/netbird/shared/management/networkmap"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/proto"
)
@@ -55,13 +56,13 @@ func TestEnvelopeToNetworkMap_RoundTrip(t *testing.T) {
func TestCalculate_FirewallRuleProtocol_NeverNetbirdSSH(t *testing.T) {
c, localPeerKey := buildSmokeComponents(t)
// Replace the smoke policy with a NetbirdSSH-protocol allow.
c.Policies = []*types.Policy{{
c.Policies = []*nmdata.Policy{{
ID: "pol-ssh", PublicID: "2", Enabled: true,
Rules: []*types.PolicyRule{{
Rules: []*nmdata.PolicyRule{{
ID: "rule-ssh",
Enabled: true,
Action: types.PolicyTrafficActionAccept,
Protocol: types.PolicyRuleProtocolNetbirdSSH,
Action: string(types.PolicyTrafficActionAccept),
Protocol: string(types.PolicyRuleProtocolNetbirdSSH),
Bidirectional: true,
Sources: []string{"group-all"},
Destinations: []string{"group-all"},
@@ -143,39 +144,39 @@ func TestDecodeEnvelope_MalformedWgKeyPeerSkipped(t *testing.T) {
func TestEnvelopeRoundTrip_AllGroupShortCircuitParity(t *testing.T) {
ctx := context.Background()
peers := map[string]*types.ComponentPeer{}
peers := map[string]*nmdata.Peer{}
for i, id := range []string{"peer-T", "peer-S", "peer-ALL", "peer-O"} {
peers[id] = &types.ComponentPeer{
ID: id,
Key: randomWgKey(t),
IP: netip.AddrFrom4([4]byte{100, 64, 0, byte(i + 1)}),
DNSLabel: id,
AgentVersion: "0.40.0",
peers[id] = &nmdata.Peer{
ID: id,
Key: randomWgKey(t),
IP: netip.AddrFrom4([4]byte{100, 64, 0, byte(i + 1)}),
DNSLabel: id,
Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"},
}
}
c := &types.NetworkMapComponents{
PeerID: "peer-T",
Network: &types.Network{
Network: &nmdata.Network{
Identifier: "net-all-groups",
Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)},
Serial: 1,
},
AccountSettings: &types.AccountSettingsInfo{},
DNSSettings: &types.DNSSettings{},
AccountSettings: &nmdata.AccountSettingsInfo{},
DNSSettings: &nmdata.DNSSettings{},
Peers: peers,
Groups: map[string]*types.ComponentGroup{
"g-src": {ID: "g-src", PublicID: "1", Name: "staff", Peers: []string{"peer-T", "peer-S"}},
"g-all": {ID: "g-all", PublicID: "2", Name: "All", Peers: []string{"peer-ALL"}},
"g-two": {ID: "g-two", PublicID: "3", Name: "second", Peers: []string{"peer-T", "peer-O"}},
Groups: map[string]*nmdata.Group{
"g-src": {PublicID: "1", Name: "staff", Peers: []string{"peer-T", "peer-S"}},
"g-all": {PublicID: "2", Name: "All", Peers: []string{"peer-ALL"}},
"g-two": {PublicID: "3", Name: "second", Peers: []string{"peer-T", "peer-O"}},
},
Policies: []*types.Policy{{
Policies: []*nmdata.Policy{{
ID: "pol-multi-dest", PublicID: "10", Enabled: true,
Rules: []*types.PolicyRule{{
Rules: []*nmdata.PolicyRule{{
ID: "rule-multi-dest",
Enabled: true,
Action: types.PolicyTrafficActionAccept,
Protocol: types.PolicyRuleProtocolALL,
Action: string(types.PolicyTrafficActionAccept),
Protocol: string(types.PolicyRuleProtocolALL),
Sources: []string{"g-src"},
Destinations: []string{"g-all", "g-two"},
}},
@@ -231,12 +232,12 @@ func TestEnvelopeToNetworkMap_EmptyComponents(t *testing.T) {
localPeerKey := randomWgKey(t)
c := types.EmptyNetworkMapComponents(&types.NetworkMapComponents{
PeerID: "peer-A",
Network: &types.Network{
Network: &nmdata.Network{
Identifier: "net-empty",
Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)},
Serial: 7,
},
Peers: map[string]*types.ComponentPeer{
Peers: map[string]*nmdata.Peer{
"peer-A": {ID: "peer-A", Key: localPeerKey, IP: netip.AddrFrom4([4]byte{100, 64, 0, 1})},
},
})
@@ -291,33 +292,33 @@ func buildSmokeComponents(t *testing.T) (*types.NetworkMapComponents, string) {
peerAKey := randomWgKey(t)
peerBKey := randomWgKey(t)
peerA := &types.ComponentPeer{
ID: "peer-A",
Key: peerAKey,
IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
DNSLabel: "peerA",
AgentVersion: "0.40.0",
peerA := &nmdata.Peer{
ID: "peer-A",
Key: peerAKey,
IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
DNSLabel: "peerA",
Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"},
}
peerB := &types.ComponentPeer{
ID: "peer-B",
Key: peerBKey,
IP: netip.AddrFrom4([4]byte{100, 64, 0, 2}),
DNSLabel: "peerB",
AgentVersion: "0.40.0",
peerB := &nmdata.Peer{
ID: "peer-B",
Key: peerBKey,
IP: netip.AddrFrom4([4]byte{100, 64, 0, 2}),
DNSLabel: "peerB",
Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"},
}
group := &types.ComponentGroup{
ID: "group-all", PublicID: "1", Name: "All",
group := &nmdata.Group{
PublicID: "1", Name: "All",
Peers: []string{"peer-A", "peer-B"},
}
policy := &types.Policy{
policy := &nmdata.Policy{
ID: "pol-allow", PublicID: "1", Enabled: true,
Rules: []*types.PolicyRule{{
Rules: []*nmdata.PolicyRule{{
ID: "rule-allow",
Enabled: true,
Action: types.PolicyTrafficActionAccept,
Protocol: types.PolicyRuleProtocolALL,
Action: string(types.PolicyTrafficActionAccept),
Protocol: string(types.PolicyRuleProtocolALL),
Bidirectional: true,
Sources: []string{"group-all"},
Destinations: []string{"group-all"},
@@ -326,21 +327,21 @@ func buildSmokeComponents(t *testing.T) (*types.NetworkMapComponents, string) {
c := &types.NetworkMapComponents{
PeerID: "peer-A",
Network: &types.Network{
Network: &nmdata.Network{
Identifier: "net-smoke",
Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)},
Serial: 1,
},
AccountSettings: &types.AccountSettingsInfo{},
DNSSettings: &types.DNSSettings{},
Peers: map[string]*types.ComponentPeer{
AccountSettings: &nmdata.AccountSettingsInfo{},
DNSSettings: &nmdata.DNSSettings{},
Peers: map[string]*nmdata.Peer{
"peer-A": peerA,
"peer-B": peerB,
},
Groups: map[string]*types.ComponentGroup{
Groups: map[string]*nmdata.Group{
"group-all": group,
},
Policies: []*types.Policy{policy},
Policies: []*nmdata.Policy{policy},
}
return c, peerAKey
}
@@ -0,0 +1,815 @@
package networkmap
import (
"slices"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/types"
)
type sshRequirements struct {
neededGroupIDs map[string]struct{}
needAllowedUserIDs bool
}
// GetPeerNetworkMapComponents computes the peer's NetworkMapComponents from the
// slim twin store. It mirrors the former Account.GetPeerNetworkMapComponents
// exactly, operating on nmdata twins throughout — no Account reference and no
// twin↔real conversion, since the produced components hold twins.
func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCustomZone nmdata.CustomZone) *types.NetworkMapComponents {
nmd.InjectProxyPolicies()
forceRoutingPeerDNS := nmd.forcesRoutingPeerDNSResolution(peerID)
peer := nmd.Peers[peerID]
if peer == nil {
return types.EmptyNetworkMapComponents(&types.NetworkMapComponents{
PeerID: peerID,
Network: nmd.Network,
Peers: map[string]*nmdata.Peer{peerID: peer},
ForceRoutingPeerDNSResolution: forceRoutingPeerDNS,
})
}
if _, ok := nmd.ValidatedPeers[peerID]; !ok {
return types.EmptyNetworkMapComponents(&types.NetworkMapComponents{
PeerID: peerID,
Network: nmd.Network,
Peers: map[string]*nmdata.Peer{peerID: peer},
ForceRoutingPeerDNSResolution: forceRoutingPeerDNS,
})
}
components := &types.NetworkMapComponents{
PeerID: peerID,
Network: nmd.Network,
AccountSettings: nmd.AccountSettings,
DNSSettings: nmd.DNSSettings,
CustomZoneDomain: peersCustomZone.Domain,
NameServerGroups: make([]*nmdata.NameServerGroup, 0),
ResourcePoliciesMap: make(map[string][]*nmdata.Policy),
RoutersMap: make(map[string]map[string]*nmdata.NetworkRouter),
NetworkResources: make([]*nmdata.NetworkResource, 0),
PostureFailedPeers: make(map[string]map[string]struct{}, len(nmd.PostureChecks)),
RouterPeers: make(map[string]*nmdata.Peer),
NetworkXIDToPublicID: nmd.NetworkXIDToPublicID,
PostureCheckXIDToPublicID: nmd.PostureCheckXIDToPublicID,
ForceRoutingPeerDNSResolution: forceRoutingPeerDNS,
}
relevantPeers, relevantGroups, relevantPolicies, relevantRoutes, sshReqs := nmd.getPeersGroupsPoliciesRoutes(peerID, peer.SSHEnabled, &components.PostureFailedPeers)
if len(sshReqs.neededGroupIDs) > 0 {
components.GroupIDToUserIDs = filterGroupIDToUserIDs(nmd.GroupIDToUserIDs, sshReqs.neededGroupIDs)
}
if sshReqs.needAllowedUserIDs {
components.AllowedUserIDs = nmd.getAllowedUserIDs()
}
components.Peers = relevantPeers
components.Groups = relevantGroups
components.Policies = relevantPolicies
components.Routes = relevantRoutes
components.AllDNSRecords = filterDNSRecordsByPeers(peersCustomZone.Records, relevantPeers, peer.SupportsIPv6() && peer.IPv6.IsValid())
peerGroups := nmd.GetPeerGroups(peerID)
components.AccountZones = nmd.appliedZones(peerGroups)
components.AccountZones = append(components.AccountZones, nmd.privateServiceZones(peerGroups)...)
for _, nsGroup := range nmd.NameServerGroups {
if nsGroup != nil && nsGroup.Enabled {
for _, gID := range nsGroup.Groups {
if _, found := relevantGroups[gID]; found {
components.NameServerGroups = append(components.NameServerGroups, nsGroup)
break
}
}
}
}
for _, resource := range nmd.NetworkResources {
if resource == nil || !resource.Enabled {
continue
}
policies, exists := nmd.ResourcePolicies[resource.ID]
if !exists {
continue
}
addSourcePeers := false
networkRoutingPeers, routerExists := nmd.Routers[resource.NetworkID]
if routerExists {
if _, ok := networkRoutingPeers[peerID]; ok {
addSourcePeers = true
}
}
for _, policy := range policies {
if policy == nil || !policy.Enabled || len(policy.Rules) == 0 || policy.Rules[0] == nil {
continue
}
if addSourcePeers {
var peers []string
if policy.Rules[0].SourceResource.Type == string(types.ResourceTypePeer) && policy.Rules[0].SourceResource.ID != "" {
peers = []string{policy.Rules[0].SourceResource.ID}
} else {
peers = nmd.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups())
}
for _, pID := range nmd.getPostureValidPeersSaveFailed(peers, policy.SourcePostureChecks, &components.PostureFailedPeers) {
if _, exists := components.Peers[pID]; !exists {
components.Peers[pID] = nmd.Peers[pID]
}
}
} else {
peerInSources := false
if policy.Rules[0].SourceResource.Type == string(types.ResourceTypePeer) && policy.Rules[0].SourceResource.ID != "" {
peerInSources = policy.Rules[0].SourceResource.ID == peerID
} else {
for _, groupID := range policy.SourceGroups() {
if group := nmd.Groups[groupID]; group != nil && slices.Contains(group.Peers, peerID) {
peerInSources = true
break
}
}
}
if !peerInSources {
continue
}
isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(policy.SourcePostureChecks, peerID)
if !isValid && len(pname) > 0 {
if _, ok := components.PostureFailedPeers[pname]; !ok {
components.PostureFailedPeers[pname] = make(map[string]struct{})
}
components.PostureFailedPeers[pname][peer.ID] = struct{}{}
continue
}
addSourcePeers = true
}
for _, rule := range policy.Rules {
if rule == nil || !rule.Enabled {
continue
}
for _, srcGroupID := range rule.Sources {
if g := nmd.Groups[srcGroupID]; g != nil {
if _, exists := components.Groups[srcGroupID]; !exists {
components.Groups[srcGroupID] = g
}
}
}
for _, dstGroupID := range rule.Destinations {
if g := nmd.Groups[dstGroupID]; g != nil {
if _, exists := components.Groups[dstGroupID]; !exists {
components.Groups[dstGroupID] = g
}
}
}
}
components.ResourcePoliciesMap[resource.ID] = policies
}
if addSourcePeers {
components.RoutersMap[resource.NetworkID] = networkRoutingPeers
for peerIDKey := range networkRoutingPeers {
p := nmd.Peers[peerIDKey]
if p == nil {
continue
}
// An unapproved peer must not carry traffic, so it is kept out of
// RouterPeers as well: the envelope encoder indexes that map into
// the wire peer table, from which the client restores every entry.
if _, validated := nmd.ValidatedPeers[peerIDKey]; !validated {
continue
}
if _, exists := components.RouterPeers[peerIDKey]; !exists {
components.RouterPeers[peerIDKey] = p
}
if _, exists := components.Peers[peerIDKey]; !exists {
components.Peers[peerIDKey] = p
}
}
components.NetworkResources = append(components.NetworkResources, resource)
}
}
filterGroupPeers(&components.Groups, components.Peers)
filterPostureFailedPeers(&components.PostureFailedPeers, components.Policies, components.ResourcePoliciesMap, components.Peers)
return components
}
func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
peerID string,
peerSSHEnabled bool,
postureFailedPeers *map[string]map[string]struct{},
) (map[string]*nmdata.Peer, map[string]*nmdata.Group, []*nmdata.Policy, []*nmdata.Route, sshRequirements) {
relevantPeerIDs := make(map[string]*nmdata.Peer, len(nmd.Peers)/4)
relevantGroupIDs := make(map[string]*nmdata.Group, len(nmd.Groups)/4)
relevantPolicies := make([]*nmdata.Policy, 0, len(nmd.Policies))
relevantRoutes := make([]*nmdata.Route, 0, len(nmd.Routes))
sshReqs := sshRequirements{neededGroupIDs: make(map[string]struct{})}
relevantPeerIDs[peerID] = nmd.Peers[peerID]
peerGroupSet := nmd.GetPeerGroups(peerID)
for groupID := range peerGroupSet {
relevantGroupIDs[groupID] = nmd.Groups[groupID]
}
routeAccessControlGroups := make(map[string]struct{})
for _, r := range nmd.Routes {
if r == nil {
continue
}
relevant := r.Peer == peerID
if !relevant {
for _, groupID := range r.PeerGroups {
if _, ok := peerGroupSet[groupID]; ok {
relevant = true
break
}
}
}
if !relevant && r.Enabled {
for _, groupID := range r.Groups {
if _, ok := peerGroupSet[groupID]; ok {
relevant = true
break
}
}
}
if !relevant {
continue
}
for _, groupID := range r.PeerGroups {
if g := nmd.Groups[groupID]; g != nil {
relevantGroupIDs[groupID] = g
}
}
for _, groupID := range r.Groups {
if g := nmd.Groups[groupID]; g != nil {
relevantGroupIDs[groupID] = g
}
}
if r.Enabled {
for _, groupID := range r.AccessControlGroups {
if g := nmd.Groups[groupID]; g != nil {
relevantGroupIDs[groupID] = g
}
routeAccessControlGroups[groupID] = struct{}{}
}
}
if r.Peer != "" {
if _, ok := nmd.ValidatedPeers[r.Peer]; ok {
if p := nmd.Peers[r.Peer]; p != nil {
relevantPeerIDs[r.Peer] = p
}
}
}
for _, groupID := range r.PeerGroups {
g := nmd.Groups[groupID]
if g == nil {
continue
}
for _, pid := range g.Peers {
if _, exists := relevantPeerIDs[pid]; exists {
continue
}
if _, ok := nmd.ValidatedPeers[pid]; !ok {
continue
}
if p := nmd.Peers[pid]; p != nil {
relevantPeerIDs[pid] = p
}
}
}
relevantRoutes = append(relevantRoutes, r)
}
for _, policy := range nmd.Policies {
if policy == nil || !policy.Enabled {
continue
}
policyRelevant := false
for _, rule := range policy.Rules {
if rule == nil || !rule.Enabled {
continue
}
if len(routeAccessControlGroups) > 0 {
for _, destGroupID := range rule.Destinations {
if _, needed := routeAccessControlGroups[destGroupID]; needed {
policyRelevant = true
for _, srcGroupID := range rule.Sources {
if g := nmd.Groups[srcGroupID]; g != nil {
relevantGroupIDs[srcGroupID] = g
}
}
for _, dstGroupID := range rule.Destinations {
if g := nmd.Groups[dstGroupID]; g != nil {
relevantGroupIDs[dstGroupID] = g
}
}
break
}
}
}
var sourcePeers, destinationPeers []string
var peerInSources, peerInDestinations bool
if rule.SourceResource.Type == string(types.ResourceTypePeer) && rule.SourceResource.ID != "" {
sourcePeers, peerInSources = nmd.getPeerFromResource(rule.SourceResource, peerID, policy.SourcePostureChecks, postureFailedPeers)
} else {
sourcePeers, peerInSources = nmd.getPeersFromGroups(rule.Sources, peerID, policy.SourcePostureChecks, postureFailedPeers)
}
if rule.DestinationResource.Type == string(types.ResourceTypePeer) && rule.DestinationResource.ID != "" {
destinationPeers, peerInDestinations = nmd.getPeerFromResource(rule.DestinationResource, peerID, nil, postureFailedPeers)
} else {
destinationPeers, peerInDestinations = nmd.getPeersFromGroups(rule.Destinations, peerID, nil, postureFailedPeers)
}
if peerInSources {
policyRelevant = true
for _, pid := range destinationPeers {
relevantPeerIDs[pid] = nmd.Peers[pid]
}
for _, dstGroupID := range rule.Destinations {
if g := nmd.Groups[dstGroupID]; g != nil {
relevantGroupIDs[dstGroupID] = g
}
}
}
if peerInDestinations {
policyRelevant = true
for _, pid := range sourcePeers {
relevantPeerIDs[pid] = nmd.Peers[pid]
}
for _, srcGroupID := range rule.Sources {
if g := nmd.Groups[srcGroupID]; g != nil {
relevantGroupIDs[srcGroupID] = g
}
}
if rule.Protocol == string(types.PolicyRuleProtocolNetbirdSSH) {
switch {
case len(rule.AuthorizedGroups) > 0:
for groupID := range rule.AuthorizedGroups {
sshReqs.neededGroupIDs[groupID] = struct{}{}
}
case rule.AuthorizedUser != "":
default:
sshReqs.needAllowedUserIDs = true
}
} else if nmdata.PolicyRuleImpliesLegacySSH(rule) && peerSSHEnabled {
sshReqs.needAllowedUserIDs = true
}
}
}
if policyRelevant {
relevantPolicies = append(relevantPolicies, policy)
}
}
return relevantPeerIDs, relevantGroupIDs, relevantPolicies, relevantRoutes, sshReqs
}
func (nmd *NetworkMapData) getPeersFromGroups(groups []string, peerID string, sourcePostureChecksIDs []string,
postureFailedPeers *map[string]map[string]struct{}) ([]string, bool) {
peerInGroups := false
filteredPeerIDs := make([]string, 0, len(groups))
seenPeerIds := make(map[string]struct{}, len(groups))
for _, gid := range groups {
group := nmd.Groups[gid]
if group == nil {
continue
}
if group.IsGroupAll() || len(groups) == 1 {
filteredPeerIDs = make([]string, 0, len(group.Peers))
peerInGroups = false
for _, pid := range group.Peers {
if !nmd.admitPolicyPeer(pid, sourcePostureChecksIDs, postureFailedPeers) {
continue
}
if pid == peerID {
peerInGroups = true
continue
}
filteredPeerIDs = append(filteredPeerIDs, pid)
}
return filteredPeerIDs, peerInGroups
}
for _, pid := range group.Peers {
if _, seen := seenPeerIds[pid]; seen {
continue
}
seenPeerIds[pid] = struct{}{}
if !nmd.admitPolicyPeer(pid, sourcePostureChecksIDs, postureFailedPeers) {
continue
}
if pid == peerID {
peerInGroups = true
continue
}
filteredPeerIDs = append(filteredPeerIDs, pid)
}
}
return filteredPeerIDs, peerInGroups
}
// getPeerFromResource resolves a rule side that names a peer directly, admitting it
// like a member of a group holding only that peer.
func (nmd *NetworkMapData) getPeerFromResource(resource nmdata.Resource, peerID string, sourcePostureChecksIDs []string,
postureFailedPeers *map[string]map[string]struct{}) ([]string, bool) {
if !nmd.admitPolicyPeer(resource.ID, sourcePostureChecksIDs, postureFailedPeers) {
return nil, false
}
if resource.ID == peerID {
return nil, true
}
return []string{resource.ID}, false
}
// admitPolicyPeer applies the per-peer admission of a rule side: the peer must exist,
// be validated and pass the rule's posture checks. A failed check is recorded in
// postureFailedPeers.
func (nmd *NetworkMapData) admitPolicyPeer(pid string, sourcePostureChecksIDs []string, postureFailedPeers *map[string]map[string]struct{}) bool {
peer, ok := nmd.Peers[pid]
if !ok || peer == nil {
return false
}
if _, ok := nmd.ValidatedPeers[pid]; !ok {
return false
}
isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(sourcePostureChecksIDs, pid)
if !isValid && len(pname) > 0 {
if _, ok := (*postureFailedPeers)[pname]; !ok {
(*postureFailedPeers)[pname] = make(map[string]struct{})
}
(*postureFailedPeers)[pname][pid] = struct{}{}
return false
}
return true
}
func (nmd *NetworkMapData) validatePostureChecksOnPeerGetFailed(sourcePostureChecksID []string, peerID string) (bool, string) {
peer, ok := nmd.Peers[peerID]
if !ok || peer == nil {
return false, ""
}
for _, postureChecksID := range sourcePostureChecksID {
if valid, cached := nmd.cachedPostureCheckResult(postureChecksID, peerID); cached {
if !valid {
return false, postureChecksID
}
continue
}
postureChecks := nmd.PostureChecks[postureChecksID]
if postureChecks == nil {
continue
}
if !postureChecks.Passes(peer) {
return false, postureChecksID
}
}
return true, ""
}
func (nmd *NetworkMapData) PrecomputePostureValidation() {
if len(nmd.PostureChecks) == 0 {
nmd.PostureValidation = nil
return
}
checkPeerIDs := make(map[string]map[string]struct{})
for _, policy := range nmd.Policies {
if policy == nil || !policy.Enabled || len(policy.SourcePostureChecks) == 0 {
continue
}
groupPeerIDs := nmd.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups())
for _, postureChecksID := range policy.SourcePostureChecks {
set := checkPeerIDs[postureChecksID]
if set == nil {
set = make(map[string]struct{}, len(groupPeerIDs))
checkPeerIDs[postureChecksID] = set
}
for _, pid := range groupPeerIDs {
set[pid] = struct{}{}
}
for _, rule := range policy.Rules {
if rule == nil {
continue
}
if rule.SourceResource.Type == string(types.ResourceTypePeer) && rule.SourceResource.ID != "" {
set[rule.SourceResource.ID] = struct{}{}
}
}
}
}
results := make(map[string]map[string]bool, len(checkPeerIDs))
for postureChecksID, peerIDs := range checkPeerIDs {
results[postureChecksID] = nmd.evaluatePostureChecksForPeers(postureChecksID, peerIDs)
}
nmd.PostureValidation = results
}
func (nmd *NetworkMapData) evaluatePostureChecksForPeers(postureChecksID string, peerIDs map[string]struct{}) map[string]bool {
postureChecks := nmd.PostureChecks[postureChecksID]
if postureChecks == nil {
return nil
}
checks := postureChecks.GetChecks()
results := make(map[string]bool, len(peerIDs))
for peerID := range peerIDs {
peer := nmd.Peers[peerID]
if peer == nil {
continue
}
results[peerID] = nmdata.PassesChecks(checks, peer)
}
return results
}
func (nmd *NetworkMapData) cachedPostureCheckResult(postureChecksID, peerID string) (bool, bool) {
results, ok := nmd.PostureValidation[postureChecksID]
if !ok {
return false, false
}
if results == nil {
return true, true
}
valid, found := results[peerID]
return valid, found
}
func (nmd *NetworkMapData) getPostureValidPeersSaveFailed(inputPeers []string, postureChecksIDs []string, postureFailedPeers *map[string]map[string]struct{}) []string {
var dest []string
for _, peerID := range inputPeers {
if _, validated := nmd.ValidatedPeers[peerID]; !validated {
continue
}
valid, pname := nmd.validatePostureChecksOnPeerGetFailed(postureChecksIDs, peerID)
if valid {
dest = append(dest, peerID)
continue
}
if pname == "" {
continue
}
if _, ok := (*postureFailedPeers)[pname]; !ok {
(*postureFailedPeers)[pname] = make(map[string]struct{})
}
(*postureFailedPeers)[pname][peerID] = struct{}{}
}
return dest
}
// forcesRoutingPeerDNSResolution reports whether the given peer must run
// routing-peer DNS resolution regardless of the account-global
// RoutingPeerDNSResolutionEnabled setting: true when the peer routes a domain
// network resource targeted by an enabled reverse-proxy service, so the peer's
// DNS forwarder starts and can resolve the target for the embedded proxy peers.
func (nmd *NetworkMapData) forcesRoutingPeerDNSResolution(peerID string) bool {
if len(nmd.ProxyTargetedDomainResourceIDs) == 0 {
return false
}
for _, resource := range nmd.NetworkResources {
if resource == nil || !resource.Enabled || resource.Type != string(types.ResourceTypeDomain) {
continue
}
if _, ok := nmd.ProxyTargetedDomainResourceIDs[resource.ID]; !ok {
continue
}
if _, isRouter := nmd.Routers[resource.NetworkID][peerID]; isRouter {
return true
}
}
return false
}
// GetPeerGroups returns the set of group IDs the peer belongs to. The
// underlying peer→groups index is built once per NetworkMapData and the
// returned set is shared — callers must not mutate it.
func (nmd *NetworkMapData) GetPeerGroups(peerID string) map[string]struct{} {
nmd.peerGroupsOnce.Do(func() {
idx := make(map[string]map[string]struct{}, len(nmd.Peers))
for groupID, group := range nmd.Groups {
if group == nil {
continue
}
for _, pid := range group.Peers {
set, ok := idx[pid]
if !ok {
set = make(map[string]struct{})
idx[pid] = set
}
set[groupID] = struct{}{}
}
}
nmd.peerGroupsIdx = idx
})
if set, ok := nmd.peerGroupsIdx[peerID]; ok {
return set
}
return map[string]struct{}{}
}
func (nmd *NetworkMapData) getUniquePeerIDsFromGroupsIDs(groups []string) []string {
peerIDs := make(map[string]struct{}, len(groups))
for _, groupID := range groups {
group := nmd.Groups[groupID]
if group == nil {
continue
}
if group.IsGroupAll() || len(groups) == 1 {
return group.Peers
}
for _, peerID := range group.Peers {
peerIDs[peerID] = struct{}{}
}
}
ids := make([]string, 0, len(peerIDs))
for peerID := range peerIDs {
ids = append(ids, peerID)
}
return ids
}
func (nmd *NetworkMapData) getAllowedUserIDs() map[string]struct{} {
return nmd.AllowedUserIDs
}
func (nmd *NetworkMapData) appliedZones(peerGroups map[string]struct{}) []nmdata.CustomZone {
if len(peerGroups) == 0 {
return nil
}
var out []nmdata.CustomZone
for _, cand := range nmd.AppliedZoneCandidates {
if peerInDistributionGroups(peerGroups, cand.DistributionGroups) {
out = append(out, cand.Zone)
}
}
return out
}
func (nmd *NetworkMapData) privateServiceZones(peerGroups map[string]struct{}) []nmdata.CustomZone {
byApex := make(map[string]*nmdata.CustomZone)
var order []string
for _, cand := range nmd.PrivateServiceCandidates {
if !peerInDistributionGroups(peerGroups, cand.AccessGroups) {
continue
}
zone, exists := byApex[cand.Zone.Domain]
if !exists {
nz := nmdata.CustomZone{
Domain: cand.Zone.Domain,
SearchDomainDisabled: cand.Zone.SearchDomainDisabled,
NonAuthoritative: cand.Zone.NonAuthoritative,
}
byApex[cand.Zone.Domain] = &nz
zone = &nz
order = append(order, cand.Zone.Domain)
}
zone.Records = append(zone.Records, cand.Zone.Records...)
}
var out []nmdata.CustomZone
for _, apex := range order {
zone := byApex[apex]
if len(zone.Records) == 0 {
continue
}
out = append(out, *zone)
}
return out
}
func peerInDistributionGroups(peerGroups map[string]struct{}, groups []string) bool {
for _, g := range groups {
if _, ok := peerGroups[g]; ok {
return true
}
}
return false
}
func filterGroupPeers(groups *map[string]*nmdata.Group, peers map[string]*nmdata.Peer) {
for groupID, groupInfo := range *groups {
filteredPeers := make([]string, 0, len(groupInfo.Peers))
for _, pid := range groupInfo.Peers {
if _, exists := peers[pid]; exists {
filteredPeers = append(filteredPeers, pid)
}
}
if len(filteredPeers) != len(groupInfo.Peers) {
ng := groupInfo.Copy()
ng.Peers = filteredPeers
(*groups)[groupID] = ng
}
}
}
func filterPostureFailedPeers(postureFailedPeers *map[string]map[string]struct{}, policies []*nmdata.Policy, resourcePoliciesMap map[string][]*nmdata.Policy, peers map[string]*nmdata.Peer) {
if len(*postureFailedPeers) == 0 {
return
}
referencedPostureChecks := make(map[string]struct{})
for _, policy := range policies {
for _, checkID := range policy.SourcePostureChecks {
referencedPostureChecks[checkID] = struct{}{}
}
}
for _, resPolicies := range resourcePoliciesMap {
for _, policy := range resPolicies {
for _, checkID := range policy.SourcePostureChecks {
referencedPostureChecks[checkID] = struct{}{}
}
}
}
for checkID, failedPeers := range *postureFailedPeers {
if _, referenced := referencedPostureChecks[checkID]; !referenced {
delete(*postureFailedPeers, checkID)
continue
}
for peerID := range failedPeers {
if _, exists := peers[peerID]; !exists {
delete(failedPeers, peerID)
}
}
if len(failedPeers) == 0 {
delete(*postureFailedPeers, checkID)
}
}
}
func filterDNSRecordsByPeers(records []nmdata.SimpleRecord, peers map[string]*nmdata.Peer, includeIPv6 bool) []nmdata.SimpleRecord {
if len(records) == 0 || len(peers) == 0 {
return nil
}
peerIPs := make(map[string]struct{}, len(peers)*2)
for _, peer := range peers {
if peer == nil {
continue
}
peerIPs[peer.IP.String()] = struct{}{}
if includeIPv6 && peer.IPv6.IsValid() {
peerIPs[peer.IPv6.String()] = struct{}{}
}
}
filteredRecords := make([]nmdata.SimpleRecord, 0, len(records))
for _, record := range records {
if _, exists := peerIPs[record.RData]; exists {
filteredRecords = append(filteredRecords, record)
}
}
return filteredRecords
}
func filterGroupIDToUserIDs(fullMap map[string][]string, neededGroupIDs map[string]struct{}) map[string][]string {
if len(neededGroupIDs) == 0 {
return nil
}
filtered := make(map[string][]string, len(neededGroupIDs))
for groupID := range neededGroupIDs {
if users, ok := fullMap[groupID]; ok {
filtered[groupID] = users
}
}
return filtered
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,79 @@
package networkmap
import (
"sync"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
// NetworkMapData is a dependency-light, slim twin of the server Account. It
// carries only the state GetPeerNetworkMapComponents needs, expressed in the
// fresh nmdata twin types. A builder converts an Account into a NetworkMapData
// once per account; the per-peer components calculation then runs on this twin
// with no reference back to the Account.
type NetworkMapData struct { //nolint:revive // established name across the codebase
Peers map[string]*nmdata.Peer
Groups map[string]*nmdata.Group
Policies []*nmdata.Policy
Routes []*nmdata.Route
NameServerGroups []*nmdata.NameServerGroup
NetworkResources []*nmdata.NetworkResource
Network *nmdata.Network
DNSSettings *nmdata.DNSSettings
AccountSettings *nmdata.AccountSettingsInfo
PostureChecks map[string]*nmdata.PostureChecks
// PostureValidation holds the precomputed posture-check results, keyed by
// posture check ID then peer ID. Filled by PrecomputePostureValidation; a
// present but nil inner map marks a check ID that resolves to no posture
// check, which the calc treats as passing.
PostureValidation map[string]map[string]bool
AllowedUserIDs map[string]struct{}
NetworkXIDToPublicID map[string]string
PostureCheckXIDToPublicID map[string]string
ValidatedPeers map[string]struct{}
ResourcePolicies map[string][]*nmdata.Policy
Routers map[string]map[string]*nmdata.NetworkRouter
GroupIDToUserIDs map[string][]string
DNSDomain string
// ProxyTargetedDomainResourceIDs is the account-level half of
// forcesRoutingPeerDNSResolution: domain network resources targeted by an
// enabled reverse-proxy service.
ProxyTargetedDomainResourceIDs map[string]struct{}
AppliedZoneCandidates []AppliedZoneCandidate
PrivateServiceCandidates []PrivateServiceCandidate
// Services are the account's reverse-proxy services, persisted ones and
// the in-memory ones synthesised from agent-network state. They are the
// source of the proxy ACLs injectProxyPolicies synthesises, which no
// builder can load because they are never written to the database.
Services []*nmdata.Service
peerGroupsOnce sync.Once
peerGroupsIdx map[string]map[string]struct{}
proxyPoliciesOnce sync.Once
}
// AppliedZoneCandidate is an account-level custom DNS zone reduced to the
// per-peer decision the components calc still makes: include the zone only when
// the peer belongs to one of its distribution groups. Record conversion is done
// once at build time.
type AppliedZoneCandidate struct {
DistributionGroups []string
Zone nmdata.CustomZone
}
// PrivateServiceCandidate is a single private service's synthesized records,
// carried per apex zone. The builder resolves proxy-cluster connectivity and
// domain-suffix matching once; the calc merges the candidates whose AccessGroups
// the peer belongs to, grouped by Zone.Domain.
type PrivateServiceCandidate struct {
AccessGroups []string
Zone nmdata.CustomZone
}
@@ -0,0 +1,18 @@
package nmdata
import "time"
// AccountSettingsInfo is the slim twin of types.AccountSettingsInfo.
type AccountSettingsInfo struct {
PeerLoginExpirationEnabled bool
PeerLoginExpiration time.Duration
PeerInactivityExpirationEnabled bool
PeerInactivityExpiration time.Duration
DNSDomain string
IPv6EnabledGroups []string
RoutingPeerDNSResolutionEnabled bool
LazyConnectionEnabled bool
AutoUpdateVersion string
AutoUpdateAlways bool
MetricsPushEnabled bool
}
@@ -0,0 +1,18 @@
package nmdata
// SimpleRecord is the slim twin of dns.SimpleRecord.
type SimpleRecord struct {
Name string
Type int
Class string
TTL int
RData string
}
// CustomZone is the slim twin of dns.CustomZone.
type CustomZone struct {
Domain string
Records []SimpleRecord
SearchDomainDisabled bool
NonAuthoritative bool
}
@@ -0,0 +1,6 @@
package nmdata
// DNSSettings is the slim twin of types.DNSSettings.
type DNSSettings struct {
DisabledManagementGroups []string
}
@@ -0,0 +1,30 @@
package nmdata
import "slices"
// GroupAllName is the reserved name of the default group that contains every
// peer in an account.
const GroupAllName = "All"
// Group is the slim twin of types.Group.
type Group struct {
ID string
Name string
PublicID string
Peers []string
Resources []Resource
}
func (g *Group) IsGroupAll() bool {
return g.Name == GroupAllName
}
func (g *Group) Copy() *Group {
return &Group{
ID: g.ID,
Name: g.Name,
PublicID: g.PublicID,
Peers: slices.Clone(g.Peers),
Resources: slices.Clone(g.Resources),
}
}
@@ -0,0 +1,84 @@
package nmdata
import (
"reflect"
"testing"
)
// TestGroupCopy_AllFieldsCopied fills every Group field with a unique non-zero
// value derived from its field path, so a field added to Group but forgotten
// in Copy fails here by name without the test needing an update. The unique
// per-path values also catch fields swapped inside Copy.
func TestGroupCopy_AllFieldsCopied(t *testing.T) {
src := &Group{}
seed := 0
fillValue(t, reflect.ValueOf(src).Elem(), "Group", &seed)
copied := src.Copy()
srcV := reflect.ValueOf(src).Elem()
copiedV := reflect.ValueOf(copied).Elem()
for i := 0; i < srcV.NumField(); i++ {
name := srcV.Type().Field(i).Name
if !reflect.DeepEqual(srcV.Field(i).Interface(), copiedV.Field(i).Interface()) {
t.Errorf("field %s not copied: src=%#v copy=%#v",
name, srcV.Field(i).Interface(), copiedV.Field(i).Interface())
}
}
for i := 0; i < srcV.NumField(); i++ {
f := srcV.Field(i)
if f.Kind() != reflect.Slice || f.Len() == 0 {
continue
}
name := srcV.Type().Field(i).Name
fillValue(t, f.Index(0), name+"-mutated", &seed)
if reflect.DeepEqual(f.Interface(), copiedV.Field(i).Interface()) {
t.Errorf("field %s shares memory with the copy", name)
}
}
}
// fillValue sets v to a deterministic non-zero value derived from its field
// path. Kinds it does not handle fail the test loudly, so the filler is
// extended together with the struct instead of silently under-testing new
// fields.
func fillValue(t *testing.T, v reflect.Value, path string, seed *int) {
t.Helper()
switch v.Kind() {
case reflect.String:
v.SetString(path)
case reflect.Bool:
v.SetBool(true)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
*seed++
v.SetInt(int64(*seed))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
*seed++
v.SetUint(uint64(*seed))
case reflect.Float32, reflect.Float64:
*seed++
v.SetFloat(float64(*seed))
case reflect.Slice:
s := reflect.MakeSlice(v.Type(), 2, 2)
fillValue(t, s.Index(0), path+"[0]", seed)
fillValue(t, s.Index(1), path+"[1]", seed)
v.Set(s)
case reflect.Struct:
settable := 0
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
if !f.CanSet() {
continue
}
settable++
fillValue(t, f, path+"."+v.Type().Field(i).Name, seed)
}
if settable == 0 {
t.Fatalf("struct %s at %s has no settable fields — extend fillValue to construct it", v.Type(), path)
}
default:
t.Fatalf("unsupported kind %s at %s — extend fillValue", v.Kind(), path)
}
}
@@ -0,0 +1,24 @@
package nmdata
import "net/netip"
// NameServerGroup is the slim twin of dns.NameServerGroup.
type NameServerGroup struct {
ID string
PublicID string
Name string
Description string
NameServers []NameServer
Groups []string
Primary bool
Domains []string
Enabled bool
SearchDomainsEnabled bool
}
// NameServer is the slim twin of dns.NameServer.
type NameServer struct {
IP netip.Addr
NSType int
Port int
}
@@ -0,0 +1,16 @@
package nmdata
import "net"
// Network is the slim twin of types.Network.
type Network struct {
Identifier string
Net net.IPNet
NetV6 net.IPNet
Dns string
Serial int64
}
func (n *Network) CurrentSerial() uint64 {
return uint64(n.Serial)
}
@@ -0,0 +1,18 @@
package nmdata
import "net/netip"
// NetworkResource is the slim twin of resources/types.NetworkResource.
type NetworkResource struct {
ID string
NetworkID string
AccountID string
PublicID string
Name string
Description string
Type string
Address string // TODO: isn't persisted in the DB
Domain string
Prefix netip.Prefix
Enabled bool
}
@@ -0,0 +1,10 @@
package nmdata
// NetworkRouter is the slim twin of routers/types.NetworkRouter.
type NetworkRouter struct {
PublicID string
PeerGroups []string
Masquerade bool
Metric int
Enabled bool
}
+129
View File
@@ -0,0 +1,129 @@
package nmdata
import (
"net"
"net/netip"
"slices"
"time"
)
// Peer capability constants mirror the proto enum values.
const (
PeerCapabilitySourcePrefixes int32 = 1
PeerCapabilityIPv6Overlay int32 = 2
PeerCapabilityComponentNetworkMap int32 = 3
)
// Peer is the slim twin of peer.Peer.
type Peer struct {
ID string
Key string
SSHKey string
DNSLabel string
UserID string
SSHEnabled bool
LoginExpirationEnabled bool
LastLogin *time.Time
IP netip.Addr
IPv6 netip.Addr
RequiresApproval bool
ExtraDNSLabels []string
Meta PeerSystemMeta
ProxyMeta ProxyMeta
Location PeerLocation
}
// ProxyMeta is the slim twin of peer.ProxyMeta.
type ProxyMeta struct {
Embedded bool
Cluster string
}
// PeerSystemMeta is the slim twin of peer.PeerSystemMeta.
type PeerSystemMeta struct {
WtVersion string
GoOS string
OSVersion string
KernelVersion string
NetworkAddresses []NetworkAddress
Files []File
Capabilities []int32
Flags Flags
SyncMessageVersion int
}
// Flags is the slim twin of peer.Flags.
type Flags struct {
ServerSSHAllowed bool
DisableIPv6 bool
}
// NetworkAddress is the slim twin of peer.NetworkAddress.
type NetworkAddress struct {
NetIP netip.Prefix
}
// File is the slim twin of peer.File.
type File struct {
Path string
ProcessIsRunning bool
}
// PeerLocation is the slim twin of peer.Location.
type PeerLocation struct {
CountryCode string
CityName string
ConnectionIP net.IP
}
func (p *Peer) HasCapability(capability int32) bool {
return slices.Contains(p.Meta.Capabilities, capability)
}
func (p *Peer) SupportsIPv6() bool {
return !p.Meta.Flags.DisableIPv6 && p.HasCapability(PeerCapabilityIPv6Overlay)
}
func (p *Peer) SupportsSourcePrefixes() bool {
return p.HasCapability(PeerCapabilitySourcePrefixes)
}
func (p *Peer) AddedWithSSOLogin() bool {
return p.UserID != ""
}
func (p *Peer) FQDN(dnsDomain string) string {
if dnsDomain == "" {
return ""
}
return p.DNSLabel + "." + dnsDomain
}
func (p *Peer) GetLastLogin() time.Time {
if p.LastLogin != nil {
return *p.LastLogin
}
return time.Time{}
}
// SessionExpiresAt mirrors peer.Peer.SessionExpiresAt.
func (p *Peer) SessionExpiresAt(accountExpirationEnabled bool, expiresIn time.Duration) time.Time {
if !accountExpirationEnabled || !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled {
return time.Time{}
}
last := p.GetLastLogin()
if last.IsZero() {
return time.Time{}
}
return last.Add(expiresIn).UTC()
}
func (p *Peer) LoginExpired(expiresIn time.Duration) (bool, time.Duration) {
if !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled {
return false, 0
}
expiresAt := p.GetLastLogin().Add(expiresIn)
now := time.Now()
timeLeft := expiresAt.Sub(now)
return timeLeft <= 0, timeLeft
}
@@ -0,0 +1,96 @@
package nmdata
const (
policyRuleProtocolALL = "all"
policyRuleProtocolTCP = "tcp"
defaultSSHPortString = "22"
nativeSSHPortString = "22022"
defaultSSHPortNumber uint16 = 22
nativeSSHPortNumber uint16 = 22022
)
// Policy is the slim twin of types.Policy.
type Policy struct {
ID string
PublicID string
Enabled bool
SourcePostureChecks []string
Rules []*PolicyRule
}
// PolicyRule is the slim twin of types.PolicyRule.
type PolicyRule struct {
ID string
PolicyID string
Enabled bool
Action string
Protocol string
Bidirectional bool
Sources []string
Destinations []string
SourceResource Resource
DestinationResource Resource
Ports []string
PortRanges []RulePortRange
AuthorizedGroups map[string][]string
AuthorizedUser string
}
// RulePortRange is the slim twin of types.RulePortRange.
type RulePortRange struct {
Start uint16
End uint16
}
// Resource is the slim twin of types.Resource.
type Resource struct {
ID string
Type string
}
func (p *Policy) SourceGroups() []string {
if len(p.Rules) == 1 && p.Rules[0] != nil {
return p.Rules[0].Sources
}
groups := make(map[string]struct{}, len(p.Rules))
for _, rule := range p.Rules {
if rule == nil {
continue
}
for _, source := range rule.Sources {
groups[source] = struct{}{}
}
}
groupIDs := make([]string, 0, len(groups))
for groupID := range groups {
groupIDs = append(groupIDs, groupID)
}
return groupIDs
}
// PolicyRuleImpliesLegacySSH is the twin-typed sibling of types.PolicyRuleImpliesLegacySSH.
func PolicyRuleImpliesLegacySSH(rule *PolicyRule) bool {
return rule.Protocol == policyRuleProtocolALL ||
(rule.Protocol == policyRuleProtocolTCP && (portsIncludesSSH(rule.Ports) || portRangeIncludesSSH(rule.PortRanges)))
}
func portRangeIncludesSSH(portRanges []RulePortRange) bool {
for _, pr := range portRanges {
if (pr.Start <= defaultSSHPortNumber && pr.End >= defaultSSHPortNumber) || (pr.Start <= nativeSSHPortNumber && pr.End >= nativeSSHPortNumber) {
return true
}
}
return false
}
func portsIncludesSSH(ports []string) bool {
for _, port := range ports {
if port == defaultSSHPortString || port == nativeSSHPortString {
return true
}
}
return false
}
@@ -0,0 +1,83 @@
package nmdata
const (
checkActionAllow = "allow"
checkActionDeny = "deny"
)
// PostureChecks is the slim twin of posture.Checks.
type PostureChecks struct {
ID string
Checks ChecksDefinition
}
// ChecksDefinition is the slim twin of posture.ChecksDefinition.
type ChecksDefinition struct {
NBVersionCheck *NBVersionCheck
OSVersionCheck *OSVersionCheck
GeoLocationCheck *GeoLocationCheck
PeerNetworkRangeCheck *PeerNetworkRangeCheck
ProcessCheck *ProcessCheck
}
// Check is the slim twin of posture.Check. It is sealed: only the check types
// in this package implement it.
type Check interface {
check(peer *Peer) (bool, error)
}
// Passes reports whether the peer satisfies every check in this bundle. It
// mirrors the server posture path: a check returning (false, _) — including on
// an evaluation error — fails the bundle.
func (pc *PostureChecks) Passes(peer *Peer) bool {
return PassesChecks(pc.GetChecks(), peer)
}
// PassesChecks is Passes over an already built check set, for callers that
// evaluate many peers against the same bundle.
func PassesChecks(checks []Check, peer *Peer) bool {
for _, c := range checks {
valid, _ := c.check(peer)
if !valid {
return false
}
}
return true
}
// PostureVerdictChanged reports whether any check in the bundles gives a different
// verdict for newPeer than for oldPeer. Checks are replayed one by one, so a change
// that moves a field but stays on the same side of a threshold does not count. An
// evaluation error is a deny, like in PassesChecks.
func PostureVerdictChanged(checks []*PostureChecks, oldPeer, newPeer *Peer) bool {
for _, pc := range checks {
for _, c := range pc.GetChecks() {
single := []Check{c}
if PassesChecks(single, oldPeer) != PassesChecks(single, newPeer) {
return true
}
}
}
return false
}
// GetChecks returns the initialized checks in the same order as posture.Checks.GetChecks.
func (pc *PostureChecks) GetChecks() []Check {
var checks []Check
if pc.Checks.NBVersionCheck != nil {
checks = append(checks, pc.Checks.NBVersionCheck)
}
if pc.Checks.OSVersionCheck != nil {
checks = append(checks, pc.Checks.OSVersionCheck)
}
if pc.Checks.GeoLocationCheck != nil {
checks = append(checks, pc.Checks.GeoLocationCheck)
}
if pc.Checks.PeerNetworkRangeCheck != nil {
checks = append(checks, pc.Checks.PeerNetworkRangeCheck)
}
if pc.Checks.ProcessCheck != nil {
checks = append(checks, pc.Checks.ProcessCheck)
}
return checks
}
@@ -0,0 +1,45 @@
package nmdata
import "fmt"
// GeoLocation is the slim twin of posture.Location.
type GeoLocation struct {
CountryCode string
CityName string
}
// GeoLocationCheck is the slim twin of posture.GeoLocationCheck.
type GeoLocationCheck struct {
Locations []GeoLocation
Action string
}
func (g *GeoLocationCheck) check(peer *Peer) (bool, error) {
if peer.Location.CountryCode == "" && peer.Location.CityName == "" {
return false, fmt.Errorf("peer's location is not set")
}
for _, loc := range g.Locations {
if loc.CountryCode == peer.Location.CountryCode {
if loc.CityName == "" || loc.CityName == peer.Location.CityName {
switch g.Action {
case checkActionDeny:
return false, nil
case checkActionAllow:
return true, nil
default:
return false, fmt.Errorf("invalid geo location action: %s", g.Action)
}
}
}
}
if g.Action == checkActionDeny {
return true, nil
}
if g.Action == checkActionAllow {
return false, nil
}
return false, fmt.Errorf("invalid geo location action: %s", g.Action)
}
@@ -0,0 +1,38 @@
package nmdata
import (
"strings"
"github.com/hashicorp/go-version"
)
// NBVersionCheck is the slim twin of posture.NBVersionCheck.
type NBVersionCheck struct {
MinVersion string
}
func (n *NBVersionCheck) check(peer *Peer) (bool, error) {
return meetsMinVersion(n.MinVersion, peer.Meta.WtVersion)
}
func meetsMinVersion(minVer, peerVer string) (bool, error) {
peerVer = sanitizeVersion(peerVer)
minVer = sanitizeVersion(minVer)
peerNBVer, err := version.NewVersion(peerVer)
if err != nil {
return false, err
}
constraints, err := version.NewConstraint(">= " + minVer)
if err != nil {
return false, err
}
return constraints.Check(peerNBVer), nil
}
func sanitizeVersion(v string) string {
parts := strings.Split(v, "-")
return parts[0]
}
@@ -0,0 +1,62 @@
package nmdata
import (
"fmt"
"net/netip"
)
// PeerNetworkRangeCheck is the slim twin of posture.PeerNetworkRangeCheck.
type PeerNetworkRangeCheck struct {
Action string
Ranges []netip.Prefix
}
func (p *PeerNetworkRangeCheck) check(peer *Peer) (bool, error) {
peerPrefixes := make([]netip.Prefix, 0, len(peer.Meta.NetworkAddresses)+1)
for _, peerNetAddr := range peer.Meta.NetworkAddresses {
peerPrefixes = append(peerPrefixes, peerNetAddr.NetIP)
}
if connIP := peer.Location.ConnectionIP; len(connIP) > 0 {
if addr, ok := netip.AddrFromSlice(connIP); ok {
addr = addr.Unmap()
peerPrefixes = append(peerPrefixes, netip.PrefixFrom(addr, addr.BitLen()))
}
}
if len(peerPrefixes) == 0 {
return false, fmt.Errorf("peer's does not contain peer network range addresses")
}
for _, peerPrefix := range peerPrefixes {
for _, rangePrefix := range p.Ranges {
if !prefixContains(rangePrefix, peerPrefix) {
continue
}
switch p.Action {
case checkActionDeny:
return false, nil
case checkActionAllow:
return true, nil
default:
return false, fmt.Errorf("invalid peer network range check action: %s", p.Action)
}
}
}
if p.Action == checkActionDeny {
return true, nil
}
if p.Action == checkActionAllow {
return false, nil
}
return false, fmt.Errorf("invalid peer network range check action: %s", p.Action)
}
func prefixContains(outer, inner netip.Prefix) bool {
outer = outer.Masked()
inner = inner.Masked()
return outer.Bits() <= inner.Bits() &&
outer.Addr().BitLen() == inner.Addr().BitLen() &&
outer.Contains(inner.Addr())
}
@@ -0,0 +1,79 @@
package nmdata
import (
"strings"
"github.com/hashicorp/go-version"
)
// MinVersionCheck is the slim twin of posture.MinVersionCheck.
type MinVersionCheck struct {
MinVersion string
}
// MinKernelVersionCheck is the slim twin of posture.MinKernelVersionCheck.
type MinKernelVersionCheck struct {
MinKernelVersion string
}
// OSVersionCheck is the slim twin of posture.OSVersionCheck.
type OSVersionCheck struct {
Android *MinVersionCheck
Darwin *MinVersionCheck
Ios *MinVersionCheck
Linux *MinKernelVersionCheck
Windows *MinKernelVersionCheck
}
func (c *OSVersionCheck) check(peer *Peer) (bool, error) {
switch peer.Meta.GoOS {
case "android":
return checkMinVersion(peer.Meta.OSVersion, c.Android)
case "darwin":
return checkMinVersion(peer.Meta.OSVersion, c.Darwin)
case "ios":
return checkMinVersion(peer.Meta.OSVersion, c.Ios)
case "linux":
kernelVersion := strings.Split(peer.Meta.KernelVersion, "-")[0]
return checkMinKernelVersion(kernelVersion, c.Linux)
case "windows":
return checkMinKernelVersion(peer.Meta.KernelVersion, c.Windows)
}
return true, nil
}
func checkMinVersion(peerVersion string, check *MinVersionCheck) (bool, error) {
if check == nil {
return false, nil
}
peerNBVersion, err := version.NewVersion(peerVersion)
if err != nil {
return false, err
}
constraints, err := version.NewConstraint(">= " + check.MinVersion)
if err != nil {
return false, err
}
return constraints.Check(peerNBVersion), nil
}
func checkMinKernelVersion(peerVersion string, check *MinKernelVersionCheck) (bool, error) {
if check == nil {
return false, nil
}
peerNBVersion, err := version.NewVersion(peerVersion)
if err != nil {
return false, err
}
constraints, err := version.NewConstraint(">= " + check.MinKernelVersion)
if err != nil {
return false, err
}
return constraints.Check(peerNBVersion), nil
}
@@ -0,0 +1,56 @@
package nmdata
import (
"fmt"
"slices"
)
// Process is the slim twin of posture.Process.
type Process struct {
LinuxPath string
MacPath string
WindowsPath string
}
// ProcessCheck is the slim twin of posture.ProcessCheck.
type ProcessCheck struct {
Processes []Process
}
func (p *ProcessCheck) check(peer *Peer) (bool, error) {
peerActiveProcesses := extractPeerActiveProcesses(peer.Meta.Files)
var pathSelector func(Process) string
switch peer.Meta.GoOS {
case "linux":
pathSelector = func(process Process) string { return process.LinuxPath }
case "darwin":
pathSelector = func(process Process) string { return process.MacPath }
case "windows":
pathSelector = func(process Process) string { return process.WindowsPath }
default:
return false, fmt.Errorf("unsupported peer's operating system: %s", peer.Meta.GoOS)
}
return p.areAllProcessesRunning(peerActiveProcesses, pathSelector), nil
}
func (p *ProcessCheck) areAllProcessesRunning(activeProcesses []string, pathSelector func(Process) string) bool {
for _, process := range p.Processes {
path := pathSelector(process)
if path == "" || !slices.Contains(activeProcesses, path) {
return false
}
}
return true
}
func extractPeerActiveProcesses(files []File) []string {
activeProcesses := make([]string, 0, len(files))
for _, file := range files {
if file.ProcessIsRunning {
activeProcesses = append(activeProcesses, file.Path)
}
}
return activeProcesses
}
@@ -0,0 +1,54 @@
package nmdata
import (
"testing"
"github.com/stretchr/testify/assert"
)
func bundle(def ChecksDefinition) []*PostureChecks {
return []*PostureChecks{{Checks: def}}
}
func TestPostureVerdictChanged_ErrorCountsAsDeny(t *testing.T) {
c := bundle(ChecksDefinition{NBVersionCheck: &NBVersionCheck{MinVersion: "1.2.0"}})
tests := []struct {
name string
oldVer, newVer string
want bool
}{
{"both above min, no flip", "1.3.0", "1.4.0", false},
{"crosses up below->above", "1.1.0", "1.3.0", true},
{"unparsable old only -> flip", "garbage", "1.3.0", true},
{"unparsable both -> no flip", "garbage", "junk", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
oldPeer := &Peer{Meta: PeerSystemMeta{WtVersion: tt.oldVer}}
newPeer := &Peer{Meta: PeerSystemMeta{WtVersion: tt.newVer}}
assert.Equal(t, tt.want, PostureVerdictChanged(c, oldPeer, newPeer))
})
}
}
func TestPostureVerdictChanged_ReplaysEachCheck(t *testing.T) {
// Old fails the version check, new fails the kernel check: the bundle denies on
// both sides, yet every single check flipped, so the posture must be re-evaluated.
c := bundle(ChecksDefinition{
NBVersionCheck: &NBVersionCheck{MinVersion: "1.0.0"},
OSVersionCheck: &OSVersionCheck{Linux: &MinKernelVersionCheck{MinKernelVersion: "5.0.0"}},
})
oldPeer := &Peer{Meta: PeerSystemMeta{WtVersion: "0.9.0", GoOS: "linux", KernelVersion: "6.0.0"}}
newPeer := &Peer{Meta: PeerSystemMeta{WtVersion: "1.1.0", GoOS: "linux", KernelVersion: "4.0.0"}}
assert.False(t, c[0].Passes(oldPeer))
assert.False(t, c[0].Passes(newPeer))
assert.True(t, PostureVerdictChanged(c, oldPeer, newPeer))
}
func TestPostureVerdictChanged_NoChecks(t *testing.T) {
oldPeer := &Peer{Meta: PeerSystemMeta{WtVersion: "1.0.0"}}
newPeer := &Peer{Meta: PeerSystemMeta{WtVersion: "2.0.0"}}
assert.False(t, PostureVerdictChanged(nil, oldPeer, newPeer))
}
@@ -0,0 +1,108 @@
package nmdata
import (
"net/netip"
"slices"
"strings"
"github.com/netbirdio/netbird/shared/management/domain"
)
// NetworkType mirrors route.NetworkType iota values.
const (
NetworkTypeInvalid = 0
NetworkTypeIPv4 = 1
NetworkTypeIPv6 = 2
NetworkTypeDomain = 3
haSeparator = "|"
)
// Route is the slim twin of route.Route.
type Route struct {
ID string
AccountID string
PublicID string
Network netip.Prefix
Domains domain.List
KeepRoute bool
NetID string
Description string
Peer string
PeerID string
PeerGroups []string
NetworkType int
Masquerade bool
Metric int
Enabled bool
Groups []string
AccessControlGroups []string
SkipAutoApply bool
}
func (r *Route) Equal(other *Route) bool {
if r == nil && other == nil {
return true
} else if r == nil || other == nil {
return false
}
return other.ID == r.ID &&
other.Description == r.Description &&
other.NetID == r.NetID &&
other.Network == r.Network &&
slices.Equal(r.Domains, other.Domains) &&
other.KeepRoute == r.KeepRoute &&
other.NetworkType == r.NetworkType &&
other.Peer == r.Peer &&
other.PeerID == r.PeerID &&
other.Metric == r.Metric &&
other.Masquerade == r.Masquerade &&
other.Enabled == r.Enabled &&
slices.Equal(r.Groups, other.Groups) &&
slices.Equal(r.PeerGroups, other.PeerGroups) &&
slices.Equal(r.AccessControlGroups, other.AccessControlGroups) &&
other.SkipAutoApply == r.SkipAutoApply
}
func (r *Route) IsDynamic() bool {
return r.NetworkType == NetworkTypeDomain
}
func (r *Route) NetString() string {
if r.IsDynamic() && r.Domains != nil {
return r.Domains.SafeString()
}
return r.Network.String()
}
func (r *Route) GetHAUniqueID() string {
return r.NetID + haSeparator + r.NetString()
}
func (r *Route) GetResourceID() string {
return strings.Split(r.ID, ":")[0]
}
func (r *Route) Copy() *Route {
return &Route{
ID: r.ID,
AccountID: r.AccountID,
PublicID: r.PublicID,
Network: r.Network,
Domains: slices.Clone(r.Domains),
KeepRoute: r.KeepRoute,
NetID: r.NetID,
Description: r.Description,
Peer: r.Peer,
PeerID: r.PeerID,
PeerGroups: slices.Clone(r.PeerGroups),
NetworkType: r.NetworkType,
Masquerade: r.Masquerade,
Metric: r.Metric,
Enabled: r.Enabled,
Groups: slices.Clone(r.Groups),
AccessControlGroups: slices.Clone(r.AccessControlGroups),
SkipAutoApply: r.SkipAutoApply,
}
}
@@ -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,111 @@
package networkmap
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/miekg/dns"
log "github.com/sirupsen/logrus"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
const peersZoneRecordTTL = 300
// PeersCustomZone builds the peers DNS zone from twin peer rows. It is the
// single source of the zone-record logic; Account.GetPeersCustomZone delegates
// here via twins.
func PeersCustomZone(ctx context.Context, accountID string, dnsDomain string, peers map[string]*nmdata.Peer, ipv6AllowedPeers map[string]struct{}) nmdata.CustomZone {
var merr *multierror.Error
if dnsDomain == "" {
log.WithContext(ctx).Error("no dns domain is set, returning empty zone")
return nmdata.CustomZone{}
}
customZone := nmdata.CustomZone{
Domain: dns.Fqdn(dnsDomain),
Records: make([]nmdata.SimpleRecord, 0, len(peers)),
}
domainSuffix := "." + dnsDomain
var sb strings.Builder
for _, peer := range peers {
if peer == nil {
continue
}
if peer.DNSLabel == "" {
merr = multierror.Append(merr, fmt.Errorf("peer %s has an empty DNS label", peer.ID))
continue
}
sb.Grow(len(peer.DNSLabel) + len(domainSuffix))
sb.WriteString(peer.DNSLabel)
sb.WriteString(domainSuffix)
fqdn := sb.String()
customZone.Records = append(customZone.Records, nmdata.SimpleRecord{
Name: fqdn,
Type: int(dns.TypeA),
Class: nbdns.DefaultClass,
TTL: peersZoneRecordTTL,
RData: peer.IP.String(),
})
// Only advertise AAAA for peers that have a valid IPv6, whose client supports it,
// and that belong to an IPv6-enabled group. Old clients don't configure v6 on their
// WireGuard interface, so resolving their AAAA causes connections to hang.
// Capability changes (client upgrade/downgrade, --disable-ipv6 toggle) propagate
// to other peers via SyncPeer/LoginPeer regardless of version change, so AAAA
// records refresh when a peer first reports the IPv6 overlay capability.
_, peerAllowed := ipv6AllowedPeers[peer.ID]
hasIPv6 := peer.IPv6.IsValid() && peer.SupportsIPv6() && peerAllowed
if hasIPv6 {
customZone.Records = append(customZone.Records, nmdata.SimpleRecord{
Name: fqdn,
Type: int(dns.TypeAAAA),
Class: nbdns.DefaultClass,
TTL: peersZoneRecordTTL,
RData: peer.IPv6.String(),
})
}
sb.Reset()
for _, extraLabel := range peer.ExtraDNSLabels {
sb.Grow(len(extraLabel) + len(domainSuffix))
sb.WriteString(extraLabel)
sb.WriteString(domainSuffix)
extraFqdn := sb.String()
customZone.Records = append(customZone.Records, nmdata.SimpleRecord{
Name: extraFqdn,
Type: int(dns.TypeA),
Class: nbdns.DefaultClass,
TTL: peersZoneRecordTTL,
RData: peer.IP.String(),
})
if hasIPv6 {
customZone.Records = append(customZone.Records, nmdata.SimpleRecord{
Name: extraFqdn,
Type: int(dns.TypeAAAA),
Class: nbdns.DefaultClass,
TTL: peersZoneRecordTTL,
RData: peer.IPv6.String(),
})
}
sb.Reset()
}
}
go func() {
if merr != nil {
log.WithContext(ctx).Errorf("error generating custom zone for account %s: %v", accountID, merr)
}
}()
return customZone
}
@@ -0,0 +1,209 @@
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,
// The envelope encoder puts public ids on the wire and degrades to an
// empty one when a policy has none. A synthesised policy has no
// persisted row to take a public id from, and its own id is already
// stable and unique, so it serves as both.
PublicID: 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,
PublicID: 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
}
File diff suppressed because it is too large Load Diff
+12 -3
View File
@@ -114,6 +114,9 @@ message BundleParameters {
// (or empty) keeps internal IP ranges, "strict" also anonymizes them.
// Unknown values are treated as "strict".
string anonymize_level = 5;
// upload_url is the service URL the client requests an upload URL from
// before uploading the bundle. Empty selects the default upload server.
string upload_url = 6;
}
message BundleResult {
@@ -231,6 +234,11 @@ message Flags {
bool disableSSHAuth = 15;
bool disableIPv6 = 16;
// remoteJobsAllowed mirrors the peer's local opt-in for management-requested
// remote jobs (e.g. debug bundles). Reported so the dashboard can surface
// peers that have opted out.
bool remoteJobsAllowed = 17;
}
// PeerCapability represents a feature the client binary supports.
@@ -1094,13 +1102,12 @@ message PolicyCompact {
// ResourceCompact mirrors types.Resource. Used by PolicyCompact to carry
// rule.SourceResource / rule.DestinationResource when the rule targets a
// specific resource (typically a peer) rather than groups.
// peer_index_set tells whether peer_index is valid (proto3 uint32 cannot
// disambiguate "0" from "unset"); set only when type == "peer".
message ResourceCompact {
string type = 1;
bool peer_index_set = 2;
uint32 peer_index = 3;
reserved 4; // future: host/subnet/domain references when needed
reserved 4;
string id = 5; // public id for domain/host/subnet resources
}
// UserNameList is a list of local-user names — used as the value type in
@@ -1124,6 +1131,8 @@ message GroupCompact {
// groups lose that property and the two sides expand policy
// destinations differently.
bool is_all = 3;
repeated ResourceCompact resources = 4;
}
// DNSSettingsCompact mirrors types.DNSSettings.
-106
View File
@@ -1,106 +0,0 @@
package types
import (
"net/netip"
"time"
)
// ComponentPeer is the self-contained peer representation used by
// NetworkMapComponents and the calculated NetworkMap. It carries exactly the
// subset of peer data that crosses the components wire format, so the shared
// calculation layer stays independent of the management server's domain
// types.
type ComponentPeer struct {
ID string
Key string
IP netip.Addr
IPv6 netip.Addr
DNSLabel string
SSHKey string
SSHEnabled bool
ServerSSHAllowed bool
AgentVersion string
SupportsSourcePrefixes bool
SupportsIPv6 bool
LoginExpirationEnabled bool
AddedWithSSOLogin bool
LastLogin time.Time
// ProxyEmbedded marks an ephemeral embedded proxy peer. Connections
// involving such a peer on either endpoint default to lazy.
ProxyEmbedded bool
}
// FQDN returns the peer's FQDN combined of the peer's DNS label and the system's DNS domain.
func (p *ComponentPeer) FQDN(dnsDomain string) string {
if dnsDomain == "" {
return ""
}
return p.DNSLabel + "." + dnsDomain
}
// LoginExpired indicates whether the peer's login has expired, mirroring the
// server-side peer semantics: only SSO-added peers with login expiration
// enabled can expire.
func (p *ComponentPeer) LoginExpired(expiresIn time.Duration) (bool, time.Duration) {
if !p.AddedWithSSOLogin || !p.LoginExpirationEnabled {
return false, 0
}
timeLeft := time.Until(p.LastLogin.Add(expiresIn))
return timeLeft <= 0, timeLeft
}
// GroupAllName is the reserved name of the default group that contains every peer in an account.
const GroupAllName = "All"
// ComponentGroup is the self-contained group representation used by
// NetworkMapComponents: just the membership view the network-map calculation
// needs, without the server's storage fields.
type ComponentGroup struct {
ID string
PublicID string
Name string
Peers []string
}
// IsGroupAll checks if the group is a default "All" group.
func (g *ComponentGroup) IsGroupAll() bool {
return g.Name == GroupAllName
}
// ComponentRouter is the self-contained network-router representation used by
// NetworkMapComponents.
type ComponentRouter struct {
NetworkID string
PublicID string
Peer string
PeerGroups []string
Masquerade bool
Metric int
Enabled bool
}
// ComponentResourceType mirrors the network-resource type enum on the
// components wire format.
type ComponentResourceType string
const (
ComponentResourceHost ComponentResourceType = "host"
ComponentResourceSubnet ComponentResourceType = "subnet"
ComponentResourceDomain ComponentResourceType = "domain"
)
// ComponentResource is the self-contained network-resource representation
// used by NetworkMapComponents.
type ComponentResource struct {
ID string
PublicID string
NetworkID string
AccountID string
Name string
Description string
Type ComponentResourceType
Address string
Domain string
Prefix netip.Prefix
Enabled bool
}
-16
View File
@@ -1,16 +0,0 @@
package types
// DNSSettings defines dns settings at the account level
type DNSSettings struct {
// DisabledManagementGroups groups whose DNS management is disabled
DisabledManagementGroups []string `gorm:"serializer:json"`
}
// Copy returns a copy of the DNS settings
func (d DNSSettings) Copy() DNSSettings {
settings := DNSSettings{
DisabledManagementGroups: make([]string, len(d.DisabledManagementGroups)),
}
copy(settings.DisabledManagementGroups, d.DisabledManagementGroups)
return settings
}
+7 -28
View File
@@ -3,6 +3,7 @@ package types
import (
"strconv"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/version"
)
@@ -23,31 +24,9 @@ type supportedFeatures struct {
type LookupMap map[string]struct{}
func PolicyRuleImpliesLegacySSH(rule *PolicyRule) bool {
return rule.Protocol == PolicyRuleProtocolALL || (rule.Protocol == PolicyRuleProtocolTCP && (portsIncludesSSH(rule.Ports) || portRangeIncludesSSH(rule.PortRanges)))
}
func portRangeIncludesSSH(portRanges []RulePortRange) bool {
for _, pr := range portRanges {
if (pr.Start <= defaultSSHPortNumber && pr.End >= defaultSSHPortNumber) || (pr.Start <= nativeSSHPortNumber && pr.End >= nativeSSHPortNumber) {
return true
}
}
return false
}
func portsIncludesSSH(ports []string) bool {
for _, port := range ports {
if port == defaultSSHPortString || port == nativeSSHPortString {
return true
}
}
return false
}
// ExpandPortsAndRanges expands Ports and PortRanges of a rule into individual firewall rules.
func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *ComponentPeer) []*FirewallRule {
features := peerSupportedFirewallFeatures(peer.AgentVersion)
func ExpandPortsAndRanges(base FirewallRule, rule *nmdata.PolicyRule, peer *nmdata.Peer) []*FirewallRule {
features := peerSupportedFirewallFeatures(peer.Meta.WtVersion)
var expanded []*FirewallRule
@@ -64,7 +43,7 @@ func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *ComponentPe
fr := base
if features.portRanges {
fr.PortRange = portRange
fr.PortRange = RulePortRange{Start: portRange.Start, End: portRange.End}
} else {
if portRange.Start != portRange.End {
continue
@@ -74,7 +53,7 @@ func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *ComponentPe
expanded = append(expanded, &fr)
}
if shouldCheckRulesForNativeSSH(features.nativeSSH, rule, peer) || rule.Protocol == PolicyRuleProtocolNetbirdSSH {
if shouldCheckRulesForNativeSSH(features.nativeSSH, rule, peer) || rule.Protocol == string(PolicyRuleProtocolNetbirdSSH) {
expanded = addNativeSSHRule(base, expanded)
}
@@ -104,8 +83,8 @@ func isPortInRule(portString string, portInt uint16, rule *FirewallRule) bool {
return rule.Port == portString || (rule.PortRange.Start <= portInt && portInt <= rule.PortRange.End)
}
func shouldCheckRulesForNativeSSH(supportsNative bool, rule *PolicyRule, peer *ComponentPeer) bool {
return supportsNative && peer.SSHEnabled && peer.ServerSSHAllowed && rule.Protocol == PolicyRuleProtocolTCP
func shouldCheckRulesForNativeSSH(supportsNative bool, rule *nmdata.PolicyRule, peer *nmdata.Peer) bool {
return supportsNative && peer.SSHEnabled && peer.Meta.Flags.ServerSSHAllowed && rule.Protocol == string(PolicyRuleProtocolTCP)
}
func peerSupportedFirewallFeatures(peerVer string) supportedFeatures {
+11 -10
View File
@@ -10,6 +10,7 @@ import (
log "github.com/sirupsen/logrus"
nbroute "github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
const (
@@ -50,7 +51,7 @@ func (r *FirewallRule) Equal(other *FirewallRule) bool {
// For static routes, source ranges match the destination family (v4 or v6).
// For dynamic routes (domain-based), separate v4 and v6 rules are generated
// so the routing peer's forwarding chain allows both address families.
func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*ComponentPeer, direction int, includeIPv6 bool) []*RouteFirewallRule {
func GenerateRouteFirewallRules(ctx context.Context, route *nmdata.Route, rule *nmdata.PolicyRule, groupPeers []*nmdata.Peer, direction int, includeIPv6 bool) []*RouteFirewallRule {
rulesExists := make(map[string]struct{})
rules := make([]*RouteFirewallRule, 0)
@@ -71,11 +72,11 @@ func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule
baseRule := RouteFirewallRule{
PolicyID: rule.PolicyID,
RouteID: route.ID,
RouteID: nbroute.ID(route.ID),
SourceRanges: sourceRanges,
Action: string(rule.Action),
Action: rule.Action,
Destination: route.Network.String(),
Protocol: string(rule.Protocol),
Protocol: rule.Protocol,
Domains: route.Domains,
IsDynamic: route.IsDynamic(),
}
@@ -93,7 +94,7 @@ func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule
v6Rule.SourceRanges = v6Sources
if isDefaultV4 {
v6Rule.Destination = "::/0"
v6Rule.RouteID = route.ID + "-v6-default"
v6Rule.RouteID = nbroute.ID(route.ID + "-v6-default")
}
if len(rule.Ports) == 0 {
rules = append(rules, generateRulesWithPortRanges(v6Rule, rule, rulesExists)...)
@@ -106,7 +107,7 @@ func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule
}
// splitPeerSourcesByFamily separates peer IPs into v4 (/32) and v6 (/128) source ranges.
func splitPeerSourcesByFamily(groupPeers []*ComponentPeer) (v4, v6 []string) {
func splitPeerSourcesByFamily(groupPeers []*nmdata.Peer) (v4, v6 []string) {
v4 = make([]string, 0, len(groupPeers))
v6 = make([]string, 0, len(groupPeers))
for _, peer := range groupPeers {
@@ -122,7 +123,7 @@ func splitPeerSourcesByFamily(groupPeers []*ComponentPeer) (v4, v6 []string) {
}
// generateRulesForPeer generates rules for a given peer based on ports and port ranges.
func generateRulesWithPortRanges(baseRule RouteFirewallRule, rule *PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule {
func generateRulesWithPortRanges(baseRule RouteFirewallRule, rule *nmdata.PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule {
rules := make([]*RouteFirewallRule, 0)
ruleIDBase := generateRuleIDBase(rule, baseRule)
@@ -138,7 +139,7 @@ func generateRulesWithPortRanges(baseRule RouteFirewallRule, rule *PolicyRule, r
if _, ok := rulesExists[ruleID]; !ok {
rulesExists[ruleID] = struct{}{}
pr := baseRule
pr.PortRange = portRange
pr.PortRange = RulePortRange{Start: portRange.Start, End: portRange.End}
rules = append(rules, &pr)
}
}
@@ -150,7 +151,7 @@ func generateRulesWithPortRanges(baseRule RouteFirewallRule, rule *PolicyRule, r
}
// generateRulesWithPorts generates rules when specific ports are provided.
func generateRulesWithPorts(ctx context.Context, baseRule RouteFirewallRule, rule *PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule {
func generateRulesWithPorts(ctx context.Context, baseRule RouteFirewallRule, rule *nmdata.PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule {
rules := make([]*RouteFirewallRule, 0)
ruleIDBase := generateRuleIDBase(rule, baseRule)
@@ -176,6 +177,6 @@ func generateRulesWithPorts(ctx context.Context, baseRule RouteFirewallRule, rul
}
// generateRuleIDBase generates the base rule ID for checking duplicates.
func generateRuleIDBase(rule *PolicyRule, baseRule RouteFirewallRule) string {
func generateRuleIDBase(rule *nmdata.PolicyRule, baseRule RouteFirewallRule) string {
return rule.ID + strings.Join(baseRule.SourceRanges, ",") + strconv.Itoa(FirewallRuleDirectionIN) + baseRule.Protocol + baseRule.Action
}
+34 -34
View File
@@ -8,12 +8,12 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
func TestSplitPeerSourcesByFamily(t *testing.T) {
peers := []*ComponentPeer{
peers := []*nmdata.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
@@ -35,7 +35,7 @@ func TestSplitPeerSourcesByFamily(t *testing.T) {
}
func TestGenerateRouteFirewallRules_V4Route(t *testing.T) {
peers := []*ComponentPeer{
peers := []*nmdata.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
@@ -45,15 +45,15 @@ func TestGenerateRouteFirewallRules_V4Route(t *testing.T) {
},
}
r := &route.Route{
r := &nmdata.Route{
ID: "route1",
Network: netip.MustParsePrefix("10.0.0.0/24"),
}
rule := &PolicyRule{
rule := &nmdata.PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
Action: string(PolicyTrafficActionAccept),
Protocol: string(PolicyRuleProtocolALL),
}
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
@@ -64,7 +64,7 @@ func TestGenerateRouteFirewallRules_V4Route(t *testing.T) {
}
func TestGenerateRouteFirewallRules_V6Route(t *testing.T) {
peers := []*ComponentPeer{
peers := []*nmdata.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
@@ -74,15 +74,15 @@ func TestGenerateRouteFirewallRules_V6Route(t *testing.T) {
},
}
r := &route.Route{
r := &nmdata.Route{
ID: "route1",
Network: netip.MustParsePrefix("2001:db8::/32"),
}
rule := &PolicyRule{
rule := &nmdata.PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
Action: string(PolicyTrafficActionAccept),
Protocol: string(PolicyRuleProtocolALL),
}
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
@@ -92,7 +92,7 @@ func TestGenerateRouteFirewallRules_V6Route(t *testing.T) {
}
func TestGenerateRouteFirewallRules_DynamicRoute_DualStack(t *testing.T) {
peers := []*ComponentPeer{
peers := []*nmdata.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
@@ -102,16 +102,16 @@ func TestGenerateRouteFirewallRules_DynamicRoute_DualStack(t *testing.T) {
},
}
r := &route.Route{
r := &nmdata.Route{
ID: "route1",
NetworkType: route.DomainNetwork,
NetworkType: nmdata.NetworkTypeDomain,
Domains: domain.List{"example.com"},
}
rule := &PolicyRule{
rule := &nmdata.PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
Action: string(PolicyTrafficActionAccept),
Protocol: string(PolicyRuleProtocolALL),
}
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
@@ -125,21 +125,21 @@ func TestGenerateRouteFirewallRules_DynamicRoute_DualStack(t *testing.T) {
}
func TestGenerateRouteFirewallRules_DynamicRoute_NoV6Peers(t *testing.T) {
peers := []*ComponentPeer{
peers := []*nmdata.Peer{
{IP: netip.MustParseAddr("100.64.0.1")},
{IP: netip.MustParseAddr("100.64.0.2")},
}
r := &route.Route{
r := &nmdata.Route{
ID: "route1",
NetworkType: route.DomainNetwork,
NetworkType: nmdata.NetworkTypeDomain,
Domains: domain.List{"example.com"},
}
rule := &PolicyRule{
rule := &nmdata.PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
Action: string(PolicyTrafficActionAccept),
Protocol: string(PolicyRuleProtocolALL),
}
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
@@ -149,7 +149,7 @@ func TestGenerateRouteFirewallRules_DynamicRoute_NoV6Peers(t *testing.T) {
}
func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) {
peers := []*ComponentPeer{
peers := []*nmdata.Peer{
{
IP: netip.MustParseAddr("100.64.0.1"),
IPv6: netip.MustParseAddr("fd00::1"),
@@ -161,15 +161,15 @@ func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) {
}
t.Run("v6 route excluded", func(t *testing.T) {
r := &route.Route{
r := &nmdata.Route{
ID: "route1",
Network: netip.MustParsePrefix("2001:db8::/32"),
}
rule := &PolicyRule{
rule := &nmdata.PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
Action: string(PolicyTrafficActionAccept),
Protocol: string(PolicyRuleProtocolALL),
}
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false)
@@ -177,16 +177,16 @@ func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) {
})
t.Run("dynamic route only v4", func(t *testing.T) {
r := &route.Route{
r := &nmdata.Route{
ID: "route1",
NetworkType: route.DomainNetwork,
NetworkType: nmdata.NetworkTypeDomain,
Domains: domain.List{"example.com"},
}
rule := &PolicyRule{
rule := &nmdata.PolicyRule{
PolicyID: "policy1",
ID: "rule1",
Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL,
Action: string(PolicyTrafficActionAccept),
Protocol: string(PolicyRuleProtocolALL),
}
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false)
+23 -285
View File
@@ -1,47 +1,28 @@
package types
import (
"encoding/binary"
"fmt"
"math/rand"
"net"
"net/netip"
"slices"
"sync"
"time"
"github.com/c-robinson/iplib"
"github.com/rs/xid"
"golang.org/x/exp/maps"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/proto"
"github.com/netbirdio/netbird/shared/management/status"
)
const (
// SubnetSize is a size of the subnet of the global network, e.g. 100.77.0.0/16
SubnetSize = 16
// NetSize is a global network size 100.64.0.0/10
NetSize = 10
// AllowedIPsFormat generates Wireguard AllowedIPs format (e.g. 100.64.30.1/32)
AllowedIPsFormat = "%s/32"
// AllowedIPsV6Format generates AllowedIPs format for v6 (e.g. fd12:3456:7890::1/128)
AllowedIPsV6Format = "%s/128"
// IPv6SubnetSize is the prefix length of per-account IPv6 subnets.
// Each account gets a /64 from its unique /48 ULA prefix.
IPv6SubnetSize = 64
)
type NetworkMap struct {
Peers []*ComponentPeer
Network *Network
Routes []*route.Route
Peers []*nmdata.Peer
Network *nmdata.Network
Routes []*nmdata.Route
DNSConfig nbdns.Config
OfflinePeers []*ComponentPeer
OfflinePeers []*nmdata.Peer
FirewallRules []*FirewallRule
RoutesFirewallRules []*RouteFirewallRule
ForwardingRules []*ForwardingRule
@@ -63,39 +44,8 @@ func (nm *NetworkMap) Merge(other *NetworkMap) {
nm.ForceRoutingPeerDNSResolution = nm.ForceRoutingPeerDNSResolution || other.ForceRoutingPeerDNSResolution
}
type comparableObject[T any] interface {
Equal(other T) bool
}
func mergeUnique[T comparableObject[T]](arr1, arr2 []T) []T {
var result []T
for _, item := range arr1 {
if !containsEqual(result, item) {
result = append(result, item)
}
}
for _, item := range arr2 {
if !containsEqual(result, item) {
result = append(result, item)
}
}
return result
}
func containsEqual[T comparableObject[T]](slice []T, element T) bool {
for _, item := range slice {
if item.Equal(element) {
return true
}
}
return false
}
func mergeUniquePeersByID(peers1, peers2 []*ComponentPeer) []*ComponentPeer {
result := make(map[string]*ComponentPeer)
func mergeUniquePeersByID(peers1, peers2 []*nmdata.Peer) []*nmdata.Peer {
result := make(map[string]*nmdata.Peer)
for _, peer := range peers1 {
result[peer.ID] = peer
}
@@ -151,245 +101,33 @@ func ipToBytes(ip net.IP) []byte {
return ip.To16()
}
type Network struct {
Identifier string `json:"id"`
Net net.IPNet `gorm:"serializer:json"`
// NetV6 is the IPv6 ULA subnet for this account's overlay. Empty if not yet allocated.
NetV6 net.IPNet `gorm:"serializer:json"`
Dns string
// Serial is an ID that increments by 1 when any change to the network happened (e.g. new peer has been added).
// Used to synchronize state to the client apps.
Serial uint64
Mu sync.Mutex `json:"-" gorm:"-"`
type comparableObject[T any] interface {
Equal(other T) bool
}
// NewNetwork creates a new Network initializing it with a Serial=0
// It takes a random /16 subnet from 100.64.0.0/10 (64 different subnets)
// and a random /64 subnet from fd00:4e42::/32 for IPv6.
func NewNetwork() *Network {
n := iplib.NewNet4(net.ParseIP("100.64.0.0"), NetSize)
sub, _ := n.Subnet(SubnetSize)
func mergeUnique[T comparableObject[T]](arr1, arr2 []T) []T {
var result []T
s := rand.NewSource(time.Now().UnixNano())
r := rand.New(s)
intn := r.Intn(len(sub))
return &Network{
Identifier: xid.New().String(),
Net: sub[intn].IPNet,
NetV6: AllocateIPv6Subnet(r),
Dns: "",
Serial: 0,
}
}
// AllocateIPv6Subnet generates a random RFC 4193 ULA /64 prefix.
// The format follows RFC 4193 section 3.1: fd + 40-bit Global ID + 16-bit Subnet ID.
// The Global ID and Subnet ID are randomized (simplified from the SHA-1 algorithm
// in section 3.2.2), giving 2^56 possible /64 subnets across all accounts.
func AllocateIPv6Subnet(r *rand.Rand) net.IPNet {
ip := make(net.IP, 16)
ip[0] = 0xfd
// Bytes 1-5: 40-bit random Global ID
ip[1] = byte(r.Intn(256))
ip[2] = byte(r.Intn(256))
ip[3] = byte(r.Intn(256))
ip[4] = byte(r.Intn(256))
ip[5] = byte(r.Intn(256))
// Bytes 6-7: 16-bit random Subnet ID
ip[6] = byte(r.Intn(256))
ip[7] = byte(r.Intn(256))
return net.IPNet{
IP: ip,
Mask: net.CIDRMask(IPv6SubnetSize, 128),
}
}
// IncSerial increments Serial by 1 reflecting that the network state has been changed
func (n *Network) IncSerial() {
n.Mu.Lock()
defer n.Mu.Unlock()
n.Serial++
}
// CurrentSerial returns the Network.Serial of the network (latest state id)
func (n *Network) CurrentSerial() uint64 {
n.Mu.Lock()
defer n.Mu.Unlock()
return n.Serial
}
func (n *Network) Copy() *Network {
n.Mu.Lock()
defer n.Mu.Unlock()
return &Network{
Identifier: n.Identifier,
Net: n.Net,
NetV6: n.NetV6,
Dns: n.Dns,
Serial: n.Serial,
}
}
// AllocatePeerIP picks an available IP from a netip.Prefix.
// This method considers already taken IPs and reuses IPs if there are gaps in takenIps.
// E.g. if prefix=100.30.0.0/16 and takenIps=[100.30.0.1, 100.30.0.4] then the result would be 100.30.0.2 or 100.30.0.3.
func AllocatePeerIP(prefix netip.Prefix, takenIps []netip.Addr) (netip.Addr, error) {
b := prefix.Masked().Addr().As4()
baseIP := binary.BigEndian.Uint32(b[:])
hostBits := 32 - prefix.Bits()
totalIPs := uint32(1 << hostBits)
taken := make(map[uint32]struct{}, len(takenIps)+1)
taken[baseIP] = struct{}{} // reserve network IP
taken[baseIP+totalIPs-1] = struct{}{} // reserve broadcast IP
for _, ip := range takenIps {
ab := ip.As4()
taken[binary.BigEndian.Uint32(ab[:])] = struct{}{}
}
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
maxAttempts := (int(totalIPs) - len(taken)) / 100
for i := 0; i < maxAttempts; i++ {
offset := uint32(rng.Intn(int(totalIPs-2))) + 1
candidate := baseIP + offset
if _, exists := taken[candidate]; !exists {
return uint32ToIP(candidate), nil
for _, item := range arr1 {
if !containsEqual(result, item) {
result = append(result, item)
}
}
for offset := uint32(1); offset < totalIPs-1; offset++ {
candidate := baseIP + offset
if _, exists := taken[candidate]; !exists {
return uint32ToIP(candidate), nil
for _, item := range arr2 {
if !containsEqual(result, item) {
result = append(result, item)
}
}
return netip.Addr{}, status.Errorf(status.PreconditionFailed, "network %s is out of IPs", prefix.String())
return result
}
// AllocateRandomPeerIP picks a random available IP from a netip.Prefix.
func AllocateRandomPeerIP(prefix netip.Prefix) (netip.Addr, error) {
b := prefix.Masked().Addr().As4()
baseIP := binary.BigEndian.Uint32(b[:])
hostBits := 32 - prefix.Bits()
totalIPs := uint32(1 << hostBits)
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
offset := uint32(rng.Intn(int(totalIPs-2))) + 1
candidate := baseIP + offset
return uint32ToIP(candidate), nil
}
// AllocateRandomPeerIPv6 picks a random host address within the given IPv6 prefix.
// Only the host bits (after the prefix length) are randomized.
func AllocateRandomPeerIPv6(prefix netip.Prefix) (netip.Addr, error) {
ones := prefix.Bits()
if ones == 0 || ones > 126 || !prefix.Addr().Is6() {
return netip.Addr{}, fmt.Errorf("invalid IPv6 subnet: %s", prefix.String())
}
ip := prefix.Addr().As16()
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
// Determine which byte the host bits start in
firstHostByte := ones / 8
// If the prefix doesn't end on a byte boundary, handle the partial byte
partialBits := ones % 8
if partialBits > 0 {
// Keep the network bits in the partial byte, randomize the rest
hostMask := byte(0xff >> partialBits)
ip[firstHostByte] = (ip[firstHostByte] & ^hostMask) | (byte(rng.Intn(256)) & hostMask)
firstHostByte++
}
// Randomize remaining full host bytes
for i := firstHostByte; i < 16; i++ {
ip[i] = byte(rng.Intn(256))
}
// Avoid all-zeros and all-ones host parts by checking only host bits.
if isHostAllZeroOrOnes(ip[:], ones) {
ip = prefix.Masked().Addr().As16()
ip[15] |= 0x01
}
return netip.AddrFrom16(ip).Unmap(), nil
}
// isHostAllZeroOrOnes checks whether all host bits (after prefixLen) are zero or all ones.
func isHostAllZeroOrOnes(ip []byte, prefixLen int) bool {
hostStart := prefixLen / 8
partialBits := prefixLen % 8
hostSlice := slices.Clone(ip[hostStart:])
if partialBits > 0 {
hostSlice[0] &= 0xff >> partialBits
}
allZero := !slices.ContainsFunc(hostSlice, func(v byte) bool { return v != 0 })
if allZero {
return true
}
// Build the all-ones mask for host bits
onesMask := make([]byte, len(hostSlice))
for i := range onesMask {
onesMask[i] = 0xff
}
if partialBits > 0 {
onesMask[0] = 0xff >> partialBits
}
return slices.Equal(hostSlice, onesMask)
}
func uint32ToIP(n uint32) netip.Addr {
var b [4]byte
binary.BigEndian.PutUint32(b[:], n)
return netip.AddrFrom4(b)
}
// generateIPs generates a list of all possible IPs of the given network excluding IPs specified in the exclusion list
func generateIPs(ipNet *net.IPNet, exclusions map[string]struct{}) ([]net.IP, int) {
var ips []net.IP
for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); incIP(ip) {
if _, ok := exclusions[ip.String()]; !ok && ip[3] != 0 {
ips = append(ips, copyIP(ip))
}
}
// remove network address, broadcast and Fake DNS resolver address
lenIPs := len(ips)
switch {
case lenIPs < 2:
return ips, lenIPs
case lenIPs < 3:
return ips[1 : len(ips)-1], lenIPs - 2
default:
return ips[1 : len(ips)-2], lenIPs - 3
}
}
func copyIP(ip net.IP) net.IP {
dup := make(net.IP, len(ip))
copy(dup, ip)
return dup
}
func incIP(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
func containsEqual[T comparableObject[T]](slice []T, element T) bool {
for _, item := range slice {
if item.Equal(element) {
return true
}
}
return false
}
@@ -1,41 +0,0 @@
package types
import (
"testing"
"github.com/stretchr/testify/assert"
)
type testObject struct {
value int
}
func (t testObject) Equal(other testObject) bool {
return t.value == other.value
}
func Test_MergeUniqueArraysWithoutDuplicates(t *testing.T) {
arr1 := []testObject{{value: 1}, {value: 2}}
arr2 := []testObject{{value: 2}, {value: 3}}
result := mergeUnique(arr1, arr2)
assert.Len(t, result, 3)
assert.Contains(t, result, testObject{value: 1})
assert.Contains(t, result, testObject{value: 2})
assert.Contains(t, result, testObject{value: 3})
}
func Test_MergeUniqueHandlesEmptyArrays(t *testing.T) {
arr1 := []testObject{}
arr2 := []testObject{}
result := mergeUnique(arr1, arr2)
assert.Empty(t, result)
}
func Test_MergeUniqueHandlesOneEmptyArray(t *testing.T) {
arr1 := []testObject{{value: 1}, {value: 2}}
arr2 := []testObject{}
result := mergeUnique(arr1, arr2)
assert.Len(t, result, 2)
assert.Contains(t, result, testObject{value: 1})
assert.Contains(t, result, testObject{value: 2})
}
+24 -247
View File
@@ -1,264 +1,41 @@
package types
import (
"encoding/binary"
"net"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewNetwork(t *testing.T) {
network := NewNetwork()
// generated net should be a subnet of a larger 100.64.0.0/10 net
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}}
assert.Equal(t, ipNet.Contains(network.Net.IP), true)
type mergeTestObject struct {
value int
}
func TestAllocatePeerIP(t *testing.T) {
prefix := netip.MustParsePrefix("100.64.0.0/24")
var ips []netip.Addr
for i := 0; i < 252; i++ {
ip, err := AllocatePeerIP(prefix, ips)
if err != nil {
t.Fatal(err)
}
ips = append(ips, ip)
}
assert.Len(t, ips, 252)
uniq := make(map[string]struct{})
for _, ip := range ips {
if _, ok := uniq[ip.String()]; !ok {
uniq[ip.String()] = struct{}{}
} else {
t.Errorf("found duplicate IP %s", ip.String())
}
}
func (t mergeTestObject) Equal(other mergeTestObject) bool {
return t.value == other.value
}
func TestAllocatePeerIPSmallSubnet(t *testing.T) {
// Test /27 network (10.0.0.0/27) - should only have 30 usable IPs (10.0.0.1 to 10.0.0.30)
prefix := netip.MustParsePrefix("10.0.0.0/27")
var ips []netip.Addr
// Allocate all available IPs in the /27 network
for i := 0; i < 30; i++ {
ip, err := AllocatePeerIP(prefix, ips)
if err != nil {
t.Fatal(err)
}
// Verify IP is within the correct range
if !prefix.Contains(ip) {
t.Errorf("allocated IP %s is not within network %s", ip.String(), prefix.String())
}
ips = append(ips, ip)
}
assert.Len(t, ips, 30)
// Verify all IPs are unique
uniq := make(map[string]struct{})
for _, ip := range ips {
if _, ok := uniq[ip.String()]; !ok {
uniq[ip.String()] = struct{}{}
} else {
t.Errorf("found duplicate IP %s", ip.String())
}
}
// Try to allocate one more IP - should fail as network is full
_, err := AllocatePeerIP(prefix, ips)
if err == nil {
t.Error("expected error when network is full, but got none")
}
func Test_MergeUniqueArraysWithoutDuplicates(t *testing.T) {
arr1 := []mergeTestObject{{value: 1}, {value: 2}}
arr2 := []mergeTestObject{{value: 2}, {value: 3}}
result := mergeUnique(arr1, arr2)
assert.Len(t, result, 3)
assert.Contains(t, result, mergeTestObject{value: 1})
assert.Contains(t, result, mergeTestObject{value: 2})
assert.Contains(t, result, mergeTestObject{value: 3})
}
func TestAllocatePeerIPVariousCIDRs(t *testing.T) {
testCases := []struct {
name string
cidr string
expectedUsable int
}{
{"/30 network", "192.168.1.0/30", 2}, // 4 total - 2 reserved = 2 usable
{"/29 network", "192.168.1.0/29", 6}, // 8 total - 2 reserved = 6 usable
{"/28 network", "192.168.1.0/28", 14}, // 16 total - 2 reserved = 14 usable
{"/27 network", "192.168.1.0/27", 30}, // 32 total - 2 reserved = 30 usable
{"/26 network", "192.168.1.0/26", 62}, // 64 total - 2 reserved = 62 usable
{"/25 network", "192.168.1.0/25", 126}, // 128 total - 2 reserved = 126 usable
{"/16 network", "10.0.0.0/16", 65534}, // 65536 total - 2 reserved = 65534 usable
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
prefix, err := netip.ParsePrefix(tc.cidr)
require.NoError(t, err)
prefix = prefix.Masked()
var ips []netip.Addr
// For larger networks, test only a subset to avoid long test runs
testCount := tc.expectedUsable
if testCount > 1000 {
testCount = 1000
}
// Allocate IPs and verify they're within the correct range
for i := 0; i < testCount; i++ {
ip, err := AllocatePeerIP(prefix, ips)
require.NoError(t, err, "failed to allocate IP %d", i)
// Verify IP is within the correct range
assert.True(t, prefix.Contains(ip), "allocated IP %s is not within network %s", ip.String(), prefix.String())
// Verify IP is not network or broadcast address
networkAddr := prefix.Masked().Addr()
hostBits := 32 - prefix.Bits()
b := networkAddr.As4()
baseIP := binary.BigEndian.Uint32(b[:])
broadcastIP := uint32ToIP(baseIP + (1 << hostBits) - 1)
assert.NotEqual(t, networkAddr, ip, "allocated network address %s", ip.String())
assert.NotEqual(t, broadcastIP, ip, "allocated broadcast address %s", ip.String())
ips = append(ips, ip)
}
assert.Len(t, ips, testCount)
// Verify all IPs are unique
uniq := make(map[string]struct{})
for _, ip := range ips {
ipStr := ip.String()
assert.NotContains(t, uniq, ipStr, "found duplicate IP %s", ipStr)
uniq[ipStr] = struct{}{}
}
})
}
func Test_MergeUniqueHandlesEmptyArrays(t *testing.T) {
arr1 := []mergeTestObject{}
arr2 := []mergeTestObject{}
result := mergeUnique(arr1, arr2)
assert.Empty(t, result)
}
func TestGenerateIPs(t *testing.T) {
ipNet := net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 255, 255, 0}}
ips, ipsLen := generateIPs(&ipNet, map[string]struct{}{"100.64.0.0": {}})
if ipsLen != 252 {
t.Errorf("expected 252 ips, got %d", len(ips))
return
}
if ips[len(ips)-1].String() != "100.64.0.253" {
t.Errorf("expected last ip to be: 100.64.0.253, got %s", ips[len(ips)-1].String())
}
}
func TestNewNetworkHasIPv6(t *testing.T) {
network := NewNetwork()
assert.NotNil(t, network.NetV6.IP, "v6 subnet should be allocated")
assert.True(t, network.NetV6.IP.To4() == nil, "v6 subnet should be IPv6")
assert.Equal(t, byte(0xfd), network.NetV6.IP[0], "v6 subnet should be ULA (fd prefix)")
ones, bits := network.NetV6.Mask.Size()
assert.Equal(t, 64, ones, "v6 subnet should be /64")
assert.Equal(t, 128, bits)
}
func TestAllocateIPv6SubnetUniqueness(t *testing.T) {
seen := make(map[string]struct{})
for i := 0; i < 100; i++ {
network := NewNetwork()
key := network.NetV6.IP.String()
_, duplicate := seen[key]
assert.False(t, duplicate, "duplicate v6 subnet: %s", key)
seen[key] = struct{}{}
}
}
func TestAllocateRandomPeerIPv6(t *testing.T) {
prefix := netip.MustParsePrefix("fd12:3456:7890:abcd::/64")
ip, err := AllocateRandomPeerIPv6(prefix)
require.NoError(t, err)
assert.True(t, ip.Is6(), "should be IPv6")
assert.True(t, prefix.Contains(ip), "should be within subnet")
// First 8 bytes (network prefix) should match
b := ip.As16()
prefixBytes := prefix.Addr().As16()
assert.Equal(t, prefixBytes[:8], b[:8], "prefix should match")
// Interface ID should not be all zeros
allZero := true
for _, v := range b[8:] {
if v != 0 {
allZero = false
break
}
}
assert.False(t, allZero, "interface ID should not be all zeros")
}
func TestAllocateRandomPeerIPv6_VariousPrefixes(t *testing.T) {
tests := []struct {
name string
cidr string
prefix int
}{
{"standard /64", "fd00:1234:5678:abcd::/64", 64},
{"small /112", "fd00:1234:5678:abcd::/112", 112},
{"large /48", "fd00:1234::/48", 48},
{"non-boundary /60", "fd00:1234:5670::/60", 60},
{"non-boundary /52", "fd00:1230::/52", 52},
{"minimum /120", "fd00:1234:5678:abcd::100/120", 120},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prefix, err := netip.ParsePrefix(tt.cidr)
require.NoError(t, err)
prefix = prefix.Masked()
assert.Equal(t, tt.prefix, prefix.Bits())
for i := 0; i < 50; i++ {
ip, err := AllocateRandomPeerIPv6(prefix)
require.NoError(t, err)
assert.True(t, prefix.Contains(ip), "IP %s should be within %s", ip, prefix)
}
})
}
}
func TestAllocateRandomPeerIPv6_PreservesNetworkBits(t *testing.T) {
// For a /112, bytes 0-13 should be preserved, only bytes 14-15 should vary
prefix := netip.MustParsePrefix("fd00:1234:5678:abcd:ef01:2345:6789:0/112")
prefixBytes := prefix.Addr().As16()
for i := 0; i < 20; i++ {
ip, err := AllocateRandomPeerIPv6(prefix)
require.NoError(t, err)
// First 14 bytes (112 bits = 14 bytes) must match the network
b := ip.As16()
assert.Equal(t, prefixBytes[:14], b[:14], "network bytes should be preserved for /112")
}
}
func TestAllocateRandomPeerIPv6_NonByteBoundary(t *testing.T) {
// For a /60, the first 7.5 bytes are network, so byte 7 is partial
prefix := netip.MustParsePrefix("fd00:1234:5678:abc0::/60")
prefixBytes := prefix.Addr().As16()
for i := 0; i < 50; i++ {
ip, err := AllocateRandomPeerIPv6(prefix)
require.NoError(t, err)
b := ip.As16()
assert.True(t, prefix.Contains(ip), "IP %s should be within %s", ip, prefix)
// First 7 bytes must match exactly
assert.Equal(t, prefixBytes[:7], b[:7], "full network bytes should match for /60")
// Byte 7: top 4 bits (0xc = 1100) must be preserved
assert.Equal(t, prefixBytes[7]&0xf0, b[7]&0xf0, "partial byte network bits should be preserved for /60")
}
func Test_MergeUniqueHandlesOneEmptyArray(t *testing.T) {
arr1 := []mergeTestObject{{value: 1}, {value: 2}}
arr2 := []mergeTestObject{}
result := mergeUnique(arr1, arr2)
assert.Len(t, result, 2)
assert.Contains(t, result, mergeTestObject{value: 1})
assert.Contains(t, result, mergeTestObject{value: 2})
}
+135 -118
View File
@@ -14,32 +14,33 @@ import (
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
type NetworkMapComponents struct {
PeerID string
Network *Network
AccountSettings *AccountSettingsInfo
DNSSettings *DNSSettings
Network *nmdata.Network
AccountSettings *nmdata.AccountSettingsInfo
DNSSettings *nmdata.DNSSettings
CustomZoneDomain string
Peers map[string]*ComponentPeer
Groups map[string]*ComponentGroup
Policies []*Policy
Routes []*route.Route
NameServerGroups []*nbdns.NameServerGroup
AllDNSRecords []nbdns.SimpleRecord
AccountZones []nbdns.CustomZone
ResourcePoliciesMap map[string][]*Policy
RoutersMap map[string]map[string]*ComponentRouter
NetworkResources []*ComponentResource
Peers map[string]*nmdata.Peer
Groups map[string]*nmdata.Group
Policies []*nmdata.Policy
Routes []*nmdata.Route
NameServerGroups []*nmdata.NameServerGroup
AllDNSRecords []nmdata.SimpleRecord
AccountZones []nmdata.CustomZone
ResourcePoliciesMap map[string][]*nmdata.Policy
RoutersMap map[string]map[string]*nmdata.NetworkRouter
NetworkResources []*nmdata.NetworkResource
GroupIDToUserIDs map[string][]string
AllowedUserIDs map[string]struct{}
PostureFailedPeers map[string]map[string]struct{}
RouterPeers map[string]*ComponentPeer
RouterPeers map[string]*nmdata.Peer
// NetworkXIDToPublicID maps Network.ID (xid) → PublicID.
// Consumed by the envelope encoder to
@@ -51,20 +52,21 @@ type NetworkMapComponents struct {
// Same role as NetworkXIDToPublicID, used for PostureFailedPeers keys and
// policy SourcePostureChecks references.
PostureCheckXIDToPublicID map[string]string
routesByPeerOnce sync.Once
routesByPeerIdx map[string][]routeIndexEntry
// true when returning an empty-like map (returned instead of nil)
empty bool
// ForceRoutingPeerDNSResolution forces the peer to run/use routing-peer DNS
// resolution regardless of the account-global setting, for reverse-proxy
// domain targets.
ForceRoutingPeerDNSResolution bool
routesByPeerOnce sync.Once
routesByPeerIdx map[string][]routeIndexEntry
// true when returning an empty-like map (returned instead of nil)
empty bool
}
type routeIndexEntry struct {
route *route.Route
route *nmdata.Route
viaGroup bool
}
@@ -80,15 +82,15 @@ func EmptyNetworkMapComponents(nm *NetworkMapComponents) *NetworkMapComponents {
return nm
}
func (c *NetworkMapComponents) GetPeerInfo(peerID string) *ComponentPeer {
func (c *NetworkMapComponents) GetPeerInfo(peerID string) *nmdata.Peer {
return c.Peers[peerID]
}
func (c *NetworkMapComponents) GetRouterPeerInfo(peerID string) *ComponentPeer {
func (c *NetworkMapComponents) GetRouterPeerInfo(peerID string) *nmdata.Peer {
return c.RouterPeers[peerID]
}
func (c *NetworkMapComponents) GetGroupInfo(groupID string) *ComponentGroup {
func (c *NetworkMapComponents) GetGroupInfo(groupID string) *nmdata.Group {
return c.Groups[groupID]
}
@@ -143,8 +145,8 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap {
peersToConnect, expiredPeers := c.filterPeersByLoginExpiration(aclPeers)
includeIPv6 := false
if p := c.Peers[targetPeerID]; p != nil {
includeIPv6 = p.SupportsIPv6 && p.IPv6.IsValid()
if p := c.GetPeerInfo(targetPeerID); p != nil {
includeIPv6 = p.SupportsIPv6() && p.IPv6.IsValid()
}
routesUpdate := filterAndExpandRoutes(c.getRoutesToSync(targetPeerID, peersToConnect, peerGroups), includeIPv6)
routesFirewallRules := c.getPeerRoutesFirewallRules(ctx, targetPeerID, includeIPv6)
@@ -175,11 +177,11 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap {
if c.CustomZoneDomain != "" && len(c.AllDNSRecords) > 0 {
customZones = append(customZones, nbdns.CustomZone{
Domain: c.CustomZoneDomain,
Records: c.AllDNSRecords,
Records: toRealRecords(c.AllDNSRecords),
})
}
customZones = append(customZones, c.AccountZones...)
customZones = append(customZones, toRealZones(c.AccountZones)...)
dnsUpdate.CustomZones = customZones
dnsUpdate.NameServerGroups = c.getPeerNSGroupsFromGroups(targetPeerID, peerGroups)
@@ -187,7 +189,7 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap {
return &NetworkMap{
Peers: peersToConnectIncludingRouters,
Network: c.Network.Copy(),
Network: c.Network,
Routes: append(filterAndExpandRoutes(networkResourcesRoutes, includeIPv6), routesUpdate...),
DNSConfig: dnsUpdate,
OfflinePeers: expiredPeers,
@@ -204,7 +206,7 @@ func (c *NetworkMapComponents) IsEmpty() bool {
return c.empty
}
func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ([]*ComponentPeer, []*FirewallRule, map[string]map[string]struct{}, bool) {
func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ([]*nmdata.Peer, []*FirewallRule, map[string]map[string]struct{}, bool) {
targetPeer := c.GetPeerInfo(targetPeerID)
if targetPeer == nil {
return nil, nil, nil, false
@@ -215,26 +217,26 @@ func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) (
sshEnabled := false
for _, policy := range c.Policies {
if !policy.Enabled {
if policy == nil || !policy.Enabled {
continue
}
for _, rule := range policy.Rules {
if !rule.Enabled {
if rule == nil || !rule.Enabled {
continue
}
var sourcePeers, destinationPeers []*ComponentPeer
var sourcePeers, destinationPeers []*nmdata.Peer
var peerInSources, peerInDestinations bool
if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" {
sourcePeers, peerInSources = c.getPeerFromResource(rule.SourceResource, targetPeerID)
if rule.SourceResource.Type == string(ResourceTypePeer) && rule.SourceResource.ID != "" {
sourcePeers, peerInSources = c.getPeerFromResource(rule.SourceResource, targetPeerID, policy.SourcePostureChecks)
} else {
sourcePeers, peerInSources = c.getAllPeersFromGroups(rule.Sources, targetPeerID, policy.SourcePostureChecks)
}
if rule.DestinationResource.Type == ResourceTypePeer && rule.DestinationResource.ID != "" {
destinationPeers, peerInDestinations = c.getPeerFromResource(rule.DestinationResource, targetPeerID)
if rule.DestinationResource.Type == string(ResourceTypePeer) && rule.DestinationResource.ID != "" {
destinationPeers, peerInDestinations = c.getPeerFromResource(rule.DestinationResource, targetPeerID, nil)
} else {
destinationPeers, peerInDestinations = c.getAllPeersFromGroups(rule.Destinations, targetPeerID, nil)
}
@@ -256,7 +258,7 @@ func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) (
generateResources(rule, sourcePeers, FirewallRuleDirectionIN)
}
if peerInDestinations && rule.Protocol == PolicyRuleProtocolNetbirdSSH {
if peerInDestinations && rule.Protocol == string(PolicyRuleProtocolNetbirdSSH) {
sshEnabled = true
switch {
case len(rule.AuthorizedGroups) > 0:
@@ -287,7 +289,7 @@ func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) (
default:
authorizedUsers[auth.Wildcard] = c.getAllowedUserIDs()
}
} else if peerInDestinations && PolicyRuleImpliesLegacySSH(rule) && targetPeer.SSHEnabled {
} else if peerInDestinations && nmdata.PolicyRuleImpliesLegacySSH(rule) && targetPeer.SSHEnabled {
sshEnabled = true
authorizedUsers[auth.Wildcard] = c.getAllowedUserIDs()
}
@@ -307,19 +309,19 @@ func (c *NetworkMapComponents) getAllowedUserIDs() map[string]struct{} {
return make(map[string]struct{})
}
func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *ComponentPeer) (func(*PolicyRule, []*ComponentPeer, int), func() ([]*ComponentPeer, []*FirewallRule)) {
func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *nmdata.Peer) (func(*nmdata.PolicyRule, []*nmdata.Peer, int), func() ([]*nmdata.Peer, []*FirewallRule)) {
rulesExists := make(map[string]struct{})
peersExists := make(map[string]struct{})
rules := make([]*FirewallRule, 0)
peers := make([]*ComponentPeer, 0)
peers := make([]*nmdata.Peer, 0)
return func(rule *PolicyRule, groupPeers []*ComponentPeer, direction int) {
return func(rule *nmdata.PolicyRule, groupPeers []*nmdata.Peer, direction int) {
protocol := rule.Protocol
if protocol == PolicyRuleProtocolNetbirdSSH {
protocol = PolicyRuleProtocolTCP
if protocol == string(PolicyRuleProtocolNetbirdSSH) {
protocol = string(PolicyRuleProtocolTCP)
}
protocolStr := string(protocol)
protocolStr := protocol
actionStr := string(rule.Action)
dirStr := strconv.Itoa(direction)
portsJoined := strings.Join(rule.Ports, ",")
@@ -365,15 +367,28 @@ func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *ComponentPeer)
PortsJoined: portsJoined,
})
}
}, func() ([]*ComponentPeer, []*FirewallRule) {
}, func() ([]*nmdata.Peer, []*FirewallRule) {
return peers, rules
}
}
func (c *NetworkMapComponents) getAllPeersFromGroups(groups []string, peerID string, sourcePostureChecksIDs []string) ([]*ComponentPeer, bool) {
func (c *NetworkMapComponents) getAllPeersFromGroups(groups []string, peerID string, sourcePostureChecksIDs []string) ([]*nmdata.Peer, bool) {
return c.filterPolicyPeers(c.getUniquePeerIDsFromGroupsIDs(groups), peerID, sourcePostureChecksIDs)
}
// getPeerFromResource resolves a rule side that names a peer directly. The peer is
// subject to the same admission as a group member, so a direct peer behaves exactly
// like a group holding only that peer.
func (c *NetworkMapComponents) getPeerFromResource(resource nmdata.Resource, peerID string, sourcePostureChecksIDs []string) ([]*nmdata.Peer, bool) {
return c.filterPolicyPeers([]string{resource.ID}, peerID, sourcePostureChecksIDs)
}
// filterPolicyPeers admits the peers of one rule side: known to the components and
// passing the rule's posture checks. It reports the admitted peers other than peerID
// and whether peerID itself is admitted on that side.
func (c *NetworkMapComponents) filterPolicyPeers(uniquePeerIDs []string, peerID string, sourcePostureChecksIDs []string) ([]*nmdata.Peer, bool) {
peerInGroups := false
uniquePeerIDs := c.getUniquePeerIDsFromGroupsIDs(groups)
filteredPeers := make([]*ComponentPeer, 0, len(uniquePeerIDs))
filteredPeers := make([]*nmdata.Peer, 0, len(uniquePeerIDs))
for _, p := range uniquePeerIDs {
peerInfo := c.GetPeerInfo(p)
@@ -425,22 +440,9 @@ func (c *NetworkMapComponents) getUniquePeerIDsFromGroupsIDs(groups []string) []
return ids
}
func (c *NetworkMapComponents) getPeerFromResource(resource Resource, peerID string) ([]*ComponentPeer, bool) {
if resource.ID == peerID {
return []*ComponentPeer{}, true
}
peerInfo := c.GetPeerInfo(resource.ID)
if peerInfo == nil {
return []*ComponentPeer{}, false
}
return []*ComponentPeer{peerInfo}, false
}
func (c *NetworkMapComponents) filterPeersByLoginExpiration(aclPeers []*ComponentPeer) ([]*ComponentPeer, []*ComponentPeer) {
peersToConnect := make([]*ComponentPeer, 0, len(aclPeers))
var expiredPeers []*ComponentPeer
func (c *NetworkMapComponents) filterPeersByLoginExpiration(aclPeers []*nmdata.Peer) ([]*nmdata.Peer, []*nmdata.Peer) {
peersToConnect := make([]*nmdata.Peer, 0, len(aclPeers))
var expiredPeers []*nmdata.Peer
for _, p := range aclPeers {
expired, _ := p.LoginExpired(c.AccountSettings.PeerLoginExpiration)
@@ -480,7 +482,7 @@ func (c *NetworkMapComponents) getPeerNSGroupsFromGroups(peerID string, groupLis
for _, gID := range nsGroup.Groups {
if _, found := groupList[gID]; found {
if !c.peerIsNameserver(peerIPStr, nsGroup) {
peerNSGroups = append(peerNSGroups, nsGroup.Copy())
peerNSGroups = append(peerNSGroups, toRealNSGroup(nsGroup))
}
break
}
@@ -490,7 +492,7 @@ func (c *NetworkMapComponents) getPeerNSGroupsFromGroups(peerID string, groupLis
return peerNSGroups
}
func (c *NetworkMapComponents) peerIsNameserver(peerIPStr string, nsGroup *nbdns.NameServerGroup) bool {
func (c *NetworkMapComponents) peerIsNameserver(peerIPStr string, nsGroup *nmdata.NameServerGroup) bool {
for _, ns := range nsGroup.NameServers {
if peerIPStr == ns.IP.String() {
return true
@@ -502,8 +504,8 @@ func (c *NetworkMapComponents) peerIsNameserver(peerIPStr string, nsGroup *nbdns
// filterAndExpandRoutes drops v6 routes for non-capable peers and duplicates
// the default v4 route (0.0.0.0/0) as ::/0 for v6-capable peers.
// TODO: the "-v6" suffix on IDs could collide with user-supplied route IDs.
func filterAndExpandRoutes(routes []*route.Route, includeIPv6 bool) []*route.Route {
filtered := make([]*route.Route, 0, len(routes))
func filterAndExpandRoutes(routes []*nmdata.Route, includeIPv6 bool) []*nmdata.Route {
filtered := make([]*nmdata.Route, 0, len(routes))
for _, r := range routes {
if !includeIPv6 && r.Network.Addr().Is6() {
continue
@@ -515,14 +517,14 @@ func filterAndExpandRoutes(routes []*route.Route, includeIPv6 bool) []*route.Rou
v6.ID = r.ID + "-v6-default"
v6.NetID = r.NetID + "-v6"
v6.Network = netip.MustParsePrefix("::/0")
v6.NetworkType = route.IPv6Network
v6.NetworkType = nmdata.NetworkTypeIPv6
filtered = append(filtered, v6)
}
}
return filtered
}
func (c *NetworkMapComponents) getRoutesToSync(peerID string, aclPeers []*ComponentPeer, peerGroups LookupMap) []*route.Route {
func (c *NetworkMapComponents) getRoutesToSync(peerID string, aclPeers []*nmdata.Peer, peerGroups LookupMap) []*nmdata.Route {
routes, peerDisabledRoutes := c.getRoutingPeerRoutes(peerID)
peerRoutesMembership := make(LookupMap)
for _, r := range append(routes, peerDisabledRoutes...) {
@@ -539,7 +541,7 @@ func (c *NetworkMapComponents) getRoutesToSync(peerID string, aclPeers []*Compon
return routes
}
func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoutes []*route.Route, disabledRoutes []*route.Route) {
func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoutes []*nmdata.Route, disabledRoutes []*nmdata.Route) {
peerInfo := c.GetPeerInfo(peerID)
if peerInfo == nil {
peerInfo = c.GetRouterPeerInfo(peerID)
@@ -548,9 +550,9 @@ func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoute
return enabledRoutes, disabledRoutes
}
seenRoute := make(map[route.ID]struct{})
seenRoute := make(map[string]struct{})
takeRoute := func(r *route.Route) {
takeRoute := func(r *nmdata.Route) {
if _, ok := seenRoute[r.ID]; ok {
return
}
@@ -569,7 +571,7 @@ func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoute
if entry.viaGroup {
newPeerRoute := entry.route.Copy()
newPeerRoute.PeerGroups = nil
newPeerRoute.ID = route.ID(string(entry.route.ID) + ":" + peerID)
newPeerRoute.ID = entry.route.ID + ":" + peerID
takeRoute(newPeerRoute)
continue
}
@@ -602,8 +604,8 @@ func (c *NetworkMapComponents) routesByPeer() map[string][]routeIndexEntry {
return c.routesByPeerIdx
}
func (c *NetworkMapComponents) filterRoutesByGroups(routes []*route.Route, groupListMap LookupMap) []*route.Route {
var filteredRoutes []*route.Route
func (c *NetworkMapComponents) filterRoutesByGroups(routes []*nmdata.Route, groupListMap LookupMap) []*nmdata.Route {
var filteredRoutes []*nmdata.Route
for _, r := range routes {
for _, groupID := range r.Groups {
_, found := groupListMap[groupID]
@@ -616,8 +618,8 @@ func (c *NetworkMapComponents) filterRoutesByGroups(routes []*route.Route, group
return filteredRoutes
}
func (c *NetworkMapComponents) filterRoutesFromPeersOfSameHAGroup(routes []*route.Route, peerMemberships LookupMap) []*route.Route {
var filteredRoutes []*route.Route
func (c *NetworkMapComponents) filterRoutesFromPeersOfSameHAGroup(routes []*nmdata.Route, peerMemberships LookupMap) []*nmdata.Route {
var filteredRoutes []*nmdata.Route
for _, r := range routes {
_, found := peerMemberships[string(r.GetHAUniqueID())]
if !found {
@@ -650,7 +652,7 @@ func (c *NetworkMapComponents) getPeerRoutesFirewallRules(ctx context.Context, p
return routesFirewallRules
}
func (c *NetworkMapComponents) getDefaultPermit(r *route.Route, includeIPv6 bool) []*RouteFirewallRule {
func (c *NetworkMapComponents) getDefaultPermit(r *nmdata.Route, includeIPv6 bool) []*RouteFirewallRule {
if r.Network.Addr().Is6() && !includeIPv6 {
return nil
}
@@ -667,7 +669,7 @@ func (c *NetworkMapComponents) getDefaultPermit(r *route.Route, includeIPv6 bool
Protocol: string(PolicyRuleProtocolALL),
Domains: r.Domains,
IsDynamic: r.IsDynamic(),
RouteID: r.ID,
RouteID: route.ID(r.ID),
}
rules := []*RouteFirewallRule{&rule}
@@ -678,7 +680,7 @@ func (c *NetworkMapComponents) getDefaultPermit(r *route.Route, includeIPv6 bool
ruleV6.SourceRanges = []string{"::/0"}
if isDefaultV4 {
ruleV6.Destination = "::/0"
ruleV6.RouteID = r.ID + "-v6-default"
ruleV6.RouteID = route.ID(r.ID + "-v6-default")
}
rules = append(rules, &ruleV6)
}
@@ -686,7 +688,7 @@ func (c *NetworkMapComponents) getDefaultPermit(r *route.Route, includeIPv6 bool
return rules
}
func (c *NetworkMapComponents) getDistributionGroupsPeers(r *route.Route) map[string]struct{} {
func (c *NetworkMapComponents) getDistributionGroupsPeers(r *nmdata.Route) map[string]struct{} {
distPeers := make(map[string]struct{})
for _, id := range r.Groups {
group := c.GetGroupInfo(id)
@@ -701,11 +703,17 @@ func (c *NetworkMapComponents) getDistributionGroupsPeers(r *route.Route) map[st
return distPeers
}
func (c *NetworkMapComponents) getAllRoutePoliciesFromGroups(accessControlGroups []string) []*Policy {
routePolicies := make([]*Policy, 0)
func (c *NetworkMapComponents) getAllRoutePoliciesFromGroups(accessControlGroups []string) []*nmdata.Policy {
routePolicies := make([]*nmdata.Policy, 0)
for _, groupID := range accessControlGroups {
for _, policy := range c.Policies {
if policy == nil {
continue
}
for _, rule := range policy.Rules {
if rule == nil {
continue
}
if slices.Contains(rule.Destinations, groupID) {
routePolicies = append(routePolicies, policy)
}
@@ -716,15 +724,15 @@ func (c *NetworkMapComponents) getAllRoutePoliciesFromGroups(accessControlGroups
return routePolicies
}
func (c *NetworkMapComponents) getRouteFirewallRules(ctx context.Context, peerID string, policies []*Policy, route *route.Route, distributionPeers map[string]struct{}, includeIPv6 bool) []*RouteFirewallRule {
func (c *NetworkMapComponents) getRouteFirewallRules(ctx context.Context, peerID string, policies []*nmdata.Policy, route *nmdata.Route, distributionPeers map[string]struct{}, includeIPv6 bool) []*RouteFirewallRule {
var fwRules []*RouteFirewallRule
for _, policy := range policies {
if !policy.Enabled {
if policy == nil || !policy.Enabled {
continue
}
for _, rule := range policy.Rules {
if !rule.Enabled {
if rule == nil || !rule.Enabled {
continue
}
@@ -736,7 +744,7 @@ func (c *NetworkMapComponents) getRouteFirewallRules(ctx context.Context, peerID
return fwRules
}
func (c *NetworkMapComponents) getRulePeers(rule *PolicyRule, postureChecks []string, peerID string, distributionPeers map[string]struct{}) []*ComponentPeer {
func (c *NetworkMapComponents) getRulePeers(rule *nmdata.PolicyRule, postureChecks []string, peerID string, distributionPeers map[string]struct{}) []*nmdata.Peer {
distPeersWithPolicy := make(map[string]struct{})
for _, id := range rule.Sources {
group := c.GetGroupInfo(id)
@@ -755,7 +763,7 @@ func (c *NetworkMapComponents) getRulePeers(rule *PolicyRule, postureChecks []st
}
}
}
if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" {
if rule.SourceResource.Type == string(ResourceTypePeer) && rule.SourceResource.ID != "" {
_, distPeer := distributionPeers[rule.SourceResource.ID]
_, valid := c.Peers[rule.SourceResource.ID]
if distPeer && valid && c.ValidatePostureChecksOnPeer(rule.SourceResource.ID, postureChecks) {
@@ -763,7 +771,7 @@ func (c *NetworkMapComponents) getRulePeers(rule *PolicyRule, postureChecks []st
}
}
distributionGroupPeers := make([]*ComponentPeer, 0, len(distPeersWithPolicy))
distributionGroupPeers := make([]*nmdata.Peer, 0, len(distPeersWithPolicy))
for pID := range distPeersWithPolicy {
peerInfo := c.GetPeerInfo(pID)
if peerInfo == nil {
@@ -774,9 +782,9 @@ 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, []*nmdata.Route, map[string]struct{}) {
var isRoutingPeer bool
var routes []*route.Route
var routes []*nmdata.Route
allSourcePeers := make(map[string]struct{})
for _, resource := range c.NetworkResources {
@@ -803,14 +811,17 @@ func (c *NetworkMapComponents) getNetworkResourcesRoutesToSync(peerID string) (b
func (c *NetworkMapComponents) processResourcePolicies(
peerID string,
resource *ComponentResource,
networkRoutingPeers map[string]*ComponentRouter,
resource *nmdata.NetworkResource,
networkRoutingPeers map[string]*nmdata.NetworkRouter,
addSourcePeers bool,
allSourcePeers map[string]struct{},
) []*route.Route {
var routes []*route.Route
) []*nmdata.Route {
var routes []*nmdata.Route
for _, policy := range c.ResourcePoliciesMap[resource.ID] {
if policy == nil || !policy.Enabled || len(policy.Rules) == 0 || policy.Rules[0] == nil {
continue
}
peers := c.getResourcePolicyPeers(policy)
if addSourcePeers {
for _, pID := range c.getPostureValidPeers(peers, policy.SourcePostureChecks) {
@@ -830,17 +841,17 @@ func (c *NetworkMapComponents) processResourcePolicies(
return routes
}
func (c *NetworkMapComponents) getResourcePolicyPeers(policy *Policy) []string {
if policy.Rules[0].SourceResource.Type == ResourceTypePeer && policy.Rules[0].SourceResource.ID != "" {
func (c *NetworkMapComponents) getResourcePolicyPeers(policy *nmdata.Policy) []string {
if policy.Rules[0].SourceResource.Type == string(ResourceTypePeer) && policy.Rules[0].SourceResource.ID != "" {
return []string{policy.Rules[0].SourceResource.ID}
}
return c.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups())
}
func (c *NetworkMapComponents) getNetworkResourcesRoutes(resource *ComponentResource, peerID string, router *ComponentRouter) []*route.Route {
func (c *NetworkMapComponents) getNetworkResourcesRoutes(resource *nmdata.NetworkResource, peerID string, router *nmdata.NetworkRouter) []*nmdata.Route {
resourceAppliedPolicies := c.ResourcePoliciesMap[resource.ID]
var routes []*route.Route
var routes []*nmdata.Route
if len(resourceAppliedPolicies) > 0 {
peerInfo := c.GetPeerInfo(peerID)
if peerInfo != nil {
@@ -851,9 +862,9 @@ func (c *NetworkMapComponents) getNetworkResourcesRoutes(resource *ComponentReso
return routes
}
func (c *NetworkMapComponents) networkResourceToRoute(resource *ComponentResource, peer *ComponentPeer, router *ComponentRouter) *route.Route {
r := &route.Route{
ID: route.ID(resource.ID + ":" + peer.ID),
func (c *NetworkMapComponents) networkResourceToRoute(resource *nmdata.NetworkResource, peer *nmdata.Peer, router *nmdata.NetworkRouter) *nmdata.Route {
r := &nmdata.Route{
ID: resource.ID + ":" + peer.ID,
AccountID: resource.AccountID,
Peer: peer.Key,
PeerID: peer.ID,
@@ -861,24 +872,24 @@ func (c *NetworkMapComponents) networkResourceToRoute(resource *ComponentResourc
Masquerade: router.Masquerade,
Enabled: resource.Enabled,
KeepRoute: true,
NetID: route.NetID(resource.Name),
NetID: resource.Name,
Description: resource.Description,
}
if resource.Type == ComponentResourceHost || resource.Type == ComponentResourceSubnet {
if resource.Type == string(ResourceTypeHost) || resource.Type == string(ResourceTypeSubnet) {
r.Network = resource.Prefix
r.NetworkType = route.IPv4Network
r.NetworkType = nmdata.NetworkTypeIPv4
if resource.Prefix.Addr().Is6() {
r.NetworkType = route.IPv6Network
r.NetworkType = nmdata.NetworkTypeIPv6
}
}
if resource.Type == ComponentResourceDomain {
if resource.Type == string(ResourceTypeDomain) {
domainList, err := domain.FromStringList([]string{resource.Domain})
if err == nil {
r.Domains = domainList
r.NetworkType = route.DomainNetwork
r.NetworkType = nmdata.NetworkTypeDomain
r.Network = netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 0, 2, 0}), 32)
}
}
@@ -896,7 +907,7 @@ func (c *NetworkMapComponents) getPostureValidPeers(inputPeers []string, posture
return dest
}
func (c *NetworkMapComponents) getPeerNetworkResourceFirewallRules(ctx context.Context, peerID string, routes []*route.Route, includeIPv6 bool) []*RouteFirewallRule {
func (c *NetworkMapComponents) getPeerNetworkResourceFirewallRules(ctx context.Context, peerID string, routes []*nmdata.Route, includeIPv6 bool) []*RouteFirewallRule {
routesFirewallRules := make([]*RouteFirewallRule, 0)
peerInfo := c.GetPeerInfo(peerID)
@@ -924,11 +935,17 @@ func (c *NetworkMapComponents) getPeerNetworkResourceFirewallRules(ctx context.C
return routesFirewallRules
}
func (c *NetworkMapComponents) getPoliciesSourcePeers(policies []*Policy) map[string]struct{} {
func (c *NetworkMapComponents) getPoliciesSourcePeers(policies []*nmdata.Policy) map[string]struct{} {
sourcePeers := make(map[string]struct{})
for _, policy := range policies {
if policy == nil {
continue
}
for _, rule := range policy.Rules {
if rule == nil {
continue
}
for _, sourceGroup := range rule.Sources {
group := c.GetGroupInfo(sourceGroup)
if group == nil {
@@ -940,7 +957,7 @@ func (c *NetworkMapComponents) getPoliciesSourcePeers(policies []*Policy) map[st
}
}
if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" {
if rule.SourceResource.Type == string(ResourceTypePeer) && rule.SourceResource.ID != "" {
sourcePeers[rule.SourceResource.ID] = struct{}{}
}
}
@@ -950,13 +967,13 @@ func (c *NetworkMapComponents) getPoliciesSourcePeers(policies []*Policy) map[st
}
func (c *NetworkMapComponents) addNetworksRoutingPeers(
networkResourcesRoutes []*route.Route,
networkResourcesRoutes []*nmdata.Route,
peerID string,
peersToConnect []*ComponentPeer,
expiredPeers []*ComponentPeer,
peersToConnect []*nmdata.Peer,
expiredPeers []*nmdata.Peer,
isRouter bool,
sourcePeers map[string]struct{},
) []*ComponentPeer {
) []*nmdata.Peer {
networkRoutesPeers := make(map[string]struct{}, len(networkResourcesRoutes))
for _, r := range networkResourcesRoutes {
@@ -1006,8 +1023,8 @@ type FirewallRuleContext struct {
PortsJoined string
}
func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *ComponentPeer, rule *PolicyRule, rc FirewallRuleContext) []*FirewallRule {
if !peer.IPv6.IsValid() || !targetPeer.SupportsIPv6 || !targetPeer.IPv6.IsValid() {
func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *nmdata.Peer, rule *nmdata.PolicyRule, rc FirewallRuleContext) []*FirewallRule {
if !peer.IPv6.IsValid() || !targetPeer.SupportsIPv6() || !targetPeer.IPv6.IsValid() {
return rules
}
@@ -1,8 +1,7 @@
package types
import (
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/route"
nmdata "github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
type GroupCompact struct {
@@ -13,26 +12,26 @@ type GroupCompact struct {
type NetworkMapComponentsCompact struct {
PeerID string
Network *Network
AccountSettings *AccountSettingsInfo
DNSSettings *DNSSettings
Network *nmdata.Network
AccountSettings *nmdata.AccountSettingsInfo
DNSSettings *nmdata.DNSSettings
CustomZoneDomain string
AllPeers []*ComponentPeer
AllPeers []*nmdata.Peer
PeerIndexes []int
RouterPeerIndexes []int
Groups map[string]*GroupCompact
AllPolicies []*Policy
AllPolicies []*nmdata.Policy
PolicyIndexes []int
ResourcePoliciesMap map[string][]int
Routes []*route.Route
NameServerGroups []*nbdns.NameServerGroup
AllDNSRecords []nbdns.SimpleRecord
AccountZones []nbdns.CustomZone
Routes []*nmdata.Route
NameServerGroups []*nmdata.NameServerGroup
AllDNSRecords []nmdata.SimpleRecord
AccountZones []nmdata.CustomZone
RoutersMap map[string]map[string]*ComponentRouter
NetworkResources []*ComponentResource
RoutersMap map[string]map[string]*nmdata.NetworkRouter
NetworkResources []*nmdata.NetworkResource
GroupIDToUserIDs map[string][]string
AllowedUserIDs map[string]struct{}
@@ -41,7 +40,7 @@ type NetworkMapComponentsCompact struct {
func (c *NetworkMapComponents) ToCompact() *NetworkMapComponentsCompact {
peerToIndex := make(map[string]int)
var allPeers []*ComponentPeer
var allPeers []*nmdata.Peer
for id, peer := range c.Peers {
if _, exists := peerToIndex[id]; !exists {
@@ -81,8 +80,8 @@ func (c *NetworkMapComponents) ToCompact() *NetworkMapComponentsCompact {
}
}
policyToIndex := make(map[*Policy]int)
var allPolicies []*Policy
policyToIndex := make(map[*nmdata.Policy]int)
var allPolicies []*nmdata.Policy
for _, policy := range c.Policies {
if _, exists := policyToIndex[policy]; !exists {
@@ -147,7 +146,7 @@ func (c *NetworkMapComponents) ToCompact() *NetworkMapComponentsCompact {
}
func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents {
peers := make(map[string]*ComponentPeer, len(c.PeerIndexes))
peers := make(map[string]*nmdata.Peer, len(c.PeerIndexes))
for _, idx := range c.PeerIndexes {
if idx >= 0 && idx < len(c.AllPeers) {
peer := c.AllPeers[idx]
@@ -155,7 +154,7 @@ func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents {
}
}
routerPeers := make(map[string]*ComponentPeer, len(c.RouterPeerIndexes))
routerPeers := make(map[string]*nmdata.Peer, len(c.RouterPeerIndexes))
for _, idx := range c.RouterPeerIndexes {
if idx >= 0 && idx < len(c.AllPeers) {
peer := c.AllPeers[idx]
@@ -163,7 +162,7 @@ func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents {
}
}
groups := make(map[string]*ComponentGroup, len(c.Groups))
groups := make(map[string]*nmdata.Group, len(c.Groups))
for id, gc := range c.Groups {
peerIDs := make([]string, 0, len(gc.PeerIndexes))
for _, idx := range gc.PeerIndexes {
@@ -171,25 +170,24 @@ func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents {
peerIDs = append(peerIDs, c.AllPeers[idx].ID)
}
}
groups[id] = &ComponentGroup{
ID: id,
groups[id] = &nmdata.Group{
Name: gc.Name,
Peers: peerIDs,
}
}
policies := make([]*Policy, len(c.PolicyIndexes))
policies := make([]*nmdata.Policy, len(c.PolicyIndexes))
for i, idx := range c.PolicyIndexes {
if idx >= 0 && idx < len(c.AllPolicies) {
policies[i] = c.AllPolicies[idx]
}
}
var resourcePoliciesMap map[string][]*Policy
var resourcePoliciesMap map[string][]*nmdata.Policy
if len(c.ResourcePoliciesMap) > 0 {
resourcePoliciesMap = make(map[string][]*Policy, len(c.ResourcePoliciesMap))
resourcePoliciesMap = make(map[string][]*nmdata.Policy, len(c.ResourcePoliciesMap))
for resID, indexes := range c.ResourcePoliciesMap {
pols := make([]*Policy, 0, len(indexes))
pols := make([]*nmdata.Policy, 0, len(indexes))
for _, idx := range indexes {
if idx >= 0 && idx < len(c.AllPolicies) {
pols = append(pols, c.AllPolicies[idx])
+70
View File
@@ -0,0 +1,70 @@
package types
import (
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
// This file holds the twin→real converters that survive the twin-NetworkMap
// refactor: only the DNS materialization. NetworkMap.DNSConfig stays a real
// nbdns.Config (the client DNS type), so Calculate converts the twin DNS
// components to nbdns at the output boundary. Peers/Routes/Network flow as
// twins all the way through and need no conversion.
func toRealNSGroup(n *nmdata.NameServerGroup) *nbdns.NameServerGroup {
if n == nil {
return nil
}
nameServers := make([]nbdns.NameServer, 0, len(n.NameServers))
for _, ns := range n.NameServers {
nameServers = append(nameServers, nbdns.NameServer{
IP: ns.IP,
NSType: nbdns.NameServerType(ns.NSType),
Port: ns.Port,
})
}
return &nbdns.NameServerGroup{
ID: n.ID,
Name: n.Name,
Description: n.Description,
NameServers: nameServers,
Groups: n.Groups,
Primary: n.Primary,
Domains: n.Domains,
Enabled: n.Enabled,
SearchDomainsEnabled: n.SearchDomainsEnabled,
}
}
func toRealRecords(recs []nmdata.SimpleRecord) []nbdns.SimpleRecord {
if recs == nil {
return nil
}
out := make([]nbdns.SimpleRecord, len(recs))
for i, r := range recs {
out[i] = nbdns.SimpleRecord{
Name: r.Name,
Type: r.Type,
Class: r.Class,
TTL: r.TTL,
RData: r.RData,
}
}
return out
}
func toRealZones(zones []nmdata.CustomZone) []nbdns.CustomZone {
if zones == nil {
return nil
}
out := make([]nbdns.CustomZone, len(zones))
for i, z := range zones {
out[i] = nbdns.CustomZone{
Domain: z.Domain,
Records: toRealRecords(z.Records),
SearchDomainDisabled: z.SearchDomainDisabled,
NonAuthoritative: z.NonAuthoritative,
}
}
return out
}
-268
View File
@@ -1,268 +0,0 @@
package types
import (
"errors"
"fmt"
"strconv"
"strings"
)
const (
// PolicyTrafficActionAccept indicates that the traffic is accepted
PolicyTrafficActionAccept = PolicyTrafficActionType("accept")
// PolicyTrafficActionDrop indicates that the traffic is dropped
PolicyTrafficActionDrop = PolicyTrafficActionType("drop")
)
const (
// PolicyRuleProtocolALL type of traffic
PolicyRuleProtocolALL = PolicyRuleProtocolType("all")
// PolicyRuleProtocolTCP type of traffic
PolicyRuleProtocolTCP = PolicyRuleProtocolType("tcp")
// PolicyRuleProtocolUDP type of traffic
PolicyRuleProtocolUDP = PolicyRuleProtocolType("udp")
// PolicyRuleProtocolICMP type of traffic
PolicyRuleProtocolICMP = PolicyRuleProtocolType("icmp")
// PolicyRuleProtocolNetbirdSSH type of traffic
PolicyRuleProtocolNetbirdSSH = PolicyRuleProtocolType("netbird-ssh")
)
const (
// PolicyRuleFlowDirect allows traffic from source to destination
PolicyRuleFlowDirect = PolicyRuleDirection("direct")
// PolicyRuleFlowBidirect allows traffic to both directions
PolicyRuleFlowBidirect = PolicyRuleDirection("bidirect")
)
const (
// DefaultRuleName is a name for the Default rule that is created for every account
DefaultRuleName = "Default"
// DefaultRuleDescription is a description for the Default rule that is created for every account
DefaultRuleDescription = "This is a default rule that allows connections between all the resources"
// DefaultPolicyName is a name for the Default policy that is created for every account
DefaultPolicyName = "Default"
// DefaultPolicyDescription is a description for the Default policy that is created for every account
DefaultPolicyDescription = "This is a default policy that allows connections between all the resources"
)
// PolicyUpdateOperation operation object with type and values to be applied
type PolicyUpdateOperation struct {
Type PolicyUpdateOperationType
Values []string
}
// Policy of the Rego query
type Policy struct {
// ID of the policy'
ID string `gorm:"primaryKey"`
PublicID string `json:"-"`
// AccountID is a reference to Account that this object belongs
AccountID string `json:"-" gorm:"index"`
// Name of the Policy
Name string
// Description of the policy visible in the UI
Description string
// Enabled status of the policy
Enabled bool
// Rules of the policy
Rules []*PolicyRule `gorm:"foreignKey:PolicyID;references:id;constraint:OnDelete:CASCADE;"`
// SourcePostureChecks are ID references to Posture checks for policy source groups
SourcePostureChecks []string `gorm:"serializer:json"`
}
// Copy returns a copy of the policy.
func (p *Policy) Copy() *Policy {
c := &Policy{
ID: p.ID,
AccountID: p.AccountID,
PublicID: p.PublicID,
Name: p.Name,
Description: p.Description,
Enabled: p.Enabled,
Rules: make([]*PolicyRule, len(p.Rules)),
SourcePostureChecks: make([]string, len(p.SourcePostureChecks)),
}
for i, r := range p.Rules {
c.Rules[i] = r.Copy()
}
copy(c.SourcePostureChecks, p.SourcePostureChecks)
return c
}
func (p *Policy) Equal(other *Policy) bool {
if p == nil || other == nil {
return p == other
}
if p.ID != other.ID ||
p.AccountID != other.AccountID ||
p.Name != other.Name ||
p.Description != other.Description ||
p.Enabled != other.Enabled {
return false
}
if !stringSlicesEqualUnordered(p.SourcePostureChecks, other.SourcePostureChecks) {
return false
}
if len(p.Rules) != len(other.Rules) {
return false
}
otherRules := make(map[string]*PolicyRule, len(other.Rules))
for _, r := range other.Rules {
otherRules[r.ID] = r
}
for _, r := range p.Rules {
otherRule, ok := otherRules[r.ID]
if !ok {
return false
}
if !r.Equal(otherRule) {
return false
}
}
return true
}
// EventMeta returns activity event meta related to this policy
func (p *Policy) EventMeta() map[string]any {
return map[string]any{"name": p.Name}
}
// UpgradeAndFix different version of policies to latest version
func (p *Policy) UpgradeAndFix() {
for _, r := range p.Rules {
// start migrate from version v0.20.3
if r.Protocol == "" {
r.Protocol = PolicyRuleProtocolALL
}
if r.Protocol == PolicyRuleProtocolALL && !r.Bidirectional {
r.Bidirectional = true
}
// -- v0.20.4
}
}
// RuleGroups returns a list of all groups referenced in the policy's rules,
// including sources and destinations.
func (p *Policy) RuleGroups() []string {
groups := make([]string, 0)
for _, rule := range p.Rules {
groups = append(groups, rule.Sources...)
groups = append(groups, rule.Destinations...)
}
return groups
}
// SourceGroups returns a slice of all unique source groups referenced in the policy's rules.
func (p *Policy) SourceGroups() []string {
if len(p.Rules) == 1 {
return p.Rules[0].Sources
}
groups := make(map[string]struct{}, len(p.Rules))
for _, rule := range p.Rules {
for _, source := range rule.Sources {
groups[source] = struct{}{}
}
}
groupIDs := make([]string, 0, len(groups))
for groupID := range groups {
groupIDs = append(groupIDs, groupID)
}
return groupIDs
}
func ParseRuleString(rule string) (PolicyRuleProtocolType, RulePortRange, error) {
rule = strings.TrimSpace(strings.ToLower(rule))
if rule == "all" {
return PolicyRuleProtocolALL, RulePortRange{}, nil
}
if rule == "icmp" {
return PolicyRuleProtocolICMP, RulePortRange{}, nil
}
split := strings.Split(rule, "/")
if len(split) != 2 {
return "", RulePortRange{}, errors.New("invalid rule format: expected protocol/port or protocol/port-range")
}
protoStr := strings.TrimSpace(split[0])
portStr := strings.TrimSpace(split[1])
var protocol PolicyRuleProtocolType
switch protoStr {
case "tcp":
protocol = PolicyRuleProtocolTCP
case "udp":
protocol = PolicyRuleProtocolUDP
case "icmp":
return "", RulePortRange{}, errors.New("icmp does not accept ports; use 'icmp' without '/…'")
case "netbird-ssh":
return PolicyRuleProtocolNetbirdSSH, RulePortRange{Start: nativeSSHPortNumber, End: nativeSSHPortNumber}, nil
default:
return "", RulePortRange{}, fmt.Errorf("invalid protocol: %q", protoStr)
}
portRange, err := parsePortRange(portStr)
if err != nil {
return "", RulePortRange{}, err
}
return protocol, portRange, nil
}
func parsePortRange(portStr string) (RulePortRange, error) {
if strings.Contains(portStr, "-") {
rangeParts := strings.Split(portStr, "-")
if len(rangeParts) != 2 {
return RulePortRange{}, fmt.Errorf("invalid port range %q", portStr)
}
start, err := parsePort(strings.TrimSpace(rangeParts[0]))
if err != nil {
return RulePortRange{}, err
}
end, err := parsePort(strings.TrimSpace(rangeParts[1]))
if err != nil {
return RulePortRange{}, err
}
if start > end {
return RulePortRange{}, fmt.Errorf("invalid port range: start %d > end %d", start, end)
}
return RulePortRange{Start: uint16(start), End: uint16(end)}, nil
}
p, err := parsePort(portStr)
if err != nil {
return RulePortRange{}, err
}
return RulePortRange{Start: uint16(p), End: uint16(p)}, nil
}
func parsePort(portStr string) (int, error) {
if portStr == "" {
return 0, errors.New("empty port")
}
p, err := strconv.Atoi(portStr)
if err != nil {
return 0, fmt.Errorf("invalid port %q: %w", portStr, err)
}
if p < 1 || p > 65535 {
return 0, fmt.Errorf("port out of range (165535): %d", p)
}
return p, nil
}
+89 -175
View File
@@ -1,22 +1,39 @@
package types
import (
"slices"
"errors"
"fmt"
"strconv"
"strings"
"github.com/netbirdio/netbird/shared/management/proto"
)
// PolicyUpdateOperationType operation type
type PolicyUpdateOperationType int
// PolicyTrafficActionType action type for the firewall
type PolicyTrafficActionType string
// PolicyRuleProtocolType type of traffic
type PolicyRuleProtocolType string
// PolicyRuleDirection direction of traffic
type PolicyRuleDirection string
const (
// PolicyTrafficActionAccept indicates that the traffic is accepted
PolicyTrafficActionAccept = PolicyTrafficActionType("accept")
// PolicyTrafficActionDrop indicates that the traffic is dropped
PolicyTrafficActionDrop = PolicyTrafficActionType("drop")
)
const (
// PolicyRuleProtocolALL type of traffic
PolicyRuleProtocolALL = PolicyRuleProtocolType("all")
// PolicyRuleProtocolTCP type of traffic
PolicyRuleProtocolTCP = PolicyRuleProtocolType("tcp")
// PolicyRuleProtocolUDP type of traffic
PolicyRuleProtocolUDP = PolicyRuleProtocolType("udp")
// PolicyRuleProtocolICMP type of traffic
PolicyRuleProtocolICMP = PolicyRuleProtocolType("icmp")
// PolicyRuleProtocolNetbirdSSH type of traffic
PolicyRuleProtocolNetbirdSSH = PolicyRuleProtocolType("netbird-ssh")
)
// RulePortRange represents a range of ports for a firewall rule.
type RulePortRange struct {
@@ -39,187 +56,84 @@ func (r *RulePortRange) Equal(other *RulePortRange) bool {
return r.Start == other.Start && r.End == other.End
}
// PolicyRule is the metadata of the policy
type PolicyRule struct {
// ID of the policy rule
ID string `gorm:"primaryKey"`
func ParseRuleString(rule string) (PolicyRuleProtocolType, RulePortRange, error) {
rule = strings.TrimSpace(strings.ToLower(rule))
if rule == "all" {
return PolicyRuleProtocolALL, RulePortRange{}, nil
}
if rule == "icmp" {
return PolicyRuleProtocolICMP, RulePortRange{}, nil
}
// PolicyID is a reference to Policy that this object belongs
PolicyID string `json:"-" gorm:"index"`
split := strings.Split(rule, "/")
if len(split) != 2 {
return "", RulePortRange{}, errors.New("invalid rule format: expected protocol/port or protocol/port-range")
}
// Name of the rule visible in the UI
Name string
protoStr := strings.TrimSpace(split[0])
portStr := strings.TrimSpace(split[1])
// Description of the rule visible in the UI
Description string
var protocol PolicyRuleProtocolType
switch protoStr {
case "tcp":
protocol = PolicyRuleProtocolTCP
case "udp":
protocol = PolicyRuleProtocolUDP
case "icmp":
return "", RulePortRange{}, errors.New("icmp does not accept ports; use 'icmp' without '/…'")
case "netbird-ssh":
return PolicyRuleProtocolNetbirdSSH, RulePortRange{Start: nativeSSHPortNumber, End: nativeSSHPortNumber}, nil
default:
return "", RulePortRange{}, fmt.Errorf("invalid protocol: %q", protoStr)
}
// Enabled status of rule in the system
Enabled bool
portRange, err := parsePortRange(portStr)
if err != nil {
return "", RulePortRange{}, err
}
// Action policy accept or drops packets
Action PolicyTrafficActionType
// Destinations policy destination groups
Destinations []string `gorm:"serializer:json"`
// DestinationResource policy destination resource that the rule is applied to
DestinationResource Resource `gorm:"serializer:json"`
// Sources policy source groups
Sources []string `gorm:"serializer:json"`
// SourceResource policy source resource that the rule is applied to
SourceResource Resource `gorm:"serializer:json"`
// Bidirectional define if the rule is applicable in both directions, sources, and destinations
Bidirectional bool
// Protocol type of the traffic
Protocol PolicyRuleProtocolType
// Ports or it ranges list
Ports []string `gorm:"serializer:json"`
// PortRanges a list of port ranges.
PortRanges []RulePortRange `gorm:"serializer:json"`
// AuthorizedGroups is a map of groupIDs and their respective access to local users via ssh
AuthorizedGroups map[string][]string `gorm:"serializer:json"`
// AuthorizedUser is a list of userIDs that are authorized to access local resources via ssh
AuthorizedUser string
return protocol, portRange, nil
}
// Copy returns a copy of a policy rule
func (pm *PolicyRule) Copy() *PolicyRule {
rule := &PolicyRule{
ID: pm.ID,
PolicyID: pm.PolicyID,
Name: pm.Name,
Description: pm.Description,
Enabled: pm.Enabled,
Action: pm.Action,
Destinations: make([]string, len(pm.Destinations)),
DestinationResource: pm.DestinationResource,
Sources: make([]string, len(pm.Sources)),
SourceResource: pm.SourceResource,
Bidirectional: pm.Bidirectional,
Protocol: pm.Protocol,
Ports: make([]string, len(pm.Ports)),
PortRanges: make([]RulePortRange, len(pm.PortRanges)),
AuthorizedGroups: make(map[string][]string, len(pm.AuthorizedGroups)),
AuthorizedUser: pm.AuthorizedUser,
}
copy(rule.Destinations, pm.Destinations)
copy(rule.Sources, pm.Sources)
copy(rule.Ports, pm.Ports)
copy(rule.PortRanges, pm.PortRanges)
for k, v := range pm.AuthorizedGroups {
rule.AuthorizedGroups[k] = make([]string, len(v))
copy(rule.AuthorizedGroups[k], v)
}
return rule
}
func (pm *PolicyRule) Equal(other *PolicyRule) bool {
if pm == nil || other == nil {
return pm == other
}
if pm.ID != other.ID ||
pm.PolicyID != other.PolicyID ||
pm.Name != other.Name ||
pm.Description != other.Description ||
pm.Enabled != other.Enabled ||
pm.Action != other.Action ||
pm.Bidirectional != other.Bidirectional ||
pm.Protocol != other.Protocol ||
pm.SourceResource != other.SourceResource ||
pm.DestinationResource != other.DestinationResource ||
pm.AuthorizedUser != other.AuthorizedUser {
return false
}
if !stringSlicesEqualUnordered(pm.Sources, other.Sources) {
return false
}
if !stringSlicesEqualUnordered(pm.Destinations, other.Destinations) {
return false
}
if !stringSlicesEqualUnordered(pm.Ports, other.Ports) {
return false
}
if !portRangeSlicesEqualUnordered(pm.PortRanges, other.PortRanges) {
return false
}
if !authorizedGroupsEqual(pm.AuthorizedGroups, other.AuthorizedGroups) {
return false
}
return true
}
func stringSlicesEqualUnordered(a, b []string) bool {
if len(a) != len(b) {
return false
}
if len(a) == 0 {
return true
}
sorted1 := make([]string, len(a))
sorted2 := make([]string, len(b))
copy(sorted1, a)
copy(sorted2, b)
slices.Sort(sorted1)
slices.Sort(sorted2)
return slices.Equal(sorted1, sorted2)
}
func portRangeSlicesEqualUnordered(a, b []RulePortRange) bool {
if len(a) != len(b) {
return false
}
if len(a) == 0 {
return true
}
cmp := func(x, y RulePortRange) int {
if x.Start != y.Start {
if x.Start < y.Start {
return -1
}
return 1
func parsePortRange(portStr string) (RulePortRange, error) {
if strings.Contains(portStr, "-") {
rangeParts := strings.Split(portStr, "-")
if len(rangeParts) != 2 {
return RulePortRange{}, fmt.Errorf("invalid port range %q", portStr)
}
if x.End != y.End {
if x.End < y.End {
return -1
}
return 1
start, err := parsePort(strings.TrimSpace(rangeParts[0]))
if err != nil {
return RulePortRange{}, err
}
return 0
end, err := parsePort(strings.TrimSpace(rangeParts[1]))
if err != nil {
return RulePortRange{}, err
}
if start > end {
return RulePortRange{}, fmt.Errorf("invalid port range: start %d > end %d", start, end)
}
return RulePortRange{Start: uint16(start), End: uint16(end)}, nil
}
sorted1 := make([]RulePortRange, len(a))
sorted2 := make([]RulePortRange, len(b))
copy(sorted1, a)
copy(sorted2, b)
slices.SortFunc(sorted1, cmp)
slices.SortFunc(sorted2, cmp)
return slices.EqualFunc(sorted1, sorted2, func(x, y RulePortRange) bool {
return x.Start == y.Start && x.End == y.End
})
p, err := parsePort(portStr)
if err != nil {
return RulePortRange{}, err
}
return RulePortRange{Start: uint16(p), End: uint16(p)}, nil
}
func authorizedGroupsEqual(a, b map[string][]string) bool {
if len(a) != len(b) {
return false
func parsePort(portStr string) (int, error) {
if portStr == "" {
return 0, errors.New("empty port")
}
for k, va := range a {
vb, ok := b[k]
if !ok {
return false
}
if !stringSlicesEqualUnordered(va, vb) {
return false
}
p, err := strconv.Atoi(portStr)
if err != nil {
return 0, fmt.Errorf("invalid port %q: %w", portStr, err)
}
return true
if p < 1 || p > 65535 {
return 0, fmt.Errorf("port out of range (165535): %d", p)
}
return p, nil
}
+6 -26
View File
@@ -1,9 +1,5 @@
package types
import (
"github.com/netbirdio/netbird/shared/management/http/api"
)
type ResourceType string
const (
@@ -13,27 +9,11 @@ const (
ResourceTypeSubnet ResourceType = "subnet"
)
type Resource struct {
ID string
Type ResourceType
}
func (r *Resource) ToAPIResponse() *api.Resource {
if r.ID == "" && r.Type == "" {
return nil
}
return &api.Resource{
Id: r.ID,
Type: api.ResourceType(r.Type),
func (t ResourceType) Valid() bool {
switch t {
case ResourceTypePeer, ResourceTypeDomain, ResourceTypeHost, ResourceTypeSubnet:
return true
default:
return false
}
}
func (r *Resource) FromAPIRequest(req *api.Resource) {
if req == nil {
return
}
r.ID = req.Id
r.Type = ResourceType(req.Type)
}