163 lines
5.0 KiB
Go
163 lines
5.0 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
DefaultHTTPListen = "0.0.0.0:8080"
|
|
DefaultControlListen = "10.88.0.1:7001"
|
|
DefaultWireGuardPort = 51820
|
|
DefaultDataDirectory = "./data"
|
|
DefaultOverlayCIDR = "10.88.0.0/16"
|
|
DefaultServerOverlayIP = "10.88.0.1"
|
|
DefaultSessionUDPPort = 6200
|
|
DefaultMTU = 1280
|
|
)
|
|
|
|
// ServerConfig contains Server bootstrap values and initial network defaults.
|
|
type ServerConfig struct {
|
|
Server ServerListeners `yaml:"server"`
|
|
Data DataConfig `yaml:"data"`
|
|
Network NetworkConfig `yaml:"network"`
|
|
}
|
|
|
|
// ServerListeners defines process listen endpoints.
|
|
type ServerListeners struct {
|
|
HTTPListen string `yaml:"http_listen"`
|
|
ControlListen string `yaml:"control_listen"`
|
|
WireGuardPort int `yaml:"wireguard_port"`
|
|
}
|
|
|
|
// DataConfig defines the Server persistence directory.
|
|
type DataConfig struct {
|
|
Directory string `yaml:"directory"`
|
|
}
|
|
|
|
// NetworkConfig supplies initial values later stored in Server SQLite.
|
|
type NetworkConfig struct {
|
|
OverlayCIDR string `yaml:"overlay_cidr"`
|
|
ServerOverlayIP string `yaml:"server_overlay_ip"`
|
|
SessionUDPPort int `yaml:"session_udp_port"`
|
|
MTU int `yaml:"mtu"`
|
|
}
|
|
|
|
// DefaultServerConfig returns the Appendix B defaults.
|
|
func DefaultServerConfig() ServerConfig {
|
|
return ServerConfig{
|
|
Server: ServerListeners{
|
|
HTTPListen: DefaultHTTPListen,
|
|
ControlListen: DefaultControlListen,
|
|
WireGuardPort: DefaultWireGuardPort,
|
|
},
|
|
Data: DataConfig{Directory: DefaultDataDirectory},
|
|
Network: NetworkConfig{
|
|
OverlayCIDR: DefaultOverlayCIDR,
|
|
ServerOverlayIP: DefaultServerOverlayIP,
|
|
SessionUDPPort: DefaultSessionUDPPort,
|
|
MTU: DefaultMTU,
|
|
},
|
|
}
|
|
}
|
|
|
|
// LoadServer loads one strict YAML document and validates the result.
|
|
func LoadServer(path string) (ServerConfig, error) {
|
|
config := DefaultServerConfig()
|
|
if err := decodeStrict(path, &config); err != nil {
|
|
return ServerConfig{}, err
|
|
}
|
|
if err := config.Validate(); err != nil {
|
|
return ServerConfig{}, fmt.Errorf("validate server config %q: %w", path, err)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
// Validate checks Phase 0 invariants without changing the host network.
|
|
func (c ServerConfig) Validate() error {
|
|
if _, err := validateIPv4Listen("server.http_listen", c.Server.HTTPListen); err != nil {
|
|
return err
|
|
}
|
|
controlIP, err := validateIPv4Listen("server.control_listen", c.Server.ControlListen)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := validatePort("server.wireguard_port", c.Server.WireGuardPort); err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimSpace(c.Data.Directory) == "" {
|
|
return fmt.Errorf("data.directory must not be empty")
|
|
}
|
|
|
|
prefix, err := netip.ParsePrefix(c.Network.OverlayCIDR)
|
|
if err != nil || !prefix.Addr().Is4() {
|
|
return fmt.Errorf("network.overlay_cidr must be a valid IPv4 CIDR")
|
|
}
|
|
if prefix != prefix.Masked() {
|
|
return fmt.Errorf("network.overlay_cidr must use its network address: got %s", prefix)
|
|
}
|
|
if prefix.Bits() == 0 {
|
|
return fmt.Errorf("network.overlay_cidr must not enable 0.0.0.0/0 Exit Node routing")
|
|
}
|
|
if prefix.Bits() > 30 {
|
|
return fmt.Errorf("network.overlay_cidr must leave addresses for the Server and at least one Node")
|
|
}
|
|
|
|
serverIP, err := netip.ParseAddr(c.Network.ServerOverlayIP)
|
|
if err != nil || !serverIP.Is4() {
|
|
return fmt.Errorf("network.server_overlay_ip must be a valid IPv4 address")
|
|
}
|
|
if !prefix.Contains(serverIP) {
|
|
return fmt.Errorf("network.server_overlay_ip must belong to network.overlay_cidr")
|
|
}
|
|
if serverIP == prefix.Addr() || serverIP == lastIPv4(prefix) {
|
|
return fmt.Errorf("network.server_overlay_ip must not be the network or broadcast address")
|
|
}
|
|
if controlIP != serverIP {
|
|
return fmt.Errorf("server.control_listen must bind network.server_overlay_ip")
|
|
}
|
|
if err := validatePort("network.session_udp_port", c.Network.SessionUDPPort); err != nil {
|
|
return err
|
|
}
|
|
if c.Network.MTU < 576 || c.Network.MTU > 65535 {
|
|
return fmt.Errorf("network.mtu must be between 576 and 65535")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateIPv4Listen(field, value string) (netip.Addr, error) {
|
|
host, portText, err := net.SplitHostPort(value)
|
|
if err != nil {
|
|
return netip.Addr{}, fmt.Errorf("%s must be an IPv4 host:port: %w", field, err)
|
|
}
|
|
address, err := netip.ParseAddr(host)
|
|
if err != nil || !address.Is4() {
|
|
return netip.Addr{}, fmt.Errorf("%s must use an IPv4 address", field)
|
|
}
|
|
port, err := strconv.Atoi(portText)
|
|
if err != nil {
|
|
return netip.Addr{}, fmt.Errorf("%s has an invalid port", field)
|
|
}
|
|
if err := validatePort(field, port); err != nil {
|
|
return netip.Addr{}, err
|
|
}
|
|
return address, nil
|
|
}
|
|
|
|
func validatePort(field string, port int) error {
|
|
if port < 1 || port > 65535 {
|
|
return fmt.Errorf("%s must be between 1 and 65535", field)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func lastIPv4(prefix netip.Prefix) netip.Addr {
|
|
bytes := prefix.Addr().As4()
|
|
value := uint32(bytes[0])<<24 | uint32(bytes[1])<<16 | uint32(bytes[2])<<8 | uint32(bytes[3])
|
|
value |= ^uint32(0) >> prefix.Bits()
|
|
return netip.AddrFrom4([4]byte{byte(value >> 24), byte(value >> 16), byte(value >> 8), byte(value)})
|
|
}
|