host_firewall: refresh the Spamhaus sets by rule set reload, flush and refill let unlisted addresses match

This commit is contained in:
2026-10-24 14:53:10 +00:00
parent ebb1377470
commit d5610a5b34
+23 -27
View File
@@ -106,11 +106,6 @@ def nft(*args):
return subprocess.run(["nft", *args], capture_output=True, text=True) 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(): def main():
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
drop4 = [r["cidr"] for r in spamhaus_records("drop_v4", 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) + "," return ",\n\t\t".join(str(n) for n in networks) + ","
persisted = ["# Generated by nft-host-spamhaus, do not edit.", "table inet host {"] 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")): 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" persisted.append(f"\tset {name} {{ type {kind}; flags interval; auto-merge; elements = {{\n"
f"\t\t{elements(sets[name])}\n\t}} }}") 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.append("}\n")
persisted_path = os.path.join(tmp, "host-spamhaus.nft") candidate = os.path.join(tmp, "host-spamhaus.nft")
update_path = os.path.join(tmp, "update.nft") with open(candidate, "w", encoding="utf-8") as f:
write(persisted_path, "\n".join(persisted)) f.write("\n".join(persisted))
write(update_path, "\n".join(update) + "\n") 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 previous = None
# the sets (boot, or a rule set rendered without them) the persisted if os.path.exists(PERSISTED):
# file is the only copy. previous = os.path.join(tmp, "previous.nft")
loaded = all(nft("list", "set", "inet", "host", n).returncode == 0 shutil.copyfile(PERSISTED, previous)
for n in ("spamhaus_v4", "spamhaus_v6")) shutil.copyfile(candidate, PERSISTED + ".new")
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")
os.chmod(PERSISTED + ".new", 0o644) os.chmod(PERSISTED + ".new", 0o644)
os.replace(PERSISTED + ".new", PERSISTED) 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: try: