概要
最近、仕事でインフラ周りを見ているのだが、突然コネクションが切れるという問題が発生し、Wiresharkを使ってパケットキャプチャし原因を調査していた。そこで思いの外ネットワークの基礎知識が忘れかけていたので、ネットワークの復習も兼ねてパケットキャプチャを自作してみることにした。
まず考えるのはネットワークインタフェースへのアクセス方法だが調べたところ以下の2種類があるようだ。
- Socket (RAW)
- BPF (Berkeley Packet Filter)
SocketはL4(トランスポートレイヤー)は簡単にアクセスできたが、もう少し低レイヤーから触ってみたかったので、PF_PACKETアドレスファミリを指定してRAW Socketを作ってみたがパケットを取得することができない。どうもMac OS X(BSD)はraw socketは許可されてないようでlibpcap使うか、BPF (Berkeley Packet Filter)を使用するしかないらしい。なのでBPFでパケットキャプチャを作ることにした。
stackoverflow.com
BPFについて
BPFの概要図
BPFは主要な機能は以下の2つ
実際にプログラムからデータリンク層にアクセスするには「/dev/bpf#」というファイルを開いて読み込む。
BPFの詳細、設計思想等については以下の論文が参考になる。
http://www.tcpdump.org/papers/bpf-usenix93.pdf
bpfのOpen
「/dev/bpf#」というファイルを開く、末尾に数値を付加して順番に開けるファイルを探す。
char buf[11] = {0};
int bpf = 0;
for (int i = 0; i < 99; ++i) {
sprintf(buf, "/dev/bpf%i", i);
bpf = open(buf, O_RDWR);
if (bpf != -1) break;
}
printf("open %s\n", buf);
ネットワークインタフェースに紐付ける
ioctl システムコールでBIOCSETIFを指定し「en0」のネットワークインタフェースに紐付ける。
struct ifreq interface;
strcpy(interface.ifr_name, "en0");
if(ioctl(bpf, BIOCSETIF, &interface) > 0) {
perror("ioctl BIOCSETIF");
return errno;
}
Man page of IOCTL
プロミスキャスモードにする
すべてのパケットが拾えるようにプロミスキャスモードに指定する。
if (ioctl(bpf, BIOCPROMISC, NULL) == -1) {
perror("ioctl BIOCPROMISC");
return errno;
}
パケットの読み込み
BPF Bufferの構造
BPFからパケットを読み込む際に注意しなければならないのはバッファから読み込んだ場合、複数パケット含まれていることを考慮しなければならない。
BPF Buffer自体のサイズは ioctl システムコールでBIOCGBLENを指定し呼び出すことで取得することができる。
int bufLength = 1;
if (ioctl(bpf, BIOCGBLEN, &bufLength) == -1) {
perror("ioctl BIOCGBLEN");
return errno;
}
BPFパケットの分割
BPFBufferからデータを取得したらパケット毎に分割する。
分割にはBPF Headerの情報を読み込んで「bh_hdrlen」と「bh_caplen」を足し合わせたサイズ分ポインタを進める。
ポインタを進める際にBPF_WORDALIGNマクロを利用するとワード境界を考慮したパディング分のサイズを付加してくれる。
ptr += BPF_WORDALIGN(bpfPacket->bh_hdrlen + bpfPacket->bh_caplen);
struct bpf_hdr {
struct timeval bh_tstamp;
u_long bh_caplen;
u_long bh_datalen;
u_short bh_hdrlen;
};
BPFBufferからBPFパケットに分割したらPayload部分がEthernet Frameになっている。
BPF Headerのサイズ分ポインタを進めて以下のEthernetHeaderにマッピングして取得する。
typedef struct {
unsigned char destAddress[6];
unsigned char srcAddress[6];
unsigned short type;
} EthernetHeader;
IP Frameの取得
Ethernet Headerのtypeが0x0800になっているパケットがIPフレームである。
Ethernet Headerのサイズ分ポインタを進めて 以下のIPHeaderにマッピングして取得する。
typedef struct {
unsigned char headerLength: 4;
unsigned char version: 4;
unsigned char tos;
unsigned short totalLength;
unsigned short id;
unsigned short fragment;
unsigned char ttl;
unsigned char protocol;
unsigned short checkSum;
unsigned char srcAddress[4];
unsigned char destAddress[4];
IpOption option;
} IpHeader;
TCP Frameの取得
IpHeaderのprotocolが0x06になっているパケットがTCPフレームである。
IPヘッダのサイズはオプションが可変なのでヘッダサイズも可変である。そのため単純な固定値では計算できず「headerLength」プロパティを参照する必要がある。
「headerLength」プロパティは単位が32bitなので実際にポインタを進める際にはヘッダサイズを4倍する。
上記の計算で求めたサイズ分ポインタを進めて 以下のTCPHeaderにマッピングして取得する。
typedef struct {
unsigned short srcPort;
unsigned short destPort;
unsigned int sequenceNum;
unsigned int acknowledgmentNum;
unsigned int header: 4;
unsigned int reserved: 6;
struct CodeBit {
unsigned int urg: 1;
unsigned int ack: 1;
unsigned int psh: 1;
unsigned int rst: 1;
unsigned int syn: 1;
unsigned int fin: 1;
} codeBit;
unsigned short windowSize;
unsigned short checkSum;
unsigned short urgentPointer;
} TCPHeader;
完成品
とりあえず作ってみた版のパケットキャプチャツール
全フィールドチェックしてないので構造体のアライメントのバグとかあるような気がする。
github.com
参考にしたページ
BPFとLinuxでのL2IFを扱うネットワークプログラミングでの違いについて – Slank Blog
kaworu.jpn.org