Ubuntu Weekly Recipe

第834回Unboundでお手軽に家庭内DNSサーバーを作ろう[Ubuntu 24.04 LTS対応版]

第386回では、Unboundを使って家庭内DNSサーバーを構築しました。ですがこの記事から9年が経ち、Ubuntuのネットワークまわりも変化しました。当時とは状況も変わってきていますので、改めて最新のLTSである24.04を使い、Unboundの構築方法を紹介します。

家庭内DNSサーバーが必要な理由

DNSサーバには大きく分けて、ゾーン情報を管理するコンテンツサーバーと、名前解決を代行するキャッシュサーバーの二種類に分けられます。コンテンツサーバーは、そのドメインの名前情報を管理するサーバーで、インターネット全体に対してサービスを提供しなければなりません。そのため基本的に、ご家庭内には不要なサーバーです。

対してキャッシュサーバーは、どのご家庭にも存在します。通常、クライアントが直接、DNSのルートサーバーと通信することはありません。クライアントはキャッシュサーバーにリクエストを投げ、実際の名前解決はキャッシュサーバーがクライアントの代理として行います。今時のご家庭であれば、Wi-Fi機能を持ったルーターを設置して、インターネットに接続しているケースが多いでしょう。通常はこのルーターがキャッシュサーバーを兼ねており、OSのネットワーク設定では、使用するDNSサーバーとして、ルーターのIPアドレスが指定されているのが一般的です。

図1 DHCPでIPアドレスが振られたUbuntuの例。DNSサーバーの欄が使用しているキャッシュサーバーとなり、ここではデフォルトゲートウェイ(ルーター)と同じになっている
resolvectlコマンドでも、使用中のDNSサーバーを確認できる
$ resolvectl dns
Global:
Link 2 (eno1): 192.168.1.1
Link 3 (enp3s0):
Link 4 (wlp2s0):

今回紹介するUnboundは、Ubuntu上に構築するDNSキャッシュサーバーです。つまり現在利用している、ルーターのキャッシュサーバー機能を置き換えられるものとなります。

「でも、ルーターの機能で間に合うなら、そんなのいらないのでは?」

そう考える人もいるでしょう。ですが自由にいじれるLinux上にサーバーを構築するのには、それなりに便利な理由というものがあるのです。

クエリログが確認できる

キャッシュサーバーは、クライアントに代わって名前を解決します。そしてリクエストされたDNSクエリとその結果は、クエリログというログに残ります。つまりクエリログを見れば、いつ、誰が、どのドメインにアクセスしたのかがわかるわけです。内部がブラックボックスなスマホアプリであっても、DNSクエリを見れば、どのサイトと通信しているかがわかります。DNSクエリによって、不審なドメインと通信しているアプリを発見した、などという例もあります。パケットキャプチャをしなくても、通信しているサイトがわかるというのは、意外と便利なものなのです。

もちろん、ルーターのキャッシュサーバーでも、クエリログを見られたり、場合によってはSyslog転送に対応している機種もあるでしょう。ですがフル機能のLinuxサーバーであれば、データの保全や加工、集計、監視といった面においてアドバンテージがあります。特に企業においては、クエリログを保全しておくと、セキュリティ的にも役立つ日が来るかもしれません。

普段使いのドメインを家庭内でも使える

コンテンツサーバーはご家庭内には不要と言いましたが、ご家庭内でも名前解決をしたくなることがあります。特に家庭内サーバーを運用していると、IPアドレスではなくドメイン名でアクセスしたいですよね。Unboundは、ローカルなゾーンを持つことができます。本来であれば正規のコンテンツサーバーへ名前解決しに行く所を、このローカルなゾーン情報を元に応答を返せます。つまりこのゾーンにプライベートIPアドレスを登録すれば、家庭内のPCやサーバーに、好きな名前をつけられるのです。

もちろん、単にIPアドレスのかわりに名前を使いたいだけであれば、DNSサーバーは必要ありません。UbuntuではAvahiがインストールされていますので、mDNSによってホスト名からIPアドレスを引くことができます。ですがこの場合、mDNSの仕様によって、ドメイン部分が「.local」となってしまいます。対してUnboundを使えば、インターネット上で使っているのと同じドメイン(example.comなど)を家庭内でも使うことができるのです。そしてこうしたドメインが使えるということは、正規のSSL証明書が使えるということでもあります。

またインターネット経由と家庭内とで、同じドメインを違うIPアドレスで運用したいような場合にも役立ちます。例えば家庭内でWebサービスを動かし、インターネットに公開しているとします。するとそのサービスのドメイン(例えばwww.example.comなど)には、ルーターに割り当てられたグローバルIPアドレスを、パブリックなDNSレコードとして登録しているはずです。ですが家庭内からそのサービスを利用する時は、プライベートIPアドレスを使って、直接接続したいですよね。Unboundに「www.example.com」のローカルデータを持たせ、プライベートIPアドレスを登録すれば、インターネット上と異なる名前解決結果を返すことができるわけです。

特定のドメインをDNSレベルでブロックできる

世の中には、接続したくないドメインというものが存在します。例えば犯罪にかかわるサイトであったり、マルウェアを配布しているサイトであったりなどです。Unboundは、前述のローカルなゾーンの機能を使い、指定されたドメインの名前解決要求を拒否したり、常にNXDOMAIN(そのドメインは存在しない)を返すことができます。クライアントはそのドメインの名前解決ができなくなるため、そのサイトには到達できなくなるというわけです。DNSレベルでブロックするため、クライアントにセキュリティソフトなどを別途インストールする必要もありませんし、PCやスマホからゲーム機まで、クライアントの種類も選びません。またファイアウォールで通信を遮断しているわけではないため、⁠回避しようと思えばできる」ゆるさも、筆者は気に入っています。

最近では「.zip」⁠.mov」のような、ファイル拡張子と同じトップレベルドメインが登場しました。これがセキュリティ的な脅威になるという話があります。例えばファイル名に見せかけてチャットサービスなどに文字を入力すると、それが勝手にハイパーリンク化され、クリックした人を有害なサイトに誘導する、といった攻撃もありうるでしょう。Unboundではトップレベルドメインごと、名前解決を無効にできます。実際に筆者の自宅では、.zipドメインはすべて、名前解決ができなくなっています。

Unboundのインストール

それでは実際に、Unboundサーバーを構築してみましょう。Unboundはunboundパッケージでインストールできます。

unboundパッケージのインストール
$ sudo apt install -U -y unbound

インストールすると、自動的にUnboundが起動します。

Unboundが127.0.0.1:53で待ち受けている状態。127.0.0.{53,54}:53を待ち受けているのは、Ubuntuのスタブリゾルバであるsystemd-resolve
$ sudo ss -lutnp
Netid   State    Recv-Q   Send-Q                         Local Address:Port     Peer Address:Port  Process                                      
udp     UNCONN   0        0                                  127.0.0.1:53            0.0.0.0:*      users:(("unbound",pid=1416,fd=5))           
udp     UNCONN   0        0                                 127.0.0.54:53            0.0.0.0:*      users:(("systemd-resolve",pid=543,fd=16))   
udp     UNCONN   0        0                              127.0.0.53%lo:53            0.0.0.0:*      users:(("systemd-resolve",pid=543,fd=14))   
udp     UNCONN   0        0                                      [::1]:53               [::]:*      users:(("unbound",pid=1416,fd=3))           
tcp     LISTEN   0        4096                              127.0.0.54:53            0.0.0.0:*      users:(("systemd-resolve",pid=543,fd=17))   
tcp     LISTEN   0        256                                127.0.0.1:53            0.0.0.0:*      users:(("unbound",pid=1416,fd=6))           
tcp     LISTEN   0        4096                           127.0.0.53%lo:53            0.0.0.0:*      users:(("systemd-resolve",pid=543,fd=15))   
tcp     LISTEN   0        256                                    [::1]:53               [::]:*      users:(("unbound",pid=1416,fd=4))           
(...略...)

ですが待ち受けているアドレスが127.0.0.1のため、外部からの名前解決要求を受け取ることができません。家庭内の他のPCからも利用できるよう、設定を変えていきましょう。

Unboundの設定

Unboundの設定ファイルは/etc/unbound/unbound.confです。このファイルは、属性キーワードと、その後に続く値で構成されています。属性キーワードの後にはコロンが続き、キーワード間は半角スペースで区切る必要があります。

unbound.confの表記方法
属性: 値

属性の後には、別の属性も指定できます。これを「句」と呼び、複数の属性をグループ化できます。

句の例
server:
	verbosity: 1
	interface: 192.168.1.5
	port: 53
	access-control: 127.0.0.0/8 allow
	access-control: 192.168.1.0/24 allow

デフォルトのunbound.confは以下のように、include-toplevelが一行書かれているだけです。

/etc/unbound/unbound.confの内容
# Unbound configuration file for Debian.
#
# See the unbound.conf(5) man page.
#
# See /usr/share/doc/unbound/examples/unbound.conf for a commented
# reference config file.
#
# The following line includes additional configuration files from the
# /etc/unbound/unbound.conf.d directory.
include-toplevel: "/etc/unbound/unbound.conf.d/*.conf"

実は以前のUnboundでは、ここは以下のように設定されていました。

以前のUnboundの設定ファイルより抜粋
include: "/etc/unbound/unbound.conf.d/*.conf"

includeはその名の通り、他のファイルから設定を読み込むために使われます。これにより、適度な粒度で設定ファイルを分割し、可読性やメンテナンス性を上げることができるわけです。include-toplevelはより構造的な設定を可能にするincludeオプションです。include-toplevelが指定されると、現在アクティブな句がすべて閉じられ、インクルードされたファイル内、もしくはinclude-toplevelの直後で、句の使用が強制されます。つまり設定ファイルは句単位の粒度で区切り、/etc/unbound/unbound.conf.d/*.confに分割して置くのがよいでしょう。

Unbound全体の設定ファイルとして、/etc/unbound/unbound.conf.d/unbound.confを以下の内容で作成しました。

/etc/unbound/unbound.conf.d/unbound.confの内容
server:
	interface: 自分自身のIPアドレス
	port: 53
	access-control: 127.0.0.0/8 allow
	access-control: LANのネットワークアドレス/サブネットマスク allow
	use-syslog: yes
	log-queries: yes
	hide-version: yes
	hide-identity: yes

forward-zone:
	name: "."
 	forward-addr: フォワード先の(プロバイダが提供している)プライマリDNS
 	forward-addr: フォワード先の(プロバイダが提供している)セカンダリDNS

interfaceはUnboundが待ち受けに利用するIPアドレスです。ここではサーバーに指定されているプライベートIPアドレスを指定しました。現在のUbuntuでは、systemd-resolvedがスタブリゾルバとして127.0.0.{53,54}:53を待ち受けているため、ここを「0.0.0.0」とすると、バインドに失敗するため注意してください。portは待ち受けるポート番号です。

access-controlは、文字通りアクセスのコントロールです。ここでは自分自身と、LAN内のIPアドレスのレンジからのアクセスを「allow(許可⁠⁠」としています。

use-syslogはSyslogを使ってログを出力する設定です。

log-queriesはクエリログを出力する設定です。

hide-versionとhide-identityは、version.server、version.bind、id.server、hostname.bindクエリを拒否する設定です。

forward-zone句には、Unboundがクエリをフォワードする設定を記述します。nameにはフォワードするゾーンの名前を指定します。ここでは「.」を指定していますが、これはすべての問い合わせを意味します。forward-addrには、このゾーンのフォワード先を指定します。ここではプロバイダが提供しているDNSサーバーのアドレスを設定しました。これで自分自身が(ローカルなデータやキャッシュで)応答できないクエリに関しては、すべてプロバイダのDNSサーバーに、再帰的に問い合わせます。

設定が完了したら、Unboundを再起動してください。これで、通常のキャッシュサーバーとして動作するようになりました。

Unboundの再起動
$ sudo systemctl restart unbound.service 

ログの設定

上記の設定でuse-syslogを有効にしました。Unboundがsyslogに出力するログファシリティはdaemonです。ですがUbuntuのデフォルト設定では、daemon.*ログは個別のログファイルに書かれず/var/log/syslogにだけ書かれてしまいます。そこでログをUnbound専用のファイルに書き出すよう、設定を変更します。⁠/etc/rsyslog/30-unbound.conf」というファイルを、以下の内容で作成します。

/etc/rsyslog/30-unbound.confの内容
if $syslogfacility-text == 'daemon' and $programname contains 'unbound' then 
  -/var/log/unbound/unbound.log
& ~

これはログファシリティが「daemon」で、プログラム名に「unbound」を含むログを、⁠/var/log/unbound/unbound.log」に出力する設定です。最後の行にある「&」は直前の条件式を、⁠~」はログの破棄を意味します。つまりここでUnboundのログは破棄され、後続の条件にはマッチしなくなります。この行を指定しないと、unbound.logとsyslogの両方に、ログが重複して書かれてしまうためです。設定ができたら、rsyslogを再起動します。

rsyslogの再起動
$ sudo systemctl restart rsyslog.service

実際にクライアントが名前解決のリクエストを投げると、⁠/var/log/unbound/unbound.log」には以下のようにログが出力されます。

クエリログの例
Oct  9 00:00:12 canopus unbound: [989:0] info: 192.168.1.100 mail.google.com. AAAA IN
Oct  9 00:01:10 canopus unbound: [989:0] info: 192.168.1.4 api.mackerelio.com. AAAA IN
Oct  9 00:01:10 canopus unbound: [989:0] info: 192.168.1.4 api.mackerelio.com. A IN
Oct  9 00:01:14 canopus unbound: [989:0] info: 192.168.1.100 play.google.com. AAAA IN
Oct  9 00:01:28 canopus unbound: [989:0] info: 192.168.1.102 cl4.apple.com. TYPE65 IN
Oct  9 00:01:28 canopus unbound: [989:0] info: 192.168.1.102 cl4.apple.com. AAAA IN
Oct  9 00:01:28 canopus unbound: [989:0] info: 192.168.1.102 cl4.apple.com. A IN
Oct  9 00:01:33 canopus unbound: [989:0] info: 192.168.1.4 motd.ubuntu.com. A IN
Oct  9 00:01:33 canopus unbound: [989:0] info: 192.168.1.4 motd.ubuntu.com. AAAA IN

クエリログは出力され続けますから、ディスクが枯渇しないよう、ログのローテートも設定しておきましょう。⁠/etc/logrotate.d/unbound」というファイルを、以下の内容で作成します。

/etc/logrotate.d/unboundの内容
/var/log/unbound/unbound.log
{
        rotate 30
        daily
        copytruncate
        nocompress
        su root syslog
        postrotate
                /usr/lib/rsyslog/rsyslog-rotate
        endscript
}

ここでは日次ローテートで、30世代分のログを保存する設定としました。このあたりは環境にあわせて変更してください。またログファイルが大きくなりがちな環境では、ログファイルの圧縮も行ったほうがいいかもしれません。

ローカルゾーンの設定

家庭内サーバーの名前解決ができるよう、ローカルゾーンを設定しましょう。ここでは例として、⁠example.com」というドメインをインターネット上で運用しており、そのサブドメインとして「host1」「host2」というサーバーを、家庭内に立てた想定です。

まずローカルゾーンを定義する設定ファイルとして、⁠/etc/unbound/unbound.conf.d/local-zone.conf」を作成します。内容は以下の通りです。

/etc/unbound/unbound.conf.d/local-zone.confの内容
server:
	local-zone: "example.com." transparent
	local-data: "host1.example.com.    300 IN A 192.168.1.20"
	local-data: "host2.example.com.    300 IN A 192.168.1.21"

server句の中に、local-zoneとして「example.com」を定義しました。またこのゾーンのタイプとして「transparent」を指定しています。これはUnboundがデータを持っていなかった場合の挙動の指定です。具体的に言うと、⁠transparent」を指定した場合、まずローカルデータに対してマッチングを行い、データが存在しなかった場合は、フォワード先のDNSサーバーに対して通常の名前解決を依頼します。ここの例で言えば、⁠host1.example.com」の名前解決要求に対しては、ローカルデータとして定義されている「192.168.1.20」を返しますが、仮に「host3.example.com」という名前解決が要求された場合は、ローカルデータが存在しないため、フォワード先に名前解決を依頼するというわけです。インターネット上で運用しているドメインに対し、ローカルなデータを追加したい場合は、このタイプを選択してください。

local-dataは文字通り、ローカルなデータの設定です。ここではhost1とhost2というサーバーのプライベートIPアドレスを設定しています。

ローカルゾーンの設定ができたら、Unboundを再起動してください。local-dataに設定した名前解決ができることと、⁠インターネット上から名前解決できる)⁠example.com」の別のレコードの名前解決に影響がないことを確認しておきましょう。

特定ドメインのブロック

先ほどはexample.comのゾーンに対し、transparentというタイプを設定しました。ゾーンには他にもいくつかのタイプが用意されており、ゾーンをブロックする際に利用できるのが「refuse」です。例えば先ほどの例のように、.zipドメインすべてをブロックするとしましょう。⁠/etc/unbound/unbound.conf.d/blacklist.conf」というファイルを、以下の内容で作成してください。

.zipドメインの名前解決要求に対してREFUSEDを応答する設定
server:
	local-zone: "zip." refuse

refuseは、そのゾーンの名前解決要求に対して「REFUSED(リクエストの拒否⁠⁠」を返します。ただしrefuseに指定されたゾーンであっても、Unboundがローカルデータを持っていた場合は、そのレコードを応答します。

これに対して「always_nxdomain」というタイプは、ローカルデータがあったとしてもすべて無視し、名前解決要求に対して常に「NXDOMAIN(ドメインが存在しない⁠⁠」を返します。例えば以下の設定では、⁠example.net」に対する名前解決要求は、すべてNXDOMAINとなります。絶対にブロックしたいドメインは、⁠always_nxdomain」を指定してしまうとよいでしょう。

特定のドメインに対して、常にNXDOMAINを応答する設定
server:
	local-zone: "example.net." always_nxdomain

このように、DNSサーバーを自前で構築することで、ご家庭のネットワークをちょっと便利に改善できるかもしれません。DNSサーバーはそれほどパワーが必要なサーバーではありませんから、ご家庭にいくつか余っているRaspberry Piなどで運用してみてはいかがでしょうか。

おすすめ記事

記事・ニュース一覧