A DNS proxy server for Tailscale networks that enables per-identity DNS routing, domain rewriting, and 4via6 translation based on ACL grants.
- Per-identity DNS routing: Route DNS requests to different backend servers based on the requesting node's identity
- Domain rewriting: Transparently rewrite domains (e.g.,
cluster1.local→cluster.local) before forwarding - 4via6 translation: Convert IPv4 addresses to Tailscale's 4via6 IPv6 addresses using site IDs
- Backend failover: Automatic failover between multiple DNS backends with health checking
- Kubernetes support: Native state storage in Kubernetes Secrets
- Grant-based configuration: Configure behavior through Tailscale ACL grants
tsdnsproxy runs as a tsnet application on your tailnet, listening on configurable addresses (default: Tailscale IP port 53). When it receives DNS requests:
- Identifies the requesting node using LocalAPI whois
- Retrieves DNS grants from the node's capabilities
- Matches the query domain against grant rules
- Applies configured transformations (rewrite, backend selection)
- Forwards the query to the appropriate backend
- Optionally translates IPv4 responses to 4via6 IPv6 addresses
- Returns the response to the client
Configure DNS behavior through Tailscale ACL grants:
- dns: Backend DNS servers to forward queries to (with failover)
- rewrite: optional - Rewrite domain before forwarding (e.g.,
api.cluster1.local→api.cluster.local) - translateid: optional - Controls DNS handling mode:
- Omit or < 0: Standard forwarding mode (forwards queries to backends, returns responses as-is)
- 0: Authoritative mode without 4via6 translation (resolves from backends, returns A/AAAA records directly)
- > 0: Authoritative mode with 4via6 translation (converts A records to AAAA using site ID)
{
"grants": [
{
"src": ["user@example.com", "group:engineering"],
"dst": ["tag:tsdnsproxy"],
"app": {
"rajsingh.info/cap/tsdnsproxy": [
{
"cluster1.local": {
"dns": ["10.1.0.10:53", "10.1.0.11:53"],
"rewrite": "cluster.local",
"translateid": 1
},
"cluster2.local": {
"dns": ["10.2.0.10:53"],
"translateid": -1
}
}
]
}
}
]
}docker run -d \
--name tsdnsproxy \
-e TS_AUTHKEY=tskey-auth-YOUR-KEY \
-e TSDNSPROXY_HOSTNAME=tsdnsproxy \
-e TSDNSPROXY_LISTEN_ADDRS=tailscale,0.0.0.0:53 \
-p 53:53/udp \
ghcr.io/rajsinghtech/tsdnsproxy:latest- Update the auth key in
k8s/deployment.yaml - Deploy using kubectl:
kubectl apply -k k8s/Or with kustomize:
kustomize build k8s/ | kubectl apply -f -go install github.com/rajsinghtech/tsdnsproxy/cmd/tsdnsproxy@latest
tsdnsproxy -authkey tskey-auth-YOUR-KEYTS_AUTHKEY: Tailscale authentication key (required)TS_CONTROLURL: Custom control server URL (optional)TSDNSPROXY_HOSTNAME: Hostname on tailnet (default:tsdnsproxy)TSDNSPROXY_STATE_DIR: State directory (default:/var/lib/tsdnsproxy)TSDNSPROXY_STATE: State storage backend (e.g.,kube:secret-name)TSDNSPROXY_OVERRIDE_DNS: Override host DNS servers (comma-separated, defaults to host's resolvers)TSDNSPROXY_LISTEN_ADDRS: Listen addresses (default:tailscale) - see Network ConfigurationTSDNSPROXY_HEALTH_ADDR: Health check endpoint address (default::8080)TSDNSPROXY_VERBOSE: Enable verbose logging (default:false)
tsdnsproxy \
-authkey tskey-auth-YOUR-KEY \
-hostname tsdnsproxy \
-listen-addrs tailscale,0.0.0.0:53 \
-statedir /var/lib/tsdnsproxy \
-state kube:tsdnsproxy-state \
-override-dns 8.8.8.8:53,8.8.4.4:53 \
-cache-expiry 5m \
-health-addr :8080 \
-verboseDomains in grants act as wildcards:
- Grant for
cluster.localmatches:cluster.localapi.cluster.localsvc.api.cluster.local
Most specific match wins:
- Query:
api.svc.cluster.local - Grants:
cluster.local,svc.cluster.local - Winner:
svc.cluster.local
tsdnsproxy supports three DNS handling modes controlled by the translateid field:
Queries are forwarded to backend DNS servers and responses are returned as-is. Use this for normal DNS proxying without modification.
{
"cluster.local": {
"dns": ["10.0.0.10:53"],
"rewrite": "svc.cluster.local",
"translateid": -1
}
}Behavior:
- Forwards queries to backend servers
- Returns responses unchanged (A, AAAA, CNAME, etc.)
- Backend handles all query types
- Recommended for most use cases
tsdnsproxy resolves queries authoritatively by querying backends directly and returning A/AAAA records without modification.
{
"cluster.local": {
"dns": ["10.0.0.10:53"],
"translateid": 0
}
}Behavior:
- Queries backend for A/AAAA records
- Returns records directly without forwarding full response
- Other query types return NODATA
- Use when you need authoritative responses without 4via6
A records are converted to AAAA records using Tailscale's 4via6 format, allowing IPv4-only services to be accessed over Tailscale's IPv6 network.
{
"cluster.local": {
"dns": ["10.0.0.10:53"],
"translateid": 42
}
}Behavior:
- A queries return NODATA
- AAAA queries return synthetic 4via6 addresses
- IPv4
10.1.2.3with Site ID42→fd7a:115c:a1e0:b1a:0:2a:a01:203 - Enables IPv4 services over Tailscale's IPv6 network
/health: Returns JSON health status/ready: Returns 200 when ready, 503 when not
Route DNS for different clusters while maintaining consistent naming:
{
"prod.cluster.local": {
"dns": ["10.1.0.10:53"],
"rewrite": "cluster.local",
"translateid": -1
},
"staging.cluster.local": {
"dns": ["10.2.0.10:53"],
"rewrite": "cluster.local",
"translateid": -1
}
}Developers can use api.cluster.local and get routed to the correct cluster based on their identity. Using translateid: -1 ensures standard DNS forwarding without modification.
Different teams see different DNS results:
{
"grants": [
{
"src": ["group:team-a"],
"dst": ["tag:tsdnsproxy"],
"app": {
"rajsingh.info/cap/tsdnsproxy": [{
"internal.local": {
"dns": ["10.1.0.10:53"],
"translateid": -1
}
}]
}
},
{
"src": ["group:team-b"],
"dst": ["tag:tsdnsproxy"],
"app": {
"rajsingh.info/cap/tsdnsproxy": [{
"internal.local": {
"dns": ["10.2.0.10:53"],
"translateid": -1
}
}]
}
}
]
}Access IPv4-only Kubernetes services over Tailscale's IPv6 network using 4via6 translation:
{
"site1.k8s": {
"dns": ["10.1.0.10:53"],
"rewrite": "svc.cluster.local",
"translateid": 1
},
"site2.k8s": {
"dns": ["10.2.0.10:53"],
"rewrite": "svc.cluster.local",
"translateid": 2
}
}Queries for api.site1.k8s return synthetic AAAA records that route to the IPv4 service via Tailscale.
go build -o tsdnsproxy ./cmd/tsdnsproxygo test ./...docker build -t tsdnsproxy:latest .This project is built by the Tailscale community. It is not an official Tailscale product.