連載
» 2018年11月22日 05時00分 公開

パケットフィルターでトレーシング? Linuxで活用が進む「Berkeley Packet Filter(BPF)」とは何かBerkeley Packet Filter(BPF)入門(1)(2/3 ページ)

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

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.

RSSについて

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

メールマガジン登録

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