Add IPv6 inside the PPP session and use enp2s0 in the example

- wan.ipv6.transport = "ppp": IPv6CP in pppd, router advertisements and
  DHCPv6 prefix delegation on the PPP device, DHCPv6 accepted on the PPP
  device, IPv6 MSS clamped to MTU-60. Allowed on a shared single-NIC
  link. A build-time check requires a PPPoE uplink.
- IPv4 MSS clamping now only matches IPv4 packets.
- The example uses the new box's NIC name (enp2s0).

Covered by the onestick VM test (radvd and a minimal DHCPv6-PD
responder as the ISP) and assertion cases.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 15:03:57 +01:00
co-authored by Claude Sonnet 5.5
parent b5429ad876
commit 70dddad206
9 changed files with 227 additions and 22 deletions
+10 -2
View File
@@ -416,8 +416,16 @@ in
{
assertion =
!(lib.elem cfg.wan.interface (map (l: l.interface) lans))
|| (cfg.wan.ipv4.type == "pppoe" && cfg.wan.ipv4.pppoe.vlan != null && !cfg.wan.ipv6.enable);
message = "router: the WAN interface `${cfg.wan.interface}` is also used by a LAN (only allowed for PPPoE on a VLAN with IPv6 disabled).";
|| (
cfg.wan.ipv4.type == "pppoe"
&& cfg.wan.ipv4.pppoe.vlan != null
&& (!cfg.wan.ipv6.enable || cfg.wan.ipv6.transport == "ppp")
);
message = "router: the WAN interface `${cfg.wan.interface}` is also used by a LAN (only allowed for PPPoE on a VLAN, with IPv6 disabled or inside the PPP session).";
}
{
assertion = cfg.wan.ipv6.transport == "native" || cfg.wan.ipv4.type == "pppoe";
message = "router: wan.ipv6.transport = \"ppp\" needs a PPPoE uplink (wan.ipv4.type = \"pppoe\").";
}
{
assertion =
+11 -3
View File
@@ -183,6 +183,10 @@ let
sharedLink = lib.elem cfg.wan.interface (map (l: l.interface) lans);
uplinks = lib.unique ([ uplink4 ] ++ lib.optional (!sharedLink) cfg.wan.interface);
mss = cfg.wan.ipv4.pppoe.mtu - 40;
mss6 = cfg.wan.ipv4.pppoe.mtu - 60;
v6ppp = cfg.wan.ipv6.enable && cfg.wan.ipv6.transport == "ppp";
# Where DHCPv6 replies arrive.
v6dev = if v6ppp then uplink4 else cfg.wan.interface;
dhcpIfaces = map (l: l.device) (lib.filter (l: l.dhcp.enable) lans);
in
{
@@ -212,7 +216,7 @@ in
iifname ${set memberIfaces} icmpv6 type echo-request accept
${lib.optionalString (!pppoe) ''iifname "${cfg.wan.interface}" udp dport 68 accept''}
${lib.optionalString cfg.wan.ipv6.enable ''iifname "${cfg.wan.interface}" udp dport 546 accept''}
${lib.optionalString cfg.wan.ipv6.enable ''iifname "${v6dev}" udp dport 546 accept''}
iifname ${set dnsIfaces} udp dport 53 accept
iifname ${set dnsIfaces} tcp dport 53 accept
@@ -237,8 +241,12 @@ in
${lib.optionalString pppoe ''
# Clamp MSS to the PPP MTU in both directions (never raised).
iifname "${uplink4}" tcp flags syn tcp option maxseg size > ${toString mss} tcp option maxseg size set ${toString mss}
oifname "${uplink4}" tcp flags syn tcp option maxseg size > ${toString mss} tcp option maxseg size set ${toString mss}
iifname "${uplink4}" meta nfproto ipv4 tcp flags syn tcp option maxseg size > ${toString mss} tcp option maxseg size set ${toString mss}
oifname "${uplink4}" meta nfproto ipv4 tcp flags syn tcp option maxseg size > ${toString mss} tcp option maxseg size set ${toString mss}
${lib.optionalString v6ppp ''
iifname "${uplink4}" meta nfproto ipv6 tcp flags syn tcp option maxseg size > ${toString mss6} tcp option maxseg size set ${toString mss6}
oifname "${uplink4}" meta nfproto ipv6 tcp flags syn tcp option maxseg size > ${toString mss6} tcp option maxseg size set ${toString mss6}
''}
''}
${lib.optionalString (
internetIfaces != [ ]
+31 -4
View File
@@ -2,6 +2,11 @@
let
cfg = config.router;
v6 = cfg.wan.ipv6.enable;
v6ppp = v6 && cfg.wan.ipv6.transport == "ppp";
# IPv6 on the physical link (not inside the PPP session).
v6native = v6 && !v6ppp;
# The device carrying DHCPv6 / RAs.
v6dev = if v6ppp then cfg.wan.ipv4.interface else cfg.wan.interface;
pppoe = cfg.wan.ipv4.type == "pppoe";
vlan = cfg.wan.ipv4.pppoe.vlan;
vlanDevice = "${cfg.wan.interface}.${toString vlan}";
@@ -20,7 +25,7 @@ let
lib.unique (map (l: l.interface) vlanLans)
);
dhcp = if pppoe then (if v6 then "ipv6" else "no") else (if v6 then "yes" else "ipv4");
dhcp = if pppoe then (if v6native then "ipv6" else "no") else (if v6 then "yes" else "ipv4");
in
{
config = lib.mkIf cfg.enable {
@@ -66,10 +71,15 @@ in
networkConfig = {
DHCP = dhcp;
}
// lib.optionalAttrs v6 {
// lib.optionalAttrs v6native {
IPv6AcceptRA = true;
KeepConfiguration = "yes";
}
// lib.optionalAttrs (pppoe && !v6native) {
# IPv6 (if any) lives inside the PPP session, never on the link.
LinkLocalAddressing = "no";
IPv6AcceptRA = false;
}
// lib.optionalAttrs (pppoe && vlan != null) {
VLAN = [ vlanDevice ];
};
@@ -78,7 +88,7 @@ in
# The router runs its own resolver; never adopt the ISP's DNS for the host.
dhcpV4Config.UseDNS = false;
}
// lib.optionalAttrs v6 {
// lib.optionalAttrs v6native {
# The ISP sends RAs (default route) without DHCPv6 flags, so solicit anyway.
dhcpV6Config = {
WithoutRA = "solicit";
@@ -87,6 +97,23 @@ in
};
};
}
// lib.optionalAttrs v6ppp {
# IPv6 inside the PPP session: pppd (IPv4, IPv6CP link-local) creates the
# device; networkd takes RAs and DHCPv6 (prefix delegation) on it.
"12-wan-ppp" = {
matchConfig.Name = cfg.wan.ipv4.interface;
networkConfig = {
DHCP = "ipv6";
IPv6AcceptRA = true;
KeepConfiguration = "yes";
};
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" = {
@@ -118,7 +145,7 @@ in
}
// lib.optionalAttrs lan.ipv6.pd {
dhcpPrefixDelegationConfig = {
UplinkInterface = cfg.wan.interface;
UplinkInterface = v6dev;
SubnetId = 0;
Announce = true;
Assign = true;
+21 -4
View File
@@ -71,10 +71,27 @@ in
};
};
ipv6.enable = mkEnableOption ''
IPv6 on the uplink: DHCPv6 (soliciting even without RA flags) plus
prefix delegation. The ISP is expected to delegate a single /64
'';
ipv6 = {
enable = mkEnableOption ''
IPv6 on the uplink: DHCPv6 (soliciting even without RA flags) plus
prefix delegation. The ISP is expected to delegate a single /64
'';
transport = mkOption {
type = types.enum [
"native"
"ppp"
];
default = "native";
description = ''
Where IPv6 runs. `native`: untagged on the uplink link, next to a
DHCPv4 or PPPoE (tagged) IPv4 uplink. `ppp`: inside the PPPoE
session (IPv6CP), so DHCPv6 and router advertisements arrive on
the PPP device; needs a PPPoE uplink, and leaves the physical link
free for a LAN.
'';
};
};
};
lans = mkOption {
+6 -1
View File
@@ -28,7 +28,12 @@ let
defaultroute
replacedefaultroute
noipdefault
nodefaultroute6
${
if wan.ipv6.enable && wan.ipv6.transport == "ppp" then
"+ipv6\n ipv6cp-use-ipaddr"
else
"nodefaultroute6"
}
noccp
mtu ${toString pppoe.mtu}
mru ${toString pppoe.mtu}