Red Hatでコンサルタントをしている織です。 赤帽エンジニアAdvent Calendar 2018の5日目の本記事では、OpenShiftの内部DNS(Kubernetesにおけるkube-dns)の動きを解説する...と見せかけて、dnsmasqにdbus経由で注入された設定情報をgdbを使って覗き見る遊びをします。
目次
1. 前置き
OpenShiftでの内部DNS
OpenShiftはKubernetesをベースとしたソフトウェアなので、kube-dnsに相当する機能(以下「内部DNS」と呼びます)を持っています。
一般的なKubernetesの場合、kube-dnsはPodとして稼働することが多いと思いますが、OpenShiftの場合は、各workerノードで動く sdn
というPodが内部DNSの役割を担っています。
各workerノードで稼働するサービスをもう少し詳しく書きますと、まずkubeletがsystemdサービスとして稼働し、さらに
- CNIデーモンの役割を担う
sdn
- Open vSwitch関連のプロセスが稼働する
ovs
がDaemonSetとして openshift-sdn
というnamespaceで稼働します。
skydnsというDNS実装がCNIデーモンにstatic linkされて、sdn
というDaemonSetとして各ノードで稼働しているわけです。
# kubectl get pod -n openshift-sdn -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE ovs-7m44w 1/1 Running 1 22d 172.16.99.41 ocp311-node1.example.com <none> ovs-b4dbv 1/1 Running 4 22d 172.16.99.42 ocp311-node2.example.com <none> ovs-hk6c9 1/1 Running 1 22d 172.16.99.21 ocp311-master1.example.com <none> ovs-hncsn 1/1 Running 1 22d 172.16.99.31 ocp311-infra1.example.com <none> sdn-4pk9z 1/1 Running 4 22d 172.16.99.42 ocp311-node2.example.com <none> sdn-cr28m 1/1 Running 1 22d 172.16.99.31 ocp311-infra1.example.com <none> sdn-mtpm6 1/1 Running 1 22d 172.16.99.21 ocp311-master1.example.com <none> sdn-z85rv 1/1 Running 1 22d 172.16.99.41 ocp311-node1.example.com <none>
また、skydnsとは別に、各ノードでは dnsmasq
がsystemdサービスとして稼働しています。
この dnsmasq
と skydns
が協力して、OpenShiftの内部DNSの機能を実現しています。
dnsmasqとskydns
dnsmasqとskydnsがどのように協力しているかについては、こちらのブログ記事に詳細な説明が載っています。 簡単にまとめると、OpenShiftでは
- 各ノードの/etc/resolv.confのnameserverは <自身のIPアドレス> を指す
- 各ノードのdnsmasqは <自身のIPアドレス>:53 で待ち受ける
- skydnsは 127.0.0.1:53 で待ち受ける
- dnsmasqには、「cluster.localドメインの名前解決は127.0.0.1にフォワードする」「それ以外は外部のDNSサーバにフォワードする」という設定を入れる
という構成を取ります。 結果として、誰かが名前解決しようとすると、そのクエリはまずdnsmasqに届いて、さらにcluster.local宛てであればskydns、それ以外であれば外部のDNSサーバにフォワードされる、という動きになります。
実際の環境で、53番を待ち受けているプロセスを確認してみます。
# lsof -P -n -i 4:53 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME dnsmasq 3778 nobody 4u IPv4 36281 0t0 UDP 172.16.99.42:53 dnsmasq 3778 nobody 5u IPv4 36282 0t0 TCP 172.16.99.42:53 (LISTEN) dnsmasq 3778 nobody 20u IPv4 38208 0t0 UDP 172.17.0.1:53 dnsmasq 3778 nobody 21u IPv4 38209 0t0 TCP 172.17.0.1:53 (LISTEN) dnsmasq 3778 nobody 22u IPv4 51778 0t0 UDP 10.131.0.1:53 dnsmasq 3778 nobody 23u IPv4 51779 0t0 TCP 10.131.0.1:53 (LISTEN) openshift 7891 root 12u IPv4 53068 0t0 UDP 127.0.0.1:53 openshift 7891 root 13u IPv4 53069 0t0 TCP 127.0.0.1:53 (LISTEN)
dnsmasqが自身のIPアドレス(この例では172.16.99.42)、openshift
というプロセス(sdn Podで稼働するプロセス)が127.0.0.1の53番で待ち受けていることがわかります (sdn Pod内のコンテナはPrivilegedなセキュリティコンテキストで稼働しています)。
dnsmasqの設定
先に紹介したブログ記事は、OpenShift v3.6を対象にしています。当時はdnsmasqの設定として、/etc/dnsmasq.d/node-dnsmasq.conf
というファイルに
server=/in-addr.arpa/127.0.0.1 server=/cluster.local/127.0.0.1
という設定が書かれたのでわかりやすかったのですが、今のバージョン(v3.11)では該当ファイルは存在しません。
どうやっているかというと、実はdbusの SetDomainServers
メソッドを使って動的にdnsmasqにこの設定を注入しています。
(pkg/dns/dnsmasq.go)
// refresh invokes dnsmasq with the requested configuration func (m *dnsmasqMonitor) refresh(conn utildbus.Connection, ready bool) error { m.lock.Lock() defer m.lock.Unlock() var addresses []string if ready { addresses = []string{ fmt.Sprintf("/in-addr.arpa/%s", m.dnsIP), fmt.Sprintf("/%s/%s", m.dnsDomain, m.dnsIP), } glog.V(4).Infof("Instructing dnsmasq to set the following servers: %v", addresses) } else { glog.V(2).Infof("DNS data is not ready, removing configuration from dnsmasq") } return conn.Object(dbusDnsmasqInterface, dbusDnsmasqPath). Call("uk.org.thekelleys.SetDomainServers", 0, addresses). Store() }
設定の確認
動的に設定を入れているのであれば、今の設定状況を確認したくなってきます。 ぱっと思いつく確認方法は、dbus経由で今の設定情報を取得できるのではないか、という妄想です。が、dnsmasqのdbusインターフェースは、Set系のメソッドばかりでGet系のメソッドがほとんどないため、dbus経由では設定確認ができなさそうです。
そんなときこそgdbの出番です。というわけで長い前置きですみませんでした。これからが本題です。
2. dnsmasqの設定情報をgdbで見る
準備
RHELでは(たぶんCentOSでも)、シンボル情報等のデバッグ情報を "debuginfo" というrpmパッケージで配布しています。例えばdnsmasq-2.76-7.el7.x86_64.rpmというパッケージのデバッグ情報は、rhel-7-server-debug-rpmsというリポジトリにあるdnsmasq-debuginfo-2.76-7.el7.x86_64.rpmというパッケージに入っています。
デバッグを進めるには、対象バイナリが使用するライブラリ等のdebuginfoパッケージも全て入れておくのが理想的です。手でひとつひとつ入れてもいいのですが、debuginfo-installという便利なコマンドが用意されていて、これを使うと関連する必要そうなdebuginfoパッケージをまとめてインストールしてくれます。 debuginfo-installコマンドは、yum-utilsというパッケージに含まれています。
今回はdnsmasqをgdbで追いかけたいので、
# debuginfo-install dnsmasq
を実行します。すると、dnsmasq-debuginfoに加えて、
- dbus-debuginfo
- glibc-debuginfo
- glibc-debuginfo-common
- libidn-debuginfo
も一緒にyum installしてくれます。
さっそくgdbを実行して、dnsmasqにアタッチしてみます。
# gdb -p $(systemctl show dnsmasq -p MainPID | sed -e 's/^MainPID=//') <snip> Reading symbols from /lib64/libz.so.1...Reading symbols from /lib64/libz.so.1...(no debugging symbols found)...done. <snip> Missing separate debuginfos, use: adebuginfo-install bzip2-libs-1.0.6-13.el7.x86_64 elfutils-libelf-0.172-2.el7.x86_64 elfutils-libs-0.172-2.el7.x86_64 libattr-2.4.46-13.el7.x86_64 libcap-2.22-9.el7.x86_64 libgcc-4.8.5-36.el7.x86_64 libgcrypt-1.5.3-14.el7.x86_64 libgpg-error-1.12-3.el7.x86_64 libselinux-2.5-14.1.el7.x86_64 lz4-1.7.5-2.el7.x86_64 pcre-8.32-17.el7.x86_64 systemd-libs-219-62.el7.x86_64 xz-libs-5.2.2-1.el7.x86_64 zlib-1.2.7-18.el7.x86_64
まだ足りないdebuginfoがあるようですが、親切にも実行するべきコマンドを教えてくれているので、そのまま実行します。
# debuginfo-install bzip2-libs-1.0.6-13.el7.x86_64 elfutils-libelf-0.172-2.el7.x86_64 elfutils-libs-0.172-2.el7.x86_64 libattr-2.4.46-13.el7.x86_64 libcap-2.22-9.el7.x86_64 libgcc-4.8.5-36.el7.x86_64 libgcrypt-1.5.3-14.el7.x86_64 libgpg-error-1.12-3.el7.x86_64 libselinux-2.5-14.1.el7.x86_64 lz4-1.7.5-2.el7.x86_64 pcre-8.32-17.el7.x86_64 systemd-libs-219-62.el7.x86_64 xz-libs-5.2.2-1.el7.x86_64 zlib-1.2.7-18.el7.x86_64
再度gdbでアタッチしてみると、先ほどのようなエラーは出なくなり、無事必要なシンボル情報をロードした上でdnsmasqにアタッチすることができました。
実際にgdbを使ってみる
dbus経由で設定を追加しているのはドメインサーバの情報です。 dnsmasqのソースコード(dnsmasq.c)を見ると、daemonというグローバル変数にこの手の情報が入っていそうです。
(src/dnsmasq.c)
struct daemon *daemon;
さっそくこれを見てみます。
(gdb) p daemon $1 = {int (int, int)} 0x7f5319f9e1f0 <daemon> (gdb) p *daemon $2 = {int (int, int)} 0x7f5319f9e1f0 <daemon>
ふむ。構造体の中身が取れていません。この変数の情報を見てみます。
(gdb) info types daemon All types matching regular expression "daemon": File dnsmasq.h: struct dnsmasq_daemon; struct dnsmasq_daemon; struct dnsmasq_daemon;
おっと。dnsmasq.hを見てみると、
(src/dnsmasq.h)
/* daemon is function in the C library.... */ #define daemon dnsmasq_daemon
と書かれていて、なんと構造体名も変数名もマクロで置き換えられている...ひどい。というわけで、気を取り直してdnsmasq_daemonの方を見てみます。
(gdb) p dnsmasq_daemon $3 = (struct dnsmasq_daemon *) 0x564f09721470 (gdb) p *dnsmasq_daemon $4 = {options = 596224, options2 = 128, default_resolv = {next = 0x0, is_default = 1, logged = 0, mtime = 0, name = 0x564f086daf63 "/etc/resolv.conf", wd = 0, file = 0x0}, resolv_files = 0x0, last_resolv = 0, servers_file = 0x0, mxnames = 0x0, naptr = 0x0, txt = 0x564f09721ea0, rr = 0x0, ptr = 0x0, host_records = 0x0, host_records_tail = 0x0, cnames = 0x0, auth_zones = 0x0, int_names = 0x0, mxtarget = 0x0, add_subnet4 = 0x0, add_subnet6 = 0x0, lease_file = 0x0, username = 0x564f086daf74 "nobody", groupname = 0x564f09729d50 "dip", scriptuser = 0x0, luascript = 0x0, authserver = 0x0, hostmaster = 0x0, <snip> watches = 0x564f09726200, tftp_trans = 0x0, tftp_done_trans = 0x0, addrbuff = 0x564f0972a370 "8\311&\032S\177", addrbuff2 = 0x0}
無事構造体の中の情報も表示することができました。ここで念のため、表示を見やすくする呪文を唱えておきます。
(gdb) set print pretty
さて、見たいのは struct server *servers
というメンバで、struct serverはlinked listのようです。
(src/dnsmasq.h)
extern struct daemon { /* datastuctures representing the command-line and config file arguments. All set (including defaults) in option.c */ <snip> struct server *servers;
(src/dnsmasq.h)
struct server { union mysockaddr addr, source_addr; char interface[IF_NAMESIZE+1]; struct serverfd *sfd; char *domain; /* set if this server only handles a domain. */ int flags, tcpfd, edns_pktsz; unsigned int queries, failed_queries; #ifdef HAVE_LOOP u32 uid; #endif struct server *next; };
serversメンバのリストをたどってみます。
(gdb) p (*dnsmasq_daemon)->servers $44 = (struct server *) 0x564f0972a2e0 (gdb) p (*dnsmasq_daemon)->servers->next $45 = (struct server *) 0x564f0972b350 (gdb) p (*dnsmasq_daemon)->servers->next->next $46 = (struct server *) 0x564f0972b6a0 (gdb) p (*dnsmasq_daemon)->servers->next->next->next $47 = (struct server *) 0x0
いくつかつながった後、最後はNULLで終わっていて、いかにもそれっぽいです。適当なリスト要素の中身を表示してみます。
(gdb) p *((*dnsmasq_daemon)->servers->next->next) $49 = { addr = { sa = { sa_family = 2, sa_data = "\000\065\177\000\000\001\000\000\000\000\000\000\000" }, in = { sin_family = 2, sin_port = 13568, sin_addr = { s_addr = 16777343 }, sin_zero = "\000\000\000\000\000\000\000" }, <snip> domain = 0x564f0972b330 "cluster.local",
なんだかいい感じです。さらにcluster.localのアドレスを確認してみます。
(gdb) p inet_ntoa(((*dnsmasq_daemon)->servers->next->next)->addr->in->sin_addr->s_addr) $54 = 0x7f531a901858 "127.0.0.1"
いいですね。期待どおり、cluster.local宛ては127.0.0.1に聞きに行く設定が入っていることがわかりました。
gdbスクリプト
gdbでは、一連のコマンドを、スクリプトにまとめて実行することができます。 linked listをたどって、該当するメンバ変数の値を表示するスクリプトを載せておきます。
define server_list set var $s = (*dnsmasq_daemon)->servers echo \n echo struct server *:\n print $s while $s echo .domain:\n print $s.domain echo .addr.in.sin_addr.s_addr:\n print $s.addr.in.sin_addr.s_addr call inet_ntoa($s.addr.in.sin_addr.s_addr) echo \n set var $s = $s->next echo struct server *:\n print $s end echo \n end server_list quit
これを使うと、それっぽく全てのupstream server情報を表示してくれます。
# gdb --batch -q -p $(systemctl show dnsmasq -p MainPID | sed -e 's/^MainPID=//') -x dnsmasq_dump_servers.gdb [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib64/libthread_db.so.1". 0x00007f5319f991f0 in __poll_nocancel () at ../sysdeps/unix/syscall-template.S:81 81 T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS) struct server *: $1 = (struct server *) 0x564f0972a2e0 .domain: $2 = 0x0 .addr.in.sin_addr.s_addr: $3 = 191041708 $4 = 0x7f531a901858 "172.16.99.11" struct server *: $5 = (struct server *) 0x564f0972b350 .domain: $6 = 0x564f0972b3e0 "in-addr.arpa" .addr.in.sin_addr.s_addr: $7 = 16777343 $8 = 0x7f531a901858 "127.0.0.1" struct server *: $9 = (struct server *) 0x564f0972b6a0 .domain: $10 = 0x564f0972b330 "cluster.local" .addr.in.sin_addr.s_addr: $11 = 16777343 $12 = 0x7f531a901858 "127.0.0.1" struct server *: $13 = (struct server *) 0x0 A debugging session is active. Inferior 1 [process 3778] will be detached. Quit anyway? (y or n) [answered Y; input not from terminal]
最後に、dbusでdnsmasqのupstream serverの情報をわざと変えて、上記gdbスクリプトで反映されていることを確認します。 コマンドラインからdbusのメソッドを呼ぶには、dbus-sendコマンドを使って下記のようにします1。
dbus-send --system --dest=uk.org.thekelleys.dnsmasq /uk/org/thekelleys/dnsmasq uk.org.thekelleys.SetDomainServers array:string:/in-addr.arpa/8.8.8.8,/cluster.local/1.1.1.1
先ほどのgdbスクリプトを実行すると、無事cluster.local宛ての問い合わせは1.1.1.1にフォワードする設定に変わったことがわかります。
# gdb --batch -q -p $(systemctl show dnsmasq -p MainPID | sed -e 's/^MainPID=//') -x dnsmasq_dump_servers.gdb <snip> struct server *: $5 = (struct server *) 0x564f0972b350 .domain: $6 = 0x564f0972b3e0 "in-addr.arpa" .addr.in.sin_addr.s_addr: $7 = 134744072 $8 = 0x7f531a901858 "8.8.8.8" struct server *: $9 = (struct server *) 0x564f0972b6a0 .domain: $10 = 0x564f0972b330 "cluster.local" .addr.in.sin_addr.s_addr: $11 = 16843009 $12 = 0x7f531a901858 "1.1.1.1"
3. One more thing...
実はdnsmasqは、dbusメソッド等でserverの設定が変わると、全てのupstream serverの情報をsyslogに吐き出す動きになっています(src/network.cのcheck_servers()、あとこちらも)。 ですので、わざわざgdbを使わなくてもログを見ればよかった...というオチでした。
# journalctl -u dnsmasq --since '10 minutes ago' -- Logs begin at Wed 2018-11-07 14:30:47 JST, end at Wed 2018-12-05 01:32:07 JST. -- Dec 05 01:22:45 ocp311-node2.example.com dnsmasq[3778]: setting upstream servers from DBus Dec 05 01:22:45 ocp311-node2.example.com dnsmasq[3778]: using nameserver 172.16.99.11#53 Dec 05 01:22:45 ocp311-node2.example.com dnsmasq[3778]: using nameserver 127.0.0.1#53 for domain in-addr.arpa Dec 05 01:22:45 ocp311-node2.example.com dnsmasq[3778]: using nameserver 127.0.0.1#53 for domain cluster.local Dec 05 01:23:34 ocp311-node2.example.com dnsmasq[3778]: setting upstream servers from DBus Dec 05 01:23:34 ocp311-node2.example.com dnsmasq[3778]: using nameserver 172.16.99.11#53 Dec 05 01:23:34 ocp311-node2.example.com dnsmasq[3778]: using nameserver 127.0.0.1#53 for domain in-addr.arpa Dec 05 01:23:34 ocp311-node2.example.com dnsmasq[3778]: using nameserver 127.0.0.1#53 for domain cluster.local
-
RHEL7.6だとこのコマンドは
setenforce 0
しないと実行できませんでした。後で報告しておきます。石川さんごめんなさい↩