Skip to content

dns,cmd/tailmixd: translate split-DNS answers to effective IPs - #14

Open
david1gp wants to merge 2 commits into
maisem:mainfrom
david1gp:split-dns-effective-ip
Open

david1gp wants to merge 2 commits into
maisem:mainfrom
david1gp:split-dns-effective-ip

Conversation

@david1gp

@david1gp david1gp commented Nov 5, 2026

Copy link
Copy Markdown

Problem

Split-DNS answers that point at a peer's canonical tailnet address are unreachable from a tailmix host.

A common setup runs a DNS server (dnsmasq, CoreDNS, …) on a tailnet node and advertises it as a split-DNS resolver for a private zone. It answers with the node's own tailnet IP:

app.internal.example.com.  A  100.101.102.103   ; real tailnet IP of "server"

With plain tailscaled this works because the canonical address is routed. tailmix only routes the effective IP it assigns to each peer (for example 100.127.0.0 / fd6d:6e65:7400::). It forwards split-DNS responses verbatim, so the host gets an address it has no route for:

  • getent ahosts app.internal.example.com → 100.101.102.103
  • the connection times out (tailmixd logs drop outbound packet: no profile route for destination)
  • server.tailnet-name.ts.net resolves to the effective IP and works

The workarounds all have downsides: per-name /etc/hosts pins to an effective IP (which can change), CNAMEs to the MagicDNS name on the server (dnsmasq can't do this cleanly), or a separate subnet route per service. All of them push tailmix's address translation onto the tailnet operator or the host administrator.

Solution

When tailmix already knows the address in an answer, translate it the same way MagicDNS answers are translated.

  • tailmixdns.Route gets a Rewrites map[netip.Addr]netip.Addr. It maps the canonical address of every node or service of the route's profile to that node's effective IP. Self IPs use the host NAT address, like addRecords does.
  • tunDNSLiveConfig fills the map from the same leases it uses for MagicDNS records. configureDNSLocked passes it to both StartForwarder and StartProfileDNSForwarder. Forwarders are rebuilt on each reconcile, so the map follows lease changes.
  • After validateResponse, the forwarder rewrites matching A → IPv4 and AAAA → IPv6 records in the Answer section only.

Safety properties:

  • Strictly per profile. A route only uses its own profile's mapping, so tailnets that reuse the same canonical address can't affect each other.
  • Unmapped addresses, CNAMEs, other record types, and the authority and additional sections pass through unchanged.
  • If nothing matches, the original bytes are returned unchanged (no re-pack).
  • If a rewrite is needed but the response can't be re-packed, the forwarder returns SERVFAIL instead of an unroutable address.
  • No new logging.

docs/design.md and docs/profile-management.md are updated. Addresses that aren't known tailnet nodes (subnet routes, public IPs) still follow host IP routing as before.

Testing

  • dns/forwarder_test.go: A and AAAA rewrite, unmapped answers unchanged, per-profile isolation, CNAME chain followed by a rewritten A record, and byte-identical output when nothing changes.
  • cmd/tailmixd/main_test.go: tunDNSLiveConfig fills each route's map from the correct profile's leases only.
  • go test ./..., go vet ./..., staticcheck, govulncheck, and go mod tidy -diff pass.
  • Manual test on Fedora (systemd-resolved, SELinux enforcing) with three tailnet profiles, where one tailnet runs a dnsmasq split-DNS server for about 140 private names. Before the change, every name resolved to the server's canonical IP and timed out. After it, they resolve to the server's effective IP and HTTPS requests succeed. MagicDNS and the other profiles are unaffected.

I've seen the comment on #10 about preferring issues first. If you'd rather track this as an issue and have it reimplemented, feel free to close this PR and use the description as the issue.

Split-DNS servers inside a tailnet can answer with a peer's canonical
tailnet address, which the host cannot route since tailmix only routes
effective IPs. Give each DNS route the canonical to effective mapping
of its own profile and have the profile forwarders rewrite matching A
and AAAA records in upstream answers, using the same leases and host
NAT addresses as the MagicDNS records. Unmapped addresses, CNAMEs, and
authority and additional records pass through untouched, and responses
without rewrites keep their original bytes.

Change-Id: I5089876d1aba81f4ef30163f986c3bbeafd16a59
New profiles previously advertised a random tailmix- name. Use the
sanitized first label of the OS hostname instead, matching upstream
Tailscale, and keep the generated name only as a fallback.

Update the profiles add help and profile-management docs, and cover
the default plus the fallback in tests.
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