連載
» 2018年12月26日 05時00分 公開

Berkeley Packet Filter(BPF)入門(3):BPFプログラムの作成方法、BPFの検証器、JITコンパイル機能 (1/3)

Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載。今回は、BPFプログラムの作成方法、BPFの検証器、JITコンパイル機能について解説します。

[味曽野雅史,OSSセキュリティ技術の会]

 Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載「Berkeley Packet Filter(BPF)入門」。初回は、BPFの歴史や概要について解説し、前回はBPFの基礎として、Linuxで用いられるBPFのアーキテクチャなどを説明しました。

 今回は、BPFプログラムの作成方法、BPFの検証器、JITコンパイル機能について解説します。

BPFプログラムの作成方法

 BPFのプログラムを作成するには主に以下の方法があります。

  • BPF命令列を直接定義する
  • BPFアセンブラを利用する
  • tcpdump(libpcap)でフィルター式をcBPFにコンパイルする
  • Clang(LLVM)でCのプログラムをeBPFにコンパイルする
  • その他

 それぞれについて以下で説明します。

BPF命令列を直接定義する

 基本的にマクロを使って命令列を定義する方法です。依存関係が最も少なく、プログラム規模が小さい場合は十分有用です。

 Linuxでは、以下にBPFの命令に関する定義があります。

 これらを利用してBPFのプログラムを作成します。

cBPFプログラムの作成

 「include/uapi/linux/filter.h」で定義されている、以下のマクロを利用して命令を構成します。

#ifndef BPF_STMT
#define BPF_STMT(code, k) { (unsigned short)(code), 0, 0, k }
#endif
#ifndef BPF_JUMP
#define BPF_JUMP(code, k, jt, jf) { (unsigned short)(code), jt, jf, k }
#endif

 なお前回、cBPFアーキテクチャについて説明した際はcBPF命令の構造体の名称を「struct cbpf_insn」としましたが、Linuxでは「struct sock_filter」という名称になっています。

 例として、「arpパケット以外をドロップする」フィルタリングプログラムを作成してみます。引数として、ethernetフレーム先頭を指すポインタが渡されると仮定します。

struct sock_filter code[] = {
    BPF_STMT(BPF_LD  + BPF_H + BPF_ABS, 12),           // A = P[12:14]
    BPF_JUMP(BPF_JMP + BPF_K + BPF_JEQ, 0x806, 0, 1),  // accept if A == 0x806, otherwise jmp drop
    BPF_STMT(BPF_RET + BPF_K, -1),                     // ret -1
    BPF_STMT(BPF_RET + BPF_K, 0),                      // drop: ret 0
};

 やっていることはethernetフレームのtypeフィールドを読み込み、その値が0x806であれば0を返します。そうでなければ-1(実際にはunsignedで評価されるため、unsigned intの最大値)を返します。

 戻り値の意味はBPFプログラムをアタッチする箇所で異なりますが、パケットフィルタリングの場合は「戻り値が受容するパケットの最大長」を意味します。

eBPFプログラムの作成

 主に「include/linux/filter.h」にあるマクロを利用してプログラムを作成します。eBPF命令の構造体は「sturct bpf_insn」という名称です。

 先ほどのcBPFと同じフィルタリングプログラムを作成してみます。R1に「sk_buff」が渡されると仮定します。

struct bpf_insn code[] = {
    BPF_MOV64_REG(BPF_REG_6, BPF_REG_1),             // R6 = R1
    BPF_LD_ABS(BPF_H, 12),                           // R0 = P[12:14]
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0x806, 2),       // jmp drop if R0 != 0x806
    BPF_MOV64_IMM(BPF_REG_0, -1),                    // R0 = -1
    BPF_EXIT_INSN(),                                 // ret
    BPF_MOV64_IMM(BPF_REG_0, 0),                     // drop: R0 = 0
    BPF_EXIT_INSN(),                                 // ret
};

 ここでは「BPF_LD BPF_ABS」命令を利用するために、最初にR6にsk_buffの値を入れています。

BPFアセンブラを利用する

・cBPFアセンブラ

 Linuxのソースに含まれるtools/bpfにcBPFのアセンブラが存在します。以下のようにしてアセンブラがコンパイルできます。

% git clone https://github.com/torvalds/linux
% cd linux
% git checkout -b v4.18 refs/tags/v4.18  # 必要に応じてカーネルバージョンの指定
% cd tools/bpf
% make

 なお、Linuxのソースリポジトリの全クローンは時間がかかります。gitの履歴が必要でなければ「https://github.com/torvalds/linux/releases」から特定のバージョンをダウンロードできます。

 また、bpf toolのコンパイルに当たっては「bison」「flex」「libelf」「bfd」「readline」が必要になります。Ubuntuの場合は以下のようにしてインストールできます。

% sudo apt install bison flex libelf binutils-dev libreadline-dev

 先ほど直接作成したBPFプログラムは以下のように書くことができます。

ldh [12]
jne #0x806, drop
ret #-1
drop: ret #0

 このプログラムを「filter.asm」というファイル名で保存します。tools/bpfにあるbpf_asmを利用して以下のようにアセンブルできます。

% ./bpf_asm -c filter.asm
{ 0x28,  0,  0, 0x0000000c },
{ 0x15,  0,  1, 0x00000806 },
{ 0x06,  0,  0, 0xffffffff },
{ 0x06,  0,  0, 0000000000 },

 ここで、出力は「struct sock_filter」のコード列になっています。

 このアセンブラで利用できるニーモニックの説明はLinuxのドキュメントに存在します("BPF engine and instruction set")。

 また、tools/bpfにあるbpf_dbgを使用することで、cBPFプログラムのディスアセンブルや、ブレークポイントありのデバッグ実行などが可能です。この使用方法も上記のドキュメントにあります。

・eBPFアセンブラ

 ユーザースペースのeBPF実装であるubpfに、eBPFのアセンブラおよびディスアセンブラが存在します。ubpfのeBPFアセンブラで利用されるニーモニックはこちらで定義されています。

 これまでと同様のプログラムは以下のように書くことができます。

ldxh r2, [r1+12]
mov r0, 0
jne r2, 0x0608, +1
mov r0, -1
exit

 なお、このプログラムではR1レジスタに渡された引数がパケット先頭のポインタを持つものとしています(ubpf VMでは「LD_ABS」「LD_IND」命令をサポートしていません)。

 以下のようにしてコンパイルできます。

% git clone https://github.com/iovisor/ubpf; cd ubpf
% ./bin/ubpf-assembler filter.asm a.out
% hexdump -C a.out
00000000  69 12 0c 00 00 00 00 00  b7 00 00 00 00 00 00 00  |i...............|
00000010  55 02 01 00 08 06 00 00  b7 00 00 00 ff ff ff ff  |U...............|
00000020  95 00 00 00 00 00 00 00                           |........|

 ubpfアセンブラの出力はバイナリになります。

 なお、もともとこのアセンブラは、ubpf VMのテスト用途に作成されたものです。Linuxで使用されるeBPFプログラム生成を目的にしたものではありません。

tcpdump(libpcap)でフィルター式をcBPFにコンパイルする

 tcpdump(libpcap)には高レベルのフィルター式をcBPFにコンパイルする機能があります。フィルター式に関するドキュメントは「Manpage of PCAP-FILTER」にあります。

 「tcpdump -d」を利用してフィルター式をコンパイルできます。例えば、これまでと同じようにarpのみ受け付けるフィルターは以下のようになります。

% tcpdump -d arp
(000) ldh      [12]
(001) jeq      #0x806           jt 2    jf 3
(002) ret      #262144
(003) ret      #0

 また「tcpdump -dd」で「struct sock_filter」のコード列を得ることができます。

% tcpdump -dd arp
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 1, 0x00000806 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },

Clang(LLVM)でCのプログラムをeBPFにコンパイルする

 LLVM 3.7からバックエンドとしてeBPFが追加され、Clangを利用してCのプログラムをeBPFへコンパイルすることが可能になりました。これが現在BPFプログラムを作成するための最も主流な方法です。

 今までと同様のプログラムは以下のように書くことができます。

 なお、ここでは簡単にするために引数としてパケットを直接受け取っていますが、実際のパケットフィルタリングではeBPFに渡される引数は「struct __sk_buff」です(このデータ構造はカーネルで実際に利用される「sk_buff」ではなく、疑似的なデータ構造です。このことの詳細は検証器の説明の際に再び触れます)。

int f(char *packet) {
    short type = *(short *)(packet + 12);
    if (type == 0x0608) {  // big endian
        return -1;
    }
    return 0;
}

 以下のコマンドでコンパイルします。結果はelfバイナリになります。

% clang -O3 -c -target bpf -o filter.o filter.c
% readelf -x .text filter.o
Hex dump of section '.text':
  0x00000000 69110c00 00000000 b7000000 ffffffff i...............
  0x00000010 15010100 08060000 b7000000 00000000 ................
  0x00000020 95000000 00000000                   ........

 先ほどのubpfのdisassenblerを利用することで、アセンブリのコードが確認できます。

% objcopy -I elf64-little -O binary filter.o filter.bin
% ./ubpf/bin/ubpf-disassembler filter.bin filter.s
% cat filter.s
ldxh r1, [r1+12]
mov r0, 0xffffffff
jeq r1, 0x608, +1
mov r0, 0x0
exit

 注意点として、CのプログラムがeBPFへコンパイルできることと、そのeBPFプログラムがカーネル内の検証器をパスするかどうかは全くの別問題です。LinuxでeBPFを動作させるためにはC側で考慮しなければならないことが多々存在します。

 また、ClangでコンパイルしたオブジェクトはELFバイナリとなりますが、実際の利用に当たってはこのELFバイナリをロードするためのローダーが必要になります。

 CからのeBPFプログラム作成をサポートするために幾つかツールやライブラリが用意されています。CによるeBPFプログラムの作成は次回以降、より詳しく説明します。

その他

 より簡単にBPFのプログラムが作成できるように、幾つかのプロジェクトではDSLからBPFへのコンパイルを行っています。

  • bpftraceply
    dtraceに似た言語からeBPFへコンパイル(トレーシング用途)
  • P4
    パケット処理を記述するためのプロトコル・ターゲット非依存な言語。バックエンドとしてeBPFをサポートしており、P4からeBPFへのコンパイルが可能(正確にはいったんP4からeBPFへコンパイル可能なCへ変換する)
  • kafel
    ポリシー記述から対応するseccomp filter用のBPFプログラムの作成(こちらもC言語に変換する)

 また、DSLとは少々異なりますが、BCCではBPFプログラムを簡単に作成するためのmodified Cを提供します。

       1|2|3 次のページへ

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。