14d9bba4eb
Clients were handed both routers' real addresses as resolvers, so a downed router meant per-query resolver timeouts. Serve pdns-recursor on the VRRP VIPs (with non-local bind so the backup can pre-bind them) and advertise the VIP via kea and radvd, so DNS follows the master. untrusted advertises Cloudflare over v6 to match its v4 config. ipsec started before the WAN's public IP was up: stream's wan carries a static modem address that satisfies wait-online before the DHCP lease, so libreswan loaded its mesh conns (left=<public IP>) unoriented and never initiated. Gate stream's wan-online.target on the DHCP default route instead, and mkForce ipsec onto wan-online.target only (dropping the strongswan/libreswan multi-user.target pull-in) so the gate actually holds on both boxes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
380 lines
12 KiB
Nix
380 lines
12 KiB
Nix
index: { lib, allAssignments, ... }:
|
|
let
|
|
inherit (builtins) elemAt;
|
|
inherit (lib.my) net mkVLAN;
|
|
inherit (lib.my.c) pubDomain;
|
|
inherit (lib.my.c.home) domain vlans prefixes vips routers routersPubV4;
|
|
|
|
name = elemAt routers index;
|
|
otherIndex = 1 - index;
|
|
in
|
|
{
|
|
nixos.systems."${name}" = {
|
|
assignments = {
|
|
modem = {
|
|
ipv4 = {
|
|
address = net.cidr.host (254 - index) prefixes.modem.v4;
|
|
gateway = null;
|
|
};
|
|
};
|
|
core = {
|
|
name = "${name}-core";
|
|
inherit domain;
|
|
mtu = 1500;
|
|
ipv4 = {
|
|
address = net.cidr.host (index + 1) prefixes.core.v4;
|
|
gateway = null;
|
|
};
|
|
};
|
|
hi = {
|
|
name = "${name}-hi";
|
|
inherit domain;
|
|
mtu = 9000;
|
|
ipv4 = {
|
|
address = net.cidr.host (index + 1) prefixes.hi.v4;
|
|
mask = 22;
|
|
gateway = null;
|
|
};
|
|
ipv6.address = net.cidr.host (index + 1) prefixes.hi.v6;
|
|
};
|
|
lo = {
|
|
name = "${name}-lo";
|
|
inherit domain;
|
|
mtu = 1500;
|
|
ipv4 = {
|
|
address = net.cidr.host (index + 1) prefixes.lo.v4;
|
|
mask = 21;
|
|
gateway = null;
|
|
};
|
|
ipv6.address = net.cidr.host (index + 1) prefixes.lo.v6;
|
|
};
|
|
untrusted = {
|
|
name = "${name}-ut";
|
|
inherit domain;
|
|
mtu = 1500;
|
|
ipv4 = {
|
|
address = net.cidr.host (index + 1) prefixes.untrusted.v4;
|
|
mask = 24;
|
|
gateway = null;
|
|
};
|
|
ipv6.address = net.cidr.host (index + 1) prefixes.untrusted.v6;
|
|
};
|
|
as211024 = {
|
|
ipv4 = {
|
|
address = net.cidr.host (index + 2) prefixes.as211024.v4;
|
|
gateway = null;
|
|
};
|
|
ipv6 = {
|
|
address = net.cidr.host ((1*65536*65536*65536) + index + 1) prefixes.as211024.v6;
|
|
gateway = net.cidr.host ((2*65536*65536*65536) + 1) prefixes.as211024.v6;
|
|
};
|
|
};
|
|
};
|
|
|
|
extraAssignments = {
|
|
router-hi.hi = {
|
|
name = "router-hi";
|
|
inherit domain;
|
|
ipv4 = {
|
|
address = vips.hi.v4;
|
|
mask = 22;
|
|
};
|
|
ipv6.address = vips.hi.v6;
|
|
};
|
|
router-lo.lo = {
|
|
name = "router-lo";
|
|
inherit domain;
|
|
ipv4 = {
|
|
address = vips.lo.v4;
|
|
mask = 21;
|
|
};
|
|
ipv6.address = vips.lo.v6;
|
|
};
|
|
router-ut.untrusted = {
|
|
name = "router-ut";
|
|
inherit domain;
|
|
ipv4.address = vips.untrusted.v4;
|
|
ipv6.address = vips.untrusted.v6;
|
|
};
|
|
};
|
|
|
|
configuration = { lib, pkgs, config, assignments, allAssignments, ... }:
|
|
let
|
|
inherit (lib) mkIf mkMerge mkForce;
|
|
inherit (lib.my) networkdAssignment;
|
|
inherit (lib.my.c) networkd;
|
|
in
|
|
{
|
|
imports = map (m: import m index) [
|
|
./keepalived.nix
|
|
./dns.nix
|
|
./radvd.nix
|
|
./kea.nix
|
|
];
|
|
|
|
config = {
|
|
environment = {
|
|
systemPackages = with pkgs; [
|
|
ethtool
|
|
conntrack-tools
|
|
];
|
|
};
|
|
|
|
services = {
|
|
resolved.settings.Resolve = {
|
|
LLMNR = false;
|
|
MulticastDNS = false;
|
|
};
|
|
|
|
iperf3 = {
|
|
enable = true;
|
|
openFirewall = true;
|
|
};
|
|
|
|
networkd-dispatcher = {
|
|
enable = true;
|
|
rules = {
|
|
# tc filter hasn't been networkd-ified yet
|
|
setup-wan-mirror = {
|
|
onState = [ "configured" ];
|
|
script = ''
|
|
#!${pkgs.runtimeShell}
|
|
if [ "$IFACE" = "wan-ifb" ]; then
|
|
${pkgs.iproute2}/bin/tc filter add dev wan parent ffff: matchall action mirred egress redirect dev "$IFACE"
|
|
fi
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
nginx.enable = true;
|
|
};
|
|
|
|
networking = { inherit domain; };
|
|
|
|
# Uniform "WAN is up" gate. Consumers attach to this target (via wantedBy +
|
|
# partOf) rather than depending on it, so it is never pulled in / prematurely
|
|
# activated. Each box wires up how the target actually gets reached: stream
|
|
# gates it on networkd's wait-online, river drives it from the pppd hooks.
|
|
systemd.targets.wan-online.description = "WAN is online";
|
|
|
|
systemd.services = {
|
|
ipsec = {
|
|
after = [ "wan-online.target" ];
|
|
# strongswan/libreswan force wantedBy=multi-user.target; drop it so the
|
|
# target is a true gate rather than mere ordering. This matters most on
|
|
# river, where the target is hook-driven and not in the boot transaction,
|
|
# so plain ordering wouldn't hold ipsec back at all. partOf re-loads ipsec
|
|
# (re-orienting its connections) whenever the WAN drops and returns.
|
|
wantedBy = mkForce [ "wan-online.target" ];
|
|
partOf = [ "wan-online.target" ];
|
|
};
|
|
|
|
ipv6-clear-default-route = {
|
|
description = "Clear IPv6 RA default route";
|
|
after = [ "wan-online.target" ];
|
|
wantedBy = [ "wan-online.target" ];
|
|
partOf = [ "wan-online.target" ];
|
|
script = ''
|
|
# Seems like we can sometimes pick up a default route somehow...
|
|
${pkgs.iproute2}/bin/ip -6 route del default via fe80::1 || true
|
|
'';
|
|
serviceConfig.Type = "oneshot";
|
|
};
|
|
};
|
|
|
|
systemd.network = {
|
|
wait-online.enable = false;
|
|
config = {
|
|
networkConfig = {
|
|
ManageForeignRoutes = false;
|
|
};
|
|
};
|
|
|
|
netdevs = mkMerge [
|
|
{
|
|
"25-wan-ifb".netdevConfig = {
|
|
Name = "wan-ifb";
|
|
Kind = "ifb";
|
|
};
|
|
"30-lan-core".netdevConfig = {
|
|
Name = "lan-core";
|
|
Kind = "macvlan";
|
|
MTUBytes = "1500";
|
|
};
|
|
}
|
|
|
|
(mkVLAN "lan-hi" vlans.hi)
|
|
(mkVLAN "lan-lo" vlans.lo)
|
|
(mkVLAN "lan-untrusted" vlans.untrusted)
|
|
];
|
|
|
|
networks =
|
|
let
|
|
mkVLANConfig = name:
|
|
let
|
|
iface = "lan-${name}";
|
|
in
|
|
{
|
|
"60-${iface}" = mkMerge [
|
|
(networkdAssignment iface assignments."${name}")
|
|
{
|
|
dns = [ "127.0.0.1" "::1" ];
|
|
domains = [ config.networking.domain ];
|
|
networkConfig.IPv6AcceptRA = mkForce false;
|
|
}
|
|
];
|
|
};
|
|
in
|
|
mkMerge [
|
|
{
|
|
"55-lan" = {
|
|
matchConfig.Name = "lan";
|
|
vlan = [ "lan-hi" "lan-lo" "lan-untrusted" "wan-tunnel" ];
|
|
macvlan = [ "lan-core" ];
|
|
networkConfig = networkd.noL3;
|
|
};
|
|
"60-lan-core" = mkMerge [
|
|
(networkdAssignment "lan-core" assignments.core)
|
|
{
|
|
matchConfig.Name = "lan-core";
|
|
networkConfig.IPv6AcceptRA = mkForce false;
|
|
}
|
|
];
|
|
|
|
"90-l2mesh-as211024" = mkMerge [
|
|
(networkdAssignment "as211024" assignments.as211024)
|
|
{
|
|
matchConfig.Name = "as211024";
|
|
networkConfig.IPv6AcceptRA = mkForce false;
|
|
routes = [
|
|
{
|
|
Destination = lib.my.c.colony.prefixes.all.v4;
|
|
Gateway = allAssignments.estuary.as211024.ipv4.address;
|
|
}
|
|
|
|
{
|
|
Destination = lib.my.c.tailscale.prefix.v4;
|
|
Gateway = allAssignments.britway.as211024.ipv4.address;
|
|
}
|
|
{
|
|
Destination = lib.my.c.tailscale.prefix.v6;
|
|
Gateway = allAssignments.britway.as211024.ipv6.address;
|
|
}
|
|
];
|
|
}
|
|
];
|
|
}
|
|
|
|
(mkVLANConfig "hi")
|
|
(mkVLANConfig "lo")
|
|
(mkVLANConfig "untrusted")
|
|
|
|
{
|
|
"60-lan-hi" = {
|
|
routes = [
|
|
{
|
|
Destination = elemAt routersPubV4 otherIndex;
|
|
Gateway = net.cidr.host (otherIndex + 1) prefixes.hi.v4;
|
|
}
|
|
];
|
|
};
|
|
}
|
|
];
|
|
};
|
|
|
|
my = {
|
|
secrets = {
|
|
files = {
|
|
"l2mesh/as211024.key" = {};
|
|
};
|
|
};
|
|
|
|
vpns = {
|
|
l2.pskFiles = {
|
|
as211024 = config.age.secrets."l2mesh/as211024.key".path;
|
|
};
|
|
};
|
|
firewall = {
|
|
trustedInterfaces = [ "lan-hi" "lan-lo" ];
|
|
udp.allowed = [ 5353 ];
|
|
tcp.allowed = [ 5353 ];
|
|
nat = {
|
|
enable = true;
|
|
externalInterface = "wan";
|
|
};
|
|
extraRules =
|
|
let
|
|
aa = allAssignments;
|
|
in
|
|
''
|
|
table inet filter {
|
|
chain input {
|
|
${lib.my.c.as211024.nftTrust}
|
|
iifname base meta l4proto { udp, tcp } th dport domain accept
|
|
iifname lan-core meta l4proto vrrp accept
|
|
}
|
|
|
|
chain routing-tcp {
|
|
ip daddr {
|
|
${aa.castle.hi.ipv4.address},
|
|
${aa.cellar.hi.ipv4.address},
|
|
${aa.palace.hi.ipv4.address}
|
|
} tcp dport ssh accept
|
|
ip6 daddr {
|
|
${aa.castle.hi.ipv6.address},
|
|
${aa.cellar.hi.ipv6.address},
|
|
${aa.palace.hi.ipv6.address}
|
|
} tcp dport ssh accept
|
|
|
|
return
|
|
}
|
|
chain routing-udp {
|
|
return
|
|
}
|
|
chain filter-routing {
|
|
tcp flags & (fin|syn|rst|ack) == syn ct state new jump routing-tcp
|
|
meta l4proto udp ct state new jump routing-udp
|
|
return
|
|
}
|
|
chain filter-untrusted {
|
|
ip daddr ${prefixes.modem.v4} reject
|
|
oifname wan accept
|
|
return
|
|
}
|
|
|
|
chain forward-early {
|
|
type filter hook forward priority -1; policy accept;
|
|
|
|
# MSS clamping to workaround IPv6 PMTUD being broken...
|
|
tcp flags syn tcp option maxseg size set rt mtu counter
|
|
|
|
# More Disney+ discrimination...
|
|
# TODO: This prefix could change (random AWS block)
|
|
ip6 daddr 2600:9000:2245::/48 drop
|
|
}
|
|
chain forward {
|
|
${lib.my.c.as211024.nftTrust}
|
|
iifname lan-untrusted jump filter-untrusted
|
|
iifname { wan, as211024, lan-untrusted } oifname { lan-hi, lan-lo } jump filter-routing
|
|
oifname as211024 accept
|
|
}
|
|
chain output { }
|
|
}
|
|
table inet nat {
|
|
chain prerouting {
|
|
ip daddr ${elemAt routersPubV4 index} meta l4proto { udp, tcp } th dport domain redirect to :5353
|
|
ip6 daddr ${assignments.as211024.ipv6.address} meta l4proto { udp, tcp } th dport domain redirect to :5353
|
|
}
|
|
chain postrouting {
|
|
oifname wan masquerade
|
|
}
|
|
}
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|