diff --git a/Sources/Homelab/2026-06-08-petal-dispatch-tailscale.md b/Sources/Homelab/2026-06-08-petal-dispatch-tailscale.md new file mode 100644 index 0000000..38eeccc --- /dev/null +++ b/Sources/Homelab/2026-06-08-petal-dispatch-tailscale.md @@ -0,0 +1,64 @@ +--- +created: '2026-06-08' +path: Sources/Homelab +project: petal-dispatch-p4 +tags: +- tailscale +- split-dns +- knot +- subnet-router +- petal-dispatch +- remote-access +- homelab +- p4 +type: session-notes +--- + +## Outcome + +Off-LAN remote access to `*.herbylab.dev` over Tailscale is working end to end. The petal-dispatch Android app and web client both connect to `https://dispatch.herbylab.dev/` from off-LAN (verified on hotel wifi), with SSE live and session context (`session_uuid`) carrying across devices. No app changes required — the hardcoded URL just works. This was the prerequisite for the phone app off-LAN, answering the open question of whether remote access plumbing was needed (it was). + +Chose **Path A** (split-DNS so original URLs work everywhere) over Path B (`.ts.net` MagicDNS hostname). A is the permanent answer; B was a lighter-touch stopgap. Committed to A and landed it. + +## Working configuration + +**1. Tailscale on knot (LXC 103, unprivileged)** +- Required `/dev/net/tun` passthrough in `/etc/pve/lxc/103.conf` (on PVE host): +``` +lxc.cgroup2.devices.allow: c 10:200 rwm +lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file +``` +- `pct restart 103`, then install via the standard script, then `tailscale up`. + +**2. knot listens on its tailnet IP** +- Added a third listen line in `/etc/knot/knot.conf` server block alongside the LAN and loopback addresses: +``` +listen: @53 +``` +- This is config-file-based, so it is **persistent** across knot restarts and reboots — not a runtime setting that needs re-applying. +- knot requires full `systemctl restart knot` to re-bind sockets (`knotc reload` does not). + +**3. Tailscale admin DNS (split DNS)** +- Restricted/custom nameserver → knot's tailnet IP, restricted to domain `herbylab.dev`. +- "Override local DNS" on. + +**4. Subnet router (because knot returns LAN IPs)** +- knot returns `10.0.11.20` (Traefik VM LAN IP), which tailnet clients can't reach directly. +- herbys-dev advertises the LAN: `sudo tailscale up --advertise-routes=10.0.0.0/16` (`/16` covers both 10.0.11.x and 10.0.21.x). +- Route **must be approved in the Tailscale admin machines page** — this is a separate gate from advertising. +- Clients accept with `sudo tailscale up --accept-routes`. + +## Key learnings + +- **`tailscale up` is all-or-nothing on prefs.** A bare `tailscale up` or a `systemctl restart tailscaled` silently drops any flag not repeated — `--advertise-routes` on the subnet router, `--accept-routes` on the client. Always repeat the full flag set. This was the single biggest time sink: the architecture was correct the whole time; advertised routes had been silently cleared by intervening bare restarts. +- **Route approval is a distinct step from route advertising.** A node can advertise a subnet and still show empty `AllowedIPs` until the route is approved in admin. No amount of `--accept-routes` on the client pulls a route that isn't authorized server-side. Verify with `tailscale status --json | grep -A20 ` → look for the subnet in `AllowedIPs`. +- **DNS working ≠ routing working.** Split-DNS resolving correctly (`dig` returns the right IP via `100.100.100.100`) is independent of whether the returned IP is reachable. The DNS-works/ping-fails split is the expected signature of "LAN IP returned, subnet router/approval not yet complete," not a bug. +- **Diagnosis order that works:** confirm client connected (`tailscale status`) → confirm route present in client table (`ip route | grep tailscale`) → if absent, confirm the route is being advertised AND approved (`AllowedIPs` on the advertising node) before touching the client again. + +## Reachability note (deferred, not done) + +This path uses herbys-dev as subnet router, which means inter-VLAN forwarding (herbys-dev is on a different VLAN than 10.0.11.20). It works today. A cleaner future option exists: since the Traefik VM is itself a tailnet node (100.86.14.2), pointing knot's service records at Traefik's tailnet IP would eliminate the subnet router, IP forwarding, and the VLAN hop entirely (Path B-style records, no MagicDNS dependency). Not needed now; noted for if the subnet-router path ever gets fragile. + +## Follow-up trigger + +- Infra node Tailscale keys are on the default **~6-month expiry** (herbys-dev, knot, traefik). When they expire, remote access dies silently until re-auth. Decided to leave as-is for now rather than disable expiry. **Trigger: ~November 2026** — re-auth or disable key expiry before then. Breadcrumb lives here so a cold debug session finds the cause fast. \ No newline at end of file