mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-19 21:29:09 +02:00
Merge remote-tracking branch 'origin/main' into refactor/permissions-manager
This commit is contained in:
@@ -2,4 +2,5 @@ FROM ubuntu:24.04
|
||||
RUN apt update && apt install -y ca-certificates && rm -fr /var/cache/apt
|
||||
ENTRYPOINT [ "/go/bin/netbird-mgmt","management"]
|
||||
CMD ["--log-file", "console"]
|
||||
COPY netbird-mgmt /go/bin/netbird-mgmt
|
||||
ARG TARGETPLATFORM
|
||||
COPY ${TARGETPLATFORM}/netbird-mgmt /go/bin/netbird-mgmt
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
FROM ubuntu:24.04
|
||||
RUN apt update && apt install -y ca-certificates && rm -fr /var/cache/apt
|
||||
ENTRYPOINT [ "/go/bin/netbird-mgmt","management","--log-level","debug"]
|
||||
CMD ["--log-file", "console"]
|
||||
COPY netbird-mgmt /go/bin/netbird-mgmt
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
FROM golang:1.26.7-bookworm AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Install build dependencies
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/dexidp/dex/storage"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/netbirdio/netbird/formatter/hook"
|
||||
admincmd "github.com/netbirdio/netbird/management/cmd/admin"
|
||||
tokencmd "github.com/netbirdio/netbird/management/cmd/token"
|
||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
activitystore "github.com/netbirdio/netbird/management/server/activity/store"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
)
|
||||
|
||||
var adminDatadir string
|
||||
|
||||
// newAdminCommands creates the admin command tree with management-specific resource openers.
|
||||
func newAdminCommands() *cobra.Command {
|
||||
cmd := admincmd.NewCommands(admincmd.Openers{
|
||||
Resources: withAdminResources,
|
||||
Store: withAdminStoreOnly,
|
||||
IDP: withAdminIDPOnly,
|
||||
})
|
||||
cmd.PersistentFlags().StringVar(&adminDatadir, "datadir", "", "Override the data directory from config (used for store.db and the default idp.db)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newLegacyTokenCommand() *cobra.Command {
|
||||
cmd := tokencmd.NewCommands(tokencmd.StoreOpener(withAdminStoreOnly))
|
||||
cmd.Deprecated = "use 'admin token' instead"
|
||||
cmd.PersistentFlags().StringVar(&nbconfig.MgmtConfigPath, "config", defaultMgmtConfig, "Netbird config file location")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// withAdminResources initializes logging, loads config, opens the management store
|
||||
// and embedded IdP storage, and calls fn.
|
||||
func withAdminResources(cmd *cobra.Command, fn func(ctx context.Context, resources admincmd.Resources) error) error {
|
||||
return withAdminConfig(cmd, true, func(ctx context.Context, config *nbconfig.Config, datadir string) error {
|
||||
managementStore, err := openAdminStore(ctx, config, datadir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer admincmd.CloseStore(ctx, managementStore)
|
||||
|
||||
idpStorage, idpStorageFile, err := admincmd.OpenIDPStorage(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer admincmd.CloseIDPStorage(idpStorage)
|
||||
|
||||
eventStore, esErr := openAdminEventStore(ctx, config, datadir)
|
||||
if esErr != nil {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Warning: audit events will not be recorded: %v\n", esErr)
|
||||
}
|
||||
if eventStore != nil {
|
||||
defer func() {
|
||||
if err := eventStore.Close(ctx); err != nil {
|
||||
log.Debugf("close activity event store: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return fn(ctx, admincmd.Resources{Store: managementStore, IDPStorage: idpStorage, IDPStorageFile: idpStorageFile, EventStore: eventStore})
|
||||
})
|
||||
}
|
||||
|
||||
// withAdminStoreOnly opens only the management store for admin subcommands that do not
|
||||
// need embedded IdP storage.
|
||||
func withAdminStoreOnly(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error {
|
||||
return withAdminConfig(cmd, false, func(ctx context.Context, config *nbconfig.Config, datadir string) error {
|
||||
managementStore, err := openAdminStore(ctx, config, datadir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer admincmd.CloseStore(ctx, managementStore)
|
||||
|
||||
return fn(ctx, managementStore)
|
||||
})
|
||||
}
|
||||
|
||||
func withAdminIDPOnly(cmd *cobra.Command, fn func(ctx context.Context, idpStorage storage.Storage, storageFile string) error) error {
|
||||
return withAdminConfig(cmd, true, func(ctx context.Context, config *nbconfig.Config, _ string) error {
|
||||
idpStorage, idpStorageFile, err := admincmd.OpenIDPStorage(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer admincmd.CloseIDPStorage(idpStorage)
|
||||
|
||||
return fn(ctx, idpStorage, idpStorageFile)
|
||||
})
|
||||
}
|
||||
|
||||
func withAdminConfig(cmd *cobra.Command, applyIDPDefaults bool, fn func(ctx context.Context, config *nbconfig.Config, datadir string) error) error {
|
||||
if err := util.InitLog("error", "console"); err != nil {
|
||||
return fmt.Errorf("init log: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.WithValue(cmd.Context(), hook.ExecutionContextKey, hook.SystemSource) //nolint:staticcheck
|
||||
|
||||
config, datadir, err := loadAdminMgmtConfig(ctx, applyIDPDefaults)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
return fn(ctx, config, datadir)
|
||||
}
|
||||
|
||||
func loadAdminMgmtConfig(ctx context.Context, applyIDPDefaults bool) (*nbconfig.Config, string, error) {
|
||||
config := &nbconfig.Config{}
|
||||
if _, err := util.ReadJsonWithEnvSub(nbconfig.MgmtConfigPath, config); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if applyIDPDefaults {
|
||||
if err := ApplyEmbeddedIdPConfig(ctx, config); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
datadir := config.Datadir
|
||||
applyAdminDatadirOverride(config, &datadir)
|
||||
return config, datadir, nil
|
||||
}
|
||||
|
||||
func applyAdminDatadirOverride(config *nbconfig.Config, datadir *string) {
|
||||
if adminDatadir == "" {
|
||||
return
|
||||
}
|
||||
|
||||
oldDatadir := *datadir
|
||||
*datadir = adminDatadir
|
||||
if config.EmbeddedIdP != nil && config.EmbeddedIdP.Storage.Type == "sqlite3" && isDefaultIDPStorageFile(config.EmbeddedIdP.Storage.Config.File, oldDatadir) {
|
||||
config.EmbeddedIdP.Storage.Config.File = filepath.Join(*datadir, "idp.db")
|
||||
}
|
||||
}
|
||||
|
||||
func isDefaultIDPStorageFile(file, datadir string) bool {
|
||||
if file == "" {
|
||||
return true
|
||||
}
|
||||
defaultFile := filepath.Join(datadir, "idp.db")
|
||||
legacyDefaultFile := path.Join(datadir, "idp.db")
|
||||
legacySlashDefaultFile := path.Join(filepath.ToSlash(datadir), "idp.db")
|
||||
return filepath.Clean(file) == filepath.Clean(defaultFile) ||
|
||||
file == legacyDefaultFile ||
|
||||
filepath.ToSlash(file) == legacySlashDefaultFile
|
||||
}
|
||||
|
||||
func openAdminStore(ctx context.Context, config *nbconfig.Config, datadir string) (store.Store, error) {
|
||||
managementStore, err := store.NewStore(ctx, config.StoreConfig.Engine, datadir, nil, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create store: %w", err)
|
||||
}
|
||||
return managementStore, nil
|
||||
}
|
||||
|
||||
func openAdminEventStore(ctx context.Context, config *nbconfig.Config, datadir string) (activity.Store, error) {
|
||||
if config.DataStoreEncryptionKey == "" {
|
||||
return nil, fmt.Errorf("data store encryption key is not configured")
|
||||
}
|
||||
eventStore, err := activitystore.NewSqlStore(ctx, datadir, config.DataStoreEncryptionKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open activity event store: %w", err)
|
||||
}
|
||||
if eventStore == nil {
|
||||
return nil, fmt.Errorf("open activity event store: returned nil store")
|
||||
}
|
||||
return eventStore, nil
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
// Package admincmd provides reusable cobra commands for self-hosted administrator helpers.
|
||||
// Both the management and combined binaries use these commands, each providing
|
||||
// their own opener to handle config loading and storage initialization.
|
||||
package admincmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dexidp/dex/storage"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/netbirdio/netbird/formatter/hook"
|
||||
nbdex "github.com/netbirdio/netbird/idp/dex"
|
||||
"github.com/netbirdio/netbird/management/cmd/proxy"
|
||||
"github.com/netbirdio/netbird/management/cmd/token"
|
||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/activity"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
|
||||
// Resources contains the storages required by the admin commands.
|
||||
type Resources struct {
|
||||
Store store.Store
|
||||
IDPStorage storage.Storage
|
||||
IDPStorageFile string
|
||||
EventStore activity.Store
|
||||
}
|
||||
|
||||
// Opener initializes command resources from the command context and calls fn.
|
||||
type Opener func(cmd *cobra.Command, fn func(ctx context.Context, resources Resources) error) error
|
||||
|
||||
// StoreOpener initializes only the management store from the command context and calls fn.
|
||||
type StoreOpener func(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error
|
||||
|
||||
// IDPOpener initializes only the embedded IdP storage from the command context and calls fn.
|
||||
type IDPOpener func(cmd *cobra.Command, fn func(ctx context.Context, idpStorage storage.Storage, storageFile string) error) error
|
||||
|
||||
// Openers contains the resource openers needed by the admin command tree.
|
||||
type Openers struct {
|
||||
Resources Opener
|
||||
Store StoreOpener
|
||||
IDP IDPOpener
|
||||
}
|
||||
|
||||
type userSelector struct {
|
||||
email string
|
||||
userID string
|
||||
}
|
||||
|
||||
func (s userSelector) normalized() userSelector {
|
||||
return userSelector{
|
||||
email: strings.TrimSpace(s.email),
|
||||
userID: strings.TrimSpace(s.userID),
|
||||
}
|
||||
}
|
||||
|
||||
func (s userSelector) validate() error {
|
||||
s = s.normalized()
|
||||
if (s.email == "") == (s.userID == "") {
|
||||
return fmt.Errorf("provide exactly one of --email or --user-id")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewCommands creates the admin command tree with the given resource openers.
|
||||
func NewCommands(openers Openers) *cobra.Command {
|
||||
adminCmd := &cobra.Command{
|
||||
Use: "admin",
|
||||
Short: "Self-hosted administrator helpers",
|
||||
Long: "Administrative helpers for self-hosted deployments using the embedded identity provider.",
|
||||
}
|
||||
|
||||
userCmd := &cobra.Command{
|
||||
Use: "user",
|
||||
Short: "Manage local embedded IdP users",
|
||||
}
|
||||
|
||||
var passwordSelector userSelector
|
||||
var password string
|
||||
var passwordFile string
|
||||
passwordCmd := &cobra.Command{
|
||||
Use: "change-password (--email email | --user-id id) (--password password | --password-file path)",
|
||||
Aliases: []string{"set-password"},
|
||||
Short: "Change a local user's password",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
if err := passwordSelector.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
newPassword, err := resolvePasswordInput(cmd, password, passwordFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return openers.IDP(cmd, func(ctx context.Context, idpStorage storage.Storage, storageFile string) error {
|
||||
return runChangePassword(ctx, idpStorage, cmd.OutOrStdout(), passwordSelector, newPassword, storageFile)
|
||||
})
|
||||
},
|
||||
}
|
||||
addUserSelectorFlags(passwordCmd, &passwordSelector)
|
||||
passwordCmd.Flags().StringVar(&password, "password", "", "New password for the user")
|
||||
passwordCmd.Flags().StringVar(&passwordFile, "password-file", "", "Read new password from file ('-' for stdin)")
|
||||
|
||||
var resetSelector userSelector
|
||||
resetMFACmd := &cobra.Command{
|
||||
Use: "reset-mfa (--email email | --user-id id)",
|
||||
Short: "Reset a local user's MFA enrollment",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
if err := resetSelector.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return openers.IDP(cmd, func(ctx context.Context, idpStorage storage.Storage, storageFile string) error {
|
||||
return runResetMFA(ctx, idpStorage, cmd.OutOrStdout(), resetSelector, storageFile)
|
||||
})
|
||||
},
|
||||
}
|
||||
addUserSelectorFlags(resetMFACmd, &resetSelector)
|
||||
|
||||
userCmd.AddCommand(passwordCmd, resetMFACmd)
|
||||
|
||||
mfaCmd := &cobra.Command{
|
||||
Use: "mfa",
|
||||
Short: "Manage local MFA for embedded IdP users",
|
||||
}
|
||||
|
||||
enableCmd := &cobra.Command{
|
||||
Use: "enable",
|
||||
Short: "Enable MFA for local embedded IdP users",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return openers.Resources(cmd, func(ctx context.Context, resources Resources) error {
|
||||
return runSetMFAEnabled(ctx, resources, cmd.OutOrStdout(), true)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
disableCmd := &cobra.Command{
|
||||
Use: "disable",
|
||||
Short: "Disable MFA for local embedded IdP users",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return openers.Resources(cmd, func(ctx context.Context, resources Resources) error {
|
||||
return runSetMFAEnabled(ctx, resources, cmd.OutOrStdout(), false)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
statusCmd := &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show local MFA status",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return openers.Resources(cmd, func(ctx context.Context, resources Resources) error {
|
||||
return runMFAStatus(ctx, resources, cmd.OutOrStdout())
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
mfaCmd.AddCommand(enableCmd, disableCmd, statusCmd)
|
||||
adminCmd.AddCommand(userCmd, mfaCmd)
|
||||
if openers.Store != nil {
|
||||
adminCmd.AddCommand(tokencmd.NewCommands(tokencmd.StoreOpener(openers.Store)))
|
||||
adminCmd.AddCommand(proxycmd.NewCommands(proxycmd.StoreOpener(openers.Store)))
|
||||
}
|
||||
return adminCmd
|
||||
}
|
||||
|
||||
// OpenEmbeddedIDPStorage opens the Dex storage configured for the embedded IdP.
|
||||
func OpenEmbeddedIDPStorage(cfg *idp.EmbeddedIdPConfig) (storage.Storage, error) {
|
||||
if cfg == nil || !cfg.Enabled {
|
||||
return nil, fmt.Errorf("admin commands require the embedded IdP to be enabled")
|
||||
}
|
||||
|
||||
yamlConfig, err := cfg.ToYAMLConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build embedded IdP config: %w", err)
|
||||
}
|
||||
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
st, err := yamlConfig.Storage.OpenStorage(logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open embedded IdP storage: %w", err)
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// CloseStore closes the management store and logs cleanup errors at debug level.
|
||||
func CloseStore(ctx context.Context, s store.Store) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
if err := s.Close(ctx); err != nil {
|
||||
log.Debugf("close store: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// OpenIDPStorage opens embedded IdP storage and returns its sqlite file path when applicable.
|
||||
func OpenIDPStorage(config *nbconfig.Config) (storage.Storage, string, error) {
|
||||
if config == nil {
|
||||
return nil, "", fmt.Errorf("management config is required")
|
||||
}
|
||||
idpStorage, err := OpenEmbeddedIDPStorage(config.EmbeddedIdP)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return idpStorage, embeddedIDPStorageFile(config), nil
|
||||
}
|
||||
|
||||
func embeddedIDPStorageFile(config *nbconfig.Config) string {
|
||||
if config.EmbeddedIdP == nil || config.EmbeddedIdP.Storage.Type != "sqlite3" {
|
||||
return ""
|
||||
}
|
||||
return config.EmbeddedIdP.Storage.Config.File
|
||||
}
|
||||
|
||||
// CloseIDPStorage closes embedded IdP storage and logs cleanup errors at debug level.
|
||||
func CloseIDPStorage(s storage.Storage) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
if err := s.Close(); err != nil {
|
||||
log.Debugf("close embedded IdP storage: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func addUserSelectorFlags(cmd *cobra.Command, selector *userSelector) {
|
||||
cmd.Flags().StringVar(&selector.email, "email", "", "User email")
|
||||
cmd.Flags().StringVar(&selector.userID, "user-id", "", "User ID")
|
||||
}
|
||||
|
||||
func resolvePasswordInput(cmd *cobra.Command, password, passwordFile string) (string, error) {
|
||||
if password != "" && passwordFile != "" {
|
||||
return "", fmt.Errorf("provide only one of --password or --password-file")
|
||||
}
|
||||
if passwordFile == "" {
|
||||
return password, nil
|
||||
}
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
if passwordFile == "-" {
|
||||
data, err = io.ReadAll(cmd.InOrStdin())
|
||||
} else {
|
||||
data, err = os.ReadFile(passwordFile)
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read password: %w", err)
|
||||
}
|
||||
return strings.TrimRight(string(data), "\r\n"), nil
|
||||
}
|
||||
|
||||
func runChangePassword(ctx context.Context, idpStorage storage.Storage, w io.Writer, selector userSelector, password string, idpStorageFile string) error {
|
||||
if idpStorage == nil {
|
||||
return fmt.Errorf("embedded IdP storage is required")
|
||||
}
|
||||
selector = selector.normalized()
|
||||
if err := selector.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if password == "" {
|
||||
return fmt.Errorf("password is required")
|
||||
}
|
||||
if err := server.ValidatePassword(password); err != nil {
|
||||
return fmt.Errorf("invalid password: %w", err)
|
||||
}
|
||||
|
||||
user, err := findLocalUser(ctx, idpStorage, selector, idpStorageFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hash password: %w", err)
|
||||
}
|
||||
|
||||
if err := idpStorage.UpdatePassword(ctx, user.Email, func(old storage.Password) (storage.Password, error) {
|
||||
old.Hash = hash
|
||||
return old, nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("update password for %s: %w", user.Email, err)
|
||||
}
|
||||
|
||||
if err := deleteLocalAuthSession(ctx, idpStorage, user.UserID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(w, "Password updated for %s.\n", user.Email)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runResetMFA(ctx context.Context, idpStorage storage.Storage, w io.Writer, selector userSelector, idpStorageFile string) error {
|
||||
if idpStorage == nil {
|
||||
return fmt.Errorf("embedded IdP storage is required")
|
||||
}
|
||||
selector = selector.normalized()
|
||||
if err := selector.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := findLocalUser(ctx, idpStorage, selector, idpStorageFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reset := false
|
||||
err = idpStorage.UpdateUserIdentity(ctx, user.UserID, idp.LocalConnectorID, func(old storage.UserIdentity) (storage.UserIdentity, error) {
|
||||
reset = reset || len(old.MFASecrets) > 0 || len(old.WebAuthnCredentials) > 0
|
||||
old.MFASecrets = map[string]*storage.MFASecret{}
|
||||
old.WebAuthnCredentials = map[string][]storage.WebAuthnCredential{}
|
||||
return old, nil
|
||||
})
|
||||
if errors.Is(err, storage.ErrNotFound) {
|
||||
if err := deleteLocalAuthSession(ctx, idpStorage, user.UserID); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "No MFA enrollment found for %s.\n", user.Email)
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("reset MFA for %s: %w", user.Email, err)
|
||||
}
|
||||
|
||||
if err := deleteLocalAuthSession(ctx, idpStorage, user.UserID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if reset {
|
||||
_, _ = fmt.Fprintf(w, "MFA reset for %s. The user will re-enroll at next login.\n", user.Email)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(w, "No MFA enrollment found for %s.\n", user.Email)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSetMFAEnabled(ctx context.Context, resources Resources, w io.Writer, enabled bool) error {
|
||||
if resources.Store == nil {
|
||||
return fmt.Errorf("management store is required")
|
||||
}
|
||||
if resources.IDPStorage == nil {
|
||||
return fmt.Errorf("embedded IdP storage is required")
|
||||
}
|
||||
|
||||
accountID, settings, err := getSingleAccountSettings(ctx, resources.Store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldEnabled := settings.LocalMfaEnabled
|
||||
newSettings := settings.Copy()
|
||||
newSettings.LocalMfaEnabled = enabled
|
||||
|
||||
if err := setIDPClientsMFA(ctx, resources.IDPStorage, enabled); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := resources.Store.SaveAccountSettings(ctx, accountID, newSettings); err != nil {
|
||||
if rollbackErr := setIDPClientsMFA(ctx, resources.IDPStorage, oldEnabled); rollbackErr != nil {
|
||||
return fmt.Errorf("save local MFA account setting: %w (also failed to roll back embedded IdP MFA state: %v)", err, rollbackErr)
|
||||
}
|
||||
return fmt.Errorf("save local MFA account setting: %w", err)
|
||||
}
|
||||
|
||||
if err := storeMFAActivity(ctx, resources.EventStore, accountID, enabled); err != nil {
|
||||
_, _ = fmt.Fprintf(w, "Warning: failed to record audit event: %v\n", err)
|
||||
}
|
||||
|
||||
state := "disabled"
|
||||
if enabled {
|
||||
state = "enabled"
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "Local MFA %s.\n", state)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMFAStatus(ctx context.Context, resources Resources, w io.Writer) error {
|
||||
if resources.Store == nil {
|
||||
return fmt.Errorf("management store is required")
|
||||
}
|
||||
if resources.IDPStorage == nil {
|
||||
return fmt.Errorf("embedded IdP storage is required")
|
||||
}
|
||||
|
||||
_, settings, err := getSingleAccountSettings(ctx, resources.Store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accountStatus := "disabled"
|
||||
if settings.LocalMfaEnabled {
|
||||
accountStatus = "enabled"
|
||||
}
|
||||
|
||||
clientStatus, err := idpClientsMFAStatus(ctx, resources.IDPStorage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(w, "Account setting: %s\n", accountStatus)
|
||||
_, _ = fmt.Fprintf(w, "Embedded IdP clients: %s\n", clientStatus)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSingleAccountSettings(ctx context.Context, s store.Store) (string, *types.Settings, error) {
|
||||
count, err := s.GetAccountsCounter(ctx)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("count accounts: %w", err)
|
||||
}
|
||||
if count != 1 {
|
||||
return "", nil, fmt.Errorf("expected exactly one account, got %d; local MFA is supported only in single-account embedded IdP deployments", count)
|
||||
}
|
||||
|
||||
accountID, err := s.GetAnyAccountID(ctx)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("get account ID: %w", err)
|
||||
}
|
||||
|
||||
settings, err := s.GetAccountSettings(ctx, store.LockingStrengthNone, accountID)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("get account settings: %w", err)
|
||||
}
|
||||
if settings == nil {
|
||||
settings = &types.Settings{}
|
||||
}
|
||||
return accountID, settings, nil
|
||||
}
|
||||
|
||||
func storeMFAActivity(ctx context.Context, eventStore activity.Store, accountID string, enabled bool) error {
|
||||
if eventStore == nil {
|
||||
return nil
|
||||
}
|
||||
event := activity.AccountLocalMfaDisabled
|
||||
if enabled {
|
||||
event = activity.AccountLocalMfaEnabled
|
||||
}
|
||||
_, err := eventStore.Save(ctx, &activity.Event{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Activity: event,
|
||||
InitiatorID: string(hook.SystemSource),
|
||||
TargetID: accountID,
|
||||
AccountID: accountID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("save local MFA audit event: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findLocalUser(ctx context.Context, idpStorage storage.Storage, selector userSelector, idpStorageFile string) (storage.Password, error) {
|
||||
selector = selector.normalized()
|
||||
if err := selector.validate(); err != nil {
|
||||
return storage.Password{}, err
|
||||
}
|
||||
|
||||
if selector.email != "" {
|
||||
user, err := idpStorage.GetPassword(ctx, selector.email)
|
||||
if errors.Is(err, storage.ErrNotFound) {
|
||||
if empty, listErr := localUsersEmpty(ctx, idpStorage); listErr != nil {
|
||||
return storage.Password{}, listErr
|
||||
} else if empty {
|
||||
return storage.Password{}, noLocalUsersError(idpStorageFile)
|
||||
}
|
||||
return storage.Password{}, fmt.Errorf("local user with email %q not found", selector.email)
|
||||
}
|
||||
if err != nil {
|
||||
return storage.Password{}, fmt.Errorf("get local user by email %q: %w", selector.email, err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
rawUserID := selector.userID
|
||||
if decodedUserID, _, err := nbdex.DecodeDexUserID(selector.userID); err == nil && decodedUserID != "" {
|
||||
rawUserID = decodedUserID
|
||||
}
|
||||
|
||||
users, err := idpStorage.ListPasswords(ctx)
|
||||
if err != nil {
|
||||
return storage.Password{}, fmt.Errorf("list local users: %w", err)
|
||||
}
|
||||
for _, user := range users {
|
||||
if user.UserID == rawUserID || user.UserID == selector.userID {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return storage.Password{}, noLocalUsersError(idpStorageFile)
|
||||
}
|
||||
|
||||
return storage.Password{}, fmt.Errorf("local user with ID %q not found", selector.userID)
|
||||
}
|
||||
|
||||
func localUsersEmpty(ctx context.Context, idpStorage storage.Storage) (bool, error) {
|
||||
users, err := idpStorage.ListPasswords(ctx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("list local users: %w", err)
|
||||
}
|
||||
return len(users) == 0, nil
|
||||
}
|
||||
|
||||
func noLocalUsersError(idpStorageFile string) error {
|
||||
location := ""
|
||||
if idpStorageFile != "" {
|
||||
location = fmt.Sprintf(" (%s)", idpStorageFile)
|
||||
}
|
||||
return fmt.Errorf("no local users exist in the embedded IdP storage%s; the management server may never have started with this config, or --datadir points at the wrong location", location)
|
||||
}
|
||||
|
||||
func deleteLocalAuthSession(ctx context.Context, idpStorage storage.Storage, userID string) error {
|
||||
err := idpStorage.DeleteAuthSession(ctx, userID, idp.LocalConnectorID)
|
||||
if err == nil || errors.Is(err, storage.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("delete local auth session for user %s: %w", userID, err)
|
||||
}
|
||||
|
||||
func setIDPClientsMFA(ctx context.Context, idpStorage storage.Storage, enabled bool) error {
|
||||
var mfaChain []string
|
||||
if enabled {
|
||||
mfaChain = []string{idp.DefaultTOTPAuthenticatorID}
|
||||
}
|
||||
|
||||
clientIDs := []string{idp.StaticClientCLI, idp.StaticClientDashboard}
|
||||
if err := nbdex.SetClientsMFAChain(ctx, idpStorage, clientIDs, mfaChain); err != nil {
|
||||
if errors.Is(err, storage.ErrNotFound) {
|
||||
return fmt.Errorf("embedded IdP client not found; start the management server once before toggling MFA: %w", err)
|
||||
}
|
||||
return fmt.Errorf("update MFA chain on embedded IdP clients: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func idpClientsMFAStatus(ctx context.Context, idpStorage storage.Storage) (string, error) {
|
||||
clientIDs := []string{idp.StaticClientCLI, idp.StaticClientDashboard}
|
||||
enabledCount := 0
|
||||
for _, clientID := range clientIDs {
|
||||
client, err := idpStorage.GetClient(ctx, clientID)
|
||||
if errors.Is(err, storage.ErrNotFound) {
|
||||
return "unknown", fmt.Errorf("embedded IdP client %q not found", clientID)
|
||||
}
|
||||
if err != nil {
|
||||
return "unknown", fmt.Errorf("get embedded IdP client %q: %w", clientID, err)
|
||||
}
|
||||
if hasAuthenticator(client.MFAChain, idp.DefaultTOTPAuthenticatorID) {
|
||||
enabledCount++
|
||||
}
|
||||
}
|
||||
|
||||
switch enabledCount {
|
||||
case 0:
|
||||
return "disabled", nil
|
||||
case len(clientIDs):
|
||||
return "enabled", nil
|
||||
default:
|
||||
return "partially enabled", nil
|
||||
}
|
||||
}
|
||||
|
||||
func hasAuthenticator(chain []string, authenticatorID string) bool {
|
||||
for _, id := range chain {
|
||||
if id == authenticatorID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package admincmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dexidp/dex/storage"
|
||||
"github.com/dexidp/dex/storage/memory"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
nbdex "github.com/netbirdio/netbird/idp/dex"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
mgmtstore "github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
|
||||
func newTestIDPStorage(t *testing.T) storage.Storage {
|
||||
t.Helper()
|
||||
|
||||
st := memory.New(slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte("OldPass1!"), bcrypt.DefaultCost)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, st.CreatePassword(context.Background(), storage.Password{
|
||||
Email: "user@example.com",
|
||||
Username: "User",
|
||||
UserID: "user-1",
|
||||
Hash: hash,
|
||||
}))
|
||||
require.NoError(t, st.CreateUserIdentity(context.Background(), storage.UserIdentity{
|
||||
UserID: "user-1",
|
||||
ConnectorID: idp.LocalConnectorID,
|
||||
MFASecrets: map[string]*storage.MFASecret{
|
||||
idp.DefaultTOTPAuthenticatorID: {
|
||||
AuthenticatorID: idp.DefaultTOTPAuthenticatorID,
|
||||
Type: "TOTP",
|
||||
Secret: "otpauth://totp/NetBird:user@example.com?secret=ABC",
|
||||
Confirmed: true,
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
},
|
||||
WebAuthnCredentials: map[string][]storage.WebAuthnCredential{
|
||||
"webauthn": {{CredentialID: []byte("credential")}},
|
||||
},
|
||||
}))
|
||||
require.NoError(t, st.CreateAuthSession(context.Background(), storage.AuthSession{
|
||||
UserID: "user-1",
|
||||
ConnectorID: idp.LocalConnectorID,
|
||||
Nonce: "nonce",
|
||||
}))
|
||||
require.NoError(t, st.CreateClient(context.Background(), storage.Client{ID: idp.StaticClientCLI, Name: "CLI"}))
|
||||
require.NoError(t, st.CreateClient(context.Background(), storage.Client{ID: idp.StaticClientDashboard, Name: "Dashboard"}))
|
||||
|
||||
return st
|
||||
}
|
||||
|
||||
func TestRunChangePassword(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
st := newTestIDPStorage(t)
|
||||
var out bytes.Buffer
|
||||
|
||||
err := runChangePassword(ctx, st, &out, userSelector{email: "user@example.com"}, "NewPass1!", "")
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, out.String(), "Password updated")
|
||||
|
||||
user, err := st.GetPassword(ctx, "user@example.com")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, bcrypt.CompareHashAndPassword(user.Hash, []byte("NewPass1!")))
|
||||
|
||||
_, err = st.GetAuthSession(ctx, "user-1", idp.LocalConnectorID)
|
||||
require.ErrorIs(t, err, storage.ErrNotFound)
|
||||
}
|
||||
|
||||
func TestRunChangePasswordValidatesPassword(t *testing.T) {
|
||||
st := newTestIDPStorage(t)
|
||||
err := runChangePassword(context.Background(), st, io.Discard, userSelector{email: "user@example.com"}, "short", "")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid password")
|
||||
}
|
||||
|
||||
func TestRunResetMFA(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
st := newTestIDPStorage(t)
|
||||
var out bytes.Buffer
|
||||
|
||||
encodedUserID := nbdex.EncodeDexUserID("user-1", idp.LocalConnectorID)
|
||||
err := runResetMFA(ctx, st, &out, userSelector{userID: encodedUserID}, "")
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, out.String(), "MFA reset")
|
||||
|
||||
identity, err := st.GetUserIdentity(ctx, "user-1", idp.LocalConnectorID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, identity.MFASecrets)
|
||||
require.Empty(t, identity.WebAuthnCredentials)
|
||||
|
||||
_, err = st.GetAuthSession(ctx, "user-1", idp.LocalConnectorID)
|
||||
require.ErrorIs(t, err, storage.ErrNotFound)
|
||||
}
|
||||
|
||||
func TestRunResetMFAWithoutEnrollment(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
st := newTestIDPStorage(t)
|
||||
require.NoError(t, st.UpdateUserIdentity(ctx, "user-1", idp.LocalConnectorID, func(old storage.UserIdentity) (storage.UserIdentity, error) {
|
||||
old.MFASecrets = nil
|
||||
old.WebAuthnCredentials = nil
|
||||
return old, nil
|
||||
}))
|
||||
|
||||
var out bytes.Buffer
|
||||
err := runResetMFA(ctx, st, &out, userSelector{email: "user@example.com"}, "")
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, out.String(), "No MFA enrollment found")
|
||||
}
|
||||
|
||||
func TestSetIDPClientsMFA(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
st := newTestIDPStorage(t)
|
||||
|
||||
require.NoError(t, setIDPClientsMFA(ctx, st, true))
|
||||
status, err := idpClientsMFAStatus(ctx, st)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "enabled", status)
|
||||
|
||||
require.NoError(t, setIDPClientsMFA(ctx, st, false))
|
||||
status, err = idpClientsMFAStatus(ctx, st)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "disabled", status)
|
||||
}
|
||||
|
||||
func newTestManagementStore(t *testing.T, localMFAEnabled bool) mgmtstore.Store {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
st, err := mgmtstore.NewStore(ctx, types.SqliteStoreEngine, t.TempDir(), nil, false)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, st.Close(ctx)) })
|
||||
require.NoError(t, st.SaveAccount(ctx, &types.Account{
|
||||
Id: "account-1",
|
||||
Settings: &types.Settings{LocalMfaEnabled: localMFAEnabled},
|
||||
}))
|
||||
return st
|
||||
}
|
||||
|
||||
func TestRunSetMFAEnabledDoesNotSaveWhenIDPUpdateFails(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
managementStore := newTestManagementStore(t, false)
|
||||
idpStorage := memory.New(slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
|
||||
err := runSetMFAEnabled(ctx, Resources{Store: managementStore, IDPStorage: idpStorage}, io.Discard, true)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "embedded IdP client")
|
||||
|
||||
settings, err := managementStore.GetAccountSettings(ctx, mgmtstore.LockingStrengthNone, "account-1")
|
||||
require.NoError(t, err)
|
||||
require.False(t, settings.LocalMfaEnabled)
|
||||
}
|
||||
|
||||
func TestRunSetMFAEnabledUpdatesSettingsAfterIDP(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
managementStore := newTestManagementStore(t, false)
|
||||
idpStorage := newTestIDPStorage(t)
|
||||
|
||||
err := runSetMFAEnabled(ctx, Resources{Store: managementStore, IDPStorage: idpStorage}, io.Discard, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
settings, err := managementStore.GetAccountSettings(ctx, mgmtstore.LockingStrengthNone, "account-1")
|
||||
require.NoError(t, err)
|
||||
require.True(t, settings.LocalMfaEnabled)
|
||||
clientStatus, err := idpClientsMFAStatus(ctx, idpStorage)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "enabled", clientStatus)
|
||||
}
|
||||
|
||||
func TestRunSetMFAEnabledSucceedsWithNilEventStore(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
managementStore := newTestManagementStore(t, false)
|
||||
idpStorage := newTestIDPStorage(t)
|
||||
var out bytes.Buffer
|
||||
var err error
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
err = runSetMFAEnabled(ctx, Resources{Store: managementStore, IDPStorage: idpStorage, EventStore: nil}, &out, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, out.String(), "Local MFA enabled")
|
||||
|
||||
settings, err := managementStore.GetAccountSettings(ctx, mgmtstore.LockingStrengthNone, "account-1")
|
||||
require.NoError(t, err)
|
||||
require.True(t, settings.LocalMfaEnabled)
|
||||
}
|
||||
|
||||
func TestUserSelectorValidate(t *testing.T) {
|
||||
require.NoError(t, userSelector{email: " user@example.com "}.validate())
|
||||
require.NoError(t, userSelector{userID: "user-1"}.validate())
|
||||
require.Error(t, userSelector{}.validate())
|
||||
require.Error(t, userSelector{email: "user@example.com", userID: "user-1"}.validate())
|
||||
}
|
||||
|
||||
func TestFindLocalUserNotFound(t *testing.T) {
|
||||
st := newTestIDPStorage(t)
|
||||
_, err := findLocalUser(context.Background(), st, userSelector{email: "missing@example.com"}, "")
|
||||
require.Error(t, err)
|
||||
require.True(t, strings.Contains(err.Error(), "not found"))
|
||||
}
|
||||
|
||||
func TestFindLocalUserZeroUsersIncludesStoragePath(t *testing.T) {
|
||||
st := memory.New(slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
_, err := findLocalUser(context.Background(), st, userSelector{email: "missing@example.com"}, "/var/lib/netbird/idp.db")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "no local users exist")
|
||||
require.Contains(t, err.Error(), "/var/lib/netbird/idp.db")
|
||||
}
|
||||
|
||||
func TestUserCommandValidatesSelectorBeforeOpeningStorage(t *testing.T) {
|
||||
opened := false
|
||||
cmd := NewCommands(Openers{
|
||||
IDP: func(cmd *cobra.Command, fn func(ctx context.Context, idpStorage storage.Storage, storageFile string) error) error {
|
||||
opened = true
|
||||
return nil
|
||||
},
|
||||
})
|
||||
cmd.SetArgs([]string{"user", "change-password", "--password", "NewPass1!"})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
err := cmd.Execute()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "provide exactly one")
|
||||
require.False(t, opened)
|
||||
}
|
||||
|
||||
func TestResolvePasswordInputFromStdin(t *testing.T) {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.SetIn(strings.NewReader("NewPass1!\n"))
|
||||
|
||||
password, err := resolvePasswordInput(cmd, "", "-")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "NewPass1!", password)
|
||||
}
|
||||
|
||||
func TestResolvePasswordInputRejectsMultipleSources(t *testing.T) {
|
||||
_, err := resolvePasswordInput(&cobra.Command{}, "NewPass1!", "-")
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
)
|
||||
|
||||
func TestApplyAdminDatadirOverrideRelocatesDefaultIDPStorage(t *testing.T) {
|
||||
oldDatadir := filepath.Join(t.TempDir(), "old")
|
||||
newDatadir := filepath.Join(t.TempDir(), "new")
|
||||
|
||||
for _, defaultFile := range []string{
|
||||
"",
|
||||
filepath.Join(oldDatadir, "idp.db"),
|
||||
path.Join(oldDatadir, "idp.db"),
|
||||
} {
|
||||
t.Run(defaultFile, func(t *testing.T) {
|
||||
cfg := &nbconfig.Config{
|
||||
EmbeddedIdP: &idp.EmbeddedIdPConfig{
|
||||
Enabled: true,
|
||||
Storage: idp.EmbeddedStorageConfig{
|
||||
Type: "sqlite3",
|
||||
Config: idp.EmbeddedStorageTypeConfig{
|
||||
File: defaultFile,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
datadir := oldDatadir
|
||||
oldAdminDatadir := adminDatadir
|
||||
adminDatadir = newDatadir
|
||||
t.Cleanup(func() { adminDatadir = oldAdminDatadir })
|
||||
|
||||
applyAdminDatadirOverride(cfg, &datadir)
|
||||
|
||||
require.Equal(t, newDatadir, datadir)
|
||||
require.Equal(t, filepath.Join(newDatadir, "idp.db"), cfg.EmbeddedIdP.Storage.Config.File)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAdminEventStoreMissingEncryptionKeyReturnsNilInterface(t *testing.T) {
|
||||
eventStore, err := openAdminEventStore(context.Background(), &nbconfig.Config{}, t.TempDir())
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "encryption key")
|
||||
require.Nil(t, eventStore)
|
||||
}
|
||||
|
||||
func TestApplyAdminDatadirOverrideKeepsExplicitIDPStorage(t *testing.T) {
|
||||
oldDatadir := filepath.Join(t.TempDir(), "old")
|
||||
newDatadir := filepath.Join(t.TempDir(), "new")
|
||||
explicitFile := filepath.Join(t.TempDir(), "custom-idp.db")
|
||||
cfg := &nbconfig.Config{
|
||||
EmbeddedIdP: &idp.EmbeddedIdPConfig{
|
||||
Enabled: true,
|
||||
Storage: idp.EmbeddedStorageConfig{
|
||||
Type: "sqlite3",
|
||||
Config: idp.EmbeddedStorageTypeConfig{
|
||||
File: explicitFile,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
datadir := oldDatadir
|
||||
oldAdminDatadir := adminDatadir
|
||||
adminDatadir = newDatadir
|
||||
t.Cleanup(func() { adminDatadir = oldAdminDatadir })
|
||||
|
||||
applyAdminDatadirOverride(cfg, &datadir)
|
||||
|
||||
require.Equal(t, newDatadir, datadir)
|
||||
require.Equal(t, explicitFile, cfg.EmbeddedIdP.Storage.Config.File)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
@@ -22,9 +23,11 @@ import (
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
|
||||
"github.com/netbirdio/netbird/formatter/hook"
|
||||
agentnetworkpricing "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/pricing"
|
||||
"github.com/netbirdio/netbird/management/internals/server"
|
||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||
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/crypt"
|
||||
)
|
||||
@@ -110,6 +113,29 @@ var (
|
||||
mgmtSingleAccModeDomain = ""
|
||||
}
|
||||
|
||||
// Load the management-side LLM pricing defaults file: an
|
||||
// explicitly configured path is required to load (a typo must
|
||||
// fail startup — the operator believes those rates are live);
|
||||
// otherwise <datadir>/defaults_llm_pricing.yaml is probed and
|
||||
// may be absent (compiled-in defaults serve). A relative path
|
||||
// is resolved against the datadir so a bare filename lands
|
||||
// alongside the store. Either way the path stays watched: the
|
||||
// reloader picks up edits — and the file appearing later —
|
||||
// without a restart.
|
||||
pricingPath := config.AgentNetwork.PricingDefaultsFile
|
||||
pricingRequired := pricingPath != ""
|
||||
if !pricingRequired {
|
||||
pricingPath = agentnetworkpricing.DefaultFileName
|
||||
}
|
||||
if !filepath.IsAbs(pricingPath) {
|
||||
pricingPath = filepath.Join(config.Datadir, pricingPath)
|
||||
}
|
||||
log.Infof("loading agent-network pricing defaults from %s (required: %v)", pricingPath, pricingRequired)
|
||||
if err := agentnetworkpricing.LoadFile(pricingPath, pricingRequired); err != nil {
|
||||
return fmt.Errorf("load agent-network pricing defaults: %v", err)
|
||||
}
|
||||
agentnetworkpricing.StartReloader(ctx, agentnetworkpricing.ReloadInterval)
|
||||
|
||||
srv := newServer(&server.Config{
|
||||
NbConfig: config,
|
||||
DNSDomain: dnsDomain,
|
||||
@@ -153,8 +179,20 @@ func LoadMgmtConfig(ctx context.Context, mgmtConfigPath string) (*nbconfig.Confi
|
||||
|
||||
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
|
||||
err := ApplyEmbeddedIdPConfig(ctx, loadedConfig)
|
||||
err = ApplyEmbeddedIdPConfig(ctx, loadedConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -198,6 +236,9 @@ func ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config) error {
|
||||
// Embedded IdP requires single account mode - multiple account mode is not supported
|
||||
return fmt.Errorf("embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP. Please remove --disable-single-account-mode flag")
|
||||
}
|
||||
if mgmtSingleAccModeDomain == "" {
|
||||
return fmt.Errorf("embedded IdP requires single account mode; --single-account-mode-domain must not be empty")
|
||||
}
|
||||
// Enable user deletion from IDP by default if EmbeddedIdP is enabled
|
||||
userDeleteFromIDPEnabled = true
|
||||
|
||||
@@ -209,7 +250,7 @@ func ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config) error {
|
||||
cfg.EmbeddedIdP.Storage.Type = "sqlite3"
|
||||
}
|
||||
if cfg.EmbeddedIdP.Storage.Config.File == "" && cfg.Datadir != "" {
|
||||
cfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, "idp.db")
|
||||
cfg.EmbeddedIdP.Storage.Config.File = filepath.Join(cfg.Datadir, "idp.db")
|
||||
}
|
||||
|
||||
issuer := cfg.EmbeddedIdP.Issuer
|
||||
|
||||
@@ -4,6 +4,13 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
"github.com/netbirdio/netbird/shared/management/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -20,34 +27,65 @@ const (
|
||||
"AuthAudience": "https://stageapp/",
|
||||
"AuthIssuer": "https://something.eu.auth0.com/",
|
||||
"OIDCConfigEndpoint": "https://something.eu.auth0.com/.well-known/openid-configuration"
|
||||
},
|
||||
"HighestSupportedSyncMessageVersion": 1,
|
||||
"PerAccountHighestSupportedSyncMessageVersion": {
|
||||
"1": 0,
|
||||
"2": 1
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
func Test_loadMgmtConfig(t *testing.T) {
|
||||
tmpFile, err := createConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create config: %s", err)
|
||||
}
|
||||
func Test_LoadMgmtConfig(t *testing.T) {
|
||||
tmpFile, err := createConfig(exampleConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
cfg, err := LoadMgmtConfig(context.Background(), tmpFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load management config: %s", err)
|
||||
}
|
||||
if cfg.Relay == nil {
|
||||
t.Fatalf("config is nil")
|
||||
}
|
||||
if len(cfg.Relay.Addresses) == 0 {
|
||||
t.Fatalf("relay address is empty")
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, cfg.Relay)
|
||||
assert.NotEmpty(t, cfg.Relay.Addresses)
|
||||
assert.Equal(t, int(grpc.ComponentNetworkMap), *cfg.HighestSupportedSyncMessageVersion)
|
||||
assert.Equal(t, map[string]int{"1": int(grpc.Base), "2": int(grpc.ComponentNetworkMap)}, cfg.PerAccountHighestSupportedSyncMessageVersion)
|
||||
}
|
||||
|
||||
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 TestApplyEmbeddedIdPConfigRequiresSingleAccountDomain(t *testing.T) {
|
||||
previousDomain := mgmtSingleAccModeDomain
|
||||
previousDisabled := disableSingleAccMode
|
||||
t.Cleanup(func() {
|
||||
mgmtSingleAccModeDomain = previousDomain
|
||||
disableSingleAccMode = previousDisabled
|
||||
})
|
||||
|
||||
mgmtSingleAccModeDomain = ""
|
||||
disableSingleAccMode = false
|
||||
cfg := &nbconfig.Config{
|
||||
EmbeddedIdP: &idp.EmbeddedIdPConfig{Enabled: true},
|
||||
}
|
||||
require.ErrorContains(t, ApplyEmbeddedIdPConfig(context.Background(), cfg), "embedded IdP requires single account mode")
|
||||
}
|
||||
|
||||
func createConfig(config string) (string, error) {
|
||||
tmpfile, err := os.CreateTemp("", "config.json")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = tmpfile.Write([]byte(exampleConfig))
|
||||
_, err = tmpfile.Write([]byte(config))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// Package proxycmd provides reusable cobra commands for managing reverse proxy instances.
|
||||
// Both the management and combined binaries use these commands, each providing
|
||||
// their own StoreOpener to handle config loading and store initialization.
|
||||
package proxycmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
rpproxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
)
|
||||
|
||||
// StoreOpener initializes a store from the command context and calls fn.
|
||||
type StoreOpener func(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error
|
||||
|
||||
const disconnectAllConfirmation = "disconnect all proxies"
|
||||
|
||||
// NewCommands creates the proxy command tree with the given store opener.
|
||||
// Returns the parent "proxy" command with the disconnect-all subcommand.
|
||||
func NewCommands(opener StoreOpener) *cobra.Command {
|
||||
var dryRun bool
|
||||
var force bool
|
||||
|
||||
proxyCmd := &cobra.Command{
|
||||
Use: "proxy",
|
||||
Short: "Manage reverse proxy instances",
|
||||
Long: "Commands for inspecting and repairing the reverse proxy instances registered with the management server.",
|
||||
}
|
||||
|
||||
disconnectAllCmd := &cobra.Command{
|
||||
Use: "disconnect-all",
|
||||
Short: "Force-mark all reverse proxy instances as disconnected",
|
||||
Long: "Lists all reverse proxy instances and force-marks them as disconnected, regardless of their session state. " +
|
||||
"Use this to repair stale connection state, e.g. after an unclean management server shutdown. " +
|
||||
"By default, it asks for manual confirmation before changing state. Use --dry-run to preview without changing state, or --force to skip confirmation. " +
|
||||
"Run during a maintenance window; affected live proxies may stay hidden until their next heartbeat or reconnect/re-register.",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return opener(cmd, func(ctx context.Context, s store.Store) error {
|
||||
return runDisconnectAll(ctx, s, cmd.OutOrStdout(), cmd.InOrStdin(), dryRun, force)
|
||||
})
|
||||
},
|
||||
}
|
||||
disconnectAllCmd.Flags().BoolVar(&dryRun, "dry-run", false, "List reverse proxy instances that would be disconnected without changing state")
|
||||
disconnectAllCmd.Flags().BoolVar(&force, "force", false, "Skip the confirmation prompt and apply the repair")
|
||||
|
||||
proxyCmd.AddCommand(disconnectAllCmd)
|
||||
return proxyCmd
|
||||
}
|
||||
|
||||
func runDisconnectAll(ctx context.Context, s store.Store, out io.Writer, in io.Reader, dryRun, force bool) error {
|
||||
proxies, err := s.GetAllProxies(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list proxies: %w", err)
|
||||
}
|
||||
|
||||
if len(proxies) == 0 {
|
||||
_, _ = fmt.Fprintln(out, "No reverse proxy instances found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
toDisconnect := 0
|
||||
w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintln(w, "ID\tCLUSTER\tIP\tACCOUNT\tSTATUS\tLAST SEEN")
|
||||
_, _ = fmt.Fprintln(w, "--\t-------\t--\t-------\t------\t---------")
|
||||
|
||||
for _, p := range proxies {
|
||||
if p.Status != rpproxy.StatusDisconnected {
|
||||
toDisconnect++
|
||||
}
|
||||
|
||||
account := "-"
|
||||
if p.AccountID != nil {
|
||||
account = *p.AccountID
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||
p.ID,
|
||||
p.ClusterAddress,
|
||||
p.IPAddress,
|
||||
account,
|
||||
p.Status,
|
||||
p.LastSeen.Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
}
|
||||
if err := w.Flush(); err != nil {
|
||||
return fmt.Errorf("write proxy list: %w", err)
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
_, _ = fmt.Fprintf(out, "\nDry run: would force-mark %d of %d reverse proxy instance(s) as disconnected.\n", toDisconnect, len(proxies))
|
||||
return nil
|
||||
}
|
||||
|
||||
if !force {
|
||||
confirmed, err := confirmDisconnectAll(out, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !confirmed {
|
||||
_, _ = fmt.Fprintln(out, "Aborted. No reverse proxy instances were changed.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
disconnected, err := s.DisconnectAllProxies(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("disconnect proxies: %w", err)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(out, "\nForce-marked %d of %d reverse proxy instance(s) as disconnected.\n", disconnected, len(proxies))
|
||||
return nil
|
||||
}
|
||||
|
||||
func confirmDisconnectAll(out io.Writer, in io.Reader) (bool, error) {
|
||||
if in == nil {
|
||||
in = strings.NewReader("")
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(out, "\nWARNING: This command changes stored reverse proxy state for every non-disconnected instance.")
|
||||
_, _ = fmt.Fprintln(out, "Run it during a maintenance window; affected live proxies may stay hidden until "+
|
||||
"their next heartbeat or reconnect/re-register.")
|
||||
_, _ = fmt.Fprintf(out, "Type %q to continue: ", disconnectAllConfirmation)
|
||||
|
||||
scanner := bufio.NewScanner(in)
|
||||
if !scanner.Scan() {
|
||||
if err := scanner.Err(); err != nil {
|
||||
return false, fmt.Errorf("read confirmation: %w", err)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return strings.EqualFold(strings.TrimSpace(scanner.Text()), disconnectAllConfirmation), nil
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package proxycmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
rpproxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
)
|
||||
|
||||
func newTestStore(t *testing.T) store.Store {
|
||||
t.Helper()
|
||||
|
||||
s, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func seedProxies(t *testing.T, ctx context.Context, s store.Store) {
|
||||
t.Helper()
|
||||
|
||||
accountID := "account-1"
|
||||
alreadyDisconnectedAt := time.Now().Add(-time.Hour)
|
||||
seed := []*rpproxy.Proxy{
|
||||
{
|
||||
ID: "proxy-1",
|
||||
SessionID: "session-1",
|
||||
ClusterAddress: "cluster-a.example.com",
|
||||
IPAddress: "10.0.0.1",
|
||||
LastSeen: time.Now(),
|
||||
Status: rpproxy.StatusConnected,
|
||||
},
|
||||
{
|
||||
ID: "proxy-2",
|
||||
SessionID: "session-2",
|
||||
ClusterAddress: "cluster-b.example.com",
|
||||
IPAddress: "10.0.0.2",
|
||||
AccountID: &accountID,
|
||||
LastSeen: time.Now(),
|
||||
Status: rpproxy.StatusConnected,
|
||||
},
|
||||
{
|
||||
ID: "proxy-3",
|
||||
SessionID: "session-3",
|
||||
ClusterAddress: "cluster-a.example.com",
|
||||
IPAddress: "10.0.0.3",
|
||||
LastSeen: time.Now().Add(-time.Hour),
|
||||
Status: rpproxy.StatusDisconnected,
|
||||
DisconnectedAt: &alreadyDisconnectedAt,
|
||||
},
|
||||
}
|
||||
for _, p := range seed {
|
||||
require.NoError(t, s.SaveProxy(ctx, p))
|
||||
}
|
||||
}
|
||||
|
||||
func proxiesByID(t *testing.T, ctx context.Context, s store.Store) map[string]*rpproxy.Proxy {
|
||||
t.Helper()
|
||||
|
||||
proxies, err := s.GetAllProxies(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, proxies, 3)
|
||||
|
||||
byID := make(map[string]*rpproxy.Proxy, len(proxies))
|
||||
for _, p := range proxies {
|
||||
byID[p.ID] = p
|
||||
}
|
||||
return byID
|
||||
}
|
||||
|
||||
func TestRunDisconnectAllWithConfirmation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestStore(t)
|
||||
seedProxies(t, ctx, s)
|
||||
|
||||
var out bytes.Buffer
|
||||
require.NoError(t, runDisconnectAll(ctx, s, &out, strings.NewReader(disconnectAllConfirmation+"\n"), false, false))
|
||||
|
||||
output := out.String()
|
||||
require.Contains(t, output, "proxy-1")
|
||||
require.Contains(t, output, "proxy-2")
|
||||
require.Contains(t, output, "proxy-3")
|
||||
require.Contains(t, output, "cluster-a.example.com")
|
||||
require.Contains(t, output, "account-1")
|
||||
require.Contains(t, output, "Type \"disconnect all proxies\" to continue")
|
||||
require.Contains(t, output, "Force-marked 2 of 3 reverse proxy instance(s) as disconnected.")
|
||||
|
||||
for _, p := range proxiesByID(t, ctx, s) {
|
||||
require.Equal(t, rpproxy.StatusDisconnected, p.Status, "proxy %s should be disconnected", p.ID)
|
||||
require.NotNil(t, p.DisconnectedAt, "proxy %s should have a disconnected timestamp", p.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDisconnectAllForceSkipsConfirmation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestStore(t)
|
||||
seedProxies(t, ctx, s)
|
||||
|
||||
var out bytes.Buffer
|
||||
require.NoError(t, runDisconnectAll(ctx, s, &out, strings.NewReader(""), false, true))
|
||||
|
||||
output := out.String()
|
||||
require.NotContains(t, output, "Type \"disconnect all proxies\" to continue")
|
||||
require.Contains(t, output, "Force-marked 2 of 3 reverse proxy instance(s) as disconnected.")
|
||||
}
|
||||
|
||||
func TestRunDisconnectAllAbortLeavesProxiesUnchanged(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestStore(t)
|
||||
seedProxies(t, ctx, s)
|
||||
|
||||
var out bytes.Buffer
|
||||
require.NoError(t, runDisconnectAll(ctx, s, &out, strings.NewReader("no\n"), false, false))
|
||||
|
||||
output := out.String()
|
||||
require.Contains(t, output, "Type \"disconnect all proxies\" to continue")
|
||||
require.Contains(t, output, "Aborted. No reverse proxy instances were changed.")
|
||||
|
||||
byID := proxiesByID(t, ctx, s)
|
||||
require.Equal(t, rpproxy.StatusConnected, byID["proxy-1"].Status)
|
||||
require.Equal(t, rpproxy.StatusConnected, byID["proxy-2"].Status)
|
||||
require.Equal(t, rpproxy.StatusDisconnected, byID["proxy-3"].Status)
|
||||
}
|
||||
|
||||
func TestRunDisconnectAllDryRunLeavesProxiesUnchanged(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestStore(t)
|
||||
seedProxies(t, ctx, s)
|
||||
|
||||
var out bytes.Buffer
|
||||
require.NoError(t, runDisconnectAll(ctx, s, &out, strings.NewReader(""), true, false))
|
||||
|
||||
output := out.String()
|
||||
require.Contains(t, output, "Dry run: would force-mark 2 of 3 reverse proxy instance(s) as disconnected.")
|
||||
require.NotContains(t, output, "Type \"disconnect all proxies\" to continue")
|
||||
|
||||
byID := proxiesByID(t, ctx, s)
|
||||
require.Equal(t, rpproxy.StatusConnected, byID["proxy-1"].Status)
|
||||
require.Equal(t, rpproxy.StatusConnected, byID["proxy-2"].Status)
|
||||
require.Equal(t, rpproxy.StatusDisconnected, byID["proxy-3"].Status)
|
||||
}
|
||||
|
||||
func TestNewCommandsDisconnectAllDryRun(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestStore(t)
|
||||
seedProxies(t, ctx, s)
|
||||
|
||||
opened := false
|
||||
cmd := NewCommands(func(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error {
|
||||
opened = true
|
||||
return fn(cmd.Context(), s)
|
||||
})
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetErr(&out)
|
||||
cmd.SetIn(strings.NewReader(""))
|
||||
cmd.SetArgs([]string{"disconnect-all", "--dry-run"})
|
||||
|
||||
require.NoError(t, cmd.ExecuteContext(ctx))
|
||||
require.True(t, opened)
|
||||
require.Contains(t, out.String(), "Dry run: would force-mark 2 of 3 reverse proxy instance(s) as disconnected.")
|
||||
}
|
||||
|
||||
func TestRunDisconnectAllEmpty(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestStore(t)
|
||||
|
||||
var out bytes.Buffer
|
||||
require.NoError(t, runDisconnectAll(ctx, s, &out, strings.NewReader(""), false, false))
|
||||
require.Contains(t, out.String(), "No reverse proxy instances found.")
|
||||
}
|
||||
+13
-3
@@ -54,6 +54,15 @@ func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
// Customize hands the fully built root command to fn so an embedding binary
|
||||
// can extend or adjust the command tree — most commonly attaching its own
|
||||
// subcommands next to (or under) the built-in ones — before calling Execute.
|
||||
// The root command is constructed in this package's init, so Customize may be
|
||||
// called from the embedding binary's main at any point before Execute.
|
||||
func Customize(fn func(root *cobra.Command)) {
|
||||
fn(rootCmd)
|
||||
}
|
||||
|
||||
func init() {
|
||||
mgmtCmd.Flags().IntVar(&mgmtPort, "port", 80, "server port to listen on (defaults to 443 if TLS is enabled, 80 otherwise")
|
||||
mgmtCmd.Flags().BoolVar(&disableLegacyManagementPort, "disable-legacy-port", false, "disabling the old legacy port (33073)")
|
||||
@@ -83,7 +92,8 @@ func init() {
|
||||
|
||||
rootCmd.AddCommand(migrationCmd)
|
||||
|
||||
tc := newTokenCommands()
|
||||
tc.PersistentFlags().StringVar(&nbconfig.MgmtConfigPath, "config", defaultMgmtConfig, "Netbird config file location")
|
||||
rootCmd.AddCommand(tc)
|
||||
ac := newAdminCommands()
|
||||
ac.PersistentFlags().StringVar(&nbconfig.MgmtConfigPath, "config", defaultMgmtConfig, "Netbird config file location")
|
||||
rootCmd.AddCommand(ac)
|
||||
rootCmd.AddCommand(newLegacyTokenCommand())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// TestCustomize verifies an embedding binary can extend the command tree: a
|
||||
// top-level command attached through the hook, and a subcommand attached under
|
||||
// the built-in admin group, are both resolvable exactly as Execute would
|
||||
// resolve them.
|
||||
func TestCustomize(t *testing.T) {
|
||||
topLevel := &cobra.Command{Use: "some-extra", RunE: func(*cobra.Command, []string) error { return nil }}
|
||||
nested := &cobra.Command{Use: "cluster", RunE: func(*cobra.Command, []string) error { return nil }}
|
||||
|
||||
Customize(func(root *cobra.Command) {
|
||||
root.AddCommand(topLevel)
|
||||
for _, c := range root.Commands() {
|
||||
if c.Name() == "admin" {
|
||||
c.AddCommand(nested)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("admin command not found in the root tree")
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
rootCmd.RemoveCommand(topLevel)
|
||||
for _, c := range rootCmd.Commands() {
|
||||
if c.Name() == "admin" {
|
||||
c.RemoveCommand(nested)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if found, _, err := rootCmd.Find([]string{"some-extra"}); err != nil || found != topLevel {
|
||||
t.Fatalf("top-level command not resolvable: found=%v err=%v", found, err)
|
||||
}
|
||||
if found, _, err := rootCmd.Find([]string{"admin", "cluster"}); err != nil || found != nested {
|
||||
t.Fatalf("nested admin subcommand not resolvable: found=%v err=%v", found, err)
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/netbirdio/netbird/formatter/hook"
|
||||
tokencmd "github.com/netbirdio/netbird/management/cmd/token"
|
||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
)
|
||||
|
||||
var tokenDatadir string
|
||||
|
||||
// newTokenCommands creates the token command tree with management-specific store opener.
|
||||
func newTokenCommands() *cobra.Command {
|
||||
cmd := tokencmd.NewCommands(withTokenStore)
|
||||
cmd.PersistentFlags().StringVar(&tokenDatadir, "datadir", "", "Override the data directory from config (where store.db is located)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// withTokenStore initializes logging, loads config, opens the store, and calls fn.
|
||||
func withTokenStore(cmd *cobra.Command, fn func(ctx context.Context, s store.Store) error) error {
|
||||
if err := util.InitLog("error", "console"); err != nil {
|
||||
return fmt.Errorf("init log: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.WithValue(cmd.Context(), hook.ExecutionContextKey, hook.SystemSource) //nolint:staticcheck
|
||||
|
||||
config, err := LoadMgmtConfig(ctx, nbconfig.MgmtConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
datadir := config.Datadir
|
||||
if tokenDatadir != "" {
|
||||
datadir = tokenDatadir
|
||||
}
|
||||
|
||||
s, err := store.NewStore(ctx, config.StoreConfig.Engine, datadir, nil, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create store: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := s.Close(ctx); err != nil {
|
||||
log.Debugf("close store: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return fn(ctx, s)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,15 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||
"github.com/netbirdio/netbird/management/server/account"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestComputeForwarderPort(t *testing.T) {
|
||||
@@ -107,3 +112,22 @@ func TestComputeForwarderPort(t *testing.T) {
|
||||
t.Errorf("Expected %d for peers with unknown version, got %d", network_map.OldForwarderPort, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetValidatedPeerWithComponents_DeletedPeer(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mockrequestBuffer := account.NewMockRequestBuffer(ctrl)
|
||||
|
||||
c := Controller{
|
||||
requestBuffer: mockrequestBuffer,
|
||||
}
|
||||
|
||||
mockrequestBuffer.EXPECT().GetAccountWithBackpressure(gomock.Any(), gomock.Any()).Return(&types.Account{}, nil)
|
||||
peer, components, netmap, posturechecks, dnsforwardPort, err := c.GetValidatedPeerWithComponents(context.TODO(), false, "test-account-id", &nbpeer.Peer{ID: "test-peer-id"})
|
||||
|
||||
assert.Nil(t, peer)
|
||||
assert.Nil(t, components)
|
||||
assert.Nil(t, netmap)
|
||||
assert.Nil(t, posturechecks)
|
||||
assert.Equal(t, int64(0), dnsforwardPort)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
// The account-side builder (types.Account.peerIPv6AllowedSet) is the reference:
|
||||
// an account with no IPv6-enabled group runs no IPv6 overlay at all, embedded
|
||||
// proxy peers included — see TestPeerIPv6AllowedEmbeddedProxy. Both builders
|
||||
// gate the same AAAA records, so the store-backed one has to agree.
|
||||
func TestIPv6AllowedPeersFromData(t *testing.T) {
|
||||
data := func(enabledGroups []string) *networkmap.NetworkMapData {
|
||||
return &networkmap.NetworkMapData{
|
||||
AccountSettings: &nmdata.AccountSettingsInfo{IPv6EnabledGroups: enabledGroups},
|
||||
Peers: map[string]*nmdata.Peer{
|
||||
"peer1": {ID: "peer1"},
|
||||
"lonely": {ID: "lonely"},
|
||||
"proxy": {ID: "proxy", ProxyMeta: nmdata.ProxyMeta{Embedded: true, Cluster: "netbird.test"}},
|
||||
},
|
||||
Groups: map[string]*nmdata.Group{
|
||||
"group-devs": {ID: "group-devs", Peers: []string{"peer1"}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("embedded proxy allowed when any v6 group exists, without group membership", func(t *testing.T) {
|
||||
allowed := IPv6AllowedPeersFromData(data([]string{"group-devs"}))
|
||||
assert.Contains(t, allowed, "proxy", "embedded proxy participates in v6 overlay")
|
||||
assert.Contains(t, allowed, "peer1", "regular peer in enabled group still allowed")
|
||||
})
|
||||
|
||||
t.Run("embedded proxy denied when no v6 group enabled", func(t *testing.T) {
|
||||
allowed := IPv6AllowedPeersFromData(data(nil))
|
||||
assert.NotContains(t, allowed, "proxy", "v6 disabled account-wide denies embedded proxies too")
|
||||
assert.Empty(t, allowed, "no peer participates in the v6 overlay")
|
||||
})
|
||||
|
||||
t.Run("non-embedded peer outside any enabled group is not pulled in", func(t *testing.T) {
|
||||
allowed := IPv6AllowedPeersFromData(data([]string{"group-devs"}))
|
||||
assert.NotContains(t, allowed, "lonely", "embedded-proxy bypass must not leak to regular peers")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/types"
|
||||
)
|
||||
|
||||
func postureSelectionData(policies ...*nmdata.Policy) *networkmap.NetworkMapData {
|
||||
return &networkmap.NetworkMapData{
|
||||
Groups: map[string]*nmdata.Group{"g-src": {ID: "g-src", Peers: []string{"peer-group"}}},
|
||||
Policies: policies,
|
||||
PostureChecks: map[string]*nmdata.PostureChecks{
|
||||
"pc1": {ID: "pc1", Checks: nmdata.ChecksDefinition{NBVersionCheck: &nmdata.NBVersionCheck{MinVersion: "0.30.0"}}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func gatedPolicy(id string, rule *nmdata.PolicyRule, checkIDs ...string) *nmdata.Policy {
|
||||
return &nmdata.Policy{ID: id, Enabled: true, SourcePostureChecks: checkIDs, Rules: []*nmdata.PolicyRule{rule}}
|
||||
}
|
||||
|
||||
func checkIDs(checks []*nmdata.PostureChecks) []string {
|
||||
ids := make([]string, 0, len(checks))
|
||||
for _, c := range checks {
|
||||
ids = append(ids, c.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func TestPeerPostureChecksFromData_SelectsPolicySourcePeers(t *testing.T) {
|
||||
groupRule := &nmdata.PolicyRule{ID: "r-group", Enabled: true, Sources: []string{"g-src"}, Destinations: []string{"g-dst"}}
|
||||
directRule := &nmdata.PolicyRule{ID: "r-direct", Enabled: true, SourceResource: nmdata.Resource{ID: "peer-direct", Type: string(types.ResourceTypePeer)}, Destinations: []string{"g-dst"}}
|
||||
|
||||
t.Run("source group member and direct source peer both get the checks", func(t *testing.T) {
|
||||
nmData := postureSelectionData(gatedPolicy("p1", groupRule, "pc1"), gatedPolicy("p2", directRule, "pc1"))
|
||||
|
||||
assert.Equal(t, []string{"pc1"}, checkIDs(peerPostureChecksFromData(nmData, "peer-group")))
|
||||
assert.Equal(t, []string{"pc1"}, checkIDs(peerPostureChecksFromData(nmData, "peer-direct")))
|
||||
assert.Empty(t, peerPostureChecksFromData(nmData, "peer-elsewhere"))
|
||||
})
|
||||
|
||||
t.Run("source resource of a non-peer type never matches a peer", func(t *testing.T) {
|
||||
hostRule := &nmdata.PolicyRule{ID: "r-host", Enabled: true, SourceResource: nmdata.Resource{ID: "peer-direct", Type: string(types.ResourceTypeHost)}, Destinations: []string{"g-dst"}}
|
||||
nmData := postureSelectionData(gatedPolicy("p1", hostRule, "pc1"))
|
||||
|
||||
assert.Empty(t, peerPostureChecksFromData(nmData, "peer-direct"))
|
||||
})
|
||||
|
||||
t.Run("same check through two policies is returned once", func(t *testing.T) {
|
||||
nmData := postureSelectionData(gatedPolicy("p1", groupRule, "pc1"), gatedPolicy("p2", groupRule, "pc1"))
|
||||
|
||||
assert.Equal(t, []string{"pc1"}, checkIDs(peerPostureChecksFromData(nmData, "peer-group")))
|
||||
})
|
||||
|
||||
t.Run("disabled policy, disabled rule and dangling check are ignored", func(t *testing.T) {
|
||||
disabledPolicy := gatedPolicy("p-off", groupRule, "pc1")
|
||||
disabledPolicy.Enabled = false
|
||||
disabledRule := &nmdata.PolicyRule{ID: "r-off", Enabled: false, Sources: []string{"g-src"}}
|
||||
nmData := postureSelectionData(disabledPolicy, gatedPolicy("p-rule-off", disabledRule, "pc1"), gatedPolicy("p-dangling", groupRule, "pc-missing"))
|
||||
|
||||
assert.Empty(t, peerPostureChecksFromData(nmData, "peer-group"))
|
||||
})
|
||||
}
|
||||
@@ -3,12 +3,16 @@ package controller
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
||||
"github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/store"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
)
|
||||
|
||||
//go:generate go tool mockgen -source=./repository.go -package=controller -destination=repository_mock.go
|
||||
|
||||
type Repository interface {
|
||||
GetAccountNetwork(ctx context.Context, accountID string) (*types.Network, error)
|
||||
GetAccountPeers(ctx context.Context, accountID string) ([]*peer.Peer, error)
|
||||
@@ -16,6 +20,11 @@ type Repository interface {
|
||||
GetPeersByIDs(ctx context.Context, accountID string, peerIDs []string) (map[string]*peer.Peer, error)
|
||||
GetPeerByID(ctx context.Context, accountID string, peerID string) (*peer.Peer, error)
|
||||
GetAccountZones(ctx context.Context, accountID string) ([]*zones.Zone, error)
|
||||
// SynthesizeAgentNetworkServices returns the in-memory reverse-proxy
|
||||
// services synthesised from the account's agent-network provider/policy
|
||||
// state. Empty for accounts without agent-network providers.
|
||||
SynthesizeAgentNetworkServices(ctx context.Context, accountID string) ([]*service.Service, error)
|
||||
GetAccountServices(ctx context.Context, accountID string) ([]*service.Service, error)
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
@@ -50,6 +59,14 @@ func (r *repository) GetPeerByID(ctx context.Context, accountID string, peerID s
|
||||
return r.store.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID)
|
||||
}
|
||||
|
||||
func (r *repository) SynthesizeAgentNetworkServices(ctx context.Context, accountID string) ([]*service.Service, error) {
|
||||
return agentnetwork.SynthesizeServices(ctx, r.store, accountID)
|
||||
}
|
||||
|
||||
func (r *repository) GetAccountServices(ctx context.Context, accountID string) ([]*service.Service, error) {
|
||||
return r.store.GetAccountServices(ctx, store.LockingStrengthNone, accountID)
|
||||
}
|
||||
|
||||
func (r *repository) GetAccountZones(ctx context.Context, accountID string) ([]*zones.Zone, error) {
|
||||
return r.store.GetAccountZones(ctx, store.LockingStrengthNone, accountID)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./repository.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -source=./repository.go -package=controller -destination=repository_mock.go
|
||||
//
|
||||
|
||||
// Package controller is a generated GoMock package.
|
||||
package controller
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
service "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
zones "github.com/netbirdio/netbird/management/internals/modules/zones"
|
||||
peer "github.com/netbirdio/netbird/management/server/peer"
|
||||
types "github.com/netbirdio/netbird/management/server/types"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockRepository is a mock of Repository interface.
|
||||
type MockRepository struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockRepositoryMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockRepositoryMockRecorder is the mock recorder for MockRepository.
|
||||
type MockRepositoryMockRecorder struct {
|
||||
mock *MockRepository
|
||||
}
|
||||
|
||||
// NewMockRepository creates a new mock instance.
|
||||
func NewMockRepository(ctrl *gomock.Controller) *MockRepository {
|
||||
mock := &MockRepository{ctrl: ctrl}
|
||||
mock.recorder = &MockRepositoryMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetAccountByPeerID mocks base method.
|
||||
func (m *MockRepository) GetAccountByPeerID(ctx context.Context, peerID string) (*types.Account, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAccountByPeerID", ctx, peerID)
|
||||
ret0, _ := ret[0].(*types.Account)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAccountByPeerID indicates an expected call of GetAccountByPeerID.
|
||||
func (mr *MockRepositoryMockRecorder) GetAccountByPeerID(ctx, peerID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountByPeerID", reflect.TypeOf((*MockRepository)(nil).GetAccountByPeerID), ctx, peerID)
|
||||
}
|
||||
|
||||
// GetAccountNetwork mocks base method.
|
||||
func (m *MockRepository) GetAccountNetwork(ctx context.Context, accountID string) (*types.Network, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAccountNetwork", ctx, accountID)
|
||||
ret0, _ := ret[0].(*types.Network)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAccountNetwork indicates an expected call of GetAccountNetwork.
|
||||
func (mr *MockRepositoryMockRecorder) GetAccountNetwork(ctx, accountID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountNetwork", reflect.TypeOf((*MockRepository)(nil).GetAccountNetwork), ctx, accountID)
|
||||
}
|
||||
|
||||
// GetAccountPeers mocks base method.
|
||||
func (m *MockRepository) GetAccountPeers(ctx context.Context, accountID string) ([]*peer.Peer, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAccountPeers", ctx, accountID)
|
||||
ret0, _ := ret[0].([]*peer.Peer)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAccountPeers indicates an expected call of GetAccountPeers.
|
||||
func (mr *MockRepositoryMockRecorder) GetAccountPeers(ctx, accountID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountPeers", reflect.TypeOf((*MockRepository)(nil).GetAccountPeers), ctx, accountID)
|
||||
}
|
||||
|
||||
// GetAccountZones mocks base method.
|
||||
func (m *MockRepository) GetAccountZones(ctx context.Context, accountID string) ([]*zones.Zone, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAccountZones", ctx, accountID)
|
||||
ret0, _ := ret[0].([]*zones.Zone)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAccountZones indicates an expected call of GetAccountZones.
|
||||
func (mr *MockRepositoryMockRecorder) GetAccountZones(ctx, accountID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountZones", reflect.TypeOf((*MockRepository)(nil).GetAccountZones), ctx, accountID)
|
||||
}
|
||||
|
||||
// GetPeerByID mocks base method.
|
||||
func (m *MockRepository) GetPeerByID(ctx context.Context, accountID, peerID string) (*peer.Peer, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPeerByID", ctx, accountID, peerID)
|
||||
ret0, _ := ret[0].(*peer.Peer)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetPeerByID indicates an expected call of GetPeerByID.
|
||||
func (mr *MockRepositoryMockRecorder) GetPeerByID(ctx, accountID, peerID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeerByID", reflect.TypeOf((*MockRepository)(nil).GetPeerByID), ctx, accountID, peerID)
|
||||
}
|
||||
|
||||
// GetPeersByIDs mocks base method.
|
||||
func (m *MockRepository) GetPeersByIDs(ctx context.Context, accountID string, peerIDs []string) (map[string]*peer.Peer, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPeersByIDs", ctx, accountID, peerIDs)
|
||||
ret0, _ := ret[0].(map[string]*peer.Peer)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetPeersByIDs indicates an expected call of GetPeersByIDs.
|
||||
func (mr *MockRepositoryMockRecorder) GetPeersByIDs(ctx, accountID, peerIDs any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPeersByIDs", reflect.TypeOf((*MockRepository)(nil).GetPeersByIDs), ctx, accountID, peerIDs)
|
||||
}
|
||||
|
||||
// SynthesizeAgentNetworkServices mocks base method.
|
||||
func (m *MockRepository) SynthesizeAgentNetworkServices(ctx context.Context, accountID string) ([]*service.Service, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SynthesizeAgentNetworkServices", ctx, accountID)
|
||||
ret0, _ := ret[0].([]*service.Service)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// SynthesizeAgentNetworkServices indicates an expected call of SynthesizeAgentNetworkServices.
|
||||
func (mr *MockRepositoryMockRecorder) SynthesizeAgentNetworkServices(ctx, accountID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SynthesizeAgentNetworkServices", reflect.TypeOf((*MockRepository)(nil).SynthesizeAgentNetworkServices), ctx, accountID)
|
||||
}
|
||||
@@ -1,38 +1,38 @@
|
||||
package network_map
|
||||
|
||||
//go:generate go run go.uber.org/mock/mockgen -package network_map -destination=interface_mock.go -source=./interface.go -build_flags=-mod=mod
|
||||
//go:generate go tool mockgen -package network_map -destination=interface_mock.go -source=./interface.go -build_flags=-mod=mod
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/posture"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
const (
|
||||
EnvNewNetworkMapBuilder = "NB_EXPERIMENT_NETWORK_MAP"
|
||||
EnvNewNetworkMapAccounts = "NB_EXPERIMENT_NETWORK_MAP_ACCOUNTS"
|
||||
|
||||
DnsForwarderPort = nbdns.ForwarderServerPort
|
||||
OldForwarderPort = nbdns.ForwarderClientPort
|
||||
DnsForwarderPortMinVersion = "v0.59.0"
|
||||
)
|
||||
|
||||
type Controller interface {
|
||||
UpdateAccountPeers(ctx context.Context, accountID string) error
|
||||
UpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error
|
||||
UpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string) error
|
||||
BufferUpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string, reason types.UpdateReason) error
|
||||
UpdateAccountPeer(ctx context.Context, accountId string, peerId string) error
|
||||
BufferUpdateAccountPeers(ctx context.Context, accountID string) error
|
||||
GetValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID string, p *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, int64, error)
|
||||
BufferUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error
|
||||
GetValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID string, peerID string) (*types.NetworkMap, []*nmdata.PostureChecks, int64, error)
|
||||
GetValidatedPeerWithComponents(ctx context.Context, isRequiresApproval bool, accountID string, p *nbpeer.Peer) (*nbpeer.Peer, *types.NetworkMapComponents, *types.NetworkMap, []*nmdata.PostureChecks, int64, error)
|
||||
GetDNSDomain(settings *types.Settings) string
|
||||
StartWarmup(context.Context)
|
||||
GetNetworkMap(ctx context.Context, peerID string) (*types.NetworkMap, error)
|
||||
CountStreams() int
|
||||
|
||||
OnPeersUpdated(ctx context.Context, accountId string, peerIDs []string) error
|
||||
OnPeersAdded(ctx context.Context, accountID string, peerIDs []string) error
|
||||
OnPeersDeleted(ctx context.Context, accountID string, peerIDs []string) error
|
||||
OnPeersUpdated(ctx context.Context, accountId string, peerIDs []string, affectedPeerIDs []string) error
|
||||
OnPeersAdded(ctx context.Context, accountID string, peerIDs []string, affectedPeerIDs []string) error
|
||||
OnPeersDeleted(ctx context.Context, accountID string, peerIDs []string, affectedPeerIDs []string) error
|
||||
DisconnectPeers(ctx context.Context, accountId string, peerIDs []string)
|
||||
OnPeerConnected(ctx context.Context, accountID string, peerID string) (chan *UpdateMessage, error)
|
||||
OnPeerDisconnected(ctx context.Context, accountID string, peerID string)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: management/internals/controllers/network_map/interface.go
|
||||
// Source: ./interface.go
|
||||
//
|
||||
// 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.
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
reflect "reflect"
|
||||
|
||||
peer "github.com/netbirdio/netbird/management/server/peer"
|
||||
posture "github.com/netbirdio/netbird/management/server/posture"
|
||||
types "github.com/netbirdio/netbird/management/server/types"
|
||||
nmdata "github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
@@ -44,17 +44,31 @@ func (m *MockController) EXPECT() *MockControllerMockRecorder {
|
||||
}
|
||||
|
||||
// BufferUpdateAccountPeers mocks base method.
|
||||
func (m *MockController) BufferUpdateAccountPeers(ctx context.Context, accountID string) error {
|
||||
func (m *MockController) BufferUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "BufferUpdateAccountPeers", ctx, accountID)
|
||||
ret := m.ctrl.Call(m, "BufferUpdateAccountPeers", ctx, accountID, reason)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// BufferUpdateAccountPeers indicates an expected call of BufferUpdateAccountPeers.
|
||||
func (mr *MockControllerMockRecorder) BufferUpdateAccountPeers(ctx, accountID any) *gomock.Call {
|
||||
func (mr *MockControllerMockRecorder) BufferUpdateAccountPeers(ctx, accountID, reason any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BufferUpdateAccountPeers", reflect.TypeOf((*MockController)(nil).BufferUpdateAccountPeers), ctx, accountID)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BufferUpdateAccountPeers", reflect.TypeOf((*MockController)(nil).BufferUpdateAccountPeers), ctx, accountID, reason)
|
||||
}
|
||||
|
||||
// BufferUpdateAffectedPeers mocks base method.
|
||||
func (m *MockController) BufferUpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string, reason types.UpdateReason) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "BufferUpdateAffectedPeers", ctx, accountID, peerIDs, reason)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// BufferUpdateAffectedPeers indicates an expected call of BufferUpdateAffectedPeers.
|
||||
func (mr *MockControllerMockRecorder) BufferUpdateAffectedPeers(ctx, accountID, peerIDs, reason any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BufferUpdateAffectedPeers", reflect.TypeOf((*MockController)(nil).BufferUpdateAffectedPeers), ctx, accountID, peerIDs, reason)
|
||||
}
|
||||
|
||||
// CountStreams mocks base method.
|
||||
@@ -112,22 +126,40 @@ func (mr *MockControllerMockRecorder) GetNetworkMap(ctx, peerID any) *gomock.Cal
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkMap", reflect.TypeOf((*MockController)(nil).GetNetworkMap), ctx, peerID)
|
||||
}
|
||||
|
||||
// GetValidatedPeerWithMap mocks base method.
|
||||
func (m *MockController) GetValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID string, p *peer.Peer) (*peer.Peer, *types.NetworkMap, []*posture.Checks, int64, error) {
|
||||
// GetValidatedPeerWithComponents mocks base method.
|
||||
func (m *MockController) GetValidatedPeerWithComponents(ctx context.Context, isRequiresApproval bool, accountID string, p *peer.Peer) (*peer.Peer, *types.NetworkMapComponents, *types.NetworkMap, []*nmdata.PostureChecks, int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetValidatedPeerWithMap", ctx, isRequiresApproval, accountID, p)
|
||||
ret := m.ctrl.Call(m, "GetValidatedPeerWithComponents", ctx, isRequiresApproval, accountID, p)
|
||||
ret0, _ := ret[0].(*peer.Peer)
|
||||
ret1, _ := ret[1].(*types.NetworkMap)
|
||||
ret2, _ := ret[2].([]*posture.Checks)
|
||||
ret3, _ := ret[3].(int64)
|
||||
ret4, _ := ret[4].(error)
|
||||
return ret0, ret1, ret2, ret3, ret4
|
||||
ret1, _ := ret[1].(*types.NetworkMapComponents)
|
||||
ret2, _ := ret[2].(*types.NetworkMap)
|
||||
ret3, _ := ret[3].([]*nmdata.PostureChecks)
|
||||
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.
|
||||
func (m *MockController) GetValidatedPeerWithMap(ctx context.Context, isRequiresApproval bool, accountID, peerID string) (*types.NetworkMap, []*nmdata.PostureChecks, int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetValidatedPeerWithMap", ctx, isRequiresApproval, accountID, peerID)
|
||||
ret0, _ := ret[0].(*types.NetworkMap)
|
||||
ret1, _ := ret[1].([]*nmdata.PostureChecks)
|
||||
ret2, _ := ret[2].(int64)
|
||||
ret3, _ := ret[3].(error)
|
||||
return ret0, ret1, ret2, ret3
|
||||
}
|
||||
|
||||
// GetValidatedPeerWithMap indicates an expected call of GetValidatedPeerWithMap.
|
||||
func (mr *MockControllerMockRecorder) GetValidatedPeerWithMap(ctx, isRequiresApproval, accountID, p any) *gomock.Call {
|
||||
func (mr *MockControllerMockRecorder) GetValidatedPeerWithMap(ctx, isRequiresApproval, accountID, peerID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatedPeerWithMap", reflect.TypeOf((*MockController)(nil).GetValidatedPeerWithMap), ctx, isRequiresApproval, accountID, p)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatedPeerWithMap", reflect.TypeOf((*MockController)(nil).GetValidatedPeerWithMap), ctx, isRequiresApproval, accountID, peerID)
|
||||
}
|
||||
|
||||
// OnPeerConnected mocks base method.
|
||||
@@ -158,45 +190,45 @@ func (mr *MockControllerMockRecorder) OnPeerDisconnected(ctx, accountID, peerID
|
||||
}
|
||||
|
||||
// OnPeersAdded mocks base method.
|
||||
func (m *MockController) OnPeersAdded(ctx context.Context, accountID string, peerIDs []string) error {
|
||||
func (m *MockController) OnPeersAdded(ctx context.Context, accountID string, peerIDs, affectedPeerIDs []string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "OnPeersAdded", ctx, accountID, peerIDs)
|
||||
ret := m.ctrl.Call(m, "OnPeersAdded", ctx, accountID, peerIDs, affectedPeerIDs)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// OnPeersAdded indicates an expected call of OnPeersAdded.
|
||||
func (mr *MockControllerMockRecorder) OnPeersAdded(ctx, accountID, peerIDs any) *gomock.Call {
|
||||
func (mr *MockControllerMockRecorder) OnPeersAdded(ctx, accountID, peerIDs, affectedPeerIDs any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnPeersAdded", reflect.TypeOf((*MockController)(nil).OnPeersAdded), ctx, accountID, peerIDs)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnPeersAdded", reflect.TypeOf((*MockController)(nil).OnPeersAdded), ctx, accountID, peerIDs, affectedPeerIDs)
|
||||
}
|
||||
|
||||
// OnPeersDeleted mocks base method.
|
||||
func (m *MockController) OnPeersDeleted(ctx context.Context, accountID string, peerIDs []string) error {
|
||||
func (m *MockController) OnPeersDeleted(ctx context.Context, accountID string, peerIDs, affectedPeerIDs []string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "OnPeersDeleted", ctx, accountID, peerIDs)
|
||||
ret := m.ctrl.Call(m, "OnPeersDeleted", ctx, accountID, peerIDs, affectedPeerIDs)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// OnPeersDeleted indicates an expected call of OnPeersDeleted.
|
||||
func (mr *MockControllerMockRecorder) OnPeersDeleted(ctx, accountID, peerIDs any) *gomock.Call {
|
||||
func (mr *MockControllerMockRecorder) OnPeersDeleted(ctx, accountID, peerIDs, affectedPeerIDs any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnPeersDeleted", reflect.TypeOf((*MockController)(nil).OnPeersDeleted), ctx, accountID, peerIDs)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnPeersDeleted", reflect.TypeOf((*MockController)(nil).OnPeersDeleted), ctx, accountID, peerIDs, affectedPeerIDs)
|
||||
}
|
||||
|
||||
// OnPeersUpdated mocks base method.
|
||||
func (m *MockController) OnPeersUpdated(ctx context.Context, accountId string, peerIDs []string) error {
|
||||
func (m *MockController) OnPeersUpdated(ctx context.Context, accountId string, peerIDs, affectedPeerIDs []string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "OnPeersUpdated", ctx, accountId, peerIDs)
|
||||
ret := m.ctrl.Call(m, "OnPeersUpdated", ctx, accountId, peerIDs, affectedPeerIDs)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// OnPeersUpdated indicates an expected call of OnPeersUpdated.
|
||||
func (mr *MockControllerMockRecorder) OnPeersUpdated(ctx, accountId, peerIDs any) *gomock.Call {
|
||||
func (mr *MockControllerMockRecorder) OnPeersUpdated(ctx, accountId, peerIDs, affectedPeerIDs any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnPeersUpdated", reflect.TypeOf((*MockController)(nil).OnPeersUpdated), ctx, accountId, peerIDs)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnPeersUpdated", reflect.TypeOf((*MockController)(nil).OnPeersUpdated), ctx, accountId, peerIDs, affectedPeerIDs)
|
||||
}
|
||||
|
||||
// StartWarmup mocks base method.
|
||||
@@ -238,15 +270,29 @@ func (mr *MockControllerMockRecorder) UpdateAccountPeer(ctx, accountId, peerId a
|
||||
}
|
||||
|
||||
// UpdateAccountPeers mocks base method.
|
||||
func (m *MockController) UpdateAccountPeers(ctx context.Context, accountID string) error {
|
||||
func (m *MockController) UpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateAccountPeers", ctx, accountID)
|
||||
ret := m.ctrl.Call(m, "UpdateAccountPeers", ctx, accountID, reason)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateAccountPeers indicates an expected call of UpdateAccountPeers.
|
||||
func (mr *MockControllerMockRecorder) UpdateAccountPeers(ctx, accountID any) *gomock.Call {
|
||||
func (mr *MockControllerMockRecorder) UpdateAccountPeers(ctx, accountID, reason any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountPeers", reflect.TypeOf((*MockController)(nil).UpdateAccountPeers), ctx, accountID)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAccountPeers", reflect.TypeOf((*MockController)(nil).UpdateAccountPeers), ctx, accountID, reason)
|
||||
}
|
||||
|
||||
// UpdateAffectedPeers mocks base method.
|
||||
func (m *MockController) UpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateAffectedPeers", ctx, accountID, peerIDs)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateAffectedPeers indicates an expected call of UpdateAffectedPeers.
|
||||
func (mr *MockControllerMockRecorder) UpdateAffectedPeers(ctx, accountID, peerIDs any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAffectedPeers", reflect.TypeOf((*MockController)(nil).UpdateAffectedPeers), ctx, accountID, peerIDs)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
package nmaptest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
// normalizeIDSpace replaces policy and route identifiers with positional
|
||||
// placeholders so a comparison can reach everything else.
|
||||
//
|
||||
// This exists only because the envelope round-trip currently substitutes each
|
||||
// internal xid with the object's public id, which is a tracked defect and not a
|
||||
// licence to differ: those identifiers reach the server again inside flow
|
||||
// events, which resolve them by internal id, so the substitution silently
|
||||
// breaks flow attribution for component-format peers. TestIDSpaceMatches
|
||||
// asserts the equality that must eventually hold; this erasure keeps the other
|
||||
// 40-odd cases reporting on semantics meanwhile. When the id space is unified,
|
||||
// delete this and the calls to it — every case should still pass.
|
||||
//
|
||||
// Cardinality and cross-references survive the erasure: two rules under one
|
||||
// policy still share a token and a route firewall rule still points at its
|
||||
// route, so a path that drops a policy, merges two policies, or misattributes a
|
||||
// rule to the wrong route still fails.
|
||||
func normalizeIDSpace(nm *proto.NetworkMap) {
|
||||
if nm == nil {
|
||||
return
|
||||
}
|
||||
policies := newTokenizer("policy")
|
||||
routes := newTokenizer("route")
|
||||
|
||||
for _, i := range orderBy(nm.Routes, routeKeyWithoutID) {
|
||||
nm.Routes[i].ID = routes.get(nm.Routes[i].ID)
|
||||
}
|
||||
for _, i := range orderBy(nm.FirewallRules, firewallKeyWithoutPolicy) {
|
||||
r := nm.FirewallRules[i]
|
||||
if len(r.PolicyID) > 0 {
|
||||
r.PolicyID = []byte(policies.get(string(r.PolicyID)))
|
||||
}
|
||||
}
|
||||
for _, i := range orderBy(nm.RoutesFirewallRules, routeFirewallKeyWithoutIDs) {
|
||||
r := nm.RoutesFirewallRules[i]
|
||||
if len(r.PolicyID) > 0 {
|
||||
r.PolicyID = []byte(policies.get(string(r.PolicyID)))
|
||||
}
|
||||
r.RouteID = routes.get(r.RouteID)
|
||||
}
|
||||
}
|
||||
|
||||
// tokenizer maps identifiers to positional placeholders in order of first use.
|
||||
type tokenizer struct {
|
||||
prefix string
|
||||
seen map[string]string
|
||||
}
|
||||
|
||||
func newTokenizer(prefix string) *tokenizer {
|
||||
return &tokenizer{prefix: prefix, seen: make(map[string]string)}
|
||||
}
|
||||
|
||||
func (t *tokenizer) get(id string) string {
|
||||
if id == "" {
|
||||
return ""
|
||||
}
|
||||
if tok, ok := t.seen[id]; ok {
|
||||
return tok
|
||||
}
|
||||
tok := fmt.Sprintf("%s#%d", t.prefix, len(t.seen))
|
||||
t.seen[id] = tok
|
||||
return tok
|
||||
}
|
||||
|
||||
// orderBy returns indices sorted by key, so placeholder numbering does not
|
||||
// depend on the identifiers being erased.
|
||||
func orderBy[T any](items []T, key func(T) string) []int {
|
||||
idx := make([]int, len(items))
|
||||
for i := range idx {
|
||||
idx[i] = i
|
||||
}
|
||||
sort.SliceStable(idx, func(a, b int) bool { return key(items[idx[a]]) < key(items[idx[b]]) })
|
||||
return idx
|
||||
}
|
||||
|
||||
func routeKeyWithoutID(r *proto.Route) string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s|%s|%s|%d|%d|%t|%t|%v",
|
||||
r.Network, r.NetID, r.Peer, r.Metric, r.NetworkType, r.Masquerade, r.KeepRoute, r.Domains)
|
||||
}
|
||||
|
||||
func firewallKeyWithoutPolicy(r *proto.FirewallRule) string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s|%d|%d|%d|%s|%s|%v",
|
||||
r.PeerIP, r.Direction, r.Action, r.Protocol, r.Port, portInfoKey(r.PortInfo), r.SourcePrefixes) //nolint:staticcheck
|
||||
}
|
||||
|
||||
func routeFirewallKeyWithoutIDs(r *proto.RouteFirewallRule) string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s|%d|%d|%s|%v|%v|%t|%d",
|
||||
r.Destination, r.Protocol, r.Action, portInfoKey(r.PortInfo), r.Domains, r.SourceRanges, r.IsDynamic, r.CustomProtocol)
|
||||
}
|
||||
|
||||
// canonicalize sorts every repeated field of the NetworkMap by a stable key.
|
||||
// The producing paths iterate Go maps while building these slices, so order
|
||||
// can differ between runs even when the content is identical; comparing
|
||||
// without this reports noise.
|
||||
func canonicalize(nm *proto.NetworkMap) {
|
||||
if nm == nil {
|
||||
return
|
||||
}
|
||||
slices.SortFunc(nm.RemotePeers, cmpRemotePeer)
|
||||
slices.SortFunc(nm.OfflinePeers, cmpRemotePeer)
|
||||
slices.SortFunc(nm.Routes, cmpRoute)
|
||||
slices.SortFunc(nm.FirewallRules, cmpFirewallRule)
|
||||
slices.SortFunc(nm.RoutesFirewallRules, cmpRouteFirewallRule)
|
||||
slices.SortFunc(nm.ForwardingRules, cmpForwardingRule)
|
||||
|
||||
for _, r := range nm.FirewallRules {
|
||||
slices.SortFunc(r.SourcePrefixes, bytes.Compare)
|
||||
}
|
||||
for _, r := range nm.RoutesFirewallRules {
|
||||
slices.Sort(r.SourceRanges)
|
||||
}
|
||||
canonicalizeDNSConfig(nm.DNSConfig)
|
||||
canonicalizeSSHAuth(nm.SshAuth)
|
||||
}
|
||||
|
||||
func canonicalizeDNSConfig(d *proto.DNSConfig) {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
for _, g := range d.NameServerGroups {
|
||||
if g == nil {
|
||||
continue
|
||||
}
|
||||
slices.Sort(g.Domains)
|
||||
slices.SortFunc(g.NameServers, func(a, b *proto.NameServer) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
if c := cmp.Compare(a.IP, b.IP); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Port, b.Port); c != 0 {
|
||||
return c
|
||||
}
|
||||
return cmp.Compare(a.NSType, b.NSType)
|
||||
})
|
||||
}
|
||||
slices.SortFunc(d.NameServerGroups, func(a, b *proto.NameServerGroup) int {
|
||||
return cmp.Compare(nsgKey(a), nsgKey(b))
|
||||
})
|
||||
for _, z := range d.CustomZones {
|
||||
if z == nil {
|
||||
continue
|
||||
}
|
||||
slices.SortFunc(z.Records, cmpSimpleRecord)
|
||||
}
|
||||
slices.SortFunc(d.CustomZones, func(a, b *proto.CustomZone) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
return cmp.Compare(a.Domain, b.Domain)
|
||||
})
|
||||
}
|
||||
|
||||
// canonicalizeSSHAuth sorts AuthorizedUsers and re-keys MachineUsers.Indexes
|
||||
// against the new ordering, preserving which machine user maps to which hashes.
|
||||
func canonicalizeSSHAuth(s *proto.SSHAuth) {
|
||||
if s == nil || len(s.AuthorizedUsers) == 0 {
|
||||
return
|
||||
}
|
||||
type hashed struct {
|
||||
bytes []byte
|
||||
old uint32
|
||||
}
|
||||
entries := make([]hashed, len(s.AuthorizedUsers))
|
||||
for i, b := range s.AuthorizedUsers {
|
||||
entries[i] = hashed{bytes: b, old: uint32(i)}
|
||||
}
|
||||
slices.SortFunc(entries, func(a, b hashed) int { return bytes.Compare(a.bytes, b.bytes) })
|
||||
|
||||
remap := make(map[uint32]uint32, len(entries))
|
||||
sorted := make([][]byte, len(entries))
|
||||
for newIdx, e := range entries {
|
||||
remap[e.old] = uint32(newIdx)
|
||||
sorted[newIdx] = e.bytes
|
||||
}
|
||||
s.AuthorizedUsers = sorted
|
||||
|
||||
for _, mu := range s.MachineUsers {
|
||||
if mu == nil {
|
||||
continue
|
||||
}
|
||||
for i, oldIdx := range mu.Indexes {
|
||||
if newIdx, ok := remap[oldIdx]; ok {
|
||||
mu.Indexes[i] = newIdx
|
||||
}
|
||||
}
|
||||
slices.Sort(mu.Indexes)
|
||||
}
|
||||
}
|
||||
|
||||
func boolCmp(a, b bool) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
if a {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func nsgKey(g *proto.NameServerGroup) string {
|
||||
if g == nil {
|
||||
return ""
|
||||
}
|
||||
var parts []string
|
||||
for _, ns := range g.NameServers {
|
||||
if ns == nil {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, ns.IP+":"+strconv.FormatInt(ns.Port, 10)+":"+strconv.FormatInt(ns.NSType, 10))
|
||||
}
|
||||
slices.Sort(parts)
|
||||
key := strings.Join(parts, ",")
|
||||
domains := append([]string(nil), g.Domains...)
|
||||
slices.Sort(domains)
|
||||
key += "|" + strings.Join(domains, "|")
|
||||
if g.Primary {
|
||||
key += "|P"
|
||||
}
|
||||
if g.SearchDomainsEnabled {
|
||||
key += "|S"
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func cmpSimpleRecord(a, b *proto.SimpleRecord) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
if c := cmp.Compare(a.Name, b.Name); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Type, b.Type); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Class, b.Class); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.RData, b.RData); c != 0 {
|
||||
return c
|
||||
}
|
||||
return cmp.Compare(a.TTL, b.TTL)
|
||||
}
|
||||
|
||||
func cmpRemotePeer(a, b *proto.RemotePeerConfig) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
return cmp.Compare(a.WgPubKey, b.WgPubKey)
|
||||
}
|
||||
|
||||
func cmpRoute(a, b *proto.Route) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
if c := cmp.Compare(a.ID, b.ID); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.NetID, b.NetID); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Network, b.Network); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Peer, b.Peer); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Metric, b.Metric); c != 0 {
|
||||
return c
|
||||
}
|
||||
return slices.Compare(a.Domains, b.Domains)
|
||||
}
|
||||
|
||||
func cmpFirewallRule(a, b *proto.FirewallRule) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
if c := bytes.Compare(a.PolicyID, b.PolicyID); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.PeerIP, b.PeerIP); c != 0 { //nolint:staticcheck
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(int32(a.Direction), int32(b.Direction)); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(int32(a.Action), int32(b.Action)); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(int32(a.Protocol), int32(b.Protocol)); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Port, b.Port); c != 0 {
|
||||
return c
|
||||
}
|
||||
return cmp.Compare(portInfoKey(a.PortInfo), portInfoKey(b.PortInfo))
|
||||
}
|
||||
|
||||
func cmpRouteFirewallRule(a, b *proto.RouteFirewallRule) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
if c := bytes.Compare(a.PolicyID, b.PolicyID); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.RouteID, b.RouteID); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.Destination, b.Destination); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(int32(a.Protocol), int32(b.Protocol)); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(portInfoKey(a.PortInfo), portInfoKey(b.PortInfo)); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(int32(a.Action), int32(b.Action)); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := slices.Compare(a.Domains, b.Domains); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := slices.Compare(a.SourceRanges, b.SourceRanges); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(a.CustomProtocol, b.CustomProtocol); c != 0 {
|
||||
return c
|
||||
}
|
||||
return boolCmp(a.IsDynamic, b.IsDynamic)
|
||||
}
|
||||
|
||||
func cmpForwardingRule(a, b *proto.ForwardingRule) int {
|
||||
if a == nil || b == nil {
|
||||
return boolCmp(a == nil, b == nil)
|
||||
}
|
||||
if c := cmp.Compare(int32(a.Protocol), int32(b.Protocol)); c != 0 {
|
||||
return c
|
||||
}
|
||||
return bytes.Compare(a.TranslatedAddress, b.TranslatedAddress)
|
||||
}
|
||||
|
||||
func portInfoKey(pi *proto.PortInfo) string {
|
||||
if pi == nil {
|
||||
return ""
|
||||
}
|
||||
switch sel := pi.PortSelection.(type) {
|
||||
case *proto.PortInfo_Port:
|
||||
return "P" + strconv.FormatUint(uint64(sel.Port), 10)
|
||||
case *proto.PortInfo_Range_:
|
||||
if sel.Range == nil {
|
||||
return "R"
|
||||
}
|
||||
return "R" + strconv.FormatUint(uint64(sel.Range.Start), 10) + "-" + strconv.FormatUint(uint64(sel.Range.End), 10)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package nmaptest
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
)
|
||||
|
||||
// LoadNetworkMapData reads a fixture holding the NetworkMapData the store
|
||||
// would return for one account. Unknown fields are rejected so fixture typos
|
||||
// fail loudly instead of silently testing a default.
|
||||
func LoadNetworkMapData(path string) (*networkmap.NetworkMapData, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open fixture: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
dec := json.NewDecoder(f)
|
||||
dec.DisallowUnknownFields()
|
||||
var nmData networkmap.NetworkMapData
|
||||
if err := dec.Decode(&nmData); err != nil {
|
||||
return nil, fmt.Errorf("decode fixture %s: %w", path, err)
|
||||
}
|
||||
return &nmData, nil
|
||||
}
|
||||
|
||||
var defaultNetworkNet = func() net.IPNet {
|
||||
_, ipnet, err := net.ParseCIDR("100.64.0.0/10")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return *ipnet
|
||||
}()
|
||||
|
||||
// applyFixtureDefaults fills the boilerplate a fixture may omit. Map-keyed
|
||||
// objects inherit their key as ID, peers get a deterministic WG-shaped key
|
||||
// and their ID as DNS label, PublicIDs default to the internal ID (the
|
||||
// envelope encoder puts public IDs on the wire and silently degrades on
|
||||
// empty ones), and a nil ValidatedPeers validates every peer — production
|
||||
// fills it through the integrated validator, not the store.
|
||||
func applyFixtureDefaults(nmData *networkmap.NetworkMapData) {
|
||||
if nmData.Network == nil {
|
||||
nmData.Network = &nmdata.Network{}
|
||||
}
|
||||
if nmData.Network.Identifier == "" {
|
||||
nmData.Network.Identifier = "network"
|
||||
}
|
||||
if nmData.Network.Net.IP == nil {
|
||||
nmData.Network.Net = defaultNetworkNet
|
||||
}
|
||||
if nmData.AccountSettings == nil {
|
||||
nmData.AccountSettings = &nmdata.AccountSettingsInfo{}
|
||||
}
|
||||
if nmData.DNSSettings == nil {
|
||||
nmData.DNSSettings = &nmdata.DNSSettings{}
|
||||
}
|
||||
|
||||
for id, p := range nmData.Peers {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
if p.ID == "" {
|
||||
p.ID = id
|
||||
}
|
||||
if p.Key == "" {
|
||||
p.Key = derivedWgKey(p.ID)
|
||||
}
|
||||
if p.DNSLabel == "" {
|
||||
p.DNSLabel = p.ID
|
||||
}
|
||||
}
|
||||
|
||||
for id, g := range nmData.Groups {
|
||||
if g == nil {
|
||||
continue
|
||||
}
|
||||
if g.ID == "" {
|
||||
g.ID = id
|
||||
}
|
||||
if g.Name == "" {
|
||||
g.Name = g.ID
|
||||
}
|
||||
if g.PublicID == "" {
|
||||
g.PublicID = g.ID
|
||||
}
|
||||
}
|
||||
|
||||
for _, policy := range nmData.Policies {
|
||||
defaultPolicyIDs(policy)
|
||||
}
|
||||
resolveResourcePolicyRefs(nmData)
|
||||
|
||||
for _, r := range nmData.Routes {
|
||||
if r != nil && r.PublicID == "" {
|
||||
r.PublicID = r.ID
|
||||
}
|
||||
}
|
||||
for _, nsg := range nmData.NameServerGroups {
|
||||
if nsg != nil && nsg.PublicID == "" {
|
||||
nsg.PublicID = nsg.ID
|
||||
}
|
||||
}
|
||||
for _, res := range nmData.NetworkResources {
|
||||
if res == nil {
|
||||
continue
|
||||
}
|
||||
if res.PublicID == "" {
|
||||
res.PublicID = res.ID
|
||||
}
|
||||
defaultXIDMapping(&nmData.NetworkXIDToPublicID, res.NetworkID)
|
||||
}
|
||||
for networkID, routers := range nmData.Routers {
|
||||
defaultXIDMapping(&nmData.NetworkXIDToPublicID, networkID)
|
||||
for _, router := range routers {
|
||||
if router != nil && router.PublicID == "" {
|
||||
router.PublicID = networkID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id, pc := range nmData.PostureChecks {
|
||||
if pc == nil {
|
||||
continue
|
||||
}
|
||||
if pc.ID == "" {
|
||||
pc.ID = id
|
||||
}
|
||||
defaultXIDMapping(&nmData.PostureCheckXIDToPublicID, pc.ID)
|
||||
}
|
||||
|
||||
if nmData.ValidatedPeers == nil {
|
||||
nmData.ValidatedPeers = make(map[string]struct{}, len(nmData.Peers))
|
||||
for id := range nmData.Peers {
|
||||
nmData.ValidatedPeers[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resolveResourcePolicyRefs lets a fixture name an account policy by ID in
|
||||
// ResourcePolicies — {"ID": "pol-x"} with no rules — instead of repeating it.
|
||||
// The real store puts the same policy pointer in both places, which is what
|
||||
// resolving the reference reproduces.
|
||||
func resolveResourcePolicyRefs(nmData *networkmap.NetworkMapData) {
|
||||
byID := make(map[string]*nmdata.Policy, len(nmData.Policies))
|
||||
for _, policy := range nmData.Policies {
|
||||
if policy != nil && policy.ID != "" {
|
||||
byID[policy.ID] = policy
|
||||
}
|
||||
}
|
||||
|
||||
for _, policies := range nmData.ResourcePolicies {
|
||||
for i, policy := range policies {
|
||||
if policy == nil {
|
||||
continue
|
||||
}
|
||||
if len(policy.Rules) == 0 {
|
||||
if full, ok := byID[policy.ID]; ok {
|
||||
policies[i] = full
|
||||
continue
|
||||
}
|
||||
}
|
||||
defaultPolicyIDs(policy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func defaultPolicyIDs(policy *nmdata.Policy) {
|
||||
if policy == nil {
|
||||
return
|
||||
}
|
||||
if policy.PublicID == "" {
|
||||
policy.PublicID = policy.ID
|
||||
}
|
||||
for i, rule := range policy.Rules {
|
||||
if rule == nil {
|
||||
continue
|
||||
}
|
||||
if rule.PolicyID == "" {
|
||||
rule.PolicyID = policy.ID
|
||||
}
|
||||
if rule.ID == "" {
|
||||
// Production gives a rule its policy's id (management/server/policy.go:205,
|
||||
// "when policy can contain multiple rules, need refactor"), so a
|
||||
// single-rule policy — the only shape the product can create today —
|
||||
// must be modelled that way or the wire ids come out unrealistic.
|
||||
rule.ID = policy.ID
|
||||
if len(policy.Rules) > 1 {
|
||||
rule.ID = fmt.Sprintf("%s-rule-%d", policy.ID, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func defaultXIDMapping(m *map[string]string, id string) {
|
||||
if id == "" {
|
||||
return
|
||||
}
|
||||
if *m == nil {
|
||||
*m = make(map[string]string)
|
||||
}
|
||||
if _, ok := (*m)[id]; !ok {
|
||||
(*m)[id] = id
|
||||
}
|
||||
}
|
||||
|
||||
// derivedWgKey returns a deterministic base64 key of 32 bytes, valid for the
|
||||
// envelope decoder's WG-key identity.
|
||||
func derivedWgKey(peerID string) string {
|
||||
sum := sha256.Sum256([]byte(peerID))
|
||||
return base64.StdEncoding.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package nmaptest_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/nmaptest"
|
||||
)
|
||||
|
||||
func TestNetworkMapGolden(t *testing.T) {
|
||||
nmaptest.RunGoldenDir(t, filepath.Join("testdata", "cases"))
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
package nmaptest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones/records"
|
||||
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
|
||||
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
|
||||
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
|
||||
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
||||
"github.com/netbirdio/netbird/management/server/posture"
|
||||
"github.com/netbirdio/netbird/management/server/types"
|
||||
"github.com/netbirdio/netbird/management/server/types/legacynmap"
|
||||
nbroute "github.com/netbirdio/netbird/route"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
sharedtypes "github.com/netbirdio/netbird/shared/management/types"
|
||||
)
|
||||
|
||||
// legacyInput is the account and the four derived arguments main's computation
|
||||
// took alongside it. The controller resolved them from the account before
|
||||
// calling; the twin carries them as fields, so the fixture is the source for
|
||||
// both halves.
|
||||
type legacyInput struct {
|
||||
account *types.Account
|
||||
accountZones []*zones.Zone
|
||||
validatedPeers map[string]struct{}
|
||||
resourcePolicies map[string][]*types.Policy
|
||||
routers map[string]map[string]*routerTypes.NetworkRouter
|
||||
groupIDToUserIDs map[string][]string
|
||||
}
|
||||
|
||||
// legacyInputFromData rebuilds the Account the fixture stands for. A fixture is
|
||||
// the value the store returns, and the store's twins carry exactly the state
|
||||
// the computation reads, so inverting them reproduces the account main would
|
||||
// have loaded — which is what lets one expectation measure all three paths.
|
||||
//
|
||||
// The inverse is only defined for what a twin carries: fields the builders drop
|
||||
// (peer names, policy descriptions, user records behind AllowedUserIDs) come
|
||||
// back as the zero value or a minimal stand-in, because no path reads them.
|
||||
func legacyInputFromData(accountID string, nmData *networkmap.NetworkMapData) legacyInput {
|
||||
account := &types.Account{
|
||||
Id: accountID,
|
||||
Network: accountNetwork(nmData.Network),
|
||||
Settings: accountSettings(nmData.AccountSettings),
|
||||
DNSSettings: types.DNSSettings{DisabledManagementGroups: nmData.DNSSettings.DisabledManagementGroups},
|
||||
Peers: make(map[string]*nbpeer.Peer, len(nmData.Peers)),
|
||||
Groups: make(map[string]*types.Group, len(nmData.Groups)),
|
||||
Policies: make([]*types.Policy, 0, len(nmData.Policies)),
|
||||
Routes: make(map[nbroute.ID]*nbroute.Route, len(nmData.Routes)),
|
||||
NameServerGroups: make(map[string]*nbdns.NameServerGroup, len(nmData.NameServerGroups)),
|
||||
NetworkResources: make([]*resourceTypes.NetworkResource, 0, len(nmData.NetworkResources)),
|
||||
PostureChecks: make([]*posture.Checks, 0, len(nmData.PostureChecks)),
|
||||
Users: make(map[string]*types.User, len(nmData.AllowedUserIDs)),
|
||||
Services: accountServices(nmData.Services),
|
||||
}
|
||||
|
||||
for id, p := range nmData.Peers {
|
||||
account.Peers[id] = accountPeer(id, p)
|
||||
}
|
||||
for id, g := range nmData.Groups {
|
||||
account.Groups[id] = accountGroup(id, g)
|
||||
}
|
||||
|
||||
policiesByID := make(map[string]*types.Policy, len(nmData.Policies))
|
||||
for _, p := range nmData.Policies {
|
||||
policy := accountPolicy(p)
|
||||
if policy == nil {
|
||||
continue
|
||||
}
|
||||
account.Policies = append(account.Policies, policy)
|
||||
policiesByID[policy.ID] = policy
|
||||
}
|
||||
|
||||
for _, r := range nmData.Routes {
|
||||
route := accountRoute(r)
|
||||
if route != nil {
|
||||
account.Routes[route.ID] = route
|
||||
}
|
||||
}
|
||||
for _, nsg := range nmData.NameServerGroups {
|
||||
group := accountNSG(nsg)
|
||||
if group != nil {
|
||||
account.NameServerGroups[group.ID] = group
|
||||
}
|
||||
}
|
||||
for _, res := range nmData.NetworkResources {
|
||||
if resource := accountNetworkResource(res); resource != nil {
|
||||
account.NetworkResources = append(account.NetworkResources, resource)
|
||||
}
|
||||
}
|
||||
for id, pc := range nmData.PostureChecks {
|
||||
if check := accountPostureChecks(id, pc, nmData.PostureCheckXIDToPublicID[id]); check != nil {
|
||||
account.PostureChecks = append(account.PostureChecks, check)
|
||||
}
|
||||
}
|
||||
for xid, publicID := range nmData.NetworkXIDToPublicID {
|
||||
account.Networks = append(account.Networks, &networkTypes.Network{ID: xid, PublicID: publicID})
|
||||
}
|
||||
// The twin keeps only the ids of the users a peer may be shared with; the
|
||||
// legacy side derives the same set from the account's user records, so a
|
||||
// bare non-blocked regular user per id is enough.
|
||||
for userID := range nmData.AllowedUserIDs {
|
||||
account.Users[userID] = &types.User{Id: userID}
|
||||
}
|
||||
|
||||
// Main's network-map controller synthesised the reverse-proxy ACLs onto the
|
||||
// account and only then derived the resource-policy map, so the frozen copy
|
||||
// has to be fed in that order to stand for what main produced.
|
||||
account.Policies = append(account.Policies, legacynmap.SynthesizeProxyPolicies(account)...)
|
||||
|
||||
return legacyInput{
|
||||
account: account,
|
||||
accountZones: accountZones(nmData.AppliedZoneCandidates),
|
||||
validatedPeers: nmData.ValidatedPeers,
|
||||
resourcePolicies: account.GetResourcePoliciesMap(),
|
||||
routers: accountRouters(nmData.Routers),
|
||||
groupIDToUserIDs: nmData.GroupIDToUserIDs,
|
||||
}
|
||||
}
|
||||
|
||||
// computeLegacy runs the fixture through main's frozen path and its own proto
|
||||
// encoder, the one comparison surface the three modes share.
|
||||
func computeLegacy(t *testing.T, ctx context.Context, legacy legacyInput, peerID string, zone nmdata.CustomZone, dnsDomain string, dnsFwdPort int64) *proto.NetworkMap {
|
||||
t.Helper()
|
||||
|
||||
require.NotNil(t, legacy.account, "legacy mode needs an account rebuilt from the fixture")
|
||||
peer := legacy.account.Peers[peerID]
|
||||
require.NotNil(t, peer, "target peer %q not in rebuilt account", peerID)
|
||||
|
||||
nm := legacynmap.GetPeerNetworkMapFromComponents(
|
||||
legacy.account, ctx, peerID, legacyCustomZone(zone), legacy.accountZones, legacy.validatedPeers,
|
||||
legacy.resourcePolicies, legacy.routers, nil, legacy.groupIDToUserIDs,
|
||||
)
|
||||
require.NotNil(t, nm, "legacy path returned no network map for peer %q", peerID)
|
||||
|
||||
return legacynmap.ToProtoNetworkMap(
|
||||
ctx, peer, nm, dnsDomain, legacy.account.Settings, nil, &cache.DNSConfigCache{}, dnsFwdPort,
|
||||
)
|
||||
}
|
||||
|
||||
// legacyCustomZone converts the peers custom zone the runner computes once for
|
||||
// every mode into the shape main's path took.
|
||||
func legacyCustomZone(z nmdata.CustomZone) nbdns.CustomZone {
|
||||
zoneRecords := make([]nbdns.SimpleRecord, 0, len(z.Records))
|
||||
for _, r := range z.Records {
|
||||
zoneRecords = append(zoneRecords, nbdns.SimpleRecord{
|
||||
Name: r.Name,
|
||||
Type: r.Type,
|
||||
Class: r.Class,
|
||||
TTL: r.TTL,
|
||||
RData: r.RData,
|
||||
})
|
||||
}
|
||||
return nbdns.CustomZone{
|
||||
Domain: z.Domain,
|
||||
Records: zoneRecords,
|
||||
SearchDomainDisabled: z.SearchDomainDisabled,
|
||||
NonAuthoritative: z.NonAuthoritative,
|
||||
}
|
||||
}
|
||||
|
||||
func accountNetwork(n *nmdata.Network) *types.Network {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.Network{
|
||||
Identifier: n.Identifier,
|
||||
Net: n.Net,
|
||||
NetV6: n.NetV6,
|
||||
Dns: n.Dns,
|
||||
Serial: uint64(n.Serial),
|
||||
}
|
||||
}
|
||||
|
||||
func accountSettings(s *nmdata.AccountSettingsInfo) *types.Settings {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.Settings{
|
||||
PeerLoginExpirationEnabled: s.PeerLoginExpirationEnabled,
|
||||
PeerLoginExpiration: s.PeerLoginExpiration,
|
||||
PeerInactivityExpirationEnabled: s.PeerInactivityExpirationEnabled,
|
||||
PeerInactivityExpiration: s.PeerInactivityExpiration,
|
||||
DNSDomain: s.DNSDomain,
|
||||
IPv6EnabledGroups: s.IPv6EnabledGroups,
|
||||
RoutingPeerDNSResolutionEnabled: s.RoutingPeerDNSResolutionEnabled,
|
||||
LazyConnectionEnabled: s.LazyConnectionEnabled,
|
||||
AutoUpdateVersion: s.AutoUpdateVersion,
|
||||
AutoUpdateAlways: s.AutoUpdateAlways,
|
||||
MetricsPushEnabled: s.MetricsPushEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
func accountPeer(id string, p *nmdata.Peer) *nbpeer.Peer {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
networkAddresses := make([]nbpeer.NetworkAddress, 0, len(p.Meta.NetworkAddresses))
|
||||
for _, na := range p.Meta.NetworkAddresses {
|
||||
networkAddresses = append(networkAddresses, nbpeer.NetworkAddress{NetIP: na.NetIP})
|
||||
}
|
||||
files := make([]nbpeer.File, 0, len(p.Meta.Files))
|
||||
for _, f := range p.Meta.Files {
|
||||
files = append(files, nbpeer.File{Path: f.Path, ProcessIsRunning: f.ProcessIsRunning})
|
||||
}
|
||||
return &nbpeer.Peer{
|
||||
ID: id,
|
||||
Key: p.Key,
|
||||
SSHKey: p.SSHKey,
|
||||
DNSLabel: p.DNSLabel,
|
||||
UserID: p.UserID,
|
||||
SSHEnabled: p.SSHEnabled,
|
||||
LoginExpirationEnabled: p.LoginExpirationEnabled,
|
||||
LastLogin: p.LastLogin,
|
||||
IP: p.IP,
|
||||
IPv6: p.IPv6,
|
||||
ExtraDNSLabels: p.ExtraDNSLabels,
|
||||
ProxyMeta: nbpeer.ProxyMeta{Embedded: p.ProxyMeta.Embedded, Cluster: p.ProxyMeta.Cluster},
|
||||
// Connected is what SynthesizePrivateServiceZones gates its records on,
|
||||
// and a fixture peer stands for a peer the store returned, so it is one
|
||||
// the account would have reported connected.
|
||||
Status: &nbpeer.PeerStatus{RequiresApproval: p.RequiresApproval, Connected: true},
|
||||
Meta: nbpeer.PeerSystemMeta{
|
||||
WtVersion: p.Meta.WtVersion,
|
||||
GoOS: p.Meta.GoOS,
|
||||
OSVersion: p.Meta.OSVersion,
|
||||
KernelVersion: p.Meta.KernelVersion,
|
||||
NetworkAddresses: networkAddresses,
|
||||
Files: files,
|
||||
Capabilities: p.Meta.Capabilities,
|
||||
SyncMessageVersion: p.Meta.SyncMessageVersion,
|
||||
Flags: nbpeer.Flags{
|
||||
ServerSSHAllowed: p.Meta.Flags.ServerSSHAllowed,
|
||||
DisableIPv6: p.Meta.Flags.DisableIPv6,
|
||||
},
|
||||
},
|
||||
Location: nbpeer.Location{
|
||||
CountryCode: p.Location.CountryCode,
|
||||
CityName: p.Location.CityName,
|
||||
ConnectionIP: p.Location.ConnectionIP,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func accountGroup(id string, g *nmdata.Group) *types.Group {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.Group{
|
||||
ID: id,
|
||||
Name: g.Name,
|
||||
PublicID: g.PublicID,
|
||||
Peers: g.Peers,
|
||||
}
|
||||
}
|
||||
|
||||
func accountPolicy(p *nmdata.Policy) *types.Policy {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
rules := make([]*types.PolicyRule, 0, len(p.Rules))
|
||||
for _, r := range p.Rules {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
var portRanges []sharedtypes.RulePortRange
|
||||
if r.PortRanges != nil {
|
||||
portRanges = make([]sharedtypes.RulePortRange, len(r.PortRanges))
|
||||
for i, pr := range r.PortRanges {
|
||||
portRanges[i] = sharedtypes.RulePortRange{Start: pr.Start, End: pr.End}
|
||||
}
|
||||
}
|
||||
rules = append(rules, &types.PolicyRule{
|
||||
ID: r.ID,
|
||||
PolicyID: r.PolicyID,
|
||||
Enabled: r.Enabled,
|
||||
Action: sharedtypes.PolicyTrafficActionType(r.Action),
|
||||
Protocol: sharedtypes.PolicyRuleProtocolType(r.Protocol),
|
||||
Bidirectional: r.Bidirectional,
|
||||
Sources: r.Sources,
|
||||
Destinations: r.Destinations,
|
||||
SourceResource: types.Resource{ID: r.SourceResource.ID, Type: sharedtypes.ResourceType(r.SourceResource.Type)},
|
||||
DestinationResource: types.Resource{ID: r.DestinationResource.ID, Type: sharedtypes.ResourceType(r.DestinationResource.Type)},
|
||||
Ports: r.Ports,
|
||||
PortRanges: portRanges,
|
||||
AuthorizedGroups: r.AuthorizedGroups,
|
||||
AuthorizedUser: r.AuthorizedUser,
|
||||
})
|
||||
}
|
||||
return &types.Policy{
|
||||
ID: p.ID,
|
||||
PublicID: p.PublicID,
|
||||
Enabled: p.Enabled,
|
||||
SourcePostureChecks: p.SourcePostureChecks,
|
||||
Rules: rules,
|
||||
}
|
||||
}
|
||||
|
||||
func accountRoute(r *nmdata.Route) *nbroute.Route {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &nbroute.Route{
|
||||
ID: nbroute.ID(r.ID),
|
||||
AccountID: r.AccountID,
|
||||
PublicID: r.PublicID,
|
||||
Network: r.Network,
|
||||
Domains: r.Domains,
|
||||
KeepRoute: r.KeepRoute,
|
||||
NetID: nbroute.NetID(r.NetID),
|
||||
Description: r.Description,
|
||||
Peer: r.Peer,
|
||||
PeerID: r.PeerID,
|
||||
PeerGroups: r.PeerGroups,
|
||||
NetworkType: nbroute.NetworkType(r.NetworkType),
|
||||
Masquerade: r.Masquerade,
|
||||
Metric: r.Metric,
|
||||
Enabled: r.Enabled,
|
||||
Groups: r.Groups,
|
||||
AccessControlGroups: r.AccessControlGroups,
|
||||
SkipAutoApply: r.SkipAutoApply,
|
||||
}
|
||||
}
|
||||
|
||||
func accountNSG(n *nmdata.NameServerGroup) *nbdns.NameServerGroup {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
nameServers := make([]nbdns.NameServer, 0, len(n.NameServers))
|
||||
for _, ns := range n.NameServers {
|
||||
nameServers = append(nameServers, nbdns.NameServer{
|
||||
IP: ns.IP,
|
||||
NSType: nbdns.NameServerType(ns.NSType),
|
||||
Port: ns.Port,
|
||||
})
|
||||
}
|
||||
return &nbdns.NameServerGroup{
|
||||
ID: n.ID,
|
||||
PublicID: n.PublicID,
|
||||
Name: n.Name,
|
||||
Description: n.Description,
|
||||
NameServers: nameServers,
|
||||
Groups: n.Groups,
|
||||
Primary: n.Primary,
|
||||
Domains: n.Domains,
|
||||
Enabled: n.Enabled,
|
||||
SearchDomainsEnabled: n.SearchDomainsEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
func accountNetworkResource(r *nmdata.NetworkResource) *resourceTypes.NetworkResource {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &resourceTypes.NetworkResource{
|
||||
ID: r.ID,
|
||||
NetworkID: r.NetworkID,
|
||||
AccountID: r.AccountID,
|
||||
PublicID: r.PublicID,
|
||||
Name: r.Name,
|
||||
Description: r.Description,
|
||||
Type: resourceTypes.NetworkResourceType(r.Type),
|
||||
Address: r.Address,
|
||||
Domain: r.Domain,
|
||||
Prefix: r.Prefix,
|
||||
Enabled: r.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func accountPostureChecks(id string, pc *nmdata.PostureChecks, publicID string) *posture.Checks {
|
||||
if pc == nil {
|
||||
return nil
|
||||
}
|
||||
out := &posture.Checks{ID: id, PublicID: publicID}
|
||||
def := pc.Checks
|
||||
if def.NBVersionCheck != nil {
|
||||
out.Checks.NBVersionCheck = &posture.NBVersionCheck{MinVersion: def.NBVersionCheck.MinVersion}
|
||||
}
|
||||
if def.OSVersionCheck != nil {
|
||||
oc := &posture.OSVersionCheck{}
|
||||
if def.OSVersionCheck.Android != nil {
|
||||
oc.Android = &posture.MinVersionCheck{MinVersion: def.OSVersionCheck.Android.MinVersion}
|
||||
}
|
||||
if def.OSVersionCheck.Darwin != nil {
|
||||
oc.Darwin = &posture.MinVersionCheck{MinVersion: def.OSVersionCheck.Darwin.MinVersion}
|
||||
}
|
||||
if def.OSVersionCheck.Ios != nil {
|
||||
oc.Ios = &posture.MinVersionCheck{MinVersion: def.OSVersionCheck.Ios.MinVersion}
|
||||
}
|
||||
if def.OSVersionCheck.Linux != nil {
|
||||
oc.Linux = &posture.MinKernelVersionCheck{MinKernelVersion: def.OSVersionCheck.Linux.MinKernelVersion}
|
||||
}
|
||||
if def.OSVersionCheck.Windows != nil {
|
||||
oc.Windows = &posture.MinKernelVersionCheck{MinKernelVersion: def.OSVersionCheck.Windows.MinKernelVersion}
|
||||
}
|
||||
out.Checks.OSVersionCheck = oc
|
||||
}
|
||||
if def.GeoLocationCheck != nil {
|
||||
gc := &posture.GeoLocationCheck{Action: def.GeoLocationCheck.Action}
|
||||
for _, loc := range def.GeoLocationCheck.Locations {
|
||||
gc.Locations = append(gc.Locations, posture.Location{CountryCode: loc.CountryCode, CityName: loc.CityName})
|
||||
}
|
||||
out.Checks.GeoLocationCheck = gc
|
||||
}
|
||||
if def.PeerNetworkRangeCheck != nil {
|
||||
out.Checks.PeerNetworkRangeCheck = &posture.PeerNetworkRangeCheck{
|
||||
Action: def.PeerNetworkRangeCheck.Action,
|
||||
Ranges: def.PeerNetworkRangeCheck.Ranges,
|
||||
}
|
||||
}
|
||||
if def.ProcessCheck != nil {
|
||||
procs := make([]posture.Process, 0, len(def.ProcessCheck.Processes))
|
||||
for _, p := range def.ProcessCheck.Processes {
|
||||
procs = append(procs, posture.Process{LinuxPath: p.LinuxPath, MacPath: p.MacPath, WindowsPath: p.WindowsPath})
|
||||
}
|
||||
out.Checks.ProcessCheck = &posture.ProcessCheck{Processes: procs}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func accountServices(services []*nmdata.Service) []*service.Service {
|
||||
if len(services) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]*service.Service, 0, len(services))
|
||||
for _, svc := range services {
|
||||
if svc == nil {
|
||||
continue
|
||||
}
|
||||
targets := make([]*service.Target, 0, len(svc.Targets))
|
||||
for _, t := range svc.Targets {
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
target := &service.Target{
|
||||
Enabled: t.Enabled,
|
||||
Port: t.Port,
|
||||
Protocol: t.Protocol,
|
||||
TargetId: t.TargetID,
|
||||
TargetType: service.TargetType(t.TargetType),
|
||||
}
|
||||
if t.Path != "" {
|
||||
path := t.Path
|
||||
target.Path = &path
|
||||
}
|
||||
targets = append(targets, target)
|
||||
}
|
||||
out = append(out, &service.Service{
|
||||
ID: svc.ID,
|
||||
Enabled: svc.Enabled,
|
||||
Private: svc.Private,
|
||||
Mode: svc.Mode,
|
||||
ProxyCluster: svc.ProxyCluster,
|
||||
AccessGroups: svc.AccessGroups,
|
||||
Targets: targets,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// accountZones inverts buildAppliedZoneCandidates. Records come back with the
|
||||
// record type the builder mapped them from; a candidate only ever carries the
|
||||
// three types it converts.
|
||||
func accountZones(candidates []networkmap.AppliedZoneCandidate) []*zones.Zone {
|
||||
if len(candidates) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]*zones.Zone, 0, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
zoneRecords := make([]*records.Record, 0, len(candidate.Zone.Records))
|
||||
for _, r := range candidate.Zone.Records {
|
||||
recordType, ok := zoneRecordType(r.Type)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
zoneRecords = append(zoneRecords, &records.Record{
|
||||
Name: strings.TrimSuffix(r.Name, "."),
|
||||
Type: recordType,
|
||||
Content: r.RData,
|
||||
TTL: r.TTL,
|
||||
})
|
||||
}
|
||||
out = append(out, &zones.Zone{
|
||||
ID: candidate.Zone.Domain,
|
||||
Domain: strings.TrimSuffix(candidate.Zone.Domain, "."),
|
||||
Enabled: true,
|
||||
EnableSearchDomain: !candidate.Zone.SearchDomainDisabled,
|
||||
DistributionGroups: candidate.DistributionGroups,
|
||||
Records: zoneRecords,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func zoneRecordType(recordType int) (records.RecordType, bool) {
|
||||
switch uint16(recordType) {
|
||||
case dns.TypeA:
|
||||
return records.RecordTypeA, true
|
||||
case dns.TypeAAAA:
|
||||
return records.RecordTypeAAAA, true
|
||||
case dns.TypeCNAME:
|
||||
return records.RecordTypeCNAME, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func accountRouters(routers map[string]map[string]*nmdata.NetworkRouter) map[string]map[string]*routerTypes.NetworkRouter {
|
||||
if len(routers) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]map[string]*routerTypes.NetworkRouter, len(routers))
|
||||
for networkID, inner := range routers {
|
||||
converted := make(map[string]*routerTypes.NetworkRouter, len(inner))
|
||||
for peerID, router := range inner {
|
||||
if router == nil {
|
||||
continue
|
||||
}
|
||||
converted[peerID] = &routerTypes.NetworkRouter{
|
||||
NetworkID: networkID,
|
||||
PublicID: router.PublicID,
|
||||
Peer: peerID,
|
||||
PeerGroups: router.PeerGroups,
|
||||
Masquerade: router.Masquerade,
|
||||
Metric: router.Metric,
|
||||
Enabled: router.Enabled,
|
||||
}
|
||||
}
|
||||
out[networkID] = converted
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
// Package nmaptest measures network map generation on the dedicated store
|
||||
// path against committed expectations. A case stands in for the store load
|
||||
// with a NetworkMapData fixture — the value NetworkMapDBStoreImpl returns for
|
||||
// one account — then runs the production per-peer pipeline the controller
|
||||
// uses, PeersCustomZone → GetPeerNetworkMapComponents → proto conversion, in
|
||||
// both wire shapes: the full map (grpc.ToSyncResponse) and the component
|
||||
// envelope expanded client-side (grpc.ToComponentSyncResponse →
|
||||
// networkmap.EnvelopeToNetworkMap). A third mode inverts the fixture back into
|
||||
// the Account it stands for and runs main's frozen path over it (legacynmap),
|
||||
// so every case is pinned to what main shipped as well.
|
||||
//
|
||||
// The expectation files are the point of the framework. They state what the
|
||||
// output should be, so a failing case means the code disagrees with the
|
||||
// expectation and the answer is normally to fix the code; an expectation
|
||||
// changes only through a deliberate reviewed edit. Nothing in this package
|
||||
// writes to testdata — there is no flag that records current behaviour into an
|
||||
// expectation, because that is how a defect becomes the baseline. Cases whose
|
||||
// expectation encodes correct behaviour the code does not yet deliver stay red
|
||||
// on purpose.
|
||||
//
|
||||
// A case lives in testdata/cases/<name>/ as case.json (manifest: description,
|
||||
// peers, optional accountID, dnsDomain, modes), nmdata.json (the fixture the
|
||||
// mocked store returns, using Go field names; zero values may be omitted and
|
||||
// applyFixtureDefaults fills the boilerplate) and golden/<peerID>.json.
|
||||
//
|
||||
// There is ONE expectation per peer, shared by every mode, because all three
|
||||
// must arrive at the same client-facing map. Full and envelope are not even
|
||||
// different computations — CalculateNetworkMapFromComponents is
|
||||
// components.Calculate and both assemble the proto with the same encode
|
||||
// helpers — so the only variable between them is what the envelope round-trip
|
||||
// did in transit, and a difference there is a round-trip fidelity defect.
|
||||
// Legacy is a different computation, main's, reached from a rebuilt account;
|
||||
// a difference there is this tree having drifted from what main shipped.
|
||||
// Results are canonicalized before comparison, since repeated proto fields
|
||||
// come from map iteration.
|
||||
package nmaptest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/maps"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
|
||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
|
||||
mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
// Mode selects the wire shape a case is verified through. Both end in a
|
||||
// *proto.NetworkMap, the one comparison surface shared by every path.
|
||||
type Mode string
|
||||
|
||||
const (
|
||||
// ModeFull is the legacy wire shape: the server runs Calculate and sends
|
||||
// the expanded map (grpc.ToSyncResponse).
|
||||
ModeFull Mode = "full"
|
||||
// ModeEnvelope is the component wire shape: the server encodes components
|
||||
// into a NetworkMapEnvelope (grpc.ToComponentSyncResponse) and the map is
|
||||
// expanded the way the client engine does (networkmap.EnvelopeToNetworkMap).
|
||||
ModeEnvelope Mode = "envelope"
|
||||
// ModeLegacy is main's frozen path: the fixture is inverted back into the
|
||||
// Account it stands for and run through legacynmap, the copy of what main
|
||||
// shipped. It is the outside measurement — the other two modes share this
|
||||
// tree's computation, so only this one can catch the whole tree drifting.
|
||||
ModeLegacy Mode = "legacy"
|
||||
|
||||
defaultAccountID = "account"
|
||||
defaultDNSDomain = "netbird.test"
|
||||
)
|
||||
|
||||
var defaultModes = []Mode{ModeFull, ModeEnvelope, ModeLegacy}
|
||||
|
||||
// Case is one nmap-generation scenario: store data for a single account, the
|
||||
// peers whose network maps are computed, and the directory holding one expected
|
||||
// *proto.NetworkMap per peer — shared by every mode.
|
||||
type Case struct {
|
||||
Name string
|
||||
AccountID string
|
||||
DNSDomain string
|
||||
Peers []string
|
||||
Modes []Mode
|
||||
Data *networkmap.NetworkMapData
|
||||
GoldenDir string
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
Description string
|
||||
AccountID string
|
||||
DNSDomain string
|
||||
Peers []string
|
||||
Modes []Mode
|
||||
}
|
||||
|
||||
// RunGoldenDir discovers and runs every fixture case under dir. A case is a
|
||||
// directory containing case.json (manifest), nmdata.json (store fixture) and
|
||||
// golden/<peerID>.json (expected proto.NetworkMap, protojson).
|
||||
func RunGoldenDir(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
require.NoError(t, err, "read cases dir")
|
||||
|
||||
ran := 0
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
caseDir := filepath.Join(dir, entry.Name())
|
||||
c, err := loadCase(caseDir)
|
||||
require.NoError(t, err, "load case %s", entry.Name())
|
||||
ran++
|
||||
t.Run(entry.Name(), func(t *testing.T) {
|
||||
RunCase(t, c)
|
||||
})
|
||||
}
|
||||
require.NotZero(t, ran, "no cases found under %s", dir)
|
||||
}
|
||||
|
||||
func loadCase(caseDir string) (Case, error) {
|
||||
raw, err := os.ReadFile(filepath.Join(caseDir, "case.json"))
|
||||
if err != nil {
|
||||
return Case{}, fmt.Errorf("read manifest: %w", err)
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
var m manifest
|
||||
if err := dec.Decode(&m); err != nil {
|
||||
return Case{}, fmt.Errorf("decode manifest: %w", err)
|
||||
}
|
||||
|
||||
data, err := LoadNetworkMapData(filepath.Join(caseDir, "nmdata.json"))
|
||||
if err != nil {
|
||||
return Case{}, err
|
||||
}
|
||||
|
||||
return Case{
|
||||
Name: filepath.Base(caseDir),
|
||||
AccountID: m.AccountID,
|
||||
DNSDomain: m.DNSDomain,
|
||||
Peers: m.Peers,
|
||||
Modes: m.Modes,
|
||||
Data: data,
|
||||
GoldenDir: filepath.Join(caseDir, "golden"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RunCase computes each target peer's network map through every enabled mode
|
||||
// and compares the canonicalized result against the peer's expectation file.
|
||||
// It mirrors the controller's store path: fill fixture defaults, precompute
|
||||
// posture validation once, then run the per-peer pipeline.
|
||||
func RunCase(t *testing.T, c Case) {
|
||||
t.Helper()
|
||||
|
||||
require.NotNil(t, c.Data, "case %s: Data is required", c.Name)
|
||||
require.NotEmpty(t, c.Peers, "case %s: Peers is required", c.Name)
|
||||
require.NotEmpty(t, c.GoldenDir, "case %s: GoldenDir is required", c.Name)
|
||||
if c.AccountID == "" {
|
||||
c.AccountID = defaultAccountID
|
||||
}
|
||||
if c.DNSDomain == "" {
|
||||
c.DNSDomain = defaultDNSDomain
|
||||
}
|
||||
if len(c.Modes) == 0 {
|
||||
c.Modes = defaultModes
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
nmData := c.Data
|
||||
applyFixtureDefaults(nmData)
|
||||
nmData.BuildPrivateServiceCandidates()
|
||||
nmData.PrecomputePostureValidation()
|
||||
|
||||
dnsDomain := c.DNSDomain
|
||||
if nmData.AccountSettings.DNSDomain != "" {
|
||||
dnsDomain = nmData.AccountSettings.DNSDomain
|
||||
}
|
||||
|
||||
zone := networkmap.PeersCustomZone(ctx, c.AccountID, dnsDomain, nmData.Peers, controller.IPv6AllowedPeersFromData(nmData))
|
||||
dnsFwdPort := controller.ComputeForwarderPortFromData(nmData.Peers, network_map.DnsForwarderPortMinVersion)
|
||||
|
||||
for _, mode := range c.Modes {
|
||||
if mode == ModeEnvelope {
|
||||
requireEnvelopeSafeKeys(t, nmData, c.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Built before any mode runs: the first per-peer computation injects the
|
||||
// synthesised proxy ACLs into the twin's policies, and the legacy side
|
||||
// synthesises its own, so inverting a twin that already carries them would
|
||||
// hand the legacy path each ACL twice.
|
||||
var legacy legacyInput
|
||||
if slices.Contains(c.Modes, ModeLegacy) {
|
||||
legacy = legacyInputFromData(c.AccountID, nmData)
|
||||
}
|
||||
|
||||
for _, peerID := range c.Peers {
|
||||
peer := nmData.Peers[peerID]
|
||||
require.NotNil(t, peer, "case %s: target peer %q not in fixture", c.Name, peerID)
|
||||
|
||||
for _, mode := range c.Modes {
|
||||
t.Run(peerID+"/"+string(mode), func(t *testing.T) {
|
||||
got := computeMode(t, ctx, mode, nmData, peerID, zone, dnsDomain, dnsFwdPort, legacy)
|
||||
canonicalize(got)
|
||||
compareGolden(t, filepath.Join(c.GoldenDir, peerID+".json"), got, mode)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// computeMode produces the peer's proto.NetworkMap the way the controller does
|
||||
// for that wire shape.
|
||||
func computeMode(t *testing.T, ctx context.Context, mode Mode, nmData *networkmap.NetworkMapData,
|
||||
peerID string, zone nmdata.CustomZone, dnsDomain string, dnsFwdPort int64, legacy legacyInput) *proto.NetworkMap {
|
||||
t.Helper()
|
||||
|
||||
peer := nmData.Peers[peerID]
|
||||
require.NotNil(t, peer, "target peer %q not in fixture", peerID)
|
||||
|
||||
switch mode {
|
||||
case ModeLegacy:
|
||||
return computeLegacy(t, ctx, legacy, peerID, zone, dnsDomain, dnsFwdPort)
|
||||
case ModeFull:
|
||||
nmap := controller.NetworkMapFromData(ctx, nmData, peerID, zone, nil)
|
||||
return mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, nmap, dnsDomain, nil,
|
||||
&cache.DNSConfigCache{}, nmData.AccountSettings, nil, nil, dnsFwdPort).NetworkMap
|
||||
case ModeEnvelope:
|
||||
components := nmData.GetPeerNetworkMapComponents(peerID, zone)
|
||||
peerGroups := maps.Keys(nmData.GetPeerGroups(peerID))
|
||||
resp := mgmtgrpc.ToComponentSyncResponse(ctx, nil, nil, nil, peer, nil, nil, components, nil,
|
||||
dnsDomain, nil, nmData.AccountSettings, nil, peerGroups, dnsFwdPort)
|
||||
res, err := networkmap.EnvelopeToNetworkMap(ctx, resp.NetworkMapEnvelope, peer.Key, dnsDomain)
|
||||
require.NoError(t, err, "expand envelope")
|
||||
return res.NetworkMap
|
||||
default:
|
||||
t.Fatalf("unknown mode %q", mode)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// requireEnvelopeSafeKeys fails fast on peer keys the envelope decoder would
|
||||
// silently drop: it re-keys peers by base64 of the raw 32-byte WG public key.
|
||||
func requireEnvelopeSafeKeys(t *testing.T, nmData *networkmap.NetworkMapData, caseName string) {
|
||||
t.Helper()
|
||||
for id, p := range nmData.Peers {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
raw, err := base64.StdEncoding.DecodeString(p.Key)
|
||||
if err != nil || len(raw) != 32 {
|
||||
t.Fatalf("case %s: peer %q Key must be base64 of 32 bytes for mode %q (the envelope decoder drops it otherwise); use a real WireGuard public key or restrict the case to mode %q",
|
||||
caseName, id, ModeEnvelope, ModeFull)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// compareGolden measures got against the committed expectation file. One
|
||||
// expectation serves every mode, because the modes run the same computation and
|
||||
// must therefore agree. The expectation is the authority: a mismatch means the
|
||||
// code does not produce what this case says it should, so it is reported as a
|
||||
// failure and not quietly absorbed.
|
||||
//
|
||||
// The full and legacy modes are compared verbatim, identifiers included, so the
|
||||
// expectation pins real ids and stays readable. The envelope mode has
|
||||
// identifiers erased on both sides first, because it currently rewrites them —
|
||||
// a tracked defect that TestIDSpaceMatches asserts against on its own, so it
|
||||
// does not have to drown out every other case here.
|
||||
// Nothing here writes to testdata. Expectation files are authored by hand and
|
||||
// only ever change through a reviewed edit, so there is no mode in which a run
|
||||
// can create or replace one. When a file is missing the computed map is printed
|
||||
// for the author to read and, if it is genuinely correct, save deliberately.
|
||||
func compareGolden(t *testing.T, path string, got *proto.NetworkMap, mode Mode) {
|
||||
t.Helper()
|
||||
|
||||
if mode == ModeEnvelope {
|
||||
normalizeIDSpace(got)
|
||||
canonicalize(got)
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
rendered, mErr := renderNetworkMap(got)
|
||||
require.NoError(t, mErr)
|
||||
t.Fatalf("no expectation file %s: %v\nThis case has nothing to measure against — write the "+
|
||||
"proto.NetworkMap this peer should receive. Mode %s currently produces:\n%s\nRead it before "+
|
||||
"saving any of it: if the code is wrong, so is this.", path, err, mode, rendered)
|
||||
}
|
||||
want := &proto.NetworkMap{}
|
||||
require.NoError(t, protojson.Unmarshal(raw, want), "parse expectation %s", path)
|
||||
canonicalize(want)
|
||||
if mode == ModeEnvelope {
|
||||
normalizeIDSpace(want)
|
||||
canonicalize(want)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
|
||||
t.Errorf("mode %s does not produce what %s expects (-want +got):\n%s\n"+
|
||||
"Every mode has to deliver the same client-facing map for the same account state. "+
|
||||
"The expectation file is the committed statement of correct output — fix the code, or change the "+
|
||||
"expectation deliberately if the intended behaviour really moved.", mode, path, diff)
|
||||
}
|
||||
}
|
||||
|
||||
// renderNetworkMap renders stable protojson: protojson output whitespace is
|
||||
// deliberately unstable, so it is reformatted through json.Indent.
|
||||
func renderNetworkMap(nm *proto.NetworkMap) ([]byte, error) {
|
||||
raw, err := protojson.Marshal(nm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := json.Indent(&buf, raw, "", " "); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf.WriteByte('\n')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Two groups joined by one allow-all policy; peer-c has SSH enabled so the legacy-SSH path fills SshAuth from AllowedUserIDs.",
|
||||
"peers": [
|
||||
"peer-a",
|
||||
"peer-c"
|
||||
]
|
||||
}
|
||||
management/internals/controllers/network_map/nmaptest/testdata/cases/basic-policy/golden/peer-a.json
Vendored
+65
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"Serial": "5",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "4deEImv8zGvsyBmmfC2G0eQkbyMzyGuz/YK7pcYETwM=",
|
||||
"allowedIps": [
|
||||
"100.64.0.3/32"
|
||||
],
|
||||
"sshConfig": {
|
||||
"sshPubKey": "c3NoLXBlZXItYw=="
|
||||
},
|
||||
"fqdn": "peer-c.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-c.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.3",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLWFsbA=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.3",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLWFsbA=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
management/internals/controllers/network_map/nmaptest/testdata/cases/basic-policy/golden/peer-c.json
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"Serial": "5",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.3/10",
|
||||
"sshConfig": {
|
||||
"sshEnabled": true
|
||||
},
|
||||
"fqdn": "peer-c.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "AvldyrZ12Pf90jzf3AXmhPwg3UcI+jtJHfbpBlupvko=",
|
||||
"allowedIps": [
|
||||
"100.64.0.2/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-b.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
},
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-b.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.2"
|
||||
},
|
||||
{
|
||||
"Name": "peer-c.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLWFsbA=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLWFsbA=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.2",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLWFsbA=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.2",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLWFsbA=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub",
|
||||
"AuthorizedUsers": [
|
||||
"u9dHvAXZJKiXITuwP9jD/A=="
|
||||
],
|
||||
"machineUsers": {
|
||||
"*": {
|
||||
"indexes": [
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"Network": {"Serial": 5},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-b": {"IP": "100.64.0.2", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-c": {"IP": "100.64.0.3", "SSHEnabled": true, "SSHKey": "ssh-peer-c", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a", "peer-b"]},
|
||||
"grp-ops": {"Peers": ["peer-c"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-all",
|
||||
"PublicID": "pol-all-pub",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "all",
|
||||
"Bidirectional": true,
|
||||
"Sources": ["grp-dev"],
|
||||
"Destinations": ["grp-ops"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"AllowedUserIDs": {"user-ops": {}}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Nameserver group and applied custom zones distributed to grp-dev; peer-a (with an extra DNS label) receives them, peer-c is outside that group and receives only the zone distributed to grp-ops. Zone flags travel per zone: both grp-dev zones are match-only (NonAuthoritative), only search-off.internal. disables the search domain, and the built-in peer zone stays authoritative.",
|
||||
"peers": [
|
||||
"peer-a",
|
||||
"peer-c"
|
||||
]
|
||||
}
|
||||
Vendored
+115
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"Serial": "8",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "AvldyrZ12Pf90jzf3AXmhPwg3UcI+jtJHfbpBlupvko=",
|
||||
"allowedIps": [
|
||||
"100.64.0.2/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-b.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"NameServerGroups": [
|
||||
{
|
||||
"NameServers": [
|
||||
{
|
||||
"IP": "8.8.8.8",
|
||||
"Port": "53"
|
||||
}
|
||||
],
|
||||
"Primary": true
|
||||
}
|
||||
],
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "corp.internal.",
|
||||
"NonAuthoritative": true,
|
||||
"Records": [
|
||||
{
|
||||
"Name": "db.corp.internal.",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "10.10.0.5"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Domain": "search-off.internal.",
|
||||
"SearchDomainDisabled": true,
|
||||
"NonAuthoritative": true,
|
||||
"Records": [
|
||||
{
|
||||
"Name": "alias.search-off.internal.",
|
||||
"Type": "5",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "app.search-off.internal."
|
||||
},
|
||||
{
|
||||
"Name": "app.search-off.internal.",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "10.10.0.6"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-b.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.2"
|
||||
},
|
||||
{
|
||||
"Name": "www.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.2",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLW1lc2g="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.2",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "ALL",
|
||||
"PolicyID": "cG9sLW1lc2g="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"Serial": "8",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.3/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-c.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-c.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Domain": "ops-only.internal.",
|
||||
"NonAuthoritative": true,
|
||||
"Records": [
|
||||
{
|
||||
"Name": "tool.ops-only.internal.",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "10.10.0.7"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
Vendored
+74
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"Network": {"Serial": 8},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "ExtraDNSLabels": ["www"], "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-b": {"IP": "100.64.0.2", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-c": {"IP": "100.64.0.3", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a", "peer-b"]},
|
||||
"grp-ops": {"Peers": ["peer-c"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-mesh",
|
||||
"PublicID": "pol-mesh-pub",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "all",
|
||||
"Bidirectional": true,
|
||||
"Sources": ["grp-dev"],
|
||||
"Destinations": ["grp-dev"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"NameServerGroups": [
|
||||
{
|
||||
"ID": "nsg-1",
|
||||
"Name": "dns-primary",
|
||||
"NameServers": [{"IP": "8.8.8.8", "Port": 53}],
|
||||
"Groups": ["grp-dev"],
|
||||
"Primary": true,
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"AppliedZoneCandidates": [
|
||||
{
|
||||
"DistributionGroups": ["grp-dev"],
|
||||
"Zone": {
|
||||
"Domain": "corp.internal.",
|
||||
"NonAuthoritative": true,
|
||||
"Records": [
|
||||
{"Name": "db.corp.internal.", "Type": 1, "Class": "IN", "TTL": 300, "RData": "10.10.0.5"}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"DistributionGroups": ["grp-dev"],
|
||||
"Zone": {
|
||||
"Domain": "search-off.internal.",
|
||||
"NonAuthoritative": true,
|
||||
"SearchDomainDisabled": true,
|
||||
"Records": [
|
||||
{"Name": "app.search-off.internal.", "Type": 1, "Class": "IN", "TTL": 300, "RData": "10.10.0.6"},
|
||||
{"Name": "alias.search-off.internal.", "Type": 5, "Class": "IN", "TTL": 300, "RData": "app.search-off.internal."}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"DistributionGroups": ["grp-ops"],
|
||||
"Zone": {
|
||||
"Domain": "ops-only.internal.",
|
||||
"NonAuthoritative": true,
|
||||
"Records": [
|
||||
{"Name": "tool.ops-only.internal.", "Type": 1, "Class": "IN", "TTL": 300, "RData": "10.10.0.7"}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "Domain network resource: the route carries the domain list and the 192.0.2.0/32 placeholder network with NetworkType 3 (dynamic), and peer-r's route firewall rules must be marked dynamic and repeat the domain. Two ports on the policy must produce one rule per port. A domain resource contributes no DNS custom zone of its own — resolution happens through the routing peer's forwarder.",
|
||||
"peers": ["peer-a", "peer-r"]
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"Serial": "22",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"allowedIps": [
|
||||
"100.64.0.9/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-domain:peer-r",
|
||||
"Network": "192.0.2.0/32",
|
||||
"NetworkType": "3",
|
||||
"Peer": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "app-domain",
|
||||
"Domains": [
|
||||
"app.internal"
|
||||
],
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"Serial": "22",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.9/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-domain:peer-r",
|
||||
"Network": "192.0.2.0/32",
|
||||
"NetworkType": "3",
|
||||
"Peer": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "app-domain",
|
||||
"Domains": [
|
||||
"app.internal"
|
||||
],
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.9"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRules": [
|
||||
{
|
||||
"sourceRanges": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"destination": "192.0.2.0/32",
|
||||
"protocol": "TCP",
|
||||
"portInfo": {
|
||||
"port": 443
|
||||
},
|
||||
"isDynamic": true,
|
||||
"domains": [
|
||||
"app.internal"
|
||||
],
|
||||
"PolicyID": "cG9sLWFwcA==",
|
||||
"RouteID": "res-domain:peer-r"
|
||||
},
|
||||
{
|
||||
"sourceRanges": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"destination": "192.0.2.0/32",
|
||||
"protocol": "TCP",
|
||||
"portInfo": {
|
||||
"port": 80
|
||||
},
|
||||
"isDynamic": true,
|
||||
"domains": [
|
||||
"app.internal"
|
||||
],
|
||||
"PolicyID": "cG9sLWFwcA==",
|
||||
"RouteID": "res-domain:peer-r"
|
||||
}
|
||||
],
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
management/internals/controllers/network_map/nmaptest/testdata/cases/net-domain-resource/nmdata.json
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Network": {"Serial": 22},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r": {"IP": "100.64.0.9", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-app",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["80", "443"],
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "res-domain", "Type": "domain"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ResourcePolicies": {"res-domain": [{"ID": "pol-app"}]},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-domain",
|
||||
"NetworkID": "net-1",
|
||||
"Name": "app-domain",
|
||||
"Type": "domain",
|
||||
"Domain": "app.internal",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-1": {
|
||||
"peer-r": {"PublicID": "router-direct", "Masquerade": true, "Metric": 9999, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "Host network resource (single /32) behind one directly-assigned router. peer-a is in the resource policy's source group and must receive one route to 10.10.0.7/32 via peer-r with KeepRoute set and NetID taken from the resource name; peer-r as the router must receive the same route plus a route firewall rule whose SourceRanges are the policy's source peers. A client never gets route firewall rules.",
|
||||
"peers": ["peer-a", "peer-r"]
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"Serial": "20",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"allowedIps": [
|
||||
"100.64.0.9/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-host:peer-r",
|
||||
"Network": "10.10.0.7/32",
|
||||
"NetworkType": "1",
|
||||
"Peer": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "web-host",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"Serial": "20",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.9/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-host:peer-r",
|
||||
"Network": "10.10.0.7/32",
|
||||
"NetworkType": "1",
|
||||
"Peer": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "web-host",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.9"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRules": [
|
||||
{
|
||||
"sourceRanges": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"destination": "10.10.0.7/32",
|
||||
"protocol": "TCP",
|
||||
"portInfo": {
|
||||
"port": 443
|
||||
},
|
||||
"PolicyID": "cG9sLXdlYg==",
|
||||
"RouteID": "res-host:peer-r"
|
||||
}
|
||||
],
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Network": {"Serial": 20},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r": {"IP": "100.64.0.9", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-web",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["443"],
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "res-host", "Type": "host"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ResourcePolicies": {"res-host": [{"ID": "pol-web"}]},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-host",
|
||||
"NetworkID": "net-1",
|
||||
"Name": "web-host",
|
||||
"Type": "host",
|
||||
"Prefix": "10.10.0.7/32",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-1": {
|
||||
"peer-r": {"PublicID": "router-direct", "Masquerade": true, "Metric": 9999, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
management/internals/controllers/network_map/nmaptest/testdata/cases/net-resource-disabled/case.json
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "A disabled resource with a valid policy and router must leave no trace: no routes and no route firewall rules for either the client or the router. Disabling a resource is the switch that revokes access without deleting the policy.",
|
||||
"peers": ["peer-a", "peer-r"]
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "25",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "25",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.9/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.9"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Network": {"Serial": 25},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r": {"IP": "100.64.0.9", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-off-resource",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["443"],
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "res-disabled", "Type": "subnet"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ResourcePolicies": {"res-disabled": [{"ID": "pol-off-resource"}]},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-disabled",
|
||||
"NetworkID": "net-1",
|
||||
"Name": "disabled-subnet",
|
||||
"Type": "subnet",
|
||||
"Prefix": "10.50.0.0/24"
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-1": {
|
||||
"peer-r": {"PublicID": "router-direct", "Masquerade": true, "Metric": 9999, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "An enabled resource with a healthy router but no policy granting access to it must produce nothing anywhere: no route for the client and none for the router either, since access to a resource is only ever created by a policy. The router also gets no route firewall rules despite being a routing peer for the network.",
|
||||
"peers": ["peer-a", "peer-r"]
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "24",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "24",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.9/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.9"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"Network": {"Serial": 24},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r": {"IP": "100.64.0.9", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]}
|
||||
},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-orphan",
|
||||
"NetworkID": "net-1",
|
||||
"Name": "orphan-subnet",
|
||||
"Type": "subnet",
|
||||
"Prefix": "10.40.0.0/24",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-1": {
|
||||
"peer-r": {"PublicID": "router-direct", "Masquerade": true, "Metric": 9999, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "A DISABLED policy granting access to a network resource must grant nothing: no route to 10.90.0.0/24 for peer-a and none for the router either, exactly as if the policy were absent. THE FULL EXPECTATION CURRENTLY FAILS, and should: resource-policy selection never checks policy.Enabled (networkmapcompute.go and networkmap_components.go both test only nil/len(Rules)/Rules[0]), so the legacy path still hands out the route — access survives disabling the policy. The envelope path happens to be correct because the encoder drops disabled policies from the wire. Fix the compute path, do not weaken this expectation.",
|
||||
"peers": ["peer-a", "peer-r"]
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "39",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "39",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.9/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.9"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Network": {"Serial": 39},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r": {"IP": "100.64.0.9", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-revoked",
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["5432"],
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "res-db", "Type": "subnet"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ResourcePolicies": {"res-db": [{"ID": "pol-revoked"}]},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-db",
|
||||
"NetworkID": "net-1",
|
||||
"Name": "db-subnet",
|
||||
"Type": "subnet",
|
||||
"Prefix": "10.90.0.0/24",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-1": {
|
||||
"peer-r": {"PublicID": "router-direct", "Masquerade": true, "Metric": 9999, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "The routing peer for the resource is not in ValidatedPeers — an unapproved peer, which the integrated validator withholds. peer-a must therefore receive no route through it and must not see it as a peer at all: traffic may not be routed through a peer the account has not approved. THE ENVELOPE EXPECTATION CURRENTLY FAILS, and should: component selection puts every routing peer into RouterPeers without checking validation, the encoder indexes them into the envelope's peer table, and the client decoder puts every peer it finds back into its peer map, so the unapproved router reappears client-side with a working route. The full path drops it correctly. Fix the component/encoder path, do not weaken this expectation.",
|
||||
"peers": ["peer-a"]
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "40",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"Network": {"Serial": 40},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r": {"IP": "100.64.0.9", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"ValidatedPeers": {"peer-a": {}},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-db",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["5432"],
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "res-db", "Type": "subnet"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ResourcePolicies": {"res-db": [{"ID": "pol-db"}]},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-db",
|
||||
"NetworkID": "net-1",
|
||||
"Name": "db-subnet",
|
||||
"Type": "subnet",
|
||||
"Prefix": "10.100.0.0/24",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-1": {
|
||||
"peer-r": {"PublicID": "router-direct", "Masquerade": true, "Metric": 9999, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "Routing peer group: one router record assigned to a peer group, which the store expands into one entry per member peer sharing the router's settings. peer-a must receive one route per routing peer — same NetID and destination, different route ID and peer — which is what gives the client an HA pair to choose between. Each router must receive only its own route, never its sibling's, plus its own route firewall rule.",
|
||||
"peers": ["peer-a", "peer-r1", "peer-r2"]
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"Serial": "23",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "Iu6Lj1HqDXfgDh5RppHCRI6RO3lMlZBCndRNrdS7QUI=",
|
||||
"allowedIps": [
|
||||
"100.64.0.11/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r1.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
},
|
||||
{
|
||||
"wgPubKey": "YC6sbWtvpB2d6W/XqsNIV9crsrXBoVDdwmhCoszjCps=",
|
||||
"allowedIps": [
|
||||
"100.64.0.12/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r2.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-ha:peer-r1",
|
||||
"Network": "10.30.0.0/24",
|
||||
"NetworkType": "1",
|
||||
"Peer": "Iu6Lj1HqDXfgDh5RppHCRI6RO3lMlZBCndRNrdS7QUI=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "ha-subnet",
|
||||
"keepRoute": true
|
||||
},
|
||||
{
|
||||
"ID": "res-ha:peer-r2",
|
||||
"Network": "10.30.0.0/24",
|
||||
"NetworkType": "1",
|
||||
"Peer": "YC6sbWtvpB2d6W/XqsNIV9crsrXBoVDdwmhCoszjCps=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "ha-subnet",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"Serial": "23",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.11/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r1.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-ha:peer-r1",
|
||||
"Network": "10.30.0.0/24",
|
||||
"NetworkType": "1",
|
||||
"Peer": "Iu6Lj1HqDXfgDh5RppHCRI6RO3lMlZBCndRNrdS7QUI=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "ha-subnet",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r1.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.11"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRules": [
|
||||
{
|
||||
"sourceRanges": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"destination": "10.30.0.0/24",
|
||||
"protocol": "TCP",
|
||||
"portInfo": {
|
||||
"port": 5432
|
||||
},
|
||||
"PolicyID": "cG9sLWhh",
|
||||
"RouteID": "res-ha:peer-r1"
|
||||
}
|
||||
],
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"Serial": "23",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.12/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r2.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-ha:peer-r2",
|
||||
"Network": "10.30.0.0/24",
|
||||
"NetworkType": "1",
|
||||
"Peer": "YC6sbWtvpB2d6W/XqsNIV9crsrXBoVDdwmhCoszjCps=",
|
||||
"Metric": "9999",
|
||||
"Masquerade": true,
|
||||
"NetID": "ha-subnet",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r2.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.12"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRules": [
|
||||
{
|
||||
"sourceRanges": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"destination": "10.30.0.0/24",
|
||||
"protocol": "TCP",
|
||||
"portInfo": {
|
||||
"port": 5432
|
||||
},
|
||||
"PolicyID": "cG9sLWhh",
|
||||
"RouteID": "res-ha:peer-r2"
|
||||
}
|
||||
],
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"Network": {"Serial": 23},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r1": {"IP": "100.64.0.11", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r2": {"IP": "100.64.0.12", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]},
|
||||
"grp-routers": {"Peers": ["peer-r1", "peer-r2"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-ha",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["5432"],
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "res-ha", "Type": "subnet"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ResourcePolicies": {"res-ha": [{"ID": "pol-ha"}]},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-ha",
|
||||
"NetworkID": "net-ha",
|
||||
"Name": "ha-subnet",
|
||||
"Type": "subnet",
|
||||
"Prefix": "10.30.0.0/24",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-ha": {
|
||||
"peer-r1": {"PublicID": "router-ha", "PeerGroups": ["grp-routers"], "Masquerade": true, "Metric": 9999, "Enabled": true},
|
||||
"peer-r2": {"PublicID": "router-ha", "PeerGroups": ["grp-routers"], "Masquerade": true, "Metric": 9999, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "Subnet network resource behind one directly-assigned router, with masquerade off and a non-default metric so both reach the wire verbatim, and an all-protocol policy from a two-peer source group. peer-r's route firewall rule must list both source peers; peer-b confirms a second client in the same group gets its own identical route.",
|
||||
"peers": ["peer-a", "peer-b", "peer-r"]
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"Serial": "21",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"allowedIps": [
|
||||
"100.64.0.9/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-subnet:peer-r",
|
||||
"Network": "10.20.0.0/24",
|
||||
"NetworkType": "1",
|
||||
"Peer": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"Metric": "500",
|
||||
"NetID": "office-subnet",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"Serial": "21",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.2/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-b.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"allowedIps": [
|
||||
"100.64.0.9/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-subnet:peer-r",
|
||||
"Network": "10.20.0.0/24",
|
||||
"NetworkType": "1",
|
||||
"Peer": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"Metric": "500",
|
||||
"NetID": "office-subnet",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-b.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"Serial": "21",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.9/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-r.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "AvldyrZ12Pf90jzf3AXmhPwg3UcI+jtJHfbpBlupvko=",
|
||||
"allowedIps": [
|
||||
"100.64.0.2/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-b.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
},
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"Routes": [
|
||||
{
|
||||
"ID": "res-subnet:peer-r",
|
||||
"Network": "10.20.0.0/24",
|
||||
"NetworkType": "1",
|
||||
"Peer": "ImPDKs2PJxHA24/N7umWi8lfEf2B0B5W/7dYZUzNS3s=",
|
||||
"Metric": "500",
|
||||
"NetID": "office-subnet",
|
||||
"keepRoute": true
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-r.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.9"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRules": [
|
||||
{
|
||||
"sourceRanges": [
|
||||
"100.64.0.1/32",
|
||||
"100.64.0.2/32"
|
||||
],
|
||||
"destination": "10.20.0.0/24",
|
||||
"protocol": "ALL",
|
||||
"portInfo": {},
|
||||
"PolicyID": "cG9sLXN1Ym5ldA==",
|
||||
"RouteID": "res-subnet:peer-r"
|
||||
}
|
||||
],
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
management/internals/controllers/network_map/nmaptest/testdata/cases/net-subnet-resource/nmdata.json
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Network": {"Serial": 21},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-b": {"IP": "100.64.0.2", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-r": {"IP": "100.64.0.9", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a", "peer-b"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-subnet",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "all",
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "res-subnet", "Type": "subnet"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ResourcePolicies": {"res-subnet": [{"ID": "pol-subnet"}]},
|
||||
"NetworkResources": [
|
||||
{
|
||||
"ID": "res-subnet",
|
||||
"NetworkID": "net-1",
|
||||
"Name": "office-subnet",
|
||||
"Type": "subnet",
|
||||
"Prefix": "10.20.0.0/24",
|
||||
"Enabled": true
|
||||
}
|
||||
],
|
||||
"Routers": {
|
||||
"net-1": {
|
||||
"peer-r": {"PublicID": "router-direct", "Metric": 500, "Enabled": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "A peer named directly as a rule source or destination is subject to approval exactly like a group member: unvalidated peer-b is neither a source for peer-c nor a destination for peer-a, while the validated direct source peer-a reaches peer-c. Legacy mode is excluded: the frozen legacynmap copy still carries the direct-peer bypass.",
|
||||
"peers": ["peer-a", "peer-c"],
|
||||
"modes": ["full", "envelope"]
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"Serial": "22",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "4deEImv8zGvsyBmmfC2G0eQkbyMzyGuz/YK7pcYETwM=",
|
||||
"allowedIps": [
|
||||
"100.64.0.3/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-c.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-c.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.3",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdC1vaw=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.3",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdC1vaw=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"Serial": "22",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.3/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-c.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-c.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdC1vaw=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdC1vaw=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"Network": {"Serial": 22},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-b": {"IP": "100.64.0.2", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-c": {"IP": "100.64.0.3", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"ValidatedPeers": {"peer-a": {}, "peer-c": {}},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]},
|
||||
"grp-ops": {"Peers": ["peer-c"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-direct-ok",
|
||||
"PublicID": "pol-direct-ok-pub",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["443"],
|
||||
"Bidirectional": true,
|
||||
"SourceResource": {"ID": "peer-a", "Type": "peer"},
|
||||
"Destinations": ["grp-ops"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "pol-src-unval",
|
||||
"PublicID": "pol-src-unval-pub",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["8443"],
|
||||
"Bidirectional": true,
|
||||
"SourceResource": {"ID": "peer-b", "Type": "peer"},
|
||||
"Destinations": ["grp-ops"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "pol-dst-unval",
|
||||
"PublicID": "pol-dst-unval-pub",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["9443"],
|
||||
"Bidirectional": true,
|
||||
"Sources": ["grp-dev"],
|
||||
"DestinationResource": {"ID": "peer-b", "Type": "peer"}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "Direct peer-to-peer policy via Source/DestinationResource of type peer, no groups involved; peer-a and peer-b see each other, bystander peer-c sees nobody.",
|
||||
"peers": ["peer-a", "peer-b", "peer-c"]
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"Serial": "15",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "AvldyrZ12Pf90jzf3AXmhPwg3UcI+jtJHfbpBlupvko=",
|
||||
"allowedIps": [
|
||||
"100.64.0.2/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-b.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-b.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.2",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdA=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.2",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdA=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"Serial": "15",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.2/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-b.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-b.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdA=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdA=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Serial": "15",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.3/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-c.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-c.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
management/internals/controllers/network_map/nmaptest/testdata/cases/policy-peer-to-peer/nmdata.json
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"Network": {"Serial": 15},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-b": {"IP": "100.64.0.2", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-c": {"IP": "100.64.0.3", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-direct",
|
||||
"PublicID": "pol-direct-pub",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["443"],
|
||||
"Bidirectional": true,
|
||||
"SourceResource": {"ID": "peer-a", "Type": "peer"},
|
||||
"DestinationResource": {"ID": "peer-b", "Type": "peer"}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "One-way udp/514 plus bidirectional tcp port-range 1000-2000 between the same groups; a disabled policy and a policy whose only rule is disabled must leave no trace.",
|
||||
"peers": ["peer-a", "peer-srv"]
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"Serial": "14",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "jEboa3bOv65XRbq8+8JuagiwWz+mM7Fc5MUfBQbOf6Y=",
|
||||
"allowedIps": [
|
||||
"100.64.0.10/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-srv.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-srv.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.10"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.10",
|
||||
"Protocol": "TCP",
|
||||
"PortInfo": {
|
||||
"range": {
|
||||
"start": 1000,
|
||||
"end": 2000
|
||||
}
|
||||
},
|
||||
"PolicyID": "cG9sLXJhbmdl"
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.10",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"PortInfo": {
|
||||
"range": {
|
||||
"start": 1000,
|
||||
"end": 2000
|
||||
}
|
||||
},
|
||||
"PolicyID": "cG9sLXJhbmdl"
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.10",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "UDP",
|
||||
"Port": "514",
|
||||
"PolicyID": "cG9sLXN5c2xvZw=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"Serial": "14",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.10/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-srv.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-srv.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.10"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Protocol": "TCP",
|
||||
"PortInfo": {
|
||||
"range": {
|
||||
"start": 1000,
|
||||
"end": 2000
|
||||
}
|
||||
},
|
||||
"PolicyID": "cG9sLXJhbmdl"
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"PortInfo": {
|
||||
"range": {
|
||||
"start": 1000,
|
||||
"end": 2000
|
||||
}
|
||||
},
|
||||
"PolicyID": "cG9sLXJhbmdl"
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Protocol": "UDP",
|
||||
"Port": "514",
|
||||
"PolicyID": "cG9sLXN5c2xvZw=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"Network": {"Serial": 14},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-srv": {"IP": "100.64.0.10", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a"]},
|
||||
"grp-svc": {"Peers": ["peer-srv"]}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-syslog",
|
||||
"PublicID": "pol-syslog-pub",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "udp",
|
||||
"Ports": ["514"],
|
||||
"Sources": ["grp-dev"],
|
||||
"Destinations": ["grp-svc"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "pol-range",
|
||||
"PublicID": "pol-range-pub",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"PortRanges": [{"Start": 1000, "End": 2000}],
|
||||
"Bidirectional": true,
|
||||
"Sources": ["grp-dev"],
|
||||
"Destinations": ["grp-svc"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "pol-off",
|
||||
"PublicID": "pol-off-pub",
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["9999"],
|
||||
"Bidirectional": true,
|
||||
"Sources": ["grp-dev"],
|
||||
"Destinations": ["grp-svc"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "pol-rule-off",
|
||||
"PublicID": "pol-rule-off-pub",
|
||||
"Enabled": true,
|
||||
"Rules": [
|
||||
{
|
||||
"Action": "accept",
|
||||
"Protocol": "udp",
|
||||
"Ports": ["1111"],
|
||||
"Bidirectional": true,
|
||||
"Sources": ["grp-dev"],
|
||||
"Destinations": ["grp-svc"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "Posture checks gate a policy's sources only, never its destinations. peer-srv-old would fail the version check, but it sits in the destination group, so peer-client must still receive it alongside peer-srv-new, and peer-srv-old must still receive peer-client. This asymmetry is deliberate in the compute path — destination peers are resolved with no posture checks passed in — and it is worth pinning because it is easy to assume a posture check protects both ends.",
|
||||
"peers": ["peer-client", "peer-srv-old"]
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"Serial": "37",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-client.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "MdeD+cDSnurizeZ/Zd7rEdIhs9VZViEnutUwkodqb1s=",
|
||||
"allowedIps": [
|
||||
"100.64.0.12/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-srv-new.netbird.test",
|
||||
"agentVersion": "1.0.0"
|
||||
},
|
||||
{
|
||||
"wgPubKey": "ph1eqUTlSeLQ6V9zLEUpck25m5K5sOQq+AHY879HZME=",
|
||||
"allowedIps": [
|
||||
"100.64.0.11/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-srv-old.netbird.test",
|
||||
"agentVersion": "0.30.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-client.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-srv-new.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.12"
|
||||
},
|
||||
{
|
||||
"Name": "peer-srv-old.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.11"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "5353"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.11",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRlc3Q="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.11",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRlc3Q="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.12",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRlc3Q="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.12",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRlc3Q="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"Serial": "37",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.11/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-srv-old.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "tKxuKEYQFPR8lCpcfVWBKVX0vGFKYXtTtFjXhoiu5zc=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-client.netbird.test",
|
||||
"agentVersion": "1.0.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-client.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-srv-old.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.11"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "5353"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRlc3Q="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRlc3Q="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Network": {"Serial": 37},
|
||||
"Peers": {
|
||||
"peer-client": {"IP": "100.64.0.1", "Meta": {"WtVersion": "1.0.0"}},
|
||||
"peer-srv-old": {"IP": "100.64.0.11", "Meta": {"WtVersion": "0.30.0"}},
|
||||
"peer-srv-new": {"IP": "100.64.0.12", "Meta": {"WtVersion": "1.0.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-clients": {"Peers": ["peer-client"]},
|
||||
"grp-srv": {"Peers": ["peer-srv-old", "peer-srv-new"]}
|
||||
},
|
||||
"PostureChecks": {
|
||||
"chk-version": {"Checks": {"NBVersionCheck": {"MinVersion": "0.45.0"}}}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-dest",
|
||||
"Enabled": true,
|
||||
"SourcePostureChecks": ["chk-version"],
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["443"],
|
||||
"Bidirectional": true,
|
||||
"Sources": ["grp-clients"],
|
||||
"Destinations": ["grp-srv"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
management/internals/controllers/network_map/nmaptest/testdata/cases/posture-direct-source/case.json
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"description": "A peer named directly as a rule source is gated by the policy's posture checks exactly like a group member: peer-b (0.40.0) fails the 0.45.0 minimum, so it gets no connectivity and peer-c must not see it, while the compliant direct source peer-a reaches peer-c. Legacy mode is excluded: the frozen legacynmap copy still carries the direct-peer bypass.",
|
||||
"peers": ["peer-b", "peer-c"],
|
||||
"modes": ["full", "envelope"]
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Serial": "21",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.2/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-b.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-b.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "5353"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"Serial": "21",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.3/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-c.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-c.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "5353"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdC1vaw=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWRpcmVjdC1vaw=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"Network": {"Serial": 21},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-b": {"IP": "100.64.0.2", "Meta": {"WtVersion": "0.40.0"}},
|
||||
"peer-c": {"IP": "100.64.0.3", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-ops": {"Peers": ["peer-c"]}
|
||||
},
|
||||
"PostureChecks": {
|
||||
"chk-ver": {"Checks": {"NBVersionCheck": {"MinVersion": "0.45.0"}}}
|
||||
},
|
||||
"PostureCheckXIDToPublicID": {"chk-ver": "chk-ver-pub"},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-direct-ok",
|
||||
"PublicID": "pol-direct-ok-pub",
|
||||
"Enabled": true,
|
||||
"SourcePostureChecks": ["chk-ver"],
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["443"],
|
||||
"Bidirectional": true,
|
||||
"SourceResource": {"ID": "peer-a", "Type": "peer"},
|
||||
"Destinations": ["grp-ops"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": "pol-direct-denied",
|
||||
"PublicID": "pol-direct-denied-pub",
|
||||
"Enabled": true,
|
||||
"SourcePostureChecks": ["chk-ver"],
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["8443"],
|
||||
"Bidirectional": true,
|
||||
"SourceResource": {"ID": "peer-b", "Type": "peer"},
|
||||
"Destinations": ["grp-ops"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Source-side NB-version posture check: peer-b (0.40.0) fails the 0.45.0 minimum, so peer-c must not see it and peer-b itself gets no policy connectivity.",
|
||||
"peers": [
|
||||
"peer-b",
|
||||
"peer-c"
|
||||
]
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Serial": "6",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.2/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-b.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-b.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "5353"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"Serial": "6",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.3/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-c.netbird.test",
|
||||
"RoutingPeerDnsResolutionEnabled": true,
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "vblMc9U8RAI6cVopcKEMTVT6lVC3D9nTTMSwot5d3L4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-a.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-a.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-c.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "5353"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWdhdGVk"
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWdhdGVk"
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Network": {"Serial": 6},
|
||||
"AccountSettings": {"RoutingPeerDNSResolutionEnabled": true},
|
||||
"Peers": {
|
||||
"peer-a": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-b": {"IP": "100.64.0.2", "Meta": {"WtVersion": "0.40.0"}},
|
||||
"peer-c": {"IP": "100.64.0.3", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-dev": {"Peers": ["peer-a", "peer-b"]},
|
||||
"grp-ops": {"Peers": ["peer-c"]}
|
||||
},
|
||||
"PostureChecks": {
|
||||
"chk-ver": {"Checks": {"NBVersionCheck": {"MinVersion": "0.45.0"}}}
|
||||
},
|
||||
"PostureCheckXIDToPublicID": {"chk-ver": "chk-ver-pub"},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-gated",
|
||||
"PublicID": "pol-gated-pub",
|
||||
"Enabled": true,
|
||||
"SourcePostureChecks": ["chk-ver"],
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Bidirectional": true,
|
||||
"Ports": ["443"],
|
||||
"Sources": ["grp-dev"],
|
||||
"Destinations": ["grp-ops"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "Geo location posture check in allow mode. An entry naming only a country matches the whole country, so peer-de passes; an entry naming a city must match that city exactly, so peer-us-ny passes while peer-us-bos does not. peer-fr matches nothing and fails. peer-nowhere has no location at all, which the check reports as an error, and an errored check denies — so it fails too.",
|
||||
"peers": ["peer-srv", "peer-de", "peer-us-bos", "peer-nowhere"]
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"Serial": "31",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.1/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-de.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "jEboa3bOv65XRbq8+8JuagiwWz+mM7Fc5MUfBQbOf6Y=",
|
||||
"allowedIps": [
|
||||
"100.64.0.10/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-srv.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-de.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-srv.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.10"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.10",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWdlbw=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.10",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWdlbw=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Serial": "31",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.5/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-nowhere.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-nowhere.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.5"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"Serial": "31",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.10/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-srv.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeers": [
|
||||
{
|
||||
"wgPubKey": "9nwvdE0wik6Fcs8Tw6WBnmOqGZmzdiTR4VZAdRBOJF4=",
|
||||
"allowedIps": [
|
||||
"100.64.0.2/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-us-ny.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
},
|
||||
{
|
||||
"wgPubKey": "s/f5frZqT3DT1o9QCuhA14Pj5GUa4JUsF4M3twrprmk=",
|
||||
"allowedIps": [
|
||||
"100.64.0.1/32"
|
||||
],
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-de.netbird.test",
|
||||
"agentVersion": "0.60.0"
|
||||
}
|
||||
],
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-de.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.1"
|
||||
},
|
||||
{
|
||||
"Name": "peer-srv.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.10"
|
||||
},
|
||||
{
|
||||
"Name": "peer-us-ny.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"FirewallRules": [
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWdlbw=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.1",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWdlbw=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.2",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWdlbw=="
|
||||
},
|
||||
{
|
||||
"PeerIP": "100.64.0.2",
|
||||
"Direction": "OUT",
|
||||
"Protocol": "TCP",
|
||||
"Port": "443",
|
||||
"PolicyID": "cG9sLWdlbw=="
|
||||
}
|
||||
],
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"Serial": "31",
|
||||
"peerConfig": {
|
||||
"address": "100.64.0.3/10",
|
||||
"sshConfig": {},
|
||||
"fqdn": "peer-us-bos.netbird.test",
|
||||
"autoUpdate": {}
|
||||
},
|
||||
"remotePeersIsEmpty": true,
|
||||
"DNSConfig": {
|
||||
"ServiceEnable": true,
|
||||
"CustomZones": [
|
||||
{
|
||||
"Domain": "netbird.test.",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "peer-us-bos.netbird.test",
|
||||
"Type": "1",
|
||||
"Class": "IN",
|
||||
"TTL": "300",
|
||||
"RData": "100.64.0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ForwarderPort": "22054"
|
||||
},
|
||||
"firewallRulesIsEmpty": true,
|
||||
"routesFirewallRulesIsEmpty": true,
|
||||
"sshAuth": {
|
||||
"UserIDClaim": "sub"
|
||||
}
|
||||
}
|
||||
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"Network": {"Serial": 31},
|
||||
"Peers": {
|
||||
"peer-de": {"IP": "100.64.0.1", "Meta": {"WtVersion": "0.60.0"}, "Location": {"CountryCode": "DE", "CityName": "Berlin"}},
|
||||
"peer-us-ny": {"IP": "100.64.0.2", "Meta": {"WtVersion": "0.60.0"}, "Location": {"CountryCode": "US", "CityName": "New York"}},
|
||||
"peer-us-bos": {"IP": "100.64.0.3", "Meta": {"WtVersion": "0.60.0"}, "Location": {"CountryCode": "US", "CityName": "Boston"}},
|
||||
"peer-fr": {"IP": "100.64.0.4", "Meta": {"WtVersion": "0.60.0"}, "Location": {"CountryCode": "FR", "CityName": "Paris"}},
|
||||
"peer-nowhere": {"IP": "100.64.0.5", "Meta": {"WtVersion": "0.60.0"}},
|
||||
"peer-srv": {"IP": "100.64.0.10", "Meta": {"WtVersion": "0.60.0"}}
|
||||
},
|
||||
"Groups": {
|
||||
"grp-clients": {"Peers": ["peer-de", "peer-us-ny", "peer-us-bos", "peer-fr", "peer-nowhere"]},
|
||||
"grp-srv": {"Peers": ["peer-srv"]}
|
||||
},
|
||||
"PostureChecks": {
|
||||
"chk-geo": {
|
||||
"Checks": {
|
||||
"GeoLocationCheck": {
|
||||
"Action": "allow",
|
||||
"Locations": [
|
||||
{"CountryCode": "DE"},
|
||||
{"CountryCode": "US", "CityName": "New York"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Policies": [
|
||||
{
|
||||
"ID": "pol-geo",
|
||||
"Enabled": true,
|
||||
"SourcePostureChecks": ["chk-geo"],
|
||||
"Rules": [
|
||||
{
|
||||
"Enabled": true,
|
||||
"Action": "accept",
|
||||
"Protocol": "tcp",
|
||||
"Ports": ["443"],
|
||||
"Bidirectional": true,
|
||||
"Sources": ["grp-clients"],
|
||||
"Destinations": ["grp-srv"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"description": "Geo location posture check in deny mode: matching the list rejects, not matching passes, so peer-ru is excluded and peer-de is admitted. peer-nowhere has no location and fails here as well — a missing location is an error and errors deny in both modes, so deny mode is not a way to admit peers whose location is unknown.",
|
||||
"peers": ["peer-srv", "peer-ru", "peer-nowhere"]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user