RustでLinuxカーネルの機能を拡張しよう!
Linuxカーネルの機能を安全に拡張できるeBPFのコードはC言語で実装する必要があると知り、がっかりしているクラウドネイティブ 世代の皆様に朗報です。実は、Rustで、eBPFのコードを実装することができます。今更、C言語(クラウドネイティブ ではない感じ)を学ぶ必要はありません!
eBPFとプログラミング言語
eBPFを活用するソフトウェアは、カーネルスペースで動作するeBPFバイトコードと、eBPFバイトコードを制御するユーザスペースのアプリケーションから構成されます。後者は、Go、Python、Rustなど様々なプログラミング言語で実装することができますが、前者は、制限のあるC言語で実装する必要があります。
Rust用eBPFライブラリ
RustでeBPFを扱う一般的な方法は、libbpf-rsライブラリです。これは、C言語でユーザスペースのアプリケーションを実装するためのlibbpfライブラリをRustから使えるようにしたもので、eBPFコードをC言語、アプリケーションをRustで実装します。
「eBPFコードもRustで実装したい!」というRustへの愛に応えるのがRedBPFライブラリです。eBPFコードとアプリケーションの両方をRustで実装することができます。
RustのeBPFコード実装
Rustで、XDPを利用し、受信したICMPパケットを破棄し、メッセージを表示するeBPFのコードを実装してみました。
参考のため、C言語で実装すると、下記のようになります。10行目でIPヘッダが保存されたメモリにアクセスする前に、メモリ領域を確認する6-8行目が、eBPFコード特有のC言語の制限の一例です。
次にRustでの実装が下記になります。メモリ領域の確認もありませんし、すっきりして、クラウドネイティブな感じですね!C言語でも、インライン関数で同じようなことができるんじゃないかという気もしますが、Rustへの愛を疑ってはいけません。
メッセージを表示するためのbpf_trace_printk関数がバイト文字列しか受け付けず、C言語の実装よりも使いにくくなっている点にも目をつぶりましょう。欠点を受け入れることも愛です。
Rustの実装を実行すると、ICMPのパケットは破棄されるのですが、メッセージが表示されません。C言語の実装では表示されるのに…
生成されたeBPFの命令の比較
Rustの実装の問題を調査するため、それぞれの実装から生成されたeBPF命令を比較してみます。C言語の実装から生成されたeBPF命令のうち、bpf_trace_printk関数の呼び出し直前の部分が下記になります。3-6行目でスタックに「PING」という文字列を用意しています。
Rustの実装の同様の部分が下記になります。こちらは、文字列をスタックに用意せず、mapと呼ばれるキーバリューストア的に使われるメモリ領域を参照しています。
mapの中身を見てみると、「PING」という文字列が保存されています。よさそうな感じですね。
$ bpftool map dump id 27
key: 00 00 00 00 value: 50 49 4e 47
ここまで来る前に、皆様は気がついたと思いますが、Rustの実装は、C言語の実装と違い、文字列がNULLで終端していません。下記のように、Rustの実装のbpf_trace_printk関数に渡す文字列をNULLで終端するように修正すれば、想定通りの動作になります。
bpf_trace_printk(b"PING\0");
まとめ
RustでのeBPFコードの実装は、ちょっとでもつまずくと、C言語の知識も必要になり、C言語で実装したほうが簡単ということが分かりました。また、C言語とRustで同じ関数の微妙な違いなど、つまずきやすくなっていることも分かりました。Rustへの愛で乗り越えましょう。NTTは仲間を募集中です。