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:
@@ -35,6 +35,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -221,6 +222,10 @@ func newApp(sys System, metrics *Collector, dev bool) *app {
|
||||
"chartW": func() float64 { return chartW },
|
||||
"chartH": func() float64 { return chartH },
|
||||
"join": strings.Join,
|
||||
"choices": choices,
|
||||
"tform": func(t WGTunnel, csrf string, isNew bool) map[string]any {
|
||||
return map[string]any{"T": t, "CSRF": csrf, "New": isNew}
|
||||
},
|
||||
}
|
||||
pages, err := a.parseTemplates()
|
||||
if err != nil {
|
||||
@@ -233,7 +238,7 @@ func newApp(sys System, metrics *Collector, dev bool) *app {
|
||||
func (a *app) parseTemplates() (map[string]*template.Template, error) {
|
||||
files := map[string][]string{"overview": {"templates/dashboard.html"}}
|
||||
pages := map[string]*template.Template{}
|
||||
for _, page := range []string{"login", "overview", "network", "dhcp", "advanced", "update"} {
|
||||
for _, page := range []string{"login", "overview", "network", "dhcp", "dns", "wireguard", "advanced", "update"} {
|
||||
t, err := template.New("layout.html").Funcs(a.funcs).
|
||||
ParseFS(assets, append([]string{"templates/layout.html", "templates/" + page + ".html"}, files[page]...)...)
|
||||
if err != nil {
|
||||
@@ -274,6 +279,14 @@ func (a *app) routes() http.Handler {
|
||||
mux.HandleFunc("POST /network", a.auth(a.network))
|
||||
mux.HandleFunc("GET /advanced", a.auth(a.advancedPage))
|
||||
mux.HandleFunc("POST /advanced", a.auth(a.advanced))
|
||||
mux.HandleFunc("POST /vlan", a.auth(a.vlan))
|
||||
mux.HandleFunc("GET /dns", a.auth(a.dnsPage))
|
||||
mux.HandleFunc("POST /dns", a.auth(a.dns))
|
||||
mux.HandleFunc("GET /wireguard", a.auth(a.wireguardPage))
|
||||
mux.HandleFunc("POST /wireguard/save", a.auth(a.wireguardSave))
|
||||
mux.HandleFunc("POST /wireguard/delete", a.auth(a.wireguardDelete))
|
||||
mux.HandleFunc("POST /wireguard/client", a.auth(a.wireguardClient))
|
||||
mux.HandleFunc("POST /wireguard/import", a.auth(a.wireguardImport))
|
||||
mux.HandleFunc("GET /dhcp", a.auth(a.dhcpPage))
|
||||
mux.HandleFunc("POST /dhcp", a.auth(a.dhcp))
|
||||
mux.HandleFunc("GET /dashboard", a.auth(a.dashboardFragment))
|
||||
@@ -450,9 +463,72 @@ type networkData struct {
|
||||
Devices []Device
|
||||
}
|
||||
|
||||
// devices lists what the interface dropdowns offer for the form f.
|
||||
func (a *app) devices(f Form) []Device {
|
||||
return f.devices(Interfaces(), a.sys.loadPrefs().VLANs)
|
||||
}
|
||||
|
||||
// vlan defines or removes a VLAN device for the dropdowns. It lives in the
|
||||
// UI's preferences: nothing is rebuilt until a WAN or LAN uses it.
|
||||
func (a *app) vlan(w http.ResponseWriter, r *http.Request, _ session) {
|
||||
s, err := loadSettings(a.sys.settingsPath())
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/network?msg="+url.QueryEscape(err.Error()), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
prefs := a.sys.loadPrefs()
|
||||
msg := ""
|
||||
switch {
|
||||
case r.PostFormValue("add") != "":
|
||||
parent := strings.TrimSpace(r.PostFormValue("parent"))
|
||||
id, ok, perr := optionalInt(r.PostFormValue("id"), 1, 4094)
|
||||
dev := fmt.Sprintf("%s.%d", parent, id)
|
||||
switch {
|
||||
case !ifaceRe.MatchString(parent) || strings.Contains(parent, "."):
|
||||
msg = "Choose the interface the VLAN is on."
|
||||
case perr != nil || !ok:
|
||||
msg = "The VLAN ID must be a number from 1 to 4094."
|
||||
case slices.Contains(prefs.VLANs, dev):
|
||||
msg = dev + " exists already."
|
||||
default:
|
||||
prefs.VLANs = append(prefs.VLANs, dev)
|
||||
slices.Sort(prefs.VLANs)
|
||||
msg = "Added " + dev + "."
|
||||
}
|
||||
case r.PostFormValue("remove") != "":
|
||||
dev := r.PostFormValue("remove")
|
||||
for _, d := range s.form().devices(Interfaces(), prefs.VLANs) {
|
||||
if d.Name == dev && !d.Removable {
|
||||
msg = dev + " is in use."
|
||||
}
|
||||
}
|
||||
if msg == "" {
|
||||
prefs.VLANs = slices.DeleteFunc(prefs.VLANs, func(v string) bool { return v == dev })
|
||||
msg = "Removed " + dev + "."
|
||||
}
|
||||
}
|
||||
if err := a.sys.savePrefs(prefs); err != nil {
|
||||
msg = "Saving: " + err.Error()
|
||||
}
|
||||
http.Redirect(w, r, "/network?msg="+url.QueryEscape(msg), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// choices are the dropdown entries for a device: all known devices, plus the
|
||||
// current value if it is not among them (an interface this machine lacks).
|
||||
func choices(devices []Device, current string) []string {
|
||||
var out []string
|
||||
for _, d := range devices {
|
||||
out = append(out, d.Name)
|
||||
}
|
||||
if current != "" && !slices.Contains(out, current) {
|
||||
out = append(out, current)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *app) networkPage(w http.ResponseWriter, r *http.Request, sess session) {
|
||||
s, err := loadSettings(a.sys.settingsPath())
|
||||
p := page{Title: "Network", CSRF: sess.csrf, LoggedIn: true}
|
||||
p := page{Title: "Network", CSRF: sess.csrf, LoggedIn: true, Flash: r.URL.Query().Get("msg")}
|
||||
if err != nil {
|
||||
p.Error = err.Error()
|
||||
a.render(w, "network", p)
|
||||
@@ -460,13 +536,14 @@ func (a *app) networkPage(w http.ResponseWriter, r *http.Request, sess session)
|
||||
}
|
||||
f := s.form()
|
||||
f.LANs = append(f.LANs, LANForm{DHCP: true}) // a row to add a LAN
|
||||
p.Data = networkData{Form: f, Interfaces: Interfaces(), Devices: f.devices(Interfaces())}
|
||||
p.Data = networkData{Form: f, Interfaces: Interfaces(), Devices: a.devices(f)}
|
||||
a.render(w, "network", p)
|
||||
}
|
||||
|
||||
func formFromRequest(r *http.Request) Form {
|
||||
f := Form{
|
||||
HostName: r.PostFormValue("hostName"),
|
||||
Domain: r.PostFormValue("domain"),
|
||||
WAN: WANForm{
|
||||
Interface: strings.TrimSpace(r.PostFormValue("wan.interface")),
|
||||
Type: r.PostFormValue("wan.type"),
|
||||
@@ -481,7 +558,6 @@ func formFromRequest(r *http.Request) Form {
|
||||
f.LANs = append(f.LANs, LANForm{
|
||||
Name: v("name"),
|
||||
Interface: strings.TrimSpace(v("interface")),
|
||||
VLAN: v("vlan"),
|
||||
Address: v("address"),
|
||||
Prefix: v("prefix"),
|
||||
Internet: v("internet") == "on",
|
||||
@@ -511,7 +587,7 @@ func (a *app) network(w http.ResponseWriter, r *http.Request, sess session) {
|
||||
f.WAN.PPPoEPassword = ""
|
||||
f.WAN.PasswordSet = s.form().WAN.PasswordSet
|
||||
p.Error = "Please correct the marked fields."
|
||||
p.Data = networkData{Form: f, Errors: fe, Interfaces: Interfaces(), Devices: f.devices(Interfaces())}
|
||||
p.Data = networkData{Form: f, Errors: fe, Interfaces: Interfaces(), Devices: a.devices(f)}
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
a.render(w, "network", p)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user