package wireguard import ( "fmt" "log" "os" "path/filepath" "strings" ) // GenerateServerConfig generates a server-side WireGuard [Peer] configuration // and writes it to /etc/wireguard/conf.d/client-.conf func GenerateServerConfig(name, publicKey string, hasPSK bool, ipv4, ipv6, psk string) (string, error) { var builder strings.Builder builder.WriteString(fmt.Sprintf("[Peer]\n")) builder.WriteString(fmt.Sprintf("# %s\n", name)) builder.WriteString(fmt.Sprintf("PublicKey = %s\n", publicKey)) if hasPSK { builder.WriteString(fmt.Sprintf("PresharedKey = %s\n", psk)) } builder.WriteString(fmt.Sprintf("AllowedIPs = %s/32", ipv4)) if ipv6 != "" { builder.WriteString(fmt.Sprintf(", %s/128", ipv6)) } builder.WriteString("\n") configContent := builder.String() configDir := "/etc/wireguard/conf.d" configPath := filepath.Join(configDir, fmt.Sprintf("client-%s.conf", name)) if err := atomicWrite(configPath, configContent); err != nil { return "", fmt.Errorf("failed to write server config: %w", err) } log.Printf("Generated server config: %s", configPath) return configPath, nil } // GenerateClientConfig generates a client-side WireGuard configuration // with [Interface] and [Peer] sections and writes it to /etc/wireguard/clients/.conf func GenerateClientConfig(name, privateKey, ipv4, ipv6, dns, serverPublicKey, endpoint string, port int, hasPSK bool, psk string) (string, error) { var builder strings.Builder // [Interface] section builder.WriteString("[Interface]\n") builder.WriteString(fmt.Sprintf("PrivateKey = %s\n", privateKey)) builder.WriteString(fmt.Sprintf("Address = %s/24", ipv4)) if ipv6 != "" { builder.WriteString(fmt.Sprintf(", %s/64", ipv6)) } builder.WriteString("\n") builder.WriteString(fmt.Sprintf("DNS = %s\n", dns)) // [Peer] section builder.WriteString("\n[Peer]\n") builder.WriteString(fmt.Sprintf("PublicKey = %s\n", serverPublicKey)) if hasPSK { builder.WriteString(fmt.Sprintf("PresharedKey = %s\n", psk)) } builder.WriteString(fmt.Sprintf("Endpoint = %s:%d\n", endpoint, port)) builder.WriteString("AllowedIPs = 0.0.0.0/0, ::/0\n") builder.WriteString("PersistentKeepalive = 25\n") configContent := builder.String() clientsDir := "/etc/wireguard/clients" configPath := filepath.Join(clientsDir, fmt.Sprintf("%s.conf", name)) if err := atomicWrite(configPath, configContent); err != nil { return "", fmt.Errorf("failed to write client config: %w", err) } log.Printf("Generated client config: %s", configPath) return configPath, nil } // atomicWrite writes content to a file atomically // Uses temp file + rename pattern for atomicity and sets permissions to 0600 func atomicWrite(path, content string) error { // Ensure parent directory exists dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("failed to create directory %s: %w", dir, err) } // Write to temp file first tempPath := path + ".tmp" if err := os.WriteFile(tempPath, []byte(content), 0600); err != nil { return fmt.Errorf("failed to write temp file: %w", err) } // Atomic rename if err := os.Rename(tempPath, path); err != nil { // Clean up temp file if rename fails _ = os.Remove(tempPath) return fmt.Errorf("failed to rename temp file: %w", err) } return nil }