Files
nixos-router/modules/router/networking.nix
T
nilsandClaude Sonnet 5.5 70dddad206 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>
2026-10-28 15:03:57 +01:00

174 lines
5.8 KiB
Nix

{ config, lib, ... }:
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}";
wanVlanOn = parent: lib.optional (pppoe && vlan != null && parent == cfg.wan.interface) vlanDevice;
lans = lib.attrValues cfg.lans;
vlanLans = lib.filter (l: l.vlan != null) lans;
# VLAN devices hang off their parent; the parent's own unit must list them.
vlansOn =
parent: map (l: l.device) (lib.filter (l: l.interface == parent) vlanLans) ++ wanVlanOn parent;
# The WAN link's own unit is only needed when no LAN shares the link.
sharedLink = lib.elem cfg.wan.interface (map (l: l.interface) lans);
untaggedParents = map (l: l.interface) (lib.filter (l: l.vlan == null) lans);
# Parents that carry only tagged LANs still need a unit to be brought up.
trunkParents = lib.filter (p: !(lib.elem p untaggedParents)) (
lib.unique (map (l: l.interface) vlanLans)
);
dhcp = if pppoe then (if v6native then "ipv6" else "no") else (if v6 then "yes" else "ipv4");
in
{
config = lib.mkIf cfg.enable {
networking.useNetworkd = true;
networking.useDHCP = false;
# Wait for the LAN devices (VLANs included: Kea only binds interfaces that
# exist when it starts) but not for the uplink: an ISP outage or a pulled
# cable must not hold up boot or a deployment.
systemd.network.wait-online.extraArgs = map (l: "--interface=${l.device}") (
lib.attrValues cfg.lans
);
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;
};
}
// lib.mapAttrs' (
name: lan:
lib.nameValuePair "06-lan-${name}" {
netdevConfig = {
Kind = "vlan";
Name = lan.device;
};
vlanConfig.Id = lan.vlan;
}
) (lib.filterAttrs (_: l: l.vlan != null) cfg.lans);
systemd.network.networks =
lib.optionalAttrs (!sharedLink) {
"10-wan" = {
matchConfig.Name = cfg.wan.interface;
networkConfig = {
DHCP = dhcp;
}
// 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 ];
};
}
// lib.optionalAttrs (!pppoe) {
# The router runs its own resolver; never adopt the ISP's DNS for the host.
dhcpV4Config.UseDNS = false;
}
// lib.optionalAttrs v6native {
# The ISP sends RAs (default route) without DHCPv6 flags, so solicit anyway.
dhcpV6Config = {
WithoutRA = "solicit";
UseDelegatedPrefix = true;
UseDNS = false;
};
};
}
// 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" = {
matchConfig.Name = vlanDevice;
networkConfig = {
LinkLocalAddressing = "no";
IPv6AcceptRA = false;
};
};
}
// lib.mapAttrs' (
name: lan:
lib.nameValuePair "20-lan-${name}" (
{
matchConfig.Name = lan.device;
address = [ "${lan.ipv4.address}/${toString lan.ipv4.prefixLength}" ];
networkConfig = {
ConfigureWithoutCarrier = true;
# Never accept RAs from the inside.
IPv6AcceptRA = false;
}
// lib.optionalAttrs (lan.vlan == null && vlansOn lan.interface != [ ]) {
VLAN = vlansOn lan.interface;
}
// lib.optionalAttrs lan.ipv6.pd {
DHCPPrefixDelegation = true;
IPv6SendRA = true;
};
}
// lib.optionalAttrs lan.ipv6.pd {
dhcpPrefixDelegationConfig = {
UplinkInterface = v6dev;
SubnetId = 0;
Announce = true;
Assign = true;
};
# No RDNSS yet: the global address is unknown at evaluation time.
ipv6SendRAConfig.EmitDNS = false;
}
)
) cfg.lans
// lib.listToAttrs (
map (
parent:
lib.nameValuePair "19-trunk-${parent}" {
matchConfig.Name = parent;
networkConfig = {
LinkLocalAddressing = "no";
IPv6AcceptRA = false;
ConfigureWithoutCarrier = true;
VLAN = vlansOn parent;
};
}
) trunkParents
);
};
}