From d5610a5b34a7524bd9d44a369f032a188f5c92d6 Mon Sep 17 00:00:00 2001 From: Ruakij Date: Sat, 24 Oct 2026 14:53:10 +0000 Subject: [PATCH] host_firewall: refresh the Spamhaus sets by rule set reload, flush and refill let unlisted addresses match --- host_firewall/files/nft-host-spamhaus | 50 ++++++++++++--------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/host_firewall/files/nft-host-spamhaus b/host_firewall/files/nft-host-spamhaus index e5b5efb..407b8ff 100644 --- a/host_firewall/files/nft-host-spamhaus +++ b/host_firewall/files/nft-host-spamhaus @@ -106,11 +106,6 @@ def nft(*args): return subprocess.run(["nft", *args], capture_output=True, text=True) -def write(path, text): - with open(path, "w", encoding="utf-8") as f: - f.write(text) - - def main(): with tempfile.TemporaryDirectory() as tmp: drop4 = [r["cidr"] for r in spamhaus_records("drop_v4", tmp)] @@ -131,36 +126,37 @@ def main(): return ",\n\t\t".join(str(n) for n in networks) + "," persisted = ["# Generated by nft-host-spamhaus, do not edit.", "table inet host {"] - update = [] for name, kind in (("spamhaus_v4", "ipv4_addr"), ("spamhaus_v6", "ipv6_addr")): persisted.append(f"\tset {name} {{ type {kind}; flags interval; auto-merge; elements = {{\n" f"\t\t{elements(sets[name])}\n\t}} }}") - update.append(f"flush set inet host {name}\n" - f"add element inet host {name} {{\n\t{elements(sets[name])}\n}}") persisted.append("}\n") - persisted_path = os.path.join(tmp, "host-spamhaus.nft") - update_path = os.path.join(tmp, "update.nft") - write(persisted_path, "\n".join(persisted)) - write(update_path, "\n".join(update) + "\n") + candidate = os.path.join(tmp, "host-spamhaus.nft") + with open(candidate, "w", encoding="utf-8") as f: + f.write("\n".join(persisted)) + checked = nft("-c", "-f", candidate) + if checked.returncode != 0: + fail(f"nft rejects the sets: {checked.stderr.strip()}") - # The running table gets the new elements in one transaction; without - # the sets (boot, or a rule set rendered without them) the persisted - # file is the only copy. - loaded = all(nft("list", "set", "inet", "host", n).returncode == 0 - for n in ("spamhaus_v4", "spamhaus_v6")) - for path in (persisted_path,) + ((update_path,) if loaded else ()): - checked = nft("-c", "-f", path) - if checked.returncode != 0: - fail(f"nft rejects {os.path.basename(path)}: {checked.stderr.strip()}") - if loaded: - applied = nft("-f", update_path) - if applied.returncode != 0: - fail(f"loading the sets failed: {applied.stderr.strip()}") - shutil.copyfile(persisted_path, PERSISTED + ".new") + previous = None + if os.path.exists(PERSISTED): + previous = os.path.join(tmp, "previous.nft") + shutil.copyfile(PERSISTED, previous) + shutil.copyfile(candidate, PERSISTED + ".new") os.chmod(PERSISTED + ".new", 0o644) os.replace(PERSISTED + ".new", PERSISTED) - print("loaded into the running table" if loaded else "persisted only") + + # A reload replaces the whole table, sets included, in one transaction. + # Flushing and refilling a running interval set instead lets packets of + # unlisted addresses match while the transaction commits. + active = subprocess.run(["systemctl", "-q", "is-active", "nft-host.service"]).returncode == 0 + if active: + reloaded = subprocess.run(["systemctl", "reload", "nft-host.service"], capture_output=True, text=True) + if reloaded.returncode != 0: + if previous: + shutil.copyfile(previous, PERSISTED) + fail(f"reloading the rule set failed, kept the previous sets: {reloaded.stderr.strip()}") + print("reloaded the rule set" if active else "persisted only") try: