Add a static ULA per LAN, announced via SLAAC and RDNSS
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>
This commit is contained in:
@@ -43,6 +43,27 @@ let
|
||||
message = "router: ${where}: device name `${lan.device}` is longer than 15 characters.";
|
||||
}
|
||||
]
|
||||
++ lib.optionals (lan.ipv6.address != null) (
|
||||
let
|
||||
m = builtins.match "([0-9a-fA-F:]+)/([0-9]{1,3})" lan.ipv6.address;
|
||||
in
|
||||
[
|
||||
{
|
||||
assertion = isV6 lan.ipv6.address && v6Cidr lan.ipv6.address && m != null;
|
||||
message = "router: ${where}.ipv6.address `${lan.ipv6.address}` is not an IPv6 address in CIDR notation.";
|
||||
}
|
||||
]
|
||||
++ lib.optionals (m != null) [
|
||||
{
|
||||
assertion = builtins.elemAt m 1 == "64";
|
||||
message = "router: ${where}.ipv6.address must be a /64 (SLAAC needs one).";
|
||||
}
|
||||
{
|
||||
assertion = builtins.match "[fF][cCdD][0-9a-fA-F]{2}:.*" (builtins.elemAt m 0) != null;
|
||||
message = "router: ${where}.ipv6.address must be a unique-local address (fc00::/7).";
|
||||
}
|
||||
]
|
||||
)
|
||||
++ lib.optionals lan.dhcp.enable (
|
||||
[
|
||||
{
|
||||
|
||||
@@ -55,13 +55,19 @@ in
|
||||
enable = true;
|
||||
settings = {
|
||||
server = {
|
||||
interface = [ "127.0.0.1" ] ++ map (l: l.ipv4.address) lans ++ map addrOf dnsAddrs;
|
||||
interface = [
|
||||
"127.0.0.1"
|
||||
]
|
||||
++ map (l: l.ipv4.address) lans
|
||||
++ map (l: addrOf l.ipv6.address) (lib.filter (l: l.ipv6.address != null) lans)
|
||||
++ map addrOf dnsAddrs;
|
||||
# LAN addresses are assigned by networkd, possibly after Unbound starts.
|
||||
ip-freebind = true;
|
||||
access-control = [
|
||||
"127.0.0.0/8 allow"
|
||||
]
|
||||
++ map (l: "${l.ipv4.subnet} allow") lans
|
||||
++ map (l: "${l.ipv6.address} allow") (lib.filter (l: l.ipv6.address != null) lans)
|
||||
++ map (a: "${netOf a} allow") dnsAddrs
|
||||
++ map (n: "${n} allow") dnsPeerNets;
|
||||
local-zone =
|
||||
|
||||
@@ -62,4 +62,20 @@ rec {
|
||||
in
|
||||
networkOf a.address p == networkOf b.address p;
|
||||
};
|
||||
|
||||
ipv6 = {
|
||||
# The first 64 bits of an address (any form, with or without a prefix
|
||||
# length) as a /64 prefix, e.g. fd2c:cdef:cde9::1/64 -> fd2c:cdef:cde9:0::/64.
|
||||
prefix64 =
|
||||
address:
|
||||
let
|
||||
bare = builtins.head (lib.splitString "/" address);
|
||||
halves = lib.splitString "::" bare;
|
||||
groups = s: lib.filter (g: g != "") (lib.splitString ":" s);
|
||||
head = groups (builtins.elemAt halves 0);
|
||||
tail = lib.optionals (lib.length halves > 1) (groups (builtins.elemAt halves 1));
|
||||
full = head ++ lib.replicate (8 - lib.length head - lib.length tail) "0" ++ tail;
|
||||
in
|
||||
"${lib.concatStringsSep ":" (lib.take 4 full)}::/64";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,7 +129,10 @@ in
|
||||
lib.nameValuePair "20-lan-${name}" (
|
||||
{
|
||||
matchConfig.Name = lan.device;
|
||||
address = [ "${lan.ipv4.address}/${toString lan.ipv4.prefixLength}" ];
|
||||
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.
|
||||
@@ -140,9 +143,22 @@ in
|
||||
}
|
||||
// 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;
|
||||
@@ -150,8 +166,19 @@ in
|
||||
Announce = true;
|
||||
Assign = true;
|
||||
};
|
||||
# No RDNSS yet: the global address is unknown at evaluation time.
|
||||
ipv6SendRAConfig.EmitDNS = false;
|
||||
}
|
||||
// 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
|
||||
|
||||
@@ -209,6 +209,19 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
ipv6.address = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "fd2c:cdef:cde9::1/64";
|
||||
description = ''
|
||||
Static unique-local (ULA) address of the router on this LAN,
|
||||
as a /64. It is announced via SLAAC, so hosts get a stable
|
||||
internal IPv6 address even without a delegated prefix, and
|
||||
the router's resolver answers on it (also advertised as
|
||||
RDNSS). Independent of `ipv6.pd`.
|
||||
'';
|
||||
};
|
||||
|
||||
allowManagement = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
|
||||
Reference in New Issue
Block a user