Files
nixos-router/modules/router/firewall.nix
T
nilsandClaude Sonnet 5.5 dcef7665b4 Add MVP router flake with VM test and tmux lab
router.* schema (wan, lans, dns.forwarders) compiled to systemd-networkd,
a generated nftables ruleset with NAT, Kea DHCPv4 and Unbound, plus build-time
assertions against lock-out and interface conflicts.

tests/basic.nix is a three-node VM test (upstream/router/client) covering WAN
DHCP, LAN DHCP and DNS, NAT, WAN-deny and LAN SSH/ping. `nix run .#lab` boots
the same topology in tmux with one console pane per VM and the driver REPL.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 13:38:41 +01:00

79 lines
2.6 KiB
Nix

{ config, lib, ... }:
let
cfg = config.router;
lans = lib.attrValues cfg.lans;
set = ifaces: "{ ${lib.concatMapStringsSep ", " (i: ''"${i}"'') ifaces} }";
lanIfaces = map (l: l.interface) lans;
mgmtIfaces = map (l: l.interface) (lib.filter (l: l.allowManagement) lans);
dhcpIfaces = map (l: l.interface) (lib.filter (l: l.dhcp.enable) lans);
in
{
config = lib.mkIf cfg.enable {
# The generated ruleset replaces networking.firewall entirely.
networking.firewall.enable = false;
networking.nftables.enable = true;
networking.nftables.tables.router = {
family = "inet";
content = ''
chain input {
type filter hook input priority filter; policy drop;
iifname "lo" accept
ct state established,related accept
ct state invalid drop
# Error types and ND are required on every interface (WAN included);
# echo is only answered on LANs.
icmp type { destination-unreachable, time-exceeded, parameter-problem } accept
icmpv6 type {
destination-unreachable, packet-too-big, time-exceeded, parameter-problem,
nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert
} accept
iifname ${set lanIfaces} icmp type echo-request accept
iifname ${set lanIfaces} icmpv6 type echo-request accept
iifname "${cfg.wan.interface}" udp dport 68 accept
iifname ${set lanIfaces} udp dport 53 accept
iifname ${set lanIfaces} tcp dport 53 accept
${lib.optionalString (dhcpIfaces != [ ]) ''iifname ${set dhcpIfaces} udp dport 67 accept''}
${lib.optionalString (mgmtIfaces != [ ]) ''iifname ${set mgmtIfaces} tcp dport 22 accept''}
}
chain forward {
type filter hook forward priority filter; policy drop;
ct state established,related accept
ct state invalid drop
icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem } accept
iifname ${set lanIfaces} oifname "${cfg.wan.interface}" accept
}
'';
};
networking.nftables.tables.router-nat = {
family = "ip";
content = ''
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "${cfg.wan.interface}" masquerade
}
'';
};
services.openssh = {
enable = true;
openFirewall = false;
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PermitRootLogin = "prohibit-password";
};
};
};
}