Rename proto/gen directories from mc-proxy to mc_proxy for valid protobuf package naming. Add CLI status subcommand for querying running instance health via gRPC. Add systemd backup service/timer and backup pruning script. Add buf.yaml and proto-lint Makefile target. Add shutdown_timeout config field. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
119 lines
2.8 KiB
Go
119 lines
2.8 KiB
Go
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
|
|
}
|