feat(dhcp): DHCPv4-Server via Kea (kea-dhcp4-server) — v1.2.92
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>
This commit is contained in:
59
internal/firewall/firewall_autorule_test.go
Normal file
59
internal/firewall/firewall_autorule_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user