--host-net cannot reach the tailnet on Docker Desktop (no tailscale0 in the LinuxKit VM netns) #332

Closed
opened 2026-06-25 10:22:06 +00:00 by coilysiren · 2 comments
Owner

Summary

--host-net (ward#330, commit 52f2176) wires --network=host correctly, but on Docker Desktop (Mac/Windows) it cannot deliver tailnet reachability. --network=host joins the container to the LinuxKit VM's network namespace, and tailscale runs on the macOS/Windows host one layer up. The VM has no tailscale0 and no MagicDNS, so a carry still cannot resolve tailnet names (api, kai-tower-3026, etc.). The flag's design premise in docs/agent-host-net.md — "a carry runs on a host that is itself on the tailnet, so --network=host makes the container inherit that host's tailscale0 and MagicDNS" — holds on native Linux but is false on Docker Desktop.

Evidence (from a live warded explore carry on Docker Desktop)

  • Container inspect: NetworkMode: host, Networks: ['host'] — the flag was applied.
  • hostname = docker-desktop (the LinuxKit VM, not the Mac).
  • No tailscale0 among the joined-netns interfaces (docker0, every br-*, bond0, services1, eth0/1 present; no tailscale device).
  • /etc/resolv.conf = 192.168.65.7 (Docker Desktop internal resolver), not 100.100.100.100 (MagicDNS).
  • getent hosts api returns nothing; curl http://api/ fails to resolve.

Root cause

On Docker Desktop the docker "host" is the LinuxKit VM, which is not a tailnet node. --network=host therefore inherits a netns with no tailscale. This is a well-documented Tailscale + Docker Desktop limitation, not a ward bug per se — but --host-net currently presents as if it unblocks tailnet access on any host, with no signal that it is a no-op on the host class agents actually develop on.

Why this matters

The in-container tailscaled route that 52f2176 filed as an optional follow-up is in fact the only path that makes --host-net useful on Docker Desktop. Host-net's value is confined to native-Linux tailnet hosts (and even there containers often need 100.100.100.100 added to resolv.conf, since container DNS is purely resolv.conf-driven and does not inherit the host's per-link systemd-resolved config — tailscale/tailscale#14467).

Proposed scope (this issue)

Low-fork, mergeable without any secret:

  1. Document the Docker Desktop limitation loudly in docs/agent-host-net.md: --host-net only reaches the tailnet on a native-Linux host that is itself a tailnet node; on Docker Desktop it joins the LinuxKit VM and does nothing for tailnet access.
  2. Detect + warn: when --host-net is requested but the joined netns has no tailscale0 (check the interface / probe MagicDNS at 100.100.100.100), emit a loud warning that the route will not reach the tailnet on this host.
  3. Promote the sidecar path from "follow-up" to the documented cross-platform answer: in-container tailscaled (auth key) with the app sharing its netns (network_mode: service:tailscale). Scope/spec only here — actual implementation + live validation needs an ephemeral tagged auth key and an ACL grant, which is a separate human-gated decision (the "no auth key, no minting" line ward#330 drew on purpose).

Out of scope (separate, human-gated)

Minting/storing a tailscale auth key, choosing the node tag, and the ACL grant that lets that tag reach the target hosts. That carries a real secret-handling decision and should not be picked by a headless run.

Sources

Diagnosed from a live read-only explore carry; filed and dispatched per explore capture-and-dispatch doctrine.

## Summary `--host-net` (ward#330, commit 52f2176) wires `--network=host` correctly, but on **Docker Desktop (Mac/Windows)** it cannot deliver tailnet reachability. `--network=host` joins the container to the **LinuxKit VM's** network namespace, and tailscale runs on the macOS/Windows host one layer up. The VM has no `tailscale0` and no MagicDNS, so a carry still cannot resolve tailnet names (`api`, `kai-tower-3026`, etc.). The flag's design premise in `docs/agent-host-net.md` — "a carry runs on a host that is itself on the tailnet, so `--network=host` makes the container inherit that host's tailscale0 and MagicDNS" — holds on native Linux but is false on Docker Desktop. ## Evidence (from a live `warded explore` carry on Docker Desktop) - Container inspect: `NetworkMode: host`, `Networks: ['host']` — the flag was applied. - `hostname` = `docker-desktop` (the LinuxKit VM, not the Mac). - No `tailscale0` among the joined-netns interfaces (`docker0`, every `br-*`, `bond0`, `services1`, `eth0/1` present; no tailscale device). - `/etc/resolv.conf` = `192.168.65.7` (Docker Desktop internal resolver), not `100.100.100.100` (MagicDNS). - `getent hosts api` returns nothing; `curl http://api/` fails to resolve. ## Root cause On Docker Desktop the docker "host" is the LinuxKit VM, which is not a tailnet node. `--network=host` therefore inherits a netns with no tailscale. This is a well-documented Tailscale + Docker Desktop limitation, not a ward bug per se — but `--host-net` currently presents as if it unblocks tailnet access on any host, with no signal that it is a no-op on the host class agents actually develop on. ## Why this matters The in-container `tailscaled` route that 52f2176 filed as an optional follow-up is in fact the **only** path that makes `--host-net` useful on Docker Desktop. Host-net's value is confined to native-Linux tailnet hosts (and even there containers often need `100.100.100.100` added to resolv.conf, since container DNS is purely resolv.conf-driven and does not inherit the host's per-link systemd-resolved config — tailscale/tailscale#14467). ## Proposed scope (this issue) Low-fork, mergeable without any secret: 1. **Document the Docker Desktop limitation** loudly in `docs/agent-host-net.md`: `--host-net` only reaches the tailnet on a native-Linux host that is itself a tailnet node; on Docker Desktop it joins the LinuxKit VM and does nothing for tailnet access. 2. **Detect + warn**: when `--host-net` is requested but the joined netns has no `tailscale0` (check the interface / probe MagicDNS at `100.100.100.100`), emit a loud warning that the route will not reach the tailnet on this host. 3. **Promote the sidecar path** from "follow-up" to the documented cross-platform answer: in-container `tailscaled` (auth key) with the app sharing its netns (`network_mode: service:tailscale`). Scope/spec only here — actual implementation + live validation needs an ephemeral tagged auth key and an ACL grant, which is a separate human-gated decision (the "no auth key, no minting" line ward#330 drew on purpose). ## Out of scope (separate, human-gated) Minting/storing a tailscale auth key, choosing the node tag, and the ACL grant that lets that tag reach the target hosts. That carries a real secret-handling decision and should not be picked by a headless run. ## Sources - https://tailscale.com/docs/features/containers/docker/docker-desktop - https://tailscale.com/docs/features/containers/docker/how-to/connect-docker-container - https://github.com/tailscale/tailscale/issues/14467 - https://tailscale.com/blog/docker-tailscale-guide _Diagnosed from a live read-only explore carry; filed and dispatched per explore capture-and-dispatch doctrine._
Author
Owner

🔒 Reserved by ward agent --driver claude — container ward-ward-issue-332-claude-d5b57fdd on host docker-desktop is carrying this issue (reserved 2026-06-25T10:22:19Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `ward-ward-issue-332-claude-d5b57fdd` on host `docker-desktop` is carrying this issue (reserved 2026-06-25T10:22:19Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

Carried this end to end. It is mostly a docs + detect/warn change, so the implementation itself was small: a pure hostNetTailnetWarning(goos, hasTailscale0) wired into the one shared launch chokepoint (createAgentContainer), plus a net.Interfaces() probe behind it. Honest caveat on the detection: I cannot truly inspect the LinuxKit VM netns from the Mac, so the signal is "ward runs on a non-Linux host => Docker Desktop => warn", with the tailscale0 probe only meaningful on native Linux. That matches the evidence in the issue but it is a heuristic, not a live netns check.

What fought back was not the code but the gate. The pre-commit suite caps docs at 4000 chars / 80 lines, and FEATURES.md + agent-flags.md were already sitting right at the cap, so adding even a terse note meant trimming unrelated wording back down to the byte. The code-comments hook (2-line contiguous blocks) also forced the detail out of doc comments and into docs/, which is the right place anyway. Separately, this container shipped without trufflehog / golangci-lint / kdlfmt, so I had to install trufflehog by hand to clear the secret-scan hook rather than skip it.

Confidence is high on the warning + tests; the sidecar section is scope/spec only by design (auth key + ACL grant stays human-gated, per the line #330 drew).

One follow-up worth filing: TestAgentLaunchSilencesDockerNoiseWhenHeadless is already red on main (HEAD == origin/main) - a shared-logfile expectation that was not updated when the ward#322 heartbeat subtest was added an extra hints=false line. Unrelated to host-net, so I left it out of this change rather than fold an orthogonal fix in.

Carried this end to end. It is mostly a docs + detect/warn change, so the implementation itself was small: a pure hostNetTailnetWarning(goos, hasTailscale0) wired into the one shared launch chokepoint (createAgentContainer), plus a net.Interfaces() probe behind it. Honest caveat on the detection: I cannot truly inspect the LinuxKit VM netns from the Mac, so the signal is "ward runs on a non-Linux host => Docker Desktop => warn", with the tailscale0 probe only meaningful on native Linux. That matches the evidence in the issue but it is a heuristic, not a live netns check. What fought back was not the code but the gate. The pre-commit suite caps docs at 4000 chars / 80 lines, and FEATURES.md + agent-flags.md were already sitting right at the cap, so adding even a terse note meant trimming unrelated wording back down to the byte. The code-comments hook (2-line contiguous blocks) also forced the detail out of doc comments and into docs/, which is the right place anyway. Separately, this container shipped without trufflehog / golangci-lint / kdlfmt, so I had to install trufflehog by hand to clear the secret-scan hook rather than skip it. Confidence is high on the warning + tests; the sidecar section is scope/spec only by design (auth key + ACL grant stays human-gated, per the line #330 drew). One follow-up worth filing: TestAgentLaunchSilencesDockerNoiseWhenHeadless is already red on main (HEAD == origin/main) - a shared-logfile expectation that was not updated when the ward#322 heartbeat subtest was added an extra hints=false line. Unrelated to host-net, so I left it out of this change rather than fold an orthogonal fix in.
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
coilyco-flight-deck/ward#332
No description provided.