eBPF は Linux カーネル内の検証済み実行環境です。アプリケーションはプログラムとマップをロードし、カーネルが定義したフックにプログラムをアタッチし、ファイルディスクリプターを介してデータを交換します。ネットワーク処理は依然として重要な用途ですが、トレーシング、セキュリティポリシー、性能分析は、もはやパケットキャプチャに依存しません。
パケットフィルターから Linux ソケットフィルターへ #
設計上の系譜は、ULTRIX や DIGITAL UNIX などのシステム向けに DEC が開発したパケットフィルターに始まります。プロセスはパケットフィルターデバイスを開き、インターフェイスを選択し、ioctl() で enfilter 命令列を設定して、受理されたリンク層パケットを読み取りました。フィルターはスタックマシンを使用し、複数のコンシューマーは優先度順に処理されました。
Steven McCanne と Van Jacobson は、パケットのコピーおよび解釈にかかるコストを削減するために Berkeley Packet Filter(BPF)を設計しました。1993 年の論文では、現在も認識できる次の 2 つの変更が説明されています。
- レジスタベースの仮想マシンが、パケットを受理するかどうか、および何バイトをコピーするかを決定する。
- 各オープン済みの
/dev/bpfインスタンスが独自のフィルターとパケットバッファーを持つため、パケットキャプチャは DEC の優先度チェーンではなくデバイスインスタンスとして表現される。
現在 cBPF と呼ばれる Classic BPF には、32 ビットのアキュムレーター A、インデックスレジスター X、16 個のスクラッチワード、パケットロード命令、条件分岐、算術演算、戻り値があります。Linux はこの命令形式を Linux Socket Filtering(LSF)に採用しましたが、フィルターを /dev/bpf ではなくソケットに直接アタッチします。
struct sock_filter code[] = {
BPF_STMT(BPF_LD | BPF_B | BPF_ABS, 9), /* IPv4 protocol byte */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_TCP, 0, 1),
BPF_STMT(BPF_RET | BPF_K, 0xffff),
BPF_STMT(BPF_RET | BPF_K, 0),
};
struct sock_fprog program = {
.len = sizeof(code) / sizeof(code[0]),
.filter = code,
};
setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER,
&program, sizeof(program));この例では、ソケットのデータが IPv4 ヘッダーから始まることを想定しているため、9 バイト目がプロトコルフィールドになります。パケットレイアウトはソケットファミリーとフックによって異なります。ソケットフィルターでは、戻り値が保持するパケットバイト数を示し、ゼロはパケットを拒否します。カーネルはインストール前に命令列を検査します。SO_LOCK_FILTER を使用すると置換または削除を防止できるため、プロセスはフィルターをアタッチしてから権限を放棄できます。libpcap と tcpdump は、高レベルのキャプチャ式をこの命令モデルにコンパイルします。
eBPF の実行モデル #
Linux の最新の BPF システムコールインターフェイスは Linux 3.18 で導入されました。eBPF は固定幅の 64 ビット命令と、11 個の 64 ビットレジスターを使用します。
R0は関数またはプログラムの戻り値を保持します。R1からR5は引数を渡し、呼び出し元保存レジスターです。R6からR9は呼び出し先保存レジスターです。R10は呼び出しごとのスタック用の読み取り専用フレームポインターです。
エントリー時、R1 は xdp_md、__sk_buff、トレーシングコンテキストなど、プログラムタイプ固有のコンテキストを指します。プログラムはフック固有のアクションを返します。XDP プログラムはパケットを通過、破棄、リダイレクト、送信できますが、トレーシングプログラムは通常ゼロを返します。
インタープリターは受理されたバイトコードを実行できます。アーキテクチャが BPF JIT を有効にしている場合、カーネルはプログラムをネイティブ命令に変換し、sysctl ポリシーに従って生成されたイメージを強化できます。JIT コンパイルによって実行コストは変わりますが、検証要件は変わりません。
検証が安全性の境界を定義する #
プログラムをアタッチする前に、検証器は可能なパスを記号的に実行します。検証器は、レジスターとスタックスロットの型、スカラー値の範囲、ポインターの来歴、アライメント、初期化状態、参照の所有権、境界を追跡します。したがって、パケット読み取りのようなコードは、要求されたバイトが data_end より前に存在することを制御フローが証明した後でのみ受理されます。
検証では、次のような特性が強制されます。
- 初期化されていないレジスターやスタックデータを使用しないこと。
- スカラー値をカーネルポインターへ任意に変換しないこと。
- プログラムタイプで許可されたコンテキストフィールドとヘルパー関数だけを使用すること。
- メモリアクセスが境界内にあり、ポインター演算が有効であること。
- 追跡対象の参照の取得と解放が釣り合っていること。
- 終了を証明できるループを含め、実行が有界であること。
受理は検証器のモデルへの適合性を証明するものであり、機能的正しさを証明するものではありません。有効なプログラムでも、すべてのパケットを破棄したり、誤ったキーを集計したり、過剰なオーバーヘッドを追加したりする可能性があります。複雑度の制限により、パス探索で必要な不変条件を確立できない場合、論理的には安全なソースコードが拒否されることもあります。
マップとヘルパー #
マップは、型固有のキーと値のストレージ、および並行性セマンティクスを持つカーネルオブジェクトです。ハッシュ、配列、CPU ごとのマップ、リングバッファー、LRU マップ、最長プレフィックスツリー、プログラム配列、ストレージマップは、それぞれ異なる通信パターンに対応します。ユーザー空間は bpf() コマンドを介してマップを作成・操作し、一般には libbpf がこれをラップします。
int map_fd = bpf_map_create(BPF_MAP_TYPE_HASH, "counts",
sizeof(__u32), sizeof(__u64),
1024, NULL);
bpf_map_update_elem(map_fd, &key, &value, BPF_ANY);プログラムは、許可されたヘルパーまたは BPF kfunc を使用して、マップ要素の検索、時刻とプロセス ID の読み取り、パケットのリダイレクト、リングバッファーイベントの出力、フック固有のデータへのアクセスを行います。利用可能なヘルパーと引数の型はプログラムタイプによって異なります。マップ検索は NULL を返す可能性があるため、検証器はデリファレンス前の null チェックを要求します。
マップは共有状態であるため、競合は引き続きアプリケーション側の問題です。CPU ごとのマップは、カウンターの CPU 間更新を回避します。アトミック操作や struct bpf_spin_lock を利用できるのは、文書化された制約が許す場合だけであり、ユーザー空間は CPU ごとの値を明示的に集計する必要があります。
プログラムタイプとアタッチポイント #
プログラムタイプは、コンテキスト、戻り値の規約、検証器の操作、利用可能なヘルパーを選択します。アタッチタイプはさらにフックを特定します。一般的な組み合わせには次のものがあります。
| 領域 | プログラムとアタッチの例 |
|---|---|
| 初期パケットパス | ネットワークデバイス上の XDP |
| トラフィック制御 | TC ingress/egress の SCHED_CLS または SCHED_ACT |
| ソケット | ソケットフィルター、reuseport 選択、ソケット検索、sockops、cgroup ソケットフック |
| トレーシング | トレースポイント、raw トレースポイント、kprobe/kretprobe、fentry/fexit、perf イベント |
| セキュリティ | LSM フックと cgroup ポリシーフック |
フックの選択は、セマンティクスとコストの両方を決定します。XDP は、対応ドライバー上では通常のネットワークスタックのソケットバッファーが割り当てられる前に実行されるのに対し、TC プログラムは skb を認識します。fentry/fexit は BTF で記述された関数境界を使用しますが、kprobe は命令アドレスを対象とするため、実装変更の影響を受けやすくなります。
権限、ライフサイクル、所有権 #
カーネルとシステムポリシーが要求された操作を許可しない限り、ロードには権限が必要です。最新のカーネルでは CAP_BPF、CAP_PERFMON、CAP_NET_ADMIN によって権限を分離できますが、古い経路や一部の操作では依然として CAP_SYS_ADMIN が使用されます。ディストリビューションのカーネルは kernel.unprivileged_bpf_disabled によって非特権ロードを無効化でき、lockdown、LSM、名前空間、トークン、委譲ポリシーによって追加の制限が課される場合があります。単一の capability 設定を普遍的なものとみなさず、実行中のカーネルを確認してください。
プログラム、マップ、BTF オブジェクト、リンクは、ファイルディスクリプターを介して公開される参照カウント方式のカーネルオブジェクトです。最後の参照を閉じるとオブジェクトは破棄されます。BPF ファイルシステム(bpffs)にオブジェクトをピン留めすると、永続的なパスと参照が与えられます。
mount -t bpf bpf /sys/fs/bpf
bpftool prog show
bpftool map show
bpftool link showbpf_link はアタッチを生のプログラム FD から独立して表現し、対応するフックに対してより安全なライフサイクルを提供します。リンクを閉じると、別の参照またはピンによってリンクが保持されていない限り、プログラムはデタッチされます。ピン留めは、作成プロセスが終了した後も現在のブート中は存続します。再起動後は、起動コードがカーネルオブジェクトを再作成する必要があり、以前のカーネルインスタンスのオブジェクトを再オープンすることはできません。
無制限のアクセスを伴わない可観測性 #
トレーシングプログラムは、レイテンシーの測定、イベントのカウント、スタックのサンプリング、スケジューラー活動の関連付け、リングバッファーを通じた構造化レコードのエクスポートを実行できます。CO-RE リロケーションは BTF を使用して、互換性のあるカーネル型レイアウト間でフィールドアクセスを適応させます。運用ツールでは、次の点も考慮する必要があります。
- コンシューマーがバッファーを排出できない場合のイベント損失。
- 高頻度フックにおけるイベントごとのオーバーヘッド。
- 適切な BTF がないカーネルでのシンボルと型の変更。
- カーネルまたはプロセスデータを公開することによるプライバシーおよびセキュリティへの影響。
ロード済みの内容を調査するには、bpftool prog show、bpftool prog dump xlated、検証ログ、プログラムの実行時統計を使用します。可観測性コードは本番カーネルパスで実行されるため、有界なオーバーヘッドと明示的なデタッチ計画を設計に含める必要があります。
コメント
GitHub で閲覧・コメントする(投稿にはログインが必要です)