229 lines
7.0 KiB
Go

package production
import (
"crypto/rand"
"encoding/base64"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"golang.org/x/crypto/curve25519"
)
// WireGuardPeer represents a WireGuard mesh peer
type WireGuardPeer struct {
PublicKey string // Base64-encoded public key
Endpoint string // e.g., "141.227.165.154:51820"
AllowedIP string // e.g., "10.0.0.2/32"
}
// WireGuardConfig holds the configuration for a WireGuard interface
type WireGuardConfig struct {
PrivateIP string // e.g., "10.0.0.1"
ListenPort int // default 51820
PrivateKey string // Base64-encoded private key
Peers []WireGuardPeer // Known peers
}
// WireGuardProvisioner manages WireGuard VPN setup
type WireGuardProvisioner struct {
configDir string // /etc/wireguard
config WireGuardConfig
}
// NewWireGuardProvisioner creates a new WireGuard provisioner
func NewWireGuardProvisioner(config WireGuardConfig) *WireGuardProvisioner {
if config.ListenPort == 0 {
config.ListenPort = 51820
}
return &WireGuardProvisioner{
configDir: "/etc/wireguard",
config: config,
}
}
// IsInstalled checks if WireGuard tools are available
func (wp *WireGuardProvisioner) IsInstalled() bool {
_, err := exec.LookPath("wg")
return err == nil
}
// Install installs the WireGuard package
func (wp *WireGuardProvisioner) Install() error {
if wp.IsInstalled() {
return nil
}
cmd := exec.Command("apt-get", "install", "-y", "wireguard", "wireguard-tools")
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to install wireguard: %w\n%s", err, string(output))
}
return nil
}
// GenerateKeyPair generates a new WireGuard private/public key pair
func GenerateKeyPair() (privateKey, publicKey string, err error) {
// Generate 32 random bytes for private key
var privBytes [32]byte
if _, err := rand.Read(privBytes[:]); err != nil {
return "", "", fmt.Errorf("failed to generate random bytes: %w", err)
}
// Clamp private key per Curve25519 spec
privBytes[0] &= 248
privBytes[31] &= 127
privBytes[31] |= 64
// Derive public key
var pubBytes [32]byte
curve25519.ScalarBaseMult(&pubBytes, &privBytes)
privateKey = base64.StdEncoding.EncodeToString(privBytes[:])
publicKey = base64.StdEncoding.EncodeToString(pubBytes[:])
return privateKey, publicKey, nil
}
// PublicKeyFromPrivate derives the public key from a private key
func PublicKeyFromPrivate(privateKey string) (string, error) {
privBytes, err := base64.StdEncoding.DecodeString(privateKey)
if err != nil {
return "", fmt.Errorf("failed to decode private key: %w", err)
}
if len(privBytes) != 32 {
return "", fmt.Errorf("invalid private key length: %d", len(privBytes))
}
var priv, pub [32]byte
copy(priv[:], privBytes)
curve25519.ScalarBaseMult(&pub, &priv)
return base64.StdEncoding.EncodeToString(pub[:]), nil
}
// GenerateConfig returns the wg0.conf file content
func (wp *WireGuardProvisioner) GenerateConfig() string {
var sb strings.Builder
sb.WriteString("# WireGuard mesh configuration (managed by Orama Network)\n")
sb.WriteString("# Do not edit manually — use orama CLI to manage peers\n\n")
sb.WriteString("[Interface]\n")
sb.WriteString(fmt.Sprintf("PrivateKey = %s\n", wp.config.PrivateKey))
sb.WriteString(fmt.Sprintf("Address = %s/24\n", wp.config.PrivateIP))
sb.WriteString(fmt.Sprintf("ListenPort = %d\n", wp.config.ListenPort))
for _, peer := range wp.config.Peers {
sb.WriteString("\n[Peer]\n")
sb.WriteString(fmt.Sprintf("PublicKey = %s\n", peer.PublicKey))
if peer.Endpoint != "" {
sb.WriteString(fmt.Sprintf("Endpoint = %s\n", peer.Endpoint))
}
sb.WriteString(fmt.Sprintf("AllowedIPs = %s\n", peer.AllowedIP))
sb.WriteString("PersistentKeepalive = 25\n")
}
return sb.String()
}
// WriteConfig writes the WireGuard config to /etc/wireguard/wg0.conf
func (wp *WireGuardProvisioner) WriteConfig() error {
confPath := filepath.Join(wp.configDir, "wg0.conf")
content := wp.GenerateConfig()
// Try direct write first (works when running as root)
if err := os.MkdirAll(wp.configDir, 0700); err == nil {
if err := os.WriteFile(confPath, []byte(content), 0600); err == nil {
return nil
}
}
// Fallback to sudo tee (for non-root, e.g. debros user)
cmd := exec.Command("sudo", "tee", confPath)
cmd.Stdin = strings.NewReader(content)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to write wg0.conf via sudo: %w\n%s", err, string(output))
}
return nil
}
// Enable starts and enables the WireGuard interface
func (wp *WireGuardProvisioner) Enable() error {
// Enable on boot
cmd := exec.Command("systemctl", "enable", "wg-quick@wg0")
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to enable wg-quick@wg0: %w\n%s", err, string(output))
}
// Start now
cmd = exec.Command("systemctl", "start", "wg-quick@wg0")
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to start wg-quick@wg0: %w\n%s", err, string(output))
}
return nil
}
// Restart restarts the WireGuard interface
func (wp *WireGuardProvisioner) Restart() error {
cmd := exec.Command("systemctl", "restart", "wg-quick@wg0")
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to restart wg-quick@wg0: %w\n%s", err, string(output))
}
return nil
}
// IsActive checks if the WireGuard interface is up
func (wp *WireGuardProvisioner) IsActive() bool {
cmd := exec.Command("systemctl", "is-active", "--quiet", "wg-quick@wg0")
return cmd.Run() == nil
}
// AddPeer adds a peer to the running WireGuard interface without restart
func (wp *WireGuardProvisioner) AddPeer(peer WireGuardPeer) error {
// Add peer to running interface
args := []string{"wg", "set", "wg0", "peer", peer.PublicKey, "allowed-ips", peer.AllowedIP, "persistent-keepalive", "25"}
if peer.Endpoint != "" {
args = append(args, "endpoint", peer.Endpoint)
}
cmd := exec.Command("sudo", args...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to add peer %s: %w\n%s", peer.AllowedIP, err, string(output))
}
// Also update config file so it persists across restarts
wp.config.Peers = append(wp.config.Peers, peer)
return wp.WriteConfig()
}
// RemovePeer removes a peer from the running WireGuard interface
func (wp *WireGuardProvisioner) RemovePeer(publicKey string) error {
cmd := exec.Command("sudo", "wg", "set", "wg0", "peer", publicKey, "remove")
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to remove peer: %w\n%s", err, string(output))
}
// Remove from config
filtered := make([]WireGuardPeer, 0, len(wp.config.Peers))
for _, p := range wp.config.Peers {
if p.PublicKey != publicKey {
filtered = append(filtered, p)
}
}
wp.config.Peers = filtered
return wp.WriteConfig()
}
// GetStatus returns the current WireGuard interface status
func (wp *WireGuardProvisioner) GetStatus() (string, error) {
cmd := exec.Command("wg", "show", "wg0")
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to get wg status: %w\n%s", err, string(output))
}
return string(output), nil
}