package wireguard import ( "bytes" "fmt" "os/exec" ) // wg-quick is managed via systemd unit instances (wg-quick@). // Für ein BEREITS laufendes Interface werden Config-Änderungen per // `wg syncconf` LIVE angewendet (siehe syncWGQuick) — ohne Link-Flap, // damit bestehende Tunnel nie abreißen. Nur das erstmalige Hochfahren // (Interface noch nicht vorhanden) nutzt `systemctl start`. restart bleibt // als Fallback, falls syncconf nicht erlaubt/möglich ist. Die sudoers- // Einträge (postinst) whitelisten exakt diese Kommandos. // interfaceExists meldet ob das wg-Interface aktuell existiert (also von // wg-quick bereits hochgefahren wurde). `ip link show` braucht kein root. func interfaceExists(iface string) bool { return exec.Command("/usr/bin/ip", "link", "show", iface).Run() == nil } // syncWGQuick wendet Config-Änderungen LIVE auf ein laufendes Interface an // (`wg syncconf`) — Peers werden hinzugefügt/entfernt/aktualisiert und der // Listen-Port gesetzt, OHNE den Tunnel abzureißen. `wg-quick strip` liefert // die reine wg-Config (ohne Address/MTU/Routes-Direktiven). Beides braucht // root (Config ist root:root 700) → sudo. func syncWGQuick(iface string) error { stripped, err := exec.Command("sudo", "-n", "/usr/bin/wg-quick", "strip", iface).Output() if err != nil { return fmt.Errorf("wg-quick strip %s: %w", iface, err) } sync := exec.Command("sudo", "-n", "/usr/bin/wg", "syncconf", iface, "/dev/stdin") sync.Stdin = bytes.NewReader(stripped) if out, err := sync.CombinedOutput(); err != nil { return fmt.Errorf("wg syncconf %s: %w: %s", iface, err, string(out)) } return nil } func startWGQuick(iface string) error { cmd := exec.Command("sudo", "-n", "/usr/bin/systemctl", "start", "wg-quick@"+iface+".service") if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("systemctl start wg-quick@%s: %w: %s", iface, err, string(out)) } return nil } func restartWGQuick(iface string) error { cmd := exec.Command("sudo", "-n", "/usr/bin/systemctl", "restart", "wg-quick@"+iface+".service") if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("systemctl restart wg-quick@%s: %w: %s", iface, err, string(out)) } return nil } func stopWGQuick(iface string) error { cmd := exec.Command("sudo", "-n", "/usr/bin/systemctl", "stop", "wg-quick@"+iface+".service") // Ignore failures — unit may not exist. _ = cmd.Run() return nil } func enableWGQuick(iface string) error { cmd := exec.Command("sudo", "-n", "/usr/bin/systemctl", "enable", "wg-quick@"+iface+".service") if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("systemctl enable wg-quick@%s: %w: %s", iface, err, string(out)) } return nil } func disableWGQuick(iface string) error { cmd := exec.Command("sudo", "-n", "/usr/bin/systemctl", "disable", "wg-quick@"+iface+".service") // Ignore failures — unit may already be disabled. _ = cmd.Run() return nil } // symlinkWGQuickConf creates (or atomically replaces) the symlink // /etc/wireguard/.conf → target via sudo. /etc/wireguard/ is // owned root:root 700 so the edgeguard user cannot write to it directly; // the sudoers entry in postinst whitelists exactly this ln command. func symlinkWGQuickConf(iface, target string) error { link := "/etc/wireguard/" + iface + ".conf" cmd := exec.Command("sudo", "-n", "/bin/ln", "-sf", target, link) if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("ln -sf %s %s: %w: %s", target, link, err, string(out)) } return nil }