From a7819edbc276b53d2825422b940cbc4623490f4c Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Mon, 6 Jul 2026 09:38:53 -0400 Subject: [PATCH] ha: bridge the Home Assistant VM onto br0 as a first-class LAN device The VM lived on libvirt's default NAT net (192.168.122.50) behind a /etc/libvirt/hooks/network hook that inserted FORWARD accepts above libvirt's REJECT. libvirtd restarts re-inserted its chains on top without firing the hook, silently cutting clients off from HA. Now the VM's NIC (virtio, was e1000) bridges into br0: pinned lease 10.42.0.50 / name 'homeassistant' via van-ap dnsmasq, mDNS discovery works, and no firewall hook can rot. Legacy http://10.42.0.1:8123 kept working via DNAT + same-subnet hairpin masquerade. Hook, libvirt DHCP reservation, and iptables rules removed; domain XML in ha/. Co-Authored-By: Claude Fable 5 --- README.md | 24 ++++++++++++ ap/nftables.conf | 21 +++++----- ap/van-ap-dnsmasq.conf | 3 ++ ha/ha_van.xml | 87 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 12 deletions(-) create mode 100644 ha/ha_van.xml diff --git a/README.md b/README.md index d4bc19b..e94551f 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ This directory is the source of truth. The live system files live under `/etc`, - Cockpit: `https://192.168.10.251:9090` (or `.27`, or ZeroTier). Log in with a Unix account; enable *Administrative access* for action buttons. - Regulatory domain **CA** (unlocks 5GHz ch149–161 @30dBm, no DFS). - ZeroTier network `d3ecf5726d041b2a`, pushed DNS domain `wrede.pvt` via `192.168.196.115` + `192.168.10.5`. +- Home Assistant (HAOS in libvirt VM `ha_van`) is bridged into `br0` at **`10.42.0.50:8123`** (pinned DHCP lease, name `homeassistant`); legacy URL `http://10.42.0.1:8123` still works via DNAT. --- @@ -127,6 +128,14 @@ This directory is the source of truth. The live system files live under `/etc`, The `hbc` binary itself (venv at `~/venvs/hbd`, symlink `~/bin/hbc`) is installed once via the heartbeat project's own installer — see §4. `deploy.sh` only starts the service once it exists. +### `ha/` — Home Assistant VM +| file | → installs to | purpose | +|---|---|---| +| `ha_van.xml` | *(reference only — `virsh define ha/ha_van.xml` to restore)* | libvirt domain: HAOS VM, virtio NIC bridged onto `br0` (MAC `52:54:00:ad:0a:01`). Disk image lives outside the repo. | + +Not touched by `deploy.sh` — the VM's LAN address/name come from `ap/van-ap-dnsmasq.conf` +(`dhcp-host` → `10.42.0.50`, `homeassistant`) and the legacy-URL DNAT from `ap/nftables.conf`. + --- ## 3. Components & how to operate @@ -185,6 +194,21 @@ heartbeat project's own installer — see §4. `deploy.sh` only starts the servi - Read-only status works as any user; Prefer/Up/Down/Restart need *Administrative access* (polkit). - **Prefer** sets the manual WAN preference (see *Manual preference* above); **Up/Down** connect/disconnect the NM device; **Restart** restarts hostapd. +### Home Assistant VM (`ha_van`) +- HAOS runs as a libvirt KVM VM whose NIC is **bridged into `br0`** — it is a first-class LAN + device, not NAT'd behind libvirt's `virbr0`. VanLink + wired clients reach it directly at + `http://10.42.0.50:8123` (or `http://homeassistant:8123` / `homeassistant.local` via mDNS); + ZT clients route in via the ZT-managed `10.42.0.0/24` route. mDNS/SSDP device discovery works + because the VM shares the clients' L2 segment. +- The old NAT-era URL `http://10.42.0.1:8123` keeps working: nftables DNATs it to `10.42.0.50`, + with a hairpin masquerade for same-subnet clients (see comments in `ap/nftables.conf`). +- History: the VM used to sit on libvirt's `default` NAT net (`192.168.122.50`) with a + `/etc/libvirt/hooks/network` hook inserting FORWARD accepts above libvirt's REJECT. That broke + whenever libvirtd re-inserted its chains on restart (hook doesn't fire then) — bridging removed + the whole failure mode. The hook and the libvirt DHCP reservation are gone. +- Operate: `virsh {start,shutdown,domstate} ha_van`; autostart is per libvirt config. Verify: + `curl -s -o /dev/null -w '%{http_code}' http://10.42.0.50:8123/` → `200`. + ### Never sleep (lid-closed operation) - wayback lives lid-closed in the van and must stay up. Stock logind `HandleLidSwitch=suspend` would sleep it on lid close (even on AC). The `power/10-vanlink-nolid.conf` drop-in sets all three lid actions to `ignore`; `deploy.sh` also masks every sleep target so idle / GUI / a stray `systemctl suspend` can't suspend it. - Verify: `busctl get-property org.freedesktop.login1 /org/freedesktop/login1 org.freedesktop.login1.Manager HandleLidSwitch` → `"ignore"`, and `systemctl is-enabled suspend.target` → `masked`. `busctl call ... CanSuspend` should return `"no"`. diff --git a/ap/nftables.conf b/ap/nftables.conf index 95766fd..4ef329c 100755 --- a/ap/nftables.conf +++ b/ap/nftables.conf @@ -6,21 +6,18 @@ delete table ip van_router_nat table ip van_router_nat { chain prerouting { type nat hook prerouting priority dstnat; policy accept; - # Expose the Home Assistant VM (on libvirt's NAT net, 192.168.122.50) on the - # router's own LAN IP, so VanLink clients reach it at http://10.42.0.1:8123. - # The matching FORWARD accept lives in iptables (re-applied by the libvirt - # /etc/libvirt/hooks/network hook), since libvirt's reject is in that chain. - ip daddr 10.42.0.1 tcp dport 8123 dnat to 192.168.122.50:8123 + # Home Assistant VM (ha_van) is bridged onto br0 at 10.42.0.50 — clients reach + # it directly. Keep the legacy http://10.42.0.1:8123 URL working for anything + # that bookmarked it (phones, ZT clients). + ip daddr 10.42.0.1 tcp dport 8123 dnat to 10.42.0.50:8123 } chain postrouting { type nat hook postrouting priority srcnat; policy accept; ip saddr 10.42.0.0/24 oifname != "br0" masquerade - # ZeroTier clients reaching the HA VM via the 10.42.0.1:8123 DNAT: masquerade - # them onto virbr0 so HA sees the router (192.168.122.1) and the reply path is - # symmetric, exactly like VanLink clients. Matched by ZT ingress interface so it - # covers direct members (192.168.196.x) AND the LAN subnets routed in behind other - # ZT nodes (192.168.0/6/8/10/13.x). Scoped to virbr0 so ZT<->ZT and ZT->WAN are - # untouched. - iifname "ztuga7c2kh" oifname "virbr0" masquerade + # Hairpin for the legacy 10.42.0.1:8123 DNAT when the client sits on the same + # subnet as the HA VM: without masquerade the VM would reply directly on br0 + # from 10.42.0.50 and the client (expecting 10.42.0.1) would drop it. ZT-sourced + # traffic doesn't match and doesn't need it — VM replies route back through us. + ip saddr 10.42.0.0/24 ip daddr 10.42.0.50 tcp dport 8123 oifname "br0" masquerade } } diff --git a/ap/van-ap-dnsmasq.conf b/ap/van-ap-dnsmasq.conf index 1639585..21dc0ce 100644 --- a/ap/van-ap-dnsmasq.conf +++ b/ap/van-ap-dnsmasq.conf @@ -5,5 +5,8 @@ domain-needed bogus-priv dhcp-authoritative dhcp-range=10.42.0.10,10.42.0.254,255.255.255.0,12h +# Home Assistant VM (libvirt ha_van, bridged onto br0) — pinned address, resolves +# as "homeassistant" via this dnsmasq; HAOS also announces homeassistant.local (mDNS). +dhcp-host=52:54:00:ad:0a:01,10.42.0.50,homeassistant dhcp-option=option:router,10.42.0.1 dhcp-option=option:dns-server,10.42.0.1 diff --git a/ha/ha_van.xml b/ha/ha_van.xml new file mode 100644 index 0000000..421ccc1 --- /dev/null +++ b/ha/ha_van.xml @@ -0,0 +1,87 @@ + + ha_van + af014c94-de20-4f52-8d6b-438f16cd82e6 + Home Assistant OS + 4194304 + 4194304 + 2 + + hvm + + + + + /usr/share/OVMF/OVMF_CODE_4M.fd + /var/lib/libvirt/qemu/nvram/ha_van_VARS.fd + + + + + + + + + + + + + destroy + restart + destroy + + + + + + /usr/bin/qemu-system-x86_64 + + + + +
+ + +
+ + +
+ + + +
+ + + +
+ + + +
+ + + + + + +
+ + + + + + + + + + + + + + +