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
This commit is contained in:
237
internal/config/config.go
Normal file
237
internal/config/config.go
Normal file
@@ -0,0 +1,237 @@
|
||||
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], "::")
|
||||
}
|
||||
Reference in New Issue
Block a user