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>
97 lines
2.7 KiB
Nix
97 lines
2.7 KiB
Nix
{ config, lib, ... }:
|
|
let
|
|
cfg = config.router;
|
|
v6 = cfg.wan.ipv6.enable;
|
|
pppoe = cfg.wan.ipv4.type == "pppoe";
|
|
vlan = cfg.wan.ipv4.pppoe.vlan;
|
|
vlanDevice = "${cfg.wan.interface}.${toString vlan}";
|
|
dhcp = if pppoe then (if v6 then "ipv6" else "no") else (if v6 then "yes" else "ipv4");
|
|
in
|
|
{
|
|
config = lib.mkIf cfg.enable {
|
|
networking.useNetworkd = true;
|
|
networking.useDHCP = false;
|
|
|
|
boot.kernel.sysctl = {
|
|
"net.ipv4.conf.all.forwarding" = 1;
|
|
"net.ipv6.conf.all.forwarding" = 1;
|
|
};
|
|
|
|
systemd.network.netdevs = lib.optionalAttrs (pppoe && vlan != null) {
|
|
"05-wan-pppoe" = {
|
|
netdevConfig = {
|
|
Kind = "vlan";
|
|
Name = vlanDevice;
|
|
};
|
|
vlanConfig.Id = vlan;
|
|
};
|
|
};
|
|
|
|
systemd.network.networks = {
|
|
"10-wan" = {
|
|
matchConfig.Name = cfg.wan.interface;
|
|
networkConfig = {
|
|
DHCP = dhcp;
|
|
}
|
|
// lib.optionalAttrs v6 {
|
|
IPv6AcceptRA = true;
|
|
KeepConfiguration = "yes";
|
|
}
|
|
// lib.optionalAttrs (pppoe && vlan != null) {
|
|
VLAN = [ vlanDevice ];
|
|
};
|
|
}
|
|
// lib.optionalAttrs (!pppoe) {
|
|
# The router runs its own resolver; never adopt the ISP's DNS for the host.
|
|
dhcpV4Config.UseDNS = false;
|
|
}
|
|
// lib.optionalAttrs v6 {
|
|
# The ISP sends RAs (default route) without DHCPv6 flags, so solicit anyway.
|
|
dhcpV6Config = {
|
|
WithoutRA = "solicit";
|
|
UseDelegatedPrefix = true;
|
|
UseDNS = false;
|
|
};
|
|
};
|
|
}
|
|
// lib.optionalAttrs (pppoe && vlan != null) {
|
|
# pppd configures the session; networkd only has to bring the device up.
|
|
"11-wan-pppoe" = {
|
|
matchConfig.Name = vlanDevice;
|
|
networkConfig = {
|
|
LinkLocalAddressing = "no";
|
|
IPv6AcceptRA = false;
|
|
};
|
|
};
|
|
}
|
|
// lib.mapAttrs' (
|
|
name: lan:
|
|
lib.nameValuePair "20-lan-${name}" (
|
|
{
|
|
matchConfig.Name = lan.interface;
|
|
address = [ "${lan.ipv4.address}/${toString lan.ipv4.prefixLength}" ];
|
|
networkConfig = {
|
|
ConfigureWithoutCarrier = true;
|
|
# Never accept RAs from the inside.
|
|
IPv6AcceptRA = false;
|
|
}
|
|
// lib.optionalAttrs lan.ipv6.pd {
|
|
DHCPPrefixDelegation = true;
|
|
IPv6SendRA = true;
|
|
};
|
|
}
|
|
// lib.optionalAttrs lan.ipv6.pd {
|
|
dhcpPrefixDelegationConfig = {
|
|
UplinkInterface = cfg.wan.interface;
|
|
SubnetId = 0;
|
|
Announce = true;
|
|
Assign = true;
|
|
};
|
|
# No RDNSS yet: the global address is unknown at evaluation time.
|
|
ipv6SendRAConfig.EmitDNS = false;
|
|
}
|
|
)
|
|
) cfg.lans;
|
|
};
|
|
}
|