Compare commits
3 Commits
v1.2.99
...
7b6409b631
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b6409b631 | ||
|
|
a2450a759c | ||
|
|
91e51890dd |
@@ -431,7 +431,14 @@ func main() {
|
||||
wgReloader := func(ctx context.Context) error {
|
||||
return wgrender.New(pool, secretsBox).Render(ctx)
|
||||
}
|
||||
handlers.NewWireguardHandler(wgIfaces, wgPeers, secretsBox, auditRepo, nodeID, withFW(wgReloader)).Register(authed)
|
||||
// Öffentlicher WG-Endpoint-Host für Peer-Configs = FQDN dieser Node
|
||||
// (aus setup.json). Verhindert den REPLACE_WITH_PUBLIC_HOST-Platzhalter,
|
||||
// an dem Clients sonst keinen Tunnel aufbauen können.
|
||||
wgPublicHost := ""
|
||||
if sst, serr := setupStore.Load(); serr == nil && sst != nil {
|
||||
wgPublicHost = sst.FQDN
|
||||
}
|
||||
handlers.NewWireguardHandler(wgIfaces, wgPeers, secretsBox, auditRepo, nodeID, withFW(wgReloader)).WithPublicHost(wgPublicHost).Register(authed)
|
||||
|
||||
// Squid forward-proxy reload — re-render squid.conf + reload
|
||||
// squid.service. sudoers im postinst whitelistet das. ACL-Count
|
||||
|
||||
@@ -177,14 +177,14 @@ type RuleLeg struct {
|
||||
// template just emits one nft line per "leg" of the cross-product.
|
||||
type ResolvedRule struct {
|
||||
ID int64
|
||||
Action string // accept | drop | reject
|
||||
Action string // accept | drop | reject
|
||||
Log bool
|
||||
Name string
|
||||
Priority int
|
||||
|
||||
SrcIfaces []string // empty = any
|
||||
DstIfaces []string // empty = any
|
||||
SrcAddrs []string // each is an nft expression like "1.2.3.4" or "10.0.0.0/24" or "{ 1.2.3.4, 5.6.7.8 }"
|
||||
SrcIfaces []string // empty = any
|
||||
DstIfaces []string // empty = any
|
||||
SrcAddrs []string // each is an nft expression like "1.2.3.4" or "10.0.0.0/24" or "{ 1.2.3.4, 5.6.7.8 }"
|
||||
DstAddrs []string
|
||||
Services []ResolvedService // empty = any
|
||||
Comment string
|
||||
@@ -192,16 +192,16 @@ type ResolvedRule struct {
|
||||
|
||||
// ResolvedNATRule is one nat-rule joined with iface-sets.
|
||||
type ResolvedNATRule struct {
|
||||
ID int64
|
||||
Kind string // dnat | snat | masquerade
|
||||
Priority int
|
||||
InIfaces []string
|
||||
OutIfaces []string
|
||||
Proto string // empty = any
|
||||
SrcCIDR string
|
||||
DstCIDR string
|
||||
DPortStart, DPortEnd int
|
||||
TargetAddr string
|
||||
ID int64
|
||||
Kind string // dnat | snat | masquerade
|
||||
Priority int
|
||||
InIfaces []string
|
||||
OutIfaces []string
|
||||
Proto string // empty = any
|
||||
SrcCIDR string
|
||||
DstCIDR string
|
||||
DPortStart, DPortEnd int
|
||||
TargetAddr string
|
||||
TargetPortStart, TargetPortEnd int
|
||||
// L3 ist "ip" oder "ip6" — Adressfamilie der Regel (aus SrcCIDR/
|
||||
// DstCIDR/TargetAddr abgeleitet). TargetHost ist TargetAddr, bei
|
||||
@@ -209,14 +209,14 @@ type ResolvedNATRule struct {
|
||||
// nft-dnat-Syntax.
|
||||
L3 string
|
||||
TargetHost string
|
||||
Comment string
|
||||
Comment string
|
||||
}
|
||||
|
||||
// ResolvedService is one nft (proto, dport-spec) tuple.
|
||||
type ResolvedService struct {
|
||||
Proto string // tcp|udp|icmp|icmpv6
|
||||
PortStart int // 0 = no port match
|
||||
PortEnd int
|
||||
Proto string // tcp|udp|icmp|icmpv6
|
||||
PortStart int // 0 = no port match
|
||||
PortEnd int
|
||||
}
|
||||
|
||||
func (g *Generator) loadView(ctx context.Context) (*View, error) {
|
||||
@@ -269,6 +269,31 @@ func (g *Generator) loadView(ctx context.Context) (*View, error) {
|
||||
}
|
||||
peerRows.Close()
|
||||
|
||||
// ── Heartbeat-IPs aus cluster_settings ins Peer-Set ──
|
||||
// Der VRRP-Heartbeat (VI_HB) läuft über hb_src_ip/hb_peer_ip (z.B.
|
||||
// 169.254.0.1/.2) — diese stehen NICHT in ha_nodes. Ohne sie würde die
|
||||
// VRRP-Accept-Regel den Heartbeat-Pfad nicht abdecken. Best-effort:
|
||||
// fehlt cluster_settings (Single-Node), bleibt es bei den ha_nodes-IPs.
|
||||
var hbSrc, hbPeer *string
|
||||
if err := g.Pool.QueryRow(ctx,
|
||||
`SELECT hb_src_ip, hb_peer_ip FROM cluster_settings WHERE id = 1`).
|
||||
Scan(&hbSrc, &hbPeer); err == nil {
|
||||
for _, ip := range []*string{hbSrc, hbPeer} {
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
parsed := net.ParseIP(*ip)
|
||||
if parsed == nil {
|
||||
continue
|
||||
}
|
||||
if parsed.To4() != nil {
|
||||
view.PeerIPv4 = append(view.PeerIPv4, parsed.String())
|
||||
} else {
|
||||
view.PeerIPv6 = append(view.PeerIPv6, parsed.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Lade Address-Objects + Groups → ID → ResolvedAddr-list ──
|
||||
addrObjs, err := g.loadAddrObjects(ctx)
|
||||
if err != nil {
|
||||
@@ -777,15 +802,15 @@ ORDER BY priority DESC, id ASC`)
|
||||
out := []ResolvedRule{}
|
||||
for rows.Next() {
|
||||
var (
|
||||
id int64
|
||||
name, action, com string
|
||||
pr int
|
||||
log bool
|
||||
srcZone, dstZone string
|
||||
srcObjID, srcGrpID *int64
|
||||
dstObjID, dstGrpID *int64
|
||||
srcCIDR, dstCIDR *string
|
||||
svcObjID, svcGrpID *int64
|
||||
id int64
|
||||
name, action, com string
|
||||
pr int
|
||||
log bool
|
||||
srcZone, dstZone string
|
||||
srcObjID, srcGrpID *int64
|
||||
dstObjID, dstGrpID *int64
|
||||
srcCIDR, dstCIDR *string
|
||||
svcObjID, svcGrpID *int64
|
||||
)
|
||||
if err := rows.Scan(
|
||||
&id, &name, &pr, &action, &log, &com,
|
||||
@@ -842,12 +867,12 @@ ORDER BY priority DESC, id ASC`)
|
||||
out := []ResolvedNATRule{}
|
||||
for rows.Next() {
|
||||
var (
|
||||
id int64
|
||||
pr int
|
||||
kind, com string
|
||||
id int64
|
||||
pr int
|
||||
kind, com string
|
||||
inZone, outZone, proto, srcCIDR, dstCIDR *string
|
||||
dpStart, dpEnd, tpStart, tpEnd int
|
||||
targetAddr string
|
||||
dpStart, dpEnd, tpStart, tpEnd int
|
||||
targetAddr string
|
||||
)
|
||||
if err := rows.Scan(
|
||||
&id, &pr, &kind, &com,
|
||||
@@ -861,7 +886,7 @@ ORDER BY priority DESC, id ASC`)
|
||||
r := ResolvedNATRule{
|
||||
ID: id, Kind: kind, Priority: pr, Comment: com,
|
||||
DPortStart: dpStart, DPortEnd: dpEnd,
|
||||
TargetAddr: targetAddr,
|
||||
TargetAddr: targetAddr,
|
||||
TargetPortStart: tpStart, TargetPortEnd: tpEnd,
|
||||
}
|
||||
if proto != nil {
|
||||
|
||||
@@ -55,6 +55,13 @@ table inet edgeguard {
|
||||
tcp dport 5432 ip6 saddr @peer_ipv6 accept
|
||||
tcp dport 6379 ip saddr @peer_ipv4 accept
|
||||
tcp dport 6379 ip6 saddr @peer_ipv6 accept
|
||||
# Cluster-internal: VRRP-Advertisements (keepalived VIP-Failover, Proto 112).
|
||||
# OHNE diese Regel überleben Adverts nur via conntrack-Reverse-Matching —
|
||||
# läuft ein conntrack-Eintrag ab/wird geflusht, werden Adverts gedroppt →
|
||||
# der Peer promotet sich → VIP-Flapping/Split-Brain. peer_ipv4/6 enthält
|
||||
# Public- UND Heartbeat-IPs (ha_nodes + cluster_settings.hb_*).
|
||||
ip protocol vrrp ip saddr @peer_ipv4 accept
|
||||
ip6 nexthdr vrrp ip6 saddr @peer_ipv6 accept
|
||||
|
||||
# ── Service-Auto-Rules (DNS/Squid/WG/...) ──
|
||||
# Aus dem laufenden Service-State abgeleitet — Operator
|
||||
|
||||
@@ -32,6 +32,10 @@ type WireguardHandler struct {
|
||||
Audit *audit.Repo
|
||||
NodeID string
|
||||
Reloader func(ctx context.Context) error
|
||||
// PublicHost ist der öffentliche Host (FQDN/IP), den Clients als
|
||||
// WireGuard-Endpoint anwählen. Wird in heruntergeladene Peer-Configs
|
||||
// geschrieben (statt eines Platzhalters). Leer → Platzhalter (Fallback).
|
||||
PublicHost string
|
||||
}
|
||||
|
||||
func NewWireguardHandler(
|
||||
@@ -45,6 +49,12 @@ func NewWireguardHandler(
|
||||
return &WireguardHandler{Ifaces: ifaces, Peers: peers, Box: box, Audit: a, NodeID: nodeID, Reloader: reloader}
|
||||
}
|
||||
|
||||
// WithPublicHost setzt den öffentlichen Endpoint-Host für Peer-Configs.
|
||||
func (h *WireguardHandler) WithPublicHost(host string) *WireguardHandler {
|
||||
h.PublicHost = strings.TrimSpace(host)
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *WireguardHandler) reload(ctx context.Context, op string) {
|
||||
if h.Reloader == nil {
|
||||
return
|
||||
@@ -702,11 +712,15 @@ func (h *WireguardHandler) peerConfigText(ctx context.Context, peerID int64) (st
|
||||
clientAllowedIPs += ", " + strings.TrimSpace(*ifc.ClientRoutes)
|
||||
}
|
||||
fmt.Fprintf(&b, "AllowedIPs = %s\n", clientAllowedIPs)
|
||||
// Endpoint — the operator's public host:port that peers dial.
|
||||
// We don't know this here (could be a CNAME or behind a load
|
||||
// balancer); leave a placeholder the operator must fill in.
|
||||
// Endpoint — der öffentliche Host:Port, den Clients anwählen. Standard
|
||||
// ist der FQDN dieser Node (PublicHost, aus setup.json). Nur wenn der
|
||||
// nicht ermittelbar ist, bleibt ein Platzhalter den der Operator füllt.
|
||||
if ifc.ListenPort != nil {
|
||||
fmt.Fprintf(&b, "Endpoint = REPLACE_WITH_PUBLIC_HOST:%d\n", *ifc.ListenPort)
|
||||
host := h.PublicHost
|
||||
if host == "" {
|
||||
host = "REPLACE_WITH_PUBLIC_HOST"
|
||||
}
|
||||
fmt.Fprintf(&b, "Endpoint = %s:%d\n", host, *ifc.ListenPort)
|
||||
}
|
||||
if p.Keepalive != nil && *p.Keepalive > 0 {
|
||||
fmt.Fprintf(&b, "PersistentKeepalive = %d\n", *p.Keepalive)
|
||||
|
||||
@@ -16,7 +16,7 @@ vrrp_script chk_gateway {
|
||||
script "/usr/lib/edgeguard/keepalived-gw-check.sh {{ .GWCheckIP }}"
|
||||
interval 5
|
||||
weight -110
|
||||
fall 2
|
||||
fall 5
|
||||
rise 2
|
||||
}
|
||||
{{ end }}
|
||||
@@ -34,6 +34,7 @@ vrrp_instance VI_1 {
|
||||
virtual_router_id {{ .RouterID }}
|
||||
priority {{ .Priority }}
|
||||
advert_int 1
|
||||
nopreempt
|
||||
{{ if .SrcIP }} unicast_src_ip {{ .SrcIP }}
|
||||
unicast_peer {
|
||||
{{ .PeerIP }}
|
||||
@@ -60,6 +61,7 @@ vrrp_instance VI_HB {
|
||||
virtual_router_id {{ .HBRouterID }}
|
||||
priority {{ .Priority }}
|
||||
advert_int 1
|
||||
nopreempt
|
||||
{{ if .HBSrcIP }} unicast_src_ip {{ .HBSrcIP }}
|
||||
unicast_peer {
|
||||
{{ .HBPeerIP }}
|
||||
|
||||
@@ -167,17 +167,19 @@ func (g *generator) buildView(cs *models.ClusterSettings, vips []VIPEntry, local
|
||||
v.HBRouterID = 52
|
||||
}
|
||||
|
||||
// pg_role=standby ist das härtere Signal — ein Standby-Node ist niemals
|
||||
// MASTER, auch wenn role='primary' noch aus dem Join-Prozess stammt.
|
||||
// Reihenfolge: standby → BACKUP; sonst primary-Check.
|
||||
// State IMMER BACKUP: das Template setzt `nopreempt`, und nopreempt wirkt
|
||||
// in keepalived NUR, wenn die Instanz im BACKUP-Zustand startet (bei state
|
||||
// MASTER wird nopreempt ignoriert). Die Priorität entscheidet weiterhin die
|
||||
// Initial-Election (primary=200 gewinnt), aber ein erholter Node reißt die
|
||||
// VIP NICHT mehr zurück → kein Flap-Back / Split-Brain. Deckt sich mit der
|
||||
// "kein Auto-Promote"-Philosophie: Promotion bleibt manuell.
|
||||
// pg_role=standby ist das härtere Signal (Standby ist nie bevorzugter Node).
|
||||
v.State = "BACKUP"
|
||||
if local.PGRole == "standby" {
|
||||
v.State = "BACKUP"
|
||||
v.Priority = 100
|
||||
} else if local.PGRole == "primary" || local.Role == "primary" {
|
||||
v.State = "MASTER"
|
||||
v.Priority = 200
|
||||
} else {
|
||||
v.State = "BACKUP"
|
||||
v.Priority = 100
|
||||
}
|
||||
|
||||
|
||||
79
internal/keepalived/keepalived_test.go
Normal file
79
internal/keepalived/keepalived_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package keepalived
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
||||
)
|
||||
|
||||
func render(t *testing.T, v View) string {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
if err := tpl.Execute(&buf, v); err != nil {
|
||||
t.Fatalf("template execute: %v", err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Split-Brain-Schutz: jede vrrp_instance MUSS `nopreempt` tragen, sonst reißt
|
||||
// ein erholter Node die VIP zurück → Flapping. nopreempt wirkt nur bei state
|
||||
// BACKUP — also muss auch der bevorzugte Node BACKUP starten.
|
||||
func testView() View {
|
||||
return View{
|
||||
State: "BACKUP", Interface: "eth0", RouterID: 51, Priority: 200,
|
||||
SrcIP: "89.163.205.6", PeerIP: "89.163.205.8", AuthPass: "edgeguard",
|
||||
VIPs: []VIPEntry{{Address: "89.163.205.100", Prefix: 24, Device: "eth0"}},
|
||||
HBInterface: "ens19", HBSrcIP: "169.254.0.1", HBPeerIP: "169.254.0.2", HBRouterID: 52,
|
||||
GWCheckIP: "89.163.205.1",
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateNopreemptOnBothInstances(t *testing.T) {
|
||||
out := render(t, testView())
|
||||
if n := strings.Count(out, "nopreempt"); n != 2 {
|
||||
t.Fatalf("erwarte nopreempt in VI_1 UND VI_HB (2×), gefunden: %d\n%s", n, out)
|
||||
}
|
||||
if strings.Contains(out, "state MASTER") {
|
||||
t.Fatalf("kein Node darf state MASTER starten (nopreempt würde ignoriert):\n%s", out)
|
||||
}
|
||||
if c := strings.Count(out, "state BACKUP"); c != 2 {
|
||||
t.Fatalf("erwarte state BACKUP in beiden Instanzen, gefunden: %d", c)
|
||||
}
|
||||
}
|
||||
|
||||
// gw-Check darf nicht zu zucken (fall 5, nicht fall 2) — ein kurzer Upstream-
|
||||
// Blip soll keinen Failover erzwingen.
|
||||
func TestTemplateGatewayCheckNotTwitchy(t *testing.T) {
|
||||
out := render(t, testView())
|
||||
if !strings.Contains(out, "fall 5") {
|
||||
t.Fatalf("chk_gateway sollte fall 5 nutzen:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// buildView: State immer BACKUP, Priorität aus pg_role.
|
||||
func TestBuildViewStateAlwaysBackup(t *testing.T) {
|
||||
g := &generator{localID: "n1"}
|
||||
cs := &models.ClusterSettings{VRRPRouterID: 51}
|
||||
pub := "89.163.205.6"
|
||||
cases := []struct {
|
||||
pgRole, role string
|
||||
wantPrio int
|
||||
}{
|
||||
{"primary", "primary", 200},
|
||||
{"standby", "primary", 100},
|
||||
{"", "primary", 200},
|
||||
{"", "", 100},
|
||||
}
|
||||
for _, c := range cases {
|
||||
local := &models.HANode{ID: "n1", PGRole: c.pgRole, Role: c.role, PublicIP: &pub}
|
||||
v := g.buildView(cs, nil, local, nil)
|
||||
if v.State != "BACKUP" {
|
||||
t.Errorf("pg_role=%q role=%q: State=%q, erwarte immer BACKUP (nopreempt)", c.pgRole, c.role, v.State)
|
||||
}
|
||||
if v.Priority != c.wantPrio {
|
||||
t.Errorf("pg_role=%q role=%q: Priority=%d, erwarte %d", c.pgRole, c.role, v.Priority, c.wantPrio)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,11 +40,17 @@ func (g *Generator) render(ctx context.Context, excludeEthernet bool) error {
|
||||
prefix int
|
||||
}
|
||||
|
||||
// is_vip-Adressen werden NIE statisch gebunden — sie gehören
|
||||
// ausschließlich keepalived (nur der VRRP-Master trägt die VIP). Würde
|
||||
// der Apply sie statisch binden, läge die VIP nach einem Failover auf
|
||||
// BEIDEN Nodes (statisch hier + keepalived drüben) → Duplicate-IP/ARP-
|
||||
// Konflikt → Tunnel/LAN bricht. Deshalb hart ausschließen.
|
||||
q := `
|
||||
SELECT ni.name, ia.address, ia.prefix
|
||||
FROM ip_addresses ia
|
||||
JOIN network_interfaces ni ON ni.id = ia.interface_id
|
||||
WHERE ia.active = true`
|
||||
WHERE ia.active = true
|
||||
AND ia.is_vip = false`
|
||||
if excludeEthernet {
|
||||
q += `
|
||||
AND ni.type != 'ethernet'`
|
||||
|
||||
@@ -1,15 +1,42 @@
|
||||
package wireguard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// wg-quick is managed via systemd unit instances (wg-quick@<iface>).
|
||||
// Reload-via-syncconf would be cheaper (no link flap) but needs more
|
||||
// per-change diffing — for v1 we restart the unit, which takes ~1s
|
||||
// and re-establishes peers cleanly. The sudoers entry shipped in
|
||||
// postinst whitelists exactly these three commands.
|
||||
// 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")
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -254,9 +255,24 @@ func (g *Generator) renderIface(ctx context.Context, ifc models.WireguardInterfa
|
||||
return fmt.Errorf("symlink: %w", err)
|
||||
}
|
||||
_ = enableWGQuick(ifc.Name)
|
||||
if !changed {
|
||||
return startWGQuick(ifc.Name)
|
||||
|
||||
// Läuft das Interface schon, werden Änderungen LIVE per `wg syncconf`
|
||||
// angewendet — KEIN Tunnel-Abbruch (WireGuard darf nie abreißen). Nur
|
||||
// das erstmalige Hochfahren nutzt `systemctl start`.
|
||||
if interfaceExists(ifc.Name) {
|
||||
if !changed {
|
||||
return nil // läuft + Config unverändert → nichts zu tun
|
||||
}
|
||||
if err := syncWGQuick(ifc.Name); err != nil {
|
||||
// Fallback (z. B. sudoers noch ohne syncconf): voller Neustart.
|
||||
// Bricht den Tunnel kurz ab — nur Notnagel.
|
||||
slog.Warn("wireguard: wg syncconf fehlgeschlagen, Fallback auf restart (kurzer Tunnel-Flap)",
|
||||
"iface", ifc.Name, "error", err)
|
||||
return restartWGQuick(ifc.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return restartWGQuick(ifc.Name)
|
||||
// Interface noch nicht oben → erstmalig hochfahren.
|
||||
return startWGQuick(ifc.Name)
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,9 @@ edgeguard ALL=(root) NOPASSWD: /bin/systemctl enable wg-quick@*.service
|
||||
edgeguard ALL=(root) NOPASSWD: /bin/systemctl disable wg-quick@*.service
|
||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/wg show all dump
|
||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/wg show *
|
||||
# WireGuard Live-Reload ohne Tunnel-Abbruch: wg syncconf + wg-quick strip
|
||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/wg syncconf *
|
||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/wg-quick strip *
|
||||
# WireGuard symlink: /etc/wireguard/ ist root:root 700; edgeguard-api
|
||||
# legt Symlinks an damit wg-quick@<iface> die Configs findet.
|
||||
edgeguard ALL=(root) NOPASSWD: /bin/ln -sf /etc/edgeguard/wireguard/* /etc/wireguard/*
|
||||
@@ -842,6 +845,25 @@ EOSQL
|
||||
# atomic-write (tempfile → rename) durchführen kann.
|
||||
install -d -m 0755 /etc/keepalived
|
||||
chown "$EG_USER":"$EG_USER" /etc/keepalived
|
||||
# ── keepalived systemd drop-in: Boot-Race-Fix ──────────────────
|
||||
# keepalived referenziert VIP-Devices (vlanXXX), die erst von
|
||||
# edgeguard-interfaces.service angelegt werden. Ohne Ordering startet
|
||||
# keepalived vor den VLANs (beide nur After=network-online.target) →
|
||||
# "interface vlanX doesn't exist" → permanenter CONFIG-Crash OHNE
|
||||
# Auto-Recovery (keepalived bleibt nach Reboot tot). After=/Wants=
|
||||
# wartet auf die Interfaces; Restart=on-failure ist das Sicherheitsnetz.
|
||||
install -d /etc/systemd/system/keepalived.service.d
|
||||
cat > /etc/systemd/system/keepalived.service.d/10-edgeguard.conf <<'KEEPALIVEDDROPIN'
|
||||
[Unit]
|
||||
After=edgeguard-interfaces.service
|
||||
Wants=edgeguard-interfaces.service
|
||||
StartLimitIntervalSec=0
|
||||
|
||||
[Service]
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
KEEPALIVEDDROPIN
|
||||
systemctl daemon-reload
|
||||
sudo -n -u "$EG_USER" /usr/bin/edgeguard-ctl render-config --only=keepalived || true
|
||||
if [ -f /etc/keepalived/keepalived.conf ]; then
|
||||
systemctl enable keepalived >/dev/null 2>&1 || true
|
||||
|
||||
Reference in New Issue
Block a user