fix(firewall+nat): NAT funktioniert end-to-end + Edge-Sysctl-Profil

Mehrere zusammenhängende Fehler beim Import der NAT-Rules von der
alten EdgeGuard-Box gefunden + behoben:

1. nft-Template: NAT-Rules landeten als Comment (gleicher
   Whitespace-Trimmer-Bug wie bei den Operator-Rules vor zwei
   commits). Fix: Body auf eigener Zeile via {{""}}-Padding.

2. nft-Syntax-Reihenfolge: emittierte 'tcp ip daddr X dport Y' →
   parser-Fehler. Korrekt ist L3-match (ip saddr/daddr) zuerst,
   dann L4 (tcp/udp dport). Reihenfolge in der dnat-Zeile
   getauscht.

3. eth0 als Iface-Row hinzugefügt (Type ethernet, role wan) damit
   der zone→iface-Lookup für 'wan' tatsächlich auf das Linux-Iface
   trifft. Vorher war nur 'WAN'-bridge in der DB, das im Kernel
   nicht existiert → iifname-match griff nicht.

4. forward-chain: ct status dnat accept (DNAT-Pakete dürfen
   forwarden) + Auto-Forward pro SNAT/masquerade-Rule für die
   Origin-Pakete (return geht via established,related).

5. postrouting_nat: ct status dnat masquerade als Hairpin-Catch-All
   — sonst antwortet das DNAT-Ziel via seinem default-GW (oft
   nicht zur EdgeGuard-Box) → SYN_SENT + UNREPLIED. Trade-off:
   Backend sieht Box-IP statt client-IP.

6. Sysctl-Profil /etc/sysctl.d/99-edgeguard.conf bei jedem Install:
   - Forwarding (ip_forward + ipv6 forwarding) — Voraussetzung für
     ALLES NAT/DNAT/Masquerade.
   - Conntrack-Buckets + max=524288 (Edge-Box trackt viele
     parallele Sessions).
   - HAProxy-Tuning (somaxconn 64k, rmem/wmem 16M, keepalive,
     tcp_tw_reuse, ip_local_port_range).
   - BBR + fq als modernes Congestion-Control + Queueing.
   - Anti-DoS: tcp_syncookies, log_martians, kptr_restrict.

Verified end-to-end:
  $ nc -v 89.163.205.100 2030
  SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.16

Version 1.0.25.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-11 00:10:42 +02:00
parent 52da8d7c9e
commit e379162a7f
8 changed files with 119 additions and 22 deletions

View File

@@ -1 +1 @@
1.0.20
1.0.25

View File

@@ -39,7 +39,7 @@ import (
wgsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/wireguard"
)
var version = "1.0.20"
var version = "1.0.25"
func main() {
addr := os.Getenv("EDGEGUARD_API_ADDR")

View File

@@ -9,7 +9,7 @@ import (
"os"
)
var version = "1.0.20"
var version = "1.0.25"
const usage = `edgeguard-ctl — EdgeGuard CLI

View File

@@ -21,7 +21,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
)
var version = "1.0.20"
var version = "1.0.25"
const (
// renewTickInterval — how often we re-evaluate expiring certs.

View File

@@ -63,6 +63,22 @@ table inet edgeguard {
ct state established,related accept
ct state invalid drop
# DNAT-rewrites aus prerouting_nat haben den ct.status DNAT-Bit
# gesetzt — die müssen forward-passieren dürfen, sonst kommen
# Port-Forwards (z.B. :2030 → 10.10.20.12:22) zwar durch das
# NAT-Rewrite, scheitern aber an policy=drop. Equivalent zu
# iptables -m conntrack --ctstate DNAT.
ct status dnat accept
# Auto-Forward für SNAT/Masquerade-Origin-Pakete. Forward-chain
# sieht das Paket VOR der postrouting-Translation; ct.status ist
# also noch nicht "snat". Wir ziehen pro NAT-Rule das SrcCIDR
# nach und erlauben new-state-Pakete von dort. Return-Pakete
# gehen via ct state established schon durch.
{{range .NATRules}}{{if or (eq .Kind "snat") (eq .Kind "masquerade")}}{{if .SrcCIDR}}
ip saddr {{.SrcCIDR}} ct state new accept comment "auto-forward for NAT rule {{.ID}}"
{{end}}{{end}}{{end}}
}
chain output {
@@ -71,29 +87,36 @@ table inet edgeguard {
chain prerouting_nat {
type nat hook prerouting priority -100;
{{- range .NATRules}}{{if eq .Kind "dnat"}}
{{range .NATRules}}{{if eq .Kind "dnat"}}
# NAT {{.ID}} (dnat{{if .Comment}} — {{.Comment}}{{end}})
{{- if .InIfaces}} iifname { {{join .InIfaces ", "}} }{{end}}
{{- if and .Proto (ne .Proto "any")}} {{.Proto}}{{else}} meta l4proto { tcp, udp }{{end}}
{{- if .SrcCIDR}} ip saddr {{.SrcCIDR}}{{end}}
{{- if .DstCIDR}} ip daddr {{.DstCIDR}}{{end}}
{{- if .DPortStart}} dport {{.DPortStart}}{{if and .DPortEnd (ne .DPortEnd .DPortStart)}}-{{.DPortEnd}}{{end}}{{end}}
{{- if .TargetAddr}} dnat to {{.TargetAddr}}{{if .TargetPortStart}}:{{.TargetPortStart}}{{if and .TargetPortEnd (ne .TargetPortEnd .TargetPortStart)}}-{{.TargetPortEnd}}{{end}}{{end}}{{end}}
{{- end}}{{end}}
{{""}}
{{/* nft-Syntax: erst L3-match (ip saddr/daddr), DANN L4 (tcp/udp dport).
Sonst quittiert der parser '... unexpected ip' an dieser Stelle. */}}
{{if .InIfaces}}iifname { {{join .InIfaces ", "}} } {{end}}{{if .SrcCIDR}}ip saddr {{.SrcCIDR}} {{end}}{{if .DstCIDR}}ip daddr {{.DstCIDR}} {{end}}{{if and .Proto (ne .Proto "any")}}{{.Proto}} {{else}}meta l4proto { tcp, udp } {{end}}{{if .DPortStart}}dport {{.DPortStart}}{{if and .DPortEnd (ne .DPortEnd .DPortStart)}}-{{.DPortEnd}}{{end}} {{end}}{{if .TargetAddr}}dnat to {{.TargetAddr}}{{if .TargetPortStart}}:{{.TargetPortStart}}{{if and .TargetPortEnd (ne .TargetPortEnd .TargetPortStart)}}-{{.TargetPortEnd}}{{end}}{{end}}{{end}}
{{end}}{{end}}
}
chain postrouting_nat {
type nat hook postrouting priority 100;
{{- range .NATRules}}{{if eq .Kind "snat"}}
# Auto-Hairpin für DNAT-Pakete: alle in prerouting_nat
# umgeschriebenen Pakete bekommen zusätzlich SNAT auf die
# Box-IP des ausgehenden Iface (masquerade). Sonst antwortet
# das DNAT-Ziel via seinem eigenen default-Gateway, das oft
# nicht zur EdgeGuard-Box zeigt → SYN_SENT + UNREPLIED.
# Trade-off: Backend sieht die Box-IP statt der echten
# client-IP (für Logging / Geo-Block: später optional via
# NAT-Rule-Flag preserve_client_ip).
ct status dnat masquerade
{{range .NATRules}}{{if eq .Kind "snat"}}
# NAT {{.ID}} (snat{{if .Comment}} — {{.Comment}}{{end}})
{{- if .OutIfaces}} oifname { {{join .OutIfaces ", "}} }{{end}}
{{- if .SrcCIDR}} ip saddr {{.SrcCIDR}}{{end}}
{{- if .TargetAddr}} snat to {{.TargetAddr}}{{end}}
{{- end}}{{if eq .Kind "masquerade"}}
{{""}}
{{if .OutIfaces}}oifname { {{join .OutIfaces ", "}} } {{end}}{{if .SrcCIDR}}ip saddr {{.SrcCIDR}} {{end}}{{if .TargetAddr}}snat to {{.TargetAddr}}{{end}}
{{end}}{{if eq .Kind "masquerade"}}
# NAT {{.ID}} (masquerade{{if .Comment}} — {{.Comment}}{{end}})
{{- if .OutIfaces}} oifname { {{join .OutIfaces ", "}} }{{end}}
{{- if .SrcCIDR}} ip saddr {{.SrcCIDR}}{{end}} masquerade
{{- end}}{{end}}
{{""}}
{{if .OutIfaces}}oifname { {{join .OutIfaces ", "}} } {{end}}{{if .SrcCIDR}}ip saddr {{.SrcCIDR}} {{end}}masquerade
{{end}}{{end}}
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "edgeguard-management-ui",
"private": true,
"version": "1.0.20",
"version": "1.0.25",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -70,7 +70,7 @@ const NAV: NavSection[] = [
},
]
const VERSION = '1.0.20'
const VERSION = '1.0.25'
export default function Sidebar({ isOpen, onClose }: SidebarProps) {
const { t } = useTranslation()

View File

@@ -50,6 +50,80 @@ edgeguard ALL=(root) NOPASSWD: /usr/bin/wg show *
SUDOERS
chmod 0440 /etc/sudoers.d/edgeguard
# ── Sysctl-Profil für Edge-Gateway (NAT + HAProxy + Forwarding) ──
# Voraussetzung für NAT/DNAT/Masquerade + sinnvolle Defaults
# für eine high-throughput Forwarding-Box. Edit nicht von Hand
# — Re-install vom Package überschreibt die Datei. Eigene
# Tweaks gehören in eine Datei mit höherer Nummer als 99.
rm -f /etc/sysctl.d/99-edgeguard-forward.conf # Vorgänger
cat > /etc/sysctl.d/99-edgeguard.conf <<'SYSCTL'
# ── Managed by edgeguard ────────────────────────────────────────────
# Lade-Reihenfolge: 99-* überschreibt distro-Defaults. Eigene
# Operator-Tweaks: /etc/sysctl.d/99-zzz-local.conf (lexikografisch
# später) — nicht in DIESE Datei!
# ─── Forwarding (NAT/DNAT/Masquerade) ───────────────────────────────
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# ─── Reverse-Path-Filter (anti-spoof, loose-Modus für asymmetrisches
# Routing wie Multi-WAN / WireGuard split) ─────────────────────
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
# ─── Conntrack — Edge-Box trackt viele parallele Sessions ─────────
net.netfilter.nf_conntrack_max = 524288
net.netfilter.nf_conntrack_tcp_timeout_established = 86400
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 30
net.netfilter.nf_conntrack_buckets = 131072
# ─── TCP/IP-Stack-Tuning für HAProxy + viele Backends ─────────────
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 16384
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_no_metrics_save = 1
net.ipv4.ip_local_port_range = 10240 65535
# ─── Modern congestion control + queueing (BBR + fq) ──────────────
# Wenn der Kernel BBR nicht hat, fällt Linux still auf cubic zurück.
net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq
# ─── Anti-DoS / Hardening ─────────────────────────────────────────
net.ipv4.tcp_syncookies = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.conf.all.log_martians = 1
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
# ─── Memory ───────────────────────────────────────────────────────
vm.swappiness = 10
vm.dirty_ratio = 20
vm.dirty_background_ratio = 5
SYSCTL
sysctl --system >/dev/null 2>&1 || true
# ── Self-signed default cert so HAProxy starts cleanly ───────
# HAProxy `bind :443 ssl crt /etc/edgeguard/tls/` needs at least
# one PEM in the directory to come up. Operator runs certbot