コマンド集 / 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.orgSERVFAIL になる。

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