
Rexは、eBPFの代わりにRustカーネル拡張プログラムをロードして実行できる、安全で使いやすいカーネル拡張フレームワークです。
____ _______ __ _____ _ _
| _ \| ____\ \/ / | ____|_ _| |_ ___ _ __ ___(_) ___ _ __ ___
| |_) | _| \ / | _| \ \/ / __/ _ \ '_ \/ __| |/ _ \| '_ \/ __|
| _ <| |___ / \ | |___ > <| || __/ | | \__ \ | (_) | | | \__ \
|_| \_\_____/_/\_\ |_____/_/\_\\__\___|_| |_|___/_|\___/|_| |_|___/
Rex は、eBPF の代わりに Rust 製カーネル拡張プログラムをロードして実行できる、安全で実用的なカーネル拡張フレームワークです。 Aya のような eBPF ベースのフレームワークとは異なり、Rex プログラムはカーネル内検証器を通過しません。代わりに、Rust の安全なサブセットで実装され、Rust コンパイラが必要な安全性チェックを行い、ネイティブコードを直接生成します。このアプローチにより、過度に制限された検証要件(例: プログラムの複雑さの制約)やその結果生じる難解な検証エラーを回避し、同時に、eBPF バックエンド + カーネル内 JIT アプローチよりも、ネイティブコンパイラバックエンド(つまり LLVM)でより良い最適化の機会を提供する可能性があります。
Rex は現在、以下の機能をサポートしています。
kprobe、perf_event、tracepoint、xdp、tc。次の例は、選択したシステムコールにアタッチし、プロセス(pid で指定)のシステムコールにエラー(errno で指定)を注入する kprobe プログラムを実装しています。ローダープログラムを含む完全な例は samples/error_injector にあります。
#![no_std]
#![no_main]
use rex::kprobe::kprobe;
use rex::map::RexHashMap;
use rex::pt_regs::PtRegs;
use rex::rex_kprobe;
use rex::rex_map;
use rex::Result;
#[allow(non_upper_case_globals)]
#[rex_map]
static pid_to_errno: RexHashMap<i32, u64> = RexHashMap::new(1, 0);
#[rex_kprobe]
pub fn err_injector(obj: &kprobe, ctx: &mut PtRegs) -> Result {
obj.bpf_get_current_task()
.map(|t| t.get_pid())
.and_then(|p| obj.bpf_map_lookup_elem(&pid_to_errno, &p).cloned())
.map(|e| obj.bpf_override_return(ctx, e))
.ok_or(0)
}
その他のサンプルプログラムは samples にあります。
詳細なガイドは こちら にあります。
追加の設計ドキュメントは docs にあります。
既存の eBPF 拡張は、安全性の保証をカーネル内 eBPF 検証器に依存しています。残念ながらこれにより、安全なプログラムが検証器に拒否されるというユーザビリティの問題が生じます。その例は次のとおりです(これらに限定されません)。
Rex は、安全な Rust の安全性保証を直接活用することで、これらの問題に対処します。開発者はいくつかの制限のある安全な Rust で記述できる任意の方法でプログラムを実装でき、プログラムの複雑さやコードジェネレータ、検証器を満足させるために(しばしば直感に反する)同じロジックの表現方法を探すことを心配する必要がなくなります。
このことを、Memcached 高速化のための最先端拡張プログラムである BPF Memcached Cache (BMC) の実装で示します。複雑な eBPF プログラムである BMC は、検証器を満足させるために、BPF テールコールで接続された複数のコンポーネントに分割され、不格好なループ/分岐実装を使用せざるを得ませんでしたが、Rex での実装 ではそれらは全く不要です。
例えば、BPF-BMC のキャッシュ無効化ロジックで、パケットペイロードから SET コマンドを検索するコードを示します:
// Searches for SET command in payload
for (unsigned int off = 0;
off < BMC_MAX_PACKET_LENGTH && payload + off + 1 <= data_end;
off++) {
if (set_found == 0 && payload[off] == 's' &&
payload + off + 3 <= data_end && payload[off + 1] == 'e' &&
payload[off + 2] == 't') {
off += 3;
set_found = 1;
}
...
}
このコードは、検証器を通過するためだけにループに余分な制約(off < BMC_MAX_PACKET_LENGTH)を導入しているだけでなく、パケットの終端(data_end)をチェックするための反復的な定型コードや、パケット内の "set" 文字列を照合するための煩雑なロジックも使用しています。
Rex の安全な Rust の力を使えば、これらの負担は一切不要です。複雑さの制限がなく、実装により自由が与えられます:
let set_iter = payload.windows(4).enumerate().filter_map(|(i, v)| {
if v == b"set " {
Some(i)
} else {
None
}
});
Rex による BMC の完全な実装は samples/bmc にあります。
Rex は GPLv2 ライセンスの下で提供されています。このリポジトリ内のサブモジュール(Linux、Rust、LLVM)は、それぞれ独自の条件でライセンスされています。詳細は対応するライセンスファイルを参照してください。さらに、memcached ベンチマークは MIT ライセンスの下で提供されています。