{ config, lib, pkgs, ... }: let # --- Blocklist sources --- # Add or remove URLs here. Each must be a plain list of IPs or CIDRs (one per line). # Comments (#) and blank lines are stripped automatically. blocklistSources = [ # DShield top attacking subnets (SANS ISC) — tab-separated: IP / mask / CIDR prefix "https://feeds.dshield.org/block.txt" # blocklist.de — scanners, bots, brute-force, scrapers "https://lists.blocklist.de/lists/all.txt" # GreenSnow — bots and port scanners "https://blocklist.greensnow.co/greensnow.txt" # Spamhaus DROP — hijacked netblocks used by cybercrime operations "https://www.spamhaus.org/drop/drop.txt" # FireHOL level2 — 48-hour attack tracker, pre-merged CIDRs (good all-rounder) "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level2.netset" ]; # nftables table and set names — must match your existing nftables config nftTable = "inet ip_drop"; nftSet = "blocked-ip4"; # How often to refresh (systemd calendar format) refreshInterval = "*:0/4"; # every 4 hours # --- The updater script --- # Written as a pkgs.writeShellApplication so nix handles the PATH / dependencies. updaterScript = pkgs.writeShellApplication { name = "nft-blocklist-update"; runtimeInputs = with pkgs; [ curl gawk iproute2 # provides 'ip' for basic sanity; not strictly needed but good to have nftables # provides 'nft' coreutils ]; text = '' set -euo pipefail NFT_TABLE="${nftTable}" NFT_SET="${nftSet}" TMPFILE=$(mktemp /tmp/nft-blocklist-XXXXXX.txt) trap 'rm -f "$TMPFILE"' EXIT echo "[blocklist] Fetching sources..." # --- Per-source fetch + normalise --- # Each source may use a different format; we normalise to bare CIDRs. fetch_and_normalise() { local url="$1" curl --silent --fail --max-time 30 --retry 3 "$url" | \ awk ' # Skip blank lines and comment lines /^[[:space:]]*$/ { next } /^[[:space:]]*#/ { next } # DShield format: "IPnetmaskprefix ..." — convert to CIDR /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[[:space:]]+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[[:space:]]+[0-9]+/ { print $1 "/" $3 next } # Plain CIDR (e.g. 1.2.3.0/24) — pass through /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\/[0-9]+/ { print $1 next } # Plain host IP (no prefix) — treat as /32 /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ { print $1 "/32" next } ' } # Fetch all sources into the temp file, skip on individual failure ${lib.concatMapStringsSep "\n" (url: '' echo "[blocklist] Fetching: ${url}" fetch_and_normalise "${url}" >> "$TMPFILE" || \ echo "[blocklist] WARNING: failed to fetch ${url}, skipping" >&2 '') blocklistSources} # Deduplicate lines (nft auto-merge handles overlapping CIDRs at load time) sort -u "$TMPFILE" -o "$TMPFILE" TOTAL=$(wc -l < "$TMPFILE") echo "[blocklist] Total entries after dedup: $TOTAL" if [ "$TOTAL" -eq 0 ]; then echo "[blocklist] ERROR: no entries fetched — aborting to avoid flushing the set" >&2 exit 1 fi # Build the nft element string: "1.2.3.0/24, 5.6.7.8/32, ..." ELEMENTS=$(paste -sd ',' "$TMPFILE") echo "[blocklist] Loading into nft set $NFT_TABLE / $NFT_SET ..." # Atomic-ish update: flush then reload inside a single nft transaction nft -f - <