ifwatchd.cに渡されるRTM_NEWADDRの設定処理をNetBSDカーネル側から見てみる


NetBSD Advent Calendar 2019 5日目の記事です。今日は4日目の記事で紹介したifwatchdの挙動をカーネル側から見てみようと思います。

カーネル側でRTM_NEWADDRはどのタイミングで設定されるのか?

ifwatchd -u up.sh ... と指定した場合、カーネルから渡されてきた RTM_NEWADDRstruct rt_msghdr->rtm_type に設定され、以下の場所で条件分岐処理されます。今回は RTM_NEWADDR が設定されるまでの流れをカーネル側から追いかけてみます。

/usr/src/usr.sbin/ifwatchd/ifwatchd.c
251 static void
252 dispatch(const void *msg, size_t len)
253 {
254         const struct rt_msghdr *hd = msg;
...
259         switch (hd->rtm_type) {
260         case RTM_NEWADDR:
261         case RTM_DELADDR:
262                 check_addrs(msg);
263                 break;

RTM_NEWADDRが設定される箇所

RTM_NEWADDRroute.h でマクロ定数として定義されています。ソースコードコメントを見ると、ネットワークインターフェスにアドレスが追加された際にこの値が設定されるようです。

/usr/src/sys/net/route.h
249 #define RTM_NEWADDR     0x16    /* address being added to iface */

RTM_NEWADDR を設定している箇所を探してみます。複数箇所で値が参照されているものの、 rt_newaddrmsg(RTM_NEWADDR, ...) という関数呼び出しを行っている箇所が着目すべき場所です。(printfデバッグで追いかけただけなので)調査手順は割愛しますが、単にネットワークインタフェースをUP/DOWNした場合は if_arp.c 内の rt_newaddrmsg(RTM_NEWADDR, ...) が処理されていました。

$ find . -type f | grep \\.c$ | xargs grep -w RTM_NEWADDR | grep rt_newaddrmsg
./net/route.c:          rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL);
./netinet/if_arp.c:             rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL);
./netinet/if_arp.c:             rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL);
./netinet/if_arp.c:             rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL);
./netinet/in.c:         rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL);
./netinet/in.c:                         rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL);
./netinet/in.c:                 rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL);
./netinet6/in6.c:               rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL);
./netinet6/in6.c:               rt_newaddrmsg(RTM_NEWADDR, &ia->ia_ifa, 0, NULL);
./netinet6/in6.c:                               rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL);
./netinet6/in6.c:                       rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL);
./netinet6/nd6.c:                               rt_newaddrmsg(RTM_NEWADDR,
./netinet6/nd6.c:                               rt_newaddrmsg(RTM_NEWADDR,
./netinet6/nd6_nbr.c:           rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL);
./netinet6/nd6_nbr.c:                   rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL);
./netinet6/nd6_nbr.c:   rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL);
./netinet6/nd6_rtr.c:

具体的な処理は以下の場所で行われてます。パッと見ではこの箇所(1750行目)にいたるまでの流れが把握しづらい感じです。そのため、この関数( arp_dad_timer() )の呼び出しもとに遡ってみます。
また、調査のヒントになりそうなキーワードを明らかにしておきましょう。"DAD"というキーワードがソースコードコメントや関数名に含まれています。これはDuplicate Address Detectionの略名であり、ARP(Address Resolution Protocol)における、重複アドレス検知処理を意味しています。 if_arp.c というファイル名から推測すると、ネットワークインタフェースをUPすると、ARPのDAD処理により重複アドレス検知が行われ、(アドレスの重複がなければ) RTM_NEWADDR が設定されるという挙動が浮かび上がってきます。さらに、 arp_dad_timer() という関数名から、DAD処理は何らかのタイマー処理を伴って行われているようにも見えます。重複アドレス検知という挙動を考えると、他のホストとやり取りを行うはずなので、応答待ちのためにタイマーを使用するであろうことは容易に想像できますね。

/usr/src/sys/netinet/if_arp.c
1686 static void
1687 arp_dad_timer(struct dadq *dp)
1688 { 
...
1744         } else if (dp->dad_arp_acount == 0) {
1745                 /*
1746                  * We are done with DAD.
1747                  * No duplicate address found.
1748                  */
1749                 ia->ia4_flags &= ~IN_IFF_TENTATIVE;
1750                 rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL);    // ★ここでRTM_NEWADDRを設定している。
1751                 ARPLOG(LOG_DEBUG,
1752                     "%s: DAD complete for %s - no duplicates found\n",
1753                     if_name(ifa->ifa_ifp), ARPLOGADDR(&ia->ia_addr.sin_addr));
1754                 dp->dad_arp_announce = ANNOUNCE_NUM;
1755                 goto announce;
1756         } else if (dp->dad_arp_acount < dp->dad_arp_announce) {
1757 announce:
1758                 /*
1759                  * Announce the address.
1760                  */

arp_dad_timer()arp_dad_starttimer() から呼ばれています。 callout_reset(9) は引数で渡された ticks を元に、 ticks/hz 秒後に指定された関数を実行します。このケースでは arp_dad_timer() が実行されることになります。

/usr/src/sys/netinet/if_arp.c
1533 static void 
1534 arp_dad_starttimer(struct dadq *dp, int ticks)
1535 {
1536 
1537         callout_reset(&dp->dad_timer_ch, ticks,
1538             (void (*)(void *))arp_dad_timer, dp);
1539 }

改めて arp_dad_timer() を見てみると、関数内の複数箇所で arp_dad_starttimer() を呼び出しています。DADの処理は arp_dad_timer() 自身が行いつつ、非同期的に状態変化を待ちながら処理するような箇所では arp_dad_starttimer() を利用して自分自身を呼び出してもらうという挙動になっています。

/usr/src/sys/netinet/if_arp.c
1686 static void
1687 arp_dad_timer(struct dadq *dp)
1688 {
...
1728         /* Need more checks? */
1729         if (dp->dad_arp_ocount < dp->dad_count) {
...
1742                 arp_dad_starttimer(dp, adelay);  // ★
1743                 goto done;
1744         } else if (dp->dad_arp_acount == 0) {
...
1756         } else if (dp->dad_arp_acount < dp->dad_arp_announce) {
1757 announce:
1758                 /*
1759                  * Announce the address.
1760                  */
1761                 arpannounce1(ifa);
1762                 dp->dad_arp_acount++;
1763                 if (dp->dad_arp_acount < dp->dad_arp_announce) {
1764                         arp_dad_starttimer(dp, ANNOUNCE_INTERVAL * hz);  // ★
1765                         goto done;
1766                 }

肝心の RTM_NEWADDR は1750行目で rt_newaddrmsg(RTM_NEWADDR, ...) で設定されます。コメントを見ると、このif文のブロックが実行される段階では、重複アドレスチェックが完了している状態になっているようです。

/usr/src/sys/netinet/if_arp.c
1744         } else if (dp->dad_arp_acount == 0) {
1745                 /*
1746                  * We are done with DAD.
1747                  * No duplicate address found.
1748                  */
1749                 ia->ia4_flags &= ~IN_IFF_TENTATIVE;
1750                 rt_newaddrmsg(RTM_NEWADDR, ifa, 0, NULL); // ★
...
1755                 goto announce;
1756         } else if (dp->dad_arp_acount < dp->dad_arp_announce) {
1757 announce:
1758                 /*
1759                  * Announce the address.
1760                  */
1761                 arpannounce1(ifa);
1762                 dp->dad_arp_acount++;
1763                 if (dp->dad_arp_acount < dp->dad_arp_announce) {
1764                         arp_dad_starttimer(dp, ANNOUNCE_INTERVAL * hz);
1765                         goto done;
1766                 }

DAD処理における関数呼び出しの流れを抜粋すると以下のようになります。関数の非同期呼び出しは arp_dad_starttimer() 経由で設定し、 arp_dad_timer() 内で(状態変化も踏まえた)DADまわりの処理を行うという構成になっています。

まとめ

ifwatchd.cRTM_NEWADDR を受け取るケースについて、NetBSDカーネル側の挙動を追いかけてみました。単にネットワークインタフェースをUPした場合についての振る舞いでしたが、今回のケースではARPまわりの処理(重複アドレスチェック)が完了した段階で RTM_NEWADDR が設定されることが分かりました。 RTM_NEWADDR をカーネル内で設定している箇所は if_arp.c だけでなく、 in.c in5.c にも存在しています。おそらくはIPアドレスが設定された場合の挙動が実装されていると予想されますが、これはまた別の機会に調査しようと思います。