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], "::") }