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:
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -126,6 +127,7 @@ func boolean(m map[string]any, def bool, keys ...string) bool {
|
||||
// Form is what the network page edits.
|
||||
type Form struct {
|
||||
HostName string
|
||||
Domain string // the local domain (router.dns.domain)
|
||||
WAN WANForm
|
||||
LANs []LANForm
|
||||
}
|
||||
@@ -141,8 +143,7 @@ type WANForm struct {
|
||||
|
||||
type LANForm struct {
|
||||
Name string
|
||||
Interface string
|
||||
VLAN string
|
||||
Interface string // a device: "eth2", or "eth2.20" for a VLAN
|
||||
Address string
|
||||
Prefix string
|
||||
Internet bool
|
||||
@@ -160,6 +161,7 @@ func (s Settings) form() Form {
|
||||
}
|
||||
f := Form{
|
||||
HostName: str(s, "system", "hostName"),
|
||||
Domain: str(router, "dns", "domain"),
|
||||
WAN: WANForm{
|
||||
Interface: str(router, "wan", "interface"),
|
||||
Type: str(router, "wan", "ipv4", "type"),
|
||||
@@ -185,10 +187,13 @@ func (s Settings) form() Form {
|
||||
if l == nil {
|
||||
continue
|
||||
}
|
||||
dev := str(l, "interface")
|
||||
if vlan := str(l, "vlan"); vlan != "" {
|
||||
dev += "." + vlan
|
||||
}
|
||||
f.LANs = append(f.LANs, LANForm{
|
||||
Name: name,
|
||||
Interface: str(l, "interface"),
|
||||
VLAN: str(l, "vlan"),
|
||||
Interface: dev,
|
||||
Address: str(l, "ipv4", "address"),
|
||||
Prefix: str(l, "ipv4", "prefixLength"),
|
||||
Internet: boolean(l, false, "internet"),
|
||||
@@ -202,8 +207,10 @@ func (s Settings) form() Form {
|
||||
}
|
||||
|
||||
var (
|
||||
nameRe = regexp.MustCompile(`^[a-z][a-z0-9-]{0,14}$`)
|
||||
ifaceRe = regexp.MustCompile(`^[a-zA-Z0-9._-]{1,15}$`)
|
||||
nameRe = regexp.MustCompile(`^[a-z][a-z0-9-]{0,14}$`)
|
||||
// A DNS name of up to 253 characters.
|
||||
domainRe = regexp.MustCompile(`^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`)
|
||||
ifaceRe = regexp.MustCompile(`^[a-zA-Z0-9._-]{1,15}$`)
|
||||
)
|
||||
|
||||
// FormErrors maps a field to what is wrong with it.
|
||||
@@ -249,16 +256,31 @@ func splitVLAN(dev string) (string, int) {
|
||||
return dev[:i], id
|
||||
}
|
||||
|
||||
// badVLAN reports a device name that looks like a VLAN (interface.digits) but
|
||||
// whose ID is not from 1 to 4094.
|
||||
func badVLAN(dev string) bool {
|
||||
i := strings.LastIndex(dev, ".")
|
||||
if i <= 0 {
|
||||
return false
|
||||
}
|
||||
if _, err := strconv.Atoi(dev[i+1:]); err != nil {
|
||||
return false
|
||||
}
|
||||
_, id := splitVLAN(dev)
|
||||
return id == 0
|
||||
}
|
||||
|
||||
// Device is a row of the interface list: a physical NIC or a VLAN on one.
|
||||
type Device struct {
|
||||
Name string
|
||||
Kind string // "physical" or "VLAN"
|
||||
Role string // "WAN", "LAN lan", or ""
|
||||
Name string
|
||||
Kind string // "physical" or "VLAN"
|
||||
Role string // "WAN", "LAN lan", or ""
|
||||
Removable bool // a VLAN defined in the UI that nothing uses
|
||||
}
|
||||
|
||||
// devices lists the physical interfaces plus the VLAN devices the settings
|
||||
// use, each with the role it plays.
|
||||
func (f Form) devices(physical []string) []Device {
|
||||
// use or the UI defined (defined), each with the role it plays.
|
||||
func (f Form) devices(physical, defined []string) []Device {
|
||||
roles := map[string][]string{}
|
||||
add := func(dev, role string) { roles[dev] = append(roles[dev], role) }
|
||||
add(f.WAN.Interface, "WAN")
|
||||
@@ -266,26 +288,31 @@ func (f Form) devices(physical []string) []Device {
|
||||
if l.Name == "" || l.Remove {
|
||||
continue
|
||||
}
|
||||
dev := l.Interface
|
||||
if l.VLAN != "" {
|
||||
dev += "." + l.VLAN
|
||||
}
|
||||
add(dev, "LAN "+l.Name)
|
||||
add(l.Interface, "LAN "+l.Name)
|
||||
}
|
||||
// Interfaces the settings or the defined VLANs refer to are listed even
|
||||
// when this machine lacks them (other hardware).
|
||||
names := slices.Clone(physical)
|
||||
var vlans []string
|
||||
for dev := range roles {
|
||||
if _, id := splitVLAN(dev); id != 0 && !slices.Contains(names, dev) {
|
||||
var vlans, extra []string
|
||||
for _, dev := range append(slices.Collect(maps.Keys(roles)), defined...) {
|
||||
base, id := splitVLAN(dev)
|
||||
if id != 0 && !slices.Contains(vlans, dev) {
|
||||
vlans = append(vlans, dev)
|
||||
}
|
||||
if !slices.Contains(names, base) && !slices.Contains(extra, base) {
|
||||
extra = append(extra, base)
|
||||
}
|
||||
}
|
||||
slices.Sort(vlans)
|
||||
slices.Sort(extra)
|
||||
names = append(names, extra...)
|
||||
var out []Device
|
||||
for _, n := range names {
|
||||
out = append(out, Device{Name: n, Kind: "physical", Role: strings.Join(roles[n], ", ")})
|
||||
for _, v := range vlans {
|
||||
if parent, _ := splitVLAN(v); parent == n {
|
||||
out = append(out, Device{Name: v, Kind: "VLAN", Role: strings.Join(roles[v], ", ")})
|
||||
out = append(out, Device{Name: v, Kind: "VLAN", Role: strings.Join(roles[v], ", "),
|
||||
Removable: len(roles[v]) == 0 && slices.Contains(defined, v)})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,11 +340,23 @@ func (s Settings) apply(f Form, passwordFile string) (string, error) {
|
||||
system["hostName"] = f.HostName
|
||||
}
|
||||
|
||||
if f.Domain = strings.ToLower(strings.TrimSpace(f.Domain)); f.Domain == "" {
|
||||
if dns, ok := router["dns"].(map[string]any); ok {
|
||||
delete(dns, "domain")
|
||||
}
|
||||
} else if !domainRe.MatchString(f.Domain) {
|
||||
errs["domain"] = "letters, digits and dashes, in labels separated by dots (home.example.org)"
|
||||
} else {
|
||||
obj(router, "dns")["domain"] = f.Domain
|
||||
}
|
||||
|
||||
// A VLAN device (enp2s0.1001) is the physical interface plus the
|
||||
// PPPoE session's VLAN.
|
||||
wanDev, wanVLAN := splitVLAN(f.WAN.Interface)
|
||||
if !ifaceRe.MatchString(f.WAN.Interface) {
|
||||
errs["wan.interface"] = "an interface name is required"
|
||||
} else if badVLAN(f.WAN.Interface) {
|
||||
errs["wan.interface"] = "the VLAN ID must be from 1 to 4094"
|
||||
}
|
||||
wan["interface"] = wanDev
|
||||
ipv4cfg := obj(wan, "ipv4")
|
||||
@@ -383,12 +422,13 @@ func (s Settings) apply(f Form, passwordFile string) (string, error) {
|
||||
l := obj(lans, lf.Name)
|
||||
if !ifaceRe.MatchString(lf.Interface) {
|
||||
errs[field("interface")] = "an interface name is required"
|
||||
} else if badVLAN(lf.Interface) {
|
||||
errs[field("interface")] = "the VLAN ID must be from 1 to 4094"
|
||||
}
|
||||
l["interface"] = lf.Interface
|
||||
if vlan, ok, err := optionalInt(lf.VLAN, 1, 4094); err != nil {
|
||||
errs[field("vlan")] = err.Error()
|
||||
} else if ok {
|
||||
l["vlan"] = vlan
|
||||
lanDev, lanVLAN := splitVLAN(lf.Interface)
|
||||
l["interface"] = lanDev
|
||||
if lanVLAN != 0 {
|
||||
l["vlan"] = lanVLAN
|
||||
} else {
|
||||
delete(l, "vlan")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user