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
98 lines
2.5 KiB
Nix
98 lines
2.5 KiB
Nix
# PPPoE uplink for IPv4. pppd owns the session (address, default route); the
|
|
# VLAN device it runs on is created in networking.nix.
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.router;
|
|
wan = cfg.wan;
|
|
pppoe = wan.ipv4.pppoe;
|
|
|
|
# Device carrying the PPPoE session: the VLAN on the uplink, or the uplink itself.
|
|
device =
|
|
if pppoe.vlan == null then
|
|
wan.interface
|
|
else if pppoe.vlanName != null then
|
|
pppoe.vlanName
|
|
else
|
|
"${wan.interface}.${toString pppoe.vlan}";
|
|
|
|
# Unit that appears once udev has seen the device (VLAN devices are created
|
|
# by networkd after boot has begun, so pppd must not start before it).
|
|
deviceUnit = "sys-subsystem-net-devices-${lib.replaceStrings [ "-" ] [ "\\x2d" ] device}.device";
|
|
|
|
staticOptions = pkgs.writeText "router-pppoe-options" ''
|
|
plugin pppoe.so
|
|
nic-${device}
|
|
ifname ${wan.ipv4.interface}
|
|
user "${pppoe.username}"
|
|
noauth
|
|
hide-password
|
|
defaultroute
|
|
replacedefaultroute
|
|
noipdefault
|
|
${
|
|
if wan.ipv6.enable && wan.ipv6.transport == "ppp" then
|
|
"+ipv6\n ipv6cp-use-ipaddr"
|
|
else
|
|
"nodefaultroute6"
|
|
}
|
|
noccp
|
|
mtu ${toString pppoe.mtu}
|
|
mru ${toString pppoe.mtu}
|
|
lcp-echo-interval 10
|
|
lcp-echo-failure 3
|
|
nodetach
|
|
persist
|
|
maxfail 0
|
|
holdoff 5
|
|
'';
|
|
|
|
prepare = pkgs.writeShellScript "router-pppoe-prepare" ''
|
|
set -eu
|
|
umask 077
|
|
password=$(cat ${lib.escapeShellArg (toString pppoe.passwordFile)})
|
|
# pppd option files use C-style escapes inside double quotes.
|
|
password=''${password//\\/\\\\}
|
|
password=''${password//\"/\\\"}
|
|
{
|
|
cat ${staticOptions}
|
|
printf 'password "%s"\n' "$password"
|
|
} > /run/router-pppoe/options
|
|
'';
|
|
in
|
|
{
|
|
config = lib.mkIf (cfg.enable && wan.ipv4.type == "pppoe") {
|
|
boot.kernelModules = [
|
|
"pppoe"
|
|
"pppox"
|
|
"ppp_generic"
|
|
];
|
|
|
|
# pppd refuses to start without a (possibly empty) system options file.
|
|
environment.etc."ppp/options".text = "";
|
|
|
|
systemd.services.router-pppoe = {
|
|
description = "PPPoE uplink (${device})";
|
|
wantedBy = [ "multi-user.target" ];
|
|
wants = [ deviceUnit ];
|
|
after = [
|
|
"network.target"
|
|
"systemd-networkd.service"
|
|
deviceUnit
|
|
];
|
|
serviceConfig = {
|
|
RuntimeDirectory = "router-pppoe";
|
|
RuntimeDirectoryMode = "0700";
|
|
ExecStartPre = prepare;
|
|
ExecStart = "${pkgs.ppp}/bin/pppd file /run/router-pppoe/options";
|
|
Restart = "always";
|
|
RestartSec = 5;
|
|
};
|
|
};
|
|
};
|
|
}
|