"""Run BIND as an authoritative server or a separate caching resolver.""" import json import os from pathlib import Path import socket import sys import time ROOT = Path("/tmp/dns-lab") # A fixed key is only used by rndc on loopback inside these disposable lab containers. KEY = 'key "lab-control" { algorithm hmac-sha256; secret "c2VtaW5hci03LWxvY2FsLWRucy1jb250cm9sLWtleSE="; };' def write_zone(address, ttl): old = json.loads((ROOT / "state.json").read_text()) if (ROOT / "state.json").exists() else {} serial = max(int(time.time()), old.get("serial", 0) + 1) (ROOT / "lab.test.zone").write_text(f"""$TTL {ttl} @ IN SOA ns.lab.test. hostmaster.lab.test. ({serial} 60 60 3600 {ttl}) IN NS ns.lab.test. ns IN A {socket.gethostbyname(socket.gethostname())} orders IN A {address} www IN CNAME orders.lab.test. """) (ROOT / "state.json").write_text(json.dumps({"address": address, "ttl": ttl, "serial": serial})) def main(): role = sys.argv[1] ROOT.mkdir(parents=True, exist_ok=True) control = KEY + '\ncontrols { inet 127.0.0.1 port 953 allow { 127.0.0.1; } keys { "lab-control"; }; };\n' (ROOT / "rndc.conf").write_text(KEY + '\noptions { default-server 127.0.0.1; default-key "lab-control"; };\n') common = f"""directory "{ROOT}"; listen-on {{ any; }}; listen-on-v6 {{ none; }}; allow-query {{ any; }}; minimal-responses no; dnssec-validation no; empty-zones-enable no; pid-file "{ROOT}/named.pid"; session-keyfile "{ROOT}/session.key"; """ if role == "auth": write_zone("192.0.2.10", 60) options = "recursion no;" zones = f'zone "lab.test" {{ type primary; file "{ROOT}/lab.test.zone"; }};' elif role in ("root", "tld"): address = socket.gethostbyname(socket.gethostname()) zone, ns = (".", "ns.root.") if role == "root" else ("test", "ns.test.") child, child_ns, child_host = ("test.", "ns.test.", "tld-dns") if role == "root" else ("lab.test.", "ns.lab.test.", "auth") child_address = socket.gethostbyname(child_host) records = f"$TTL 60\n@ IN SOA {ns} hostmaster.{ns} (1 60 60 3600 60)\n@ IN NS {ns}\n{ns} IN A {address}\n{child} IN NS {child_ns}\n{child_ns} IN A {child_address}\n" (ROOT / "hierarchy.zone").write_text(records) options = "recursion no;" zones = f'zone "{zone}" {{ type primary; file "{ROOT}/hierarchy.zone"; }};' elif role == "resolver": address = socket.gethostbyname("auth") options = """recursion yes; allow-recursion { any; }; allow-query-cache { any; }; prefetch 0; stale-answer-enable no; stale-cache-enable no; servfail-ttl 0;""" zones = "\n".join(f'zone "{name}" {{ type forward; forward only; forwarders {{ {address}; }}; }};' for name in ("lab.test", "broken.test")) else: raise ValueError("Unknown DNS role") config = control + f"options {{ {common}\n{options} }};\n" + zones (ROOT / "named.conf").write_text(config) os.execvp("named", ["named", "-g", "-c", str(ROOT / "named.conf"), "-u", "root", "-n", "1"]) if __name__ == "__main__": main()