Add the appliance web UI look, interface names, MCP and REST API, local zone
Web UI: dark/light themes (appliance layout, sidebar, switch checkboxes), theme preference on the Advanced page, Changes page with the pending diff. Interface names: router.interfaces.<name>.mac, lans.<l>.deviceName and pppoe.vlanName; a rename is staged for the next reboot by router-apply. Remote access: an MCP server (POST /mcp) and an OpenAPI REST API (/api/v1, /api/openapi.json) with the same tokens, scopes, limits and audit log; tools to read status, settings, leases, logs (get_logs), and to edit and check the saved settings (never applied). Tokens on the API access page. Firewall: a reserved `local` zone (the router itself) as the destination of rules, so the web UI or SSH can be allowed from the WAN. PLAN.md lists what is still left. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
@@ -206,8 +206,8 @@ func (s Settings) applyZones(zf ZoneForm, errs FormErrors) {
|
||||
case !nameRe.MatchString(m.Zone):
|
||||
errs[fmt.Sprintf("member[%d]", i)] = "letters, digits and dashes, starting with a letter"
|
||||
continue
|
||||
case m.Zone == "wan":
|
||||
errs[fmt.Sprintf("member[%d]", i)] = "wan is the uplink's zone and cannot be assigned"
|
||||
case m.Zone == "wan" || m.Zone == "local":
|
||||
errs[fmt.Sprintf("member[%d]", i)] = m.Zone + " is a reserved zone (the uplink, the router) and cannot be assigned"
|
||||
continue
|
||||
}
|
||||
used[m.Zone] = true
|
||||
@@ -226,8 +226,8 @@ func (s Settings) applyZones(zf ZoneForm, errs FormErrors) {
|
||||
declared := slices.Clone(s.declaredZones())
|
||||
for _, z := range splitList(zf.New) {
|
||||
switch {
|
||||
case !nameRe.MatchString(z) || z == "wan":
|
||||
errs["newzone"] = z + ": letters, digits and dashes, starting with a letter (wan is taken)"
|
||||
case !nameRe.MatchString(z) || z == "wan" || z == "local":
|
||||
errs["newzone"] = z + ": letters, digits and dashes, starting with a letter (wan and local are taken)"
|
||||
case !slices.Contains(declared, z):
|
||||
declared = append(declared, z)
|
||||
}
|
||||
@@ -279,9 +279,12 @@ func (s Settings) applyFirewall(zf ZoneForm, rows []FirewallRule, nats []NATRule
|
||||
if !slices.Contains(zones, r.From) && r.From != "wan" {
|
||||
errs[field("from")] = "choose a zone"
|
||||
}
|
||||
if !slices.Contains(zones, r.To) {
|
||||
if !slices.Contains(zones, r.To) && r.To != "local" {
|
||||
errs[field("to")] = "choose a zone"
|
||||
}
|
||||
if r.To == "local" && r.NATOnly {
|
||||
errs[field("to")] = "a port forward leads to a host in a zone, not to the router"
|
||||
}
|
||||
if r.From == r.To && r.From != "" {
|
||||
errs[field("to")] = "traffic inside a zone is already allowed"
|
||||
}
|
||||
@@ -312,7 +315,7 @@ func (s Settings) applyFirewall(zf ZoneForm, rows []FirewallRule, nats []NATRule
|
||||
}
|
||||
if r.From == "wan" {
|
||||
// What the module requires, so that a typo cannot open a whole zone.
|
||||
if r.Destination == "" {
|
||||
if r.Destination == "" && r.To != "local" {
|
||||
errs[field("destination")] = "rules from the internet must name the host or network inside"
|
||||
}
|
||||
if r.Protocol == "any" {
|
||||
@@ -402,6 +405,7 @@ type firewallData struct {
|
||||
Rows []FirewallRule
|
||||
NATs []NATRule
|
||||
FromZones []string // the zones and wan
|
||||
ToZones []string // the zones and local (the router itself)
|
||||
Other []string
|
||||
Zones []string
|
||||
Members []ZoneMember
|
||||
@@ -414,6 +418,7 @@ func (s Settings) firewallData(rows []FirewallRule, nats []NATRule, members []Zo
|
||||
_, other := s.firewallRules()
|
||||
d := firewallData{Rows: rows, NATs: nats, Other: other, Zones: s.zones(), Members: members, Errors: errs}
|
||||
d.FromZones = append([]string{"wan"}, d.Zones...)
|
||||
d.ToZones = append(slices.Clone(d.Zones), "local")
|
||||
if d.Members == nil {
|
||||
d.Members = s.zoneMembers()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user