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
コメント