はじめに
この記事は NTTテクノクロス Advent Calendar 2019 の1日目の記事です。
こんにちは。NTTテクノクロスの盛合(@moriai)です。会社では、IoTソリューションやクラウド基盤サービスを中心に、私たちの取り組みを社外へ発信したり、技術ノウハウを社内に展開したり、社内開発環境の整備をお手伝いしたりといった業務で、マネージャとして働いています。また、日曜プログラマとして Swift や Rust をいじって GitHub にコードを載せたり、家庭内ネットワーク管理者としてトラブルシューティングなどをしています。
今日はクラウド系技術を家庭内ネットワークで活用したら、ちょっとだけ管理者が幸せになったお話を書いていきます。
家庭内ネットワーク管理者としての困りごと
自宅ではマンションのインターネット回線にWifiステーションを設置しています。長男のパソコンにWifiの設定を一度してやったら、家族のみんなが自分の端末をWifiに繋げて使っていたりするので、「このMACアドレス誰やねん?」ということが年に何回もあったりします。たいていは長男に聞けば状況が分かって、スマホの機種変、学校や職場のパソコンの持ち込み、ゲーム機をお年玉で買った、ということなのですが、家庭内ネットワーク管理者としては、精神衛生上よろしくないかなというところです。
インターネット回線(足回りがVDSL)としては常に70Mbps前後の速度が出ていて不満はないのですが、ポチッとした時にブラウザの反応が遅れることがあったり、そんなサイトはないと怒られることがあったりと、どうも DNS の反応が悪いドメインや時々引けないドメインがあるようです。
また、日曜プログラマとして Kubernetes や OpenShift のローカルクラスタで遊んでいると、自動的に割り当てられる「ほげほげ.nip.io」とかのホスト名検索が特に遅いし、内部で閉じても問題のなさそうな DNS クエリを外に飛ばさなくてもいいのにと思っています。
ということで、一念発起して、家庭内ネットワークにDNSを立ててみることにしました。
どう解決すれば良さそうか?
要件 を次のように整理してみました。
- プライベート 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 に対応している。
- MAC アドレスからホスト名が分かれば誰の端末かが想像がつくが、MAC アドレスから直接ホスト名に紐付ける仕組みは IP over Ethernet の世界にはない。自宅のネットワークでの IP アドレスは DHCP で適当に割り当てられるため、IPアドレスでは誰の端末かは分からない。OS には IP アドレスからホスト名に自動的に変換する仕組みが備わっており、それが動作するようにすれば、
- ワイルドカード DNS(*.nip.io など) → ローカルで IPアドレスへ解決
- 他者のドメインを勝手にアドレス解決したりするのはまずいと思うが、ワイルドカードをローカルに解決することをローカルに提供するのであれば、許容範囲だと信じたい。
- 解決によく失敗するドメイン → パブリック DNS(1.1.1.1 や 8.8.8.8 など)へフォワードして解決
- DNS を立てる Mac(Macbook Pro)は外に持ち出して公衆Wifiに繋げることもあるため、DoT(DNS over TSL)や DoH(DNS over HTTPS)の設定も確認しておきたい。
- それ以外 → プロバイダ DNS で解決
- 全てをパブリック DNS にフォワードしてしまった場合、DNS を活用してネットワーク的に近いサーバにトラフィックを振り向けるような仕組みが使えなくなる問題があるので、現時点ではなるべくプロバイダ DNS を活用したい。
ということで、ドメイン名によって、適切な DNS サーバへフォワードできる仕組み、ホスト名から IP アドレスへの変換をルールによって記述できる仕組みがあれば良さそうです。調べてみたら、CoreDNS でだいたいうまくいきそうだったので、これで DNS を立ててみることにします。
CoreDNS とは?
CoreDNS はいわゆる「クラウドネイティブ」な DNSサーバプログラムで、CNCF(Cloud Native Computing Foundation)の1プロジェクトとして開発されています。高速で柔軟かつシンプル、プラグインによる拡張性などが売りのソフトウェアで、Kubernetes のクラスタ内で名前解決とサービスディスカバリの役割を担っています。
minikube 1.5.2(Kubernetes 1.16.2)を使っている時は、こんな感じで 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 が2つあることがわかります。次に、ConfigMap をみてみましょう。
$ kubectl -n kube-system get configmap coredns -o 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 の設定ファイルの中身になります。基本的な書き方は次の通りです。
ドメイン名:ポート番号 {
プラグイン名 {
プラグインのオプション
...
}
...
}
coredns-config.yaml
の例では、ルート( . )=全てのドメインのクエリを扱う53番ポートを開き、errors, health, ready, kubernetes, ... のプラグインでクエリを処理することが宣言されています。プラグインの設定が評価される順番は Corefile に記載された順序ではなく、CoreDNS のビルド時のコンフィグファイルに記載された順序となりますので、Corefile のブロック内でのプラグインの順序はあまり気にする必要はありません。主なプラグインの機能の概要は次の通りです。ここでは評価される順に記載しています。CoreDNS 1.6.5 では標準で45個のプラグインが組み込まれています。なお、Corefile や各プラグインの詳細については CoreDNS のサイトをご覧ください。
プラグイン | 機能の概要 |
---|---|
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
を挿入して保存します。
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 をデプロイして、C/C++ の workspace を1つ作った時の 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 から入手できますし、Go言語の環境が整っていれば、ソースからのビルドも make
一発で終わります。
次に、CoreDNS の設定です。
実は、CoreDNS には 要件1. を満たすような mDNS のプラグインがなく1、プラグインをいきなり自作するのも大変そうなので、DNS のクエリを mDNS にフォワードする別のソフトウェアを用意し、そこへ forward することにします。ただ、そのようなソフトウェアを見つけられず、たまたま触っていた Vitaly Shukela さんの Rust 言語で書かれた dnscache に2行追加するだけでその機能が実現できてしまったので、この dnscache 改造版を使うことにします。このソースコードは https://github.com/moriai/dnscache の mdns ブランチで公開しています。Rust言語の環境をインストールした後、
$ 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 を使います。Go言語、かつ、コンパクトというのが採用理由です。macOS 用のバイナリは提供されていないようなので、ソースからビルドします。
$ go install blitiri.com.ar/go/dnss
部品が一通り揃ったので、それらを /usr/local/bin
にコピーし、CoreDNS を中心に繋げてみます。
なお、要件3. と 要件4. の振り分け、つまり、パブリックDNSとプロバイダDNSの振り分けは難しいのですが、プロバイダDNSにフォワードすることを基本とし、不調な場合のみ、パブリックDNSに手動で切り替えることにします。また、パブリックDNSへの接続手段もとりあえずは手動で我慢することにします。
全体のシステム構成は次のようになります。青色の矢印は DNS クエリの流れを示しています。
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して、いくつか試してみましょう。
$ 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
...
$ 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 では、クラウド、AI/機械学習、Web/モバイルアプリ開発、ブロックチェーン、PostgreSQL、AWS、サイバーセキュリティなどの専門家が続々登場し、バラエティに富んだ記事がアップされる予定です(おじさんが覚えたての技術を書くのはこの記事だけだと思います)。
NTTテクノクロス Advent Calendar 2019 の購読ボタンと各記事のいいねボタンを、ポチッとしていただけると、たいへん嬉しいです。
それでは2日目の @ea-yasuda さんにバトンタッチしましょう。どんな記事かな。ワクワク。
-
OpenShift のレポジトリに mDNS のプラグインを見つけたのですが、動かしてみたところ、定期的に mDNS でネットワークに接続されたホストの情報(_workstation._tcp.local などへの応答)を集めてキャッシュし、外からの DNS クエリに対しては、そのキャッシュの中を検索して応答を返すといった挙動だったので、今回の要件には合いませんでした。単に設定が悪かっただけかもしれませんが。 ↩
-
実は macOS には、ドメインごとに DNS サーバを指定する機能が備わっていますので、CoreDNS を使わなくてもフォワード先をドメインごとに切り替えることができます。これについては、また改めて書きたいと思います。 ↩