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
@@ -10,3 +10,7 @@ RequiredForOnline=no
|
|||||||
[Network]
|
[Network]
|
||||||
Address=10.42.0.1/24
|
Address=10.42.0.1/24
|
||||||
ConfigureWithoutCarrier=yes
|
ConfigureWithoutCarrier=yes
|
||||||
|
# Resolve .local (mDNS) for devices on the van's own LAN (ESPHome nodes,
|
||||||
|
# etc.) — scoped to br0 only, not the WAN links, since mDNS is link-local
|
||||||
|
# and that's the only interface those devices are actually on.
|
||||||
|
MulticastDNS=yes
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Fixed upstream resolvers for this router — see deploy.conf's DNS_RESOLVERS.
|
||||||
|
# Never the WAN-provided ones: the failover/60-van-wan-dns NM dispatcher
|
||||||
|
# strips each WAN's DHCP/RA-provided DNS from resolved as soon as it appears
|
||||||
|
# (NM's own ipv4/ipv6.ignore-auto-dns can't be set as a config-file default —
|
||||||
|
# it's rejected as an unknown key there, even though it's a real per-
|
||||||
|
# connection property), so these are the only unicast resolvers in play,
|
||||||
|
# regardless of whether the WAN is Wapana, Starlink, or cellular.
|
||||||
|
# Domains=~. makes them the default route for every query (there being no
|
||||||
|
# competing per-link DNS to prioritize over them in the first place).
|
||||||
|
#
|
||||||
|
# This does NOT affect .local (mDNS) resolution — that's handled separately,
|
||||||
|
# per-link, only on br0 (see 21-van-br0.network's MulticastDNS=yes), so
|
||||||
|
# ESPHome/other mDNS devices on the van's own LAN still resolve.
|
||||||
|
#
|
||||||
|
# ZeroTier's own DNS (zt.wrede.pvt, via zerotier-systemd-manager) is a
|
||||||
|
# separate, more-specific routing domain on the ztuga7c2kh link and is
|
||||||
|
# unaffected by this — resolved always prefers a domain-specific route over
|
||||||
|
# the Domains=~. fallback.
|
||||||
|
[Resolve]
|
||||||
|
DNS=@DNS_RESOLVERS@
|
||||||
|
Domains=~.
|
||||||
|
# Global default: resolved gates per-link MulticastDNS=yes settings behind
|
||||||
|
# this — a link can't enable mDNS on its own if the global default is "no"
|
||||||
|
# (confirmed: "Setting mDNS support level yes for X, but the global support
|
||||||
|
# level is no"). br0 (21-van-br0.network) opts in; every WAN link is opted
|
||||||
|
# back out explicitly by failover/60-van-wan-dns so mDNS stays scoped to the
|
||||||
|
# van's own LAN and never leaks onto Wapana/Starlink/cellular.
|
||||||
|
MulticastDNS=yes
|
||||||
@@ -24,3 +24,9 @@ STARLINK_IFACE=enxd8ec5eeb3512
|
|||||||
|
|
||||||
# Cellular modem USB vendor ID (Quectel EC25-AF)
|
# Cellular modem USB vendor ID (Quectel EC25-AF)
|
||||||
MODEM_USB_VENDOR=2c7c
|
MODEM_USB_VENDOR=2c7c
|
||||||
|
|
||||||
|
# Fixed upstream DNS resolvers — this router always uses these, never a WAN's
|
||||||
|
# own DHCP/RA-provided servers (NetworkManager is told to ignore those
|
||||||
|
# entirely; see ap/van-wan-dns.conf + ap/99-van-router-dns.conf). Keeps
|
||||||
|
# resolution identical on Wapana, Starlink, or cellular.
|
||||||
|
DNS_RESOLVERS="1.1.1.1 8.8.8.8"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ render() { # render <file> -> stdout, with @TOKEN@ placeholders substituted
|
|||||||
-e "s|@LAN_USB_IFACE@|$LAN_USB_IFACE|g" \
|
-e "s|@LAN_USB_IFACE@|$LAN_USB_IFACE|g" \
|
||||||
-e "s|@STARLINK_IFACE@|$STARLINK_IFACE|g" \
|
-e "s|@STARLINK_IFACE@|$STARLINK_IFACE|g" \
|
||||||
-e "s|@MODEM_USB_VENDOR@|$MODEM_USB_VENDOR|g" \
|
-e "s|@MODEM_USB_VENDOR@|$MODEM_USB_VENDOR|g" \
|
||||||
|
-e "s|@DNS_RESOLVERS@|$DNS_RESOLVERS|g" \
|
||||||
"$1"
|
"$1"
|
||||||
}
|
}
|
||||||
install_rendered() { # install_rendered <src> <dst> [mode]
|
install_rendered() { # install_rendered <src> <dst> [mode]
|
||||||
@@ -104,6 +105,26 @@ install -D -m0644 ap/regdomain.service /etc/systemd/system/regdomain.servic
|
|||||||
install -D -m0644 ap/rtw89.conf /etc/modprobe.d/rtw89.conf
|
install -D -m0644 ap/rtw89.conf /etc/modprobe.d/rtw89.conf
|
||||||
install -D -m0644 ap/99-van-router.conf /etc/sysctl.d/99-van-router.conf
|
install -D -m0644 ap/99-van-router.conf /etc/sysctl.d/99-van-router.conf
|
||||||
|
|
||||||
|
echo "== dns =="
|
||||||
|
# Fixed upstream resolvers (deploy.conf's DNS_RESOLVERS), never a WAN's own
|
||||||
|
# DHCP/RA-provided DNS — see the files themselves for the full rationale.
|
||||||
|
# (NM's ipv4/ipv6.ignore-auto-dns can't be set as a config-file connection
|
||||||
|
# default — NM rejects it there — so 60-van-wan-dns enforces this directly
|
||||||
|
# against resolved instead, on every WAN connect/lease event.)
|
||||||
|
# .local (mDNS) is handled separately, scoped to br0 (see 21-van-br0.network
|
||||||
|
# above). ZeroTier-managed DNS (zt.wrede.pvt) is a separate, additive path.
|
||||||
|
install_rendered ap/99-van-router-dns.conf /etc/systemd/resolved.conf.d/99-van-router-dns.conf
|
||||||
|
install -D -m0755 failover/60-van-wan-dns /etc/NetworkManager/dispatcher.d/60-van-wan-dns
|
||||||
|
# ZeroTier-managed DNS for zt.wrede.pvt (see README's "ZeroTier managed DNS").
|
||||||
|
# allowDNS on the network + the search-domain drop-in are repo-tracked so a
|
||||||
|
# reimage doesn't need the manual `zerotier-cli set ... allowDNS=1` step
|
||||||
|
# remembered by hand; the manager binary itself is a hand-installed .deb
|
||||||
|
# (not in apt) — see https://github.com/zerotier/zerotier-systemd-manager/releases.
|
||||||
|
install -D -m0600 dns/zt-network.local.conf /var/lib/zerotier-one/networks.d/d3ecf5726d041b2a.local.conf
|
||||||
|
install -D -m0644 dns/zt-search.conf /etc/systemd/network/99-ztuga7c2kh.network.d/search.conf
|
||||||
|
dpkg -s zerotier-systemd-manager >/dev/null 2>&1 \
|
||||||
|
|| warn "zerotier-systemd-manager not installed — zt.wrede.pvt won't resolve. Install the arm64 .deb from https://github.com/zerotier/zerotier-systemd-manager/releases"
|
||||||
|
|
||||||
echo "== failover =="
|
echo "== failover =="
|
||||||
install -D -m0755 failover/van-failover /usr/local/sbin/van-failover
|
install -D -m0755 failover/van-failover /usr/local/sbin/van-failover
|
||||||
install -D -m0644 failover/config.json /etc/van-failover/config.json
|
install -D -m0644 failover/config.json /etc/van-failover/config.json
|
||||||
@@ -177,6 +198,10 @@ echo "== home assistant =="
|
|||||||
# deploy never bounces HA — after editing the .container, restart it manually.
|
# deploy never bounces HA — after editing the .container, restart it manually.
|
||||||
install -D -m0644 ha/homeassistant.container /etc/containers/systemd/homeassistant.container
|
install -D -m0644 ha/homeassistant.container /etc/containers/systemd/homeassistant.container
|
||||||
install -d -m0755 /srv/homeassistant
|
install -d -m0755 /srv/homeassistant
|
||||||
|
# ESPHome dashboard (sibling container, same rationale as HA above — no
|
||||||
|
# Supervisor/add-on store here).
|
||||||
|
install -D -m0644 ha/esphome.container /etc/containers/systemd/esphome.container
|
||||||
|
install -d -m0755 /srv/esphome
|
||||||
|
|
||||||
echo "== hardware watchdog =="
|
echo "== hardware watchdog =="
|
||||||
install -D -m0644 power/10-vanlink-watchdog.conf /etc/systemd/system.conf.d/10-vanlink-watchdog.conf
|
install -D -m0644 power/10-vanlink-watchdog.conf /etc/systemd/system.conf.d/10-vanlink-watchdog.conf
|
||||||
@@ -187,6 +212,11 @@ systemctl daemon-reload
|
|||||||
# Re-exec PID1 so the system.conf.d watchdog drop-in takes effect (daemon-reload
|
# Re-exec PID1 so the system.conf.d watchdog drop-in takes effect (daemon-reload
|
||||||
# alone does NOT re-arm RuntimeWatchdogSec). Safe online.
|
# alone does NOT re-arm RuntimeWatchdogSec). Safe online.
|
||||||
systemctl daemon-reexec
|
systemctl daemon-reexec
|
||||||
|
# Safe online: picks up 99-van-router-dns.conf immediately. NM's
|
||||||
|
# ignore-auto-dns only takes effect on a connection's next activation though —
|
||||||
|
# an already-up WAN keeps its currently-applied DNS until it reconnects (or
|
||||||
|
# reboot), deliberately not forced here (reconnecting a WAN flaps it).
|
||||||
|
systemctl restart systemd-resolved
|
||||||
# networkd here owns only the AP radios + bridge + wired LAN port (no real uplink);
|
# networkd here owns only the AP radios + bridge + wired LAN port (no real uplink);
|
||||||
# its wait-online would just stall network-online.target. NM-wait-online covers WANs.
|
# its wait-online would just stall network-online.target. NM-wait-online covers WANs.
|
||||||
systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true
|
systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true
|
||||||
@@ -195,6 +225,7 @@ systemctl enable regdomain.service hostapd hostapd-2g van-ap-dnsmasq nftables sy
|
|||||||
# bluetooth: host BlueZ serves the onboard hci0 to the HA container over D-Bus
|
# bluetooth: host BlueZ serves the onboard hci0 to the HA container over D-Bus
|
||||||
systemctl enable --now bluetooth >/dev/null 2>&1 || true
|
systemctl enable --now bluetooth >/dev/null 2>&1 || true
|
||||||
systemctl start homeassistant || warn "homeassistant failed to start (podman/quadlet — check journalctl -u homeassistant)"
|
systemctl start homeassistant || warn "homeassistant failed to start (podman/quadlet — check journalctl -u homeassistant)"
|
||||||
|
systemctl start esphome || warn "esphome failed to start (podman/quadlet — check journalctl -u esphome)"
|
||||||
systemctl restart van-thermal
|
systemctl restart van-thermal
|
||||||
systemctl restart van-gps-owntracks
|
systemctl restart van-gps-owntracks
|
||||||
# Pick up unmanaged-devices changes so NM releases/keeps the right interfaces.
|
# Pick up unmanaged-devices changes so NM releases/keeps the right interfaces.
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
allowManaged=1
|
||||||
|
allowGlobal=0
|
||||||
|
allowDefault=0
|
||||||
|
allowDNS=1
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# vim: ft=systemd
|
||||||
|
# Adds zt.wrede.pvt as a search domain (bare-hostname completion, e.g.
|
||||||
|
# `ssh rosepark` -> rosepark.zt.wrede.pvt) alongside the routing-only
|
||||||
|
# ~zt.wrede.pvt entry that zerotier-systemd-manager writes into
|
||||||
|
# 99-ztuga7c2kh.network itself (that file is fully manager-owned — don't
|
||||||
|
# edit it directly, this .network.d drop-in layers on top instead).
|
||||||
|
[Network]
|
||||||
|
Domains=zt.wrede.pvt
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# ESPHome dashboard as a Podman Quadlet, same pattern as homeassistant.container
|
||||||
|
# (this isn't Home Assistant Supervised, so there's no add-on store — a sibling
|
||||||
|
# container is the equivalent). Installed by deploy.sh to /etc/containers/systemd/;
|
||||||
|
# systemd generates esphome.service from it.
|
||||||
|
#
|
||||||
|
# Host networking: the dashboard binds :6052 directly (http://10.42.0.1:6052),
|
||||||
|
# and ESPHome's mDNS-based OTA discovery/flashing of already-provisioned nodes
|
||||||
|
# needs to see the LAN as the host does — a bridged network would need explicit
|
||||||
|
# port/mDNS forwarding for the same result.
|
||||||
|
[Unit]
|
||||||
|
Description=ESPHome dashboard (Podman container)
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
Image=ghcr.io/esphome/esphome:stable
|
||||||
|
ContainerName=esphome
|
||||||
|
Network=host
|
||||||
|
Volume=/srv/esphome:/config
|
||||||
|
Environment=TZ=America/Toronto
|
||||||
|
# Initial flash of a new device needs USB serial access; OTA re-flashes of an
|
||||||
|
# already-provisioned node don't. Uncomment and set the real path to flash
|
||||||
|
# over USB (find it with `ls /dev/ttyUSB* /dev/ttyACM*` after plugging the
|
||||||
|
# device in):
|
||||||
|
#Volume=/dev/ttyUSB0:/dev/ttyUSB0
|
||||||
|
#AddCapability=SYS_ADMIN
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=always
|
||||||
|
# First start pulls the image; PlatformIO also downloads toolchains on a
|
||||||
|
# node's first compile — both want a working WAN, not just a fast one.
|
||||||
|
TimeoutStartSec=900
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user