# A router configured by a settings file instead of Nix code, so that it can # be changed on the box itself (by the web UI or by hand) and rebuilt there. # # settings.json holds the `router` options as JSON plus a few system settings: # # { # "system": { "hostName": "router", "timeZone": "UTC", "sshKeys": [ "ssh-ed25519 ..." ], # "stateVersion": "26.11" }, # "router": { "wan": { "interface": "enp1s0" }, "lans": { "lan": { ... } } } # } # # The module system validates it like any configuration: unknown keys, wrong # types and the router's assertions all fail the build. Secrets stay out of it # (files under /var/lib/router, referenced by path). # # On the device the configuration lives in /var/lib/router/config: a flake # (pinned to this router's source), settings.json and an optional local.nix. # `router-apply` builds it and activates it with commit-confirm: unless # `router-confirm` follows within router.rollback.timeout, the previous system # comes back. { config, lib, pkgs, ... }: let cfg = config.routerSettings; settings = lib.importJSON cfg.file; system = settings.system or { }; apply = pkgs.writeShellApplication { name = "router-apply"; runtimeInputs = [ config.nix.package pkgs.coreutils ]; text = '' timeout=''${1:-${config.router.rollback.timeout}} echo "router: building ${cfg.directory}" new=$(nix build --no-link --print-out-paths --out-link /run/router-pending \ "path:${cfg.directory}#nixosConfigurations.router.config.system.build.toplevel") if [ "$new" = "$(readlink -f /run/current-system)" ]; then echo "router: no changes" exit 0 fi router-arm "$timeout" echo "router: activating $new" "$new/bin/switch-to-configuration" test ''; }; in { # Outside `router`: the settings file defines that whole tree. options.routerSettings = { file = lib.mkOption { type = lib.types.path; description = "The settings file (JSON) this system is built from."; }; directory = lib.mkOption { type = lib.types.str; default = "/var/lib/router/config"; description = "Where the configuration (flake and settings) lives on the device."; }; }; config = { router = settings.router // { enable = true; }; networking.hostName = system.hostName or "router"; time.timeZone = system.timeZone or "UTC"; users.users.root.openssh.authorizedKeys.keys = system.sshKeys or [ ]; system.stateVersion = system.stateVersion or config.system.nixos.release; nix.settings.experimental-features = [ "nix-command" "flakes" ]; environment.systemPackages = [ apply ]; # Rebuilding on the device must work without internet (the uplink may be # what is being configured). What a changed configuration rebuilds is all # generated by NixOS, but those builds need these packages; found by # diffing the derivation graphs of typical changes (pool, addresses, host # name, PPPoE on a VLAN, a VLAN LAN, IPv6 PD/ULA, a port forward, DNS # records) against the installed system. system.extraDependencies = with pkgs; [ stdenvNoCC findXMLCatalogs jq.dev kmod.dev libcap-text-verifier libxslt.bin # nftables checks its ruleset at build time inside lkl. libredirect lklWithFirewall.lib lndir makeShellWrapper mdadm nukeReferences # Switching the uplink to PPPoE is a typical first change. ppp ]; }; }