Files
router/tools/ipadm/nftpf.go
T
mike 8591223aca ipadm: Port-Forwarding hinzufügen (-pa/-pl/-pd) + nftables-Include verdrahten
- ipadm verwaltet jetzt auch WAN->LAN Port-Forwards, referenziert per
  Hostname aus der bestehenden Host-DB (folgt IP-Änderungen automatisch)
- ipadm -u generiert zusätzlich /etc/nftables.d/portforward.conf, validiert
  via 'nft -c -f' und reloadet nftables (Rollback bei ungültiger Config,
  wie beim dnsmasq-Teil)
- /etc/nftables.conf bindet dafür neu /etc/nftables.d/*.conf ein
- Host-Store-Locking-Logik in generischen LineStore[T] extrahiert, von
  Host- und PortForward-Store gemeinsam genutzt
2026-08-18 11:43:29 +02:00

101 lines
3.2 KiB
Go

package main
import (
"fmt"
"os"
"os/exec"
"strings"
)
// renderPortForwards builds the content of the generated nftables include
// file (a standalone "inet portforward" table) from the current forwards.
// Forwards whose target host no longer exists in the host DB are skipped
// and reported back as warnings instead of failing the whole render.
func renderPortForwards(hosts []Host, fwds []PortForward, wanIface string) (content string, warnings []string) {
ipByHost := map[string]string{}
for _, h := range hosts {
ipByHost[strings.ToLower(h.Name)] = h.IP
}
sorted := make([]PortForward, len(fwds))
copy(sorted, fwds)
sortPortFwds(sorted)
var rules strings.Builder
for _, f := range sorted {
ip, ok := ipByHost[strings.ToLower(f.Host)]
if !ok {
warnings = append(warnings, fmt.Sprintf("Port-Forward %d/%s -> %q übersprungen: Host nicht (mehr) in der ipadm-Datenbank", f.WanPort, f.Proto, f.Host))
continue
}
switch f.Proto {
case "tcp", "udp":
fmt.Fprintf(&rules, "\t\tiifname %q %s dport %d dnat ip to %s:%d\n", wanIface, f.Proto, f.WanPort, ip, f.LanPort)
case "both":
fmt.Fprintf(&rules, "\t\tiifname %q meta l4proto { tcp, udp } th dport %d dnat ip to %s:%d\n", wanIface, f.WanPort, ip, f.LanPort)
}
}
var b strings.Builder
b.WriteString("# Generated by ipadm -u — DO NOT EDIT MANUALLY.\n")
b.WriteString("# Edit forwards with `ipadm -pa`/`ipadm -pd` and re-run `ipadm -u` to regenerate this file.\n\n")
b.WriteString("table inet portforward {\n")
b.WriteString("\tchain prerouting {\n")
b.WriteString("\t\ttype nat hook prerouting priority dstnat;\n")
b.WriteString(rules.String())
b.WriteString("\t}\n")
b.WriteString("}\n")
return b.String(), warnings
}
// applyPortForwards writes the generated table to path, validates the full
// nftables ruleset, and on success reloads nftables. On validation failure
// the previous file content is restored and the service is left untouched.
func applyPortForwards(path string, content string) error {
var backup []byte
hadFile := false
if b, err := os.ReadFile(path); err == nil {
backup = b
hadFile = true
} else if !os.IsNotExist(err) {
return fmt.Errorf("kann bestehende %s nicht lesen: %w", path, err)
}
if err := os.MkdirAll(dirOf(path), 0755); err != nil {
return fmt.Errorf("kann Verzeichnis für %s nicht anlegen: %w", path, err)
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
return fmt.Errorf("kann %s nicht schreiben: %w", path, err)
}
if err := runNftTest(); err != nil {
if hadFile {
os.WriteFile(path, backup, 0644)
} else {
os.Remove(path)
}
return fmt.Errorf("nftables-Konfiguration ungültig, Änderung zurückgerollt: %w", err)
}
if err := reloadNftables(); err != nil {
return fmt.Errorf("%s geschrieben, aber Reload fehlgeschlagen: %w", path, err)
}
return nil
}
func runNftTest() error {
out, err := exec.Command("nft", "-c", "-f", "/etc/nftables.conf").CombinedOutput()
if err != nil {
return fmt.Errorf("%s", strings.TrimSpace(string(out)))
}
return nil
}
func reloadNftables() error {
out, err := exec.Command("systemctl", "reload", "nftables").CombinedOutput()
if err != nil {
return fmt.Errorf("%s", strings.TrimSpace(string(out)))
}
return nil
}