Add the home gateway config and tunnel source addresses

- wireguard.<n>.sourceAddresses: source for router-originated traffic
  into a tunnel without an address of its own (PreferredSource on the
  routes); checked at build time.
- examples/home: antigua's routing role ported to the router module
  (PPPoE with IPv6 inside the session, LAN with ULA and reservation,
  local DNS and forward zones, the freifunk/family/mobile tunnels with
  explicit zone rules, ddclient following the PPP device).
  nixosConfigurations.home, packages.installer-home and a flake check
  that builds it.
- PLAN.md: the remaining antigua gaps and the open question about
  services that stay behind the router.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 16:53:14 +01:00
co-authored by Claude Sonnet 5.5
parent c395a43a78
commit 406196adce
8 changed files with 322 additions and 5 deletions
+22
View File
@@ -380,6 +380,28 @@ let
assertion = lib.stringLength w.interface <= 15;
message = "router: ${where}: device name `${w.interface}` is longer than 15 characters.";
}
{
assertion = lib.all anyIp w.sourceAddresses;
message = "router: ${where}.sourceAddresses must be IPv4/IPv6 addresses.";
}
{
assertion =
lib.length (lib.filter isV6 w.sourceAddresses) <= 1
&& lib.length (lib.filter (s: !isV6 s) w.sourceAddresses) <= 1;
message = "router: ${where}.sourceAddresses may hold at most one address per family.";
}
{
assertion = lib.all (
s:
isV6 s
|| !(validIp s)
|| lib.elem s (
map (l: l.ipv4.address) lans
++ map (a: builtins.head (lib.splitString "/" a)) (lib.concatMap (x: x.addresses) wgs)
)
) w.sourceAddresses;
message = "router: ${where}.sourceAddresses: an IPv4 source must be the router's address on a LAN or a tunnel.";
}
{
assertion = lib.all validCidr w.addresses;
message = "router: ${where}.addresses must be IPv4/IPv6 addresses in CIDR notation.";
+13
View File
@@ -296,6 +296,19 @@ in
empty means no transfer network (fine for site-to-site).
'';
};
sourceAddresses = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "192.168.10.1" ];
description = ''
Source address (at most one per family) for traffic the router
itself sends into this tunnel, e.g. DNS queries to the other
site. Needed when the tunnel has no address of its own, so the
other side sees a source it routes back instead of whatever the
kernel picks. IPv4 ones must be an address the router holds on
a LAN or a tunnel.
'';
};
zone = mkOption {
type = types.str;
default = name;
+13 -1
View File
@@ -64,7 +64,19 @@ in
IPv6AcceptRA = false;
};
# Cryptokey routing: a peer's allowedIPs are the destinations sent into the tunnel.
routes = lib.concatMap (p: map (ip: { Destination = ip; }) p.allowedIPs) (lib.attrValues wg.peers);
routes = lib.concatMap (
p:
map (
ip:
let
sameFamily = lib.filter (s: lib.hasInfix ":" s == lib.hasInfix ":" ip) wg.sourceAddresses;
in
{
Destination = ip;
}
// lib.optionalAttrs (sameFamily != [ ]) { PreferredSource = builtins.head sameFamily; }
) p.allowedIPs
) (lib.attrValues wg.peers);
}
) cfg.wireguard;
};