Add IPv6 prefix delegation and PPPoE uplink
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>
This commit is contained in:
+71
-4
@@ -17,10 +17,10 @@ let
|
||||
fsType = "ext4";
|
||||
};
|
||||
system.stateVersion = "25.05";
|
||||
router = {
|
||||
router = lib.recursiveUpdate {
|
||||
enable = true;
|
||||
wan.interface = "eth0";
|
||||
} // router;
|
||||
} router;
|
||||
}
|
||||
];
|
||||
}).config;
|
||||
@@ -73,6 +73,59 @@ let
|
||||
};
|
||||
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 { };
|
||||
@@ -94,15 +147,29 @@ let
|
||||
) 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 == [ ] then
|
||||
if problems ++ subnetProblem ++ fullEvalProblems == [ ] then
|
||||
"touch $out"
|
||||
else
|
||||
''
|
||||
printf '%s\n' ${lib.escapeShellArgs (problems ++ subnetProblem)} >&2
|
||||
printf '%s\n' ${lib.escapeShellArgs (problems ++ subnetProblem ++ fullEvalProblems)} >&2
|
||||
exit 1
|
||||
''
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user