mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-02 11:39:06 +02:00
[client, management] offload client config generation to the client (#6711)
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io> Co-authored-by: crn4 <vladimir@netbird.io> Co-authored-by: pascal <pascal@netbird.io>
This commit is contained in:
co-authored by
crn4
pascal
parent
ed682fad87
commit
8435682ac8
@@ -45,7 +45,7 @@ jobs:
|
|||||||
display_name: Linux
|
display_name: Linux
|
||||||
name: ${{ matrix.display_name }}
|
name: ${{ matrix.display_name }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 15
|
timeout-minutes: 25
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
@@ -79,4 +79,4 @@ jobs:
|
|||||||
skip-cache: true
|
skip-cache: true
|
||||||
skip-save-cache: true
|
skip-save-cache: true
|
||||||
cache-invalidation-interval: 0
|
cache-invalidation-interval: 0
|
||||||
args: --timeout=12m
|
args: --timeout=20m
|
||||||
|
|||||||
@@ -351,6 +351,7 @@ func (a *Auth) setSystemInfoFlags(info *system.Info) {
|
|||||||
a.config.BlockLANAccess,
|
a.config.BlockLANAccess,
|
||||||
a.config.BlockInbound,
|
a.config.BlockInbound,
|
||||||
a.config.DisableIPv6,
|
a.config.DisableIPv6,
|
||||||
|
a.config.SyncMessageVersion,
|
||||||
a.config.EnableSSHRoot,
|
a.config.EnableSSHRoot,
|
||||||
a.config.EnableSSHSFTP,
|
a.config.EnableSSHSFTP,
|
||||||
a.config.EnableSSHLocalPortForwarding,
|
a.config.EnableSSHLocalPortForwarding,
|
||||||
|
|||||||
@@ -621,6 +621,7 @@ func createEngineConfig(key wgtypes.Key, config *profilemanager.Config, peerConf
|
|||||||
BlockLANAccess: config.BlockLANAccess,
|
BlockLANAccess: config.BlockLANAccess,
|
||||||
BlockInbound: config.BlockInbound,
|
BlockInbound: config.BlockInbound,
|
||||||
DisableIPv6: config.DisableIPv6,
|
DisableIPv6: config.DisableIPv6,
|
||||||
|
SyncMessageVersion: config.SyncMessageVersion,
|
||||||
|
|
||||||
LazyConnection: lazyconn.ParseState(config.LazyConnection),
|
LazyConnection: lazyconn.ParseState(config.LazyConnection),
|
||||||
|
|
||||||
@@ -696,6 +697,7 @@ func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte,
|
|||||||
config.BlockLANAccess,
|
config.BlockLANAccess,
|
||||||
config.BlockInbound,
|
config.BlockInbound,
|
||||||
config.DisableIPv6,
|
config.DisableIPv6,
|
||||||
|
config.SyncMessageVersion,
|
||||||
config.EnableSSHRoot,
|
config.EnableSSHRoot,
|
||||||
config.EnableSSHSFTP,
|
config.EnableSSHSFTP,
|
||||||
config.EnableSSHLocalPortForwarding,
|
config.EnableSSHLocalPortForwarding,
|
||||||
|
|||||||
@@ -676,6 +676,7 @@ func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder)
|
|||||||
configContent.WriteString(fmt.Sprintf("BlockLANAccess: %v\n", g.internalConfig.BlockLANAccess))
|
configContent.WriteString(fmt.Sprintf("BlockLANAccess: %v\n", g.internalConfig.BlockLANAccess))
|
||||||
configContent.WriteString(fmt.Sprintf("BlockInbound: %v\n", g.internalConfig.BlockInbound))
|
configContent.WriteString(fmt.Sprintf("BlockInbound: %v\n", g.internalConfig.BlockInbound))
|
||||||
configContent.WriteString(fmt.Sprintf("DisableIPv6: %v\n", g.internalConfig.DisableIPv6))
|
configContent.WriteString(fmt.Sprintf("DisableIPv6: %v\n", g.internalConfig.DisableIPv6))
|
||||||
|
configContent.WriteString(fmt.Sprintf("SyncMessageVersion: %v\n", g.internalConfig.SyncMessageVersion))
|
||||||
|
|
||||||
if g.internalConfig.DisableNotifications != nil {
|
if g.internalConfig.DisableNotifications != nil {
|
||||||
configContent.WriteString(fmt.Sprintf("DisableNotifications: %v\n", *g.internalConfig.DisableNotifications))
|
configContent.WriteString(fmt.Sprintf("DisableNotifications: %v\n", *g.internalConfig.DisableNotifications))
|
||||||
|
|||||||
@@ -887,6 +887,8 @@ func TestAddConfig_AllFieldsCovered(t *testing.T) {
|
|||||||
ClientCertKeyPath: "/tmp/key",
|
ClientCertKeyPath: "/tmp/key",
|
||||||
LazyConnection: "on",
|
LazyConnection: "on",
|
||||||
MTU: 1280,
|
MTU: 1280,
|
||||||
|
DisableIPv6: true,
|
||||||
|
SyncMessageVersion: func(v int) *int { return &v }(1),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, anonymize := range []bool{false, true} {
|
for _, anonymize := range []bool{false, true} {
|
||||||
|
|||||||
@@ -64,7 +64,10 @@ import (
|
|||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
mgm "github.com/netbirdio/netbird/shared/management/client"
|
mgm "github.com/netbirdio/netbird/shared/management/client"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
|
sharedgrpc "github.com/netbirdio/netbird/shared/management/grpc"
|
||||||
|
nbnetworkmap "github.com/netbirdio/netbird/shared/management/networkmap"
|
||||||
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
types "github.com/netbirdio/netbird/shared/management/types"
|
||||||
"github.com/netbirdio/netbird/shared/netiputil"
|
"github.com/netbirdio/netbird/shared/netiputil"
|
||||||
auth "github.com/netbirdio/netbird/shared/relay/auth/hmac"
|
auth "github.com/netbirdio/netbird/shared/relay/auth/hmac"
|
||||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
@@ -147,6 +150,7 @@ type EngineConfig struct {
|
|||||||
BlockLANAccess bool
|
BlockLANAccess bool
|
||||||
BlockInbound bool
|
BlockInbound bool
|
||||||
DisableIPv6 bool
|
DisableIPv6 bool
|
||||||
|
SyncMessageVersion *int
|
||||||
|
|
||||||
// LazyConnection is the MDM-sourced lazy-connection override; StateUnset defers to
|
// LazyConnection is the MDM-sourced lazy-connection override; StateUnset defers to
|
||||||
// the env var and management feature flag.
|
// the env var and management feature flag.
|
||||||
@@ -220,6 +224,13 @@ type Engine struct {
|
|||||||
// networkSerial is the latest CurrentSerial (state ID) of the network sent by the Management service
|
// networkSerial is the latest CurrentSerial (state ID) of the network sent by the Management service
|
||||||
networkSerial uint64
|
networkSerial uint64
|
||||||
|
|
||||||
|
// latestComponents is the most-recent NetworkMapComponents decoded from
|
||||||
|
// a NetworkMapEnvelope (capability=3 peers only). Held alongside the
|
||||||
|
// NetworkMap that Calculate() produced from it so future incremental
|
||||||
|
// updates have a base to apply changes against. nil for legacy-format
|
||||||
|
// peers. Guarded by syncMsgMux.
|
||||||
|
latestComponents *types.NetworkMapComponents
|
||||||
|
|
||||||
networkMonitor *networkmonitor.NetworkMonitor
|
networkMonitor *networkmonitor.NetworkMonitor
|
||||||
|
|
||||||
sshServer sshServer
|
sshServer sshServer
|
||||||
@@ -963,8 +974,12 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
|
|
||||||
e.ApplySessionDeadline(update.GetSessionExpiresAt())
|
e.ApplySessionDeadline(update.GetSessionExpiresAt())
|
||||||
|
|
||||||
if update.NetworkMap != nil && update.NetworkMap.PeerConfig != nil {
|
// Envelope sync responses carry PeerConfig at the top level; legacy
|
||||||
e.handleAutoUpdateVersion(update.NetworkMap.PeerConfig.AutoUpdate)
|
// NetworkMap syncs carry it under NetworkMap.PeerConfig.
|
||||||
|
if pc := update.GetPeerConfig(); pc != nil {
|
||||||
|
e.handleAutoUpdateVersion(pc.GetAutoUpdate())
|
||||||
|
} else if nm := update.GetNetworkMap(); nm != nil && nm.GetPeerConfig() != nil {
|
||||||
|
e.handleAutoUpdateVersion(nm.GetPeerConfig().GetAutoUpdate())
|
||||||
}
|
}
|
||||||
|
|
||||||
done := e.phase("netbird_config")
|
done := e.phase("netbird_config")
|
||||||
@@ -974,12 +989,47 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decode the network map from either the components envelope or the
|
||||||
|
// legacy proto.NetworkMap before the posture-check gating below, so the
|
||||||
|
// "is there a network map" decision covers both wire shapes.
|
||||||
|
var (
|
||||||
|
nm *mgmProto.NetworkMap
|
||||||
|
components *types.NetworkMapComponents
|
||||||
|
)
|
||||||
|
if version := update.GetVersion(); version == int32(sharedgrpc.ComponentNetworkMap) {
|
||||||
|
// Components-format peer: decode the envelope back to typed
|
||||||
|
// components, run Calculate() locally, and convert to the wire
|
||||||
|
// NetworkMap shape the rest of the engine consumes. Components are
|
||||||
|
// retained so future incremental updates can apply deltas instead
|
||||||
|
// of doing a full reconstruction.
|
||||||
|
envelope := update.GetNetworkMapEnvelope()
|
||||||
|
if envelope == nil {
|
||||||
|
return fmt.Errorf("received a SyncReponse indicating use of components network map, but components are missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
localKey := e.config.WgPrivateKey.PublicKey().String()
|
||||||
|
dnsName := ""
|
||||||
|
if pc := update.GetPeerConfig(); pc != nil {
|
||||||
|
// PeerConfig.Fqdn = "<dns_label>.<dns_domain>" — extract the
|
||||||
|
// shared domain by stripping the peer's own label prefix. Falls
|
||||||
|
// back to empty if the FQDN doesn't have the expected shape.
|
||||||
|
dnsName = extractDNSDomainFromFQDN(pc.GetFqdn())
|
||||||
|
}
|
||||||
|
result, err := nbnetworkmap.EnvelopeToNetworkMap(e.ctx, envelope, localKey, dnsName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decode network map envelope: %w", err)
|
||||||
|
}
|
||||||
|
nm = result.NetworkMap
|
||||||
|
components = result.Components
|
||||||
|
} else {
|
||||||
|
nm = update.GetNetworkMap()
|
||||||
|
}
|
||||||
|
|
||||||
// Posture checks are bound to the network map presence:
|
// Posture checks are bound to the network map presence:
|
||||||
// NetworkMap != nil, checks present -> apply the received checks
|
// NetworkMap != nil, checks present -> apply the received checks
|
||||||
// NetworkMap != nil, checks nil -> posture checks were removed, clear them
|
// NetworkMap != nil, checks nil -> posture checks were removed, clear them
|
||||||
// NetworkMap == nil -> config-only update (e.g. relay token rotation),
|
// NetworkMap == nil -> config-only update (e.g. relay token rotation),
|
||||||
// leave the previously applied checks untouched
|
// leave the previously applied checks untouched
|
||||||
nm := update.GetNetworkMap()
|
|
||||||
if nm == nil {
|
if nm == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -992,6 +1042,14 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
done = e.phase("persist")
|
done = e.phase("persist")
|
||||||
|
// Only retain the components view when the server sent the envelope
|
||||||
|
// path. A legacy proto.NetworkMap means components == nil; writing it
|
||||||
|
// here would clobber a previously-cached snapshot, breaking the
|
||||||
|
// incremental-delta base on a future envelope sync.
|
||||||
|
if components != nil {
|
||||||
|
e.latestComponents = components
|
||||||
|
}
|
||||||
|
|
||||||
e.persistSyncResponse(update)
|
e.persistSyncResponse(update)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
@@ -1005,6 +1063,19 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractDNSDomainFromFQDN returns the trailing dotted domain part of the
|
||||||
|
// receiving peer's FQDN — the same value the management server fills as
|
||||||
|
// dnsName when it builds the legacy NetworkMap. "peer42.netbird.cloud" →
|
||||||
|
// "netbird.cloud". An empty string is returned for unrecognized formats.
|
||||||
|
func extractDNSDomainFromFQDN(fqdn string) string {
|
||||||
|
for i := 0; i < len(fqdn); i++ {
|
||||||
|
if fqdn[i] == '.' && i+1 < len(fqdn) {
|
||||||
|
return fqdn[i+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// updateNetbirdConfig applies the management-provided NetBird configuration:
|
// updateNetbirdConfig applies the management-provided NetBird configuration:
|
||||||
// STUN/TURN and relay servers, flow logging and DNS settings. A nil config is a no-op,
|
// STUN/TURN and relay servers, flow logging and DNS settings. A nil config is a no-op,
|
||||||
// which is the case for sync updates carrying only a network map.
|
// which is the case for sync updates carrying only a network map.
|
||||||
@@ -1164,6 +1235,7 @@ func (e *Engine) applyInfoFlags(info *system.Info) {
|
|||||||
e.config.BlockLANAccess,
|
e.config.BlockLANAccess,
|
||||||
e.config.BlockInbound,
|
e.config.BlockInbound,
|
||||||
e.config.DisableIPv6,
|
e.config.DisableIPv6,
|
||||||
|
e.config.SyncMessageVersion,
|
||||||
e.config.EnableSSHRoot,
|
e.config.EnableSSHRoot,
|
||||||
e.config.EnableSSHSFTP,
|
e.config.EnableSSHSFTP,
|
||||||
e.config.EnableSSHLocalPortForwarding,
|
e.config.EnableSSHLocalPortForwarding,
|
||||||
@@ -2032,6 +2104,7 @@ func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, bool, err
|
|||||||
e.config.BlockLANAccess,
|
e.config.BlockLANAccess,
|
||||||
e.config.BlockInbound,
|
e.config.BlockInbound,
|
||||||
e.config.DisableIPv6,
|
e.config.DisableIPv6,
|
||||||
|
e.config.SyncMessageVersion,
|
||||||
e.config.EnableSSHRoot,
|
e.config.EnableSSHRoot,
|
||||||
e.config.EnableSSHSFTP,
|
e.config.EnableSSHSFTP,
|
||||||
e.config.EnableSSHLocalPortForwarding,
|
e.config.EnableSSHLocalPortForwarding,
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ type ConfigInput struct {
|
|||||||
BlockLANAccess *bool
|
BlockLANAccess *bool
|
||||||
BlockInbound *bool
|
BlockInbound *bool
|
||||||
DisableIPv6 *bool
|
DisableIPv6 *bool
|
||||||
|
SyncMessageVersion *int
|
||||||
|
|
||||||
DisableNotifications *bool
|
DisableNotifications *bool
|
||||||
|
|
||||||
@@ -137,6 +138,7 @@ type Config struct {
|
|||||||
BlockLANAccess bool
|
BlockLANAccess bool
|
||||||
BlockInbound bool
|
BlockInbound bool
|
||||||
DisableIPv6 bool
|
DisableIPv6 bool
|
||||||
|
SyncMessageVersion *int
|
||||||
|
|
||||||
DisableNotifications *bool
|
DisableNotifications *bool
|
||||||
|
|
||||||
@@ -587,6 +589,12 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
|
|||||||
updated = true
|
updated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if input.SyncMessageVersion != nil && *input.SyncMessageVersion != *config.SyncMessageVersion {
|
||||||
|
log.Infof("setting SyncMessageVersion to %v", *input.SyncMessageVersion)
|
||||||
|
*config.SyncMessageVersion = *input.SyncMessageVersion
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
if input.DisableNotifications != nil && (config.DisableNotifications == nil || *input.DisableNotifications != *config.DisableNotifications) {
|
if input.DisableNotifications != nil && (config.DisableNotifications == nil || *input.DisableNotifications != *config.DisableNotifications) {
|
||||||
if *input.DisableNotifications {
|
if *input.DisableNotifications {
|
||||||
log.Infof("disabling notifications")
|
log.Infof("disabling notifications")
|
||||||
|
|||||||
@@ -79,13 +79,15 @@ type Info struct {
|
|||||||
EnableSSHLocalPortForwarding bool
|
EnableSSHLocalPortForwarding bool
|
||||||
EnableSSHRemotePortForwarding bool
|
EnableSSHRemotePortForwarding bool
|
||||||
DisableSSHAuth bool
|
DisableSSHAuth bool
|
||||||
|
|
||||||
|
SyncMessageVersion *int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Info) SetFlags(
|
func (i *Info) SetFlags(
|
||||||
rosenpassEnabled, rosenpassPermissive bool,
|
rosenpassEnabled, rosenpassPermissive bool,
|
||||||
serverSSHAllowed *bool,
|
serverSSHAllowed *bool,
|
||||||
disableClientRoutes, disableServerRoutes,
|
disableClientRoutes, disableServerRoutes,
|
||||||
disableDNS, disableFirewall, blockLANAccess, blockInbound, disableIPv6 bool,
|
disableDNS, disableFirewall, blockLANAccess, blockInbound, disableIPv6 bool, syncMessageVersion *int,
|
||||||
enableSSHRoot, enableSSHSFTP, enableSSHLocalPortForwarding, enableSSHRemotePortForwarding *bool,
|
enableSSHRoot, enableSSHSFTP, enableSSHLocalPortForwarding, enableSSHRemotePortForwarding *bool,
|
||||||
disableSSHAuth *bool,
|
disableSSHAuth *bool,
|
||||||
) {
|
) {
|
||||||
@@ -103,6 +105,8 @@ func (i *Info) SetFlags(
|
|||||||
i.BlockInbound = blockInbound
|
i.BlockInbound = blockInbound
|
||||||
i.DisableIPv6 = disableIPv6
|
i.DisableIPv6 = disableIPv6
|
||||||
|
|
||||||
|
i.SyncMessageVersion = syncMessageVersion
|
||||||
|
|
||||||
if enableSSHRoot != nil {
|
if enableSSHRoot != nil {
|
||||||
i.EnableSSHRoot = *enableSSHRoot
|
i.EnableSSHRoot = *enableSSHRoot
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-10
@@ -74,6 +74,9 @@ type ServerConfig struct {
|
|||||||
ActivityStore StoreConfig `yaml:"activityStore"`
|
ActivityStore StoreConfig `yaml:"activityStore"`
|
||||||
AuthStore StoreConfig `yaml:"authStore"`
|
AuthStore StoreConfig `yaml:"authStore"`
|
||||||
ReverseProxy ReverseProxyConfig `yaml:"reverseProxy"`
|
ReverseProxy ReverseProxyConfig `yaml:"reverseProxy"`
|
||||||
|
|
||||||
|
SupportedSyncMessageVersions *int `yaml:"supportedSyncMessageVersions,omitempty"`
|
||||||
|
PerAccountSupportedSyncMessageVersions map[string]int `yaml:"perAccountSupportedSyncMessageVersions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLSConfig contains TLS/HTTPS settings
|
// TLSConfig contains TLS/HTTPS settings
|
||||||
@@ -696,16 +699,18 @@ func (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {
|
|||||||
httpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull
|
httpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull
|
||||||
|
|
||||||
return &nbconfig.Config{
|
return &nbconfig.Config{
|
||||||
Stuns: stuns,
|
Stuns: stuns,
|
||||||
Relay: relayConfig,
|
Relay: relayConfig,
|
||||||
Signal: signalConfig,
|
Signal: signalConfig,
|
||||||
Datadir: mgmt.DataDir,
|
Datadir: mgmt.DataDir,
|
||||||
DataStoreEncryptionKey: mgmt.Store.EncryptionKey,
|
DataStoreEncryptionKey: mgmt.Store.EncryptionKey,
|
||||||
HttpConfig: httpConfig,
|
HttpConfig: httpConfig,
|
||||||
StoreConfig: storeConfig,
|
StoreConfig: storeConfig,
|
||||||
ReverseProxy: reverseProxy,
|
ReverseProxy: reverseProxy,
|
||||||
DisableDefaultPolicy: mgmt.DisableDefaultPolicy,
|
DisableDefaultPolicy: mgmt.DisableDefaultPolicy,
|
||||||
EmbeddedIdP: embeddedIdP,
|
EmbeddedIdP: embeddedIdP,
|
||||||
|
HighestSupportedSyncMessageVersion: c.Server.SupportedSyncMessageVersions,
|
||||||
|
PerAccountHighestSupportedSyncMessageVersion: c.Server.PerAccountSupportedSyncMessageVersions,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
relayServer "github.com/netbirdio/netbird/relay/server"
|
relayServer "github.com/netbirdio/netbird/relay/server"
|
||||||
"github.com/netbirdio/netbird/relay/server/listener"
|
"github.com/netbirdio/netbird/relay/server/listener"
|
||||||
"github.com/netbirdio/netbird/relay/server/listener/ws"
|
"github.com/netbirdio/netbird/relay/server/listener/ws"
|
||||||
|
syncgrpc "github.com/netbirdio/netbird/shared/management/grpc"
|
||||||
sharedMetrics "github.com/netbirdio/netbird/shared/metrics"
|
sharedMetrics "github.com/netbirdio/netbird/shared/metrics"
|
||||||
"github.com/netbirdio/netbird/shared/relay/auth"
|
"github.com/netbirdio/netbird/shared/relay/auth"
|
||||||
"github.com/netbirdio/netbird/shared/signal/proto"
|
"github.com/netbirdio/netbird/shared/signal/proto"
|
||||||
@@ -505,6 +506,16 @@ func createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (m
|
|||||||
}
|
}
|
||||||
mgmtPort, _ := strconv.Atoi(portStr)
|
mgmtPort, _ := strconv.Atoi(portStr)
|
||||||
|
|
||||||
|
if err := syncgrpc.ValidateSyncMessageVersion(mgmtConfig.HighestSupportedSyncMessageVersion); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for accountId, version := range mgmtConfig.PerAccountHighestSupportedSyncMessageVersion {
|
||||||
|
if err := syncgrpc.ValidateSyncMessageVersion(&version); err != nil {
|
||||||
|
return nil, fmt.Errorf("unrecognized sync message version in perAccountSupportedSyncMessageVersions for account %s %w", accountId, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mgmtSrv := newServer(
|
mgmtSrv := newServer(
|
||||||
&mgmtServer.Config{
|
&mgmtServer.Config{
|
||||||
NbConfig: mgmtConfig,
|
NbConfig: mgmtConfig,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ type NameServerGroup struct {
|
|||||||
ID string `gorm:"primaryKey"`
|
ID string `gorm:"primaryKey"`
|
||||||
// AccountID is a reference to Account that this object belongs
|
// AccountID is a reference to Account that this object belongs
|
||||||
AccountID string `gorm:"index"`
|
AccountID string `gorm:"index"`
|
||||||
|
PublicID string `json:"-"`
|
||||||
// Name group name
|
// Name group name
|
||||||
Name string
|
Name string
|
||||||
// Description group description
|
// Description group description
|
||||||
|
|||||||
+1
-1
@@ -308,7 +308,7 @@ func (s *Storage) OpenStorage(logger *slog.Logger) (storage.Storage, error) {
|
|||||||
if file == "" {
|
if file == "" {
|
||||||
return nil, fmt.Errorf("sqlite3 storage requires 'file' config")
|
return nil, fmt.Errorf("sqlite3 storage requires 'file' config")
|
||||||
}
|
}
|
||||||
return (&sql.SQLite3{File: file}).Open(logger)
|
return newSQLite3(file).Open(logger)
|
||||||
case "postgres":
|
case "postgres":
|
||||||
dsn, _ := s.Config["dsn"].(string)
|
dsn, _ := s.Config["dsn"].(string)
|
||||||
if dsn == "" {
|
if dsn == "" {
|
||||||
|
|||||||
+1
-2
@@ -20,7 +20,6 @@ import (
|
|||||||
"github.com/dexidp/dex/server"
|
"github.com/dexidp/dex/server"
|
||||||
"github.com/dexidp/dex/server/signer"
|
"github.com/dexidp/dex/server/signer"
|
||||||
"github.com/dexidp/dex/storage"
|
"github.com/dexidp/dex/storage"
|
||||||
"github.com/dexidp/dex/storage/sql"
|
|
||||||
"github.com/go-jose/go-jose/v4"
|
"github.com/go-jose/go-jose/v4"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
@@ -79,7 +78,7 @@ func NewProvider(ctx context.Context, config *Config) (*Provider, error) {
|
|||||||
|
|
||||||
// Initialize SQLite storage
|
// Initialize SQLite storage
|
||||||
dbPath := filepath.Join(config.DataDir, "oidc.db")
|
dbPath := filepath.Join(config.DataDir, "oidc.db")
|
||||||
sqliteConfig := &sql.SQLite3{File: dbPath}
|
sqliteConfig := newSQLite3(dbPath)
|
||||||
stor, err := sqliteConfig.Open(logger)
|
stor, err := sqliteConfig.Open(logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open storage: %w", err)
|
return nil, fmt.Errorf("failed to open storage: %w", err)
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build cgo
|
||||||
|
|
||||||
|
package dex
|
||||||
|
|
||||||
|
import (
|
||||||
|
sql "github.com/dexidp/dex/storage/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newSQLite3 builds the dex SQLite3 config. CGO builds use the upstream
|
||||||
|
// struct that takes a File path. Non-CGO builds get an empty stub whose
|
||||||
|
// Open() returns the dex "SQLite not available" error — correct behaviour
|
||||||
|
// for binaries that can't link sqlite3 (e.g. cross-compiled ARM targets).
|
||||||
|
func newSQLite3(file string) *sql.SQLite3 {
|
||||||
|
return &sql.SQLite3{File: file}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build !cgo
|
||||||
|
|
||||||
|
package dex
|
||||||
|
|
||||||
|
import (
|
||||||
|
sql "github.com/dexidp/dex/storage/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newSQLite3 for non-CGO builds. The dex SQLite3 stub has no fields and its
|
||||||
|
// Open() returns an error documenting the missing CGO support — correct
|
||||||
|
// behaviour for cross-compiled artefacts that never actually run the
|
||||||
|
// embedded IdP. The `file` argument is ignored.
|
||||||
|
func newSQLite3(_ string) *sql.SQLite3 {
|
||||||
|
return &sql.SQLite3{}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/internals/server"
|
"github.com/netbirdio/netbird/management/internals/server"
|
||||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
nbdomain "github.com/netbirdio/netbird/shared/management/domain"
|
nbdomain "github.com/netbirdio/netbird/shared/management/domain"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/grpc"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
"github.com/netbirdio/netbird/util/crypt"
|
"github.com/netbirdio/netbird/util/crypt"
|
||||||
)
|
)
|
||||||
@@ -153,8 +154,20 @@ func LoadMgmtConfig(ctx context.Context, mgmtConfigPath string) (*nbconfig.Confi
|
|||||||
|
|
||||||
ApplyCommandLineOverrides(loadedConfig)
|
ApplyCommandLineOverrides(loadedConfig)
|
||||||
|
|
||||||
|
err := grpc.ValidateSyncMessageVersion(loadedConfig.HighestSupportedSyncMessageVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for account, version := range loadedConfig.PerAccountHighestSupportedSyncMessageVersion {
|
||||||
|
err := grpc.ValidateSyncMessageVersion(&version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unrecognized sync message version for account %s, %w", account, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Apply EmbeddedIdP config to HttpConfig if embedded IdP is enabled
|
// Apply EmbeddedIdP config to HttpConfig if embedded IdP is enabled
|
||||||
err := ApplyEmbeddedIdPConfig(ctx, loadedConfig)
|
err = ApplyEmbeddedIdPConfig(ctx, loadedConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/shared/management/grpc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -20,34 +23,49 @@ const (
|
|||||||
"AuthAudience": "https://stageapp/",
|
"AuthAudience": "https://stageapp/",
|
||||||
"AuthIssuer": "https://something.eu.auth0.com/",
|
"AuthIssuer": "https://something.eu.auth0.com/",
|
||||||
"OIDCConfigEndpoint": "https://something.eu.auth0.com/.well-known/openid-configuration"
|
"OIDCConfigEndpoint": "https://something.eu.auth0.com/.well-known/openid-configuration"
|
||||||
|
},
|
||||||
|
"HighestSupportedSyncMessageVersion": 1,
|
||||||
|
"PerAccountHighestSupportedSyncMessageVersion": {
|
||||||
|
"1": 0,
|
||||||
|
"2": 1
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_loadMgmtConfig(t *testing.T) {
|
func Test_LoadMgmtConfig(t *testing.T) {
|
||||||
tmpFile, err := createConfig()
|
tmpFile, err := createConfig(exampleConfig)
|
||||||
if err != nil {
|
assert.NoError(t, err)
|
||||||
t.Fatalf("failed to create config: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := LoadMgmtConfig(context.Background(), tmpFile)
|
cfg, err := LoadMgmtConfig(context.Background(), tmpFile)
|
||||||
if err != nil {
|
assert.NoError(t, err)
|
||||||
t.Fatalf("failed to load management config: %s", err)
|
assert.NotEmpty(t, cfg.Relay)
|
||||||
}
|
assert.NotEmpty(t, cfg.Relay.Addresses)
|
||||||
if cfg.Relay == nil {
|
assert.Equal(t, int(grpc.ComponentNetworkMap), *cfg.HighestSupportedSyncMessageVersion)
|
||||||
t.Fatalf("config is nil")
|
assert.Equal(t, map[string]int{"1": int(grpc.Base), "2": int(grpc.ComponentNetworkMap)}, cfg.PerAccountHighestSupportedSyncMessageVersion)
|
||||||
}
|
|
||||||
if len(cfg.Relay.Addresses) == 0 {
|
|
||||||
t.Fatalf("relay address is empty")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createConfig() (string, error) {
|
func Test_LoadMgmtConfig_Empty(t *testing.T) {
|
||||||
|
tmpFile, err := createConfig(`{
|
||||||
|
"HttpConfig": {
|
||||||
|
"AuthAudience": "https://stageapp/",
|
||||||
|
"AuthIssuer": "https://something.eu.auth0.com/",
|
||||||
|
"OIDCConfigEndpoint": "https://something.eu.auth0.com/.well-known/openid-configuration"
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
cfg, err := LoadMgmtConfig(context.Background(), tmpFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, cfg.HighestSupportedSyncMessageVersion)
|
||||||
|
assert.Nil(t, cfg.PerAccountHighestSupportedSyncMessageVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createConfig(config string) (string, error) {
|
||||||
tmpfile, err := os.CreateTemp("", "config.json")
|
tmpfile, err := os.CreateTemp("", "config.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
_, err = tmpfile.Write([]byte(exampleConfig))
|
_, err = tmpfile.Write([]byte(config))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server/store"
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
sharedgrpc "github.com/netbirdio/netbird/shared/management/grpc"
|
||||||
"github.com/netbirdio/netbird/shared/management/proto"
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
"github.com/netbirdio/netbird/shared/management/status"
|
"github.com/netbirdio/netbird/shared/management/status"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
@@ -56,6 +57,10 @@ type Controller struct {
|
|||||||
proxyController port_forwarding.Controller
|
proxyController port_forwarding.Controller
|
||||||
|
|
||||||
integratedPeerValidator integrated_validator.IntegratedValidator
|
integratedPeerValidator integrated_validator.IntegratedValidator
|
||||||
|
|
||||||
|
serverSupportedSyncMessageVersion sharedgrpc.SyncMessageVersion
|
||||||
|
|
||||||
|
perAccountServerSupportedSyncMessageVersions map[string]sharedgrpc.SyncMessageVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
type bufferUpdate struct {
|
type bufferUpdate struct {
|
||||||
@@ -90,8 +95,10 @@ func NewController(ctx context.Context, store store.Store, metrics telemetry.App
|
|||||||
dnsDomain: dnsDomain,
|
dnsDomain: dnsDomain,
|
||||||
config: config,
|
config: config,
|
||||||
|
|
||||||
proxyController: proxyController,
|
proxyController: proxyController,
|
||||||
EphemeralPeersManager: ephemeralPeersManager,
|
EphemeralPeersManager: ephemeralPeersManager,
|
||||||
|
serverSupportedSyncMessageVersion: sharedgrpc.SyncMessageVersionFromConfig(config.HighestSupportedSyncMessageVersion),
|
||||||
|
perAccountServerSupportedSyncMessageVersions: sharedgrpc.SyncMessageVersionsFromMap(config.PerAccountHighestSupportedSyncMessageVersion),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,18 +229,53 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
|
|||||||
c.metrics.CountCalcPostureChecksDuration(time.Since(start))
|
c.metrics.CountCalcPostureChecksDuration(time.Since(start))
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
|
|
||||||
remotePeerNetworkMap := account.GetPeerNetworkMapFromComponents(ctx, p.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
peerGroups := account.GetPeerGroups(p.ID)
|
||||||
|
proxyNetworkMap := proxyNetworkMaps[p.ID]
|
||||||
|
var update *proto.SyncResponse
|
||||||
|
|
||||||
|
commonSyncMessageVersion := sharedgrpc.HighestCommonSyncMessageVersion(
|
||||||
|
c.perAccountOrGlobalSupportedSyncMessageVersions(accountID),
|
||||||
|
sharedgrpc.SyncMessageVersionFromConfig(&peer.Meta.SyncMessageVersion))
|
||||||
|
|
||||||
|
log.WithContext(ctx).
|
||||||
|
WithFields(log.Fields{
|
||||||
|
"sync_message_version": commonSyncMessageVersion,
|
||||||
|
"server_sync_message_version": c.perAccountOrGlobalSupportedSyncMessageVersions(peer.AccountID),
|
||||||
|
"peer_sync_message_version": sharedgrpc.SyncMessageVersionFromConfig(&peer.Meta.SyncMessageVersion),
|
||||||
|
}).Debug("common highest sync message version")
|
||||||
|
|
||||||
|
if commonSyncMessageVersion == sharedgrpc.ComponentNetworkMap {
|
||||||
|
components := account.GetPeerNetworkMapComponents(
|
||||||
|
ctx, p.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, groupIDToUserIDs)
|
||||||
|
|
||||||
|
c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start))
|
||||||
|
|
||||||
|
start = time.Now()
|
||||||
|
// proxyNetworkMap rides the envelope as a ProxyPatch sidecar;
|
||||||
|
// the client merges it into Calculate()'s output the same
|
||||||
|
// way the legacy server did via NetworkMap.Merge.
|
||||||
|
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||||
|
c.metrics.CountToComponentSyncResponseDuration(time.Since(start))
|
||||||
|
|
||||||
|
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
||||||
|
Update: update,
|
||||||
|
MessageType: network_map.MessageTypeNetworkMap,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nmap := account.GetPeerNetworkMapFromComponents(
|
||||||
|
ctx, p.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
||||||
|
|
||||||
c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start))
|
c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start))
|
||||||
|
|
||||||
proxyNetworkMap, ok := proxyNetworkMaps[p.ID]
|
if proxyNetworkMap != nil {
|
||||||
if ok {
|
nmap.Merge(proxyNetworkMap)
|
||||||
remotePeerNetworkMap.Merge(proxyNetworkMap)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
peerGroups := account.GetPeerGroups(p.ID)
|
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
update := grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||||
c.metrics.CountToSyncResponseDuration(time.Since(start))
|
c.metrics.CountToSyncResponseDuration(time.Since(start))
|
||||||
|
|
||||||
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
||||||
@@ -251,6 +293,13 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Controller) perAccountOrGlobalSupportedSyncMessageVersions(accountId string) sharedgrpc.SyncMessageVersion {
|
||||||
|
if perAccount, ok := c.perAccountServerSupportedSyncMessageVersions[accountId]; ok {
|
||||||
|
return perAccount
|
||||||
|
}
|
||||||
|
return c.serverSupportedSyncMessageVersion
|
||||||
|
}
|
||||||
|
|
||||||
// UpdatePeers updates all peers that belong to an account.
|
// UpdatePeers updates all peers that belong to an account.
|
||||||
// Should be called when changes have to be synced to peers.
|
// Should be called when changes have to be synced to peers.
|
||||||
func (c *Controller) UpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error {
|
func (c *Controller) UpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error {
|
||||||
@@ -352,18 +401,53 @@ func (c *Controller) sendUpdateForAffectedPeers(ctx context.Context, accountID s
|
|||||||
c.metrics.CountCalcPostureChecksDuration(time.Since(start))
|
c.metrics.CountCalcPostureChecksDuration(time.Since(start))
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
|
|
||||||
remotePeerNetworkMap := account.GetPeerNetworkMapFromComponents(ctx, p.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
peerGroups := account.GetPeerGroups(p.ID)
|
||||||
|
proxyNetworkMap := proxyNetworkMaps[p.ID]
|
||||||
|
var update *proto.SyncResponse
|
||||||
|
|
||||||
|
commonSyncMessageVersion := sharedgrpc.HighestCommonSyncMessageVersion(
|
||||||
|
c.perAccountOrGlobalSupportedSyncMessageVersions(accountID),
|
||||||
|
sharedgrpc.SyncMessageVersionFromConfig(&peer.Meta.SyncMessageVersion))
|
||||||
|
|
||||||
|
log.WithContext(ctx).
|
||||||
|
WithFields(log.Fields{
|
||||||
|
"sync_message_version": commonSyncMessageVersion,
|
||||||
|
"server_sync_message_version": c.perAccountOrGlobalSupportedSyncMessageVersions(peer.AccountID),
|
||||||
|
"peer_sync_message_version": sharedgrpc.SyncMessageVersionFromConfig(&peer.Meta.SyncMessageVersion),
|
||||||
|
}).Debug("common highest sync message version")
|
||||||
|
|
||||||
|
if commonSyncMessageVersion == sharedgrpc.ComponentNetworkMap {
|
||||||
|
components := account.GetPeerNetworkMapComponents(
|
||||||
|
ctx, p.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, groupIDToUserIDs)
|
||||||
|
|
||||||
|
c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start))
|
||||||
|
|
||||||
|
start = time.Now()
|
||||||
|
// proxyNetworkMap rides the envelope as a ProxyPatch sidecar;
|
||||||
|
// the client merges it into Calculate()'s output the same
|
||||||
|
// way the legacy server did via NetworkMap.Merge.
|
||||||
|
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||||
|
c.metrics.CountToComponentSyncResponseDuration(time.Since(start))
|
||||||
|
|
||||||
|
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
||||||
|
Update: update,
|
||||||
|
MessageType: network_map.MessageTypeNetworkMap,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nmap := account.GetPeerNetworkMapFromComponents(
|
||||||
|
ctx, p.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
||||||
|
|
||||||
c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start))
|
c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start))
|
||||||
|
|
||||||
proxyNetworkMap, ok := proxyNetworkMaps[p.ID]
|
if proxyNetworkMap != nil {
|
||||||
if ok {
|
nmap.Merge(proxyNetworkMap)
|
||||||
remotePeerNetworkMap.Merge(proxyNetworkMap)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
peerGroups := account.GetPeerGroups(p.ID)
|
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
update := grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
|
||||||
c.metrics.CountToSyncResponseDuration(time.Since(start))
|
c.metrics.CountToSyncResponseDuration(time.Since(start))
|
||||||
|
|
||||||
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
|
||||||
@@ -451,13 +535,7 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
remotePeerNetworkMap := account.GetPeerNetworkMapFromComponents(ctx, peerId, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
proxyNetworkMap := proxyNetworkMaps[peer.ID]
|
||||||
|
|
||||||
proxyNetworkMap, ok := proxyNetworkMaps[peer.ID]
|
|
||||||
if ok {
|
|
||||||
remotePeerNetworkMap.Merge(proxyNetworkMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
extraSettings, err := c.settingsManager.GetExtraSettings(ctx, peer.AccountID)
|
extraSettings, err := c.settingsManager.GetExtraSettings(ctx, peer.AccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get extra settings: %v", err)
|
return fmt.Errorf("failed to get extra settings: %v", err)
|
||||||
@@ -466,7 +544,45 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe
|
|||||||
peerGroups := account.GetPeerGroups(peerId)
|
peerGroups := account.GetPeerGroups(peerId)
|
||||||
dnsFwdPort := computeForwarderPort(maps.Values(account.Peers), network_map.DnsForwarderPortMinVersion)
|
dnsFwdPort := computeForwarderPort(maps.Values(account.Peers), network_map.DnsForwarderPortMinVersion)
|
||||||
|
|
||||||
update := grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, peer, nil, nil, remotePeerNetworkMap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSettings, maps.Keys(peerGroups), dnsFwdPort)
|
var update *proto.SyncResponse
|
||||||
|
|
||||||
|
commonSyncMessageVersion := sharedgrpc.HighestCommonSyncMessageVersion(
|
||||||
|
c.perAccountOrGlobalSupportedSyncMessageVersions(accountId),
|
||||||
|
sharedgrpc.SyncMessageVersionFromConfig(&peer.Meta.SyncMessageVersion))
|
||||||
|
|
||||||
|
log.WithContext(ctx).
|
||||||
|
WithFields(log.Fields{
|
||||||
|
"sync_message_version": commonSyncMessageVersion,
|
||||||
|
"server_sync_message_version": c.perAccountOrGlobalSupportedSyncMessageVersions(peer.AccountID),
|
||||||
|
"peer_sync_message_version": sharedgrpc.SyncMessageVersionFromConfig(&peer.Meta.SyncMessageVersion),
|
||||||
|
}).Debug("common highest sync message version")
|
||||||
|
|
||||||
|
if commonSyncMessageVersion == sharedgrpc.ComponentNetworkMap {
|
||||||
|
components := account.GetPeerNetworkMapComponents(
|
||||||
|
ctx, peer.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, groupIDToUserIDs)
|
||||||
|
|
||||||
|
// proxyNetworkMap rides the envelope as a ProxyPatch sidecar;
|
||||||
|
// the client merges it into Calculate()'s output the same
|
||||||
|
// way the legacy server did via NetworkMap.Merge.
|
||||||
|
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, peer, nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSettings, maps.Keys(peerGroups), dnsFwdPort)
|
||||||
|
|
||||||
|
c.peersUpdateManager.SendUpdate(ctx, peer.ID, &network_map.UpdateMessage{
|
||||||
|
Update: update,
|
||||||
|
MessageType: network_map.MessageTypeNetworkMap,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nmap := account.GetPeerNetworkMapFromComponents(
|
||||||
|
ctx, peer.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
|
||||||
|
|
||||||
|
if proxyNetworkMap != nil {
|
||||||
|
nmap.Merge(proxyNetworkMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, peer, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSettings, maps.Keys(peerGroups), dnsFwdPort)
|
||||||
|
|
||||||
c.peersUpdateManager.SendUpdate(ctx, peer.ID, &network_map.UpdateMessage{
|
c.peersUpdateManager.SendUpdate(ctx, peer.ID, &network_map.UpdateMessage{
|
||||||
Update: update,
|
Update: update,
|
||||||
MessageType: network_map.MessageTypeNetworkMap,
|
MessageType: network_map.MessageTypeNetworkMap,
|
||||||
@@ -513,6 +629,65 @@ func (c *Controller) BufferUpdateAccountPeers(ctx context.Context, accountID str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetValidatedPeerWithComponents is the components-format counterpart of
|
||||||
|
// GetValidatedPeerWithMap. It returns raw NetworkMapComponents for capable
|
||||||
|
// peers along with the proxy NetworkMap fragment (BYOP / port-forwarding
|
||||||
|
// data the legacy server folds in via NetworkMap.Merge). The gRPC layer
|
||||||
|
// encodes both into the wire envelope. Callers must gate on capability
|
||||||
|
// themselves before dispatching here — this method does NOT branch on it.
|
||||||
|
func (c *Controller) GetValidatedPeerWithComponents(ctx context.Context, isRequiresApproval bool, accountID string, peer *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMapComponents, *types.NetworkMap, []*posture.Checks, int64, error) {
|
||||||
|
if isRequiresApproval {
|
||||||
|
network, err := c.repo.GetAccountNetwork(ctx, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, 0, err
|
||||||
|
}
|
||||||
|
return peer, &types.NetworkMapComponents{Network: network.Copy()}, nil, nil, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.injectAllProxyPolicies(ctx, account)
|
||||||
|
|
||||||
|
approvedPeersMap, err := c.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
postureChecks, err := c.getPeerPostureChecks(account, peer.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accountZones, err := c.repo.GetAccountZones(ctx, account.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the proxy network map fragment for this peer alongside the
|
||||||
|
// components — same single-account-load path the streaming controller
|
||||||
|
// uses, so initial-sync delivers BYOP/forwarding patches synchronously
|
||||||
|
// instead of waiting for the next streaming push.
|
||||||
|
proxyNetworkMaps, err := c.proxyController.GetProxyNetworkMaps(ctx, account.Id, peer.ID, account.Peers)
|
||||||
|
if err != nil {
|
||||||
|
log.WithContext(ctx).Errorf("failed to get proxy network maps: %v", err)
|
||||||
|
return nil, nil, nil, nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsDomain := c.GetDNSDomain(account.Settings)
|
||||||
|
peersCustomZone := account.GetPeersCustomZone(ctx, dnsDomain)
|
||||||
|
|
||||||
|
resourcePolicies := account.GetResourcePoliciesMap()
|
||||||
|
routers := account.GetResourceRoutersMap()
|
||||||
|
groupIDToUserIDs := account.GetActiveGroupUsers()
|
||||||
|
components := account.GetPeerNetworkMapComponents(ctx, peer.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, groupIDToUserIDs)
|
||||||
|
dnsFwdPort := computeForwarderPort(maps.Values(account.Peers), network_map.DnsForwarderPortMinVersion)
|
||||||
|
|
||||||
|
return peer, components, proxyNetworkMaps[peer.ID], postureChecks, dnsFwdPort, nil
|
||||||
|
}
|
||||||
|
|
||||||
// BufferUpdateAffectedPeers accumulates peer IDs and flushes them after the buffer interval.
|
// BufferUpdateAffectedPeers accumulates peer IDs and flushes them after the buffer interval.
|
||||||
func (c *Controller) BufferUpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string, reason types.UpdateReason) error {
|
func (c *Controller) BufferUpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string, reason types.UpdateReason) error {
|
||||||
if len(peerIDs) == 0 {
|
if len(peerIDs) == 0 {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type Controller interface {
|
|||||||
UpdateAccountPeer(ctx context.Context, accountId string, peerId string) error
|
UpdateAccountPeer(ctx context.Context, accountId string, peerId string) error
|
||||||
BufferUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error
|
BufferUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error
|
||||||
GetValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID string, peerID string) (*types.NetworkMap, []*posture.Checks, int64, error)
|
GetValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID string, peerID string) (*types.NetworkMap, []*posture.Checks, int64, error)
|
||||||
|
GetValidatedPeerWithComponents(ctx context.Context, isRequiresApproval bool, accountID string, p *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMapComponents, *types.NetworkMap, []*posture.Checks, int64, error)
|
||||||
GetDNSDomain(settings *types.Settings) string
|
GetDNSDomain(settings *types.Settings) string
|
||||||
StartWarmup(context.Context)
|
StartWarmup(context.Context)
|
||||||
GetNetworkMap(ctx context.Context, peerID string) (*types.NetworkMap, error)
|
GetNetworkMap(ctx context.Context, peerID string) (*types.NetworkMap, error)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: management/internals/controllers/network_map/interface.go
|
// Source: ./interface.go
|
||||||
//
|
//
|
||||||
// Generated by this command:
|
// Generated by this command:
|
||||||
//
|
//
|
||||||
// mockgen -package network_map -destination=management/internals/controllers/network_map/interface_mock.go -source=management/internals/controllers/network_map/interface.go -build_flags=-mod=mod
|
// mockgen -package network_map -destination=interface_mock.go -source=./interface.go -build_flags=-mod=mod
|
||||||
//
|
//
|
||||||
|
|
||||||
// Package network_map is a generated GoMock package.
|
// Package network_map is a generated GoMock package.
|
||||||
@@ -126,8 +126,27 @@ func (mr *MockControllerMockRecorder) GetNetworkMap(ctx, peerID any) *gomock.Cal
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkMap", reflect.TypeOf((*MockController)(nil).GetNetworkMap), ctx, peerID)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkMap", reflect.TypeOf((*MockController)(nil).GetNetworkMap), ctx, peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetValidatedPeerWithComponents mocks base method.
|
||||||
|
func (m *MockController) GetValidatedPeerWithComponents(ctx context.Context, isRequiresApproval bool, accountID string, p *peer.Peer) (*peer.Peer, *types.NetworkMapComponents, *types.NetworkMap, []*posture.Checks, int64, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetValidatedPeerWithComponents", ctx, isRequiresApproval, accountID, p)
|
||||||
|
ret0, _ := ret[0].(*peer.Peer)
|
||||||
|
ret1, _ := ret[1].(*types.NetworkMapComponents)
|
||||||
|
ret2, _ := ret[2].(*types.NetworkMap)
|
||||||
|
ret3, _ := ret[3].([]*posture.Checks)
|
||||||
|
ret4, _ := ret[4].(int64)
|
||||||
|
ret5, _ := ret[5].(error)
|
||||||
|
return ret0, ret1, ret2, ret3, ret4, ret5
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValidatedPeerWithComponents indicates an expected call of GetValidatedPeerWithComponents.
|
||||||
|
func (mr *MockControllerMockRecorder) GetValidatedPeerWithComponents(ctx, isRequiresApproval, accountID, p any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatedPeerWithComponents", reflect.TypeOf((*MockController)(nil).GetValidatedPeerWithComponents), ctx, isRequiresApproval, accountID, p)
|
||||||
|
}
|
||||||
|
|
||||||
// GetValidatedPeerWithMap mocks base method.
|
// GetValidatedPeerWithMap mocks base method.
|
||||||
func (m *MockController) GetValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID string, peerID string) (*types.NetworkMap, []*posture.Checks, int64, error) {
|
func (m *MockController) GetValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID, peerID string) (*types.NetworkMap, []*posture.Checks, int64, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "GetValidatedPeerWithMap", ctx, isRequiresApproval, accountID, peerID)
|
ret := m.ctrl.Call(m, "GetValidatedPeerWithMap", ctx, isRequiresApproval, accountID, peerID)
|
||||||
ret0, _ := ret[0].(*types.NetworkMap)
|
ret0, _ := ret[0].(*types.NetworkMap)
|
||||||
@@ -171,7 +190,7 @@ func (mr *MockControllerMockRecorder) OnPeerDisconnected(ctx, accountID, peerID
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OnPeersAdded mocks base method.
|
// OnPeersAdded mocks base method.
|
||||||
func (m *MockController) OnPeersAdded(ctx context.Context, accountID string, peerIDs []string, affectedPeerIDs []string) error {
|
func (m *MockController) OnPeersAdded(ctx context.Context, accountID string, peerIDs, affectedPeerIDs []string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "OnPeersAdded", ctx, accountID, peerIDs, affectedPeerIDs)
|
ret := m.ctrl.Call(m, "OnPeersAdded", ctx, accountID, peerIDs, affectedPeerIDs)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
@@ -185,7 +204,7 @@ func (mr *MockControllerMockRecorder) OnPeersAdded(ctx, accountID, peerIDs, affe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OnPeersDeleted mocks base method.
|
// OnPeersDeleted mocks base method.
|
||||||
func (m *MockController) OnPeersDeleted(ctx context.Context, accountID string, peerIDs []string, affectedPeerIDs []string) error {
|
func (m *MockController) OnPeersDeleted(ctx context.Context, accountID string, peerIDs, affectedPeerIDs []string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "OnPeersDeleted", ctx, accountID, peerIDs, affectedPeerIDs)
|
ret := m.ctrl.Call(m, "OnPeersDeleted", ctx, accountID, peerIDs, affectedPeerIDs)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
@@ -199,7 +218,7 @@ func (mr *MockControllerMockRecorder) OnPeersDeleted(ctx, accountID, peerIDs, af
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OnPeersUpdated mocks base method.
|
// OnPeersUpdated mocks base method.
|
||||||
func (m *MockController) OnPeersUpdated(ctx context.Context, accountId string, peerIDs []string, affectedPeerIDs []string) error {
|
func (m *MockController) OnPeersUpdated(ctx context.Context, accountId string, peerIDs, affectedPeerIDs []string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "OnPeersUpdated", ctx, accountId, peerIDs, affectedPeerIDs)
|
ret := m.ctrl.Call(m, "OnPeersUpdated", ctx, accountId, peerIDs, affectedPeerIDs)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ type Config struct {
|
|||||||
// EmbeddedIdP contains configuration for the embedded Dex OIDC provider.
|
// EmbeddedIdP contains configuration for the embedded Dex OIDC provider.
|
||||||
// When set, Dex will be embedded in the management server and serve requests at /oauth2/
|
// When set, Dex will be embedded in the management server and serve requests at /oauth2/
|
||||||
EmbeddedIdP *idp.EmbeddedIdPConfig
|
EmbeddedIdP *idp.EmbeddedIdPConfig
|
||||||
|
|
||||||
|
HighestSupportedSyncMessageVersion *int
|
||||||
|
|
||||||
|
PerAccountHighestSupportedSyncMessageVersion map[string]int
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAuthAudiences returns the audience from the http config and device authorization flow config
|
// GetAuthAudiences returns the audience from the http config and device authorization flow config
|
||||||
|
|||||||
@@ -0,0 +1,769 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||||
|
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
nbroute "github.com/netbirdio/netbird/route"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wgKeyRawLen is the raw byte length of a WireGuard public key.
|
||||||
|
const wgKeyRawLen = 32
|
||||||
|
|
||||||
|
// ComponentsEnvelopeInput bundles the data the component-format encoder needs.
|
||||||
|
// The envelope is fully self-contained — every field needed by the client's
|
||||||
|
// local Calculate() comes from the components struct itself. The only
|
||||||
|
// externally-supplied data is the receiving peer's PeerConfig (which is
|
||||||
|
// computed alongside the components in the network_map controller and reused
|
||||||
|
// from the legacy proto path) and the dns_domain string.
|
||||||
|
type ComponentsEnvelopeInput struct {
|
||||||
|
Components *types.NetworkMapComponents
|
||||||
|
PeerConfig *proto.PeerConfig
|
||||||
|
DNSDomain string
|
||||||
|
DNSForwarderPort int64
|
||||||
|
// UserIDClaim is the OIDC claim name the client should embed in
|
||||||
|
// SshAuth.UserIDClaim when reconstructing the NetworkMap. Empty value
|
||||||
|
// is OK — client treats empty as "no SshAuth to build".
|
||||||
|
UserIDClaim string
|
||||||
|
// ProxyPatch carries pre-expanded NetworkMap fragments injected by
|
||||||
|
// external controllers (BYOP/port-forwarding). Nil when no proxy data
|
||||||
|
// is present; encoder skips the field in that case.
|
||||||
|
ProxyPatch *proto.ProxyPatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeNetworkMapEnvelope converts NetworkMapComponents into the component
|
||||||
|
// wire envelope. The encoder is intentionally non-deterministic: it iterates
|
||||||
|
// Go maps in their native (random) order. Indexes inside the envelope
|
||||||
|
// (peer_indexes, source_group_ids, agent_version_idx, router_peer_indexes)
|
||||||
|
// are self-consistent within a single encode, so the decoder reconstructs
|
||||||
|
// the same typed objects regardless of emit order. Tests that need to
|
||||||
|
// compare envelopes do so semantically via proto round-trip + canonicalize,
|
||||||
|
// not byte-equal.
|
||||||
|
//
|
||||||
|
// Callers must NOT concatenate or merge envelopes from different encodes —
|
||||||
|
// index spaces are local to a single envelope.
|
||||||
|
func EncodeNetworkMapEnvelope(in ComponentsEnvelopeInput) *proto.NetworkMapEnvelope {
|
||||||
|
c := in.Components
|
||||||
|
|
||||||
|
// Graceful degrade when components is nil — matches the legacy path's
|
||||||
|
// behaviour for missing/unvalidated peers (return a NetworkMap with only
|
||||||
|
// Network populated). The receiver gets an envelope it can decode
|
||||||
|
// without crashing; AccountSettings stays non-nil so client-side
|
||||||
|
// dereferences are safe.
|
||||||
|
if c.IsEmpty() {
|
||||||
|
// Match legacy missing-peer minimum: a NetworkMap with only Network
|
||||||
|
// populated. The receiver gets enough to bootstrap (Network
|
||||||
|
// identifier, dns_domain, account_settings) and the peer itself.
|
||||||
|
return &proto.NetworkMapEnvelope{
|
||||||
|
Payload: &proto.NetworkMapEnvelope_Full{
|
||||||
|
Full: &proto.NetworkMapComponentsFull{
|
||||||
|
PeerConfig: in.PeerConfig,
|
||||||
|
// components.Peers always contains the target peer
|
||||||
|
Peers: []*proto.PeerCompact{toPeerCompact(c.Peers[c.PeerID])},
|
||||||
|
DnsDomain: in.DNSDomain,
|
||||||
|
DnsForwarderPort: in.DNSForwarderPort,
|
||||||
|
UserIdClaim: in.UserIDClaim,
|
||||||
|
AccountSettings: &proto.AccountSettingsCompact{},
|
||||||
|
ProxyPatch: in.ProxyPatch,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1: build dedup tables. Every routing peer (in c.RouterPeers) and
|
||||||
|
// every regular peer (in c.Peers) must be indexed before any encoder
|
||||||
|
// looks up indexes via e.peerOrder — otherwise routes / routers_map for
|
||||||
|
// peers that exist only in c.RouterPeers would silently lose their
|
||||||
|
// peer_index reference.
|
||||||
|
enc := newComponentEncoder(c)
|
||||||
|
enc.indexAllPeers()
|
||||||
|
routerIdxs := enc.indexRouterPeers(c.RouterPeers)
|
||||||
|
|
||||||
|
// Phase 2: gather every policy that any consumer references (peer-pair
|
||||||
|
// policies + resource-only policies) so encodeResourcePoliciesMap can
|
||||||
|
// translate every *Policy pointer to a wire index.
|
||||||
|
allPolicies := unionPolicies(c.Policies, c.ResourcePoliciesMap)
|
||||||
|
policies := enc.encodePolicies(allPolicies)
|
||||||
|
|
||||||
|
// Phase 3: emit. Order of struct field expressions no longer matters:
|
||||||
|
// every encoder either reads from the dedup tables or works on
|
||||||
|
// independent input.
|
||||||
|
full := &proto.NetworkMapComponentsFull{
|
||||||
|
Serial: networkSerial(c.Network),
|
||||||
|
PeerConfig: in.PeerConfig,
|
||||||
|
Network: toAccountNetwork(c.Network),
|
||||||
|
AccountSettings: toAccountSettingsCompact(c.AccountSettings),
|
||||||
|
DnsForwarderPort: in.DNSForwarderPort,
|
||||||
|
UserIdClaim: in.UserIDClaim,
|
||||||
|
ProxyPatch: in.ProxyPatch,
|
||||||
|
DnsSettings: enc.encodeDNSSettings(c.DNSSettings),
|
||||||
|
DnsDomain: in.DNSDomain,
|
||||||
|
CustomZoneDomain: c.CustomZoneDomain,
|
||||||
|
AgentVersions: enc.agentVersions,
|
||||||
|
Peers: enc.peers,
|
||||||
|
RouterPeerIndexes: routerIdxs,
|
||||||
|
Policies: policies,
|
||||||
|
Groups: enc.encodeGroups(),
|
||||||
|
Routes: enc.encodeRoutes(c.Routes),
|
||||||
|
NameserverGroups: enc.encodeNameServerGroups(c.NameServerGroups),
|
||||||
|
AllDnsRecords: encodeSimpleRecords(c.AllDNSRecords),
|
||||||
|
AccountZones: encodeCustomZones(c.AccountZones),
|
||||||
|
NetworkResources: enc.encodeNetworkResources(c.NetworkResources),
|
||||||
|
RoutersMap: enc.encodeRoutersMap(c.RoutersMap),
|
||||||
|
ResourcePoliciesMap: enc.encodeResourcePoliciesMap(c.ResourcePoliciesMap),
|
||||||
|
GroupIdToUserIds: enc.encodeGroupIDToUserIDs(c.GroupIDToUserIDs),
|
||||||
|
AllowedUserIds: stringSetToSlice(c.AllowedUserIDs),
|
||||||
|
PostureFailedPeers: enc.encodePostureFailedPeers(c.PostureFailedPeers),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &proto.NetworkMapEnvelope{
|
||||||
|
Payload: &proto.NetworkMapEnvelope_Full{Full: full},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// networkSerial returns c.Network.CurrentSerial() with a nil guard. The
|
||||||
|
// production path always populates c.Network, but the encoder is exported
|
||||||
|
// and a hand-built components struct may omit it.
|
||||||
|
func networkSerial(n *types.Network) uint64 {
|
||||||
|
if n == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return n.CurrentSerial()
|
||||||
|
}
|
||||||
|
|
||||||
|
type componentEncoder struct {
|
||||||
|
components *types.NetworkMapComponents
|
||||||
|
|
||||||
|
peerOrder map[string]uint32
|
||||||
|
peers []*proto.PeerCompact
|
||||||
|
|
||||||
|
agentVersionOrder map[string]uint32
|
||||||
|
agentVersions []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newComponentEncoder(c *types.NetworkMapComponents) *componentEncoder {
|
||||||
|
return &componentEncoder{
|
||||||
|
components: c,
|
||||||
|
peerOrder: make(map[string]uint32, len(c.Peers)),
|
||||||
|
peers: make([]*proto.PeerCompact, 0, len(c.Peers)),
|
||||||
|
agentVersionOrder: make(map[string]uint32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) indexAllPeers() {
|
||||||
|
for _, p := range e.components.Peers {
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.appendPeer(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) appendPeer(p *nbpeer.Peer) uint32 {
|
||||||
|
if idx, ok := e.peerOrder[p.ID]; ok {
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
idx := uint32(len(e.peers))
|
||||||
|
e.peerOrder[p.ID] = idx
|
||||||
|
e.peers = append(e.peers, toPeerCompact(p))
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexRouterPeers ensures every router peer is in the peer dedup table
|
||||||
|
// (c.RouterPeers may contain peers not in c.Peers when validation rules drop
|
||||||
|
// them) and returns their wire indexes for the RouterPeerIndexes field. Must
|
||||||
|
// run before any encoder that resolves peer ids via e.peerOrder.
|
||||||
|
func (e *componentEncoder) indexRouterPeers(routers map[string]*nbpeer.Peer) []uint32 {
|
||||||
|
if len(routers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]uint32, 0, len(routers))
|
||||||
|
for _, p := range routers {
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, e.appendPeer(p))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeGroups() []*proto.GroupCompact {
|
||||||
|
if len(e.components.Groups) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]*proto.GroupCompact, 0, len(e.components.Groups))
|
||||||
|
for _, g := range e.components.Groups {
|
||||||
|
peerIdxs := make([]uint32, 0, len(g.Peers))
|
||||||
|
for _, peerID := range g.Peers {
|
||||||
|
if idx, ok := e.peerOrder[peerID]; ok {
|
||||||
|
peerIdxs = append(peerIdxs, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, &proto.GroupCompact{
|
||||||
|
Id: g.PublicID,
|
||||||
|
PeerIndexes: peerIdxs,
|
||||||
|
IsAll: g.IsGroupAll(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodePolicies flattens Policy{Rules} → []PolicyCompact. Returns the wire
|
||||||
|
// list and a map from policy pointer to the indexes of its emitted rules in
|
||||||
|
// that list — used by encodeResourcePoliciesMap to translate
|
||||||
|
// ResourcePoliciesMap[resourceID][]*Policy into wire-side indexes.
|
||||||
|
func (e *componentEncoder) encodePolicies(policies []*types.Policy) []*proto.PolicyCompact {
|
||||||
|
if len(policies) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]*proto.PolicyCompact, 0, len(policies))
|
||||||
|
|
||||||
|
for _, pol := range policies {
|
||||||
|
if !pol.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, r := range pol.Rules {
|
||||||
|
if r == nil || !r.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, e.encodePolicyRule(pol, r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodePolicyRule maps a single PolicyRule under pol to a PolicyCompact entry.
|
||||||
|
func (e *componentEncoder) encodePolicyRule(pol *types.Policy, r *types.PolicyRule) *proto.PolicyCompact {
|
||||||
|
return &proto.PolicyCompact{
|
||||||
|
Id: pol.PublicID,
|
||||||
|
Action: networkmap.GetProtoAction(string(r.Action)),
|
||||||
|
Protocol: networkmap.GetProtoProtocol(string(r.Protocol)),
|
||||||
|
Bidirectional: r.Bidirectional,
|
||||||
|
Ports: portsToUint32(r.Ports),
|
||||||
|
PortRanges: portRangesToProto(r.PortRanges),
|
||||||
|
SourceGroupIds: e.groupPublicXids(r.Sources),
|
||||||
|
DestinationGroupIds: e.groupPublicXids(r.Destinations),
|
||||||
|
AuthorizedUser: r.AuthorizedUser,
|
||||||
|
AuthorizedGroups: e.encodeAuthorizedGroups(r.AuthorizedGroups),
|
||||||
|
SourceResource: e.resourceToProto(r.SourceResource),
|
||||||
|
DestinationResource: e.resourceToProto(r.DestinationResource),
|
||||||
|
SourcePostureCheckIds: e.postureCheckSeqs(pol.SourcePostureChecks),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// groupPublicXids maps the xid group IDs in src to their public xids,
|
||||||
|
// dropping any group with invalid public xid.
|
||||||
|
func (e *componentEncoder) groupPublicXids(src []string) []string {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(src))
|
||||||
|
for _, gid := range src {
|
||||||
|
if id, ok := e.groupPublicXid(gid); ok {
|
||||||
|
out = append(out, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// unionPolicies merges c.Policies with every policy referenced by
|
||||||
|
// c.ResourcePoliciesMap, deduplicating by pointer identity. Resource-only
|
||||||
|
// policies (relevant to a NetworkResource but not to peer-pair traffic)
|
||||||
|
// only live in ResourcePoliciesMap; without this union step they'd be lost
|
||||||
|
// from the wire and the client's resource-policy lookup would come back
|
||||||
|
// empty.
|
||||||
|
func unionPolicies(policies []*types.Policy, resourcePolicies map[string][]*types.Policy) []*types.Policy {
|
||||||
|
// Fast path: non-router peers have no resource-only policies, so the
|
||||||
|
// "union" is identical to `policies`. Skip the dedup map allocation.
|
||||||
|
if len(resourcePolicies) == 0 {
|
||||||
|
return policies
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(policies))
|
||||||
|
out := make([]*types.Policy, 0, len(policies))
|
||||||
|
for _, p := range policies {
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[p.ID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[p.ID] = struct{}{}
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
for _, list := range resourcePolicies {
|
||||||
|
for _, p := range list {
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[p.ID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[p.ID] = struct{}{}
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeAuthorizedGroups translates rule.AuthorizedGroups (map keyed by
|
||||||
|
// group xid → local-user names) to the wire form (map keyed by group
|
||||||
|
// account_seq_id → UserNameList). Groups without a seq id are dropped —
|
||||||
|
// matches how source/destination group references handle the same case.
|
||||||
|
func (e *componentEncoder) encodeAuthorizedGroups(m map[string][]string) map[string]*proto.UserNameList {
|
||||||
|
if len(m) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[string]*proto.UserNameList, len(m))
|
||||||
|
for groupID, names := range m {
|
||||||
|
id, ok := e.groupPublicXid(groupID)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[id] = &proto.UserNameList{Names: names}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) groupPublicXid(groupID string) (string, bool) {
|
||||||
|
g, ok := e.components.Groups[groupID]
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return g.PublicID, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// resourceToProto translates types.Resource for the wire. For peer-typed
|
||||||
|
// resources the peer id is converted to a peer index into the envelope's
|
||||||
|
// peers array. For other resource types only the type string is shipped
|
||||||
|
// today (Calculate's resource-typed rule path consults SourceResource only
|
||||||
|
// for "peer" — other types fall through to group-based lookup).
|
||||||
|
func (e *componentEncoder) resourceToProto(r types.Resource) *proto.ResourceCompact {
|
||||||
|
if r.ID == "" && r.Type == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := &proto.ResourceCompact{Type: string(r.Type)}
|
||||||
|
if r.Type == types.ResourceTypePeer && r.ID != "" {
|
||||||
|
if idx, ok := e.peerOrder[r.ID]; ok {
|
||||||
|
out.PeerIndexSet = true
|
||||||
|
out.PeerIndex = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// postureCheckSeqs translates a slice of posture-check xids to their
|
||||||
|
// public xids. Unresolvable xids are silently dropped — matches how group/peer
|
||||||
|
// references handle the same case.
|
||||||
|
func (e *componentEncoder) postureCheckSeqs(xids []string) []string {
|
||||||
|
if len(xids) == 0 || len(e.components.PostureCheckXIDToPublicID) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(xids))
|
||||||
|
for _, xid := range xids {
|
||||||
|
if seq, ok := e.components.PostureCheckXIDToPublicID[xid]; ok {
|
||||||
|
out = append(out, seq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// networkSeq translates a Network xid to its public id using
|
||||||
|
// the NetworkMapComponents.NetworkXIDToPublicID lookup. Returns (0,false) when
|
||||||
|
// the xid isn't known — callers decide whether to skip the parent record.
|
||||||
|
func (e *componentEncoder) networkPublicId(xid string) (string, bool) {
|
||||||
|
if xid == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
id, ok := e.components.NetworkXIDToPublicID[xid]
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return id, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeDNSSettings(s *types.DNSSettings) *proto.DNSSettingsCompact {
|
||||||
|
if s == nil || len(s.DisabledManagementGroups) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := &proto.DNSSettingsCompact{
|
||||||
|
DisabledManagementGroupIds: make([]string, 0, len(s.DisabledManagementGroups)),
|
||||||
|
}
|
||||||
|
for _, gid := range s.DisabledManagementGroups {
|
||||||
|
if id, ok := e.groupPublicXid(gid); ok {
|
||||||
|
out.DisabledManagementGroupIds = append(out.DisabledManagementGroupIds, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeRoutes(routes []*nbroute.Route) []*proto.RouteRaw {
|
||||||
|
if len(routes) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*proto.RouteRaw, 0, len(routes))
|
||||||
|
for _, r := range routes {
|
||||||
|
if r == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rr := &proto.RouteRaw{
|
||||||
|
Id: r.PublicID,
|
||||||
|
NetId: string(r.NetID),
|
||||||
|
Description: r.Description,
|
||||||
|
KeepRoute: r.KeepRoute,
|
||||||
|
NetworkType: int32(r.NetworkType),
|
||||||
|
Masquerade: r.Masquerade,
|
||||||
|
Metric: int32(r.Metric),
|
||||||
|
Enabled: r.Enabled,
|
||||||
|
SkipAutoApply: r.SkipAutoApply,
|
||||||
|
Domains: r.Domains.ToPunycodeList(),
|
||||||
|
GroupIds: e.groupPublicXids(r.Groups),
|
||||||
|
AccessControlGroupIds: e.groupPublicXids(r.AccessControlGroups),
|
||||||
|
PeerGroupIds: e.groupPublicXids(r.PeerGroups),
|
||||||
|
}
|
||||||
|
if r.Network.IsValid() {
|
||||||
|
rr.NetworkCidr = r.Network.String()
|
||||||
|
}
|
||||||
|
if r.Peer != "" {
|
||||||
|
if idx, ok := e.peerOrder[r.Peer]; ok {
|
||||||
|
rr.PeerIndexSet = true
|
||||||
|
rr.PeerIndex = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, rr)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeNameServerGroups(nsgs []*nbdns.NameServerGroup) []*proto.NameServerGroupRaw {
|
||||||
|
if len(nsgs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*proto.NameServerGroupRaw, 0, len(nsgs))
|
||||||
|
for _, nsg := range nsgs {
|
||||||
|
if nsg == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry := &proto.NameServerGroupRaw{
|
||||||
|
Id: nsg.PublicID,
|
||||||
|
Nameservers: encodeNameServers(nsg.NameServers),
|
||||||
|
GroupIds: e.groupPublicXids(nsg.Groups),
|
||||||
|
Primary: nsg.Primary,
|
||||||
|
Domains: nsg.Domains,
|
||||||
|
Enabled: nsg.Enabled,
|
||||||
|
SearchDomainsEnabled: nsg.SearchDomainsEnabled,
|
||||||
|
}
|
||||||
|
out = append(out, entry)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeNameServers(servers []nbdns.NameServer) []*proto.NameServer {
|
||||||
|
if len(servers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*proto.NameServer, 0, len(servers))
|
||||||
|
for _, s := range servers {
|
||||||
|
out = append(out, &proto.NameServer{
|
||||||
|
IP: s.IP.String(),
|
||||||
|
NSType: int64(s.NSType),
|
||||||
|
Port: int64(s.Port),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeSimpleRecords(records []nbdns.SimpleRecord) []*proto.SimpleRecord {
|
||||||
|
if len(records) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*proto.SimpleRecord, 0, len(records))
|
||||||
|
for _, r := range records {
|
||||||
|
out = append(out, &proto.SimpleRecord{
|
||||||
|
Name: r.Name,
|
||||||
|
Type: int64(r.Type),
|
||||||
|
Class: r.Class,
|
||||||
|
TTL: int64(r.TTL),
|
||||||
|
RData: r.RData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeCustomZones(zones []nbdns.CustomZone) []*proto.CustomZone {
|
||||||
|
if len(zones) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*proto.CustomZone, 0, len(zones))
|
||||||
|
for _, z := range zones {
|
||||||
|
out = append(out, &proto.CustomZone{
|
||||||
|
Domain: z.Domain,
|
||||||
|
Records: encodeSimpleRecords(z.Records),
|
||||||
|
SearchDomainDisabled: z.SearchDomainDisabled,
|
||||||
|
NonAuthoritative: z.NonAuthoritative,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeNetworkResources(resources []*resourceTypes.NetworkResource) []*proto.NetworkResourceRaw {
|
||||||
|
if len(resources) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*proto.NetworkResourceRaw, 0, len(resources))
|
||||||
|
for _, r := range resources {
|
||||||
|
if r == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry := &proto.NetworkResourceRaw{
|
||||||
|
Id: r.PublicID,
|
||||||
|
Name: r.Name,
|
||||||
|
Description: r.Description,
|
||||||
|
Type: string(r.Type),
|
||||||
|
Address: r.Address,
|
||||||
|
DomainValue: r.Domain,
|
||||||
|
Enabled: r.Enabled,
|
||||||
|
}
|
||||||
|
if id, ok := e.networkPublicId(r.NetworkID); ok {
|
||||||
|
entry.NetworkSeq = id
|
||||||
|
}
|
||||||
|
if r.Prefix.IsValid() {
|
||||||
|
entry.PrefixCidr = r.Prefix.String()
|
||||||
|
}
|
||||||
|
out = append(out, entry)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeRoutersMap(routersMap map[string]map[string]*routerTypes.NetworkRouter) map[string]*proto.NetworkRouterList {
|
||||||
|
if len(routersMap) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[string]*proto.NetworkRouterList, len(routersMap))
|
||||||
|
for networkXID, routers := range routersMap {
|
||||||
|
if len(routers) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id, ok := e.networkPublicId(networkXID)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entries := make([]*proto.NetworkRouterEntry, 0, len(routers))
|
||||||
|
for peerID, r := range routers {
|
||||||
|
if r == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry := &proto.NetworkRouterEntry{
|
||||||
|
Id: r.PublicID,
|
||||||
|
PeerGroupIds: e.groupPublicXids(r.PeerGroups),
|
||||||
|
Masquerade: r.Masquerade,
|
||||||
|
Metric: int32(r.Metric),
|
||||||
|
Enabled: r.Enabled,
|
||||||
|
}
|
||||||
|
if idx, ok := e.peerOrder[peerID]; ok {
|
||||||
|
entry.PeerIndexSet = true
|
||||||
|
entry.PeerIndex = idx
|
||||||
|
}
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
out[id] = &proto.NetworkRouterList{Entries: entries}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeResourcePoliciesMap(rpm map[string][]*types.Policy) map[string]*proto.PolicyIds {
|
||||||
|
if len(rpm) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// resourceXIDToPublicID is local to one encode — built from components.NetworkResources
|
||||||
|
// (small slice). Network resources without seq id are dropped, matching how
|
||||||
|
// other components-without-seq are silently filtered.
|
||||||
|
resourceXIDToPublicID := make(map[string]string, len(e.components.NetworkResources))
|
||||||
|
for _, r := range e.components.NetworkResources {
|
||||||
|
if r != nil {
|
||||||
|
resourceXIDToPublicID[r.ID] = r.PublicID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out := make(map[string]*proto.PolicyIds, len(rpm))
|
||||||
|
for resourceXID, policies := range rpm {
|
||||||
|
resId, ok := resourceXIDToPublicID[resourceXID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ids := make([]string, 0, len(policies))
|
||||||
|
for _, pol := range policies {
|
||||||
|
ids = append(ids, pol.PublicID)
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[resId] = &proto.PolicyIds{Ids: ids}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodeGroupIDToUserIDs(m map[string][]string) map[string]*proto.UserIDList {
|
||||||
|
if len(m) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[string]*proto.UserIDList, len(m))
|
||||||
|
for groupID, userIDs := range m {
|
||||||
|
id, ok := e.groupPublicXid(groupID)
|
||||||
|
if !ok || len(userIDs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[id] = &proto.UserIDList{UserIds: userIDs}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringSetToSlice(s map[string]struct{}) []string {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(s))
|
||||||
|
for k := range s {
|
||||||
|
out = append(out, k)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *componentEncoder) encodePostureFailedPeers(m map[string]map[string]struct{}) map[string]*proto.PeerIndexSet {
|
||||||
|
if len(m) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[string]*proto.PeerIndexSet, len(m))
|
||||||
|
for checkXID, failedPeerIDs := range m {
|
||||||
|
id, ok := e.components.PostureCheckXIDToPublicID[checkXID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idxs := make([]uint32, 0, len(failedPeerIDs))
|
||||||
|
for peerID := range failedPeerIDs {
|
||||||
|
if idx, ok := e.peerOrder[peerID]; ok {
|
||||||
|
idxs = append(idxs, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(idxs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[id] = &proto.PeerIndexSet{PeerIndexes: idxs}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// toAccountSettingsCompact always returns a non-nil message — the client
|
||||||
|
// dereferences it unconditionally during Calculate(), so a nil here would
|
||||||
|
// crash the receiver. A missing types.AccountSettingsInfo on the server
|
||||||
|
// (which shouldn't happen in production but the encoder is exported)
|
||||||
|
// degrades to login_expiration_enabled = false, which makes
|
||||||
|
// LoginExpired() return false for every peer.
|
||||||
|
func toAccountSettingsCompact(s *types.AccountSettingsInfo) *proto.AccountSettingsCompact {
|
||||||
|
if s == nil {
|
||||||
|
return &proto.AccountSettingsCompact{}
|
||||||
|
}
|
||||||
|
return &proto.AccountSettingsCompact{
|
||||||
|
PeerLoginExpirationEnabled: s.PeerLoginExpirationEnabled,
|
||||||
|
PeerLoginExpirationNs: int64(s.PeerLoginExpiration),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toAccountNetwork(n *types.Network) *proto.AccountNetwork {
|
||||||
|
if n == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := &proto.AccountNetwork{
|
||||||
|
Identifier: n.Identifier,
|
||||||
|
NetCidr: n.Net.String(),
|
||||||
|
Dns: n.Dns,
|
||||||
|
Serial: n.CurrentSerial(),
|
||||||
|
}
|
||||||
|
if len(n.NetV6.IP) > 0 {
|
||||||
|
out.NetV6Cidr = n.NetV6.String()
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPeerCompact(p *nbpeer.Peer) *proto.PeerCompact {
|
||||||
|
pc := &proto.PeerCompact{
|
||||||
|
WgPubKey: decodeWgKey(p.Key),
|
||||||
|
SshPubKey: []byte(p.SSHKey),
|
||||||
|
DnsLabel: p.DNSLabel,
|
||||||
|
AgentVersion: p.Meta.WtVersion,
|
||||||
|
AddedWithSsoLogin: p.UserID != "",
|
||||||
|
LoginExpirationEnabled: p.LoginExpirationEnabled,
|
||||||
|
SshEnabled: p.SSHEnabled,
|
||||||
|
SupportsIpv6: p.SupportsIPv6(),
|
||||||
|
SupportsSourcePrefixes: p.SupportsSourcePrefixes(),
|
||||||
|
ServerSshAllowed: p.Meta.Flags.ServerSSHAllowed,
|
||||||
|
}
|
||||||
|
if p.LastLogin != nil {
|
||||||
|
pc.LastLoginUnixNano = p.LastLogin.UnixNano()
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case !p.IP.IsValid():
|
||||||
|
// leave Ip nil
|
||||||
|
case p.IP.Is4() || p.IP.Is4In6():
|
||||||
|
ip := p.IP.Unmap().As4()
|
||||||
|
pc.Ip = ip[:]
|
||||||
|
default:
|
||||||
|
ip := p.IP.As16()
|
||||||
|
pc.Ip = ip[:]
|
||||||
|
}
|
||||||
|
if p.IPv6.IsValid() {
|
||||||
|
ip := p.IPv6.As16()
|
||||||
|
pc.Ipv6 = ip[:]
|
||||||
|
}
|
||||||
|
return pc
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeWgKey returns the raw 32 bytes of a base64-encoded WireGuard public
|
||||||
|
// key, or nil for an empty / malformed key.
|
||||||
|
func decodeWgKey(s string) []byte {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]byte, wgKeyRawLen)
|
||||||
|
n, err := base64.StdEncoding.Decode(out, []byte(s))
|
||||||
|
if err != nil || n != wgKeyRawLen {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func portsToUint32(ports []string) []uint32 {
|
||||||
|
if len(ports) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]uint32, 0, len(ports))
|
||||||
|
for _, p := range ports {
|
||||||
|
v, err := strconv.ParseUint(p, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, uint32(v))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func portRangesToProto(ranges []types.RulePortRange) []*proto.PortInfo_Range {
|
||||||
|
if len(ranges) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*proto.PortInfo_Range, 0, len(ranges))
|
||||||
|
for _, r := range ranges {
|
||||||
|
out = append(out, &proto.PortInfo_Range{
|
||||||
|
Start: uint32(r.Start),
|
||||||
|
End: uint32(r.End),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,785 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"cmp"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
goproto "google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||||
|
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
nbroute "github.com/netbirdio/netbird/route"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testWgKeyA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq="
|
||||||
|
const testWgKeyB = "BBCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq="
|
||||||
|
const testWgKeyC = "CBCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq="
|
||||||
|
|
||||||
|
// canonicalize rewrites a NetworkMapComponentsFull in place into a canonical
|
||||||
|
// form: peers reordered by wg_pub_key, with the rest of the message rewritten
|
||||||
|
// to reference the new peer indexes. Groups, policies, and router indexes are
|
||||||
|
// also sorted. After canonicalize, two envelopes built from the same logical
|
||||||
|
// input compare byte-equal via proto.Equal.
|
||||||
|
//
|
||||||
|
// This lives on the test side — the encoder itself emits in map-iteration
|
||||||
|
// order. Test-side normalization is the contract for "two encodes are
|
||||||
|
// equivalent".
|
||||||
|
func canonicalize(full *proto.NetworkMapComponentsFull) {
|
||||||
|
if full == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type peerEntry struct {
|
||||||
|
peer *proto.PeerCompact
|
||||||
|
oldIdx uint32
|
||||||
|
}
|
||||||
|
entries := make([]peerEntry, len(full.Peers))
|
||||||
|
for i, p := range full.Peers {
|
||||||
|
entries[i] = peerEntry{peer: p, oldIdx: uint32(i)}
|
||||||
|
}
|
||||||
|
// DnsLabel is unique per peer; it tiebreaks on equal WgPubKey (e.g. both
|
||||||
|
// nil from malformed keys, or both empty for placeholders).
|
||||||
|
slices.SortFunc(entries, func(a, b peerEntry) int {
|
||||||
|
if c := bytes.Compare(a.peer.WgPubKey, b.peer.WgPubKey); c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return cmp.Compare(a.peer.DnsLabel, b.peer.DnsLabel)
|
||||||
|
})
|
||||||
|
|
||||||
|
remap := make(map[uint32]uint32, len(entries))
|
||||||
|
newPeers := make([]*proto.PeerCompact, len(entries))
|
||||||
|
for newIdx, e := range entries {
|
||||||
|
remap[e.oldIdx] = uint32(newIdx)
|
||||||
|
newPeers[newIdx] = e.peer
|
||||||
|
}
|
||||||
|
full.Peers = newPeers
|
||||||
|
|
||||||
|
full.RouterPeerIndexes = remapAndSort(full.RouterPeerIndexes, remap)
|
||||||
|
for _, g := range full.Groups {
|
||||||
|
g.PeerIndexes = remapAndSort(g.PeerIndexes, remap)
|
||||||
|
}
|
||||||
|
slices.SortFunc(full.Groups, func(a, b *proto.GroupCompact) int { return cmp.Compare(a.Id, b.Id) })
|
||||||
|
|
||||||
|
for _, r := range full.Routes {
|
||||||
|
if r.PeerIndexSet {
|
||||||
|
if newIdx, ok := remap[r.PeerIndex]; ok {
|
||||||
|
r.PeerIndex = newIdx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(r.GroupIds)
|
||||||
|
slices.Sort(r.AccessControlGroupIds)
|
||||||
|
slices.Sort(r.PeerGroupIds)
|
||||||
|
}
|
||||||
|
slices.SortFunc(full.Routes, func(a, b *proto.RouteRaw) int { return cmp.Compare(a.Id, b.Id) })
|
||||||
|
|
||||||
|
for _, list := range full.RoutersMap {
|
||||||
|
for _, entry := range list.Entries {
|
||||||
|
if entry.PeerIndexSet {
|
||||||
|
if newIdx, ok := remap[entry.PeerIndex]; ok {
|
||||||
|
entry.PeerIndex = newIdx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(entry.PeerGroupIds)
|
||||||
|
}
|
||||||
|
slices.SortFunc(list.Entries, func(a, b *proto.NetworkRouterEntry) int { return cmp.Compare(a.Id, b.Id) })
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, set := range full.PostureFailedPeers {
|
||||||
|
set.PeerIndexes = remapAndSort(set.PeerIndexes, remap)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range full.Policies {
|
||||||
|
slices.Sort(p.SourceGroupIds)
|
||||||
|
slices.Sort(p.DestinationGroupIds)
|
||||||
|
}
|
||||||
|
// Sort policies by (Id, source_group_ids, destination_group_ids) so that
|
||||||
|
// multiple PolicyCompact entries sharing the same Id (one per rule, when
|
||||||
|
// a Policy has multiple rules) still get a deterministic order. After
|
||||||
|
// sorting we remap indexes in ResourcePoliciesMap.
|
||||||
|
policyOldOrder := make(map[*proto.PolicyCompact]uint32, len(full.Policies))
|
||||||
|
for i, p := range full.Policies {
|
||||||
|
policyOldOrder[p] = uint32(i)
|
||||||
|
}
|
||||||
|
slices.SortFunc(full.Policies, func(a, b *proto.PolicyCompact) int {
|
||||||
|
if c := cmp.Compare(a.Id, b.Id); c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
if c := slices.Compare(a.SourceGroupIds, b.SourceGroupIds); c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return slices.Compare(a.DestinationGroupIds, b.DestinationGroupIds)
|
||||||
|
})
|
||||||
|
policyRemap := make(map[uint32]uint32, len(full.Policies))
|
||||||
|
for newIdx, p := range full.Policies {
|
||||||
|
policyRemap[policyOldOrder[p]] = uint32(newIdx)
|
||||||
|
}
|
||||||
|
for _, idxs := range full.ResourcePoliciesMap {
|
||||||
|
slices.Sort(idxs.Ids)
|
||||||
|
}
|
||||||
|
for _, list := range full.GroupIdToUserIds {
|
||||||
|
slices.Sort(list.UserIds)
|
||||||
|
}
|
||||||
|
slices.Sort(full.AllowedUserIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func remapAndSort(idxs []uint32, remap map[uint32]uint32) []uint32 {
|
||||||
|
out := make([]uint32, 0, len(idxs))
|
||||||
|
for _, i := range idxs {
|
||||||
|
if newIdx, ok := remap[i]; ok {
|
||||||
|
out = append(out, newIdx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.Sort(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// envelopesEquivalent decodes both envelopes, canonicalizes them, and reports
|
||||||
|
// whether they're proto.Equal. Use instead of byte-comparing marshaled output:
|
||||||
|
// the encoder is intentionally non-deterministic.
|
||||||
|
func envelopesEquivalent(a, b *proto.NetworkMapEnvelope) bool {
|
||||||
|
canonicalize(a.GetFull())
|
||||||
|
canonicalize(b.GetFull())
|
||||||
|
return goproto.Equal(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestComponents() *types.NetworkMapComponents {
|
||||||
|
peerA := &nbpeer.Peer{
|
||||||
|
ID: "peer-a",
|
||||||
|
Key: testWgKeyA,
|
||||||
|
IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
|
||||||
|
DNSLabel: "peera",
|
||||||
|
SSHKey: "ssh-a",
|
||||||
|
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now()},
|
||||||
|
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}
|
||||||
|
peerB := &nbpeer.Peer{
|
||||||
|
ID: "peer-b",
|
||||||
|
Key: testWgKeyB,
|
||||||
|
IP: netip.AddrFrom4([4]byte{100, 64, 0, 2}),
|
||||||
|
IPv6: netip.AddrFrom16([16]byte{0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}),
|
||||||
|
DNSLabel: "peerb",
|
||||||
|
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.25.0"},
|
||||||
|
}
|
||||||
|
peerC := &nbpeer.Peer{
|
||||||
|
ID: "peer-c",
|
||||||
|
Key: testWgKeyC,
|
||||||
|
IP: netip.AddrFrom4([4]byte{100, 64, 0, 3}),
|
||||||
|
DNSLabel: "peerc",
|
||||||
|
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.NetworkMapComponents{
|
||||||
|
PeerID: "peer-a",
|
||||||
|
Network: &types.Network{
|
||||||
|
Identifier: "net-test",
|
||||||
|
Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)},
|
||||||
|
Serial: 7,
|
||||||
|
},
|
||||||
|
AccountSettings: &types.AccountSettingsInfo{
|
||||||
|
PeerLoginExpirationEnabled: true,
|
||||||
|
PeerLoginExpiration: 2 * time.Hour,
|
||||||
|
},
|
||||||
|
Peers: map[string]*nbpeer.Peer{
|
||||||
|
"peer-a": peerA,
|
||||||
|
"peer-b": peerB,
|
||||||
|
"peer-c": peerC,
|
||||||
|
},
|
||||||
|
Groups: map[string]*types.Group{
|
||||||
|
"group-src": {ID: "group-src", PublicID: "1", Name: "Src", Peers: []string{"peer-a"}},
|
||||||
|
"group-dst": {ID: "group-dst", PublicID: "2", Name: "Dst", Peers: []string{"peer-b", "peer-c"}},
|
||||||
|
},
|
||||||
|
Policies: []*types.Policy{
|
||||||
|
{
|
||||||
|
ID: "pol-1",
|
||||||
|
PublicID: "10",
|
||||||
|
Enabled: true,
|
||||||
|
Rules: []*types.PolicyRule{{
|
||||||
|
ID: "rule-1", Enabled: true, Action: types.PolicyTrafficActionAccept,
|
||||||
|
Protocol: types.PolicyRuleProtocolTCP, Bidirectional: true,
|
||||||
|
Ports: []string{"22", "80"},
|
||||||
|
PortRanges: []types.RulePortRange{{Start: 8000, End: 8100}},
|
||||||
|
Sources: []string{"group-src"},
|
||||||
|
Destinations: []string{"group-dst"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RouterPeers: map[string]*nbpeer.Peer{"peer-c": peerC},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_Basic(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
env := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{
|
||||||
|
Components: c,
|
||||||
|
DNSDomain: "netbird.cloud",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NotNil(t, env)
|
||||||
|
full := env.GetFull()
|
||||||
|
require.NotNil(t, full, "envelope must contain Full payload")
|
||||||
|
|
||||||
|
assert.EqualValues(t, 7, full.Serial)
|
||||||
|
assert.Equal(t, "netbird.cloud", full.DnsDomain)
|
||||||
|
|
||||||
|
require.NotNil(t, full.Network)
|
||||||
|
assert.Equal(t, "net-test", full.Network.Identifier)
|
||||||
|
assert.Equal(t, "100.64.0.0/10", full.Network.NetCidr)
|
||||||
|
|
||||||
|
require.NotNil(t, full.AccountSettings)
|
||||||
|
assert.True(t, full.AccountSettings.PeerLoginExpirationEnabled)
|
||||||
|
assert.EqualValues(t, (2 * time.Hour).Nanoseconds(), full.AccountSettings.PeerLoginExpirationNs)
|
||||||
|
|
||||||
|
require.Len(t, full.Peers, 3)
|
||||||
|
byLabel := map[string]*proto.PeerCompact{}
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
assert.Len(t, p.WgPubKey, 32, "wg key must be raw 32 bytes")
|
||||||
|
assert.Len(t, p.Ip, 4, "ipv4 must be raw 4 bytes")
|
||||||
|
byLabel[p.DnsLabel] = p
|
||||||
|
}
|
||||||
|
assert.Len(t, byLabel["peerb"].Ipv6, 16, "peer-b has ipv6 → 16 bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_RepeatEncodesEquivalent(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
|
||||||
|
expected := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c})
|
||||||
|
|
||||||
|
// Hammer it 100 times — Go map iteration is randomized per call, so each
|
||||||
|
// run produces different wire bytes, but the canonicalized form must
|
||||||
|
// match.
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
got := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c})
|
||||||
|
require.True(t, envelopesEquivalent(expected, got),
|
||||||
|
"encode #%d must be semantically equivalent to first encode", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_ConcurrentEncodesEquivalent(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
|
||||||
|
expected := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c})
|
||||||
|
|
||||||
|
const goroutines = 50
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(goroutines)
|
||||||
|
results := make([]*proto.NetworkMapEnvelope, goroutines)
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
i := i
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
results[i] = EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
for i, got := range results {
|
||||||
|
require.NotNil(t, got, "goroutine %d returned nil", i)
|
||||||
|
require.True(t, envelopesEquivalent(expected, got),
|
||||||
|
"goroutine %d produced inequivalent envelope", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_GroupsByAccountPublicId(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.Groups, 2)
|
||||||
|
|
||||||
|
groupByID := map[string]*proto.GroupCompact{}
|
||||||
|
for _, g := range full.Groups {
|
||||||
|
groupByID[g.Id] = g
|
||||||
|
}
|
||||||
|
require.Contains(t, groupByID, "1")
|
||||||
|
require.Contains(t, groupByID, "2")
|
||||||
|
assert.Len(t, groupByID["1"].PeerIndexes, 1)
|
||||||
|
assert.Len(t, groupByID["2"].PeerIndexes, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_PolicyExpansion(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.Policies, 1)
|
||||||
|
pc := full.Policies[0]
|
||||||
|
assert.EqualValues(t, "10", pc.Id)
|
||||||
|
assert.Equal(t, proto.RuleAction_ACCEPT, pc.Action)
|
||||||
|
assert.Equal(t, proto.RuleProtocol_TCP, pc.Protocol)
|
||||||
|
assert.True(t, pc.Bidirectional)
|
||||||
|
assert.Equal(t, []uint32{22, 80}, pc.Ports)
|
||||||
|
require.Len(t, pc.PortRanges, 1)
|
||||||
|
assert.EqualValues(t, 8000, pc.PortRanges[0].Start)
|
||||||
|
assert.EqualValues(t, 8100, pc.PortRanges[0].End)
|
||||||
|
assert.Equal(t, []string{"1"}, pc.SourceGroupIds)
|
||||||
|
assert.Equal(t, []string{"2"}, pc.DestinationGroupIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_RouterIndexes(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.RouterPeerIndexes, 1)
|
||||||
|
idx := full.RouterPeerIndexes[0]
|
||||||
|
require.Less(t, int(idx), len(full.Peers))
|
||||||
|
assert.Equal(t, "peerc", full.Peers[idx].DnsLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_DisabledPolicySkipped(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.Policies[0].Enabled = false
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
assert.Empty(t, full.Policies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_TwoPeersSameMalformedKey(t *testing.T) {
|
||||||
|
// Both peers have nil WgPubKey after decode; canonicalize must still
|
||||||
|
// produce a stable order using DnsLabel as a tiebreaker, so 100 encodes
|
||||||
|
// canonicalize identically.
|
||||||
|
c := newTestComponents()
|
||||||
|
c.Peers["peer-a"].Key = "garbage-a-!!!"
|
||||||
|
c.Peers["peer-b"].Key = "garbage-b-!!!"
|
||||||
|
|
||||||
|
expected := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c})
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
got := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c})
|
||||||
|
require.True(t, envelopesEquivalent(expected, got),
|
||||||
|
"encode #%d with two same-key peers must canonicalize equivalently", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_MalformedWgKey(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.Peers["peer-a"].Key = "not-base64-!!!"
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.Peers, 3)
|
||||||
|
|
||||||
|
var byLabel = map[string]*proto.PeerCompact{}
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
byLabel[p.DnsLabel] = p
|
||||||
|
}
|
||||||
|
assert.Nil(t, byLabel["peera"].WgPubKey, "peer with malformed key encodes nil WgPubKey")
|
||||||
|
assert.Len(t, byLabel["peerb"].WgPubKey, 32, "other peers retain their key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_IPv6OnlyPeer(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
v6Only := &nbpeer.Peer{
|
||||||
|
ID: "peer-v6",
|
||||||
|
Key: testWgKeyA,
|
||||||
|
IPv6: netip.AddrFrom16([16]byte{0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9}),
|
||||||
|
DNSLabel: "peerv6",
|
||||||
|
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}
|
||||||
|
c.Peers["peer-v6"] = v6Only
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
var found *proto.PeerCompact
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
if p.DnsLabel == "peerv6" {
|
||||||
|
found = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, found, "ipv6-only peer must be present")
|
||||||
|
assert.Empty(t, found.Ip, "no IPv4 address → empty Ip")
|
||||||
|
assert.Len(t, found.Ipv6, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_PeerWithoutIP(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.Peers["peer-noip"] = &nbpeer.Peer{
|
||||||
|
ID: "peer-noip",
|
||||||
|
Key: testWgKeyA,
|
||||||
|
DNSLabel: "peernoip",
|
||||||
|
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
var found *proto.PeerCompact
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
if p.DnsLabel == "peernoip" {
|
||||||
|
found = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, found)
|
||||||
|
assert.Empty(t, found.Ip)
|
||||||
|
assert.Empty(t, found.Ipv6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_EmptyInput(t *testing.T) {
|
||||||
|
c := &types.NetworkMapComponents{
|
||||||
|
Network: &types.Network{Identifier: "x", Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)}},
|
||||||
|
}
|
||||||
|
|
||||||
|
env := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c})
|
||||||
|
|
||||||
|
full := env.GetFull()
|
||||||
|
require.NotNil(t, full)
|
||||||
|
assert.Empty(t, full.Peers)
|
||||||
|
assert.Empty(t, full.Groups)
|
||||||
|
assert.Empty(t, full.Policies)
|
||||||
|
assert.Empty(t, full.RouterPeerIndexes)
|
||||||
|
require.NotNil(t, full.AccountSettings, "AccountSettingsCompact must always be emitted (client dereferences it unconditionally)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_PeerLoginExpirationFields(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
now := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||||
|
c.Peers["peer-a"].UserID = "user-1"
|
||||||
|
c.Peers["peer-a"].LoginExpirationEnabled = true
|
||||||
|
c.Peers["peer-a"].LastLogin = &now
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
var pa *proto.PeerCompact
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
if p.DnsLabel == "peera" {
|
||||||
|
pa = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, pa)
|
||||||
|
assert.True(t, pa.AddedWithSsoLogin)
|
||||||
|
assert.True(t, pa.LoginExpirationEnabled)
|
||||||
|
assert.Equal(t, now.UnixNano(), pa.LastLoginUnixNano)
|
||||||
|
|
||||||
|
// peer-b has no UserID and no LastLogin → all fields zero-value.
|
||||||
|
var pb *proto.PeerCompact
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
if p.DnsLabel == "peerb" {
|
||||||
|
pb = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, pb)
|
||||||
|
assert.False(t, pb.AddedWithSsoLogin)
|
||||||
|
assert.False(t, pb.LoginExpirationEnabled)
|
||||||
|
assert.Zero(t, pb.LastLoginUnixNano)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_RoutesRoundTrip(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.Routes = []*nbroute.Route{
|
||||||
|
{
|
||||||
|
ID: "route-peer",
|
||||||
|
PublicID: "100",
|
||||||
|
NetID: "net-A",
|
||||||
|
Description: "via peer-c",
|
||||||
|
Network: netip.MustParsePrefix("10.0.0.0/16"),
|
||||||
|
Peer: "peer-c", // peer ID, not WG key
|
||||||
|
Groups: []string{"group-src"},
|
||||||
|
AccessControlGroups: []string{"group-dst"},
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "route-peergroup",
|
||||||
|
PublicID: "101",
|
||||||
|
NetID: "net-B",
|
||||||
|
Network: netip.MustParsePrefix("10.1.0.0/16"),
|
||||||
|
PeerGroups: []string{"group-src", "group-dst"},
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.Routes, 2)
|
||||||
|
byNetID := map[string]*proto.RouteRaw{}
|
||||||
|
for _, r := range full.Routes {
|
||||||
|
byNetID[r.NetId] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
r1 := byNetID["net-A"]
|
||||||
|
require.NotNil(t, r1)
|
||||||
|
assert.True(t, r1.PeerIndexSet, "route with peer must set peer_index_set")
|
||||||
|
require.Less(t, int(r1.PeerIndex), len(full.Peers))
|
||||||
|
assert.Equal(t, "peerc", full.Peers[r1.PeerIndex].DnsLabel)
|
||||||
|
assert.Equal(t, []string{"1"}, r1.GroupIds, "group-src has AccountSeqID 1")
|
||||||
|
assert.Equal(t, []string{"2"}, r1.AccessControlGroupIds, "group-dst has AccountSeqID 2")
|
||||||
|
assert.Empty(t, r1.PeerGroupIds)
|
||||||
|
|
||||||
|
r2 := byNetID["net-B"]
|
||||||
|
require.NotNil(t, r2)
|
||||||
|
assert.False(t, r2.PeerIndexSet, "route with peer_groups must NOT set peer_index_set")
|
||||||
|
assert.ElementsMatch(t, []string{"1", "2"}, r2.PeerGroupIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_RouteWithMissingPeerLeavesIndexUnset(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.Routes = []*nbroute.Route{{
|
||||||
|
ID: "route-x",
|
||||||
|
PublicID: "100",
|
||||||
|
Peer: "peer-not-in-components",
|
||||||
|
Network: netip.MustParsePrefix("10.0.0.0/16"),
|
||||||
|
Enabled: true,
|
||||||
|
}}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.Routes, 1)
|
||||||
|
assert.False(t, full.Routes[0].PeerIndexSet,
|
||||||
|
"missing peer reference must not pretend to point at peer index 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_ResourceOnlyPolicyShippedAndIndexed(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
// Policy that exists ONLY in ResourcePoliciesMap, not in c.Policies. This
|
||||||
|
// is the I1 case — without unionPolicies the encoder would silently
|
||||||
|
// drop it from the wire.
|
||||||
|
resourceOnlyPolicy := &types.Policy{
|
||||||
|
ID: "pol-resource", PublicID: "99", Enabled: true,
|
||||||
|
Rules: []*types.PolicyRule{{
|
||||||
|
ID: "rule-r", Enabled: true, Action: types.PolicyTrafficActionAccept,
|
||||||
|
Protocol: types.PolicyRuleProtocolTCP,
|
||||||
|
Sources: []string{"group-src"},
|
||||||
|
Destinations: []string{"group-dst"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
c.ResourcePoliciesMap = map[string][]*types.Policy{
|
||||||
|
"resource-x": {c.Policies[0], resourceOnlyPolicy}, // shared + resource-only
|
||||||
|
}
|
||||||
|
// Resource must appear in components.NetworkResources with a seq id —
|
||||||
|
// encoder uses that to translate the xid map key to uint32.
|
||||||
|
c.NetworkResources = []*resourceTypes.NetworkResource{
|
||||||
|
{ID: "resource-x", PublicID: "77", Name: "res-x", Enabled: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.Policies, 2, "encoded policies must include both peer-traffic and resource-only")
|
||||||
|
|
||||||
|
policyByID := map[string]*proto.PolicyCompact{}
|
||||||
|
policyIds := make([]string, 0)
|
||||||
|
for _, p := range full.Policies {
|
||||||
|
policyByID[p.Id] = p
|
||||||
|
policyIds = append(policyIds, p.Id)
|
||||||
|
}
|
||||||
|
require.Contains(t, policyByID, "10", "original peer-traffic policy id 10")
|
||||||
|
require.Contains(t, policyByID, "99", "resource-only policy id 99")
|
||||||
|
|
||||||
|
require.Contains(t, full.ResourcePoliciesMap, "77")
|
||||||
|
ids := full.ResourcePoliciesMap["77"].Ids
|
||||||
|
require.Len(t, ids, 2)
|
||||||
|
assert.ElementsMatch(t, policyIds, ids,
|
||||||
|
"resource policies map must reference both wire policy indexes")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_NameServerGroups(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.NameServerGroups = []*nbdns.NameServerGroup{{
|
||||||
|
ID: "nsg-1", PublicID: "50", Name: "Main", Description: "primary",
|
||||||
|
NameServers: []nbdns.NameServer{{
|
||||||
|
IP: netip.MustParseAddr("8.8.8.8"), NSType: nbdns.UDPNameServerType, Port: 53,
|
||||||
|
}},
|
||||||
|
Groups: []string{"group-src", "group-not-persisted"},
|
||||||
|
Primary: true, Enabled: true,
|
||||||
|
Domains: []string{"corp.example"},
|
||||||
|
}}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.NameserverGroups, 1)
|
||||||
|
nsg := full.NameserverGroups[0]
|
||||||
|
assert.EqualValues(t, "50", nsg.Id)
|
||||||
|
assert.True(t, nsg.Primary)
|
||||||
|
require.Len(t, nsg.Nameservers, 1)
|
||||||
|
assert.Equal(t, "8.8.8.8", nsg.Nameservers[0].IP)
|
||||||
|
assert.Equal(t, []string{"1"}, nsg.GroupIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_PostureFailedPeers(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.PostureCheckXIDToPublicID = map[string]string{"check-1": "33"}
|
||||||
|
c.PostureFailedPeers = map[string]map[string]struct{}{
|
||||||
|
"check-1": {
|
||||||
|
"peer-a": {},
|
||||||
|
"peer-b": {},
|
||||||
|
"peer-not-in-account": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Contains(t, full.PostureFailedPeers, "33")
|
||||||
|
idxs := full.PostureFailedPeers["33"].PeerIndexes
|
||||||
|
assert.Len(t, idxs, 2, "missing peer is silently dropped (filterPostureFailedPeers guarantees presence in real data)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_RoutersMap(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.NetworkXIDToPublicID = map[string]string{"net-1": "5"}
|
||||||
|
c.RoutersMap = map[string]map[string]*routerTypes.NetworkRouter{
|
||||||
|
"net-1": {
|
||||||
|
"peer-c": {
|
||||||
|
ID: "router-1", PublicID: "200",
|
||||||
|
Peer: "peer-c", Masquerade: true, Metric: 10, Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Contains(t, full.RoutersMap, "5")
|
||||||
|
entries := full.RoutersMap["5"].Entries
|
||||||
|
require.Len(t, entries, 1)
|
||||||
|
e := entries[0]
|
||||||
|
assert.EqualValues(t, "200", e.Id)
|
||||||
|
assert.True(t, e.PeerIndexSet)
|
||||||
|
require.Less(t, int(e.PeerIndex), len(full.Peers))
|
||||||
|
assert.Equal(t, "peerc", full.Peers[e.PeerIndex].DnsLabel)
|
||||||
|
assert.True(t, e.Masquerade)
|
||||||
|
assert.EqualValues(t, 10, e.Metric)
|
||||||
|
assert.True(t, e.Enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_RouterPeerNotInComponentsPeers(t *testing.T) {
|
||||||
|
// Router peer in c.RouterPeers but NOT in c.Peers (validation may have
|
||||||
|
// filtered it). indexRouterPeers runs before encodeRoutersMap, so the
|
||||||
|
// peer_index reference must still resolve.
|
||||||
|
c := newTestComponents()
|
||||||
|
delete(c.Peers, "peer-c")
|
||||||
|
routerPeer := &nbpeer.Peer{
|
||||||
|
ID: "peer-c", Key: testWgKeyC, IP: netip.AddrFrom4([4]byte{100, 64, 0, 3}),
|
||||||
|
DNSLabel: "peerc", Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}
|
||||||
|
c.RouterPeers = map[string]*nbpeer.Peer{"peer-c": routerPeer}
|
||||||
|
c.NetworkXIDToPublicID = map[string]string{"net-1": "5"}
|
||||||
|
c.RoutersMap = map[string]map[string]*routerTypes.NetworkRouter{
|
||||||
|
"net-1": {"peer-c": {ID: "r-1", PublicID: "1", Peer: "peer-c", Enabled: true}},
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Contains(t, full.RoutersMap, "5")
|
||||||
|
require.Len(t, full.RoutersMap["5"].Entries, 1)
|
||||||
|
e := full.RoutersMap["5"].Entries[0]
|
||||||
|
assert.True(t, e.PeerIndexSet, "router peer must be indexed even when not in c.Peers")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_GroupIDToUserIDs(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
c.GroupIDToUserIDs = map[string][]string{
|
||||||
|
"group-src": {"user-1", "user-2"},
|
||||||
|
"group-missing": {"user-4"}, // group not in components → drop
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.Len(t, full.GroupIdToUserIds, 1, "only present groups survive")
|
||||||
|
require.Contains(t, full.GroupIdToUserIds, "1")
|
||||||
|
assert.ElementsMatch(t, []string{"user-1", "user-2"}, full.GroupIdToUserIds["1"].UserIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToProxyPatch_EmptyInputReturnsNil(t *testing.T) {
|
||||||
|
assert.Nil(t, toProxyPatch(nil, "netbird.cloud", false, false))
|
||||||
|
assert.Nil(t, toProxyPatch(&types.NetworkMap{}, "netbird.cloud", false, false),
|
||||||
|
"empty NetworkMap (no peers, rules, routes etc) → nil patch so proto3 omits the field")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToProxyPatch_PopulatesAllFields(t *testing.T) {
|
||||||
|
nm := &types.NetworkMap{
|
||||||
|
Peers: []*nbpeer.Peer{{
|
||||||
|
ID: "ext-peer", Key: testWgKeyA, IP: netip.AddrFrom4([4]byte{100, 64, 0, 9}),
|
||||||
|
DNSLabel: "extpeer", Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}},
|
||||||
|
FirewallRules: []*types.FirewallRule{{
|
||||||
|
PeerIP: "100.64.0.9", Action: "accept", Direction: 0, Protocol: "tcp",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
patch := toProxyPatch(nm, "netbird.cloud", false, false)
|
||||||
|
|
||||||
|
require.NotNil(t, patch)
|
||||||
|
assert.Len(t, patch.Peers, 1)
|
||||||
|
assert.Len(t, patch.FirewallRules, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEncodeNetworkMapEnvelope_ProxyPatchPropagated covers the ProxyPatch
|
||||||
|
// pass-through in both encoder branches (normal path + nil-Components
|
||||||
|
// graceful-degrade). Guards against a regression that drops `ProxyPatch:`
|
||||||
|
// from one of the envelope struct literals.
|
||||||
|
func TestEncodeNetworkMapEnvelope_ProxyPatchPropagated(t *testing.T) {
|
||||||
|
patch := &proto.ProxyPatch{
|
||||||
|
ForwardingRules: []*proto.ForwardingRule{{
|
||||||
|
Protocol: proto.RuleProtocol_TCP,
|
||||||
|
DestinationPort: &proto.PortInfo{PortSelection: &proto.PortInfo_Port{Port: 80}},
|
||||||
|
TranslatedAddress: net.IPv4(10, 0, 0, 1).To4(),
|
||||||
|
TranslatedPort: &proto.PortInfo{PortSelection: &proto.PortInfo_Port{Port: 8080}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("normal_path", func(t *testing.T) {
|
||||||
|
c := newTestComponents()
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{
|
||||||
|
Components: c,
|
||||||
|
ProxyPatch: patch,
|
||||||
|
}).GetFull()
|
||||||
|
|
||||||
|
require.NotNil(t, full.ProxyPatch, "ProxyPatch must propagate through the normal encode path")
|
||||||
|
assert.Len(t, full.ProxyPatch.ForwardingRules, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty_components_graceful_degrade", func(t *testing.T) {
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{
|
||||||
|
Components: emptyNetworkMapComponents(),
|
||||||
|
ProxyPatch: patch,
|
||||||
|
}).GetFull()
|
||||||
|
|
||||||
|
require.NotNil(t, full.ProxyPatch, "ProxyPatch must propagate through the nil-Components branch too")
|
||||||
|
assert.Len(t, full.ProxyPatch.ForwardingRules, 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_NilComponentsGracefulDegrade(t *testing.T) {
|
||||||
|
// nil Components → minimal envelope, no crash. Matches the legacy
|
||||||
|
// behaviour for missing/unvalidated peers.
|
||||||
|
env := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{
|
||||||
|
Components: emptyNetworkMapComponents(),
|
||||||
|
DNSDomain: "netbird.cloud",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NotNil(t, env)
|
||||||
|
full := env.GetFull()
|
||||||
|
require.NotNil(t, full)
|
||||||
|
require.NotNil(t, full.AccountSettings, "AccountSettings must always be non-nil")
|
||||||
|
assert.Equal(t, "netbird.cloud", full.DnsDomain)
|
||||||
|
assert.Len(t, full.Peers, 1)
|
||||||
|
assert.Empty(t, full.Policies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeNetworkMapEnvelope_AccountSettingsAlwaysEmitted(t *testing.T) {
|
||||||
|
c := &types.NetworkMapComponents{
|
||||||
|
Network: &types.Network{Identifier: "x", Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)}},
|
||||||
|
// AccountSettings deliberately nil
|
||||||
|
}
|
||||||
|
|
||||||
|
full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull()
|
||||||
|
|
||||||
|
require.NotNil(t, full.AccountSettings, "client dereferences AccountSettings unconditionally during Calculate(); a nil here would crash the receiver")
|
||||||
|
assert.False(t, full.AccountSettings.PeerLoginExpirationEnabled)
|
||||||
|
assert.Zero(t, full.AccountSettings.PeerLoginExpirationNs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func emptyNetworkMapComponents() *types.NetworkMapComponents {
|
||||||
|
return types.EmptyNetworkMapComponents(
|
||||||
|
&types.NetworkMapComponents{
|
||||||
|
PeerID: "peer-id", Peers: map[string]*nbpeer.Peer{"peer-id": {}}},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
integrationsConfig "github.com/netbirdio/management-integrations/integrations/config"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/ssh/auth"
|
||||||
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
"github.com/netbirdio/netbird/management/server/posture"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
sharedgrpc "github.com/netbirdio/netbird/shared/management/grpc"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToComponentSyncResponse builds a SyncResponse carrying the compact
|
||||||
|
// NetworkMapEnvelope for capability-aware peers. The legacy proto.NetworkMap
|
||||||
|
// field is intentionally left empty — capable peers ignore it and the
|
||||||
|
// envelope alone is the authoritative wire shape.
|
||||||
|
//
|
||||||
|
// PeerConfig is computed once server-side using the receiving peer's own
|
||||||
|
// account-level network metadata. EnableSSH inside PeerConfig is left at
|
||||||
|
// peer.SSHEnabled (the peer's local setting); account-policy-driven SSH is
|
||||||
|
// computed by the client from the envelope's GroupIDToUserIDs / AllowedUserIDs
|
||||||
|
// inside Calculate(), so the SshConfig.SshEnabled bit may flip true on the
|
||||||
|
// client even though the server-side PeerConfig reports false.
|
||||||
|
func ToComponentSyncResponse(
|
||||||
|
ctx context.Context,
|
||||||
|
config *nbconfig.Config,
|
||||||
|
httpConfig *nbconfig.HttpServerConfig,
|
||||||
|
deviceFlowConfig *nbconfig.DeviceAuthorizationFlow,
|
||||||
|
peer *nbpeer.Peer,
|
||||||
|
turnCredentials *Token,
|
||||||
|
relayCredentials *Token,
|
||||||
|
components *types.NetworkMapComponents,
|
||||||
|
proxyPatch *types.NetworkMap,
|
||||||
|
dnsName string,
|
||||||
|
checks []*posture.Checks,
|
||||||
|
settings *types.Settings,
|
||||||
|
extraSettings *types.ExtraSettings,
|
||||||
|
peerGroups []string,
|
||||||
|
dnsFwdPort int64,
|
||||||
|
) *proto.SyncResponse {
|
||||||
|
//
|
||||||
|
// 'component' parameter is expected to never be nil
|
||||||
|
// 'peer' parameter is expected to never be nil
|
||||||
|
//
|
||||||
|
// TODO (dmitri) consider using invariants?
|
||||||
|
//
|
||||||
|
enableSSH := computeSSHEnabledForPeer(components, peer)
|
||||||
|
peerConfig := toPeerConfig(peer, components.Network, dnsName, settings, httpConfig, deviceFlowConfig, enableSSH)
|
||||||
|
|
||||||
|
includeIPv6 := peer.SupportsIPv6() && peer.IPv6.IsValid()
|
||||||
|
useSourcePrefixes := peer.SupportsSourcePrefixes()
|
||||||
|
|
||||||
|
userIDClaim := auth.DefaultUserIDClaim
|
||||||
|
if httpConfig != nil && httpConfig.AuthUserIDClaim != "" {
|
||||||
|
userIDClaim = httpConfig.AuthUserIDClaim
|
||||||
|
}
|
||||||
|
|
||||||
|
envelope := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{
|
||||||
|
Components: components,
|
||||||
|
PeerConfig: peerConfig,
|
||||||
|
DNSDomain: dnsName,
|
||||||
|
DNSForwarderPort: dnsFwdPort,
|
||||||
|
UserIDClaim: userIDClaim,
|
||||||
|
ProxyPatch: toProxyPatch(proxyPatch, dnsName, includeIPv6, useSourcePrefixes),
|
||||||
|
})
|
||||||
|
|
||||||
|
resp := &proto.SyncResponse{
|
||||||
|
PeerConfig: peerConfig,
|
||||||
|
NetworkMapEnvelope: envelope,
|
||||||
|
Checks: toProtocolChecks(ctx, checks),
|
||||||
|
Version: int32(sharedgrpc.ComponentNetworkMap),
|
||||||
|
}
|
||||||
|
|
||||||
|
nbConfig := toNetbirdConfig(config, turnCredentials, relayCredentials, extraSettings, settings)
|
||||||
|
resp.NetbirdConfig = integrationsConfig.ExtendNetBirdConfig(peer.ID, peerGroups, nbConfig, extraSettings)
|
||||||
|
|
||||||
|
// settings == nil → field stays nil → "no info in this snapshot", client
|
||||||
|
// preserves the deadline it already had. settings non-nil → emit either a
|
||||||
|
// valid deadline or the explicit-zero "disabled" sentinel via
|
||||||
|
// encodeSessionExpiresAt.
|
||||||
|
if settings != nil {
|
||||||
|
resp.SessionExpiresAt = encodeSessionExpiresAt(
|
||||||
|
peer.SessionExpiresAt(settings.PeerLoginExpirationEnabled, settings.PeerLoginExpiration),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// toProxyPatch converts a proxy-injected *types.NetworkMap into the wire
|
||||||
|
// patch the components envelope ships alongside. Returns nil when there are
|
||||||
|
// no fragments to merge — proto3 omits a nil message field, so the receiver
|
||||||
|
// sees no patch and skips the merge step entirely.
|
||||||
|
//
|
||||||
|
// We reuse the legacy proto-conversion helpers (toProtocolRoutes,
|
||||||
|
// toProtocolFirewallRules, toProtocolRoutesFirewallRules,
|
||||||
|
// appendRemotePeerConfig, ForwardingRule.ToProto) because the proxy
|
||||||
|
// delivers fragments pre-expanded — there's no raw component shape to
|
||||||
|
// derive them from. Components purity isn't violated: proxy data isn't
|
||||||
|
// policy-graph-derived, it's externally injected post-Calculate, so the
|
||||||
|
// client merges it on top of its locally-computed NetworkMap.
|
||||||
|
func toProxyPatch(nm *types.NetworkMap, dnsName string, includeIPv6, useSourcePrefixes bool) *proto.ProxyPatch {
|
||||||
|
if nm == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(nm.Peers) == 0 && len(nm.OfflinePeers) == 0 && len(nm.FirewallRules) == 0 &&
|
||||||
|
len(nm.Routes) == 0 && len(nm.RoutesFirewallRules) == 0 && len(nm.ForwardingRules) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
patch := &proto.ProxyPatch{
|
||||||
|
Peers: networkmap.AppendRemotePeerConfig(nil, nm.Peers, dnsName, includeIPv6),
|
||||||
|
OfflinePeers: networkmap.AppendRemotePeerConfig(nil, nm.OfflinePeers, dnsName, includeIPv6),
|
||||||
|
FirewallRules: networkmap.ToProtocolFirewallRules(nm.FirewallRules, includeIPv6, useSourcePrefixes),
|
||||||
|
Routes: networkmap.ToProtocolRoutes(nm.Routes),
|
||||||
|
RouteFirewallRules: networkmap.ToProtocolRoutesFirewallRules(nm.RoutesFirewallRules),
|
||||||
|
}
|
||||||
|
if len(nm.ForwardingRules) > 0 {
|
||||||
|
patch.ForwardingRules = make([]*proto.ForwardingRule, 0, len(nm.ForwardingRules))
|
||||||
|
for _, r := range nm.ForwardingRules {
|
||||||
|
patch.ForwardingRules = append(patch.ForwardingRules, r.ToProto())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return patch
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeSSHEnabledForPeer mirrors the SSH-server-activation bit that
|
||||||
|
// Calculate() folds into NetworkMap.EnableSSH. Components-format peers
|
||||||
|
// receive a freshly-computed PeerConfig.SshConfig.SshEnabled at sync time;
|
||||||
|
// without this helper the field would be incorrectly false for any peer
|
||||||
|
// that's the destination of an SSH-enabling policy without having
|
||||||
|
// peer.SSHEnabled set locally.
|
||||||
|
//
|
||||||
|
// Mirrors the two activation paths Calculate() uses:
|
||||||
|
// 1. Explicit: rule.Protocol == NetbirdSSH and peer is in the rule's
|
||||||
|
// destinations.
|
||||||
|
// 2. Legacy implicit: rule covers TCP/22 or TCP/22022 (or ALL), peer is in
|
||||||
|
// destinations, AND the peer has SSHEnabled set locally — this is the
|
||||||
|
// "allow-all/TCP-22 implies SSH activation for SSH-capable peers" path.
|
||||||
|
//
|
||||||
|
// The full SSH AuthorizedUsers map is still produced by the client when it
|
||||||
|
// runs Calculate() over the envelope.
|
||||||
|
func computeSSHEnabledForPeer(c *types.NetworkMapComponents, peer *nbpeer.Peer) bool {
|
||||||
|
if c == nil || peer == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Mirror Calculate's `getAllPeersFromGroups` invariant: target peer must
|
||||||
|
// exist in c.Peers, otherwise no rule applies to it.
|
||||||
|
if _, ok := c.Peers[peer.ID]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, policy := range c.Policies {
|
||||||
|
if policy == nil || !policy.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, rule := range policy.Rules {
|
||||||
|
if ruleEnablesSSHForPeer(c, rule, peer) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ruleEnablesSSHForPeer returns true when rule is active, targets peer, and
|
||||||
|
// either explicitly authorises SSH or covers the legacy TCP/22 path while the
|
||||||
|
// peer itself has SSH enabled locally.
|
||||||
|
func ruleEnablesSSHForPeer(c *types.NetworkMapComponents, rule *types.PolicyRule, peer *nbpeer.Peer) bool {
|
||||||
|
if rule == nil || !rule.Enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !peerInDestinations(c, rule, peer.ID) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if rule.Protocol == types.PolicyRuleProtocolNetbirdSSH {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return peer.SSHEnabled && types.PolicyRuleImpliesLegacySSH(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
// peerInDestinations reports whether peerID is in any of rule.Destinations'
|
||||||
|
// groups (or matches DestinationResource if it's a peer-typed resource —
|
||||||
|
// for non-peer types Calculate falls through to group lookup, so we mirror
|
||||||
|
// that exactly to avoid silent divergence).
|
||||||
|
func peerInDestinations(c *types.NetworkMapComponents, rule *types.PolicyRule, peerID string) bool {
|
||||||
|
if rule.DestinationResource.Type == types.ResourceTypePeer && rule.DestinationResource.ID != "" {
|
||||||
|
return rule.DestinationResource.ID == peerID
|
||||||
|
}
|
||||||
|
for _, groupID := range rule.Destinations {
|
||||||
|
if c.IsPeerInGroup(peerID, groupID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestComputeSSHEnabledForPeer covers both Calculate-mirroring branches:
|
||||||
|
// explicit NetbirdSSH protocol, and the legacy implicit case where a
|
||||||
|
// TCP/22 (or 22022 / ALL / port-range-covering-22) rule activates SSH when
|
||||||
|
// the destination peer has SSHEnabled=true locally.
|
||||||
|
func TestComputeSSHEnabledForPeer(t *testing.T) {
|
||||||
|
const targetPeerID = "target"
|
||||||
|
const targetGroupID = "g_dst"
|
||||||
|
|
||||||
|
mkComponents := func(rule *types.PolicyRule, sshEnabled bool) (*types.NetworkMapComponents, *nbpeer.Peer) {
|
||||||
|
peer := &nbpeer.Peer{ID: targetPeerID, SSHEnabled: sshEnabled}
|
||||||
|
group := &types.Group{ID: targetGroupID, Name: "dst", Peers: []string{targetPeerID}}
|
||||||
|
return &types.NetworkMapComponents{
|
||||||
|
Peers: map[string]*nbpeer.Peer{targetPeerID: peer},
|
||||||
|
Groups: map[string]*types.Group{targetGroupID: group},
|
||||||
|
Policies: []*types.Policy{{
|
||||||
|
ID: "p",
|
||||||
|
Enabled: true,
|
||||||
|
Rules: []*types.PolicyRule{rule},
|
||||||
|
}},
|
||||||
|
}, peer
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
peerSSH bool
|
||||||
|
rule types.PolicyRule
|
||||||
|
wantEnabled bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "explicit-netbird-ssh-activates-regardless-of-peer-ssh",
|
||||||
|
peerSSH: false,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolNetbirdSSH,
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit-tcp-22-with-peer-ssh",
|
||||||
|
peerSSH: true,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolTCP, Ports: []string{"22"},
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit-tcp-22-without-peer-ssh-disabled",
|
||||||
|
peerSSH: false,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolTCP, Ports: []string{"22"},
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit-tcp-22022-with-peer-ssh",
|
||||||
|
peerSSH: true,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolTCP, Ports: []string{"22022"},
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit-all-protocol-with-peer-ssh",
|
||||||
|
peerSSH: true,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolALL,
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit-port-range-covers-22",
|
||||||
|
peerSSH: true,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true,
|
||||||
|
Protocol: types.PolicyRuleProtocolTCP,
|
||||||
|
PortRanges: []types.RulePortRange{{Start: 20, End: 30}},
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tcp-80-no-ssh",
|
||||||
|
peerSSH: true,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolTCP, Ports: []string{"80"},
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disabled-rule-skipped",
|
||||||
|
peerSSH: true,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: false, Protocol: types.PolicyRuleProtocolNetbirdSSH,
|
||||||
|
Destinations: []string{targetGroupID},
|
||||||
|
},
|
||||||
|
wantEnabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "peer-not-in-destinations",
|
||||||
|
peerSSH: true,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolNetbirdSSH,
|
||||||
|
Destinations: []string{"g_other"}, // target not in this group
|
||||||
|
},
|
||||||
|
wantEnabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "peer-typed-destination-resource-matches",
|
||||||
|
peerSSH: false,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true,
|
||||||
|
Protocol: types.PolicyRuleProtocolNetbirdSSH,
|
||||||
|
DestinationResource: types.Resource{ID: targetPeerID, Type: types.ResourceTypePeer},
|
||||||
|
},
|
||||||
|
wantEnabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-peer-destination-resource-falls-through-to-groups",
|
||||||
|
peerSSH: false,
|
||||||
|
rule: types.PolicyRule{
|
||||||
|
Enabled: true,
|
||||||
|
Protocol: types.PolicyRuleProtocolNetbirdSSH,
|
||||||
|
DestinationResource: types.Resource{ID: targetPeerID, Type: "host"}, // wrong type
|
||||||
|
Destinations: []string{targetGroupID}, // saved by group fallback
|
||||||
|
},
|
||||||
|
wantEnabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
c, peer := mkComponents(&tc.rule, tc.peerSSH)
|
||||||
|
got := computeSSHEnabledForPeer(c, peer)
|
||||||
|
assert.Equal(t, tc.wantEnabled, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestComputeSSHEnabledForPeer_TargetMissingFromComponents covers the
|
||||||
|
// belt-and-suspenders presence guard mirroring Calculate's
|
||||||
|
// getAllPeersFromGroups invariant.
|
||||||
|
func TestComputeSSHEnabledForPeer_TargetMissingFromComponents(t *testing.T) {
|
||||||
|
peer := &nbpeer.Peer{ID: "missing", SSHEnabled: true}
|
||||||
|
c := &types.NetworkMapComponents{
|
||||||
|
Peers: map[string]*nbpeer.Peer{}, // target peer NOT present
|
||||||
|
Groups: map[string]*types.Group{
|
||||||
|
"g": {ID: "g", Peers: []string{"missing"}},
|
||||||
|
},
|
||||||
|
Policies: []*types.Policy{{
|
||||||
|
ID: "p", Enabled: true,
|
||||||
|
Rules: []*types.PolicyRule{{
|
||||||
|
Enabled: true, Protocol: types.PolicyRuleProtocolNetbirdSSH,
|
||||||
|
Destinations: []string{"g"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
assert.False(t, computeSSHEnabledForPeer(c, peer),
|
||||||
|
"missing target peer must short-circuit to false, not consult policies")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestComputeSSHEnabledForPeer_NilInputs guards the cheap nil-checks at
|
||||||
|
// function entry — Calculate doesn't accept nil either, but the helper is
|
||||||
|
// exported indirectly via ToComponentSyncResponse and may receive nil
|
||||||
|
// components on graceful-degrade paths.
|
||||||
|
func TestComputeSSHEnabledForPeer_NilInputs(t *testing.T) {
|
||||||
|
assert.False(t, computeSSHEnabledForPeer(nil, &nbpeer.Peer{ID: "x"}))
|
||||||
|
assert.False(t, computeSSHEnabledForPeer(&types.NetworkMapComponents{}, nil))
|
||||||
|
}
|
||||||
@@ -10,24 +10,20 @@ import (
|
|||||||
|
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
nbversion "github.com/netbirdio/netbird/version"
|
nbversion "github.com/netbirdio/netbird/version"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
goproto "google.golang.org/protobuf/proto"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
integrationsConfig "github.com/netbirdio/management-integrations/integrations/config"
|
integrationsConfig "github.com/netbirdio/management-integrations/integrations/config"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/ssh/auth"
|
"github.com/netbirdio/netbird/client/ssh/auth"
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
||||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
"github.com/netbirdio/netbird/management/server/posture"
|
"github.com/netbirdio/netbird/management/server/posture"
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
nbroute "github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||||
"github.com/netbirdio/netbird/shared/management/proto"
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
"github.com/netbirdio/netbird/shared/netiputil"
|
"github.com/netbirdio/netbird/shared/netiputil"
|
||||||
"github.com/netbirdio/netbird/shared/sshauth"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -169,8 +165,8 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb
|
|||||||
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH),
|
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH),
|
||||||
NetworkMap: &proto.NetworkMap{
|
NetworkMap: &proto.NetworkMap{
|
||||||
Serial: networkMap.Network.CurrentSerial(),
|
Serial: networkMap.Network.CurrentSerial(),
|
||||||
Routes: toProtocolRoutes(networkMap.Routes),
|
Routes: networkmap.ToProtocolRoutes(networkMap.Routes),
|
||||||
DNSConfig: toProtocolDNSConfig(networkMap.DNSConfig, dnsCache, dnsFwdPort),
|
DNSConfig: networkmap.ToProtocolDNSConfig(networkMap.DNSConfig, dnsCache, dnsFwdPort),
|
||||||
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH),
|
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH),
|
||||||
},
|
},
|
||||||
Checks: toProtocolChecks(ctx, checks),
|
Checks: toProtocolChecks(ctx, checks),
|
||||||
@@ -183,7 +179,7 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb
|
|||||||
response.NetworkMap.PeerConfig = response.PeerConfig
|
response.NetworkMap.PeerConfig = response.PeerConfig
|
||||||
|
|
||||||
remotePeers := make([]*proto.RemotePeerConfig, 0, len(networkMap.Peers)+len(networkMap.OfflinePeers))
|
remotePeers := make([]*proto.RemotePeerConfig, 0, len(networkMap.Peers)+len(networkMap.OfflinePeers))
|
||||||
remotePeers = appendRemotePeerConfig(remotePeers, networkMap.Peers, dnsName, includeIPv6)
|
remotePeers = networkmap.AppendRemotePeerConfig(remotePeers, networkMap.Peers, dnsName, includeIPv6)
|
||||||
|
|
||||||
if !shouldSkipSendingDeprecatedRemotePeers(peer.Meta.WtVersion) {
|
if !shouldSkipSendingDeprecatedRemotePeers(peer.Meta.WtVersion) {
|
||||||
response.RemotePeers = remotePeers
|
response.RemotePeers = remotePeers
|
||||||
@@ -193,13 +189,13 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb
|
|||||||
response.RemotePeersIsEmpty = len(remotePeers) == 0
|
response.RemotePeersIsEmpty = len(remotePeers) == 0
|
||||||
response.NetworkMap.RemotePeersIsEmpty = response.RemotePeersIsEmpty
|
response.NetworkMap.RemotePeersIsEmpty = response.RemotePeersIsEmpty
|
||||||
|
|
||||||
response.NetworkMap.OfflinePeers = appendRemotePeerConfig(nil, networkMap.OfflinePeers, dnsName, includeIPv6)
|
response.NetworkMap.OfflinePeers = networkmap.AppendRemotePeerConfig(nil, networkMap.OfflinePeers, dnsName, includeIPv6)
|
||||||
|
|
||||||
firewallRules := toProtocolFirewallRules(networkMap.FirewallRules, includeIPv6, useSourcePrefixes)
|
firewallRules := networkmap.ToProtocolFirewallRules(networkMap.FirewallRules, includeIPv6, useSourcePrefixes)
|
||||||
response.NetworkMap.FirewallRules = firewallRules
|
response.NetworkMap.FirewallRules = firewallRules
|
||||||
response.NetworkMap.FirewallRulesIsEmpty = len(firewallRules) == 0
|
response.NetworkMap.FirewallRulesIsEmpty = len(firewallRules) == 0
|
||||||
|
|
||||||
routesFirewallRules := toProtocolRoutesFirewallRules(networkMap.RoutesFirewallRules)
|
routesFirewallRules := networkmap.ToProtocolRoutesFirewallRules(networkMap.RoutesFirewallRules)
|
||||||
response.NetworkMap.RoutesFirewallRules = routesFirewallRules
|
response.NetworkMap.RoutesFirewallRules = routesFirewallRules
|
||||||
response.NetworkMap.RoutesFirewallRulesIsEmpty = len(routesFirewallRules) == 0
|
response.NetworkMap.RoutesFirewallRulesIsEmpty = len(routesFirewallRules) == 0
|
||||||
|
|
||||||
@@ -212,7 +208,7 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb
|
|||||||
}
|
}
|
||||||
|
|
||||||
if networkMap.AuthorizedUsers != nil {
|
if networkMap.AuthorizedUsers != nil {
|
||||||
hashedUsers, machineUsers := buildAuthorizedUsersProto(ctx, networkMap.AuthorizedUsers)
|
hashedUsers, machineUsers := networkmap.BuildAuthorizedUsersProto(ctx, networkMap.AuthorizedUsers)
|
||||||
userIDClaim := auth.DefaultUserIDClaim
|
userIDClaim := auth.DefaultUserIDClaim
|
||||||
if httpConfig != nil && httpConfig.AuthUserIDClaim != "" {
|
if httpConfig != nil && httpConfig.AuthUserIDClaim != "" {
|
||||||
userIDClaim = httpConfig.AuthUserIDClaim
|
userIDClaim = httpConfig.AuthUserIDClaim
|
||||||
@@ -252,33 +248,6 @@ func encodeSessionExpiresAt(deadline time.Time) *timestamppb.Timestamp {
|
|||||||
return timestamppb.New(deadline)
|
return timestamppb.New(deadline)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildAuthorizedUsersProto(ctx context.Context, authorizedUsers map[string]map[string]struct{}) ([][]byte, map[string]*proto.MachineUserIndexes) {
|
|
||||||
userIDToIndex := make(map[string]uint32)
|
|
||||||
var hashedUsers [][]byte
|
|
||||||
machineUsers := make(map[string]*proto.MachineUserIndexes, len(authorizedUsers))
|
|
||||||
|
|
||||||
for machineUser, users := range authorizedUsers {
|
|
||||||
indexes := make([]uint32, 0, len(users))
|
|
||||||
for userID := range users {
|
|
||||||
idx, exists := userIDToIndex[userID]
|
|
||||||
if !exists {
|
|
||||||
hash, err := sshauth.HashUserID(userID)
|
|
||||||
if err != nil {
|
|
||||||
log.WithContext(ctx).Errorf("failed to hash user id %s: %v", userID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
idx = uint32(len(hashedUsers))
|
|
||||||
userIDToIndex[userID] = idx
|
|
||||||
hashedUsers = append(hashedUsers, hash[:])
|
|
||||||
}
|
|
||||||
indexes = append(indexes, idx)
|
|
||||||
}
|
|
||||||
machineUsers[machineUser] = &proto.MachineUserIndexes{Indexes: indexes}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hashedUsers, machineUsers
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldSkipSendingDeprecatedRemotePeers(peerVersion string) bool {
|
func shouldSkipSendingDeprecatedRemotePeers(peerVersion string) bool {
|
||||||
if nbversion.IsDevelopmentVersion(peerVersion) {
|
if nbversion.IsDevelopmentVersion(peerVersion) {
|
||||||
return true
|
return true
|
||||||
@@ -292,51 +261,6 @@ func shouldSkipSendingDeprecatedRemotePeers(peerVersion string) bool {
|
|||||||
return precomputedDeprecatedRemotePeersConstraint.Check(peerNBVersion)
|
return precomputedDeprecatedRemotePeersConstraint.Check(peerNBVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nbpeer.Peer, dnsName string, includeIPv6 bool) []*proto.RemotePeerConfig {
|
|
||||||
for _, rPeer := range peers {
|
|
||||||
allowedIPs := []string{rPeer.IP.String() + "/32"}
|
|
||||||
if includeIPv6 && rPeer.IPv6.IsValid() {
|
|
||||||
allowedIPs = append(allowedIPs, rPeer.IPv6.String()+"/128")
|
|
||||||
}
|
|
||||||
dst = append(dst, &proto.RemotePeerConfig{
|
|
||||||
WgPubKey: rPeer.Key,
|
|
||||||
AllowedIps: allowedIPs,
|
|
||||||
SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
|
|
||||||
Fqdn: rPeer.FQDN(dnsName),
|
|
||||||
AgentVersion: rPeer.Meta.WtVersion,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// toProtocolDNSConfig converts nbdns.Config to proto.DNSConfig using the cache
|
|
||||||
func toProtocolDNSConfig(update nbdns.Config, cache *cache.DNSConfigCache, forwardPort int64) *proto.DNSConfig {
|
|
||||||
protoUpdate := &proto.DNSConfig{
|
|
||||||
ServiceEnable: update.ServiceEnable,
|
|
||||||
CustomZones: make([]*proto.CustomZone, 0, len(update.CustomZones)),
|
|
||||||
NameServerGroups: make([]*proto.NameServerGroup, 0, len(update.NameServerGroups)),
|
|
||||||
ForwarderPort: forwardPort,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, zone := range update.CustomZones {
|
|
||||||
protoZone := convertToProtoCustomZone(zone)
|
|
||||||
protoUpdate.CustomZones = append(protoUpdate.CustomZones, protoZone)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, nsGroup := range update.NameServerGroups {
|
|
||||||
cacheKey := nsGroup.ID
|
|
||||||
if cachedGroup, exists := cache.GetNameServerGroup(cacheKey); exists {
|
|
||||||
protoUpdate.NameServerGroups = append(protoUpdate.NameServerGroups, cachedGroup)
|
|
||||||
} else {
|
|
||||||
protoGroup := convertToProtoNameServerGroup(nsGroup)
|
|
||||||
cache.SetNameServerGroup(cacheKey, protoGroup)
|
|
||||||
protoUpdate.NameServerGroups = append(protoUpdate.NameServerGroups, protoGroup)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return protoUpdate
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToResponseProto(configProto nbconfig.Protocol) proto.HostConfig_Protocol {
|
func ToResponseProto(configProto nbconfig.Protocol) proto.HostConfig_Protocol {
|
||||||
switch configProto {
|
switch configProto {
|
||||||
case nbconfig.UDP:
|
case nbconfig.UDP:
|
||||||
@@ -354,203 +278,6 @@ func ToResponseProto(configProto nbconfig.Protocol) proto.HostConfig_Protocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toProtocolRoutes(routes []*nbroute.Route) []*proto.Route {
|
|
||||||
protoRoutes := make([]*proto.Route, 0, len(routes))
|
|
||||||
for _, r := range routes {
|
|
||||||
protoRoutes = append(protoRoutes, toProtocolRoute(r))
|
|
||||||
}
|
|
||||||
return protoRoutes
|
|
||||||
}
|
|
||||||
|
|
||||||
func toProtocolRoute(route *nbroute.Route) *proto.Route {
|
|
||||||
return &proto.Route{
|
|
||||||
ID: string(route.ID),
|
|
||||||
NetID: string(route.NetID),
|
|
||||||
Network: route.Network.String(),
|
|
||||||
Domains: route.Domains.ToPunycodeList(),
|
|
||||||
NetworkType: int64(route.NetworkType),
|
|
||||||
Peer: route.Peer,
|
|
||||||
Metric: int64(route.Metric),
|
|
||||||
Masquerade: route.Masquerade,
|
|
||||||
KeepRoute: route.KeepRoute,
|
|
||||||
SkipAutoApply: route.SkipAutoApply,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// toProtocolFirewallRules converts the firewall rules to the protocol firewall rules.
|
|
||||||
// When useSourcePrefixes is true, the compact SourcePrefixes field is populated
|
|
||||||
// alongside the deprecated PeerIP for forward compatibility.
|
|
||||||
// Wildcard rules ("0.0.0.0") are expanded into separate v4 and v6 SourcePrefixes
|
|
||||||
// when includeIPv6 is true.
|
|
||||||
func toProtocolFirewallRules(rules []*types.FirewallRule, includeIPv6, useSourcePrefixes bool) []*proto.FirewallRule {
|
|
||||||
result := make([]*proto.FirewallRule, 0, len(rules))
|
|
||||||
for i := range rules {
|
|
||||||
rule := rules[i]
|
|
||||||
|
|
||||||
fwRule := &proto.FirewallRule{
|
|
||||||
PolicyID: []byte(rule.PolicyID),
|
|
||||||
PeerIP: rule.PeerIP, //nolint:staticcheck // populated for backward compatibility
|
|
||||||
Direction: getProtoDirection(rule.Direction),
|
|
||||||
Action: getProtoAction(rule.Action),
|
|
||||||
Protocol: getProtoProtocol(rule.Protocol),
|
|
||||||
Port: rule.Port,
|
|
||||||
}
|
|
||||||
|
|
||||||
if useSourcePrefixes && rule.PeerIP != "" {
|
|
||||||
result = append(result, populateSourcePrefixes(fwRule, rule, includeIPv6)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if shouldUsePortRange(fwRule) {
|
|
||||||
fwRule.PortInfo = rule.PortRange.ToProto()
|
|
||||||
}
|
|
||||||
|
|
||||||
result = append(result, fwRule)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// populateSourcePrefixes sets SourcePrefixes on fwRule and returns any
|
|
||||||
// additional rules needed (e.g. a v6 wildcard clone when the peer IP is unspecified).
|
|
||||||
func populateSourcePrefixes(fwRule *proto.FirewallRule, rule *types.FirewallRule, includeIPv6 bool) []*proto.FirewallRule {
|
|
||||||
addr, err := netip.ParseAddr(rule.PeerIP)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !addr.IsUnspecified() {
|
|
||||||
fwRule.SourcePrefixes = [][]byte{netiputil.EncodeAddr(addr.Unmap())}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPv4Unspecified/0 is always valid, error is impossible.
|
|
||||||
v4Wildcard, _ := netiputil.EncodePrefix(netip.PrefixFrom(netip.IPv4Unspecified(), 0))
|
|
||||||
fwRule.SourcePrefixes = [][]byte{v4Wildcard}
|
|
||||||
|
|
||||||
if !includeIPv6 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
v6Rule := goproto.Clone(fwRule).(*proto.FirewallRule)
|
|
||||||
v6Rule.PeerIP = "::" //nolint:staticcheck // populated for backward compatibility
|
|
||||||
// IPv6Unspecified/0 is always valid, error is impossible.
|
|
||||||
v6Wildcard, _ := netiputil.EncodePrefix(netip.PrefixFrom(netip.IPv6Unspecified(), 0))
|
|
||||||
v6Rule.SourcePrefixes = [][]byte{v6Wildcard}
|
|
||||||
if shouldUsePortRange(v6Rule) {
|
|
||||||
v6Rule.PortInfo = rule.PortRange.ToProto()
|
|
||||||
}
|
|
||||||
return []*proto.FirewallRule{v6Rule}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getProtoDirection converts the direction to proto.RuleDirection.
|
|
||||||
func getProtoDirection(direction int) proto.RuleDirection {
|
|
||||||
if direction == types.FirewallRuleDirectionOUT {
|
|
||||||
return proto.RuleDirection_OUT
|
|
||||||
}
|
|
||||||
return proto.RuleDirection_IN
|
|
||||||
}
|
|
||||||
|
|
||||||
func toProtocolRoutesFirewallRules(rules []*types.RouteFirewallRule) []*proto.RouteFirewallRule {
|
|
||||||
result := make([]*proto.RouteFirewallRule, len(rules))
|
|
||||||
for i := range rules {
|
|
||||||
rule := rules[i]
|
|
||||||
result[i] = &proto.RouteFirewallRule{
|
|
||||||
SourceRanges: rule.SourceRanges,
|
|
||||||
Action: getProtoAction(rule.Action),
|
|
||||||
Destination: rule.Destination,
|
|
||||||
Protocol: getProtoProtocol(rule.Protocol),
|
|
||||||
PortInfo: getProtoPortInfo(rule),
|
|
||||||
IsDynamic: rule.IsDynamic,
|
|
||||||
Domains: rule.Domains.ToPunycodeList(),
|
|
||||||
PolicyID: []byte(rule.PolicyID),
|
|
||||||
RouteID: string(rule.RouteID),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// getProtoAction converts the action to proto.RuleAction.
|
|
||||||
func getProtoAction(action string) proto.RuleAction {
|
|
||||||
if action == string(types.PolicyTrafficActionDrop) {
|
|
||||||
return proto.RuleAction_DROP
|
|
||||||
}
|
|
||||||
return proto.RuleAction_ACCEPT
|
|
||||||
}
|
|
||||||
|
|
||||||
// getProtoProtocol converts the protocol to proto.RuleProtocol.
|
|
||||||
func getProtoProtocol(protocol string) proto.RuleProtocol {
|
|
||||||
switch types.PolicyRuleProtocolType(protocol) {
|
|
||||||
case types.PolicyRuleProtocolALL:
|
|
||||||
return proto.RuleProtocol_ALL
|
|
||||||
case types.PolicyRuleProtocolTCP:
|
|
||||||
return proto.RuleProtocol_TCP
|
|
||||||
case types.PolicyRuleProtocolUDP:
|
|
||||||
return proto.RuleProtocol_UDP
|
|
||||||
case types.PolicyRuleProtocolICMP:
|
|
||||||
return proto.RuleProtocol_ICMP
|
|
||||||
default:
|
|
||||||
return proto.RuleProtocol_UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getProtoPortInfo converts the port info to proto.PortInfo.
|
|
||||||
func getProtoPortInfo(rule *types.RouteFirewallRule) *proto.PortInfo {
|
|
||||||
var portInfo proto.PortInfo
|
|
||||||
if rule.Port != 0 {
|
|
||||||
portInfo.PortSelection = &proto.PortInfo_Port{Port: uint32(rule.Port)}
|
|
||||||
} else if portRange := rule.PortRange; portRange.Start != 0 && portRange.End != 0 {
|
|
||||||
portInfo.PortSelection = &proto.PortInfo_Range_{
|
|
||||||
Range: &proto.PortInfo_Range{
|
|
||||||
Start: uint32(portRange.Start),
|
|
||||||
End: uint32(portRange.End),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &portInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldUsePortRange(rule *proto.FirewallRule) bool {
|
|
||||||
return rule.Port == "" && (rule.Protocol == proto.RuleProtocol_UDP || rule.Protocol == proto.RuleProtocol_TCP)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to convert nbdns.CustomZone to proto.CustomZone
|
|
||||||
func convertToProtoCustomZone(zone nbdns.CustomZone) *proto.CustomZone {
|
|
||||||
protoZone := &proto.CustomZone{
|
|
||||||
Domain: zone.Domain,
|
|
||||||
Records: make([]*proto.SimpleRecord, 0, len(zone.Records)),
|
|
||||||
SearchDomainDisabled: zone.SearchDomainDisabled,
|
|
||||||
NonAuthoritative: zone.NonAuthoritative,
|
|
||||||
}
|
|
||||||
for _, record := range zone.Records {
|
|
||||||
protoZone.Records = append(protoZone.Records, &proto.SimpleRecord{
|
|
||||||
Name: record.Name,
|
|
||||||
Type: int64(record.Type),
|
|
||||||
Class: record.Class,
|
|
||||||
TTL: int64(record.TTL),
|
|
||||||
RData: record.RData,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return protoZone
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to convert nbdns.NameServerGroup to proto.NameServerGroup
|
|
||||||
func convertToProtoNameServerGroup(nsGroup *nbdns.NameServerGroup) *proto.NameServerGroup {
|
|
||||||
protoGroup := &proto.NameServerGroup{
|
|
||||||
Primary: nsGroup.Primary,
|
|
||||||
Domains: nsGroup.Domains,
|
|
||||||
SearchDomainsEnabled: nsGroup.SearchDomainsEnabled,
|
|
||||||
NameServers: make([]*proto.NameServer, 0, len(nsGroup.NameServers)),
|
|
||||||
}
|
|
||||||
for _, ns := range nsGroup.NameServers {
|
|
||||||
protoGroup.NameServers = append(protoGroup.NameServers, &proto.NameServer{
|
|
||||||
IP: ns.IP.String(),
|
|
||||||
Port: int64(ns.Port),
|
|
||||||
NSType: int64(ns.NSType),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return protoGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildJWTConfig constructs JWT configuration for SSH servers from management server config
|
// buildJWTConfig constructs JWT configuration for SSH servers from management server config
|
||||||
func buildJWTConfig(config *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow) *proto.JWTConfig {
|
func buildJWTConfig(config *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow) *proto.JWTConfig {
|
||||||
if config == nil || config.AuthAudience == "" {
|
if config == nil || config.AuthAudience == "" {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
||||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestToProtocolDNSConfigWithCache(t *testing.T) {
|
func TestToProtocolDNSConfigWithCache(t *testing.T) {
|
||||||
@@ -64,13 +65,13 @@ func TestToProtocolDNSConfigWithCache(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// First run with config1
|
// First run with config1
|
||||||
result1 := toProtocolDNSConfig(config1, &cache, int64(network_map.DnsForwarderPort))
|
result1 := networkmap.ToProtocolDNSConfig(config1, &cache, int64(network_map.DnsForwarderPort))
|
||||||
|
|
||||||
// Second run with config2
|
// Second run with config2
|
||||||
result2 := toProtocolDNSConfig(config2, &cache, int64(network_map.DnsForwarderPort))
|
result2 := networkmap.ToProtocolDNSConfig(config2, &cache, int64(network_map.DnsForwarderPort))
|
||||||
|
|
||||||
// Third run with config1 again
|
// Third run with config1 again
|
||||||
result3 := toProtocolDNSConfig(config1, &cache, int64(network_map.DnsForwarderPort))
|
result3 := networkmap.ToProtocolDNSConfig(config1, &cache, int64(network_map.DnsForwarderPort))
|
||||||
|
|
||||||
// Verify that result1 and result3 are identical
|
// Verify that result1 and result3 are identical
|
||||||
if !reflect.DeepEqual(result1, result3) {
|
if !reflect.DeepEqual(result1, result3) {
|
||||||
@@ -102,15 +103,14 @@ func BenchmarkToProtocolDNSConfig(b *testing.B) {
|
|||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
toProtocolDNSConfig(testData, cache, int64(network_map.DnsForwarderPort))
|
networkmap.ToProtocolDNSConfig(testData, cache, int64(network_map.DnsForwarderPort))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
b.Run(fmt.Sprintf("WithoutCache-Size%d", size), func(b *testing.B) {
|
b.Run(fmt.Sprintf("WithoutCache-Size%d", size), func(b *testing.B) {
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
cache := &cache.DNSConfigCache{}
|
networkmap.ToProtocolDNSConfig(testData, nil, int64(network_map.DnsForwarderPort))
|
||||||
toProtocolDNSConfig(testData, cache, int64(network_map.DnsForwarderPort))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/shared/management/client/common"
|
"github.com/netbirdio/netbird/shared/management/client/common"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/grpc"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||||
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||||
@@ -245,6 +246,7 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
|
|||||||
realIP := getRealIP(ctx)
|
realIP := getRealIP(ctx)
|
||||||
sRealIP := realIP.String()
|
sRealIP := realIP.String()
|
||||||
peerMeta := extractPeerMeta(ctx, syncReq.GetMeta())
|
peerMeta := extractPeerMeta(ctx, syncReq.GetMeta())
|
||||||
|
|
||||||
userID, err := s.accountManager.GetUserIDByPeerKey(ctx, peerKey.String())
|
userID, err := s.accountManager.GetUserIDByPeerKey(ctx, peerKey.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.syncSem.Add(-1)
|
s.syncSem.Add(-1)
|
||||||
@@ -683,8 +685,9 @@ func extractPeerMeta(ctx context.Context, meta *proto.PeerSystemMeta) nbpeer.Pee
|
|||||||
LazyConnectionEnabled: meta.GetFlags().GetLazyConnectionEnabled(),
|
LazyConnectionEnabled: meta.GetFlags().GetLazyConnectionEnabled(),
|
||||||
DisableIPv6: meta.GetFlags().GetDisableIPv6(),
|
DisableIPv6: meta.GetFlags().GetDisableIPv6(),
|
||||||
},
|
},
|
||||||
Files: files,
|
Files: files,
|
||||||
Capabilities: capabilitiesToInt32(meta.GetCapabilities()),
|
Capabilities: capabilitiesToInt32(meta.GetCapabilities()),
|
||||||
|
SyncMessageVersion: int(meta.GetSyncMessageVersion()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1016,7 +1019,43 @@ func (s *Server) sendInitialSync(ctx context.Context, peerKey wgtypes.Key, peer
|
|||||||
return status.Errorf(codes.Internal, "failed to get peer groups %s", err)
|
return status.Errorf(codes.Internal, "failed to get peer groups %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
plainResp := ToSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, peer, turnToken, relayToken, networkMap, s.networkMapController.GetDNSDomain(settings), postureChecks, nil, settings, settings.Extra, peerGroups, dnsFwdPort)
|
dnsName := s.networkMapController.GetDNSDomain(settings)
|
||||||
|
|
||||||
|
var plainResp *proto.SyncResponse
|
||||||
|
|
||||||
|
commonSyncMessageVersion := grpc.HighestCommonSyncMessageVersion(
|
||||||
|
s.perAccountOrGlobalSyncMessageVersions(peer.AccountID),
|
||||||
|
grpc.SyncMessageVersionFromConfig(&peer.Meta.SyncMessageVersion))
|
||||||
|
|
||||||
|
log.WithContext(ctx).
|
||||||
|
WithFields(log.Fields{
|
||||||
|
"sync_message_version": commonSyncMessageVersion,
|
||||||
|
"server_sync_message_version": s.perAccountOrGlobalSyncMessageVersions(peer.AccountID),
|
||||||
|
"peer_sync_message_version": grpc.SyncMessageVersionFromConfig(&peer.Meta.SyncMessageVersion),
|
||||||
|
}).Debug("common highest sync message version")
|
||||||
|
|
||||||
|
if commonSyncMessageVersion == grpc.ComponentNetworkMap {
|
||||||
|
// Capable peer: discard the legacy NetworkMap that SyncAndMarkPeer
|
||||||
|
// computed and recompute the raw components instead. This wastes one
|
||||||
|
// Calculate() call per initial-sync — the component-based wire
|
||||||
|
// format is what the peer actually consumes. The streaming path
|
||||||
|
// (network_map.Controller.UpdateAccountPeers) skips this duplication
|
||||||
|
// because it dispatches by capability before computing.
|
||||||
|
//
|
||||||
|
// TODO: refactor SyncPeer / SyncAndMarkPeer / their mocks + manager
|
||||||
|
// interfaces to return PeerNetworkMapResult so the initial-sync path
|
||||||
|
// stops doing duplicate work. Deferred until the client-side
|
||||||
|
// decoder lands and there's a real deployment of capability=3 peers
|
||||||
|
// worth optimizing for.
|
||||||
|
freshPeer, components, proxyPatch, freshPostureChecks, freshDnsFwdPort, err := s.networkMapController.GetValidatedPeerWithComponents(ctx, false, peer.AccountID, peer)
|
||||||
|
if err != nil {
|
||||||
|
log.WithContext(ctx).Errorf("failed to build components for peer %s on initial sync: %v", peer.ID, err)
|
||||||
|
return status.Errorf(codes.Internal, "failed to build initial sync envelope")
|
||||||
|
}
|
||||||
|
plainResp = ToComponentSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, freshPeer, turnToken, relayToken, components, proxyPatch, dnsName, freshPostureChecks, settings, settings.Extra, peerGroups, freshDnsFwdPort)
|
||||||
|
} else {
|
||||||
|
plainResp = ToSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, peer, turnToken, relayToken, networkMap, dnsName, postureChecks, nil, settings, settings.Extra, peerGroups, dnsFwdPort)
|
||||||
|
}
|
||||||
|
|
||||||
key, err := s.secretsManager.GetWGKey()
|
key, err := s.secretsManager.GetWGKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1041,6 +1080,13 @@ func (s *Server) sendInitialSync(ctx context.Context, peerKey wgtypes.Key, peer
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) perAccountOrGlobalSyncMessageVersions(accountId string) grpc.SyncMessageVersion {
|
||||||
|
if version, ok := s.config.PerAccountHighestSupportedSyncMessageVersion[accountId]; ok {
|
||||||
|
return grpc.SyncMessageVersionFromConfig(&version)
|
||||||
|
}
|
||||||
|
return grpc.SyncMessageVersionFromConfig(s.config.HighestSupportedSyncMessageVersion)
|
||||||
|
}
|
||||||
|
|
||||||
// GetDeviceAuthorizationFlow returns a device authorization flow information
|
// GetDeviceAuthorizationFlow returns a device authorization flow information
|
||||||
// This is used for initiating an Oauth 2 device authorization grant flow
|
// This is used for initiating an Oauth 2 device authorization grant flow
|
||||||
// which will be used by our clients to Login
|
// which will be used by our clients to Login
|
||||||
|
|||||||
@@ -1648,6 +1648,10 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, g := range newGroupsToCreate {
|
||||||
|
g.PublicID = xid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
if err = transaction.CreateGroups(ctx, userAuth.AccountId, newGroupsToCreate); err != nil {
|
if err = transaction.CreateGroups(ctx, userAuth.AccountId, newGroupsToCreate); err != nil {
|
||||||
return fmt.Errorf("error saving groups: %w", err)
|
return fmt.Errorf("error saving groups: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3170,6 +3170,16 @@ func TestAccount_SetJWTGroups(t *testing.T) {
|
|||||||
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, "user2")
|
user, err := manager.Store.GetUserByUserID(context.Background(), store.LockingStrengthNone, "user2")
|
||||||
assert.NoError(t, err, "unable to get user")
|
assert.NoError(t, err, "unable to get user")
|
||||||
assert.Len(t, user.AutoGroups, 1, "new group should be added")
|
assert.Len(t, user.AutoGroups, 1, "new group should be added")
|
||||||
|
|
||||||
|
var newJWTGroup *types.Group
|
||||||
|
for _, g := range groups {
|
||||||
|
if g.Name == "group3" {
|
||||||
|
newJWTGroup = g
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, newJWTGroup, "JIT-created JWT group not found")
|
||||||
|
assert.NotEqual(t, "", newJWTGroup.PublicID, "JIT-created JWT group must have a non-empty PublicID")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("remove all JWT groups when list is empty", func(t *testing.T) {
|
t.Run("remove all JWT groups when list is empty", func(t *testing.T) {
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ func (am *DefaultAccountManager) CreateGroup(ctx context.Context, accountID, use
|
|||||||
events := am.prepareGroupEvents(ctx, transaction, accountID, userID, newGroup)
|
events := am.prepareGroupEvents(ctx, transaction, accountID, userID, newGroup)
|
||||||
eventsToStore = append(eventsToStore, events...)
|
eventsToStore = append(eventsToStore, events...)
|
||||||
|
|
||||||
|
newGroup.PublicID = xid.New().String()
|
||||||
|
|
||||||
if err := transaction.CreateGroup(ctx, newGroup); err != nil {
|
if err := transaction.CreateGroup(ctx, newGroup); err != nil {
|
||||||
return status.Errorf(status.Internal, "failed to create group: %v", err)
|
return status.Errorf(status.Internal, "failed to create group: %v", err)
|
||||||
}
|
}
|
||||||
@@ -158,6 +160,8 @@ func (am *DefaultAccountManager) UpdateGroup(ctx context.Context, accountID, use
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newGroup.PublicID = oldGroup.PublicID
|
||||||
|
|
||||||
if err = transaction.UpdateGroup(ctx, newGroup); err != nil {
|
if err = transaction.UpdateGroup(ctx, newGroup); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -235,6 +239,7 @@ func (am *DefaultAccountManager) CreateGroups(ctx context.Context, accountID, us
|
|||||||
}
|
}
|
||||||
|
|
||||||
newGroup.AccountID = accountID
|
newGroup.AccountID = accountID
|
||||||
|
newGroup.PublicID = xid.New().String()
|
||||||
|
|
||||||
if err = transaction.CreateGroup(ctx, newGroup); err != nil {
|
if err = transaction.CreateGroup(ctx, newGroup); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -327,6 +332,12 @@ func (am *DefaultAccountManager) updateSingleGroup(ctx context.Context, accountI
|
|||||||
|
|
||||||
newGroup.AccountID = accountID
|
newGroup.AccountID = accountID
|
||||||
|
|
||||||
|
oldGroup, err := transaction.GetGroupByID(ctx, store.LockingStrengthNone, accountID, newGroup.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newGroup.PublicID = oldGroup.PublicID
|
||||||
|
|
||||||
if err := transaction.UpdateGroup(ctx, newGroup); err != nil {
|
if err := transaction.UpdateGroup(ctx, newGroup); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -341,7 +352,6 @@ func (am *DefaultAccountManager) updateSingleGroup(ctx context.Context, accountI
|
|||||||
|
|
||||||
events = am.prepareGroupEvents(ctx, transaction, accountID, userID, newGroup)
|
events = am.prepareGroupEvents(ctx, transaction, accountID, userID, newGroup)
|
||||||
|
|
||||||
var err error
|
|
||||||
snap, err = affectedpeers.Load(ctx, transaction, accountID, change)
|
snap, err = affectedpeers.Load(ctx, transaction, accountID, change)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/rs/xid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
@@ -635,3 +636,50 @@ func RemoveDuplicatePeerKeys(ctx context.Context, db *gorm.DB) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BackfillPublicIDs[T any](ctx context.Context, db *gorm.DB) error {
|
||||||
|
var model T
|
||||||
|
|
||||||
|
if !db.Migrator().HasTable(&model) {
|
||||||
|
log.WithContext(ctx).Debugf("Table for %T does not exist, no backfill needed", model)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt := &gorm.Statement{DB: db}
|
||||||
|
err := stmt.Parse(&model)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse model: %w", err)
|
||||||
|
}
|
||||||
|
tableName := stmt.Schema.Table
|
||||||
|
|
||||||
|
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if !tx.Migrator().HasColumn(&model, "public_id") {
|
||||||
|
log.WithContext(ctx).Infof("Column public_id does not exist in table %s, adding it", tableName)
|
||||||
|
if err := tx.Migrator().AddColumn(&model, "public_id"); err != nil {
|
||||||
|
return fmt.Errorf("add column public_id: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []map[string]any
|
||||||
|
if err := tx.Table(tableName).Select("id", "public_id").Where("public_id IS NULL").Or("public_id = ''").Find(&rows).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to find rows with empty public_id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rows) == 0 {
|
||||||
|
log.WithContext(ctx).Infof("No rows with empty public_id found in table %s, no migration needed", tableName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
if err := tx.Table(tableName).Where("id = ?", row["id"]).Update("public_id", xid.New().String()).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to update row with id %v: %w", row["id"], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithContext(ctx).Infof("Backfill of empty public_id in table %s completed", tableName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ func (am *DefaultAccountManager) CreateNameServerGroup(ctx context.Context, acco
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newNSGroup.PublicID = xid.New().String()
|
||||||
|
|
||||||
if err = transaction.SaveNameServerGroup(ctx, newNSGroup); err != nil {
|
if err = transaction.SaveNameServerGroup(ctx, newNSGroup); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -116,6 +118,8 @@ func (am *DefaultAccountManager) SaveNameServerGroup(ctx context.Context, accoun
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nsGroupToSave.PublicID = oldNSGroup.PublicID
|
||||||
|
|
||||||
if err = transaction.SaveNameServerGroup(ctx, nsGroupToSave); err != nil {
|
if err = transaction.SaveNameServerGroup(ctx, nsGroupToSave); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,9 +71,16 @@ func (m *managerImpl) CreateNetwork(ctx context.Context, userID string, network
|
|||||||
|
|
||||||
network.ID = xid.New().String()
|
network.ID = xid.New().String()
|
||||||
|
|
||||||
err = m.store.SaveNetwork(ctx, network)
|
err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||||
|
network.PublicID = xid.New().String()
|
||||||
|
|
||||||
|
if err := transaction.SaveNetwork(ctx, network); err != nil {
|
||||||
|
return fmt.Errorf("failed to save network: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to save network: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.accountManager.StoreEvent(ctx, userID, network.ID, network.AccountID, activity.NetworkCreated, network.EventMeta())
|
m.accountManager.StoreEvent(ctx, userID, network.ID, network.AccountID, activity.NetworkCreated, network.EventMeta())
|
||||||
@@ -102,14 +109,25 @@ func (m *managerImpl) UpdateNetwork(ctx context.Context, userID string, network
|
|||||||
return nil, status.NewPermissionDeniedError()
|
return nil, status.NewPermissionDeniedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = m.store.GetNetworkByID(ctx, store.LockingStrengthUpdate, network.AccountID, network.ID)
|
err = m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||||
|
existing, err := transaction.GetNetworkByID(ctx, store.LockingStrengthUpdate, network.AccountID, network.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get network: %w", err)
|
||||||
|
}
|
||||||
|
network.PublicID = existing.PublicID
|
||||||
|
|
||||||
|
if err := transaction.SaveNetwork(ctx, network); err != nil {
|
||||||
|
return fmt.Errorf("failed to save network: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get network: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.accountManager.StoreEvent(ctx, userID, network.ID, network.AccountID, activity.NetworkUpdated, network.EventMeta())
|
m.accountManager.StoreEvent(ctx, userID, network.ID, network.AccountID, activity.NetworkUpdated, network.EventMeta())
|
||||||
|
|
||||||
return network, m.store.SaveNetwork(ctx, network)
|
return network, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *managerImpl) DeleteNetwork(ctx context.Context, accountID, userID, networkID string) error {
|
func (m *managerImpl) DeleteNetwork(ctx context.Context, accountID, userID, networkID string) error {
|
||||||
|
|||||||
@@ -255,3 +255,73 @@ func Test_UpdateNetworkFailsWithPermissionDenied(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Nil(t, updatedNetwork)
|
require.Nil(t, updatedNetwork)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test_CreateNetworkAllocatesSeqID verifies that CreateNetwork sets a
|
||||||
|
// non-zero AccountSeqID on the persisted network (allocated through the
|
||||||
|
// account_seq_counters table).
|
||||||
|
func Test_CreateNetworkSetsPublicId(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
const accountID = "testAccountId"
|
||||||
|
const userID = "testAdminId"
|
||||||
|
|
||||||
|
s, cleanUp, err := store.NewTestStoreFromSQL(ctx, "../testdata/networks.sql", t.TempDir())
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(cleanUp)
|
||||||
|
|
||||||
|
am := mock_server.MockAccountManager{}
|
||||||
|
permissionsManager := permissions.NewManager(s)
|
||||||
|
groupsManager := groups.NewManagerMock()
|
||||||
|
routerManager := routers.NewManagerMock()
|
||||||
|
resourcesManager := resources.NewManager(s, permissionsManager, groupsManager, &am, nil)
|
||||||
|
manager := NewManager(s, permissionsManager, resourcesManager, routerManager, &am)
|
||||||
|
|
||||||
|
created, err := manager.CreateNetwork(ctx, userID, &types.Network{
|
||||||
|
AccountID: accountID,
|
||||||
|
Name: "seq-allocation-test",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, "", created.PublicID, "CreateNetwork must allocate a non-zero AccountSeqID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test_UpdateNetworkPreservesSeqID verifies UpdateNetwork does not reset
|
||||||
|
// AccountSeqID even when the caller passes a zero value (the shape REST
|
||||||
|
// handlers produce because the field is `json:"-"`).
|
||||||
|
func Test_UpdateNetworkPreservesPublicId(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
const accountID = "testAccountId"
|
||||||
|
const userID = "testAdminId"
|
||||||
|
|
||||||
|
s, cleanUp, err := store.NewTestStoreFromSQL(ctx, "../testdata/networks.sql", t.TempDir())
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(cleanUp)
|
||||||
|
|
||||||
|
am := mock_server.MockAccountManager{}
|
||||||
|
permissionsManager := permissions.NewManager(s)
|
||||||
|
groupsManager := groups.NewManagerMock()
|
||||||
|
routerManager := routers.NewManagerMock()
|
||||||
|
resourcesManager := resources.NewManager(s, permissionsManager, groupsManager, &am, nil)
|
||||||
|
manager := NewManager(s, permissionsManager, resourcesManager, routerManager, &am)
|
||||||
|
|
||||||
|
created, err := manager.CreateNetwork(ctx, userID, &types.Network{
|
||||||
|
AccountID: accountID,
|
||||||
|
Name: "seq-preserve-original",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
originalPublicId := created.PublicID
|
||||||
|
require.NotZero(t, originalPublicId)
|
||||||
|
|
||||||
|
update := &types.Network{
|
||||||
|
AccountID: accountID,
|
||||||
|
ID: created.ID,
|
||||||
|
Name: "seq-preserve-renamed",
|
||||||
|
}
|
||||||
|
require.Equal(t, "", update.PublicID, "incoming struct must mirror an HTTP handler shape")
|
||||||
|
|
||||||
|
_, err = manager.UpdateNetwork(ctx, userID, update)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := manager.GetNetwork(ctx, accountID, userID, created.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, originalPublicId, got.PublicID, "PublicID must survive UpdateNetwork")
|
||||||
|
require.Equal(t, "seq-preserve-renamed", got.Name)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rs/xid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||||
@@ -146,6 +147,8 @@ func (m *managerImpl) createResourceInTransaction(ctx context.Context, transacti
|
|||||||
return nil, nil, fmt.Errorf("failed to get network: %w", err)
|
return nil, nil, fmt.Errorf("failed to get network: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource.PublicID = xid.New().String()
|
||||||
|
|
||||||
if err = transaction.SaveNetworkResource(ctx, resource); err != nil {
|
if err = transaction.SaveNetworkResource(ctx, resource); err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to save network resource: %w", err)
|
return nil, nil, fmt.Errorf("failed to save network resource: %w", err)
|
||||||
}
|
}
|
||||||
@@ -245,6 +248,7 @@ func (m *managerImpl) UpdateResource(ctx context.Context, userID string, resourc
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get network resource: %w", err)
|
return fmt.Errorf("failed to get network resource: %w", err)
|
||||||
}
|
}
|
||||||
|
resource.PublicID = oldResource.PublicID
|
||||||
|
|
||||||
oldGroups, err := m.groupsManager.GetResourceGroupsInTransaction(ctx, transaction, store.LockingStrengthNone, resource.AccountID, resource.ID)
|
oldGroups, err := m.groupsManager.GetResourceGroupsInTransaction(ctx, transaction, store.LockingStrengthNone, resource.AccountID, resource.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type NetworkResource struct {
|
|||||||
ID string `gorm:"primaryKey"`
|
ID string `gorm:"primaryKey"`
|
||||||
NetworkID string `gorm:"index"`
|
NetworkID string `gorm:"index"`
|
||||||
AccountID string `gorm:"index"`
|
AccountID string `gorm:"index"`
|
||||||
|
PublicID string `json:"-"`
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
Type NetworkResourceType
|
Type NetworkResourceType
|
||||||
@@ -96,6 +97,7 @@ func (n *NetworkResource) Copy() *NetworkResource {
|
|||||||
ID: n.ID,
|
ID: n.ID,
|
||||||
AccountID: n.AccountID,
|
AccountID: n.AccountID,
|
||||||
NetworkID: n.NetworkID,
|
NetworkID: n.NetworkID,
|
||||||
|
PublicID: n.PublicID,
|
||||||
Name: n.Name,
|
Name: n.Name,
|
||||||
Description: n.Description,
|
Description: n.Description,
|
||||||
Type: n.Type,
|
Type: n.Type,
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ func (m *managerImpl) CreateRouter(ctx context.Context, userID string, router *t
|
|||||||
|
|
||||||
router.ID = xid.New().String()
|
router.ID = xid.New().String()
|
||||||
|
|
||||||
|
router.PublicID = xid.New().String()
|
||||||
|
|
||||||
err = transaction.CreateNetworkRouter(ctx, router)
|
err = transaction.CreateNetworkRouter(ctx, router)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create network router: %w", err)
|
return fmt.Errorf("failed to create network router: %w", err)
|
||||||
@@ -199,6 +201,11 @@ func (m *managerImpl) updateRouterInTransaction(ctx context.Context, transaction
|
|||||||
return nil, nil, affectedpeers.Change{}, status.NewRouterNotPartOfNetworkError(router.ID, router.NetworkID)
|
return nil, nil, affectedpeers.Change{}, status.NewRouterNotPartOfNetworkError(router.ID, router.NetworkID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preserve PublicID from the existing router so the upstream
|
||||||
|
// UpdateNetworkRouter (which does Updates(router) with Select("*"))
|
||||||
|
// doesn't clobber it with the request's zero value.
|
||||||
|
router.PublicID = existing.PublicID
|
||||||
|
|
||||||
if err = transaction.UpdateNetworkRouter(ctx, router); err != nil {
|
if err = transaction.UpdateNetworkRouter(ctx, router); err != nil {
|
||||||
return nil, nil, affectedpeers.Change{}, fmt.Errorf("failed to update network router: %w", err)
|
return nil, nil, affectedpeers.Change{}, fmt.Errorf("failed to update network router: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type NetworkRouter struct {
|
|||||||
ID string `gorm:"primaryKey"`
|
ID string `gorm:"primaryKey"`
|
||||||
NetworkID string `gorm:"index"`
|
NetworkID string `gorm:"index"`
|
||||||
AccountID string `gorm:"index"`
|
AccountID string `gorm:"index"`
|
||||||
|
PublicID string `json:"-"`
|
||||||
Peer string
|
Peer string
|
||||||
PeerGroups []string `gorm:"serializer:json"`
|
PeerGroups []string `gorm:"serializer:json"`
|
||||||
Masquerade bool
|
Masquerade bool
|
||||||
@@ -81,6 +82,7 @@ func (n *NetworkRouter) Copy() *NetworkRouter {
|
|||||||
ID: n.ID,
|
ID: n.ID,
|
||||||
NetworkID: n.NetworkID,
|
NetworkID: n.NetworkID,
|
||||||
AccountID: n.AccountID,
|
AccountID: n.AccountID,
|
||||||
|
PublicID: n.PublicID,
|
||||||
Peer: n.Peer,
|
Peer: n.Peer,
|
||||||
PeerGroups: n.PeerGroups,
|
PeerGroups: n.PeerGroups,
|
||||||
Masquerade: n.Masquerade,
|
Masquerade: n.Masquerade,
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Network struct {
|
type Network struct {
|
||||||
ID string `gorm:"primaryKey"`
|
ID string `gorm:"primaryKey"`
|
||||||
AccountID string `gorm:"index"`
|
AccountID string `gorm:"index"`
|
||||||
|
|
||||||
|
PublicID string `json:"-"`
|
||||||
|
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
}
|
}
|
||||||
@@ -41,11 +44,12 @@ func (n *Network) FromAPIRequest(req *api.NetworkRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy returns a copy of a posture checks.
|
// Copy returns a copy of a network.
|
||||||
func (n *Network) Copy() *Network {
|
func (n *Network) Copy() *Network {
|
||||||
return &Network{
|
return &Network{
|
||||||
ID: n.ID,
|
ID: n.ID,
|
||||||
AccountID: n.AccountID,
|
AccountID: n.AccountID,
|
||||||
|
PublicID: n.PublicID,
|
||||||
Name: n.Name,
|
Name: n.Name,
|
||||||
Description: n.Description,
|
Description: n.Description,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ import (
|
|||||||
|
|
||||||
// Peer capability constants mirror the proto enum values.
|
// Peer capability constants mirror the proto enum values.
|
||||||
const (
|
const (
|
||||||
PeerCapabilitySourcePrefixes int32 = 1
|
PeerCapabilitySourcePrefixes int32 = 1
|
||||||
PeerCapabilityIPv6Overlay int32 = 2
|
PeerCapabilityIPv6Overlay int32 = 2
|
||||||
|
PeerCapabilityComponentNetworkMap int32 = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// Peer represents a machine connected to the network.
|
// Peer represents a machine connected to the network.
|
||||||
@@ -172,6 +173,7 @@ type PeerSystemMeta struct { //nolint:revive
|
|||||||
Flags Flags `gorm:"serializer:json"`
|
Flags Flags `gorm:"serializer:json"`
|
||||||
Files []File `gorm:"serializer:json"`
|
Files []File `gorm:"serializer:json"`
|
||||||
Capabilities []int32 `gorm:"serializer:json"`
|
Capabilities []int32 `gorm:"serializer:json"`
|
||||||
|
SyncMessageVersion int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool {
|
func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool {
|
||||||
@@ -218,6 +220,14 @@ func (p *Peer) SupportsSourcePrefixes() bool {
|
|||||||
return p.HasCapability(PeerCapabilitySourcePrefixes)
|
return p.HasCapability(PeerCapabilitySourcePrefixes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SupportsComponentNetworkMap reports whether the peer assembles its
|
||||||
|
// NetworkMap from server-shipped components instead of consuming a fully
|
||||||
|
// expanded NetworkMap. Determines whether the network_map controller skips
|
||||||
|
// Calculate() server-side and emits the components envelope.
|
||||||
|
func (p *Peer) SupportsComponentNetworkMap() bool {
|
||||||
|
return p.HasCapability(PeerCapabilityComponentNetworkMap)
|
||||||
|
}
|
||||||
|
|
||||||
func capabilitiesEqual(a, b []int32) bool {
|
func capabilitiesEqual(a, b []int32) bool {
|
||||||
if len(a) != len(b) {
|
if len(a) != len(b) {
|
||||||
return false
|
return false
|
||||||
@@ -406,6 +416,9 @@ func diffMeta(oldMeta, newMeta PeerSystemMeta, oldLocation, newLocation Location
|
|||||||
if !sameMultiset(oldMeta.Files, newMeta.Files) {
|
if !sameMultiset(oldMeta.Files, newMeta.Files) {
|
||||||
add("files", fmt.Sprintf("%v", oldMeta.Files), fmt.Sprintf("%v", newMeta.Files))
|
add("files", fmt.Sprintf("%v", oldMeta.Files), fmt.Sprintf("%v", newMeta.Files))
|
||||||
}
|
}
|
||||||
|
if oldMeta.SyncMessageVersion != newMeta.SyncMessageVersion {
|
||||||
|
add("sync_meta_version", fmt.Sprintf("%d", oldMeta.SyncMessageVersion), fmt.Sprintf("%d", newMeta.SyncMessageVersion))
|
||||||
|
}
|
||||||
|
|
||||||
if !oldLocation.equal(newLocation) {
|
if !oldLocation.equal(newLocation) {
|
||||||
add("connection_ip", oldLocation.ConnectionIP, newLocation.ConnectionIP)
|
add("connection_ip", oldLocation.ConnectionIP, newLocation.ConnectionIP)
|
||||||
|
|||||||
@@ -67,10 +67,13 @@ func (am *DefaultAccountManager) SavePolicy(ctx context.Context, accountID, user
|
|||||||
|
|
||||||
action = activity.PolicyUpdated
|
action = activity.PolicyUpdated
|
||||||
|
|
||||||
|
policy.PublicID = existingPolicy.PublicID
|
||||||
|
|
||||||
if err = transaction.SavePolicy(ctx, policy); err != nil {
|
if err = transaction.SavePolicy(ctx, policy); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
policy.PublicID = xid.New().String()
|
||||||
if err = transaction.CreatePolicy(ctx, policy); err != nil {
|
if err = transaction.CreatePolicy(ctx, policy); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ type Checks struct {
|
|||||||
// AccountID is a reference to the Account that this object belongs
|
// AccountID is a reference to the Account that this object belongs
|
||||||
AccountID string `json:"-" gorm:"index"`
|
AccountID string `json:"-" gorm:"index"`
|
||||||
|
|
||||||
|
PublicID string `json:"-"`
|
||||||
|
|
||||||
// Checks is a set of objects that perform the actual checks
|
// Checks is a set of objects that perform the actual checks
|
||||||
Checks ChecksDefinition `gorm:"serializer:json"`
|
Checks ChecksDefinition `gorm:"serializer:json"`
|
||||||
}
|
}
|
||||||
@@ -167,6 +169,7 @@ func (pc *Checks) Copy() *Checks {
|
|||||||
Name: pc.Name,
|
Name: pc.Name,
|
||||||
Description: pc.Description,
|
Description: pc.Description,
|
||||||
AccountID: pc.AccountID,
|
AccountID: pc.AccountID,
|
||||||
|
PublicID: pc.PublicID,
|
||||||
Checks: pc.Checks.Copy(),
|
Checks: pc.Checks.Copy(),
|
||||||
}
|
}
|
||||||
return checks
|
return checks
|
||||||
|
|||||||
@@ -52,7 +52,15 @@ func (am *DefaultAccountManager) SavePostureChecks(ctx context.Context, accountI
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isUpdate {
|
if isUpdate {
|
||||||
|
existing, err := transaction.GetPostureChecksByID(ctx, store.LockingStrengthNone, accountID, postureChecks.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
postureChecks.PublicID = existing.PublicID
|
||||||
|
|
||||||
action = activity.PostureCheckUpdated
|
action = activity.PostureCheckUpdated
|
||||||
|
} else {
|
||||||
|
postureChecks.PublicID = xid.New().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
postureChecks.AccountID = accountID
|
postureChecks.AccountID = accountID
|
||||||
|
|||||||
@@ -563,3 +563,61 @@ func TestArePostureCheckChangesAffectPeers(t *testing.T) {
|
|||||||
assert.Empty(t, directPeerIDs)
|
assert.Empty(t, directPeerIDs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSavePostureChecks_AllocatesSeqIDOnCreate verifies that the create path
|
||||||
|
// (no incoming ID) allocates a non-zero AccountSeqID via the
|
||||||
|
// account_seq_counters table.
|
||||||
|
func TestSavePostureChecks_AllocatesSeqIDOnCreate(t *testing.T) {
|
||||||
|
am, _, err := createManager(t)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
account, err := initTestPostureChecksAccount(am)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
created, err := am.SavePostureChecks(context.Background(), account.Id, adminUserID, &posture.Checks{
|
||||||
|
Name: "seq-allocation-test",
|
||||||
|
Checks: posture.ChecksDefinition{
|
||||||
|
NBVersionCheck: &posture.NBVersionCheck{MinVersion: "0.26.0"},
|
||||||
|
},
|
||||||
|
}, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, "", created.PublicID, "SavePostureChecks on create must create PublicID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSavePostureChecks_PreservesSeqIDOnUpdate verifies the update path does
|
||||||
|
// not reset AccountSeqID even when the caller passes a zero value (REST
|
||||||
|
// handler shape, because the field is `json:"-"`).
|
||||||
|
func TestSavePostureChecks_PreservesSeqIDOnUpdate(t *testing.T) {
|
||||||
|
am, _, err := createManager(t)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
account, err := initTestPostureChecksAccount(am)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
created, err := am.SavePostureChecks(context.Background(), account.Id, adminUserID, &posture.Checks{
|
||||||
|
Name: "seq-preserve-original",
|
||||||
|
Checks: posture.ChecksDefinition{
|
||||||
|
NBVersionCheck: &posture.NBVersionCheck{MinVersion: "0.26.0"},
|
||||||
|
},
|
||||||
|
}, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
originalPublicID := created.PublicID
|
||||||
|
require.NotEqual(t, "", originalPublicID)
|
||||||
|
|
||||||
|
update := &posture.Checks{
|
||||||
|
ID: created.ID,
|
||||||
|
Name: "seq-preserve-renamed",
|
||||||
|
Checks: posture.ChecksDefinition{
|
||||||
|
NBVersionCheck: &posture.NBVersionCheck{MinVersion: "0.27.0"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Equal(t, "", update.PublicID, "incoming struct must mirror an HTTP handler shape")
|
||||||
|
|
||||||
|
_, err = am.SavePostureChecks(context.Background(), account.Id, adminUserID, update, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := am.GetPostureChecks(context.Background(), account.Id, created.ID, adminUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, originalPublicID, got.PublicID, "PublicID must survive SavePostureChecks update")
|
||||||
|
require.Equal(t, "seq-preserve-renamed", got.Name)
|
||||||
|
}
|
||||||
|
|||||||
@@ -175,6 +175,8 @@ func (am *DefaultAccountManager) CreateRoute(ctx context.Context, accountID stri
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newRoute.PublicID = xid.New().String()
|
||||||
|
|
||||||
if err = transaction.SaveRoute(ctx, newRoute); err != nil {
|
if err = transaction.SaveRoute(ctx, newRoute); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -222,6 +224,7 @@ func (am *DefaultAccountManager) SaveRoute(ctx context.Context, accountID, userI
|
|||||||
}
|
}
|
||||||
|
|
||||||
routeToSave.AccountID = accountID
|
routeToSave.AccountID = accountID
|
||||||
|
routeToSave.PublicID = oldRoute.PublicID
|
||||||
|
|
||||||
if err = transaction.SaveRoute(ctx, routeToSave); err != nil {
|
if err = transaction.SaveRoute(ctx, routeToSave); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -642,6 +642,22 @@ func (s *SqlStore) SaveUser(ctx context.Context, user *types.User) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateGroups creates the given list of groups to the database.
|
// CreateGroups creates the given list of groups to the database.
|
||||||
|
// groupUpsertColumns is the explicit allowlist of columns that get updated when
|
||||||
|
// CreateGroups / UpdateGroups hit a PK conflict. public_id is intentionally
|
||||||
|
// omitted so a caller passing an entity with the zero value (e.g. an HTTP
|
||||||
|
// handler-built struct) cannot reset the persisted public_id during an upsert.
|
||||||
|
// Keep this in sync with the Group schema in management/server/types/group.go.
|
||||||
|
func groupUpsertColumns() clause.Set {
|
||||||
|
return clause.AssignmentColumns([]string{
|
||||||
|
"account_id",
|
||||||
|
"name",
|
||||||
|
"issued",
|
||||||
|
"integration_ref_id",
|
||||||
|
"integration_ref_integration_type",
|
||||||
|
"resources",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SqlStore) CreateGroups(ctx context.Context, accountID string, groups []*types.Group) error {
|
func (s *SqlStore) CreateGroups(ctx context.Context, accountID string, groups []*types.Group) error {
|
||||||
if len(groups) == 0 {
|
if len(groups) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -651,8 +667,9 @@ func (s *SqlStore) CreateGroups(ctx context.Context, accountID string, groups []
|
|||||||
result := tx.
|
result := tx.
|
||||||
Clauses(
|
Clauses(
|
||||||
clause.OnConflict{
|
clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "id"}},
|
||||||
Where: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: "groups.account_id", Value: accountID}}},
|
Where: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: "groups.account_id", Value: accountID}}},
|
||||||
UpdateAll: true,
|
DoUpdates: groupUpsertColumns(),
|
||||||
},
|
},
|
||||||
).
|
).
|
||||||
Omit(clause.Associations).
|
Omit(clause.Associations).
|
||||||
@@ -676,8 +693,9 @@ func (s *SqlStore) UpdateGroups(ctx context.Context, accountID string, groups []
|
|||||||
result := tx.
|
result := tx.
|
||||||
Clauses(
|
Clauses(
|
||||||
clause.OnConflict{
|
clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "id"}},
|
||||||
Where: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: "groups.account_id", Value: accountID}}},
|
Where: clause.Where{Exprs: []clause.Expression{clause.Eq{Column: "groups.account_id", Value: accountID}}},
|
||||||
UpdateAll: true,
|
DoUpdates: groupUpsertColumns(),
|
||||||
},
|
},
|
||||||
).
|
).
|
||||||
Omit(clause.Associations).
|
Omit(clause.Associations).
|
||||||
@@ -1851,7 +1869,7 @@ func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Pee
|
|||||||
meta_kernel_version, meta_network_addresses, meta_system_serial_number, meta_system_product_name, meta_system_manufacturer,
|
meta_kernel_version, meta_network_addresses, meta_system_serial_number, meta_system_product_name, meta_system_manufacturer,
|
||||||
meta_environment, meta_flags, meta_files, meta_capabilities, peer_status_last_seen, peer_status_session_started_at,
|
meta_environment, meta_flags, meta_files, meta_capabilities, peer_status_last_seen, peer_status_session_started_at,
|
||||||
peer_status_connected, peer_status_login_expired, peer_status_requires_approval, location_connection_ip,
|
peer_status_connected, peer_status_login_expired, peer_status_requires_approval, location_connection_ip,
|
||||||
location_country_code, location_city_name, location_geo_name_id, proxy_meta_embedded, proxy_meta_cluster, ipv6
|
location_country_code, location_city_name, location_geo_name_id, proxy_meta_embedded, proxy_meta_cluster, ipv6, meta_sync_message_version
|
||||||
FROM peers WHERE account_id = $1`
|
FROM peers WHERE account_id = $1`
|
||||||
rows, err := s.pool.Query(ctx, query, accountID)
|
rows, err := s.pool.Query(ctx, query, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1873,6 +1891,7 @@ func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Pee
|
|||||||
metaSystemSerialNumber, metaSystemProductName, metaSystemManufacturer sql.NullString
|
metaSystemSerialNumber, metaSystemProductName, metaSystemManufacturer sql.NullString
|
||||||
locationCountryCode, locationCityName, proxyCluster sql.NullString
|
locationCountryCode, locationCityName, proxyCluster sql.NullString
|
||||||
locationGeoNameID sql.NullInt64
|
locationGeoNameID sql.NullInt64
|
||||||
|
metaSyncMessageVersion sql.NullInt32
|
||||||
)
|
)
|
||||||
|
|
||||||
err := row.Scan(&p.ID, &p.AccountID, &p.Key, &ip, &p.Name, &p.DNSLabel, &p.UserID, &p.SSHKey, &sshEnabled,
|
err := row.Scan(&p.ID, &p.AccountID, &p.Key, &ip, &p.Name, &p.DNSLabel, &p.UserID, &p.SSHKey, &sshEnabled,
|
||||||
@@ -1882,7 +1901,7 @@ func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Pee
|
|||||||
&metaSystemSerialNumber, &metaSystemProductName, &metaSystemManufacturer, &env, &flags, &files, &capabilities,
|
&metaSystemSerialNumber, &metaSystemProductName, &metaSystemManufacturer, &env, &flags, &files, &capabilities,
|
||||||
&peerStatusLastSeen, &peerStatusSessionStartedAt, &peerStatusConnected, &peerStatusLoginExpired,
|
&peerStatusLastSeen, &peerStatusSessionStartedAt, &peerStatusConnected, &peerStatusLoginExpired,
|
||||||
&peerStatusRequiresApproval, &connIP, &locationCountryCode, &locationCityName, &locationGeoNameID,
|
&peerStatusRequiresApproval, &connIP, &locationCountryCode, &locationCityName, &locationGeoNameID,
|
||||||
&proxyEmbedded, &proxyCluster, &ipv6)
|
&proxyEmbedded, &proxyCluster, &ipv6, &metaSyncMessageVersion)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if lastLogin.Valid {
|
if lastLogin.Valid {
|
||||||
@@ -2002,6 +2021,9 @@ func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Pee
|
|||||||
if connIP != nil {
|
if connIP != nil {
|
||||||
_ = json.Unmarshal(connIP, &p.Location.ConnectionIP)
|
_ = json.Unmarshal(connIP, &p.Location.ConnectionIP)
|
||||||
}
|
}
|
||||||
|
if metaSyncMessageVersion.Valid {
|
||||||
|
p.Meta.SyncMessageVersion = int(metaSyncMessageVersion.Int32)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return p, err
|
return p, err
|
||||||
})
|
})
|
||||||
@@ -2057,7 +2079,7 @@ func (s *SqlStore) getUsers(ctx context.Context, accountID string) ([]types.User
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqlStore) getGroups(ctx context.Context, accountID string) ([]*types.Group, error) {
|
func (s *SqlStore) getGroups(ctx context.Context, accountID string) ([]*types.Group, error) {
|
||||||
const query = `SELECT id, account_id, name, issued, resources, integration_ref_id, integration_ref_integration_type FROM groups WHERE account_id = $1`
|
const query = `SELECT id, account_id, public_id, name, issued, resources, integration_ref_id, integration_ref_integration_type FROM groups WHERE account_id = $1`
|
||||||
rows, err := s.pool.Query(ctx, query, accountID)
|
rows, err := s.pool.Query(ctx, query, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -2067,7 +2089,7 @@ func (s *SqlStore) getGroups(ctx context.Context, accountID string) ([]*types.Gr
|
|||||||
var resources []byte
|
var resources []byte
|
||||||
var refID sql.NullInt64
|
var refID sql.NullInt64
|
||||||
var refType sql.NullString
|
var refType sql.NullString
|
||||||
err := row.Scan(&g.ID, &g.AccountID, &g.Name, &g.Issued, &resources, &refID, &refType)
|
err := row.Scan(&g.ID, &g.AccountID, &g.PublicID, &g.Name, &g.Issued, &resources, &refID, &refType)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if refID.Valid {
|
if refID.Valid {
|
||||||
g.IntegrationReference.ID = int(refID.Int64)
|
g.IntegrationReference.ID = int(refID.Int64)
|
||||||
@@ -2092,7 +2114,7 @@ func (s *SqlStore) getGroups(ctx context.Context, accountID string) ([]*types.Gr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqlStore) getPolicies(ctx context.Context, accountID string) ([]*types.Policy, error) {
|
func (s *SqlStore) getPolicies(ctx context.Context, accountID string) ([]*types.Policy, error) {
|
||||||
const query = `SELECT id, account_id, name, description, enabled, source_posture_checks FROM policies WHERE account_id = $1`
|
const query = `SELECT id, account_id, public_id, name, description, enabled, source_posture_checks FROM policies WHERE account_id = $1`
|
||||||
rows, err := s.pool.Query(ctx, query, accountID)
|
rows, err := s.pool.Query(ctx, query, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -2101,7 +2123,7 @@ func (s *SqlStore) getPolicies(ctx context.Context, accountID string) ([]*types.
|
|||||||
var p types.Policy
|
var p types.Policy
|
||||||
var checks []byte
|
var checks []byte
|
||||||
var enabled sql.NullBool
|
var enabled sql.NullBool
|
||||||
err := row.Scan(&p.ID, &p.AccountID, &p.Name, &p.Description, &enabled, &checks)
|
err := row.Scan(&p.ID, &p.AccountID, &p.PublicID, &p.Name, &p.Description, &enabled, &checks)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if enabled.Valid {
|
if enabled.Valid {
|
||||||
p.Enabled = enabled.Bool
|
p.Enabled = enabled.Bool
|
||||||
@@ -2119,7 +2141,7 @@ func (s *SqlStore) getPolicies(ctx context.Context, accountID string) ([]*types.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqlStore) getRoutes(ctx context.Context, accountID string) ([]route.Route, error) {
|
func (s *SqlStore) getRoutes(ctx context.Context, accountID string) ([]route.Route, error) {
|
||||||
const query = `SELECT id, account_id, network, domains, keep_route, net_id, description, peer, peer_groups, network_type, masquerade, metric, enabled, groups, access_control_groups, skip_auto_apply FROM routes WHERE account_id = $1`
|
const query = `SELECT id, account_id, public_id, network, domains, keep_route, net_id, description, peer, peer_groups, network_type, masquerade, metric, enabled, groups, access_control_groups, skip_auto_apply FROM routes WHERE account_id = $1`
|
||||||
rows, err := s.pool.Query(ctx, query, accountID)
|
rows, err := s.pool.Query(ctx, query, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -2129,7 +2151,7 @@ func (s *SqlStore) getRoutes(ctx context.Context, accountID string) ([]route.Rou
|
|||||||
var network, domains, peerGroups, groups, accessGroups []byte
|
var network, domains, peerGroups, groups, accessGroups []byte
|
||||||
var keepRoute, masquerade, enabled, skipAutoApply sql.NullBool
|
var keepRoute, masquerade, enabled, skipAutoApply sql.NullBool
|
||||||
var metric sql.NullInt64
|
var metric sql.NullInt64
|
||||||
err := row.Scan(&r.ID, &r.AccountID, &network, &domains, &keepRoute, &r.NetID, &r.Description, &r.Peer, &peerGroups, &r.NetworkType, &masquerade, &metric, &enabled, &groups, &accessGroups, &skipAutoApply)
|
err := row.Scan(&r.ID, &r.AccountID, &r.PublicID, &network, &domains, &keepRoute, &r.NetID, &r.Description, &r.Peer, &peerGroups, &r.NetworkType, &masquerade, &metric, &enabled, &groups, &accessGroups, &skipAutoApply)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if keepRoute.Valid {
|
if keepRoute.Valid {
|
||||||
r.KeepRoute = keepRoute.Bool
|
r.KeepRoute = keepRoute.Bool
|
||||||
@@ -2171,7 +2193,7 @@ func (s *SqlStore) getRoutes(ctx context.Context, accountID string) ([]route.Rou
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqlStore) getNameServerGroups(ctx context.Context, accountID string) ([]nbdns.NameServerGroup, error) {
|
func (s *SqlStore) getNameServerGroups(ctx context.Context, accountID string) ([]nbdns.NameServerGroup, error) {
|
||||||
const query = `SELECT id, account_id, name, description, name_servers, groups, "primary", domains, enabled, search_domains_enabled FROM name_server_groups WHERE account_id = $1`
|
const query = `SELECT id, account_id, public_id, name, description, name_servers, groups, "primary", domains, enabled, search_domains_enabled FROM name_server_groups WHERE account_id = $1`
|
||||||
rows, err := s.pool.Query(ctx, query, accountID)
|
rows, err := s.pool.Query(ctx, query, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -2180,7 +2202,7 @@ func (s *SqlStore) getNameServerGroups(ctx context.Context, accountID string) ([
|
|||||||
var n nbdns.NameServerGroup
|
var n nbdns.NameServerGroup
|
||||||
var ns, groups, domains []byte
|
var ns, groups, domains []byte
|
||||||
var primary, enabled, searchDomainsEnabled sql.NullBool
|
var primary, enabled, searchDomainsEnabled sql.NullBool
|
||||||
err := row.Scan(&n.ID, &n.AccountID, &n.Name, &n.Description, &ns, &groups, &primary, &domains, &enabled, &searchDomainsEnabled)
|
err := row.Scan(&n.ID, &n.AccountID, &n.PublicID, &n.Name, &n.Description, &ns, &groups, &primary, &domains, &enabled, &searchDomainsEnabled)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if primary.Valid {
|
if primary.Valid {
|
||||||
n.Primary = primary.Bool
|
n.Primary = primary.Bool
|
||||||
@@ -2216,7 +2238,7 @@ func (s *SqlStore) getNameServerGroups(ctx context.Context, accountID string) ([
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqlStore) getPostureChecks(ctx context.Context, accountID string) ([]*posture.Checks, error) {
|
func (s *SqlStore) getPostureChecks(ctx context.Context, accountID string) ([]*posture.Checks, error) {
|
||||||
const query = `SELECT id, account_id, name, description, checks FROM posture_checks WHERE account_id = $1`
|
const query = `SELECT id, account_id, public_id, name, description, checks FROM posture_checks WHERE account_id = $1`
|
||||||
rows, err := s.pool.Query(ctx, query, accountID)
|
rows, err := s.pool.Query(ctx, query, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -2224,7 +2246,7 @@ func (s *SqlStore) getPostureChecks(ctx context.Context, accountID string) ([]*p
|
|||||||
checks, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*posture.Checks, error) {
|
checks, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*posture.Checks, error) {
|
||||||
var c posture.Checks
|
var c posture.Checks
|
||||||
var checksDef []byte
|
var checksDef []byte
|
||||||
err := row.Scan(&c.ID, &c.AccountID, &c.Name, &c.Description, &checksDef)
|
err := row.Scan(&c.ID, &c.AccountID, &c.PublicID, &c.Name, &c.Description, &checksDef)
|
||||||
if err == nil && checksDef != nil {
|
if err == nil && checksDef != nil {
|
||||||
_ = json.Unmarshal(checksDef, &c.Checks)
|
_ = json.Unmarshal(checksDef, &c.Checks)
|
||||||
}
|
}
|
||||||
@@ -2404,7 +2426,7 @@ func (s *SqlStore) getServices(ctx context.Context, accountID string) ([]*rpserv
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqlStore) getNetworks(ctx context.Context, accountID string) ([]*networkTypes.Network, error) {
|
func (s *SqlStore) getNetworks(ctx context.Context, accountID string) ([]*networkTypes.Network, error) {
|
||||||
const query = `SELECT id, account_id, name, description FROM networks WHERE account_id = $1`
|
const query = `SELECT id, account_id, public_id, name, description FROM networks WHERE account_id = $1`
|
||||||
rows, err := s.pool.Query(ctx, query, accountID)
|
rows, err := s.pool.Query(ctx, query, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -2421,7 +2443,7 @@ func (s *SqlStore) getNetworks(ctx context.Context, accountID string) ([]*networ
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqlStore) getNetworkRouters(ctx context.Context, accountID string) ([]*routerTypes.NetworkRouter, error) {
|
func (s *SqlStore) getNetworkRouters(ctx context.Context, accountID string) ([]*routerTypes.NetworkRouter, error) {
|
||||||
const query = `SELECT id, network_id, account_id, peer, peer_groups, masquerade, metric, enabled FROM network_routers WHERE account_id = $1`
|
const query = `SELECT id, network_id, account_id, public_id, peer, peer_groups, masquerade, metric, enabled FROM network_routers WHERE account_id = $1`
|
||||||
rows, err := s.pool.Query(ctx, query, accountID)
|
rows, err := s.pool.Query(ctx, query, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -2431,7 +2453,7 @@ func (s *SqlStore) getNetworkRouters(ctx context.Context, accountID string) ([]*
|
|||||||
var peerGroups []byte
|
var peerGroups []byte
|
||||||
var masquerade, enabled sql.NullBool
|
var masquerade, enabled sql.NullBool
|
||||||
var metric sql.NullInt64
|
var metric sql.NullInt64
|
||||||
err := row.Scan(&r.ID, &r.NetworkID, &r.AccountID, &r.Peer, &peerGroups, &masquerade, &metric, &enabled)
|
err := row.Scan(&r.ID, &r.NetworkID, &r.AccountID, &r.PublicID, &r.Peer, &peerGroups, &masquerade, &metric, &enabled)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if masquerade.Valid {
|
if masquerade.Valid {
|
||||||
r.Masquerade = masquerade.Bool
|
r.Masquerade = masquerade.Bool
|
||||||
@@ -2459,7 +2481,7 @@ func (s *SqlStore) getNetworkRouters(ctx context.Context, accountID string) ([]*
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqlStore) getNetworkResources(ctx context.Context, accountID string) ([]*resourceTypes.NetworkResource, error) {
|
func (s *SqlStore) getNetworkResources(ctx context.Context, accountID string) ([]*resourceTypes.NetworkResource, error) {
|
||||||
const query = `SELECT id, network_id, account_id, name, description, type, domain, prefix, enabled FROM network_resources WHERE account_id = $1`
|
const query = `SELECT id, network_id, account_id, public_id, name, description, type, domain, prefix, enabled FROM network_resources WHERE account_id = $1`
|
||||||
rows, err := s.pool.Query(ctx, query, accountID)
|
rows, err := s.pool.Query(ctx, query, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -2468,7 +2490,7 @@ func (s *SqlStore) getNetworkResources(ctx context.Context, accountID string) ([
|
|||||||
var r resourceTypes.NetworkResource
|
var r resourceTypes.NetworkResource
|
||||||
var prefix []byte
|
var prefix []byte
|
||||||
var enabled sql.NullBool
|
var enabled sql.NullBool
|
||||||
err := row.Scan(&r.ID, &r.NetworkID, &r.AccountID, &r.Name, &r.Description, &r.Type, &r.Domain, &prefix, &enabled)
|
err := row.Scan(&r.ID, &r.NetworkID, &r.AccountID, &r.PublicID, &r.Name, &r.Description, &r.Type, &r.Domain, &prefix, &enabled)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if enabled.Valid {
|
if enabled.Valid {
|
||||||
r.Enabled = enabled.Bool
|
r.Enabled = enabled.Bool
|
||||||
@@ -3830,7 +3852,7 @@ func (s *SqlStore) UpdateGroup(ctx context.Context, group *types.Group) error {
|
|||||||
return status.Errorf(status.InvalidArgument, "group is nil")
|
return status.Errorf(status.InvalidArgument, "group is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.Omit(clause.Associations).Save(group).Error; err != nil {
|
if err := s.db.Omit(clause.Associations, "public_id").Save(group).Error; err != nil {
|
||||||
log.WithContext(ctx).Errorf("failed to save group to store: %v", err)
|
log.WithContext(ctx).Errorf("failed to save group to store: %v", err)
|
||||||
return status.Errorf(status.Internal, "failed to save group to store")
|
return status.Errorf(status.Internal, "failed to save group to store")
|
||||||
}
|
}
|
||||||
@@ -3918,7 +3940,7 @@ func (s *SqlStore) CreatePolicy(ctx context.Context, policy *types.Policy) error
|
|||||||
|
|
||||||
// SavePolicy saves a policy to the database.
|
// SavePolicy saves a policy to the database.
|
||||||
func (s *SqlStore) SavePolicy(ctx context.Context, policy *types.Policy) error {
|
func (s *SqlStore) SavePolicy(ctx context.Context, policy *types.Policy) error {
|
||||||
result := s.db.Session(&gorm.Session{FullSaveAssociations: true}).Save(policy)
|
result := s.db.Session(&gorm.Session{FullSaveAssociations: true}).Omit("public_id").Save(policy)
|
||||||
if err := result.Error; err != nil {
|
if err := result.Error; err != nil {
|
||||||
log.WithContext(ctx).Errorf("failed to save policy to the store: %s", err)
|
log.WithContext(ctx).Errorf("failed to save policy to the store: %s", err)
|
||||||
return status.Errorf(status.Internal, "failed to save policy to store")
|
return status.Errorf(status.Internal, "failed to save policy to store")
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T
|
|||||||
}
|
}
|
||||||
t.Setenv("NETBIRD_STORE_ENGINE", string(engine))
|
t.Setenv("NETBIRD_STORE_ENGINE", string(engine))
|
||||||
store, cleanUp, err := NewTestStoreFromSQL(context.Background(), testDataFile, t.TempDir())
|
store, cleanUp, err := NewTestStoreFromSQL(context.Background(), testDataFile, t.TempDir())
|
||||||
|
assert.NoError(t, err, "engine: ", string(engine))
|
||||||
t.Cleanup(cleanUp)
|
t.Cleanup(cleanUp)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
t.Run(string(engine), func(t *testing.T) {
|
t.Run(string(engine), func(t *testing.T) {
|
||||||
@@ -561,53 +562,60 @@ func TestSqlStore_GetPeerByIP_NotFound(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSqlStore_SavePeer(t *testing.T) {
|
func TestSqlStore_SavePeer(t *testing.T) {
|
||||||
store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir())
|
populateFields := testing_helpers.NewPopulateFields()
|
||||||
t.Cleanup(cleanUp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b")
|
runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) {
|
||||||
require.NoError(t, err)
|
account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
// save status of non-existing peer
|
metadata := nbpeer.PeerSystemMeta{}
|
||||||
peer := &nbpeer.Peer{
|
reflectedMetadata := reflect.ValueOf(&metadata).Elem()
|
||||||
Key: "peerkey",
|
|
||||||
ID: "testpeer",
|
|
||||||
IP: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
|
||||||
IPv6: netip.MustParseAddr("fd00::1"),
|
|
||||||
Meta: nbpeer.PeerSystemMeta{Hostname: "testingpeer"},
|
|
||||||
Name: "peer name",
|
|
||||||
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
|
|
||||||
CreatedAt: time.Now().UTC(),
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
|
||||||
err = store.SavePeer(ctx, account.Id, peer)
|
|
||||||
assert.Error(t, err)
|
|
||||||
parsedErr, ok := status.FromError(err)
|
|
||||||
require.True(t, ok)
|
|
||||||
require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
|
|
||||||
|
|
||||||
// save new status of existing peer
|
numOfFields, err := populateFields.PopulateAll(reflectedMetadata)
|
||||||
account.Peers[peer.ID] = peer
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 32, numOfFields)
|
||||||
|
|
||||||
err = store.SaveAccount(context.Background(), account)
|
// save status of non-existing peer
|
||||||
require.NoError(t, err)
|
peer := &nbpeer.Peer{
|
||||||
|
Key: "peerkey",
|
||||||
|
ID: "testpeer",
|
||||||
|
IP: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||||
|
IPv6: netip.MustParseAddr("fd00::1"),
|
||||||
|
Meta: metadata, //nbpeer.PeerSystemMeta{Hostname: "testingpeer"},
|
||||||
|
Name: "peer name",
|
||||||
|
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()},
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
err = store.SavePeer(ctx, account.Id, peer)
|
||||||
|
assert.Error(t, err)
|
||||||
|
parsedErr, ok := status.FromError(err)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error")
|
||||||
|
|
||||||
updatedPeer := peer.Copy()
|
// save new status of existing peer
|
||||||
updatedPeer.Status.Connected = false
|
account.Peers[peer.ID] = peer
|
||||||
updatedPeer.Meta.Hostname = "updatedpeer"
|
|
||||||
|
|
||||||
err = store.SavePeer(ctx, account.Id, updatedPeer)
|
err = store.SaveAccount(context.Background(), account)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
account, err = store.GetAccount(context.Background(), account.Id)
|
updatedPeer := peer.Copy()
|
||||||
require.NoError(t, err)
|
updatedPeer.Status.Connected = false
|
||||||
|
updatedPeer.Meta.Hostname = "updatedpeer"
|
||||||
|
|
||||||
actual := account.Peers[peer.ID]
|
err = store.SavePeer(ctx, account.Id, updatedPeer)
|
||||||
assert.Equal(t, updatedPeer.Meta, actual.Meta)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, updatedPeer.Status.Connected, actual.Status.Connected)
|
|
||||||
assert.Equal(t, updatedPeer.Status.LoginExpired, actual.Status.LoginExpired)
|
account, err = store.GetAccount(context.Background(), account.Id)
|
||||||
assert.Equal(t, updatedPeer.Status.RequiresApproval, actual.Status.RequiresApproval)
|
require.NoError(t, err)
|
||||||
assert.WithinDurationf(t, updatedPeer.Status.LastSeen, actual.Status.LastSeen.UTC(), time.Millisecond, "LastSeen should be equal")
|
|
||||||
|
actual := account.Peers[peer.ID]
|
||||||
|
assert.Equal(t, updatedPeer.Meta, actual.Meta)
|
||||||
|
assert.Equal(t, updatedPeer.Status.Connected, actual.Status.Connected)
|
||||||
|
assert.Equal(t, updatedPeer.Status.LoginExpired, actual.Status.LoginExpired)
|
||||||
|
assert.Equal(t, updatedPeer.Status.RequiresApproval, actual.Status.RequiresApproval)
|
||||||
|
assert.WithinDurationf(t, updatedPeer.Status.LastSeen, actual.Status.LastSeen.UTC(), time.Millisecond, "LastSeen should be equal")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSqlStore_SavePeerStatus(t *testing.T) {
|
func TestSqlStore_SavePeerStatus(t *testing.T) {
|
||||||
|
|||||||
@@ -582,6 +582,30 @@ func getMigrationsPreAuto(ctx context.Context) []migrationFunc {
|
|||||||
func(db *gorm.DB) error {
|
func(db *gorm.DB) error {
|
||||||
return migration.CleanupOrphanedResources[domain.Domain, types.Account](ctx, db, "account_id")
|
return migration.CleanupOrphanedResources[domain.Domain, types.Account](ctx, db, "account_id")
|
||||||
},
|
},
|
||||||
|
func(db *gorm.DB) error {
|
||||||
|
return migration.BackfillPublicIDs[types.Policy](ctx, db)
|
||||||
|
},
|
||||||
|
func(db *gorm.DB) error {
|
||||||
|
return migration.BackfillPublicIDs[types.Group](ctx, db)
|
||||||
|
},
|
||||||
|
func(db *gorm.DB) error {
|
||||||
|
return migration.BackfillPublicIDs[route.Route](ctx, db)
|
||||||
|
},
|
||||||
|
func(db *gorm.DB) error {
|
||||||
|
return migration.BackfillPublicIDs[resourceTypes.NetworkResource](ctx, db)
|
||||||
|
},
|
||||||
|
func(db *gorm.DB) error {
|
||||||
|
return migration.BackfillPublicIDs[routerTypes.NetworkRouter](ctx, db)
|
||||||
|
},
|
||||||
|
func(db *gorm.DB) error {
|
||||||
|
return migration.BackfillPublicIDs[dns.NameServerGroup](ctx, db)
|
||||||
|
},
|
||||||
|
func(db *gorm.DB) error {
|
||||||
|
return migration.BackfillPublicIDs[networkTypes.Network](ctx, db)
|
||||||
|
},
|
||||||
|
func(db *gorm.DB) error {
|
||||||
|
return migration.BackfillPublicIDs[posture.Checks](ctx, db)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,495 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
context "context"
|
|
||||||
reflect "reflect"
|
|
||||||
time "time"
|
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
|
|
||||||
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetAllAgentNetworkProviders mocks base method.
|
|
||||||
func (m *MockStore) GetAllAgentNetworkProviders(ctx context.Context, lockStrength LockingStrength) ([]*agentNetworkTypes.Provider, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAllAgentNetworkProviders", ctx, lockStrength)
|
|
||||||
ret0, _ := ret[0].([]*agentNetworkTypes.Provider)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllAgentNetworkProviders indicates an expected call of GetAllAgentNetworkProviders.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAllAgentNetworkProviders(ctx, lockStrength interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllAgentNetworkProviders", reflect.TypeOf((*MockStore)(nil).GetAllAgentNetworkProviders), ctx, lockStrength)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkMetrics mocks base method.
|
|
||||||
func (m *MockStore) GetAgentNetworkMetrics(ctx context.Context) (AgentNetworkMetrics, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAgentNetworkMetrics", ctx)
|
|
||||||
ret0, _ := ret[0].(AgentNetworkMetrics)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkMetrics indicates an expected call of GetAgentNetworkMetrics.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAgentNetworkMetrics(ctx interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentNetworkMetrics", reflect.TypeOf((*MockStore)(nil).GetAgentNetworkMetrics), ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAccountAgentNetworkProviders mocks base method.
|
|
||||||
func (m *MockStore) GetAccountAgentNetworkProviders(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*agentNetworkTypes.Provider, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAccountAgentNetworkProviders", ctx, lockStrength, accountID)
|
|
||||||
ret0, _ := ret[0].([]*agentNetworkTypes.Provider)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAccountAgentNetworkProviders indicates an expected call of GetAccountAgentNetworkProviders.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAccountAgentNetworkProviders(ctx, lockStrength, accountID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountAgentNetworkProviders", reflect.TypeOf((*MockStore)(nil).GetAccountAgentNetworkProviders), ctx, lockStrength, accountID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkProviderByID mocks base method.
|
|
||||||
func (m *MockStore) GetAgentNetworkProviderByID(ctx context.Context, lockStrength LockingStrength, accountID, providerID string) (*agentNetworkTypes.Provider, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAgentNetworkProviderByID", ctx, lockStrength, accountID, providerID)
|
|
||||||
ret0, _ := ret[0].(*agentNetworkTypes.Provider)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkProviderByID indicates an expected call of GetAgentNetworkProviderByID.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAgentNetworkProviderByID(ctx, lockStrength, accountID, providerID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentNetworkProviderByID", reflect.TypeOf((*MockStore)(nil).GetAgentNetworkProviderByID), ctx, lockStrength, accountID, providerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAgentNetworkProvider mocks base method.
|
|
||||||
func (m *MockStore) SaveAgentNetworkProvider(ctx context.Context, provider *agentNetworkTypes.Provider) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "SaveAgentNetworkProvider", ctx, provider)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAgentNetworkProvider indicates an expected call of SaveAgentNetworkProvider.
|
|
||||||
func (mr *MockStoreMockRecorder) SaveAgentNetworkProvider(ctx, provider interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAgentNetworkProvider", reflect.TypeOf((*MockStore)(nil).SaveAgentNetworkProvider), ctx, provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAgentNetworkProvider mocks base method.
|
|
||||||
func (m *MockStore) DeleteAgentNetworkProvider(ctx context.Context, accountID, providerID string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "DeleteAgentNetworkProvider", ctx, accountID, providerID)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAgentNetworkProvider indicates an expected call of DeleteAgentNetworkProvider.
|
|
||||||
func (mr *MockStoreMockRecorder) DeleteAgentNetworkProvider(ctx, accountID, providerID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAgentNetworkProvider", reflect.TypeOf((*MockStore)(nil).DeleteAgentNetworkProvider), ctx, accountID, providerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAccountAgentNetworkPolicies mocks base method.
|
|
||||||
func (m *MockStore) GetAccountAgentNetworkPolicies(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*agentNetworkTypes.Policy, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAccountAgentNetworkPolicies", ctx, lockStrength, accountID)
|
|
||||||
ret0, _ := ret[0].([]*agentNetworkTypes.Policy)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAccountAgentNetworkPolicies indicates an expected call of GetAccountAgentNetworkPolicies.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAccountAgentNetworkPolicies(ctx, lockStrength, accountID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountAgentNetworkPolicies", reflect.TypeOf((*MockStore)(nil).GetAccountAgentNetworkPolicies), ctx, lockStrength, accountID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkPolicyByID mocks base method.
|
|
||||||
func (m *MockStore) GetAgentNetworkPolicyByID(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) (*agentNetworkTypes.Policy, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAgentNetworkPolicyByID", ctx, lockStrength, accountID, policyID)
|
|
||||||
ret0, _ := ret[0].(*agentNetworkTypes.Policy)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkPolicyByID indicates an expected call of GetAgentNetworkPolicyByID.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAgentNetworkPolicyByID(ctx, lockStrength, accountID, policyID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentNetworkPolicyByID", reflect.TypeOf((*MockStore)(nil).GetAgentNetworkPolicyByID), ctx, lockStrength, accountID, policyID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAgentNetworkPolicy mocks base method.
|
|
||||||
func (m *MockStore) SaveAgentNetworkPolicy(ctx context.Context, policy *agentNetworkTypes.Policy) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "SaveAgentNetworkPolicy", ctx, policy)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAgentNetworkPolicy indicates an expected call of SaveAgentNetworkPolicy.
|
|
||||||
func (mr *MockStoreMockRecorder) SaveAgentNetworkPolicy(ctx, policy interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAgentNetworkPolicy", reflect.TypeOf((*MockStore)(nil).SaveAgentNetworkPolicy), ctx, policy)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAgentNetworkPolicy mocks base method.
|
|
||||||
func (m *MockStore) DeleteAgentNetworkPolicy(ctx context.Context, accountID, policyID string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "DeleteAgentNetworkPolicy", ctx, accountID, policyID)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAgentNetworkPolicy indicates an expected call of DeleteAgentNetworkPolicy.
|
|
||||||
func (mr *MockStoreMockRecorder) DeleteAgentNetworkPolicy(ctx, accountID, policyID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAgentNetworkPolicy", reflect.TypeOf((*MockStore)(nil).DeleteAgentNetworkPolicy), ctx, accountID, policyID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAccountAgentNetworkGuardrails mocks base method.
|
|
||||||
func (m *MockStore) GetAccountAgentNetworkGuardrails(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*agentNetworkTypes.Guardrail, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAccountAgentNetworkGuardrails", ctx, lockStrength, accountID)
|
|
||||||
ret0, _ := ret[0].([]*agentNetworkTypes.Guardrail)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAccountAgentNetworkGuardrails indicates an expected call of GetAccountAgentNetworkGuardrails.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAccountAgentNetworkGuardrails(ctx, lockStrength, accountID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountAgentNetworkGuardrails", reflect.TypeOf((*MockStore)(nil).GetAccountAgentNetworkGuardrails), ctx, lockStrength, accountID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkGuardrailByID mocks base method.
|
|
||||||
func (m *MockStore) GetAgentNetworkGuardrailByID(ctx context.Context, lockStrength LockingStrength, accountID, guardrailID string) (*agentNetworkTypes.Guardrail, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAgentNetworkGuardrailByID", ctx, lockStrength, accountID, guardrailID)
|
|
||||||
ret0, _ := ret[0].(*agentNetworkTypes.Guardrail)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkGuardrailByID indicates an expected call of GetAgentNetworkGuardrailByID.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAgentNetworkGuardrailByID(ctx, lockStrength, accountID, guardrailID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentNetworkGuardrailByID", reflect.TypeOf((*MockStore)(nil).GetAgentNetworkGuardrailByID), ctx, lockStrength, accountID, guardrailID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAgentNetworkGuardrail mocks base method.
|
|
||||||
func (m *MockStore) SaveAgentNetworkGuardrail(ctx context.Context, guardrail *agentNetworkTypes.Guardrail) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "SaveAgentNetworkGuardrail", ctx, guardrail)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAgentNetworkGuardrail indicates an expected call of SaveAgentNetworkGuardrail.
|
|
||||||
func (mr *MockStoreMockRecorder) SaveAgentNetworkGuardrail(ctx, guardrail interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAgentNetworkGuardrail", reflect.TypeOf((*MockStore)(nil).SaveAgentNetworkGuardrail), ctx, guardrail)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAgentNetworkGuardrail mocks base method.
|
|
||||||
func (m *MockStore) DeleteAgentNetworkGuardrail(ctx context.Context, accountID, guardrailID string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "DeleteAgentNetworkGuardrail", ctx, accountID, guardrailID)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAgentNetworkGuardrail indicates an expected call of DeleteAgentNetworkGuardrail.
|
|
||||||
func (mr *MockStoreMockRecorder) DeleteAgentNetworkGuardrail(ctx, accountID, guardrailID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAgentNetworkGuardrail", reflect.TypeOf((*MockStore)(nil).DeleteAgentNetworkGuardrail), ctx, accountID, guardrailID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkSettings mocks base method.
|
|
||||||
func (m *MockStore) GetAgentNetworkSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*agentNetworkTypes.Settings, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAgentNetworkSettings", ctx, lockStrength, accountID)
|
|
||||||
ret0, _ := ret[0].(*agentNetworkTypes.Settings)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkSettings indicates an expected call of GetAgentNetworkSettings.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAgentNetworkSettings(ctx, lockStrength, accountID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentNetworkSettings", reflect.TypeOf((*MockStore)(nil).GetAgentNetworkSettings), ctx, lockStrength, accountID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkSettingsByCluster mocks base method.
|
|
||||||
func (m *MockStore) GetAgentNetworkSettingsByCluster(ctx context.Context, lockStrength LockingStrength, cluster string) ([]*agentNetworkTypes.Settings, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAgentNetworkSettingsByCluster", ctx, lockStrength, cluster)
|
|
||||||
ret0, _ := ret[0].([]*agentNetworkTypes.Settings)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkSettingsByCluster indicates an expected call of GetAgentNetworkSettingsByCluster.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAgentNetworkSettingsByCluster(ctx, lockStrength, cluster interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentNetworkSettingsByCluster", reflect.TypeOf((*MockStore)(nil).GetAgentNetworkSettingsByCluster), ctx, lockStrength, cluster)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAgentNetworkSettings mocks base method.
|
|
||||||
func (m *MockStore) SaveAgentNetworkSettings(ctx context.Context, settings *agentNetworkTypes.Settings) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "SaveAgentNetworkSettings", ctx, settings)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAgentNetworkSettings indicates an expected call of SaveAgentNetworkSettings.
|
|
||||||
func (mr *MockStoreMockRecorder) SaveAgentNetworkSettings(ctx, settings interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAgentNetworkSettings", reflect.TypeOf((*MockStore)(nil).SaveAgentNetworkSettings), ctx, settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncrementAgentNetworkConsumption mocks base method.
|
|
||||||
func (m *MockStore) IncrementAgentNetworkConsumption(ctx context.Context, accountID string, kind agentNetworkTypes.ConsumptionDimension, dimID string, windowSeconds int64, windowStart time.Time, tokensIn, tokensOut int64, costUSD float64) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "IncrementAgentNetworkConsumption", ctx, accountID, kind, dimID, windowSeconds, windowStart, tokensIn, tokensOut, costUSD)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncrementAgentNetworkConsumption indicates an expected call of IncrementAgentNetworkConsumption.
|
|
||||||
func (mr *MockStoreMockRecorder) IncrementAgentNetworkConsumption(ctx, accountID, kind, dimID, windowSeconds, windowStart, tokensIn, tokensOut, costUSD interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementAgentNetworkConsumption", reflect.TypeOf((*MockStore)(nil).IncrementAgentNetworkConsumption), ctx, accountID, kind, dimID, windowSeconds, windowStart, tokensIn, tokensOut, costUSD)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkConsumption mocks base method.
|
|
||||||
func (m *MockStore) GetAgentNetworkConsumption(ctx context.Context, lockStrength LockingStrength, accountID string, kind agentNetworkTypes.ConsumptionDimension, dimID string, windowSeconds int64, windowStart time.Time) (*agentNetworkTypes.Consumption, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAgentNetworkConsumption", ctx, lockStrength, accountID, kind, dimID, windowSeconds, windowStart)
|
|
||||||
ret0, _ := ret[0].(*agentNetworkTypes.Consumption)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkConsumption indicates an expected call of GetAgentNetworkConsumption.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAgentNetworkConsumption(ctx, lockStrength, accountID, kind, dimID, windowSeconds, windowStart interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentNetworkConsumption", reflect.TypeOf((*MockStore)(nil).GetAgentNetworkConsumption), ctx, lockStrength, accountID, kind, dimID, windowSeconds, windowStart)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkConsumptionBatch mocks base method.
|
|
||||||
func (m *MockStore) GetAgentNetworkConsumptionBatch(ctx context.Context, lockStrength LockingStrength, accountID string, keys []agentNetworkTypes.ConsumptionKey) (map[agentNetworkTypes.ConsumptionKey]*agentNetworkTypes.Consumption, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAgentNetworkConsumptionBatch", ctx, lockStrength, accountID, keys)
|
|
||||||
ret0, _ := ret[0].(map[agentNetworkTypes.ConsumptionKey]*agentNetworkTypes.Consumption)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkConsumptionBatch indicates an expected call of GetAgentNetworkConsumptionBatch.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAgentNetworkConsumptionBatch(ctx, lockStrength, accountID, keys interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentNetworkConsumptionBatch", reflect.TypeOf((*MockStore)(nil).GetAgentNetworkConsumptionBatch), ctx, lockStrength, accountID, keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncrementAgentNetworkConsumptionBatch mocks base method.
|
|
||||||
func (m *MockStore) IncrementAgentNetworkConsumptionBatch(ctx context.Context, accountID string, keys []agentNetworkTypes.ConsumptionKey, tokensIn, tokensOut int64, costUSD float64) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "IncrementAgentNetworkConsumptionBatch", ctx, accountID, keys, tokensIn, tokensOut, costUSD)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncrementAgentNetworkConsumptionBatch indicates an expected call of IncrementAgentNetworkConsumptionBatch.
|
|
||||||
func (mr *MockStoreMockRecorder) IncrementAgentNetworkConsumptionBatch(ctx, accountID, keys, tokensIn, tokensOut, costUSD interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementAgentNetworkConsumptionBatch", reflect.TypeOf((*MockStore)(nil).IncrementAgentNetworkConsumptionBatch), ctx, accountID, keys, tokensIn, tokensOut, costUSD)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAgentNetworkConsumption mocks base method.
|
|
||||||
func (m *MockStore) ListAgentNetworkConsumption(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*agentNetworkTypes.Consumption, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ListAgentNetworkConsumption", ctx, lockStrength, accountID)
|
|
||||||
ret0, _ := ret[0].([]*agentNetworkTypes.Consumption)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAgentNetworkConsumption indicates an expected call of ListAgentNetworkConsumption.
|
|
||||||
func (mr *MockStoreMockRecorder) ListAgentNetworkConsumption(ctx, lockStrength, accountID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAgentNetworkConsumption", reflect.TypeOf((*MockStore)(nil).ListAgentNetworkConsumption), ctx, lockStrength, accountID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAccountAgentNetworkBudgetRules mocks base method.
|
|
||||||
func (m *MockStore) GetAccountAgentNetworkBudgetRules(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*agentNetworkTypes.AccountBudgetRule, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAccountAgentNetworkBudgetRules", ctx, lockStrength, accountID)
|
|
||||||
ret0, _ := ret[0].([]*agentNetworkTypes.AccountBudgetRule)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAccountAgentNetworkBudgetRules indicates an expected call of GetAccountAgentNetworkBudgetRules.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAccountAgentNetworkBudgetRules(ctx, lockStrength, accountID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountAgentNetworkBudgetRules", reflect.TypeOf((*MockStore)(nil).GetAccountAgentNetworkBudgetRules), ctx, lockStrength, accountID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkBudgetRuleByID mocks base method.
|
|
||||||
func (m *MockStore) GetAgentNetworkBudgetRuleByID(ctx context.Context, lockStrength LockingStrength, accountID, ruleID string) (*agentNetworkTypes.AccountBudgetRule, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAgentNetworkBudgetRuleByID", ctx, lockStrength, accountID, ruleID)
|
|
||||||
ret0, _ := ret[0].(*agentNetworkTypes.AccountBudgetRule)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkBudgetRuleByID indicates an expected call of GetAgentNetworkBudgetRuleByID.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAgentNetworkBudgetRuleByID(ctx, lockStrength, accountID, ruleID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentNetworkBudgetRuleByID", reflect.TypeOf((*MockStore)(nil).GetAgentNetworkBudgetRuleByID), ctx, lockStrength, accountID, ruleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAgentNetworkBudgetRule mocks base method.
|
|
||||||
func (m *MockStore) SaveAgentNetworkBudgetRule(ctx context.Context, rule *agentNetworkTypes.AccountBudgetRule) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "SaveAgentNetworkBudgetRule", ctx, rule)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAgentNetworkBudgetRule indicates an expected call of SaveAgentNetworkBudgetRule.
|
|
||||||
func (mr *MockStoreMockRecorder) SaveAgentNetworkBudgetRule(ctx, rule interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAgentNetworkBudgetRule", reflect.TypeOf((*MockStore)(nil).SaveAgentNetworkBudgetRule), ctx, rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAgentNetworkBudgetRule mocks base method.
|
|
||||||
func (m *MockStore) DeleteAgentNetworkBudgetRule(ctx context.Context, accountID, ruleID string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "DeleteAgentNetworkBudgetRule", ctx, accountID, ruleID)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAgentNetworkBudgetRule indicates an expected call of DeleteAgentNetworkBudgetRule.
|
|
||||||
func (mr *MockStoreMockRecorder) DeleteAgentNetworkBudgetRule(ctx, accountID, ruleID interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAgentNetworkBudgetRule", reflect.TypeOf((*MockStore)(nil).DeleteAgentNetworkBudgetRule), ctx, accountID, ruleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAgentNetworkAccessLog mocks base method.
|
|
||||||
func (m *MockStore) CreateAgentNetworkAccessLog(ctx context.Context, entry *agentNetworkTypes.AgentNetworkAccessLog, groups []agentNetworkTypes.AgentNetworkAccessLogGroup) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "CreateAgentNetworkAccessLog", ctx, entry, groups)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAgentNetworkAccessLog indicates an expected call of CreateAgentNetworkAccessLog.
|
|
||||||
func (mr *MockStoreMockRecorder) CreateAgentNetworkAccessLog(ctx, entry, groups interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAgentNetworkAccessLog", reflect.TypeOf((*MockStore)(nil).CreateAgentNetworkAccessLog), ctx, entry, groups)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAgentNetworkUsage mocks base method.
|
|
||||||
func (m *MockStore) CreateAgentNetworkUsage(ctx context.Context, usage *agentNetworkTypes.AgentNetworkUsage, groups []agentNetworkTypes.AgentNetworkUsageGroup) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "CreateAgentNetworkUsage", ctx, usage, groups)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAgentNetworkUsage indicates an expected call of CreateAgentNetworkUsage.
|
|
||||||
func (mr *MockStoreMockRecorder) CreateAgentNetworkUsage(ctx, usage, groups interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAgentNetworkUsage", reflect.TypeOf((*MockStore)(nil).CreateAgentNetworkUsage), ctx, usage, groups)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkAccessLogs mocks base method.
|
|
||||||
func (m *MockStore) GetAgentNetworkAccessLogs(ctx context.Context, lockStrength LockingStrength, accountID string, filter agentNetworkTypes.AgentNetworkAccessLogFilter) ([]*agentNetworkTypes.AgentNetworkAccessLog, int64, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAgentNetworkAccessLogs", ctx, lockStrength, accountID, filter)
|
|
||||||
ret0, _ := ret[0].([]*agentNetworkTypes.AgentNetworkAccessLog)
|
|
||||||
ret1, _ := ret[1].(int64)
|
|
||||||
ret2, _ := ret[2].(error)
|
|
||||||
return ret0, ret1, ret2
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkAccessLogs indicates an expected call of GetAgentNetworkAccessLogs.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAgentNetworkAccessLogs(ctx, lockStrength, accountID, filter interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentNetworkAccessLogs", reflect.TypeOf((*MockStore)(nil).GetAgentNetworkAccessLogs), ctx, lockStrength, accountID, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkAccessLogSessions mocks base method.
|
|
||||||
func (m *MockStore) GetAgentNetworkAccessLogSessions(ctx context.Context, lockStrength LockingStrength, accountID string, filter agentNetworkTypes.AgentNetworkAccessLogFilter) ([]*agentNetworkTypes.AgentNetworkAccessLogSession, int64, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAgentNetworkAccessLogSessions", ctx, lockStrength, accountID, filter)
|
|
||||||
ret0, _ := ret[0].([]*agentNetworkTypes.AgentNetworkAccessLogSession)
|
|
||||||
ret1, _ := ret[1].(int64)
|
|
||||||
ret2, _ := ret[2].(error)
|
|
||||||
return ret0, ret1, ret2
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkAccessLogSessions indicates an expected call of GetAgentNetworkAccessLogSessions.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAgentNetworkAccessLogSessions(ctx, lockStrength, accountID, filter interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentNetworkAccessLogSessions", reflect.TypeOf((*MockStore)(nil).GetAgentNetworkAccessLogSessions), ctx, lockStrength, accountID, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkUsageRows mocks base method.
|
|
||||||
func (m *MockStore) GetAgentNetworkUsageRows(ctx context.Context, lockStrength LockingStrength, accountID string, filter agentNetworkTypes.AgentNetworkAccessLogFilter) ([]*agentNetworkTypes.AgentNetworkUsage, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAgentNetworkUsageRows", ctx, lockStrength, accountID, filter)
|
|
||||||
ret0, _ := ret[0].([]*agentNetworkTypes.AgentNetworkUsage)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAgentNetworkUsageRows indicates an expected call of GetAgentNetworkUsageRows.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAgentNetworkUsageRows(ctx, lockStrength, accountID, filter interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentNetworkUsageRows", reflect.TypeOf((*MockStore)(nil).GetAgentNetworkUsageRows), ctx, lockStrength, accountID, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteOldAgentNetworkAccessLogs mocks base method.
|
|
||||||
func (m *MockStore) DeleteOldAgentNetworkAccessLogs(ctx context.Context, accountID string, olderThan time.Time) (int64, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "DeleteOldAgentNetworkAccessLogs", ctx, accountID, olderThan)
|
|
||||||
ret0, _ := ret[0].(int64)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteOldAgentNetworkAccessLogs indicates an expected call of DeleteOldAgentNetworkAccessLogs.
|
|
||||||
func (mr *MockStoreMockRecorder) DeleteOldAgentNetworkAccessLogs(ctx, accountID, olderThan interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldAgentNetworkAccessLogs", reflect.TypeOf((*MockStore)(nil).DeleteOldAgentNetworkAccessLogs), ctx, accountID, olderThan)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllAgentNetworkSettings mocks base method.
|
|
||||||
func (m *MockStore) GetAllAgentNetworkSettings(ctx context.Context, lockStrength LockingStrength) ([]*agentNetworkTypes.Settings, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetAllAgentNetworkSettings", ctx, lockStrength)
|
|
||||||
ret0, _ := ret[0].([]*agentNetworkTypes.Settings)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllAgentNetworkSettings indicates an expected call of GetAllAgentNetworkSettings.
|
|
||||||
func (mr *MockStoreMockRecorder) GetAllAgentNetworkSettings(ctx, lockStrength interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllAgentNetworkSettings", reflect.TypeOf((*MockStore)(nil).GetAllAgentNetworkSettings), ctx, lockStrength)
|
|
||||||
}
|
|
||||||
@@ -10,19 +10,20 @@ import (
|
|||||||
|
|
||||||
// UpdateChannelMetrics represents all metrics related to the UpdateChannel
|
// UpdateChannelMetrics represents all metrics related to the UpdateChannel
|
||||||
type UpdateChannelMetrics struct {
|
type UpdateChannelMetrics struct {
|
||||||
createChannelDurationMicro metric.Int64Histogram
|
createChannelDurationMicro metric.Int64Histogram
|
||||||
closeChannelDurationMicro metric.Int64Histogram
|
closeChannelDurationMicro metric.Int64Histogram
|
||||||
closeChannelsDurationMicro metric.Int64Histogram
|
closeChannelsDurationMicro metric.Int64Histogram
|
||||||
closeChannels metric.Int64Histogram
|
closeChannels metric.Int64Histogram
|
||||||
sendUpdateDurationMicro metric.Int64Histogram
|
sendUpdateDurationMicro metric.Int64Histogram
|
||||||
getAllConnectedPeersDurationMicro metric.Int64Histogram
|
getAllConnectedPeersDurationMicro metric.Int64Histogram
|
||||||
getAllConnectedPeers metric.Int64Histogram
|
getAllConnectedPeers metric.Int64Histogram
|
||||||
hasChannelDurationMicro metric.Int64Histogram
|
hasChannelDurationMicro metric.Int64Histogram
|
||||||
calcPostureChecksDurationMicro metric.Int64Histogram
|
calcPostureChecksDurationMicro metric.Int64Histogram
|
||||||
calcPeerNetworkMapDurationMs metric.Int64Histogram
|
calcPeerNetworkMapDurationMs metric.Int64Histogram
|
||||||
mergeNetworkMapDurationMicro metric.Int64Histogram
|
mergeNetworkMapDurationMicro metric.Int64Histogram
|
||||||
toSyncResponseDurationMicro metric.Int64Histogram
|
toSyncResponseDurationMicro metric.Int64Histogram
|
||||||
ctx context.Context
|
toComponentSyncResponseDurationMicro metric.Int64Histogram
|
||||||
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUpdateChannelMetrics creates an instance of UpdateChannel
|
// NewUpdateChannelMetrics creates an instance of UpdateChannel
|
||||||
@@ -125,20 +126,29 @@ func NewUpdateChannelMetrics(ctx context.Context, meter metric.Meter) (*UpdateCh
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toComponentSyncResponseDurationMicro, err := meter.Int64Histogram("management.updatechannel.tocomponentsyncresponse.duration.micro",
|
||||||
|
metric.WithUnit("microseconds"),
|
||||||
|
metric.WithDescription("Duration of how long it takes to convert components to component sync response"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &UpdateChannelMetrics{
|
return &UpdateChannelMetrics{
|
||||||
createChannelDurationMicro: createChannelDurationMicro,
|
createChannelDurationMicro: createChannelDurationMicro,
|
||||||
closeChannelDurationMicro: closeChannelDurationMicro,
|
closeChannelDurationMicro: closeChannelDurationMicro,
|
||||||
closeChannelsDurationMicro: closeChannelsDurationMicro,
|
closeChannelsDurationMicro: closeChannelsDurationMicro,
|
||||||
closeChannels: closeChannels,
|
closeChannels: closeChannels,
|
||||||
sendUpdateDurationMicro: sendUpdateDurationMicro,
|
sendUpdateDurationMicro: sendUpdateDurationMicro,
|
||||||
getAllConnectedPeersDurationMicro: getAllConnectedPeersDurationMicro,
|
getAllConnectedPeersDurationMicro: getAllConnectedPeersDurationMicro,
|
||||||
getAllConnectedPeers: getAllConnectedPeers,
|
getAllConnectedPeers: getAllConnectedPeers,
|
||||||
hasChannelDurationMicro: hasChannelDurationMicro,
|
hasChannelDurationMicro: hasChannelDurationMicro,
|
||||||
calcPostureChecksDurationMicro: calcPostureChecksDurationMicro,
|
calcPostureChecksDurationMicro: calcPostureChecksDurationMicro,
|
||||||
calcPeerNetworkMapDurationMs: calcPeerNetworkMapDurationMs,
|
calcPeerNetworkMapDurationMs: calcPeerNetworkMapDurationMs,
|
||||||
mergeNetworkMapDurationMicro: mergeNetworkMapDurationMicro,
|
mergeNetworkMapDurationMicro: mergeNetworkMapDurationMicro,
|
||||||
toSyncResponseDurationMicro: toSyncResponseDurationMicro,
|
toSyncResponseDurationMicro: toSyncResponseDurationMicro,
|
||||||
ctx: ctx,
|
toComponentSyncResponseDurationMicro: toComponentSyncResponseDurationMicro,
|
||||||
|
ctx: ctx,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,3 +203,7 @@ func (metrics *UpdateChannelMetrics) CountMergeNetworkMapDuration(duration time.
|
|||||||
func (metrics *UpdateChannelMetrics) CountToSyncResponseDuration(duration time.Duration) {
|
func (metrics *UpdateChannelMetrics) CountToSyncResponseDuration(duration time.Duration) {
|
||||||
metrics.toSyncResponseDurationMicro.Record(metrics.ctx, duration.Microseconds())
|
metrics.toSyncResponseDurationMicro.Record(metrics.ctx, duration.Microseconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (metrics *UpdateChannelMetrics) CountToComponentSyncResponseDuration(duration time.Duration) {
|
||||||
|
metrics.toComponentSyncResponseDurationMicro.Record(metrics.ctx, duration.Microseconds())
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
"github.com/netbirdio/netbird/shared/management/status"
|
"github.com/netbirdio/netbird/shared/management/status"
|
||||||
"github.com/netbirdio/netbird/version"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -42,27 +41,8 @@ const (
|
|||||||
PublicCategory = "public"
|
PublicCategory = "public"
|
||||||
PrivateCategory = "private"
|
PrivateCategory = "private"
|
||||||
UnknownCategory = "unknown"
|
UnknownCategory = "unknown"
|
||||||
|
|
||||||
// firewallRuleMinPortRangesVer defines the minimum peer version that supports port range rules.
|
|
||||||
firewallRuleMinPortRangesVer = "0.48.0"
|
|
||||||
// firewallRuleMinNativeSSHVer defines the minimum peer version that supports native SSH features in the firewall rules.
|
|
||||||
firewallRuleMinNativeSSHVer = "0.60.0"
|
|
||||||
|
|
||||||
// nativeSSHPortString defines the default port number as a string used for native SSH connections; this port is used by clients when hijacking ssh connections.
|
|
||||||
nativeSSHPortString = "22022"
|
|
||||||
nativeSSHPortNumber = 22022
|
|
||||||
// defaultSSHPortString defines the standard SSH port number as a string, commonly used for default SSH connections.
|
|
||||||
defaultSSHPortString = "22"
|
|
||||||
defaultSSHPortNumber = 22
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type supportedFeatures struct {
|
|
||||||
nativeSSH bool
|
|
||||||
portRanges bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type LookupMap map[string]struct{}
|
|
||||||
|
|
||||||
// AccountMeta is a struct that contains a stripped down version of the Account object.
|
// AccountMeta is a struct that contains a stripped down version of the Account object.
|
||||||
// It doesn't carry any peers, groups, policies, or routes, etc. Just some metadata (e.g. ID, created by, created at, etc).
|
// It doesn't carry any peers, groups, policies, or routes, etc. Just some metadata (e.g. ID, created by, created at, etc).
|
||||||
type AccountMeta struct {
|
type AccountMeta struct {
|
||||||
@@ -1071,7 +1051,7 @@ func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.P
|
|||||||
default:
|
default:
|
||||||
authorizedUsers[auth.Wildcard] = a.getAllowedUserIDs()
|
authorizedUsers[auth.Wildcard] = a.getAllowedUserIDs()
|
||||||
}
|
}
|
||||||
} else if peerInDestinations && policyRuleImpliesLegacySSH(rule) && peer.SSHEnabled {
|
} else if peerInDestinations && PolicyRuleImpliesLegacySSH(rule) && peer.SSHEnabled {
|
||||||
sshEnabled = true
|
sshEnabled = true
|
||||||
authorizedUsers[auth.Wildcard] = a.getAllowedUserIDs()
|
authorizedUsers[auth.Wildcard] = a.getAllowedUserIDs()
|
||||||
}
|
}
|
||||||
@@ -1137,15 +1117,15 @@ func (a *Account) connResourcesGenerator(ctx context.Context, targetPeer *nbpeer
|
|||||||
if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 {
|
if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 {
|
||||||
rules = append(rules, &fr)
|
rules = append(rules, &fr)
|
||||||
} else {
|
} else {
|
||||||
rules = append(rules, expandPortsAndRanges(fr, rule, targetPeer)...)
|
rules = append(rules, ExpandPortsAndRanges(fr, rule, targetPeer)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
rules = appendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, firewallRuleContext{
|
rules = AppendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, FirewallRuleContext{
|
||||||
direction: direction,
|
Direction: direction,
|
||||||
dirStr: strconv.Itoa(direction),
|
DirStr: strconv.Itoa(direction),
|
||||||
protocolStr: string(protocol),
|
ProtocolStr: string(protocol),
|
||||||
actionStr: string(rule.Action),
|
ActionStr: string(rule.Action),
|
||||||
portsJoined: strings.Join(rule.Ports, ","),
|
PortsJoined: strings.Join(rule.Ports, ","),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, func() ([]*nbpeer.Peer, []*FirewallRule) {
|
}, func() ([]*nbpeer.Peer, []*FirewallRule) {
|
||||||
@@ -1153,10 +1133,6 @@ func (a *Account) connResourcesGenerator(ctx context.Context, targetPeer *nbpeer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func policyRuleImpliesLegacySSH(rule *PolicyRule) bool {
|
|
||||||
return rule.Protocol == PolicyRuleProtocolALL || (rule.Protocol == PolicyRuleProtocolTCP && (portsIncludesSSH(rule.Ports) || portRangeIncludesSSH(rule.PortRanges)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PeerSSHEnabledFromPolicies is the network-map-free equivalent of the sshEnabled
|
// PeerSSHEnabledFromPolicies is the network-map-free equivalent of the sshEnabled
|
||||||
// determination in GetPeerConnectionResources / CalculateNetworkMapFromComponents.
|
// determination in GetPeerConnectionResources / CalculateNetworkMapFromComponents.
|
||||||
func PeerSSHEnabledFromPolicies(policies []*Policy, peerID string, peerGroupIDs map[string]struct{}, peerSSHEnabled bool) bool {
|
func PeerSSHEnabledFromPolicies(policies []*Policy, peerID string, peerGroupIDs map[string]struct{}, peerSSHEnabled bool) bool {
|
||||||
@@ -1171,7 +1147,7 @@ func PeerSSHEnabledFromPolicies(policies []*Policy, peerID string, peerGroupIDs
|
|||||||
}
|
}
|
||||||
|
|
||||||
isSSHRule := rule.Protocol == PolicyRuleProtocolNetbirdSSH ||
|
isSSHRule := rule.Protocol == PolicyRuleProtocolNetbirdSSH ||
|
||||||
(policyRuleImpliesLegacySSH(rule) && peerSSHEnabled)
|
(PolicyRuleImpliesLegacySSH(rule) && peerSSHEnabled)
|
||||||
if !isSSHRule {
|
if !isSSHRule {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1198,24 +1174,6 @@ func ruleHasDestination(rule *PolicyRule, peerID string, peerGroupIDs map[string
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// getAllPeersFromGroups for given peer ID and list of groups
|
// getAllPeersFromGroups for given peer ID and list of groups
|
||||||
//
|
//
|
||||||
// Returns a list of peers from specified groups that pass specified posture checks
|
// Returns a list of peers from specified groups that pass specified posture checks
|
||||||
@@ -1315,7 +1273,7 @@ func (a *Account) getRouteFirewallRules(ctx context.Context, peerID string, poli
|
|||||||
}
|
}
|
||||||
|
|
||||||
rulePeers := a.getRulePeers(rule, policy.SourcePostureChecks, peerID, distributionPeers, validatedPeersMap)
|
rulePeers := a.getRulePeers(rule, policy.SourcePostureChecks, peerID, distributionPeers, validatedPeersMap)
|
||||||
rules := generateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN, includeIPv6)
|
rules := GenerateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN, includeIPv6)
|
||||||
fwRules = append(fwRules, rules...)
|
fwRules = append(fwRules, rules...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1808,96 +1766,6 @@ func (a *Account) createProxyPolicy(svc *service.Service, target *service.Target
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// expandPortsAndRanges expands Ports and PortRanges of a rule into individual firewall rules
|
|
||||||
func expandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer) []*FirewallRule {
|
|
||||||
features := peerSupportedFirewallFeatures(peer.Meta.WtVersion)
|
|
||||||
|
|
||||||
var expanded []*FirewallRule
|
|
||||||
|
|
||||||
for _, port := range rule.Ports {
|
|
||||||
fr := base
|
|
||||||
fr.Port = port
|
|
||||||
expanded = append(expanded, &fr)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, portRange := range rule.PortRanges {
|
|
||||||
// prefer PolicyRule.Ports
|
|
||||||
if len(rule.Ports) > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fr := base
|
|
||||||
|
|
||||||
if features.portRanges {
|
|
||||||
fr.PortRange = portRange
|
|
||||||
} else {
|
|
||||||
// Peer doesn't support port ranges, only allow single-port ranges
|
|
||||||
if portRange.Start != portRange.End {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fr.Port = strconv.FormatUint(uint64(portRange.Start), 10)
|
|
||||||
}
|
|
||||||
expanded = append(expanded, &fr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if shouldCheckRulesForNativeSSH(features.nativeSSH, rule, peer) || rule.Protocol == PolicyRuleProtocolNetbirdSSH {
|
|
||||||
expanded = addNativeSSHRule(base, expanded)
|
|
||||||
}
|
|
||||||
|
|
||||||
return expanded
|
|
||||||
}
|
|
||||||
|
|
||||||
// addNativeSSHRule adds a native SSH rule (port 22022) to the expanded rules if the base rule has port 22 configured.
|
|
||||||
func addNativeSSHRule(base FirewallRule, expanded []*FirewallRule) []*FirewallRule {
|
|
||||||
shouldAdd := false
|
|
||||||
for _, fr := range expanded {
|
|
||||||
if isPortInRule(nativeSSHPortString, 22022, fr) {
|
|
||||||
return expanded
|
|
||||||
}
|
|
||||||
if isPortInRule(defaultSSHPortString, 22, fr) {
|
|
||||||
shouldAdd = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !shouldAdd {
|
|
||||||
return expanded
|
|
||||||
}
|
|
||||||
|
|
||||||
fr := base
|
|
||||||
fr.Port = nativeSSHPortString
|
|
||||||
return append(expanded, &fr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPortInRule(portString string, portInt uint16, rule *FirewallRule) bool {
|
|
||||||
return rule.Port == portString || (rule.PortRange.Start <= portInt && portInt <= rule.PortRange.End)
|
|
||||||
}
|
|
||||||
|
|
||||||
// shouldCheckRulesForNativeSSH determines whether specific policy rules should be checked for native SSH support.
|
|
||||||
// While users can add the nativeSSHPortString, we look for cases when they used port 22 and based on SSH enabled
|
|
||||||
// in both management and client, we indicate to add the native port.
|
|
||||||
func shouldCheckRulesForNativeSSH(supportsNative bool, rule *PolicyRule, peer *nbpeer.Peer) bool {
|
|
||||||
return supportsNative && peer.SSHEnabled && peer.Meta.Flags.ServerSSHAllowed && rule.Protocol == PolicyRuleProtocolTCP
|
|
||||||
}
|
|
||||||
|
|
||||||
// peerSupportedFirewallFeatures checks if the peer version supports port ranges.
|
|
||||||
func peerSupportedFirewallFeatures(peerVer string) supportedFeatures {
|
|
||||||
if version.IsDevelopmentVersion(peerVer) {
|
|
||||||
return supportedFeatures{true, true}
|
|
||||||
}
|
|
||||||
|
|
||||||
var features supportedFeatures
|
|
||||||
|
|
||||||
meetMinVer, err := posture.MeetsMinVersion(firewallRuleMinNativeSSHVer, peerVer)
|
|
||||||
features.nativeSSH = err == nil && meetMinVer
|
|
||||||
|
|
||||||
if features.nativeSSH {
|
|
||||||
features.portRanges = true
|
|
||||||
} else {
|
|
||||||
meetMinVer, err = posture.MeetsMinVersion(firewallRuleMinPortRangesVer, peerVer)
|
|
||||||
features.portRanges = err == nil && meetMinVer
|
|
||||||
}
|
|
||||||
|
|
||||||
return features
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterZoneRecordsForPeers filters DNS records to only include peers to connect.
|
// filterZoneRecordsForPeers filters DNS records to only include peers to connect.
|
||||||
// AAAA records are excluded when the requesting peer lacks IPv6 capability.
|
// AAAA records are excluded when the requesting peer lacks IPv6 capability.
|
||||||
func filterZoneRecordsForPeers(peer *nbpeer.Peer, customZone nbdns.CustomZone, peersToConnect, expiredPeers []*nbpeer.Peer) []nbdns.SimpleRecord {
|
func filterZoneRecordsForPeers(peer *nbpeer.Peer, customZone nbdns.CustomZone, peersToConnect, expiredPeers []*nbpeer.Peer) []nbdns.SimpleRecord {
|
||||||
|
|||||||
@@ -16,6 +16,39 @@ import (
|
|||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetPeerNetworkMapResult dispatches to either the legacy-NetworkMap path or
|
||||||
|
// the components path based on the peer's capability and the kill switch.
|
||||||
|
// Capable peers (PeerCapabilityComponentNetworkMap) get the raw components
|
||||||
|
// shape — the server skips Calculate() entirely for them, saving CPU
|
||||||
|
// proportional to the number of capable peers in the account. Legacy peers
|
||||||
|
// (or any peer when componentsDisabled is true) get the fully-expanded
|
||||||
|
// NetworkMap as before.
|
||||||
|
func (a *Account) GetPeerNetworkMapResult(
|
||||||
|
ctx context.Context,
|
||||||
|
peerID string,
|
||||||
|
componentsDisabled bool,
|
||||||
|
peersCustomZone nbdns.CustomZone,
|
||||||
|
accountZones []*zones.Zone,
|
||||||
|
validatedPeersMap map[string]struct{},
|
||||||
|
resourcePolicies map[string][]*Policy,
|
||||||
|
routers map[string]map[string]*routerTypes.NetworkRouter,
|
||||||
|
metrics *telemetry.AccountManagerMetrics,
|
||||||
|
groupIDToUserIDs map[string][]string,
|
||||||
|
) PeerNetworkMapResult {
|
||||||
|
peer := a.Peers[peerID]
|
||||||
|
if !componentsDisabled && peer != nil && peer.SupportsComponentNetworkMap() {
|
||||||
|
components := a.GetPeerNetworkMapComponents(
|
||||||
|
ctx, peerID, peersCustomZone, accountZones, validatedPeersMap, resourcePolicies, routers, groupIDToUserIDs,
|
||||||
|
)
|
||||||
|
return PeerNetworkMapResult{Components: components}
|
||||||
|
}
|
||||||
|
return PeerNetworkMapResult{
|
||||||
|
NetworkMap: a.GetPeerNetworkMapFromComponents(
|
||||||
|
ctx, peerID, peersCustomZone, accountZones, validatedPeersMap, resourcePolicies, routers, metrics, groupIDToUserIDs,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Account) GetPeerNetworkMapFromComponents(
|
func (a *Account) GetPeerNetworkMapFromComponents(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
peerID string,
|
peerID string,
|
||||||
@@ -40,8 +73,8 @@ func (a *Account) GetPeerNetworkMapFromComponents(
|
|||||||
groupIDToUserIDs,
|
groupIDToUserIDs,
|
||||||
)
|
)
|
||||||
|
|
||||||
if components == nil {
|
if components.IsEmpty() {
|
||||||
return &NetworkMap{Network: a.Network.Copy()}
|
return &NetworkMap{Network: components.Network}
|
||||||
}
|
}
|
||||||
|
|
||||||
nm := CalculateNetworkMapFromComponents(ctx, components)
|
nm := CalculateNetworkMapFromComponents(ctx, components)
|
||||||
@@ -71,26 +104,54 @@ func (a *Account) GetPeerNetworkMapComponents(
|
|||||||
routers map[string]map[string]*routerTypes.NetworkRouter,
|
routers map[string]map[string]*routerTypes.NetworkRouter,
|
||||||
groupIDToUserIDs map[string][]string,
|
groupIDToUserIDs map[string][]string,
|
||||||
) *NetworkMapComponents {
|
) *NetworkMapComponents {
|
||||||
|
|
||||||
peer := a.Peers[peerID]
|
peer := a.Peers[peerID]
|
||||||
|
// this can never happen, things are very wrong if it did
|
||||||
|
// TODO (dmitri) maybe consider using invariants?
|
||||||
if peer == nil {
|
if peer == nil {
|
||||||
return nil
|
log.WithField("peer id", peerID).Error("NetworkMapComponents are computed for a peer missing from the account")
|
||||||
|
return EmptyNetworkMapComponents(&NetworkMapComponents{
|
||||||
|
PeerID: peerID,
|
||||||
|
Network: a.Network.Copy(),
|
||||||
|
// must include the target peer as it's required on the client
|
||||||
|
Peers: map[string]*nbpeer.Peer{peerID: peer},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := validatedPeersMap[peerID]; !ok {
|
if _, ok := validatedPeersMap[peerID]; !ok {
|
||||||
return nil
|
// Mirror legacy graceful-degrade: GetPeerNetworkMapFromComponents
|
||||||
|
// returns &NetworkMap{Network: a.Network.Copy()} when components is
|
||||||
|
// nil. Match that floor so the receiving client always sees the
|
||||||
|
// account Network identifier, not a fully-empty envelope.
|
||||||
|
return EmptyNetworkMapComponents(&NetworkMapComponents{
|
||||||
|
PeerID: peerID,
|
||||||
|
Network: a.Network.Copy(),
|
||||||
|
// must include the target peer as it's required on the client
|
||||||
|
Peers: map[string]*nbpeer.Peer{peerID: peer},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
components := &NetworkMapComponents{
|
components := &NetworkMapComponents{
|
||||||
PeerID: peerID,
|
PeerID: peerID,
|
||||||
Network: a.Network.Copy(),
|
Network: a.Network.Copy(),
|
||||||
NameServerGroups: make([]*nbdns.NameServerGroup, 0),
|
NameServerGroups: make([]*nbdns.NameServerGroup, 0),
|
||||||
CustomZoneDomain: peersCustomZone.Domain,
|
CustomZoneDomain: peersCustomZone.Domain,
|
||||||
ResourcePoliciesMap: make(map[string][]*Policy),
|
ResourcePoliciesMap: make(map[string][]*Policy),
|
||||||
RoutersMap: make(map[string]map[string]*routerTypes.NetworkRouter),
|
RoutersMap: make(map[string]map[string]*routerTypes.NetworkRouter),
|
||||||
NetworkResources: make([]*resourceTypes.NetworkResource, 0),
|
NetworkResources: make([]*resourceTypes.NetworkResource, 0),
|
||||||
PostureFailedPeers: make(map[string]map[string]struct{}, len(a.PostureChecks)),
|
PostureFailedPeers: make(map[string]map[string]struct{}, len(a.PostureChecks)),
|
||||||
RouterPeers: make(map[string]*nbpeer.Peer),
|
RouterPeers: make(map[string]*nbpeer.Peer),
|
||||||
|
NetworkXIDToPublicID: make(map[string]string, len(a.Networks)),
|
||||||
|
PostureCheckXIDToPublicID: make(map[string]string, len(a.PostureChecks)),
|
||||||
|
}
|
||||||
|
for _, n := range a.Networks {
|
||||||
|
if n != nil {
|
||||||
|
components.NetworkXIDToPublicID[n.ID] = n.PublicID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, pc := range a.PostureChecks {
|
||||||
|
if pc != nil {
|
||||||
|
components.PostureCheckXIDToPublicID[pc.ID] = pc.PublicID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
components.AccountSettings = &AccountSettingsInfo{
|
components.AccountSettings = &AccountSettingsInfo{
|
||||||
@@ -102,6 +163,7 @@ func (a *Account) GetPeerNetworkMapComponents(
|
|||||||
|
|
||||||
components.DNSSettings = &a.DNSSettings
|
components.DNSSettings = &a.DNSSettings
|
||||||
|
|
||||||
|
// relevantPeers always contains the target peer (peerID)
|
||||||
relevantPeers, relevantGroups, relevantPolicies, relevantRoutes, sshReqs := a.getPeersGroupsPoliciesRoutes(ctx, peerID, peer.SSHEnabled, validatedPeersMap, &components.PostureFailedPeers)
|
relevantPeers, relevantGroups, relevantPolicies, relevantRoutes, sshReqs := a.getPeersGroupsPoliciesRoutes(ctx, peerID, peer.SSHEnabled, validatedPeersMap, &components.PostureFailedPeers)
|
||||||
|
|
||||||
if len(sshReqs.neededGroupIDs) > 0 {
|
if len(sshReqs.neededGroupIDs) > 0 {
|
||||||
@@ -209,21 +271,26 @@ func (a *Account) GetPeerNetworkMapComponents(
|
|||||||
components.ResourcePoliciesMap[resource.ID] = policies
|
components.ResourcePoliciesMap[resource.ID] = policies
|
||||||
}
|
}
|
||||||
|
|
||||||
components.RoutersMap[resource.NetworkID] = networkRoutingPeers
|
// Only expose router peers and the per-network routers_map when this
|
||||||
for peerIDKey := range networkRoutingPeers {
|
// target peer actually has access to the resource (either as a router
|
||||||
if p := a.Peers[peerIDKey]; p != nil {
|
// itself or via a policy that includes it as a source). Without this
|
||||||
if _, exists := components.RouterPeers[peerIDKey]; !exists {
|
// gate, every peer's envelope was leaking router peers of every
|
||||||
components.RouterPeers[peerIDKey] = p
|
// network in the account — accounts with many tenants/networks
|
||||||
}
|
// shipped tens of unrelated peers in `peers[]` and `routers_map`.
|
||||||
if _, exists := components.Peers[peerIDKey]; !exists {
|
if addSourcePeers {
|
||||||
if _, validated := validatedPeersMap[peerIDKey]; validated {
|
components.RoutersMap[resource.NetworkID] = networkRoutingPeers
|
||||||
components.Peers[peerIDKey] = p
|
for peerIDKey := range networkRoutingPeers {
|
||||||
|
if p := a.Peers[peerIDKey]; p != nil {
|
||||||
|
if _, exists := components.RouterPeers[peerIDKey]; !exists {
|
||||||
|
components.RouterPeers[peerIDKey] = p
|
||||||
|
}
|
||||||
|
if _, exists := components.Peers[peerIDKey]; !exists {
|
||||||
|
if _, validated := validatedPeersMap[peerIDKey]; validated {
|
||||||
|
components.Peers[peerIDKey] = p
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if addSourcePeers {
|
|
||||||
components.NetworkResources = append(components.NetworkResources, resource)
|
components.NetworkResources = append(components.NetworkResources, resource)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,18 +321,44 @@ func (a *Account) getPeersGroupsPoliciesRoutes(
|
|||||||
|
|
||||||
relevantPeerIDs[peerID] = a.GetPeer(peerID)
|
relevantPeerIDs[peerID] = a.GetPeer(peerID)
|
||||||
|
|
||||||
|
peerGroupSet := make(map[string]struct{}, 8)
|
||||||
for groupID, group := range a.Groups {
|
for groupID, group := range a.Groups {
|
||||||
if slices.Contains(group.Peers, peerID) {
|
if slices.Contains(group.Peers, peerID) {
|
||||||
relevantGroupIDs[groupID] = a.GetGroup(groupID)
|
relevantGroupIDs[groupID] = a.GetGroup(groupID)
|
||||||
|
peerGroupSet[groupID] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
routeAccessControlGroups := make(map[string]struct{})
|
routeAccessControlGroups := make(map[string]struct{})
|
||||||
for _, r := range a.Routes {
|
for _, r := range a.Routes {
|
||||||
for _, groupID := range r.Groups {
|
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 {
|
||||||
relevantGroupIDs[groupID] = a.GetGroup(groupID)
|
relevantGroupIDs[groupID] = a.GetGroup(groupID)
|
||||||
}
|
}
|
||||||
for _, groupID := range r.PeerGroups {
|
for _, groupID := range r.Groups {
|
||||||
relevantGroupIDs[groupID] = a.GetGroup(groupID)
|
relevantGroupIDs[groupID] = a.GetGroup(groupID)
|
||||||
}
|
}
|
||||||
if r.Enabled {
|
if r.Enabled {
|
||||||
@@ -274,6 +367,44 @@ func (a *Account) getPeersGroupsPoliciesRoutes(
|
|||||||
routeAccessControlGroups[groupID] = struct{}{}
|
routeAccessControlGroups[groupID] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include route advertisers in relevantPeerIDs. The envelope
|
||||||
|
// encoder writes route.peer_index by looking up r.Peer in the
|
||||||
|
// shipped peers list; if the advertiser is policy-isolated from
|
||||||
|
// the target peer (no rule edge between them), it would otherwise
|
||||||
|
// be omitted and the decoder would fail to resolve r.Peer, leaving
|
||||||
|
// the client without a WG tunnel target for this route. Legacy
|
||||||
|
// NetworkMap.Routes shipped the WG public key inline, so the
|
||||||
|
// equivalence path doesn't surface this — but the dependency is
|
||||||
|
// real once a client actually tries to use the route.
|
||||||
|
// Gate by validatedPeersMap so non-validated advertisers stay out
|
||||||
|
// (matches the network-resource router behaviour at the bottom of
|
||||||
|
// this loop, and the legacy invariant that only validated peers
|
||||||
|
// reach a client's view).
|
||||||
|
if r.Peer != "" {
|
||||||
|
if _, ok := validatedPeersMap[r.Peer]; ok {
|
||||||
|
if p := a.GetPeer(r.Peer); p != nil {
|
||||||
|
relevantPeerIDs[r.Peer] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, groupID := range r.PeerGroups {
|
||||||
|
g := a.GetGroup(groupID)
|
||||||
|
if g == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, pid := range g.Peers {
|
||||||
|
if _, exists := relevantPeerIDs[pid]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := validatedPeersMap[pid]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p := a.GetPeer(pid); p != nil {
|
||||||
|
relevantPeerIDs[pid] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
relevantRoutes = append(relevantRoutes, r)
|
relevantRoutes = append(relevantRoutes, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,7 +484,7 @@ func (a *Account) getPeersGroupsPoliciesRoutes(
|
|||||||
default:
|
default:
|
||||||
sshReqs.needAllowedUserIDs = true
|
sshReqs.needAllowedUserIDs = true
|
||||||
}
|
}
|
||||||
} else if policyRuleImpliesLegacySSH(rule) && peerSSHEnabled {
|
} else if PolicyRuleImpliesLegacySSH(rule) && peerSSHEnabled {
|
||||||
sshReqs.needAllowedUserIDs = true
|
sshReqs.needAllowedUserIDs = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,6 +617,13 @@ func (a *Account) getPostureValidPeersSaveFailed(inputPeers []string, postureChe
|
|||||||
return dest
|
return dest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filterGroupPeers trims each group's Peers slice to only those peers that
|
||||||
|
// also appear in `peers`. Groups whose filtered list is empty are NOT
|
||||||
|
// deleted from the map — they're kept so the components wire encoder can
|
||||||
|
// still resolve seq references from routes/policies/access-control groups
|
||||||
|
// that name them. Calculate() tolerates groups with empty Peers (the inner
|
||||||
|
// loops simply iterate zero times), so retaining them is behaviourally a
|
||||||
|
// no-op for the legacy path that consumes the same NetworkMapComponents.
|
||||||
func filterGroupPeers(groups *map[string]*Group, peers map[string]*nbpeer.Peer) {
|
func filterGroupPeers(groups *map[string]*Group, peers map[string]*nbpeer.Peer) {
|
||||||
for groupID, groupInfo := range *groups {
|
for groupID, groupInfo := range *groups {
|
||||||
filteredPeers := make([]string, 0, len(groupInfo.Peers))
|
filteredPeers := make([]string, 0, len(groupInfo.Peers))
|
||||||
@@ -495,9 +633,7 @@ func filterGroupPeers(groups *map[string]*Group, peers map[string]*nbpeer.Peer)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(filteredPeers) == 0 {
|
if len(filteredPeers) != len(groupInfo.Peers) {
|
||||||
delete(*groups, groupID)
|
|
||||||
} else if len(filteredPeers) != len(groupInfo.Peers) {
|
|
||||||
ng := groupInfo.Copy()
|
ng := groupInfo.Copy()
|
||||||
ng.Peers = filteredPeers
|
ng.Peers = filteredPeers
|
||||||
(*groups)[groupID] = ng
|
(*groups)[groupID] = ng
|
||||||
|
|||||||
@@ -666,7 +666,7 @@ func Test_ExpandPortsAndRanges_SSHRuleExpansion(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := expandPortsAndRanges(tt.base, tt.rule, tt.peer)
|
result := ExpandPortsAndRanges(tt.base, tt.rule, tt.peer)
|
||||||
|
|
||||||
var ports []string
|
var ports []string
|
||||||
for _, fr := range result {
|
for _, fr := range result {
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
nbroute "github.com/netbirdio/netbird/route"
|
||||||
|
sharedtypes "github.com/netbirdio/netbird/shared/management/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Type aliases for types relocated to shared/management/types so that the
|
||||||
|
// client-side compute path can depend on them
|
||||||
|
|
||||||
|
type DNSSettings = sharedtypes.DNSSettings
|
||||||
|
|
||||||
|
type FirewallRule = sharedtypes.FirewallRule
|
||||||
|
|
||||||
|
type Group = sharedtypes.Group
|
||||||
|
type GroupPeer = sharedtypes.GroupPeer
|
||||||
|
|
||||||
|
type Network = sharedtypes.Network
|
||||||
|
type NetworkMap = sharedtypes.NetworkMap
|
||||||
|
type ForwardingRule = sharedtypes.ForwardingRule
|
||||||
|
|
||||||
|
type Policy = sharedtypes.Policy
|
||||||
|
type PolicyUpdateOperation = sharedtypes.PolicyUpdateOperation
|
||||||
|
|
||||||
|
type PolicyRule = sharedtypes.PolicyRule
|
||||||
|
type PolicyUpdateOperationType = sharedtypes.PolicyUpdateOperationType
|
||||||
|
type PolicyTrafficActionType = sharedtypes.PolicyTrafficActionType
|
||||||
|
type PolicyRuleProtocolType = sharedtypes.PolicyRuleProtocolType
|
||||||
|
type PolicyRuleDirection = sharedtypes.PolicyRuleDirection
|
||||||
|
type RulePortRange = sharedtypes.RulePortRange
|
||||||
|
|
||||||
|
type Resource = sharedtypes.Resource
|
||||||
|
type ResourceType = sharedtypes.ResourceType
|
||||||
|
|
||||||
|
type RouteFirewallRule = sharedtypes.RouteFirewallRule
|
||||||
|
|
||||||
|
type NetworkMapComponents = sharedtypes.NetworkMapComponents
|
||||||
|
|
||||||
|
var EmptyNetworkMapComponents = sharedtypes.EmptyNetworkMapComponents
|
||||||
|
|
||||||
|
type AccountSettingsInfo = sharedtypes.AccountSettingsInfo
|
||||||
|
|
||||||
|
type GroupCompact = sharedtypes.GroupCompact
|
||||||
|
type NetworkMapComponentsCompact = sharedtypes.NetworkMapComponentsCompact
|
||||||
|
|
||||||
|
type LookupMap = sharedtypes.LookupMap
|
||||||
|
type FirewallRuleContext = sharedtypes.FirewallRuleContext
|
||||||
|
|
||||||
|
const (
|
||||||
|
GroupIssuedAPI = sharedtypes.GroupIssuedAPI
|
||||||
|
GroupIssuedJWT = sharedtypes.GroupIssuedJWT
|
||||||
|
GroupIssuedIntegration = sharedtypes.GroupIssuedIntegration
|
||||||
|
GroupAllName = sharedtypes.GroupAllName
|
||||||
|
)
|
||||||
|
|
||||||
|
// Function forwarders preserve types.X(...) call sites that previously
|
||||||
|
// resolved to package-local funcs. Plain forwarders (not var aliases) keep
|
||||||
|
// the symbol immutable and allow the inliner to flatten the call.
|
||||||
|
|
||||||
|
func PolicyRuleImpliesLegacySSH(rule *PolicyRule) bool {
|
||||||
|
return sharedtypes.PolicyRuleImpliesLegacySSH(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer) []*FirewallRule {
|
||||||
|
return sharedtypes.ExpandPortsAndRanges(base, rule, peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *nbpeer.Peer, rule *PolicyRule, rc FirewallRuleContext) []*FirewallRule {
|
||||||
|
return sharedtypes.AppendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, rc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CalculateNetworkMapFromComponents(ctx context.Context, components *NetworkMapComponents) *NetworkMap {
|
||||||
|
return sharedtypes.CalculateNetworkMapFromComponents(ctx, components)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int, includeIPv6 bool) []*RouteFirewallRule {
|
||||||
|
return sharedtypes.GenerateRouteFirewallRules(ctx, route, rule, groupPeers, direction, includeIPv6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllocateIPv6Subnet(r *rand.Rand) net.IPNet {
|
||||||
|
return sharedtypes.AllocateIPv6Subnet(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNetwork() *Network {
|
||||||
|
return sharedtypes.NewNetwork()
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllocatePeerIP(prefix netip.Prefix, takenIps []netip.Addr) (netip.Addr, error) {
|
||||||
|
return sharedtypes.AllocatePeerIP(prefix, takenIps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllocateRandomPeerIP(prefix netip.Prefix) (netip.Addr, error) {
|
||||||
|
return sharedtypes.AllocateRandomPeerIP(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllocateRandomPeerIPv6(prefix netip.Prefix) (netip.Addr, error) {
|
||||||
|
return sharedtypes.AllocateRandomPeerIPv6(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseRuleString(rule string) (PolicyRuleProtocolType, RulePortRange, error) {
|
||||||
|
return sharedtypes.ParseRuleString(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
FirewallRuleDirectionIN = sharedtypes.FirewallRuleDirectionIN
|
||||||
|
FirewallRuleDirectionOUT = sharedtypes.FirewallRuleDirectionOUT
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ResourceTypePeer = sharedtypes.ResourceTypePeer
|
||||||
|
ResourceTypeDomain = sharedtypes.ResourceTypeDomain
|
||||||
|
ResourceTypeHost = sharedtypes.ResourceTypeHost
|
||||||
|
ResourceTypeSubnet = sharedtypes.ResourceTypeSubnet
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PolicyTrafficActionAccept = sharedtypes.PolicyTrafficActionAccept
|
||||||
|
PolicyTrafficActionDrop = sharedtypes.PolicyTrafficActionDrop
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PolicyRuleProtocolALL = sharedtypes.PolicyRuleProtocolALL
|
||||||
|
PolicyRuleProtocolTCP = sharedtypes.PolicyRuleProtocolTCP
|
||||||
|
PolicyRuleProtocolUDP = sharedtypes.PolicyRuleProtocolUDP
|
||||||
|
PolicyRuleProtocolICMP = sharedtypes.PolicyRuleProtocolICMP
|
||||||
|
PolicyRuleProtocolNetbirdSSH = sharedtypes.PolicyRuleProtocolNetbirdSSH
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PolicyRuleFlowDirect = sharedtypes.PolicyRuleFlowDirect
|
||||||
|
PolicyRuleFlowBidirect = sharedtypes.PolicyRuleFlowBidirect
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultRuleName = sharedtypes.DefaultRuleName
|
||||||
|
DefaultRuleDescription = sharedtypes.DefaultRuleDescription
|
||||||
|
DefaultPolicyName = sharedtypes.DefaultPolicyName
|
||||||
|
DefaultPolicyDescription = sharedtypes.DefaultPolicyDescription
|
||||||
|
)
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/xid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@@ -88,13 +89,13 @@ func buildScalableTestAccount(numPeers, numGroups int, withDefaultPolicy bool) (
|
|||||||
for i := start; i < end; i++ {
|
for i := start; i < end; i++ {
|
||||||
groupPeers = append(groupPeers, fmt.Sprintf("peer-%d", i))
|
groupPeers = append(groupPeers, fmt.Sprintf("peer-%d", i))
|
||||||
}
|
}
|
||||||
groups[groupID] = &types.Group{ID: groupID, Name: fmt.Sprintf("Group %d", g), Peers: groupPeers}
|
groups[groupID] = &types.Group{ID: groupID, PublicID: xid.New().String(), Name: fmt.Sprintf("Group %d", g), Peers: groupPeers}
|
||||||
}
|
}
|
||||||
|
|
||||||
policies := make([]*types.Policy, 0, numGroups+2)
|
policies := make([]*types.Policy, 0, numGroups+2)
|
||||||
if withDefaultPolicy {
|
if withDefaultPolicy {
|
||||||
policies = append(policies, &types.Policy{
|
policies = append(policies, &types.Policy{
|
||||||
ID: "policy-all", Name: "Default-Allow", Enabled: true,
|
ID: "policy-all", PublicID: xid.New().String(), Name: "Default-Allow", Enabled: true,
|
||||||
Rules: []*types.PolicyRule{{
|
Rules: []*types.PolicyRule{{
|
||||||
ID: "rule-all", Name: "Allow All", Enabled: true, Action: types.PolicyTrafficActionAccept,
|
ID: "rule-all", Name: "Allow All", Enabled: true, Action: types.PolicyTrafficActionAccept,
|
||||||
Protocol: types.PolicyRuleProtocolALL, Bidirectional: true,
|
Protocol: types.PolicyRuleProtocolALL, Bidirectional: true,
|
||||||
@@ -107,7 +108,7 @@ func buildScalableTestAccount(numPeers, numGroups int, withDefaultPolicy bool) (
|
|||||||
groupID := fmt.Sprintf("group-%d", g)
|
groupID := fmt.Sprintf("group-%d", g)
|
||||||
dstGroup := fmt.Sprintf("group-%d", (g+1)%numGroups)
|
dstGroup := fmt.Sprintf("group-%d", (g+1)%numGroups)
|
||||||
policies = append(policies, &types.Policy{
|
policies = append(policies, &types.Policy{
|
||||||
ID: fmt.Sprintf("policy-%d", g), Name: fmt.Sprintf("Policy %d", g), Enabled: true,
|
ID: fmt.Sprintf("policy-%d", g), PublicID: xid.New().String(), Name: fmt.Sprintf("Policy %d", g), Enabled: true,
|
||||||
Rules: []*types.PolicyRule{{
|
Rules: []*types.PolicyRule{{
|
||||||
ID: fmt.Sprintf("rule-%d", g), Name: fmt.Sprintf("Rule %d", g), Enabled: true,
|
ID: fmt.Sprintf("rule-%d", g), Name: fmt.Sprintf("Rule %d", g), Enabled: true,
|
||||||
Action: types.PolicyTrafficActionAccept, Protocol: types.PolicyRuleProtocolTCP,
|
Action: types.PolicyTrafficActionAccept, Protocol: types.PolicyRuleProtocolTCP,
|
||||||
@@ -120,7 +121,7 @@ func buildScalableTestAccount(numPeers, numGroups int, withDefaultPolicy bool) (
|
|||||||
|
|
||||||
if numGroups >= 2 {
|
if numGroups >= 2 {
|
||||||
policies = append(policies, &types.Policy{
|
policies = append(policies, &types.Policy{
|
||||||
ID: "policy-drop", Name: "Drop DB traffic", Enabled: true,
|
ID: "policy-drop", PublicID: xid.New().String(), Name: "Drop DB traffic", Enabled: true,
|
||||||
Rules: []*types.PolicyRule{{
|
Rules: []*types.PolicyRule{{
|
||||||
ID: "rule-drop", Name: "Drop DB", Enabled: true, Action: types.PolicyTrafficActionDrop,
|
ID: "rule-drop", Name: "Drop DB", Enabled: true, Action: types.PolicyTrafficActionDrop,
|
||||||
Protocol: types.PolicyRuleProtocolTCP, Ports: []string{"5432"}, Bidirectional: true,
|
Protocol: types.PolicyRuleProtocolTCP, Ports: []string{"5432"}, Bidirectional: true,
|
||||||
@@ -144,6 +145,7 @@ func buildScalableTestAccount(numPeers, numGroups int, withDefaultPolicy bool) (
|
|||||||
groupID := fmt.Sprintf("group-%d", r%numGroups)
|
groupID := fmt.Sprintf("group-%d", r%numGroups)
|
||||||
routes[routeID] = &route.Route{
|
routes[routeID] = &route.Route{
|
||||||
ID: routeID,
|
ID: routeID,
|
||||||
|
PublicID: xid.New().String(),
|
||||||
Network: netip.MustParsePrefix(fmt.Sprintf("10.%d.0.0/16", r)),
|
Network: netip.MustParsePrefix(fmt.Sprintf("10.%d.0.0/16", r)),
|
||||||
Peer: peers[routePeerID].Key,
|
Peer: peers[routePeerID].Key,
|
||||||
PeerID: routePeerID,
|
PeerID: routePeerID,
|
||||||
@@ -178,18 +180,18 @@ func buildScalableTestAccount(numPeers, numGroups int, withDefaultPolicy bool) (
|
|||||||
}
|
}
|
||||||
routerPeerID := fmt.Sprintf("peer-%d", routerPeerIdx)
|
routerPeerID := fmt.Sprintf("peer-%d", routerPeerIdx)
|
||||||
|
|
||||||
networksList = append(networksList, &networkTypes.Network{ID: netID, Name: fmt.Sprintf("Network %d", nr), AccountID: "test-account"})
|
networksList = append(networksList, &networkTypes.Network{ID: netID, PublicID: xid.New().String(), Name: fmt.Sprintf("Network %d", nr), AccountID: "test-account"})
|
||||||
networkResources = append(networkResources, &resourceTypes.NetworkResource{
|
networkResources = append(networkResources, &resourceTypes.NetworkResource{
|
||||||
ID: resID, NetworkID: netID, AccountID: "test-account", Enabled: true,
|
ID: resID, PublicID: xid.New().String(), NetworkID: netID, AccountID: "test-account", Enabled: true,
|
||||||
Address: fmt.Sprintf("svc-%d.netbird.cloud", nr),
|
Address: fmt.Sprintf("svc-%d.netbird.cloud", nr),
|
||||||
})
|
})
|
||||||
networkRouters = append(networkRouters, &routerTypes.NetworkRouter{
|
networkRouters = append(networkRouters, &routerTypes.NetworkRouter{
|
||||||
ID: fmt.Sprintf("router-%d", nr), NetworkID: netID, Peer: routerPeerID,
|
ID: fmt.Sprintf("router-%d", nr), PublicID: xid.New().String(), NetworkID: netID, Peer: routerPeerID,
|
||||||
Enabled: true, AccountID: "test-account",
|
Enabled: true, AccountID: "test-account",
|
||||||
})
|
})
|
||||||
|
|
||||||
policies = append(policies, &types.Policy{
|
policies = append(policies, &types.Policy{
|
||||||
ID: fmt.Sprintf("policy-res-%d", nr), Name: fmt.Sprintf("Resource Policy %d", nr), Enabled: true,
|
ID: fmt.Sprintf("policy-res-%d", nr), PublicID: xid.New().String(), Name: fmt.Sprintf("Resource Policy %d", nr), Enabled: true,
|
||||||
SourcePostureChecks: []string{"posture-check-ver"},
|
SourcePostureChecks: []string{"posture-check-ver"},
|
||||||
Rules: []*types.PolicyRule{{
|
Rules: []*types.PolicyRule{{
|
||||||
ID: fmt.Sprintf("rule-res-%d", nr), Name: fmt.Sprintf("Allow Resource %d", nr), Enabled: true,
|
ID: fmt.Sprintf("rule-res-%d", nr), Name: fmt.Sprintf("Allow Resource %d", nr), Enabled: true,
|
||||||
@@ -215,12 +217,12 @@ func buildScalableTestAccount(numPeers, numGroups int, withDefaultPolicy bool) (
|
|||||||
DNSSettings: types.DNSSettings{DisabledManagementGroups: []string{}},
|
DNSSettings: types.DNSSettings{DisabledManagementGroups: []string{}},
|
||||||
NameServerGroups: map[string]*nbdns.NameServerGroup{
|
NameServerGroups: map[string]*nbdns.NameServerGroup{
|
||||||
"ns-group-main": {
|
"ns-group-main": {
|
||||||
ID: "ns-group-main", Name: "Main NS", Enabled: true, Groups: []string{"group-all"},
|
ID: "ns-group-main", PublicID: xid.New().String(), Name: "Main NS", Enabled: true, Groups: []string{"group-all"},
|
||||||
NameServers: []nbdns.NameServer{{IP: netip.MustParseAddr("8.8.8.8"), NSType: nbdns.UDPNameServerType, Port: 53}},
|
NameServers: []nbdns.NameServer{{IP: netip.MustParseAddr("8.8.8.8"), NSType: nbdns.UDPNameServerType, Port: 53}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
PostureChecks: []*posture.Checks{
|
PostureChecks: []*posture.Checks{
|
||||||
{ID: "posture-check-ver", Name: "Check version", Checks: posture.ChecksDefinition{
|
{ID: "posture-check-ver", PublicID: xid.New().String(), Name: "Check version", Checks: posture.ChecksDefinition{
|
||||||
NBVersionCheck: &posture.NBVersionCheck{MinVersion: "0.26.0"},
|
NBVersionCheck: &posture.NBVersionCheck{MinVersion: "0.26.0"},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package types_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
goproto "google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
||||||
|
mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wireBenchScales — trimmed scale set for wire-size measurements. Encoding
|
||||||
|
// and marshalling are linear, so the largest extremes don't add signal.
|
||||||
|
var wireBenchScales = []benchmarkScale{
|
||||||
|
{"100peers_5groups", 100, 5},
|
||||||
|
{"500peers_20groups", 500, 20},
|
||||||
|
{"1000peers_50groups", 1000, 50},
|
||||||
|
{"5000peers_100groups", 5000, 100},
|
||||||
|
}
|
||||||
|
|
||||||
|
// assignValidWgKeys overwrites every peer's Key with a valid base64-encoded
|
||||||
|
// 32-byte string. The default scalableTestAccount uses unparsable strings
|
||||||
|
// like "key-peer-0", which makes the components encoder emit a nil WgPubKey
|
||||||
|
// and the legacy encoder ship 10-char placeholders — both shrink the wire
|
||||||
|
// size in unrealistic ways. Production peers always have valid 44-char base64
|
||||||
|
// keys, so any benchmark/breakdown that wants honest numbers must call this.
|
||||||
|
func assignValidWgKeys(account *types.Account) {
|
||||||
|
for _, p := range account.Peers {
|
||||||
|
var raw [32]byte
|
||||||
|
_, _ = rand.Read(raw[:])
|
||||||
|
p.Key = base64.StdEncoding.EncodeToString(raw[:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkNetworkMapWireEncode reports per-call ns and the marshaled wire
|
||||||
|
// size for both encoding paths. Run with:
|
||||||
|
//
|
||||||
|
// go test -run=^$ -bench=BenchmarkNetworkMapWireEncode -benchmem ./management/server/types/
|
||||||
|
func BenchmarkNetworkMapWireEncode(b *testing.B) {
|
||||||
|
skipCIBenchmark(b)
|
||||||
|
|
||||||
|
for _, scale := range wireBenchScales {
|
||||||
|
account, validatedPeers := scalableTestAccount(scale.peers, scale.groups)
|
||||||
|
// populateAccountSeqIDs(account)
|
||||||
|
assignValidWgKeys(account)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
resourcePolicies := account.GetResourcePoliciesMap()
|
||||||
|
routers := account.GetResourceRoutersMap()
|
||||||
|
groupIDToUserIDs := account.GetActiveGroupUsers()
|
||||||
|
|
||||||
|
peerID := "peer-0"
|
||||||
|
peer := account.Peers[peerID]
|
||||||
|
|
||||||
|
networkMap := account.GetPeerNetworkMapFromComponents(ctx, peerID, nbdns.CustomZone{}, nil, validatedPeers, resourcePolicies, routers, nil, groupIDToUserIDs)
|
||||||
|
components := account.GetPeerNetworkMapComponents(ctx, peerID, nbdns.CustomZone{}, nil, validatedPeers, resourcePolicies, routers, groupIDToUserIDs)
|
||||||
|
|
||||||
|
dnsCache := &cache.DNSConfigCache{}
|
||||||
|
settings := &types.Settings{}
|
||||||
|
|
||||||
|
// Pre-encode once so the size metric is identical for every run inside
|
||||||
|
// the same scale; the b.Loop call only re-runs encode + Marshal.
|
||||||
|
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0)
|
||||||
|
legacyBytes, err := goproto.Marshal(legacyResp.NetworkMap)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("marshal legacy networkmap: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
envelopeInput := mgmtgrpc.ComponentsEnvelopeInput{
|
||||||
|
Components: components,
|
||||||
|
PeerConfig: legacyResp.NetworkMap.PeerConfig,
|
||||||
|
DNSDomain: "netbird.cloud",
|
||||||
|
}
|
||||||
|
envelope := mgmtgrpc.EncodeNetworkMapEnvelope(envelopeInput)
|
||||||
|
envelopeBytes, err := goproto.Marshal(envelope)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("marshal envelope: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Run(fmt.Sprintf("legacy/%s", scale.name), func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ReportMetric(float64(len(legacyBytes)), "bytes/msg")
|
||||||
|
b.ResetTimer()
|
||||||
|
for range b.N {
|
||||||
|
resp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0)
|
||||||
|
if _, err := goproto.Marshal(resp.NetworkMap); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run(fmt.Sprintf("components/%s", scale.name), func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ReportMetric(float64(len(envelopeBytes)), "bytes/msg")
|
||||||
|
b.ResetTimer()
|
||||||
|
for range b.N {
|
||||||
|
env := mgmtgrpc.EncodeNetworkMapEnvelope(envelopeInput)
|
||||||
|
if _, err := goproto.Marshal(env); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkNetworkMapWireSize is a fast snapshot of the wire size by scale
|
||||||
|
// without a tight encode loop. Run with -bench to see one ns/op + bytes per
|
||||||
|
// scale (treat the timing as informational; the sample is one Marshal per
|
||||||
|
// scale, not the full b.N loop).
|
||||||
|
func BenchmarkNetworkMapWireSize(b *testing.B) {
|
||||||
|
skipCIBenchmark(b)
|
||||||
|
|
||||||
|
for _, scale := range wireBenchScales {
|
||||||
|
account, validatedPeers := scalableTestAccount(scale.peers, scale.groups)
|
||||||
|
// populateAccountSeqIDs(account)
|
||||||
|
assignValidWgKeys(account)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
resourcePolicies := account.GetResourcePoliciesMap()
|
||||||
|
routers := account.GetResourceRoutersMap()
|
||||||
|
groupIDToUserIDs := account.GetActiveGroupUsers()
|
||||||
|
|
||||||
|
peerID := "peer-0"
|
||||||
|
peer := account.Peers[peerID]
|
||||||
|
|
||||||
|
networkMap := account.GetPeerNetworkMapFromComponents(ctx, peerID, nbdns.CustomZone{}, nil, validatedPeers, resourcePolicies, routers, nil, groupIDToUserIDs)
|
||||||
|
components := account.GetPeerNetworkMapComponents(ctx, peerID, nbdns.CustomZone{}, nil, validatedPeers, resourcePolicies, routers, groupIDToUserIDs)
|
||||||
|
|
||||||
|
dnsCache := &cache.DNSConfigCache{}
|
||||||
|
settings := &types.Settings{}
|
||||||
|
|
||||||
|
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0)
|
||||||
|
legacyBytes, err := goproto.Marshal(legacyResp.NetworkMap)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("marshal legacy networkmap: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
env := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{
|
||||||
|
Components: components,
|
||||||
|
PeerConfig: legacyResp.NetworkMap.PeerConfig,
|
||||||
|
DNSDomain: "netbird.cloud",
|
||||||
|
})
|
||||||
|
envBytes, err := goproto.Marshal(env)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("marshal envelope: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Run(fmt.Sprintf("size/%s", scale.name), func(b *testing.B) {
|
||||||
|
b.ReportMetric(float64(len(legacyBytes)), "legacy_bytes")
|
||||||
|
b.ReportMetric(float64(len(envBytes)), "components_bytes")
|
||||||
|
ratio := float64(len(envBytes)) / float64(len(legacyBytes))
|
||||||
|
b.ReportMetric(ratio, "components/legacy")
|
||||||
|
for range b.N {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package types_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
goproto "google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
||||||
|
mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestNetworkMapWireBreakdown is a one-shot diagnostic: it computes the wire
|
||||||
|
// size attributable to each top-level field of both the legacy NetworkMap and
|
||||||
|
// the components NetworkMapEnvelope at the 5000-peer scale, so the migration
|
||||||
|
// docs can attribute the size reduction to each optimization. Runs only on
|
||||||
|
// demand via -run TestNetworkMapWireBreakdown.
|
||||||
|
func TestNetworkMapWireBreakdown(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("size diagnostic, skipped with -short")
|
||||||
|
}
|
||||||
|
if os.Getenv("NB_RUN_WIRE_BREAKDOWN") != "1" {
|
||||||
|
t.Skip("set NB_RUN_WIRE_BREAKDOWN=1 to run wire breakdown diagnostic")
|
||||||
|
}
|
||||||
|
|
||||||
|
const peerCount, groupCount = 5000, 100
|
||||||
|
account, validatedPeers := scalableTestAccount(peerCount, groupCount)
|
||||||
|
assignValidWgKeys(account)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
resourcePolicies := account.GetResourcePoliciesMap()
|
||||||
|
routers := account.GetResourceRoutersMap()
|
||||||
|
groupIDToUserIDs := account.GetActiveGroupUsers()
|
||||||
|
|
||||||
|
peerID := "peer-0"
|
||||||
|
peer := account.Peers[peerID]
|
||||||
|
networkMap := account.GetPeerNetworkMapFromComponents(ctx, peerID, nbdns.CustomZone{}, nil, validatedPeers, resourcePolicies, routers, nil, groupIDToUserIDs)
|
||||||
|
components := account.GetPeerNetworkMapComponents(ctx, peerID, nbdns.CustomZone{}, nil, validatedPeers, resourcePolicies, routers, groupIDToUserIDs)
|
||||||
|
|
||||||
|
dnsCache := &cache.DNSConfigCache{}
|
||||||
|
settings := &types.Settings{}
|
||||||
|
|
||||||
|
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0)
|
||||||
|
legacyTotal := mustMarshalSize(t, legacyResp.NetworkMap)
|
||||||
|
|
||||||
|
envelope := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{
|
||||||
|
Components: components,
|
||||||
|
PeerConfig: legacyResp.NetworkMap.PeerConfig,
|
||||||
|
DNSDomain: "netbird.cloud",
|
||||||
|
})
|
||||||
|
componentsTotal := mustMarshalSize(t, envelope)
|
||||||
|
|
||||||
|
t.Logf("\n=== LEGACY NetworkMap (%d peers, %d groups) ===", peerCount, groupCount)
|
||||||
|
t.Logf(" Total: %d bytes\n", legacyTotal)
|
||||||
|
|
||||||
|
legacyBreakdown := []struct {
|
||||||
|
name string
|
||||||
|
nm *proto.NetworkMap
|
||||||
|
}{
|
||||||
|
{"RemotePeers", &proto.NetworkMap{RemotePeers: legacyResp.NetworkMap.RemotePeers}},
|
||||||
|
{"OfflinePeers", &proto.NetworkMap{OfflinePeers: legacyResp.NetworkMap.OfflinePeers}},
|
||||||
|
{"FirewallRules", &proto.NetworkMap{FirewallRules: legacyResp.NetworkMap.FirewallRules}},
|
||||||
|
{"Routes", &proto.NetworkMap{Routes: legacyResp.NetworkMap.Routes}},
|
||||||
|
{"RoutesFirewallRules", &proto.NetworkMap{RoutesFirewallRules: legacyResp.NetworkMap.RoutesFirewallRules}},
|
||||||
|
{"DNSConfig", &proto.NetworkMap{DNSConfig: legacyResp.NetworkMap.DNSConfig}},
|
||||||
|
{"PeerConfig", &proto.NetworkMap{PeerConfig: legacyResp.NetworkMap.PeerConfig}},
|
||||||
|
{"SshAuth", &proto.NetworkMap{SshAuth: legacyResp.NetworkMap.SshAuth}},
|
||||||
|
}
|
||||||
|
for _, e := range legacyBreakdown {
|
||||||
|
size := mustMarshalSize(t, e.nm)
|
||||||
|
t.Logf(" %-22s %8d bytes %5.1f%%", e.name, size, pct(size, legacyTotal))
|
||||||
|
}
|
||||||
|
|
||||||
|
full := envelope.GetFull()
|
||||||
|
if full == nil {
|
||||||
|
t.Fatalf("expected full network map envelope payload, got nil")
|
||||||
|
}
|
||||||
|
t.Logf("\n=== COMPONENTS NetworkMapEnvelope (%d peers, %d groups) ===", peerCount, groupCount)
|
||||||
|
t.Logf(" Total: %d bytes (%.1f%% of legacy)\n", componentsTotal, pct(componentsTotal, legacyTotal))
|
||||||
|
|
||||||
|
componentsBreakdown := []struct {
|
||||||
|
name string
|
||||||
|
nm *proto.NetworkMapComponentsFull
|
||||||
|
}{
|
||||||
|
{"Peers", &proto.NetworkMapComponentsFull{Peers: full.Peers}},
|
||||||
|
{"Policies", &proto.NetworkMapComponentsFull{Policies: full.Policies}},
|
||||||
|
{"Groups", &proto.NetworkMapComponentsFull{Groups: full.Groups}},
|
||||||
|
{"Routes (raw)", &proto.NetworkMapComponentsFull{Routes: full.Routes}},
|
||||||
|
{"NameServerGroups", &proto.NetworkMapComponentsFull{NameserverGroups: full.NameserverGroups}},
|
||||||
|
{"AllDNSRecords", &proto.NetworkMapComponentsFull{AllDnsRecords: full.AllDnsRecords}},
|
||||||
|
{"AccountZones", &proto.NetworkMapComponentsFull{AccountZones: full.AccountZones}},
|
||||||
|
{"NetworkResources", &proto.NetworkMapComponentsFull{NetworkResources: full.NetworkResources}},
|
||||||
|
{"RoutersMap", &proto.NetworkMapComponentsFull{RoutersMap: full.RoutersMap}},
|
||||||
|
{"ResourcePoliciesMap", &proto.NetworkMapComponentsFull{ResourcePoliciesMap: full.ResourcePoliciesMap}},
|
||||||
|
{"GroupIDToUserIDs", &proto.NetworkMapComponentsFull{GroupIdToUserIds: full.GroupIdToUserIds}},
|
||||||
|
{"AllowedUserIDs", &proto.NetworkMapComponentsFull{AllowedUserIds: full.AllowedUserIds}},
|
||||||
|
{"PostureFailedPeers", &proto.NetworkMapComponentsFull{PostureFailedPeers: full.PostureFailedPeers}},
|
||||||
|
{"DNSSettings", &proto.NetworkMapComponentsFull{DnsSettings: full.DnsSettings}},
|
||||||
|
{"PeerConfig", &proto.NetworkMapComponentsFull{PeerConfig: full.PeerConfig}},
|
||||||
|
{"AgentVersions", &proto.NetworkMapComponentsFull{AgentVersions: full.AgentVersions}},
|
||||||
|
}
|
||||||
|
for _, e := range componentsBreakdown {
|
||||||
|
size := mustMarshalSize(t, e.nm)
|
||||||
|
t.Logf(" %-22s %8d bytes %5.1f%%", e.name, size, pct(size, componentsTotal))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("\n=== Per-PeerCompact average ===")
|
||||||
|
if len(full.Peers) > 0 {
|
||||||
|
t.Logf(" PeerCompact avg: %d bytes/peer", mustMarshalSize(t, &proto.NetworkMapComponentsFull{Peers: full.Peers})/len(full.Peers))
|
||||||
|
}
|
||||||
|
if len(legacyResp.NetworkMap.RemotePeers) > 0 {
|
||||||
|
t.Logf(" RemotePeer avg: %d bytes/peer",
|
||||||
|
mustMarshalSize(t, &proto.NetworkMap{RemotePeers: legacyResp.NetworkMap.RemotePeers})/len(legacyResp.NetworkMap.RemotePeers))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("\n=== FirewallRule expansion footprint ===")
|
||||||
|
t.Logf(" legacy FirewallRules count: %d", len(legacyResp.NetworkMap.FirewallRules))
|
||||||
|
t.Logf(" components Policies count: %d", len(full.Policies))
|
||||||
|
t.Logf(" components Groups count: %d", len(full.Groups))
|
||||||
|
|
||||||
|
totalGroupPeerIdxs := 0
|
||||||
|
for _, g := range full.Groups {
|
||||||
|
totalGroupPeerIdxs += len(g.PeerIndexes)
|
||||||
|
}
|
||||||
|
t.Logf(" components peer-index refs across all groups: %d", totalGroupPeerIdxs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustMarshalSize(t *testing.T, m goproto.Message) int {
|
||||||
|
b, err := goproto.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
return len(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pct(part, total int) float64 {
|
||||||
|
if total == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 100 * float64(part) / float64(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stops fmt being unused if the breakdown loop above is later commented out.
|
||||||
|
var _ = fmt.Sprintf
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
// PeerNetworkMapResult is what the network_map controller produces for a
|
||||||
|
// single peer. Exactly one of NetworkMap or Components is populated depending
|
||||||
|
// on the peer's capability:
|
||||||
|
//
|
||||||
|
// - Components-capable peers (PeerCapabilityComponentNetworkMap) get
|
||||||
|
// Components: the raw types.NetworkMapComponents the client decodes and
|
||||||
|
// runs Calculate() on locally. NetworkMap stays nil — the server skips
|
||||||
|
// the expansion entirely.
|
||||||
|
// - Legacy peers (or any peer when the kill switch is set) get NetworkMap:
|
||||||
|
// the fully-expanded view the legacy gRPC path consumes.
|
||||||
|
//
|
||||||
|
// The gRPC layer (ToSyncResponseForPeer) dispatches by which field is
|
||||||
|
// non-nil; callers must not rely on both being set.
|
||||||
|
type PeerNetworkMapResult struct {
|
||||||
|
NetworkMap *NetworkMap
|
||||||
|
Components *NetworkMapComponents
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsComponents reports whether the result carries the components shape.
|
||||||
|
// Use this in preference to direct nil checks on the fields.
|
||||||
|
func (r PeerNetworkMapResult) IsComponents() bool {
|
||||||
|
return r.Components != nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package types_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// helper: marks the given peer as components-capable.
|
||||||
|
func markCapable(p *nbpeer.Peer) {
|
||||||
|
p.Meta.Capabilities = append(p.Meta.Capabilities, nbpeer.PeerCapabilityComponentNetworkMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPeerNetworkMapResult_CapablePeerGetsComponents(t *testing.T) {
|
||||||
|
account, validatedPeers := scalableTestAccount(10, 2)
|
||||||
|
markCapable(account.Peers["peer-0"])
|
||||||
|
|
||||||
|
resourcePolicies := account.GetResourcePoliciesMap()
|
||||||
|
routers := account.GetResourceRoutersMap()
|
||||||
|
groupIDToUserIDs := account.GetActiveGroupUsers()
|
||||||
|
|
||||||
|
result := account.GetPeerNetworkMapResult(
|
||||||
|
context.Background(),
|
||||||
|
"peer-0",
|
||||||
|
false, // componentsDisabled
|
||||||
|
nbdns.CustomZone{},
|
||||||
|
nil,
|
||||||
|
validatedPeers,
|
||||||
|
resourcePolicies,
|
||||||
|
routers,
|
||||||
|
nil,
|
||||||
|
groupIDToUserIDs,
|
||||||
|
)
|
||||||
|
|
||||||
|
require.True(t, result.IsComponents(), "capable peer must get the components shape")
|
||||||
|
assert.Nil(t, result.NetworkMap)
|
||||||
|
require.NotNil(t, result.Components)
|
||||||
|
assert.Equal(t, "peer-0", result.Components.PeerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPeerNetworkMapResult_LegacyPeerGetsNetworkMap(t *testing.T) {
|
||||||
|
account, validatedPeers := scalableTestAccount(10, 2)
|
||||||
|
// peer-0 left without the component capability
|
||||||
|
|
||||||
|
resourcePolicies := account.GetResourcePoliciesMap()
|
||||||
|
routers := account.GetResourceRoutersMap()
|
||||||
|
groupIDToUserIDs := account.GetActiveGroupUsers()
|
||||||
|
|
||||||
|
result := account.GetPeerNetworkMapResult(
|
||||||
|
context.Background(),
|
||||||
|
"peer-0",
|
||||||
|
false,
|
||||||
|
nbdns.CustomZone{},
|
||||||
|
nil,
|
||||||
|
validatedPeers,
|
||||||
|
resourcePolicies,
|
||||||
|
routers,
|
||||||
|
nil,
|
||||||
|
groupIDToUserIDs,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.False(t, result.IsComponents())
|
||||||
|
assert.Nil(t, result.Components)
|
||||||
|
require.NotNil(t, result.NetworkMap, "legacy peer must get a NetworkMap")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPeerNetworkMapResult_KillSwitchOverridesCapability(t *testing.T) {
|
||||||
|
// Capable peer + componentsDisabled=true → falls back to legacy.
|
||||||
|
account, validatedPeers := scalableTestAccount(10, 2)
|
||||||
|
markCapable(account.Peers["peer-0"])
|
||||||
|
|
||||||
|
resourcePolicies := account.GetResourcePoliciesMap()
|
||||||
|
routers := account.GetResourceRoutersMap()
|
||||||
|
groupIDToUserIDs := account.GetActiveGroupUsers()
|
||||||
|
|
||||||
|
result := account.GetPeerNetworkMapResult(
|
||||||
|
context.Background(),
|
||||||
|
"peer-0",
|
||||||
|
true, // componentsDisabled = true (kill switch)
|
||||||
|
nbdns.CustomZone{},
|
||||||
|
nil,
|
||||||
|
validatedPeers,
|
||||||
|
resourcePolicies,
|
||||||
|
routers,
|
||||||
|
nil,
|
||||||
|
groupIDToUserIDs,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.False(t, result.IsComponents(), "kill switch must force legacy NetworkMap path")
|
||||||
|
assert.Nil(t, result.Components)
|
||||||
|
require.NotNil(t, result.NetworkMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPeerNetworkMapResult_IsComponents(t *testing.T) {
|
||||||
|
assert.True(t, types.PeerNetworkMapResult{Components: &types.NetworkMapComponents{}}.IsComponents())
|
||||||
|
assert.False(t, types.PeerNetworkMapResult{NetworkMap: &types.NetworkMap{}}.IsComponents())
|
||||||
|
assert.False(t, types.PeerNetworkMapResult{}.IsComponents())
|
||||||
|
}
|
||||||
@@ -95,6 +95,7 @@ type Route struct {
|
|||||||
ID ID `gorm:"primaryKey"`
|
ID ID `gorm:"primaryKey"`
|
||||||
// AccountID is a reference to Account that this object belongs
|
// AccountID is a reference to Account that this object belongs
|
||||||
AccountID string `gorm:"index"`
|
AccountID string `gorm:"index"`
|
||||||
|
PublicID string `json:"-"`
|
||||||
// Network and Domains are mutually exclusive
|
// Network and Domains are mutually exclusive
|
||||||
Network netip.Prefix `gorm:"serializer:json"`
|
Network netip.Prefix `gorm:"serializer:json"`
|
||||||
Domains domain.List `gorm:"serializer:json"`
|
Domains domain.List `gorm:"serializer:json"`
|
||||||
@@ -128,6 +129,7 @@ func (r *Route) Copy() *Route {
|
|||||||
route := &Route{
|
route := &Route{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
AccountID: r.AccountID,
|
AccountID: r.AccountID,
|
||||||
|
PublicID: r.PublicID,
|
||||||
Description: r.Description,
|
Description: r.Description,
|
||||||
NetID: r.NetID,
|
NetID: r.NetID,
|
||||||
Network: r.Network,
|
Network: r.Network,
|
||||||
|
|||||||
@@ -316,33 +316,87 @@ func TestClient_Sync(t *testing.T) {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case resp := <-ch:
|
case resp := <-ch:
|
||||||
if resp.GetPeerConfig() == nil {
|
if resp.GetPeerConfig() == nil && resp.GetNetworkMap().GetPeerConfig() == nil {
|
||||||
t.Error("expecting non nil PeerConfig got nil")
|
t.Error("expecting non nil PeerConfig got nil")
|
||||||
}
|
}
|
||||||
if resp.GetNetbirdConfig() == nil {
|
if resp.GetNetbirdConfig() == nil {
|
||||||
t.Error("expecting non nil NetbirdConfig got nil")
|
t.Error("expecting non nil NetbirdConfig got nil")
|
||||||
}
|
}
|
||||||
// we test network map peers from 0.29.3 and dev builds
|
// Top-level RemotePeers is deprecated and must stay empty for
|
||||||
|
// v0.29.3+ (and dev) clients — the field rides inside NetworkMap
|
||||||
|
// (legacy) or the NetworkMapEnvelope (components) instead.
|
||||||
if len(resp.GetRemotePeers()) != 0 {
|
if len(resp.GetRemotePeers()) != 0 {
|
||||||
t.Error("expecting top-level RemotePeers to be empty for v0.29.3+ clients")
|
t.Error("expecting top-level RemotePeers to be empty for v0.29.3+ clients")
|
||||||
}
|
}
|
||||||
networkMap := resp.GetNetworkMap()
|
// Component-capable clients receive a NetworkMapEnvelope; the
|
||||||
if len(networkMap.GetRemotePeers()) != 1 {
|
// remote-peers list is encoded inside it. Decode it and check the
|
||||||
t.Errorf("expecting RemotePeers size %d got %d", 1, len(networkMap.GetRemotePeers()))
|
// envelope's peers slice. Legacy peers populate NetworkMap.RemotePeers;
|
||||||
|
// both shapes must surface exactly one remote peer.
|
||||||
|
remotePeerKeys := remotePeerKeysFromSync(resp, testKey.PublicKey().String())
|
||||||
|
if len(remotePeerKeys) != 1 {
|
||||||
|
t.Errorf("expecting RemotePeers size %d got %d", 1, len(remotePeerKeys))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if resp.GetNetworkMap() != nil && resp.GetNetworkMap().GetRemotePeersIsEmpty() {
|
||||||
if networkMap.GetRemotePeersIsEmpty() {
|
|
||||||
t.Error("expecting RemotePeers property to be false, got true")
|
t.Error("expecting RemotePeers property to be false, got true")
|
||||||
}
|
}
|
||||||
if networkMap.GetRemotePeers()[0].GetWgPubKey() != remoteKey.PublicKey().String() {
|
if remotePeerKeys[0] != remoteKey.PublicKey().String() {
|
||||||
t.Errorf("expecting RemotePeer public key %s got %s", remoteKey.PublicKey().String(), networkMap.GetRemotePeers()[0].GetWgPubKey())
|
t.Errorf("expecting RemotePeer public key %s got %s", remoteKey.PublicKey().String(), remotePeerKeys[0])
|
||||||
}
|
}
|
||||||
case <-time.After(3 * time.Second):
|
case <-time.After(3 * time.Second):
|
||||||
t.Error("timeout waiting for test to finish")
|
t.Error("timeout waiting for test to finish")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remotePeerKeysFromSync extracts the remote-peer WG keys from either the
|
||||||
|
// legacy NetworkMap.RemotePeers list or the components NetworkMapEnvelope's
|
||||||
|
// inner peers slice (filtering out the local receiving peer identified by
|
||||||
|
// localKey, since the envelope's peers list is index-addressed and includes
|
||||||
|
// the local peer alongside remotes).
|
||||||
|
func remotePeerKeysFromSync(resp *mgmtProto.SyncResponse, localKey string) []string {
|
||||||
|
if rp := resp.GetRemotePeers(); len(rp) > 0 {
|
||||||
|
out := make([]string, 0, len(rp))
|
||||||
|
for _, p := range rp {
|
||||||
|
out = append(out, p.GetWgPubKey())
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
if rp := resp.GetNetworkMap().GetRemotePeers(); len(rp) > 0 {
|
||||||
|
out := make([]string, 0, len(rp))
|
||||||
|
for _, p := range rp {
|
||||||
|
out = append(out, p.GetWgPubKey())
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
env := resp.GetNetworkMapEnvelope().GetFull()
|
||||||
|
if env == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(env.GetPeers()))
|
||||||
|
for _, p := range env.GetPeers() {
|
||||||
|
key := wgKeyFromBytes(p.GetWgPubKey())
|
||||||
|
if key == "" || key == localKey {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, key)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// wgKeyFromBytes mirrors the client-side decoder: the envelope ships raw 32
|
||||||
|
// bytes; reconstruct the standard base64 key the test compares against.
|
||||||
|
func wgKeyFromBytes(raw []byte) string {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var k wgtypes.Key
|
||||||
|
if len(raw) != len(k) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
copy(k[:], raw)
|
||||||
|
return k.String()
|
||||||
|
}
|
||||||
|
|
||||||
func Test_SystemMetaDataFromClient(t *testing.T) {
|
func Test_SystemMetaDataFromClient(t *testing.T) {
|
||||||
s, lis, mgmtMockServer, serverKey := startMockManagement(t)
|
s, lis, mgmtMockServer, serverKey := startMockManagement(t)
|
||||||
defer s.GracefulStop()
|
defer s.GracefulStop()
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
"github.com/netbirdio/netbird/encryption"
|
"github.com/netbirdio/netbird/encryption"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
|
nbmgmtgrpc "github.com/netbirdio/netbird/shared/management/grpc"
|
||||||
"github.com/netbirdio/netbird/shared/management/proto"
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
"github.com/netbirdio/netbird/util/wsproxy"
|
"github.com/netbirdio/netbird/util/wsproxy"
|
||||||
)
|
)
|
||||||
@@ -1026,6 +1027,8 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Capabilities: peerCapabilities(*info),
|
Capabilities: peerCapabilities(*info),
|
||||||
|
|
||||||
|
SyncMessageVersion: syncMessageVersion(*info),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1039,3 +1042,10 @@ func peerCapabilities(info system.Info) []proto.PeerCapability {
|
|||||||
}
|
}
|
||||||
return caps
|
return caps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func syncMessageVersion(info system.Info) int32 {
|
||||||
|
if info.SyncMessageVersion != nil {
|
||||||
|
return int32(*info.SyncMessageVersion)
|
||||||
|
}
|
||||||
|
return int32(nbmgmtgrpc.HighestSyncMessageVersion)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SyncMessageVersion uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
Base SyncMessageVersion = iota
|
||||||
|
ComponentNetworkMap
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultSyncMessageVersion = Base
|
||||||
|
const HighestSyncMessageVersion = ComponentNetworkMap
|
||||||
|
|
||||||
|
var ErrorUnrecognizedSyncMessageVersion = errors.New("unrecognized SyncMessageVersion")
|
||||||
|
|
||||||
|
func ValidateSyncMessageVersion(v *int) error {
|
||||||
|
// empty list == we support all available versions
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if *v < 0 || *v > int(HighestSyncMessageVersion) {
|
||||||
|
return fmt.Errorf("sync message version must between 0 and %d, %w", HighestSyncMessageVersion, ErrorUnrecognizedSyncMessageVersion)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns SyncMessage version from config, or highest available version if the config is missing or
|
||||||
|
// base if it is invalid
|
||||||
|
// the assumption is ValidateSyncMessageVersion() has been called before using SyncMessageVersionFromConfig()
|
||||||
|
func SyncMessageVersionFromConfig(v *int) SyncMessageVersion {
|
||||||
|
if v == nil {
|
||||||
|
return DefaultSyncMessageVersion
|
||||||
|
}
|
||||||
|
if *v < 0 || *v > int(HighestSyncMessageVersion) {
|
||||||
|
return Base
|
||||||
|
}
|
||||||
|
|
||||||
|
return SyncMessageVersion(*v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert per-account supported versions to SyncMessageVersion
|
||||||
|
// the assumption is ValidateSyncMessageVersion() has been called before using SyncMessageVersionsFromMap()
|
||||||
|
func SyncMessageVersionsFromMap(toconvert map[string]int) map[string]SyncMessageVersion {
|
||||||
|
// no per-account overrides
|
||||||
|
if len(toconvert) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
toret := make(map[string]SyncMessageVersion)
|
||||||
|
|
||||||
|
for account, version := range toconvert {
|
||||||
|
toret[account] = SyncMessageVersionFromConfig(&version)
|
||||||
|
}
|
||||||
|
return toret
|
||||||
|
}
|
||||||
|
|
||||||
|
// return highest common sync message version, or Default (which is always available)
|
||||||
|
func HighestCommonSyncMessageVersion(a SyncMessageVersion, b SyncMessageVersion) SyncMessageVersion {
|
||||||
|
if a > b {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidation(t *testing.T) {
|
||||||
|
assert.NoError(t, ValidateSyncMessageVersion(nil))
|
||||||
|
assert.NoError(t, ValidateSyncMessageVersion(toIntPtr(0)))
|
||||||
|
assert.NoError(t, ValidateSyncMessageVersion(toIntPtr(1)))
|
||||||
|
assert.ErrorIs(t, ValidateSyncMessageVersion(toIntPtr(int(^uint(0)>>1))), ErrorUnrecognizedSyncMessageVersion)
|
||||||
|
assert.ErrorIs(t, ValidateSyncMessageVersion(toIntPtr(-1)), ErrorUnrecognizedSyncMessageVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersionFromConfig(t *testing.T) {
|
||||||
|
assert.Equal(t, DefaultSyncMessageVersion, SyncMessageVersionFromConfig(nil))
|
||||||
|
assert.Equal(t, Base, SyncMessageVersionFromConfig(toIntPtr(0)))
|
||||||
|
assert.Equal(t, ComponentNetworkMap, SyncMessageVersionFromConfig(toIntPtr(1)))
|
||||||
|
assert.Equal(t, DefaultSyncMessageVersion, SyncMessageVersionFromConfig(toIntPtr(-1)))
|
||||||
|
assert.Equal(t, DefaultSyncMessageVersion, SyncMessageVersionFromConfig(toIntPtr(int(^uint(0)>>1))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPerAccountConversionStringToEnum(t *testing.T) {
|
||||||
|
assert.Equal(t, map[string]SyncMessageVersion{"1": HighestSyncMessageVersion}, SyncMessageVersionsFromMap(map[string]int{"1": 1}))
|
||||||
|
assert.Equal(t, map[string]SyncMessageVersion{"2": DefaultSyncMessageVersion}, SyncMessageVersionsFromMap(map[string]int{"2": -1}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommonVersions(t *testing.T) {
|
||||||
|
assert.Equal(t, Base, HighestCommonSyncMessageVersion(Base, HighestSyncMessageVersion))
|
||||||
|
assert.Equal(t, Base, HighestCommonSyncMessageVersion(HighestSyncMessageVersion, Base))
|
||||||
|
assert.Equal(t, Base, HighestCommonSyncMessageVersion(Base, Base))
|
||||||
|
assert.Equal(t, HighestSyncMessageVersion, HighestCommonSyncMessageVersion(HighestSyncMessageVersion, HighestSyncMessageVersion))
|
||||||
|
}
|
||||||
|
|
||||||
|
func toIntPtr(v int) *int {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
@@ -0,0 +1,550 @@
|
|||||||
|
package networkmap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||||
|
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
nbroute "github.com/netbirdio/netbird/route"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DecodeEnvelope converts a NetworkMapEnvelope into a NetworkMapComponents
|
||||||
|
// the client can run Calculate() over. Every ID-reference on the wire is a
|
||||||
|
// xid from corresponding public_id field.
|
||||||
|
//
|
||||||
|
// ID scheme on the client side:
|
||||||
|
//
|
||||||
|
// Peers base64(wg_pub_key) // stable across snapshots
|
||||||
|
func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, error) {
|
||||||
|
full := env.GetFull()
|
||||||
|
if full == nil {
|
||||||
|
return nil, fmt.Errorf("envelope has no Full payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &types.NetworkMapComponents{
|
||||||
|
PeerID: "", // engine fills its own peer id from PeerConfig
|
||||||
|
Network: decodeAccountNetwork(full.Network),
|
||||||
|
AccountSettings: decodeAccountSettings(full.AccountSettings),
|
||||||
|
CustomZoneDomain: full.CustomZoneDomain,
|
||||||
|
Peers: make(map[string]*nbpeer.Peer, len(full.Peers)),
|
||||||
|
Groups: make(map[string]*types.Group, 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)),
|
||||||
|
AllDNSRecords: decodeSimpleRecords(full.AllDnsRecords),
|
||||||
|
AccountZones: decodeCustomZones(full.AccountZones),
|
||||||
|
ResourcePoliciesMap: make(map[string][]*types.Policy),
|
||||||
|
RoutersMap: make(map[string]map[string]*routerTypes.NetworkRouter),
|
||||||
|
NetworkResources: make([]*resourceTypes.NetworkResource, 0, len(full.NetworkResources)),
|
||||||
|
RouterPeers: make(map[string]*nbpeer.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{
|
||||||
|
DisabledManagementGroups: full.DnsSettings.DisabledManagementGroupIds,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.DNSSettings = &types.DNSSettings{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1: peers. The envelope's peers slice is index-addressed on the
|
||||||
|
// wire; we re-key by the peer's WireGuard public key (base64) so the
|
||||||
|
// in-memory components struct uses a stable identifier across
|
||||||
|
// snapshots. peerIDByIndex lets downstream phases resolve wire indexes
|
||||||
|
// back to that key. A peer with a missing or malformed wg_pub_key is
|
||||||
|
// skipped (and its index keeps "" so any cross-reference falls into the
|
||||||
|
// same missing-peer branch downstream) — matches legacy behaviour, which
|
||||||
|
// degrades gracefully rather than aborting the whole sync on a single
|
||||||
|
// bad row.
|
||||||
|
peerIDByIndex := make([]string, len(full.Peers))
|
||||||
|
for idx, pc := range full.Peers {
|
||||||
|
if pc == nil {
|
||||||
|
log.Warnf("envelope: peers[%d] is nil, skipping", idx)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(pc.WgPubKey) != 32 {
|
||||||
|
log.Warnf("envelope: peers[%d] wg_pub_key length %d (want 32), skipping", idx, len(pc.WgPubKey))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
peerID := base64.StdEncoding.EncodeToString(pc.WgPubKey)
|
||||||
|
peer := decodePeerCompact(pc, peerID)
|
||||||
|
c.Peers[peerID] = peer
|
||||||
|
peerIDByIndex[idx] = peerID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: groups.
|
||||||
|
for i, gc := range full.Groups {
|
||||||
|
if gc == nil {
|
||||||
|
return nil, fmt.Errorf("invalid envelope: groups[%d] is nil", i)
|
||||||
|
}
|
||||||
|
groupID := gc.Id
|
||||||
|
peerIDs := make([]string, 0, len(gc.PeerIndexes))
|
||||||
|
for _, idx := range gc.PeerIndexes {
|
||||||
|
if int(idx) < len(peerIDByIndex) {
|
||||||
|
peerIDs = append(peerIDs, peerIDByIndex[idx])
|
||||||
|
} else {
|
||||||
|
log.WithField("peer idx", idx).Error("unrecognized peer idx during decoding")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group := &types.Group{
|
||||||
|
ID: groupID,
|
||||||
|
PublicID: gc.Id,
|
||||||
|
Peers: peerIDs,
|
||||||
|
}
|
||||||
|
if gc.IsAll {
|
||||||
|
group.Name = types.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))
|
||||||
|
for i, pc := range full.Policies {
|
||||||
|
if pc == nil {
|
||||||
|
return nil, fmt.Errorf("invalid envelope: policies[%d] is nil", i)
|
||||||
|
}
|
||||||
|
policy := decodePolicyCompact(pc, pc.Id, peerIDByIndex)
|
||||||
|
c.Policies = append(c.Policies, policy)
|
||||||
|
policyByID[pc.Id] = policy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 4: routes.
|
||||||
|
for i, rr := range full.Routes {
|
||||||
|
if rr == nil {
|
||||||
|
return nil, fmt.Errorf("invalid envelope: routes[%d] is nil", i)
|
||||||
|
}
|
||||||
|
c.Routes = append(c.Routes, decodeRouteRaw(rr, peerIDByIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 5: NSGs.
|
||||||
|
for i, nsg := range full.NameserverGroups {
|
||||||
|
if nsg == nil {
|
||||||
|
return nil, fmt.Errorf("invalid envelope: nameserver_groups[%d] is nil", i)
|
||||||
|
}
|
||||||
|
c.NameServerGroups = append(c.NameServerGroups, decodeNameServerGroupRaw(nsg))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 6: network resources.
|
||||||
|
for i, nr := range full.NetworkResources {
|
||||||
|
if nr == nil {
|
||||||
|
return nil, fmt.Errorf("invalid envelope: network_resources[%d] is nil", i)
|
||||||
|
}
|
||||||
|
c.NetworkResources = append(c.NetworkResources, decodeNetworkResource(nr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]*routerTypes.NetworkRouter, len(list.Entries))
|
||||||
|
for _, entry := range list.Entries {
|
||||||
|
if !entry.PeerIndexSet {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if int(entry.PeerIndex) >= len(peerIDByIndex) {
|
||||||
|
log.WithField("peer idx", entry.PeerIndex).Error("unrecognized peer id when decoding router map")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
peerID := peerIDByIndex[entry.PeerIndex]
|
||||||
|
inner[peerID] = &routerTypes.NetworkRouter{
|
||||||
|
ID: "",
|
||||||
|
NetworkID: networkID,
|
||||||
|
PublicID: entry.Id,
|
||||||
|
Peer: peerID,
|
||||||
|
PeerGroups: entry.PeerGroupIds,
|
||||||
|
Masquerade: entry.Masquerade,
|
||||||
|
Metric: int(entry.Metric),
|
||||||
|
Enabled: entry.Enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(inner) > 0 {
|
||||||
|
c.RoutersMap[networkID] = inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 8: resource_policies_map (resource seq id → list of *types.Policy
|
||||||
|
// pointers from the decoded policies slice). Resource ID is synthesized
|
||||||
|
// the same way as in decodeNetworkResource.
|
||||||
|
for resourceID, ids := range full.ResourcePoliciesMap {
|
||||||
|
if len(ids.Ids) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
policies := make([]*types.Policy, 0, len(ids.Ids))
|
||||||
|
for _, id := range ids.Ids {
|
||||||
|
if p, ok := policyByID[id]; ok {
|
||||||
|
policies = append(policies, p)
|
||||||
|
} else {
|
||||||
|
log.WithField("policy id", id).Error("unrecognized policy when decoding resource policies")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(policies) > 0 {
|
||||||
|
c.ResourcePoliciesMap[resourceID] = 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...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 10: posture_failed_peers — wire keys are posture-check seq ids,
|
||||||
|
// values are peer indexes that need to be turned into peer ids. PolicyRule
|
||||||
|
// SourcePostureChecks (also synth ids) reference the same key space.
|
||||||
|
for checkID, set := range full.PostureFailedPeers {
|
||||||
|
failed := make(map[string]struct{}, len(set.PeerIndexes))
|
||||||
|
for _, idx := range set.PeerIndexes {
|
||||||
|
if int(idx) < len(peerIDByIndex) {
|
||||||
|
failed[peerIDByIndex[idx]] = struct{}{}
|
||||||
|
} else {
|
||||||
|
log.WithField("peer idx", idx).Error("unrecognized peer when decoding posture failed peers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(failed) > 0 {
|
||||||
|
c.PostureFailedPeers[checkID] = failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 11: router_peer_indexes — peers that act as routers. They're
|
||||||
|
// already in c.Peers (router peers are appended to the global peers
|
||||||
|
// list by the encoder); RouterPeers is the subset.
|
||||||
|
for _, idx := range full.RouterPeerIndexes {
|
||||||
|
if int(idx) < len(peerIDByIndex) {
|
||||||
|
peerID := peerIDByIndex[idx]
|
||||||
|
c.RouterPeers[peerID] = c.Peers[peerID]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeAccountNetwork(an *proto.AccountNetwork) *types.Network {
|
||||||
|
if an == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
n := &types.Network{
|
||||||
|
Identifier: an.Identifier,
|
||||||
|
Dns: an.Dns,
|
||||||
|
Serial: an.Serial,
|
||||||
|
}
|
||||||
|
if an.NetCidr != "" {
|
||||||
|
if _, ipnet, err := net.ParseCIDR(an.NetCidr); err == nil && ipnet != nil {
|
||||||
|
n.Net = *ipnet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if an.NetV6Cidr != "" {
|
||||||
|
if _, ipnet, err := net.ParseCIDR(an.NetV6Cidr); err == nil && ipnet != nil {
|
||||||
|
n.NetV6 = *ipnet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeAccountSettings(as *proto.AccountSettingsCompact) *types.AccountSettingsInfo {
|
||||||
|
if as == nil {
|
||||||
|
return &types.AccountSettingsInfo{}
|
||||||
|
}
|
||||||
|
return &types.AccountSettingsInfo{
|
||||||
|
PeerLoginExpirationEnabled: as.PeerLoginExpirationEnabled,
|
||||||
|
PeerLoginExpiration: time.Duration(as.PeerLoginExpirationNs),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePeerCompact(pc *proto.PeerCompact, peerID string) *nbpeer.Peer {
|
||||||
|
var caps []int32
|
||||||
|
if pc.SupportsSourcePrefixes {
|
||||||
|
caps = append(caps, nbpeer.PeerCapabilitySourcePrefixes)
|
||||||
|
}
|
||||||
|
if pc.SupportsIpv6 {
|
||||||
|
caps = append(caps, nbpeer.PeerCapabilityIPv6Overlay)
|
||||||
|
}
|
||||||
|
peer := &nbpeer.Peer{
|
||||||
|
ID: peerID,
|
||||||
|
Key: peerID,
|
||||||
|
SSHKey: string(pc.SshPubKey),
|
||||||
|
SSHEnabled: pc.SshEnabled,
|
||||||
|
DNSLabel: pc.DnsLabel,
|
||||||
|
LoginExpirationEnabled: pc.LoginExpirationEnabled,
|
||||||
|
Meta: nbpeer.PeerSystemMeta{
|
||||||
|
WtVersion: pc.AgentVersion,
|
||||||
|
Capabilities: caps,
|
||||||
|
Flags: nbpeer.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 {
|
||||||
|
t := time.Unix(0, pc.LastLoginUnixNano)
|
||||||
|
peer.LastLogin = &t
|
||||||
|
}
|
||||||
|
switch len(pc.Ip) {
|
||||||
|
case 4:
|
||||||
|
peer.IP = netip.AddrFrom4([4]byte{pc.Ip[0], pc.Ip[1], pc.Ip[2], pc.Ip[3]})
|
||||||
|
case 16:
|
||||||
|
var a [16]byte
|
||||||
|
copy(a[:], pc.Ip)
|
||||||
|
peer.IP = netip.AddrFrom16(a)
|
||||||
|
}
|
||||||
|
if len(pc.Ipv6) == 16 {
|
||||||
|
var a [16]byte
|
||||||
|
copy(a[:], pc.Ipv6)
|
||||||
|
peer.IPv6 = netip.AddrFrom16(a)
|
||||||
|
}
|
||||||
|
return peer
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePolicyCompact(pc *proto.PolicyCompact, policyID string, peerIDByIndex []string) *types.Policy {
|
||||||
|
rule := &types.PolicyRule{
|
||||||
|
ID: policyID, // 1 rule per policy → reuse synthesized id
|
||||||
|
PolicyID: policyID,
|
||||||
|
Enabled: true,
|
||||||
|
Action: actionFromProto(pc.Action),
|
||||||
|
Protocol: protocolFromProto(pc.Protocol),
|
||||||
|
Bidirectional: pc.Bidirectional,
|
||||||
|
Ports: uint32SliceToStrings(pc.Ports),
|
||||||
|
PortRanges: portRangesFromProto(pc.PortRanges),
|
||||||
|
Sources: pc.SourceGroupIds,
|
||||||
|
Destinations: pc.DestinationGroupIds,
|
||||||
|
AuthorizedUser: pc.AuthorizedUser,
|
||||||
|
AuthorizedGroups: authorizedGroupsFromProto(pc.AuthorizedGroups),
|
||||||
|
SourceResource: resourceFromProto(pc.SourceResource, peerIDByIndex),
|
||||||
|
DestinationResource: resourceFromProto(pc.DestinationResource, peerIDByIndex),
|
||||||
|
}
|
||||||
|
return &types.Policy{
|
||||||
|
ID: policyID,
|
||||||
|
PublicID: pc.Id,
|
||||||
|
Enabled: true,
|
||||||
|
Rules: []*types.PolicyRule{rule},
|
||||||
|
SourcePostureChecks: pc.SourcePostureCheckIds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{}
|
||||||
|
}
|
||||||
|
out := types.Resource{Type: types.ResourceType(r.Type)}
|
||||||
|
if r.PeerIndexSet && int(r.PeerIndex) < len(peerIDByIndex) {
|
||||||
|
out.ID = peerIDByIndex[r.PeerIndex]
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// authorizedGroupsFromProto inverts encodeAuthorizedGroups: the wire form
|
||||||
|
// keys by group account_seq_id, the typed PolicyRule field keys by group
|
||||||
|
// xid string. We rebuild using the same synthetic scheme the rest of the
|
||||||
|
// decoder uses ("g<seq>").
|
||||||
|
func authorizedGroupsFromProto(m map[string]*proto.UserNameList) map[string][]string {
|
||||||
|
if len(m) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[string][]string, len(m))
|
||||||
|
for id, list := range m {
|
||||||
|
if list == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[id] = append([]string(nil), list.Names...)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeRouteRaw(rr *proto.RouteRaw, peerIDByIndex []string) *nbroute.Route {
|
||||||
|
r := &nbroute.Route{
|
||||||
|
ID: nbroute.ID(rr.Id),
|
||||||
|
PublicID: rr.Id,
|
||||||
|
NetID: nbroute.NetID(rr.NetId),
|
||||||
|
Description: rr.Description,
|
||||||
|
Domains: domainsFromPunycode(rr.Domains),
|
||||||
|
KeepRoute: rr.KeepRoute,
|
||||||
|
NetworkType: nbroute.NetworkType(rr.NetworkType),
|
||||||
|
Masquerade: rr.Masquerade,
|
||||||
|
Metric: int(rr.Metric),
|
||||||
|
Enabled: rr.Enabled,
|
||||||
|
Groups: rr.GroupIds,
|
||||||
|
AccessControlGroups: rr.AccessControlGroupIds,
|
||||||
|
PeerGroups: rr.PeerGroupIds,
|
||||||
|
SkipAutoApply: rr.SkipAutoApply,
|
||||||
|
}
|
||||||
|
if rr.NetworkCidr != "" {
|
||||||
|
if p, err := netip.ParsePrefix(rr.NetworkCidr); err == nil {
|
||||||
|
r.Network = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rr.PeerIndexSet && int(rr.PeerIndex) < len(peerIDByIndex) {
|
||||||
|
r.Peer = peerIDByIndex[rr.PeerIndex]
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeNameServerGroupRaw(nsg *proto.NameServerGroupRaw) *nbdns.NameServerGroup {
|
||||||
|
out := &nbdns.NameServerGroup{
|
||||||
|
ID: nsg.Id,
|
||||||
|
PublicID: nsg.Id,
|
||||||
|
Groups: nsg.GroupIds,
|
||||||
|
Primary: nsg.Primary,
|
||||||
|
Domains: nsg.Domains,
|
||||||
|
Enabled: nsg.Enabled,
|
||||||
|
SearchDomainsEnabled: nsg.SearchDomainsEnabled,
|
||||||
|
NameServers: make([]nbdns.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{
|
||||||
|
IP: addr,
|
||||||
|
NSType: nbdns.NameServerType(ns.NSType),
|
||||||
|
Port: int(ns.Port),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeNetworkResource(nr *proto.NetworkResourceRaw) *resourceTypes.NetworkResource {
|
||||||
|
out := &resourceTypes.NetworkResource{
|
||||||
|
ID: nr.Id,
|
||||||
|
PublicID: nr.Id,
|
||||||
|
NetworkID: nr.NetworkSeq,
|
||||||
|
Name: nr.Name,
|
||||||
|
Description: nr.Description,
|
||||||
|
Type: resourceTypes.NetworkResourceType(nr.Type),
|
||||||
|
Address: nr.Address,
|
||||||
|
Domain: nr.DomainValue,
|
||||||
|
Enabled: nr.Enabled,
|
||||||
|
}
|
||||||
|
if nr.PrefixCidr != "" {
|
||||||
|
if p, err := netip.ParsePrefix(nr.PrefixCidr); err == nil {
|
||||||
|
out.Prefix = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeSimpleRecords(records []*proto.SimpleRecord) []nbdns.SimpleRecord {
|
||||||
|
out := make([]nbdns.SimpleRecord, 0, len(records))
|
||||||
|
for _, r := range records {
|
||||||
|
out = append(out, nbdns.SimpleRecord{
|
||||||
|
Name: r.Name,
|
||||||
|
Type: int(r.Type),
|
||||||
|
Class: r.Class,
|
||||||
|
TTL: int(r.TTL),
|
||||||
|
RData: r.RData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeCustomZones(zones []*proto.CustomZone) []nbdns.CustomZone {
|
||||||
|
out := make([]nbdns.CustomZone, 0, len(zones))
|
||||||
|
for _, z := range zones {
|
||||||
|
out = append(out, nbdns.CustomZone{
|
||||||
|
Domain: z.Domain,
|
||||||
|
Records: decodeSimpleRecords(z.Records),
|
||||||
|
SearchDomainDisabled: z.SearchDomainDisabled,
|
||||||
|
NonAuthoritative: z.NonAuthoritative,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func uint32SliceToStrings(ports []uint32) []string {
|
||||||
|
if len(ports) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]string, len(ports))
|
||||||
|
for i, p := range ports {
|
||||||
|
out[i] = strconv.FormatUint(uint64(p), 10)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func portRangesFromProto(ranges []*proto.PortInfo_Range) []types.RulePortRange {
|
||||||
|
if len(ranges) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]types.RulePortRange, 0, len(ranges))
|
||||||
|
for _, r := range ranges {
|
||||||
|
if r == nil || r.Start > 65535 || r.End > 65535 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, types.RulePortRange{
|
||||||
|
Start: uint16(r.Start),
|
||||||
|
End: uint16(r.End),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func actionFromProto(a proto.RuleAction) types.PolicyTrafficActionType {
|
||||||
|
if a == proto.RuleAction_DROP {
|
||||||
|
return types.PolicyTrafficActionDrop
|
||||||
|
}
|
||||||
|
return types.PolicyTrafficActionAccept
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolFromProto(p proto.RuleProtocol) types.PolicyRuleProtocolType {
|
||||||
|
switch p {
|
||||||
|
case proto.RuleProtocol_TCP:
|
||||||
|
return types.PolicyRuleProtocolTCP
|
||||||
|
case proto.RuleProtocol_UDP:
|
||||||
|
return types.PolicyRuleProtocolUDP
|
||||||
|
case proto.RuleProtocol_ICMP:
|
||||||
|
return types.PolicyRuleProtocolICMP
|
||||||
|
case proto.RuleProtocol_ALL:
|
||||||
|
return types.PolicyRuleProtocolALL
|
||||||
|
case proto.RuleProtocol_NETBIRD_SSH:
|
||||||
|
return types.PolicyRuleProtocolNetbirdSSH
|
||||||
|
default:
|
||||||
|
return types.PolicyRuleProtocolALL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringSliceToSet(s []string) map[string]struct{} {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[string]struct{}, len(s))
|
||||||
|
for _, v := range s {
|
||||||
|
out[v] = struct{}{}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// domainsFromPunycode is a thin wrapper that converts a punycode list back to
|
||||||
|
// the domain.List type the route.Route struct expects. It accepts the
|
||||||
|
// punycode strings as-is (no extra decoding) — symmetric with
|
||||||
|
// route.Domains.ToPunycodeList() used in the encoder.
|
||||||
|
func domainsFromPunycode(punycoded []string) domain.List {
|
||||||
|
if len(punycoded) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(domain.List, 0, len(punycoded))
|
||||||
|
for _, d := range punycoded {
|
||||||
|
out = append(out, domain.Domain(d))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
// Package networkmap contains the shared NetworkMap helpers that both the
|
||||||
|
// management server and the client agent need.
|
||||||
|
//
|
||||||
|
// The proto-conversion helpers (types.NetworkMap → proto.NetworkMap) live
|
||||||
|
// here so the client can run the same conversion locally after deriving its
|
||||||
|
// NetworkMap from a NetworkMapEnvelope, without taking a dependency on the
|
||||||
|
// server-side conversion package (which pulls in cloud integrations and is
|
||||||
|
// otherwise an unwanted internal import on the client).
|
||||||
|
//
|
||||||
|
// The helpers are pure functions over inputs — no caches, no IO, no logging
|
||||||
|
// beyond a context-aware error log when an individual user-id hash fails.
|
||||||
|
package networkmap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
goproto "google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/types"
|
||||||
|
nbroute "github.com/netbirdio/netbird/route"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
"github.com/netbirdio/netbird/shared/netiputil"
|
||||||
|
"github.com/netbirdio/netbird/shared/sshauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToProtocolRoutes converts a slice of typed routes to their proto form.
|
||||||
|
func ToProtocolRoutes(routes []*nbroute.Route) []*proto.Route {
|
||||||
|
protoRoutes := make([]*proto.Route, 0, len(routes))
|
||||||
|
for _, r := range routes {
|
||||||
|
protoRoutes = append(protoRoutes, ToProtocolRoute(r))
|
||||||
|
}
|
||||||
|
return protoRoutes
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToProtocolRoute converts one typed route to its proto form.
|
||||||
|
func ToProtocolRoute(route *nbroute.Route) *proto.Route {
|
||||||
|
return &proto.Route{
|
||||||
|
ID: string(route.ID),
|
||||||
|
NetID: string(route.NetID),
|
||||||
|
Network: route.Network.String(),
|
||||||
|
Domains: route.Domains.ToPunycodeList(),
|
||||||
|
NetworkType: int64(route.NetworkType),
|
||||||
|
Peer: route.Peer,
|
||||||
|
Metric: int64(route.Metric),
|
||||||
|
Masquerade: route.Masquerade,
|
||||||
|
KeepRoute: route.KeepRoute,
|
||||||
|
SkipAutoApply: route.SkipAutoApply,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToProtocolFirewallRules converts the firewall rules to the protocol form.
|
||||||
|
// When useSourcePrefixes is true, the compact SourcePrefixes field is
|
||||||
|
// populated alongside the deprecated PeerIP for forward compatibility.
|
||||||
|
// Wildcard rules ("0.0.0.0") are expanded into separate v4/v6 SourcePrefixes
|
||||||
|
// when includeIPv6 is true.
|
||||||
|
func ToProtocolFirewallRules(rules []*types.FirewallRule, includeIPv6, useSourcePrefixes bool) []*proto.FirewallRule {
|
||||||
|
result := make([]*proto.FirewallRule, 0, len(rules))
|
||||||
|
for i := range rules {
|
||||||
|
rule := rules[i]
|
||||||
|
|
||||||
|
fwRule := &proto.FirewallRule{
|
||||||
|
PolicyID: []byte(rule.PolicyID),
|
||||||
|
PeerIP: rule.PeerIP, //nolint:staticcheck // populated for backward compatibility
|
||||||
|
Direction: GetProtoDirection(rule.Direction),
|
||||||
|
Action: GetProtoAction(rule.Action),
|
||||||
|
Protocol: GetProtoProtocol(rule.Protocol),
|
||||||
|
Port: rule.Port,
|
||||||
|
}
|
||||||
|
|
||||||
|
if useSourcePrefixes && rule.PeerIP != "" {
|
||||||
|
result = append(result, populateSourcePrefixes(fwRule, rule, includeIPv6)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ShouldUsePortRange(fwRule) {
|
||||||
|
fwRule.PortInfo = rule.PortRange.ToProto()
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, fwRule)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// populateSourcePrefixes sets SourcePrefixes on fwRule and returns any
|
||||||
|
// additional rules needed (e.g. a v6 wildcard clone when the peer IP is
|
||||||
|
// unspecified).
|
||||||
|
func populateSourcePrefixes(fwRule *proto.FirewallRule, rule *types.FirewallRule, includeIPv6 bool) []*proto.FirewallRule {
|
||||||
|
addr, err := netip.ParseAddr(rule.PeerIP)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !addr.IsUnspecified() {
|
||||||
|
fwRule.SourcePrefixes = [][]byte{netiputil.EncodeAddr(addr.Unmap())}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v4Wildcard, _ := netiputil.EncodePrefix(netip.PrefixFrom(netip.IPv4Unspecified(), 0))
|
||||||
|
fwRule.SourcePrefixes = [][]byte{v4Wildcard}
|
||||||
|
|
||||||
|
if !includeIPv6 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v6Rule := goproto.Clone(fwRule).(*proto.FirewallRule)
|
||||||
|
v6Rule.PeerIP = "::" //nolint:staticcheck // populated for backward compatibility
|
||||||
|
v6Wildcard, _ := netiputil.EncodePrefix(netip.PrefixFrom(netip.IPv6Unspecified(), 0))
|
||||||
|
v6Rule.SourcePrefixes = [][]byte{v6Wildcard}
|
||||||
|
if ShouldUsePortRange(v6Rule) {
|
||||||
|
v6Rule.PortInfo = rule.PortRange.ToProto()
|
||||||
|
}
|
||||||
|
return []*proto.FirewallRule{v6Rule}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProtoDirection converts the direction to proto.RuleDirection.
|
||||||
|
func GetProtoDirection(direction int) proto.RuleDirection {
|
||||||
|
if direction == types.FirewallRuleDirectionOUT {
|
||||||
|
return proto.RuleDirection_OUT
|
||||||
|
}
|
||||||
|
return proto.RuleDirection_IN
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProtoAction converts the action to proto.RuleAction.
|
||||||
|
func GetProtoAction(action string) proto.RuleAction {
|
||||||
|
if action == string(types.PolicyTrafficActionDrop) {
|
||||||
|
return proto.RuleAction_DROP
|
||||||
|
}
|
||||||
|
return proto.RuleAction_ACCEPT
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProtoProtocol converts the protocol to proto.RuleProtocol.
|
||||||
|
func GetProtoProtocol(protocol string) proto.RuleProtocol {
|
||||||
|
switch types.PolicyRuleProtocolType(protocol) {
|
||||||
|
case types.PolicyRuleProtocolALL:
|
||||||
|
return proto.RuleProtocol_ALL
|
||||||
|
case types.PolicyRuleProtocolTCP:
|
||||||
|
return proto.RuleProtocol_TCP
|
||||||
|
case types.PolicyRuleProtocolUDP:
|
||||||
|
return proto.RuleProtocol_UDP
|
||||||
|
case types.PolicyRuleProtocolICMP:
|
||||||
|
return proto.RuleProtocol_ICMP
|
||||||
|
case types.PolicyRuleProtocolNetbirdSSH:
|
||||||
|
return proto.RuleProtocol_NETBIRD_SSH
|
||||||
|
default:
|
||||||
|
return proto.RuleProtocol_UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProtoPortInfo converts route-firewall-rule port info to proto.PortInfo.
|
||||||
|
func GetProtoPortInfo(rule *types.RouteFirewallRule) *proto.PortInfo {
|
||||||
|
var portInfo proto.PortInfo
|
||||||
|
if rule.Port != 0 {
|
||||||
|
portInfo.PortSelection = &proto.PortInfo_Port{Port: uint32(rule.Port)}
|
||||||
|
} else if portRange := rule.PortRange; portRange.Start != 0 && portRange.End != 0 {
|
||||||
|
portInfo.PortSelection = &proto.PortInfo_Range_{
|
||||||
|
Range: &proto.PortInfo_Range{
|
||||||
|
Start: uint32(portRange.Start),
|
||||||
|
End: uint32(portRange.End),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &portInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldUsePortRange reports whether the firewall rule should use a port
|
||||||
|
// range rather than a single port (TCP/UDP without a single port).
|
||||||
|
func ShouldUsePortRange(rule *proto.FirewallRule) bool {
|
||||||
|
return rule.Port == "" && (rule.Protocol == proto.RuleProtocol_UDP || rule.Protocol == proto.RuleProtocol_TCP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToProtocolRoutesFirewallRules converts a slice of typed route-firewall
|
||||||
|
// rules to proto.
|
||||||
|
func ToProtocolRoutesFirewallRules(rules []*types.RouteFirewallRule) []*proto.RouteFirewallRule {
|
||||||
|
result := make([]*proto.RouteFirewallRule, len(rules))
|
||||||
|
for i := range rules {
|
||||||
|
rule := rules[i]
|
||||||
|
result[i] = &proto.RouteFirewallRule{
|
||||||
|
SourceRanges: rule.SourceRanges,
|
||||||
|
Action: GetProtoAction(rule.Action),
|
||||||
|
Destination: rule.Destination,
|
||||||
|
Protocol: GetProtoProtocol(rule.Protocol),
|
||||||
|
PortInfo: GetProtoPortInfo(rule),
|
||||||
|
IsDynamic: rule.IsDynamic,
|
||||||
|
Domains: rule.Domains.ToPunycodeList(),
|
||||||
|
PolicyID: []byte(rule.PolicyID),
|
||||||
|
RouteID: string(rule.RouteID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToProtoCustomZone converts an nbdns.CustomZone to its proto form.
|
||||||
|
func ConvertToProtoCustomZone(zone nbdns.CustomZone) *proto.CustomZone {
|
||||||
|
protoZone := &proto.CustomZone{
|
||||||
|
Domain: zone.Domain,
|
||||||
|
Records: make([]*proto.SimpleRecord, 0, len(zone.Records)),
|
||||||
|
SearchDomainDisabled: zone.SearchDomainDisabled,
|
||||||
|
NonAuthoritative: zone.NonAuthoritative,
|
||||||
|
}
|
||||||
|
for _, record := range zone.Records {
|
||||||
|
protoZone.Records = append(protoZone.Records, &proto.SimpleRecord{
|
||||||
|
Name: record.Name,
|
||||||
|
Type: int64(record.Type),
|
||||||
|
Class: record.Class,
|
||||||
|
TTL: int64(record.TTL),
|
||||||
|
RData: record.RData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return protoZone
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToProtoNameServerGroup converts a NameServerGroup to its proto form.
|
||||||
|
func ConvertToProtoNameServerGroup(nsGroup *nbdns.NameServerGroup) *proto.NameServerGroup {
|
||||||
|
protoGroup := &proto.NameServerGroup{
|
||||||
|
Primary: nsGroup.Primary,
|
||||||
|
Domains: nsGroup.Domains,
|
||||||
|
SearchDomainsEnabled: nsGroup.SearchDomainsEnabled,
|
||||||
|
NameServers: make([]*proto.NameServer, 0, len(nsGroup.NameServers)),
|
||||||
|
}
|
||||||
|
for _, ns := range nsGroup.NameServers {
|
||||||
|
protoGroup.NameServers = append(protoGroup.NameServers, &proto.NameServer{
|
||||||
|
IP: ns.IP.String(),
|
||||||
|
Port: int64(ns.Port),
|
||||||
|
NSType: int64(ns.NSType),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return protoGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSConfigCache is the cache contract for amortising NameServerGroup
|
||||||
|
// proto-conversion across peers in the same account. Server uses a concrete
|
||||||
|
// implementation; client passes nil (no cross-peer caching needed when
|
||||||
|
// rebuilding a single NetworkMap from an envelope).
|
||||||
|
type DNSConfigCache interface {
|
||||||
|
GetNameServerGroup(key string) (*proto.NameServerGroup, bool)
|
||||||
|
SetNameServerGroup(key string, value *proto.NameServerGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToProtocolDNSConfig converts nbdns.Config to proto.DNSConfig. If cache is
|
||||||
|
// non-nil, NameServerGroup proto values are cached by NSG.ID across calls —
|
||||||
|
// the server amortises this across peers, the client passes nil.
|
||||||
|
func ToProtocolDNSConfig(update nbdns.Config, cache DNSConfigCache, forwardPort int64) *proto.DNSConfig {
|
||||||
|
protoUpdate := &proto.DNSConfig{
|
||||||
|
ServiceEnable: update.ServiceEnable,
|
||||||
|
CustomZones: make([]*proto.CustomZone, 0, len(update.CustomZones)),
|
||||||
|
NameServerGroups: make([]*proto.NameServerGroup, 0, len(update.NameServerGroups)),
|
||||||
|
ForwarderPort: forwardPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, zone := range update.CustomZones {
|
||||||
|
protoUpdate.CustomZones = append(protoUpdate.CustomZones, ConvertToProtoCustomZone(zone))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, nsGroup := range update.NameServerGroups {
|
||||||
|
if cache != nil {
|
||||||
|
if cachedGroup, exists := cache.GetNameServerGroup(nsGroup.ID); exists {
|
||||||
|
protoUpdate.NameServerGroups = append(protoUpdate.NameServerGroups, cachedGroup)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protoGroup := ConvertToProtoNameServerGroup(nsGroup)
|
||||||
|
if cache != nil {
|
||||||
|
cache.SetNameServerGroup(nsGroup.ID, protoGroup)
|
||||||
|
}
|
||||||
|
protoUpdate.NameServerGroups = append(protoUpdate.NameServerGroups, protoGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
return protoUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendRemotePeerConfig appends typed peers as proto.RemotePeerConfig
|
||||||
|
// entries to dst and returns the result.
|
||||||
|
func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nbpeer.Peer, dnsName string, includeIPv6 bool) []*proto.RemotePeerConfig {
|
||||||
|
for _, rPeer := range peers {
|
||||||
|
allowedIPs := []string{rPeer.IP.String() + "/32"}
|
||||||
|
if includeIPv6 && rPeer.IPv6.IsValid() {
|
||||||
|
allowedIPs = append(allowedIPs, rPeer.IPv6.String()+"/128")
|
||||||
|
}
|
||||||
|
dst = append(dst, &proto.RemotePeerConfig{
|
||||||
|
WgPubKey: rPeer.Key,
|
||||||
|
AllowedIps: allowedIPs,
|
||||||
|
SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
|
||||||
|
Fqdn: rPeer.FQDN(dnsName),
|
||||||
|
AgentVersion: rPeer.Meta.WtVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildAuthorizedUsersProto deduplicates user-IDs into a hashed list and
|
||||||
|
// builds per-machine-user index maps. Returns (hashedUsers, machineUsers).
|
||||||
|
// Errors from individual hash failures are logged via the provided context;
|
||||||
|
// they leave the offending user out of the result but don't abort the build.
|
||||||
|
func BuildAuthorizedUsersProto(ctx context.Context, authorizedUsers map[string]map[string]struct{}) ([][]byte, map[string]*proto.MachineUserIndexes) {
|
||||||
|
userIDToIndex := make(map[string]uint32)
|
||||||
|
var hashedUsers [][]byte
|
||||||
|
machineUsers := make(map[string]*proto.MachineUserIndexes, len(authorizedUsers))
|
||||||
|
|
||||||
|
for machineUser, users := range authorizedUsers {
|
||||||
|
indexes := make([]uint32, 0, len(users))
|
||||||
|
for userID := range users {
|
||||||
|
idx, exists := userIDToIndex[userID]
|
||||||
|
if !exists {
|
||||||
|
hash, err := sshauth.HashUserID(userID)
|
||||||
|
if err != nil {
|
||||||
|
log.WithContext(ctx).WithError(err).Error("failed to hash user id")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idx = uint32(len(hashedUsers))
|
||||||
|
userIDToIndex[userID] = idx
|
||||||
|
hashedUsers = append(hashedUsers, hash[:])
|
||||||
|
}
|
||||||
|
indexes = append(indexes, idx)
|
||||||
|
}
|
||||||
|
machineUsers[machineUser] = &proto.MachineUserIndexes{Indexes: indexes}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashedUsers, machineUsers
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package networkmap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnvelopeResult is what the client engine consumes after receiving a
|
||||||
|
// component-format NetworkMap. Both fields are populated:
|
||||||
|
//
|
||||||
|
// - NetworkMap is the *proto.NetworkMap shape the engine reads today via
|
||||||
|
// update.GetNetworkMap() — built from the envelope's components by
|
||||||
|
// running Calculate() locally + converting back through the shared
|
||||||
|
// proto helpers + merging the optional ProxyPatch.
|
||||||
|
// - Components is the *types.NetworkMapComponents the engine retains so
|
||||||
|
// future incremental delta updates have a base to apply changes
|
||||||
|
// against. The client keeps it under its sync lock.
|
||||||
|
type EnvelopeResult struct {
|
||||||
|
NetworkMap *proto.NetworkMap
|
||||||
|
Components *types.NetworkMapComponents
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnvelopeToNetworkMap is the full client-side pipeline: decode the
|
||||||
|
// component envelope back to a typed NetworkMapComponents, run Calculate()
|
||||||
|
// locally to produce the typed NetworkMap, convert it to the wire form the
|
||||||
|
// engine consumes, and fold in any ProxyPatch the server attached.
|
||||||
|
//
|
||||||
|
// localPeerKey is the receiving peer's WG pub key (used to derive
|
||||||
|
// includeIPv6 / useSourcePrefixes from the receiving peer's own record in
|
||||||
|
// the components struct, mirroring legacy ToSyncResponse behaviour).
|
||||||
|
//
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode envelope: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the receiving peer in the decoded components by WG key.
|
||||||
|
// c.Peers is keyed by canonical base64 of the raw 32-byte pub key
|
||||||
|
// (decoder re-encodes the bytes off the wire). The caller may pass a
|
||||||
|
// non-canonical encoding (some persisted production keys carry
|
||||||
|
// non-zero trailing padding bits that survived a legacy import), so
|
||||||
|
// round-trip through raw bytes once to canonicalize before lookup.
|
||||||
|
canonicalKey := canonicalizeWgKey(localPeerKey)
|
||||||
|
localPeer := components.Peers[canonicalKey]
|
||||||
|
if localPeer == nil {
|
||||||
|
return nil, fmt.Errorf("receiving peer (wg_key prefix %q) not found among %d decoded peers — components have no PeerID, Calculate would return empty", trimKey(localPeerKey), len(components.Peers))
|
||||||
|
}
|
||||||
|
components.PeerID = canonicalKey
|
||||||
|
|
||||||
|
includeIPv6 := localPeer.SupportsIPv6() && localPeer.IPv6.IsValid()
|
||||||
|
useSourcePrefixes := localPeer.SupportsSourcePrefixes()
|
||||||
|
|
||||||
|
typedNM := components.Calculate(ctx)
|
||||||
|
|
||||||
|
full := env.GetFull()
|
||||||
|
dnsFwdPort := int64(0)
|
||||||
|
if full != nil {
|
||||||
|
dnsFwdPort = full.DnsForwarderPort
|
||||||
|
}
|
||||||
|
|
||||||
|
protoNM := &proto.NetworkMap{
|
||||||
|
Serial: typedNM.Network.CurrentSerial(),
|
||||||
|
}
|
||||||
|
if full != nil {
|
||||||
|
protoNM.PeerConfig = full.PeerConfig
|
||||||
|
}
|
||||||
|
protoNM.Routes = ToProtocolRoutes(typedNM.Routes)
|
||||||
|
protoNM.DNSConfig = ToProtocolDNSConfig(typedNM.DNSConfig, nil, dnsFwdPort)
|
||||||
|
|
||||||
|
remotePeers := AppendRemotePeerConfig(nil, typedNM.Peers, dnsName, includeIPv6)
|
||||||
|
protoNM.RemotePeers = remotePeers
|
||||||
|
protoNM.RemotePeersIsEmpty = len(remotePeers) == 0
|
||||||
|
|
||||||
|
protoNM.OfflinePeers = AppendRemotePeerConfig(nil, typedNM.OfflinePeers, dnsName, includeIPv6)
|
||||||
|
|
||||||
|
firewallRules := ToProtocolFirewallRules(typedNM.FirewallRules, includeIPv6, useSourcePrefixes)
|
||||||
|
protoNM.FirewallRules = firewallRules
|
||||||
|
protoNM.FirewallRulesIsEmpty = len(firewallRules) == 0
|
||||||
|
|
||||||
|
routesFirewallRules := ToProtocolRoutesFirewallRules(typedNM.RoutesFirewallRules)
|
||||||
|
protoNM.RoutesFirewallRules = routesFirewallRules
|
||||||
|
protoNM.RoutesFirewallRulesIsEmpty = len(routesFirewallRules) == 0
|
||||||
|
|
||||||
|
if typedNM.AuthorizedUsers != nil {
|
||||||
|
hashedUsers, machineUsers := BuildAuthorizedUsersProto(ctx, typedNM.AuthorizedUsers)
|
||||||
|
userIDClaim := ""
|
||||||
|
if full != nil {
|
||||||
|
userIDClaim = full.UserIdClaim
|
||||||
|
}
|
||||||
|
protoNM.SshAuth = &proto.SSHAuth{
|
||||||
|
AuthorizedUsers: hashedUsers,
|
||||||
|
MachineUsers: machineUsers,
|
||||||
|
UserIDClaim: userIDClaim,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if typedNM.ForwardingRules != nil {
|
||||||
|
forwardingRules := make([]*proto.ForwardingRule, 0, len(typedNM.ForwardingRules))
|
||||||
|
for _, rule := range typedNM.ForwardingRules {
|
||||||
|
forwardingRules = append(forwardingRules, rule.ToProto())
|
||||||
|
}
|
||||||
|
protoNM.ForwardingRules = forwardingRules
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the proxy patch the server attached. Mirrors the legacy
|
||||||
|
// NetworkMap.Merge step that the server runs after Calculate().
|
||||||
|
if full != nil && full.ProxyPatch != nil {
|
||||||
|
mergeProxyPatch(protoNM, full.ProxyPatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &EnvelopeResult{
|
||||||
|
NetworkMap: protoNM,
|
||||||
|
Components: components,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeProxyPatch folds a ProxyPatch's pre-expanded fragments into the
|
||||||
|
// proto.NetworkMap that Calculate() produced. Mirrors types.NetworkMap.Merge
|
||||||
|
// — same six collections, deduplicated where the legacy merge dedupes.
|
||||||
|
func mergeProxyPatch(nm *proto.NetworkMap, patch *proto.ProxyPatch) {
|
||||||
|
nm.RemotePeers = appendUniquePeers(nm.RemotePeers, patch.Peers)
|
||||||
|
nm.OfflinePeers = appendUniquePeers(nm.OfflinePeers, patch.OfflinePeers)
|
||||||
|
nm.FirewallRules = append(nm.FirewallRules, patch.FirewallRules...)
|
||||||
|
nm.Routes = append(nm.Routes, patch.Routes...)
|
||||||
|
nm.RoutesFirewallRules = append(nm.RoutesFirewallRules, patch.RouteFirewallRules...)
|
||||||
|
nm.ForwardingRules = append(nm.ForwardingRules, patch.ForwardingRules...)
|
||||||
|
if len(nm.RemotePeers) > 0 {
|
||||||
|
nm.RemotePeersIsEmpty = false
|
||||||
|
}
|
||||||
|
if len(nm.FirewallRules) > 0 {
|
||||||
|
nm.FirewallRulesIsEmpty = false
|
||||||
|
}
|
||||||
|
if len(nm.RoutesFirewallRules) > 0 {
|
||||||
|
nm.RoutesFirewallRulesIsEmpty = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendUniquePeers dedupes by WgPubKey — mirrors legacy
|
||||||
|
// mergeUniquePeersByID's intent (legacy keyed off Peer.ID; in proto form the
|
||||||
|
// closest stable identifier is WgPubKey).
|
||||||
|
func appendUniquePeers(dst, extra []*proto.RemotePeerConfig) []*proto.RemotePeerConfig {
|
||||||
|
if len(extra) == 0 {
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(dst))
|
||||||
|
for _, p := range dst {
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[p.WgPubKey] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, p := range extra {
|
||||||
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[p.WgPubKey]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[p.WgPubKey] = struct{}{}
|
||||||
|
dst = append(dst, p)
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimKey(s string) string {
|
||||||
|
if len(s) > 12 {
|
||||||
|
return s[:12]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// canonicalizeWgKey normalises a base64-encoded WireGuard public key so it
|
||||||
|
// matches the canonical encoding emitted by the envelope decoder. Returns
|
||||||
|
// the input unchanged when it does not decode to 32 raw bytes (caller will
|
||||||
|
// hit a miss in the peer map and surface the error).
|
||||||
|
func canonicalizeWgKey(s string) string {
|
||||||
|
raw, err := base64.StdEncoding.DecodeString(s)
|
||||||
|
if err != nil || len(raw) != 32 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(raw)
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
package networkmap_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
goproto "google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
nbnetworkmap "github.com/netbirdio/netbird/shared/management/networkmap"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestEnvelopeToNetworkMap_RoundTrip exercises the full client-side pipeline:
|
||||||
|
// build a small components struct, encode an envelope, marshal/unmarshal the
|
||||||
|
// wire bytes, decode back via EnvelopeToNetworkMap, and verify the result is
|
||||||
|
// non-empty and consistent.
|
||||||
|
func TestEnvelopeToNetworkMap_RoundTrip(t *testing.T) {
|
||||||
|
c, localPeerKey := buildSmokeComponents(t)
|
||||||
|
|
||||||
|
envelope := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{
|
||||||
|
Components: c,
|
||||||
|
DNSDomain: "netbird.cloud",
|
||||||
|
})
|
||||||
|
|
||||||
|
wire, err := goproto.Marshal(envelope)
|
||||||
|
require.NoError(t, err, "marshal envelope")
|
||||||
|
|
||||||
|
var decoded proto.NetworkMapEnvelope
|
||||||
|
require.NoError(t, goproto.Unmarshal(wire, &decoded), "unmarshal envelope")
|
||||||
|
|
||||||
|
result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud")
|
||||||
|
require.NoError(t, err, "EnvelopeToNetworkMap")
|
||||||
|
require.NotNil(t, result)
|
||||||
|
require.NotNil(t, result.NetworkMap, "decoded NetworkMap must be non-nil")
|
||||||
|
require.NotNil(t, result.Components, "Components must be retained for future delta updates")
|
||||||
|
require.NotNil(t, result.Components.AccountSettings)
|
||||||
|
require.NotEmpty(t, result.NetworkMap.RemotePeers, "two-peer allow policy should produce one remote peer")
|
||||||
|
require.NotEmpty(t, result.NetworkMap.FirewallRules, "two-peer allow policy should produce firewall rules")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCalculate_FirewallRuleProtocol_NeverNetbirdSSH guards against the
|
||||||
|
// scenario where a rule with Protocol=NetbirdSSH leaks the enum value into
|
||||||
|
// proto.FirewallRule.Protocol. Calculate() must rewrite NetbirdSSH → TCP
|
||||||
|
// before forming firewall rules. Without that rewrite, agents fall into
|
||||||
|
// UNKNOWN-protocol handling, which on some platforms downgrades to
|
||||||
|
// allow-all — a real security regression.
|
||||||
|
func TestCalculate_FirewallRuleProtocol_NeverNetbirdSSH(t *testing.T) {
|
||||||
|
c, localPeerKey := buildSmokeComponents(t)
|
||||||
|
// Replace the smoke policy with a NetbirdSSH-protocol allow.
|
||||||
|
c.Policies = []*types.Policy{{
|
||||||
|
ID: "pol-ssh", PublicID: "2", Enabled: true,
|
||||||
|
Rules: []*types.PolicyRule{{
|
||||||
|
ID: "rule-ssh",
|
||||||
|
Enabled: true,
|
||||||
|
Action: types.PolicyTrafficActionAccept,
|
||||||
|
Protocol: types.PolicyRuleProtocolNetbirdSSH,
|
||||||
|
Bidirectional: true,
|
||||||
|
Sources: []string{"group-all"},
|
||||||
|
Destinations: []string{"group-all"},
|
||||||
|
}},
|
||||||
|
}}
|
||||||
|
|
||||||
|
envelope := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{
|
||||||
|
Components: c,
|
||||||
|
DNSDomain: "netbird.cloud",
|
||||||
|
})
|
||||||
|
wire, err := goproto.Marshal(envelope)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var decoded proto.NetworkMapEnvelope
|
||||||
|
require.NoError(t, goproto.Unmarshal(wire, &decoded))
|
||||||
|
|
||||||
|
result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, result.NetworkMap.FirewallRules, "ssh policy should produce firewall rules")
|
||||||
|
for i, fr := range result.NetworkMap.FirewallRules {
|
||||||
|
require.NotEqualf(t, proto.RuleProtocol_NETBIRD_SSH, fr.Protocol,
|
||||||
|
"FirewallRules[%d].Protocol must be the rewritten TCP, not NETBIRD_SSH", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvelopeToNetworkMap_NilEnvelope(t *testing.T) {
|
||||||
|
_, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), nil, "key", "netbird.cloud")
|
||||||
|
require.Error(t, err, "nil envelope must produce an error rather than panic")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvelopeToNetworkMap_FullPayloadMissing(t *testing.T) {
|
||||||
|
env := &proto.NetworkMapEnvelope{}
|
||||||
|
_, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), env, "key", "netbird.cloud")
|
||||||
|
require.Error(t, err, "envelope with no Full payload must produce an error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDecodeEnvelope_MalformedWgKeyPeerSkipped feeds an envelope where one
|
||||||
|
// peer has a wg_pub_key that is not 32 bytes long. The decoder must skip
|
||||||
|
// that peer (keeping the rest of the snapshot usable) instead of aborting
|
||||||
|
// the whole sync — mirrors legacy behaviour that tolerates an occasional
|
||||||
|
// bad row.
|
||||||
|
func TestDecodeEnvelope_MalformedWgKeyPeerSkipped(t *testing.T) {
|
||||||
|
c, localPeerKey := buildSmokeComponents(t)
|
||||||
|
envelope := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{
|
||||||
|
Components: c,
|
||||||
|
DNSDomain: "netbird.cloud",
|
||||||
|
})
|
||||||
|
require.NotNil(t, envelope.GetFull())
|
||||||
|
|
||||||
|
full := envelope.GetFull()
|
||||||
|
require.Len(t, full.Peers, 2, "smoke fixture should have two peers")
|
||||||
|
|
||||||
|
// Truncate the second peer's wg_pub_key so it fails the length gate.
|
||||||
|
for _, p := range full.Peers {
|
||||||
|
if base64.StdEncoding.EncodeToString(p.WgPubKey) != localPeerKey {
|
||||||
|
p.WgPubKey = p.WgPubKey[:31]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wire, err := goproto.Marshal(envelope)
|
||||||
|
require.NoError(t, err, "marshal envelope")
|
||||||
|
var decoded proto.NetworkMapEnvelope
|
||||||
|
require.NoError(t, goproto.Unmarshal(wire, &decoded), "unmarshal envelope")
|
||||||
|
|
||||||
|
result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud")
|
||||||
|
require.NoError(t, err, "EnvelopeToNetworkMap must tolerate one bad peer key")
|
||||||
|
require.NotNil(t, result)
|
||||||
|
require.NotNil(t, result.Components)
|
||||||
|
require.Len(t, result.Components.Peers, 1, "the well-formed peer survives, the malformed one is dropped")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEnvelopeRoundTrip_AllGroupShortCircuitParity reproduces prod accounts
|
||||||
|
// with several groups literally named "All" where the "All"-named group does
|
||||||
|
// not contain every peer. Server-side Calculate short-circuits destination
|
||||||
|
// expansion at the first group named "All" (getUniquePeerIDsFromGroupsIDs),
|
||||||
|
// ignoring the remaining destination groups. The wire must preserve enough
|
||||||
|
// group identity for the decoded components to short-circuit identically —
|
||||||
|
// otherwise the client unions all destination groups and emits extra
|
||||||
|
// firewall rules the server never produced.
|
||||||
|
func TestEnvelopeRoundTrip_AllGroupShortCircuitParity(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
peers := map[string]*nbpeer.Peer{}
|
||||||
|
for i, id := range []string{"peer-T", "peer-S", "peer-ALL", "peer-O"} {
|
||||||
|
peers[id] = &nbpeer.Peer{
|
||||||
|
ID: id,
|
||||||
|
Key: randomWgKey(t),
|
||||||
|
IP: netip.AddrFrom4([4]byte{100, 64, 0, byte(i + 1)}),
|
||||||
|
DNSLabel: id,
|
||||||
|
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &types.NetworkMapComponents{
|
||||||
|
PeerID: "peer-T",
|
||||||
|
Network: &types.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{},
|
||||||
|
Peers: peers,
|
||||||
|
Groups: map[string]*types.Group{
|
||||||
|
"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"}},
|
||||||
|
},
|
||||||
|
Policies: []*types.Policy{{
|
||||||
|
ID: "pol-multi-dest", PublicID: "10", Enabled: true,
|
||||||
|
Rules: []*types.PolicyRule{{
|
||||||
|
ID: "rule-multi-dest",
|
||||||
|
Enabled: true,
|
||||||
|
Action: types.PolicyTrafficActionAccept,
|
||||||
|
Protocol: types.PolicyRuleProtocolALL,
|
||||||
|
Sources: []string{"g-src"},
|
||||||
|
Destinations: []string{"g-all", "g-two"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
serverNM := c.Calculate(ctx)
|
||||||
|
require.NotNil(t, serverNM)
|
||||||
|
|
||||||
|
envelope := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{
|
||||||
|
Components: c,
|
||||||
|
DNSDomain: "netbird.cloud",
|
||||||
|
})
|
||||||
|
wire, err := goproto.Marshal(envelope)
|
||||||
|
require.NoError(t, err, "marshal envelope")
|
||||||
|
var decodedEnv proto.NetworkMapEnvelope
|
||||||
|
require.NoError(t, goproto.Unmarshal(wire, &decodedEnv), "unmarshal envelope")
|
||||||
|
|
||||||
|
result, err := nbnetworkmap.EnvelopeToNetworkMap(ctx, &decodedEnv, peers["peer-T"].Key, "netbird.cloud")
|
||||||
|
require.NoError(t, err, "EnvelopeToNetworkMap")
|
||||||
|
clientNM := result.NetworkMap
|
||||||
|
|
||||||
|
serverRules := make([]string, 0, len(serverNM.FirewallRules))
|
||||||
|
for _, r := range serverNM.FirewallRules {
|
||||||
|
serverRules = append(serverRules, fmt.Sprintf("%s/%d", r.PeerIP, r.Direction))
|
||||||
|
}
|
||||||
|
clientRules := make([]string, 0, len(clientNM.FirewallRules))
|
||||||
|
for _, r := range clientNM.FirewallRules {
|
||||||
|
clientRules = append(clientRules, fmt.Sprintf("%s/%d", r.PeerIP, r.Direction)) // nolint:staticcheck
|
||||||
|
}
|
||||||
|
require.ElementsMatch(t, serverRules, clientRules,
|
||||||
|
"client-side Calculate must expand destination groups exactly like the server")
|
||||||
|
|
||||||
|
serverPeers := make([]string, 0, len(serverNM.Peers))
|
||||||
|
for _, p := range serverNM.Peers {
|
||||||
|
serverPeers = append(serverPeers, p.Key)
|
||||||
|
}
|
||||||
|
clientPeers := make([]string, 0, len(clientNM.RemotePeers))
|
||||||
|
for _, p := range clientNM.RemotePeers {
|
||||||
|
clientPeers = append(clientPeers, p.WgPubKey)
|
||||||
|
}
|
||||||
|
require.ElementsMatch(t, serverPeers, clientPeers,
|
||||||
|
"client-side Calculate must connect the same remote peers as the server")
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSmokeComponents returns a minimal NetworkMapComponents (2 peers, 1
|
||||||
|
// group, 1 allow policy) plus the receiving peer's WG public key. Sufficient
|
||||||
|
// to validate the encode → marshal → decode → Calculate pipeline produces
|
||||||
|
// non-empty output.
|
||||||
|
func buildSmokeComponents(t *testing.T) (*types.NetworkMapComponents, string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
peerAKey := randomWgKey(t)
|
||||||
|
peerBKey := randomWgKey(t)
|
||||||
|
|
||||||
|
peerA := &nbpeer.Peer{
|
||||||
|
ID: "peer-A",
|
||||||
|
Key: peerAKey,
|
||||||
|
IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}),
|
||||||
|
DNSLabel: "peerA",
|
||||||
|
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}
|
||||||
|
peerB := &nbpeer.Peer{
|
||||||
|
ID: "peer-B",
|
||||||
|
Key: peerBKey,
|
||||||
|
IP: netip.AddrFrom4([4]byte{100, 64, 0, 2}),
|
||||||
|
DNSLabel: "peerB",
|
||||||
|
Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"},
|
||||||
|
}
|
||||||
|
|
||||||
|
group := &types.Group{
|
||||||
|
ID: "group-all", PublicID: "1", Name: "All",
|
||||||
|
Peers: []string{"peer-A", "peer-B"},
|
||||||
|
}
|
||||||
|
|
||||||
|
policy := &types.Policy{
|
||||||
|
ID: "pol-allow", PublicID: "1", Enabled: true,
|
||||||
|
Rules: []*types.PolicyRule{{
|
||||||
|
ID: "rule-allow",
|
||||||
|
Enabled: true,
|
||||||
|
Action: types.PolicyTrafficActionAccept,
|
||||||
|
Protocol: types.PolicyRuleProtocolALL,
|
||||||
|
Bidirectional: true,
|
||||||
|
Sources: []string{"group-all"},
|
||||||
|
Destinations: []string{"group-all"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &types.NetworkMapComponents{
|
||||||
|
PeerID: "peer-A",
|
||||||
|
Network: &types.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]*nbpeer.Peer{
|
||||||
|
"peer-A": peerA,
|
||||||
|
"peer-B": peerB,
|
||||||
|
},
|
||||||
|
Groups: map[string]*types.Group{
|
||||||
|
"group-all": group,
|
||||||
|
},
|
||||||
|
Policies: []*types.Policy{policy},
|
||||||
|
}
|
||||||
|
return c, peerAKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomWgKey(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
var raw [32]byte
|
||||||
|
_, err := rand.Read(raw[:])
|
||||||
|
require.NoError(t, err)
|
||||||
|
return base64.StdEncoding.EncodeToString(raw[:])
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -150,6 +150,14 @@ message SyncResponse {
|
|||||||
// SSO-registered; client clears its anchor
|
// SSO-registered; client clears its anchor
|
||||||
// set, valid timestamp → new absolute UTC deadline
|
// set, valid timestamp → new absolute UTC deadline
|
||||||
google.protobuf.Timestamp sessionExpiresAt = 7;
|
google.protobuf.Timestamp sessionExpiresAt = 7;
|
||||||
|
|
||||||
|
// NetworkMapEnvelope carries the component-based wire format for peers that
|
||||||
|
// advertise PeerCapabilityComponentNetworkMap. When set, NetworkMap (field 5)
|
||||||
|
// is left empty: management ships components and the client runs Calculate()
|
||||||
|
// locally instead of receiving an expanded NetworkMap.
|
||||||
|
NetworkMapEnvelope NetworkMapEnvelope = 8;
|
||||||
|
|
||||||
|
int32 Version = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SyncMetaRequest {
|
message SyncMetaRequest {
|
||||||
@@ -229,6 +237,8 @@ enum PeerCapability {
|
|||||||
PeerCapabilitySourcePrefixes = 1;
|
PeerCapabilitySourcePrefixes = 1;
|
||||||
// Client handles IPv6 overlay addresses and firewall rules.
|
// Client handles IPv6 overlay addresses and firewall rules.
|
||||||
PeerCapabilityIPv6Overlay = 2;
|
PeerCapabilityIPv6Overlay = 2;
|
||||||
|
// Client receives NetworkMap as components and assembles it locally.
|
||||||
|
PeerCapabilityComponentNetworkMap = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PeerSystemMeta is machine meta data like OS and version.
|
// PeerSystemMeta is machine meta data like OS and version.
|
||||||
@@ -252,6 +262,7 @@ message PeerSystemMeta {
|
|||||||
Flags flags = 17;
|
Flags flags = 17;
|
||||||
|
|
||||||
repeated PeerCapability capabilities = 18;
|
repeated PeerCapability capabilities = 18;
|
||||||
|
int32 syncMessageVersion = 19;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LoginResponse {
|
message LoginResponse {
|
||||||
@@ -617,6 +628,13 @@ enum RuleProtocol {
|
|||||||
UDP = 3;
|
UDP = 3;
|
||||||
ICMP = 4;
|
ICMP = 4;
|
||||||
CUSTOM = 5;
|
CUSTOM = 5;
|
||||||
|
// NETBIRD_SSH (types.PolicyRuleProtocolType "netbird-ssh") is the marker
|
||||||
|
// policy rule that drives SSH-server activation in Calculate(). The legacy
|
||||||
|
// proto.FirewallRule path doesn't ship this value (Calculate already
|
||||||
|
// expands SSH rules into TCP/22 before encoding), but the components path
|
||||||
|
// ships RAW policies — the client must see this protocol to derive
|
||||||
|
// AuthorizedUsers locally.
|
||||||
|
NETBIRD_SSH = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RuleDirection {
|
enum RuleDirection {
|
||||||
@@ -757,3 +775,435 @@ message StopExposeRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message StopExposeResponse {}
|
message StopExposeResponse {}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Component-based NetworkMap wire format (PeerCapabilityComponentNetworkMap).
|
||||||
|
//
|
||||||
|
// Peers that advertise this capability receive NetworkMap building blocks
|
||||||
|
// (peers + groups + policies + routes + dns + ssh + forwarding) and run the
|
||||||
|
// expansion (Calculate) locally instead of receiving a fully-expanded
|
||||||
|
// NetworkMap from the server.
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
// NetworkMapEnvelope wraps either a full snapshot or a delta. Only Full is
|
||||||
|
// emitted today; Delta is reserved for the incremental-update work.
|
||||||
|
message NetworkMapEnvelope {
|
||||||
|
oneof payload {
|
||||||
|
NetworkMapComponentsFull full = 1;
|
||||||
|
NetworkMapComponentsDelta delta = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkMapComponentsFull is the full per-peer component snapshot. The
|
||||||
|
// client decodes it into a types.NetworkMapComponents and runs Calculate()
|
||||||
|
// locally to produce the same NetworkMap the legacy server path would have
|
||||||
|
// produced. Every field carries RAW component data — no server-side
|
||||||
|
// expansion (firewall rules, DNS config, SSH auth, route firewall rules,
|
||||||
|
// forwarding rules) is shipped; the client computes those itself.
|
||||||
|
message NetworkMapComponentsFull {
|
||||||
|
uint64 serial = 1;
|
||||||
|
|
||||||
|
// Peer config for the receiving peer (legacy proto.PeerConfig kept as-is —
|
||||||
|
// it carries the receiving peer's own overlay address, FQDN, SSH config).
|
||||||
|
PeerConfig peer_config = 2;
|
||||||
|
|
||||||
|
// Account-level network metadata (id, IPv4/IPv6 overlay subnets, DNS,
|
||||||
|
// serial). Mirrors types.Network.
|
||||||
|
AccountNetwork network = 3;
|
||||||
|
|
||||||
|
// Account-level settings the client needs for its local Calculate().
|
||||||
|
AccountSettingsCompact account_settings = 4;
|
||||||
|
|
||||||
|
// Account DNS settings (mirrors types.DNSSettings).
|
||||||
|
DNSSettingsCompact dns_settings = 5;
|
||||||
|
|
||||||
|
// Domain shared across all peers in this account, e.g. "netbird.cloud".
|
||||||
|
// Each peer's FQDN is dns_label + "." + dns_domain.
|
||||||
|
string dns_domain = 6;
|
||||||
|
|
||||||
|
// Custom-zone domain for this peer's view (c.CustomZoneDomain). Empty when
|
||||||
|
// the peer has no custom zone records.
|
||||||
|
string custom_zone_domain = 7;
|
||||||
|
|
||||||
|
// Deduplicated agent versions; PeerCompact.agent_version_idx indexes here.
|
||||||
|
// Empty string at index 0 if any peer has no version.
|
||||||
|
repeated string agent_versions = 8;
|
||||||
|
|
||||||
|
// All peers (deduplicated). The client splits peers into online / offline
|
||||||
|
// locally using account_settings.peer_login_expiration on receive.
|
||||||
|
repeated PeerCompact peers = 9;
|
||||||
|
|
||||||
|
// Indexes into peers for the subset that may act as routers.
|
||||||
|
repeated uint32 router_peer_indexes = 10;
|
||||||
|
|
||||||
|
// Policies that affect the receiving peer.
|
||||||
|
repeated PolicyCompact policies = 11;
|
||||||
|
|
||||||
|
// Groups in unspecified order — clients key off id (public_id).
|
||||||
|
repeated GroupCompact groups = 12;
|
||||||
|
|
||||||
|
// Routes relevant to this peer, raw shape (mirrors []*route.Route).
|
||||||
|
repeated RouteRaw routes = 13;
|
||||||
|
|
||||||
|
// Nameserver groups (mirrors []*nbdns.NameServerGroup).
|
||||||
|
repeated NameServerGroupRaw nameserver_groups = 14;
|
||||||
|
|
||||||
|
// All DNS records the client needs to assemble its custom zone. Reuses
|
||||||
|
// the existing SimpleRecord wire shape.
|
||||||
|
repeated SimpleRecord all_dns_records = 15;
|
||||||
|
|
||||||
|
// Custom zones (typically the peer's own zone). Reuses the existing
|
||||||
|
// CustomZone wire shape.
|
||||||
|
repeated CustomZone account_zones = 16;
|
||||||
|
|
||||||
|
// Network resources (mirrors []*resourceTypes.NetworkResource).
|
||||||
|
repeated NetworkResourceRaw network_resources = 17;
|
||||||
|
|
||||||
|
// Routers per network. Outer key: network public_id. Each entry is
|
||||||
|
// the set of routers backing that network for this peer's view.
|
||||||
|
map<string, NetworkRouterList> routers_map = 18;
|
||||||
|
|
||||||
|
// For each NetworkResource public_id, the indexes into policies[]
|
||||||
|
// that apply to it.
|
||||||
|
map<string, PolicyIds> resource_policies_map = 19;
|
||||||
|
|
||||||
|
// Group-id (public_id) → user ids authorized for SSH on members.
|
||||||
|
map<string, UserIDList> group_id_to_user_ids = 20;
|
||||||
|
|
||||||
|
// Account-level allowed user ids (used by Calculate() when assembling SSH
|
||||||
|
// authorized users for the receiving peer).
|
||||||
|
repeated string allowed_user_ids = 21;
|
||||||
|
|
||||||
|
// Per posture-check public_id, the set of peer indexes that failed
|
||||||
|
// the check. Server-side evaluation result; clients do not re-evaluate.
|
||||||
|
map<string, PeerIndexSet> posture_failed_peers = 22;
|
||||||
|
|
||||||
|
// Account-level DNS forwarder port (mirrors the legacy
|
||||||
|
// proto.DNSConfig.ForwarderPort). Computed by the controller from peer
|
||||||
|
// versions; clients fold it into their Calculate() DNS output.
|
||||||
|
int64 dns_forwarder_port = 23;
|
||||||
|
|
||||||
|
// Pre-expanded NetworkMap fragments injected post-Calculate by external
|
||||||
|
// controllers (BYOP / port-forwarding proxies). The receiving client
|
||||||
|
// merges these into its locally-computed NetworkMap the same way the
|
||||||
|
// legacy server does via NetworkMap.Merge — so downstream consumers see
|
||||||
|
// a unified merged result regardless of source.
|
||||||
|
ProxyPatch proxy_patch = 24;
|
||||||
|
|
||||||
|
// SSH UserIDClaim — server-side HttpServerConfig.AuthUserIDClaim, or
|
||||||
|
// "sub" by default. Populated in proto.SSHAuth.UserIDClaim when the
|
||||||
|
// client rebuilds the NetworkMap from this envelope. Empty when the
|
||||||
|
// account has no AuthorizedUsers (and thus no SshAuth to populate).
|
||||||
|
string user_id_claim = 25;
|
||||||
|
|
||||||
|
// Reserved for future component additions (incremental_serial, parent_seq,
|
||||||
|
// etc.) without forcing a renumber.
|
||||||
|
reserved 26 to 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyPatch carries NetworkMap fragments that don't fit the component-graph
|
||||||
|
// model — they're pre-expanded by external controllers (BYOP /
|
||||||
|
// port-forwarding proxies) and injected post-Calculate. Fields use the
|
||||||
|
// legacy wire types because the proxy delivers them pre-formed; there is
|
||||||
|
// no raw component shape to convert from. Empty when no proxy is active.
|
||||||
|
message ProxyPatch {
|
||||||
|
repeated RemotePeerConfig peers = 1;
|
||||||
|
repeated RemotePeerConfig offline_peers = 2;
|
||||||
|
repeated FirewallRule firewall_rules = 3;
|
||||||
|
repeated Route routes = 4;
|
||||||
|
repeated RouteFirewallRule route_firewall_rules = 5;
|
||||||
|
repeated ForwardingRule forwarding_rules = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountSettingsCompact carries the account-level settings the client needs
|
||||||
|
// to evaluate locally. Mirrors the subset of types.AccountSettingsInfo that
|
||||||
|
// Calculate() actually reads — login-expiration (used to filter expired
|
||||||
|
// peers). Inactivity expiration is purely server-side bookkeeping and is not
|
||||||
|
// shipped.
|
||||||
|
message AccountSettingsCompact {
|
||||||
|
bool peer_login_expiration_enabled = 1;
|
||||||
|
// Login expiration window. Unit is nanoseconds (matches time.Duration).
|
||||||
|
int64 peer_login_expiration_ns = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountNetwork is the account-level overlay metadata. Mirrors types.Network
|
||||||
|
// so the client can populate NetworkMap.Network without a server round-trip.
|
||||||
|
message AccountNetwork {
|
||||||
|
string identifier = 1;
|
||||||
|
// IPv4 overlay subnet in CIDR form (e.g. "100.64.0.0/16").
|
||||||
|
string net_cidr = 2;
|
||||||
|
// IPv6 ULA overlay subnet in CIDR form (e.g. "fd00:4e42::/64"). Empty when
|
||||||
|
// the account has no IPv6 overlay yet.
|
||||||
|
string net_v6_cidr = 3;
|
||||||
|
string dns = 4;
|
||||||
|
uint64 serial = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkMapComponentsDelta is reserved for the incremental update
|
||||||
|
// protocol. Field numbers 1–100 are pre-allocated to keep room for the
|
||||||
|
// planned event types without needing a renumber.
|
||||||
|
message NetworkMapComponentsDelta {
|
||||||
|
reserved 1 to 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerCompact is the wire-shape of a remote peer used by the component
|
||||||
|
// format. It carries every field of types.Peer that the client's local
|
||||||
|
// Calculate() reads — including the trio needed to evaluate
|
||||||
|
// LoginExpired() (added_with_sso_login + login_expiration_enabled +
|
||||||
|
// last_login_unix_nano). Fields the client does not consume (Status,
|
||||||
|
// CreatedAt, etc.) are not shipped.
|
||||||
|
message PeerCompact {
|
||||||
|
// Raw 32-byte WireGuard public key (no base64 wrapping).
|
||||||
|
bytes wg_pub_key = 1;
|
||||||
|
|
||||||
|
// Raw 4-byte IPv4 overlay address. Always a /32 host route, so no prefix
|
||||||
|
// byte is needed.
|
||||||
|
bytes ip = 2;
|
||||||
|
|
||||||
|
// Raw 16-byte IPv6 overlay address; always a /128 host route. Empty when
|
||||||
|
// the peer has no IPv6 overlay address.
|
||||||
|
bytes ipv6 = 3;
|
||||||
|
|
||||||
|
// Raw SSH public key bytes (or empty).
|
||||||
|
bytes ssh_pub_key = 4;
|
||||||
|
|
||||||
|
// DNS label without the account's domain suffix. Full FQDN is
|
||||||
|
// dns_label + "." + NetworkMapComponentsFull.dns_domain.
|
||||||
|
string dns_label = 5;
|
||||||
|
|
||||||
|
string agent_version = 6;
|
||||||
|
|
||||||
|
// True iff the peer was added via SSO login (i.e., types.Peer.UserID is
|
||||||
|
// non-empty). Combined with login_expiration_enabled and
|
||||||
|
// last_login_unix_nano this lets the client reproduce
|
||||||
|
// (*Peer).LoginExpired() locally.
|
||||||
|
bool added_with_sso_login = 7;
|
||||||
|
|
||||||
|
// True when the peer's login can expire — mirrors
|
||||||
|
// types.Peer.LoginExpirationEnabled.
|
||||||
|
bool login_expiration_enabled = 8;
|
||||||
|
|
||||||
|
// Unix-nanosecond timestamp of the peer's last login. 0 when the peer has
|
||||||
|
// never logged in (server stores nil; client treats 0 as "epoch", which
|
||||||
|
// makes a fresh peer immediately expired iff login_expiration_enabled is
|
||||||
|
// true — the same semantics as types.Peer.GetLastLogin).
|
||||||
|
int64 last_login_unix_nano = 9;
|
||||||
|
|
||||||
|
// True when the peer has an SSH server enabled locally. Used by the
|
||||||
|
// legacy SSH path in Calculate() (`policyRuleImpliesLegacySSH`): a rule
|
||||||
|
// with protocol ALL/TCP-with-SSH-ports activates SSH for the receiving
|
||||||
|
// peer when this bit is set, even without an explicit NetbirdSSH rule.
|
||||||
|
bool ssh_enabled = 10;
|
||||||
|
|
||||||
|
// Mirror of types.Peer.SupportsIPv6() — !Meta.Flags.DisableIPv6 &&
|
||||||
|
// HasCapability(PeerCapabilityIPv6Overlay). Used by the local peer's
|
||||||
|
// Calculate() when deciding whether to emit IPv6 firewall rules
|
||||||
|
// (appendIPv6FirewallRule) against this peer's IPv6 address.
|
||||||
|
bool supports_ipv6 = 11;
|
||||||
|
|
||||||
|
// Mirror of types.Peer.SupportsSourcePrefixes() —
|
||||||
|
// HasCapability(PeerCapabilitySourcePrefixes). Determines whether the
|
||||||
|
// local peer's Calculate() emits SourcePrefixes alongside legacy PeerIP
|
||||||
|
// fields in proto.FirewallRule.
|
||||||
|
bool supports_source_prefixes = 12;
|
||||||
|
|
||||||
|
// Mirror of types.Peer.Meta.Flags.ServerSSHAllowed. Read by Calculate()
|
||||||
|
// when expanding TCP port-22 firewall rules — the native SSH companion
|
||||||
|
// (port 22022) is only added when this flag is set and the peer agent
|
||||||
|
// version supports it.
|
||||||
|
bool server_ssh_allowed = 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicyCompact is the compact form of a policy rule. Group references use
|
||||||
|
// the public_ids; the client resolves
|
||||||
|
// them against NetworkMapComponentsFull.groups. Direction is derived per-peer
|
||||||
|
// on the client (ingress when the peer is in destination_group_ids, egress
|
||||||
|
// when in source_group_ids; both when bidirectional).
|
||||||
|
message PolicyCompact {
|
||||||
|
// public_id. Used as a stable reference for
|
||||||
|
// ResourcePoliciesMap.indexes and future delta updates.
|
||||||
|
string id = 1;
|
||||||
|
|
||||||
|
RuleAction action = 2;
|
||||||
|
RuleProtocol protocol = 3;
|
||||||
|
bool bidirectional = 4;
|
||||||
|
|
||||||
|
// Single ports referenced by the rule.
|
||||||
|
repeated uint32 ports = 5;
|
||||||
|
|
||||||
|
// Port ranges (start..end) referenced by the rule.
|
||||||
|
repeated PortInfo.Range port_ranges = 6;
|
||||||
|
|
||||||
|
// Group ids (public_ids) of source / destination groups.
|
||||||
|
repeated string source_group_ids = 7;
|
||||||
|
repeated string destination_group_ids = 8;
|
||||||
|
|
||||||
|
// SSH authorization fields. PolicyRule.AuthorizedGroups maps the rule's
|
||||||
|
// applicable group ids (public_ids) to a list of local-user names —
|
||||||
|
// when a peer in one of those groups is the SSH destination, the named
|
||||||
|
// local users gain access. AuthorizedUser is the single-user form
|
||||||
|
// (legacy: rule scopes SSH to one specific user id).
|
||||||
|
//
|
||||||
|
// Both fields are only consumed by Calculate() when the rule's protocol
|
||||||
|
// is NetbirdSSH (or the legacy implicit-SSH heuristic).
|
||||||
|
map<string, UserNameList> authorized_groups = 9;
|
||||||
|
string authorized_user = 10;
|
||||||
|
|
||||||
|
// Resource-typed rule sources/destinations. When a rule targets a specific
|
||||||
|
// peer (rather than groups), Calculate() reads SourceResource /
|
||||||
|
// DestinationResource — without these the rule's connection resources
|
||||||
|
// can't be produced on the client. ResourceCompact's peer_index refers to
|
||||||
|
// NetworkMapComponentsFull.peers; type is the raw ResourceType string
|
||||||
|
// ("peer", "host", "subnet", "domain"). Only "peer" is meaningful for
|
||||||
|
// Calculate's resource-typed rule path today.
|
||||||
|
ResourceCompact source_resource = 11;
|
||||||
|
ResourceCompact destination_resource = 12;
|
||||||
|
|
||||||
|
// Posture-check ids gating this policy's source peers. Calculate()
|
||||||
|
// reads them when filtering rule peers (peers that fail any listed check
|
||||||
|
// are dropped from sourcePeers). Match keys in
|
||||||
|
// NetworkMapComponentsFull.posture_failed_peers.
|
||||||
|
repeated string source_posture_check_ids = 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserNameList is a list of local-user names — used as the value type in
|
||||||
|
// PolicyCompact.authorized_groups.
|
||||||
|
message UserNameList {
|
||||||
|
repeated string names = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupCompact is the wire-shape of a group: public id, optional
|
||||||
|
// name, and indexes into NetworkMapComponentsFull.peers identifying members.
|
||||||
|
message GroupCompact {
|
||||||
|
// id comes from PublicID. Used by PolicyCompact.source_group_ids / destination_group_ids.
|
||||||
|
string id = 1;
|
||||||
|
|
||||||
|
// Indexes into NetworkMapComponentsFull.peers.
|
||||||
|
repeated uint32 peer_indexes = 2;
|
||||||
|
|
||||||
|
// True when the group is named "All" (types.Group.IsGroupAll). The
|
||||||
|
// client-side Calculate short-circuits group→peer expansion on such
|
||||||
|
// groups exactly like the server does; without this bit the decoded
|
||||||
|
// groups lose that property and the two sides expand policy
|
||||||
|
// destinations differently.
|
||||||
|
bool is_all = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSSettingsCompact mirrors types.DNSSettings.
|
||||||
|
message DNSSettingsCompact {
|
||||||
|
// Group ids (public_id) whose DNS management is disabled.
|
||||||
|
repeated string disabled_management_group_ids = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RouteRaw mirrors *route.Route (the domain type), trimmed to fields that
|
||||||
|
// types.NetworkMapComponents.Calculate() reads. Group references are
|
||||||
|
// public_ids; the routing peer (when set) is referenced by index into
|
||||||
|
// NetworkMapComponentsFull.peers.
|
||||||
|
message RouteRaw {
|
||||||
|
string id = 1; // public_id
|
||||||
|
string net_id = 2;
|
||||||
|
string description = 3;
|
||||||
|
|
||||||
|
// Either network_cidr (e.g. "10.0.0.0/16") or domains is set, not both.
|
||||||
|
string network_cidr = 4;
|
||||||
|
repeated string domains = 5;
|
||||||
|
bool keep_route = 6;
|
||||||
|
|
||||||
|
// Routing peer reference: peer_index_set tells whether peer_index is valid
|
||||||
|
// (proto3 uint32 cannot disambiguate "0" from "unset"). Mutually exclusive
|
||||||
|
// with peer_group_ids.
|
||||||
|
//
|
||||||
|
// peer_index decodes back to types.Peer.ID (the peer's xid string), NOT
|
||||||
|
// to its WireGuard public key. This matches the server-side data flow:
|
||||||
|
// c.Routes carry route.Peer = peer.ID, and getRoutingPeerRoutes mutates
|
||||||
|
// it to peer.Key only after the route has been admitted to the network
|
||||||
|
// map. Decoders MUST set Route.Peer = peer.ID; the legacy Calculate()
|
||||||
|
// path will substitute the WG key downstream.
|
||||||
|
bool peer_index_set = 7;
|
||||||
|
uint32 peer_index = 8;
|
||||||
|
repeated string peer_group_ids = 9;
|
||||||
|
|
||||||
|
int32 network_type = 10;
|
||||||
|
bool masquerade = 11;
|
||||||
|
int32 metric = 12;
|
||||||
|
bool enabled = 13;
|
||||||
|
repeated string group_ids = 14;
|
||||||
|
repeated string access_control_group_ids = 15;
|
||||||
|
bool skip_auto_apply = 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameServerGroupRaw mirrors *nbdns.NameServerGroup. Distinct from the
|
||||||
|
// legacy NameServerGroup (which is the wire-trimmed shape consumed by
|
||||||
|
// proto.DNSConfig and lacks the Name/Description/Groups/Enabled fields).
|
||||||
|
message NameServerGroupRaw {
|
||||||
|
string id = 1;
|
||||||
|
// Reuses the legacy NameServer wire shape (IP as string).
|
||||||
|
repeated NameServer nameservers = 2;
|
||||||
|
// Group ids the NSG distributes nameservers to.
|
||||||
|
repeated string group_ids = 3;
|
||||||
|
bool primary = 4;
|
||||||
|
repeated string domains = 5;
|
||||||
|
bool enabled = 6;
|
||||||
|
bool search_domains_enabled = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkResourceRaw mirrors *resourceTypes.NetworkResource.
|
||||||
|
//
|
||||||
|
message NetworkResourceRaw {
|
||||||
|
string id = 1;
|
||||||
|
string network_seq = 2;
|
||||||
|
string name = 3;
|
||||||
|
string description = 4;
|
||||||
|
// Resource type: "host" / "subnet" / "domain".
|
||||||
|
string type = 5;
|
||||||
|
string address = 6;
|
||||||
|
string domain_value = 7; // resource.Domain
|
||||||
|
string prefix_cidr = 8;
|
||||||
|
bool enabled = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkRouterList carries the routers backing one network.
|
||||||
|
message NetworkRouterList {
|
||||||
|
// Routers in this network, keyed by peer_index (the routing peer).
|
||||||
|
repeated NetworkRouterEntry entries = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkRouterEntry mirrors a single *routerTypes.NetworkRouter; the routing
|
||||||
|
// peer is referenced by index into NetworkMapComponentsFull.peers.
|
||||||
|
message NetworkRouterEntry {
|
||||||
|
string id = 1;
|
||||||
|
uint32 peer_index = 2;
|
||||||
|
bool peer_index_set = 3;
|
||||||
|
repeated string peer_group_ids = 4;
|
||||||
|
bool masquerade = 5;
|
||||||
|
int32 metric = 6;
|
||||||
|
bool enabled = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PolicyIds {
|
||||||
|
repeated string ids = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserIDList is a list of user ids — used as the value type in
|
||||||
|
// NetworkMapComponentsFull.group_id_to_user_ids.
|
||||||
|
message UserIDList {
|
||||||
|
repeated string user_ids = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerIndexSet is a set of peer indexes — used as the value type in
|
||||||
|
// NetworkMapComponentsFull.posture_failed_peers.
|
||||||
|
message PeerIndexSet {
|
||||||
|
repeated uint32 peer_indexes = 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||||
|
"github.com/netbirdio/netbird/management/server/posture"
|
||||||
|
"github.com/netbirdio/netbird/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
firewallRuleMinPortRangesVer = "0.48.0"
|
||||||
|
firewallRuleMinNativeSSHVer = "0.60.0"
|
||||||
|
|
||||||
|
nativeSSHPortString = "22022"
|
||||||
|
nativeSSHPortNumber = 22022
|
||||||
|
defaultSSHPortString = "22"
|
||||||
|
defaultSSHPortNumber = 22
|
||||||
|
)
|
||||||
|
|
||||||
|
type supportedFeatures struct {
|
||||||
|
nativeSSH bool
|
||||||
|
portRanges bool
|
||||||
|
}
|
||||||
|
|
||||||
|
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 *nbpeer.Peer) []*FirewallRule {
|
||||||
|
features := peerSupportedFirewallFeatures(peer.Meta.WtVersion)
|
||||||
|
|
||||||
|
var expanded []*FirewallRule
|
||||||
|
|
||||||
|
for _, port := range rule.Ports {
|
||||||
|
fr := base
|
||||||
|
fr.Port = port
|
||||||
|
expanded = append(expanded, &fr)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, portRange := range rule.PortRanges {
|
||||||
|
if len(rule.Ports) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fr := base
|
||||||
|
|
||||||
|
if features.portRanges {
|
||||||
|
fr.PortRange = portRange
|
||||||
|
} else {
|
||||||
|
if portRange.Start != portRange.End {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fr.Port = strconv.FormatUint(uint64(portRange.Start), 10)
|
||||||
|
}
|
||||||
|
expanded = append(expanded, &fr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCheckRulesForNativeSSH(features.nativeSSH, rule, peer) || rule.Protocol == PolicyRuleProtocolNetbirdSSH {
|
||||||
|
expanded = addNativeSSHRule(base, expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
return expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
func addNativeSSHRule(base FirewallRule, expanded []*FirewallRule) []*FirewallRule {
|
||||||
|
shouldAdd := false
|
||||||
|
for _, fr := range expanded {
|
||||||
|
if isPortInRule(nativeSSHPortString, 22022, fr) {
|
||||||
|
return expanded
|
||||||
|
}
|
||||||
|
if isPortInRule(defaultSSHPortString, 22, fr) {
|
||||||
|
shouldAdd = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !shouldAdd {
|
||||||
|
return expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
fr := base
|
||||||
|
fr.Port = nativeSSHPortString
|
||||||
|
return append(expanded, &fr)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 *nbpeer.Peer) bool {
|
||||||
|
return supportsNative && peer.SSHEnabled && peer.Meta.Flags.ServerSSHAllowed && rule.Protocol == PolicyRuleProtocolTCP
|
||||||
|
}
|
||||||
|
|
||||||
|
func peerSupportedFirewallFeatures(peerVer string) supportedFeatures {
|
||||||
|
if version.IsDevelopmentVersion(peerVer) {
|
||||||
|
return supportedFeatures{true, true}
|
||||||
|
}
|
||||||
|
|
||||||
|
var features supportedFeatures
|
||||||
|
|
||||||
|
meetMinVer, err := posture.MeetsMinVersion(firewallRuleMinNativeSSHVer, peerVer)
|
||||||
|
features.nativeSSH = err == nil && meetMinVer
|
||||||
|
|
||||||
|
if features.nativeSSH {
|
||||||
|
features.portRanges = true
|
||||||
|
} else {
|
||||||
|
meetMinVer, err = posture.MeetsMinVersion(firewallRuleMinPortRangesVer, peerVer)
|
||||||
|
features.portRanges = err == nil && meetMinVer
|
||||||
|
}
|
||||||
|
|
||||||
|
return features
|
||||||
|
}
|
||||||
@@ -47,11 +47,11 @@ func (r *FirewallRule) Equal(other *FirewallRule) bool {
|
|||||||
return reflect.DeepEqual(r, other)
|
return reflect.DeepEqual(r, other)
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateRouteFirewallRules generates a list of firewall rules for a given route.
|
// GenerateRouteFirewallRules generates a list of firewall rules for a given route.
|
||||||
// For static routes, source ranges match the destination family (v4 or v6).
|
// For static routes, source ranges match the destination family (v4 or v6).
|
||||||
// For dynamic routes (domain-based), separate v4 and v6 rules are generated
|
// For dynamic routes (domain-based), separate v4 and v6 rules are generated
|
||||||
// so the routing peer's forwarding chain allows both address families.
|
// so the routing peer's forwarding chain allows both address families.
|
||||||
func generateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int, includeIPv6 bool) []*RouteFirewallRule {
|
func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int, includeIPv6 bool) []*RouteFirewallRule {
|
||||||
rulesExists := make(map[string]struct{})
|
rulesExists := make(map[string]struct{})
|
||||||
rules := make([]*RouteFirewallRule, 0)
|
rules := make([]*RouteFirewallRule, 0)
|
||||||
|
|
||||||
+6
-6
@@ -57,7 +57,7 @@ func TestGenerateRouteFirewallRules_V4Route(t *testing.T) {
|
|||||||
Protocol: PolicyRuleProtocolALL,
|
Protocol: PolicyRuleProtocolALL,
|
||||||
}
|
}
|
||||||
|
|
||||||
rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
|
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
|
||||||
|
|
||||||
require.Len(t, rules, 1)
|
require.Len(t, rules, 1)
|
||||||
assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges, "v4 route should only have v4 sources")
|
assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges, "v4 route should only have v4 sources")
|
||||||
@@ -86,7 +86,7 @@ func TestGenerateRouteFirewallRules_V6Route(t *testing.T) {
|
|||||||
Protocol: PolicyRuleProtocolALL,
|
Protocol: PolicyRuleProtocolALL,
|
||||||
}
|
}
|
||||||
|
|
||||||
rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
|
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
|
||||||
|
|
||||||
require.Len(t, rules, 1)
|
require.Len(t, rules, 1)
|
||||||
assert.Equal(t, []string{"fd00::1/128"}, rules[0].SourceRanges, "v6 route should only have v6 sources")
|
assert.Equal(t, []string{"fd00::1/128"}, rules[0].SourceRanges, "v6 route should only have v6 sources")
|
||||||
@@ -115,7 +115,7 @@ func TestGenerateRouteFirewallRules_DynamicRoute_DualStack(t *testing.T) {
|
|||||||
Protocol: PolicyRuleProtocolALL,
|
Protocol: PolicyRuleProtocolALL,
|
||||||
}
|
}
|
||||||
|
|
||||||
rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
|
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
|
||||||
|
|
||||||
require.Len(t, rules, 2, "dynamic route should produce both v4 and v6 rules")
|
require.Len(t, rules, 2, "dynamic route should produce both v4 and v6 rules")
|
||||||
assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges)
|
assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges)
|
||||||
@@ -143,7 +143,7 @@ func TestGenerateRouteFirewallRules_DynamicRoute_NoV6Peers(t *testing.T) {
|
|||||||
Protocol: PolicyRuleProtocolALL,
|
Protocol: PolicyRuleProtocolALL,
|
||||||
}
|
}
|
||||||
|
|
||||||
rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
|
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true)
|
||||||
|
|
||||||
require.Len(t, rules, 1, "no v6 peers means only v4 rule")
|
require.Len(t, rules, 1, "no v6 peers means only v4 rule")
|
||||||
assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges)
|
assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges)
|
||||||
@@ -173,7 +173,7 @@ func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) {
|
|||||||
Protocol: PolicyRuleProtocolALL,
|
Protocol: PolicyRuleProtocolALL,
|
||||||
}
|
}
|
||||||
|
|
||||||
rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false)
|
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false)
|
||||||
assert.Empty(t, rules, "v6 route should produce no rules when includeIPv6 is false")
|
assert.Empty(t, rules, "v6 route should produce no rules when includeIPv6 is false")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) {
|
|||||||
Protocol: PolicyRuleProtocolALL,
|
Protocol: PolicyRuleProtocolALL,
|
||||||
}
|
}
|
||||||
|
|
||||||
rules := generateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false)
|
rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false)
|
||||||
require.Len(t, rules, 1, "dynamic route with includeIPv6=false should produce only v4 rule")
|
require.Len(t, rules, 1, "dynamic route with includeIPv6=false should produce only v4 rule")
|
||||||
assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges)
|
assert.Equal(t, []string{"100.64.0.1/32", "100.64.0.2/32"}, rules[0].SourceRanges)
|
||||||
})
|
})
|
||||||
@@ -19,6 +19,8 @@ type Group struct {
|
|||||||
// AccountID is a reference to Account that this object belongs
|
// AccountID is a reference to Account that this object belongs
|
||||||
AccountID string `json:"-" gorm:"index"`
|
AccountID string `json:"-" gorm:"index"`
|
||||||
|
|
||||||
|
PublicID string `json:"-"`
|
||||||
|
|
||||||
// Name visible in the UI
|
// Name visible in the UI
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
@@ -74,6 +76,7 @@ func (g *Group) Copy() *Group {
|
|||||||
group := &Group{
|
group := &Group{
|
||||||
ID: g.ID,
|
ID: g.ID,
|
||||||
AccountID: g.AccountID,
|
AccountID: g.AccountID,
|
||||||
|
PublicID: g.PublicID,
|
||||||
Name: g.Name,
|
Name: g.Name,
|
||||||
Issued: g.Issued,
|
Issued: g.Issued,
|
||||||
Peers: make([]string, len(g.Peers)),
|
Peers: make([]string, len(g.Peers)),
|
||||||
+45
-23
@@ -44,8 +44,21 @@ type NetworkMapComponents struct {
|
|||||||
|
|
||||||
RouterPeers map[string]*nbpeer.Peer
|
RouterPeers map[string]*nbpeer.Peer
|
||||||
|
|
||||||
routesByPeerOnce sync.Once
|
// NetworkXIDToPublicID maps Network.ID (xid) → PublicID.
|
||||||
routesByPeerIdx map[string][]routeIndexEntry
|
// Consumed by the envelope encoder to
|
||||||
|
// translate RoutersMap keys and NetworkResource.NetworkID references
|
||||||
|
// to compact uint32 ids. Legacy Calculate() doesn't consult it.
|
||||||
|
NetworkXIDToPublicID map[string]string
|
||||||
|
|
||||||
|
// PostureCheckXIDToPublicID maps posture.Checks.ID (xid) → PublicID.
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
type routeIndexEntry struct {
|
type routeIndexEntry struct {
|
||||||
@@ -60,6 +73,11 @@ type AccountSettingsInfo struct {
|
|||||||
PeerInactivityExpiration time.Duration
|
PeerInactivityExpiration time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EmptyNetworkMapComponents(nm *NetworkMapComponents) *NetworkMapComponents {
|
||||||
|
nm.empty = true
|
||||||
|
return nm
|
||||||
|
}
|
||||||
|
|
||||||
func (c *NetworkMapComponents) GetPeerInfo(peerID string) *nbpeer.Peer {
|
func (c *NetworkMapComponents) GetPeerInfo(peerID string) *nbpeer.Peer {
|
||||||
return c.Peers[peerID]
|
return c.Peers[peerID]
|
||||||
}
|
}
|
||||||
@@ -178,6 +196,10 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *NetworkMapComponents) IsEmpty() bool {
|
||||||
|
return c.empty
|
||||||
|
}
|
||||||
|
|
||||||
func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ([]*nbpeer.Peer, []*FirewallRule, map[string]map[string]struct{}, bool) {
|
func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ([]*nbpeer.Peer, []*FirewallRule, map[string]map[string]struct{}, bool) {
|
||||||
targetPeer := c.GetPeerInfo(targetPeerID)
|
targetPeer := c.GetPeerInfo(targetPeerID)
|
||||||
if targetPeer == nil {
|
if targetPeer == nil {
|
||||||
@@ -261,7 +283,7 @@ func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) (
|
|||||||
default:
|
default:
|
||||||
authorizedUsers[auth.Wildcard] = c.getAllowedUserIDs()
|
authorizedUsers[auth.Wildcard] = c.getAllowedUserIDs()
|
||||||
}
|
}
|
||||||
} else if peerInDestinations && policyRuleImpliesLegacySSH(rule) && targetPeer.SSHEnabled {
|
} else if peerInDestinations && PolicyRuleImpliesLegacySSH(rule) && targetPeer.SSHEnabled {
|
||||||
sshEnabled = true
|
sshEnabled = true
|
||||||
authorizedUsers[auth.Wildcard] = c.getAllowedUserIDs()
|
authorizedUsers[auth.Wildcard] = c.getAllowedUserIDs()
|
||||||
}
|
}
|
||||||
@@ -328,15 +350,15 @@ func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *nbpeer.Peer) (
|
|||||||
if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 {
|
if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 {
|
||||||
rules = append(rules, &fr)
|
rules = append(rules, &fr)
|
||||||
} else {
|
} else {
|
||||||
rules = append(rules, expandPortsAndRanges(fr, rule, targetPeer)...)
|
rules = append(rules, ExpandPortsAndRanges(fr, rule, targetPeer)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
rules = appendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, firewallRuleContext{
|
rules = AppendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, FirewallRuleContext{
|
||||||
direction: direction,
|
Direction: direction,
|
||||||
dirStr: dirStr,
|
DirStr: dirStr,
|
||||||
protocolStr: protocolStr,
|
ProtocolStr: protocolStr,
|
||||||
actionStr: actionStr,
|
ActionStr: actionStr,
|
||||||
portsJoined: portsJoined,
|
PortsJoined: portsJoined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, func() ([]*nbpeer.Peer, []*FirewallRule) {
|
}, func() ([]*nbpeer.Peer, []*FirewallRule) {
|
||||||
@@ -703,7 +725,7 @@ func (c *NetworkMapComponents) getRouteFirewallRules(ctx context.Context, peerID
|
|||||||
}
|
}
|
||||||
|
|
||||||
rulePeers := c.getRulePeers(rule, policy.SourcePostureChecks, peerID, distributionPeers)
|
rulePeers := c.getRulePeers(rule, policy.SourcePostureChecks, peerID, distributionPeers)
|
||||||
rules := generateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN, includeIPv6)
|
rules := GenerateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN, includeIPv6)
|
||||||
fwRules = append(fwRules, rules...)
|
fwRules = append(fwRules, rules...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -972,21 +994,21 @@ func (c *NetworkMapComponents) addNetworksRoutingPeers(
|
|||||||
return peersToConnect
|
return peersToConnect
|
||||||
}
|
}
|
||||||
|
|
||||||
type firewallRuleContext struct {
|
type FirewallRuleContext struct {
|
||||||
direction int
|
Direction int
|
||||||
dirStr string
|
DirStr string
|
||||||
protocolStr string
|
ProtocolStr string
|
||||||
actionStr string
|
ActionStr string
|
||||||
portsJoined string
|
PortsJoined string
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *nbpeer.Peer, rule *PolicyRule, rc firewallRuleContext) []*FirewallRule {
|
func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *nbpeer.Peer, rule *PolicyRule, rc FirewallRuleContext) []*FirewallRule {
|
||||||
if !peer.IPv6.IsValid() || !targetPeer.SupportsIPv6() || !targetPeer.IPv6.IsValid() {
|
if !peer.IPv6.IsValid() || !targetPeer.SupportsIPv6() || !targetPeer.IPv6.IsValid() {
|
||||||
return rules
|
return rules
|
||||||
}
|
}
|
||||||
|
|
||||||
v6IP := peer.IPv6.String()
|
v6IP := peer.IPv6.String()
|
||||||
v6RuleID := rule.ID + v6IP + rc.dirStr + rc.protocolStr + rc.actionStr + rc.portsJoined
|
v6RuleID := rule.ID + v6IP + rc.DirStr + rc.ProtocolStr + rc.ActionStr + rc.PortsJoined
|
||||||
if _, ok := rulesExists[v6RuleID]; ok {
|
if _, ok := rulesExists[v6RuleID]; ok {
|
||||||
return rules
|
return rules
|
||||||
}
|
}
|
||||||
@@ -995,12 +1017,12 @@ func appendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct
|
|||||||
v6fr := FirewallRule{
|
v6fr := FirewallRule{
|
||||||
PolicyID: rule.ID,
|
PolicyID: rule.ID,
|
||||||
PeerIP: v6IP,
|
PeerIP: v6IP,
|
||||||
Direction: rc.direction,
|
Direction: rc.Direction,
|
||||||
Action: rc.actionStr,
|
Action: rc.ActionStr,
|
||||||
Protocol: rc.protocolStr,
|
Protocol: rc.ProtocolStr,
|
||||||
}
|
}
|
||||||
if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 {
|
if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 {
|
||||||
return append(rules, &v6fr)
|
return append(rules, &v6fr)
|
||||||
}
|
}
|
||||||
return append(rules, expandPortsAndRanges(v6fr, rule, targetPeer)...)
|
return append(rules, ExpandPortsAndRanges(v6fr, rule, targetPeer)...)
|
||||||
}
|
}
|
||||||
@@ -56,6 +56,8 @@ type Policy struct {
|
|||||||
// ID of the policy'
|
// ID of the policy'
|
||||||
ID string `gorm:"primaryKey"`
|
ID string `gorm:"primaryKey"`
|
||||||
|
|
||||||
|
PublicID string `json:"-"`
|
||||||
|
|
||||||
// AccountID is a reference to Account that this object belongs
|
// AccountID is a reference to Account that this object belongs
|
||||||
AccountID string `json:"-" gorm:"index"`
|
AccountID string `json:"-" gorm:"index"`
|
||||||
|
|
||||||
@@ -80,6 +82,7 @@ func (p *Policy) Copy() *Policy {
|
|||||||
c := &Policy{
|
c := &Policy{
|
||||||
ID: p.ID,
|
ID: p.ID,
|
||||||
AccountID: p.AccountID,
|
AccountID: p.AccountID,
|
||||||
|
PublicID: p.PublicID,
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
Description: p.Description,
|
Description: p.Description,
|
||||||
Enabled: p.Enabled,
|
Enabled: p.Enabled,
|
||||||
Reference in New Issue
Block a user