モダン実装でステキな DNS フルリゾルバ Knot Resolver を紹介するよ

この記事は NTTコミュニケーションズ Advent Calendar 2021 23日目の記事です。

はじめに

こんにちは。デジタル改革推進部の髙田(@mikit_t)です。

業務では社内向けのデータ分析基盤の設計・開発および運用を行なっています。 データドリブン経営を推進するため、社内に散らばる様々なデータを収集・蓄積。データサイエンティストはもちろんのこと、各部署でのデータドリブンな意思決定に貢献できるよう活動しています。

社内のデータ分析コンペティションの環境も我々の分析基盤上で開催しています。 今回はデータ分析については触れませんので、データ分析に興味がある方は 社内でデータ分析コンペティションを開催しました の記事を参照してみてください。

本稿では、オンプレでサーバ運用するにあたって必ず必要になってくる DNSフルリゾルバ のうち、比較的新しい実装の Knot Resolver を紹介します。

書き始めたら大変長くなってしまったので、何回かに分割して掲載していきたいと思います。

DNSフルリゾルバ

おさらい

アプリケーションがホスト名による通信するためには、ホスト名を IPアドレスに変換する必要があります。 一般に、これを名前解決といいます。 名前解決をしてくれるのが「フルリゾルバ」、フルリゾルバに対して名前解決要求を出すのが「スタブリゾルバ」です。 「フルリゾルバ」は OS のネットワーク設定のところに設定する IP アドレスというとわかりやすいかもしれません。

Knot Resolver

チェコ CZ NIC がメンテしている実装で、2014年ごろ登場しました。 シンプルな core と拡張モジュールでの実装となっています。 拡張モジュール開発はユーザも任意に行えます。言語としては C, Lua, Go が使えます。 設定自体も Lua で書きます。

ほかのフルリゾルバ実装ではあまり見かけない、魅力的な特徴は以下の通りです。 かなりモダンな設計になっています。

  • シングルスタックでの実装となっており、複雑なスレッドプログラミングをしていない
  • キャッシュのバックエンドを永続化できる
    • バックエンドには lmdb, etcd を利用できる
    • インスタンス起動時に「あたためた」キャッシュをプリロードできる
  • Zero downtime restart
    • 複数インスタンスを並列起動することが推奨されている
    • インスタンスをひとつずつ順番に再起動することで、ダウンタイムを 0 にできる
  • BIND でいう rndc、unbound でいう unbound-control のような管理ツールが用意されていない
    • UNIX ドメインソケット経由で、設定の確認・変更を行える
  • Prometheus メトリクスのエンドポイントを内蔵している

まだ開発途上の新しいソフトウェアのため、設定項目名がカジュアルに変更されたり、メモリリークの修正が入ったりなどしているのが観測されています。 利用する場合はアップデートの際に ChangeLog をよく読むことをお勧めします。 利用実績としては Cloudflare が利用していると発表しています。

インストール・起動設定

  • インストール

コマンドラインについては Ubuntu 20.04 LTS で実施した内容となっています。

https://www.knot-resolver.cz/download/ の通りやっていきます。

# wget https://secure.nic.cz/files/knot-resolver/knot-resolver-release.deb
# dpkg -i knot-resolver-release.deb
# apt update
# apt install -y knot-resolver

Ubuntu 20.04 LTS の公式レポジトリにも Knot Resolver はありますが、3.2.1-3ubuntu2 と古いバージョンのコードベースとなっています。 執筆時点での最新版は 5.4.3 となっています。新しい機能を使うためには、CZNIC 公式レポジトリからのインストールをする必要があります。

  • systemd-resolved を止める

ローカルインタフェースの port 53 を listen している systemd-resolved を止めます。

$ sudo systemctl stop systemd-resolved.service
$ sudo systemctl disable systemd-resolved.service

systemd-resolved に名前解決要求をするようになっているため、これを無効化します。

$ sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
  • 起動設定

パッケージをインストールしてもサービスが有効化されませんので、やっておきます。

$ sudo systemctl enable --now kresd@1.service
  • 確認

動いているか確認してみます。

$ systemctl |grep kres
  kres-cache-gc.service loaded active running   Knot Resolver Garbage Collector daemon
  kresd@1.service       loaded active running   Knot Resolver daemon
  system-kresd.slice    loaded active active    system-kresd.slice

サービス起動OK。ひとつ DNS 名前解決してみましょう。

$ dig engineers.ntt.com @127.0.0.1

; <<>> DiG 9.16.1-Ubuntu <<>> engineers.ntt.com @127.0.0.1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26986
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

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

;; ANSWER SECTION:
engineers.ntt.com.  300 IN  A   13.115.18.61
engineers.ntt.com.  300 IN  A   13.230.115.161

;; Query time: 120 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Thu Dec 23 02:20:55 UTC 2021
;; MSG SIZE  rcvd: 78

OK です。

設定

  • デフォルト設定

デフォルトの設定ファイルが /etc/knot-resolver/kresd.conf に入っています。

-- SPDX-License-Identifier: CC0-1.0
-- vim:syntax=lua:set ts=4 sw=4:
-- Refer to manual: https://knot-resolver.readthedocs.org/en/stable/

-- Network interface configuration
net.listen('127.0.0.1', 53, { kind = 'dns' })
net.listen('127.0.0.1', 853, { kind = 'tls' })
--net.listen('127.0.0.1', 443, { kind = 'doh2' })
net.listen('::1', 53, { kind = 'dns', freebind = true })
net.listen('::1', 853, { kind = 'tls', freebind = true })
--net.listen('::1', 443, { kind = 'doh2' })

-- Load useful modules
modules = {
    'hints > iterate',  -- Allow loading /etc/hosts or custom root hints
    'stats',            -- Track internal statistics
    'predict',          -- Prefetch expiring/frequent records
}

-- Cache size
cache.size = 100 * MB

-- で始まる行はコメントになります。 内容を見ていきます。

  • net.listen()
    • どのアドレスとポートで、どんなサービスをするかを設定します。
    • kind: dns
      • 通常の DNS 名前解決のサービスです。
    • kind: tls
      • DNS over TLS のサービスです。
  • modules
    • ロードするモジュールを指定します。この部分はこのままで特に問題ないです。
    • hints > iterate: キャッシュよりもヒントを優先させることを指定しています。
    • stats: 各種メトリクスを収集します。prometheus エンドポイントを利用する場合は必須の設定です。
    • predict: キャッシュヒットの効率を高めるため、プリフェッチを行います。
  • cache.size
    • キャッシュに利用するメモリサイズを指定します。

運用において必要となりそうな設定を上げてみます。

  • サービスポートの設定

net.listen()127.0.0.1 ::1 のみだと自ホストからしか使えません。

オンプレ環境の他の機器からの名前解決要求を受け付けるための IPアドレスとポートを設定します。

net.listen('192.0.2.53', 53, { kind = 'dns' })
  • アクセス元制限の設定

net.listen() でサービス用のアドレスを設定したら、アクセス元制限をする必要があります。

これを書かないと オープンリゾルバ となってしまいますので、特に GIP を net.listen() で設定する場合には気をつけましょう。

-- ACL
modules = { 'view' }
view:addr('192.0.2.0/24', policy.all(policy.PASS))
view:addr('127.0.0.1', policy.all(policy.PASS))
view:addr('::1', policy.all(policy.PASS))
view:addr('0.0.0.0/0', policy.all(policy.REFUSE))
view:addr('::0/0', policy.all(policy.REFUSE))

この例では、192.0.2.0/24 とローカルインタフェースからの接続のみ許可、その他は拒否するようにしています。

  • log

ログレベルの設定をします。

crit, err, warning, notice, info, debug のいずれかを設定します。デフォルトは notice です。

-- log
log_level('debug')
  • bogus_log

DNSSEC 検証に失敗したログを出力します。

-- dnssec validation failure logging
modules.load('bogus_log')
  • nsid

RFC 5001 で定義されている nsid を使うと、複数インスタンスでの運用時、どのインスタンスが答えを返したかがわかるようになり便利です。

-- nsid
local systemd_instance = os.getenv("SYSTEMD_INSTANCE")
modules.load('nsid')
nsid.name(systemd_instance)
  • 設定を保存

ここまでの設定を /etc/knot-resolver/kresd.conf に書いておきます。

Run-time reconfiguration

ncsocat を使って UNIX ドメインソケット経由で knot resolver のインスタンスと通信し、インスタンスの設定をライブに確認・変更することができます。

ソケットファイルを確認します。

$ sudo ls -l /run/knot-resolver/control/
total 0
srwxr-xr-x 1 knot-resolver knot-resolver 0 Dec 22 20:13 1

今はインスタンスが 1つしかいないので、ソケットファイルも 1つだけあるのが確認できます。

ソケットファイルを指定して、socat を起動します。

$ sudo socat - UNIX-CONNECT:/run/knot-resolver/control/1
> help()
'help()
    show this help
quit()
    quit
hostname()
    hostname
package_version()
    return package version
user(name[, group])
    change process user (and group)
log_level(level)
    logging level (crit, err, warning, notice, info or debug)
(snip)

設定内容を確認してみます。

> log_level()
'notice'

ログレベルのデフォルト設定が返ってきました。

キャッシュのクリア

キャッシュのクリアをしてみます。

$ dig engineers.ntt.com @127.0.0.1
(snip)
;; ANSWER SECTION:
engineers.ntt.com.  300 IN  A   13.115.18.61
engineers.ntt.com.  300 IN  A   13.230.115.161

これでキャッシュに engineers.ntt.com の A レコードが保持されました。TTL は 300秒です。

> cache.clear('com.')
{
    ['count'] = 16,
    ['round'] = 1,
}

com. 配下のキャッシュを削除しました。count は消したレコードの数です。 再び名前解決を行うと、TTL が 300 の同じ結果が返ってくるはずです。

cache.clear() は指定された名前空間配下のすべてのキャッシュを消しますが、第二引数に true を指定すると、その名前だけを削除します。

> cache.clear('com.', true)
{
    ['count'] = 3,
    ['round'] = 1,
}

true としたため、com. の 3レコードのみ削除されたことがわかります。

knot-resolver には残念ながら、キャッシュの内容を dump するようなインタフェースはまだ用意されていません

Multiple Instances

インスタンスを2つ追加起動してみます。

$ sudo systemctl start kresd@2.service
$ sudo systemctl start kresd@3.service

確認してみます。dig に +nsid をつけて、nsid を要求します。

$ dig engineers.ntt.com +nsid @127.0.0.1

; <<>> DiG 9.16.1-Ubuntu <<>> engineers.ntt.com +nsid @127.0.0.1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17096
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; NSID: 33 ("3")
;; QUESTION SECTION:
;engineers.ntt.com.     IN  A

;; ANSWER SECTION:
engineers.ntt.com.  269 IN  A   13.115.18.61
engineers.ntt.com.  269 IN  A   13.230.115.161

;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Thu Dec 23 03:02:56 UTC 2021
;; MSG SIZE  rcvd: 83

NSID: 33 ("3") とあるとおり、3番目のインスタンスが返事をしています。

$ dig engineers.ntt.com +nsid @127.0.0.1

; <<>> DiG 9.16.1-Ubuntu <<>> engineers.ntt.com +nsid @127.0.0.1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 94
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

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

;; ANSWER SECTION:
engineers.ntt.com.  278 IN  A   13.230.115.161
engineers.ntt.com.  278 IN  A   13.115.18.61

;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Thu Dec 23 03:02:47 UTC 2021
;; MSG SIZE  rcvd: 78

nsid を返してこない返答もあります。これは最初に起動した、1つ目のインスタンスの返事です。

ソケットファイルも 3つできています。

$ sudo ls -l /run/knot-resolver/control/
total 0
srwxr-xr-x 1 knot-resolver knot-resolver 0 Dec 23 02:06 1
srwxr-xr-x 1 knot-resolver knot-resolver 0 Dec 23 03:02 2
srwxr-xr-x 1 knot-resolver knot-resolver 0 Dec 23 03:02 3

Zero-downtime restarts

複数インスタンスがサービスを分散処理しているのを確認できました。

1つ目のインスタンスに設定を読み込ませるため、再起動してみましょう。

別端末で dig を仕掛けて、名前解決に問題が起きないか確認しておきます。

$ while true; do echo "`date`; `dig engineers.ntt.com @127.0.0.1 +nsid | grep NSID`"; done
Thu 23 Dec 2021 03:12:51 AM UTC;
Thu 23 Dec 2021 03:12:51 AM UTC;
Thu 23 Dec 2021 03:12:51 AM UTC;
Thu 23 Dec 2021 03:12:51 AM UTC; ; NSID: 32 ("2")
Thu 23 Dec 2021 03:12:51 AM UTC;
Thu 23 Dec 2021 03:12:51 AM UTC;
Thu 23 Dec 2021 03:12:51 AM UTC; ; NSID: 32 ("2")
Thu 23 Dec 2021 03:12:51 AM UTC; ; NSID: 33 ("3")
Thu 23 Dec 2021 03:12:51 AM UTC; ; NSID: 32 ("2")

NSID つき、NSID なし、2種類の応答があります。

$ sudo systemctl restart kresd@1.service

と再起動すると、新しい設定が読み込まれます。

Thu 23 Dec 2021 03:12:53 AM UTC;
Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 33 ("3")
Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2")
Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 33 ("3")
Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2")
Thu 23 Dec 2021 03:12:53 AM UTC;
Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2")
Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2")
Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2")
Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2")
Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2")
Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 32 ("2")
Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 33 ("3")
Thu 23 Dec 2021 03:12:53 AM UTC; ; NSID: 33 ("3")
Thu 23 Dec 2021 03:12:54 AM UTC; ; NSID: 31 ("1")
Thu 23 Dec 2021 03:12:54 AM UTC; ; NSID: 33 ("3")
Thu 23 Dec 2021 03:12:54 AM UTC; ; NSID: 32 ("2")
Thu 23 Dec 2021 03:12:54 AM UTC; ; NSID: 32 ("2")

NSID 1 の応答が返ってくるようになりました。

おわりに

これまで紹介してきましたように、Knot Resolver はとてもモダンで、調べれば調べるほど面白いソフトウェアです。が、この記事では、魅力を十分に伝え切れたとは言えません。 Prometheus endpoint によるサーバ状況の可視化と監視、etcd でのキャッシュ内容永続化とインスタンス間の共有、DNSTAP によるクエリログ取得と分析など、まだまだ書きたいトピックがありますので、また時間を見つけて書いていきたいと思います。

それでは、明日の記事をお楽しみに!

© NTT Communications Corporation All Rights Reserved.