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:
@@ -0,0 +1,86 @@
|
||||
# PPPoE uplink for IPv4. pppd owns the session (address, default route); the
|
||||
# VLAN device it runs on is created in networking.nix.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.router;
|
||||
wan = cfg.wan;
|
||||
pppoe = wan.ipv4.pppoe;
|
||||
|
||||
# Device carrying the PPPoE session: the VLAN on the uplink, or the uplink itself.
|
||||
device = if pppoe.vlan == null then wan.interface else "${wan.interface}.${toString pppoe.vlan}";
|
||||
|
||||
# Unit that appears once udev has seen the device (VLAN devices are created
|
||||
# by networkd after boot has begun, so pppd must not start before it).
|
||||
deviceUnit = "sys-subsystem-net-devices-${lib.replaceStrings [ "-" ] [ "\\x2d" ] device}.device";
|
||||
|
||||
staticOptions = pkgs.writeText "router-pppoe-options" ''
|
||||
plugin pppoe.so
|
||||
nic-${device}
|
||||
ifname ${wan.ipv4.interface}
|
||||
user "${pppoe.username}"
|
||||
noauth
|
||||
hide-password
|
||||
defaultroute
|
||||
replacedefaultroute
|
||||
noipdefault
|
||||
nodefaultroute6
|
||||
noccp
|
||||
mtu ${toString pppoe.mtu}
|
||||
mru ${toString pppoe.mtu}
|
||||
lcp-echo-interval 10
|
||||
lcp-echo-failure 3
|
||||
nodetach
|
||||
persist
|
||||
maxfail 0
|
||||
holdoff 5
|
||||
'';
|
||||
|
||||
prepare = pkgs.writeShellScript "router-pppoe-prepare" ''
|
||||
set -eu
|
||||
umask 077
|
||||
password=$(cat ${lib.escapeShellArg (toString pppoe.passwordFile)})
|
||||
# pppd option files use C-style escapes inside double quotes.
|
||||
password=''${password//\\/\\\\}
|
||||
password=''${password//\"/\\\"}
|
||||
{
|
||||
cat ${staticOptions}
|
||||
printf 'password "%s"\n' "$password"
|
||||
} > /run/router-pppoe/options
|
||||
'';
|
||||
in
|
||||
{
|
||||
config = lib.mkIf (cfg.enable && wan.ipv4.type == "pppoe") {
|
||||
boot.kernelModules = [
|
||||
"pppoe"
|
||||
"pppox"
|
||||
"ppp_generic"
|
||||
];
|
||||
|
||||
# pppd refuses to start without a (possibly empty) system options file.
|
||||
environment.etc."ppp/options".text = "";
|
||||
|
||||
systemd.services.router-pppoe = {
|
||||
description = "PPPoE uplink (${device})";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
wants = [ deviceUnit ];
|
||||
after = [
|
||||
"network.target"
|
||||
"systemd-networkd.service"
|
||||
deviceUnit
|
||||
];
|
||||
serviceConfig = {
|
||||
RuntimeDirectory = "router-pppoe";
|
||||
RuntimeDirectoryMode = "0700";
|
||||
ExecStartPre = prepare;
|
||||
ExecStart = "${pkgs.ppp}/bin/pppd file /run/router-pppoe/options";
|
||||
Restart = "always";
|
||||
RestartSec = 5;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user