Linux上でオレオレifconfigを作るには

自分の書いているプログラムからNICの設定を直接弄りたくなった事はないだろうか?
プログラム内からifconfigコマンドに引数をつけて実行すれば簡単に実現できるが、それはあんまり格好良くないし、情報を取り出そうとするとifconfigコマンドの出力文字列をパースしたりしなければならなくなって格好悪い。
よりプログラムの書き方として正しいのは、ifconfigコマンドが叩いているAPIを自力で叩いてみる事だろう。

というわけで、ここではifconfigコマンドがNICを操作するために叩いているAPIを自力で叩いてNICの設定を変更してみようと思う。
まずは最も単純なプログラムとして、NICのリンクステータスをUP/DOWNするというのを試してみよう。

ソケットへのioctlによるNICリンクステータス変更

ifconfigコマンドで行われているNICリンクステータスの変更処理を最小限のコードにまとめると、大体以下のようになる。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <net/if.h>
#include <sys/ioctl.h>

int main(int argc, char **argv)
{
	int ret, fd;
	struct ifreq req;

	if (argc < 3) {
		fprintf(stderr, "more args required\n");
		return -1;
	}

	fd = socket(AF_INET, SOCK_DGRAM, 0);
	strncpy(req.ifr_name, argv[1], IFNAMSIZ-1);
	if (ioctl(fd, SIOCGIFFLAGS, &req)) {
		perror("ioctl");
		return -1;
	}
	if (!strcmp(argv[2], "up")) {
		req.ifr_flags |= IFF_UP;
	} else if (!strcmp(argv[2], "down")) {
		req.ifr_flags &= ~IFF_UP;
	} else {
		fprintf(stderr, "Invalid arg\n");
		return -1;
	}
	if (ioctl(fd, SIOCSIFFLAGS, &req)) {
		perror("ioctl");
		return -1;
	}

	return 0;
}

以下のような感じで動作を試すことが出来る。

cc -o sk_link sk_link.c
sudo ./sk_link eth0 down
/sbin/ifconfig eth0
sudo ./sk_link eth0 up
/sbin/ifconfig eth0

ここでは、IP用のソケットを開き、ファイルディスクリプタに対してSIOCGIFFLAGS(IFフラグのゲット)・SIOCSIFFLAGS(IFフラグのセット) ioctlを行う事によりNICのステータスを変更している。
このやり方は*BSDにも共通する方法だ。

しかし、ご存じのようにifconfigコマンドはobsoleteなコマンドとされており、メンテナンスが停止している。
このため、Arch Linuxなど一部のディストリビューションでは標準インストールのパッケージから外されている。
ここで、ifconfigコマンドに代わって使われるようになったのがiproute2のipコマンドだ。

では、iproute2はifconfigコマンドと同様にソケットのファイルディスクリプタに対するioctlでNICの制御を行っているのだろうか?
実は、違うAPIを使っているのだ。どうやら、こちらが新しいインタフェースで、ifconfigコマンドが使っているのは互換性を提供するための古いインタフェースという事のようだ。

次に、新しいインタフェースを使ってNICのリンクステータスを変更してみよう。

netlinkによるNICリンクステータス変更

iproute2で行われているNICリンクステータスの変更処理を最小限のコードにまとめると、大体以下のようになる。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <net/if.h>
#include <asm/types.h>
#include <libnetlink.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>

struct iplink_req {
	struct nlmsghdr		n;
	struct ifinfomsg	i;
	char			buf[1024];
};

int main(int argc, char **argv)
{
	int ret;
	struct rtnl_handle rth = { .fd = -1 };
	struct iplink_req req;

	if (argc < 3) {
		fprintf(stderr, "more args required\n");
		return -1;
	}

	if (rtnl_open(&rth, 0) < 0) {
		fprintf(stderr, "Cannot open rtnetlink\n");
		return EXIT_FAILURE;
	}

	memset(&req, 0, sizeof(req));

	req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
	req.n.nlmsg_flags = NLM_F_REQUEST;
	req.n.nlmsg_type = RTM_NEWLINK;
	req.i.ifi_family = AF_UNSPEC;
	if (!strcmp(argv[2], "up")) {
		req.i.ifi_change |= IFF_UP;
		req.i.ifi_flags |= IFF_UP;
	} else if (!strcmp(argv[2], "down")) {
		req.i.ifi_change |= IFF_UP;
		req.i.ifi_flags &= ~IFF_UP;
	} else {
		fprintf(stderr, "Invalid arg\n");
		return -1;
	}
	req.i.ifi_index = if_nametoindex(argv[1]);
	if (req.i.ifi_index == 0) {
		fprintf(stderr, "Cannot find device \"%s\"\n", argv[1]);
		return -1;
	}
	ret = rtnl_talk(&rth, &req.n, 0, 0, NULL, NULL, NULL);
	rtnl_close(&rth);

	return ret;
}

以下のような感じで動作を試すことが出来る。
iproute2に付属のlibnetlinkに依存しているため、インストールにはlibnetlinkのヘッダとライブラリ本体がインストールされている必要がある。

apt-get install iproute-dev
cc -o nl_link nl_link.c -lnetlink
sudo ./nl_link eth0 down
/sbin/ifconfig eth0
sudo ./nl_link eth0 up
/sbin/ifconfig eth0

rtnl_openでハンドルを開き、rtnl_talkでreq.n(struct nlmsghdr)に指定したメッセージを送ることによりNICの設定変更を行っていると推測できる。
だが、rtnl_*というAPIの内側では何を行っているのだろうか。
ライブラリからコードを抽出し、ここに展開してみる。

netlinkによるNICリンクステータス変更(libnetlink非依存版)

ライブラリを展開するとおおむねこのような感じになる。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <errno.h>
#include <time.h>
#include <unistd.h>
#include <net/if.h>
#include <asm/types.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>

struct iplink_req {
	struct nlmsghdr	n;
	struct ifinfomsg i;
	char buf[1024];
};

int main(int argc, char **argv)
{
	int fd;
	struct iplink_req req;
	int status;
	struct sockaddr_nl nladdr;
	struct iovec iov;
	struct msghdr msg = {
		.msg_name = &nladdr,
		.msg_namelen = sizeof(nladdr),
		.msg_iov = &iov,
		.msg_iovlen = 1,
	};

	if (argc < 3) {
		fprintf(stderr, "more args required\n");
		return -1;
	}

	fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
	if (fd < 0) {
		perror("Cannot open netlink socket");
		return -1;
	}

	memset(&req, 0, sizeof(req));

	req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
	req.n.nlmsg_flags = NLM_F_REQUEST;
	req.n.nlmsg_type = RTM_NEWLINK;
	req.i.ifi_family = AF_UNSPEC;
	if (!strcmp(argv[2], "up")) {
		req.i.ifi_change |= IFF_UP;
		req.i.ifi_flags |= IFF_UP;
	} else if (!strcmp(argv[2], "down")) {
		req.i.ifi_change |= IFF_UP;
		req.i.ifi_flags &= ~IFF_UP;
	} else {
		fprintf(stderr, "Invalid arg\n");
		return -1;
	}
	req.i.ifi_index = if_nametoindex(argv[1]);
	if (req.i.ifi_index == 0) {
		fprintf(stderr, "Cannot find device \"%s\"\n", argv[1]);
		return -1;
	}

	memset(&nladdr, 0, sizeof(nladdr));
	nladdr.nl_family = AF_NETLINK;
	nladdr.nl_pid = 0;
	nladdr.nl_groups = 0;

	req.n.nlmsg_seq = time(NULL);
	req.n.nlmsg_flags |= NLM_F_ACK;
	iov.iov_base = (void *)&req.n;
	iov.iov_len = req.n.nlmsg_len;

	status = sendmsg(fd, &msg, 0);

	if (status < 0) {
		perror("Cannot talk to rtnetlink");
		return -1;
	}

	close(fd);

	return 0;
}

以下のような感じで動作を試すことが出来る。
このコードではライブラリは必要ない。

cc -o nl2_link nl2_link.c
sudo ./nl2_link eth0 down
/sbin/ifconfig eth0
sudo ./nl2_link eth0 up
/sbin/ifconfig eth0

socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)で特殊なソケットを開き、そこにstruct nlmsghdrで指定したメッセージをsendmsgにより送り込んでいる。
ここでは省略したが、recvmsgでメッセージを受け取ることも可能だ。
man netlinkをみてみると、NETLINK_ROUTEの他にもNETLINK_NETFILTERやNETLINK_SELINUXなど様々なカーネル内サブシステムと通信を行うことが可能なようだ。
クラシックなioctlによる方法と異なり、送るメッセージの種類はデータ内に記述され、ioctlではなくsendmsg/recvmsgによってメッセージの送受信が行われるようだ。
このやり方の方が柔軟性に優れているという事なのかもしれない。

Netlinkのライブラリはiproute2に付属するものとは別にlibnlというものがあり、Pythonバインディングも存在するようだ。
Rubyバインディングが無いのが残念だが、ともあれカーネルの設定をスクリプティング言語で柔軟に変更出来そうだ。

…と、話題が脇道にそれたが、自前プログラムからNICを制御しようと思ったら、このようにNetlinkを使って設定変更をリクエストするのが正しそうだ。
IPアドレスの変更など、リクエスト内容に関する話題はまた今度の機会に書いてみようと思う。