package firewall import ( "bytes" "os" "os/exec" "strings" "testing" ) // TestTemplate_autoRuleIface prüft, dass eine Auto-Rule mit Iface als // `iifname ""`-gescopte Zeile rendert (DHCP udp/67 auf LAN) und dass // DstIP-basierte Auto-Rules unverändert bleiben. func TestTemplate_autoRuleIface(t *testing.T) { view := &View{ AutoRules: []AutoFWRule{ {Proto: "udp", Port: 67, Iface: "eth1", Comment: "DHCP (Kea) auf eth1"}, {Proto: "udp", Port: 53, DstIP: "10.0.0.1", Comment: "DNS"}, }, } var buf bytes.Buffer if err := tpl.Execute(&buf, view); err != nil { t.Fatalf("template execute: %v", err) } out := buf.String() if !strings.Contains(out, `iifname "eth1" udp dport 67 accept comment "auto: DHCP (Kea) auf eth1"`) { t.Errorf("missing iface-scoped DHCP auto-rule\n----\n%s", out) } // Regression: DstIP-Auto-Rule ohne Iface bleibt unverändert. if !strings.Contains(out, `ip daddr 10.0.0.1 udp dport 53 accept`) { t.Errorf("DstIP auto-rule changed\n----\n%s", out) } // Echte nft-Syntaxvalidierung (braucht root → via sudo, sonst skip). nft, err := exec.LookPath("nft") if err != nil { t.Skip("nft not in PATH") } f, err := os.CreateTemp(t.TempDir(), "autorule-*.nft") if err != nil { t.Fatal(err) } _, _ = f.WriteString(out) f.Close() var cmd *exec.Cmd if os.Geteuid() == 0 { cmd = exec.Command(nft, "-c", "-f", f.Name()) } else { cmd = exec.Command("sudo", "-n", nft, "-c", "-f", f.Name()) } if combined, err := cmd.CombinedOutput(); err != nil { msg := string(combined) if strings.Contains(msg, "Operation not permitted") || strings.Contains(msg, "password is required") { t.Skipf("nft -c needs root: %s", strings.TrimSpace(msg)) } t.Fatalf("nft -c rejected ruleset: %v\n%s\n----\n%s", err, combined, out) } }