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
387 lines
14 KiB
Nix
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);
|
|
};
|
|
}
|