Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載。今回は、BPFプログラムの作成方法、BPFの検証器、JITコンパイル機能について解説します。
Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載「Berkeley Packet Filter(BPF)入門」。初回は、BPFの歴史や概要について解説し、前回はBPFの基礎として、Linuxで用いられるBPFのアーキテクチャなどを説明しました。
今回は、BPFプログラムの作成方法、BPFの検証器、JITコンパイル機能について解説します。
BPFのプログラムを作成するには主に以下の方法があります。
それぞれについて以下で説明します。
基本的にマクロを使って命令列を定義する方法です。依存関係が最も少なく、プログラム規模が小さい場合は十分有用です。
Linuxでは、以下にBPFの命令に関する定義があります。
これらを利用してBPFのプログラムを作成します。
「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プログラムをアタッチする箇所で異なりますが、パケットフィルタリングの場合は「戻り値が受容するパケットの最大長」を意味します。
主に「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の値を入れています。
・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にコンパイルする機能があります。フィルター式に関するドキュメントは「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 },
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へのコンパイルを行っています。
また、DSLとは少々異なりますが、BCCではBPFプログラムを簡単に作成するためのmodified Cを提供します。
Copyright © ITmedia, Inc. All Rights Reserved.