ifwatchdでネットワークインタフェースの状態変更を監視する

だいぶ遅くなってしまいましたが、[NetBSD Advent Calendar 2019](https://qiita.com/advent-calendar/2019/netbsd) 4日目の記事です。今日は[ifwatchd](https://netbsd.gw.com/cgi-bin/man-cgi?ifwatchd++NetBSD-current)について書こうと思います。

## ifwatchdとは

[ifwatchd](https://netbsd.gw.com/cgi-bin/man-cgi?ifwatchd++NetBSD-current)はネットワークインタフェースへのIPアドレス追加・削除を監視するツールです。例えば、PCルータで家庭内LANとインターネットを接続しているような場合に、インターネット側のネットワークが切断→再接続されることがあります。その際にIPアドレスが変わる事もあるのですが、出先からsshで接続するような場合に新しいIPアドレスを何らかの方法で把握できるようにしておきたいものです。ifwatchdはこのようなケースに有効なツールとなっています。

また、ifwatchdはNetBSD 1.6から追加されたツールとなっていますが、[QNX](http://www.qnx.com/developers/docs/qnxcar2/index.jsp?topic=%2Fcom.qnx.doc.neutrino.utilities%2Ftopic%2Fi%2Fifwatchd.html)にも同名のコマンドが存在しています。
(利用可能なオプションが似通っているので、もしかしたらNetBSDのifwatchdをベースにしているのかもしれませんね)

## ifwatchdを使ってみる

さっそくifwatchdを試してみます。今回は `-d` と `-u` オプションを指定してみます。[ifwatchd(8)](https://www.freebsd.org/cgi/man.cgi?query=ifwatchd&apropos=0&sektion=0&manpath=NetBSD+3.0&arch=default&format=html)によると、`-d` `-u` でネットワークインタフェースがdown/upした時に実行するスクリプトを指定するようです。

```sh
SYNOPSIS
     ifwatchd [-hiqv] [-A arrival-script] [-c carrier-script]
              [-D departure-script] [-d down-script] [-u up-script]
              [-n no-carrier-script] ifname(s)
...
     -d down-script
             Specify the command to invoke on “interface down” events (or:
             deletion of an address from an interface).
...
     -u up-script
             Specify the command to invoke on “interface up” events (or:
             addition of an address to an interface).
```

`up.sh` `down.sh` を以下のようなシェルスクリプトとして作成します。

```sh:up.sh
#!/bin/sh

echo '-=> up'
new_addr=`ifconfig wm2 | grep inet[^6] | sed -e "s/\/.*$//" -e "s/^.* //"`
echo "-=> new address $new_addr"
```

```sh:down.sh
#!/bin/sh

echo '-=> down'
```

`ifwatchd` を実行します。 `-i` は起動直後のネットワークインタフェースのチェックをスキップするオプションで、 `-v` はデバッグ用に冗長な出力を行うためのオプションです。この例では `wm2` というネットワークインタフェースを監視しています。
`-v` を付けない場合はそのままバックグラウンドで `ifwatchd` が起動します。

```sh
$ sudo ifwatchd -v -i -u up.sh -d down.sh wm2
```

ここで `wm2` をdown→upさせてみます。

```sh
$ sudo ifconfig wm2 down ; sleep 3 ; sudo ifconfig wm2
```

少し待つと、 `ifwatchd` を実行している端末に以下が出力されます。単にネットワークインタフェースをdown/upしただけなのでIPアドレスはそのままですが、[NetBSDでPCルータを作る](https://qiita.com/furandon_pig/items/a6f53de431eb19ea2154)ような場合は、 `pppoectl` によるIPアドレスの再設定を検知することができます。この例では単に(upした際の)IPアドレスを表示しているだけですが、再設定されたIPアドレスをメールやSlackで通知するといった連携が可能になります。

```sh
calling: up.sh wm2 /dev/null 9600 192.168.100.206 192.168.100.255
-=> up
-=> new address 192.168.100.206
```

## ifwatchdのソースコードを眺めてみる

併せて `ifwatchd` のソースコードも概観してみましょう。オプションからすると指定されたシェルスクリプトを実行するという、比較的シンプルな挙動のようにも思えます。
ソースコードの場所は以下になります。単一のCソースファイルから成る小さなプログラムのように見えます。

```sh
$ which ifwatchd 
/usr/sbin/ifwatchd
$ ls /usr/src/usr.sbin/ifwatchd/
CVS/        Makefile    ifwatchd.8  ifwatchd.c
```

まずはソケットの作成です。 `PF_ROUTE` なRawソケットを作成し、[setsockopt(2)](https://www.freebsd.org/cgi/man.cgi?query=setsockopt&apropos=0&sektion=0&manpath=NetBSD+8.1&arch=default&format=html)で `RO_MSGFILTER` オプションを設定しています。

```c:/usr/src/usr.sbin/ifwatchd/ifwatchd.c
103 int
104 main(int argc, char **argv)
105 {
...
193         s = socket(PF_ROUTE, SOCK_RAW, 0);
...
198         if (setsockopt(s, PF_ROUTE, RO_MSGFILTER,
199             &msgfilter, sizeof(msgfilter)) < 0)
200                 syslog(LOG_ERR, "RO_MSGFILTER: %m");
```

FreeBSDのmanページによると、[socket(2)](https://www.freebsd.org/cgi/man.cgi?query=socket&apropos=0&sektion=0&manpath=FreeBSD+12.1-RELEASE&arch=default&format=html)で指定する `PF_ROUTE` は"Internal routing protocol"と説明されています。

```
     int
     socket(int domain, int type, int protocol);
...
     The domain argument specifies a communications domain within which
     communication will take place; this selects the protocol family which
     should be used.  These families are defined in the include file
     <sys/socket.h>.  The currently understood formats are:
...
           PF_ROUTE        Internal routing protocol,
```

[setsockopt(2)](https://www.freebsd.org/cgi/man.cgi?query=setsockopt&apropos=0&sektion=0&manpath=NetBSD+8.1&arch=default&format=html)で指定している `RO_MSGFILTER` は `net/route.h` のコメントで"array of which rtm_type to send to client"と説明されています。

```c:/usr/src/sys/net/route.h
256 #define RO_MSGFILTER    1       /* array of which rtm_type to send to client */
```

`RTM_*` なマクロ定数は `sys/net/route.h` で定義されています。これらの値がカーネルからクライアント側に渡されてくるという振る舞いになるようです。

```c:/usr/src/sys/net/route.h
222 #define RTM_VERSION     4       /* Up the ante and ignore older versions */
223 
224 #define RTM_ADD         0x1     /* Add Route */
225 #define RTM_DELETE      0x2     /* Delete Route */
226 #define RTM_CHANGE      0x3     /* Change Metrics or flags */
227 #define RTM_GET         0x4     /* Report Metrics */
228 #define RTM_LOSING      0x5     /* Kernel Suspects Partitioning */
229 #define RTM_REDIRECT    0x6     /* Told to use different route */
230 #define RTM_MISS        0x7     /* Lookup failed on this address */
231 #define RTM_LOCK        0x8     /* fix specified metrics */
232 #define RTM_OLDADD      0x9     /* caused by SIOCADDRT */
233 #define RTM_OLDDEL      0xa     /* caused by SIOCDELRT */
234 // #define RTM_RESOLVE  0xb     /* req to resolve dst to LL addr */
235 #define RTM_ONEWADDR    0xc     /* Old (pre-8.0) RTM_NEWADDR message */
236 #define RTM_ODELADDR    0xd     /* Old (pre-8.0) RTM_DELADDR message */
237 #define RTM_OOIFINFO    0xe     /* Old (pre-1.5) RTM_IFINFO message */
238 #define RTM_OIFINFO     0xf     /* Old (pre-64bit time) RTM_IFINFO message */
239 #define RTM_IFANNOUNCE  0x10    /* iface arrival/departure */
240 #define RTM_IEEE80211   0x11    /* IEEE80211 wireless event */
241 #define RTM_SETGATE     0x12    /* set prototype gateway for clones
242                                  * (see example in arp_rtrequest).
243                                  */
244 #define RTM_LLINFO_UPD  0x13    /* indication to ARP/NDP/etc. that link-layer  
245                                  * address has changed
246                                  */
247 #define RTM_IFINFO      0x14    /* iface/link going up/down etc. */
248 #define RTM_OCHGADDR    0x15    /* Old (pre-8.0) RTM_CHGADDR message */
249 #define RTM_NEWADDR     0x16    /* address being added to iface */
250 #define RTM_DELADDR     0x17    /* address being removed from iface */
251 #define RTM_CHGADDR     0x18    /* address properties changed */
```

`RTM_*` なメッセージの取得は[recvmsg(2)](https://www.freebsd.org/cgi/man.cgi?query=recvmsg&apropos=0&sektion=0&manpath=NetBSD+8.1&arch=default&format=html)で行っています。

```c:/usr/src/usr.sbin/ifwatchd/ifwatchd.c
205         iov[0].iov_base = buf;
206         iov[0].iov_len = sizeof(buf);
207         memset(&msg, 0, sizeof(msg));
208         msg.msg_iov = iov;
209         msg.msg_iovlen = 1;
210 
211         for (;;) {
212                 n = recvmsg(s, &msg, 0);
213                 if (n == -1) {
214                         syslog(LOG_ERR, "recvmsg: %m");
215                         exit(EXIT_FAILURE);
216                 }
217                 if (n != 0)
218                         dispatch(iov[0].iov_base, n);
219         }
```

`recvmsg()` で取得したメッセージが `dispatch()` で処理されます。例えば、ネットワークインタフェースがUPすると、`hd->rtm_type` には `RTM_NEWADDR` が渡され、262行目の `check_addrs()` が呼ばれます。

```c:/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;
```

`check_addrs()` 内の343行目で `RTM_NEWADDR` の判定が行われ、変数 `ev` に `UP` が設定されます。この状態で `invoke_script()` が呼ばれ、変数 `ev` の値に応じたスクリプト( `ifwatchd` コマンドの実行時引数で指定したシェルスクリプト)が実行されます。

```c:/usr/src/usr.sbin/ifwatchd/ifwatchd.c
309 static void
310 check_addrs(const struct ifa_msghdr *ifam)
311 {
...
342         if (ifa != NULL && ifd != NULL) {
343                 ev = ifam->ifam_type == RTM_DELADDR ? DOWN : UP;
344                 aflag = check_addrflags(ifa->sa_family, ifam->ifam_addrflags);
345                 if ((ev == UP && aflag == READY) || ev == DOWN)
346                         invoke_script(ifd->ifname, ev, ifa, brd);
347         }
348 }
```

変数 `ev` の値は配列 `scripts[]` のインデックスとして扱われます。 `ev` の値が `UP` の場合は、 `scripts[]` の `&up_script` に設定されているシェルスクリプトが実行されるという処理になっています。

```c:/usr/src/usr.sbin/ifwatchd/ifwatchd.c
 86 static const char **scripts[] = {
 87         &arrival_script,
 88         &departure_script,
 89         &up_script,
 90         &down_script,
 91         &carrier_script,
 92         &no_carrier_script
 93 };
...
350 static void
351 invoke_script(const char *ifname, enum event ev,
352     const struct sockaddr *sa, const struct sockaddr *dest)
353 {
354         char addr[NI_MAXHOST], daddr[NI_MAXHOST];
355         const char *script;
...
361         script = *scripts[ev];
...
415         switch (vfork()) {
416         case -1:
417                 syslog(LOG_ERR, "cannot fork: %m");
418                 break;
419         case 0:
420                 if (execl(script, script, ifname, DummyTTY, DummySpeed,
421                     addr, daddr, NULL) == -1) {
422                         syslog(LOG_ERR, "could not execute \"%s\": %m",
423                             script);
424                 }
425                 _exit(EXIT_FAILURE);
426         default:
427                 (void) wait(&status);
428         }
429 }
```

これで `ifwatchd.c` の大まかな処理の流れが俯瞰できました。

## まとめ

NetBSDの[ifwatchd](https://netbsd.gw.com/cgi-bin/man-cgi?ifwatchd++NetBSD-current)の簡単な使い方とソースコードの解説を行いました。IPアドレスが振られたことを検知して処理するという機能は、PCルータにおけるWAN側アドレスが変わってしまった場合の通知・対応やリモートで立ち上げたマシンの起動待ち処理等に応用できそうです。