2021の17日目の記事になります。はじめにこの記事ではeBPFを活用してLinuxカーネルにフック用プログラムを注入することにより、ネットワークパケット処理を拡張する例を示します。その実装にあたり、Rustとlibbpfの統合を行うlibbpf-rsを使った開発体験を記したいと思います。TL;DRlibbpf-rsによってRustとeBPFを組み合わせたプログラムのコンパイルやロード処理の手間は省けるようになります。実際、システムコールの呼び出し部分はほとんど意識する必要がありませんでした。一方でeBPFプログラミングで特に苦労したのは以下の点でした。デバッグとテストつまりeBPFプログラミングにおいて周辺的な問題がツールによって解決されていき、よりプログラムの機能そのものの問題に時間を割くことができたと言えそうです。いえスムーズに開発するためにはやはりテストフレームワークやテストハーネスが欲しいと感じるようになったので、そのあたりは更に調査が必要です。eBPFカーネル内部のイベントをトリガとして呼び出されるプログラムをユーザ空間から注入できる機能です。専用のバイトコードをカーネル内の仮想マシンに解釈させることで命令を実行します。ソース言語をこのeBPF専用バイトコードとしてコンパイルしてカーネルに組み込むことができる、という拡張性の高さからネットワーキングやセキュリティの分野で活用されています。eBPFの拡張性を活用したCiliumがGKEの新たなコンテナネットワーキングの実装に選ばれた実績を見ると、それなりに注目度が高いことが伺えます。登場eBPFプログラミングの登場人物eBPFプログラムはカーネル空間側で実行されますが、実用上はそのeBPFプログラムをカーネルにロードし、実行結果を取り出すためにユーザ空間側で動作するプログラムも必要となります。カーネル空間、ユーザ空間2種類のプログラムをビルドして配布しつつ、それぞれのプログラムごとにビルドツールが分かれがちであるため、ツールのつなぎをMakefileでなんとかすることが多いです。crateが提供するワークフローによって少しでも開発の手間をなくせるか実際に試してみます。eBPFプログラミングの流れeBPFプログラムを開発する場合、一般的には下記のようなステップが必要になります。カーネルで動作するeBPF用のC言語のプログラムを書くclang/llvmでコンパイルしeBPFのバイトコードを生成する(ELF形式)eBPFバイトコードをカーネルにロードするためのスケルトンを生成するスケルトンをincludeしユーザ空間側のプログラムを書く4をコンパイルし2の結果と合わせて配布・実行するCで書く場合たとえばCを基礎に置いたアプリケーション開発を行う場合は下記のようにclangやbpftoolなどを組み合わせます。something.hスケルトンをincludeしユーザ空間側のプログラムを書くスケルトンはlibbpfのヘッダに依存しているため依存関係として別途インストールが必要4をコンパイルし2の結果と合わせて配布・実行するこのようにツールを組み合わせながら自前でMakefileなどを書いて、それぞれのステップで正しく成果物が作られるようにする必要があります。Rustで書く場合RustでeBPFのバインディングを利用する場合のcrateは、libbpf-rsayaなどがあります。今回はlinuxのソースツリーでもメンテされているlibbpfを使ったバインディングである前者のlibbpf-rsを使ってみます。libbpf-rsを使ってRustベースの開発を行う場合は以下のようになります。いうのがポイントです。ビルド全体のライフサイクルがcargoに包括されるため他のRustのcrateとほぼ同じ道具立てで開発を進めることができます。GoのeBPFプログラミングでも概ねCと同様のツールチェインになっており、コマンド実行でスケルトンを生成するようです。ciliumのeBPFプログラムコンパイルlibbpf-rslibbpf-rsRustのプログラムからeBPFのシステムコールおよびeBPF関連オブジェクトを取り扱いやすくしてくれるライブラリです。実体としてはlibbpfのRust用ラッパーですが、ただ関数シグニチャをRust側にポーティングしているだけではなく、Rustプログラミングと親和性が高くなるような仕掛けが追加されています。eBPFのプログラムの周辺操作を行うスケルトンを自動生成するlibbpf-rsとそれに付随するツールlibbpf-cargoが提供するAPIを使うことで、ユーザ空間側のRustプログラムからeBPFプログラムを扱いやすくなります。libbpf-cargoはclang/llvmで出力したeBPFのバイトコードをRust上から操作するための構造体やメソッドを定義したRustのソースコードをスケルトンとして生成します。libbpf_rs::OpenObject,}さらにこのスケルトンからeBPFのシステムコールに直接eBPFのバイトコードを入力できるように、バイトコードそのものをスケルトンコードの定数として埋め込んでいます。buildの成果物に静的にリンクされるため実行環境でlibbpfをインストールしておく必要がありません。libbpf-rsを使ってみる実際にlibbpf-rsを使ってみます。8001ポートへとリダイレクトします。Path)で実装します。現実にありえる正気のユースケースとは思えませんが、こうしたパケットのヘッダ書き換えが発展するともっといい感じのデータパスがLinuxカーネルで作れます。eBPFプログラミング環境のセットアップCをベースにeBPFプログラムを開発するためのセットアップと手順については下記が詳しいです。libbpf-bootstrap今回はこれを参考にしつつlibbpf-rsを使ったRustのプロジェクトでのセットアップを確認します。依存関係としてlibbpf-rsを追加しておきます。buildでeBPFプログラムのコンパイルも起動するためには、ビルドスクリプトの依存関係としてlibbpf-cargoも追加しておきます。"0.9"eBPFのコード置き場を作るeBPFプログラムとしてカーネル空間で動作するコードは多くの場合Cで記述します。"GPL";このようなコードをcargo経由で呼び出すclangがコンパイルします。eBPFプログラムのコードの置き場となるディレクトリを作っておきます。src/bpf/c/libbpf-rs公式だとsrc/bpf/にeBPF用のCのコードを配置することが例として紹介されますが、個人的にはRustのプロジェクトの中にCのコードが混ざることを明示したいので下記のようなディレクトリを用意しています。それを元に作られるRustのスケルトンはsrc/bpf/に出力されるように、後でビルドスクリプトを調整します。下記を追記しておきます。src/bpf/*.rseBPF用のCのコードからスケルトンコードは導出できるため、特にgit管理する必要はありません。スケルトンコードを後から手動で修正することもありません。libbpf-cargoを使ったビルドスクリプトを用意するlibbpf-cargoはeBPFプログラムのコンパイルやRustへのインテグレーションをおこなうワークフローを自動化するためのcargo用ライブラリです。makeなどでcargoコマンド経由でclangを呼び出すことができます。makeも忘れずに実行するのは普通にめんどくさいです。build時に合わせてeBPFプログラムもビルドできるようにします。crateのdocにも下記の説明があるので、むしろそちらが正攻法のようです。interfaceビルドスクリプトを用意するbuild.rsなんならここが今回一番Rustのコードを書いたかもしれません(それでいいのか)。やっていることはシンプルです。スケルトンコードをモジュールとしてcrate内に公開するためのmod.rsを自動生成する一度書いてしまうと後はeBPFのソースコードが増えたところで変わらないので、割と作りっぱなしになります。eBPFプログラムを書くudp_redirect.bpf.c今回はUDPヘッダの宛先ポートを書き換える処理が必要なのですが、実行が簡単などの理由からXDPというeBPFプログラムの種別で実装します。XDPではeBPFプログラムをカーネルへロードして、特定のネットワークデバイスにアタッチした場合、ネットワークデバイスがバケットを受信したタイミングでeBPFプログラムを呼び出します。eBPFプログラムはパケットの解析や編集などを行った後に、次の処理を指示する返却コードを返します。単純なXDPのeBPFプログラムだと下記のようなコードになります。XDP_PASS;}上記ではxdp_mdとしてデバイスが受信したパケットの内容を含むメタデータを受け取っています。このeBPFコードを土台にしてxdp_mdの内容を参照・更新することで、パケットの解析・編集処理を肉付けしていきます。eBPFプログラムでパケット処理を行うXDPと呼ばれる技術のチュートリアルはxdp-tutorialが非常に詳しく、またidiomaticな作法が多数掲載されています。今回もこれを参考にしつつ実装してみました。コードのほとんどがヘッダの解析とデータのアクセス範囲チェックです。今回の課題で実現したいUDPの宛先ポートの書き換えを行っているのは下記です。}カーネル内で確保されているデータの書き換えをカーネルモジュールなしで実現することができるというeBPFの恩恵を端的に示しています。};正直、スケルトンコードをmain関数から呼び出すだけなので、ほぼ定型文で面白みのあるコードにはなりませんでした。ユースケースが複雑になるにつれ、ユーザ空間側のプログラムもより複雑になります。今回はあまりに簡単な例だったのでこれだけで終わっていますが、ダイナミックにXDPの処理内容を変更するためにeBPFプログラムとデータをやりとりするようになると更に多くのコードがユーザ空間側にも必要になってくるでしょう。8001ポートへとリダイレクトします。この課題がeBPFプログラムの実装で解決できたかを確認していきます。GNU/LinuxまずはXDPのフィルタを設定する前の動作を確認します。1パケットキャプチャの出力を確認する宛先8000番でUDPのデータグラムが確認できます。XDPアタッチ後XDPアタッチRustで書いたユーザ空間側のプログラムを実行することでeBPFプログラムをロードしてXDPとしてループバックデバイスにアタッチします。./xdp-redirectこれでUDPの宛先ポートが8000から8001に書き換わるはずです。2パケットキャプチャの出力を確認する予想通り宛先ポートが8001になりました。388001ポートではlistenしているポートもないためunreachable扱いになっています。やってみて解った課題テストeBPFプログラムをロードする際に、危険な操作がないかどうかカーネル側でもチェックをしてくれるverifierという機構があります。このverifierの検査が通りロードに成功すれば、ひとまず安全なコードであるとは言えそうなのですが、業務として正しく動作するかは相変わらずテストする必要があります。eBPFでカバーする領域が増えれば増えるほどテストは必要になるし、カーネルで動作させたい程度には重要なプログラムではあるはずなので、十分にテストしておきたくなるのが人情です。現状では確立されたテストフレームワークは無さそうなので自前で道具を作っていくことになるようです。テストしているようです。言っても普通にセットアップが面倒くさいlibbpfをビルドするために必要なlibzやlibelfは開発環境に入れておく必要はあるし、cargoのビルドスクリプトも最初だけとはいえやっぱり手書きしているわけです。Cargo.tomlにdependenciesを追加したらすぐに開発を始められるというものではないのが難しいところです。いうプロジェクトがあるので、もしかしたらそちらではいくらかセットアップが楽になっているのかもしれない、という期待もあります。拝見するとプロジェクトテンプレートを使って足場を作るようです。これがセットアップの手間を軽減しているかというとなんとも言えませんが。新しめのカーネルじゃないと厳しいeBPFの機能はLinuxカーネルのバージョンアップとともに徐々に追加されているため、使っているディストリビューションの最新バージョンのカーネルじゃないと使いたい機能が入っていなかったりします。見るだけではなく、各ディストリビューションのドキュメントも探してみる必要があります。読んでも自明じゃないものもあったりするので難しいです。Mapを活用した実装を試してみたいところです。宣伝Supershipではプロダクト開発やサービス開発に関わる人を絶賛募集しております。ご興味がある方は以下リンクよりご確認ください。採用サイト是非ともよろしくお願いします。