コマンド集 / Technitium DNS
Technitium DNS コマンド集
Technitium DNS Server の導入と更新、ゾーンとレコードの API 操作(curl)、フォワーダー、解決確認、ログ、split-horizon や WARP との組み合わせで踏んだ罠を引ける。
Debian 13 の非特権 LXC 2 台(dns01 / dns02)で Technitium DNS Server をクラスタ(Primary / Secondary)として動かし、権威ゾーン lab.example.jp と逆引きを持たせている環境向け。API はすべて http://127.0.0.1:5380/api/... に token を付けて呼ぶ。
導入と更新#
インストール(更新も同じスクリプト)#
公式のインストーラは既に入っていれば更新として動く。
curl -sSL https://download.technitium.com/dns/install.sh | bash
systemctl status dns
⚠️ http://<IP>:5380 は平文 HTTP。管理者パスワードもセッションも LAN を暗号化なしで流れる。構築中と緊急時だけ使い、入口が整ったら管理者パスワードを変えること。
トークンの持ち方#
API トークンは環境変数か 0600 のファイルで持ち、コマンドラインに直書きしない。
T="$(op read op://<vault>/<item>/credential)"
curl -s -G 'http://127.0.0.1:5380/api/user/session/get' --data-urlencode "token=$T" | jq '.status'
⚠️ レスポンスを丸ごと表示しない。session/get のトップレベルにトークンが入っている。jq で出してよいキーだけ名指しする(allow-list)。構築中に /root/.dns-token のような控えを置いたら必ず消す。
LXC 側で TUN を許す(必要な場合のみ)#
CT 内で VPN 系のデーモンを動かすときの設定。DNS 単体なら不要。
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
クラスタ(HA)#
Primary を初期化する#
curl -s -G 'http://127.0.0.1:5380/api/admin/cluster/init' \
--data-urlencode "token=$T" \
--data-urlencode 'clusterDomain=lab.example.jp' \
--data-urlencode 'primaryNodeIpAddresses=<PrimaryのIP>'
Secondary を参加させる#
管理画面(Administration → Cluster → Initialize → Join)から。API の cluster/initJoin は Primary の管理者パスワードを要求するので自動化に向かない。
Primary Node URL https://dns01.lab.example.jp:53443/
Primary Node IP Address <PrimaryのIP>
Certificate Validation Ignore certificate errors
⚠️ URL に IP を書くと弾かれる(the Primary Node URL must use the domain name)。URL はホスト名、IP は別欄。Join すると Secondary の設定は Primary の内容で上書きされるので、先に Primary を作り込む。自己署名のままでよく、join 後は DANE-EE で相互認証する。
2 台が同一か点検する#
差分が出たらどちらかが壊れている。
for ct in <ctid1> <ctid2>; do
pct exec $ct -- /bin/sh -c 'systemctl is-active dns cloudflared | tr "\n" " "; echo'
done
⚠️ 設定は 2 台で同期する。 片方だけに settings/set を投げても両方変わる。段階投入はできないので、退路が要るなら片方を止めるなど別の手段を使う。
ゾーンとレコード(API)#
ゾーン一覧・作成#
curl -s -G 'http://127.0.0.1:5380/api/zones/list' --data-urlencode "token=$T" | jq '.response.zones[].name'
curl -s -G 'http://127.0.0.1:5380/api/zones/create' --data-urlencode "token=$T" \
--data-urlencode 'zone=lab.example.jp' --data-urlencode 'type=Primary'
curl -s -G 'http://127.0.0.1:5380/api/zones/create' --data-urlencode "token=$T" \
--data-urlencode 'zone=0.0.10.in-addr.arpa' --data-urlencode 'type=Primary'
A レコードを足す(PTR も同時に)#
curl -s -G 'http://127.0.0.1:5380/api/zones/records/add' --data-urlencode "token=$T" \
--data-urlencode 'zone=lab.example.jp' --data-urlencode 'domain=node1.lab.example.jp' \
--data-urlencode 'type=A' --data-urlencode 'ipAddress=<node1のIP>' \
--data-urlencode 'ttl=3600' --data-urlencode 'ptr=true'
レコードを読む・消す#
curl -s -G 'http://127.0.0.1:5380/api/zones/records/get' --data-urlencode "token=$T" \
--data-urlencode 'zone=lab.example.jp' --data-urlencode 'domain=lab.example.jp' \
--data-urlencode 'listZone=true' | jq '.response.records[] | {name,type,rData}'
curl -s -G 'http://127.0.0.1:5380/api/zones/records/delete' --data-urlencode "token=$T" \
--data-urlencode 'zone=lab.example.jp' --data-urlencode 'domain=<FQDN>' \
--data-urlencode 'type=A' --data-urlencode 'ipAddress=<IP>'
AD へ NS 委任を置く#
ad.lab.example.jp は DC が持つ。親ゾーンに NS と glue を置き、条件付き転送は使わない。
curl -s -G 'http://127.0.0.1:5380/api/zones/records/add' --data-urlencode "token=$T" \
--data-urlencode 'zone=lab.example.jp' --data-urlencode 'domain=ad.lab.example.jp' \
--data-urlencode 'type=NS' --data-urlencode 'nameServer=dc01.ad.lab.example.jp' \
--data-urlencode 'glue=<dc01のIP>'
1 つの名前だけキャッシュを消す#
作る前に引いてしまうと Technitium が「無い」を覚え、作った直後からラボの中だけ引けない。公開名を足したら必ずこれを打つ。
curl -s -G 'http://127.0.0.1:5380/api/cache/delete' --data-urlencode "token=$T" \
--data-urlencode 'domain=<FQDN>'
⚠️ 消すのはドメイン単位。cache/flush で全消しすると直後に全部が上流へ問い合わせに行き、上流のレート制限に当たった実例あり。
フォワーダー#
見る・変える#
上流は DoH で複数並べると最速採用になる。DNSSEC 検証は ON にしておく。
curl -s -G 'http://127.0.0.1:5380/api/settings/get' --data-urlencode "token=$T" \
| jq '.response | {forwarders, forwarderProtocol, dnssecValidation, recursion}'
curl -s -G 'http://127.0.0.1:5380/api/settings/set' --data-urlencode "token=$T" \
--data-urlencode 'forwarders=https://cloudflare-dns.com/dns-query,https://dns.quad9.net/dns-query' \
--data-urlencode 'forwarderProtocol=Https' --data-urlencode 'dnssecValidation=true'
⚠️ settings/get の出力を丸ごと出さない。変更は両系同時に効く。
ブロックリストと許可ゾーン#
広告ブロックの例外(許可ゾーン)は再構築で復元されない唯一の情報。理由つきで別に記録する。
curl -s -G 'http://127.0.0.1:5380/api/allowed/add' --data-urlencode "token=$T" --data-urlencode 'domain=<例外にする名前>'
curl -s -G 'http://127.0.0.1:5380/api/allowed/list' --data-urlencode "token=$T" | jq '.response.allowedZones'
解決を確かめる#
dig(Linux)#
dig はクライアントのキャッシュを迂回する。dig は通るのにアプリだけ失敗するなら、サーバーではなくクライアントのキャッシュ。
dig @<DNSサーバーのIP> node1.lab.example.jp A +short
dig @<DNSサーバーのIP> -x <IP> +short
dig @<DNSサーバーのIP> dc01.ad.lab.example.jp +short
dig @<DNSサーバーのIP> dnssec-failed.org +dnssec
⚠️ 委任先の確認は dig +trace より、親と子の両方に直接聞く方が早い。DNSSEC 検証が生きていれば dnssec-failed.org は SERVFAIL になる。
nslookup / Resolve-DnsName(Windows)#
Resolve-DnsName -Name node1.lab.example.jp -Server <DNSサーバーのIP>
Resolve-DnsName -Name _ldap._tcp.ad.lab.example.jp -Type SRV
nslookup -type=NS ad.lab.example.jp <DNSサーバーのIP>
ipconfig /flushdns
上流のブロックが効いているか#
Cloudflare Gateway などを上流にしたときの確認。ブロックページの IP が返れば効いている。
dig @<DNSサーバーのIP> malware.testcategory.com +short
dig @1.1.1.1 malware.testcategory.com +short
ログ#
サービスと API#
journalctl -u dns --no-pager -n 50
ls -la /etc/dns/logs/
curl -s -G 'http://127.0.0.1:5380/api/logs/list' --data-urlencode "token=$T" | jq '.response.logFiles[].fileName'
⚠️ 自動更新の timer はコンテナの中にある。ホストで systemctl list-timers を見ても出ない。
統計#
curl -s -G 'http://127.0.0.1:5380/api/dashboard/stats/get' --data-urlencode "token=$T" \
--data-urlencode 'type=LastHour' | jq '.response.stats'
困ったとき#
公開名を作ったのにラボの中だけ引けない#
作る前に引いた否定キャッシュが残っている。cache/delete をその名前だけに打つ。作る手順の中に後始末を入れておかないと 2 回目を踏む。
curl -s -G 'http://127.0.0.1:5380/api/cache/delete' --data-urlencode "token=$T" --data-urlencode 'domain=<FQDN>'
split-horizon と公開ゾーン#
内部ゾーン名を公開ドメインのサブドメイン(lab.example.jp)にしてあるので、外の権威と内側の権威が別物になる。内側で持たない名前は上流へ抜ける。
dig @<DNSサーバーのIP> <公開の名前> +short
dig @1.1.1.1 <公開の名前> +short
⚠️ 内側の権威ゾーンに無い名前は、そのゾーンが権威なら NXDOMAIN になる。公開側にだけある名前は内側のゾーンにも足すか、ゾーンの切り方を見直す。
WARP(CGNAT 帯)とログの読み違い#
WARP を入れた端末は 100.64.0.0/10 の CGNAT 帯から来る。これを「インターネット宛」と数えると報告が嘘になる。
ip route get 100.96.0.1
CT の resolv.conf が古い#
pct set --nameserver は動いている CT の /etc/resolv.conf を書き換えない。自分自身が内部名を引けないと、上に載せたコネクタが壊れる。
pct exec <ctid> -- /bin/cat /etc/resolv.conf
pct push <ctid> ./resolv.conf /etc/resolv.conf
2 台とも落ちていないか#
片方が生きていれば解決は続く。「DNS が全部おかしい」ときは両方を疑う。
for ip in <DNS1のIP> <DNS2のIP>; do dig @$ip lab.example.jp SOA +short +time=2 +tries=1; done