#!/usr/bin/env python3 """Sync Technitium hubris.network A-records -> NetBird managed DNS zone. Technitium is the single authoring source; this reconciles NetBird to match.""" import json, urllib.request, urllib.parse, ssl, sys NB_API = "https://netbird.hubris.network/api" ZONE_ID = "d7gaad00qfrc73er3n1g" NB_TOKEN = open("/opt/dns-sync/netbird-token").read().strip() TECH = "http://127.0.0.1:5380/api" TECH_PW = open("/opt/technitium/admin_password.txt").read().strip() ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE def jget(url): with urllib.request.urlopen(url, context=ctx, timeout=15) as r: return json.load(r) def nb(method, path, data=None): req = urllib.request.Request(NB_API+path, method=method) req.add_header("Authorization", "Token "+NB_TOKEN) body=None if data is not None: req.add_header("Content-Type","application/json"); body=json.dumps(data).encode() with urllib.request.urlopen(req, data=body, context=ctx, timeout=15) as r: t=r.read(); return json.loads(t) if t else {} # 1. source of truth: Technitium named A-records tok = jget(f"{TECH}/user/login?user=admin&pass={urllib.parse.quote(TECH_PW)}&includeInfo=false")["token"] recs = jget(f"{TECH}/zones/records/get?token={tok}&zone=hubris.network&domain=hubris.network&listZone=true")["response"]["records"] source = { r["name"]: r["rData"]["ipAddress"] for r in recs if r.get("type")=="A" and not r["name"].startswith("*") and r["name"]!="hubris.network" } # 2. current NetBird records cur = { r["name"]: (r["id"], r["content"]) for r in nb("GET", f"/dns/zones/{ZONE_ID}/records") if r.get("type")=="A" } # 3. reconcile c=u=d=0; changes=[] for name, ip in source.items(): if name not in cur: nb("POST", f"/dns/zones/{ZONE_ID}/records", {"name":name,"type":"A","content":ip,"ttl":300}); c+=1; changes.append(f"+ {name} {ip}") elif cur[name][1] != ip: nb("DELETE", f"/dns/zones/{ZONE_ID}/records/{cur[name][0]}") nb("POST", f"/dns/zones/{ZONE_ID}/records", {"name":name,"type":"A","content":ip,"ttl":300}); u+=1; changes.append(f"~ {name} {cur[name][1]}->{ip}") for name,(rid,ip) in cur.items(): if name not in source: nb("DELETE", f"/dns/zones/{ZONE_ID}/records/{rid}"); d+=1; changes.append(f"- {name} {ip}") print(f"dns-sync: {len(source)} source records | +{c} ~{u} -{d}") for ch in changes: print(" "+ch)