Files
nixos-router/modules/router/pppoe.nix
T
nilsandClaude Sonnet 5.5 6038367252 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>
2026-10-27 16:29:52 +01:00

87 lines
2.3 KiB
Nix

# 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;
};
};
};
}