Derive LAN subnets and validate addressing at build time
Add an IPv4 helper lib (parse, netmask, cidr, overlap) and derive router.lans.<name>.ipv4.subnet from address and prefixLength instead of requiring it twice. New assertions reject invalid addresses, DHCP pools that are reversed, outside the subnet or contain the router address, and overlapping LANs. Add `lib` unit tests and an `assertions` check that evaluates broken configs and compares the exact error messages. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# Evaluation-time tests: broken configs must fail with exactly the expected
|
||||
# assertion messages, and a valid one with none.
|
||||
{ self, pkgs }:
|
||||
let
|
||||
inherit (pkgs) lib;
|
||||
|
||||
evalRouter =
|
||||
router:
|
||||
(import "${pkgs.path}/nixos/lib/eval-config.nix" {
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
self.nixosModules.router
|
||||
{
|
||||
boot.loader.grub.enable = false;
|
||||
fileSystems."/" = {
|
||||
device = "x";
|
||||
fsType = "ext4";
|
||||
};
|
||||
system.stateVersion = "25.05";
|
||||
router = {
|
||||
enable = true;
|
||||
wan.interface = "eth0";
|
||||
} // router;
|
||||
}
|
||||
];
|
||||
}).config;
|
||||
|
||||
lan = overrides: lib.recursiveUpdate {
|
||||
interface = "eth1";
|
||||
ipv4.address = "192.168.10.1";
|
||||
dhcp.pool = { start = "192.168.10.100"; end = "192.168.10.199"; };
|
||||
} overrides;
|
||||
|
||||
failed = router: map (a: a.message) (lib.filter (a: !a.assertion) (evalRouter router).assertions);
|
||||
|
||||
cases = {
|
||||
valid = { router.lans.main = lan { }; expect = [ ]; };
|
||||
noManagement = {
|
||||
router.lans.main = lan { allowManagement = false; };
|
||||
expect = [ "locked out" ];
|
||||
};
|
||||
wanIsLan = {
|
||||
router.lans.main = lan { interface = "eth0"; };
|
||||
expect = [ "WAN interface `eth0` is also used by a LAN" ];
|
||||
};
|
||||
sharedInterface = {
|
||||
router.lans = {
|
||||
a = lan { };
|
||||
b = lan { ipv4.address = "192.168.20.1"; dhcp.pool = { start = "192.168.20.100"; end = "192.168.20.199"; }; };
|
||||
};
|
||||
expect = [ "two LANs share the same interface" ];
|
||||
};
|
||||
badAddress = {
|
||||
router.lans.main = lan { ipv4.address = "192.168.10.300"; dhcp.enable = false; };
|
||||
expect = [ "`192.168.10.300` is not a valid IPv4 address" ];
|
||||
};
|
||||
poolOutsideSubnet = {
|
||||
router.lans.main = lan { dhcp.pool = { start = "192.168.11.100"; end = "192.168.11.199"; }; };
|
||||
expect = [ "pool is not inside 192.168.10.0/24" ];
|
||||
};
|
||||
poolReversed = {
|
||||
router.lans.main = lan { dhcp.pool = { start = "192.168.10.199"; end = "192.168.10.100"; }; };
|
||||
expect = [ "starts after it ends" ];
|
||||
};
|
||||
poolContainsRouter = {
|
||||
router.lans.main = lan { dhcp.pool = { start = "192.168.10.1"; end = "192.168.10.50"; }; };
|
||||
expect = [ "contains the router's own address 192.168.10.1" ];
|
||||
};
|
||||
overlappingLans = {
|
||||
router.lans = {
|
||||
a = lan { };
|
||||
b = lan { interface = "eth2"; ipv4.address = "192.168.10.2"; };
|
||||
};
|
||||
expect = [ "`a` (192.168.10.0/24) and `b` (192.168.10.0/24) overlap" ];
|
||||
};
|
||||
nonOverlappingLans = {
|
||||
router.lans = {
|
||||
a = lan { };
|
||||
b = lan { interface = "eth2"; ipv4.address = "192.168.20.1"; dhcp.pool = { start = "192.168.20.100"; end = "192.168.20.199"; }; };
|
||||
};
|
||||
expect = [ ];
|
||||
};
|
||||
};
|
||||
|
||||
problems = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
name: c:
|
||||
let
|
||||
got = failed c.router;
|
||||
missing = lib.filter (e: !lib.any (m: lib.hasInfix e m) got) c.expect;
|
||||
in
|
||||
lib.optional (missing != [ ] || lib.length got != lib.length c.expect)
|
||||
"${name}: expected ${builtins.toJSON c.expect}, got ${builtins.toJSON got}"
|
||||
) cases
|
||||
);
|
||||
|
||||
subnet = (evalRouter { lans.main = lan { ipv4.prefixLength = 20; ipv4.address = "172.16.37.1"; dhcp.enable = false; }; }).router.lans.main.ipv4.subnet;
|
||||
subnetProblem = lib.optional (subnet != "172.16.32.0/20") "derived subnet: expected 172.16.32.0/20, got ${subnet}";
|
||||
in
|
||||
pkgs.runCommand "router-assertion-tests" { } (
|
||||
if problems ++ subnetProblem == [ ] then
|
||||
"touch $out"
|
||||
else
|
||||
''
|
||||
printf '%s\n' ${lib.escapeShellArgs (problems ++ subnetProblem)} >&2
|
||||
exit 1
|
||||
''
|
||||
)
|
||||
Reference in New Issue
Block a user