On a Tailscale host, boxes inherit the tailnet's DNS — flaky resolution, and tailnet names resolve from inside a box #33

Closed
opened 2026-07-14 00:42:20 +00:00 by dan-claude-bot · 0 comments
dan-claude-bot commented 2026-07-14 00:42:20 +00:00 (Migrated from github.com)

The drill host runs Tailscale. That turns out to interact with claudebox's network in two ways — one that makes boxes flaky, and one that quietly weakens the isolation the tool is for.

What was observed

A cold claudebox new failed with cloud-init status: error. The box could not resolve anything:

Err:1 http://deb.debian.org/debian trixie/main amd64 libcares2
  Temporary failure resolving 'deb.debian.org'
curl: (6) Could not resolve host: claude.ai

Routing was fine — only name resolution was dead. And drill/doctor.sh confirmed the host was otherwise clean (dns.mode unset, no NIC filtering, the six shipped ACL rules). Meanwhile the Incus journal shows its dnsmasq re-reading the host's resolver over and over, and flipping upstreams:

dnsmasq: reading /etc/resolv.conf
dnsmasq: using nameserver 192.168.178.1#53          ← the LAN router
dnsmasq: reading /etc/resolv.conf
dnsmasq: using nameserver 100.100.100.100#53        ← Tailscale MagicDNS
dnsmasq: using nameserver fd7a:115c:a1e0::53#53     ← Tailscale MagicDNS (v6)

Problem 1 — box DNS inherits a resolver that moves under it (reliability)

Incus's dnsmasq forwards box queries to whatever is in the host's /etc/resolv.conf at that moment. On a Tailscale host that file is not stable: tailscaled rewrites it to point at MagicDNS (100.100.100.100), and it flips back on network changes, restarts, and route updates.

So a box's DNS depends on the host's tailnet state. If Tailscale is restarting, degraded, or its DNS is briefly not answering, every lookup inside every box fails — and a cold mint dies in cloud-init, several minutes in, with an error that points at Debian's mirrors rather than at the real cause.

Status: strongly consistent with the evidence, not yet proven by a repro. The alternative explanation is a transient upstream failure that happened to land mid-mint.

Problem 2 — the tailnet is reachable by NAME, even though it is blocked by ADDRESS (isolation)

This one is structural, and it does not need a repro.

host/setup-host.sh drops egress to 100.64.0.0/10 — the CGNAT range, which is exactly Tailscale's address space. So a box cannot connect to a tailnet peer. Good, and clearly deliberate.

But the same setup opens a carve-out to the gateway (allow 10.87.0.1/32) so that dnsmasq can serve DNS — and that dnsmasq forwards to Tailscale's MagicDNS. Which means a box can ask the host's resolver about the tailnet and get answers:

  • tailnet peer names resolve — a box can enumerate the machines on your private network by name, and learn their 100.x addresses
  • split-DNS / internal search domains configured in the Tailscale admin console are served to the box too — so any internal-only zone (*.corp.example.com) becomes resolvable from inside a box that is supposed to see nothing but the public internet

The box cannot reach any of it. But "isolated" for this tool has always meant reaches the public internet and nothing else — and name-level reconnaissance of a private network is not nothing. It is the same shape as the sibling-box DNS enumeration in #16: connection blocked, enumeration wide open, and nothing in the design says otherwise.

An agent running inside a box is exactly the sort of thing that might go looking.

Proposed fix — give claudenet its own resolver, and stop inheriting the host's

Pin dnsmasq's upstream so it does not follow the host's resolver at all:

incus network set claudenet raw.dnsmasq "$(printf 'no-resolv\nserver=1.1.1.1\nserver=8.8.8.8\n')"

no-resolv makes dnsmasq ignore /etc/resolv.conf entirely; the explicit server= lines give it a stable public upstream. That fixes both problems at once:

  • box DNS no longer depends on the host's tailnet state (problem 1)
  • box DNS no longer traverses MagicDNS, so tailnet names and split-DNS zones stop resolving from inside a box (problem 2)

Open questions for whoever implements it:

  • Is raw.dnsmasq the right lever, or does the Incus bridge expose a first-class dns.nameservers-style key? Verify on a live Incus — do not take this issue's word for it. (This repo has already shipped one design decision based on an unverified assumption; see drill/RUNS.md.)
  • Which upstreams? Hardcoding 1.1.1.1 picks a vendor. A BOX_DNS-style setting with a sane default is probably right.
  • Should this be unconditional, or only when Tailscale (or any resolver in 100.64.0.0/10) is detected on the host? Unconditional is simpler and strictly more predictable — a box's DNS should not be a function of the host's VPN posture, whatever that posture is.

Scope

Not just Tailscale. Any host-level VPN or split-DNS resolver (Tailscale, ZeroTier, corporate VPN clients, systemd-resolved with per-link DNS) will be inherited by boxes the same way. Tailscale is simply the one that surfaced it, because 100.64.0.0/10 is already in the ACL — the ACL author clearly thought about tailnet connectivity and, understandably, not about tailnet names.

Provenance

Surfaced while running the drill for the #15 audit, on a Tailscale-connected host. Related: #16 (DNS enumeration of sibling boxes — same class, different target), #26 (the forensics run that captured the dnsmasq upstream flapping).

The drill host runs Tailscale. That turns out to interact with claudebox's network in two ways — one that makes boxes flaky, and one that quietly weakens the isolation the tool is *for*. ## What was observed A cold `claudebox new` failed with `cloud-init status: error`. The box could not resolve **anything**: ``` Err:1 http://deb.debian.org/debian trixie/main amd64 libcares2 Temporary failure resolving 'deb.debian.org' curl: (6) Could not resolve host: claude.ai ``` Routing was fine — only *name resolution* was dead. And `drill/doctor.sh` confirmed the host was otherwise clean (`dns.mode` unset, no NIC filtering, the six shipped ACL rules). Meanwhile the Incus journal shows its dnsmasq re-reading the host's resolver over and over, and **flipping upstreams**: ``` dnsmasq: reading /etc/resolv.conf dnsmasq: using nameserver 192.168.178.1#53 ← the LAN router dnsmasq: reading /etc/resolv.conf dnsmasq: using nameserver 100.100.100.100#53 ← Tailscale MagicDNS dnsmasq: using nameserver fd7a:115c:a1e0::53#53 ← Tailscale MagicDNS (v6) ``` ## Problem 1 — box DNS inherits a resolver that moves under it (reliability) Incus's dnsmasq forwards box queries to whatever is in the **host's** `/etc/resolv.conf` at that moment. On a Tailscale host that file is not stable: `tailscaled` rewrites it to point at MagicDNS (`100.100.100.100`), and it flips back on network changes, restarts, and route updates. So a box's DNS depends on the host's tailnet state. If Tailscale is restarting, degraded, or its DNS is briefly not answering, **every lookup inside every box fails** — and a cold mint dies in cloud-init, several minutes in, with an error that points at Debian's mirrors rather than at the real cause. *Status: strongly consistent with the evidence, not yet proven by a repro.* The alternative explanation is a transient upstream failure that happened to land mid-mint. ## Problem 2 — the tailnet is reachable by NAME, even though it is blocked by ADDRESS (isolation) This one is structural, and it does not need a repro. `host/setup-host.sh` drops egress to `100.64.0.0/10` — the CGNAT range, which is **exactly Tailscale's address space**. So a box cannot *connect* to a tailnet peer. Good, and clearly deliberate. But the same setup opens a carve-out to the gateway (`allow 10.87.0.1/32`) so that dnsmasq can serve DNS — and **that dnsmasq forwards to Tailscale's MagicDNS**. Which means a box can ask the host's resolver about the tailnet and get answers: - **tailnet peer names resolve** — a box can enumerate the machines on your private network by name, and learn their `100.x` addresses - **split-DNS / internal search domains** configured in the Tailscale admin console are served to the box too — so any internal-only zone (`*.corp.example.com`) becomes resolvable from inside a box that is supposed to see nothing but the public internet The box cannot *reach* any of it. But "isolated" for this tool has always meant *reaches the public internet and nothing else* — and name-level reconnaissance of a private network is not nothing. It is the same shape as the sibling-box DNS enumeration in [#16](https://github.com/heavy-duty/claudebox/issues/16): connection blocked, **enumeration wide open**, and nothing in the design says otherwise. An agent running inside a box is exactly the sort of thing that might go looking. ## Proposed fix — give claudenet its own resolver, and stop inheriting the host's Pin dnsmasq's upstream so it does not follow the host's resolver at all: ```sh incus network set claudenet raw.dnsmasq "$(printf 'no-resolv\nserver=1.1.1.1\nserver=8.8.8.8\n')" ``` `no-resolv` makes dnsmasq ignore `/etc/resolv.conf` entirely; the explicit `server=` lines give it a stable public upstream. That fixes **both** problems at once: - box DNS no longer depends on the host's tailnet state (problem 1) - box DNS no longer traverses MagicDNS, so tailnet names and split-DNS zones stop resolving from inside a box (problem 2) Open questions for whoever implements it: - Is `raw.dnsmasq` the right lever, or does the Incus bridge expose a first-class `dns.nameservers`-style key? **Verify on a live Incus — do not take this issue's word for it.** (This repo has already shipped one design decision based on an unverified assumption; see `drill/RUNS.md`.) - Which upstreams? Hardcoding `1.1.1.1` picks a vendor. A `BOX_DNS`-style setting with a sane default is probably right. - Should this be unconditional, or only when Tailscale (or any resolver in `100.64.0.0/10`) is detected on the host? **Unconditional is simpler and strictly more predictable** — a box's DNS should not be a function of the host's VPN posture, whatever that posture is. ## Scope Not just Tailscale. Any host-level VPN or split-DNS resolver (Tailscale, ZeroTier, corporate VPN clients, `systemd-resolved` with per-link DNS) will be inherited by boxes the same way. Tailscale is simply the one that surfaced it, because `100.64.0.0/10` is already in the ACL — the ACL author clearly thought about tailnet *connectivity* and, understandably, not about tailnet *names*. ## Provenance Surfaced while running the drill for the [#15](https://github.com/heavy-duty/claudebox/issues/15) audit, on a Tailscale-connected host. Related: [#16](https://github.com/heavy-duty/claudebox/issues/16) (DNS enumeration of sibling boxes — same class, different target), [#26](https://github.com/heavy-duty/claudebox/issues/26) (the forensics run that captured the dnsmasq upstream flapping).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/box#33
No description provided.