入門eBPF を読んだので、 読書メモを残しておきたい。去年発売された本でずっと読みたかったもの。 lizrice/learning-ebpf - github.com にサンプルコードが豊富に準備されているので参考になる。 文脈は意識せずとりあえず個人的に気になったところをメモしておく。

BCC

まずはBCCを使った例から始まる。 bpf_trace_printk() を使うと擬似ファイル/sys/kernel/debug/tracing/trace_pipeにテキストを出力できる。 簡易的にはこれを使えば良いが、eBPFプログラムごとに出力先を分けたい場合には、BPF MAP を使って自前で カーネルとユーザ間でデータをやり取りすれば良い。 PerfリングバッファやBPFリングバッファを使えば柔軟なデータ構造(構造体)でやり取りできる。 前者はCPUごとに領域が分かれているが、後者は全てのCPUで共通の領域で順番も正しい。さらに性能も良いらしい。 Tail Callという仕組みを使えば別のeBPFプログラムを呼び出せる。 Tail Callは完了しても元のeBPFプログラムには戻ってこないのでジャンプのようなもの。 Tail Callを使うにはあらかじめ BPF_MAP_TYPE_PROG_ARRAY 型のMAPを準備しておく必要がある。

仮想マシン

汎用レジスタは10個、そしてスタックフレームポインタがある 1。 Calling Conventionについて解説がされていた。 reg0がeBPFプログラムの引数で、reg1がその戻り値となる。 関数呼び出しではreg1からreg5が引数となる。 命令長は64bitだけど、それを組み合わせたワイド命令もある。 SEC()マクロでコンパイル後のセクション名がわかる。 CからeBPFバイトコードへ、あるいはRustからeBPFバイトコードへコンパイルできる。

bpf(2)

bpftool コマンドは eBPF のロードやアタッチができる便利なツールだが、その中では bpf(2)が使われている。 多くの操作は bpf(2) で実現できるが、アタッチに関してはいくつかバリエーションがあり、bpf(2)な場合もあればperf_even_open(2)ioctl(2)を組み合わせる場合もある。 カーネルにロードされたeBPFプログラムやMAPは、複数参照カウンタが0になれば自動削除されるが、BPF linkや特殊なファイルへのピニングによって参照カウンタを1つ増やせる。 これによって bpftool コマンドは実行が終了したとしても、eBPFプログラムをロードしたままにできる。

CO-RE, libbpf

CO-RE (Compile Once - Run Everywhere)を使うには BCC ではなく libbpf を使う。 カーネル5.4以降でサポートされている 。vmlinux.h を include するとカーネル内の多くの構造体を使える。 bpf_core_read を使うと実行カーネルバージョンとコンパイル時カーネルバージョンの差を埋めるよう、自動的にリロケートを考慮しつつカーネルデータを読み込める。 -g をつけてコンパイルしておくとeBPF検証機のデバッグも楽になる。 以前はループをunrollする必要があったが、今はbpf_loopbpf_for_eachが用意されている。

プログラムタイプ

30個くらいのプログラムタイムと40を超えるアタッチメントタイプがある。 bpftool features でプログラムタイプごとに利用できるヘルパ関数一覧を取得できる。 Kfuncs という仕組みを使うと、カーネル内の関数をBPFサブシステムに登録できる。 さらにCORE BPF Kfuncs というカーネルバージョン互換のあるものもある x86限定であるが、kprobe/kretprobeよりもfentry/fexitを使うと良い。 fentry/fexitはカーネル5.5でのBPFトランポリンのアイデアと一緒に導入された。 fexit は引数と返り値をまとめて取得できる点でも便利。 tracepoint は安定したインターフェースを提供してくれている。 さらにBTF Tracepointもあり、カーネルバージョンごとの構造体メンバの差分を吸収してくれる。 uprobe/uretprobeやUSDTでユーザ空間の関数にアタッチできる。 プログラムタイプにはLSM(Linux Security Module)向けのものもある。 これによってeBPFからセキュリティポリシーを強制できる。もともとはカーネルモジュールがやっていたこと。

ネットワーク

ソケット、TC、XDP、Flow dissector 2、LWT(Light Weight Tunnel)、cgroup、赤外線コントローラなどがターゲットとなる。 ただLWTはあまり使われていないらしい。cgroupはもちろん他の用途もあるけど、eBPFに関連するのはほぼネットワークのもの。 プログラムタイプとアラッチメントタイプの関連はbpf_prog_load_check_attach()にまとめられている。 ネットワークに関してはCilium、MetaでのXDP利用、CloudflareでのDDoS Mitigationなど事例が多い。 本書の中では Inline で動くロードバランサのXDP実装が解説されていた。 TCはXDPと違い、ingress/egress両方をサポートしている点やsk_buff構造体を使える点で差分がある。

SSL_read()SSL_write()などOpenSSLライブラリ内部の関数にuprobeでアタッチすると平文を取得できる。 もちろん静的にリンクされていたり、別のライブラリを使っていたりすれば適用できない。 Pixie の OpenSSL Tracer や BCC のsslsnif がこれを実装している。

Ciliumではhost netns 内のネットワークスタックをバイパスするために eBPF を使っている。 特に iptables はIPアドレスの変更頻度が高い Kubernetes と相性が良くない。 また評価時の計算量も問題になる 3。 IPSecやWireGuardによる Kubernetes の透過的暗号化にも eBPF が絡んでいる。 どれほどeBPFと関連するかつかめていないが、BIG TCP 4などのアイデアも言及されていた。

セキュリティ

Falcoのように eBPF で syscall の出入り口でフックしアラートを発火させる仕組みがある。 また kprobe にアタッチするタイプのプロジェクトとして cilium/tetragon がある。 ここでは、引数のポインタがさすデータが変わるTOCTOU(Iime-of-check to time-of-use)が発生しうる 5 6。 関連するテクニックとして bpf_send_signal() を使うとhookの中でシグナルを送ることができる。つまりわずかな時間差による攻撃を防げる。

言語

高レベルな言語として bpftrace がある。サクッとトレースしたい場合には便利に使える。 cilium/ebpf-goを使うとCで書かれたeBPFコードを自動でビルドしてバイトコードをGoコードに埋め込んでくれる。CO-REをサポートしていて点や全てGoで書かれている点が特徴。 Aya を使うとカーネルランド・ユーザランドどちらもRustで実装できる。 aya-tool で Rust のデータ構造を自動生成してくれる。

開発時にはBPF_PROG_RUNを使うとユーザ空間でデバッグできる。/proc/sys/kernel/bpf_stats_enabledで統計情報を出せるようになるので便利。 今後の進化として、eBPFプログラムの署名(これはCO-REによりバイナリ変換の影響が課題)、寿命の長いカーネルポインタ、メモリ割り当てがある。