Verwalteter DHCPv4-Server analog Unbound/Squid/Chrony. - Migration 0041: dhcp_settings (singleton, node-lokal), dhcp_subnets, dhcp_reservations. - internal/kea: Renderer baut Kea-JSON via Go-Struct→Marshal (garantiert valide), managed /etc/edgeguard/kea/kea-dhcp4.conf (Symlink von /etc/kea), Service-Lifecycle an enabled gekoppelt (default AUS, kein rogue DHCP). Interface per NAME (cluster-sicher, kein node-lokaler FK). - internal/services/dhcp + internal/handlers/dhcp.go: Settings + Subnet/Reservation-CRUD, Validierung (CIDR/IP/MAC/interface exists). - configgen: Stop/Enable/DisableService. Firewall: AutoFWRule.Iface → udp/67 pro LAN-Interface gescopt (kein WAN). Cluster: subnets/reservations repliziert (hashSpec), dhcp_settings node-lokal (localOnlyTables). - main.go + render.go + WithAllReloaders Wiring. Packaging: kea-dhcp4-server Dependency, /etc/edgeguard/kea Dir, Symlink, disable-on-install, sudoers (restart/stop/enable/disable). - UI: DHCP-Seite (Settings + Subnets + Reservierungen pro Subnet), Route/Nav/i18n de/en, HA-Warnung 'nur auf einer Node aktivieren'. - Tests (guarded EG_FWTEST_DSN): Kea-Renderer gegen DB (valides JSON + Felder), FW-Auto-Rule-Iface inkl. nft -c. Scope v1: DHCPv4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
60 lines
1.8 KiB
Go
60 lines
1.8 KiB
Go
package firewall
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestTemplate_autoRuleIface prüft, dass eine Auto-Rule mit Iface als
|
|
// `iifname "<x>"`-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)
|
|
}
|
|
}
|