BPF/eBPFとは?カーネルのBPFサンプルを使ってみよう。

IT

2021年もはじまりました。
あけましておめでとうございます。

何について書こうと迷いましたが、今回はLinux kernelのeBPFについてご紹介したいと思います。

BPF/eBPFとは

BPFとはBerkeley Packet Filterの略で、効率的なパケットフィルタとして1992年の「The BSD Packet Filter: A New Architecture for User-Level Packet Capture」という論文で登場しました。

BPFでは、フィルタリング用の仮想的なマシン(VM)を持ち、その仮想マシン用のプログラム
を実行することでパケットフィルタリングを行います。

2014年からは、BPFをより汎用的なカーネル内仮想マシンとして利用するため、従来のBPFを大きく拡張する変更が入り、現在に至るまで活発に開発が継続されています。

例えば、2つの32-bitレジスタから10の64-bitレジスタに拡張され、より複雑なプログラムに対応できるようになりました。

この拡張されたBPFが「eBPF(extended BPF)」と呼ばれます。

BPFの実行環境

今回使用するRed Hat Enterprise Linux release 8.2では、デフォルトでBPFを利用できるように設定されています。

お使いの環境が、BPFに関するカーネルコンフィグが有効になっているか、今一度確認してみましょう。

# grep BPF /boot/config-`uname -r`

サンプルプログラムのコンパイル

BPF命令列を直接定義したり、BPFアセンブラを利用する方法もありますが、ここでは最も主流なClangを利用してCのプログラムをeBPFへコンパイルしてみましょう。

また、カーネルのソースコードにはいくつかのサンプルが含まれているので、BPFのサンプルプログラムをコンパイルしてみます。

# cd <source_dir   e.g. linux-4.18.0-193.el8.x86_64>
# make oldconfig
# make headers_install
# make samples/bpf/

サンプルプログラムのディレクトリ配下では、xx_user.cといるユーザーランドのプログラムと、xx_kern.cがそのプログラムの中で利用されるBPFプログラムになっています。

それでは、先程コンパイルしたプログラムを実行してみましょう。

# ./tracex1
            ping-16111 [000] ..s1 18348.513568: 0: skb 00000000a6d959e2 len 104
            ping-16111 [000] ..s1 18348.513621: 0: skb 00000000a1558cd8 len 104

            ping-16111 [000] ..s1 18349.536360: 0: skb 00000000a6d959e2 len 104
            ping-16111 [000] ..s1 18349.536450: 0: skb 00000000a1558cd8 len 104

            ping-16111 [000] ..s1 18350.560359: 0: skb 00000000a6d959e2 len 104
            ping-16111 [000] ..s1 18350.560450: 0: skb 00000000a1558cd8 len 104

ユーザーランド側のプログラムのtracex1_user.cを例に見てみましょう。

// SPDX-License-Identifier: GPL-2.0
#include <stdio.h>
#include <linux/bpf.h>
#include <unistd.h>
#include <bpf/bpf.h>
#include "bpf_load.h"

int main(int ac, char **argv)
{
	FILE *f;
	char filename[256];

	snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);

	if (load_bpf_file(filename)) {
		printf("%s", bpf_log_buf);
		return 1;
	}

	f = popen("taskset 1 ping -c5 localhost", "r");
	(void) f;

	read_trace_pipe();

	return 0;
}

ここでは、ローカルホストに対して、pingを5回実行するという簡単な処理をしています。

また、load_bpf_fileを利用してバイナリファイルをカーネルにロードしています。このload_bpf_file()関数は、bpf_load.hで定義されています。

次に、カーネル側のプログラムtracex1_kern.cを見てみましょう。

/* Copyright (c) 2013-2015 PLUMgrid, http://plumgrid.com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General Public
 * License as published by the Free Software Foundation.
 */
#include <linux/skbuff.h>
#include <linux/netdevice.h>
#include <uapi/linux/bpf.h>
#include <linux/version.h>
#include "bpf_helpers.h"
#include "bpf_tracing.h"

#define _(P) ({typeof(P) val = 0; bpf_probe_read(&val, sizeof(val), &P); val;})

/* kprobe is NOT a stable ABI
 * kernel functions can be removed, renamed or completely change semantics.
 * Number of arguments and their positions can change, etc.
 * In such case this bpf+kprobe example will no longer be meaningful
 */
SEC("kprobe/__netif_receive_skb_core")
int bpf_prog1(struct pt_regs *ctx)
{
	/* attaches to kprobe netif_receive_skb,
	 * looks for packets on loobpack device and prints them
	 */
	char devname[IFNAMSIZ];
	struct net_device *dev;
	struct sk_buff *skb;
	int len;

	/* non-portable! works for the given kernel only */
	skb = (struct sk_buff *) PT_REGS_PARM1(ctx);
	dev = _(skb->dev);
	len = _(skb->len);

	bpf_probe_read(devname, sizeof(devname), dev->name);

	if (devname[0] == 'l' && devname[1] == 'o') {
		char fmt[] = "skb %p len %d\n";
		/* using bpf_trace_printk() for DEBUG ONLY */
		bpf_trace_printk(fmt, sizeof(fmt), skb, len);
	}

	return 0;
}

char _license[] SEC("license") = "GPL";
u32 _version SEC("version") = LINUX_VERSION_CODE;

ここではSEC(“kprobe/__netif_receive_skb_core”)というセクションヘッダを使って、kprobeにアッタチしトレーシングします。

これにより、特定のカーネル関数呼び出し時にBPFプログラムを実行することが可能になります。

まとめ

LinuxのBPFは、パケットフィルタリングのみならず、さまざまな箇所でカーネル内の操作をフックしてプログラマブルにすることができます。

今回は、カーネルのトレーシングをサンプルプログラムを使って実行してみました。

参考

カーネルドキュメント:Documentation/networking/filter.txt

コメント

タイトルとURLをコピーしました