BPF(Berkeley Packet Filter)によるパケット送受信
Linuxでパケットを自由に作成して送信したり、パケットをキャプチャしたい場合はAF_PACKETを使うと説明しましたが、Mac OS Xを含むBSD系OSではBPF(Berkeley Packet Filter)を使います。
LinuxにもBPFは存在するのでややこしいですが基本的に別ものです。BSD系OSにおいてはBPFは/dev/bpfX(X:0〜)のデバイスファイル経由で使います。/dev/bpfをopen()してファイルディスクリプターにread()/write()することで送受信します。
OS X / FreeBSDでのBPFを使った送受信の例を以下にアップしました。
https://github.com/kztomita/bpf-example
以下で送受信に重要な部分を解説していきます。
BPFのopenと設定
BPFの開き方と設定はbpf.cを参照してください。
BPFのopen
while (1) { sprintf(buff, "/dev/bpf%d", i); fd = open(buff, O_RDWR); if (fd != -1) { break; } /* error */ if (errno == EBUSY) { i++; continue; } else if (errno == ENOENT) { fprintf(stderr, "All bpf are busy.\n"); return -1; } fprintf(stderr, "Can't open bpf.\n"); perror("open"); return -1; }
/dev/bpfX デバイスファイルを開くだけですが、/dev/bpfは既に使用中だとEBUSYになるので順番にopenできるものを探しています。
openしたBPFはBIOCSETIFを使って、送受信するインターフェースを指定する必要があります。
送受信インターフェースの指定
memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, if_name, sizeof(ifr.ifr_name) - 1); if (ioctl(fd, BIOCSETIF, &ifr) == -1) { perror("ioctl(BIOCSETIF)"); close(fd); return -1; }
これで基本的に送受信できるようになりますが、例では追加で以下の設定を行っています。
BIOCIMMEDIATE
デフォルトでは受信バッファがいっぱいになるまでread()はブロックしますが、1(Immediate Mode)にすると、パケット受信後直ちにread()がリターンするようになります。
BIOCPROMISC
Promiscuousモードの設定。
BIOCSDIRECTION
read()で送受信どのパケットを受け取るかを指定します。
受信パケットのみ(BPF_D_IN)
送信パケットのみ(BPF_D_OUT)
送受信両方のパケット(BPF_D_INOUT) - default
FreeBSDで指定可。OS Xには存在しない。
BIOCSHDRCMPLT
送信時にL2ヘッダのSource Macアドレスを自動設定するか否かを指定します。
0: L2ヘッダのSource MACは送信時にインタフェースのMACが自動設定されます。
1: L2ヘッダのSource MACの書き換えは行いません(write()で指定したままのデータを送信)。
Source MACの書き換えを行いたい場合などは1に設定します。
BIOCSHDRCMPLT(Header Completion)を1にすると補完(Completion)されなくなるのでまぎらわしいです。デフォルトは0(補完あり)です。
送受信
送受信にはwrite()/read()を使います。BPFはあくまでソケットではないのでsend()/recv()は使えません。
bpf_send_garp.c では送信例としてGratuitous ARPを送信しています。自分のIPアドレスやMACアドレスを取得するため色々やっていますが、L2ヘッダを含めてパケットデータを作成しwrite()しているだけです。
受信の例は bpf_capture.c を参照してください。
read()時に気をつけないといけないのはread()に渡すバッファのサイズです。BIOCGBLEN ioctlが返すサイズと同じしておかないとEINVALになります。
受信バッファの確保
if (ioctl(fd, BIOCGBLEN, &blen) == -1) { perror("ioctl()"); close(fd); return -1; } buff = malloc(blen); if (buff == NULL) { fprintf(stderr, "Can't allocate memory.\n"); close(fd); return -1; }
read()が返すデータには、受信パケットの情報(タイムスタンプやサイズ)を格納したヘッダ情報(struct bpf_hdr)とパケットデータが含まれます。また、ヘッダ情報1、パケット1、ヘッダ情報2、パケット2、、、のように複数のパケットが格納される場合もあるので、受信バッファのちょっとした解析処理が必要になります。
受信バッファの解析処理
while (1) {
received = read(fd, (void *) buff, blen);
if (received == -1) {
perror("read");
break;
}
end = buff + received;
bpfhdrp = (struct bpf_hdr *) buff;
while ((u_char *) bpfhdrp < end) {
if ((u_char *) bpfhdrp + sizeof(struct bpf_hdr) > end) {
fprintf(stderr, "Insufficient buffer size.\n");
break;
}
printf("Captured Length: %d, Packet Length: %d\n",
bpfhdrp->bh_caplen, bpfhdrp->bh_datalen);
packet = (u_char *) bpfhdrp + bpfhdrp->bh_hdrlen;
if (packet + bpfhdrp->bh_caplen > end) {
fprintf(stderr, "Insufficient buffer size.\n");
break;
}
dump_buffer(packet, bpfhdrp->bh_caplen, MAX_DUMP_SIZE);
/* next bpf_hdr */
bpfhdrp = (struct bpf_hdr *)
((char *) bpfhdrp +
BPF_WORDALIGN(bpfhdrp->bh_hdrlen + bpfhdrp->bh_caplen));
}
}
BIOCIMMEDIATEでImmediate Modeにすればread()は基本的に1パケットずつ返す形になりますが、FreeBSDにおいては、ICMP Echo(ping)のようにパケットをカーネル内で折り返し送信する場合は、read()はICMP Echo/Reply 両方のパケットを格納して返すようです。ということでImmediate Modeにおいても複数パケットが返されることを想定した解析処理が必要です。横着しないないようにしましょう。
以前の記事でも書きましたが、受信についてはpcapを使うべきでしょう。
[参考]
- Mac OS X や FreeBSD上で man 4 bpf
[関連記事]
投稿日:2021/11/14 00:12