Files
MrAlders0n 768889243c release: v2.0.3 (#215)
* chore: add .github/CODEOWNERS for main branch policy

Signed-off-by: MrAlders0n <MrAlders0n@smal.ca>

* chore: add .github/workflows/main-source-guard.yml for main branch policy

Signed-off-by: MrAlders0n <MrAlders0n@smal.ca>

* feat(meshmapper): authenticate API imports (#211)

* feat(observers): add traffic-sorted directory (#212)

* fix(observers): rank available traffic over rolling seven days

* chore: bump version to 2.1.0

* chore: set release version to 2.0.2

* feat(api): add /info with mobile min app version (#214)

* chore: bump version to 2.0.3

---------

Signed-off-by: MrAlders0n <MrAlders0n@smal.ca>
2026-10-06 21:53:30 -04:00

660 lines
25 KiB
Go

// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
// Package config loads the Beacon configuration file and seeds the database
// with regions, IATA overrides, and channel keys on startup.
package config
import (
"fmt"
"net/netip"
"net/url"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"time"
"unicode"
"gopkg.in/yaml.v3"
)
// Config is the top-level structure of the Beacon config file.
type Config struct {
Auth AuthConfig `yaml:"auth"`
Backup BackupConfig `yaml:"backup"`
Log LogConfig `yaml:"log"`
Server ServerConfig `yaml:"server"`
IATAs map[string]IATAConfig `yaml:"iatas"`
Regions []RegionConfig `yaml:"regions"`
ChannelKeys ChannelKeysConfig `yaml:"channel_keys"`
Telemetry TelemetryConfig `yaml:"telemetry"`
WebSocket WebSocketConfig `yaml:"websocket"`
Packets PacketsConfig `yaml:"packets"`
Routes RoutesConfig `yaml:"routes"`
Ingest IngestFilterConfig `yaml:"ingest"`
Scopes []ScopeConfig `yaml:"scopes"`
MeshMapper MeshMapperConfig `yaml:"meshmapper"`
Cache CacheConfig `yaml:"cache"`
CORS CORSConfig `yaml:"cors"`
RateLimit RateLimitConfig `yaml:"ratelimit"`
Background BackgroundConfig `yaml:"background"`
Presence PresenceConfig `yaml:"presence"`
Nodes NodesConfig `yaml:"nodes"`
Observers ObserversConfig `yaml:"observers"`
Analytics AnalyticsConfig `yaml:"analytics"`
Mobile MobileConfig `yaml:"mobile"`
}
// MobileConfig holds settings the BEACON Mobile app reads from /info.
type MobileConfig struct {
// MinAppVersion is the oldest app version (X.Y.Z) allowed to use this server; empty means any.
MinAppVersion string `yaml:"min_app_version"`
}
// BackupConfig enables protected downloads. Disabled by default.
type BackupConfig struct {
Enabled bool `yaml:"enabled"`
}
// AuthConfig holds the operator key for the protected admin subtree.
// The key is excluded from JSON; it must not be exposed by configuration APIs.
type AuthConfig struct {
APIKey string `yaml:"api_key" json:"-"`
}
// LogConfig controls application verbosity and stderr output format.
type LogConfig struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
}
// ServerConfig controls which direct peers may supply the client address.
type ServerConfig struct {
// TrustedProxies accepts IPv4/IPv6 CIDRs; an empty list trusts no proxy.
// netip.Prefix validates each CIDR while the configuration is loaded.
TrustedProxies []netip.Prefix `yaml:"trusted_proxies"`
}
func (c *ServerConfig) UnmarshalYAML(node *yaml.Node) error {
// Pointers retain null list entries, which yaml would otherwise discard.
// Leave them as invalid prefixes for Load's validation below.
var raw struct {
TrustedProxies []*netip.Prefix `yaml:"trusted_proxies"`
}
if err := node.Decode(&raw); err != nil {
return err
}
c.TrustedProxies = make([]netip.Prefix, len(raw.TrustedProxies))
for i, prefix := range raw.TrustedProxies {
if prefix != nil {
c.TrustedProxies[i] = *prefix
}
}
return nil
}
// ResolvedConfig holds all runtime configuration with defaults applied.
type ResolvedConfig struct {
RateLimit ResolvedRateLimitConfig
TelemetryResolution time.Duration
TelemetryRetention time.Duration
RollupRetention time.Duration
PacketRetention time.Duration
RouteRetention time.Duration
RouteGrace time.Duration
RouteMinObservations int
MaxConnsPerIP int
MaxConnectsPerMinute int
ViewRefreshInterval time.Duration
ReconfirmInterval time.Duration
CleanupInterval time.Duration
PresenceFlushInterval time.Duration
PresencePacketTTL time.Duration
// ClockDriftThreshold is the |device clock - server clock| magnitude, measured from a
// node's ADVERT timestamp, above which the node API reports clockOutOfSync=true for
// that node. Only meaningful for repeaters/room servers (nodeType 2/3).
ClockDriftThreshold time.Duration
// NodeStaleThreshold and NodeDeleteAfter mirror ClockDriftThreshold's "0 means unset,
// resolve to a default" pattern -- see NodesConfig.
NodeStaleThreshold time.Duration
NodeDeleteAfter time.Duration
ObserverDeleteAfter time.Duration
}
// RateLimitConfig controls the per-client REST API request budget.
type RateLimitConfig struct {
Enabled *bool `yaml:"enabled"` // Defaults to true; false disables both windows.
RequestsPerMinute int `yaml:"requests_per_minute"`
Burst int `yaml:"burst"` // One-second window cap, not token-bucket capacity.
}
// ResolvedRateLimitConfig has defaults applied and no optional values.
type ResolvedRateLimitConfig struct {
Enabled bool
RequestsPerMinute int
Burst int
}
// PresenceConfig controls coalescing of presence bookkeeping writes
// (observer last_seen, observer_brokers, packet last_heard_at bumps).
type PresenceConfig struct {
// FlushInterval is how often coalesced bumps are flushed to Postgres.
// Defaults to 30s if not set.
FlushInterval duration `yaml:"flush_interval"`
// PacketTTL is how long a packet hash with no re-observations stays
// coalesced before the next observation writes through again.
// Defaults to 30s if not set.
PacketTTL duration `yaml:"packet_ttl"`
}
// BackgroundConfig controls the intervals for background maintenance tasks.
type BackgroundConfig struct {
// ViewRefresh is how often materialized views are refreshed.
// Defaults to 1h if not set.
ViewRefresh duration `yaml:"view_refresh"`
// Reconfirm prunes stale and ambiguous resolved paths and neigbors.
Reconfirm duration `yaml:"reconfirm"`
// Cleanup is how often old telemetry and packet rows are pruned.
// Defaults to 1h if not set.
Cleanup duration `yaml:"cleanup"`
}
// CORSConfig controls Cross-Origin Resource Sharing behaviour.
// If omitted, Beacon defaults to allowing all origins, which is appropriate
// for a public read-only API. Operators exposing write endpoints should
// restrict AllowedOrigins to known frontends.
type CORSConfig struct {
// AllowedOrigins is the list of origins permitted to make cross-origin
// requests. Use ["*"] to allow all origins (default if omitted).
AllowedOrigins []string `yaml:"allowed_origins"`
// AllowedMethods is the list of HTTP methods allowed in CORS requests.
// Defaults to [GET, HEAD, OPTIONS] if omitted.
AllowedMethods []string `yaml:"allowed_methods"`
// AllowedHeaders is the list of request headers allowed in CORS requests.
// Defaults to [Accept, Authorization, Content-Type] if omitted.
AllowedHeaders []string `yaml:"allowed_headers"`
// AllowCredentials indicates whether the request can include user
// credentials (cookies, HTTP authentication). Defaults to false.
AllowCredentials bool `yaml:"allow_credentials"`
// MaxAge is the number of seconds the browser may cache a preflight
// response. Defaults to 300 if omitted.
MaxAge int `yaml:"max_age"`
}
// CacheConfig controls Redis caching behaviour.
// If Addr is not set (via REDIS_ADDR env var), caching is disabled entirely
// and Beacon falls back to querying PostgreSQL directly for all reads.
type CacheConfig struct {
// TTL is the default cache entry lifetime applied to all categories
// that do not have an explicit override in TTLs. Defaults to 1h if not set.
TTL duration `yaml:"ttl"`
// TTLs holds optional per-category TTL overrides. Any category left
// at zero will fall back to TTL.
TTLs CacheTTLsConfig `yaml:"ttls"`
}
// CacheTTLsConfig holds per-category TTL overrides for the cache layer.
// Each field is optional — omit it in config to inherit the global TTL.
type CacheTTLsConfig struct {
// Stats controls the TTL for aggregated network statistics endpoints
// (overview, observations, payload breakdown, top nodes/observers, radio presets, scope stats).
// Rollup-backed keys also change with the rollup revision.
Stats duration `yaml:"stats"`
// Reference controls the TTL for mostly-static reference data
// (IATAs, regions, scopes). These change only when new observers
// arrive or config is reseeded.
Reference duration `yaml:"reference"`
// Nodes controls the TTL for individual node detail responses.
// Acts as a safety-net expiry alongside explicit invalidation on upsert.
Nodes duration `yaml:"nodes"`
// Observers controls the TTL for individual observer detail responses.
// Acts as a safety-net expiry alongside explicit invalidation on upsert.
Observers duration `yaml:"observers"`
}
// ScopeConfig defines a regional transport scope.
// Name can be provided with or without the # or $ prefix.
// Beacon normalizes plain names by prepending #.
type ScopeConfig struct {
Name string `yaml:"name"` // e.g. "bc", "#west", "$private"
Region string `yaml:"region"` // configured region slug; lists the scope under that region's IATAs
}
// TelemetryConfig controls observer telemetry storage behaviour.
type TelemetryConfig struct {
// Retention is how long telemetry rows are kept before the cleanup job removes them.
// Defaults to 744h (31 days) if not set.
Retention duration `yaml:"retention"`
// Resolution is how frequently a telemetry snapshot is stored per observer.
// Status messages arriving within the same resolution window are deduplicated.
// Defaults to 1h if not set.
Resolution duration `yaml:"resolution"`
}
// WebSocketConfig controls WebSocket connection behaviour.
// Settings here apply to the /ws endpoint only.
type WebSocketConfig struct {
// MaxConnectionsPerIP is the maximum number of concurrent WebSocket
// connections allowed from a single IP address. Defaults to 5 if not set.
MaxConnectionsPerIP int `yaml:"max_connections_per_ip"`
// MaxConnectsPerMinute limits upgrade attempts, including failed handshakes.
// Zero/omitted defaults to 10; IPv6 addresses share a /64 attempt budget.
MaxConnectsPerMinute int `yaml:"max_connects_per_minute"`
// AllowedOrigins are extra origins allowed to open /ws: exact, or scheme://*.domain[:port]
// for every subdomain of domain (never the apex). Defaults to same-host only.
AllowedOrigins []string `yaml:"allowed_origins"`
}
// PacketsConfig controls packet retention behaviour.
type PacketsConfig struct {
// Retention covers packets, observations and channel messages. Defaults to 168h.
Retention duration `yaml:"retention"`
}
// AnalyticsConfig controls hourly rollup retention behaviour.
type AnalyticsConfig struct {
// RollupRetention is how long hourly rollups are kept and the longest window /stats/series accepts. Defaults to 2160h (90 days).
RollupRetention duration `yaml:"rollup_retention"`
}
// RoutesConfig controls known-route retention behaviour.
type RoutesConfig struct {
// Retention is how long a route is kept after it was last observed.
// Defaults to 336h (14 days) if not set.
Retention duration `yaml:"retention"`
// Grace is how long a route observed fewer than MinObservations times is
// kept. Defaults to 168h (7 days) if not set.
Grace duration `yaml:"grace"`
// MinObservations is the observation count below which Grace applies
// instead of Retention. Defaults to 3 if not set.
MinObservations int `yaml:"min_observations"`
}
// NodesConfig controls node-derived signal thresholds.
type NodesConfig struct {
// MarkForeign annotates repeaters outside the union of IATA borders, with
// imported MeshMapper boundaries replacing border files. Disabled by default;
// enabling it requires a border file or meshmapper.zones.
MarkForeign bool `yaml:"mark_foreign"`
// ClockDriftThreshold is the |device clock - server clock| magnitude, measured from a
// repeater/room server's ADVERT timestamp, above which the node API reports
// clockOutOfSync=true for that node. Defaults to 5m if not set.
ClockDriftThreshold duration `yaml:"clock_drift_threshold"`
// StaleThreshold is how long since a node's last_seen before the node API reports
// stale=true for it. Defaults to 24h if not set.
StaleThreshold duration `yaml:"stale_threshold"`
// DeleteAfter is how long since a node's last_seen before the cleanup job deletes the
// node entirely. Defaults to 720h (30 days) if not set.
DeleteAfter duration `yaml:"delete_after"`
}
// ObserversConfig controls optional observer age-out.
type ObserversConfig struct {
// DeleteAfter is how long an observer must be unseen before it can be deleted.
// Omitted or nonpositive disables age-out. Retained packet observations,
// telemetry and ownership records always protect the observer from deletion.
DeleteAfter duration `yaml:"delete_after"`
}
// duration is a wrapper around time.Duration that supports YAML unmarshalling
// from human-readable strings like "24h", "7d", "30d".
type duration struct {
time.Duration
}
func (d *duration) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
parsed, err := time.ParseDuration(s)
if err != nil {
return err
}
d.Duration = parsed
return nil
}
// ChannelKeysConfig holds both hashtag-derived and explicit channel keys.
// Hashtag keys are derived automatically: secret = SHA256("#tag")[:16],
// channel_hash = SHA256(secret)[0]. Explicit keys are provided as hex strings
// keyed by the channel hash hex (e.g. "11" for 0x11).
type ChannelKeysConfig struct {
// Hashtags lists hashtag names (without the # prefix), each optionally scoped to a region.
// Beacon derives the PSK and channel hash automatically.
Hashtags []HashtagConfig `yaml:"hashtags"`
// Keys maps channel hash hex → explicit key config.
Keys map[string]ExplicitKeyConfig `yaml:"keys"`
}
// ExplicitKeyConfig holds an explicit channel key and optional display name.
type ExplicitKeyConfig struct {
Key string `yaml:"key"` // hex-encoded key bytes
Name string `yaml:"name"` // optional display name
Region string `yaml:"region"` // optional region slug; empty lists the channel Beacon-wide
}
// IATAConfig holds optional overrides for a known IATA code.
type IATAConfig struct {
Name string `yaml:"name"`
Lat *float64 `yaml:"lat"`
Lng *float64 `yaml:"lng"`
// BorderFile is a path to a GeoJSON Feature file (Polygon or MultiPolygon geometry) for
// this IATA's region border map. Relative paths are resolved against the directory
// containing the main config file (see Load). Validated and bbox-computed at seed time --
// see border.go.
BorderFile string `yaml:"borderFile"`
}
// RegionConfig defines a super-region and its member IATAs.
type RegionConfig struct {
Slug string `yaml:"slug"`
Name string `yaml:"name"`
Description string `yaml:"description"`
DisplayOrder int `yaml:"display_order"`
CenterLat *float64 `yaml:"center_lat"`
CenterLng *float64 `yaml:"center_lng"`
ZoomLevel *int `yaml:"zoom_level"`
IATAs []string `yaml:"iatas"`
}
// IngestFilterConfig restricts which packets Beacon stores based on the
// observer's IATA geographic location. Both filters are optional — if neither
// is set all IATAs are accepted. If both are set an IATA passes if it matches
// either (OR semantics).
//
// Country codes are ISO 3166-1 alpha-2 (e.g. "CA", "US").
// Continent codes are two-letter OurAirports codes: AF, AN, AS, EU, NA, OC, SA.
type IngestFilterConfig struct {
// AllowCountries is a list of ISO 3166-1 alpha-2 country codes to accept.
// Packets from observers in other countries are dropped at ingest.
AllowCountries []string `yaml:"allow_countries"`
// AllowContinents is a list of continent codes to accept.
// Packets from observers in other continents are dropped at ingest.
AllowContinents []string `yaml:"allow_continents"`
}
// Load reads and parses the config file at path.
// Returns an empty Config (not an error) if the file does not exist,
// so Beacon starts cleanly without a config file.
func Load(path string) (*Config, error) {
cfg := &Config{}
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
cfg.MeshMapper.APIKey = strings.TrimSpace(os.Getenv("MESHMAPPER_API_KEY"))
cfg.Auth.APIKey = os.Getenv("BEACON_API_KEY")
if err := cfg.validateAPIKey(); err != nil {
return nil, err
}
return cfg, nil
}
return nil, err
}
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil, err
}
if value, set := os.LookupEnv("BEACON_API_KEY"); set {
cfg.Auth.APIKey = value
}
if value, set := os.LookupEnv("MESHMAPPER_API_KEY"); set {
cfg.MeshMapper.APIKey = value
}
cfg.MeshMapper.APIKey = strings.TrimSpace(cfg.MeshMapper.APIKey)
if err := cfg.validateAPIKey(); err != nil {
return nil, err
}
if err := cfg.validateDurations(); err != nil {
return nil, err
}
for i, prefix := range cfg.Server.TrustedProxies {
if !prefix.IsValid() {
return nil, fmt.Errorf("server.trusted_proxies[%d] must be a valid CIDR", i)
}
}
if cfg.RateLimit.RequestsPerMinute < 0 || cfg.RateLimit.Burst < 0 {
return nil, fmt.Errorf("ratelimit.requests_per_minute and ratelimit.burst must be positive or zero for defaults")
}
if cfg.WebSocket.MaxConnectsPerMinute < 0 {
return nil, fmt.Errorf("websocket.max_connects_per_minute must be positive or zero for the default")
}
for i, origin := range cfg.WebSocket.AllowedOrigins {
if err := validateOrigin(origin); err != nil {
return nil, fmt.Errorf("websocket.allowed_origins[%d]: %w", i, err)
}
}
if d := cfg.Packets.Retention.Duration; d != 0 && d < 24*time.Hour {
return nil, fmt.Errorf("packets.retention must be at least 24h")
}
if d := cfg.Analytics.RollupRetention.Duration; d != 0 && d < 24*time.Hour {
return nil, fmt.Errorf("analytics.rollup_retention must be at least 24h")
}
configDir := filepath.Dir(path)
if err := cfg.validateMeshMapper(); err != nil {
return nil, err
}
if err := cfg.validateChannelKeys(); err != nil {
return nil, err
}
if err := cfg.validateScopes(); err != nil {
return nil, err
}
if err := cfg.validateMobile(); err != nil {
return nil, err
}
if cfg.Nodes.MarkForeign && !cfg.MeshMapper.Zones.Enabled && !cfg.hasBorderFile() {
return nil, fmt.Errorf("nodes.mark_foreign requires an iatas.*.borderFile or meshmapper.zones")
}
for iata, details := range cfg.IATAs {
if details.BorderFile != "" && !filepath.IsAbs(details.BorderFile) {
details.BorderFile = filepath.Join(configDir, details.BorderFile)
cfg.IATAs[iata] = details
}
}
return cfg, nil
}
// minAPIKeyLength keeps the admin key out of guessing range.
const minAPIKeyLength = 16
// validateAPIKey trims the key, since Bearer tokens can't carry whitespace; errors never echo it.
func (c *Config) validateAPIKey() error {
c.Auth.APIKey = strings.TrimSpace(c.Auth.APIKey)
switch key := c.Auth.APIKey; {
case key == "":
return nil
case strings.IndexFunc(key, unicode.IsSpace) >= 0:
return fmt.Errorf("auth.api_key / BEACON_API_KEY must not contain whitespace")
case len(key) < minAPIKeyLength:
return fmt.Errorf("auth.api_key / BEACON_API_KEY must be at least %d characters", minAPIKeyLength)
}
return nil
}
var appVersionPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+$`)
// validateMobile keeps min_app_version to plain X.Y.Z so every app can compare it.
func (c *Config) validateMobile() error {
c.Mobile.MinAppVersion = strings.TrimSpace(c.Mobile.MinAppVersion)
if v := c.Mobile.MinAppVersion; v != "" && !appVersionPattern.MatchString(v) {
return fmt.Errorf("mobile.min_app_version must be X.Y.Z (digits only), got %q", v)
}
return nil
}
// validateDurations rejects negative intervals, which would otherwise panic tickers or invert cutoffs.
// observers.delete_after is exempt: nonpositive disables it.
func (c *Config) validateDurations() error {
for _, d := range []struct {
key string
value duration
}{
{"presence.flush_interval", c.Presence.FlushInterval},
{"presence.packet_ttl", c.Presence.PacketTTL},
{"background.view_refresh", c.Background.ViewRefresh},
{"background.reconfirm", c.Background.Reconfirm},
{"background.cleanup", c.Background.Cleanup},
{"cache.ttl", c.Cache.TTL},
{"cache.ttls.stats", c.Cache.TTLs.Stats},
{"cache.ttls.reference", c.Cache.TTLs.Reference},
{"cache.ttls.nodes", c.Cache.TTLs.Nodes},
{"cache.ttls.observers", c.Cache.TTLs.Observers},
{"telemetry.retention", c.Telemetry.Retention},
{"telemetry.resolution", c.Telemetry.Resolution},
{"routes.retention", c.Routes.Retention},
{"routes.grace", c.Routes.Grace},
{"nodes.clock_drift_threshold", c.Nodes.ClockDriftThreshold},
{"nodes.stale_threshold", c.Nodes.StaleThreshold},
{"nodes.delete_after", c.Nodes.DeleteAfter},
{"meshmapper.scopes.refresh_interval", c.MeshMapper.Scopes.RefreshInterval},
{"meshmapper.zones.refresh_interval", c.MeshMapper.Zones.RefreshInterval},
{"meshmapper.channels.refresh_interval", c.MeshMapper.Channels.RefreshInterval},
} {
if d.value.Duration < 0 {
return fmt.Errorf("%s must not be negative (omit it or use 0 for the default)", d.key)
}
}
return nil
}
// validateOrigin allows exact origins or a leading "*." subdomain wildcard; the WebSocket
// library treats entries as path.Match patterns, so every other pattern character is rejected.
func validateOrigin(origin string) error {
u, err := url.Parse(origin)
if err != nil {
return fmt.Errorf("invalid origin %q: %w", origin, err)
}
host := u.Hostname()
// At least two labels after the wildcard, so it never spans a whole TLD.
if rest, ok := strings.CutPrefix(host, "*."); ok {
if labels := strings.Split(rest, "."); len(labels) >= 2 && !slices.Contains(labels, "") {
host = rest
}
}
if (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" || u.User != nil ||
u.Path != "" || u.RawQuery != "" || u.Fragment != "" || u.Opaque != "" ||
strings.Contains(host, "*") || strings.ContainsAny(origin, `?[]\`) {
return fmt.Errorf("origin %q must be scheme://host[:port] or scheme://*.domain[:port] with an http or https scheme", origin)
}
return nil
}
// Resolve returns a ResolvedConfig with defaults applied for any zero values.
func Resolve(cfg *Config) ResolvedConfig {
r := ResolvedConfig{
RateLimit: ResolvedRateLimitConfig{
Enabled: cfg.RateLimit.Enabled == nil || *cfg.RateLimit.Enabled,
RequestsPerMinute: cfg.RateLimit.RequestsPerMinute,
Burst: cfg.RateLimit.Burst,
},
TelemetryResolution: cfg.Telemetry.Resolution.Duration,
TelemetryRetention: cfg.Telemetry.Retention.Duration,
RollupRetention: cfg.Analytics.RollupRetention.Duration,
PacketRetention: cfg.Packets.Retention.Duration,
RouteRetention: cfg.Routes.Retention.Duration,
RouteGrace: cfg.Routes.Grace.Duration,
RouteMinObservations: cfg.Routes.MinObservations,
MaxConnsPerIP: cfg.WebSocket.MaxConnectionsPerIP,
MaxConnectsPerMinute: cfg.WebSocket.MaxConnectsPerMinute,
ViewRefreshInterval: cfg.Background.ViewRefresh.Duration,
ReconfirmInterval: cfg.Background.Reconfirm.Duration,
CleanupInterval: cfg.Background.Cleanup.Duration,
PresenceFlushInterval: cfg.Presence.FlushInterval.Duration,
PresencePacketTTL: cfg.Presence.PacketTTL.Duration,
ClockDriftThreshold: cfg.Nodes.ClockDriftThreshold.Duration,
NodeStaleThreshold: cfg.Nodes.StaleThreshold.Duration,
NodeDeleteAfter: cfg.Nodes.DeleteAfter.Duration,
ObserverDeleteAfter: cfg.Observers.DeleteAfter.Duration,
}
if r.RateLimit.RequestsPerMinute == 0 {
r.RateLimit.RequestsPerMinute = 300
}
if r.RateLimit.Burst == 0 {
r.RateLimit.Burst = r.RateLimit.RequestsPerMinute
}
if r.TelemetryResolution == 0 {
r.TelemetryResolution = time.Hour
}
if r.TelemetryRetention == 0 {
r.TelemetryRetention = 31 * 24 * time.Hour
}
if r.RollupRetention == 0 {
r.RollupRetention = 90 * 24 * time.Hour
}
if r.PacketRetention == 0 {
r.PacketRetention = 7 * 24 * time.Hour
}
if r.RouteRetention == 0 {
r.RouteRetention = 14 * 24 * time.Hour
}
if r.RouteGrace == 0 {
r.RouteGrace = 7 * 24 * time.Hour
}
if r.RouteMinObservations == 0 {
r.RouteMinObservations = 3
}
if r.MaxConnsPerIP == 0 {
r.MaxConnsPerIP = 5
}
if r.MaxConnectsPerMinute == 0 {
r.MaxConnectsPerMinute = 10
}
if r.ViewRefreshInterval == 0 {
r.ViewRefreshInterval = time.Hour
}
if r.ReconfirmInterval == 0 {
r.ReconfirmInterval = time.Hour
}
if r.CleanupInterval == 0 {
r.CleanupInterval = time.Hour
}
if r.PresenceFlushInterval == 0 {
r.PresenceFlushInterval = 30 * time.Second
}
if r.PresencePacketTTL == 0 {
r.PresencePacketTTL = 30 * time.Second
}
if r.ClockDriftThreshold == 0 {
r.ClockDriftThreshold = 5 * time.Minute
}
if r.NodeStaleThreshold == 0 {
r.NodeStaleThreshold = 24 * time.Hour
}
if r.NodeDeleteAfter == 0 {
r.NodeDeleteAfter = 30 * 24 * time.Hour
}
return r
}
func (r ResolvedConfig) String() string {
return fmt.Sprintf(
"telemetryResolution=%s telemetryRetention=%s rollupRetention=%s packetRetention=%s routeRetention=%s routeGrace=%s routeMinObs=%d maxConnsPerIP=%d maxConnectsPerMinute=%d viewRefresh=%s reconfirm=%s cleanup=%s presenceFlush=%s presencePacketTTL=%s clockDriftThreshold=%s nodeStaleThreshold=%s nodeDeleteAfter=%s observerDeleteAfter=%s rateLimitEnabled=%t requestsPerMinute=%d burst=%d",
r.TelemetryResolution, r.TelemetryRetention, r.RollupRetention, r.PacketRetention, r.RouteRetention, r.RouteGrace, r.RouteMinObservations,
r.MaxConnsPerIP, r.MaxConnectsPerMinute, r.ViewRefreshInterval, r.ReconfirmInterval, r.CleanupInterval,
r.PresenceFlushInterval, r.PresencePacketTTL, r.ClockDriftThreshold,
r.NodeStaleThreshold, r.NodeDeleteAfter, r.ObserverDeleteAfter,
r.RateLimit.Enabled, r.RateLimit.RequestsPerMinute, r.RateLimit.Burst,
)
}