tailscale / tailscale/tailscale
control/controlhttp: DialPlan static resolver rejects HTTP proxy hostname
- Dominant language
- Go
- Stars
- 36.5k
- Forks
- 3.2k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 123
Description
### What is the issue?
When a client uses an explicit HTTP(S) CONNECT proxy and receives a control DialPlan with IP candidates, every DialPlan candidate can fail because the candidate's static DNS resolver is also used to connect to the proxy endpoint.
For an address-based candidate, `control/controlhttp` builds a `dnscache.Resolver` with `SingleHostStaticResult` and sets `SingleHost` to the control hostname. The same HTTP transport then installs both the proxy function and the DNS-cache-backed dialer. Since `net/http` first dials the proxy endpoint, the resolver sees the proxy hostname rather than the control hostname and returns:
```text
controlhttp: failed dialing using DialPlan, falling back to DNS
dnscache: unexpected hostname "" doesn't match expected ""
```
After all candidates fail, the built-in ordinary-DNS fallback succeeds through the same proxy.
### Environment
- Linux with systemd
- Tailscale 1.98.9
- Working explicit HTTP CONNECT proxy configured with `HTTP_PROXY` / `HTTPS_PROXY`
- Control-plane egress is available through the proxy
- Observed on three independent control reconnects
The fallback completed in approximately 1–2 seconds each time. The backend remained `Running`, the daemon did not restart, map polling recovered, and peer/data-plane traffic remained healthy.
### Steps to reproduce
1. Run a Linux client with a working explicit HTTP or HTTPS CONNECT proxy for control-plane access.
2. Allow the client to receive a control DialPlan containing address candidates.
3. Trigger a normal control reconnect.
4. Inspect the daemon journal for the signature above.
5. Observe that the DialPlan candidates fail on the proxy hostname, followed by a successful ordinary-DNS fallback through the proxy.
### Expected behavior
The connection to the proxy endpoint should use a resolver that can resolve the proxy, while the DialPlan static IP remains scoped to the CONNECT target/control hostname. Alternatively, address-based DialPlan candidates could be skipped when the selected proxy path cannot safely consume the single-host resolver.
### Source pointers
- DialPlan candidate and DNS fallback:
https://github.com/tailscale/tailscale/blob/v1.102.2/control/controlhttp/client.go#L1514-L1628
- Static resolver plus proxy-aware transport:
https://github.com/tailscale/tailscale/blob/v1.102.2/control/controlhttp/client.go#L2050-L2182
- Single-host hostname guard:
https://github.com/tailscale/tailscale/blob/v1.102.2/net/dnscache/dnscache.go#L1739-L1746
The same resolver/transport composition is present in the reviewed v1.98.9, v1.98.10, v1.102.2, and current `main` source.
### Related issues
- #14233 tracks excessive verbosity of joined DialPlan errors, but not this proxy/static-resolver interaction.
- #16222 fixed TLS certificate hostname verification for an HTTPS CONNECT proxy itself; it appears to be a different path.
Suggested regression coverage: one integration test for DialPlan + HTTP CONNECT proxy and one for DialPlan + HTTPS CONNECT proxy.
Contributor guide
Research direction
Start in control/controlhttp/client.go at the DialPlan candidate and DNS fallback code around lines 1514–1628, then inspect the static resolver and proxy-aware transport around lines 2050–2182. Read the single-host guard in net/dnscache/dnscache.go around lines 1739–1746. Done means DialPlan candidates work correctly with explicit HTTP and HTTPS CONNECT proxies, with regression coverage for both paths.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100