Skip to content

DNS robustness: fix bootstrap deadlock + secondary AdGuard on the Pi - #58

Merged
ulises-c merged 16 commits into
mainfrom
feat/dns-robustness
Jul 12, 2026
Merged

DNS robustness: fix bootstrap deadlock + secondary AdGuard on the Pi#58
ulises-c merged 16 commits into
mainfrom
feat/dns-robustness

Conversation

@ulises-c

Copy link
Copy Markdown
Owner

Why

A scheduled-maintenance network outage took the home network's only DNS down,
and it couldn't self-heal. Two problems:

  1. Bootstrap deadlock (primary). The server's AdGuard Home shares its Tailscale
    sidecar's netns, and the sidecar's OAuth bootstrap needs DNS to reach
    api.tailscale.com — but the only DNS is that same AdGuard. Cold start after the
    outage deadlocked: sidecar needs DNS → DNS needs the sidecar.
  2. Single point of failure. One resolver on one box = the whole LAN loses DNS
    when it goes down.

What this does

Fix the deadlock (fix(server): break AdGuard/Tailscale DNS bootstrap deadlock)
Pin static public resolvers on adguard-ts (dns: [9.9.9.10, 1.1.1.1]) so the
sidecar's bootstrap never depends on AdGuard. Verified in place: the sidecar now
resolves api.tailscale.com independently.

Add a secondary resolver on the Pi — new linux-pi/ node (parallel to
linux-server/):

  • linux-pi/adguard — backup AdGuard Home, host-networked and independent of
    Tailscale
    , so :53 keeps serving LAN clients even when the tailnet/internet is
    down. A decoupled adguard-pi-ts sidecar serves only the HTTPS admin UI and
    carries the same bootstrap guard. (Deliberately does not copy the primary's
    netns coupling.)
  • linux-pi/adguardhome-syncbakito/adguardhome-sync on the Pi, pulling the
    primary's filters/rewrites/upstreams/rules into the replica on a cron.
  • Docs: linux-pi/README.md deploy runbook + the "secondary DNS is not clean
    failover" caveat; CLAUDE.md/TODO.md updated.

Status / follow-ups

Draft — the deadlock fix is applied and verified on the server; the Pi node is
not deployed yet (owner will run the linux-pi/README.md runbook on ollie-pi4:
free :53, fill .envs, docker compose up -d, add the Pi as secondary DNS on the
router). Still open (tracked in TODO): secondary DHCP scope (DHCP is single-homed).

Test plan (on the Pi, post-deploy)

  • dig @<pi-ip> example.com resolves; a blocked domain returns 0.0.0.0 (filtering).
  • Stop adguard-pi-tsdig @<pi-ip> still resolves (DNS independent of Tailscale).
  • https://adguard-pi.<tailnet>.ts.net UI loads; sync makes the Pi's lists match the primary.
  • Router hands out [server, pi]; stop the primary AdGuard → clients still resolve via the Pi.

🤖 Generated with Claude Code

ulises-c and others added 14 commits July 11, 2026 04:57
Pin static public resolvers (9.9.9.10, 1.1.1.1) on the adguard-ts
sidecar so its OAuth bootstrap can resolve api.tailscale.com without
depending on AdGuard Home, which shares this sidecar's network namespace.

Previously a cold start (e.g. after a network outage) deadlocked: the
sidecar needs DNS to authenticate with the Tailscale control plane, but
the only DNS on the box is the AdGuard instance that cannot start until
the sidecar's netns is up. Pinning an external resolver for this one
container breaks the cycle without routing other traffic around AdGuard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce a linux-pi/ node (parallel to linux-server/) and a backup
AdGuard Home on the Raspberry Pi to remove the home network's single
point of DNS failure exposed by the 2026-07 outage.

Unlike the primary (linux-server/adguard), AdGuard here is host-networked
and NOT wrapped in a Tailscale sidecar's netns, so :53 keeps serving LAN
clients even when the tailnet or internet is down. A decoupled
adguard-pi-ts sidecar provides only the HTTPS admin UI and carries the
dns: [9.9.9.10, 1.1.1.1] bootstrap guard from the primary's fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an adguardhome-sync stack (bakito/adguardhome-sync) that runs on the
Pi and pulls the primary's filters, rewrites, upstreams and rules into
the secondary on a cron, so the two stay in lockstep. DHCP sync is
disabled; credentials and URLs live in a gitignored .env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add linux-pi/README.md (deploy runbook, resilience rationale, and the
"secondary DNS is not clean failover" caveat), point the root CLAUDE.md
at the new node, and record the post-outage DNS resilience work in
TODO.md: bootstrap-deadlock fix done, Pi secondary added, secondary DHCP
still open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deploy a homepage instance on ollie-pi4 (host-networked on :3000) with a
decoupled homepage-pi-ts sidecar at https://homepage-pi.<tailnet>.ts.net.
It carries cards for the Pi's services (backup AdGuard, MotionEye, CUPS)
and a resources widget that shows the Pi's own CPU/mem/disk/temp, since
homepage runs on the Pi host. Runtime + homepage-auto-generated files
are gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add decoupled Tailscale sidecars fronting the Pi's existing host services
— MotionEye (:8765) and CUPS (:631) — at motioneye-pi.<tailnet>.ts.net and
cups-pi.<tailnet>.ts.net. The services stay untouched host processes;
these stacks add only the HTTPS front doors, each with the same
dns: [9.9.9.10, 1.1.1.1] bootstrap guard as the other sidecars. CUPS
validates the Host header, so its admin pages may need `ServerAlias *`
(noted in the compose + README).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Secondary Server (Pi)" group to the main server's homepage that
links to the Pi dashboard and siteMonitor-pings it for up/down + latency,
driven by a new HOMEPAGE_VAR_PI_HOMEPAGE_DOMAIN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… hop

Extend linux-pi/README with the homepage/motioneye/cups stacks, the main
homepage link, deploy steps, and a troubleshooting section for the
sidecar -> host.docker.internal:<port> hop that every Pi HTTPS front door
depends on (old Docker without host-gateway, or ufw dropping the
bridge->host path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the two things learned wiring up the Pi HTTPS front doors: the
first HTTPS request to a sidecar hangs briefly while tailscale serve
provisions its Let's Encrypt cert (not a fault — retry), and a sidecar's
connection bootstraps via DERP then upgrades to a direct LAN path (~5ms),
so it is fine for MotionEye video once warmed up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Secondary Server (Pi)" card used `widget: {type: siteMonitor}`, which
homepage rejects with "missing widget type". Move it to the top-level
`siteMonitor:` service field so the up/down + latency monitor renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bookmarks

Add a "Main Server" card to the Pi dashboard that links to and monitors the
primary server — the reverse of its "Secondary Server (Pi)" card — via a new
HOMEPAGE_VAR_MAIN_HOMEPAGE_DOMAIN. Track an empty bookmarks.yaml so homepage
stops auto-generating its Github/Reddit/YouTube example bookmarks (un-ignored
so the empty file ships). Also corrects the Pi's motioneye/cups cards to the
top-level siteMonitor field, the same fix as the main server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reorder the main homepage into three rows: Secondary Server (Pi) on top,
Management | Network | Storage side-by-side (nested under a header-less
"Core" group with style: row), and NAS below with its 2-column disks. No
services or widgets changed - the group blocks were re-indented under
the row wrapper, comments intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The disk R/W widgets asked for kernel names (disk:sda/sdb/sdc), but the
Glances rename patch (linux-server/glances) reports persistent by-label
names, so the widgets errored with "read_bytes undefined". Use
disk:WD14TB / Seagate_4TB / WD_1TB to match. Capacity widgets key off
mount paths and were unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DAS drives weren't mounted at boot (disconnected), causing Homepage
capacity widgets to show no data. Glances restart fixed that.

Kernel device names (sda/sdb/sdc) shuffled when the DAS was reconnected,
breaking R/W speed widgets. Added a Glances entrypoint that resolves
/dev/disk/by-label/ at startup and monkey-patches the diskio plugin to
use persistent label-based names (Seagate_4TB, WD_1TB, WD14TB).

Changes:
- glances/entrypoint.sh — resolves labels → kernel names at container start
- glances/rename_disks.py — monkey-patches Glances diskio plugin
- glances/docker-compose.yml — mounts entrypoint, rename script, and
  /dev/disk/by-label into the container
- homepage/config/services.yaml — metric: disk:sda/sdb/sdc → disk:<label>
- post-install.md — added DAS drives section documenting the setup

(cherry picked from commit ba31db2f7e0ac724d2702b4e1b82627c8747f5d3)
ulises-c added 2 commits July 12, 2026 05:06
# Conflicts:
#	.gitignore
#	linux-server/homepage/config/services.yaml
- backup.sh: restic over SFTP to main server DAS (wd1tb primary, wd14tb copy)
- systemd timer at 03:45 daily with failure alerts
- nginx:alpine status card server on :8099 for homepage widget
- LEARNINGS.md: SFTP gotchas (nologin shell output, restic Go SSH client)
- Pi homepage: add backup status card under Pi Services
@ulises-c
ulises-c force-pushed the feat/dns-robustness branch from 99adc73 to 8c8d2d9 Compare July 12, 2026 08:00
@ulises-c
ulises-c marked this pull request as ready for review July 12, 2026 08:55
@ulises-c
ulises-c merged commit 24de843 into main Jul 12, 2026
5 checks passed
@ulises-c
ulises-c deleted the feat/dns-robustness branch July 12, 2026 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant