lans.<n>.ipv6.address (a ULA /64) is assigned to the LAN, announced with an explicit prefix (networkd only announces the delegated prefix when prefix delegation is on) and advertised as RDNSS; Unbound listens and answers on it. Checked at build time: valid IPv6 CIDR, /64, fc00::/7. Covered by the onestick VM test (client SLAAC address, ping and DNS over the ULA, RA captured on the wire), assertion cases and a unit test for the /64 prefix helper. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
201 lines
6.9 KiB
Nix
201 lines
6.9 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}"
|
|
]
|
|
++ lib.optional (lan.ipv6.address != null) lan.ipv6.address;
|
|
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;
|
|
}
|
|
// lib.optionalAttrs (lan.ipv6.pd || lan.ipv6.address != null) {
|
|
IPv6SendRA = true;
|
|
};
|
|
}
|
|
// lib.optionalAttrs (lan.ipv6.address != null) {
|
|
# With prefix delegation networkd announces only the delegated
|
|
# prefix, so announce the ULA explicitly (SLAAC, on-link).
|
|
ipv6Prefixes = [
|
|
{
|
|
Prefix = (import ./lib.nix { inherit lib; }).ipv6.prefix64 lan.ipv6.address;
|
|
OnLink = true;
|
|
AddressAutoconfiguration = true;
|
|
}
|
|
];
|
|
}
|
|
// lib.optionalAttrs lan.ipv6.pd {
|
|
dhcpPrefixDelegationConfig = {
|
|
UplinkInterface = v6dev;
|
|
SubnetId = 0;
|
|
Announce = true;
|
|
Assign = true;
|
|
};
|
|
}
|
|
// lib.optionalAttrs (lan.ipv6.pd || lan.ipv6.address != null) {
|
|
# RDNSS points hosts at the router's resolver on its stable ULA; the
|
|
# delegated global address is unknown at evaluation time, so without
|
|
# a static `ipv6.address` there is nothing to announce.
|
|
ipv6SendRAConfig =
|
|
if lan.ipv6.address != null then
|
|
{
|
|
EmitDNS = true;
|
|
DNS = [ (builtins.head (lib.splitString "/" lan.ipv6.address)) ];
|
|
}
|
|
else
|
|
{ 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
|
|
);
|
|
};
|
|
}
|