Add token expiry, edit notice, log allowlist, local-zone test; merge PLAN.md into SPECS.md

get_logs needs a unit from an allowlist (router-*, kea, unbound, networkd,
nftables, ddclient, acme, sshd, kernel). API tokens are valid for 1, 7, 30
(default) or 90 days, or without limit. The overview and the Changes page
say which token made the unapplied changes.

options.json (router://options) no longer holds the options' source paths:
they differ between builds of a release, so a router rebuilding itself
offline had to build the file and failed (found by the update test).

Web UI: panels flow into columns on wide screens and the width is no longer
capped; on phones the navigation is a menu button; the Advanced page uses
panels; VLAN dropdowns show their chosen names; the duplicate WireGuard
heading is gone.

basic: a subtest for wan -> local rules.

SPECS.md is now the only document (decisions, how each part works, and what
is not done yet); PLAN.md is gone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
2026-11-01 19:06:27 +01:00
co-authored by Claude Sonnet 5.5
parent 586274af4e
commit b1fa7b1396
25 changed files with 536 additions and 294 deletions
+35 -1
View File
@@ -228,7 +228,7 @@ pkgs.testers.runNixOSTest {
})
];
router = { lib, ... }: {
router = { lib, pkgs, ... }: {
imports = [ self.nixosModules.router ];
virtualisation.vlans = [
1
@@ -348,6 +348,27 @@ pkgs.testers.runNixOSTest {
protocol = "tcp";
ports = [ 8081 ];
}
# The router itself: no destination, only these senders and ports.
{
from = "wan";
to = "local";
protocol = "tcp";
ports = [ 8022 ];
sources = [ "${ispV4}/32" ];
}
{
from = "wan";
to = "local";
protocol = "tcp";
ports = [ 8023 ];
sources = [ "192.0.2.0/24" ];
}
{
from = "main";
to = "local";
protocol = "tcp";
ports = [ 8024 ];
}
];
};
};
@@ -355,6 +376,7 @@ pkgs.testers.runNixOSTest {
# Unbound answers `.test` locally (RFC 6761) and would validate DNSSEC
# against a fake ISP resolver that cannot provide it; neither applies to
# a real forwarder, so relax both for the made-up test domain only.
environment.systemPackages = [ pkgs.netcat ];
services.unbound.settings.server = {
local-zone = [ "test. transparent" ];
domain-insecure = [ "example.test" ];
@@ -465,6 +487,18 @@ pkgs.testers.runNixOSTest {
client_ip = client.succeed("ip -4 -o addr show dev eth1 | awk '{print $4}' | cut -d/ -f1").strip()
upstream.fail(f"ping -c 1 -W 2 {client_ip}")
with subtest("a wan -> local rule opens the router's port to the named sender only"):
for port in (8022, 8023, 8024, 8025):
router.succeed(f"systemd-run --unit=probe{port} ${pkgs.bash}/bin/bash -c 'while true; do ${pkgs.netcat}/bin/nc -l {port} < /dev/null; done'")
router.wait_until_succeeds("ss -ltn | grep -c ':8025 ' >/dev/null", timeout=30)
upstream.succeed("nc -z -w 2 ${routerV4} 8022")
# Open for another sender only, for the LAN only, and not at all.
upstream.fail("nc -z -w 2 ${routerV4} 8023")
upstream.fail("nc -z -w 2 ${routerV4} 8024")
upstream.fail("nc -z -w 2 ${routerV4} 8025")
client.succeed("nc -z -w 2 192.168.10.1 8024")
client.fail("nc -z -w 2 192.168.10.1 8022")
with subtest("router gets an IPv6 address, the delegated /64 and a default route"):
router.wait_until_succeeds("ip -6 addr show dev eth1 scope global | grep -q 2001:db8:1::100")
router.wait_until_succeeds("ip -6 addr show dev eth2 scope global | grep -q 2001:db8:2:")