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>
133 lines
4.1 KiB
Nix
133 lines
4.1 KiB
Nix
{ config, lib, ... }:
|
|
let
|
|
cfg = config.router;
|
|
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 {
|
|
assertions = [
|
|
{
|
|
assertion = lans != [ ];
|
|
message = "router: at least one LAN must be defined.";
|
|
}
|
|
{
|
|
assertion = lib.any (l: l.allowManagement) lans;
|
|
message = "router: no LAN has allowManagement = true; you would be locked out of the router.";
|
|
}
|
|
{
|
|
assertion = !(lib.elem cfg.wan.interface (map (l: l.interface) lans));
|
|
message = "router: the WAN interface `${cfg.wan.interface}` is also used by a LAN.";
|
|
}
|
|
{
|
|
assertion = cfg.wan.ipv4.type != "pppoe" || cfg.wan.ipv4.pppoe.username != "";
|
|
message = "router: router.wan.ipv4.pppoe.username must be set for a PPPoE uplink.";
|
|
}
|
|
{
|
|
assertion = cfg.wan.ipv4.type != "pppoe" || cfg.wan.ipv4.pppoe.passwordFile != null;
|
|
message = "router: router.wan.ipv4.pppoe.passwordFile must be set for a PPPoE uplink.";
|
|
}
|
|
{
|
|
assertion = lib.length (lib.filter (l: l.ipv6.pd) lans) <= 1;
|
|
message = "router: more than one LAN sets ipv6.pd, but the delegated /64 cannot be split.";
|
|
}
|
|
{
|
|
assertion = !(lib.any (l: l.ipv6.pd) lans) || cfg.wan.ipv6.enable;
|
|
message = "router: a LAN sets ipv6.pd but router.wan.ipv6.enable is false.";
|
|
}
|
|
{
|
|
assertion =
|
|
let
|
|
ifaces = map (l: l.interface) lans;
|
|
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;
|
|
};
|
|
}
|