Files
wgtool/wireguard_setup.go
2026-03-22 00:54:58 -07:00

860 lines
26 KiB
Go

package main
import (
"bufio"
"crypto/rand"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"regexp"
"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"
)
// Script constants
const (
ScriptVersion = "3.0.0"
DefaultPort = "51820"
MaxHostname = 63
MaxInterface = 15
)
// WireGuard peer configuration
type WireGuardPeer struct {
Name string
PublicKey string
AllowedIPs string
Endpoint string
PersistentKeepalive int
Description string
}
// Node configuration
type NodeConfig 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"`
Interface string `json:"interface"`
Generated string `json:"generated"`
ScriptVer string `json:"script_version"`
RunningRoot bool `json:"running_as_root"`
}
// Application state
type AppState struct {
ForceMode bool
RunningAsRoot bool
WGDirectory string
StepCounter int
}
// Default peers - automatically included in every config
var defaultPeers = []WireGuardPeer{
{
Name: "Zion",
PublicKey: "2ztJbrN1x1NWanzPGLiKL19ZkdOhm5Y7WeKEWBT5cyg=",
AllowedIPs: "10.8.0.0/24",
Endpoint: "ugh.im:51820",
PersistentKeepalive: 25,
Description: "Central server (always included)",
},
{
Name: "CTH",
PublicKey: "NBktXKy1s0n2lIlIMODvOqKNwAtYdoZH5feKt5P43i0=",
AllowedIPs: "10.8.0.10/32",
Endpoint: "aw2cd67.glddns.com:53535",
PersistentKeepalive: 25,
Description: "Secondary server (always included)",
},
}
// Reserved IP addresses
var reservedIPs = map[string]string{
"10.8.0.1": "Zion",
"10.8.0.10": "CTH",
"10.8.0.2": "Aza",
"10.8.0.20": "Nyar",
"10.8.0.99": "Galaxy",
"10.8.0.7": "nanocube",
"10.8.0.42": "jupiter",
"10.8.0.8": "HASS",
"10.8.0.40": "framebot",
}
// Static server ports
var staticServerPorts = map[string]string{
"10.8.0.1": "51820", // Zion
"10.8.0.10": "53535", // CTH
"10.8.0.99": "54382", // Galaxy
}
// Validation regexes
var (
ipRegex = regexp.MustCompile(`^10\.8\.0\.\d{1,3}$`)
hostnameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$`)
interfaceRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9]*$`)
keyRegex = regexp.MustCompile(`^[A-Za-z0-9+/]{43}=$`)
)
// Utility functions
func step(n int, msg string) {
fmt.Printf("%sStep %d:%s %s\n", Blue, n, Reset, msg)
}
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(title string) {
fmt.Printf("%s================================%s\n", Blue, Reset)
fmt.Printf("%s%s%s\n", Blue, title, 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) > MaxHostname {
return fmt.Errorf("hostname too long (max %d characters)", MaxHostname)
}
if !hostnameRegex.MatchString(hostname) {
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")
}
if !ipRegex.MatchString(ip) {
return fmt.Errorf("IP should be in 10.8.0.x range for NextGen network")
}
if parsedIP := net.ParseIP(ip); parsedIP == nil {
return fmt.Errorf("invalid IP address format")
}
if peerName, exists := reservedIPs[ip]; exists {
return fmt.Errorf("IP %s is already reserved for %s", ip, peerName)
}
return nil
}
func validateInterface(name string) error {
if name == "" {
return nil // Allow empty for default
}
if len(name) > MaxInterface {
return fmt.Errorf("interface name too long (max %d characters)", MaxInterface)
}
if !interfaceRegex.MatchString(name) {
return fmt.Errorf("interface name must start with a letter and contain only letters and numbers")
}
return nil
}
func validateWireGuardKey(key string) error {
if key == "" {
return fmt.Errorf("key cannot be empty")
}
if !keyRegex.MatchString(key) {
return fmt.Errorf("invalid WireGuard key format")
}
return nil
}
// User input functions
func getUserInput(prompt string, validator func(string) error, forceMode bool) string {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print(prompt)
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
if err := validator(input); err != nil {
if forceMode {
printWarning(fmt.Sprintf("Validation failed: %s, but continuing due to force mode", err.Error()))
return input
}
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, forceMode bool) 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) {
if forceMode {
if err := os.MkdirAll(input, 0755); err != nil {
printError(fmt.Sprintf("Failed to create directory '%s': %v", input, err))
continue
}
} else {
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
}
}
func getYesNoInput(prompt string, defaultYes bool, forceMode bool) bool {
if forceMode {
return defaultYes
}
reader := bufio.NewReader(os.Stdin)
for {
if defaultYes {
fmt.Printf("%s (Y/n): ", prompt)
} else {
fmt.Printf("%s (y/N): ", prompt)
}
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(strings.ToLower(input))
if input == "" {
return defaultYes
}
if input == "y" || input == "yes" {
return true
}
if input == "n" || input == "no" {
return false
}
printError("Please enter 'y' or 'n'")
}
}
func getChoice(prompt string, choices []string, forceMode bool) int {
if forceMode && len(choices) > 0 {
return 1 // Default to first choice in force mode
}
reader := bufio.NewReader(os.Stdin)
for {
fmt.Println(prompt)
for i, choice := range choices {
fmt.Printf("%d. %s\n", i+1, choice)
}
fmt.Print("Enter your choice: ")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
if choice, err := strconv.Atoi(input); err == nil && choice >= 1 && choice <= len(choices) {
return choice
}
printError(fmt.Sprintf("Invalid choice. Please enter a number between 1 and %d.", len(choices)))
}
}
// WireGuard key generation (safe version)
func generateWireGuardKeys() (string, string, error) {
// Generate private key
privateKeyBytes := make([]byte, 32)
if _, err := rand.Read(privateKeyBytes); err != nil {
return "", "", fmt.Errorf("failed to generate random bytes: %w", err)
}
// Ensure the key is valid for curve25519
privateKeyBytes[0] &= 248
privateKeyBytes[31] &= 127
privateKeyBytes[31] |= 64
// Generate public key (safe conversion)
var privateKeyArray [32]byte
copy(privateKeyArray[:], privateKeyBytes)
var publicKeyBytes [32]byte
curve25519.ScalarBaseMult(&publicKeyBytes, &privateKeyArray)
privateKey := base64.StdEncoding.EncodeToString(privateKeyBytes[:])
publicKey := base64.StdEncoding.EncodeToString(publicKeyBytes[:])
return privateKey, publicKey, nil
}
// Configuration generation
func generateConfig(hostname, ipAddress, privateKey, routingMode, interfaceName 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 := staticServerPorts[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")
}
// Add default peers
for _, peer := range defaultPeers {
config.WriteString(fmt.Sprintf("\n# %s (%s)\n", peer.Name, peer.Description))
config.WriteString("[Peer]\n")
config.WriteString(fmt.Sprintf("PublicKey = %s\n", peer.PublicKey))
// Set AllowedIPs based on routing mode
if routingMode == "full_tunnel" {
config.WriteString("AllowedIPs = 0.0.0.0/0, ::/0\n")
} else {
config.WriteString(fmt.Sprintf("AllowedIPs = %s\n", peer.AllowedIPs))
}
config.WriteString(fmt.Sprintf("Endpoint = %s\n", peer.Endpoint))
config.WriteString(fmt.Sprintf("PersistentKeepalive = %d\n", peer.PersistentKeepalive))
}
return config.String()
}
// Generate peer configuration for Zion
func generateZionPeerConfig(hostname, publicKey, ipAddress string) string {
var config strings.Builder
config.WriteString(fmt.Sprintf("# %s\n", hostname))
config.WriteString("[Peer]\n")
config.WriteString(fmt.Sprintf("PublicKey = %s\n", publicKey))
config.WriteString(fmt.Sprintf("AllowedIPs = %s/32\n", ipAddress))
return config.String()
}
// System checks
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
}
func isRunningAsRoot() bool {
return os.Geteuid() == 0
}
func checkFileExists(filepath string, forceMode bool) bool {
if _, err := os.Stat(filepath); err == nil {
if forceMode {
printWarning(fmt.Sprintf("File '%s' already exists, overwriting due to force mode", filepath))
return true
}
printWarning(fmt.Sprintf("File '%s' already exists", filepath))
return getYesNoInput("Do you want to overwrite it?", false, false)
}
return true
}
// Display instructions
func displayZionInstructions(hostname, publicKey, ipAddress string) {
printHeader("IMPORTANT: Update Zion Server Configuration")
fmt.Println()
printWarning("You MUST add this peer to Zion's configuration file:")
fmt.Println(" /etc/wireguard/wg0.conf")
fmt.Println()
fmt.Println("Add the following peer section to Zion's config:")
fmt.Println("----------------------------------------")
fmt.Println(generateZionPeerConfig(hostname, publicKey, ipAddress))
fmt.Println("----------------------------------------")
fmt.Println()
printWarning("After updating Zion's config:")
fmt.Println("1. Save the file")
fmt.Println("2. Restart Zion's WireGuard: sudo systemctl restart wg-quick@wg0")
fmt.Println("3. Then start this node's WireGuard: sudo wg-quick up " + hostname)
fmt.Println()
// Show Zion's current configuration structure
fmt.Println("Zion's current configuration structure:")
fmt.Println("----------------------------------------")
fmt.Println("[Interface]")
fmt.Println("Address = 10.8.0.1/24")
fmt.Println("ListenPort = 51820")
fmt.Println("PrivateKey = <zion_private_key>")
fmt.Println("PostUp = <iptables_rules>")
fmt.Println("PostDown = <iptables_rules>")
fmt.Println()
// Show existing peers
for name, pubKey := range map[string]string{
"Cth": "NBktXKy1s0n2lIlIMODvOqKNwAtYdoZH5feKt5P43i0=",
"Aza": "qmTKA257DLOrfhk5Zw8RyRmBSonmm6epbloT0P0ZWDc=",
"Nyar": "2BA7L1oJP1tK6dIUNHMgcZmOmYmlyPRe2RaBqfUsEWo=",
"Galaxy": "QBNt00VSedxPlq3ZvsdYaqIcbudCAyxv9TG65aPVZzM=",
"nanocube": "/ZImoATDIS0e0N08CD7mqWbhtGlSnynpPuY04Ed4Zyc=",
"jupiter": "YIFQ43ULk/YoCgOv3SBU6+MOrbxd+mlvaw9rT8uoNmw=",
"HASS": "C+Poz/7DaXCxe4HZiL6D5cld4jMt5o1gBq3iPiBzrg0=",
"framebot": "loS3yZapqmt6lP53Q+s4EvUzw6FmwgZC8jzgLluJ1Es=",
} {
fmt.Printf("#%s\n", name)
fmt.Println("[Peer]")
fmt.Printf("PublicKey = %s\n", pubKey)
fmt.Printf("AllowedIPs = 10.8.0.x/32\n")
fmt.Println()
}
fmt.Println("# Add your peer here:")
fmt.Printf("# %s\n", hostname)
fmt.Println("# [Peer]")
fmt.Printf("# PublicKey = %s\n", publicKey)
fmt.Printf("# AllowedIPs = %s/32\n", ipAddress)
fmt.Println("----------------------------------------")
fmt.Println()
}
func displayFullTunnelInstructions() {
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()
}
func displayNextSteps(hostname, interfaceName, publicKey, ipAddress, routingMode string, runningAsRoot bool) {
step(4, "Next Steps")
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", filepath.Join("wireguard_configs", fmt.Sprintf("%s.conf", interfaceName)))
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()
if routingMode == "full_tunnel" {
displayFullTunnelInstructions()
}
// 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)
}
}
// Main function
func main() {
// Parse command line flags
var forceMode bool
flag.BoolVar(&forceMode, "force", false, "Skip confirmations and use defaults")
flag.Parse()
// Initialize application state
state := &AppState{
ForceMode: forceMode,
RunningAsRoot: isRunningAsRoot(),
StepCounter: 1,
}
printHeader("WireGuard Configuration Setup v" + ScriptVersion)
fmt.Println("This script will help you create WireGuard keys and configuration files.")
fmt.Println("Based on the CURRENT_WORKING configuration with Zion as central server.")
fmt.Println("Default peers (Zion & CTH) will be automatically included.")
fmt.Println()
if forceMode {
printWarning("Force mode enabled - skipping confirmations and using defaults")
fmt.Println()
}
// Determine WireGuard directory
if state.RunningAsRoot {
state.WGDirectory = "/etc/wireguard"
printStatus("Running as root - using system directories")
printStatus(fmt.Sprintf("WireGuard directory: %s", state.WGDirectory))
} else {
state.WGDirectory = "wireguard_configs"
printWarning("Not running as root - using current directory")
printStatus(fmt.Sprintf("WireGuard directory: %s", state.WGDirectory))
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 !state.RunningAsRoot {
step(state.StepCounter, "Directory Selection")
state.StepCounter++
printStatus("Choose where to save WireGuard files:")
fmt.Printf(" - Current directory: %s\n", state.WGDirectory)
fmt.Printf(" - Home directory: %s\n", os.Getenv("HOME"))
fmt.Println(" - Custom directory")
fmt.Println()
state.WGDirectory = getDirectoryInput("Enter directory path for WireGuard files", state.WGDirectory, forceMode)
fmt.Println()
}
// Create directory
if err := os.MkdirAll(state.WGDirectory, 0755); err != nil {
printError(fmt.Sprintf("Failed to create directory: %v", err))
os.Exit(1)
}
// Get node information
step(state.StepCounter, "Node Information")
state.StepCounter++
fmt.Println()
hostname := getUserInput("Enter hostname for this node: ", validateHostname, forceMode)
// 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, forceMode)
// 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): ", validateInterface, forceMode)
if interfaceName == "" {
interfaceName = "wg0"
}
// Check if configuration file already exists
configFile := filepath.Join(state.WGDirectory, fmt.Sprintf("%s.conf", interfaceName))
if !checkFileExists(configFile, forceMode) {
printError("Operation cancelled. Please choose a different interface name or remove the existing file.")
os.Exit(1)
}
// Configuration options
fmt.Println()
step(state.StepCounter, "Configuration Options")
state.StepCounter++
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()
var configChoice int
if forceMode {
configChoice = 2 // Default to complete config in force mode
printStatus("Force mode: Using complete config generation")
} else {
configChoice = getChoice("Enter your choice:", []string{"Generate keys only", "Generate keys + complete config"}, false)
}
// 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 int
if forceMode {
routingChoice = 1 // Default to WireGuard-only in force mode
printStatus("Force mode: Using WireGuard-only routing")
} else {
routingChoice = getChoice("Enter your choice:", []string{"WireGuard traffic only", "All traffic through VPN"}, false)
}
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()
// 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(state.WGDirectory, fmt.Sprintf("%s_private.key", hostname))
publicKeyFile := filepath.Join(state.WGDirectory, fmt.Sprintf("%s_public.key", hostname))
if err := os.WriteFile(privateKeyFile, []byte(privateKey), 0600); err != nil {
printError(fmt.Sprintf("Failed to save private key: %v", err))
os.Exit(1)
}
if err := os.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
step(state.StepCounter, "Node Information")
state.StepCounter++
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.Printf("INTERFACE: %s\n", interfaceName)
fmt.Printf("ROUTING MODE: %s\n", routingMode)
fmt.Println("==========================================")
fmt.Println()
// Save structured info
infoFile := filepath.Join(state.WGDirectory, fmt.Sprintf("%s_wg_info.json", hostname))
if state.RunningAsRoot {
infoFile = filepath.Join("/tmp", fmt.Sprintf("%s_wg_info.json", hostname))
}
nodeConfig := NodeConfig{
Hostname: hostname,
IPAddress: ipAddress,
PrivateKey: privateKey,
PublicKey: publicKey,
RoutingMode: routingMode,
Interface: interfaceName,
Generated: time.Now().Format(time.RFC3339),
ScriptVer: ScriptVersion,
RunningRoot: state.RunningAsRoot,
}
infoData, err := json.MarshalIndent(nodeConfig, "", " ")
if err != nil {
printError(fmt.Sprintf("Failed to marshal config: %v", err))
os.Exit(1)
}
if err := os.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 {
printStatus(fmt.Sprintf("Generating complete %s.conf...", interfaceName))
configContent := generateConfig(hostname, ipAddress, privateKey, routingMode, interfaceName)
if err := os.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 state.RunningAsRoot {
printStatus("Permissions set to 600")
}
fmt.Println()
// Display Zion integration instructions
displayZionInstructions(hostname, publicKey, ipAddress)
// Display next steps
displayNextSteps(hostname, interfaceName, publicKey, ipAddress, routingMode, state.RunningAsRoot)
// Config preview
fmt.Printf("%sConfig Preview:%s\n", Blue, Reset)
fmt.Println("----------------------------------------")
lines := strings.Split(configContent, "\n")
for i, line := range lines {
if i >= 8 { // Show more lines to include default peers
break
}
fmt.Println(line)
}
fmt.Printf(" [... and %d total lines]\n", len(lines))
fmt.Println("----------------------------------------")
fmt.Println()
} else {
// Manual config generation path
step(state.StepCounter, "Next Steps")
state.StepCounter++
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(state.WGDirectory, fmt.Sprintf("%s.conf", interfaceName)))
if state.RunningAsRoot {
fmt.Printf(" - Set permissions: chmod 600 %s\n", filepath.Join(state.WGDirectory, 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(state.WGDirectory, 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 state.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()
}