benzcloud-server/internal/core/config/config.go

106 lines
3.0 KiB
Go

package config
import (
"encoding/json"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"sync"
)
// Config holds the complete state and configuration for benzcloud-server.
type Config struct {
BaseDomain string `json:"base_domain"` // e.g. benzjeremy.de or intern
ServerLocalIP string `json:"server_local_ip"` // LAN IP, e.g. 192.168.0.5
HTTPPort int `json:"http_port"` // default 8080
DNSPort int `json:"dns_port"` // default 53 (or 5353 fallback)
VPNPort int `json:"vpn_port"` // default 4242 (UDP)
OverlaySubnet string `json:"overlay_subnet"` // e.g. 10.42.0.0/16
ServerVPNIP string `json:"server_vpn_ip"` // e.g. 10.42.0.1
SetupCompleted bool `json:"setup_completed"`
DataDir string `json:"data_dir"`
MasterSalt string `json:"master_salt"` // hex encoded salt for PBKDF2
ServerToken string `json:"server_token"` // 32-byte CSPRNG token
mu sync.RWMutex
}
var ErrNotConfigured = errors.New("benzcloud-server is not configured yet (setup pending)")
// DefaultConfig returns reasonable default settings.
func DefaultConfig(dataDir string) *Config {
localIP := detectLocalIP()
return &Config{
BaseDomain: "intern",
ServerLocalIP: localIP,
HTTPPort: 8080,
DNSPort: 53,
VPNPort: 4242,
OverlaySubnet: "10.42.0.0/16",
ServerVPNIP: "10.42.0.1",
SetupCompleted: false,
DataDir: dataDir,
}
}
// detectLocalIP tries to determine the active outbound LAN IP address.
func detectLocalIP() string {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return "127.0.0.1"
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP.String()
}
// ConfigPath returns the path to config.json inside dataDir.
func (c *Config) ConfigPath() string {
return filepath.Join(c.DataDir, "config.json")
}
// Save persists the configuration to disk with secure 0600 permissions.
func (c *Config) Save() error {
c.mu.Lock()
defer c.mu.Unlock()
if err := os.MkdirAll(c.DataDir, 0700); err != nil {
return fmt.Errorf("failed to create data dir: %w", err)
}
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
tmpFile := c.ConfigPath() + ".tmp"
if err := os.WriteFile(tmpFile, data, 0600); err != nil {
return fmt.Errorf("failed to write tmp config: %w", err)
}
if err := os.Rename(tmpFile, c.ConfigPath()); err != nil {
return fmt.Errorf("failed to commit config file: %w", err)
}
return nil
}
// Load loads the configuration from dataDir.
func Load(dataDir string) (*Config, error) {
cfgPath := filepath.Join(dataDir, "config.json")
data, err := os.ReadFile(cfgPath)
if err != nil {
if os.IsNotExist(err) {
return DefaultConfig(dataDir), ErrNotConfigured
}
return nil, fmt.Errorf("failed to read config file: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
cfg.DataDir = dataDir
return &cfg, nil
}