NetBSD Advent Calendar 2019 5日目の記事です。今日は4日目の記事で紹介したifwatchdの挙動をカーネル側から見てみようと思います。
カーネル側でRTM_NEWADDRはどのタイミングで設定されるのか?
ifwatchd -u up.sh ...
と指定した場合、カーネルから渡されてきた RTM_NEWADDR
が struct rt_msghdr->rtm_type
に設定され、以下の場所で条件分岐処理されます。今回は RTM_NEWADDR
が設定されるまでの流れをカーネル側から追いかけてみます。
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_NEWADDR
は 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処理は何らかのタイマー処理を伴って行われているようにも見えます。重複アドレス検知という挙動を考えると、他のホストとやり取りを行うはずなので、応答待ちのためにタイマーを使用するであろうことは容易に想像できますね。
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()
が実行されることになります。
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()
を利用して自分自身を呼び出してもらうという挙動になっています。
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文のブロックが実行される段階では、重複アドレスチェックが完了している状態になっているようです。
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.c
で RTM_NEWADDR
を受け取るケースについて、NetBSDカーネル側の挙動を追いかけてみました。単にネットワークインタフェースをUPした場合についての振る舞いでしたが、今回のケースではARPまわりの処理(重複アドレスチェック)が完了した段階で RTM_NEWADDR
が設定されることが分かりました。 RTM_NEWADDR
をカーネル内で設定している箇所は if_arp.c
だけでなく、 in.c
in5.c
にも存在しています。おそらくはIPアドレスが設定された場合の挙動が実装されていると予想されますが、これはまた別の機会に調査しようと思います。