Split DNS setup with tsdnsproxy for overlapping CIDR ranges querying over 4via6 subnets
TailscaleでAmazon VPCで使用しているCIDRレンジをsubnet routingしようとすると、高確率でレンジが衝突します。 到達性については4via6という仕組みで解決しますが、DNSについてはもうひと工夫必要となり、そこが今回の記事の本題となります。
例となるシナリオ
前提
- AWS上の2つのVPCをsubnet routingでTailscaleへ繋ぎ込む
- VPC-1とVPC-2は
10.0.0.0/16を使用している (overlapping CIDR ranges) - VPC内ではAWS Cloud Mapを使用し、Route53のPrivate DNS hosted zoneでサービスディスカバリを構成している
- つまり、VPC内のAmazon Provided DNS (10.0.0.2)とRoute53が連携している状況
- VPC-1
- 4via6 site-id: 1
- 4via6 subnet:
fd7a:115c:a1e0:b1a:0:1:a00:0/112(tailscale debug via 1 10.0.0.0/16) - Cloud Map Namespace:
stg.example.internal
- VPC-2
- 4via6 site-id: 2
- 4via6 subnet:
fd7a:115c:a1e0:b1a:0:2:a00:0/112(tailscale debug via 2 10.0.0.0/16) - Cloud Map Namespace:
prd.example.internal
- 生成した2つの4via6 subnetをそれぞれのsubnet routerから広告する
解決したい課題
- VPC内のIPv4アドレスが分かれば4via6のIPv6アドレスへ変換することで、stg/prdの到達性が得られる (例えばRDSなど)が、変換が手動
- VPC内では
rds.stg.example.internalで済むところが、 Tailnet内からアクセスするには同じDNS名が使えない- 理由1: TailscaleのSplit DNS機能で
stg.example.internalとprd.example.ineternalに対して4via6アドレスでAmazon Provided DNS (10.0.0.2)を指定しても得られるIPv4アドレスでは到達できない (overlappingしているから) - 理由2: 仮にVPC内のIPv4アドレスをAmazon Provided DNS経由で得られても、到達するには4via6アドレスのAAAAレコードに変換するプロキシが必要
- 理由1: TailscaleのSplit DNS機能で
解決策: tsdnsproxy
DNSプロキシ実装 tsdnsproxy を導入し、Split DNSと 透過4via6アドレス変換を実装することで、VPC内と同じDNS名でアクセスが可能になります。
tsdnsproxyについて
tsdnsproxy はTailscaleのコミュニティ (作者はTailscaleの社員) OSSプロダクトで、以下の特徴があります。
- Go製のプロダクトで、tsnetライブラリで構築されているため、単体でtailnetに参加することが可能
- ドメインベースでupstream DNS (backend)を設定できる
- upstream毎に4via6変換をするか設定できるので、tsnet-awareなシンプルなDNSプロキシとしても使用可能
- 設定はACLのGrants内に書けるので、メンテナンス性に優れている
実装
以下の手順で実装していきます。
- 設定をGrantsに記述する
- Auth Keyの発行
- tsdnsproxyの起動
- 動作確認
- Tailscaleの管理画面 - DNS - Add Nameserverで “Restrict to domain” (Split DNS) としてtsdnsproxyのTailnet IPを指定する
Step 1: tsdnsproxy設定
ACLのtsdnsproxyに関するGrantsを以下のように設定します。
{
"grants": [
{
"app": {
"rajsingh.info/cap/tsdnsproxy": [
{
"stg.example.internal": {
"dns": [
"[fd7a:115c:a1e0:b1a:0:1:a0f:2]:53"
],
"translateid": 1
},
"prd.example.internal": {
"dns": [
"[fd7a:115c:a1e0:b1a:0:2:a0f:2]:53"
],
"translateid": 2
}
}
]
},
"dst": [
"tag:tsdnsproxy"
],
"src": [
"*"
]
}
]
}
ポイント:
- upstream backendは複数書けますが、今回の用途ではAmazon Provided DNS1つを指定しています。
- translateidが4via6で使用するsite-idになります。0または0未満で挙動を変えられます。
tsdnsproxyノード自体はtag:tsdnsproxyで参加させ、ACLで他のノードから到達できるようにしておきます。また、逆にtsdnsproxyのノードから転送先に対しても到達できるようにしておきます。
Step 2: Auth Keyの発行
Tailscale管理画面 - Settings - Keys からAuth keysを発行します。
Device Settingsでは、
- Pre-approved
- Tags -
tag:tsdnsproxy(なければ先にACL内でtagOwnersを設定してください)
を選択します。Auth Keyはreusableである必要はありません。1度tailnetに参加できれば、以後はnode keyで参加できるためです。
Step 3: tsdnsproxyの起動
今回は docker compose + systemd でサービス化しました。ここではdocker composeの設定を記します。systemdのことはLLMに聞けば教えてくれます。
services:
tsdnsproxy:
image: ghcr.io/rajsinghtech/tsdnsproxy:main
container_name: tsdnsproxy
restart: unless-stopped
environment:
- TS_AUTHKEY=${TS_AUTHKEY}
- TSDNSPROXY_HOSTNAME=${TSDNSPROXY_HOSTNAME:-tsdnsproxy}
- TSDNSPROXY_STATE_DIR=/var/lib/tsdnsproxy
- TSDNSPROXY_LISTEN_ADDRS=${TSDNSPROXY_LISTEN_ADDRS:-tailscale}
- TSDNSPROXY_HEALTH_ADDR=:8080
- TSDNSPROXY_VERBOSE=${TSDNSPROXY_VERBOSE:-true}
- TSDNSPROXY_ACCEPT_ROUTES=true
volumes:
- tsdnsproxy-state:/var/lib/tsdnsproxy
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/ready"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
volumes:
tsdnsproxy-state:
ポイント:
TSDNSPROXY_ACCEPT_ROUTESをtrueにすることで4via6のsubnet routesをtsdnsproxyが認識できるようになります- 自動的にDNSクエリの送信にtsnet.Server#Dialが使われるようになります
- この機能はわたしが実装しました (宣伝)
設定ができれば docker compose up で起動してください。
Step 4: 動作確認
tsdnsproxyがtailnet IP 100.93.1.1 で動作しているとします。
dig @100.93.1.1でまずは応答があるか確認します.(root)のNSの一覧が返ってくるはずです- tsdnsproxy側にも接続があったことを示すログが出力されているはずです
基本的な動作は問題ないとして、次にCloud Map連携の動作確認をします。VPC-1とVPC-2のCloud Mapのサービスに1つ追加しておきます
例えば、 VPC-1に dns.stg.example.internal として 10.0.0.2 (Amazon Provided DNS) を登録します。
VPC-1内で dns.stg.example.internal を名前解決してみます。なお、EC2インスタンスがtailnetに参加している場合Split DNS設定が反映されているため4via6経由になってしまいます。subnet router配下のtailnet未参加のノードはこの影響を受けません。
# VPC-1内のEC2インスタンス (このインスタンスはtailnetに参加していない場合)
dig +short dns.stg.example.internal
10.0.0.2
# VPC-1内のEC2インスタンス (このインスタンスはtailnetに参加している場合
dig +short @10.0.0.2 dns.stg.example.internal
10.0.0.2
では、通常のtsdnsproxy経由で名前解決してみましょう。AAAAレコードのみに応答します。
dig +short @100.93.1.1 -t aaaa dns.stg.example.internal
fd7a:115c:a1e0:b1a:0:1:a00:2
きちんと4via6アドレスに変換されていることが確認できます。
Step 5: Split DNSを設定する
仕上げにTailscale管理画面からSplit DNSを構成します。
Tailscaleの管理画面 - DNS - Add Nameserver (Custom) で “Restrict to domain” (Split DNS) としてtsdnsproxyのTailnet IP (ここでは100.93.1.1)を指定します。

Split DNS
Exit Node使用時にも有効にした場合は “Use with exit node” を指定しておきます。
最後の動作確認として、tailnetに参加しているlaptop等で名前解決をしてみます。
dig -t aaaa dns.stg.example.internal
; <<>> DiG 9.10.6 <<>> -t aaaa dns.stg.example.internal
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27426
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available
;; QUESTION SECTION:
;dns.stg.example.internal. IN AAAA
;; ANSWER SECTION:
dns.stg.example.internal. 300 IN AAAA fd7a:115c:a1e0:b1a:0:1:a00:2
;; Query time: 59 msec
;; SERVER: 100.100.100.100#53(100.100.100.100)
;; WHEN: Sun Jan 25 12:26:14 JST 2026
;; MSG SIZE rcvd: 75
TailscaleのMagicDNS (100.100.100.100)がきちんとSplit DNSを解決できており、tsdnsproxyによって4via6アドレスに変換されていることが確認できます。ACLで到達性があればそのまま接続可能となります。
最後に
tsdnsproxyのGrantsのsrcを制御することで、誰にどの名前解決を許可するかをさらに細かく制御できます。もっというと、srcによって同じドメインを別のbackendに転送することができるので、ドメインが被っていても一部対応可能です。
tsdnsproxyでSplit DNSと4via6を組合せることで構成の幅がさらに広がりました。