Unboundでお手軽DNSシンクホール

Unboundのロゴ

背景 見出しにジャンプ

以前からルータのDNSフォワーダにDNSシンクホールをさせていた。
しかし近年、ChromiumやAppleデバイスでブロック漏れが発生する理由に、対応していないクエリタイプ(SVCB/HTTPS)があることや、FQDN単位でのルールしか書けないためにサブドメイン対応が冗長だったこと、CNAME Cloakingを阻止できないことからDNSソフトウェアの利用は視野に入れていた。
YAMAHA RTXと異なり、NEC IXルータではクエリタイプ毎にフォワード先を切り替えられないため、検討が加速した。BINDやknot、PowerDNSがある中、初学者にも書きやすそうな縛りのない構文が魅力的と感じてUnboundを選んだ。
結果的にはソースコードの変更を要したが、満足に機能している。

更新履歴 見出しにジャンプ

日時 修正・追加内容
2024-08-15
  • UnboudとSELinuxを共存させるには、SELinuxを止める必要があった
  • 歯切れが悪すぎた項目を、歯切れの悪い内容に修正
  • IPv6推進派でないことを明記
  • Twitterの隠れた画像・動画サーバに言及
  • 鶏と卵になるので、セルラー時にVPN経由のDoTをActiveにする設定を削除
2024-08-26
  • block_aaaaの追加
  • Chromiumに対応したincoming-num-tcp
  • 4over6.info
  • infra-cache-*設定により、低速なDNSサーバには速やかに退いていただく
  • private-addressから除外するprivate-domain
  • SpamhausのDROPルールを使う
  • AdmiralがHTTPS RRで漏れてたorz
  • rpz-cname-override:について、完全に誤った理解をしていた
  • 暗号化DNSの自動設定について、DDRとDNRの対応状況を踏まえた説明
2024-09-19
  • QoSとの連係
2024-09-30
  • DoHをブロックしてくれるかもしれない、use-application-dns.net
  • iCloud Private Relayのブロックと、「IPアドレスのトラッキングを制限」による通信の許可
  • Twitter(twitter.com, x.com, t.co etc.)のIPv6化
  • TestFlightのプッシュ通知をIPv6化から除外
  • iOS 18でもDDRは機能しない

ソースコードの変更 見出しにジャンプ

git clone https://github.com/NLnetLabs/unbound.git -b release-1.21.0rc1 -o upstream
-	if(!scrub_message(pkt, prs, &iq->qinfo_out, iq->dp->name, 
-		qstate->env->scratch, qstate->env, qstate, ie)) {
-		/* if 0x20 enabled, start fallback, but we have no message */
-		if(event == module_event_capsfail && !iq->caps_fallback) {
-			iq->caps_fallback = 1;
-			iq->caps_server = 0;
-			iq->caps_reply = NULL;
-			iq->caps_response = NULL;
-			iq->caps_minimisation_state = DONOT_MINIMISE_STATE;
-			iq->state = QUERYTARGETS_STATE;
-			iq->num_current_queries--;
-			verbose(VERB_DETAIL, "Capsforid: scrub failed, starting fallback with no response");
-		}
-		iq->scrub_failures++;
-		goto handle_it;
-	}
cd unbound
git remote add PRblockaaaa https://github.com/strlcat/unbound.git
git cherry-pick 026af6b

ビルド 見出しにジャンプ

依存関係を解消
Installation — Unbound 1.19.3 documentation
DNSアプリの機能比較 on Rocky8編 - 20230623-kosaka.pdf

sudo dnf install git tar gcc make openssl openssl-devel expat-devel systemd-devel flex bison
sudo dnf install libevent-devel # outgoing-port-permitが1024を超えているので必要
sudo dnf install clang # gccの代わりにclangでコンパイルしたいなら

systemdあり, 1024ポート制限超え--with-libevent有効でビルド
【学習メモ】unboundをインストールして最適化と言われる内容を適用する #unbound - Qiita
build with --enable-fully-static cannot specify username · Issue #886 · NLnetLabs/unbound
[Unbound-users] unbound performance tuning
Performance Tuning — Unbound 1.19.3 documentation

cd unbound
./configure --help

# --enable-subnet: response-ip-dataを壊す?
# --disable-explicit-port-randomisation: 遅い?
./configure --enable-systemd --enable-static --enable-fully-static --with-libevent #CFLAGS="-O3" CC=clang CXX=clang++
make -j4

インストール 見出しにジャンプ

sudo make install
cd contrib
sudo cp unbound.service unbound.socket /usr/lib/systemd/system/

CentOS8でunboundをソースからコンパイルして使う #dns - Qiita

サービスの起動 見出しにジャンプ

unbound.confを触らないでunboundを立ち上げても、DNSSECのトラストアンカーを更新するunbound-anchor.serviceが実行されていないため、おそらく起動しない。
今回のユースケースでは、DNSSECを無効化で対応した。

sudo systemctl daemon-reload
sudo systemctl enable unbound # OS再起動時に自動起動
sudo systemctl start unbound
sudo systemctl status unbound

OS再起動時にunboundが起動しない 見出しにジャンプ

error: can't bind socket: Cannot assign requested address for...

これで壊れて
- Fix #773: When used with systemd-networkd, unbound does not start · NLnetLabs/unbound@5c041c0 · GitHub
これで直った
- For #773: In contrib/unbound.service.in set unbound to start after · NLnetLabs/unbound@d43760a · GitHub
感謝人人

StartLimitBurstを超過しないために、RestartSecを伸ばす回避策もあったようだ。
私的サーバー構築日誌:LAN内DNSサーバー Unbound #dns - Qiita
Systemd Restart=always is not honored - Unix & Linux Stack Exchange

SELinux 見出しにジャンプ

SELinuxが有効な環境で、Unboundには下記のようなエラーが発生すると思う。

journalctl -xeu unbound.service
# unbound.service: Failed to set up special execution directory in /var/lib: Permission denied
# unbound.service: Failed at step STATE_DIRECTORY spawning /usr/local/sbin/unbound: Permission denied

実際、UnboundにおいてもSELinuxを寛容モードに変更するとエラーは無くなった。

getenforce
# Enforcing

# permissiveモード
sudo grubby --update-kernel ALL --args enforcing=0
getenforce
# Permissive

Changing SELinux States and Modes :: Fedora Docs
SELinuxの設定ファイル/etc/selinux/config公式ドキュメントでは、無効化selinux=0と再度有効化するコマンドが平然と紹介されているが、後者は二度とOSが立ち上がらなくなる点に留意したい。
「SELinuxのせいで動かない」撲滅ガイド #CentOS - Qiita

ソフトウェアは本来のLinuxの動作に基づいてエラーを処理するため、誤った動作をするSELinux環境は、エラーメッセージがアテにならない。監査ログ/var/log/audit/audit.logと併せて読む必要がある。
Fedora ServerにプリインストールされているCockpitは、unboundをSELinuxから除外するソリューションを提案した。これによりUnboundにSELinuxが強制されなくなるため、SELinuxを強制することができた。

sudo ausearch -c '(unbound)' --raw | audit2allow -M my-unbound
sudo semodule -X 300 -i my-unbound.pp

# Enforcingに戻す
sudo grubby --update-kernel ALL --remove-args enforcing

アンインストール 見出しにジャンプ

sudo systemctl stop unbound
sudo make uninstall
sudo rm -r /usr/local/etc/unbound

firewalld 見出しにジャンプ

端末がUnboundへクエリする、内向きのトラフィックを許可する。外向きは通常許可されている。

# Do53 53/tcp 53/udp
sudo firewall-cmd --add-service=dns --permanent
# DoT 853/tcp
sudo firewall-cmd --add-service=dns-over-tls --permanent

sudo firewall-cmd --reload
sudo firewall-cmd --list-all

上記はWAN境界で外部からのアクセスを破棄できている前提のFW設定だ。それが難しいなら、access-controlは気休めにしかならないため、少なくともLinuxのFWで落としたい。

unbound.conf 見出しにジャンプ

パッケージマネージャから導入すると/etc/unbound/unbound.conf自ビルドしたunbound/usr/local/etc/unbound/unbound.confをデフォルトで使う。
この記事では後者に寄せて書いている。
FreeBSD に於ける/etc と /usr/local/etc

configの更新はunbound-control reloadで行えるが、一部systemctl restart unboundを要する。
unbound-checkconfで設定に誤りがないか事前に確認できる。
unboundconf/update.sh at main · nyanshiba/unboundconf

DNSSECの無効化 見出しにジャンプ

キャッシュDNSソフトウェアとして、unbound.confのデフォルトでDNSSEC検証が有効になっている。
しかしDNSフォワーダとしてUnboundを利用するユースケースでは、例えばISPのキャッシュDNSがDNSSEC検証に対応していないし、local-zoneやRPZで応答を改ざんすると機能しないので、無効化した。

server:
# DNSSEC OKじゃない
- # disable-edns-do: no
+ disable-edns-do: yes

# validatorモジュールの無効化
- module-config: "ipsecmod validator iterator"
+ module-config: "iterator"

# トラストアンカーの削除
- auto-trust-anchor-file: "/usr/local/etc/unbound/root.key"
- auto-trust-anchor-file: "/var/lib/unbound/root.key"
- trusted-keys-file: /etc/unbound/keys.d/*.key

Unbound - DNSSECを無効にする方法 | 日本Unboundユーザー会 2. 3.
NLnet Labs Documentation - Unbound - Howto Turn Off DNSSEC

Listenインターフェースの設定 見出しにジャンプ

https://unbound.docs.nlnetlabs.nl/en/latest/manpages/unbound.conf.html#unbound-conf-interface

server:
	interface: 10.0.3.5@53
	port: 53
	# do-udp: yes
	# do-tcp: yes
	incoming-num-tcp: 1024 # libeventなしの場合、*-num-tcpの合計<=125
  • ここで指定したインターフェースのIPアドレスをDHCP、IPv6アドレスをDHCPv6やRA RDNSSオプションで通知することで、端末はUnboundで名前解決するようになる。フレッツ東のHGWは、DHCP機能で任意のDNSサーバを通知できないらしい。
    暗号化DNSはDDRやDNRで自動設定する。
  • IPアドレスのほか、eth0などのインターフェース名にも対応している。RHEL系では0.0.0.0 ::0は使えない。
  • ブラウザエンジンのシェアの殆どを占めるChromiumは、UDPフラグメントに関係なく、ソースポートやtransaction IDのエントロピーが低いとTCPにフォールバックする。
    Huge amount of "ERR_NAME_NOT_RESOLVED" error [40891645] - Chromium
    標準的なブラウザ(Firefox)にこのような実装はないが、接続性のためネットワーク側での対応は必要だ。

Unboundはincoming-num-tcpのデフォルト値が非常に小さいので注意
summerday2023-chrome - 20230623-yamaguchi.pdf
日本DNSオペレーターズグループ | DNS Summer Day 2023

https://github.com/NLnetLabs/unbound/issues/366#issuecomment-844165241
そもそもプライベートネットワークでListenするDNSサーバ宛にTCPを強制する意味はほぼないし、NAT動作によっては内側ポートをトリガーにするだけでは不十分だ

EDNS buffer size 見出しにジャンプ

DNSのペイロードサイズは512 bytesまでだったが、EDNS拡張(頭痛が痛い?)により4096 bytesまで許可された。IPv4は理論値1472 bytes、IPv6は1452 bytesを超えたらフラグメントするしかないが、TCPフォールバックのオーバーヘッドを嫌ってかRFC 6891はMTUを考慮しない最大値を推している
しかし実際のネットワークでは、UDPのフラグメントは再構成のオーバーヘッドはおろか、接続性やセキュリティやの問題があると分かっている。

IPv6 Fragmentation Failure Rate: 56%
3-slides-119-iepg-sessa-is-the-dns-ready-for-ipv6-geoff-huston-00.pdf

server:
	# edns-buffer-size: 1232

UnboundのデフォルトはDNS flag dayで提案された1232 bytesで、IPv6の最小MTU 1280 bytesからUDPヘッダ 48 bytesを除いたデータグラムのサイズを示している。DNSSECを使っていない状態では、1232 bytesを超える応答は殆どなく、パフォーマンスへの影響はない。

EDNSを考慮していない機器との互換性をとって512 bytesとするのもよいだろう。1220 bytes以下のBufsizeでは、DNSSECを無効化が必須だ。
draft-ietf-dnsop-avoid-fragmentation-16
dig の適切な使い方 (10) モダンなサーバ (EDNS 対応) のポイント

IPv6でNGNのキャッシュDNSにフォワードするだけなら、大きなBufsizeを設定してもフラグメントは起こらないはずだ。
DNS 2XL | blabs
dns-troubleshoot.pptx - dns-troubleshoot-1.pdf

QUICもUDPのフラグメントを回避するために、小さいデータグラムを使用している。この方面からPMTUDが発展すると期待したい。
QUIC Analysis - A UDP-Based Multiplexed and Secure Transport

id.server 見出しにジャンプ

クエリクラスがINではなくCHなことに留意する。

dig id.server. ch txt +noall +answer
dig hostname.bind. ch txt +noall +answer
# id.server.              0       CH      TXT     "fedora"

dig version.server. ch txt +noall +answer
dig version.bind. ch txt +noall +answer
# version.server.         0       CH      TXT     "unbound 1.20.0"

dig trustanchor.unbound. ch txt +noall +answer

ISPのキャッシュDNSではREFUSEDされたので、模倣した。

server:
	hide-identity: yes
	hide-version: yes
	hide-trustanchor: yes

forward-addr 見出しにジャンプ

unboundは大前提としてフルリゾルバのためのソフトウェアだが、local-zoneRPZを利用するだけのユースケースにも対応している。
forward-addrを設定すると、CPEが搭載しているようなDNSフォワーダ/プロキシのように、特定のキャッシュDNSにFQDNを問い合わせる。

IPv4でDNSを使える環境は限られる 見出しにジャンプ

IPv4/IPv6接続判定ツールでv6プラス判定が出たり、MAP-E展開にこなれていないISPをお使いなら、8.8.8.81.1.1.1を使うべきでないNAT Behaviorを制御できるなら止めないが、多くはそうでないだろう。
フレッツのIPv6 IPoEおよびIPv4 over IPv6では、キャッシュDNSはIPv6のみ提供され、IPv4は提供されない。そもそもIPv4/IPv6デュアルスタックの中核となるDNSをIPv4延命技術に依存していたら、それはIPv6への移行でもなんでもなく障害点が増えただけのIPv4&IPv6キメラスタックだ。該当する環境は、IPv6で問い合わせよう。

些細なことを言えば、Do53のクエリ毎にNAPTキャッシュ生成&ショートパケットをencapslationしながらブラウジングしたいかという話でもある。IPv4 PPPoEにパケットを流すのに比べて、IPv4 over IPv6では気を遣ってしまう。

Public DNSを使うべきか 見出しにジャンプ

フォワード先の候補には、ISPが提供するキャッシュDNSや、GoogleやCloudflareなどが提供するPublic DNSが挙げられる。
パフォーマンスの観点でいえば、ISPのDNSが適切に運用されていないとき、後者はときによい。

ISPのDNSキャッシュサーバはTTLを越えてキャッシュを保持するか?

ただしプライバシー面でいえば、日本では通信の秘密が保障されているし、セキュリティの観点では攻撃面は同一ASからthe Internetに広がる点には留意したい。特にDNS over 53(Do53)(Cleartext)でクエリすれば、AS外のインターネット区間で中間者攻撃される恐れがあるため、Public DNS利用時はDNS over TLS(DoT)やDNS over HTTPS(DoH)が望ましい。
8.8.8.8に対するBGPハイジャックの話:Geekなぺーじ

DoT、DoHはUDPを使わずDNS ampの踏み台に利用される恐れがないため、オープンリゾルバとしてIIJ以外のユーザにも開放
4. フォーカス・リサーチ(3)IIJとDNSの30年 | Internet Infrastructure Review(IIR)Vol.59 | IIJの技術 | インターネットイニシアティブ(IIJ)

ISPのDNSで機能するDo53に比べ、Public DNSで利用できるDoTやDoHは常にオーバーヘッドが課されるが、TCPやHTTP/2のフロー制御が効いてくるシーンもある。
残念ながらUnboundで利用する場合、端末はDo53でクエリを繰り出すので、ブラウザでDoHを利用するときのようなパフォーマンス上の恩恵はない。

[1907.08089] Comparing the Effects of DNS, DoT, and DoH on Web Performance

server:
	edns-tcp-keepalive: yes

forward-zone:
	name: '.' # 全てのゾーンをフォワードする
	forward-addr: 2606:4700:4700::1111@853 # Cloudflare DNS via DoT
	forward-addr: 2001:4860:4860::8888@443 # Google Public DNS via DoH
	forward-tls-upstream: yes

UnboundにもTCPコネクションの再利用tcp-reuse-timeout: 60000は一応あるが、DoTは(num-threads: 1でも)コネクションを度々切断していた。DoHは都度コネクションを張っていたのでオーバーヘッドになりそうだ。
Unbound 1.13.1 .. 1.16.1 memory leak (complete RAM exhaustion) due to high volume of TCP FD's · Issue #724 · NLnetLabs/unbound
[bug] DoT - TCP reset on egress · Issue #137 · NLnetLabs/unbound DoTサーバが切断してくる場合もあるとか

ISP内サーバの利用 見出しにジャンプ

フレッツ東のIPv6 IPoEを利用するISPは、CPEや端末に対してDHCPv6のオプションでキャッシュDNSサーバのIPv6アドレスを通知する。このとき、多くのISPがNTT東日本に割り当てられた共通のアドレス2404:1a8:7f01:a::3 2404:1a8:7f01:b::3を利用する。
NTT ComのIPv6 PPPoEでは、同ASのアドレスがIPCPv6で通知される。4ヘクステット目が4は東, 104は西になっており、BGP.Toolsによると送信元が東京, 大阪になっている。東京/大阪それぞれに9つのIPv6アドレス、IPv4は星の数ほどあった。
OCN モバイル ONE - Wi-Fiルーター:マルウェア不正通信ブロックサービスの適用を解除したい | 個人向けOCNお客さまサポート
twitterのDNS Attackが、OCNのDNSキャッシュサーバーに残っていたので記録
驚くべきことに、東京のサーバにフォワードしたときも、送信元にかなりの率で大阪が含まれていた。(フレッツ東のNTT Comにおいて)送信元が一貫してフォッサマグナを跨がなかったのは下記だけだった。DHCPv6やIPCPv6で自動設定される内容と若干異なっていた。

forward-zone:
	name: "."
	forward-addr: 2001:380:0:4::1 # nv6-ef701.ocn.ad.jp.
	forward-addr: 2001:380:0:4::2 # nv6-ef702.ocn.ad.jp.
	forward-addr: 2404:1a8:7f01:a::3

フレッツ東共通のa/bも同ASからクエリされており、bは送信元に大阪が含まれていた。IPv6 Hop Limitも1小さかった。各ISP内のキャッシュDNSを指すAnycastになっているのだろう。

NGN網内のサービス情報サイト(東)利用には、DHCPv6で通知されたサーチリストにあるflets-east.jp.ゾーンを、少なくともNGNのキャッシュDNSにフォワードする必要がある。

forward-zone:
	name: "flets-east.jp."
	forward-addr: 2404:1a8:7f01:a::3
	forward-addr: 2404:1a8:7f01:b::3

forward-zone:
	name: "iptvf.jp."
	forward-addr: 2404:1a8:7f01:a::3
	forward-addr: 2404:1a8:7f01:b::3

送信元アドレスやEDNS Client Subnetによって応答を変えるタイプのGSLBでは、キャッシュDNSによって接続先がレイテンシの小さいISP内キャッシュからピアリング、トランジットへと変わり、体験が悪化する場合がある。
国内トラフィックエンジニアリングの現状 | PPT
例えば、一部のISPで http://test.edge.apple/debug/ にアクセスすると、Apple Edge Cache*.ec.edge.appleが使われれているのではないかと思う。

同じドメイン名test.edge.apple.の応答がDNSによって変わっているので、ISPのDNSから別のものに変更することで体験が悪化する例といえる。ISPの力関係によって、同じサービスでも接続状況が異なるため、常に 「○○DNSがオススメ」はすべて詐欺だ。
顧客は無知を貫けばよいが、ISP視点で見ればトランジットのコスト増を招くため思わしくない。こちらの記事はそんなISP側の視点で書かれたものではないかと思う。
DNSを変更するとネットワークは速くなるか | IIJ Engineers Blog
Anyone having significant issues with some of Apple's services? ISP related? | MyBroadband Forum ECSも効くらしいので、Public DNSで利用できる可能性は高いかも?
IIRC you're replacing the whole OS inage on every update now. Also, their edge c... | Hacker News 逆にApple Edge Cacheが遅いからDNSをISPのものから変えている、という話もありそう。
このようなISP内キャッシュの利用に全てををフォワードname: '.'する必要はないが、サービスを逐一除外するほどのメリットがPublic DNSになければ、ISPのDNSを利用すればよい。

高速なDNSの自動選択 見出しにジャンプ

fast-server-permil: 0のとき、クエリ毎に複数のforward-server:が等コストでラウンドロビンされる。
Unboundにクエリを集約することでDNS RRLに引っかかりやすくなるのではないか、といった懸念を軽減できるかもしれない。

900/1000クエリに、3つのRTTが低いサーバを選択する例
https://unbound.docs.nlnetlabs.nl/en/latest/manpages/unbound.conf.html#unbound-conf-fast-server-permil

server:
	fast-server-permil: 900
	fast-server-num: 3

	# infra-host-ttl: 900
	infra-cache-min-rtt: 100
	infra-cache-max-rtt: 5000
	infra-keep-probing: yes
  • クエリが失敗したとき、タイムアウトの度にinfra-cache-min-rttからinfra-cache-max-rttの間で増加する再送タイマの後に再試行される。
    forward-addr先の再帰を待機するにはいささか過敏すぎるのでこれを100msecに伸ばし、NAPTやSPIのタイマー値を超える応答は待っても意味がないので、5000msecに制限した。
    https://unbound.docs.nlnetlabs.nl/en/latest/manpages/unbound.conf.html#unbound-conf-infra-cache-min-rtt
  • infra-cache-max-rttに到達または連続してタイムアウトしたforward-addrは、少なくともinfra-host-ttlの間使われない。短くするか、infra-keep-probing: yesで早められる。
  • infra-cacheの内容はdump_infraで確認できる。恐らくpingがRTT(Round Trip Time)を指し、rtt(roundtrip-timeout)(RTTではない)とrto(roundtrip backoff?)が再送タイマを指している。
    下記では、低速なDNSサーバは往々にして再送タイマが増加していることが分かる。この後、最も遅い2001:380:0:4::2はinfra-cacheから削除された。
unbound-control dump_infra
# 2001:380:0:4::2 . ttl 176 ping 79 var 96 rtt 463 rto 463 tA 0 tAAAA 0 tother 0 ednsknown 1 edns 0 delay 0 lame dnssec 0 rec 0 A 0 other 0
# 2404:1a8:7f01:a::3 . ttl 176 ping 16 var 4 rtt 100 rto 100 tA 0 tAAAA 0 tother 0 ednsknown 1 edns 0 delay 0 lame dnssec 0 rec 0 A 0 other 0
# 2001:380:0:4::1 . ttl 176 ping 44 var 49 rtt 240 rto 240 tA 0 tAAAA 1 tother 0 ednsknown 1 edns 0 delay 0 lame dnssec 0 rec 0 A 0 other 0
# 2001:380:0:4::3 . ttl 272 ping 20 var 8 rtt 100 rto 100 tA 0 tAAAA 0 tother 0 ednsknown 1 edns 0 delay 0 lame dnssec 0 rec 0 A 0 other 0
  • infra-keep-probingは有効化。全てのforward-addrのrtoがinfra-cache-max-rttに達したときにSERVFAILになるため。

Documenation clarification on RTT band · Issue #567 · NLnetLabs/unbound
NLnet Labs Documentation - Unbound - Unbound Timeout Information

CNAME Scrubbing 見出しにジャンプ

dig logincdn.msauth.net aaaa +noall +answer
logincdn.msauth.net.    139     IN      CNAME   lgincdnmsftuswe2.azureedge.net.
lgincdnmsftuswe2.azureedge.net. 373 IN  CNAME   lgincdnmsftuswe2.afd.azureedge.net.
lgincdnmsftuswe2.afd.azureedge.net. 71 IN CNAME firstparty-azurefd-prod.trafficmanager.net.
firstparty-azurefd-prod.trafficmanager.net. 30 IN CNAME shed.dual-low.s-part-0018.t-0009.t-msedge.net.      
shed.dual-low.s-part-0018.t-0009.t-msedge.net. 30 IN CNAME s-part-0018.t-0009.t-msedge.net.
s-part-0018.t-0009.t-msedge.net. 30 IN  AAAA    2620:1ec:bdf::46

logincdn.msauth.netはCNAMEチェーンが長いことで有名なFQDNの一つだ。
Resolving records through more than 8 CNAME fails due to hardcoded MAX_RESTART_COUNT · Issue #438 · NLnetLabs/unbound
フルリゾルバは、仮にlogincdn.msauth.net. IN CNAMEの応答にIN AAAA 2620:1ec:bdf::46が付随しても信頼しない。信頼できるt-msedge.net.の応答を聞くまでCNAMEチェーンを一つずつ再クエリする。このように信頼のないCNAMEチェーンを再検索することを、Scrubbing(擦る)という。
Strange DNS Samples 4. CNAME に付随する A を信じてはいけない

Clients should assume that other recordsmay have come from the server's cache. Where authoritative answersare required, the client should query again, using the canonical nameassociated with the alias.
RFC 2181: Clarifications to the DNS Specification

Unboundのforward-addr:はフルリゾルバと共通のコードベースなので、同じくCNAMEの解決に数往復掛かってしまう。パフォーマンスへの影響は大きい。
Consider disabling CNAME scrubbing for forwarded queries · Issue #132 · NLnetLabs/unbound
こちらのIssueでは、Do53では依然として利点があるとして話が進んでいるが、通信の秘密のない国や、Public DNSに限った話に聞こえる。殆どのスタブリゾルバはキャッシュDNSを信頼しているので、Scrubを止めても追加のリスクはない。
完全には止められていないが、ソースコードの変更により2往復で済むようになった。上記の例だとs-part-0018.t-0009.t-msedge.net.だけがScrubされるが、RPZを評価できるし、Appleのスタブリゾルバも同じ動作なので先読みされる形になる。

access-control-tag 見出しにジャンプ

dig @dns.home.arpa example.com +noall +comments
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 30804
;; flags: qr rd ad; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; EDE: 18 (Prohibited)

access-control:で許可されていない送信元は、アプリケーション層で拒否される。L7は保護にならないのでACLはネットワーク側で行うべきだ。

server:
	access-control: 10.0.0.0/21 allow

local-zone-tag:rpz-tag:で、一部の端末にDNSシンクホールを適用したいときに使うタグを、予め定義する。

server:
	define-tag: "admin dualstack guest windows"

	# access-control: 10.0.0.0/21 allow access-control-tag:にあればallowされる

	access-control-tag: 10.0.0.0/21 "dualstack"
	access-control-tag: 10.0.0.0/24 "dualstack admin"
	access-control-tag: 10.0.0.2/32 "dualstack admin windows"
	access-control-tag: 10.0.0.12/32 "admin"
	access-control-tag: 10.0.5.57/32 "dualstack windows"
	access-control-tag: 10.0.7.0/24 "dualstack guest"

この例では、小さいIP範囲10.0.0.2/32が優先されるが、大きいIP範囲10.0.0.0/21のタグを継承しないので、dualstackから冗長に書く必要がある。
逆にデュアルスタックでない端末10.0.0.12/32には書かなければ適用されない。

セキュリティ 見出しにジャンプ

価値の高い広告を提供するアドネットワークは、時にMalvertisingを仲介する悪意あるコンテンツになり下がることがある。紹介料で稼ぐメディアやその利用者、最終的には適切な広告出稿を行う広告主に不利益をもたらす。
このような管理下にないドメインへの対処に一般的なのはuBlock Originや拡張機能版AdGuardだ。復号後のパス名までルールに書けるのは強みである一方、拡張機能を全面的に信頼する必要があることや、複数の端末に自動的に展開できない欠点がある。

米CISAによると、Malvertising防止には拡張機能ではなく自組織のDNSでの提供が推奨されている。
Capacity Enhancement Guide: Securing Web Browsers and Defending Against Malvertising for Federal Agencies - Capacity_Enhancement_Guide-Securing_Web_Browsers_and_Defending_Against_Malvertising_for_Federal_Agencies.pdf
DNS型のセキュリティは、DNSの応答を改ざんし、ホストを取得できなくすることで、ネットワークレベルで防御する。
商用ではCisco UmbrellaやFortiGateが挙げられる。個人がセルフホストするものではpi-holeやAdGuard Homeがあるが、以前にも指摘したように、Appleデバイスでブロック漏れが発生していないか、パケットレベルで確認すべきだ。

local-zone 見出しにジャンプ

local-dataを用いると、権威のないゾーンのRRsetを編集できる。
応答を改ざんするので、当然DNSSECの信頼は失われる。DNSSECを無効にしないと動かない。
https://unbound.docs.nlnetlabs.nl/en/latest/manpages/unbound.conf.html#unbound-conf-local-zone
Ad DNSブロッキングおよび、kawango DNSブロッキングの実装をしてみたら、超快適だった とも ちゃ日記(Tomo cha) - 元大学生のOL日記-

local-zone type ログレベル local-data local-dataと異なるタイプ 子ゾーン response-ip 備考
deny 応答 (DROP) 端末はタイムアウトを待つので、何かしら応答すべき
inform_deny INFO
refuse 応答 REFUSED 非対応 response-ipでも使えるが、悟られたくない感じはある
always_refuse 常にREFUSED REFUSED
static 応答 NODATA,
local-data IN SOA
NXDOMAIN,
local-data IN SOA
非対応 ゾーン頂点にSOAレコードを置くと、全てのクエリタイプで否定応答が任意のTTL端末にキャッシュされる。
transparent 応答 NODATA 名前解決 非対応 local-dataを書けばtransparentゾーンが作成されるため、AS112ゾーンを除きlocal-zoneは書かなくてよい。
inform INFO 名前解決
always_transparent 常に名前解決 名前解決
typetransparent 応答 非対応

ゾーンでなくクエリタイプ単位で評価する性質から、AAAAはlocal-data、Aは名前解決といった混在が可能。
ただし、例えばlocal-dataにAAAAを設定しても、透過したHTTPSやA応答にCNAMEが含まれると矛盾が生じる。通常スタブリゾルバでは影響ないが、AppleデバイスはCNAME先を確認するためlocal-dataを参照させるには一捻り必要

redirect 応答
(CNAMEを解決)
NODATA local-data response-ip-data ほぼワイルドカードドメイン。唯一、local-dataに書かれたCNAMEを解決する。非常に遅いので、TTL付きの否定応答だけ欲しいならstaticを使う。
inform_redirect INFO 非対応
nodefault 応答 名前解決 非対応

逆引きクエリを抑制するためにハードコーディングされたAS112ゾーンデフォルト応答を無効化するだけ。local-dataを応答するために使うならstaticが確実。
第81回IETF報告:DNS関連WG報告 - JPNIC

block_a IN AはNODATA
他は名前解決
非対応

Happy Eyeballsさせず、IPv6接続のみ試行して欲しいときに使う。
typetransparentと同じくCNAME応答に注意。

block_aaaa IN AAAAはNODATA
他は名前解決
非対応

正式な機能ではないので、ソースコードの変更が必要。

always_nodata NODATA 非対応 プライベートリレーへのアクセスをブロックするときの、推奨された応答形式。

iCloudプライベートリレーに向けたネットワークやWebサーバの準備 - iCloud - Apple Developer
ただし、staticと違って否定応答は端末にキャッシュされない。モバイルデバイスのように、キャッシュフラッシュが面倒な端末の動作確認にはいいかも。

always_nxdomain NXDOMAIN
always_null 応答 IN A 0.0.0.0
IN AAAA ::
IN HTTPS等はNODATA
非対応 否定応答ではないが、A/AAAAに限りブロック応答が端末で3600秒キャッシュされる。

このため、レガシーなスタブリゾルバでは100%、ChromiumやAppleデバイスでは2/3のクエリを一行でキャッシュさせられる。
Windows以外での安全面を考えると否定応答が望ましい。
否定応答の方が高速な一方で、NXDOMAINだとFirefoxがDoHを試行するという話もあるが、0.0.0.0DoHのブレークアウトを防げる訳ではない。
I use Unbound in a similar way, although I return 0.0.0.0 rather than NXDOMAIN. ... | Hacker News

noview 応答 非対応 view機能から除外するゾーン
安全なTLDだけ通す 見出しにジャンプ

maliciousなドメインは日々変化する。TLDによってはAbuse対応が悪かったり、安価に取得できるためだ。
Networks, Countries, Registrars & TLD charts | Reputation Statistics | Spamhaus
Unboundでは. com. example.com.などのゾーン単位で操作できるので、いっそのことTLDごとブロックしてしまえばよい。
荒い網なので期待はできないが、例えば一体何のためかUser-Agentによるリダイレクト機能を持つ短縮URLサービスなどは一行で落とせる。

このようにルートゾーン.以下をリダイレクトすると、transparentしたTLD以外に否定応答を返せる。
syslogを取りたいのでinform_redirectにしているだけで、SOAを返すだけならstaticがよい

server:
	local-zone: "com." transparent
	local-zone: "." inform_redirect
	local-data: ". 60 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024080800 1800 900 604800 86400"

逆に、TLD毎にブラックリスト式で記述できる。AS112ゾーン以外は名前解決される。

server:
	local-zone: "com." static
	local-data: "com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1723351845 1800 900 604800 86400"

SOAレコードを置くと、SOAのTTLとMINIMUMフィールドまで(つまり短い方)ネガティブキャッシュが効くと期待できる。
否定応答がわからなくなる話 (11) RFC 2308: 5. 否定回答のキャッシング
これにより端末は高頻度でクエリしなくなるため、バッテリー寿命に優しいDNSができる。
また、SOAの内容は実際のものを参考にした。できる限り自然なRCODE・RRsetに徹して、往生際の悪いソフトウェアにUnboundがブロックしているのではなく正常な応答と見せかけたいからだ。

dig . +noall +authority | sed -E 's/\t+/ /g'
# . 86400 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024080800 1800 900 604800 86400
dig com. +noall +authority | sed -E 's/\t+/ /g'
# com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1723351845 1800 900 604800 86400

comでもunbound-checkconfに怒られないが、末尾にルートゾーンを示す.を付けることを推奨する。
IANAのRoot Zone Databaseなどを利用して、gTLDとccTLDを取得しlocal-zoneのベースをつくるとよいだろう。

NTP 見出しにジャンプ

Linux環境は大抵時刻合わせにpool.ntp.org.を利用しているが、某通信機器メーカーをdisる傍ら、福岡大のNTPに問い合わせていたら目も当てられない。
中には疑わしいNTPサーバが存在するのも事実で、攻撃先を収集するために利用される可能性があるのではないかと個人的に思っている。

DHCPv6のオプションで降ってきた、フレッツ東のNGN網内NTPサーバに向けた。
ブロックルールを書くことだけが、DNSシンクホールの役割ではないのだ。

server:
	local-zone: "www.pool.ntp.org." transparent
	local-zone: "pool.ntp.org." redirect
	local-data: "pool.ntp.org. 10800 IN AAAA 2404:1a8:1102::a"
	local-data: "pool.ntp.org. 10800 IN AAAA 2404:1a8:1102::b"
	local-data: "pool.ntp.org. 10800 IN AAAA 2404:1a8:1102::c"

ついでにWindows, Apple, Androidも対策。

server:
	local-data: "ntp.nict.jp. 10800 IN AAAA 2404:1a8:1102::c"
	local-data: "ntp-b2.nict.go.jp. 10800 IN AAAA 2404:1a8:1102::c"
	local-data: "time.windows.com. 10800 IN AAAA 2404:1a8:1102::c"
	local-data: "time-ios.apple.com. 10800 IN A 10.0.0.254"
	local-data: "time-ios.apple.com. 10800 IN AAAA 2404:1a8:1102::c"
	local-data: "time-macos.apple.com. 10800 IN A 10.0.0.254"
	local-data: "time-macos.apple.com. 10800 IN AAAA 2404:1a8:1102::c"
	local-data: "time.apple.com. 10800 IN A 10.0.0.254"
	local-data: "time.apple.com. 10800 IN AAAA 2404:1a8:1102::c"
	local-data: "time1.apple.com. 10800 IN AAAA 2404:1a8:1102::a"
	local-data: "time2.apple.com. 10800 IN AAAA 2404:1a8:1102::b"
	local-data: "time3.apple.com. 10800 IN AAAA 2404:1a8:1102::c"
	local-data: "time4.apple.com. 10800 IN AAAA 2404:1a8:1102::a"
	local-data: "time5.apple.com. 10800 IN AAAA 2404:1a8:1102::b"
	local-data: "time6.apple.com. 10800 IN AAAA 2404:1a8:1102::c"
	local-data: "time7.apple.com. 10800 IN AAAA 2404:1a8:1102::a"
	local-data: "time.euro.apple.com. 10800 IN AAAA 2404:1a8:1102::c"
	local-data: "time.google.com. 10800 IN AAAA 2404:1a8:1102::c"
	local-data: "time1.google.com. 10800 IN AAAA 2404:1a8:1102::a"
	local-data: "time2.google.com. 10800 IN AAAA 2404:1a8:1102::b"
	local-data: "time3.google.com. 10800 IN AAAA 2404:1a8:1102::c"
	local-data: "time4.google.com. 10800 IN AAAA 2404:1a8:1102::a"
	local-data: "time.facebook.com. 10800 IN AAAA 2404:1a8:1102::c"
	local-data: "time1.facebook.com. 10800 IN AAAA 2404:1a8:1102::a"
	local-data: "time2.facebook.com. 10800 IN AAAA 2404:1a8:1102::b"
	local-data: "time3.facebook.com. 10800 IN AAAA 2404:1a8:1102::c"
	local-data: "time4.facebook.com. 10800 IN AAAA 2404:1a8:1102::a"
	local-data: "time.cloudflare.com. 10800 IN AAAA 2404:1a8:1102::c"

NTT GINのNTPサーバも見つけたが、異種NTPを混在させても無視されるので使う機会はないだろう。

server:
	local-zone: "time.gin.ntt.net." redirect
	local-data: "time.gin.ntt.net. 60 IN CNAME time1.gin.ntt.net."
	local-zone: "time2.gin.ntt.net." redirect
	local-data: "time2.gin.ntt.net. 60 IN CNAME time1.gin.ntt.net."
dns-blocklists 見出しにジャンプ

私はページ読み込みに違和感を感じたときにFirefoxのDevToolsを開いて悪性ドメインを見つけている。とはいえ、今となっては真っ当な商売ヅラしているECサイト、L0(米議会)からL7を推測して語られることの多い陽キャ御用達SNSなどは経験上ルールが追いつかない。
hagezi/dns-blocklistsはDNSソフトウェア向けに現在rpzのみ提供されている。RPZはCNAME Cloaking対策でもなければ無駄な処理なので、dnsmasq用ルールからいくつか選んで、unbound.conf形式に書き替えて使っている。

- local=/aan.amazon.co.jp/
+ local-zone: "aan.amazon.co.jp." always_nodata
...

include:を書いた行に別ファイルの内容が反映される。応答内容を書き換える機能なので、外部のものを使うときは中身を確認しよう。

server:
  # chroot: "/usr/local/etc/unbound"
  # username: "unbound"
-   # directory: "/usr/local/etc/unbound"
+   directory: "/usr/local/etc/unbound"
...
+   include: *.txt

rpz 見出しにジャンプ

local-zoneと違いHTTPを介して外部からルールセットをインポートできるほか、Scrub時も評価されるためCNAME Cloakingにも対応できる。
Response Policy Zones — Unbound 1.19.3 documentation
Response Policy Zones in Unbound. We are incredibly happy to introduce… | by Ralph Dolmans | The NLnet Labs Blog | Medium

Public DNS 見出しにジャンプ

せっかくセキュリティ目的でunboundを導入しても、Public DNSへ端末がブレークアウトするのを許してしまえば無用の長物だ。
例えば、AndroidはGoogle Public DNSにDoTを試行するし、一部のモバイルアプリではDo53やDoHを利用して、端末のスタブリゾルバを回避する動きがある。

Do53やDoTは5tuple Firewallで853/tcpをブロックすれば事足りるが、名の知れたPublic DNSはIPアドレスで証明書を取得しているため、それだけはL3 FWでブロックせざるを得ない。

openssl s_client -connect 8.8.8.8:443 | openssl x509 -noout -text | grep DNS:
# DNS:dns.google, DNS:dns.google.com, DNS:*.dns.google.com, DNS:8888.google, DNS:dns64.dns.google, IP Address:8.8.8.8, IP Address:8.8.4.4, IP Address:2001:4860:4860:0:0:0:0:8888, IP Address:2001:4860:4860:0:0:0:0:8844, IP Address:2001:4860:4860:0:0:0:0:6464, IP Address:2001:4860:4860:0:0:0:0:64

幸いなことに、殆どのDoHは証明書の検証を行うときにDNS名が必要なため、DNSでブロックできる。
Blocking DNS-over-HTTPS (DoH) - General - Pi-hole Userspace
Block DNS over HTTPS (DoH), using pfsense - Block DOH with pfsense.pdf
Using Unbound response policy zones - Unbound response policy zones.pdf

rpz:
	name: rpz.block.doh
	url: https://raw.githubusercontent.com/jpgpi250/piholemanual/master/DOH.rpz
	rpz-action-override: nodata
	rpz-log: yes
	rpz-log-name: rpz.block.doh

外部のルールを直接利用するときは、安全のためrpz-action-override:を使用すべきだ。
RethinkDNSは有用なサービスのため、ホワイトリストにするとよい。

server:
	local-data: "rethinkdns.com. 300 IN AAAA 2606:4700:3030::6815:533e"
	local-data: "rethinkdns.com. 300 IN AAAA 2606:4700:3030::ac43:d6f6"
use-application-dns.net 見出しにジャンプ

悪意のある通信はL3 FWで防ぐしかないが、善意の通信はUnboundで歓迎する。
DoHを利用しないように指示するCanary domainの仕組み - ASnoKaze blog

server:
	local-zone: "use-application-dns.net." inform_redirect
iCloud Private Relay 見出しにジャンプ

iCloud Private Relay経由で名前解決されると、当然Unboundの庇護下に入らない。

server:
	local-zone: "mask.icloud.com." static
	local-data: "mask.icloud.com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1723185585 1800 900 604800 86400"
	local-zone: "mask-api.icloud.com." static
	local-data: "mask-api.icloud.com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1723185585 1800 900 604800 86400"
	local-zone: "mask-h2.icloud.com." static
	local-data: "mask-h2.icloud.com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1723185585 1800 900 604800 86400"

AppleはNODATAstaticかNXDOMAINalways_nxdomainでのブロックを想定している。TTLを指定できるstaticを使用した。
cf. ネットワークのトラフィック監査への対応 iCloudプライベートリレーに向けたネットワークやWebサーバの準備 - iCloud - Apple Developer

ドメイン名が似ているmask-canary.icloud.comを、NODATAや0.0.0.0でブロックしてしまうと、既知のトラッカーがありそうなサイト(Google検索やニュースサイト)で5秒程度のタイムアウトが発生した。
恐らく調査目的でセキュリティ上の問題はないが、防ぎたければWi-Fiの設定で「IPアドレスのトラッキングを制限」をオフにするしかない。
"Limit IP Address Tracking" setting on iOS circumvents ad-blocking - Help / Community Help - Pi-hole Userspace

CNAME Cloaking 見出しにジャンプ

Unboundを導入する理由の一つでもあったCNAME Cloakingとはどのような技術か。
何のことはない、CDN利用にCNAMEを向けることで「same-siteであるもののネットワークは異なる」のと同義で、これをMalvertisingやプライバシー侵害判定を受けたドメイン名を隠すのに利用しているだけだ。
「Same-site」と「same-origin」 | Articles | web.dev

下記の例では、通常のスタブリゾルバをバックエンドにするDNSシンクホールにはtargetlr.adobe.com.にしか見えず、これをルールに書くしかない。一方、フルリゾルバ、正確にはCNAME ScrubbingするDNSフォワーダでもよいが、data.adobedc.net.であることをRPZで判定できる。

dig targetlr.adobe.com +nostats

; <<>> DiG 9.18.26 <<>> targetlr.adobe.com +nostats
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 21745
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 2

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;targetlr.adobe.com.            IN      A

;; ANSWER SECTION:
targetlr.adobe.com.     8154    IN      CNAME   adobeinternalmobiles.tt.omtrdc.net.
adobeinternalmobiles.tt.omtrdc.net. 60 IN CNAME adobetarget.data.adobedc.net.

;; ADDITIONAL SECTION:
rpz.block.cloaking.     300     IN      SOA     a.root-servers.net. nstld.verisign-grs.com. 2024081300 1800 900 604800 86400

例外的に、AppleのスタブリゾルバはScrubbing動作し、ITPのサードパーティCookie制限に利用される。誰でもAppプライバシーレポートを見ればその様子を確認できる。
Tracking Prevention in WebKit | WebKit
このITPの強化はさらなるイタチごっこを引き起こした。CNAMEがドメイン名を隠せないとなれば、今度はNSをサービス側がCNAME flatteningを行える権威DNSに委任することで、直接Aレコードを設定するサービスが現れた。これに対しITPではファーストパーティとのIP範囲の一致を見るようになった。CDN利用のリスクを直視すれば自然ともいえるが、美しい回避策ではないと思う。こちらもrpz-ipで対応できる。
Cap cookie lifetimes to 7 days for responses from third party IP addresses by whsieh · Pull Request #5347 · WebKit/WebKit
現在進行形で続くこれらのイタチごっこは、セキュリティの観点から見れば、maliciousなサードパーティリソースをどのように防ぐかという話が、そのファーストパーティ自身がmaliciousなのでブロックするのかというグラデーションを移動しただけに過ぎない。自らが提供するサービスを危険にさらす技術に投資するでなく、脆弱でないか考えることが長期的な利益につながるだろう。

rpz:
	name: rpz.block.cloaking
	zonefile: /usr/local/etc/unbound/rpz.block.cloaking.zone
	rpz-log: yes
	rpz-log-name: rpz.block.cloaking
$ORIGIN rpz.block.cloaking.
$TTL 300
rpz.block.cloaking. IN SOA a.root-servers.net. nstld.verisign-grs.com. (
        2024081300
        1800
        900
        604800
        86400
)

; https://github.com/AdguardTeam/cname-trackers/blob/master/data/combined_original_trackers.txt
*.data.adobedc.net CNAME *.

; gtm
32.21.32.239.216.rpz-ip CNAME *.
32.21.34.239.216.rpz-ip CNAME *.
32.21.36.239.216.rpz-ip CNAME *.
32.21.38.239.216.rpz-ip CNAME *.
128.15.zz.32.4802.4860.2001.rpz-ip CNAME *.
128.15.zz.34.4802.4860.2001.rpz-ip CNAME *.
128.15.zz.36.4802.4860.2001.rpz-ip CNAME *.
128.15.zz.38.4802.4860.2001.rpz-ip CNAME *.
ghs.googlehosted.com CNAME *.
  • directory:後であれば相対パスでもよいが、unbound-checkconfがエラーになるので絶対パスを書いている
  • RPZはゾーンファイルを拡張した構文になっている。
    Zonefile example — NSD 4.3.9 documentation
  • CNAME Cloakingが流行りだした当初から継続してメンテナンスされているAdGuardのルールを採用した。
    cname-trackers/data/combined_original_trackers.txt at master · AdguardTeam/cname-trackers · GitHub
    前述の通りUnboundはScrubするため、用意された冗長なRPZは必要なく、簡素なオリジナルルールをRPZに書き換えればよい。
    • *.data.adobedc.netは省略形で、正式には*.data.adobedc.net.rpz.block.cloaking.のため、末尾に.を付けるとunbound-checkconfで構文エラーになる。
  • rpz-ipを用いると、A/AAAA応答を引っ掛けられる。CIDR表記のresponse-ipの方が書きやすいので、他のDNSソフトウェアでの使用を想定したときにrpz-ipを用いるとよい。
    DNSの逆引きと似たり寄ったりで、CIDR表記のネットマスク128を先頭に、ヘクステット毎に先に接尾辞、最後に接頭辞が来るように並び替えられ、ゼロの連続::zzで表現する。
    • ghs.googlehosted.comはGoogle Tag Managerに使われており、CNAME名(頭痛が痛い?)とIPアドレスが公開されている。様々なサービスが利用しているため、除外が必要かもしれない。
      CNAME record values - Google Workspace Admin Help
server:
	# rpz.block.cloaking ghs.googlehosted.com
	local-data: "nordot.app. 300 IN AAAA 2001:4860:4802:32::15"
	local-data: "bugs.chromium.org. 300 IN AAAA 2001:4860:4802:34::15"
	local-data: "dwango.co.jp. 300 IN AAAA 2001:4860:4802:36::15"
	local-data: "material.io. 300 IN AAAA 2001:4860:4802:38::15"
	local-data: "blog.google. 300 IN AAAA 2001:4860:4802:32::15"
abuse 見出しにジャンプ

RPZは複数のDNSソフトウェアで使える共通規格のため、各社からブロックリストが提供されている。
https://dnsrpz.info/
DNSセキュリティを語る上で避けられないあのSpamhausが無償で提供する、最低限のDo Not Route or Peer (DROP)Service FeedsUnboundのIssueにあった設定例を参考に設定した。

rpz:
	name: drop.ip.dtq
	zonefile: /usr/local/etc/unbound/drop.ip.dtq.zone
	master: 34.194.195.25 # US.spamhaus.zone
	rpz-action-override: nodata
	rpz-log: yes
	rpz-log-name: 'drop.ip.dtq.spamhaus'

rpz:
	name: coinblocker.srv
	zonefile: /usr/local/etc/unbound/coinblocker.srv.zone
	master: 34.194.195.25
	rpz-action-override: nodata
	rpz-log: yes
	rpz-log-name: 'coinblocker.srv.spamhaus'
...
  • name:はSpamhausにあるゾーン名と一致させ、ログの接頭辞だけ変えている。RPZゾーン(頭痛が痛い?)は$ORIGINが異なると使えなくなるためだ。
  • Free Use用のホストはIPv6が機能していなかった。Unboundのフォールバックが遅いので、IPアドレスを設定した。IP TTLはUSの方が1大きかった。

response-ip 見出しにジャンプ

https://unbound.docs.nlnetlabs.nl/en/latest/manpages/unbound.conf.html#unbound-conf-response-ip

DNS Rebinding 見出しにジャンプ

DNSリバインディング攻撃は、ブラウザの保護機構をバイパスするために温故知新された、古くからある攻撃手法だ。
要はWebと権威DNSで連携し、maliciousなWebにアクセスがあれば、すかさず権威DNS側でRRsetを対象の内部ホストに書き換えてしまおうというまさに今、浸透を待っているあなたに耳よりの技術だ。
ドメイン名だけを見てアクセス許可を行う傾向にあるブラウザは、ドメイン名がいつの間にか内部ホストを指していても素通ししてしまう。

Private Network AccessのIP範囲をベースに、DNSリバインディング攻撃が刺さりそうなアドレス範囲をCIDR表記で表現した。
State of DNS Rebinding in 2023 | NCC Group Research Blog | Making the world safer and more secure

server:
	cache-min-ttl: 60

	# "this network" ("This host on this network" 0.0.0.0/32 for non-Windows OS)
	private-address: 0.0.0.0/8
	# IPv4 Loopback
	private-address: 127.0.0.0/8
	local-zone: "localhost." nodefault
	# Private-Use
	private-address: 10.0.0.0/8
	private-address: 172.16.0.0/12
	private-address: 192.168.0.0/16
	# Shared Address Space (e.g. Tailscale)
	private-address: 100.64.0.0/10
	# IPv4 Link-Local
	private-address: 169.254.0.0/16

	# IPv4-mapped Address ::ffff:0:0/96, IPv6 Unspecified Address ::/128, IPv6 Loopback ::1/128
	private-address: ::/3
	# Unique-Local fc00::/7, Link-Local Unicast fe80::/10
	private-address: 8000::/1

	private-domain: "dot.home.arpa."

これにより、管理下にないドメイン名を介して内部ホストへ不正にアクセスされなくなる。
インターネット到達性のあるアドレス範囲にACLがされていない(DMZ, 旧帝大, IPv6 GUA)ならこの限りではない。

自ビルド版では動かなかったので、response-ipで書き直した。

server:
	response-ip: 0.0.0.0/8 redirect
	response-ip: 127.0.0.0/8 redirect
	response-ip: 10.0.0.0/8 redirect
	response-ip: 172.16.0.0/12 redirect
	response-ip: 192.168.0.0/16 redirect
	response-ip: 100.64.0.0/10 redirect
	response-ip: 169.254.0.0/16 redirect
	response-ip: ::/3 redirect
	response-ip: 8000::/1 redirect
Google Public DNSを自宅に設置する 見出しにジャンプ

Public DNSによる脱獄は対策したが、それだけでは面白みがない。直接IPアドレスを叩いてくるアプリケーションなどは逆NATでもするほかないが、ご丁寧にUnboundにクエリして頂けたのなら、ぜひとも他のクエリも歓迎したい。

server:
	# dns.google.
	response-ip: 8.8.4.0/24 redirect
	response-ip-data: 8.8.4.0/24 "60 IN A 10.0.3.5"
	response-ip: 8.8.8.0/24 redirect
	response-ip-data: 8.8.8.0/24 "60 IN A 10.0.3.5"
	response-ip: 2001:4860:4840::/42 always_nxdomain

	# one.one.one.one.
	response-ip: 1.1.1.0/24 redirect
	response-ip-data: 1.1.1.0/24 "60 IN A 10.0.3.5"
	response-ip: 1.0.0.0/24 redirect
	response-ip-data: 1.0.0.0/24 "60 IN A 10.0.3.5"
	response-ip: 2606:4700:4700::/40 always_nxdomain
アンチアンチアドブロッカー 見出しにジャンプ

unboundに施したセキュリティが干渉し、安全なコンテンツまで表示できなくなってしまうWebサイトがある。
jkrejcha/AdmiraList: A text-based list of 1000+ Admiral domains and IP addresses

server:
	module-config: "respip iterator"

	# Admiral
	response-ip: 104.18.24.111/32 always_nxdomain
	response-ip: 104.18.25.111/32 always_nxdomain
	response-ip: 2606:4700::6812:186f/128 always_nxdomain
	response-ip: 2606:4700::6812:196f/128 always_nxdomain

	local-zone: "getadmiral.com." static
	local-data: "getadmiral.com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1723184575 1800 900 604800 86400"
	local-zone: "cdnral.com." static
	local-data: "cdnral.com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1723184575 1800 900 604800 86400"
	local-zone: "scarceshock.com." static
	local-data: "scarceshock.com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1723184575 1800 900 604800 86400"
	local-zone: "succeedscene.com." static
	local-data: "succeedscene.com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1723184575 1800 900 604800 86400"
	...
  • これまで推測が難しいFQDNとCDNを利用してブロッカーの回避を試みていたが、最近はCloudflareを挟んでいるため簡単にルールを記述できる。
  • respipではHTTPS RRのipv4hint/ipv6hintがすり抜けてしまうため、代わりにinform_redirectを用いて監視するのもよい。発見したFQDNはlocal-zone設定する。

HTTPS RRで遊んでいたら、AliasModeを解釈できるApple macOS/iOS 17+で機能する、DNSを使ったアンチアンチアンチアンチアドブロッカーが実現してしまった。
RFC 9460: Service Binding and Parameter Specification via the DNS (SVCB and HTTPS Resource Records)
実用性はともかく面白いので共有しておく。

server:
	# www.cbsnews.com
	local-data: 'scarceshock.com. 300 IN HTTPS 0 invalid.'
	local-data: 'succeedscene.com. 300 IN HTTPS 0 invalid.'
	# www.youtube.com
	local-zone: 'jnn-pa.googleapis.com.' typetransparent
	local-zone-tag: 'jnn-pa.googleapis.com.' admin
	local-data: 'jnn-pa.googleapis.com. 300 IN HTTPS 0 invalid.'

これにより、通常ブラウザレベルでないと機能しないFirefoxのShimのような動きになる。

ルールを自分で書くメリットとして、ブロック漏れや過剰なブロックに対応しやすい利点がある。

home.arpa. 見出しにジャンプ

内部ドメインには好き勝手にそれらしい名前のTLDが使われていて、.localがmDNSと被ったり、.homeが後にTLDに追加されるなど混沌としている。また、これらの内部ドメインの問い合わせが外に漏れると、ルートサーバの過負荷に繋がったり、セキュリティリスクが伴う。
ICANNのSAC113 SSAC Advisory on Private-Use TLDsは、パブリックに登録されたドメイン名のサブドメインを使うのが最善としている。例えばexample.com.を持っているならserver.example.com.とか。
内部構成を公開したくないなどのユースケースもあるので、他の選択肢にICANNのinternal., IANAのhome.arpa.が挙げられている。
RFC 8375 - Special-Use Domain 'home.arpa.'

server:
	local-zone: "router.home.arpa." static
	local-zone-tag: "router.home.arpa." admin
	local-data: "router.home.arpa. 10800 IN A 10.0.0.254"

	local-data: "server.home.arpa. 10800 IN A 10.0.2.8"
	local-data: "dns.home.arpa. 10800 IN A 10.0.3.5"
	local-data: "printer.home.arpa. 10800 IN A 10.0.4.62"

NGN IPv6のキャッシュDNS(東)をlocal-dataに設定する例。元の応答を調べるときにエイリアスがあると捗るよ。

server:
	local-zone: "flets." static
	local-data: "ntp.flets. 3600 IN AAAA 2404:1a8:1102::a"
	local-data: "ntp.flets. 3600 IN AAAA 2404:1a8:1102::b"
	local-data: "ntp.flets. 3600 IN AAAA 2404:1a8:1102::c"
	local-data: "dns.flets. 3600 IN AAAA 2404:1a8:7f01:a::3"
	local-data: "dns.osa.flets. 3600 IN AAAA 2404:1a8:7f01:a::3"
	local-zone: "ocn." static
	local-data: "dns.ocn. 3600 IN AAAA 2001:380:0:4::1"
	local-data: "dns.ocn. 3600 IN AAAA 2001:380:0:4::2"
	local-data: "dns.osa.ocn. 3600 IN AAAA 2001:380:0:104::1"
	local-data: "dns.osa.ocn. 3600 IN AAAA 2001:380:0:104::2"
dig @dns.flets. example.com

Discord 見出しにジャンプ

DiscordはCloudflare CDNを利用している。

```sh
dig discord.com ns
dig @sima.ns.cloudflare.com. discord.com a +norec +noall +answer
# discord.com.            300     IN      A       162.159.138.232
# discord.com.            300     IN      A       162.159.128.233
# discord.com.            300     IN      A       162.159.135.232
# discord.com.            300     IN      A       162.159.137.232
# discord.com.            300     IN      A       162.159.136.232

whois -h whois.cymru.com -v 162.159.128.233
Warning: RIPE flags used with a traditional server.
AS      | IP               | BGP Prefix          | CC | Registry | Allocated  | AS Name
13335   | 162.159.128.233  | 162.159.128.0/19    | US | arin     | 2013-05-23 | CLOUDFLARENET, US

Cloudflareは積極的にIPv6やHTTP/3などのモダンな技術に対応してきたが、Discordが使用するドメインにはAAAAレコードが設定されていないため、実際にはIPv4で接続する。

dig @sima.ns.cloudflare.com. discord.com aaaa +norec +noall +answer +comments
# ;; Got answer:
# ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 48529
# ;; flags: qr aa; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

# ;; OPT PSEUDOSECTION:
# ; EDNS: version: 0, flags:; udp: 1232
dig @sima.ns.cloudflare.com. discord.com type65 +norec +noall +answer
# discord.com.            300     IN      HTTPS   1 . alpn="h3,h2" ipv4hint=162.159.128.233,162.159.135.232,162.159.136.232,162.159.137.232,162.159.138.232

他のホストも調べるが、AAAAレコードはほぼ設定されていない。discord.com., discordapp.com., discordapp.net., discord.gg., discord.media.ゾーン毎にそれぞれ5つのIPアドレスが存在するようだ。
RFC 9460 SvcParamが網羅的に存在するためHTTPSレコードをクエリするが、通常はA/AAAAをクエリすると留意してほしい。

discord.com.            300     IN      HTTPS   1 . alpn="h3,h2" ipv4hint=162.159.128.233,162.159.135.232,162.159.136.232,162.159.137.232,162.159.138.232
ptb.discord.com.        300     IN      HTTPS   1 . alpn="h3,h2" ipv4hint=162.159.128.233,162.159.135.232,162.159.136.232,162.159.137.232,162.159.138.232
streamkit.discord.com.  300     IN      HTTPS   1 . alpn="h3,h2" ipv4hint=162.159.128.233,162.159.135.232,162.159.136.232,162.159.137.232,162.159.138.232
updates.discord.com.    300     IN      HTTPS   1 . alpn="h3,h2" ipv4hint=162.159.128.233,162.159.135.232,162.159.136.232,162.159.137.232,162.159.138.232
support.discord.com.    300     IN      HTTPS   1 . alpn="h3,h2" ipv4hint=162.159.128.233,162.159.135.232,162.159.136.232,162.159.137.232,162.159.138.232

cdn.discordapp.com.     300     IN      HTTPS   1 . alpn="h3,h2" ipv4hint=162.159.129.233,162.159.130.233,162.159.133.233,162.159.134.233,162.159.135.233
discordapp.com.         300     IN      HTTPS   1 . alpn="h3,h2" ipv4hint=162.159.129.233,162.159.130.233,162.159.133.233,162.159.134.233,162.159.135.233
streamkit.discordapp.com. 300   IN      HTTPS   1 . alpn="h3,h2" ipv4hint=162.159.129.233,162.159.130.233,162.159.133.233,162.159.134.233,162.159.135.233

media.discordapp.net.   300     IN      HTTPS   1 . alpn="h3,h2" ipv4hint=162.159.128.232,162.159.129.232,162.159.130.232,162.159.133.232,162.159.134.232
images-ext-1.discordapp.net. 300 IN     HTTPS   1 . alpn="h3,h2" ipv4hint=162.159.128.232,162.159.129.232,162.159.130.232,162.159.133.232,162.159.134.232
images-ext-2.discordapp.net. 300 IN     HTTPS   1 . alpn="h3,h2" ipv4hint=162.159.128.232,162.159.129.232,162.159.130.232,162.159.133.232,162.159.134.232

gateway.discord.gg.     300     IN      HTTPS   1 . alpn="h2" ipv4hint=162.159.130.234,162.159.133.234,162.159.134.234,162.159.135.234,162.159.136.234

latency.discord.media.  300     IN      HTTPS   1 . alpn="h2" ipv4hint=162.159.128.235,162.159.129.235,162.159.130.235,162.159.137.234,162.159.138.234 ipv6hint=2606:4700:7::a29f:80eb,2606:4700:7::a29f:81eb,2606:4700:7::a29f:82eb,2606:4700:7::a29f:89ea,2606:4700:7::a29f:8aea

このように、CDNが対応している方式が使われていない事例があり、サービス提供側と利用者側に温度差がある。
実際、Discordのサポートフォーラムには完全なIPv6サポートを求める投稿があるが、日本においてIPv4接続性のない環境は考えにくいし、個人的にはIPv6推進派ですらない。IPv6化のメリットはせいぜいフレッツで高速だとか、大量のHTTP/3トラフィックが発生する状況くらいなものだ。

同Cloudflareを利用するnyanshiba.comはというと、ipv6hintやAAAAが設定されている。

dig nyanshiba.com https +noall +answer
# nyanshiba.com.          300     IN      HTTPS   1 . alpn="h3,h2" ipv4hint=104.21.0.152,172.67.151.30 ipv6hint=2606:4700:3031::6815:98,2606:4700:3032::ac43:971e

CloudflareのIPv6アドレス割り当てには法則があり、基本的に2606:4700::/96の接頭辞に、IPv4アドレスのdecimalをhexに変換した接尾辞を持つ。Anycast方式のため、IPアドレスは世界共通。
DiscordはAAAAがないものの、この法則に従って変換すると機能するサービスの一つだ。
Discord works on IPv6 if you force it : r/ipv6
そこで、他のドメイン名にも対応するために、CLoudflareを利用するサービスのHTTPSレコードのipv4hintを渡すとIPv6アドレスに変換するスクリプトを作成した。
unboundconf/calc-cfv6addr.ps1 at main · nyanshiba/unboundconf

./calc-cfv6addr.ps1 -ip 162.159.128.233,162.159.135.232,162.159.136.232,162.159.137.232,162.159.138.232
# 2606:4700::a29f:80e9
# 2606:4700::a29f:87e8
# 2606:4700::a29f:88e8
# 2606:4700::a29f:89e8
# 2606:4700::a29f:8ae8

unboundに設定すると下記の通り。同じIPアドレスを使う他のドメインにも同様に設定する。新規チャットの購読gateway.discord.gg.や、ここに上がっていない通話のICEサーバはIPv6非対応だ。
IPv4でも接続できるように、typetransparentでIN Aを透過するとよい。

server:
	local-zone: 'discord.com.' typetransparent
	local-data: 'discord.com. 300 IN HTTPS 1 . alpn="h3,h2" ipv4hint=162.159.128.233,162.159.135.232,162.159.136.232,162.159.137.232,162.159.138.232 ipv6hint=2606:4700::a29f:80e9,2606:4700::a29f:87e8,2606:4700::a29f:88e8,2606:4700::a29f:89e8,2606:4700::a29f:8ae8'
	local-data: 'discord.com. 300 IN AAAA 2606:4700::a29f:80e9'
	local-data: 'discord.com. 300 IN AAAA 2606:4700::a29f:87e8'
	local-data: 'discord.com. 300 IN AAAA 2606:4700::a29f:88e8'
	local-data: 'discord.com. 300 IN AAAA 2606:4700::a29f:89e8'
	local-data: 'discord.com. 300 IN AAAA 2606:4700::a29f:8ae8'
	...
	local-data: 'cdn.discordapp.com. 300 IN HTTPS 1 . alpn="h3,h2" ipv4hint=162.159.129.233,162.159.130.233,162.159.133.233,162.159.134.233,162.159.135.233 ipv6hint=2606:4700::a29f:81e9,2606:4700::a29f:82e9,2606:4700::a29f:85e9,2606:4700::a29f:86e9,2606:4700::a29f:87e9'
	...
	local-data: 'media.discordapp.net. 1800 IN HTTPS 1 . alpn="h3,h2" ipv4hint=162.159.128.232,162.159.129.232,162.159.130.232,162.159.133.232,162.159.134.232 ipv6hint=2606:4700::a29f:80e8,2606:4700::a29f:81e8,2606:4700::a29f:82e8,2606:4700::a29f:85e8,2606:4700::a29f:86e8'
	...

QoSに引っ掛けやすいように、Prefixを変えてもよい。

	local-zone: 'media.discordapp.net' typetransparent
	local-data: 'media.discordapp.net. 1800 IN HTTPS 1 . alpn="h3,h2" ipv4hint=162.159.128.232,162.159.129.232,162.159.130.232,162.159.133.232,162.159.134.232 ipv6hint=2606:4700::a29f:80e8,2606:4700::a29f:81e8,2606:4700::a29f:82e8,2606:4700::a29f:85e8,2606:4700::a29f:86e8'
	local-data: 'media.discordapp.net. 1800 IN AAAA 2606:4700:3032::a29f:80e8'
	local-data: 'media.discordapp.net. 1800 IN AAAA 2606:4700:3032::a29f:81e8'
	local-data: 'media.discordapp.net. 1800 IN AAAA 2606:4700:3032::a29f:82e8'
	local-data: 'media.discordapp.net. 1800 IN AAAA 2606:4700:3032::a29f:85e8'
	local-data: 'media.discordapp.net. 1800 IN AAAA 2606:4700:3032::a29f:86e8'

CloudflareとFastlyを利用するStack Overflowによる、IPv6対応が難しい理由。ちょっと勿体ない。
Stack Overflow not reachable via IPv6 - Meta Stack Overflow

server:
	local-zone: 'meta.stackoverflow.com.' typetransparent
	local-data: 'meta.stackoverflow.com. 300 IN HTTPS 1 . alpn="h2" ipv4hint=104.18.32.7,172.64.155.249 ipv6hint=2606:4700::6812:2007,2606:4700::ac40:9bf9'
	local-data: 'meta.stackoverflow.com. 300 IN AAAA 2606:4700::6812:2007'
	local-data: 'meta.stackoverflow.com. 300 IN AAAA 2606:4700::ac40:9bf9'
	local-zone: 'i.sstatic.net.' typetransparent
	local-data: 'i.sstatic.net. 300 IN HTTPS 1 . alpn="h2" ipv4hint=104.18.41.33,172.64.146.223 ipv6hint=2606:4700::6812:2921,2606:4700::ac40:92df'
	local-data: 'i.sstatic.net. 300 IN AAAA 2606:4700::6812:2921'
	local-data: 'i.sstatic.net. 300 IN AAAA 2606:4700::ac40:92df'

このようにIPv6化の余地があるサービスは大抵Why No IPv6?(強い思想に注意)に載っている

Twitter 見出しにジャンプ

2024年9月現在、Twitter(自称𝕏)は多くの通信をIPv6で行い、Unboundで殆どの通信をIPv6化できる。

Cloudflare 見出しにジャンプ

イーロン・マスク氏がTwitterを買収されてから月日が経ち、少なくともt.co, twitter.com/x.com, api.twitter.com/api.x.comに関してはCloudflareにBYOIPしているようだ。

curl -4 -i https://t.co/
# server: cloudflare tsa_m

Discordと同様にIPv6アドレスを計算すると、IPv6化できる。

server:
	local-data: 'twitter.com. 1800 IN AAAA 2606:4700::68f4:2a01'
	local-data: 'twitter.com. 1800 IN AAAA 2606:4700::68f4:2a41'
	local-data: 'twitter.com. 1800 IN AAAA 2606:4700::68f4:2a81'
	local-data: 'twitter.com. 1800 IN AAAA 2606:4700::68f4:2ac1'
	local-data: 'x.com. 1800 IN AAAA 2606:4700::68f4:2a01'
	local-data: 'x.com. 1800 IN AAAA 2606:4700::68f4:2a41'
	local-data: 'x.com. 1800 IN AAAA 2606:4700::68f4:2a81'
	local-data: 'x.com. 1800 IN AAAA 2606:4700::68f4:2ac1'

HTTP/3は非対応。
コミュニティノートに非対応。

server:
	# local-zone: 't.co.' static
	# local-data: 't.co. 300 IN HTTPS 1 . alpn="h2" ipv4hint=104.244.42.5,104.244.42.69,104.244.42.133,104.244.42.197 ipv6hint=2606:4700::68f4:2a05,2606:4700::68f4:2a45,2606:4700::68f4:2a85,2606:4700::68f4:2ac5'
	# local-data: 't.co. 300 IN A 104.244.42.5'
	# local-data: 't.co. 300 IN A 104.244.42.69'
	# local-data: 't.co. 300 IN A 104.244.42.133'
	# local-data: 't.co. 300 IN A 104.244.42.197'
	# local-data: 't.co. 300 IN AAAA 2606:4700::68f4:2a05'
	# local-data: 't.co. 300 IN AAAA 2606:4700::68f4:2a45'
	# local-data: 't.co. 300 IN AAAA 2606:4700::68f4:2a85'
	# local-data: 't.co. 300 IN AAAA 2606:4700::68f4:2ac5'
	local-zone: 't.co.' typetransparent
	local-data: 't.co. 300 IN HTTPS 1 . alpn="h2" ipv4hint=162.159.140.229,172.66.0.227 ipv6hint=2606:4700::ac42:e3,2606:4700::ac42:e3'
	local-data: 't.co. 300 IN AAAA 2606:4700::ac42:e3'
	local-data: 't.co. 300 IN AAAA 2606:4700::ac42:e3'
	# local-data: 't.co. 300 IN A 162.159.140.229'
	# local-data: 't.co. 300 IN A 172.66.0.227'

t.coは外部ドメインへのバウンストラッキングに使われる。Cloudflareに移行することで、チャレンジが頻発するようになってしまった。
CloudflareのWAFがIPアドレス単位のrate-limitをかけているか知らないが、CGNATなどのIPv4延命技術下では、同じIPアドレスから異なるユーザが大量にアクセスが予想されるため、避けたい。これにはIPv6化が有効だ。
t.coが以前使っていたAS13414のIPと、現行のCloudflareのIPから、合わせて計6つIPv6アドレスが使えるので、安定したものを選ぶとよい。

server:
	# 500 https://api.x.com/1.1/onboarding/task.json on desktop
	local-zone: 'api.x.com.' typetransparent
	local-zone-tag: "api.x.com." admin
	local-data: 'api.x.com. 60 IN AAAA 2606:4700::68f4:2a02'
	local-data: 'api.x.com. 60 IN AAAA 2606:4700::68f4:2a42'
	local-data: 'api.x.com. 60 IN AAAA 2606:4700::68f4:2a82'
	local-data: 'api.x.com. 60 IN AAAA 2606:4700::68f4:2ac2'

Web版のTwitterでは、サインイン時https://api.x.com/1.1/onboarding/task.jsonが500を返すため、IPv4で接続しなければならない。上記は使えないと思ってよいだろう。

server:
	# 500 https://api.twitter.com/1.1/onboarding/task.json on ios
	local-zone: 'api.twitter.com.' typetransparent
	local-zone-tag: 'api.twitter.com.' dualstack
	local-data: 'api.twitter.com. 300 IN HTTPS 1 . alpn="h2" ipv4hint=104.244.42.2,104.244.42.66,104.244.42.130,104.244.42.194 ipv6hint=2606:4700::68f4:2a02,2606:4700::68f4:2a42,2606:4700::68f4:2a82,2606:4700::68f4:2ac2'
	local-data: 'api.twitter.com. 300 IN AAAA 2606:4700::68f4:2a02'
	local-data: 'api.twitter.com. 300 IN AAAA 2606:4700::68f4:2a42'
	local-data: 'api.twitter.com. 300 IN AAAA 2606:4700::68f4:2a82'
	local-data: 'api.twitter.com. 300 IN AAAA 2606:4700::68f4:2ac2'
	# after 300sec, 200 https://tpop-api.twitter.com/1.1/onboarding/task.json

Twitter for iPhone(com.atebits.Tweetie2)では、未だにapi.twitter.comが使われている。
一見Web版と瓜二つに見えるが、実際のフローは全く異なる。の通り、typetransparentではAppleのスタブリゾルバがIN CNAME tpop-api.twitter.com.をScrubするためだ。

dig api.twitter.com a +noall +answer
# api.twitter.com.        565     IN      CNAME   tpop-api.twitter.com.
# tpop-api.twitter.com.   450     IN      A       104.244.42.2

このためAppleデバイス向けにはapi.twitter.com. IN Atpop-api.twitter.com. IN AAAAを設定しないと、tpop-api.twitter.com. 450 IN A - api.twitter.com. 300 IN Aの150秒間はIPv4で接続してしまう。
逆に言えば、DNSのレイヤでは通常api.twitter.com以下のパスまで扱えないところ、https://api.twitter.com/1.1/onboarding/task.jsonhttps://tpop-api.twitter.com/1.1/onboarding/task.jsonで判別できるため、IPv6化しても設定変更なく使用できる訳だ。api.twitter.com.のTTLでIPv6の頻度を変えられるのも分かるだろう。キャッシュDNSで減算されたTTL次第で割合が変動し、その最大値は権威次第と留意しよう。

CloudFront 見出しにジャンプ

Twitterに𝕏というあだ名が付く以前は、twitter.com.はAWS CloudFront特有のヘッダX-Amz-Cf-Pop:を返していた。CloudFrontは、共有IPアドレスへのトラフィックをSNIを使って区別している。
CloudFront で HTTPS リクエストを処理する方法を選択する - Amazon CloudFront
[WEB-197] Minecraft authentication server not reachable over IPv6 - Jira
つまりAAAAが設定されていなくても、例えば以下のようにAAAAを含むCloudFrontドメイン名に向けることで、IPv6化できていた。現在は利用できない。

server:
	local-zone: "twitter.com." redirect
	local-zone-tag: "twitter.com" "admin"
	local-data: "twitter.com. 60 IN CNAME dr49lng3n1n2s.cloudfront.net." # aws.amazon.com.

CloudFrontは基本的にIPv6やHTTP/3に対応しているため、よく使っているサービスも実は対応しているかもしれない。

server:
	local-zone: 'www.amazon.co.jp.' typetransparent
	local-data: 'www.amazon.co.jp 1800 IN HTTPS 1 . alpn="h3,h2"'
	local-zone: 'm.media-amazon.com.' typetransparent
	local-data: 'm.media-amazon.com 1800 IN HTTPS 1 . alpn="h3,h2"'
	local-zone: 'images-fe.ssl-images-amazon.com.' typetransparent
	local-data: 'images-fe.ssl-images-amazon.com 1800 IN HTTPS 1 . alpn="h3,h2"'

HTTP/3対応版のcurlで確認できる。
New – HTTP/3 Support for Amazon CloudFront | AWS News Blog
curl/docs/HTTP3.md at master · curl/curl

curl --http3-only --connect-timeout 1 --head -i "https://www.amazon.co.jp/"
# HTTP/3 405

TCP MSS 見出しにジャンプ

Twitterの画像サーバpbs.twimg.comはEdgecastpbs-ec.twimg.com, Fastlypbs-ft.twimg.com, 電気自動車メーカーのCEOに買収されるまではAkamaipbs-ak.twimg.comが、時期不明でGoogle Cloudpbs-gc.twimg.comが稼働していた模様。
動画のvideo.twimg.comや、Web版の動作に関わるabs.twimg.comも命名規則は同じで、いずれもIPv6に対応している。

重箱の隅をつつくと、CDNによってTCP MSSに大きく開きがある。EdgecastもGoogleもCloudflarevideo-cf.twimg.comもそうだが、QUICに対応すると同時に、TCPのMSSもまるでEDNSバッファサイズのように小さくしてしまう実装が多い。
日本のフレッツ系回線ではIPv6のデータグラムが逆転して大きいという状態なのに、ここにきてIPv6だけデータグラムが小さいCDNがあるとなっては書き換えるしかない。

server:

	local-zone: "pbs.twimg.com." redirect
	# pbs-ec.twimg.com IPv6 MSS: 1220, IPv4: 1420
	# pbs-ft.twimg.com IPv6 MSS: 1394, IPv4: 1414
	local-data: "pbs.twimg.com. 3600 IN CNAME dualstack.twimg.twitter.map.fastly.net."

	local-zone: "abs.twimg.com." redirect
	local-data: "abs.twimg.com. 3600 IN CNAME dualstack.twimg.twitter.map.fastly.net."

	local-zone: "video.twimg.com." redirect
	# video-cf.twimg.com IPv6 MSS: 1360
	local-data: "video.twimg.com. 3600 IN CNAME dualstack.video.twitter.map.fastly.net."

純粋にTCP MSSの長さだけ考えるならAAAAをブロックするのも手。block_aaaaは存在しないが、transparentで表現できなくもない

	# local-zone: "pbs.twimg.com." transparent 暗黙
	local-data: "pbs.twimg.com. 3600 IN A ..." # 静的にIPアドレスを設定

Fastlyでは、ドメイン名にdualstack.を付けるだけでIPv6に対応できる場合がある。また、2a04:4e42::/32をインクリメントして、IPv6対応ホストを見つけるプロジェクトもある。
画像やスクリプト関係は接尾辞159、動画は158だが、実はtwimg.com.2a04:4e42::157から2a04:4e42::160まで許可されている。3へクステット目が0000ならAnycastで、日本では15,1a,36,8c辺りが使えるのではないかと思う。

openssl s_client -connect [2a04:4e42::157]:443 | openssl x509 -noout -text | grep DNS:
openssl s_client -connect [2a04:4e42::160]:443 | openssl x509 -noout -text | grep DNS:
#                 DNS:*.twimg.com, DNS:twimg.com, DNS:platform.twitter.com, DNS:cdn.syndication.twimg.com

IPv6Data/fastly at master · miyurusankalpa/IPv6Data · GitHub

HTTP/3化 見出しにジャンプ

Edgecastpbs-ec.twimg.com.はHTTP/3に対応しているが、Alt-SvcにないためHTTP/3にアップグレードされることはない。
HTTPSの接続情報を通知する "HTTPS DNSレコード" の提案仕様 (2021/07更新) - ASnoKaze blog
HTTPS RRを用意すれば、AppleデバイスでHTTP/3接続される。
TN3102: HTTP/3 in your app | Apple Developer Documentation

server:
	local-zone: 'pbs.twimg.com.' typetransparent
	local-data: "pbs.twimg.com. 60 IN AAAA 2606:2800:248:1707:10d3:19d0:1ba2:1a23"
	local-data: 'pbs.twimg.com. 60 IN HTTPS 1 . alpn="h3" ipv6hint=2606:2800:248:1707:10d3:19d0:1ba2:1a23'

Appleデバイスでは、Alt-Svcがあれば従う(HTTP/2にダウングレードする)が、無ければHTTPS RRを見ているような気がする。
RFC 9460: Service Binding and Parameter Specification via the DNS (SVCB and HTTPS Resource Records)

アプリの表示崩れを修正 見出しにジャンプ

最近𝕏(通称Twitter)利用者から「興味がない」が押せない広告や、画面いっぱいにユーザ名も何もない謎の余白が広がるといった不具合が寄せられているが、当環境では再現しなかった。

TwitterにはTwitter広告があるのでまさかと思ったが、確かにアプリからdoubleclick.net.のクエリがなされていた。

unbound-control set_option log-queries: yes
[1722315240] unbound[4955:3] info: 10.0.0.13 firebaselogging-pa.googleapis.com. HTTPS IN
[1722315240] unbound[4955:3] info: 10.0.0.13 googleads.g.doubleclick.net. A IN
[1722315240] unbound[4955:3] info: 10.0.0.13 googleads.g.doubleclick.net. AAAA IN
[1722315240] unbound[4955:3] info: 10.0.0.13 googleads.g.doubleclick.net. HTTPS IN
[1722315240] unbound[4955:3] info: 10.0.0.13 dualstack.video.twitter.map.fastly.net. HTTPS IN
unbound-control set_option log-queries: no

doubleclick.net.はGoogle広告に使われているドメインで、これはMalvertisingを含む場合があるため既にセキュリティ対策が施されていた。

server:
	local-zone: "doubleclick.net." static
	local-data: "doubleclick.net. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1 1800 900 604800 86400"

Twitter(現𝕏)では"とりあえず動くからリリース"した様子が伺えた。

動きを減らす 見出しにジャンプ

Twitter Appには、画面遷移のアニメーションやライブエンゲージメントを減らすアクセシビリティ設定「動きを減らす」がある。
アニメーションはUIの軽快さに一役買っている一方、ライブエンゲージメントは常にprobe.twitter.comへTCPコネクションを張り続けて、RTやふぁぼをリアルタイムに取得するため、バッテリーへの影響が大きい。発熱の問題を報告するユーザも少なくない。
そこで私はアニメーションはそのままに、ライブエンゲージメントを止めるルールを書いた。

server:
	module-config: "respip iterator"

	# probe.twitter.com
	response-ip: 104.244.42.3/32 always_nxdomain
	response-ip: 104.244.42.67/32 always_nxdomain
	response-ip: 104.244.42.131/32 always_nxdomain
	response-ip: 104.244.42.195/32 always_nxdomain

	local-zone: "api-stream.twitter.com." static
	local-zone: "api-stream.x.com." static
	local-zone: "probe.twitter.com." static
	local-zone: "probe.x.com." static

	local-zone: 'albtls.t.co.' static
	local-zone: 'cftls.t.co.' static
	local-zone: "dc-api.twitter.com." static

	local-zone: "api-0-4-0.twitter.com." static
	local-zone: "api-0-4-2.twitter.com." static
	local-zone: "api-0-4-3.twitter.com." static
	# local-zone: "api-0-4-4.twitter.com."
	local-zone: "api-0-4-5.twitter.com." static
	local-zone: "api-0-4-6.twitter.com." static
	local-zone: "api-0-4-7.twitter.com." static
	local-zone: "api-0-4-8.twitter.com." static
	local-zone: "api-0-5-0.twitter.com." static
	local-zone: "api-1-0-0.twitter.com." static
	# local-zone: "api-2-0-0.twitter.com."
	# local-zone: "api-3-0-0.twitter.com."
	# local-zone: "api-20-0-0.twitter.com."
	# local-zone: "api-21-0-0.twitter.com."
	# local-zone: "api-23-0-0.twitter.com."
	# local-zone: "api-24-0-0.twitter.com."
	# local-zone: "api-26-0-0.twitter.com."
	# local-zone: "api-28-0-0.twitter.com."
	# local-zone: "api-30-0-0.twitter.com."
	local-zone: "api-31-0-0.twitter.com." static
	# local-zone: "api-32-0-0.twitter.com."
	local-zone: "api-33-0-0.twitter.com." static
	# local-zone: "api-34-0-0.twitter.com."
	# local-zone: "api-36-0-0.twitter.com."
	local-zone: "api-37-0-0.twitter.com." static
	# local-zone: "api-38-0-0.twitter.com."
	# local-zone: "api-39-0-0.twitter.com."
	# local-zone: "api-40-0-0.twitter.com."
	# local-zone: "api-41-0-0.twitter.com."
	# local-zone: "api-42-0-0.twitter.com."
	local-zone: "api-43-0-0.twitter.com." static
	local-zone: "api-44-0-0.twitter.com." static
	# local-zone: "api-45-0-0.twitter.com."
	local-zone: "api-46-0-0.twitter.com." static

	local-data: "publish.twitter.com. 1800 IN A 104.244.42.3"
	local-data: "publish.twitter.com. 1800 IN A 104.244.42.67"
	local-data: "publish.twitter.com. 1800 IN A 104.244.42.131"
	local-data: "publish.twitter.com. 1800 IN A 104.244.42.195"

Apple Push Notification service 見出しにジャンプ

Apple, Google含めプッシュ通知と呼ばれるものは大概、息の長いTCPコネクションで行われる。
この際、Keep-Alive間隔の長さゆえCPEやAFTRのNAPTキャッシュからセッション情報が消される場合がある。こうなると最悪、端末がKeep-Alive間隔待ってから新たにTCPコネクションを張りなおすまでプッシュ通知は届かない。逆にRFC通りに動作するとAterm病と汚名を付けられるくらいには難のあるところだ。
ともあれNATしないIPv6を通せば何ら問題ない。

Appleは17.0.0.0/8つまり世界のIPv4アドレスプールの1/256を占める米国企業だ。そのためIPv4でも満足に事業を展開できるはず。
しかしながら、2016年という早い時期からアプリストアの審査条件にDNS64/NAT64対応を加えている。
IPv6のみのネットワークのサポート - サポート - Apple Developer
AppleがIPv6を推進する理由はひとえに、顧客側の接続性にあると言えよう。
(思想が強いだけかもしれないが)

Appleのプッシュ通知サービス(APNs)には5223/tcpが使われており、管理者向けにIPv6アドレスの範囲も公開されている。
エンタープライズネットワークで Apple 製品を使う - Apple サポート (日本)
Apple 製のデバイスで Apple プッシュ通知が届かない場合 - Apple サポート (日本)
ところが、iPhoneをtcp port 5223でパケットキャプチャしても17.0.0.0/8の通信しか流れていなかった。どうやら2022年の一時期を除き、IPv4シングルスタックで提供されてるようだ(iOS 18現在)。

syslog_20220325_223125:
  45732: 2022/03/20 16:45:57: [INSPECT] LAN2[out][63900] TCP 2001:db8:b0ba:10ee::53e1.52859 > 2620:149:a44:30c::9.5223 (2022/03/20 16:41:57)
  45734: 2022/03/20 16:45:59: [INSPECT] LAN2[out][63900] TCP 2001:db8:b0ba:10ee::53e1.52858 > 2620:149:a44:30c::6.5223 (2022/03/20 16:41:57)
  62272: 2022/03/23 03:07:40: [INSPECT] LAN2[out][63900] TCP 2001:db8:b0ba:10ee::9395.52401 > 2a01:b740:a42:50c::9.5223 (2022/03/23 03:03:41)
  62648: 2022/03/23 08:48:05: [INSPECT] LAN2[out][63900] TCP 2001:db8:b0ba:10ee::9395.52400 > 2a01:b740:a42:50c::b.5223 (2022/03/23 03:03:40)

syslog_20220402_163206:
  21582: 2022/03/27 02:32:29: [INSPECT] LAN2[out][63900] TCP 2001:db8:b0ba:10ee::7436.59855 > 2620:149:a44:1100::6.5223 (2022/03/27 02:10:43)

syslog_20220605_135143:
  28059: 2022/05/22 01:06:29: [INSPECT] LAN2[out][63900] TCP 2001:db8:b0ba:10ee::36f4.62427 > 2403:300:a42:c0a::7.5223 (2022/05/21 17:09:51)

syslog_20220924_122548:
  71022: 2022/09/17 16:32:44: [INSPECT] LAN2[out][63900] TCP 2001:db8:b0ba:10ee::af64.49708 > 2620:149:a44:a0c::8.5223 (2022/09/17 16:24:31)

syslog_20221118_010457:
  80047: 2022/11/17 15:14:45: [INSPECT] LAN2[out][63900] TCP 2001:db8:b0ba:10ee::ccab.49916 > 2620:149:a44:80c::c.5223 (2022/11/17 08:49:52)
dig 0-courier.push.apple.com aaaa +noall +answer
0-courier.push.apple.com. 7716  IN      CNAME   0.courier-push-apple.com.akadns.net.
0.courier-push-apple.com.akadns.net. 28 IN CNAME apac-asia-courier-4.push-apple.com.akadns.net.

私にそんな趣味はないが、プッシュ通知が遅れて一コメを取れなかったら泣いてしまう人もいるだろう。
プッシュ通知は再送を保証されていない[要出典]ので、単に新着動画を見逃すこともあるだろうし、例えば推しのVTuberの枠を開いたころには既に打ち上っていて軌道に乗っていたら、再突入試験でもない限りげんなりするだろう。

アクセスログにあるホストへTCP接続すると、courier2.push.apple.comがAAAAレコードを持つと分かった。

openssl s_client -connect [2403:300:a42:20c::6]:5223 | openssl x509 -noout -text | grep DNS:
# DNS:courier.push.apple.com, DNS:courier2.push.apple.com, DNS:windows.courier.push.apple.com 

dig courier2.push.apple.com aaaa +noall +answer
# courier2.push.apple.com. 28800  IN      CNAME   1.courier2-push-apple.com.akadns.net.
# 1.courier2-push-apple.com.akadns.net. 60 IN CNAME apac-asia-courier-vs.push-apple.com.akadns.net.
# apac-asia-courier-vs.push-apple.com.akadns.net. 60 IN AAAA 2403:300:a42:c0a::5
# ...

これをUnboundに設定すると下記のようになる。

server:
	local-zone: "init-p01st.push.apple.com." transparent
	local-zone: "homekit.push.apple.com." transparent
	local-zone: "init.push.apple.com." transparent
	local-zone: "sandbox.push.apple.com." transparent # TestFlight init., n-courier., courier.
	local-zone: "push.apple.com." inform_redirect
	local-zone-tag: "push.apple.com" dualstack
	local-data: "push.apple.com. 60 IN AAAA 2403:300:a42:902::6"
	local-data: "push.apple.com. 60 IN AAAA 2403:300:a42:902::7"
	local-data: "push.apple.com. 60 IN AAAA 2403:300:a42:902::8"
	local-data: "push.apple.com. 60 IN AAAA 2403:300:a42:902::9"
	local-data: "push.apple.com. 60 IN AAAA 2403:300:a42:902::a"
  ...
	local-data: "push.apple.com. 60 IN AAAA 2403:300:a42:902::13"
  • 0-courier.push.apple.comから50-courier.push.apple.comまでを一括設定するために、push.apple.com.ゾーンをリダイレクトする。
    これにinit.push.apple.com.等が巻き込まれないためにtransparentで除外する。
    これにより*push.apple.comはlocal-dataの内容だけを応答する。後述するが、IPv4接続性が必要でもtypetransparentは適合しない。
  • APNsへのIPv4接続性が必要な場合、このlocal-zoneの応答をIPv6接続性のあるaccess-control-tagに限定する。
  • typetransparentやCNAMEにredirectするとIPv4を優先してしまうので、AAAAをハードコーディングした。 CNAMEがある場合、それ以外のレコードは存在してはならないため、空のAを追加することはできない。 RPZとblock_aを組み合わせる方法もあるが、
  • いずれにせよapac-asia-courier-vs.push-apple.com.akadns.net.にCNAMEを向けるのは、APACのCDNをランダムに応答するためかえって遅延が大きくなる。
    IPinfo.ioでのregion確認、SecurityTrailsで別リージョンの発見行った。nmapの計測では東西を跨がない方が遅延が小さかった。
# 2403:300:a42:900::/56 Tokyo
nmap -6 -Pn -p 5223 2403:300:a42:900::6 # apac-china-courier-vs.
nmap -6 -Pn -p 5223 2403:300:a42:900::8 # apac-china-courier-vs.
nmap -6 -Pn -p 5223 2403:300:a42:901::6
nmap -6 -Pn -p 5223 2403:300:a42:901::13
nmap -6 -Pn -p 5223 2403:300:a42:902::6
nmap -6 -Pn -p 5223 2403:300:a42:902::13
nmap -6 -Pn -p 5223 2403:300:a42:903::6
nmap -6 -Pn -p 5223 2403:300:a42:903::15
# 2403:300:a42:200::/56 Osaka
nmap -6 -Pn -p 5223 2403:300:a42:20c::6
nmap -6 -Pn -p 5223 2403:300:a42:20c::7
# 2403:300:a42:b00::/56 apac-taiwan-courier-vs.
nmap -6 -Pn -p 5223 2403:300:a42:b0c::6
# 2403:300:a42:c00::/56 Singapore
nmap -6 -Pn -p 5223 2403:300:a42:c0a::6

APNsホストはpingに応答しないが、当然TCPはリッスンしているのでRTTを計測できる。nmapならWindows 10のWSL1でも動く。

  • apac-asia-courier-vs.に含まれる最も遅延が小さいapac-china-courier-vs.は、やんごとなき理由か5223/tcpでAPNsに繋がらなかったため、除外した。
  • 調査の副産物で、apac-china-courier-vs.への5223/tcpが失敗且つAがNODATAだと443/tcpにフォールバックするが、このように空のlocal-data(ANSWER: 1)で応答すると5223/tcpを試行し続けた。
    未知の要因で発生する443/tcpへのフォールバックを回避するのに使えるが、5223/tcpが壊れ気味なのでおすすめしない。
	local-data: "push.apple.com. 60 IN A"

任意の回線をスピードテストする 見出しにジャンプ

Webのスピードテストは、IPv4/IPv6どちらをテストしたか不明瞭だったり、選べないことが多い。
そんなときはDNSで振り分けるのが簡単だ。Speedtest by OoklaはIPv6計測専用にしたいし、Wi-FiミレルはIPv4計測用に使いたかった。

server:
	local-zone: "prod.hosts.ooklaserver.net." block_a

	local-zone: "wifimireru.inonius.net" typetransparent
	local-data: "wifimireru.inonius.net 3600 IN AAAA"

Minecraft 見出しにジャンプ

Minecraft Launcherの高速化 見出しにジャンプ

IPv4を優先したりするので。突然ランチャーアップデートが来ても、一秒でも早くゲームに参加したい。

server:
	local-zone: "redstone-launcher.mojang.com." block_a
	local-zone: "api.mojang.com." block_a
	local-zone: "pc.realms.minecraft.net." block_a
	local-zone: "textures.minecraft.net." block_a

Minecraftサーバの管理者にメッセージを送る 見出しにジャンプ

Minecraftサーバへの接続は、SRV, A, AAAAの順で試行される。接続に使われたSRVレコードの内容はサーバ側に伝わる。
Protocol - wiki.vg
例えば、下記のように設定をするとmc.example.net.への接続にmake-peace-with.cat.を利用でき、猫と和解するよう相手に要求できる。管理者がコネクション確立時のログを読んでいれば、の話だが。

server:
	local-zone: "cat." static
	local-data: "_minecraft._tcp.make-peace-with.cat. 60 IN SRV 0 0 25565 mc.example.net."

DoT downstream 見出しにジャンプ

Unbound <--> キャッシュDNS間のupstreamにDo53を使っているにも拘らず、端末 <--> Unbound間のdownstreamにDoTを使う馬鹿みたいな設定を行った。
Do53はキャッシュポイズニングの軽減にポートランダマイゼーションを行うが、フルトラストネットワークでは無駄なオーバーヘッドでしかないからだ。

DoTはApple macOS 11+/iOS 14+、Android 9+、Windows 11と主にOSの対応が主なのに対し、DoHはブラウザでの対応が中心だから、自動設定に重きを置くならDoT、パフォーマンスに重きを置くならDoHになるだろう。
実際のところ、例えばApple iOSが繰り出すDo53クエリは、リゾルバへのDoSではないかと思うほど高速だ。主に非同期なURLSessionを使っているためと思われる。それでも、Appleデバイスは通常のHappy Eyeballs比で1.5 - 3倍のクエリを発するため、暗号化のオーバーヘッドを超えるかも分からんな、程度のモチベーションだ。

server:
	interface: 10.0.3.5@853
	port: 853

	tcp-idle-timeout: 120000
	edns-tcp-keepalive: yes
	# edns-tcp-keepalive-timeout: 120000

	# unbound-control-setup で生成した自己署名証明書
	tls-service-key: "/usr/local/etc/unbound/unbound_server.key"
	tls-service-pem: "/usr/local/etc/unbound/unbound_server.pem"

	tls-port: 853

tcp-idle-timeoutはdownstreamにも有効らしく、頻繫にコネクションを張りなおしていては意味がないので2分(2MSL)に設定した。edns-tcp-keepalive: yes時はedns-tcp-keepalive-timeoutを優先するとか。
Reduce number of TLS connections to forwarded (DoT) when using "forward-tls-upstream" · Issue #47 · NLnetLabs/unbound

# WSL2
dig @10.0.3.5 www.google.com aaaa +tls 

DDR 見出しにジャンプ

Do53をListenするリゾルバを端末に自動設定するなら、多くのOSが対応しているDHCPやDHCPv6, RAオプションが使用できる。
ただし、DoTやDoHはIPアドレスだけでなくホスト名や接続方式、ポート番号、DoHなら更にパスの情報を用いて接続するため、端末が_dns.resolver.arpa. IN SVCBをクエリすることで機能するDDRと、DHCP/DHCPv6オプションで通知するDNRを使う。
RFC 9462 - Discovery of Designated Resolvers
RFC 9463 - DHCP and Router Advertisement Options for the Discovery of Network-designated Resolvers (DNR)

Apple macOS 13+/iOS 16+はDDRやMDMによるDoT/DoHの自動設定に対応している。後述の構成プロファイルで暗号化リゾルバを手動設定できるが、Do53はできない。
AppとサーバのDNSセキュリティの改善 - WWDC22 - ビデオ - Apple Developer

Android 9以降はOpportunistic Discovery(Do53リゾルバのアドレスにDoTを試行)&日和見暗号化する。
DNS over HTTPS/TLS (DoH/DoT)の設定方法 | IIJ Engineers Blog
RubyKaigi 2023でのセキュアなDNSリゾルバの運用 (DNSOPS.JP BoF IW2023) - 20231121-hanazuki.pdf

Windows 11はDDRはBuild 22489で正式対応、DNRはBuild 25982以降で対応する。
Introducing DNR support for Windows Insiders

Unboundが管理するのはDDRの方だ。

server:
	local-zone: '_dns.resolver.arpa.' static
	local-zone-tag: '_dns.resolver.arpa.' windows
	local-data: '_dns.resolver.arpa. 60 IN SVCB 1 dot.example.com. alpn="dot" port=853 ipv4hint=10.0.3.5'

	# DNS rebinding対策から除外
	local-zone: 'dot.example.com' static
	local-data: 'dot.example.com 300 IN A 10.0.3.5'

	# certbot
	tls-service-key: "/etc/letsencrypt/live/dot.example.com/privkey.pem"
	tls-service-pem: "/etc/letsencrypt/live/dot.example.com/fullchain.pem"
certbot certonly --manual --preferred-challenges dns -d dot.example.com -m [email protected]

openssl s_client -connect 10.0.3.5:853 | openssl x509 -noout -text | grep -A1 'Subject Alternative Name'
#            X509v3 Subject Alternative Name:
#                DNS:dot.example.com

とりあえずcertbotでSSL証明書を発行する #Let’sEncrypt - Qiita

  • Appleデバイス(iOS 17.6.1時点)は、DDRを試行するものの下記エラーが出てDoT接続が持続しなかった。自己署名かは関係なかった。
[1723521202] unbound[6426:1] error: ssl handshake failed crypto error:0A000416:SSL routines::ssl/tls alert certificate unknown

SSL handshake failed · Issue #561 · NLnetLabs/unbound


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>PayloadContent</key>
    <array>
        <dict>
            <key>DNSSettings</key>
            <dict>
                <key>DNSProtocol</key>
                <string>TLS</string>
                <key>ServerAddresses</key>
                <array>
                    <string>10.0.3.5</string>
                </array>
                <key>ServerName</key>
                <string>dot.example.com</string>
            </dict>
            <key>OnDemandRules</key>
            <array>
                <dict>
                    <key>InterfaceTypeMatch</key>
                    <string>WiFi</string>
                    <key>SSIDMatch</key>
                    <array>
                        <string>ssid1</string>
                        <string>ssid2</string>
                    </array>
                    <key>Action</key>
                    <string>Connect</string>
                </dict>
                <dict>
                    <key>Action</key>
                    <string>Disconnect</string>
                </dict>
            </array>
            <key>PayloadDescription</key>
            <string>com.example.dotpayload</string>
            <key>PayloadDisplayName</key>
            <string>com.example.dotpayload</string>
            <key>PayloadIdentifier</key>
            <string>com.example.dotpayload</string>
            <key>PayloadType</key>
            <string>com.apple.dnsSettings.managed</string>
            <key>PayloadUUID</key>
            <string>6d16eda2-3a25-4db1-8f51-5c7ea915a7f1</string>
            <key>PayloadVersion</key>
            <integer>1</integer>
        </dict>
    </array>
    <key>PayloadDisplayName</key>
    <string>Unbound DoT</string>
    <key>PayloadIdentifier</key>
    <string>com.example.dot</string>
    <key>PayloadType</key>
    <string>Configuration</string>
    <key>PayloadUUID</key>
    <string>703ddb36-2a72-46f3-b566-2bd1970e4b5b</string>
    <key>PayloadVersion</key>
    <integer>1</integer>
</dict>
</plist>