dns: fixed 1.1.1.1/8.8.8.8 lockdown + scoped mDNS, esphome sibling container
- deploy.conf: DNS_RESOLVERS, always 1.1.1.1/8.8.8.8, never a WAN's own DHCP/RA-provided servers (previously whatever Wapana handed out). - ap/99-van-router-dns.conf: global resolved config (fixed DNS, Domains=~., global MulticastDNS=yes — a prerequisite for any per-link mDNS to work at all, not just an on/off toggle). - failover/60-van-wan-dns: NM dispatcher that strips each WAN's DNS/search- domain and disables its mDNS via resolvectl on every connect/lease event (NM's own ipv4/ipv6.ignore-auto-dns can't be set as a config-file default — confirmed rejected as an unknown key — so this enforces it directly instead), retried over ~5s to beat NM's own async DNS commit. Also logs what each WAN advertised, never used, to /run/van-wan-dns/. - ap/21-van-br0.network: MulticastDNS=yes, scoped to the van's own LAN only — .local/mDNS now resolves for ESPHome and other LAN devices without leaking mDNS onto Wapana/Starlink/cellular. - dns/: ZeroTier-managed DNS (zt.wrede.pvt) made reproducible — installed the official zerotier-systemd-manager package (verified against upstream checksums), additive to the above so *.zt.wrede.pvt keeps resolving over the overlay independent of WAN. - ha/esphome.container: ESPHome dashboard as a sibling Podman Quadlet to Home Assistant, same host-network/config-volume pattern. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
5d88e1b30c
commit
8efb8dba5f
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""NM dispatcher: never let a WAN's DHCP/RA-provided DNS servers get used,
|
||||
and keep mDNS scoped to the van's own LAN (never a WAN).
|
||||
|
||||
This router only resolves via the fixed servers in
|
||||
/etc/systemd/resolved.conf.d/99-van-router-dns.conf (deploy.conf's
|
||||
DNS_RESOLVERS) — never whatever a WAN happens to hand out (Wapana, a
|
||||
campsite AP, Starlink, cellular). NetworkManager's own ipv4/ipv6.ignore-auto-
|
||||
dns can't be set as a config-file connection default: NM rejects it there
|
||||
("unknown key") even though it's a real, settable per-connection property —
|
||||
so this enforces the same outcome directly against systemd-resolved instead,
|
||||
on every WAN connect/lease event, for every current and future WAN profile.
|
||||
|
||||
Also disables mDNS on the WAN link: 99-van-router-dns.conf sets the global
|
||||
MulticastDNS default to "yes" (a prerequisite for br0's own per-link
|
||||
MulticastDNS=yes to mean anything — resolved gates per-link "yes" behind the
|
||||
global default), so every link starts out mDNS-enabled unless told
|
||||
otherwise; this opts each WAN back out as it comes up.
|
||||
|
||||
Also records what DNS was advertised (but never used) to /run/van-wan-dns/,
|
||||
for reference/debugging.
|
||||
|
||||
NM dispatcher calling convention: argv = [iface, action].
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
STATE_DIR = "/run/van-wan-dns"
|
||||
|
||||
|
||||
def clear_link_dns(iface):
|
||||
subprocess.run(["resolvectl", "dns", iface, ""], check=False)
|
||||
subprocess.run(["resolvectl", "domain", iface, ""], check=False)
|
||||
subprocess.run(["resolvectl", "mdns", iface, "no"], check=False)
|
||||
|
||||
|
||||
def main():
|
||||
iface, action = sys.argv[1], sys.argv[2]
|
||||
if action not in ("up", "dhcp4-change", "dhcp6-change"):
|
||||
return
|
||||
|
||||
# Strip whatever DNS/search-domain this link just got from DHCP/RA —
|
||||
# global DNS=/Domains=~. (99-van-router-dns.conf) then wins for everything.
|
||||
# NM commits its own DNS to resolved asynchronously, shortly *after* this
|
||||
# dispatcher fires — a single clear here loses that race, so retry over a
|
||||
# few seconds (confirmed empirically: one clear immediately gets
|
||||
# clobbered, a clear a couple seconds later sticks).
|
||||
for _ in range(5):
|
||||
clear_link_dns(iface)
|
||||
time.sleep(1)
|
||||
|
||||
out = subprocess.run(["nmcli", "-t", "-f", "DHCP4.OPTION", "device", "show", iface],
|
||||
capture_output=True, text=True, check=False).stdout
|
||||
|
||||
servers = domain = None
|
||||
for line in out.splitlines():
|
||||
if ":" not in line or " = " not in line:
|
||||
continue
|
||||
_, kv = line.split(":", 1)
|
||||
key, _, val = kv.partition(" = ")
|
||||
if key == "domain_name_servers":
|
||||
servers = val.split()
|
||||
elif key == "domain_name":
|
||||
domain = val.strip() or None
|
||||
|
||||
if not servers:
|
||||
return
|
||||
|
||||
os.makedirs(STATE_DIR, exist_ok=True)
|
||||
with open(f"{STATE_DIR}/{iface}.json", "w") as f:
|
||||
json.dump({
|
||||
"iface": iface,
|
||||
"dns_advertised": servers,
|
||||
"domain_advertised": domain,
|
||||
"updated": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
||||
}, f, indent=2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user