Files
PFVCluster/docs/server-build/tailscale.md
T
mrcharles 75e229c0ab docs(tailscale): document subnet router HA + fix resolv.conf immutable bug
Adds section documenting the redundant subnet router architecture:
both netinfra-01 and netinfra-02 now advertise 192.168.0.0/22 for
automatic failover. This fixes the SPOF where all remote monitoring
(Uptime Kuma on Cloudron VPS) routed through netinfra-01 alone —
any DERP bounce or Docker LinkChange event took down visibility to
every LAN host simultaneously.

Also documents the resolv.conf immutable flag fix on netinfra-01
(chattr -i) that was preventing tailscaled from updating DNS config
after every link change event.

[#377]
2026-08-06 12:53:36 -05:00

147 lines
6.4 KiB
Markdown

# Tailscale vs. Managed DNS — Architecture Analysis
> **Status:** **RESOLVED.** The pfv-netinfra-01/02 pair now runs production
> Technitium DNS with all `knel.net` records replicated from tailscale-router
> via the DNS cluster setup. Both LAN IPs serve authoritative records for
> `knel.net` and recurse externally. This document records the original
> conflict, how it was resolved, and the recommended client configuration.
## 1. Executive summary
Every host in this build runs the Tailscale client, and Tailscale's MagicDNS
manages `/etc/resolv.conf` by default (pointing at `100.100.100.100`). This
previously conflicted with a managed `resolv.conf` pointing at the LAN
resolvers. The root cause was that the LAN Technitium instances did not have
the `knel.net` zone populated — **that is now fixed.**
The pfv-netinfra-01/02 pair now serves identical, authoritative `knel.net`
records (replicated from production via [`dns-cluster-setup/`](../dns-cluster-setup/README.md)).
Both LAN IPs resolve `knel.net` device names and recurse externally. The
managed `resolv.conf` is now safe to deploy.
**Recommendation:** Deploy the managed `resolv.conf` (`.252`/`.253`) on hosts
where you want tunnel-independent DNS. Leave Tailscale managing DNS on hosts
where MagicDNS device names must resolve without a LAN path (e.g. laptops off
-network). See [§5](#5-recommendation) for details.
## 2. How name resolution works today (post-cluster-setup)
Probed from `sectestbed-sandbox` (192.168.3.50) after the DNS cluster was
deployed:
| Query path | External name (`github.com`) | `knel.net` device name (`pfv-netinfra-01.knel.net`) |
|---|---|---|
| Via Tailscale resolver (`100.100.100.100`) | resolves | resolves → `100.70.181.72` (Tailscale CGNAT) |
| Direct `dig @192.168.3.252` (Technitium primary, LAN) | resolves (recurses) | **resolves**`100.70.181.72` |
| Direct `dig @192.168.3.253` (Technitium secondary, LAN) | resolves (recurses) | **resolves**`100.70.181.72` |
**Both LAN resolvers now serve `knel.net` records identically.** The
Technitium zone is no longer stale — it was replicated from production
(tailscale-router) as part of the DNS cluster setup.
### What changed
Previously (before the DNS cluster setup), querying the LAN IPs returned
NXDOMAIN for `knel.net` device names because the Technitium `knel.net` zone
was empty (SOA serial `2025062313`, dated 2025-06-23). After replicating
production config to both netinfra hosts, all 124 zones — including
`knel.net` with all current device records — are served authoritatively on
both `.252` and `.253`.
## 3. The DNS server pair
| Host | IP | Role | Services |
|------|----|------|----------|
| pfv-netinfra-01 | 192.168.3.252 | **Primary** | Technitium (authoritative, port 5300) + Pi-hole (recursive, port 53) |
| pfv-netinfra-02 | 192.168.3.253 | **Secondary** | Technitium (replicated via rsync, port 5300) + Pi-hole (recursive, port 53) |
Zone replication is rsync-based (every 60s via systemd timer) because
Technitium's AXFR uses port 53, which is occupied by Pi-hole on these hosts.
See [`dns-cluster-setup/README.md`](../dns-cluster-setup/README.md) for
full details.
## 4. NTP (fully resolved)
NTP is independent of DNS: `provisioning/ConfigFiles/NTP/ntp.conf` points
directly at the LAN IPs with no DNS dependency:
```
server 192.168.3.252 iburst
server 192.168.3.253 iburst
```
Both servers respond with stratum 2/3. The client config uses `restrict`
rules (not `interface listen`) to avoid the loopback-binding bug that
prevented sync. This is safe under both Tailscale-managed and LAN-pinned
resolver configurations.
## 5. Recommendation
### On fixed servers (always on-LAN)
**Deploy the managed `resolv.conf`** (`provisioning/ConfigFiles/Resolv/`):
- Points at `.252`/`.253` with failover
- `knel.net` records resolve on both servers
- External names recurse on both servers
- DNS survives `tailscaled` outages (unlike Tailscale-managed DNS)
To prevent Tailscale from overwriting the managed file:
```bash
tailscale up --accept-dns=false
```
### On laptops / roaming hosts
**Let Tailscale manage DNS** (default `accept-dns=true`):
- MagicDNS resolves `knel.net` device names via the tunnel
- No dependency on LAN reachability
- Accept the `tailscaled` dependency (if the tunnel is down, you're off-network anyway)
## 6. Subnet router HA (redundant routing)
> **Redmine:** [#377](https://projects.knownelement.com/issues/377)
Both DNS nodes advertise the LAN subnet as Tailscale subnet routes,
providing automatic failover for remote monitoring (Uptime Kuma on the
Cloudron VPS) and off-site access:
| Node | Tailscale IP | Advertised route | Role |
|------|-------------|-----------------|------|
| pfv-netinfra-01 | 100.70.181.72 | 192.168.0.0/22 | Primary subnet router + exit node |
| pfv-netinfra-02 | 100.71.171.20 | 192.168.0.0/22 | Secondary subnet router (failover) |
To configure on a node:
```bash
sudo tailscale set --advertise-routes=192.168.0.0/22
```
**Both routes must be approved** in the Tailscale admin console
(https://login.tailscale.com/admin/machines) for failover to activate.
Tailscale uses the primary router by default and fails over to the
secondary if the primary's tunnel drops.
### Known instability factors (2026-08-06, [#377])
- **DERP relay bouncing:** netinfra-01 was observed switching between DERP
relays (#9, #12, #13) every ~15 minutes, causing brief tunnel disruptions.
- **Docker LinkChange events:** container create/destroy adds/removes bridge
interfaces, triggering `LinkChange: major, rebinding` in tailscaled and
momentarily dropping all connections.
- **resolv.conf immutable flag:** netinfra-01 had the `+i` (immutable)
attribute on `/etc/resolv.conf`, preventing tailscaled from updating DNS
config after link changes. Fixed by `chattr -i /etc/resolv.conf`.
## 7. Known items / future work
1. **Pi-hole upstream configuration.** Pi-hole on both hosts should forward
to the local Technitium instance (port 5300) for `knel.net` and to an
external resolver for everything else. Verify this is configured on both
nodes.
2. **Zone transfer via AXFR.** Currently using rsync because Technitium's
AXFR expects port 53. If Technitium's listen port can be changed, or
Pi-hole can be configured to proxy AXFR, the rsync timer could be
replaced with native DNS zone transfer.
3. **`accept-dns=false` automation.** The provisioning code should set
`--accept-dns=false` on Tailscale during setup (after deploying the
managed `resolv.conf`) so Tailscale doesn't overwrite it on reboot.