Add a web UI for settings-configured routers
router-webui (Go, standard library only) edits settings.json: host name, WAN (DHCP or PPPoE), LANs with DHCP, and the raw JSON for the rest; keys the forms do not know are kept. It applies and confirms by starting router-apply.service and router-confirm.service, which a switch never restarts, and shows the build log and the rollback countdown. HTTPS with a self-signed certificate, a PBKDF2 password hash, CSRF tokens and an Origin check; reachable from management LANs only (new router.managementPorts). router-apply records the settings it built and router-confirm keeps them as settings.confirmed.json (new rollback.onConfirm hook), so the UI can tell unapplied changes and discard them. checks.apply now also logs in from a LAN client, discards the change left by the rollback, edits the DHCP pool, applies and confirms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+61
-1
@@ -3,7 +3,9 @@
|
||||
# without network, then
|
||||
# - rebuilding the unchanged settings gives exactly the installed system,
|
||||
# - a changed setting is built offline, activated and confirmed,
|
||||
# - an unconfirmed change is rolled back.
|
||||
# - an unconfirmed change is rolled back,
|
||||
# - and the same through the web UI from a LAN client: discard the
|
||||
# unapplied change, edit the DHCP pool, apply, confirm.
|
||||
{ self, pkgs }:
|
||||
let
|
||||
inherit (pkgs) lib;
|
||||
@@ -75,6 +77,19 @@ pkgs.testers.runNixOSTest {
|
||||
nix.settings.substituters = lib.mkForce [ ];
|
||||
};
|
||||
|
||||
client =
|
||||
{ lib, ... }:
|
||||
{
|
||||
imports = [ common.tools ];
|
||||
virtualisation.vlans = [ 2 ];
|
||||
networking.interfaces.eth1.ipv4.addresses = lib.mkForce [
|
||||
{
|
||||
address = "192.168.10.50";
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
target = {
|
||||
imports = [ sharedDisk ];
|
||||
# Evaluating a NixOS system takes memory.
|
||||
@@ -136,5 +151,50 @@ pkgs.testers.runNixOSTest {
|
||||
assert pool_end() == "192.168.10.120"
|
||||
target.wait_until_succeeds(f"test $(readlink -f /run/current-system) = {confirmed}", timeout=120)
|
||||
assert pool_end() == "192.168.10.150"
|
||||
|
||||
ui = "https://192.168.10.1"
|
||||
|
||||
def curl(args):
|
||||
return client.succeed(f"curl -sk -b /tmp/jar -c /tmp/jar {args}")
|
||||
|
||||
def csrf(path):
|
||||
return curl(f"{ui}{path} | grep -o 'name=\"csrf\" value=\"[^\"]*' | head -1 | cut -d'\"' -f4").strip()
|
||||
|
||||
with subtest("the web UI is reachable from the LAN and wants the password"):
|
||||
target.succeed("echo hunter2hunter2 | router-webui passwd /var/lib/router/webui-password")
|
||||
target.wait_for_unit("router-webui.service")
|
||||
client.start()
|
||||
client.wait_until_succeeds(f"curl -sk -o /dev/null {ui}/login", timeout=60)
|
||||
assert curl(f"-o /dev/null -w '%{{http_code}}' -d password=wrong {ui}/login") == "401"
|
||||
assert curl(f"-o /dev/null -w '%{{http_code}}' -d password=hunter2hunter2 {ui}/login") == "303"
|
||||
|
||||
with subtest("the web UI shows the rolled back change as unapplied and discards it"):
|
||||
page = curl(f"{ui}/")
|
||||
assert "not part of the confirmed system" in page, page
|
||||
token = csrf("/")
|
||||
curl(f"-d csrf={token} {ui}/discard")
|
||||
assert "192.168.10.150" in target.succeed("cat /var/lib/router/config/settings.json")
|
||||
assert "match the confirmed system" in curl(f"{ui}/")
|
||||
|
||||
with subtest("a change made in the web UI is applied and confirmed"):
|
||||
token = csrf("/network")
|
||||
form = " ".join(f"--data-urlencode '{k}={v}'" for k, v in {
|
||||
"csrf": token, "hostName": "router", "wan.interface": "eth1", "wan.type": "dhcp",
|
||||
"lans": "1", "lan.0.name": "main", "lan.0.interface": "eth2", "lan.0.address": "192.168.10.1",
|
||||
"lan.0.internet": "on", "lan.0.dhcp": "on",
|
||||
"lan.0.poolStart": "192.168.10.100", "lan.0.poolEnd": "192.168.10.130",
|
||||
}.items())
|
||||
assert curl(f"-o /dev/null -w '%{{http_code}}' -X POST {ui}/network") == "403"
|
||||
assert curl(f"-o /dev/null -w '%{{http_code}}' {form} {ui}/network") == "303"
|
||||
curl(f"-d csrf={csrf('/')} {ui}/apply")
|
||||
target.wait_until_succeeds("test $(systemctl show -p ActiveState --value router-apply.service) = inactive"
|
||||
" && test $(systemctl show -p Result --value router-apply.service) = success", timeout=600)
|
||||
assert pool_end() == "192.168.10.130"
|
||||
assert "Waiting for confirmation" in curl(f"{ui}/")
|
||||
curl(f"-d csrf={csrf('/')} {ui}/confirm")
|
||||
target.wait_until_fails("systemctl is-active router-rollback.timer", timeout=60)
|
||||
target.wait_until_succeeds("test $(systemctl show -p ActiveState --value router-confirm.service) = inactive")
|
||||
assert target.succeed("readlink -f /nix/var/nix/profiles/system").strip() == current()
|
||||
assert "match the confirmed system" in curl(f"{ui}/")
|
||||
'';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user