Tailscale Split DNS: Reach Internal Services Directly From the Tailnet
Scenario
A home server runs a bunch of services and has no public IP. The domains’ A records point to a VPS with a public IP, and Caddy on the VPS reverse-proxies back to the home server. Devices inside the tailnet take the VPS detour too, adding an extra hop.
The goal: when Tailscale is up, domains resolve directly to the home server’s Tailscale IP; when it’s down, they fall back to the VPS public IP. Same domains, two answers.
Run a dnsmasq on the home server
dnsmasq answers the domain queries coming from the tailnet. It runs on the home server itself and answers with its own Tailscale IP. After installing (Arch: pacman -S dnsmasq, Debian: apt install dnsmasq), write a config:
# /etc/dnsmasq.d/services.conf
address=/app1.example.com/<tailnet-ip>
address=/app2.example.com/<tailnet-ip>
# listen only on the tailscale interface and loopback
listen-address=<tailnet-ip>,127.0.0.1
bind-dynamic
# explicit upstream, bypassing tailscale's 100.100.100.100 to avoid a resolution loop
no-resolv
server=<gateway-ip>
# Alibaba Cloud public DNS
server=223.5.5.5
server=223.6.6.6One address= line per domain. Domains not listed still forward upstream normally.
no-resolv is required. Tailscale rewrites the server’s /etc/resolv.conf to nameserver 100.100.100.100. If dnsmasq forwards to the default upstream from resolv.conf, then once split DNS is configured the server’s own lookups for *.example.com go to 100.100.100.100, and Tailscale’s resolver forwards them back to this dnsmasq — a loop. Pointing upstream at the router or a public resolver explicitly avoids it.
Start and enable:
sudo systemctl enable --now dnsmasqWith systemd, add a drop-in so dnsmasq starts after tailscaled:
# /etc/systemd/system/dnsmasq.service.d/tailscale.conf
[Unit]
After=tailscaled.service
Wants=tailscaled.serviceAdd one split DNS entry in the admin console
- Open https://login.tailscale.com/admin/dns
- Nameservers → Add nameserver → Custom
- Enter the home server’s Tailscale IP (
<tailnet-ip>) - Enter
example.comas the domain; it matches all subdomains
One entry covers every subdomain. From then on, tailnet queries for *.example.com go to this dnsmasq.
When an exit node is in use, split DNS is skipped by default — enable “Use with exit node” on the nameserver.
Certificates
On the direct path the client connects to the home server’s 443 port, so the home server has to terminate TLS itself. With no public inbound, the only option is the DNS-01 challenge. For Caddy with Cloudflare DNS-01, see Caddy Cloudflare DNS-01: Get Wildcard TLS Certificates. A wildcard *.example.com certificate covers all services; Caddy issues and renews it automatically.
Verification
On Windows, run this once with Tailscale on and once with it off:
nslookup app.example.comWith Tailscale on it should return <tailnet-ip>; with it off, the VPS public IP. If the result doesn’t change, clear the cache with ipconfig /flushdns.
dnsmasq answers locally-defined records with TTL 0, so clients don’t cache and the switch is immediate. Lowering the public record’s TTL (e.g. 60 seconds) also helps on the fallback side. Browsers with Secure DNS (DoH) enabled bypass the local resolver — those domains keep using the public DNS / VPS path. It still works, just without the direct route.