Add WireGuard, DNS and more to the web UI and router-setup

Web UI: a WireGuard page (tunnels, peers, keys made on the router, client
configs with a QR code, import of wg-quick configs, live status), a DNS page
(records, wildcards, forward zones), interface dropdowns with VLAN devices,
IPv6 options and a local domain. router-setup asks about VLANs, IPv6 and
the domain. The router publishes its own name in DNS and hands the local
domain to DHCP clients; wg and jq are on the router. Includes a script
that takes over antigua's tunnels.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
2026-10-29 23:53:34 +01:00
co-authored by Claude Sonnet 5.5
parent 54040a1402
commit 84f6002d05
23 changed files with 2484 additions and 64 deletions
+32 -6
View File
@@ -89,14 +89,14 @@ func TestApplyValidates(t *testing.T) {
s := mustParse(t)
f := s.form()
f.LANs[0].Address = "not-an-ip"
f.LANs[0].VLAN = "5000"
f.LANs[0].Interface = "eth2.5000"
f.LANs = append(f.LANs, LANForm{Name: "Bad Name", Interface: "eth3", Address: "10.0.0.1"})
_, err := s.apply(f, "/pw")
var fe FormErrors
if !errors.As(err, &fe) {
t.Fatalf("expected form errors, got %v", err)
}
for _, k := range []string{"lans[0].address", "lans[0].vlan", "lans[1].name"} {
for _, k := range []string{"lans[0].address", "lans[0].interface", "lans[1].name"} {
if fe[k] == "" {
t.Errorf("no error for %s: %v", k, fe)
}
@@ -106,12 +106,12 @@ func TestApplyValidates(t *testing.T) {
func TestApplyAddsAndRemovesLANs(t *testing.T) {
s := mustParse(t)
f := s.form()
f.LANs = append(f.LANs, LANForm{Name: "guest", Interface: "eth2", VLAN: "20", Address: "192.168.30.1",
f.LANs = append(f.LANs, LANForm{Name: "guest", Interface: "eth2.20", Address: "192.168.30.1",
DHCP: true, PoolStart: "192.168.30.100", PoolEnd: "192.168.30.199"})
if _, err := s.apply(f, "/pw"); err != nil {
t.Fatal(err)
}
if got := s.form().LANs; len(got) != 2 || got[0].Name != "guest" || got[0].VLAN != "20" {
if got := s.form().LANs; len(got) != 2 || got[0].Name != "guest" || got[0].Interface != "eth2.20" {
t.Fatalf("after adding: %+v", got)
}
f = s.form()
@@ -153,8 +153,8 @@ func TestVLANUplinkAndIPv6(t *testing.T) {
if !s.form().LANs[0].IPv6PD || s.form().WAN.Interface != "enp2s0.1001" {
t.Errorf("form does not round-trip: %+v", s.form())
}
devs := s.form().devices([]string{"enp2s0", "wlp3s0"})
if len(devs) != 3 || devs[1].Name != "enp2s0.1001" || devs[1].Role != "WAN" {
devs := s.form().devices([]string{"enp2s0", "wlp3s0"}, []string{"enp2s0.77"})
if len(devs) != 5 || !devs[2].Removable || devs[1].Name != "enp2s0.1001" || devs[1].Role != "WAN" {
t.Errorf("devices = %+v", devs)
}
@@ -174,3 +174,29 @@ func TestVLANUplinkAndIPv6(t *testing.T) {
t.Error("IPv6 still on")
}
}
func TestLocalDomain(t *testing.T) {
s := mustParse(t)
f := s.form()
f.Domain = " Home.Example.org "
if _, err := s.apply(f, "/pw"); err != nil {
t.Fatal(err)
}
if got := str(s, "router", "dns", "domain"); got != "home.example.org" {
t.Errorf("domain = %q", got)
}
if s.form().Domain != "home.example.org" {
t.Error("the form does not show it")
}
f.Domain = "bad domain"
if _, err := s.apply(f, "/pw"); err == nil {
t.Error("a domain with a space was accepted")
}
f.Domain = ""
if _, err := s.apply(f, "/pw"); err != nil {
t.Fatal(err)
}
if got := str(s, "router", "dns", "domain"); got != "" {
t.Errorf("domain not removed: %q", got)
}
}