Files
wild-central/internal/config/config.go

151 lines
5.3 KiB
Go

package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
// ExtraPort is a TCP or UDP port that the firewall should allow on the WAN interface.
type ExtraPort struct {
Port int `yaml:"port" json:"port"`
Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"` // "tcp" (default) or "udp"
Label string `yaml:"label,omitempty" json:"label,omitempty"`
}
// UnmarshalYAML handles both plain integers (legacy: extraPorts: [22]) and structured entries
// (extraPorts: [{port: 22, protocol: tcp, label: SSH}]).
func (p *ExtraPort) UnmarshalYAML(value *yaml.Node) error {
if value.Kind == yaml.ScalarNode {
var port int
if err := value.Decode(&port); err != nil {
return fmt.Errorf("extraPort: expected integer, got %q", value.Value)
}
p.Port = port
return nil
}
type alias ExtraPort
return value.Decode((*alias)(p))
}
// UnmarshalJSON handles both plain integers (legacy: extraPorts: [22]) and structured entries.
func (p *ExtraPort) UnmarshalJSON(data []byte) error {
if len(data) > 0 && data[0] != '{' {
var port int
if err := json.Unmarshal(data, &port); err != nil {
return fmt.Errorf("extraPort: expected integer or object, got: %s", data)
}
p.Port = port
return nil
}
type alias ExtraPort
var a alias
if err := json.Unmarshal(data, &a); err != nil {
return err
}
*p = ExtraPort(a)
return nil
}
// SplitExtraPorts separates ExtraPorts into TCP and UDP slices.
// Ports with no Protocol or Protocol "tcp" are TCP; Protocol "udp" are UDP.
func SplitExtraPorts(ports []ExtraPort) (tcp []int, udp []int) {
for _, p := range ports {
if strings.EqualFold(p.Protocol, "udp") {
udp = append(udp, p.Port)
} else {
tcp = append(tcp, p.Port)
}
}
return
}
// HAProxyCustomRoute defines a custom TCP proxy rule for non-Wild Cloud services
type HAProxyCustomRoute struct {
Name string `yaml:"name" json:"name"`
Port int `yaml:"port" json:"port"`
Backend string `yaml:"backend" json:"backend"`
}
// DHCPStaticLease defines a static DHCP address assignment
type DHCPStaticLease struct {
MAC string `yaml:"mac" json:"mac"`
IP string `yaml:"ip" json:"ip"`
Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`
}
// State represents Wild Central's runtime state — operator settings, networking
// configuration, DDNS, DHCP, etc. Persisted in {dataDir}/state.yaml and mutated
// via the API. This is NOT boot-time config (which comes from env vars).
type State struct {
Operator struct {
Email string `yaml:"email,omitempty" json:"email,omitempty"`
} `yaml:"operator,omitempty" json:"operator,omitempty"`
Cloud struct {
Central struct {
Domain string `yaml:"domain,omitempty" json:"domain,omitempty"` // e.g. "central.payne.io"
} `yaml:"central,omitempty" json:"central,omitempty"`
Dnsmasq struct {
IP string `yaml:"ip,omitempty" json:"ip,omitempty"`
Interface string `yaml:"interface,omitempty" json:"interface,omitempty"`
DHCP struct {
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
RangeStart string `yaml:"rangeStart,omitempty" json:"rangeStart,omitempty"`
RangeEnd string `yaml:"rangeEnd,omitempty" json:"rangeEnd,omitempty"`
LeaseTime string `yaml:"leaseTime,omitempty" json:"leaseTime,omitempty"`
Gateway string `yaml:"gateway,omitempty" json:"gateway,omitempty"`
StaticLeases []DHCPStaticLease `yaml:"staticLeases,omitempty" json:"staticLeases,omitempty"`
} `yaml:"dhcp,omitempty" json:"dhcp,omitempty"`
} `yaml:"dnsmasq,omitempty" json:"dnsmasq,omitempty"`
HAProxy struct {
CustomRoutes []HAProxyCustomRoute `yaml:"customRoutes,omitempty" json:"customRoutes,omitempty"`
} `yaml:"haproxy,omitempty" json:"haproxy,omitempty"`
Nftables struct {
Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
WANInterface string `yaml:"wanInterface,omitempty" json:"wanInterface,omitempty"`
ExtraPorts []ExtraPort `yaml:"extraPorts,omitempty" json:"extraPorts,omitempty"`
} `yaml:"nftables,omitempty" json:"nftables,omitempty"`
DDNS struct {
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`
IntervalMinutes int `yaml:"intervalMinutes,omitempty" json:"intervalMinutes,omitempty"`
} `yaml:"ddns,omitempty" json:"ddns,omitempty"`
} `yaml:"cloud,omitempty" json:"cloud,omitempty"`
}
// LoadState loads state from the specified path
func LoadState(configPath string) (*State, error) {
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("reading config file %s: %w", configPath, err)
}
config := &State{}
if err := yaml.Unmarshal(data, config); err != nil {
return nil, fmt.Errorf("parsing config file: %w", err)
}
return config, nil
}
// SaveState saves the state to the specified path
func SaveState(config *State, configPath string) error {
// Ensure the directory exists
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
return fmt.Errorf("creating config directory: %w", err)
}
data, err := yaml.Marshal(config)
if err != nil {
return fmt.Errorf("marshaling config: %w", err)
}
return os.WriteFile(configPath, data, 0644)
}