mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-15 23:06:38 +00:00
[self-hosted] add netbird server (#5232)
* Unified NetBird combined server (Management, Signal, Relay, STUN) as a single executable with richer YAML configuration, validation, and defaults. * Official Dockerfile/image for single-container deployment. * Optional in-process profiling endpoint for diagnostics. * Multiplexing to route HTTP/gRPC/WebSocket traffic via one port; runtime hooks to inject custom handlers. * **Chores** * Updated deployment scripts, compose files, and reverse-proxy templates to target the combined server; added example configs and getting-started updates.
This commit is contained in:
@@ -106,6 +106,26 @@ builds:
|
|||||||
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
|
|
||||||
|
- id: netbird-server
|
||||||
|
dir: combined
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=1
|
||||||
|
- >-
|
||||||
|
{{- if eq .Runtime.Goos "linux" }}
|
||||||
|
{{- if eq .Arch "arm64"}}CC=aarch64-linux-gnu-gcc{{- end }}
|
||||||
|
{{- if eq .Arch "arm"}}CC=arm-linux-gnueabihf-gcc{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
binary: netbird-server
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- arm
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
|
|
||||||
- id: netbird-upload
|
- id: netbird-upload
|
||||||
dir: upload-server
|
dir: upload-server
|
||||||
env: [CGO_ENABLED=0]
|
env: [CGO_ENABLED=0]
|
||||||
@@ -520,6 +540,55 @@ dockers:
|
|||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
- "--label=maintainer=dev@netbird.io"
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-amd64
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-amd64
|
||||||
|
ids:
|
||||||
|
- netbird-server
|
||||||
|
goarch: amd64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: combined/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/amd64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm64v8
|
||||||
|
ids:
|
||||||
|
- netbird-server
|
||||||
|
goarch: arm64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: combined/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm
|
||||||
|
ids:
|
||||||
|
- netbird-server
|
||||||
|
goarch: arm
|
||||||
|
goarm: 6
|
||||||
|
use: buildx
|
||||||
|
dockerfile: combined/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
docker_manifests:
|
docker_manifests:
|
||||||
- name_template: netbirdio/netbird:{{ .Version }}
|
- name_template: netbirdio/netbird:{{ .Version }}
|
||||||
image_templates:
|
image_templates:
|
||||||
@@ -598,6 +667,18 @@ docker_manifests:
|
|||||||
- netbirdio/upload:{{ .Version }}-arm
|
- netbirdio/upload:{{ .Version }}-arm
|
||||||
- netbirdio/upload:{{ .Version }}-amd64
|
- netbirdio/upload:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/netbird-server:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-arm
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/netbird-server:latest
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-arm
|
||||||
|
- netbirdio/netbird-server:{{ .Version }}-amd64
|
||||||
|
|
||||||
- name_template: ghcr.io/netbirdio/netbird:{{ .Version }}
|
- name_template: ghcr.io/netbirdio/netbird:{{ .Version }}
|
||||||
image_templates:
|
image_templates:
|
||||||
- ghcr.io/netbirdio/netbird:{{ .Version }}-arm64v8
|
- ghcr.io/netbirdio/netbird:{{ .Version }}-arm64v8
|
||||||
@@ -675,6 +756,19 @@ docker_manifests:
|
|||||||
- ghcr.io/netbirdio/upload:{{ .Version }}-arm64v8
|
- ghcr.io/netbirdio/upload:{{ .Version }}-arm64v8
|
||||||
- ghcr.io/netbirdio/upload:{{ .Version }}-arm
|
- ghcr.io/netbirdio/upload:{{ .Version }}-arm
|
||||||
- ghcr.io/netbirdio/upload:{{ .Version }}-amd64
|
- ghcr.io/netbirdio/upload:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/netbird-server:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: ghcr.io/netbirdio/netbird-server:latest
|
||||||
|
image_templates:
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm64v8
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-arm
|
||||||
|
- ghcr.io/netbirdio/netbird-server:{{ .Version }}-amd64
|
||||||
|
|
||||||
brews:
|
brews:
|
||||||
- ids:
|
- ids:
|
||||||
- default
|
- default
|
||||||
|
|||||||
5
combined/Dockerfile
Normal file
5
combined/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
RUN apt update && apt install -y ca-certificates && rm -fr /var/cache/apt
|
||||||
|
ENTRYPOINT [ "/go/bin/netbird-server" ]
|
||||||
|
CMD ["--config", "/etc/netbird/config.yaml"]
|
||||||
|
COPY netbird-server /go/bin/netbird-server
|
||||||
715
combined/cmd/config.go
Normal file
715
combined/cmd/config.go
Normal file
@@ -0,0 +1,715 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/idp"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
"github.com/netbirdio/netbird/util/crypt"
|
||||||
|
|
||||||
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CombinedConfig is the root configuration for the combined server.
|
||||||
|
// The combined server is primarily a Management server with optional embedded
|
||||||
|
// Signal, Relay, and STUN services.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// - STUN: Runs locally on port 3478 by default; disabled if server.stuns is set
|
||||||
|
//
|
||||||
|
// All user-facing settings are under "server". The relay/signal/management
|
||||||
|
// fields are internal and populated automatically from server settings.
|
||||||
|
type CombinedConfig struct {
|
||||||
|
Server ServerConfig `yaml:"server"`
|
||||||
|
|
||||||
|
// Internal configs - populated from Server settings, not user-configurable
|
||||||
|
Relay RelayConfig `yaml:"-"`
|
||||||
|
Signal SignalConfig `yaml:"-"`
|
||||||
|
Management ManagementConfig `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerConfig contains server-wide settings
|
||||||
|
// In simplified mode, this contains all configuration
|
||||||
|
type ServerConfig struct {
|
||||||
|
ListenAddress string `yaml:"listenAddress"`
|
||||||
|
MetricsPort int `yaml:"metricsPort"`
|
||||||
|
HealthcheckAddress string `yaml:"healthcheckAddress"`
|
||||||
|
LogLevel string `yaml:"logLevel"`
|
||||||
|
LogFile string `yaml:"logFile"`
|
||||||
|
TLS TLSConfig `yaml:"tls"`
|
||||||
|
|
||||||
|
// Simplified config fields (used when relay/signal/management sections are omitted)
|
||||||
|
ExposedAddress string `yaml:"exposedAddress"` // Public address with protocol (e.g., "https://example.com:443")
|
||||||
|
StunPorts []int `yaml:"stunPorts"` // STUN ports (empty to disable local STUN)
|
||||||
|
AuthSecret string `yaml:"authSecret"` // Shared secret for relay authentication
|
||||||
|
DataDir string `yaml:"dataDir"` // Data directory for all services
|
||||||
|
|
||||||
|
// 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
|
||||||
|
Stuns []HostConfig `yaml:"stuns"` // External STUN servers (disables local STUN)
|
||||||
|
Relays RelaysConfig `yaml:"relays"` // External relay servers (disables local relay)
|
||||||
|
SignalURI string `yaml:"signalUri"` // External signal server (disables local signal)
|
||||||
|
|
||||||
|
// Management settings (simplified mode)
|
||||||
|
DisableAnonymousMetrics bool `yaml:"disableAnonymousMetrics"`
|
||||||
|
DisableGeoliteUpdate bool `yaml:"disableGeoliteUpdate"`
|
||||||
|
Auth AuthConfig `yaml:"auth"`
|
||||||
|
Store StoreConfig `yaml:"store"`
|
||||||
|
ReverseProxy ReverseProxyConfig `yaml:"reverseProxy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLSConfig contains TLS/HTTPS settings
|
||||||
|
type TLSConfig struct {
|
||||||
|
CertFile string `yaml:"certFile"`
|
||||||
|
KeyFile string `yaml:"keyFile"`
|
||||||
|
LetsEncrypt LetsEncryptConfig `yaml:"letsencrypt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LetsEncryptConfig contains Let's Encrypt settings
|
||||||
|
type LetsEncryptConfig struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
DataDir string `yaml:"dataDir"`
|
||||||
|
Domains []string `yaml:"domains"`
|
||||||
|
Email string `yaml:"email"`
|
||||||
|
AWSRoute53 bool `yaml:"awsRoute53"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RelayConfig contains relay service settings
|
||||||
|
type RelayConfig struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
ExposedAddress string `yaml:"exposedAddress"`
|
||||||
|
AuthSecret string `yaml:"authSecret"`
|
||||||
|
LogLevel string `yaml:"logLevel"`
|
||||||
|
Stun StunConfig `yaml:"stun"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StunConfig contains embedded STUN service settings
|
||||||
|
type StunConfig struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
Ports []int `yaml:"ports"`
|
||||||
|
LogLevel string `yaml:"logLevel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignalConfig contains signal service settings
|
||||||
|
type SignalConfig struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
LogLevel string `yaml:"logLevel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManagementConfig contains management service settings
|
||||||
|
type ManagementConfig struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
LogLevel string `yaml:"logLevel"`
|
||||||
|
DataDir string `yaml:"dataDir"`
|
||||||
|
DnsDomain string `yaml:"dnsDomain"`
|
||||||
|
DisableAnonymousMetrics bool `yaml:"disableAnonymousMetrics"`
|
||||||
|
DisableGeoliteUpdate bool `yaml:"disableGeoliteUpdate"`
|
||||||
|
DisableDefaultPolicy bool `yaml:"disableDefaultPolicy"`
|
||||||
|
Auth AuthConfig `yaml:"auth"`
|
||||||
|
Stuns []HostConfig `yaml:"stuns"`
|
||||||
|
Relays RelaysConfig `yaml:"relays"`
|
||||||
|
SignalURI string `yaml:"signalUri"`
|
||||||
|
Store StoreConfig `yaml:"store"`
|
||||||
|
ReverseProxy ReverseProxyConfig `yaml:"reverseProxy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthConfig contains authentication/identity provider settings
|
||||||
|
type AuthConfig struct {
|
||||||
|
Issuer string `yaml:"issuer"`
|
||||||
|
LocalAuthDisabled bool `yaml:"localAuthDisabled"`
|
||||||
|
SignKeyRefreshEnabled bool `yaml:"signKeyRefreshEnabled"`
|
||||||
|
Storage AuthStorageConfig `yaml:"storage"`
|
||||||
|
DashboardRedirectURIs []string `yaml:"dashboardRedirectURIs"`
|
||||||
|
CLIRedirectURIs []string `yaml:"cliRedirectURIs"`
|
||||||
|
Owner *AuthOwnerConfig `yaml:"owner,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthStorageConfig contains auth storage settings
|
||||||
|
type AuthStorageConfig struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
File string `yaml:"file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthOwnerConfig contains initial admin user settings
|
||||||
|
type AuthOwnerConfig struct {
|
||||||
|
Email string `yaml:"email"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HostConfig represents a STUN/TURN/Signal host
|
||||||
|
type HostConfig struct {
|
||||||
|
URI string `yaml:"uri"`
|
||||||
|
Proto string `yaml:"proto,omitempty"` // udp, dtls, tcp, http, https - defaults based on URI scheme
|
||||||
|
Username string `yaml:"username,omitempty"`
|
||||||
|
Password string `yaml:"password,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RelaysConfig contains external relay server settings for clients
|
||||||
|
type RelaysConfig struct {
|
||||||
|
Addresses []string `yaml:"addresses"`
|
||||||
|
CredentialsTTL string `yaml:"credentialsTTL"`
|
||||||
|
Secret string `yaml:"secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreConfig contains database settings
|
||||||
|
type StoreConfig struct {
|
||||||
|
Engine string `yaml:"engine"`
|
||||||
|
EncryptionKey string `yaml:"encryptionKey"`
|
||||||
|
DSN string `yaml:"dsn"` // Connection string for postgres or mysql engines
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReverseProxyConfig contains reverse proxy settings
|
||||||
|
type ReverseProxyConfig struct {
|
||||||
|
TrustedHTTPProxies []string `yaml:"trustedHTTPProxies"`
|
||||||
|
TrustedHTTPProxiesCount uint `yaml:"trustedHTTPProxiesCount"`
|
||||||
|
TrustedPeers []string `yaml:"trustedPeers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig returns a CombinedConfig with default values
|
||||||
|
func DefaultConfig() *CombinedConfig {
|
||||||
|
return &CombinedConfig{
|
||||||
|
Server: ServerConfig{
|
||||||
|
ListenAddress: ":443",
|
||||||
|
MetricsPort: 9090,
|
||||||
|
HealthcheckAddress: ":9000",
|
||||||
|
LogLevel: "info",
|
||||||
|
LogFile: "console",
|
||||||
|
StunPorts: []int{3478},
|
||||||
|
DataDir: "/var/lib/netbird/",
|
||||||
|
Auth: AuthConfig{
|
||||||
|
Storage: AuthStorageConfig{
|
||||||
|
Type: "sqlite3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Store: StoreConfig{
|
||||||
|
Engine: "sqlite",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Relay: RelayConfig{
|
||||||
|
// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults
|
||||||
|
Stun: StunConfig{
|
||||||
|
Enabled: false,
|
||||||
|
Ports: []int{3478},
|
||||||
|
// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Signal: SignalConfig{
|
||||||
|
// LogLevel inherited from Server.LogLevel via ApplySimplifiedDefaults
|
||||||
|
},
|
||||||
|
Management: ManagementConfig{
|
||||||
|
DataDir: "/var/lib/netbird/",
|
||||||
|
Auth: AuthConfig{
|
||||||
|
Storage: AuthStorageConfig{
|
||||||
|
Type: "sqlite3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Relays: RelaysConfig{
|
||||||
|
CredentialsTTL: "12h",
|
||||||
|
},
|
||||||
|
Store: StoreConfig{
|
||||||
|
Engine: "sqlite",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasRequiredSettings returns true if the configuration has the required server settings
|
||||||
|
func (c *CombinedConfig) hasRequiredSettings() bool {
|
||||||
|
return c.Server.ExposedAddress != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseExposedAddress extracts protocol, host, and host:port from the exposed address
|
||||||
|
// Input format: "https://example.com:443" or "http://example.com:8080" or "example.com:443"
|
||||||
|
// Returns: protocol ("https" or "http"), hostname only, and host:port
|
||||||
|
func parseExposedAddress(exposedAddress string) (protocol, hostname, hostPort string) {
|
||||||
|
// Default to https if no protocol specified
|
||||||
|
protocol = "https"
|
||||||
|
hostPort = exposedAddress
|
||||||
|
|
||||||
|
// Check for protocol prefix
|
||||||
|
if strings.HasPrefix(exposedAddress, "https://") {
|
||||||
|
protocol = "https"
|
||||||
|
hostPort = strings.TrimPrefix(exposedAddress, "https://")
|
||||||
|
} else if strings.HasPrefix(exposedAddress, "http://") {
|
||||||
|
protocol = "http"
|
||||||
|
hostPort = strings.TrimPrefix(exposedAddress, "http://")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract hostname (without port)
|
||||||
|
hostname = hostPort
|
||||||
|
if host, _, err := net.SplitHostPort(hostPort); err == nil {
|
||||||
|
hostname = host
|
||||||
|
}
|
||||||
|
|
||||||
|
return protocol, hostname, hostPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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).
|
||||||
|
func (c *CombinedConfig) ApplySimplifiedDefaults() {
|
||||||
|
if !c.hasRequiredSettings() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse exposed address to extract protocol and hostname
|
||||||
|
exposedProto, exposedHost, exposedHostPort := parseExposedAddress(c.Server.ExposedAddress)
|
||||||
|
|
||||||
|
// Check for external service overrides
|
||||||
|
hasExternalRelay := len(c.Server.Relays.Addresses) > 0
|
||||||
|
hasExternalSignal := c.Server.SignalURI != ""
|
||||||
|
hasExternalStuns := len(c.Server.Stuns) > 0
|
||||||
|
|
||||||
|
// Default stunPorts to [3478] if not specified and no external STUN
|
||||||
|
if len(c.Server.StunPorts) == 0 && !hasExternalStuns {
|
||||||
|
c.Server.StunPorts = []int{3478}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.applyRelayDefaults(exposedProto, exposedHostPort, hasExternalRelay, hasExternalStuns)
|
||||||
|
c.applySignalDefaults(hasExternalSignal)
|
||||||
|
c.applyManagementDefaults(exposedHost)
|
||||||
|
|
||||||
|
// Auto-configure client settings (stuns, relays, signalUri)
|
||||||
|
c.autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort, hasExternalStuns, hasExternalRelay, hasExternalSignal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyRelayDefaults configures the relay service if no external relay is configured.
|
||||||
|
func (c *CombinedConfig) applyRelayDefaults(exposedProto, exposedHostPort string, hasExternalRelay, hasExternalStuns bool) {
|
||||||
|
if hasExternalRelay {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Relay.Enabled = true
|
||||||
|
relayProto := "rel"
|
||||||
|
if exposedProto == "https" {
|
||||||
|
relayProto = "rels"
|
||||||
|
}
|
||||||
|
c.Relay.ExposedAddress = fmt.Sprintf("%s://%s", relayProto, exposedHostPort)
|
||||||
|
c.Relay.AuthSecret = c.Server.AuthSecret
|
||||||
|
if c.Relay.LogLevel == "" {
|
||||||
|
c.Relay.LogLevel = c.Server.LogLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable local STUN only if no external STUN servers and stunPorts are configured
|
||||||
|
if !hasExternalStuns && len(c.Server.StunPorts) > 0 {
|
||||||
|
c.Relay.Stun.Enabled = true
|
||||||
|
c.Relay.Stun.Ports = c.Server.StunPorts
|
||||||
|
if c.Relay.Stun.LogLevel == "" {
|
||||||
|
c.Relay.Stun.LogLevel = c.Server.LogLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applySignalDefaults configures the signal service if no external signal is configured.
|
||||||
|
func (c *CombinedConfig) applySignalDefaults(hasExternalSignal bool) {
|
||||||
|
if hasExternalSignal {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Signal.Enabled = true
|
||||||
|
if c.Signal.LogLevel == "" {
|
||||||
|
c.Signal.LogLevel = c.Server.LogLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyManagementDefaults configures the management service (always enabled).
|
||||||
|
func (c *CombinedConfig) applyManagementDefaults(exposedHost string) {
|
||||||
|
c.Management.Enabled = true
|
||||||
|
if c.Management.LogLevel == "" {
|
||||||
|
c.Management.LogLevel = c.Server.LogLevel
|
||||||
|
}
|
||||||
|
if c.Management.DataDir == "" || c.Management.DataDir == "/var/lib/netbird/" {
|
||||||
|
c.Management.DataDir = c.Server.DataDir
|
||||||
|
}
|
||||||
|
c.Management.DnsDomain = exposedHost
|
||||||
|
c.Management.DisableAnonymousMetrics = c.Server.DisableAnonymousMetrics
|
||||||
|
c.Management.DisableGeoliteUpdate = c.Server.DisableGeoliteUpdate
|
||||||
|
// Copy auth config from server if management auth issuer is not set
|
||||||
|
if c.Management.Auth.Issuer == "" && c.Server.Auth.Issuer != "" {
|
||||||
|
c.Management.Auth = c.Server.Auth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy store config from server if not set
|
||||||
|
if c.Management.Store.Engine == "" || c.Management.Store.Engine == "sqlite" {
|
||||||
|
if c.Server.Store.Engine != "" {
|
||||||
|
c.Management.Store = c.Server.Store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy reverse proxy config from server
|
||||||
|
if len(c.Server.ReverseProxy.TrustedHTTPProxies) > 0 || c.Server.ReverseProxy.TrustedHTTPProxiesCount > 0 || len(c.Server.ReverseProxy.TrustedPeers) > 0 {
|
||||||
|
c.Management.ReverseProxy = c.Server.ReverseProxy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// autoConfigureClientSettings sets up STUN/relay/signal URIs for clients
|
||||||
|
// External overrides from server config take precedence over auto-generated values
|
||||||
|
func (c *CombinedConfig) autoConfigureClientSettings(exposedProto, exposedHost, exposedHostPort string, hasExternalStuns, hasExternalRelay, hasExternalSignal bool) {
|
||||||
|
// Determine relay protocol from exposed protocol
|
||||||
|
relayProto := "rel"
|
||||||
|
if exposedProto == "https" {
|
||||||
|
relayProto = "rels"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure STUN servers for clients
|
||||||
|
if hasExternalStuns {
|
||||||
|
// Use external STUN servers from server config
|
||||||
|
c.Management.Stuns = c.Server.Stuns
|
||||||
|
} else if len(c.Server.StunPorts) > 0 && len(c.Management.Stuns) == 0 {
|
||||||
|
// Auto-configure local STUN servers for all ports
|
||||||
|
for _, port := range c.Server.StunPorts {
|
||||||
|
c.Management.Stuns = append(c.Management.Stuns, HostConfig{
|
||||||
|
URI: fmt.Sprintf("stun:%s:%d", exposedHost, port),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure relay for clients
|
||||||
|
if hasExternalRelay {
|
||||||
|
// 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.Management.Relays.Secret == "" {
|
||||||
|
c.Management.Relays.Secret = c.Server.AuthSecret
|
||||||
|
}
|
||||||
|
if c.Management.Relays.CredentialsTTL == "" {
|
||||||
|
c.Management.Relays.CredentialsTTL = "12h"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure signal for clients
|
||||||
|
if hasExternalSignal {
|
||||||
|
// Use external signal URI from server config
|
||||||
|
c.Management.SignalURI = c.Server.SignalURI
|
||||||
|
} else if c.Management.SignalURI == "" {
|
||||||
|
// Auto-configure local signal
|
||||||
|
c.Management.SignalURI = fmt.Sprintf("%s://%s", exposedProto, exposedHostPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig loads configuration from a YAML file
|
||||||
|
func LoadConfig(configPath string) (*CombinedConfig, error) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
|
||||||
|
if configPath == "" {
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate internal configs from server settings
|
||||||
|
cfg.ApplySimplifiedDefaults()
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the configuration
|
||||||
|
func (c *CombinedConfig) Validate() error {
|
||||||
|
if c.Server.ExposedAddress == "" {
|
||||||
|
return fmt.Errorf("server.exposedAddress is required")
|
||||||
|
}
|
||||||
|
if c.Server.DataDir == "" {
|
||||||
|
return fmt.Errorf("server.dataDir is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate STUN ports
|
||||||
|
seen := make(map[int]bool)
|
||||||
|
for _, port := range c.Server.StunPorts {
|
||||||
|
if port <= 0 || port > 65535 {
|
||||||
|
return fmt.Errorf("invalid server.stunPorts value %d: must be between 1 and 65535", port)
|
||||||
|
}
|
||||||
|
if seen[port] {
|
||||||
|
return fmt.Errorf("duplicate STUN port %d in server.stunPorts", port)
|
||||||
|
}
|
||||||
|
seen[port] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// authSecret is required only if running local relay (no external relay configured)
|
||||||
|
hasExternalRelay := len(c.Server.Relays.Addresses) > 0
|
||||||
|
if !hasExternalRelay && c.Server.AuthSecret == "" {
|
||||||
|
return fmt.Errorf("server.authSecret is required when running local relay")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasTLSCert returns true if TLS certificate files are configured
|
||||||
|
func (c *CombinedConfig) HasTLSCert() bool {
|
||||||
|
return c.Server.TLS.CertFile != "" && c.Server.TLS.KeyFile != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasLetsEncrypt returns true if Let's Encrypt is configured
|
||||||
|
func (c *CombinedConfig) HasLetsEncrypt() bool {
|
||||||
|
return c.Server.TLS.LetsEncrypt.Enabled &&
|
||||||
|
c.Server.TLS.LetsEncrypt.DataDir != "" &&
|
||||||
|
len(c.Server.TLS.LetsEncrypt.Domains) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseExplicitProtocol parses an explicit protocol string to nbconfig.Protocol
|
||||||
|
func parseExplicitProtocol(proto string) (nbconfig.Protocol, bool) {
|
||||||
|
switch strings.ToLower(proto) {
|
||||||
|
case "udp":
|
||||||
|
return nbconfig.UDP, true
|
||||||
|
case "dtls":
|
||||||
|
return nbconfig.DTLS, true
|
||||||
|
case "tcp":
|
||||||
|
return nbconfig.TCP, true
|
||||||
|
case "http":
|
||||||
|
return nbconfig.HTTP, true
|
||||||
|
case "https":
|
||||||
|
return nbconfig.HTTPS, true
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseStunProtocol determines protocol for STUN/TURN servers.
|
||||||
|
// stun: → UDP, stuns: → DTLS, turn: → UDP, turns: → DTLS
|
||||||
|
// Explicit proto overrides URI scheme. Defaults to UDP.
|
||||||
|
func parseStunProtocol(uri, proto string) nbconfig.Protocol {
|
||||||
|
if proto != "" {
|
||||||
|
if p, ok := parseExplicitProtocol(proto); ok {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = strings.ToLower(uri)
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(uri, "stuns:"):
|
||||||
|
return nbconfig.DTLS
|
||||||
|
case strings.HasPrefix(uri, "turns:"):
|
||||||
|
return nbconfig.DTLS
|
||||||
|
default:
|
||||||
|
// stun:, turn:, or no scheme - default to UDP
|
||||||
|
return nbconfig.UDP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSignalProtocol determines protocol for Signal servers.
|
||||||
|
// https:// → HTTPS, http:// → HTTP. Defaults to HTTPS.
|
||||||
|
func parseSignalProtocol(uri string) nbconfig.Protocol {
|
||||||
|
uri = strings.ToLower(uri)
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(uri, "http://"):
|
||||||
|
return nbconfig.HTTP
|
||||||
|
default:
|
||||||
|
// https:// or no scheme - default to HTTPS
|
||||||
|
return nbconfig.HTTPS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stripSignalProtocol removes the protocol prefix from a signal URI.
|
||||||
|
// Returns just the host:port (e.g., "selfhosted2.demo.netbird.io:443").
|
||||||
|
func stripSignalProtocol(uri string) string {
|
||||||
|
uri = strings.TrimPrefix(uri, "https://")
|
||||||
|
uri = strings.TrimPrefix(uri, "http://")
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToManagementConfig converts CombinedConfig to management server config
|
||||||
|
func (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {
|
||||||
|
mgmt := c.Management
|
||||||
|
|
||||||
|
// Build STUN hosts
|
||||||
|
var stuns []*nbconfig.Host
|
||||||
|
for _, s := range mgmt.Stuns {
|
||||||
|
stuns = append(stuns, &nbconfig.Host{
|
||||||
|
URI: s.URI,
|
||||||
|
Proto: parseStunProtocol(s.URI, s.Proto),
|
||||||
|
Username: s.Username,
|
||||||
|
Password: s.Password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build relay config
|
||||||
|
var relayConfig *nbconfig.Relay
|
||||||
|
if len(mgmt.Relays.Addresses) > 0 || mgmt.Relays.Secret != "" {
|
||||||
|
var ttl time.Duration
|
||||||
|
if mgmt.Relays.CredentialsTTL != "" {
|
||||||
|
var err error
|
||||||
|
ttl, err = time.ParseDuration(mgmt.Relays.CredentialsTTL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid relay credentials TTL %q: %w", mgmt.Relays.CredentialsTTL, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
relayConfig = &nbconfig.Relay{
|
||||||
|
Addresses: mgmt.Relays.Addresses,
|
||||||
|
CredentialsTTL: util.Duration{Duration: ttl},
|
||||||
|
Secret: mgmt.Relays.Secret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build signal config
|
||||||
|
var signalConfig *nbconfig.Host
|
||||||
|
if mgmt.SignalURI != "" {
|
||||||
|
signalConfig = &nbconfig.Host{
|
||||||
|
URI: stripSignalProtocol(mgmt.SignalURI),
|
||||||
|
Proto: parseSignalProtocol(mgmt.SignalURI),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build store config
|
||||||
|
storeConfig := nbconfig.StoreConfig{
|
||||||
|
Engine: types.Engine(mgmt.Store.Engine),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build reverse proxy config
|
||||||
|
reverseProxy := nbconfig.ReverseProxy{
|
||||||
|
TrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,
|
||||||
|
}
|
||||||
|
for _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {
|
||||||
|
if prefix, err := netip.ParsePrefix(p); err == nil {
|
||||||
|
reverseProxy.TrustedHTTPProxies = append(reverseProxy.TrustedHTTPProxies, prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, p := range mgmt.ReverseProxy.TrustedPeers {
|
||||||
|
if prefix, err := netip.ParsePrefix(p); err == nil {
|
||||||
|
reverseProxy.TrustedPeers = append(reverseProxy.TrustedPeers, prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build HTTP config (required, even if empty)
|
||||||
|
httpConfig := &nbconfig.HttpServerConfig{}
|
||||||
|
|
||||||
|
// Build embedded IDP config (always enabled in combined server)
|
||||||
|
storageFile := mgmt.Auth.Storage.File
|
||||||
|
if storageFile == "" {
|
||||||
|
storageFile = path.Join(mgmt.DataDir, "idp.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
embeddedIdP := &idp.EmbeddedIdPConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Issuer: mgmt.Auth.Issuer,
|
||||||
|
LocalAuthDisabled: mgmt.Auth.LocalAuthDisabled,
|
||||||
|
SignKeyRefreshEnabled: mgmt.Auth.SignKeyRefreshEnabled,
|
||||||
|
Storage: idp.EmbeddedStorageConfig{
|
||||||
|
Type: mgmt.Auth.Storage.Type,
|
||||||
|
Config: idp.EmbeddedStorageTypeConfig{
|
||||||
|
File: storageFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DashboardRedirectURIs: mgmt.Auth.DashboardRedirectURIs,
|
||||||
|
CLIRedirectURIs: mgmt.Auth.CLIRedirectURIs,
|
||||||
|
}
|
||||||
|
|
||||||
|
if mgmt.Auth.Owner != nil && mgmt.Auth.Owner.Email != "" {
|
||||||
|
embeddedIdP.Owner = &idp.OwnerConfig{
|
||||||
|
Email: mgmt.Auth.Owner.Email,
|
||||||
|
Hash: mgmt.Auth.Owner.Password, // Will be hashed if plain text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set HTTP config fields for embedded IDP
|
||||||
|
httpConfig.AuthIssuer = mgmt.Auth.Issuer
|
||||||
|
httpConfig.IdpSignKeyRefreshEnabled = mgmt.Auth.SignKeyRefreshEnabled
|
||||||
|
|
||||||
|
return &nbconfig.Config{
|
||||||
|
Stuns: stuns,
|
||||||
|
Relay: relayConfig,
|
||||||
|
Signal: signalConfig,
|
||||||
|
Datadir: mgmt.DataDir,
|
||||||
|
DataStoreEncryptionKey: mgmt.Store.EncryptionKey,
|
||||||
|
HttpConfig: httpConfig,
|
||||||
|
StoreConfig: storeConfig,
|
||||||
|
ReverseProxy: reverseProxy,
|
||||||
|
DisableDefaultPolicy: mgmt.DisableDefaultPolicy,
|
||||||
|
EmbeddedIdP: embeddedIdP,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyEmbeddedIdPConfig applies embedded IdP configuration to the management config.
|
||||||
|
// This mirrors the logic in management/cmd/management.go ApplyEmbeddedIdPConfig.
|
||||||
|
func ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config, mgmtPort int, disableSingleAccMode bool) error {
|
||||||
|
if cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embedded IdP requires single account mode
|
||||||
|
if disableSingleAccMode {
|
||||||
|
return fmt.Errorf("embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set LocalAddress for embedded IdP, used for internal JWT validation
|
||||||
|
cfg.EmbeddedIdP.LocalAddress = fmt.Sprintf("localhost:%d", mgmtPort)
|
||||||
|
|
||||||
|
// Set storage defaults based on Datadir
|
||||||
|
if cfg.EmbeddedIdP.Storage.Type == "" {
|
||||||
|
cfg.EmbeddedIdP.Storage.Type = "sqlite3"
|
||||||
|
}
|
||||||
|
if cfg.EmbeddedIdP.Storage.Config.File == "" && cfg.Datadir != "" {
|
||||||
|
cfg.EmbeddedIdP.Storage.Config.File = path.Join(cfg.Datadir, "idp.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
issuer := cfg.EmbeddedIdP.Issuer
|
||||||
|
|
||||||
|
// Ensure HttpConfig exists
|
||||||
|
if cfg.HttpConfig == nil {
|
||||||
|
cfg.HttpConfig = &nbconfig.HttpServerConfig{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set HttpConfig values from EmbeddedIdP
|
||||||
|
cfg.HttpConfig.AuthIssuer = issuer
|
||||||
|
cfg.HttpConfig.AuthAudience = "netbird-dashboard"
|
||||||
|
cfg.HttpConfig.CLIAuthAudience = "netbird-cli"
|
||||||
|
cfg.HttpConfig.AuthUserIDClaim = "sub"
|
||||||
|
cfg.HttpConfig.AuthKeysLocation = issuer + "/keys"
|
||||||
|
cfg.HttpConfig.OIDCConfigEndpoint = issuer + "/.well-known/openid-configuration"
|
||||||
|
cfg.HttpConfig.IdpSignKeyRefreshEnabled = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureEncryptionKey generates an encryption key if not set.
|
||||||
|
// Unlike management server, we don't write back to the config file.
|
||||||
|
func EnsureEncryptionKey(ctx context.Context, cfg *nbconfig.Config) error {
|
||||||
|
if cfg.DataStoreEncryptionKey != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithContext(ctx).Infof("DataStoreEncryptionKey is not set, generating a new key")
|
||||||
|
key, err := crypt.GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate datastore encryption key: %v", err)
|
||||||
|
}
|
||||||
|
cfg.DataStoreEncryptionKey = key
|
||||||
|
keyPreview := key[:8] + "..."
|
||||||
|
log.WithContext(ctx).Warnf("DataStoreEncryptionKey generated (%s); add it to your config file under 'server.store.encryptionKey' to persist across restarts", keyPreview)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogConfigInfo logs informational messages about the loaded configuration
|
||||||
|
func LogConfigInfo(cfg *nbconfig.Config) {
|
||||||
|
if cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {
|
||||||
|
log.Infof("running with the embedded IdP: %v", cfg.EmbeddedIdP.Issuer)
|
||||||
|
}
|
||||||
|
if cfg.Relay != nil {
|
||||||
|
log.Infof("Relay addresses: %v", cfg.Relay.Addresses)
|
||||||
|
}
|
||||||
|
}
|
||||||
33
combined/cmd/pprof.go
Normal file
33
combined/cmd/pprof.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
//go:build pprof
|
||||||
|
// +build pprof
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
_ "net/http/pprof"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
addr := pprofAddr()
|
||||||
|
go pprof(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pprofAddr() string {
|
||||||
|
listenAddr := os.Getenv("NB_PPROF_ADDR")
|
||||||
|
if listenAddr == "" {
|
||||||
|
return "localhost:6969"
|
||||||
|
}
|
||||||
|
|
||||||
|
return listenAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func pprof(listenAddr string) {
|
||||||
|
log.Infof("listening pprof on: %s\n", listenAddr)
|
||||||
|
if err := http.ListenAndServe(listenAddr, nil); err != nil {
|
||||||
|
log.Fatalf("Failed to start pprof: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
711
combined/cmd/root.go
Normal file
711
combined/cmd/root.go
Normal file
@@ -0,0 +1,711 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coder/websocket"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/encryption"
|
||||||
|
mgmtServer "github.com/netbirdio/netbird/management/internals/server"
|
||||||
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
|
"github.com/netbirdio/netbird/relay/healthcheck"
|
||||||
|
relayServer "github.com/netbirdio/netbird/relay/server"
|
||||||
|
"github.com/netbirdio/netbird/relay/server/listener/ws"
|
||||||
|
sharedMetrics "github.com/netbirdio/netbird/shared/metrics"
|
||||||
|
"github.com/netbirdio/netbird/shared/relay/auth"
|
||||||
|
"github.com/netbirdio/netbird/shared/signal/proto"
|
||||||
|
signalServer "github.com/netbirdio/netbird/signal/server"
|
||||||
|
"github.com/netbirdio/netbird/stun"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
"github.com/netbirdio/netbird/util/wsproxy"
|
||||||
|
wsproxyserver "github.com/netbirdio/netbird/util/wsproxy/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configPath string
|
||||||
|
config *CombinedConfig
|
||||||
|
|
||||||
|
rootCmd = &cobra.Command{
|
||||||
|
Use: "combined",
|
||||||
|
Short: "Combined Netbird server (Management + Signal + Relay + STUN)",
|
||||||
|
Long: `Combined Netbird server for self-hosted deployments.
|
||||||
|
|
||||||
|
All services (Management, Signal, Relay) are multiplexed on a single port.
|
||||||
|
Optional STUN server runs on separate UDP ports.
|
||||||
|
|
||||||
|
Configuration is loaded from a YAML file specified with --config.`,
|
||||||
|
SilenceUsage: true,
|
||||||
|
SilenceErrors: true,
|
||||||
|
RunE: execute,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to YAML configuration file (required)")
|
||||||
|
_ = rootCmd.MarkPersistentFlagRequired("config")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute() error {
|
||||||
|
return rootCmd.Execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForExitSignal() {
|
||||||
|
osSigs := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(osSigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-osSigs
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(cmd *cobra.Command, _ []string) error {
|
||||||
|
if err := initializeConfig(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Management is required as the base server when signal or relay are enabled
|
||||||
|
if (config.Signal.Enabled || config.Relay.Enabled) && !config.Management.Enabled {
|
||||||
|
return fmt.Errorf("management must be enabled when signal or relay are enabled (provides the base HTTP server)")
|
||||||
|
}
|
||||||
|
|
||||||
|
servers, err := createAllServers(cmd.Context(), config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register services with management's gRPC server using AfterInit hook
|
||||||
|
setupServerHooks(servers, config)
|
||||||
|
|
||||||
|
// Start management server (this also starts the HTTP listener)
|
||||||
|
if servers.mgmtSrv != nil {
|
||||||
|
if err := servers.mgmtSrv.Start(cmd.Context()); err != nil {
|
||||||
|
cleanupSTUNListeners(servers.stunListeners)
|
||||||
|
return fmt.Errorf("failed to start management server: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start all other servers
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
startServers(&wg, servers.relaySrv, servers.healthcheck, servers.stunServer, servers.metricsServer)
|
||||||
|
|
||||||
|
waitForExitSignal()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = shutdownServers(ctx, servers.relaySrv, servers.healthcheck, servers.stunServer, servers.mgmtSrv, servers.metricsServer)
|
||||||
|
wg.Wait()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// initializeConfig loads and validates the configuration, then initializes logging.
|
||||||
|
func initializeConfig() error {
|
||||||
|
var err error
|
||||||
|
config, err = LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := util.InitLog(config.Server.LogLevel, config.Server.LogFile); err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize log: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dsn := config.Server.Store.DSN; dsn != "" {
|
||||||
|
switch strings.ToLower(config.Server.Store.Engine) {
|
||||||
|
case "postgres":
|
||||||
|
os.Setenv("NB_STORE_ENGINE_POSTGRES_DSN", dsn)
|
||||||
|
case "mysql":
|
||||||
|
os.Setenv("NB_STORE_ENGINE_MYSQL_DSN", dsn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Starting combined NetBird server")
|
||||||
|
logConfig(config)
|
||||||
|
logEnvVars()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverInstances holds all server instances created during startup.
|
||||||
|
type serverInstances struct {
|
||||||
|
relaySrv *relayServer.Server
|
||||||
|
mgmtSrv *mgmtServer.BaseServer
|
||||||
|
signalSrv *signalServer.Server
|
||||||
|
healthcheck *healthcheck.Server
|
||||||
|
stunServer *stun.Server
|
||||||
|
stunListeners []*net.UDPConn
|
||||||
|
metricsServer *sharedMetrics.Metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
// createAllServers creates all server instances based on configuration.
|
||||||
|
func createAllServers(ctx context.Context, cfg *CombinedConfig) (*serverInstances, error) {
|
||||||
|
metricsServer, err := sharedMetrics.NewServer(cfg.Server.MetricsPort, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create metrics server: %w", err)
|
||||||
|
}
|
||||||
|
servers := &serverInstances{
|
||||||
|
metricsServer: metricsServer,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, tlsSupport, err := handleTLSConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to setup TLS config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := servers.createRelayServer(cfg, tlsSupport); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := servers.createManagementServer(ctx, cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := servers.createSignalServer(ctx, cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := servers.createHealthcheckServer(cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverInstances) createRelayServer(cfg *CombinedConfig, tlsSupport bool) error {
|
||||||
|
if !cfg.Relay.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
s.stunListeners, err = createSTUNListeners(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedSecret := sha256.Sum256([]byte(cfg.Relay.AuthSecret))
|
||||||
|
authenticator := auth.NewTimedHMACValidator(hashedSecret[:], 24*time.Hour)
|
||||||
|
|
||||||
|
relayCfg := relayServer.Config{
|
||||||
|
Meter: s.metricsServer.Meter,
|
||||||
|
ExposedAddress: cfg.Relay.ExposedAddress,
|
||||||
|
AuthValidator: authenticator,
|
||||||
|
TLSSupport: tlsSupport,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.relaySrv, err = createRelayServer(relayCfg, s.stunListeners)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Relay server created")
|
||||||
|
|
||||||
|
if len(s.stunListeners) > 0 {
|
||||||
|
s.stunServer = stun.NewServer(s.stunListeners, cfg.Relay.Stun.LogLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverInstances) createManagementServer(ctx context.Context, cfg *CombinedConfig) error {
|
||||||
|
if !cfg.Management.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mgmtConfig, err := cfg.ToManagementConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create management config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, portStr, portErr := net.SplitHostPort(cfg.Server.ListenAddress)
|
||||||
|
if portErr != nil {
|
||||||
|
portStr = "443"
|
||||||
|
}
|
||||||
|
mgmtPort, _ := strconv.Atoi(portStr)
|
||||||
|
|
||||||
|
if err := ApplyEmbeddedIdPConfig(ctx, mgmtConfig, mgmtPort, false); err != nil {
|
||||||
|
cleanupSTUNListeners(s.stunListeners)
|
||||||
|
return fmt.Errorf("failed to apply embedded IdP config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := EnsureEncryptionKey(ctx, mgmtConfig); err != nil {
|
||||||
|
cleanupSTUNListeners(s.stunListeners)
|
||||||
|
return fmt.Errorf("failed to ensure encryption key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
LogConfigInfo(mgmtConfig)
|
||||||
|
|
||||||
|
s.mgmtSrv, err = createManagementServer(cfg, mgmtConfig)
|
||||||
|
if err != nil {
|
||||||
|
cleanupSTUNListeners(s.stunListeners)
|
||||||
|
return fmt.Errorf("failed to create management server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject externally-managed AppMetrics so management uses the shared metrics server
|
||||||
|
appMetrics, err := telemetry.NewAppMetricsWithMeter(ctx, s.metricsServer.Meter)
|
||||||
|
if err != nil {
|
||||||
|
cleanupSTUNListeners(s.stunListeners)
|
||||||
|
return fmt.Errorf("failed to create management app metrics: %w", err)
|
||||||
|
}
|
||||||
|
mgmtServer.Inject[telemetry.AppMetrics](s.mgmtSrv, appMetrics)
|
||||||
|
|
||||||
|
log.Infof("Management server created")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverInstances) createSignalServer(ctx context.Context, cfg *CombinedConfig) error {
|
||||||
|
if !cfg.Signal.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
s.signalSrv, err = signalServer.NewServer(ctx, s.metricsServer.Meter, "signal_")
|
||||||
|
if err != nil {
|
||||||
|
cleanupSTUNListeners(s.stunListeners)
|
||||||
|
return fmt.Errorf("failed to create signal server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Signal server created")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverInstances) createHealthcheckServer(cfg *CombinedConfig) error {
|
||||||
|
hCfg := healthcheck.Config{
|
||||||
|
ListenAddress: cfg.Server.HealthcheckAddress,
|
||||||
|
ServiceChecker: s.relaySrv,
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
s.healthcheck, err = createHealthCheck(hCfg, s.stunListeners)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupServerHooks registers services with management's gRPC server.
|
||||||
|
func setupServerHooks(servers *serverInstances, cfg *CombinedConfig) {
|
||||||
|
if servers.mgmtSrv == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
servers.mgmtSrv.AfterInit(func(s *mgmtServer.BaseServer) {
|
||||||
|
grpcSrv := s.GRPCServer()
|
||||||
|
|
||||||
|
if servers.signalSrv != nil {
|
||||||
|
proto.RegisterSignalExchangeServer(grpcSrv, servers.signalSrv)
|
||||||
|
log.Infof("Signal server registered on port %s", cfg.Server.ListenAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.SetHandlerFunc(createCombinedHandler(grpcSrv, s.APIHandler(), servers.relaySrv, servers.metricsServer.Meter, cfg))
|
||||||
|
if servers.relaySrv != nil {
|
||||||
|
log.Infof("Relay WebSocket handler added (path: /relay)")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func startServers(wg *sync.WaitGroup, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, metricsServer *sharedMetrics.Metrics) {
|
||||||
|
if srv != nil {
|
||||||
|
instanceURL := srv.InstanceURL()
|
||||||
|
log.Infof("Relay server instance URL: %s", instanceURL.String())
|
||||||
|
log.Infof("Relay WebSocket multiplexed on management port (no separate relay listener)")
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
log.Infof("running metrics server: %s%s", metricsServer.Addr, metricsServer.Endpoint)
|
||||||
|
if err := metricsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Fatalf("failed to start metrics server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := httpHealthcheck.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Fatalf("failed to start healthcheck server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if stunServer != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := stunServer.Listen(); err != nil {
|
||||||
|
if errors.Is(err, stun.ErrServerClosed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Errorf("STUN server error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shutdownServers(ctx context.Context, srv *relayServer.Server, httpHealthcheck *healthcheck.Server, stunServer *stun.Server, mgmtSrv *mgmtServer.BaseServer, metricsServer *sharedMetrics.Metrics) error {
|
||||||
|
var errs error
|
||||||
|
|
||||||
|
if err := httpHealthcheck.Shutdown(ctx); err != nil {
|
||||||
|
errs = multierror.Append(errs, fmt.Errorf("failed to close healthcheck server: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if stunServer != nil {
|
||||||
|
if err := stunServer.Shutdown(); err != nil {
|
||||||
|
errs = multierror.Append(errs, fmt.Errorf("failed to close STUN server: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if srv != nil {
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
errs = multierror.Append(errs, fmt.Errorf("failed to close relay server: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mgmtSrv != nil {
|
||||||
|
log.Infof("shutting down management and signal servers")
|
||||||
|
if err := mgmtSrv.Stop(); err != nil {
|
||||||
|
errs = multierror.Append(errs, fmt.Errorf("failed to close management server: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if metricsServer != nil {
|
||||||
|
log.Infof("shutting down metrics server")
|
||||||
|
if err := metricsServer.Shutdown(ctx); err != nil {
|
||||||
|
errs = multierror.Append(errs, fmt.Errorf("failed to close metrics server: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func createHealthCheck(hCfg healthcheck.Config, stunListeners []*net.UDPConn) (*healthcheck.Server, error) {
|
||||||
|
httpHealthcheck, err := healthcheck.NewServer(hCfg)
|
||||||
|
if err != nil {
|
||||||
|
cleanupSTUNListeners(stunListeners)
|
||||||
|
return nil, fmt.Errorf("failed to create healthcheck server: %w", err)
|
||||||
|
}
|
||||||
|
return httpHealthcheck, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRelayServer(cfg relayServer.Config, stunListeners []*net.UDPConn) (*relayServer.Server, error) {
|
||||||
|
srv, err := relayServer.NewServer(cfg)
|
||||||
|
if err != nil {
|
||||||
|
cleanupSTUNListeners(stunListeners)
|
||||||
|
return nil, fmt.Errorf("failed to create relay server: %w", err)
|
||||||
|
}
|
||||||
|
return srv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupSTUNListeners(stunListeners []*net.UDPConn) {
|
||||||
|
for _, l := range stunListeners {
|
||||||
|
_ = l.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSTUNListeners(cfg *CombinedConfig) ([]*net.UDPConn, error) {
|
||||||
|
var stunListeners []*net.UDPConn
|
||||||
|
if cfg.Relay.Stun.Enabled {
|
||||||
|
for _, port := range cfg.Relay.Stun.Ports {
|
||||||
|
listener, err := net.ListenUDP("udp", &net.UDPAddr{Port: port})
|
||||||
|
if err != nil {
|
||||||
|
cleanupSTUNListeners(stunListeners)
|
||||||
|
return nil, fmt.Errorf("failed to create STUN listener on port %d: %w", port, err)
|
||||||
|
}
|
||||||
|
stunListeners = append(stunListeners, listener)
|
||||||
|
log.Infof("STUN server listening on UDP port %d", port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stunListeners, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTLSConfig(cfg *CombinedConfig) (*tls.Config, bool, error) {
|
||||||
|
tlsCfg := cfg.Server.TLS
|
||||||
|
|
||||||
|
if tlsCfg.LetsEncrypt.AWSRoute53 {
|
||||||
|
log.Debugf("using Let's Encrypt DNS resolver with Route 53 support")
|
||||||
|
r53 := encryption.Route53TLS{
|
||||||
|
DataDir: tlsCfg.LetsEncrypt.DataDir,
|
||||||
|
Email: tlsCfg.LetsEncrypt.Email,
|
||||||
|
Domains: tlsCfg.LetsEncrypt.Domains,
|
||||||
|
}
|
||||||
|
tc, err := r53.GetCertificate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return tc, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.HasLetsEncrypt() {
|
||||||
|
log.Infof("setting up TLS with Let's Encrypt")
|
||||||
|
certManager, err := encryption.CreateCertManager(tlsCfg.LetsEncrypt.DataDir, tlsCfg.LetsEncrypt.Domains...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("failed creating LetsEncrypt cert manager: %w", err)
|
||||||
|
}
|
||||||
|
return certManager.TLSConfig(), true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.HasTLSCert() {
|
||||||
|
log.Debugf("using file based TLS config")
|
||||||
|
tc, err := encryption.LoadTLSConfig(tlsCfg.CertFile, tlsCfg.KeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return tc, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (*mgmtServer.BaseServer, error) {
|
||||||
|
mgmt := cfg.Management
|
||||||
|
|
||||||
|
dnsDomain := mgmt.DnsDomain
|
||||||
|
singleAccModeDomain := dnsDomain
|
||||||
|
|
||||||
|
// Extract port from listen address
|
||||||
|
_, portStr, err := net.SplitHostPort(cfg.Server.ListenAddress)
|
||||||
|
if err != nil {
|
||||||
|
// If no port specified, assume default
|
||||||
|
portStr = "443"
|
||||||
|
}
|
||||||
|
mgmtPort, _ := strconv.Atoi(portStr)
|
||||||
|
|
||||||
|
mgmtSrv := mgmtServer.NewServer(
|
||||||
|
mgmtConfig,
|
||||||
|
dnsDomain,
|
||||||
|
singleAccModeDomain,
|
||||||
|
mgmtPort,
|
||||||
|
cfg.Server.MetricsPort,
|
||||||
|
mgmt.DisableAnonymousMetrics,
|
||||||
|
mgmt.DisableGeoliteUpdate,
|
||||||
|
// Always enable user deletion from IDP in combined server (embedded IdP is always enabled)
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
|
return mgmtSrv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createCombinedHandler creates an HTTP handler that multiplexes Management, Signal (via wsproxy), and Relay WebSocket traffic
|
||||||
|
func createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, relaySrv *relayServer.Server, meter metric.Meter, cfg *CombinedConfig) http.Handler {
|
||||||
|
wsProxy := wsproxyserver.New(grpcServer, wsproxyserver.WithOTelMeter(meter))
|
||||||
|
|
||||||
|
var relayAcceptFn func(conn net.Conn)
|
||||||
|
if relaySrv != nil {
|
||||||
|
relayAcceptFn = relaySrv.RelayAccept()
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
// Native gRPC traffic (HTTP/2 with gRPC content-type)
|
||||||
|
case r.ProtoMajor == 2 && (strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") ||
|
||||||
|
strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc+proto")):
|
||||||
|
grpcServer.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
// WebSocket proxy for Management gRPC
|
||||||
|
case r.URL.Path == wsproxy.ProxyPath+wsproxy.ManagementComponent:
|
||||||
|
wsProxy.Handler().ServeHTTP(w, r)
|
||||||
|
|
||||||
|
// WebSocket proxy for Signal gRPC
|
||||||
|
case r.URL.Path == wsproxy.ProxyPath+wsproxy.SignalComponent:
|
||||||
|
if cfg.Signal.Enabled {
|
||||||
|
wsProxy.Handler().ServeHTTP(w, r)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Signal service not enabled", http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relay WebSocket
|
||||||
|
case r.URL.Path == "/relay":
|
||||||
|
if relayAcceptFn != nil {
|
||||||
|
handleRelayWebSocket(w, r, relayAcceptFn, cfg)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Relay service not enabled", http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Management HTTP API (default)
|
||||||
|
default:
|
||||||
|
httpHandler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRelayWebSocket handles incoming WebSocket connections for the relay service
|
||||||
|
func handleRelayWebSocket(w http.ResponseWriter, r *http.Request, acceptFn func(conn net.Conn), cfg *CombinedConfig) {
|
||||||
|
acceptOptions := &websocket.AcceptOptions{
|
||||||
|
OriginPatterns: []string{"*"},
|
||||||
|
}
|
||||||
|
|
||||||
|
wsConn, err := websocket.Accept(w, r, acceptOptions)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to accept relay ws connection: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
connRemoteAddr := r.RemoteAddr
|
||||||
|
if r.Header.Get("X-Real-Ip") != "" && r.Header.Get("X-Real-Port") != "" {
|
||||||
|
connRemoteAddr = net.JoinHostPort(r.Header.Get("X-Real-Ip"), r.Header.Get("X-Real-Port"))
|
||||||
|
}
|
||||||
|
|
||||||
|
rAddr, err := net.ResolveTCPAddr("tcp", connRemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
_ = wsConn.Close(websocket.StatusInternalError, "internal error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lAddr, err := net.ResolveTCPAddr("tcp", cfg.Server.ListenAddress)
|
||||||
|
if err != nil {
|
||||||
|
_ = wsConn.Close(websocket.StatusInternalError, "internal error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Relay WS client connected from: %s", rAddr)
|
||||||
|
|
||||||
|
conn := ws.NewConn(wsConn, lAddr, rAddr)
|
||||||
|
acceptFn(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// logConfig prints all configuration parameters for debugging
|
||||||
|
func logConfig(cfg *CombinedConfig) {
|
||||||
|
log.Info("=== Configuration ===")
|
||||||
|
logServerConfig(cfg)
|
||||||
|
logComponentsConfig(cfg)
|
||||||
|
logRelayConfig(cfg)
|
||||||
|
logManagementConfig(cfg)
|
||||||
|
log.Info("=== End Configuration ===")
|
||||||
|
}
|
||||||
|
|
||||||
|
func logServerConfig(cfg *CombinedConfig) {
|
||||||
|
log.Info("--- Server ---")
|
||||||
|
log.Infof(" Listen address: %s", cfg.Server.ListenAddress)
|
||||||
|
log.Infof(" Exposed address: %s", cfg.Server.ExposedAddress)
|
||||||
|
log.Infof(" Healthcheck address: %s", cfg.Server.HealthcheckAddress)
|
||||||
|
log.Infof(" Metrics port: %d", cfg.Server.MetricsPort)
|
||||||
|
log.Infof(" Log level: %s", cfg.Server.LogLevel)
|
||||||
|
log.Infof(" Data dir: %s", cfg.Server.DataDir)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cfg.HasTLSCert():
|
||||||
|
log.Infof(" TLS: cert=%s, key=%s", cfg.Server.TLS.CertFile, cfg.Server.TLS.KeyFile)
|
||||||
|
case cfg.HasLetsEncrypt():
|
||||||
|
log.Infof(" TLS: Let's Encrypt (domains=%v)", cfg.Server.TLS.LetsEncrypt.Domains)
|
||||||
|
default:
|
||||||
|
log.Info(" TLS: disabled (using reverse proxy)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logComponentsConfig(cfg *CombinedConfig) {
|
||||||
|
log.Info("--- Components ---")
|
||||||
|
log.Infof(" Management: %v (log level: %s)", cfg.Management.Enabled, cfg.Management.LogLevel)
|
||||||
|
log.Infof(" Signal: %v (log level: %s)", cfg.Signal.Enabled, cfg.Signal.LogLevel)
|
||||||
|
log.Infof(" Relay: %v (log level: %s)", cfg.Relay.Enabled, cfg.Relay.LogLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logRelayConfig(cfg *CombinedConfig) {
|
||||||
|
if !cfg.Relay.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Info("--- Relay ---")
|
||||||
|
log.Infof(" Exposed address: %s", cfg.Relay.ExposedAddress)
|
||||||
|
log.Infof(" Auth secret: %s...", maskSecret(cfg.Relay.AuthSecret))
|
||||||
|
if cfg.Relay.Stun.Enabled {
|
||||||
|
log.Infof(" STUN ports: %v (log level: %s)", cfg.Relay.Stun.Ports, cfg.Relay.Stun.LogLevel)
|
||||||
|
} else {
|
||||||
|
log.Info(" STUN: disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logManagementConfig(cfg *CombinedConfig) {
|
||||||
|
if !cfg.Management.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Info("--- Management ---")
|
||||||
|
log.Infof(" Data dir: %s", cfg.Management.DataDir)
|
||||||
|
log.Infof(" DNS domain: %s", cfg.Management.DnsDomain)
|
||||||
|
log.Infof(" Store engine: %s", cfg.Management.Store.Engine)
|
||||||
|
if cfg.Server.Store.DSN != "" {
|
||||||
|
log.Infof(" Store DSN: %s", maskDSNPassword(cfg.Server.Store.DSN))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info(" Auth (embedded IdP):")
|
||||||
|
log.Infof(" Issuer: %s", cfg.Management.Auth.Issuer)
|
||||||
|
log.Infof(" Dashboard redirect URIs: %v", cfg.Management.Auth.DashboardRedirectURIs)
|
||||||
|
log.Infof(" CLI redirect URIs: %v", cfg.Management.Auth.CLIRedirectURIs)
|
||||||
|
|
||||||
|
log.Info(" Client settings:")
|
||||||
|
log.Infof(" Signal URI: %s", cfg.Management.SignalURI)
|
||||||
|
for _, s := range cfg.Management.Stuns {
|
||||||
|
log.Infof(" STUN: %s", s.URI)
|
||||||
|
}
|
||||||
|
if len(cfg.Management.Relays.Addresses) > 0 {
|
||||||
|
log.Infof(" Relay addresses: %v", cfg.Management.Relays.Addresses)
|
||||||
|
log.Infof(" Relay credentials TTL: %s", cfg.Management.Relays.CredentialsTTL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// logEnvVars logs all NB_ environment variables that are currently set
|
||||||
|
func logEnvVars() {
|
||||||
|
log.Info("=== Environment Variables ===")
|
||||||
|
found := false
|
||||||
|
for _, env := range os.Environ() {
|
||||||
|
if strings.HasPrefix(env, "NB_") {
|
||||||
|
key, _, _ := strings.Cut(env, "=")
|
||||||
|
value := os.Getenv(key)
|
||||||
|
if strings.Contains(strings.ToLower(key), "secret") || strings.Contains(strings.ToLower(key), "key") || strings.Contains(strings.ToLower(key), "password") {
|
||||||
|
value = maskSecret(value)
|
||||||
|
}
|
||||||
|
log.Infof(" %s=%s", key, value)
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
log.Info(" (none set)")
|
||||||
|
}
|
||||||
|
log.Info("=== End Environment Variables ===")
|
||||||
|
}
|
||||||
|
|
||||||
|
// maskDSNPassword masks the password in a DSN string.
|
||||||
|
// Handles both key=value format ("password=secret") and URI format ("user:secret@host").
|
||||||
|
func maskDSNPassword(dsn string) string {
|
||||||
|
// Key=value format: "host=localhost user=nb password=secret dbname=nb"
|
||||||
|
if strings.Contains(dsn, "password=") {
|
||||||
|
parts := strings.Fields(dsn)
|
||||||
|
for i, p := range parts {
|
||||||
|
if strings.HasPrefix(p, "password=") {
|
||||||
|
parts[i] = "password=****"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// URI format: "user:password@host..."
|
||||||
|
if atIdx := strings.Index(dsn, "@"); atIdx != -1 {
|
||||||
|
prefix := dsn[:atIdx]
|
||||||
|
if colonIdx := strings.Index(prefix, ":"); colonIdx != -1 {
|
||||||
|
return prefix[:colonIdx+1] + "****" + dsn[atIdx:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dsn
|
||||||
|
}
|
||||||
|
|
||||||
|
// maskSecret returns first 4 chars of secret followed by "..."
|
||||||
|
func maskSecret(secret string) string {
|
||||||
|
if len(secret) <= 4 {
|
||||||
|
return "****"
|
||||||
|
}
|
||||||
|
return secret[:4] + "..."
|
||||||
|
}
|
||||||
111
combined/config-simple.yaml.example
Normal file
111
combined/config-simple.yaml.example
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# NetBird Combined Server Configuration
|
||||||
|
# Copy this file to config.yaml and customize for your deployment
|
||||||
|
#
|
||||||
|
# This is a Management server with optional embedded Signal, Relay, and STUN services.
|
||||||
|
# By default, all services run locally. You can use external services instead by
|
||||||
|
# setting the corresponding override fields.
|
||||||
|
#
|
||||||
|
# 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)
|
||||||
|
# - STUN: Local on port 3478 by default; set 'stuns' to use external instead
|
||||||
|
|
||||||
|
server:
|
||||||
|
# Main HTTP/gRPC port for all services (Management, Signal, Relay)
|
||||||
|
listenAddress: ":443"
|
||||||
|
|
||||||
|
# Public address that peers will use to connect to this server
|
||||||
|
# Used for relay connections and management DNS domain
|
||||||
|
# Format: protocol://hostname:port (e.g., https://server.mycompany.com:443)
|
||||||
|
exposedAddress: "https://server.mycompany.com:443"
|
||||||
|
|
||||||
|
# STUN server ports (defaults to [3478] if not specified; set 'stuns' to use external)
|
||||||
|
# stunPorts:
|
||||||
|
# - 3478
|
||||||
|
|
||||||
|
# Metrics endpoint port
|
||||||
|
metricsPort: 9090
|
||||||
|
|
||||||
|
# Healthcheck endpoint address
|
||||||
|
healthcheckAddress: ":9000"
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
logLevel: "info" # Default log level for all components: panic, fatal, error, warn, info, debug, trace
|
||||||
|
logFile: "console" # "console" or path to log file
|
||||||
|
|
||||||
|
# TLS configuration (optional)
|
||||||
|
tls:
|
||||||
|
certFile: ""
|
||||||
|
keyFile: ""
|
||||||
|
letsencrypt:
|
||||||
|
enabled: false
|
||||||
|
dataDir: ""
|
||||||
|
domains: []
|
||||||
|
email: ""
|
||||||
|
awsRoute53: false
|
||||||
|
|
||||||
|
# Shared secret for relay authentication (required when running local relay)
|
||||||
|
authSecret: "your-secret-key-here"
|
||||||
|
|
||||||
|
# Data directory for all services
|
||||||
|
dataDir: "/var/lib/netbird/"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# External Service Overrides (optional)
|
||||||
|
# Use these to point to external Signal, Relay, or STUN servers instead of
|
||||||
|
# running them locally. When set, the corresponding local service is disabled.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# External STUN servers - disables local STUN server
|
||||||
|
# stuns:
|
||||||
|
# - uri: "stun:stun.example.com:3478"
|
||||||
|
# - uri: "stun:stun.example.com:3479"
|
||||||
|
|
||||||
|
# External relay servers - disables local relay server
|
||||||
|
# relays:
|
||||||
|
# addresses:
|
||||||
|
# - "rels://relay.example.com:443"
|
||||||
|
# credentialsTTL: "12h"
|
||||||
|
# secret: "relay-shared-secret"
|
||||||
|
|
||||||
|
# External signal server - disables local signal server
|
||||||
|
# signalUri: "https://signal.example.com:443"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Management Settings
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Metrics and updates
|
||||||
|
disableAnonymousMetrics: false
|
||||||
|
disableGeoliteUpdate: false
|
||||||
|
|
||||||
|
# Embedded authentication/identity provider (Dex) configuration (always enabled)
|
||||||
|
auth:
|
||||||
|
# OIDC issuer URL - must be publicly accessible
|
||||||
|
issuer: "https://server.mycompany.com/oauth2"
|
||||||
|
localAuthDisabled: false
|
||||||
|
signKeyRefreshEnabled: false
|
||||||
|
# OAuth2 redirect URIs for dashboard
|
||||||
|
dashboardRedirectURIs:
|
||||||
|
- "https://app.netbird.io/nb-auth"
|
||||||
|
- "https://app.netbird.io/nb-silent-auth"
|
||||||
|
# OAuth2 redirect URIs for CLI
|
||||||
|
cliRedirectURIs:
|
||||||
|
- "http://localhost:53000/"
|
||||||
|
# Optional initial admin user
|
||||||
|
# owner:
|
||||||
|
# email: "admin@example.com"
|
||||||
|
# password: "initial-password"
|
||||||
|
|
||||||
|
# Store configuration
|
||||||
|
store:
|
||||||
|
engine: "sqlite" # sqlite, postgres, or mysql
|
||||||
|
dsn: "" # Connection string for postgres or mysql
|
||||||
|
encryptionKey: ""
|
||||||
|
|
||||||
|
# Reverse proxy settings (optional)
|
||||||
|
# reverseProxy:
|
||||||
|
# trustedHTTPProxies: []
|
||||||
|
# trustedHTTPProxiesCount: 0
|
||||||
|
# trustedPeers: []
|
||||||
115
combined/config.yaml.example
Normal file
115
combined/config.yaml.example
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# Simplified Combined NetBird Server Configuration
|
||||||
|
# Copy this file to config.yaml and customize for your deployment
|
||||||
|
|
||||||
|
# Server-wide settings
|
||||||
|
server:
|
||||||
|
# Main HTTP/gRPC port for all services (Management, Signal, Relay)
|
||||||
|
listenAddress: ":443"
|
||||||
|
|
||||||
|
# Metrics endpoint port
|
||||||
|
metricsPort: 9090
|
||||||
|
|
||||||
|
# Healthcheck endpoint address
|
||||||
|
healthcheckAddress: ":9000"
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
logLevel: "info" # panic, fatal, error, warn, info, debug, trace
|
||||||
|
logFile: "console" # "console" or path to log file
|
||||||
|
|
||||||
|
# TLS configuration (optional)
|
||||||
|
tls:
|
||||||
|
certFile: ""
|
||||||
|
keyFile: ""
|
||||||
|
letsencrypt:
|
||||||
|
enabled: false
|
||||||
|
dataDir: ""
|
||||||
|
domains: []
|
||||||
|
email: ""
|
||||||
|
awsRoute53: false
|
||||||
|
|
||||||
|
# Relay service configuration
|
||||||
|
relay:
|
||||||
|
# Enable/disable the relay service
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Public address that peers will use to connect to this relay
|
||||||
|
# Format: hostname:port or ip:port
|
||||||
|
exposedAddress: "relay.example.com:443"
|
||||||
|
|
||||||
|
# Shared secret for relay authentication (required when enabled)
|
||||||
|
authSecret: "your-secret-key-here"
|
||||||
|
|
||||||
|
# Log level for relay (reserved for future use, currently uses global log level)
|
||||||
|
logLevel: "info"
|
||||||
|
|
||||||
|
# Embedded STUN server (optional)
|
||||||
|
stun:
|
||||||
|
enabled: false
|
||||||
|
ports: [3478]
|
||||||
|
logLevel: "info"
|
||||||
|
|
||||||
|
# Signal service configuration
|
||||||
|
signal:
|
||||||
|
# Enable/disable the signal service
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Log level for signal (reserved for future use, currently uses global log level)
|
||||||
|
logLevel: "info"
|
||||||
|
|
||||||
|
# Management service configuration
|
||||||
|
management:
|
||||||
|
# Enable/disable the management service
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Data directory for management service
|
||||||
|
dataDir: "/var/lib/netbird/"
|
||||||
|
|
||||||
|
# DNS domain for the management server
|
||||||
|
dnsDomain: ""
|
||||||
|
|
||||||
|
# Metrics and updates
|
||||||
|
disableAnonymousMetrics: false
|
||||||
|
disableGeoliteUpdate: false
|
||||||
|
|
||||||
|
auth:
|
||||||
|
# OIDC issuer URL - must be publicly accessible
|
||||||
|
issuer: "https://management.example.com/oauth2"
|
||||||
|
localAuthDisabled: false
|
||||||
|
signKeyRefreshEnabled: false
|
||||||
|
# OAuth2 redirect URIs for dashboard
|
||||||
|
dashboardRedirectURIs:
|
||||||
|
- "https://app.example.com/nb-auth"
|
||||||
|
- "https://app.example.com/nb-silent-auth"
|
||||||
|
# OAuth2 redirect URIs for CLI
|
||||||
|
cliRedirectURIs:
|
||||||
|
- "http://localhost:53000/"
|
||||||
|
# Optional initial admin user
|
||||||
|
# owner:
|
||||||
|
# email: "admin@example.com"
|
||||||
|
# password: "initial-password"
|
||||||
|
|
||||||
|
# External STUN servers (for client config)
|
||||||
|
stuns: []
|
||||||
|
# - uri: "stun:stun.example.com:3478"
|
||||||
|
|
||||||
|
# External relay servers (for client config)
|
||||||
|
relays:
|
||||||
|
addresses: []
|
||||||
|
# - "rels://relay.example.com:443"
|
||||||
|
credentialsTTL: "12h"
|
||||||
|
secret: ""
|
||||||
|
|
||||||
|
# External signal server URI (for client config)
|
||||||
|
signalUri: ""
|
||||||
|
|
||||||
|
# Store configuration
|
||||||
|
store:
|
||||||
|
engine: "sqlite" # sqlite, postgres, or mysql
|
||||||
|
dsn: "" # Connection string for postgres or mysql
|
||||||
|
encryptionKey: ""
|
||||||
|
|
||||||
|
# Reverse proxy settings
|
||||||
|
reverseProxy:
|
||||||
|
trustedHTTPProxies: []
|
||||||
|
trustedHTTPProxiesCount: 0
|
||||||
|
trustedPeers: []
|
||||||
13
combined/main.go
Normal file
13
combined/main.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/combined/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := cmd.Execute(); err != nil {
|
||||||
|
log.Fatalf("failed to execute command: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -55,7 +55,7 @@ var (
|
|||||||
// detect whether user specified a port
|
// detect whether user specified a port
|
||||||
userPort := cmd.Flag("port").Changed
|
userPort := cmd.Flag("port").Changed
|
||||||
|
|
||||||
config, err = loadMgmtConfig(ctx, nbconfig.MgmtConfigPath)
|
config, err = LoadMgmtConfig(ctx, nbconfig.MgmtConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed reading provided config file: %s: %v", nbconfig.MgmtConfigPath, err)
|
return fmt.Errorf("failed reading provided config file: %s: %v", nbconfig.MgmtConfigPath, err)
|
||||||
}
|
}
|
||||||
@@ -133,35 +133,35 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadMgmtConfig(ctx context.Context, mgmtConfigPath string) (*nbconfig.Config, error) {
|
func LoadMgmtConfig(ctx context.Context, mgmtConfigPath string) (*nbconfig.Config, error) {
|
||||||
loadedConfig := &nbconfig.Config{}
|
loadedConfig := &nbconfig.Config{}
|
||||||
if _, err := util.ReadJsonWithEnvSub(mgmtConfigPath, loadedConfig); err != nil {
|
if _, err := util.ReadJsonWithEnvSub(mgmtConfigPath, loadedConfig); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
applyCommandLineOverrides(loadedConfig)
|
ApplyCommandLineOverrides(loadedConfig)
|
||||||
|
|
||||||
// Apply EmbeddedIdP config to HttpConfig if embedded IdP is enabled
|
// Apply EmbeddedIdP config to HttpConfig if embedded IdP is enabled
|
||||||
err := applyEmbeddedIdPConfig(ctx, loadedConfig)
|
err := ApplyEmbeddedIdPConfig(ctx, loadedConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := applyOIDCConfig(ctx, loadedConfig); err != nil {
|
if err := ApplyOIDCConfig(ctx, loadedConfig); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logConfigInfo(loadedConfig)
|
LogConfigInfo(loadedConfig)
|
||||||
|
|
||||||
if err := ensureEncryptionKey(ctx, mgmtConfigPath, loadedConfig); err != nil {
|
if err := EnsureEncryptionKey(ctx, mgmtConfigPath, loadedConfig); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return loadedConfig, nil
|
return loadedConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyCommandLineOverrides applies command-line flag overrides to the config
|
// ApplyCommandLineOverrides applies command-line flag overrides to the config
|
||||||
func applyCommandLineOverrides(cfg *nbconfig.Config) {
|
func ApplyCommandLineOverrides(cfg *nbconfig.Config) {
|
||||||
if mgmtLetsencryptDomain != "" {
|
if mgmtLetsencryptDomain != "" {
|
||||||
cfg.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain
|
cfg.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain
|
||||||
}
|
}
|
||||||
@@ -174,9 +174,9 @@ func applyCommandLineOverrides(cfg *nbconfig.Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyEmbeddedIdPConfig populates HttpConfig and EmbeddedIdP storage from config when embedded IdP is enabled.
|
// ApplyEmbeddedIdPConfig populates HttpConfig and EmbeddedIdP storage from config when embedded IdP is enabled.
|
||||||
// This allows users to only specify EmbeddedIdP config without duplicating values in HttpConfig.
|
// This allows users to only specify EmbeddedIdP config without duplicating values in HttpConfig.
|
||||||
func applyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config) error {
|
func ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config) error {
|
||||||
if cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {
|
if cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -222,8 +222,8 @@ func applyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyOIDCConfig fetches and applies OIDC configuration if endpoint is specified
|
// ApplyOIDCConfig fetches and applies OIDC configuration if endpoint is specified
|
||||||
func applyOIDCConfig(ctx context.Context, cfg *nbconfig.Config) error {
|
func ApplyOIDCConfig(ctx context.Context, cfg *nbconfig.Config) error {
|
||||||
oidcEndpoint := cfg.HttpConfig.OIDCConfigEndpoint
|
oidcEndpoint := cfg.HttpConfig.OIDCConfigEndpoint
|
||||||
if oidcEndpoint == "" {
|
if oidcEndpoint == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -249,16 +249,16 @@ func applyOIDCConfig(ctx context.Context, cfg *nbconfig.Config) error {
|
|||||||
oidcConfig.JwksURI, cfg.HttpConfig.AuthKeysLocation)
|
oidcConfig.JwksURI, cfg.HttpConfig.AuthKeysLocation)
|
||||||
cfg.HttpConfig.AuthKeysLocation = oidcConfig.JwksURI
|
cfg.HttpConfig.AuthKeysLocation = oidcConfig.JwksURI
|
||||||
|
|
||||||
if err := applyDeviceAuthFlowConfig(ctx, cfg, &oidcConfig, oidcEndpoint); err != nil {
|
if err := ApplyDeviceAuthFlowConfig(ctx, cfg, &oidcConfig, oidcEndpoint); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
applyPKCEFlowConfig(ctx, cfg, &oidcConfig)
|
ApplyPKCEFlowConfig(ctx, cfg, &oidcConfig)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyDeviceAuthFlowConfig applies OIDC config to DeviceAuthorizationFlow if enabled
|
// ApplyDeviceAuthFlowConfig applies OIDC config to DeviceAuthorizationFlow if enabled
|
||||||
func applyDeviceAuthFlowConfig(ctx context.Context, cfg *nbconfig.Config, oidcConfig *OIDCConfigResponse, oidcEndpoint string) error {
|
func ApplyDeviceAuthFlowConfig(ctx context.Context, cfg *nbconfig.Config, oidcConfig *OIDCConfigResponse, oidcEndpoint string) error {
|
||||||
if cfg.DeviceAuthorizationFlow == nil || strings.ToLower(cfg.DeviceAuthorizationFlow.Provider) == string(nbconfig.NONE) {
|
if cfg.DeviceAuthorizationFlow == nil || strings.ToLower(cfg.DeviceAuthorizationFlow.Provider) == string(nbconfig.NONE) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -285,8 +285,8 @@ func applyDeviceAuthFlowConfig(ctx context.Context, cfg *nbconfig.Config, oidcCo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyPKCEFlowConfig applies OIDC config to PKCEAuthorizationFlow if configured
|
// ApplyPKCEFlowConfig applies OIDC config to PKCEAuthorizationFlow if configured
|
||||||
func applyPKCEFlowConfig(ctx context.Context, cfg *nbconfig.Config, oidcConfig *OIDCConfigResponse) {
|
func ApplyPKCEFlowConfig(ctx context.Context, cfg *nbconfig.Config, oidcConfig *OIDCConfigResponse) {
|
||||||
if cfg.PKCEAuthorizationFlow == nil {
|
if cfg.PKCEAuthorizationFlow == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -299,8 +299,8 @@ func applyPKCEFlowConfig(ctx context.Context, cfg *nbconfig.Config, oidcConfig *
|
|||||||
cfg.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint = oidcConfig.AuthorizationEndpoint
|
cfg.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint = oidcConfig.AuthorizationEndpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
// logConfigInfo logs informational messages about the loaded configuration
|
// LogConfigInfo logs informational messages about the loaded configuration
|
||||||
func logConfigInfo(cfg *nbconfig.Config) {
|
func LogConfigInfo(cfg *nbconfig.Config) {
|
||||||
if cfg.EmbeddedIdP != nil {
|
if cfg.EmbeddedIdP != nil {
|
||||||
log.Infof("running with the embedded IdP: %v", cfg.EmbeddedIdP.Issuer)
|
log.Infof("running with the embedded IdP: %v", cfg.EmbeddedIdP.Issuer)
|
||||||
}
|
}
|
||||||
@@ -309,8 +309,8 @@ func logConfigInfo(cfg *nbconfig.Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureEncryptionKey generates and saves a DataStoreEncryptionKey if not set
|
// EnsureEncryptionKey generates and saves a DataStoreEncryptionKey if not set
|
||||||
func ensureEncryptionKey(ctx context.Context, configPath string, cfg *nbconfig.Config) error {
|
func EnsureEncryptionKey(ctx context.Context, configPath string, cfg *nbconfig.Config) error {
|
||||||
if cfg.DataStoreEncryptionKey != "" {
|
if cfg.DataStoreEncryptionKey != "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func Test_loadMgmtConfig(t *testing.T) {
|
|||||||
t.Fatalf("failed to create config: %s", err)
|
t.Fatalf("failed to create config: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := loadMgmtConfig(context.Background(), tmpFile)
|
cfg, err := LoadMgmtConfig(context.Background(), tmpFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to load management config: %s", err)
|
t.Fatalf("failed to load management config: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/netbirdio/netbird/management/server/idp"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"go.opentelemetry.io/otel/metric"
|
"go.opentelemetry.io/otel/metric"
|
||||||
"golang.org/x/crypto/acme/autocert"
|
"golang.org/x/crypto/acme/autocert"
|
||||||
@@ -19,6 +18,8 @@ import (
|
|||||||
"golang.org/x/net/http2/h2c"
|
"golang.org/x/net/http2/h2c"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/idp"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/encryption"
|
"github.com/netbirdio/netbird/encryption"
|
||||||
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
"github.com/netbirdio/netbird/management/server/metrics"
|
"github.com/netbirdio/netbird/management/server/metrics"
|
||||||
@@ -138,6 +139,14 @@ func (s *BaseServer) Start(ctx context.Context) error {
|
|||||||
go metricsWorker.Run(srvCtx)
|
go metricsWorker.Run(srvCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run afterInit hooks before starting any servers
|
||||||
|
// This allows registering additional gRPC services (e.g., Signal) before Serve() is called
|
||||||
|
for _, fn := range s.afterInit {
|
||||||
|
if fn != nil {
|
||||||
|
fn(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var compatListener net.Listener
|
var compatListener net.Listener
|
||||||
if s.mgmtPort != ManagementLegacyPort {
|
if s.mgmtPort != ManagementLegacyPort {
|
||||||
// The Management gRPC server was running on port 33073 previously. Old agents that are already connected to it
|
// The Management gRPC server was running on port 33073 previously. Old agents that are already connected to it
|
||||||
@@ -178,12 +187,6 @@ func (s *BaseServer) Start(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fn := range s.afterInit {
|
|
||||||
if fn != nil {
|
|
||||||
fn(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithContext(ctx).Infof("management server version %s", version.NetbirdVersion())
|
log.WithContext(ctx).Infof("management server version %s", version.NetbirdVersion())
|
||||||
log.WithContext(ctx).Infof("running HTTP server and gRPC server on the same port: %s", s.listener.Addr().String())
|
log.WithContext(ctx).Infof("running HTTP server and gRPC server on the same port: %s", s.listener.Addr().String())
|
||||||
s.serveGRPCWithHTTP(ctx, s.listener, rootHandler, tlsEnabled)
|
s.serveGRPCWithHTTP(ctx, s.listener, rootHandler, tlsEnabled)
|
||||||
@@ -255,7 +258,23 @@ func (s *BaseServer) SetContainer(key string, container any) {
|
|||||||
log.Tracef("container with key %s set successfully", key)
|
log.Tracef("container with key %s set successfully", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetHandlerFunc allows overriding the default HTTP handler function.
|
||||||
|
// This is useful for multiplexing additional services on the same port.
|
||||||
|
func (s *BaseServer) SetHandlerFunc(handler http.Handler) {
|
||||||
|
s.container["customHandler"] = handler
|
||||||
|
log.Tracef("custom handler set successfully")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *BaseServer) handlerFunc(_ context.Context, gRPCHandler *grpc.Server, httpHandler http.Handler, meter metric.Meter) http.Handler {
|
func (s *BaseServer) handlerFunc(_ context.Context, gRPCHandler *grpc.Server, httpHandler http.Handler, meter metric.Meter) http.Handler {
|
||||||
|
// Check if a custom handler was set (for multiplexing additional services)
|
||||||
|
if customHandler, ok := s.GetContainer("customHandler"); ok {
|
||||||
|
if handler, ok := customHandler.(http.Handler); ok {
|
||||||
|
log.Tracef("using custom handler")
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use default handler
|
||||||
wsProxy := wsproxyserver.New(gRPCHandler, wsproxyserver.WithOTelMeter(meter))
|
wsProxy := wsproxyserver.New(gRPCHandler, wsproxyserver.WithOTelMeter(meter))
|
||||||
|
|
||||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
|||||||
@@ -2643,7 +2643,7 @@ func getGormConfig() *gorm.Config {
|
|||||||
|
|
||||||
// newPostgresStore initializes a new Postgres store.
|
// newPostgresStore initializes a new Postgres store.
|
||||||
func newPostgresStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {
|
func newPostgresStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {
|
||||||
dsn, ok := os.LookupEnv(postgresDsnEnv)
|
dsn, ok := lookupDSNEnv(postgresDsnEnv, postgresDsnEnvLegacy)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("%s is not set", postgresDsnEnv)
|
return nil, fmt.Errorf("%s is not set", postgresDsnEnv)
|
||||||
}
|
}
|
||||||
@@ -2652,7 +2652,7 @@ func newPostgresStore(ctx context.Context, metrics telemetry.AppMetrics, skipMig
|
|||||||
|
|
||||||
// newMysqlStore initializes a new MySQL store.
|
// newMysqlStore initializes a new MySQL store.
|
||||||
func newMysqlStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {
|
func newMysqlStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {
|
||||||
dsn, ok := os.LookupEnv(mysqlDsnEnv)
|
dsn, ok := lookupDSNEnv(mysqlDsnEnv, mysqlDsnEnvLegacy)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("%s is not set", mysqlDsnEnv)
|
return nil, fmt.Errorf("%s is not set", mysqlDsnEnv)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,10 +243,20 @@ type Store interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
postgresDsnEnv = "NETBIRD_STORE_ENGINE_POSTGRES_DSN"
|
postgresDsnEnv = "NB_STORE_ENGINE_POSTGRES_DSN"
|
||||||
mysqlDsnEnv = "NETBIRD_STORE_ENGINE_MYSQL_DSN"
|
postgresDsnEnvLegacy = "NETBIRD_STORE_ENGINE_POSTGRES_DSN"
|
||||||
|
mysqlDsnEnv = "NB_STORE_ENGINE_MYSQL_DSN"
|
||||||
|
mysqlDsnEnvLegacy = "NETBIRD_STORE_ENGINE_MYSQL_DSN"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// lookupDSNEnv checks the NB_ env var first, then falls back to the legacy NETBIRD_ env var.
|
||||||
|
func lookupDSNEnv(nbKey, legacyKey string) (string, bool) {
|
||||||
|
if v, ok := os.LookupEnv(nbKey); ok {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
return os.LookupEnv(legacyKey)
|
||||||
|
}
|
||||||
|
|
||||||
var supportedEngines = []types.Engine{types.SqliteStoreEngine, types.PostgresStoreEngine, types.MysqlStoreEngine}
|
var supportedEngines = []types.Engine{types.SqliteStoreEngine, types.PostgresStoreEngine, types.MysqlStoreEngine}
|
||||||
|
|
||||||
func getStoreEngineFromEnv() types.Engine {
|
func getStoreEngineFromEnv() types.Engine {
|
||||||
@@ -531,7 +541,7 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind types.Engine)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind types.Engine) (*SqlStore, func(), error) {
|
func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind types.Engine) (*SqlStore, func(), error) {
|
||||||
dsn, ok := os.LookupEnv(postgresDsnEnv)
|
dsn, ok := lookupDSNEnv(postgresDsnEnv, postgresDsnEnvLegacy)
|
||||||
if !ok || dsn == "" {
|
if !ok || dsn == "" {
|
||||||
var err error
|
var err error
|
||||||
_, dsn, err = testutil.CreatePostgresTestContainer()
|
_, dsn, err = testutil.CreatePostgresTestContainer()
|
||||||
@@ -569,7 +579,7 @@ func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind types.Eng
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newReusedMysqlStore(ctx context.Context, store *SqlStore, kind types.Engine) (*SqlStore, func(), error) {
|
func newReusedMysqlStore(ctx context.Context, store *SqlStore, kind types.Engine) (*SqlStore, func(), error) {
|
||||||
dsn, ok := os.LookupEnv(mysqlDsnEnv)
|
dsn, ok := lookupDSNEnv(mysqlDsnEnv, mysqlDsnEnvLegacy)
|
||||||
if !ok || dsn == "" {
|
if !ok || dsn == "" {
|
||||||
var err error
|
var err error
|
||||||
_, dsn, err = testutil.CreateMysqlTestContainer()
|
_, dsn, err = testutil.CreateMysqlTestContainer()
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ type defaultAppMetrics struct {
|
|||||||
Meter metric2.Meter
|
Meter metric2.Meter
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
externallyManaged bool
|
||||||
idpMetrics *IDPMetrics
|
idpMetrics *IDPMetrics
|
||||||
httpMiddleware *HTTPMiddleware
|
httpMiddleware *HTTPMiddleware
|
||||||
grpcMetrics *GRPCMetrics
|
grpcMetrics *GRPCMetrics
|
||||||
@@ -171,6 +172,9 @@ func (appMetrics *defaultAppMetrics) Close() error {
|
|||||||
// Expose metrics on a given port and endpoint. If endpoint is empty a defaultEndpoint one will be used.
|
// Expose metrics on a given port and endpoint. If endpoint is empty a defaultEndpoint one will be used.
|
||||||
// Exposes metrics in the Prometheus format https://prometheus.io/
|
// Exposes metrics in the Prometheus format https://prometheus.io/
|
||||||
func (appMetrics *defaultAppMetrics) Expose(ctx context.Context, port int, endpoint string) error {
|
func (appMetrics *defaultAppMetrics) Expose(ctx context.Context, port int, endpoint string) error {
|
||||||
|
if appMetrics.externallyManaged {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if endpoint == "" {
|
if endpoint == "" {
|
||||||
endpoint = defaultEndpoint
|
endpoint = defaultEndpoint
|
||||||
}
|
}
|
||||||
@@ -252,3 +256,49 @@ func NewDefaultAppMetrics(ctx context.Context) (AppMetrics, error) {
|
|||||||
accountManagerMetrics: accountManagerMetrics,
|
accountManagerMetrics: accountManagerMetrics,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewAppMetricsWithMeter creates AppMetrics using an externally provided meter.
|
||||||
|
// The caller is responsible for exposing metrics via HTTP. Expose() and Close() are no-ops.
|
||||||
|
func NewAppMetricsWithMeter(ctx context.Context, meter metric2.Meter) (AppMetrics, error) {
|
||||||
|
idpMetrics, err := NewIDPMetrics(ctx, meter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize IDP metrics: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware, err := NewMetricsMiddleware(ctx, meter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize HTTP middleware metrics: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcMetrics, err := NewGRPCMetrics(ctx, meter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize gRPC metrics: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
storeMetrics, err := NewStoreMetrics(ctx, meter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize store metrics: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChannelMetrics, err := NewUpdateChannelMetrics(ctx, meter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize update channel metrics: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountManagerMetrics, err := NewAccountManagerMetrics(ctx, meter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize account manager metrics: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &defaultAppMetrics{
|
||||||
|
Meter: meter,
|
||||||
|
ctx: ctx,
|
||||||
|
externallyManaged: true,
|
||||||
|
idpMetrics: idpMetrics,
|
||||||
|
httpMiddleware: middleware,
|
||||||
|
grpcMetrics: grpcMetrics,
|
||||||
|
storeMetrics: storeMetrics,
|
||||||
|
updateChannelMetrics: updateChannelMetrics,
|
||||||
|
accountManagerMetrics: accountManagerMetrics,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import (
|
|||||||
"github.com/netbirdio/netbird/encryption"
|
"github.com/netbirdio/netbird/encryption"
|
||||||
"github.com/netbirdio/netbird/relay/healthcheck"
|
"github.com/netbirdio/netbird/relay/healthcheck"
|
||||||
"github.com/netbirdio/netbird/relay/server"
|
"github.com/netbirdio/netbird/relay/server"
|
||||||
|
"github.com/netbirdio/netbird/shared/metrics"
|
||||||
"github.com/netbirdio/netbird/shared/relay/auth"
|
"github.com/netbirdio/netbird/shared/relay/auth"
|
||||||
"github.com/netbirdio/netbird/signal/metrics"
|
|
||||||
"github.com/netbirdio/netbird/stun"
|
"github.com/netbirdio/netbird/stun"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -134,3 +135,10 @@ func (r *Server) ListenerProtocols() []protocol.Protocol {
|
|||||||
func (r *Server) InstanceURL() url.URL {
|
func (r *Server) InstanceURL() url.URL {
|
||||||
return r.relay.InstanceURL()
|
return r.relay.InstanceURL()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RelayAccept returns the relay's Accept function for handling incoming connections.
|
||||||
|
// This allows external HTTP handlers to route connections to the relay without
|
||||||
|
// starting the relay's own listeners.
|
||||||
|
func (r *Server) RelayAccept() func(conn net.Conn) {
|
||||||
|
return r.relay.Accept
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ func Execute() error {
|
|||||||
func init() {
|
func init() {
|
||||||
stopCh = make(chan int)
|
stopCh = make(chan int)
|
||||||
defaultLogFile = "/var/log/netbird/signal.log"
|
defaultLogFile = "/var/log/netbird/signal.log"
|
||||||
defaultSignalSSLDir = "/var/lib/netbird/"
|
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
defaultLogFile = os.Getenv("PROGRAMDATA") + "\\Netbird\\" + "signal.log"
|
defaultLogFile = os.Getenv("PROGRAMDATA") + "\\Netbird\\" + "signal.log"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
"golang.org/x/net/http2/h2c"
|
"golang.org/x/net/http2/h2c"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/signal/metrics"
|
"github.com/netbirdio/netbird/shared/metrics"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/encryption"
|
"github.com/netbirdio/netbird/encryption"
|
||||||
"github.com/netbirdio/netbird/shared/signal/proto"
|
"github.com/netbirdio/netbird/shared/signal/proto"
|
||||||
@@ -41,8 +41,8 @@ var (
|
|||||||
signalPort int
|
signalPort int
|
||||||
metricsPort int
|
metricsPort int
|
||||||
signalLetsencryptDomain string
|
signalLetsencryptDomain string
|
||||||
signalSSLDir string
|
signalLetsencryptEmail string
|
||||||
defaultSignalSSLDir string
|
signalLetsencryptDataDir string
|
||||||
signalCertFile string
|
signalCertFile string
|
||||||
signalCertKey string
|
signalCertKey string
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ func getTLSConfigurations() ([]grpc.ServerOption, *autocert.Manager, *tls.Config
|
|||||||
}
|
}
|
||||||
|
|
||||||
if signalLetsencryptDomain != "" {
|
if signalLetsencryptDomain != "" {
|
||||||
certManager, err = encryption.CreateCertManager(signalSSLDir, signalLetsencryptDomain)
|
certManager, err = encryption.CreateCertManager(signalLetsencryptDataDir, signalLetsencryptDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, certManager, nil, err
|
return nil, certManager, nil, err
|
||||||
}
|
}
|
||||||
@@ -326,9 +326,11 @@ func loadTLSConfig(certFile string, certKey string) (*tls.Config, error) {
|
|||||||
func init() {
|
func init() {
|
||||||
runCmd.PersistentFlags().IntVar(&signalPort, "port", 80, "Server port to listen on (defaults to 443 if TLS is enabled, 80 otherwise")
|
runCmd.PersistentFlags().IntVar(&signalPort, "port", 80, "Server port to listen on (defaults to 443 if TLS is enabled, 80 otherwise")
|
||||||
runCmd.Flags().IntVar(&metricsPort, "metrics-port", 9090, "metrics endpoint http port. Metrics are accessible under host:metrics-port/metrics")
|
runCmd.Flags().IntVar(&metricsPort, "metrics-port", 9090, "metrics endpoint http port. Metrics are accessible under host:metrics-port/metrics")
|
||||||
runCmd.Flags().StringVar(&signalSSLDir, "ssl-dir", defaultSignalSSLDir, "server ssl directory location. *Required only for Let's Encrypt certificates.")
|
runCmd.PersistentFlags().StringVar(&signalLetsencryptDataDir, "letsencrypt-data-dir", "", "a directory to store Let's Encrypt data. Required if Let's Encrypt is enabled.")
|
||||||
runCmd.Flags().StringVar(&signalLetsencryptDomain, "letsencrypt-domain", "", "a domain to issue Let's Encrypt certificate for. Enables TLS using Let's Encrypt. Will fetch and renew certificate, and run the server with TLS")
|
runCmd.PersistentFlags().StringVar(&signalLetsencryptDataDir, "ssl-dir", "", "server ssl directory location. *Required only for Let's Encrypt certificates. Deprecated: use --letsencrypt-data-dir")
|
||||||
runCmd.Flags().StringVar(&signalCertFile, "cert-file", "", "Location of your SSL certificate. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
|
runCmd.PersistentFlags().StringVar(&signalLetsencryptDomain, "letsencrypt-domain", "", "a domain to issue Let's Encrypt certificate for. Enables TLS using Let's Encrypt. Will fetch and renew certificate, and run the server with TLS")
|
||||||
runCmd.Flags().StringVar(&signalCertKey, "cert-key", "", "Location of your SSL certificate private key. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
|
runCmd.PersistentFlags().StringVar(&signalLetsencryptEmail, "letsencrypt-email", "", "email address to use for Let's Encrypt certificate registration")
|
||||||
|
runCmd.PersistentFlags().StringVar(&signalCertFile, "cert-file", "", "Location of your SSL certificate. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
|
||||||
|
runCmd.PersistentFlags().StringVar(&signalCertKey, "cert-key", "", "Location of your SSL certificate private key. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
|
||||||
setFlagsFromEnvVars(runCmd)
|
setFlagsFromEnvVars(runCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,15 +24,19 @@ type AppMetrics struct {
|
|||||||
MessageSize metric.Int64Histogram
|
MessageSize metric.Int64Histogram
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) {
|
func NewAppMetrics(meter metric.Meter, prefix ...string) (*AppMetrics, error) {
|
||||||
activePeers, err := meter.Int64UpDownCounter("active_peers",
|
p := ""
|
||||||
|
if len(prefix) > 0 {
|
||||||
|
p = prefix[0]
|
||||||
|
}
|
||||||
|
activePeers, err := meter.Int64UpDownCounter(p+"active_peers",
|
||||||
metric.WithDescription("Number of active connected peers"),
|
metric.WithDescription("Number of active connected peers"),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
peerConnectionDuration, err := meter.Int64Histogram("peer_connection_duration_seconds",
|
peerConnectionDuration, err := meter.Int64Histogram(p+"peer_connection_duration_seconds",
|
||||||
metric.WithExplicitBucketBoundaries(getPeerConnectionDurationBucketBoundaries()...),
|
metric.WithExplicitBucketBoundaries(getPeerConnectionDurationBucketBoundaries()...),
|
||||||
metric.WithDescription("Duration of how long a peer was connected"),
|
metric.WithDescription("Duration of how long a peer was connected"),
|
||||||
)
|
)
|
||||||
@@ -40,28 +44,28 @@ func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
registrations, err := meter.Int64Counter("registrations_total",
|
registrations, err := meter.Int64Counter(p+"registrations_total",
|
||||||
metric.WithDescription("Total number of peer registrations"),
|
metric.WithDescription("Total number of peer registrations"),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
deregistrations, err := meter.Int64Counter("deregistrations_total",
|
deregistrations, err := meter.Int64Counter(p+"deregistrations_total",
|
||||||
metric.WithDescription("Total number of peer deregistrations"),
|
metric.WithDescription("Total number of peer deregistrations"),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
registrationFailures, err := meter.Int64Counter("registration_failures_total",
|
registrationFailures, err := meter.Int64Counter(p+"registration_failures_total",
|
||||||
metric.WithDescription("Total number of peer registration failures"),
|
metric.WithDescription("Total number of peer registration failures"),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
registrationDelay, err := meter.Float64Histogram("registration_delay_milliseconds",
|
registrationDelay, err := meter.Float64Histogram(p+"registration_delay_milliseconds",
|
||||||
metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
|
metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
|
||||||
metric.WithDescription("Duration of how long it takes to register a peer"),
|
metric.WithDescription("Duration of how long it takes to register a peer"),
|
||||||
)
|
)
|
||||||
@@ -69,7 +73,7 @@ func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
getRegistrationDelay, err := meter.Float64Histogram("get_registration_delay_milliseconds",
|
getRegistrationDelay, err := meter.Float64Histogram(p+"get_registration_delay_milliseconds",
|
||||||
metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
|
metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
|
||||||
metric.WithDescription("Duration of how long it takes to load a connection from the registry"),
|
metric.WithDescription("Duration of how long it takes to load a connection from the registry"),
|
||||||
)
|
)
|
||||||
@@ -77,21 +81,21 @@ func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
messagesForwarded, err := meter.Int64Counter("messages_forwarded_total",
|
messagesForwarded, err := meter.Int64Counter(p+"messages_forwarded_total",
|
||||||
metric.WithDescription("Total number of messages forwarded to peers"),
|
metric.WithDescription("Total number of messages forwarded to peers"),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
messageForwardFailures, err := meter.Int64Counter("message_forward_failures_total",
|
messageForwardFailures, err := meter.Int64Counter(p+"message_forward_failures_total",
|
||||||
metric.WithDescription("Total number of message forwarding failures"),
|
metric.WithDescription("Total number of message forwarding failures"),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
messageForwardLatency, err := meter.Float64Histogram("message_forward_latency_milliseconds",
|
messageForwardLatency, err := meter.Float64Histogram(p+"message_forward_latency_milliseconds",
|
||||||
metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
|
metric.WithExplicitBucketBoundaries(getStandardBucketBoundaries()...),
|
||||||
metric.WithDescription("Duration of how long it takes to forward a message to a peer"),
|
metric.WithDescription("Duration of how long it takes to forward a message to a peer"),
|
||||||
)
|
)
|
||||||
@@ -100,7 +104,7 @@ func NewAppMetrics(meter metric.Meter) (*AppMetrics, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
messageSize, err := meter.Int64Histogram(
|
messageSize, err := meter.Int64Histogram(
|
||||||
"message.size.bytes",
|
p+"message.size.bytes",
|
||||||
metric.WithUnit("bytes"),
|
metric.WithUnit("bytes"),
|
||||||
metric.WithExplicitBucketBoundaries(getMessageSizeBucketBoundaries()...),
|
metric.WithExplicitBucketBoundaries(getMessageSizeBucketBoundaries()...),
|
||||||
metric.WithDescription("Records the size of each message sent"),
|
metric.WithDescription("Records the size of each message sent"),
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ type Server struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new Signal server
|
// NewServer creates a new Signal server
|
||||||
func NewServer(ctx context.Context, meter metric.Meter) (*Server, error) {
|
func NewServer(ctx context.Context, meter metric.Meter, metricsPrefix ...string) (*Server, error) {
|
||||||
appMetrics, err := metrics.NewAppMetrics(meter)
|
appMetrics, err := metrics.NewAppMetrics(meter, metricsPrefix...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("creating app metrics: %v", err)
|
return nil, fmt.Errorf("creating app metrics: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func NewServer(conns []*net.UDPConn, logLevel string) *Server {
|
|||||||
// Use the formatter package to set up formatter, ReportCaller, and context hook
|
// Use the formatter package to set up formatter, ReportCaller, and context hook
|
||||||
formatter.SetTextFormatter(stunLogger)
|
formatter.SetTextFormatter(stunLogger)
|
||||||
|
|
||||||
logger := stunLogger.WithField("component", "stun-server")
|
logger := stunLogger.WithField("component", "stun")
|
||||||
logger.Infof("STUN server log level set to: %s", level.String())
|
logger.Infof("STUN server log level set to: %s", level.String())
|
||||||
|
|
||||||
return &Server{
|
return &Server{
|
||||||
|
|||||||
Reference in New Issue
Block a user