#!/usr/bin/env python3 # # Regenerate asset/nginx/blackbeard-deny.conf: the networks the public vhost # refuses, as an nginx `geo` map from the prefixes each ASN announces. # # ./script/deny-networks.py > asset/nginx/blackbeard-deny.conf # # Then review the diff and install it with `infra-setup.sh --role edge`. The # output is committed rather than fetched at install time so that what the edge # refuses is something a reviewer has read, and so a RIPEstat outage cannot turn # an install into an empty list. # # Hosting networks only. AS35540 (OVH Telecom) is left out on purpose: it is # OVH's residential broadband, and the point is headless pollers, not people. # See blackbeard/observer#24 for the log analysis that justified each entry. import datetime import ipaddress import json import sys import urllib.request ASNS = { 24940: "Hetzner Online (dedicated)", 213230: "Hetzner Cloud", 212317: "Hetzner Cloud (US, Singapore)", 16276: "OVH SAS (hosting)", } SOURCE = "https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS{}" def announced(asn): with urllib.request.urlopen(SOURCE.format(asn), timeout=60) as r: body = json.load(r) if body.get("status") != "ok": # A failed query must not produce a file that quietly stops denying a # provider — the diff would show a deletion nobody asked for. sys.exit(f"RIPEstat answered {body.get('status')!r} for AS{asn}") nets = [ipaddress.ip_network(p["prefix"]) for p in body["data"]["prefixes"]] if not nets: sys.exit(f"AS{asn} announces nothing; refusing to write an empty entry") # Collapsing removes duplicates and more-specifics; nginx's geo warns on a # duplicate network and the file is smaller for it. v4 = ipaddress.collapse_addresses(n for n in nets if n.version == 4) v6 = ipaddress.collapse_addresses(n for n in nets if n.version == 6) return list(v4) + list(v6) def main(): today = datetime.date.today().isoformat() out = [ "# GENERATED by script/deny-networks.py — do not edit by hand; regenerate.", f"# Announced prefixes per RIPEstat, {today}.", "#", "# Installed to /etc/nginx/conf.d/blackbeard-deny.conf. `$blackbeard_denied`", "# is 1 for a client inside any network below; the public vhost refuses it.", "# `geo` reads $remote_addr, which conf.d/proxy-protocol.conf has already", "# restored to the client's address — without that every request would come", "# from 127.0.0.1 and nothing would ever match.", "", "geo $blackbeard_denied {", " default 0;", ] for asn, name in ASNS.items(): nets = announced(asn) out.append("") out.append(f" # AS{asn} {name}: {len(nets)} networks") out.extend(f" {n} 1;" for n in nets) out.append("}") print("\n".join(out)) if __name__ == "__main__": main()