Zwei Bugs, die WireGuard-Verbindungen verhinderten/abrissen: 1) Client-Config-Endpoint war hartkodierter Platzhalter REPLACE_WITH_PUBLIC_HOST → neue Clients bauten nie einen Tunnel auf (Host löst nicht auf). Jetzt: WireguardHandler.PublicHost (aus setup.json FQDN, main.go) → Endpoint = <fqdn>:<port>. Platzhalter nur noch als Fallback wenn FQDN unbekannt. 2) Renderer machte bei JEDER Config-Änderung 'systemctl restart wg-quick@<iface>' → voller Link-Flap, alle Peers droppen (verstößt gegen 'wireguard darf nie abbrechen'). Jetzt: laufendes Interface → 'wg-quick strip | wg syncconf' (Peers/Listen-Port live, KEIN Abbruch); nur erstmaliges Hochfahren via systemctl start; restart nur noch als Fallback mit WARN. interfaceExists() via 'ip link show'. Neue sudoers: wg syncconf *, wg-quick strip *. Ein edgeguard-api-Restart (Deploy) fasst wg-quick@<iface> nicht an → Tunnel bleibt während Deploy bestehen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
3.5 KiB
Go
91 lines
3.5 KiB
Go
package wireguard
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os/exec"
|
|
)
|
|
|
|
// wg-quick is managed via systemd unit instances (wg-quick@<iface>).
|
|
// 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/<iface>.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
|
|
}
|