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>
90 lines
2.4 KiB
Go
90 lines
2.4 KiB
Go
package kea
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/database"
|
|
)
|
|
|
|
// Guarded integration test: set EG_FWTEST_DSN (sonst skip).
|
|
func TestRender_DHCPConfig(t *testing.T) {
|
|
dsn := os.Getenv("EG_FWTEST_DSN")
|
|
if dsn == "" {
|
|
t.Skip("set EG_FWTEST_DSN to run the kea renderer test")
|
|
}
|
|
ctx := context.Background()
|
|
var mErr error
|
|
for i := 0; i < 3; i++ {
|
|
if mErr = database.Migrate(ctx, dsn); mErr == nil {
|
|
break
|
|
}
|
|
time.Sleep(700 * time.Millisecond)
|
|
}
|
|
if mErr != nil {
|
|
t.Fatalf("migrate: %v", mErr)
|
|
}
|
|
pool, err := database.Open(ctx, dsn)
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
|
|
for _, q := range []string{
|
|
`DELETE FROM dhcp_reservations`,
|
|
`DELETE FROM dhcp_subnets`,
|
|
`UPDATE dhcp_settings SET enabled=true, default_lease=3600, max_lease=7200, domain_name='lan', dns_servers='1.1.1.1, 8.8.8.8' WHERE id=1`,
|
|
} {
|
|
if _, err := pool.Exec(ctx, q); err != nil {
|
|
t.Fatalf("seed (%s): %v", q, err)
|
|
}
|
|
}
|
|
var subID int64
|
|
if err := pool.QueryRow(ctx, `
|
|
INSERT INTO dhcp_subnets (name, interface_name, subnet_cidr, pool_start, pool_end, gateway, dns_servers, active)
|
|
VALUES ('lan','eth1','10.0.0.0/24','10.0.0.100','10.0.0.200','10.0.0.1','',true) RETURNING id`).Scan(&subID); err != nil {
|
|
t.Fatalf("seed subnet: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO dhcp_reservations (subnet_id, mac_address, ip_address, hostname, active)
|
|
VALUES ($1,'aa:bb:cc:dd:ee:ff','10.0.0.50','printer',true)`, subID); err != nil {
|
|
t.Fatalf("seed reservation: %v", err)
|
|
}
|
|
|
|
out, err := New(pool).RenderToString(ctx)
|
|
if err != nil {
|
|
t.Fatalf("render: %v", err)
|
|
}
|
|
if !json.Valid([]byte(out)) {
|
|
t.Fatalf("rendered config is not valid JSON:\n%s", out)
|
|
}
|
|
for _, want := range []string{
|
|
`"10.0.0.0/24"`,
|
|
`"10.0.0.100 - 10.0.0.200"`,
|
|
`"aa:bb:cc:dd:ee:ff"`,
|
|
`"routers"`,
|
|
`"eth1"`,
|
|
`"hw-address"`,
|
|
`"valid-lifetime": 3600`,
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("rendered config missing %q\n----\n%s", want, out)
|
|
}
|
|
}
|
|
|
|
// Best-effort: echte Kea-Validierung, falls die Binary da ist.
|
|
if _, statErr := os.Stat(keaBinary); statErr == nil {
|
|
f, _ := os.CreateTemp(t.TempDir(), "kea-*.conf")
|
|
_, _ = f.WriteString(out)
|
|
f.Close()
|
|
if combined, err := exec.Command(keaBinary, "-t", f.Name()).CombinedOutput(); err != nil {
|
|
t.Fatalf("kea-dhcp4 -t rejected rendered config: %v\n%s", err, combined)
|
|
}
|
|
}
|
|
}
|