はじめに
「eBPFという単語は聞いたことがあるし、CiliumやFalcoが使っているらしいことも知っている。でも、結局それが何をしている技術なのかは説明できない」
私はしばらくこの状態でした。ネットワークやセキュリティのツールを評価する場面で「eBPFベース」という枕詞に出会うたびに、その言葉が何を保証しているのかが分からないまま読み飛ばしていたのです。
本書『入門 eBPF ―Linuxカーネルの可視化と機能拡張』は、まさにその「分かった気になっている状態」を解体してくれる一冊でした。単なるツールの使い方ガイドではなく、カーネルという実行基盤を安全に拡張するという設計問題に、eBPFがどう答えを出したのかを追体験させる構成になっています。
本記事では、設計・アーキテクチャに関心のあるエンジニアの視点から、本書の内容と読みどころを整理します。
| 項目 | 内容 |
|---|---|
| 書名 | 入門 eBPF ―Linuxカーネルの可視化と機能拡張 |
| 原題 | Learning eBPF |
| 著者 | Liz Rice |
| 訳者 | 武内 覚、近藤 宇智朗 |
| 出版社 | オライリー・ジャパン |
| 発行 | 2023年12月 |
| ISBN | 978-4-8144-0056-0 |
著者のLiz Rice氏はIsovalent社のCOO(チーフオープンソースオフィサー)で、Ciliumの開発者でもあります。CNCFの技術監視委員会の議長やKubeCon+CloudNativeConの共同議長を務めた経歴を持ち、『コンテナセキュリティ』の著者としても知られています。つまり、eBPFを「使う側」ではなく「作る側・広める側」の中心にいる人物が書いた入門書です。
本書が答えようとしている問い
本書のまえがきで、著者は次の3つの問いを立てています。
- なぜeBPFが多くのインフラ関連ツールの基盤技術として選ばれているのか
- eBPFはどのようにして性能を向上させているのか
- トレースからネットワークトラフィックの暗号化まで、なぜこれほど広い領域で役に立つのか
この3つに答えることが本書のゴールです。したがって本書は、APIリファレンス的な網羅性よりも、**「大きく変更される可能性の低い基本と原則」**に焦点を絞っています。急速に進化する技術を扱う書籍としては、この割り切りは非常に賢明だと感じました。
対象読者
著者は対象読者を「eBPFに興味があり、その仕組みをもっと知りたい開発者、システム管理者、オペレーター、学生」と定義しています。重要なのは、自分でeBPFコードを書く予定のない人にとっても有用だと明言している点です。
- 運用・セキュリティ・基盤の業務をしていれば、数年以内にeBPFベースのツールに遭遇する可能性が高い
- そのツールの内部を理解しておけば、より効果的に使える
- 「どのイベントがeBPFプログラムをトリガーするか」を知っていれば、性能メトリクスが何を採取しているのかを正確に理解できる
この主張には強く納得しました。抽象の下にある実装を知っているかどうかで、ツールの出力に対する解像度が変わるからです。
前提知識
- Linuxの基本的なシェルコマンドが使えること
- コンパイラ型言語でのプログラミングに慣れていること(Makefileの理解を含む)
- ポインタの概念に親しんでいること
サンプルコードはPython、C、Goで書かれていますが、すべてに熟達している必要はありません。ただし**「ポインタが何を指しているか」を追える読者であること**は実質的な必須条件です。検証器の章はこの理解がないと読み進めるのが厳しくなります。
章構成
全11章、日本語版で本文は11章構成です。大きく4つのブロックに分けて捉えると読みやすくなります。
| ブロック | 章 | テーマ |
|---|---|---|
| 動機づけ | 1章 | なぜeBPFなのか |
| 仕組みの解剖 | 2〜6章 | Hello World、仮想マシン、システムコール、CO-RE、検証器 |
| 応用領域 | 7〜9章 | プログラムタイプ、ネットワーク、セキュリティ |
| 実践と展望 | 10〜11章 | 開発言語の選択、将来 |
各章末には演習問題が用意されており、これが単なる読み物で終わらせない仕掛けになっています。サンプルコードは著者のGitHubリポジトリ(lizrice/learning-ebpf)で公開されています。
1章:なぜカーネルを拡張したいのか
個人的に、本書で最も設計的な示唆に富んでいたのがこの章です。
歴史のたどりかた
eBPFのルーツは1993年、ローレンス・バークレー国立研究所のSteven McCanneとVan Jacobsonによる論文「The BSD Packet Filter」です。当時はネットワークパケットを通過させるか拒絶するかを決めるフィルタを、擬似的な機械の上で動かすという仕組みでした。
ldh [12]
jeq #ETHERTYPE IP, L1, L2
L1: ret #TRUE
L2: ret #0
論文から引用された上記のコードは、IPパケット以外を除外する4命令のフィルタです。12バイト目から2バイトをロードし、IPを示す値と比較し、一致すれば受理、しなければ拒否する。たったこれだけですが、「プログラマが書いたフィルタをカーネル内で実行する」というeBPFの核はすでにここにあるという著者の指摘は鮮やかでした。
その後の流れは以下の通りです。
- 1997年:カーネル2.1.75でLinuxに導入、tcpdumpで利用される
- 2012年:カーネルv3.5でseccomp-bpfが導入。システムコールの許可・禁止判定にBPFを使う
- 2014年:カーネルv3.18で「拡張されたBPF」=eBPFへ
2014年の変更が転換点です。命令セットの64ビット化とインタプリタの書き直しに加えて、以下が同時に導入されました。
- eBPF Map:eBPFプログラムとユーザ空間の双方からアクセスできるデータ構造
- bpf()システムコール:ユーザ空間とカーネル内eBPFプログラムのやり取り
- BPFヘルパ関数
- eBPF検証器:プログラムの実行が安全かを検査する仕組み
つまりeBPFは「パケットフィルタの拡張」ではなく、プログラムの投入・データ共有・安全性検査という3点セットを備えた実行基盤として再設計された、と読むべきです。
2015年にkprobeへのアタッチが可能になり、2016年にはBrendan Gregg氏のNetflixでのトレース知見とCiliumプロジェクトの発表が重なります。翌年にはFacebook(現Meta)がレイヤ4ロードバランサーのKatranをオープンソース化し、2017年以降Facebookに届くすべてのパケットがXDPを通るという状態になりました。CloudflareのDDoS防衛もeBPFを使っています。
カーネルを変えるための3つの選択肢
本書の1章で私が最も価値を感じたのは、「カーネルの振る舞いを変えたい」という要求に対する選択肢を比較した部分です。
選択肢1:アップストリームに貢献する
Linuxカーネルは執筆時点で3,000万行規模のコードベースです。技術的な難易度に加えて、コミュニティに受け入れられる必要があります。本書が引用する調査によれば、送られたパッチのうち受理されるのは33%程度、しかもほとんどが3〜6ヶ月を要するとのことです。
さらに、受理されてからが長い。カーネルのリリースは2〜3ヶ月ごとですが、多くの組織はディストリビューションが提供するカーネルを使います。本書の執筆時点のRHEL 8.5(2021年11月リリース)が採用していたのは2018年8月リリースのカーネル4.18でした。アイデアから本番環境に届くまで、文字通り数年かかります。
選択肢2:カーネルモジュールを書く
独立して配布できるため、アップストリームの承認は不要です。しかし課題は安全性です。カーネルコードがクラッシュすればマシンごと停止しますし、カーネルは特権コードなのでマシン上のあらゆるデータにアクセスできます。「このモジュールを実行しても安全だ」とユーザが確信する手段がないというのが本質的な問題です。
選択肢3:eBPF
eBPFは安全性に対して全く異なるアプローチを取ります。eBPF検証器が「安全に実行できる」と判定したプログラムだけがロードされるという設計です。ここでの安全とは、マシンをクラッシュさせない、無限ループしない、不正なデータへのアクセスを許さない、という意味です。
この3択の比較は、設計の議論としてそのまま他の領域に転用できます。「拡張性を提供したいが、拡張コードの品質を保証できない」という問題は、プラグイン機構やサンドボックス設計を考える際に必ず出てくるからです。eBPFの答えは「レビューで担保する(アップストリーム)」でも「信頼で担保する(モジュール)」でもなく、**「ロード時の機械的検証で担保する」**でした。
動的ロードと性能
eBPFプログラムは動的にロード・削除でき、アタッチした時点ですでに実行中のプロセスも含めてトリガーされます。アプリケーションの変更も再設定も再起動も不要という点が、可観測性やセキュリティのツールにとって決定的な強みになります。
性能面では、2018年のXDPに関する論文が引用されています。
- ルーティング:カーネル内実装比で2.5倍
- ロードバランシング:IPVS比で4.3倍
理由は明快で、JITコンパイル後はネイティブなマシン命令として実行されること、そしてカーネルとユーザ空間の状態遷移が不要であることです。加えて、必要なイベントだけをカーネル内でフィルタしてからユーザ空間に渡すため、データ転送コストそのものを削減できます。
クラウドネイティブ環境での意味
Kubernetesのノード上では、すべてのコンテナが同じカーネル上で動作します。したがってそのカーネルにeBPFプログラムを載せれば、ノード上の全ワークロードを観測できることになります。サイドカーモデルとの対比が本書では丁寧に語られており、Pod単位でプロキシを注入する必要がないという設計上の差は、運用コストの観点で無視できません。
2〜4章:仕組みを手で確かめる
2章:BPF Mapという共有データ構造
2章はBCC(BPF Compiler Collection)を使ったHello Worldです。ここで最も重要な概念がBPF Mapです。
Mapは広義のKey-Valueストアで、典型的なユースケースは以下の3つです。
- ユーザ空間で設定を書き込み、eBPFプログラムから読む
- あるeBPFプログラムが保存した状態を、別のプログラム(または将来の自分)が読む
- eBPFプログラムが書いた結果やメトリクスを、ユーザ空間のアプリが取得して表示する
Mapの種類は豊富です。
- 配列型(キーは常に4バイトのインデックス)
- ハッシュテーブル型(任意のデータ型がキー)
- FIFOキュー、FILOスタック、LRU、最長プレフィクスマッチ、ブルームフィルタ
- sockmap / devmap:ソケットやネットワークデバイスの情報を保持し、トラフィックのリダイレクトに使う
- プログラム配列:eBPFプログラムの集合を保持し、Tail Callを実現する
- MapのMap
さらに、CPUコアごとに別のメモリ領域を持つper-CPU Mapや、カーネルv5.1以降のスピンロックによる排他制御もあります。
Mapの設計は「カーネル空間とユーザ空間という信頼境界をまたいでどう状態を共有するか」という問題への解答です。境界の両側から触れる単一のデータ構造を用意し、その型と操作を限定する。プロセス間通信の設計を考えたことがある人なら、この構造には既視感があるはずです。
3章:eBPF仮想マシンの実体
3章ではBCCが隠していた部分を剥がし、C言語で書いたコードがeBPFバイトコードになり、機械語になるまでを追います。
レジスタモデル
- 汎用レジスタ:
r0〜r9の10個 -
r10:スタックフレームポインタ(読み出し専用) - コンテクスト引数は実行前に
r1にロードされる - 戻り値は
r0に格納される - 関数呼び出しの引数は
r1〜r5
命令の構造
struct bpf_insn {
__u8 code; /* opcode */
__u8 dst_reg:4; /* dest register */
__u8 src_reg:4; /* source register */
__s16 off; /* signed offset */
__s32 imm; /* signed immediate constant */
};
1命令は64ビット(8バイト)。64ビット値をレジスタに設定するような命令はこれに収まらないため、16バイトのワイド命令エンコーディングを使います。
JITコンパイル
初期実装ではeBPF仮想マシンが実行のたびにバイトコードを逐次翻訳していましたが、現在は性能上の理由からJITコンパイルに置き換わっています。興味深いのは、インタプリタに存在しうるSpectre関連の脆弱性を回避したいという背景もあったという点です。性能最適化とセキュリティ対策が同じ方向を向いた例として記憶に残りました。
この章ではbpftoolの使い方も一通り触れられます。プログラムのロード、バイトコードの表示、JIT後の機械語の表示、XDPイベントへのアタッチ、デタッチ、アンロード。そしてeBPFプログラムのグローバル変数がMapによって実装されているという実装上の事実も明かされます。
4章:bpf()システムコール
bpftoolやBCCが裏で何をしているのかを、システムコールのレベルまで降りて確認する章です。
-
BPF_PROG_LOAD:プログラムのロード -
BPF_MAP_CREATE:Mapの作成 -
BPF_MAP_UPDATE_ELEM/LOOKUP_ELEM/DELETE_ELEM:Map操作 -
BPF_MAP_GET_NEXT_KEY:Map内の全エントリ走査 -
BPF_RAW_TRACEPOINT_OPEN:Raw Tracepointへのアタッチ
設計的に面白いのはライフサイクル管理です。カーネルはeBPFプログラムやMapへの参照カウントを管理し、ゼロになったら削除します。そのため、ユーザ空間のプロセスが終了してもプログラムを生かしておきたい場合には、ピン留め(BPFファイルシステムへの登録)やBPF Linkによって参照を追加します。
strace -e bpf でツールの挙動を観察する演習が用意されており、抽象の下で何が起きているかを自分の目で確かめられる構成になっています。
5章:CO-RE ―― 移植性という設計課題
個人的に、本書で最も「設計の本」らしいと感じた章です。
問題の所在
eBPFプログラムはカーネルのデータ構造にアクセスします。しかしデータ構造の定義はカーネルのバージョンによって変わります。あるマシンでコンパイルしたプログラムが、別のマシンで正しく動く保証がないわけです。
BCCのアプローチとその限界
BCCはこの問題を「実行するマシン上で、実行時にコンパイルする」ことで解決していました。確実に対象マシンのカーネルヘッダを使えるからです。しかし本書は、この方式の問題点を容赦なく列挙します。
- 実行する全マシンにコンパイラツールチェーンとカーネルヘッダが必要
- 実行のたびにコンパイルが走り、起動に数秒かかることもある
- 開発マシンと実行マシンが同一なら、実行時コンパイルは計算資源の無駄
- コンテナイメージに同梱しても、ツールの数だけツールチェーンが重複する
- 組み込みデバイスではコンパイルに必要なメモリが足りない可能性がある
結論として、**「他人に配布するeBPFツールをBCCで作るのは向いていない」**と明言されています。学習用としては優秀だが本番用ではない、という切り分けです。
CO-REの構成要素
CO-RE(Compile Once, Run Everywhere)は以下の要素の組み合わせで成立しています。
- BTF(BPF Type Format):データ構造や関数シグネチャのレイアウトを表現する形式。カーネルv5.4以降が対応
-
カーネルヘッダ:bpftoolで実行中のシステムから
vmlinux.hを生成できる -
コンパイラのサポート:Clangを
-g付きで使うと、CO-RE再配置用のデータをバイナリに含める - libbpf:ロード時に、実行環境のBTFと突き合わせて命令を書き換える(再配置)
- BPFスケルトン:ユーザ空間コードの雛形を自動生成
つまりCO-REの本質は、**「コンパイル時に型情報を埋め込んでおき、ロード時に環境差分を吸収する」**という遅延バインディングです。ビルド時に固定していた前提を、ロード時まで先送りする。この構造は、プラグインシステムやABI互換性の設計を考える上で非常に参考になります。
6章:検証器 ―― 安全性を機械的に保証する
eBPFとカーネルモジュールを分ける最大の特徴が検証器です。
検証プロセス
検証器はプログラムの考えうるすべての実行経路を評価します。命令を先頭から1つずつ見ていき、実際には実行せずに評価を進め、各レジスタの状態をbpf_reg_state構造体に記録します。
レジスタの型は以下のように追跡されます。
-
NOT_INIT:まだ値がセットされていない -
SCALAR_VALUE:ポインタでない値がセットされた -
PTR_TO_CTX:コンテクストへのポインタ -
PTR_TO_PACKET:ネットワークパケットへのポインタ -
PTR_TO_MAP_KEY/PTR_TO_MAP_VALUE
型だけでなく、そのレジスタが取りうる値の範囲も追跡されます。これが不正なメモリアクセスの検出に使われます。
分岐命令に到達すると、全レジスタの状態をスタックにプッシュして片方の経路をたどり、return命令に到達したら戻ってもう片方を評価する、という深さ優先探索です。
制約と最適化
処理できる命令数の上限は100万命令。かつては4,096命令で、これがeBPFプログラムの複雑さを大きく制限していました(非特権ユーザの実行時には現在も4,096が適用されます)。
全組み合わせの検証は計算量的に厳しいため、**state pruning(状態の枝刈り)**という最適化が入っています。ある命令を、以前と同じレジスタ状態で再訪した場合、その先の検証は不要と判断します。
本書には最適化そのものの変遷も書かれていて興味深いです。かつては各ジャンプ命令の前後で状態を保持していましたが、それだと平均4命令ごとに保持することになり、しかもほとんどの保持内容が一致しなかった。結果としてジャンプの有無に関わらず10命令ごとに保持するほうが効率的だと判明した、という話です。
検証器はソースコードを見ていない
重要な注意点として、検証器の対象はソースコードではなくバイトコードです。コンパイラの最適化により両者は1対1に対応しません。たとえば到達不可能なコードをソースに書いても、コンパイラが消してしまうため検証エラーにはならない、という具合です。
著者は「最初にeBPFに興味を持ったとき、検証器を通すのは黒魔術のように思えた。見たところ正しいコードがしばしば拒否された」と率直に書いています。改善は進んでいるものの、実際にeBPFを書くならエラーとの格闘は避けられません。eBPF SlackやStack Overflowが助けを求める場として紹介されている点も含め、正直な記述だと感じました。
7〜9章:どこにフックし、何を判断するか
7章:プログラムタイプとアタッチメントポイント
eBPFプログラムは「どのイベントで動くか」によってタイプが決まり、渡されるコンテクストの中身も、戻り値に対するカーネルの反応もタイプごとに異なります。
トレーシング系のフック:
- kprobe / kretprobe:カーネル関数の入口と出口
- fentry / fexit:より新しく効率的な方式
- Tracepoint / Raw Tracepoint / BTF有効Tracepoint
- uprobe など、ユーザ空間へのアタッチ
- LSM(Linux Security Module)フック
ネットワーク系のフック:
- ソケット
- トラフィックコントロール(TC)
- XDP
- フローディセクタ、軽量トンネリング、cgroup、赤外線コントローラ
加えて、ヘルパ関数とKfuncsについても扱われます。
8章:ネットワーク
XDPプログラムの戻り値は、パケットに対する「評決」です。
| 戻り値 | 意味 |
|---|---|
XDP_PASS |
通常通りネットワークスタックに送る |
XDP_DROP |
即座に破棄する |
XDP_TX |
届いたのと同じインタフェースから送り返す |
XDP_REDIRECT |
別のインタフェースに送る |
XDP_ABORTED |
破棄するが、エラーや想定外の事態に起因するとみなす |
パケットのパースではxdp_md構造体を使います。dataとdata_endが実質的にポインタで、検証器を通すためにはアクセス範囲がこの2点の間にあることを明示する必要があるという点が、6章の内容と直結してきます。data_metaとdataの間の領域は、複数のeBPFプログラムがスタック上の異なる場所で同じパケットを処理する際の協調に使われます。
後半はロードバランサー、XDPオフローディング、トラフィックコントロール、パケットの暗号化・復号、そしてKubernetesネットワーク(iptablesの回避、ネットワークポリシーの強制、コネクションの暗号化)と実用的なトピックが続きます。
9章:セキュリティ ―― 観測点の設計が正しさを決める
この章は、**「観測する場所を間違えると、そもそも正しい判断ができない」**という設計上の教訓が詰まっています。
seccomp / seccomp-bpf
システムコールを制限してアプリケーションの能力を絞る古典的な手法です。seccomp-bpfではBPFコードで許可・禁止を決められますが、2つの制約があります。
- 引数がポインタの場合、その先のデータを参照できない(値渡しの引数しか使えない)
- 設定はプロセス開始時に適用する必要があり、後から変更できない
TOCTOU問題
システムコールの入口にフックする方式には、TOCTOU(Time Of Check to Time Of Use)という根本的な問題があります。
eBPFプログラムがシステムコール入口でトリガーされたとき、引数がポインタなら、カーネルはその先のデータをカーネル内にコピーしてから使います。つまりeBPFプログラムが確認したデータと、カーネルが実際に使うデータが同じである保証がない。この隙間で攻撃者がデータを書き換えられます。
seccomp_unotifyのマニュアルには「セキュリティポリシーの実装のためには利用できない」と明記されているとのことで、これは仕様として認められた限界です。
Linux向けSysmonは入口と出口の両方にアタッチすることで対処していますが、出口で確認する以上記録はできても防止はできないというトレードオフが残ります。
BPF LSM
カーネルv5.7で追加。LSMフックはカーネルが内部データ構造にアクセスする直前に起動するため、TOCTOUの隙間がありません。フックは100種類以上あります。
SEC("lsm/path_chmod")
int BPF_PROG(path_chmod, const struct path *path, umode_t mode)
{
bpf_printk("Change mode of file name %s\n", path->dentry->d_iname);
return 0;
}
0以外を返せばアクセス権の変更が拒否されます。判断がカーネル内で完結するため性能上も有利です。
Cilium Tetragon
LSM APIではなくカーネルの任意の関数にアタッチするアプローチを取ります。ここで著者が持ち出す区別が示唆に富んでいます。
- システムコールとLSMは公式に安定したインタフェース
- しかしそれは3,000万行のうちごく一部
- 長期間変更されておらず今後も変わりそうにない箇所は、公式ではないが実質的に安定しているとみなせる
新しいカーネルが広く使われるまで数年かかることを踏まえれば、実質安定な関数にアタッチするプログラムも数年は動く。「安定性の保証」ではなく「安定性の期待値」で設計するという判断です。TetragonにカーネルアップストリームのGoの開発者が多く参加していることが、この判断を支えています。
検知から防止へ
多くのeBPFセキュリティツールは、検知後にユーザ空間へ通知します。しかしこの通知は非同期であり、アクションを起こす頃にはデータが抜かれた後、悪意あるコードがディスクに永続化された後かもしれません。
Tetragonはカーネルv5.3以降のbpf_send_signal()ヘルパ関数を使い、ポリシー違反を検知した時点でカーネル内から同期的にSIGKILLを送ることでこれを解決しています。
ただし著者は同時に警告しています。誤ったポリシーは正常なアプリケーションを殺します。そのため監査モードで動かして予期せぬ事態が起きないことを確認してから、防止モードに切り替えるという運用が推奨されています。
「検知して通知する」と「その場で止める」の間には、非同期と同期という実装上の差以上に、誤検知のコストをどちらが負担するかという設計判断があります。ネットワーク系のツールが予防モードで運用されるのが当たり前なのに、それ以外のセキュリティツールが監査モードに留まりがちなのは、偽陽性の多さゆえだという指摘は的確でした。
10章:言語とライブラリの選択
実装に進む際の選択肢が整理されています。
| 用途 | 選択肢 |
|---|---|
| トレース情報を手早く集めたい | bpftrace |
| Pythonに慣れており、まず動かしたい | BCC(実行時コンパイルが欠点) |
| CO-RE対応で配布したい(C) | libbpf |
| CO-RE対応で配布したい(Go) | cilium/ebpf、libbpfgo |
| CO-RE対応で配布したい(Rust) | Aya |
その他、gobpf、libbpf-rs、Redbpf、Rust-bccなども紹介されています。**「広く配布し、異なるカーネルバージョンで動かしたいならCO-REが必要」**という判断基準が明示されているのが親切です。
BPFプログラムのテストとデバッグ、複数のeBPFプログラムを協調させる際の注意点にも触れられています。
11章:これからどうなるか
eBPF財団とWindows対応
2021年、Google、Isovalent、Meta、Microsoft、NetflixがLinux財団の支援を受けてeBPF財団を設立しました。中立的な団体として資金と知的財産を保有し、複数の営利企業が協力できる場を提供します。
推進要因の一つが、MicrosoftのWindows向けeBPF開発でした。OSが違ってもプログラムが動くようにするには標準化が必要になります。
Windows版の実装が興味深いのは、ライセンスの制約が設計を規定している点です。LinuxカーネルはGPL v2なので、その中の検証器をWindowsで使うとWindowsのソースをGPL v2で頒布する必要が生じます。そのためPREVAIL検証器とuBPF JITコンパイラという、より許容的なライセンスの実装が採用されています。また、検証とJITコンパイルはカーネルではなくユーザ空間のWindows Secure環境で行われます。
検討中の課題
- 署名されたeBPFプログラム:サプライチェーンセキュリティの観点で望まれるが、CO-REではローダがBTFを見てプログラムを動的に書き換えるため、正当な再配置と悪意ある改変の区別が難しい
- 長寿命のカーネルポインタ:現状ポインタは実行中しか有効でなく、Mapに保存できない
-
メモリ割り当て:
kmalloc()の直接呼び出しは安全でないため、eBPF固有の代替案が検討中
「eBPFはプラットフォームであり、機能ではない」
11.4節のタイトルがそのまま本書の結論です。著者はコンテナ技術との類比でこう述べています。
10年ほど前、誰もがコンテナとは何かを語っていました。今はeBPFが同じ段階にある。しかし将来、ほとんどのユーザは直接eBPFプログラムを書かないだろう。コンテナを使う人が名前空間やcgroupを直接触らないのと同じように、eBPFベースのツールを通じて間接的に使うことになる。
そのうえで、eBPFプログラミングのスキルは「持っていることが望ましいが、持っている人はまだ珍しい」状態が続くだろうと予測しています。ビジネスアプリ開発に対するカーネル開発スキルに近い位置づけだ、と。
設計・アーキテクチャの観点で持ち帰ったこと
本書を読み終えて、eBPFの知識そのもの以上に、設計判断のパターンとして残ったものがいくつかあります。
1. 拡張性と安全性を「検証」で両立させる
拡張コードの安全性を、レビュー(人間の目)でも信頼(提供元の評判)でもなく、ロード時の機械的検証で担保する。この選択によって、eBPFは「誰でも書ける」と「カーネルを壊さない」を同時に成立させました。プラグイン機構やユーザ定義ロジックの実行基盤を設計する際、真っ先に思い出したい構造です。
2. バインディングのタイミングを後ろにずらす
CO-REは、コンパイル時に確定していた型のオフセットをロード時まで遅延させ、環境差分を吸収します。「いつ決めるか」を変えるだけで移植性の問題が解けるという発想は、バージョン互換性に悩む多くの場面に応用が効きます。
3. 観測点の選択が正しさを決める
TOCTOU問題は、機能の実装ミスではなくフックする場所の選択に起因する構造的な欠陥です。システムコール入口は観測には便利だが、厳密なセキュリティ判断には不十分。LSMフックはデータがカーネルにコピーされた後なので安全。この「どこで見るか」の議論は、監査ログやバリデーションの配置を考える際にそのまま使えます。
4. 「公式に安定」と「実質的に安定」を使い分ける
Tetragonが取った判断は、保守的に見れば危うい選択です。しかし、カーネルの更新サイクルが数年であるという現実を踏まえれば、合理的なリスクの取り方でもあります。安定性を二値で捉えず、期待値と時間軸で評価するという姿勢は、外部依存を扱うあらゆる場面で参考になります。
5. 検知と防止のあいだにあるトレードオフ
非同期通知は安全側に倒れますが、攻撃の成立を許します。同期的な強制終了は防げますが、誤検知が即座に障害になります。どちらが正しいかではなく、偽陽性のコストを誰がいつ払うかの設計として捉え直す視点を得ました。
気になった点
不満というほどではありませんが、読む前に知っておくとよい点をいくつか挙げます。
- サンプルコードはカーネルv5.15でテストされている:本書執筆時点では主要なディストリビューションがまだ追いついていないバージョンでした。手を動かす場合は環境の準備に一手間かかります
- BPF LSMはカーネル5.7以降:9章の後半で扱われる機能の多くは、環境によっては試せません
- 検証器の章は難易度が高い:レジスタとポインタのメンタルモデルがないと、ログの読み方の説明で置いていかれます。3章を飛ばさずに読むことをお勧めします
- リファレンスではない:著者自身が「陳腐化しない包括的なリファレンスを作るのは困難」と述べている通り、最新の機能一覧を求める本ではありません
まとめ
『入門 eBPF』は、カーネルという触りにくい領域を、安全に・動的に・移植可能に拡張するという設計問題への解答書として読むと、最も価値が高い一冊だと感じました。
訳者まえがきに「技術に飛びつく前に少しだけしゃがんで、eBPFの概念や内部実装について思いを馳せてみるのも面白い」という一節があります。まさにその通りで、単に隠蔽された技術を使うのではなく、メンタルモデルを構築してから活用したいタイプのエンジニアには強く勧められます。
こんな人に向いています。
- eBPFベースのツールを評価・導入する立場にあり、その保証範囲を理解したい
- 拡張性を持つシステムの設計で、安全性の担保方法を検討している
- 可観測性やセキュリティのツールが「何をどこで見ているのか」を正確に把握したい
- 低レイヤの技術を掘り下げるのが好き
逆に、今すぐ動くeBPFツールのレシピが欲しい場合は、本書より各プロジェクトのドキュメントを当たったほうが早いでしょう。
「eBPFは何をしている技術なのか」を自分の言葉で説明できるようになりたい人にとって、本書はその土台をきちんと作ってくれます。
参考リンク
- eBPFコミュニティサイト: https://ebpf.io
- 本書サンプルコード: https://github.com/lizrice/learning-ebpf
- Ciliumドキュメント(BPFとXDPのリファレンス): https://docs.cilium.io
- LinuxカーネルのBPFドキュメント
- Brendan Gregg氏のサイト: https://www.brendangregg.com
- Andrii Nakryiko氏のサイト(CO-REとlibbpf): https://nakryiko.com