クラウドネイティブな DNS（CoreDNS）で家庭内ネットワーク管理者が幸せになった話

## はじめに

この記事は [NTTテクノクロス Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ntt-tx) の1日目の記事です。

こんにちは。NTTテクノクロスの盛合（@moriai）です。会社では、[IoTソリューション](https://www.ntt-tx.co.jp/products/service/bigdata_ai_iot.html?utm_source=qiita&utm_medium=ac2019&utm_campaign=01)や[クラウド基盤サービス](https://www.ntt-tx.co.jp/products/osscloud/?utm_source=qiita&utm_medium=ac2019&utm_campaign=01)を中心に、私たちの取り組みを社外へ発信したり、技術ノウハウを社内に展開したり、社内開発環境の整備をお手伝いしたりといった業務で、マネージャとして働いています。また、日曜プログラマとして Swift や Rust をいじって [GitHub にコードを載せたり](https://github.com/moriai)、家庭内ネットワーク管理者としてトラブルシューティングなどをしています。

今日はクラウド系技術を家庭内ネットワークで活用したら、ちょっとだけ管理者が幸せになったお話を書いていきます。

## 家庭内ネットワーク管理者としての困りごと

自宅ではマンションのインターネット回線にWifiステーションを設置しています。長男のパソコンにWifiの設定を一度してやったら、家族のみんなが自分の端末をWifiに繋げて使っていたりするので、「このMACアドレス誰やねん？」ということが年に何回もあったりします。たいていは長男に聞けば状況が分かって、スマホの機種変、学校や職場のパソコンの持ち込み、ゲーム機をお年玉で買った、ということなのですが、家庭内ネットワーク管理者としては、精神衛生上よろしくないかなというところです。

インターネット回線（足回りがVDSL）としては常に70Mbps前後の速度が出ていて不満はないのですが、ポチッとした時にブラウザの反応が遅れることがあったり、そんなサイトはないと怒られることがあったりと、どうも DNS の反応が悪いドメインや時々引けないドメインがあるようです。

また、日曜プログラマとして Kubernetes や OpenShift のローカルクラスタで遊んでいると、自動的に割り当てられる「ほげほげ.nip.io」とかのホスト名検索が特に遅いし、内部で閉じても問題のなさそうな DNS クエリを外に飛ばさなくてもいいのにと思っています。

ということで、一念発起して、家庭内ネットワークにDNSを立ててみることにしました。

## どう解決すれば良さそうか？

**要件** を次のように整理してみました。

1. プライベート IP アドレス（16.172.in-addr.arpa,..., 168.192.in-addr.arpa,...） → マルチキャスト DNS（mDNS）で IP アドレスからホスト名へ解決、失敗したら /etc/hosts で解決
	- MAC アドレスからホスト名が分かれば誰の端末かが想像がつくが、MAC アドレスから直接ホスト名に紐付ける仕組みは IP over Ethernet の世界にはない。自宅のネットワークでの IP アドレスは DHCP で適当に割り当てられるため、IPアドレスでは誰の端末かは分からない。OS には IP アドレスからホスト名に自動的に変換する仕組みが備わっており、それが動作するようにすれば、`arp -a` や `netstat` などでホスト名で表示され、悩みはほぼ解決しそう。macOS/iOS や macOS 対応を謳っている周辺機器（プリンタやスキャナとか）なら mDNS で IP アドレスからホスト名が引ける。Windows でも最近は mDNS に対応している。
2. ワイルドカード DNS（\*.nip.io など） → ローカルで IPアドレスへ解決
	- 他者のドメインを勝手にアドレス解決したりするのはまずいと思うが、ワイルドカードをローカルに解決することをローカルに提供するのであれば、許容範囲だと信じたい。
3. 解決によく失敗するドメイン → パブリック DNS（1.1.1.1 や 8.8.8.8 など）へフォワードして解決
	- DNS を立てる Mac（Macbook Pro）は外に持ち出して公衆Wifiに繋げることもあるため、DoT（DNS over TSL）や DoH（DNS over HTTPS）の設定も確認しておきたい。
4. それ以外 → プロバイダ DNS で解決
	- 全てをパブリック DNS にフォワードしてしまった場合、DNS を活用してネットワーク的に近いサーバにトラフィックを振り向けるような仕組みが使えなくなる問題があるので、現時点ではなるべくプロバイダ DNS を活用したい。

ということで、ドメイン名によって、適切な DNS サーバへフォワードできる仕組み、ホスト名から IP アドレスへの変換をルールによって記述できる仕組みがあれば良さそうです。調べてみたら、CoreDNS でだいたいうまくいきそうだったので、これで DNS を立ててみることにします。

## CoreDNS とは？

[CoreDNS](https://coredns.io) はいわゆる「クラウドネイティブ」な DNSサーバプログラムで、[CNCF（Cloud Native Computing Foundation）](https://www.cncf.io)の１プロジェクトとして開発されています。高速で柔軟かつシンプル、プラグインによる拡張性などが売りのソフトウェアで、Kubernetes のクラスタ内で名前解決とサービスディスカバリの役割を担っています。

[minikube 1.5.2（Kubernetes 1.16.2）](https://minikube.sigs.k8s.io)を使っている時は、こんな感じで CoreDNS が動いていることが確認できます。

```
$ kubectl -n kube-system get pods
NAME                                        READY   STATUS    RESTARTS   AGE
coredns-5644d7b6d9-69kkz                    1/1     Running   3          4d
coredns-5644d7b6d9-f8lp6                    1/1     Running   3          4d
...
```

この例では、`coredns` の pod が２つあることがわかります。次に、ConfigMap をみてみましょう。

```
$ kubectl -n kube-system get configmap coredns -o yaml > coredns-config.yaml
```
```yaml:coredns-config.yaml
apiVersion: v1
data:
  Corefile: |
    .:53 {
        errors
        health
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
           ttl 30
        }
        prometheus :9153
        forward . /etc/resolv.conf
        cache 30
        loop
        reload
        loadbalance
    }
...
```

`Corefile` が CoreDNS の設定ファイルの中身になります。基本的な書き方は次の通りです。

```text:Corefile
ドメイン名:ポート番号 {
    プラグイン名 {
        プラグインのオプション
        ...
    }
    ...
}
```

`coredns-config.yaml`の例では、ルート（ . ）＝全てのドメインのクエリを扱う53番ポートを開き、errors, health, ready, kubernetes, ... のプラグインでクエリを処理することが宣言されています。プラグインの設定が評価される順番は Corefile に記載された順序ではなく、[CoreDNS のビルド時のコンフィグファイル](https://github.com/coredns/coredns/blob/master/plugin.cfg)に記載された順序となりますので、Corefile のブロック内でのプラグインの順序はあまり気にする必要はありません。主なプラグインの機能の概要は次の通りです。ここでは評価される順に記載しています。CoreDNS 1.6.5 では標準で45個のプラグインが組み込まれています。なお、Corefile や各プラグインの詳細については [CoreDNS のサイト](https://coredns.io)をご覧ください。

|プラグイン       |機能の概要|
|---------------|--------|
|reload		|設定ファイルの変更を検出したら自動的にリロードする。|
|ready		|全てのプラグインがレディになっているかをチェックできる HTTP ポートをオープンする。デフォルトは 8181 番。|
|health		|プロセス全体のヘルスチェック用の HTTP ポートをオープンする。デフォルトは 8080 番。|
|prometheus	|Prometheus からのメートリック取得用の HTTP ポートをオープンする。デフォルトは localhost の 9153 番。|
|errors		|エラーメッセージを標準出力に出力する。|
|log		|クエリのダンプを標準出力に出力する。|
|cache		|クエリに対するレスポンスをキャッシュする。キャッシュの有効期間は最長で 3600 秒。|
|template	|Go言語の template パッケージを用いて、クエリをレスポンス用に動的に書き換える。|
|hosts		|/etc/hosts 形式のゾーンデータを読み込む。|
|kubernetes	|Kubernetes のサービスディスカバリをオンにする。|
|file		|RFC1035 形式のゾーンデータを読み込む。|
|auto		|file の拡張版。ファイル名をゾーン名にマッピングする機能などを追加。|
|forward	|他の DNS サーバにクエリをフォワードする。|

さて、coredns がどんなクエリを処理しているかを探るために、Corefile を変更してみましょう。

```
$ kubectl -n kube-system edit configmap coredns
```

デフォルトのエディタが立ち上がります。`errors` の前あたりが分かりやすいので、そこに `log` を挿入して保存します。

```yaml
apiVersion: v1
data:
  Corefile: |
    .:53 {
        log
        errors
        health
        ready
        ...
    }
...
```

coredns の pod は複数あるので、それぞれに対して `kubectrl logs -f`をバックグラウンドで起動します。

```
$ for p in $(kubectl get pods -n kube-system -l k8s-app=kube-dns -o name); do kubectl logs -n kube-system -f $p & done
```

Kubernetes のクラスタを起動しただけでは、ほとんど DNS のクエリは飛びませんが、アプリケーションを立ち上げると、たくさんのクエリが飛んでくる様子が観察できます。みなさんの手元の環境で、観察してみてください。ここでは、minikube に [Eclipse Che](https://www.eclipse.org/che/) をデプロイして、C/C++ の workspace を１つ作った時の CoreDNS のログから、クエリのホスト名とステータスを拾い出してみます（重複したクエリは uniq しています）。

```
...
che-che.192.168.64.8.nip.io. NOERROR
plugin-registry-che.192.168.64.8.nip.io. NOERROR
github.com.che.svc.cluster.local. NXDOMAIN
github.com.svc.cluster.local. NXDOMAIN
github.com.cluster.local. NXDOMAIN
github.com. NOERROR
github-production-release-asset-2e65be.s3.amazonaws.com.che.svc.cluster.local. NXDOMAIN
github-production-release-asset-2e65be.s3.amazonaws.com.svc.cluster.local. NXDOMAIN
github-production-release-asset-2e65be.s3.amazonaws.com.cluster.local. NXDOMAIN
github-production-release-asset-2e65be.s3.amazonaws.com. NOERROR
serverf2l3keao-theia-ide9r4-server-3100.192.168.64.8.nip.io. NOERROR
dc.services.visualstudio.com.che.svc.cluster.local. NXDOMAIN
dc.services.visualstudio.com.svc.cluster.local. NXDOMAIN
dc.services.visualstudio.com.cluster.local. NXDOMAIN
dc.services.visualstudio.com. NOERROR
serverbanq5gt7-che-machine-execsl1-server-4444.192.168.64.8.nip.io. NOERROR
...
```

Kubernetes のクラスターの中から順番にサービスのありかを探している様子が良く分かりますね。

## CoreDNS を自宅ネットワークで動かしてみる

だいぶ前置きが長くなりましたが、ここからが本題です。

IPマルチキャストを扱うため、ネイティブな macOS 環境に DNS を構築します。macOS 用のバイナリは [GitHub から入手できます](https://github.com/coredns/coredns/releases)し、[Go言語の環境が整っていれば](https://golang.org/dl/)、[ソースからのビルド](https://github.com/coredns/coredns/blob/master/README.md)も `make` 一発で終わります。

次に、CoreDNS の設定です。

実は、CoreDNS には **要件1.** を満たすような mDNS のプラグインがなく[^1]、プラグインをいきなり自作するのも大変そうなので、DNS のクエリを mDNS にフォワードする別のソフトウェアを用意し、そこへ forward することにします。ただ、そのようなソフトウェアを見つけられず、たまたま触っていた [Vitaly Shukela さんの Rust 言語で書かれた dnscache](https://github.com/vi/dnscache) に２行追加するだけでその機能が実現できてしまったので、この dnscache 改造版を使うことにします。このソースコードは https://github.com/moriai/dnscache の mdns ブランチで公開しています。[Rust言語の環境をインストール](https://www.rust-lang.org/learn/get-started)した後、

```
$ cargo install --git https://github.com/moriai/dnscache.git --branch mdns
```

でバイナリをビルドし、インストールすることができます。

**要件2.** は、CoreDNS の template プラグインで対応します。書き換えのルールとしては、とりあえず `文字列.数字列a.数字列b.数字列c.数字列d.nip.io` を `数字列a.数字列b.数字列c.数字列d` に置換するルールだけで良さそうなので、

```
template IN A nip.io {
	match (^|[.])(?P<a>[0-9]*)[.](?P<b>[0-9]*)[.](?P<c>[0-9]*)[.](?P<d>[0-9]*)[.]nip[.]io[.]$
	answer "{{ .Name }} 60 IN A {{ .Group.a }}.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
	fallthrough
}
```

としてみます。

**要件3.** と **要件4.** は、CoreDNS の forward プラグインを使います。ただし、通常の DNS と DoT はこのプラグインで直接対応できますが、DoH は未サポートですので、mDNS と同様に DoH クライアントを用意し、そこへフォワードできるようにしておきます。DoH クライアントの実装はたくさんあるのですが、今回は [Alberto Bertogli さんの dnss](https://blitiri.com.ar/git/r/dnss/) を使います。Go言語、かつ、コンパクトというのが採用理由です。macOS 用のバイナリは提供されていないようなので、ソースからビルドします。

```
$ go install blitiri.com.ar/go/dnss
```

部品が一通り揃ったので、それらを `/usr/local/bin` にコピーし、CoreDNS を中心に繋げてみます。

なお、**要件3.** と **要件4.** の振り分け、つまり、パブリックDNSとプロバイダDNSの振り分けは難しいのですが、プロバイダDNSにフォワードすることを基本とし、不調な場合のみ、パブリックDNSに手動で切り替えることにします。また、パブリックDNSへの接続手段もとりあえずは手動で我慢することにします。

全体のシステム構成は次のようになります。青色の矢印は DNS クエリの流れを示しています。

![fig.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318080/19678508-26b2-8625-ed60-8b0b05a5d0c7.png)

Corefile は次の通りです。

```text:Corefile
16.172.in-addr.arpa {
	forward . 127.0.0.1:10053 {
		health_check 5m
	}
	cache 30
	log
	errors
	on startup /usr/local/etc/coredns/run.dnscache.sh
}

local {
	forward . 127.0.0.1:10053 {
		health_check 5m
	}
	cache 30
	log
	errors
}

. {
	template IN A nip.io {
		match (^|[.])(?P<a>[0-9]*)[.](?P<b>[0-9]*)[.](?P<c>[0-9]*)[.](?P<d>[0-9]*)[.]nip[.]io[.]$
		answer "{{ .Name }} 60 IN A {{ .Group.a }}.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
		fallthrough
	}

# 普通の DNS のプロトコルでパブリック DNS へフォワード
#	forward . 1.1.1.1:53 1.0.0.1:53

# DNS over TLS でパブリック DNS へフォワード
	forward . tls://1.1.1.1 tls://1.0.0.1 {
		tls_servername cloudflare-dns.com
		health_check 5s
	}

#　DNS over HTTPS でパブリック DNS へフォワード
#	forward . 127.0.0.1:11053 {
#		health_check 5s
#	}
#	on startup /usr/local/etc/coredns/run.dnss.sh

# 普通の DNS のプロトコルでプロバイダ DNS へフォワード
#	forward . x.x.x.x:53 y.y.y.y:53 {
#		health_check 5s
#	}

	cache 30
	log
	errors
	reload
}
```

それでは、macOS の DNS を `127.0.0.1` に設定[^2]して、いくつか試してみましょう。

```text:DNSなし
$ arp -an
? (172.16.xx.1) at xx:xx:xx:xx:38:3a on en0 ifscope [ethernet]
? (172.16.xx.5) at xx:xx:xx:xx:71:3 on en0 ifscope [ethernet]
? (172.16.xx.7) at xx:xx:xx:xx:d2:4b on en0 ifscope [ethernet]
? (172.16.xx.12) at xx:xx:xx:xx:47:23 on en0 ifscope [ethernet]
? (172.16.xx.20) at xx:xx:xx:xx:7c:fe on en0 ifscope [ethernet]
? (172.16.xx.34) at xx:xx:xx:xx:d0:5a on en0 ifscope [ethernet]
? (172.16.xx.68) at xx:xx:xx:xx:19:47 on en0 ifscope permanent [ethernet]
? (172.16.xx.85) at xx:xx:xx:xx:ba:b2 on en0 ifscope [ethernet]
...
$ netstat -p tcp -n
Active Internet connections
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0      0  172.16.xx.68.63098     172.16.xx.12.50171     ESTABLISHED
tcp4       0      0  172.16.xx.68.63098     172.16.xx.7.49493      ESTABLISHED
tcp4       0      0  172.16.xx.68.63942     172.16.xx.5.445        ESTABLISHED
tcp4       0      0  172.16.xx.68.63098     172.16.xx.34.49323     ESTABLISHED
tcp4       0      0  172.16.xx.68.61127     17.57.145.52.5223      ESTABLISHED
...
```
```text:DNSあり
$ arp -a
my-home-gate (172.16.xx.1) at xx:xx:xx:xx:38:3a on en0 ifscope [ethernet]
aibook.local (172.16.xx.5) at xx:xx:xx:xx:71:3 on en0 ifscope [ethernet]
ipadmini (172.16.xx.7) at xx:xx:xx:xx:d2:4b on en0 ifscope [ethernet]
iphone8.local (172.16.xx.12) at xx:xx:xx:xx:47:23 on en0 ifscope [ethernet]
? (172.16.xx.20) at xx:xx:xx:xx:7c:fe on en0 ifscope [ethernet]
macbook5g.local (172.16.xx.34) at xx:xx:xx:xx:d0:5a on en0 ifscope [ethernet]
macbook9.local (172.16.xx.68) at xx:xx:xx:xx:19:47 on en0 ifscope permanent [ethernet]
canonprinter.local (172.16.xx.85) at xx:xx:xx:xx:ba:b2 on en0 ifscope [ethernet]
...
$ netstat -p tcp
Active Internet connections
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0      0  macbook9.local.63098   iphone8.50171          ESTABLISHED
tcp4       0      0  macbook9.local.63098   ipadmini.49493         ESTABLISHED
tcp4       0      0  macbook9.local.63942   aibook.local.microsoft ESTABLISHED
tcp4       0      0  macbook9.local.63098   macbook5g.local.49323  ESTABLISHED
tcp4       0      0  macbook9.local.61127   17.57.145.52.5223      ESTABLISHED
...
```

アドレスを伏せ字にしたので分かりずらいのですが、これまでは、ネットワーク関連のコマンドに`-n`オプション（IPアドレスからホスト名への変換をしない）をつけて起動していましたので、出力の全体が一様に見えてしまい、知らないマシンがいるかどうかはいちいちチェックする必要がありました。DNS を動かすことで、`-n` をつけなくてもサクサク返ってくるし、知らないマシンのみがIPアドレスで表示され、わかりやすくなりました。さらに、知らないマシンがあると数秒表示が止まる挙動が悪くなく、個人的には結構幸せな気分で、無駄に `arp -a` を叩いたりしています。

あとは、Mac がブートしたら自動的に coredns がデーモンとして立ち上がるようにすればおしまいです。こちらについては、launchd や launchctl の使い方を検索すれば沢山の記事が見つかりますので、そちらをご参照ください。

今後、ネットワークの接続を自宅ネットワークから公衆Wifiに切り替えた時に、Corefile もそれに合わせたものに自動的に切り替えることもトライしたいなあと思っています。ただ、ネットワークのロケーションが変更されたことをスマートに検知する方法が良くわからないため、ペンディングになっています。

## おわりに

今日は CoreDNS という Kubernetes の中でも、あまり目立たない存在にスポットライトをあて、Kubernetes から取り出して、ネイティブ（ベアメタル？）な環境で CoreDNS を使ってみました。Kubernetes を利用する立場からは、Kubernetes は大きなブラックボックスのイメージになってしまいますが、実は、単体でも役立っているオープンソースソフトウェアが色々と含まれていて、中身を探求していくのも面白いし、全体の動作イメージがわかって良いです。

さて、今年の [NTTテクノクロス Advent Calendar](https://qiita.com/advent-calendar/2019/ntt-tx) では、[クラウド](https://www.ntt-tx.co.jp/column/クラウド.html?utm_source=qiita&utm_medium=ac2019&utm_campaign=01)、[AI/機械学習](https://www.ntt-tx.co.jp/column/AI.html?utm_source=qiita&utm_medium=ac2019&utm_campaign=01)、[Web/モバイルアプリ開発](https://www.ntt-tx.co.jp/column/モバイル.html?utm_source=qiita&utm_medium=ac2019&utm_campaign=01)、[ブロックチェーン](https://www.ntt-tx.co.jp/column/ブロックチェーン.html?utm_source=qiita&utm_medium=ac2019&utm_campaign=01)、[PostgreSQL](https://www.ntt-tx.co.jp/column/PosgreSQL.html?utm_source=qiita&utm_medium=ac2019&utm_campaign=01)、[AWS](https://www.ntt-tx.co.jp/column/AWS.html?utm_source=qiita&utm_medium=ac2019&utm_campaign=01)、[サイバーセキュリティ](https://www.ntt-tx.co.jp/column/サイバーセキュリティ.html?utm_source=qiita&utm_medium=ac2019&utm_campaign=01)などの専門家が続々登場し、バラエティに富んだ記事がアップされる予定です（おじさんが覚えたての技術を書くのはこの記事だけだと思います）。

[NTTテクノクロス Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ntt-tx) の購読ボタンと各記事のいいねボタンを、ポチッとしていただけると、たいへん嬉しいです。

それでは２日目の @ea-yasuda さんにバトンタッチしましょう。[どんな記事かな](/ea-yasuda/items/d8095afcaf966f97c78d)。ワクワク。


[^1]: OpenShift のレポジトリに [mDNS のプラグイン](https://github.com/openshift/coredns-mdns)を見つけたのですが、動かしてみたところ、定期的に mDNS でネットワークに接続されたホストの情報（\_workstation.\_tcp.local などへの応答）を集めてキャッシュし、外からの DNS クエリに対しては、そのキャッシュの中を検索して応答を返すといった挙動だったので、今回の要件には合いませんでした。単に設定が悪かっただけかもしれませんが。
[^2]: 実は macOS には、ドメインごとに DNS サーバを指定する機能が備わっていますので、CoreDNS を使わなくてもフォワード先をドメインごとに切り替えることができます。これについては、また改めて書きたいと思います。