Files
hse-2026/materials/07-naming-lookup/seminar/dns/server.py
T
2026-10-21 23:03:28 +03:00

70 lines
3.0 KiB
Python

"""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()