Files
wgtool/scripting/wgez-setup.go
2026-03-22 00:54:58 -07:00

696 lines
21 KiB
Go

package main
import (
"bufio"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"golang.org/x/crypto/curve25519"
)
// Colors for terminal output
const (
Red = "\033[0;31m"
Green = "\033[0;32m"
Yellow = "\033[1;33m"
Blue = "\033[0;34m"
Reset = "\033[0m"
)
// Configuration structs
type WGConfig struct {
Hostname string `json:"hostname"`
IPAddress string `json:"ip_address"`
PrivateKey string `json:"private_key"`
PublicKey string `json:"public_key"`
RoutingMode string `json:"routing_mode"`
Generated string `json:"generated"`
ScriptVer string `json:"script_version"`
RunningRoot bool `json:"running_as_root"`
}
type StaticServer struct {
IP string
Port string
}
// Global variables
var (
staticServers = map[string]string{
"10.8.0.1": "51820",
"10.8.0.10": "53535",
"10.8.0.99": "54382",
}
peers = map[string]Peer{
"zion": {
Name: "Zion peer (central server) - for access to entire network",
PublicKey: "2ztJbrN1x1NWanzPGLiKL19ZkdOhm5Y7WeKEWBT5cyg=",
Endpoint: "ugh.im:51820",
Keepalive: 25,
},
"cthulhu": {
Name: "Cthulhu (optional if port 53 is also forwarded to bypass firewalls)",
PublicKey: "NBktXKy1s0n2lIlIMODvOqKNwAtYdoZH5feKt5P43i0=",
AllowedIPs: "10.8.0.10/32",
Endpoint: "aw2cd67.glddns.com:53535",
Keepalive: 25,
},
"galaxy": {
Name: "Galaxy (located in Europe, NL)",
PublicKey: "QBNt00VSedxPlq3ZvsdYaqIcbudCAyxv9TG65aPVZzM=",
AllowedIPs: "10.8.0.99/32",
Endpoint: "galaxyspin.space:54382",
Keepalive: 25,
},
}
)
type Peer struct {
Name string
PublicKey string
AllowedIPs string
Endpoint string
Keepalive int
}
// Utility functions
func printStatus(message string) {
fmt.Printf("%s[INFO]%s %s\n", Green, Reset, message)
}
func printWarning(message string) {
fmt.Printf("%s[WARNING]%s %s\n", Yellow, Reset, message)
}
func printError(message string) {
fmt.Printf("%s[ERROR]%s %s\n", Red, Reset, message)
}
func printHeader() {
fmt.Printf("%s================================%s\n", Blue, Reset)
fmt.Printf("%s NextGen WireGuard Easy Setup %s\n", Blue, Reset)
fmt.Printf("%s================================%s\n", Blue, Reset)
fmt.Println()
}
// Validation functions
func validateHostname(hostname string) error {
if hostname == "" {
return fmt.Errorf("hostname cannot be empty")
}
if len(hostname) > 63 {
return fmt.Errorf("hostname too long (max 63 characters)")
}
// Basic alphanumeric and hyphen validation
for _, char := range hostname {
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') || char == '-') {
return fmt.Errorf("invalid hostname format. Use alphanumeric characters and hyphens only")
}
}
return nil
}
func validateIP(ip string) error {
if ip == "" {
return fmt.Errorf("IP address cannot be empty")
}
parts := strings.Split(ip, ".")
if len(parts) != 4 {
return fmt.Errorf("invalid IP format")
}
for _, part := range parts {
num, err := strconv.Atoi(part)
if err != nil || num < 0 || num > 255 {
return fmt.Errorf("invalid IP octet: %s", part)
}
}
// Check if it's in the expected range
if !strings.HasPrefix(ip, "10.8.0.") && !strings.HasPrefix(ip, "10.0.0.") {
printWarning("IP should be in 10.8.0.x or 10.0.0.x range for NextGen network")
}
return nil
}
func validateInterfaceName(iface string) error {
if iface == "" {
return fmt.Errorf("interface name cannot be empty")
}
if len(iface) > 15 {
return fmt.Errorf("interface name too long (max 15 characters)")
}
// Check if starts with letter and contains only alphanumeric
if len(iface) == 0 || !((iface[0] >= 'a' && iface[0] <= 'z') || (iface[0] >= 'A' && iface[0] <= 'Z')) {
return fmt.Errorf("interface name must start with a letter")
}
for _, char := range iface {
if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) {
return fmt.Errorf("interface name can only contain letters and numbers")
}
}
return nil
}
// User input functions
func getUserInput(prompt string, validator func(string) error) string {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print(prompt)
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
if err := validator(input); err != nil {
printError(err.Error())
fmt.Print("Continue anyway? (y/N): ")
response, _ := reader.ReadString('\n')
response = strings.TrimSpace(strings.ToLower(response))
if response == "y" || response == "yes" {
return input
}
continue
}
return input
}
}
func getDirectoryInput(prompt, defaultDir string) string {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("%s [%s]: ", prompt, defaultDir)
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
if input == "" {
input = defaultDir
}
// Check if directory exists
if _, err := os.Stat(input); os.IsNotExist(err) {
fmt.Printf("Directory '%s' doesn't exist. Create it? (Y/n): ", input)
response, _ := reader.ReadString('\n')
response = strings.TrimSpace(strings.ToLower(response))
if response == "n" || response == "no" {
continue
}
if err := os.MkdirAll(input, 0755); err != nil {
printError(fmt.Sprintf("Failed to create directory '%s': %v", input, err))
continue
}
}
// Check if directory is writable
if info, err := os.Stat(input); err == nil {
if info.Mode()&0200 == 0 {
printError(fmt.Sprintf("Directory '%s' is not writable", input))
continue
}
}
return input
}
}
// WireGuard key generation
func generateWireGuardKeys() (string, string, error) {
// Generate private key
privateKeyBytes := make([]byte, 32)
if _, err := rand.Read(privateKeyBytes); err != nil {
return "", "", err
}
// Ensure the key is valid for curve25519
privateKeyBytes[0] &= 248
privateKeyBytes[31] &= 127
privateKeyBytes[31] |= 64
// Generate public key
var publicKeyBytes [32]byte
curve25519.ScalarBaseMult(&publicKeyBytes, (*[32]byte)(privateKeyBytes))
privateKey := base64.StdEncoding.EncodeToString(privateKeyBytes[:])
publicKey := base64.StdEncoding.EncodeToString(publicKeyBytes[:])
return privateKey, publicKey, nil
}
// Configuration generation
func generateConfig(hostname, ipAddress, privateKey, routingMode string) string {
var config strings.Builder
// Interface section
config.WriteString("[Interface]\n")
config.WriteString(fmt.Sprintf("Address = %s/24\n", ipAddress))
config.WriteString(fmt.Sprintf("PrivateKey = %s\n", privateKey))
// Add ListenPort for static servers
if port, exists := staticServers[ipAddress]; exists {
config.WriteString(fmt.Sprintf("ListenPort = %s\n", port))
}
// Add DNS for full tunnel mode
if routingMode == "full_tunnel" {
config.WriteString("DNS = 1.1.1.1, 8.8.8.8\n")
}
// Zion peer (always enabled)
config.WriteString("\n#Zion peer (central server) - for access to entire network\n")
config.WriteString("[Peer]\n")
config.WriteString(fmt.Sprintf("PublicKey = %s\n", peers["zion"].PublicKey))
// Set AllowedIPs based on routing mode
if routingMode == "full_tunnel" {
config.WriteString("AllowedIPs = 0.0.0.0/0, ::/0\n")
} else {
config.WriteString("AllowedIPs = 10.8.0.0/24\n")
}
config.WriteString(fmt.Sprintf("Endpoint = %s\n", peers["zion"].Endpoint))
config.WriteString(fmt.Sprintf("PersistentKeepalive = %d\n", peers["zion"].Keepalive))
// Optional peers (commented out)
config.WriteString("\n#Cthulhu (optional if port 53 is also forwarded to bypass firewalls)\n")
config.WriteString("#[Peer]\n")
config.WriteString(fmt.Sprintf("#PublicKey = %s\n", peers["cthulhu"].PublicKey))
config.WriteString(fmt.Sprintf("#AllowedIPs = %s\n", peers["cthulhu"].AllowedIPs))
config.WriteString(fmt.Sprintf("#Endpoint = %s\n", peers["cthulhu"].Endpoint))
config.WriteString(fmt.Sprintf("#PersistentKeepalive = %d\n", peers["cthulhu"].Keepalive))
config.WriteString("\n#Galaxy (located in Europe, NL)\n")
config.WriteString("#[Peer]\n")
config.WriteString(fmt.Sprintf("#PublicKey = %s\n", peers["galaxy"].PublicKey))
config.WriteString(fmt.Sprintf("#AllowedIPs = %s\n", peers["galaxy"].AllowedIPs))
config.WriteString(fmt.Sprintf("#Endpoint = %s\n", peers["galaxy"].Endpoint))
config.WriteString(fmt.Sprintf("#PersistentKeepalive = %d\n", peers["galaxy"].Keepalive))
return config.String()
}
// Check dependencies
func checkDependencies() error {
deps := []string{"wg", "wg-quick"}
for _, dep := range deps {
if _, err := exec.LookPath(dep); err != nil {
return fmt.Errorf("missing dependency: %s", dep)
}
}
return nil
}
// Check if running as root
func isRunningAsRoot() bool {
return os.Geteuid() == 0
}
// Main function
func main() {
printHeader()
// Check if running as root
runningAsRoot := isRunningAsRoot()
var wgDir string
if runningAsRoot {
wgDir = "/etc/wireguard"
printStatus("Running as root - using system directories")
printStatus(fmt.Sprintf("WireGuard directory: %s", wgDir))
} else {
wgDir = "."
printWarning("Not running as root - using current directory")
printStatus(fmt.Sprintf("WireGuard directory: %s", wgDir))
printWarning("You'll need to manually copy config files to /etc/wireguard/ later")
}
fmt.Println()
// Check dependencies
if err := checkDependencies(); err != nil {
printError(err.Error())
printStatus("Install with: apt install wireguard-tools")
os.Exit(1)
}
// Get directory for non-root users
if !runningAsRoot {
fmt.Printf("%sStep 1: Directory Selection%s\n", Blue, Reset)
fmt.Println()
printStatus("Choose where to save WireGuard files:")
fmt.Printf(" - Current directory: %s\n", wgDir)
fmt.Printf(" - Home directory: %s\n", os.Getenv("HOME"))
fmt.Println(" - Custom directory")
fmt.Println()
wgDir = getDirectoryInput("Enter directory path for WireGuard files", wgDir)
fmt.Println()
}
// Get hostname
stepNum := "2"
if !runningAsRoot {
stepNum = "2"
} else {
stepNum = "1"
}
fmt.Printf("%sStep %s: Node Information%s\n", Blue, stepNum, Reset)
fmt.Println()
hostname := getUserInput("Enter hostname for this node: ", validateHostname)
// Get IP address
fmt.Println()
fmt.Println("Available IP ranges:")
fmt.Println(" - 10.8.0.x (recommended for NextGen network)")
fmt.Println(" - 10.0.0.x (alternative range)")
fmt.Println()
ipAddress := getUserInput("Enter IP address for this node (e.g., 10.8.0.30): ", validateIP)
// Get interface name
fmt.Println()
fmt.Println("Interface name options:")
fmt.Println(" - wg0 (default, most common)")
fmt.Println(" - wg1, wg2, etc. (if wg0 is already in use)")
fmt.Println(" - Custom name (e.g., nextgen, vpn, etc.)")
fmt.Println()
interfaceName := getUserInput("Enter interface name (default: wg0): ", validateInterfaceName)
if interfaceName == "" {
interfaceName = "wg0"
}
// Configuration options
fmt.Println()
if !runningAsRoot {
stepNum = "3"
} else {
stepNum = "2"
}
fmt.Printf("%sStep %s: Configuration Options%s\n", Blue, stepNum, Reset)
fmt.Println()
fmt.Println("Choose an option:")
fmt.Println("1. Generate keys only (manual config creation)")
fmt.Println("2. Generate keys + complete config (recommended)")
fmt.Println()
reader := bufio.NewReader(os.Stdin)
var configChoice string
for {
fmt.Print("Enter your choice (1 or 2): ")
configChoice, _ = reader.ReadString('\n')
configChoice = strings.TrimSpace(configChoice)
if configChoice == "1" || configChoice == "2" {
break
}
printError("Invalid choice. Please enter 1 or 2.")
}
// Traffic routing options
var routingMode string
if configChoice == "2" {
fmt.Println()
fmt.Println("Traffic routing options:")
fmt.Println("1. WireGuard traffic only (10.8.0.x network only)")
fmt.Println("2. All traffic through VPN (full tunnel)")
fmt.Println()
fmt.Println("Note: Full tunnel routes ALL internet traffic through the VPN.")
fmt.Println(" WireGuard-only keeps your regular internet traffic separate.")
fmt.Println()
var routingChoice string
for {
fmt.Print("Enter your choice (1 or 2): ")
routingChoice, _ = reader.ReadString('\n')
routingChoice = strings.TrimSpace(routingChoice)
if routingChoice == "1" || routingChoice == "2" {
break
}
printError("Invalid choice. Please enter 1 or 2.")
}
if routingChoice == "1" {
routingMode = "wg_only"
printStatus("Selected: WireGuard traffic only")
} else {
routingMode = "full_tunnel"
printStatus("Selected: All traffic through VPN")
}
}
printStatus(fmt.Sprintf("Starting setup for %s (%s)...", hostname, ipAddress))
fmt.Println()
// Create directories
if err := os.MkdirAll(wgDir, 0755); err != nil {
printError(fmt.Sprintf("Failed to create directory: %v", err))
os.Exit(1)
}
// Generate keys
printStatus("Generating WireGuard keys...")
privateKey, publicKey, err := generateWireGuardKeys()
if err != nil {
printError(fmt.Sprintf("Failed to generate keys: %v", err))
os.Exit(1)
}
// Save keys
privateKeyFile := filepath.Join(wgDir, fmt.Sprintf("%s_private.key", hostname))
publicKeyFile := filepath.Join(wgDir, fmt.Sprintf("%s_public.key", hostname))
if err := ioutil.WriteFile(privateKeyFile, []byte(privateKey), 0600); err != nil {
printError(fmt.Sprintf("Failed to save private key: %v", err))
os.Exit(1)
}
if err := ioutil.WriteFile(publicKeyFile, []byte(publicKey), 0600); err != nil {
printError(fmt.Sprintf("Failed to save public key: %v", err))
os.Exit(1)
}
printStatus("Keys generated successfully!")
fmt.Printf(" Private key: %s\n", privateKeyFile)
fmt.Printf(" Public key: %s\n", publicKeyFile)
fmt.Println()
// Display node information
if !runningAsRoot {
stepNum = "4"
} else {
stepNum = "3"
}
fmt.Printf("%sStep %s: Node Information%s\n", Blue, stepNum, Reset)
fmt.Println("==========================================")
fmt.Printf("HOSTNAME: %s\n", hostname)
fmt.Printf("IP ADDRESS: %s\n", ipAddress)
fmt.Printf("PRIVATE KEY: %s\n", privateKey)
fmt.Printf("PUBLIC KEY: %s\n", publicKey)
fmt.Println("==========================================")
fmt.Println()
// Save structured info
infoFile := filepath.Join(wgDir, fmt.Sprintf("%s_wg_info.json", hostname))
if runningAsRoot {
infoFile = filepath.Join("/tmp", fmt.Sprintf("%s_wg_info.json", hostname))
}
wgConfig := WGConfig{
Hostname: hostname,
IPAddress: ipAddress,
PrivateKey: privateKey,
PublicKey: publicKey,
RoutingMode: routingMode,
Generated: time.Now().Format(time.RFC3339),
ScriptVer: "2.2",
RunningRoot: runningAsRoot,
}
infoData, err := json.MarshalIndent(wgConfig, "", " ")
if err != nil {
printError(fmt.Sprintf("Failed to marshal config: %v", err))
os.Exit(1)
}
if err := ioutil.WriteFile(infoFile, infoData, 0600); err != nil {
printError(fmt.Sprintf("Failed to save info file: %v", err))
os.Exit(1)
}
printStatus(fmt.Sprintf("Information saved to: %s", infoFile))
fmt.Println()
// Generate complete config if requested
if configChoice == "2" {
configFile := filepath.Join(wgDir, fmt.Sprintf("%s.conf", interfaceName))
printStatus(fmt.Sprintf("Generating complete %s.conf...", interfaceName))
configContent := generateConfig(hostname, ipAddress, privateKey, routingMode)
if err := ioutil.WriteFile(configFile, []byte(configContent), 0600); err != nil {
printError(fmt.Sprintf("Failed to write config file: %v", err))
os.Exit(1)
}
printStatus(fmt.Sprintf("Config written to: %s", configFile))
if runningAsRoot {
printStatus("Permissions set to 600")
}
fmt.Println()
// Next steps
if !runningAsRoot {
stepNum = "5"
} else {
stepNum = "4"
}
fmt.Printf("%sStep %s: Next Steps%s\n", Blue, stepNum, Reset)
fmt.Println()
if runningAsRoot {
printStatus("Ready to start WireGuard:")
fmt.Printf(" systemctl enable --now wg-quick@%s\n", interfaceName)
} else {
printWarning("To enable WireGuard (requires root):")
fmt.Printf(" sudo cp %s /etc/wireguard/\n", configFile)
fmt.Printf(" sudo chmod 600 /etc/wireguard/%s.conf\n", interfaceName)
fmt.Printf(" sudo systemctl enable --now wg-quick@%s\n", interfaceName)
}
fmt.Println()
printWarning("IMPORTANT: Update other nodes with this peer info:")
fmt.Printf(" PublicKey = %s\n", publicKey)
fmt.Printf(" AllowedIPs = %s/32\n", ipAddress)
fmt.Println()
// Config preview
fmt.Printf("%sConfig Preview:%s\n", Blue, Reset)
fmt.Println("----------------------------------------")
lines := strings.Split(configContent, "\n")
for i, line := range lines {
if i >= 5 {
break
}
fmt.Println(line)
}
fmt.Printf(" [... and %d total lines]\n", len(lines))
fmt.Println("----------------------------------------")
fmt.Println()
// Full tunnel instructions
if routingMode == "full_tunnel" {
fmt.Println()
printWarning("FULL TUNNEL MODE DETECTED - Endpoint Changes Required:")
fmt.Println()
fmt.Println("Since this node will route ALL traffic through the VPN, you need to:")
fmt.Println()
fmt.Println("1. Update Zion's config (/etc/wireguard/wg0.conf) to allow this peer:")
fmt.Println(" - Add the peer section as shown above")
fmt.Println(" - Ensure Zion has proper iptables rules for NAT/masquerading")
fmt.Println()
fmt.Println("2. Check Zion's iptables rules (run on Zion server):")
fmt.Println(" sudo iptables -t nat -L POSTROUTING")
fmt.Println(" sudo iptables -L FORWARD")
fmt.Println()
fmt.Println("3. If Zion doesn't have proper NAT rules, add them:")
fmt.Println(" sudo iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE")
fmt.Println(" sudo iptables -A FORWARD -i wg0 -j ACCEPT")
fmt.Println(" sudo iptables -A FORWARD -o wg0 -j ACCEPT")
fmt.Println()
fmt.Println("4. Enable IP forwarding on Zion (if not already enabled):")
fmt.Println(" echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf")
fmt.Println(" sudo sysctl -p")
fmt.Println()
}
// Zion update instructions
fmt.Printf("%s========================================%s\n", Red, Reset)
fmt.Printf("%s !!! NOW UPDATE ZION SERVER !!! %s\n", Red, Reset)
fmt.Printf("%s========================================%s\n", Red, Reset)
fmt.Println()
printWarning("You MUST add this peer to Zion's config (/etc/wireguard/wg0.conf):")
fmt.Println()
fmt.Printf("%s#%s%s\n", Yellow, hostname, Reset)
fmt.Printf("%s[Peer]%s\n", Yellow, Reset)
fmt.Printf("%sPublicKey = %s%s\n", Yellow, publicKey, Reset)
fmt.Printf("%sAllowedIPs = %s/32%s\n", Yellow, ipAddress, Reset)
fmt.Println()
printWarning("After updating Zion, restart its WireGuard:")
fmt.Println(" systemctl restart wg-quick@wg0")
fmt.Println()
if runningAsRoot {
printWarning("Then restart this node's WireGuard:")
fmt.Printf(" systemctl restart wg-quick@%s\n", interfaceName)
} else {
printWarning("Then restart this node's WireGuard:")
fmt.Printf(" sudo systemctl restart wg-quick@%s\n", interfaceName)
}
} else {
// Manual config generation path
if !runningAsRoot {
stepNum = "5"
} else {
stepNum = "4"
}
fmt.Printf("%sStep %s: Next Steps%s\n", Blue, stepNum, Reset)
fmt.Println()
printStatus("Keys generated successfully!")
fmt.Printf(" Private key: %s\n", privateKeyFile)
fmt.Printf(" Public key: %s\n", publicKeyFile)
fmt.Println()
printWarning("Next steps:")
fmt.Printf(" - Create %s.conf manually using the keys above\n", interfaceName)
fmt.Printf(" - Copy config to %s\n", filepath.Join(wgDir, fmt.Sprintf("%s.conf", interfaceName)))
if runningAsRoot {
fmt.Printf(" - Set permissions: chmod 600 %s\n", filepath.Join(wgDir, fmt.Sprintf("%s.conf", interfaceName)))
fmt.Printf(" - Enable/start: systemctl enable --now wg-quick@%s\n", interfaceName)
} else {
fmt.Printf(" - Copy to system: sudo cp %s /etc/wireguard/\n", filepath.Join(wgDir, fmt.Sprintf("%s.conf", interfaceName)))
fmt.Printf(" - Set permissions: sudo chmod 600 /etc/wireguard/%s.conf\n", interfaceName)
fmt.Printf(" - Enable/start: sudo systemctl enable --now wg-quick@%s\n", interfaceName)
}
fmt.Println()
fmt.Printf("%s========================================%s\n", Red, Reset)
fmt.Printf("%s !!! NOW UPDATE ZION SERVER !!! %s\n", Red, Reset)
fmt.Printf("%s========================================%s\n", Red, Reset)
fmt.Println()
printWarning("You MUST add this peer to Zion's config (/etc/wireguard/wg0.conf):")
fmt.Println()
fmt.Printf("%s#%s%s\n", Yellow, hostname, Reset)
fmt.Printf("%s[Peer]%s\n", Yellow, Reset)
fmt.Printf("%sPublicKey = %s%s\n", Yellow, publicKey, Reset)
fmt.Printf("%sAllowedIPs = %s/32%s\n", Yellow, ipAddress, Reset)
fmt.Println()
printWarning("After updating Zion, restart its WireGuard:")
fmt.Println(" systemctl restart wg-quick@wg0")
fmt.Println()
if runningAsRoot {
printWarning("Then restart this node's WireGuard:")
fmt.Printf(" systemctl restart wg-quick@%s\n", interfaceName)
} else {
printWarning("Then restart this node's WireGuard:")
fmt.Printf(" sudo systemctl restart wg-quick@%s\n", interfaceName)
}
}
fmt.Println()
printStatus(fmt.Sprintf("Setup complete for %s!", hostname))
fmt.Println()
}