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:
@@ -1,7 +1,68 @@
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
cfg = config.router;
|
||||
lans = lib.attrValues cfg.lans;
|
||||
ipv4 = (import ./lib.nix { inherit lib; }).ipv4;
|
||||
|
||||
lansByName = cfg.lans;
|
||||
lans = lib.attrValues lansByName;
|
||||
|
||||
validIp = s: ipv4.parse s != null;
|
||||
|
||||
# Assertions about a single LAN; skipped (reported once) when its addresses
|
||||
# do not even parse, so we never feed null into the integer helpers.
|
||||
perLan =
|
||||
name: lan:
|
||||
let
|
||||
where = "router.lans.${name}";
|
||||
addrOk = validIp lan.ipv4.address;
|
||||
startOk = validIp lan.dhcp.pool.start;
|
||||
endOk = validIp lan.dhcp.pool.end;
|
||||
addr = ipv4.parse lan.ipv4.address;
|
||||
start = ipv4.parse lan.dhcp.pool.start;
|
||||
end = ipv4.parse lan.dhcp.pool.end;
|
||||
net = {
|
||||
address = addr;
|
||||
inherit (lan.ipv4) prefixLength;
|
||||
};
|
||||
parsed = addrOk && startOk && endOk;
|
||||
in
|
||||
[
|
||||
{
|
||||
assertion = addrOk;
|
||||
message = "router: ${where}.ipv4.address `${lan.ipv4.address}` is not a valid IPv4 address.";
|
||||
}
|
||||
]
|
||||
++ lib.optionals lan.dhcp.enable (
|
||||
[
|
||||
{
|
||||
assertion = startOk && endOk;
|
||||
message = "router: ${where}.dhcp.pool.start/end must be valid IPv4 addresses.";
|
||||
}
|
||||
]
|
||||
++ lib.optionals parsed [
|
||||
{
|
||||
assertion = start <= end;
|
||||
message = "router: ${where}.dhcp.pool starts after it ends.";
|
||||
}
|
||||
{
|
||||
assertion = ipv4.inSubnet net start && ipv4.inSubnet net end;
|
||||
message = "router: ${where}.dhcp.pool is not inside ${lan.ipv4.subnet}.";
|
||||
}
|
||||
{
|
||||
assertion = !(start <= addr && addr <= end);
|
||||
message = "router: ${where}.dhcp.pool contains the router's own address ${lan.ipv4.address}.";
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
overlapPairs = lib.concatLists (
|
||||
lib.imap0 (
|
||||
i: a:
|
||||
map (b: {
|
||||
inherit a b;
|
||||
}) (lib.drop (i + 1) (lib.attrNames lansByName))
|
||||
) (lib.attrNames lansByName)
|
||||
);
|
||||
in
|
||||
{
|
||||
config = lib.mkIf cfg.enable {
|
||||
@@ -26,6 +87,30 @@ in
|
||||
lib.length ifaces == lib.length (lib.unique ifaces);
|
||||
message = "router: two LANs share the same interface.";
|
||||
}
|
||||
];
|
||||
]
|
||||
++ lib.concatLists (lib.mapAttrsToList perLan lansByName)
|
||||
++ map (
|
||||
{ a, b }:
|
||||
let
|
||||
la = lansByName.${a};
|
||||
lb = lansByName.${b};
|
||||
in
|
||||
{
|
||||
# Only comparable when both addresses parse; the per-LAN assertion reports the rest.
|
||||
assertion =
|
||||
!(validIp la.ipv4.address && validIp lb.ipv4.address)
|
||||
|| !(ipv4.overlaps
|
||||
{
|
||||
address = ipv4.parse la.ipv4.address;
|
||||
inherit (la.ipv4) prefixLength;
|
||||
}
|
||||
{
|
||||
address = ipv4.parse lb.ipv4.address;
|
||||
inherit (lb.ipv4) prefixLength;
|
||||
}
|
||||
);
|
||||
message = "router: LANs `${a}` (${la.ipv4.subnet}) and `${b}` (${lb.ipv4.subnet}) overlap.";
|
||||
}
|
||||
) overlapPairs;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user