だいぶ遅くなってしまいましたが、NetBSD Advent Calendar 2019 4日目の記事です。今日はifwatchdについて書こうと思います。
ifwatchdとは
ifwatchdはネットワークインタフェースへのIPアドレス追加・削除を監視するツールです。例えば、PCルータで家庭内LANとインターネットを接続しているような場合に、インターネット側のネットワークが切断→再接続されることがあります。その際にIPアドレスが変わる事もあるのですが、出先からsshで接続するような場合に新しいIPアドレスを何らかの方法で把握できるようにしておきたいものです。ifwatchdはこのようなケースに有効なツールとなっています。
また、ifwatchdはNetBSD 1.6から追加されたツールとなっていますが、QNXにも同名のコマンドが存在しています。
(利用可能なオプションが似通っているので、もしかしたらNetBSDのifwatchdをベースにしているのかもしれませんね)
ifwatchdを使ってみる
さっそくifwatchdを試してみます。今回は -d
と -u
オプションを指定してみます。ifwatchd(8)によると、-d
-u
でネットワークインタフェースがdown/upした時に実行するスクリプトを指定するようです。
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
を以下のようなシェルスクリプトとして作成します。
#!/bin/sh
echo '-=> up'
new_addr=`ifconfig wm2 | grep inet[^6] | sed -e "s/\/.*$//" -e "s/^.* //"`
echo "-=> new address $new_addr"
#!/bin/sh
echo '-=> down'
ifwatchd
を実行します。 -i
は起動直後のネットワークインタフェースのチェックをスキップするオプションで、 -v
はデバッグ用に冗長な出力を行うためのオプションです。この例では wm2
というネットワークインタフェースを監視しています。
-v
を付けない場合はそのままバックグラウンドで ifwatchd
が起動します。
$ sudo ifwatchd -v -i -u up.sh -d down.sh wm2
ここで wm2
をdown→upさせてみます。
$ sudo ifconfig wm2 down ; sleep 3 ; sudo ifconfig wm2
少し待つと、 ifwatchd
を実行している端末に以下が出力されます。単にネットワークインタフェースをdown/upしただけなのでIPアドレスはそのままですが、NetBSDでPCルータを作るような場合は、 pppoectl
によるIPアドレスの再設定を検知することができます。この例では単に(upした際の)IPアドレスを表示しているだけですが、再設定されたIPアドレスをメールやSlackで通知するといった連携が可能になります。
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ソースファイルから成る小さなプログラムのように見えます。
$ which ifwatchd
/usr/sbin/ifwatchd
$ ls /usr/src/usr.sbin/ifwatchd/
CVS/ Makefile ifwatchd.8 ifwatchd.c
まずはソケットの作成です。 PF_ROUTE
なRawソケットを作成し、setsockopt(2)で RO_MSGFILTER
オプションを設定しています。
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)で指定する 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)で指定している RO_MSGFILTER
は net/route.h
のコメントで"array of which rtm_type to send to client"と説明されています。
256 #define RO_MSGFILTER 1 /* array of which rtm_type to send to client */
RTM_*
なマクロ定数は 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)で行っています。
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()
が呼ばれます。
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
コマンドの実行時引数で指定したシェルスクリプト)が実行されます。
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
に設定されているシェルスクリプトが実行されるという処理になっています。
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の簡単な使い方とソースコードの解説を行いました。IPアドレスが振られたことを検知して処理するという機能は、PCルータにおけるWAN側アドレスが変わってしまった場合の通知・対応やリモートで立ち上げたマシンの起動待ち処理等に応用できそうです。