{ description = "nixos-router: a NixOS-based distribution for routers/firewalls"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; outputs = { self, nixpkgs }: let systems = [ "x86_64-linux" "aarch64-linux" ]; forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); # What a release is: shown by the web UI, compared by router-update # (the serial never goes down: the commit time) and checked against the # settings (the schema is the number of migrations/N.jq). migrations = builtins.filter (n: nixpkgs.lib.hasSuffix ".jq" n) ( builtins.attrNames (builtins.readDir ./migrations) ); thisRelease = { version = nixpkgs.lib.fileContents ./VERSION; serial = self.lastModified or 0; rev = self.rev or self.dirtyRev or "unknown"; schema = builtins.length migrations; }; devPkgs = nixpkgs.legacyPackages.x86_64-linux; # The development helpers (see DEVELOPING.md) as apps. devScript = name: inputs: file: let p = devPkgs.writeShellApplication { inherit name; runtimeInputs = inputs; text = builtins.readFile file; # Interactive helpers: the word splitting of $@ and the like is intended. excludeShellChecks = [ "SC2086" ]; }; in { type = "app"; program = "${p}/bin/${name}"; }; # Public keys of the update signing key (see `nix run .#release`). defaultUpdateKeys = nixpkgs.lib.optional (builtins.pathExists ./keys/update.pub) ( nixpkgs.lib.fileContents ./keys/update.pub ); in { nixosModules = { router = ./modules/router; default = self.nixosModules.router; # Disk layout contract shared by router-install and target configs. installed-layout = ./modules/installer/layout.nix; # The installer pieces; `lib.mkInstaller` wires them up. installer = ./modules/installer/install-script.nix; installer-iso = ./modules/installer/iso.nix; # The generic installer's `router-setup` (see lib.setupModule). setup = ./modules/installer/setup.nix; # Appliance disk image: dm-verity store, volatile root, state on /var. image = ./modules/image; # Configuration from a settings file (JSON), rebuilt on the device. settings = ./modules/settings; }; # A router built from settings.json; this is what the device's own # configuration flake (see mkConfigDir) evaluates. lib.mkRouterSystem = { settings, modules ? [ ], updateKeys ? defaultUpdateKeys, # The configuration flake passes it on: a `path:` flake has no commit. release ? thisRelease, }: nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ self.nixosModules.router self.nixosModules.settings self.nixosModules.installed-layout { routerSettings.file = settings; routerSettings.release = release; routerUpdate.publicKeys = updateKeys; # The device rebuilds itself, offline if need be: it needs the # sources its configuration flake points to. system.extraDependencies = [ self.outPath nixpkgs.outPath ]; } ] ++ modules; }; # The configuration directory installed to /var/lib/router/config: a # flake pinned to exactly this router source, the settings and an # optional local.nix with extra NixOS configuration. lib.mkConfigDir = { pkgs, settings, local ? null, }: pkgs.runCommand "router-config" { } '' mkdir -p $out cp ${self.lib.configFlake pkgs} $out/flake.nix cp ${settings} $out/settings.json ${pkgs.lib.optionalString (local != null) "cp ${local} $out/local.nix"} ''; # The configuration directory's flake.nix (shared by mkConfigDir and # router-setup, so both build exactly the same system). lib.configFlake = pkgs: pkgs.writeText "flake.nix" '' { inputs.nixos-router.url = "path:${self.outPath}"; outputs = { nixos-router, ... }: { nixosConfigurations.router = nixos-router.lib.mkRouterSystem { release = { version = "${thisRelease.version}"; serial = ${toString thisRelease.serial}; rev = "${thisRelease.rev}"; schema = ${toString thisRelease.schema}; }; settings = ./settings.json; modules = if builtins.pathExists ./local.nix then [ ./local.nix ] else [ ]; }; }; } ''; # The generic installer: `router-setup` asks for the basics, builds # the router offline and installs it; the rest is done in the web UI. # nix build .#installer # router-setup's configuration: the flake template and the reference # system whose closure makes the offline build possible. `local` is an # extra module that the installed systems will import as well. lib.setupModule = { local ? null, }: { pkgs, ... }: { imports = [ self.nixosModules.setup ]; router.setup = { flake = self.lib.configFlake pkgs; reference = self.lib.referenceSystem { inherit pkgs local; }; schemaVersion = thisRelease.schema; release = self.lib.mkRelease { inherit pkgs; includeReference = false; }; }; }; # A router built the way installed ones are: its closure carries what # building any other settings needs, so it can be done offline. lib.referenceSystem = { pkgs, local ? null, }: (self.lib.mkRouterSystem { settings = builtins.toFile "settings.json" ( builtins.toJSON { version = thisRelease.schema; router = { wan.interface = "eth0"; lans.lan = { interface = "eth1"; internet = true; ipv4.address = "192.168.1.1"; dhcp.pool = { start = "192.168.1.100"; end = "192.168.1.200"; }; }; }; } ); # A path, not a derivation: the module system would read its attributes. modules = pkgs.lib.optional (local != null) "${local}"; }).config.system.build.toplevel; # An update: this source, its nixpkgs, the configuration flake that # points at them and (unless the router has them already) the reference # system with everything its offline builds need. router-update takes # this store path from a signed binary cache (`nix run .#release`). lib.mkRelease = { pkgs, includeReference ? true, }: pkgs.runCommand "router-release-${thisRelease.version}" { } '' mkdir -p $out/migrations printf %s ${thisRelease.version} > $out/version printf %s ${toString thisRelease.serial} > $out/serial printf %s ${toString thisRelease.schema} > $out/schema printf %s ${thisRelease.rev} > $out/rev cp ${self.lib.configFlake pkgs} $out/flake.nix for f in ${nixpkgs.lib.concatMapStringsSep " " (n: "${./migrations}/${n}") migrations}; do cp "$f" $out/migrations/ done # These references are what make the closure. echo ${self.outPath} ${nixpkgs.outPath} \ ${nixpkgs.lib.optionalString includeReference (self.lib.referenceSystem { inherit pkgs; })} \ > $out/closure ''; # `nix run .#release -- --key KEY [--out DIR]`: signs a release as a # binary cache and writes the bundle and the pull-server layout. lib.mkReleaseTool = { pkgs, release }: pkgs.writeShellApplication { name = "router-release"; runtimeInputs = with pkgs; [ nix gnutar coreutils jq ]; text = '' release=${release} ${builtins.readFile ./scripts/release.sh} ''; }; packages.x86_64-linux.release = self.lib.mkRelease { pkgs = nixpkgs.legacyPackages.x86_64-linux; }; apps.x86_64-linux.release = let tool = self.lib.mkReleaseTool { pkgs = nixpkgs.legacyPackages.x86_64-linux; release = self.packages.x86_64-linux.release; }; in { type = "app"; program = "${tool}/bin/router-release"; }; # The generic installer ISO: # nix build .#installer nixosConfigurations.installer = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ self.nixosModules.installer-iso (self.lib.setupModule { }) ]; }; packages.x86_64-linux.installer = self.nixosConfigurations.installer.config.system.build.isoImage; # Installer ISO for a given nixosSystem: # nix build .#installer-example (see packages) lib.mkInstaller = { configuration }: nixpkgs.lib.nixosSystem { inherit (configuration.pkgs.stdenv.hostPlatform) system; modules = [ self.nixosModules.installer-iso { router.installer.system = configuration.config.system.build.toplevel; # Reach the live system with the keys that will reach the router. users.users.root.openssh.authorizedKeys.keys = configuration.config.users.users.root.openssh.authorizedKeys.keys; } ]; }; packages.x86_64-linux.installer-example = (self.lib.mkInstaller { configuration = self.nixosConfigurations.example; }) .config.system.build.isoImage; # A deployable starting point; copy examples/desktop and adapt it. nixosConfigurations.example = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ self.nixosModules.router ./examples/desktop ]; }; # Appliance image (verity store, volatile root) for trying on hardware: # nix build .#image -> result/*.raw nixosConfigurations.image = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ self.nixosModules.router self.nixosModules.image ./examples/image ]; }; packages.x86_64-linux.image = self.nixosConfigurations.image.config.system.build.image; # `nix run .#deploy -- `: activate a new # configuration with commit-confirm; see scripts/deploy.sh. apps.x86_64-linux.deploy = let pkgs = nixpkgs.legacyPackages.x86_64-linux; deploy = pkgs.writeShellApplication { name = "router-deploy"; runtimeInputs = with pkgs; [ nixos-rebuild openssh coreutils ]; text = builtins.readFile ./scripts/deploy.sh; }; in { type = "app"; program = "${deploy}/bin/router-deploy"; }; # Development loop (see DEVELOPING.md): # nix run .#fast quick checks, no VMs # nix run .#test -- update VM tests, one at a time, with a summary # nix run .#webui-dev the web UI locally, templates hot-reload apps.x86_64-linux.fast = devScript "router-fast" (with devPkgs; [ git go_1_27 nix coreutils gnugrep ]) ./scripts/fast.sh; apps.x86_64-linux.test = devScript "router-test" (with devPkgs; [ nix coreutils gnugrep gnused ]) ./scripts/test.sh; apps.x86_64-linux.webui-dev = devScript "router-webui-dev" (with devPkgs; [ git go_1_27 iproute2 jq coreutils ]) ./scripts/webui-dev.sh; # VM tests need KVM; only wired up for x86_64 until aarch64 CI exists. checks.x86_64-linux = import ./tests { inherit self; pkgs = nixpkgs.legacyPackages.x86_64-linux; } // { # The example must keep building as a complete system. example = self.nixosConfigurations.example.config.system.build.toplevel; }; # `nix run .#lab`: boot the basic test topology in tmux, one console pane # per VM plus the interactive test-driver REPL. apps.x86_64-linux.lab = let pkgs = nixpkgs.legacyPackages.x86_64-linux; test = self.checks.x86_64-linux.basic; lab = pkgs.writeShellApplication { name = "router-lab"; runtimeInputs = with pkgs; [ tmux util-linux gnugrep coreutils ]; text = import ./tests/lab.nix { inherit (pkgs) lib; driver = test.driverInteractive; nodes = builtins.attrNames test.nodes; }; }; in { type = "app"; program = "${lab}/bin/router-lab"; }; formatter = forAllSystems (pkgs: pkgs.nixfmt); }; }