# 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](https://github.com/thekelleys/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:

```ini
# /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.6
```

One `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:

```bash
sudo systemctl enable --now dnsmasq
```

With systemd, add a drop-in so dnsmasq starts after tailscaled:

```ini
# /etc/systemd/system/dnsmasq.service.d/tailscale.conf
[Unit]
After=tailscaled.service
Wants=tailscaled.service
```

## Add one split DNS entry in the admin console

1. Open https://login.tailscale.com/admin/dns
2. Nameservers → Add nameserver → Custom
3. Enter the home server's Tailscale IP (`<tailnet-ip>`)
4. Enter `example.com` as 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](https://caddyserver.com) with Cloudflare DNS-01, see [Caddy Cloudflare DNS-01: Get Wildcard TLS Certificates](/posts/caddy-cloudflare-dns-challenge/). 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:

```powershell
nslookup app.example.com
```

With 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.


---

> Author: Nite  
> URL: https://www.nite07.com/en/posts/tailscale-split-dns/  

