IPv6: router.wan.ipv6.enable mirrors the existing uplink config (DHCPv6 with WithoutRA=solicit, delegated prefix). The ISP delegates a single /64, so exactly one LAN may claim it via router.lans.<name>.ipv6.pd (asserted); other LANs stay IPv4-only. SLAAC only, no NAT66. PPPoE: router.wan.ipv4.type = "pppoe" runs pppd on an optional VLAN of the uplink, with the password read from a file at service start (never the Nix store). NAT, forwarding and MSS clamping (MTU 1492) use the derived PPP device; native IPv6 stays on the untagged link. Tests: basic.nix is parameterised by uplink type; the pppoe variant uses a fake ISP with pppoe-server (VLAN 7, CHAP). Both variants cover IPv6 PD, SLAAC, routing without NAT and WAN-deny. Assertion tests gain PD/PPPoE cases and a full-system evaluation step. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
176 lines
5.4 KiB
Nix
176 lines
5.4 KiB
Nix
# 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 = lib.recursiveUpdate {
|
|
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" ];
|
|
};
|
|
pdOnOneLan = {
|
|
router = {
|
|
wan.ipv6.enable = true;
|
|
lans.main = lan { ipv6.pd = true; };
|
|
};
|
|
expect = [ ];
|
|
};
|
|
pdOnTwoLans = {
|
|
router = {
|
|
wan.ipv6.enable = true;
|
|
lans = {
|
|
a = lan { ipv6.pd = true; };
|
|
b = lan {
|
|
interface = "eth2";
|
|
ipv4.address = "192.168.20.1";
|
|
dhcp.pool = { start = "192.168.20.100"; end = "192.168.20.199"; };
|
|
ipv6.pd = true;
|
|
};
|
|
};
|
|
};
|
|
expect = [ "delegated /64 cannot be split" ];
|
|
};
|
|
pdWithoutWanIpv6 = {
|
|
router.lans.main = lan { ipv6.pd = true; };
|
|
expect = [ "router.wan.ipv6.enable is false" ];
|
|
};
|
|
pppoeMissingCredentials = {
|
|
router = {
|
|
wan.ipv4.type = "pppoe";
|
|
lans.main = lan { };
|
|
};
|
|
expect = [
|
|
"pppoe.username must be set"
|
|
"pppoe.passwordFile must be set"
|
|
];
|
|
};
|
|
pppoeValid = {
|
|
router = {
|
|
wan = {
|
|
ipv4 = {
|
|
type = "pppoe";
|
|
pppoe = {
|
|
vlan = 7;
|
|
username = "user@isp";
|
|
passwordFile = "/var/lib/secrets/pppoe";
|
|
};
|
|
};
|
|
ipv6.enable = true;
|
|
};
|
|
lans.main = lan { ipv6.pd = true; };
|
|
};
|
|
expect = [ ];
|
|
};
|
|
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
|
|
);
|
|
|
|
# Assertions alone do not force every option (e.g. networkd unit checks), so
|
|
# fully evaluate the system for the configs that are meant to be valid.
|
|
fullEval =
|
|
name:
|
|
let
|
|
r = builtins.tryEval (builtins.seq (evalRouter cases.${name}.router).system.build.toplevel.drvPath true);
|
|
in
|
|
lib.optional (!r.success) "${name}: full system evaluation failed";
|
|
fullEvalProblems = lib.concatMap fullEval [
|
|
"valid"
|
|
"pdOnOneLan"
|
|
"pppoeValid"
|
|
];
|
|
|
|
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 ++ fullEvalProblems == [ ] then
|
|
"touch $out"
|
|
else
|
|
''
|
|
printf '%s\n' ${lib.escapeShellArgs (problems ++ subnetProblem ++ fullEvalProblems)} >&2
|
|
exit 1
|
|
''
|
|
)
|