Files
edgeguard-native/internal/wireguard/systemd.go
Debian 91e51890dd fix(wireguard): Tunnel reißt nie ab + Client-Endpoint auto-befüllt — v1.2.100
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>
2026-06-06 20:51:52 +02:00

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
}