Files
nixos-router/flake.nix
nilsandClaude Sonnet 5.5 a58600823f Add a generic NAT table: port forward, source NAT, masquerade
router.firewall.nat replaces the dnat/toPort/hairpin fields of the filter
rules. Filter rules gain natOnly, and may use wan with log and rate limit
in the web UI. The Firewall page edits the NAT table and can add the
matching allow rule. Settings schema 1 (migrations/1.jq) converts old
forwards.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
2026-10-30 15:54:29 +01:00

387 lines
14 KiB
Nix

{
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 -- <flake-attr> <user@host>`: 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);
};
}