パケットフィルターでトレーシング? Linuxで活用が進む「Berkeley Packet Filter(BPF)」とは何か:Berkeley Packet Filter(BPF)入門(1)(2/3 ページ)
Linuxにおける利用が急速に増えている「Berkeley Packet Filter(BPF)」について、基礎から応用まで幅広く紹介する連載。初回は、BPFの歴史や概要について。
BPFの利用例
BPFのよくある利用例を図にすると、以下のようになります。
まずユーザーはBPFプログラムをC言語で記述し、それをコンパイルしたのちbpfシステムコールを利用してカーネルにロードします。カーネル内では検証器によりBPFの安全性を検証した後、必要であればJITコンパイルを行ってBPFプログラムのロードを完了します。BPFプログラムとのデータのやりとりが必要な場合はBPF mapもbpfシステムコール経由で作成します。
BPFプログラムはカーネルの対応するイベントにひも付けられます。そして、カーネルはそのイベントが生じた場合にBPFプログラムを呼び出します。例えば、パケットフィルタリングであればカーネルがパケットを受信した適当なタイミングでフィルタリング用のBPFプログラムが呼び出されます。
呼び出されたBPFプログラムは処理を実行し、戻り値を返します。この戻り値は呼び出し元のその後の処理に影響を与えます。また、場合によってはBPFプログラムが引数として渡されたデータを編集することもあります。さらに、BPFプログラムはカーネル内のヘルパー関数を呼び出したり、プログラムからBPF mapにアクセスしてデータを読み書きしたりすることができます。ユーザープログラムは後からBPF mapにアクセスすることでBPFプログラムからの情報を取得できます。
実際のBPFの雰囲気をつかんでもらうために、以下に幾つかBPFのプログラム例を示します。具体的なプログラムの作成方法は今後の連載で扱っていきます。
パケットフィルタリング
以下はethernetのタイプ番号がIPのパケットのみ受け付けるBPFプログラムの例です。
int bpf_prog(struct __sk_buff *skb) { int type = load_half(skb, offsetof(struct ethhdr, h_proto)); if (type != ETH_P_IP){ return DROP; } return ACCEPT; }
「load_half()」によりイーサネットフレームのヘッダにアクセスし、そのtypeの値に応じてフィルタリングを行っています。このプログラムはraw socketにアタッチすると、ユーザープログラムはethernetのタイプ番号がIPのパケットのみ受信するようになります。
トレーシング
下記はIPヘッダのプロトコル別に送信パケット数を計数するプログラムの例です(「samples/bpf/sockex1_kern.c」から抜粋)。
SEC("socket1") int bpf_prog1(struct __sk_buff *skb) { int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol)); long *value; if (skb->pkt_type != PACKET_OUTGOING) return 0; value = bpf_map_lookup_elem(&my_map, &index); if (value) __sync_fetch_and_add(value, skb->len); return 0; } char _license[] SEC("license") = "GPL";
このBPFプログラムはraw socketにアタッチすると、プログラムはIPヘッダのプロトコルを取得した後、パケットが送信パケットであれば「__sync_fetch_and_add()」を利用してeBPF mapのデータ構造を更新します。後からユーザーアプリケーションがこのデータ構造にアクセスすることで、プロトコロル別の送信パケット数を取得できます。
下記はディスク書き込みの遅延時間を表示するプログラムの例です(「bcc/examples/tracing/disksnoop.py」から抜粋)。
#include <uapi/linux/ptrace.h> #include <linux/blkdev.h> BPF_HASH(start, struct request *); void trace_start(struct pt_regs *ctx, struct request *req) { // stash start timestamp by request ptr u64 ts = bpf_ktime_get_ns(); start.update(&req, &ts); } void trace_completion(struct pt_regs *ctx, struct request *req) { u64 *tsp, delta; tsp = start.lookup(&req); if (tsp != 0) { delta = bpf_ktime_get_ns() - *tsp; bpf_trace_printk("%d %x %d\\n", req->__data_len, req->cmd_flags, delta / 1000); start.delete(&req); } }
このプログラムはカーネルのダイナミックトレーシング機構である「kprobe」を利用して、「trace_start()」を「blk_start_request()」に、「trace_completion()」を「blk_account_io_completion()」(それぞれカーネル内関数)にアタッチします。すると、「blk_start_request()」を呼び出す際に「trace_start()」が、「blk_account_io_completion()」を呼び出す際に「trace_completion()」が実行されるようになります。「trace_start()」でI/O開始時刻を保存し、「trace_completion」でその値を元に遅延時間を計算、出力しています。
この例では、前述のBCCでのプログラム作成をサポートするツールを利用しています。
BPFプログラムのライセンス
BPFのプログラムはシステムコールでアタッチする際、カーネルモジュールと同様にライセンスを指定する必要があります。GPLである必要はありませんが、一部のカーネル内ヘルパー関数はGPL互換ライセンスでなければ呼ぶことができません。
ヘルパー関数とライセンスの関係はこちらにまとまっています。
カーネルバージョンごとのBPFへの対応状況
eBPFはLinux 3.15(2014年)にカーネル本体に導入されました。それ以降現在に至るまで活発に開発されています。
こちらにBPFに関する主要コミットと、そのコミットが最初に含まれるカーネルバージョンがまとめられています。
BPFの機能を試す場合、なるべく新しいカーネルを利用した方がいいですが、トレーシング利用を考える場合はperf eventのサポートが追加されたLinux 4.9が一つの目安となります。
参考までに、主要ディストリビューションのカーネルバージョンを以下に示します。
ディストリビューション | カーネルバージョン |
---|---|
RHEL 7.x | 3.10 |
Fedora 28 | 4.16 |
Debian 8.0(Jessie) | 3.16 |
Debian 9.0(Stretch) | 4.9 |
Ubuntu 16.04.0(Xenial) | 4.4 |
Ubuntu 16.04.1 | 4.4 |
Ubuntu 16.04.2 | 4.8 |
Ubuntu 16.04.3 | 4.10 |
Ubuntu 16.04.4 | 4.13 |
Ubuntu 16.04.5 | 4.15 |
Ubuntu 18.04.0(Bionic) | 4.15 |
Ubuntu 18.04.1 | 4.15 |
openSUSE Leap 15.0 | 4.12 |
Amazon Linux 2018.3 | 4.14 |
なお、ディストリビューションによってはBPFの機能がバックポートされている可能性もあるので、詳細については、各ディストリビューションの情報を確認してください。
Red Hat Enterprise Linux(RHEL)は次期バージョンの8で、BCCやXDPをサポートする予定です(Release Notes)。
本連載ではLinux Kernel 4.18を利用して検証を行っています。Linuxのソースコードに関する記述もLinux 4.18時点のものに基づきます。BPFの開発は非常に活発であり、本連載の記述事項も今後十分変わり得ることにご留意ください。
Copyright © ITmedia, Inc. All Rights Reserved.
関連記事
- 2018年上半期に話題になったSpectreとその変異、Linuxカーネルでの対応まとめ
連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェアの脆弱性に関する情報を取り上げ、解説していく。2018年の上半期は、「Meltdown」「Spectre」とその変異(Variant)の脆弱性に悩まされた。今回はいつもとは異なり、上半期のまとめも兼ねて、Meltdown/Spectreの各変異をバージョンを追いかけながら整理する。 - 2017年版Linuxカーネル開発レポート公開――支援している企業トップ10とは?
The Linux Foundationは2017年版Linuxカーネル開発レポートを公開した。Linuxカーネル4.8から4.13までの開発に焦点を当て、カーネル開発に携わった開発者や変更数などについて言及した。 - Linuxカーネルのソースコードを読んで、システムコールを探る
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。前回まで、printf()内の中身をさまざまな方法で探り、write()やint $0x80の呼び出しまでたどり着いた。今回は、さらにその先にあるLinuxカーネル側のシステムコールを見ていく。