- 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
238 lines
5.7 KiB
Go
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], "::")
|
|
}
|