package config import ( "fmt" "os" "time" "github.com/pelletier/go-toml/v2" ) type Config struct { Listeners []Listener `toml:"listeners"` Database Database `toml:"database"` GRPC GRPC `toml:"grpc"` Firewall Firewall `toml:"firewall"` Proxy Proxy `toml:"proxy"` Log Log `toml:"log"` } type Database struct { Path string `toml:"path"` } type GRPC struct { Addr string `toml:"addr"` TLSCert string `toml:"tls_cert"` TLSKey string `toml:"tls_key"` CACert string `toml:"ca_cert"` // CA cert for verifying the server (client-side) ClientCA string `toml:"client_ca"` // CA cert for verifying clients (server-side mTLS) } type Listener struct { Addr string `toml:"addr"` Routes []Route `toml:"routes"` } type Route struct { Hostname string `toml:"hostname"` Backend string `toml:"backend"` } type Firewall struct { GeoIPDB string `toml:"geoip_db"` BlockedIPs []string `toml:"blocked_ips"` BlockedCIDRs []string `toml:"blocked_cidrs"` BlockedCountries []string `toml:"blocked_countries"` } type Proxy struct { ConnectTimeout Duration `toml:"connect_timeout"` IdleTimeout Duration `toml:"idle_timeout"` ShutdownTimeout Duration `toml:"shutdown_timeout"` } type Log struct { Level string `toml:"level"` } // Duration wraps time.Duration for TOML string unmarshalling. type Duration struct { time.Duration } func (d *Duration) UnmarshalText(text []byte) error { var err error d.Duration, err = time.ParseDuration(string(text)) return err } func Load(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading config: %w", err) } var cfg Config if err := toml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parsing config: %w", err) } if err := cfg.validate(); err != nil { return nil, fmt.Errorf("invalid config: %w", err) } return &cfg, nil } func (c *Config) validate() error { if c.Database.Path == "" { return fmt.Errorf("database.path is required") } // Validate listeners if provided (used for seeding on first run). for i, l := range c.Listeners { if l.Addr == "" { return fmt.Errorf("listener %d: addr is required", i) } seen := make(map[string]bool) for j, r := range l.Routes { if r.Hostname == "" { return fmt.Errorf("listener %d (%s), route %d: hostname is required", i, l.Addr, j) } if r.Backend == "" { return fmt.Errorf("listener %d (%s), route %d: backend is required", i, l.Addr, j) } if seen[r.Hostname] { return fmt.Errorf("listener %d (%s), route %d: duplicate hostname %q", i, l.Addr, j, r.Hostname) } seen[r.Hostname] = true } } if len(c.Firewall.BlockedCountries) > 0 && c.Firewall.GeoIPDB == "" { return fmt.Errorf("firewall: geoip_db is required when blocked_countries is set") } return nil }