Linuxではどうやって生のパケットを扱うんでしょうねって話

★はじめに

BSDではbpfを使用して生のパケットをやりとりすることができます。 ところがlinuxのカーネルにはbpfなんてものはありません。 でも、bootpやdhcp, tcpdumpなんかでプロトコル無しなパケットを やりとりしているはずです。いったいどうやっているんでしょう。

★packetインターフェースってのがあるのよ

linuxのアプリケーションでは、ネットワークインターフェースへの入出力は、 すべてsocketインターフェースを使用することになります。 socketを使用するということは、カーネルのサポートするなんらかの プロトコルを使用するということです。生のパケットをやりとりするためには packet interface(PF_PACKET)というプロトコルドライバ?を利用します。

★具体的にはこんな感じ

以下にpacketインターフェースを使用して生のデータを送受信するまでの、 おおまかな流れを示します。

1. socketを取得

socket()の引数のプロトコルとしてPF_PACKET、ソケットの種類にSOCK_RAWを 指定して、packet interfaceを使用するソケットを取得します。

	pd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

ちなみに以下のbindをしなくても、この段階ですべてのインターフェースからの パケットが受信できるし送信も可能です。

2. インターフェースにbind

2.1 インターフェースの名前から、interface indexを取得する。

bindやsendtoするときに使用するリンクレベルアドレスsockaddr_llには インターフェースのインデックス番号が必要なので、インターフェース名から インデックス番号をしらべます。


	struct ifreq ifr;

	memset(&ifr, 0, sizeof(ifr));
	strncpy(ifr.ifr_name, "eth0", IFNAMSIZ);
        ioctl(pd, SIOCGIFINDEX, &ifr);
	interface_index = ifr.ifr_ifindex;

2.2 bindする

bindすることによって特定のインターフェースからのパケットのみを 受信するようになります。

	struct sockaddr_ll sll;

	memset(&sll, 0xff, sizeof(sll));
	sll.sll_family = AF_PACKET;	/* allways AF_PACKET */
	sll.sll_protocol = htons(ETH_P_ALL);
	sll.sll_ifindex = interface_index;
	bind(pd, (struct sockaddr *)&sll, sizeof sll);

3. 既に受信済のデータを捨てる。

packet インターフェースは、特定のインターフェースにbindされるまで、 すべてのインターフェースからのパケットを受信し続けています。 そのため、実際の受信ループに入る前に、それら受信済の不必要なパケット を捨てておいたほうがよいと思います。

	do {
		fd_set fds;
		struct timeval t;
		FD_ZERO(&fds);	
		FD_SET(pd, &fds);
		memset(&t, 0, sizeof(t));
		i = select(FD_SETSIZE, &fds, NULL, NULL, &t);
		if (i > 0)
			recv(pd, buf, i, 0);
	} while (i);

4. 受信

	recv(pd, buf, sizeof(buf), 0);

ひとたびbindさえできてしまえば、sendtoとrecvで簡単に送受信できます。

受信したデータの先頭には物理層のヘッダも含まれています。 めでたしめでたし。

5. 送信

	struct sockaddr_ll sll;

	memset(&sll, 0, sizeof(sll));
	sll.sll_ifindex = ifindex;
	c = sendto(pd, tmp, c, 0, (struct sockaddr *)&sll, sizeof(sll));

packetインターフェースでは、connectedな状態になることはないので、 send()を使用することはできません。 そのため、常にsendto()を使用することになります。

★サンプルプログラム

テスト用に作成したプログラムです。
netdump.c
指定したインターフェースから受信したパケットを表示します。
受信のみのサンプルですが、インターフェースをpromiscousモードにする コードを含みます。
nettype.c
インターフェースと、ノードIDを指定して、送受信を行ないます。

おわりに

ネットワークインターフェースに流れるデータを扱うんだから、ソケット経由で 扱えた方が、bpfのようにデバイスとして見せるよりも美しいように思えます。

しかし、パケットインターフェースには、フィルタ機能がありません。 bpfはBerkery Packet Filterの名を示す通り、filterが売りの一つです。 bpfのフィルタはインストラクションセットをもつ仮想マシンとして実装されていて、 忘れかけていたマシン語プログラミング(ハンドアセンブル)の楽しさを思い出させてくれる、とっても楽しい機能です。linuxでも使いたいにきまってます。

linuxではフィルタ機能はユーザランドのライブラリで実現します。 pcapなるライブラリがそれで、tcpdumpに含まれています。 フィルタプログラミングはlinuxでも楽しめるみたいです。

というわけで次回はlibpcapで遊んでみようかな。

★参考

manual pages

packet(7)
packetインターフェース
netdevice(7)
networkデバイスのioctl

dhcp関連のソース

dhcp-2.0
serverとclient両方のソースがあり、lpfという bsdの bpfに近いインターフェースを提供する ライブラリを含む。
dhcpcd-1.3.17-pl5
dhcp client daemon のソース
bindせずに使用しているがいいのだろうか?

FENIX/ MEMBERS/ thomas's HOME/ MEMO/ linux_raw_packet

佐藤益弘 thomas@fenix.ne.jp