eBPFは前から気になっていましたがなかなか学ぶ機会がなくしばらく放置してました。 ただ年末くらいにオライリーから本が発売されたので入門 eBPFを読んでeBPFに入門しています。
この本にはサンプルコードが色々含まれており、自分で触りながらの学習もできるようになっています1。 それをただ写経するだけだとあまり面白くないと思ったので、Rustで書きつつ入門していきます。 Rust自体は昔結構触っていた時期もありますが、しばらく触ってなかったのでRustのリハビリも兼ねています。
そもそもeBPFとは?
eBPFは、extended Berkley Packet Filterの略であり2、BPFの拡張版という位置づけになっています。
Packet Filterという名前の通り、BPFはユーザが書いたネットワークパケットのフィルタリングプログラムをカーネル内で動かすための仮想機械として誕生しました。 しかし、その後になってカーネル内の命令をフックとしてBPFプログラムを実行できるようになったり、セキュリティ系の機能にアタッチできるようになったりと、ネットワークにとどまらずカーネルの機能を手軽に拡張できるための仕組みとして活用されるようになりました。
カーネルを拡張するための仕組みというと、まず思いつくのはカーネルモジュールですが、クラッシュするようなカーネルモジュールを書いてしまうとカーネルそのものもクラッシュしてしまったり、セキュリティに気をつける必要があったりという手間もあります。 一方eBPFでは、カーネルにロードする際にeBPF検証器を使ってプログラムを検査を行うことで、安全なプログラムのみが実行されることを担保することができます。
以上のような利点によって、eBPFは広く使われるようになりつつあり、有名どころだとサイドカー無しでのサービスメッシュを実現するCilliumなどで使われています。
eBPFプログラムははざっと以下のような仕組みで動いています。
まず、ユーザがCやRustで書いたeBPFプログラムを、eBPFバイトコードにコンパイルします。 eBPFバイトコードはネイティブの機械語命令に似ていますが、そのまま動かすことはできない命令系統になっています。
次にユーザプログラムはbpf()システムコールを用いて、eBPFプログラムをアタッチポイント(ex. パケットが届いた・カーネルの関数が呼ばれたなどのフック)にアタッチします。 このアタッチポイントのイベントが起きると、カーネルはeBPFプログラムをJITもしくは逐次翻訳してネイティブ機械語プログラムに変換し、eBPFプログラムを実行します。
また、ユーザプログラムとeBPFプログラムの間でデータのやり取りはMapというデータ構造を用いることで実現します。
Ayaを用いたRustでのHello eBPF
eBPFの仕組みを抑えたところで、実際にeBPFを試してみようと思います。
本ではBCCというpythonを用いたeBPFプログラムのフレームワークをサンプルとして用いていますが、それをただなぞるだけだと芸がないので、Rustを用いて試すことにします。
Rustを用いたeBPFフレームワークとして、libbpf-rs・Redbpf・Aya・Rust-bccが紹介されていますが、その中でも最も有名そうなAyaを使うことにしました。
公式ドキュメントにセットアップの手順が書いてあるのでそれに沿ってサンプルプログラムを動かしてみます。
必要なツールのインストール
今回使用するrustcのバージョンは1.76.0です。
[main] koutarou@koutarou-desktop/INS> rustup show
Default host: x86_64-unknown-linux-gnu
rustup home: /home/koutarou/.rustup
installed toolchains
--------------------
stable-x86_64-unknown-linux-gnu
nightly-x86_64-unknown-linux-gnu
1.46.0-x86_64-unknown-linux-gnu
installed targets for active toolchain
--------------------------------------
x86_64-pc-windows-msvc
x86_64-unknown-linux-gnu
active toolchain
----------------
stable-x86_64-unknown-linux-gnu (default)
rustc 1.76.0 (07dca489a 2024-02-04)
まずbpf-linkerをインストールします。 名前的にeBPFバイトコードに対応したリンカーでしょうか。
[master] koutarou@koutarou-desktop/INS> cargo install bpf-linker
Updating crates.io index
Downloaded bpf-linker v0.9.11
Downloaded 1 crate (31.5 KB) in 0.97s
Installing bpf-linker v0.9.11
Updating crates.io index
Downloaded anstyle v1.0.6
Downloaded cc v1.0.86
Downloaded utf8parse v0.2.1
Downloaded camino v1.1.6
Downloaded simplelog v0.12.1
Downloaded anstyle-query v1.0.2
Downloaded time-core v0.1.2
Downloaded anstream v0.6.12
Downloaded lazy_static v1.4.0
Downloaded log v0.4.20
Downloaded clap_lex v0.7.0
Downloaded clap_derive v4.5.0
Downloaded cfg-if v1.0.0
Downloaded cargo-platform v0.1.7
Downloaded anyhow v1.0.80
Downloaded heck v0.4.1
Downloaded colorchoice v1.0.0
Downloaded num_threads v0.1.7
Downloaded once_cell v1.19.0
Downloaded proc-macro2 v1.0.78
Downloaded ryu v1.0.17
Downloaded prettyplease v0.2.16
Downloaded time-macros v0.2.17
Downloaded thiserror-impl v1.0.57
Downloaded thiserror v1.0.57
Downloaded serde_derive v1.0.197
Downloaded serde v1.0.197
Downloaded semver v1.0.22
Downloaded unicode-ident v1.0.12
Downloaded regex-lite v0.1.5
Downloaded time v0.3.34
Downloaded clap_builder v4.5.1
Downloaded syn v2.0.50
Downloaded clap v4.5.1
Downloaded strsim v0.11.0
Downloaded quote v1.0.35
Downloaded powerfmt v0.2.0
Downloaded libc v0.2.153
Downloaded itoa v1.0.10
Downloaded deranged v0.3.11
Downloaded termcolor v1.1.3
Downloaded cargo_metadata v0.18.1
Downloaded anstyle-parse v0.2.3
Downloaded aya-rustc-llvm-proxy v0.9.1
Downloaded num-conv v0.1.0
Downloaded ar v0.9.0
Downloaded libloading v0.8.1
Downloaded llvm-sys v180.0.0-rc2
Downloaded serde_json v1.0.114
Downloaded 49 crates (2.7 MB) in 1.52s
Compiling proc-macro2 v1.0.78
Compiling unicode-ident v1.0.12
Compiling serde v1.0.197
Compiling semver v1.0.22
Compiling anyhow v1.0.80
Compiling thiserror v1.0.57
Compiling serde_json v1.0.114
Compiling camino v1.1.6
Compiling libc v0.2.153
Compiling regex-lite v0.1.5
Compiling cc v1.0.86
Compiling lazy_static v1.4.0
Compiling ryu v1.0.17
Compiling prettyplease v0.2.16
Compiling itoa v1.0.10
Compiling utf8parse v0.2.1
Compiling colorchoice v1.0.0
Compiling anstyle v1.0.6
Compiling anstyle-query v1.0.2
Compiling num-conv v0.1.0
Compiling powerfmt v0.2.0
Compiling time-core v0.1.2
Compiling clap_lex v0.7.0
Compiling num_threads v0.1.7
Compiling heck v0.4.1
Compiling strsim v0.11.0
Compiling cfg-if v1.0.0
Compiling anstyle-parse v0.2.3
Compiling once_cell v1.19.0
Compiling log v0.4.20
Compiling libloading v0.8.1
Compiling termcolor v1.1.3
Compiling time-macros v0.2.17
Compiling deranged v0.3.11
Compiling ar v0.9.0
Compiling anstream v0.6.12
Compiling clap_builder v4.5.1
Compiling quote v1.0.35
Compiling syn v2.0.50
Compiling time v0.3.34
Compiling simplelog v0.12.1
Compiling serde_derive v1.0.197
Compiling thiserror-impl v1.0.57
Compiling clap_derive v4.5.0
Compiling clap v4.5.1
Compiling cargo-platform v0.1.7
Compiling llvm-sys v180.0.0-rc2
Compiling cargo_metadata v0.18.1
Compiling aya-rustc-llvm-proxy v0.9.1
Compiling bpf-linker v0.9.11
Finished release [optimized + debuginfo] target(s) in 16.09s
Installing /home/koutarou/.cargo/bin/bpf-linker
Installed package `bpf-linker v0.9.11` (executable `bpf-linker`)
次に、Ayaのテンプレートプログラムを作るためにcargo-generateを入れます。
[02/23/24 9:10]/home/koutarou/develop/rust-learn/ebpf-hello
[master] koutarou@koutarou-desktop/INS> cargo install cargo-generate
Updating crates.io index
Downloaded cargo-generate v0.19.0
Downloaded 1 crate (96.2 KB) in 1.08s
Installing cargo-generate v0.19.0
Updating crates.io index
Downloaded crunchy v0.2.2
Downloaded console v0.15.8
Downloaded crypto-common v0.1.6
Downloaded clap_lex v0.6.0
Downloaded shell-words v1.1.0
Downloaded sha1_smol v1.0.0
Downloaded fs_at v0.1.10
Downloaded walkdir v2.4.0
Downloaded terminal-prompt v0.2.3
Downloaded liquid v0.26.4
Downloaded smartstring v1.0.1
Downloaded gix-path v0.10.5
Downloaded gix-hash v0.13.3
Downloaded bitflags v2.4.2
Downloaded winnow v0.5.40
Downloaded prodash v26.2.2
Downloaded toml v0.8.10
Downloaded path-absolutize v3.1.1
Downloaded number_prefix v0.4.0
Downloaded liquid-lib v0.26.4
Downloaded faster-hex v0.9.0
Downloaded doc-comment v0.3.3
Downloaded gix-validate v0.8.3
Downloaded gix-object v0.39.0
Downloaded const-random-macro v0.1.16
Downloaded ucd-trie v0.1.6
Downloaded names v0.14.0
Downloaded globset v0.4.14
Downloaded cvt v0.1.2
Downloaded anymap2 v0.13.0
Downloaded clap_derive v4.4.7
Downloaded gix-sec v0.10.4
Downloaded const-random v0.1.17
Downloaded gix-config-value v0.14.4
Downloaded gix-config v0.32.1
Downloaded gix-tempfile v11.0.1
Downloaded gix-date v0.8.3
Downloaded kstring v2.0.0
Downloaded autocfg v1.1.0
Downloaded bstr v1.9.0
Downloaded rand v0.8.5
Downloaded dirs v5.0.1
Downloaded rand_chacha v0.3.1
Downloaded env_logger v0.10.2
Downloaded rand_core v0.6.4
Downloaded crossbeam-utils v0.8.19
Downloaded crossbeam-deque v0.8.5
Downloaded btoi v0.4.3
Downloaded equivalent v1.0.1
Downloaded bitflags v1.3.2
Downloaded getrandom v0.2.12
Downloaded ppv-lite86 v0.2.17
Downloaded same-file v1.0.6
Downloaded dirs-sys v0.4.1
Downloaded scopeguard v1.2.0
Downloaded form_urlencoded v1.2.1
Downloaded gix-utils v0.1.9
Downloaded gix-trace v0.1.7
Downloaded digest v0.10.7
Downloaded termcolor v1.4.1
Downloaded smallvec v1.13.1
Downloaded sha2 v0.10.8
Downloaded fastrand v2.0.1
Downloaded tempfile v3.8.1
Downloaded regex-syntax v0.8.2
Downloaded home v0.5.9
Downloaded tiny-keccak v2.0.2
Downloaded static_assertions v1.1.0
Downloaded lock_api v0.4.11
Downloaded num-traits v0.2.18
Downloaded tinyvec_macros v0.1.1
Downloaded toml_datetime v0.6.5
Downloaded zeroize v1.7.0
Downloaded version_check v0.9.4
Downloaded pkg-config v0.3.30
Downloaded paste v1.0.14
Downloaded ignore v0.4.22
Downloaded cpufeatures v0.2.12
Downloaded pest_generator v2.7.7
Downloaded either v1.10.0
Downloaded block-buffer v0.10.4
Downloaded tinyvec v1.6.0
Downloaded humantime v2.1.0
Downloaded errno v0.3.8
Downloaded openssl-sys v0.9.101
Downloaded pest_meta v2.7.7
Downloaded unicode-bidi v0.3.15
Downloaded parking_lot v0.12.1
Downloaded ahash v0.8.9
Downloaded crossbeam-epoch v0.9.18
Downloaded memchr v2.7.1
Downloaded regex v1.10.3
Downloaded indexmap v2.2.3
Downloaded toml_edit v0.22.6
Downloaded url v2.5.0
Downloaded itertools v0.10.5
Downloaded unicode-segmentation v1.11.0
Downloaded hashbrown v0.14.3
Downloaded unicode-normalization v0.1.23
Downloaded gix-fs v0.8.1
Downloaded zerocopy v0.7.32
Downloaded winnow v0.6.2
Downloaded rustix v0.38.31
Downloaded aho-corasick v1.1.2
Downloaded sanitize-filename v0.5.0
Downloaded vcpkg v0.2.15
Downloaded idna v0.5.0
Downloaded path-dedot v3.1.1
Downloaded dialoguer v0.11.0
Downloaded nix v0.26.4
Downloaded gix-glob v0.14.1
Downloaded git2 v0.18.2
Downloaded regex-automata v0.4.5
Downloaded gix-lock v11.0.1
Downloaded gix-features v0.36.1
Downloaded gix-ref v0.39.1
Downloaded clap v4.4.18
Downloaded normpath v1.2.0
Downloaded serde_spanned v0.6.5
Downloaded rhai_codegen v1.6.0
Downloaded liquid-derive v0.26.4
Downloaded gix-actor v0.28.1
Downloaded fs-err v2.11.0
Downloaded remove_dir_all v0.8.2
Downloaded linux-raw-sys v0.4.13
Downloaded libgit2-sys v0.16.2+1.7.2
Downloaded auth-git2 v0.5.3
Downloaded liquid-core v0.26.4
Downloaded indicatif v0.17.8
Downloaded libz-sys v1.1.15
Downloaded clap_builder v4.4.18
Downloaded typenum v1.17.0
Downloaded unicode-bom v2.0.3
Downloaded parking_lot_core v0.9.9
Downloaded openssl-probe v0.1.5
Downloaded is-terminal v0.4.12
Downloaded generic-array v0.14.7
Downloaded percent-encoding v2.3.1
Downloaded option-ext v0.2.0
Downloaded rhai v1.16.3
Downloaded unicode-width v0.1.11
Downloaded portable-atomic v1.6.0
Downloaded pest_derive v2.7.7
Downloaded memmap2 v0.9.4
Downloaded pest v2.7.7
Downloaded libssh2-sys v0.3.0
Downloaded 146 crates (15.7 MB) in 1.58s (largest was `libz-sys` at 4.0 MB)
Compiling proc-macro2 v1.0.78
Compiling libc v0.2.153
Compiling unicode-ident v1.0.12
Compiling thiserror v1.0.57
Compiling autocfg v1.1.0
Compiling memchr v2.7.1
Compiling serde v1.0.197
Compiling cfg-if v1.0.0
Compiling regex-syntax v0.8.2
Compiling pkg-config v0.3.30
Compiling bitflags v2.4.2
Compiling once_cell v1.19.0
Compiling vcpkg v0.2.15
Compiling tinyvec_macros v0.1.1
Compiling same-file v1.0.6
Compiling num-conv v0.1.0
Compiling gix-trace v0.1.7
Compiling powerfmt v0.2.0
Compiling time-core v0.1.2
Compiling itoa v1.0.10
Compiling num_threads v0.1.7
Compiling tinyvec v1.6.0
Compiling rustix v0.38.31
Compiling walkdir v2.4.0
Compiling sha1_smol v1.0.0
Compiling prodash v26.2.2
Compiling smallvec v1.13.1
Compiling deranged v0.3.11
Compiling crunchy v0.2.2
Compiling time-macros v0.2.17
Compiling ucd-trie v0.1.6
Compiling version_check v0.9.4
Compiling parking_lot_core v0.9.9
Compiling linux-raw-sys v0.4.13
Compiling fastrand v2.0.1
Compiling num-traits v0.2.18
Compiling lock_api v0.4.11
Compiling percent-encoding v2.3.1
Compiling tiny-keccak v2.0.2
Compiling scopeguard v1.2.0
Compiling static_assertions v1.1.0
Compiling home v0.5.9
Compiling crossbeam-utils v0.8.19
Compiling lazy_static v1.4.0
Compiling log v0.4.20
Compiling aho-corasick v1.1.2
Compiling winnow v0.5.40
Compiling either v1.10.0
Compiling unicode-bidi v0.3.15
Compiling smartstring v1.0.1
Compiling itertools v0.10.5
Compiling ahash v0.8.9
Compiling form_urlencoded v1.2.1
Compiling equivalent v1.0.1
Compiling hashbrown v0.14.3
Compiling bitflags v1.3.2
Compiling quote v1.0.35
Compiling anymap2 v0.13.0
Compiling unicode-width v0.1.11
Compiling doc-comment v0.3.3
Compiling ppv-lite86 v0.2.17
Compiling unicode-normalization v0.1.23
Compiling syn v2.0.50
Compiling portable-atomic v1.6.0
Compiling option-ext v0.2.0
Compiling cc v1.0.86
Compiling getrandom v0.2.12
Compiling fs-err v2.11.0
Compiling cvt v0.1.2
Compiling unicode-segmentation v1.11.0
Compiling names v0.14.0
Compiling clap_lex v0.6.0
Compiling const-random-macro v0.1.16
Compiling heck v0.4.1
Compiling anyhow v1.0.80
Compiling paste v1.0.14
Compiling winnow v0.6.2
Compiling anstyle v1.0.6
Compiling idna v0.5.0
Compiling gix-utils v0.1.9
Compiling crossbeam-epoch v0.9.18
Compiling zerocopy v0.7.32
Compiling openssl-probe v0.1.5
Compiling semver v1.0.22
Compiling clap_builder v4.4.18
Compiling path-dedot v3.1.1
Compiling number_prefix v0.4.0
Compiling const-random v0.1.17
Compiling zeroize v1.7.0
Compiling humantime v2.1.0
Compiling normpath v1.2.0
Compiling unicode-bom v2.0.3
Compiling btoi v0.4.3
Compiling nix v0.26.4
Compiling time v0.3.34
Compiling dirs-sys v0.4.1
Compiling console v0.15.8
Compiling memmap2 v0.9.4
Compiling rand_core v0.6.4
Compiling is-terminal v0.4.12
Compiling gix-sec v0.10.4
Compiling terminal-prompt v0.2.3
Compiling dirs v5.0.1
Compiling crossbeam-deque v0.8.5
Compiling parking_lot v0.12.1
Compiling rand_chacha v0.3.1
Compiling url v2.5.0
Compiling termcolor v1.4.1
Compiling shell-words v1.1.0
Compiling path-absolutize v3.1.1
Compiling regex-automata v0.4.5
Compiling indicatif v0.17.8
Compiling rand v0.8.5
Compiling tempfile v3.8.1
Compiling fs_at v0.1.10
Compiling openssl-sys v0.9.101
Compiling libz-sys v1.1.15
Compiling libssh2-sys v0.3.0
Compiling libgit2-sys v0.16.2+1.7.2
Compiling remove_dir_all v0.8.2
Compiling thiserror-impl v1.0.57
Compiling serde_derive v1.0.197
Compiling liquid-derive v0.26.4
Compiling clap_derive v4.4.7
Compiling rhai_codegen v1.6.0
Compiling bstr v1.9.0
Compiling regex v1.10.3
Compiling env_logger v0.10.2
Compiling sanitize-filename v0.5.0
Compiling globset v0.4.14
Compiling gix-date v0.8.3
Compiling gix-path v0.10.5
Compiling pest v2.7.7
Compiling gix-validate v0.8.3
Compiling dialoguer v0.11.0
Compiling rhai v1.16.3
Compiling gix-actor v0.28.1
Compiling gix-config-value v0.14.4
Compiling ignore v0.4.22
Compiling clap v4.4.18
Compiling pest_meta v2.7.7
Compiling pest_generator v2.7.7
Compiling pest_derive v2.7.7
Compiling faster-hex v0.9.0
Compiling kstring v2.0.0
Compiling indexmap v2.2.3
Compiling gix-hash v0.13.3
Compiling serde_spanned v0.6.5
Compiling toml_datetime v0.6.5
Compiling liquid-core v0.26.4
Compiling gix-features v0.36.1
Compiling gix-fs v0.8.1
Compiling gix-object v0.39.0
Compiling gix-glob v0.14.1
Compiling gix-tempfile v11.0.1
Compiling toml_edit v0.22.6
Compiling gix-lock v11.0.1
Compiling gix-ref v0.39.1
Compiling liquid-lib v0.26.4
Compiling gix-config v0.32.1
Compiling toml v0.8.10
Compiling git2 v0.18.2
Compiling liquid v0.26.4
Compiling auth-git2 v0.5.3
Compiling cargo-generate v0.19.0
Finished release [optimized] target(s) in 1m 05s
Installing /home/koutarou/.cargo/bin/cargo-generate
Installed package `cargo-generate v0.19.0` (executable `cargo-generate`)
最後にbpftoolを入れます。 bpftoolというパッケージは存在しないようなので、提案されたパッケージ候補のうち適当なものを入れます。
[master] koutarou@koutarou-desktop/INS> sudo apt install bpftool
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています... 完了
状態情報を読み取っています... 完了
パッケージ bpftool は、以下によって提供される仮想パッケージです:
linux-nvidia-6.2-tools-common 6.2.0-1003.3~22.04.1
linux-lowlatency-hwe-6.5-tools-common 6.5.0-17.17.1.1.1~22.04.1
linux-hwe-6.5-tools-common 6.5.0-18.18~22.04.1
linux-hwe-6.2-tools-common 6.2.0-39.40~22.04.1
インストールするには、明示的にいずれかを選択する必要があります。
E: パッケージ 'bpftool' にはインストール候補がありません
[master] koutarou@koutarou-desktop/INS> sudo apt install linux-hwe-6.5-tools-common
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています... 完了
状態情報を読み取っています... 完了
以下のパッケージが自動でインストールされましたが、もう必要とされていません:
golang-1.18 golang-1.18-doc golang-1.18-go golang-1.18-src golang-doc golang-go golang-src
これを削除するには 'sudo apt autoremove' を利用してください。
以下のパッケージが新たにインストールされます:
linux-hwe-6.5-tools-common
アップグレード: 0 個、新規インストール: 1 個、削除: 0 個、保留: 3 個。
39.9 kB のアーカイブを取得する必要があります。
この操作後に追加で 373 kB のディスク容量が消費されます。
[Fetching Title...](取得:1 http://ftp.jaist.ac.jp/pub/Linux/ubuntu jammy-updates/main amd64 linux-hwe-6.5-tools-common all 6.5.0-18.18~22.04.1 [39.9 kB])
39.9 kB を 20秒 で取得しました (1,966 B/s)
以前に未選択のパッケージ linux-hwe-6.5-tools-common を選択しています。
(データベースを読み込んでいます ... 現在 568191 個のファイルとディレクトリがインストールされています。)
.../linux-hwe-6.5-tools-common_6.5.0-18.18~22.04.1_all.deb を展開する準備をしています ...
linux-hwe-6.5-tools-common (6.5.0-18.18~22.04.1) を展開しています...
linux-hwe-6.5-tools-common (6.5.0-18.18~22.04.1) を設定しています ...
v5.15.136というバージョンのbpftoolが入りました。
[master] koutarou@koutarou-desktop/INS> bpftool --version
/usr/lib/linux-tools/5.15.0-94-generic/bpftool v5.15.136
features:
サンプルプロジェクトの作成・実行
cargo-generateを用いてAya公式が用意しているテンプレートプログラムを作成します。 プロジェクトの名前とeBPFプログラムのタイプ(どの種類のイベントにアタッチするか)を選びます。 ここではデフォルトになっているXDPというタイプを選択します。 このタイプでは、ネットワークパケットを扱うことができます。
koutarou@koutarou-desktop/INS> cargo generate https://github.com/aya-rs/aya-template
⚠️ Favorite `https://github.com/aya-rs/aya-template` not found in config, using it as a git repository: https://github.com/aya-rs/aya-template
🤷 Project Name: ebpf-xdp-hello
🔧 Destination: /home/koutarou/develop/rust-learn/ebpf-xdp-hello ...
🔧 project-name: ebpf-xdp-hello ...
🔧 Generating template ...
✔ 🤷 Which type of eBPF program? · xdp
🔧 Moving generated files into: `/home/koutarou/develop/rust-learn/ebpf-xdp-hello`...
🔧 Initializing a fresh Git repository
✨ Done! New project created /home/koutarou/develop/rust-learn/ebpf-xdp-hello
ディレクトリ構造は以下のようになっています。 色々ありますが、重要なのは
- ebpf-xdp-hello/src/main.rs:ユーザ側プログラム
- ebpf-xdp-hello-ebpf/src/main.rs:eBPFプログラム
の2つです。
[main] koutarou@koutarou-desktop/INS> tree
.
├── Cargo.toml
├── README.md
├── ebpf-xdp-hello
│ ├── Cargo.toml
│ └── src
│ └── main.rs
├── ebpf-xdp-hello-common
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── ebpf-xdp-hello-ebpf
│ ├── Cargo.toml
│ ├── rust-toolchain.toml
│ └── src
│ └── main.rs
└── xtask
├── Cargo.toml
└── src
├── build_ebpf.rs
├── main.rs
└── run.rs
8 directories, 13 files
中身を見る前に挙動を確認してみましょう。
[main] koutarou@koutarou-desktop/INS> RUST_LOG=info cargo xtask run -- -i br0
warning: virtual workspace defaulting to `resolver = "1"` despite one or more workspace members being on edition 2021 which implies `resolver = "2"`
note: to keep the current resolver, specify `workspace.resolver = "1"` in the workspace root's manifest
note: to use the edition 2021 resolver, specify `workspace.resolver = "2"` in the workspace root's manifest
note: for more details see https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions
Compiling proc-macro2 v1.0.78
Compiling unicode-ident v1.0.12
Compiling utf8parse v0.2.1
Compiling anstyle-query v1.0.2
Compiling colorchoice v1.0.0
Compiling anstyle v1.0.6
Compiling clap_lex v0.7.0
Compiling heck v0.4.1
Compiling anyhow v1.0.80
Compiling strsim v0.11.0
Compiling anstyle-parse v0.2.3
Compiling anstream v0.6.12
Compiling clap_builder v4.5.1
Compiling quote v1.0.35
Compiling syn v2.0.50
Compiling clap_derive v4.5.0
Compiling clap v4.5.1
Compiling xtask v0.1.0 (/home/koutarou/develop/rust-learn/ebpf-xdp-hello/xtask)
Finished dev [unoptimized + debuginfo] target(s) in 2.03s
Running `target/debug/xtask run -- -i br0`
Compiling proc-macro2 v1.0.78
Compiling unicode-ident v1.0.12
Compiling compiler_builtins v0.1.108
Compiling version_check v0.9.4
Compiling core v0.0.0 (/home/koutarou/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core)
Compiling rustversion v1.0.14
Compiling aya-bpf-cty v0.2.1 (https://github.com/aya-rs/aya#417679be)
Compiling aya-bpf-bindings v0.1.0 (https://github.com/aya-rs/aya#417679be)
Compiling proc-macro-error-attr v1.0.4
Compiling proc-macro-error v1.0.4
Compiling quote v1.0.35
Compiling aya-bpf v0.1.0 (https://github.com/aya-rs/aya#417679be)
Compiling syn v2.0.50
Compiling num_enum_derive v0.7.2
Compiling aya-bpf-macros v0.1.0 (https://github.com/aya-rs/aya#417679be)
Compiling num_enum v0.7.2
Compiling aya-log-common v0.1.13 (https://github.com/aya-rs/aya#417679be)
Compiling aya-log-parser v0.1.11-dev.0 (https://github.com/aya-rs/aya#417679be)
Compiling aya-log-ebpf-macros v0.1.0 (https://github.com/aya-rs/aya#417679be)
Compiling rustc-std-workspace-core v1.99.0 (/home/koutarou/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/rustc-std-workspace-core)
Compiling ebpf-xdp-hello-common v0.1.0 (/home/koutarou/develop/rust-learn/ebpf-xdp-hello/ebpf-xdp-hello-common)
Compiling aya-log-ebpf v0.1.0 (https://github.com/aya-rs/aya#417679be))
Compiling ebpf-xdp-hello-ebpf v0.1.0 (/home/koutarou/develop/rust-learn/ebpf-xdp-hello/ebpf-xdp-hello-ebpf)
Finished `dev` profile [optimized] target(s) in 3.77s
warning: virtual workspace defaulting to `resolver = "1"` despite one or more workspace members being on edition 2021 which implies `resolver = "2"`
note: to keep the current resolver, specify `workspace.resolver = "1"` in the workspace root's manifest
note: to use the edition 2021 resolver, specify `workspace.resolver = "2"` in the workspace root's manifest
note: for more details see https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions
Compiling libc v0.2.153
Compiling version_check v0.9.4
Compiling memchr v2.7.1
Compiling thiserror v1.0.57
Compiling cfg-if v1.0.0
Compiling once_cell v1.19.0
Compiling zerocopy v0.7.32
Compiling thiserror-impl v1.0.57
Compiling log v0.4.20
Compiling allocator-api2 v0.2.16
Compiling tokio-macros v2.2.0
Compiling pin-project-lite v0.2.13
Compiling bytes v1.5.0
Compiling num_enum_derive v0.7.2
Compiling lazy_static v1.4.0
Compiling bitflags v2.4.2
Compiling assert_matches v1.5.0
Compiling regex-syntax v0.8.2
Compiling humantime v2.1.0
Compiling termcolor v1.4.1
Compiling ahash v0.8.9
Compiling core-error v0.0.0
Compiling object v0.32.2
Compiling aho-corasick v1.1.2
Compiling hashbrown v0.14.3
Compiling num_enum v0.7.2
Compiling aya-log-common v0.1.13 (https://github.com/aya-rs/aya#417679be)
Compiling socket2 v0.5.6
Compiling num_cpus v1.16.0
Compiling mio v0.8.10
Compiling signal-hook-registry v1.4.1
Compiling is-terminal v0.4.12
Compiling tokio v1.36.0
Compiling regex-automata v0.4.5
Compiling aya-obj v0.1.0 (https://github.com/aya-rs/aya#417679be)
Compiling regex v1.10.3
Compiling env_logger v0.10.2
Compiling aya v0.11.0 (https://github.com/aya-rs/aya#417679be)
Compiling aya-log v0.1.13 (https://github.com/aya-rs/aya#417679be)
Compiling ebpf-xdp-hello-common v0.1.0 (/home/koutarou/develop/rust-learn/ebpf-xdp-hello/ebpf-xdp-hello-common)
Compiling ebpf-xdp-hello v0.1.0 (/home/koutarou/develop/rust-learn/ebpf-xdp-hello/ebpf-xdp-hello)
Finished dev [unoptimized + debuginfo] target(s) in 3.48s
[2024-02-25T04:38:02Z INFO ebpf_xdp_hello] Waiting for Ctrl-C...
[2024-02-25T04:38:02Z INFO ebpf_xdp_hello] received a packet
[2024-02-25T04:38:03Z INFO ebpf_xdp_hello] received a packet
[2024-02-25T04:38:03Z INFO ebpf_xdp_hello] received a packet
[2024-02-25T04:38:03Z INFO ebpf_xdp_hello] received a packet
[2024-02-25T04:38:04Z INFO ebpf_xdp_hello] received a packet
[2024-02-25T04:38:04Z INFO ebpf_xdp_hello] received a packet
[2024-02-25T04:38:04Z INFO ebpf_xdp_hello] received a packet
[2024-02-25T04:38:04Z INFO ebpf_xdp_hello] received a packet
[2024-02-25T04:38:04Z INFO ebpf_xdp_hello] received a packet
[2024-02-25T04:38:04Z INFO ebpf_xdp_hello] received a packet
[2024-02-25T04:38:05Z INFO ebpf_xdp_hello] received a packet
^C[2024-02-25T04:38:05Z INFO ebpf_xdp_hello] Exiting...
eBPFプログラムのビルド・ユーザプログラムのビルドがされた後、received a packetという文字列がそれなりの頻度で出てきます。
出力を見るに、パケットを受信したらそれを通知しているプログラムのようです。 いずれにしても、AyaでHello Worldをすることに成功しました。
Hello Worldプログラムの中身
ユーザ側プログラム
ユーザ側プログラムは以下のようになっています。 細かいところは公式ドキュメントに詳しく解説が載っているのでここでは要点だけを解説します。
use anyhow::Context;
use aya::programs::{Xdp, XdpFlags};
use aya::{include_bytes_aligned, Bpf};
use aya_log::BpfLogger;
use clap::Parser;
use log::{info, warn, debug};
use tokio::signal;
#[derive(Debug, Parser)]
struct Opt {
#[clap(short, long, default_value = "eth0")]
iface: String,
}
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let opt = Opt::parse();
env_logger::init();
// Bump the memlock rlimit. This is needed for older kernels that don't use the
// new memcg based accounting, see https://lwn.net/Articles/837122/
let rlim = libc::rlimit {
rlim_cur: libc::RLIM_INFINITY,
rlim_max: libc::RLIM_INFINITY,
};
let ret = unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlim) };
if ret != 0 {
debug!("remove limit on locked memory failed, ret is: {}", ret);
}
// This will include your eBPF object file as raw bytes at compile-time and load it at
// runtime. This approach is recommended for most real-world use cases. If you would
// like to specify the eBPF program at runtime rather than at compile-time, you can
// reach for `Bpf::load_file` instead.
#[cfg(debug_assertions)]
let mut bpf = Bpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/debug/ebpf-xdp-hello"
))?;
#[cfg(not(debug_assertions))]
let mut bpf = Bpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/release/ebpf-xdp-hello"
))?;
if let Err(e) = BpfLogger::init(&mut bpf) {
// This can happen if you remove all log statements from your eBPF program.
warn!("failed to initialize eBPF logger: {}", e);
}
let program: &mut Xdp = bpf.program_mut("ebpf_xdp_hello").unwrap().try_into()?;
program.load()?;
program.attach(&opt.iface, XdpFlags::default())
.context("failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE")?;
info!("Waiting for Ctrl-C...");
signal::ctrl_c().await?;
info!("Exiting...");
Ok(())
}
まず、この部分ではeBPFプログラムの実行ファイルをメモリ内にロードしています。
let mut bpf = Bpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/debug/ebpf-xdp-hello"
))?;
次にこの部分で、eBPFプログラムを操作(カーネルへのロードやアタッチ)するためのハンドラを取得します。 program_mutメソッドの引数に与えているのは、eBPFプログラムの名前となっています(後述)。
let program: &mut Xdp = bpf.program_mut("ebpf_xdp_hello").unwrap().try_into()?;
最後にこの部分でeBPFプログラムをカーネルにロードし、指定したタイプのイベント(今回の例ではネットワークパケットを扱うXDP)にアタッチしています。
program.load()?;
program.attach(&opt.iface, XdpFlags::default())
.context("failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE")?;
他にも色々処理をしていますが、eBPFの基本的な部分は上記でカバーされており、ユーザプログラムはeBPFプログラムをカーネルにロードして適切にアタッチする、ということを行っているに過ぎません。
eBPFプログラム
次にeBPFプログラムを見てみましょう。
#![no_std]
#![no_main]
use aya_bpf::{bindings::xdp_action, macros::xdp, programs::XdpContext};
use aya_log_ebpf::info;
#[xdp]
pub fn ebpf_xdp_hello(ctx: XdpContext) -> u32 {
match try_ebpf_xdp_hello(ctx) {
Ok(ret) => ret,
Err(_) => xdp_action::XDP_ABORTED,
}
}
fn try_ebpf_xdp_hello(ctx: XdpContext) -> Result<u32, u32> {
info!(&ctx, "received a packet");
Ok(xdp_action::XDP_PASS)
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}
まず、xdpマクロで関数がeBPFプログラムであることを示します。 この関数名が、ユーザプログラムで指定したeBPFプログラム名と一致していることがわかると思います。 また、引数のXdpContextは、カーネルがeBPFプログラムを呼び出すときに渡してくれる値で、アタッチされるタイプごとに異なります。 また、返り値の取り扱いもタイプごとに異なりますが、XDPの場合、そのパケットの取り扱い(通すのかドロップするのかなど)の判定結果として使われます。 この関数では、try_ebpf_xdp_helloという関数でエラーとなった場合パケットをドロップするということになっています。
#[xdp]
pub fn ebpf_xdp_hello(ctx: XdpContext) -> u32 {
match try_ebpf_xdp_hello(ctx) {
Ok(ret) => ret,
Err(_) => xdp_action::XDP_ABORTED,
}
}
呼び出し先の関数では、出力に出ていたメッセージを出力した後パケットを素通しする返り値を返しています。 つまり、このプログラムでは、すべてのパケットは素通しされます。
fn try_ebpf_xdp_hello(ctx: XdpContext) -> Result<u32, u32> {
info!(&ctx, "received a packet");
Ok(xdp_action::XDP_PASS)
}
なお、メッセージの出力をする際にeBPFプログラムのログをユーザプログラム側で普通に受け取っていますが、これはAyaの機能によるもので、eBPFの仕組みを直接使っているわけではありません(多分)。 データのやり取りを行う際には、Mapを明示的に利用する必要があります。
Ayaによるシステムコール監視プログラム
サンプルプログラムでHello Worldしてみたところで、実際に自分でなにかプログラムを書いてみましょう。 といってもいきなりすごいものはかけないので、本での題材となっているプログラムを移植してみる形でお茶を濁します。
第二章ではexecveシステムコールをフックとして、呼び出したpidとuid・コマンド名・適当な文字列をPerfリングバッファを介してユーザプログラムに送り、ユーザプログラムはそれを表示するというプログラムを題材としており、今回はこれを移植してみます。
本では、カーネルコードの関数といったシンボルや任意のコードにアタッチできるkprobeというタイプのアタッチポイントを使っていましたが、代わりに予め決められたフックにアタッチするtracepointというタイプのアタッチポイントを使っていきます。
tracepointについて
tracepointでフックできるアタッチポイントは、/sys/kernel/tracing/events配下にその一覧があります。 今回は、execveシステムコールが呼び出されたときにeBPFプログラムを発火させたいので、syscallsカテゴリのsys_enter_execveというアタッチポイントを選びます。
このディレクトリの各アタッチポイントのディレクトリには、eBPFプログラムに渡される引数のフォーマットが指定されているので、これを見つつプログラムを書いていきます。 例えば、今回選んだsys_enter_execveのフォーマットは以下のようになっています。
[main] koutarou@koutarou-desktop/INS> sudo cat /sys/kernel/tracing/events/syscalls/sys_enter_execve/format
name: sys_enter_execve
ID: 721
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:int __syscall_nr; offset:8; size:4; signed:1;
field:const char * filename; offset:16; size:8; signed:0;
field:const char *const * argv; offset:24; size:8; signed:0;
field:const char *const * envp; offset:32; size:8; signed:0;
print fmt: "filename: 0x%08lx, argv: 0x%08lx, envp: 0x%08lx", ((unsigned long)(REC->filename)), ((unsigned long)(REC->argv)), ((unsigned long)(REC->envp))
commonと書かれているフィールドはアタッチポイント共通ですが、その下のフィールドは各アタッチポイント毎に異なります。 例えばfilenameフィールドはexecveシステムコールで実行するファイルの名前へのポインタとなっています。
参考
- Using the Linux Kernel Tracepoints; The Linux Kernel documentation
- Event Tracing; The Linux Kernel documentation
プログラム解説
サンプルプロジェクトと同様にcargo generateを用いて雛形を作ります。 このとき、XDPではなくTracePointを選択することでtracepointタイプのプログラムの雛形が作られます。
そのためツリー構造はサンプルと同じです。
[main] koutarou@koutarou-desktop/INS> tree
.
├── Cargo.lock
├── Cargo.toml
├── README.md
├── buffer
│ ├── Cargo.toml
│ └── src
│ └── main.rs
├── buffer-common
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── buffer-ebpf
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── rust-toolchain.toml
│ └── src
│ └── main.rs
└── xtask
├── Cargo.toml
└── src
├── build_ebpf.rs
├── main.rs
└── run.rs
8 directories, 15 files
変更するプログラムは、
- buffer-common/src/main.rs:ユーザプログラムとeBPFプログラムの両方で共通して用いる型定義
- buffer/src/main.rs:ユーザプログラム
- buffer-ebpf/src/main.rs:eBPFプログラム の3つです。
共通の型定義
両方のプログラム間をリングバッファMapを介してデータのやり取りを行うため、そのデータ型の定義をします。
ユーザプログラム側で、Mapから受け取ったポインタを無理やり型にキャストするために、メモリレイアウトをCにしてあります。
フィールドはpid・uid・コマンド名・メッセージの4つで、コマンド・メッセージについては文字列を扱うために固定長配列としています。
#![no_std]
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct Log {
pub pid: u32,
pub uid: u32,
pub command: [u8; 16],
pub message: [u8; 24],
}
eBPFプログラム
Aya側で用意されているヘルパー関数を使って必要なフィールドの値を読み取り、それをリングバッファMapに入れるということをしています。
#![no_std]
#![no_main]
use aya_bpf::{
helpers::bpf_get_current_comm,
macros::{map, tracepoint},
maps::RingBuf,
programs::TracePointContext,
BpfContext,
};
use buffer_common::Log;
#[map(name = "LOG")]
static mut LOG: RingBuf = RingBuf::with_byte_size(1024, 0);
#[tracepoint]
pub fn buffer(ctx: TracePointContext) -> u32 {
match try_buffer(ctx) {
Ok(ret) => ret,
Err(ret) => ret,
}
}
fn slice_to_array<const N: usize>(s: &[u8]) -> [u8; N] {
let mut a: [u8; N] = [0; N];
for i in 0..N {
if i < s.len() {
a[i] = s[i];
} else {
a[i] = 0;
}
}
a
}
fn try_buffer(ctx: TracePointContext) -> Result<u32, u32> {
let pid = ctx.pid();
let uid = ctx.uid();
let command = bpf_get_current_comm().unwrap();
let message = "Hello world!";
let log = Log {
pid,
uid,
command,
message: slice_to_array(message.as_bytes()),
};
unsafe { LOG.output::<Log>(&log, 0).unwrap() };
Ok(0)
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}
ユーザープログラム
適切なアタッチポイントにプログラムをアタッチし、リングバッファMapから値を読み取る無限ループとなっています。 読み取った値を無理やりポインタ操作でキャストしているのが少し汚いです(もっといい方法がありそうなものだが…)。
use aya::maps::RingBuf;
use aya::programs::TracePoint;
use aya::{include_bytes_aligned, Bpf};
use log::{debug, info};
use tokio::signal;
use buffer_common::Log;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
//env_logger::init();
// Bump the memlock rlimit. This is needed for older kernels that don't use the
// new memcg based accounting, see https://lwn.net/Articles/837122/
let rlim = libc::rlimit {
rlim_cur: libc::RLIM_INFINITY,
rlim_max: libc::RLIM_INFINITY,
};
let ret = unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlim) };
if ret != 0 {
debug!("remove limit on locked memory failed, ret is: {}", ret);
}
// This will include your eBPF object file as raw bytes at compile-time and load it at
// runtime. This approach is recommended for most real-world use cases. If you would
// like to specify the eBPF program at runtime rather than at compile-time, you can
// reach for `Bpf::load_file` instead.
#[cfg(debug_assertions)]
let mut bpf = Bpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/debug/buffer"
))?;
#[cfg(not(debug_assertions))]
let mut bpf = Bpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/release/buffer"
))?;
let program: &mut TracePoint = bpf.program_mut("buffer").unwrap().try_into()?;
program.load()?;
program.attach("syscalls", "sys_enter_execve")?;
let mut ring_buffer_log = RingBuf::try_from(bpf.map_mut("LOG").unwrap())?;
loop {
if let Some(entry) = ring_buffer_log.next() {
let ptr = &*entry;
let log: Log = unsafe { std::ptr::read(ptr.as_ptr() as *const _) };
let command = std::str::from_utf8(&log.command).unwrap();
let message = std::str::from_utf8(&log.message).unwrap();
println!("{} {} {}", log.pid, command, message);
}
}
Ok(())
}
挙動
裏で静的サイトジェネレータのAstroやそれを動かすnpmが動いていることがわかります。
[main] koutarou@koutarou-desktop/INS> cargo xtask run
...
65106 conky Hello world!
65108 sh Hello world!
65107 sh Hello world!
65109 sh Hello world!
65112 zsh Hello world!
65112 zsh Hello world!
65112 npm Hello world!
65112 npm Hello world!
65112 npm Hello world!
65112 npm Hello world!
65112 npm Hello world!
65112 npm Hello world!
65112 npm Hello world!
65112 npm Hello world!
65112 npm Hello world!
65112 npm Hello world!
65112 npm Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65123 npm run dev Hello world!
65124 sh Hello world!
65124 astro Hello world!
まとめ
この記事では、Ayaを用いてeBPFの簡単なコードを書いてみました。 MapやAya自体の使い方などまだ慣れていない部分も多いですが、わかってしまえばそこまで難しいものでもないという印象を受けました。
さらに本を読み進めていきつつ、より高度なプログラムを書いていきたいです。 また、eBPFを取り巻くカーネル機能の勉強もしてみてもいいかもしれませんね。
Footnotes
-
サンプルコードそのものはGitHub - lizrice/learning-ebpfに上がっています。 ↩
-
本の中では、もともとはBSD Packet Filterの略であったとされています。ただ今ではBerkleyであると言われているとも書かれています。 ↩