インフラ・DevOps
カーネルにL7ファイアウォールを実装しました
We Put an L7 Firewall in the Kernel (yeet.cx)
要約
このブログ記事では、eBPFとJavaScriptを使用してカーネル内で動作するL7(アプリケーション層)ファイアウォールを開発した経験について説明しています。従来のユーザー空間プロキシとは異なり、このソリューションはパケット処理をカーネル内で行うことで、ミリ秒単位の遅延をナノ秒単位に短縮し、ポリシーの変更を再ビルドや再デプロイなしに即座に反映させます。
全文翻訳
2026年7月6日 9分で読めるカーネル内のL7ファイアウォール著:Jason Evans「これはJane Streetのシットだ。」あるフォーチュン500企業のエンジニアリングディレクターの反応でした。IPアドレスとポートだけでなく、HTTPヘッダーのようなアプリケーションデータに基づいて決定を行うファイアウォールは、ほとんどの場合、ユーザー空間プロキシでその決定を実行します。パケットはカーネルからコピーされ、TCPストリームが再構築され、インスペクションエンジンが実行され、結果がカーネルにコピーバックされます。これは機能しますが、リクエストごとにミリ秒のコストがかかります。私たちは、そのレイヤーをより速く、より簡単に実行できると考えました。そこで、eBPFを使用してカーネル内で決定を行い、ポリシーをJavaScriptアプリケーションとして記述できるファイアウォールを構築しました。楽しそうですよね?そうでした。決定はナノ秒で完了し、ポリシーは単なるJavaScriptアプリケーションなので、変更には再ビルド、再デプロイ、再起動は不要です。これは既に実際のエンタープライズトラフィックの前に稼働しています。その仕組みと、なぜこの方法で構築したのかを説明します。
なぜカーネルが今これをできるのかHTTPでのカーネル内決定は、かつては非現実的でした。それは偶然ではありません。eBPF検証者は、終了を証明できないループを持つプログラムを拒否するため、直感的に到達するほとんどの解析を無効にします。以前の作業は部分的に進歩しましたが、狭い範囲にとどまっていました。2024年のeBPFにおけるL7ヘッダー解析に関するある研究では、約48バイトのペイロードをマッチングできましたが、実際のHTTP/2ヘッダーブロックを決定するには全く不十分でした。その天井が移動しました。最近のカーネル作業により、カーネル内L7マッチングは数十バイトからキロバイトの範囲に押し上げられ、私たちだけがこれに依存しているわけではありません。2026年5月、ETH Zürich、NYU、Nvidia、Politecnico di Milanoのチームは「Offloading L7 Policies to the Kernel (L7FP)」を発表しました。これは、ほとんどの実際のサービスメッシュL7ポリシーをカーネル内で強制するeBPFデータプレーンを合成するものです。この論文は、私たちのものとは異なる2つの発見のために役立ちます。彼らは2,417のオープンソースプロジェクトを調査し、デプロイされたL7ポリシーの89%がカーネル変更なしでeBPFで実行されていることを発見しました。これは、カーネル内L7がエッジケースではなく、ワークロードの大部分を占めることを意味します。そして、検証者の制限内で解析するために、彼らはAho-Corasick DFAにたどり着きました。偶然にも、私たちのCEO兼アーキテクトであるJulian Goldsteinは、公開された論文を発見する前に、全く同じ理由でこのアプローチにたどり着きました。
何に決定するか通常のパケットフィルターは、5タプル(送信元IP、宛先IP、送信元ポート、宛先ポート、プロトコル)でマッチングします。これはレイヤー3と4です。私たちのものは、HTTP/2内の値でマッチングします::authority (ホスト)、User-Agent、および送信元アドレス。信頼されたロードバランサーの後ろにある場合は、X-Forwarded-ForとPROXYプロトコルの解決も行います。ルールはそれらをブール論理で組み合わせます。例:host == "api.example.com" && src in 127.0.0.0/8 && ua ~ "Diffbot" 現在の一般的な使用法は、特定のホスト上のUser-Agent (GPTBot, ClaudeBot, PerplexityBot, curl/など) でクローラーやエージェントをブロックすることです。これは送信元制約付きで、ルールは期待されるトラフィックの発生元にのみ適用されます。
決定が実行される場所と、その理由マッチングはXDP、NICドライバーのeBPFフックで実行されます。カーネルがソケットバッファを割り当てる前、そしてパケットがネットワークスタックに触れる前です。パスまたはドロップの判定は、その場で生成されます。私たちは意図的にXDPでフックしており、これが上記の学術研究と私たちを隔てる最大の点です。L7FPはソケットレイヤーでフックし、カーネルがTCPストリームを再構築しTLSを復号化した後です。これにより、クリーンなバイトストリームが得られます。早期にフックすることは、ストリームを自分で再構築する必要があることを意味します(断片化セクションで詳しく説明します)。そして、XDPがそれに見合うだけの価値があるため、そのコストを支払います。XDPでドロップされたパケットは、ソケットバッファにならず、スタックに入りません。これはLinuxで最も安価なドロップです:カーネルは、いずれにしても拒否するトラフィックに何も費やしません。強制もユーザー空間の下に位置します。JSからルールを記述・提出し、ランタイムがそれらをカーネルプログラムが読み取る構造にコンパイルします。表現力のある作業、ルールの記述、設定の管理、ログのストリーミングはすべて、通常のJSアプリケーションで行われます。決定自体はカーネル内で実行され、そこでは安価であり、侵害されたユーザー空間プロセスが静かにそれをオフにすることはできません。ルールの更新はgit pushです。再ビルド、再デプロイ、再起動は不要です。素晴らしい。
マッチングと、Aho-Corasickを使用する理由User-Agentを禁止されたサブ文字列のリストに対して一つずつチェックすることは、パターンごとにヘッダーをスキャンすることを意味します。代わりに、すべてのルールにわたるすべてのUAサブ文字列を1つのAho-Corasickオートマトンにコンパイルします。これは、ヘッダーをバイトごとに読み取り、単一パスでヒットしたすべてのパターンを報告するDFAです。スキャンはヘッダーの長さに線形であり、パターンを追加しても遅くなりません。10個のパターンでも1万個のパターンでも、ヘッダーは依然として数百バイトであり、1回のパスで処理されます。より多くのルールはテーブルサイズを消費し、スキャン時間を消費しません。また、カーネルに入れる唯一の形状でもあります。検証者は、すべてのループが終了することを証明できる場合にのみプログラムをロードします。単一の境界付きループとバイトごとの1つのテーブルルックアップは、ヘッダー長で制限されるため、証明は容易です。パターンを1つずつマッチングするとネストされたループになり、検証者を通過するのがはるかに困難になります。その制約が、長年カーネル内L7を困難にしてきた理由であり、私たちとL7FPチームの両方がDFAにたどり着いた理由です。基本的に、私たちは巨大な「すごろく」盤をコンパイルします。ファイアウォールが見るすべてのパケットはこのゲームをプレイします:勝てばブロックされ、負ければ許可されます。
容量とルールの更新方法オートマトンは16ビットの状態IDを使用するため、最大32,767の状態を保持できます。実際には、これは現在約4,000のUser-Agentパターンであり、余裕があります。なぜなら、実際のブロックリストはプレフィックスを共有し、最悪の場合よりもタイトにパックされるからです。約16,000パターンへのパスがありますが、コンパイル時にメモリをより多く消費するため、計画段階でありデフォルトではありません(モデル化されており、まだ出荷されていません)。ルール変更は帯域外で行われます。ルールセットを編集すると、オートマトンが再コンパイルされ、スワップインされます。通常は数秒で、パケットパスの外で行われます。新しいセットが準備できるまで、パケットは現在のルールでマッチングされ続けます。その後、スワップはアトミックに行われます。各変更でオートマトン全体を再構築することは、フォールバックリンクがグローバルであるため、データ構造のプロパティです。そのため、インプレース編集ではなく、バックグラウンドコンパイルとスワップとして更新を扱います。
決定のコスト単一コアは、毎秒約500万回のHTTP/2ヘッダー決定を実行し、ルールを追加してもこの数値は維持されます。各決定は、システムコールよりも安価です。計算式を示します。カーネルプログラムから直接、Prometheusヒストグラムとしてパケットごとの処理時間をエクスポートします。これは、eBPFプログラムがパケットごとに決定に費やす時間を測定します。ほとんどの決定はナノ秒で完了します。あるキャプチャでは、p50は200ナノ秒未満、p95は約775ナノ秒でした。p99は通常1マイクロ秒未満にとどまり、トラフィックが多い場合は低マイクロ秒の範囲に上昇します。遠いテールでは、時折3ミリ秒へのスパイクが見られます。200ナノ秒を文脈に入れるために、すべてのシステムエンジニアが持ち歩くレイテンシーラダーにおけるおおよその位置を示します:操作 おおよそのコスト L1キャッシュ参照 ~1 ns メインメモリ参照 ~100 ns このファイアウォールのp50決定 <200 ns システムコール (getpid, ラウンドトリップ) ~1–2 µs コンテキストスイッチ ~3–5 µs ユーザー空間プロキシホップ (コピーアップ、再構築、検査、コピーダウン) ~1+ ms p50決定は、DRAMからの2回の読み取りと同じくらいのコストで、単一のシステムコールよりも安価です。パケットは、カーネルがユーザー空間に質問するよりも多くの時間を費やす前にフィルタリングされます。この数値について隠さないことが2つあります。これはカーネルの決定時間であり、エンドツーエンドのアプライアンスレイテンシーではありません。ユーザー空間のインスペクションレイヤーは、リクエストごとにシステムコール、コピー、TCP再構築、スケジューラにコストを支払います。そこにミリ秒がかかります。私たちの数値には、ボックス全体を通過する完全なラウンドトリップは含まれていないため、ベンダーの公開するエンドツーエンドレイテンシーとは直接比較できません。また、