package config import ( "fmt" "os" "path/filepath" "gopkg.in/yaml.v3" ) type Config struct { // Server identification and display. Server ServerConfig `yaml:"server"` // REST API settings. API APIConfig `yaml:"api"` // Prometheus metrics endpoint settings. Metrics MetricsConfig `yaml:"metrics"` // Shadowsocks proxy settings. Shadowsocks ShadowsocksConfig `yaml:"shadowsocks"` // AmneziaWG VPN settings. AmneziaWG AmneziaWGConfig `yaml:"amneziawg"` // ACME (Let's Encrypt) certificate settings. ACME ACMEConfig `yaml:"acme"` // Path to persistent state file. // Defaults to "state.yaml" in the same directory as the config file. State string `yaml:"state_file"` // configDir is the directory containing the config file, used for resolving relative paths. configDir string } type ServerConfig struct { // Human-readable server name. Name string `yaml:"name"` // Unique server identifier (auto-generated if empty). ID string `yaml:"id"` // Hostname or IP used in access key URLs. Hostname string `yaml:"hostname"` } type APIConfig struct { // Listen address for the management API (e.g., ":8081"). ListenAddr string `yaml:"listen_addr"` // Secret prefix for URL-based authentication (e.g., "SecretPath"). // API will be served at //... Secret string `yaml:"secret"` // Path to TLS certificate file (optional, used for certSha256 in access.yaml). CertFile string `yaml:"cert_file"` } type MetricsConfig struct { // Listen address for the Prometheus /metrics and /healthz endpoint. // Defaults to "127.0.0.1:8081". ListenAddr string `yaml:"listen_addr"` // Node exporter collectors to enable (e.g., [cpu, meminfo, diskstats]). // Empty means no node_exporter collectors. NodeExporterCollectors []string `yaml:"node_exporter_collectors"` } type AmneziaWGConfig struct { // Enable AmneziaWG protocol. Enabled bool `yaml:"enabled"` // UDP listen port for AWG + HTTP/3 mux. ListenPort int `yaml:"listen_port"` // TUN device name. TUNName string `yaml:"tun_name"` // Server subnet in CIDR notation (e.g., "10.14.0.0/24"). // Server takes the first usable IP (.1), peers are allocated from .2 onwards. Address string `yaml:"address"` // TUN MTU. MTU int `yaml:"mtu"` // Server interface private key (base64). Auto-generated if empty. PrivateKey string `yaml:"private_key"` // DNS server for client configs. DNS string `yaml:"dns"` // Hostname for AWG client endpoint. If empty and muxer is enabled, falls back to server.hostname. Hostname string `yaml:"hostname"` // HTTP/3 + AWG muxer on the same UDP port. // When enabled, AWG and QUIC/HTTP3 share the listen port. // nil = auto (enabled when domain is set), true = force on, false = force off. MuxEnabled *bool `yaml:"mux_enabled"` // HTTP/3 cover for DPI resistance. // Domain for Let's Encrypt certificate. Defaults to server.hostname if empty. // If still empty after defaulting, HTTP/3 server is disabled. Domain string `yaml:"domain"` // Obfuscation parameters. Jc int `yaml:"jc"` Jmin int `yaml:"jmin"` Jmax int `yaml:"jmax"` S1 int `yaml:"s1"` S2 int `yaml:"s2"` S3 int `yaml:"s3"` S4 int `yaml:"s4"` H1 string `yaml:"h1"` // "min-max" or single value H2 string `yaml:"h2"` H3 string `yaml:"h3"` H4 string `yaml:"h4"` } type ACMEConfig struct { // Directory for cached TLS certificates. CertCache string `yaml:"cert_cache"` // HTTP port for ACME HTTP-01 challenges. HTTPPort int `yaml:"http_port"` } type ShadowsocksConfig struct { // Enable Shadowsocks protocol. Defaults to true. Enabled *bool `yaml:"enabled"` // Default port for new access keys. 0 means pick a random unused port on first start. DefaultPort int `yaml:"default_port"` // Default cipher for new access keys. DefaultCipher string `yaml:"default_cipher"` // UDP NAT timeout as a duration string (e.g., "5m"). NATTimeout string `yaml:"nat_timeout"` // Replay protection history size (0 = disabled). ReplayHistory int `yaml:"replay_history"` // Path to IP-to-country MaxMind MMDB file. IPCountryDB string `yaml:"ip_country_db"` // Path to IP-to-ASN MaxMind MMDB file. IPASNDB string `yaml:"ip_asn_db"` } func Load(filename string) (*Config, error) { absPath, err := filepath.Abs(filename) if err != nil { return nil, fmt.Errorf("resolving config path: %w", err) } data, err := os.ReadFile(absPath) if err != nil { return nil, fmt.Errorf("reading config file: %w", err) } cfg := &Config{ configDir: filepath.Dir(absPath), } if err := yaml.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("parsing config: %w", err) } cfg.setDefaults() return cfg, nil } func (c *Config) setDefaults() { if c.API.ListenAddr == "" { c.API.ListenAddr = ":8081" } if c.Metrics.ListenAddr == "" { c.Metrics.ListenAddr = "127.0.0.1:8081" } if c.Shadowsocks.Enabled == nil { t := true c.Shadowsocks.Enabled = &t } if c.Shadowsocks.DefaultCipher == "" { c.Shadowsocks.DefaultCipher = "chacha20-ietf-poly1305" } if c.Shadowsocks.NATTimeout == "" { c.Shadowsocks.NATTimeout = "5m" } if c.Server.Name == "" { c.Server.Name = "Outline Server" } if c.State == "" { c.State = "state.yaml" } // AmneziaWG defaults. if c.AmneziaWG.Enabled { if c.AmneziaWG.ListenPort == 0 { c.AmneziaWG.ListenPort = 443 } if c.AmneziaWG.TUNName == "" { c.AmneziaWG.TUNName = "awg0" } if c.AmneziaWG.MTU == 0 { c.AmneziaWG.MTU = 1420 } } // AmneziaWG domain defaults to server hostname. if c.AmneziaWG.Enabled && c.AmneziaWG.Domain == "" { c.AmneziaWG.Domain = c.Server.Hostname } // ACME defaults. if c.ACME.CertCache == "" { c.ACME.CertCache = "/var/lib/shroud/certs" } if c.ACME.HTTPPort == 0 { c.ACME.HTTPPort = 80 } } // AWGMuxEnabled returns whether the AWG+HTTP/3 muxer is enabled. // If MuxEnabled is explicitly set, that value is used. // Otherwise, muxer is enabled when a domain is configured. func (c *AmneziaWGConfig) AWGMuxEnabled() bool { if c.MuxEnabled != nil { return *c.MuxEnabled } return c.Domain != "" } // AWGHostname returns the hostname for AWG client endpoints. // Uses awg.hostname if set; falls back to serverHostname when muxer is enabled. func (c *AmneziaWGConfig) AWGHostname(serverHostname string) string { if c.Hostname != "" { return c.Hostname } if c.AWGMuxEnabled() { return serverHostname } return "" } // StateFile returns the absolute path to the state file. func (c *Config) StateFile() string { if filepath.IsAbs(c.State) { return c.State } return filepath.Join(c.configDir, c.State) }