- All ipadm-internal messages (usage, prompts, errors) are now English - Usage text is built programmatically from a (left, desc) row list with computed column width instead of hand-aligned spaces, so it can't drift out of alignment again when rows are added/changed - README's literal usage block updated to match; rest of the repo docs stay German
101 lines
3.1 KiB
Go
101 lines
3.1 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 skipped: host no longer in the ipadm database", 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("cannot read existing %s: %w", path, err)
|
|
}
|
|
|
|
if err := os.MkdirAll(dirOf(path), 0755); err != nil {
|
|
return fmt.Errorf("cannot create directory for %s: %w", path, err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
return fmt.Errorf("cannot write %s: %w", path, err)
|
|
}
|
|
|
|
if err := runNftTest(); err != nil {
|
|
if hadFile {
|
|
os.WriteFile(path, backup, 0644)
|
|
} else {
|
|
os.Remove(path)
|
|
}
|
|
return fmt.Errorf("nftables config invalid, change rolled back: %w", err)
|
|
}
|
|
|
|
if err := reloadNftables(); err != nil {
|
|
return fmt.Errorf("%s written, but reload failed: %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
|
|
}
|