OpenShiftの内部DNSについて語る...のではなく、dnsmasqとgdbで遊ぶ話

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サービスとして稼働しています。 この dnsmasqskydns が協力して、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()
}

https://github.com/openshift/origin/blob/06cfa24d74f473832ab68798fdbfb743a4af2b93/pkg/dns/dnsmasq.go#L134-L135

設定の確認

動的に設定を入れているのであれば、今の設定状況を確認したくなってきます。 ぱっと思いつく確認方法は、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

  1. RHEL7.6だとこのコマンドは setenforce 0 しないと実行できませんでした。後で報告しておきます。石川さんごめんなさい

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません