BPFとLinuxでのL2インターフェースを扱うネットワークプログラミングでの違いについて
この記事を書こうと思ったきっかけ
osx上でc言語で書いたパケットキャプチャでパケットをキャプチャしていた時に並行して wiresharkと見比べていたのですが、なんだかwiresharkと比べてパケットを幾つかドロップしていたため、 少し調べた(他人に頼った)ところ、bpfでの確保したディスクリプタはlinuxのそれと幾つか動作の 違いがあるとのことなので、簡単にまとめることにしました。 今回この記事を書く上で情報を提供してくれた方々にはとても感謝です。
どんな記事か
l2レベルでのネットワークプログラミングについて興味がある方を対象に bsdとlinuxでの動作の違いをまとめました。 間違っていたりご意見がある方はtwitter(@slankdev)などにご反応いただけると幸いです。 ファイルディスクリプタを開いた後に、readシステムコールでパケットを受信する時の挙動に違いがあるため それについてまとめています。
Linuxでのパケットの受信
LinuxはsocketシステムコールでPF_PACKETっていうアドレスファイミリを指定してファイルディスクリプタを 開くとそれでL2レベルでパケットの送受信が可能になります。 その後はreadシステムコールでパケットを受信、writeシステムコールでパケットを送信することができます。
今回はLinuxに関しての説明は省略します。もしいってくだされば喜んで書きます。
BSDでのパケットの受信
BSDではPF_PACKETのアドレスファイミリはなく、socketシステムコールは使いません。 BPFというインターフェースからopenシステムコールを使ってディスクリプタを確保します。 開くところと、writeでパケットを送信するところまではいいのですが、readの動作にLinuxとBSDで 決定的な動作の違いがあります。
先ほど説明したとうり、Linuxはreadを一回呼ぶとカーネルのバッファから受信したパケットを一つ受け取るのに 対して、BSDはreadを一回呼ぶと、その時カーネルバッファに溜まっている全てのパケットをreadします。 その代わりにパケット一つ一つに対してBPFヘッダというものをつけてreadします。
簡単に動作の違いを示します。
Linuxの場合 pic.twitter.com/X4QteSoEok
— slankdev (@slankdev) 2016年3月13日
BSDの場合 pic.twitter.com/H6waszLR9h
— slankdev (@slankdev) 2016年3月13日
対応策
以下の図のようにしてしまえばいいわけなので簡単です。
実装はこうする pic.twitter.com/tWTJhHR4IA
— slankdev (@slankdev) 2016年3月13日
今回はC++で簡単にBPFのディスクリプタをラップするクラスを作ってサンプルとして示します。 クラスのソースコードは以下のようになります。
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <vector>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <net/if.h>
#include <net/if_dl.h>
#include <net/bpf.h>
#include <fcntl.h>
#include <unistd.h>
class stream {
private:
int fd;
std::vector<uint8_t> buffer;
uint8_t* buffer_point;
ssize_t buffer_size_readed;
void ioctl(unsigned long l, void* p) {
int res = ::ioctl(fd, l, p);
if (res < 0) {
close();
printf("ioctl \n");
exit(-1);
}
}
size_t write(const void* buffer, size_t bufferlen) {
ssize_t res = ::write(fd, buffer, bufferlen);
if (res < 0) {
perror("write");
close();
exit(-1);
}
return res;
}
size_t read(void* buffer, size_t bufferlen) {
buffer_size_readed = ::read(fd, buffer, bufferlen);
if (buffer_size_readed < 0) {
perror("read");
close();
exit(-1);
}
return buffer_size_readed;
}
public:
struct bpf_header {
uint64_t timestamp;
uint32_t caplen;
uint32_t datalen;
uint16_t headerlen;
};
void recv(void* user_buffer, size_t user_bufferlen) {
if (buffer_point == NULL) {
buffer_size_readed = read(buffer.data(), buffer.size());
buffer_point = buffer.data();
}
struct bpf_header* bh = (bpf_header*)buffer_point;
size_t copylen = bh->caplen;
if (user_bufferlen < bh->caplen)
copylen = user_bufferlen;
memcpy(user_buffer, buffer_point+(bh->headerlen), copylen);
buffer_point += bh->headerlen;
buffer_point += bh->caplen;
if (buffer_point - buffer.data() >= buffer_size_readed)
buffer_point = NULL;
}
void send(const void* buffer, size_t bufferlen) {
write(buffer, bufferlen);
}
void open(const char* device_name, size_t buffer_size=0) {
for (int i=0; i<4; i++) {
std::string str = "/dev/bpf";
str += std::to_string(i);
fd = ::open(str.c_str(), O_RDWR);
if (fd >= 0) break;
}
if (fd < 0) {
printf("cant open \n");
exit(-1);
}
if (buffer_size == 0) {
ioctl(BIOCGBLEN, &buffer_size);
printf("set default buffer size: %zd \n", buffer_size);
}
buffer.resize(buffer_size);
ioctl(BIOCSBLEN, &buffer_size);
/* bind to device */
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, device_name, IFNAMSIZ);
ioctl(BIOCSETIF, &ifr);
/* other config */
unsigned int one = 1;
ioctl(BIOCPROMISC, NULL); // set promisc
ioctl(BIOCIMMEDIATE, &one); //if recv packet then call read fast
ioctl(BIOCSSEESENT, &one); // set recv sendPacket
ioctl(BIOCFLUSH, NULL); // flush recv buffer
ioctl(BIOCSHDRCMPLT, &one); // no complite src macaddr
buffer_point = NULL; // init buffer_point
}
void close() {
if (fd >= 0)
::close(fd);
}
};
まとめ
他にもLinuxとBSDで実装の違いがあるみたいで、新たに勉強の種になったので、 とっても楽しかったです。
参考情報
- man bpf
- man bpfの日本語訳的な
- pkttools
- libpcap