Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions combined/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import (
// Architecture:
// - Management: Always runs locally (this IS the management server)
// - Signal: Runs locally by default; disabled if server.signalUri is set
// - Relay: Runs locally by default; disabled if server.relays is set
// - Relay: Runs locally by default; server.additionalRelays adds more relay
// addresses, while server.relays disables the local relay and replaces them
// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set
Comment on lines +32 to 34
//
// All user-facing settings are under "server". The relay/signal/management
Expand Down Expand Up @@ -59,6 +60,9 @@ type ServerConfig struct {
AuthSecret string `yaml:"authSecret"` // Shared secret for relay authentication
DataDir string `yaml:"dataDir"` // Data directory for all services

// Additional relay addresses advertised alongside the local relay
AdditionalRelays []string `yaml:"additionalRelays"`

// External service overrides (simplified mode)
// When these are set, the corresponding local service is NOT started
// and these values are used for client configuration instead
Expand Down Expand Up @@ -273,6 +277,8 @@ func parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort st
// ApplySimplifiedDefaults populates internal relay/signal/management configs from server settings.
// Management is always enabled. Signal, Relay, and STUN are enabled unless external
// overrides are configured (server.signalUri, server.relays, server.stuns).
// server.additionalRelays does not override the local relay; its addresses are
// advertised to clients in addition to the auto-configured local relay address.
func (c *CombinedConfig) ApplySimplifiedDefaults() {
if !c.hasRequiredSettings() {
return
Expand Down Expand Up @@ -395,10 +401,9 @@ func (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost,
// Use external relay config from server
c.Management.Relays = c.Server.Relays
} else if len(c.Management.Relays.Addresses) == 0 {
// Auto-configure local relay
c.Management.Relays.Addresses = []string{
fmt.Sprintf("%s://%s", relayProto, exposedHostPort),
}
// Auto-configure the local relay, then advertise any additional relays.
localRelay := fmt.Sprintf("%s://%s", relayProto, exposedHostPort)
c.Management.Relays.Addresses = append([]string{localRelay}, c.Server.AdditionalRelays...)
}
if c.Management.Relays.Secret == "" {
c.Management.Relays.Secret = c.Server.AuthSecret
Expand Down
117 changes: 117 additions & 0 deletions combined/cmd/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package cmd

import (
"os"
"path/filepath"
"reflect"
"testing"
)

func TestApplySimplifiedDefaultsWithAdditionalRelays(t *testing.T) {
cfg := DefaultConfig()
cfg.Server.ExposedAddress = "https://netbird.example.com:443"
cfg.Server.AuthSecret = "shared-relay-secret"
cfg.Server.AdditionalRelays = []string{
"rels://relay-eu.example.com:443",
"rels://relay-us.example.com:443",
}

cfg.ApplySimplifiedDefaults()

if !cfg.Relay.Enabled {
t.Fatal("local relay should remain enabled when additional relays are configured")
}
if !cfg.Relay.Stun.Enabled {
t.Fatal("local STUN should remain enabled when additional relays are configured")
}

wantAddresses := []string{
"rels://netbird.example.com:443",
"rels://relay-eu.example.com:443",
"rels://relay-us.example.com:443",
}
if !reflect.DeepEqual(cfg.Management.Relays.Addresses, wantAddresses) {
t.Fatalf("relay addresses = %v, want %v", cfg.Management.Relays.Addresses, wantAddresses)
}
if cfg.Management.Relays.Secret != cfg.Server.AuthSecret {
t.Fatalf("relay secret = %q, want server auth secret", cfg.Management.Relays.Secret)
}
}

func TestApplySimplifiedDefaultsWithRelayOverride(t *testing.T) {
cfg := DefaultConfig()
cfg.Server.ExposedAddress = "https://netbird.example.com:443"
cfg.Server.Relays = RelaysConfig{
Addresses: []string{"rels://relay.example.com:443"},
CredentialsTTL: "24h",
Secret: "external-relay-secret",
}

cfg.ApplySimplifiedDefaults()

if cfg.Relay.Enabled {
t.Fatal("local relay should remain disabled when server.relays overrides it")
}
if cfg.Relay.Stun.Enabled {
t.Fatal("local STUN should remain disabled with the relay override")
}
if !reflect.DeepEqual(cfg.Management.Relays, cfg.Server.Relays) {
t.Fatalf("management relay config = %+v, want %+v", cfg.Management.Relays, cfg.Server.Relays)
}
}

func TestApplySimplifiedDefaultsWithRelayOverrideIgnoresAdditionalRelays(t *testing.T) {
cfg := DefaultConfig()
cfg.Server.ExposedAddress = "https://netbird.example.com:443"
cfg.Server.Relays = RelaysConfig{
Addresses: []string{"rels://relay.example.com:443"},
}
cfg.Server.AdditionalRelays = []string{
"rels://additional-relay.example.com:443",
}

cfg.ApplySimplifiedDefaults()

if !reflect.DeepEqual(cfg.Management.Relays.Addresses, cfg.Server.Relays.Addresses) {
t.Fatalf("management relay addresses = %v, want %v", cfg.Management.Relays.Addresses, cfg.Server.Relays.Addresses)
}
}

func TestAdditionalRelaysStillRequireLocalRelaySecret(t *testing.T) {
cfg := DefaultConfig()
cfg.Server.ExposedAddress = "https://netbird.example.com:443"
cfg.Server.AuthSecret = ""
cfg.Server.AdditionalRelays = []string{"rels://relay.example.com:443"}

if err := cfg.Validate(); err == nil {
t.Fatal("Validate() should require server.authSecret because the local relay is enabled")
}
}

func TestLoadConfigParsesAdditionalRelays(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.yaml")
configData := []byte(`server:
exposedAddress: "https://netbird.example.com:443"
authSecret: "shared-relay-secret"
additionalRelays:
- "rels://relay-eu.example.com:443"
- "rels://relay-us.example.com:443"
`)
if err := os.WriteFile(configPath, configData, 0o600); err != nil {
t.Fatalf("write test config: %v", err)
}

cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}

wantAddresses := []string{
"rels://netbird.example.com:443",
"rels://relay-eu.example.com:443",
"rels://relay-us.example.com:443",
}
if !reflect.DeepEqual(cfg.Management.Relays.Addresses, wantAddresses) {
t.Fatalf("relay addresses = %v, want %v", cfg.Management.Relays.Addresses, wantAddresses)
}
}
9 changes: 8 additions & 1 deletion combined/config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
# Architecture:
# - Management: Always runs locally (this IS the management server)
# - Signal: Local by default; set 'signalUri' to use external (disables local)
# - Relay: Local by default; set 'relays' to use external (disables local)
# - Relay: Local by default; set 'additionalRelays' to advertise more relays,
# or set 'relays' to replace the local relay with external relays
# - STUN: Local on port 3478 by default; set 'stuns' to use external instead
Comment on lines +11 to 13

server:
Expand Down Expand Up @@ -51,6 +52,12 @@ server:
# Data directory for all services
dataDir: "/var/lib/netbird/"

# Additional relay servers - keeps the local relay and advertises these too (ignored if relays.addresses is set).
# The additional relay servers must use the same authSecret as this server.
# additionalRelays:
# - "rels://relay-eu.example.com:443"
# - "rels://relay-us.example.com:443"

# ============================================================================
# External Service Overrides (optional)
# Use these to point to external Signal, Relay, or STUN servers instead of
Expand Down
Loading