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"`
// 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>/...
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"`
// HTTP/3 cover for DPI resistance.
// Domain for Let's Encrypt certificate. If empty, HTTP/3 server is disabled.
Domain string `yaml:"domain"`
// Directory for cached TLS certificates.
CertCache string `yaml:"cert_cache"`
// HTTP port for ACME HTTP-01 challenges.
ACMEHTTPPort int `yaml:"acme_http_port"`
// 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 ShadowsocksConfig struct {
// 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.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
}
if c.AmneziaWG.CertCache == "" {
c.AmneziaWG.CertCache = "/var/lib/shroud/certs"
}
if c.AmneziaWG.ACMEHTTPPort == 0 {
c.AmneziaWG.ACMEHTTPPort = 80
}
}
}
// 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)
}