パケットキャプチャを作ってみる
概要
最近、仕事でインフラ周りを見ているのだが、突然コネクションが切れるという問題が発生し、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でパケットキャプチャを作ることにした。
BPFについて
BPFの概要図
BPFは主要な機能は以下の2つ
- L2(データリンク層)のデータにアクセス
- パケットフィルタ
実際にプログラムからデータリンク層にアクセスするには「/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; }
プロミスキャスモードにする
すべてのパケットが拾えるようにプロミスキャスモードに指定する。
if (ioctl(bpf, BIOCPROMISC, NULL) == -1) { perror("ioctl BIOCPROMISC"); return errno; }
パケットの読み込み
BPF Bufferの構造
BPFからパケットを読み込む際に注意しなければならないのはバッファから読み込んだ場合、複数パケット含まれていることを考慮しなければならない。
BPF Buffer自体のサイズは ioctl システムコールでBIOCGBLENを指定し呼び出すことで取得することができる。
int bufLength = 1; // BIOCGBLEN : 受信バッファの必要サイズ 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; /* bpf ヘッダの長さ (この構造体+ 境界調整パディング) */ };
Ethernet Frameの取得
BPFBufferからBPFパケットに分割したらPayload部分がEthernet Frameになっている。
BPF Headerのサイズ分ポインタを進めて以下のEthernetHeaderにマッピングして取得する。
typedef struct { // 送信先Macアドレス unsigned char destAddress[6]; // 送信元Macアドレス 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 version: 4; // unsigned char headerLength: 4; // サービスタイプ(IPパケットの優先度) unsigned char tos; // IPパケット全体のサイズをbyte単位で数えたもの unsigned short totalLength; // IPフラグメンテーション用 unsigned short id; unsigned short fragment; // Time to Live unsigned char ttl; // プロトコル番号 unsigned char protocol; // チェックサム unsigned short checkSum; // 送信元IPアドレス unsigned char srcAddress[4]; // 送信先IPアドレス 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; // ack unsigned int ack: 1; // Push(1:バッファリングしない) unsigned int psh: 1; // TCP 接続リセット unsigned int rst: 1; // synchronize unsigned int syn: 1; // tcp接続終了 unsigned int fin: 1; } codeBit; // ウィンドウサイズ unsigned short windowSize; // チェックサム unsigned short checkSum; // 緊急ポインタ unsigned short urgentPointer; } TCPHeader;
完成品
とりあえず作ってみた版のパケットキャプチャツール
全フィールドチェックしてないので構造体のアライメントのバグとかあるような気がする。