Tsdnsproxy
Tailscale dns proxy to handle 4via6 conversions and domain rewrites via ACL's
Install / Use
/learn @rajsinghtech/TsdnsproxyREADME
tsdnsproxy
A DNS proxy server for Tailscale networks that enables per-identity DNS routing, domain rewriting, and 4via6 translation based on ACL grants.
Features
- 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
How It Works
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
ACL Grant Configuration
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
}
}
]
}
}
]
}
Installation
Docker
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
Kubernetes
- Update the auth key in
k8s/deployment.yaml - Deploy using kubectl:
kubectl apply -k k8s/
Or with kustomize:
kustomize build k8s/ | kubectl apply -f -
Binary
go install github.com/rajsinghtech/tsdnsproxy/cmd/tsdnsproxy@latest
tsdnsproxy -authkey tskey-auth-YOUR-KEY
Configuration
Environment Variables
TS_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-nameorarn:aws:ssm:...)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_ACCEPT_ROUTES: Accept subnet routes and query DNS over tailnet (default:false)TSDNSPROXY_ADVERTISE_TAGS: ACL tags to advertise (comma-separated, default: none)TSDNSPROXY_VERBOSE: Enable verbose logging (default:false)
Command Line Flags
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 \
-verbose
Domain Matching
Domains 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
DNS Handling Modes
tsdnsproxy supports three DNS handling modes controlled by the translateid field:
Standard Forwarding Mode (translateid < 0 or omitted)
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
Authoritative Mode Without Translation (translateid: 0)
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
4via6 Translation Mode (translateid > 0)
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 Checks
/health: Returns JSON health status/ready: Returns 200 when ready, 503 when not
Example Use Cases
Multi-Cluster Kubernetes
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.
Split-Horizon DNS
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
}
}]
}
}
]
}
Split-Horizon DNS over 4via6
Forwarding DNS queries over 4via6 allows DNS servers to run on overlapping IP or CIDR ranges. Enable TSDNSPROXY_ACCEPT_ROUTES to accept the 4via6 subnets (this also enables the TS dialer).
{
"site1.vpc": {
"dns": ["[fd7a:115c:a1e0:b1a:0:1:a0f:2]:53"],
"translateid": 1
},
"site2.vpc": {
"dns": ["[fd7a:115c:a1e0:b1a:0:2:a0f:2]:53"],
"translateid": 2
}
}
In this case, Both DNS server for site1.vpc and site2.vpc run on `10.15.0.2/16 but it will be queried over 4via6. This setup is useful when a service discovery differs between VPCs.
IPv4-only Services via Tailscale IPv6
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.
Development
Building
go build -o tsdnsproxy ./cmd/tsdnsproxy
Testing
go test ./...
Docker Build
docker build -t tsdnsproxy:latest .
Community
This project is built by the Tailscale community. It is not an official Tailscale product.
License
Related Skills
node-connect
343.1kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
90.0kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
343.1kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
343.1kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
