fix(wg+fw): Peer-Sync-Bug via sudo-Symlink + Site-to-Site-Masquerade

- WireGuard-Peer-Änderungen landeten nicht im laufenden Interface:
  /etc/wireguard/ ist root:root 700, os.Readlink schlug fehl →
  ensureWGQuickSymlink fiel immer in den Error-Pfad. Fix: Symlink
  via sudo /bin/ln -sf (sudoers-Entry in postinst ergänzt).

- Site-to-Site-Masquerade: Roadwarrior-Clients (z. B. 192.168.99.3)
  konnten LANs hinter anderen Peers nicht erreichen, weil das remote
  Gateway die VPN-Client-IP nicht als Tunnel-Route kannte. Fix: auto
  masquerade in nftables postrouting_nat pro WireGuard-Server-Interface
  (oifname "wg7" ip saddr 192.168.99.0/24 masquerade).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-21 16:31:54 +02:00
parent bc5d81d966
commit 6445e162a6
6 changed files with 56 additions and 26 deletions

View File

@@ -111,6 +111,21 @@ type View struct {
// interface) can be forwarded by the box. Without this, the
// forward policy=drop silently kills all inter-peer packets.
WGServerIfaces []string
// WGSiteMasq drives masquerade rules in postrouting_nat: one entry
// per active WireGuard server interface. Without masquerade, traffic
// from VPN roadwarrior clients (e.g. 192.168.99.3) forwarded to a
// site-to-site LAN (10.0.10.0/24) comes back with the client's tunnel
// IP as destination. The remote gateway (Unify Home) doesn't know
// that IP and drops the reply. Masquerade rewrites the source to the
// server's own tunnel IP so return traffic follows the same path back.
WGSiteMasq []WGSiteMasqEntry
}
// WGSiteMasqEntry is one WireGuard server interface's masquerade config.
type WGSiteMasqEntry struct {
Iface string // wg interface name, e.g. "wg7"
VPNNet string // network CIDR of the VPN subnet, e.g. "192.168.99.0/24"
}
// AutoFWRule is one auto-emitted inbound rule. Proto is "tcp" or
@@ -286,15 +301,21 @@ func (g *Generator) loadView(ctx context.Context) (*View, error) {
// ── Auto-Rules aus laufender Service-Config ──
view.AutoRules = g.loadAutoRules(ctx)
// ── WireGuard server-iface names (für forward-chain) ──
// ── WireGuard server-iface names (für forward-chain + site-to-site masquerade) ──
wgRows, err := g.Pool.Query(ctx,
`SELECT name FROM wireguard_interfaces WHERE active AND mode = 'server'`)
`SELECT name, address_cidr FROM wireguard_interfaces WHERE active AND mode = 'server'`)
if err == nil {
defer wgRows.Close()
for wgRows.Next() {
var name string
if wgRows.Scan(&name) == nil {
var name, cidr string
if wgRows.Scan(&name, &cidr) == nil {
view.WGServerIfaces = append(view.WGServerIfaces, name)
if _, ipNet, err := net.ParseCIDR(cidr); err == nil {
view.WGSiteMasq = append(view.WGSiteMasq, WGSiteMasqEntry{
Iface: name,
VPNNet: ipNet.String(),
})
}
}
}
}

View File

@@ -138,6 +138,16 @@ table inet edgeguard {
# client-IP (für Logging / Geo-Block: später optional via
# NAT-Rule-Flag preserve_client_ip).
ct status dnat masquerade
# Auto-Masquerade für WireGuard site-to-site: VPN-Clients (z. B. Roadwarrior
# mit 192.168.99.3) greifen auf LANs hinter anderen Peers zu (z. B. 10.0.10.0/24).
# Das entfernte Gateway (z. B. Unify Home) sieht als Return-Destination die
# VPN-Client-IP — die es nicht in seiner Routing-Table hat → Reply wird gedroppt.
# Masquerade schreibt die Source auf die lokale Tunnel-IP um; Return-Traffic
# findet so den Weg zurück durch den Tunnel.
{{range .WGSiteMasq}}
oifname "{{.Iface}}" ip saddr {{.VPNNet}} masquerade comment "auto: WireGuard site-to-site masquerade {{.Iface}}"
{{end}}
{{range .NATRules}}{{if eq .Kind "snat"}}
# NAT {{.ID}} (snat{{if .Comment}} — {{.Comment}}{{end}})
{{""}}