diff --git a/machines/ramune/configuration.nix b/machines/ramune/configuration.nix index 17cf741..e48da3c 100644 --- a/machines/ramune/configuration.nix +++ b/machines/ramune/configuration.nix @@ -39,9 +39,12 @@ { macAddress = "c4:f1:74:51:4c:f2"; ipAddress = "10.0.0.124"; } # eero router { macAddress = "1c:1d:d3:de:4b:06"; ipAddress = "10.0.0.35"; } # irnbru ]; - nftables.blockForward = [ - "ec:e5:12:1d:23:40" # tado - ]; + nftables = { + blocklist.enable = true; + blockForward = [ + "ec:e5:12:1d:23:40" # tado + ]; + }; }; automation = { enable = true; diff --git a/modules/router/default.nix b/modules/router/default.nix index d82f353..7b60701 100644 --- a/modules/router/default.nix +++ b/modules/router/default.nix @@ -18,6 +18,7 @@ with lib; { ./network.nix ./timeserver.nix ./nftables.nix + ./nftables-blocklist.nix ./upnp.nix ./kernel.nix ]; diff --git a/modules/router/nftables-blocklist.nix b/modules/router/nftables-blocklist.nix new file mode 100644 index 0000000..0626dac --- /dev/null +++ b/modules/router/nftables-blocklist.nix @@ -0,0 +1,103 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.modules.router; + blcfg = cfg.nftables.blocklist; + setV4 = "blocklist_v4"; + setV6 = "blocklist_v6"; + + updateScript = let + v4Urls = concatStringsSep " " (map escapeShellArg blcfg.urls); + v6Urls = concatStringsSep " " (map escapeShellArg blcfg.urlsV6); + in pkgs.writeShellApplication { + name = "nftables-blocklist-update"; + runtimeInputs = with pkgs; [ curl nftables gawk coreutils ]; + text = '' + dir="$STATE_DIRECTORY" + + nft list tables || exit 0 + + fetch() { + local out="$1"; shift + local tmp="$dir/.tmp" ok=false + : > "$tmp" + for url in "$@"; do + if curl -sfL --max-time 30 --retry 2 "$url" >> "$tmp"; then + ok=true + fi + done + if "$ok"; then mv "$tmp" "$out"; else rm -f "$tmp"; fi + } + + fetch "$dir/v4.json" ${v4Urls} + fetch "$dir/v6.json" ${v6Urls} + + touch "$dir/v4.json" "$dir/v6.json" + awk -F'"' ' + BEGINFILE { elems = ""; sep = ""; n = 0 } + $2 == "cidr" { elems = elems sep $4; sep = ", "; n++ } + ENDFILE { + print "flush set inet filter " SET + if (n) print "add element inet filter " SET " { " elems " }" + printf "%d %s entries\n", n, SET > "/dev/stderr" + } + ' SET=${setV4} "$dir/v4.json" SET=${setV6} "$dir/v6.json" \ + | nft -f - + ''; + }; +in { + options.modules.router.nftables.blocklist = { + enable = mkOption { + default = false; + description = "Whether to enable IP blocklist using Spamhaus DROP"; + type = types.bool; + }; + + urls = mkOption { + default = [ + "https://www.spamhaus.org/drop/drop_v4.json" + ]; + description = "URLs to fetch IPv4 blocklists from (NDJSON with cidr field)"; + type = types.listOf types.str; + }; + + urlsV6 = mkOption { + default = [ + "https://www.spamhaus.org/drop/drop_v6.json" + ]; + description = "URLs to fetch IPv6 blocklists from (NDJSON with cidr field)"; + type = types.listOf types.str; + }; + }; + + config = mkIf (cfg.nftables.enable && blcfg.enable) { + systemd.services.nftables-blocklist = { + description = "Update nftables IP blocklist"; + after = [ "nftables.service" "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + StateDirectory = "nftables-blocklist"; + ExecStart = getExe updateScript; + }; + }; + + systemd.timers.nftables-blocklist = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "*-*-* 00/12:00:00"; + RandomizedDelaySec = "1h"; + Persistent = true; + }; + }; + + # Re-populate blocklist sets after nftables starts or reloads (flushRuleset clears them) + systemd.services.nftables.serviceConfig = let + trigger = "${pkgs.systemd}/bin/systemctl start --no-block nftables-blocklist.service"; + in { + ExecStartPost = [ trigger ]; + ExecReload = [ trigger ]; + }; + }; +} diff --git a/modules/router/nftables.nix b/modules/router/nftables.nix index 1f4f3ac..71445fa 100644 --- a/modules/router/nftables.nix +++ b/modules/router/nftables.nix @@ -3,6 +3,9 @@ with lib; let cfg = config.modules.router; + blcfg = cfg.nftables.blocklist; + blSetV4 = "blocklist_v4"; + blSetV6 = "blocklist_v6"; extern = cfg.interfaces.external; intern = cfg.interfaces.internal; @@ -75,7 +78,25 @@ in { udp dport {${udpPorts}} ct state new meter udp6-conncount { ip6 saddr . udp dport ct count over 150 } counter drop udp dport {${udpPorts}} ct state new accept ''; + blocklistSets = optionalString blcfg.enable '' + set ${blSetV4} { + type ipv4_addr + flags interval + auto-merge + } + set ${blSetV6} { + type ipv6_addr + flags interval + auto-merge + } + ''; + blocklistRules = optionalString blcfg.enable '' + ip saddr @${blSetV4} counter drop + ip6 saddr @${blSetV6} counter drop + ''; in '' + ${blocklistSets} + chain prerouting { type nat hook prerouting priority dstnat; policy accept; ${capturePortsRules} @@ -93,6 +114,8 @@ in { iifname { ${concatIfnames trustedInterfaces} } accept + ${blocklistRules} + tcp flags & (fin|syn|rst|ack) != syn ct state new counter drop tcp flags & (fin|syn|rst|psh|ack|urg) == fin|syn|rst|psh|ack|urg counter drop tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 counter drop @@ -195,5 +218,6 @@ in { ''; }; }; + }; }