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
+213
View File
@@ -0,0 +1,213 @@
# The home gateway that replaces antigua's routing role (ported from antigua's
# hand-written networkd/pppd/dnsmasq/WireGuard configuration).
#
# ISP -- switch port --+-- enp2s0 (untagged) LAN 192.168.168.0/24 (+ ULA, + delegated /64)
# +-- enp2s0.1001 PPPoE session: IPv4 and IPv6 (IPv6CP)
#
# Secrets are NOT in this repository. Put them in the directory handed to
# `router-install --secrets`; it lands in /var/lib/router on the box:
#
# pppoe-password the PPPoE password (from the ISP)
# cloudflare-ddns-token Cloudflare API token for ddclient
# wireguard/freifunk.key private keys (the ones antigua uses today keep the
# wireguard/family.key peers working without any change on their side;
# wireguard/family.psk never run both boxes with the same keys at once)
# wireguard/mobile-clients.key
# wireguard/mobile-clients.psk
#
# Before installing: fill in the PPPoE username below.
{ config, lib, ... }:
let
secrets = "/var/lib/router";
k8s = {
v4 = "10.43.162.50";
v6 = "fd32:66ac:e01f::f294";
};
in
{
imports = [ ../../modules/installer/layout.nix ];
networking.hostName = "gateway";
time.timeZone = "Europe/Berlin";
system.stateVersion = "26.11";
users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHqyMLgAhEdnrRrd2CeYbphGDMPq/cBDjFJ4q4ItZusn nils@Nilss-Mac-mini.mch.stinnesbeck.com"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBFxbNNk87tvMxruuKvrryK7iAdf4xgS4xFqjKejNAI6 nils@nixtop"
];
router = {
enable = true;
wan = {
interface = "enp2s0";
ipv4 = {
type = "pppoe";
pppoe = {
vlan = 1001;
# From antigua's networking.nix (pppd `user`); kept out of this file.
username = "CHANGE-ME";
passwordFile = "${secrets}/pppoe-password";
};
};
# IPv6 runs inside the PPP session, with one delegated /64.
ipv6 = {
enable = true;
transport = "ppp";
};
};
lans.lan = {
interface = "enp2s0";
internet = true;
ipv4.address = "192.168.168.254";
ipv6 = {
pd = true;
address = "fd2c:cdef:cde9::1/64";
};
dhcp = {
pool = {
start = "192.168.168.100";
end = "192.168.168.200";
};
reservations.truenas = {
mac = "a8:a1:59:0b:4b:97";
address = "192.168.168.251";
};
};
};
dns = {
domain = "mch.stinnesbeck.com";
forwarders = [
"1.1.1.1"
"2606:4700:4700::1111"
];
hosts = {
gateway = [ "192.168.168.254" ];
"photos.stinnesbeck.com" = [
k8s.v4
k8s.v6
];
"git.stinnesbeck.com" = [
k8s.v4
k8s.v6
];
};
overrides."k8s.stinnesbeck.com" = [
k8s.v4
k8s.v6
];
forwardZones."tdf.stinnesbeck.com" = [ "10.1.0.254" ];
};
wireguard = {
# Provider-style dial-out: only the treasure-maps.com hosts go through
# it (antigua accepts 0.0.0.0/0 but routes just these), masqueraded. The
# kill switch keeps them from leaking out of the WAN if the tunnel is down.
freifunk = {
privateKeyFile = "${secrets}/wireguard/freifunk.key";
zone = "freifunk";
masquerade = true;
addresses = [
"10.255.1.8/32"
"fd80:3ea2:e399:203a::8/128"
];
peers.supernode = {
publicKey = "5B/YTaDPVWVApUyHshJp899iXXlBy8rBqJUpYvKo+1s=";
endpoint = "5.9.220.114:42001";
persistentKeepalive = 15;
allowedIPs = [
"185.90.196.65/32"
"185.90.196.97/32"
"185.90.196.129/32"
];
};
};
# Site-to-site with Troisdorf; no transfer network. The site may use the
# router (SSH, DNS), and the router reaches it from its LAN addresses.
family = {
privateKeyFile = "${secrets}/wireguard/family.key";
listenPort = 36432;
zone = "family";
allowManagement = true;
dns = true;
sourceAddresses = [
"192.168.168.254"
"fd2c:cdef:cde9::1"
];
peers.troisdorf = {
publicKey = "BsQODhcHnEN4tXn60t72iejZvgYvVNt5BDVK+69+RgM=";
endpoint = "fw-tdf.stinnesbeck.com:36432";
persistentKeepalive = 25;
presharedKeyFile = "${secrets}/wireguard/family.psk";
allowedIPs = [
"10.1.0.0/24"
"fd73:f65e:6b64::/48"
];
};
};
# Road warrior.
mobile = {
privateKeyFile = "${secrets}/wireguard/mobile-clients.key";
listenPort = 36433;
zone = "mobile";
allowManagement = true;
dns = true;
peers.laptop = {
publicKey = "Jts5u8zIs28v6RIEYCCufDDiWXf+gbElT63wUFWQxX4=";
persistentKeepalive = 25;
presharedKeyFile = "${secrets}/wireguard/mobile-clients.psk";
allowedIPs = [ "10.2.0.2/32" ];
};
};
};
# antigua trusted these interfaces completely and forwarded between all of
# them. Here it is spelled out: the LAN and Troisdorf reach each other, the
# LAN reaches the freifunk hosts, road warriors reach the LAN. Nothing
# initiates from freifunk.
firewall.rules = [
{
from = "lan";
to = "family";
}
{
from = "family";
to = "lan";
}
{
from = "lan";
to = "freifunk";
}
{
from = "mobile";
to = "lan";
}
];
};
# Dynamic DNS from the PPP device's addresses.
services.ddclient = {
enable = true;
interval = "5min";
protocol = "cloudflare";
username = "token";
passwordFile = "${secrets}/cloudflare-ddns-token";
zone = "stinnesbeck.com";
domains = [
"fw-mch.stinnesbeck.com"
"car.k8s.stinnesbeck.com"
"photos.stinnesbeck.com"
"git.stinnesbeck.com"
"link.stinnesbeck.com"
"pw.k8s.stinnesbeck.com"
];
usev4 = "ifv4, ifv4=${config.router.wan.ipv4.interface}";
usev6 = "ifv6, ifv6=${config.router.wan.ipv4.interface}";
ssl = true;
verbose = true;
};
}