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:
@@ -6,6 +6,19 @@ let
|
||||
lansByName = cfg.lans;
|
||||
lans = lib.attrValues lansByName;
|
||||
|
||||
# A name chosen for a network card or a VLAN device: short, plain, and not
|
||||
# one the kernel or udev hands out itself (a rename could then collide).
|
||||
# Keep in step with reservedName in the web UI (pkgs/router-webui/settings.go).
|
||||
chosenNameOk =
|
||||
s:
|
||||
builtins.match "[a-z][a-z0-9-]{0,14}" s != null
|
||||
&& builtins.match "(eth|wlan|ppp|wg|veth|bond|tun|tap|dummy|ifb|docker|virbr|br|lo)[0-9]*|(en|wl|ww|sl)[opsx][0-9a-f].*" s == null;
|
||||
chosenNames =
|
||||
lib.attrNames cfg.interfaces
|
||||
++ lib.concatMap (l: lib.optional (l.deviceName != null) l.deviceName) lans
|
||||
++ lib.optional (cfg.wan.ipv4.pppoe.vlanName != null) cfg.wan.ipv4.pppoe.vlanName;
|
||||
macs = lib.mapAttrsToList (_: i: i.mac) cfg.interfaces;
|
||||
|
||||
validIp = s: ipv4.parse s != null;
|
||||
hostnameOk = s: builtins.match "[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?" s != null;
|
||||
# A DNS name: labels of letters, digits, dashes and underscores.
|
||||
@@ -37,6 +50,10 @@ let
|
||||
assertion = addrOk;
|
||||
message = "router: ${where}.ipv4.address `${lan.ipv4.address}` is not a valid IPv4 address.";
|
||||
}
|
||||
{
|
||||
assertion = lan.deviceName == null || lan.vlan != null;
|
||||
message = "router: ${where}.deviceName needs a vlan.";
|
||||
}
|
||||
{
|
||||
# IFNAMSIZ is 16 including the terminating NUL.
|
||||
assertion = lib.stringLength lan.device <= 15;
|
||||
@@ -208,12 +225,16 @@ let
|
||||
message = "router: ${where}: unknown source zone `${r.from}` (zones: wan, ${lib.concatStringsSep ", " zoneNames}).";
|
||||
}
|
||||
{
|
||||
assertion = lib.elem r.to zoneNames;
|
||||
assertion = r.to == "local" || lib.elem r.to zoneNames;
|
||||
message =
|
||||
if r.to == "wan" then
|
||||
"router: ${where}: `wan` cannot be a destination; use `internet = true` on the LAN or tunnel."
|
||||
else
|
||||
"router: ${where}: unknown destination zone `${r.to}` (zones: ${lib.concatStringsSep ", " zoneNames}).";
|
||||
"router: ${where}: unknown destination zone `${r.to}` (zones: local, ${lib.concatStringsSep ", " zoneNames}).";
|
||||
}
|
||||
{
|
||||
assertion = !(r.to == "local" && r.natOnly);
|
||||
message = "router: ${where}: natOnly does not apply to the router itself (`local`); a port forward leads to a host in a zone.";
|
||||
}
|
||||
{
|
||||
assertion = r.from != r.to;
|
||||
@@ -249,7 +270,7 @@ let
|
||||
# Rules from the outside must not be able to expose a whole zone by accident.
|
||||
++ lib.optionals fromWan [
|
||||
{
|
||||
assertion = r.destination != null;
|
||||
assertion = r.destination != null || r.to == "local";
|
||||
message = "router: ${where}: rules from `wan` must name a destination host or network.";
|
||||
}
|
||||
{
|
||||
@@ -509,6 +530,22 @@ in
|
||||
assertion = lans != [ ];
|
||||
message = "router: at least one LAN must be defined.";
|
||||
}
|
||||
{
|
||||
assertion = lib.length macs == lib.length (lib.unique macs);
|
||||
message = "router.interfaces: two names are given to the same MAC address.";
|
||||
}
|
||||
{
|
||||
assertion = lib.all chosenNameOk chosenNames;
|
||||
message = "router: names for interfaces and VLAN devices are lower case letters, digits and dashes, start with a letter, have at most 15 characters and do not look like a kernel name (eth0, wlan0, enp2s0, ...): ${lib.concatStringsSep ", " (lib.filter (n: !chosenNameOk n) chosenNames)}.";
|
||||
}
|
||||
{
|
||||
assertion = lib.length chosenNames == lib.length (lib.unique chosenNames);
|
||||
message = "router: a name is used twice among `interfaces` and the VLAN devices.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.wan.ipv4.pppoe.vlanName == null || cfg.wan.ipv4.pppoe.vlan != null;
|
||||
message = "router.wan.ipv4.pppoe.vlanName needs pppoe.vlan.";
|
||||
}
|
||||
{
|
||||
assertion = lib.any (l: l.allowManagement) lans;
|
||||
message = "router: no LAN has allowManagement = true; you would be locked out of the router.";
|
||||
@@ -567,6 +604,10 @@ in
|
||||
assertion = !(lib.elem "wan" zoneNames);
|
||||
message = "router: `wan` is a reserved zone name; give the LAN or tunnel a different `zone`.";
|
||||
}
|
||||
{
|
||||
assertion = !(lib.elem "local" zoneNames);
|
||||
message = "router: `local` is a reserved zone name (the router itself); give the LAN or tunnel a different `zone`.";
|
||||
}
|
||||
]
|
||||
++ lib.concatLists (lib.mapAttrsToList wgAssertions cfg.wireguard)
|
||||
++ [
|
||||
|
||||
Reference in New Issue
Block a user