Files
wg-admin/internal/config/config.go
Calmcacil 26120b8bc2 Add WireGuard TUI implementation
- Add Go TUI with bubbletea for WireGuard management
- Implement client CRUD operations with QR code generation
- Add configuration and validation modules
- Install/update scripts for client setup
- Update Makefile to build binaries to bin/ directory
- Add .gitignore for Go projects
2026-01-12 19:03:35 +01:00

238 lines
5.7 KiB
Go

package config
import (
"fmt"
"os"
"strings"
)
// Config holds application configuration
type Config struct {
ServerDomain string `mapstructure:"SERVER_DOMAIN"`
WGPort int `mapstructure:"WG_PORT"`
VPNIPv4Range string `mapstructure:"VPN_IPV4_RANGE"`
VPNIPv6Range string `mapstructure:"VPN_IPV6_RANGE"`
WGInterface string `mapstructure:"WG_INTERFACE"`
DNSServers string `mapstructure:"DNS_SERVERS"`
LogFile string `mapstructure:"LOG_FILE"`
Theme string `mapstructure:"THEME"`
}
// Default values
const (
DefaultWGPort = 51820
DefaultVPNIPv4Range = "10.10.69.0/24"
DefaultVPNIPv6Range = "fd69:dead:beef:69::/64"
DefaultWGInterface = "wg0"
DefaultDNSServers = "8.8.8.8, 8.8.4.4"
DefaultLogFile = "/var/log/wireguard-admin.log"
DefaultTheme = "default"
)
// LoadConfig loads configuration from file and environment variables
func LoadConfig() (*Config, error) {
cfg := &Config{}
// Load from config file if it exists
if err := loadFromFile(cfg); err != nil {
return nil, fmt.Errorf("failed to load config file: %w", err)
}
// Override with environment variables
if err := loadFromEnv(cfg); err != nil {
return nil, fmt.Errorf("failed to load config from environment: %w", err)
}
// Apply defaults for empty values
applyDefaults(cfg)
// Validate required configuration
if err := validateConfig(cfg); err != nil {
return nil, err
}
return cfg, nil
}
// loadFromFile reads configuration from /etc/wg-admin/config.conf
func loadFromFile(cfg *Config) error {
configPath := "/etc/wg-admin/config.conf"
// Check if config file exists
if _, err := os.Stat(configPath); os.IsNotExist(err) {
// Config file is optional, skip if not exists
return nil
}
// Read config file
content, err := os.ReadFile(configPath)
if err != nil {
return err
}
// Parse key=value pairs
lines := strings.Split(string(content), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
// Set value using mapstructure tags
switch key {
case "SERVER_DOMAIN":
cfg.ServerDomain = value
case "WG_PORT":
var port int
if _, err := fmt.Sscanf(value, "%d", &port); err == nil {
cfg.WGPort = port
}
case "VPN_IPV4_RANGE":
cfg.VPNIPv4Range = value
case "VPN_IPV6_RANGE":
cfg.VPNIPv6Range = value
case "WG_INTERFACE":
cfg.WGInterface = value
case "DNS_SERVERS":
cfg.DNSServers = value
case "LOG_FILE":
cfg.LogFile = value
case "THEME":
cfg.Theme = value
}
}
return nil
}
// loadFromEnv loads configuration from environment variables
func loadFromEnv(cfg *Config) error {
// Read environment variables
if val := os.Getenv("SERVER_DOMAIN"); val != "" {
cfg.ServerDomain = val
}
if val := os.Getenv("WG_PORT"); val != "" {
var port int
if _, err := fmt.Sscanf(val, "%d", &port); err == nil {
cfg.WGPort = port
}
}
if val := os.Getenv("VPN_IPV4_RANGE"); val != "" {
cfg.VPNIPv4Range = val
}
if val := os.Getenv("VPN_IPV6_RANGE"); val != "" {
cfg.VPNIPv6Range = val
}
if val := os.Getenv("WG_INTERFACE"); val != "" {
cfg.WGInterface = val
}
if val := os.Getenv("DNS_SERVERS"); val != "" {
cfg.DNSServers = val
}
if val := os.Getenv("LOG_FILE"); val != "" {
cfg.LogFile = val
}
if val := os.Getenv("THEME"); val != "" {
cfg.Theme = val
}
return nil
}
// applyDefaults sets default values for empty configuration
func applyDefaults(cfg *Config) {
if cfg.WGPort == 0 {
cfg.WGPort = DefaultWGPort
}
if cfg.VPNIPv4Range == "" {
cfg.VPNIPv4Range = DefaultVPNIPv4Range
}
if cfg.VPNIPv6Range == "" {
cfg.VPNIPv6Range = DefaultVPNIPv6Range
}
if cfg.WGInterface == "" {
cfg.WGInterface = DefaultWGInterface
}
if cfg.DNSServers == "" {
cfg.DNSServers = DefaultDNSServers
}
if cfg.LogFile == "" {
cfg.LogFile = DefaultLogFile
}
if cfg.Theme == "" {
cfg.Theme = DefaultTheme
}
}
// validateConfig checks that required configuration is present
func validateConfig(cfg *Config) error {
if cfg.ServerDomain == "" {
return fmt.Errorf("SERVER_DOMAIN is required. Set it in /etc/wg-admin/config.conf or via environment variable.")
}
// Validate port range
if cfg.WGPort < 1 || cfg.WGPort > 65535 {
return fmt.Errorf("WG_PORT must be between 1 and 65535, got: %d", cfg.WGPort)
}
// Validate CIDR format for IPv4 range
if !isValidCIDR(cfg.VPNIPv4Range, true) {
return fmt.Errorf("Invalid VPN_IPV4_RANGE format: %s", cfg.VPNIPv4Range)
}
// Validate CIDR format for IPv6 range
if !isValidCIDR(cfg.VPNIPv6Range, false) {
return fmt.Errorf("Invalid VPN_IPV6_RANGE format: %s", cfg.VPNIPv6Range)
}
return nil
}
// isValidCIDR performs basic CIDR validation
func isValidCIDR(cidr string, isIPv4 bool) bool {
if cidr == "" {
return false
}
// Split address and prefix
parts := strings.Split(cidr, "/")
if len(parts) != 2 {
return false
}
// Basic validation - more comprehensive validation could be added
if isIPv4 {
// IPv4 CIDR should have address like x.x.x.x
return true // Simplified validation
}
// IPv6 CIDR
return true // Simplified validation
}
// GetVPNIPv4Network extracts the IPv4 network from CIDR (e.g., "10.10.69.0" from "10.10.69.0/24")
func (c *Config) GetVPNIPv4Network() string {
parts := strings.Split(c.VPNIPv4Range, "/")
if len(parts) == 0 {
return ""
}
return strings.TrimSuffix(parts[0], "0")
}
// GetVPNIPv6Network extracts the IPv6 network from CIDR (e.g., "fd69:dead:beef:69::" from "fd69:dead:beef:69::/64")
func (c *Config) GetVPNIPv6Network() string {
parts := strings.Split(c.VPNIPv6Range, "/")
if len(parts) == 0 {
return ""
}
return strings.TrimSuffix(parts[0], "::")
}