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
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user