最近は秋風を感じる日もあって夏も終わっちゃうなぁとしみじみしていますいかがお過ごしでしょうか。
宣伝ですが、最近は就活も終わり @gtpv2 という垢で linux netdevを見てキャッキャッするだけのツイートをしています。ぜひそっちも仲良くしてください。
最近 Twitter 上で猫も杓子も XDP!XDP!XDP! と流行ってきているのをとても感じています。以前は一部のキレッキレの企業エンジニアが書いてたと思ったら学生の研究になってたり。
私としましても、最近P4Runtime 経由で ARP のやりとりを kernel に無理やり移譲するみたいな実装を書いたりしているとやはり kernel様 の加護を得ているパケット処理フレームワークが世の中には求められているなぁとしみじみ思います。世はまさに大XDP時代ですね。
しかしながら、XDP を使うと eBPF 特有の書きにくさ、デバッグのし辛さがあり俺はパケットをいい感じに書き換えたいだけなのになんでこんな制約だらけのマゾみたいなコード書いてるんだろうとキレそうになるのでパケット処理王になるための道は非常に遠いと思います。
さて、今回はそれに対する処方箋として XDP のプログラムをデバッグをする方法をメモとしてまとめます。 昨年書いた記事(今日から始めるXDPと取り巻く環境について)のデバッグ話を補完するような感じでしょうか。XDPわからない太郎な人はこれを読んでからみるのを勧めます。そこの玄人様は退屈だと思うのでブラウザバックでお願いします〜
なお、結論から書いておきますが高速パケット処理話は基本的に気合で直す以外の選択肢はないので、そもそも速度いらないならAFなんちゃらなソケットから処理してもらうのが良さそうです...
アタッチが失敗する
意味通りアタッチが失敗してなんか知らないけど、うまく動かないときの話
大体こういうのは eBPFVerifier にブチギレられてることが多いと思います
llvm-objdump -S -no-show-raw-insn hoge.o
とりあえずこれでアセンブラを眺めてみる。
clangでコンパイルするときに -g を付けていればバイトコードに対応したソースコードも表示されるんでとりあえずコンパイルするときはつけておくと良い。
アセンブラの見方は割と気合でやるしかないんですが、別にアセンブラが読めなくても、死んでいる箇所とコードの対応がわかるのでその周辺がダメなのかもとあたりをつけてあげることはできると思います。
ちなみにちょっと初心者ある話としてはこんな風に言われてたらprogの名前が言われて
# ip link set dev ens3 xdpgeneric obj xdp_accept_ssh.o Program section ‘prog’ not found in ELF file!
例えばコードに SEC("accept_ssh_only_main") とか書いていたら以下のようにプログラムのセクションを書いてください。
ip link set dev ens3 xdpgeneric obj xdp_accept_ssh.o sec accept_ssh_only_main
これはディフォルトのセクションネームが違うときは明示的に指定する必要があるという話です
Printデバッグ
主に実行時の条件文に入ったかなどの挙動を見る時に利用します
大体の人が bpf_trace_printk をして実行時の出力を見る感じでしょうか?
自分もそうなんですが、どうやってみてますか?BCCで流れてきた物を見てるだけでしょうか?
自分の場合は trace-cmd を利用し,以下のようなコマンドで動かしてます。
sudo trace-cmd record -e 'xdp:*' -O trace_printk sudo trace-cmd report > trace.log
printk を仕込んで deploy して実行したあと、その当該ホストでこれを一つ目のコマンドを動かして採取しそれを取り出します。 雑に less とかして眺めるのもよいですが、結構いっぱいprintを仕込んだ場合は kernelsharkなどを使ってGUIで眺めておくと目に優しいです。
XDPcap
これはXDPから吐き出しているパケットが、どういう挙動をした結果どういうパケットを吐いているのかを見る時に使えます。
cloudflareが造ってる謹製のツールで、これを使うとXDPがどんな挙動でどういう判断を下したのかをみることができます。具体的にはpacketに XDP(TX|PASS|...) の判断の情報加えわって pcap にdumpできます。
まず事前準備として XDPのプログラムに渡す ヘッターファイルと tcpdumpにあたるコントロールコマンドをfetchします。
wget https://raw.githubusercontent.com/cloudflare/xdpcap/master/hook.h go get -u github.com/cloudflare/xdpcap/cmd/xdpcap
次にxdpが動くノードでマウントをしてない場合はしておくと良い。
sudo mount bpffs /sys/fs/bpf -t bpf
ノリとしては以下のようなコードを描いて(これはpassするだけの簡単なコード)
#include "hook.h" // https://github.com/cloudflare/xdpcap struct bpf_map_def SEC("maps") xdpcap_hook = XDPCAP_HOOK(); int xdp_pass_func(struct xdp_md *ctx) { return xdpcap_exit(ctx, &xdpcap_hook, XDP_PASS); }
このプログラムを起動した状態で icmpだけ取り出したい場合は以下のようなコマンドを動かすことができます
sudo xdpcap /sys/fs/bpf/xdpcap_hook - "icmp" | sudo tcpdump -n -r - sudo xdpcap /sys/fs/bpf/xdpcap_hook dump.pcap "icmp"

eBPF Mapへの書き込み
Printデバッグと同様ですが、ある程度大きいペイロードとして利用する時に使ったり、定数書き換えとかに利用します 。
特にコードぽいものは示しませんが、デバッグ用に適当に構造体を作ってそれをやり取りするだけで便利です。 例えばペイロードごとねじ込むみたいなことをしてあげると全部は見たくないが・・・みたいなニーズに合わせやすかったり、何かしらの挙動のデバッグをする時に適当に定数書き換えをしておくと雑に動きを変えれて良いです。実際XDPのグローバル変数は長さ1のmapとして展開されます。
挙動のテスト
関数の実行の整合性を見る時に利用します。
具体的には bpf_prog_test_run という関数を使ってやってローカルでgeneric ebpfを呼び出してE2Eテストをすることができます。
複雑なプログラムなどを事前に細かい粒度で開発するのにお勧めです。例えばチェックサムだったり、encap/decapの場合は有効でしょう。
コントロール側の雰囲気としてはこんな感じです。
const char *file = "./main.o"; //対象の関数 struct bpf_object *obj; __u8 buf[PACKETSIZE]; int err, prog_fd; __u32 duration, retval, size, repeat=1; struct ipv4_packet pkt_v4; err = bpf_prog_load(file, BPF_PROG_TYPE_XDP, &obj, &prog_fd); if (err) { fprintf(stderr, "ERR: loading eBPF object file (%d): %s\n", err, strerror(-err)); return -1; }; err = bpf_prog_test_run( prog_fd, // file disk repeat, // repeat nums &pkt_v4, // input packet sizeof(pkt_v4), // input packet size buf, // output packet &size, // output packet size &retval, // return bpf type &duration );
こればかり描いても仕方ないのでお気持ちを少し語っておくと、自分はよくGo+XDPで書いてるんですが、残念ながらGo+bpf_prog_test_runはあまり向いてなくてどういうことかというと go test と sudo の相性がダメというところに帰着してきます。具体的にはGOPATH以下がrootの権限になるので次回以降動かす時にビミョい気持ちになります。
なので個人的なお勧めとしてはこういうことが起きない C or BCC(Python) でやると良さげだと思います。
もしCでやるときは気をつけて欲しいこととしてはunpackの挙動のアノテーションを __attribute__((__packed__)); としないとclangは解釈してくれないので(GCCは問題がない)その辺も注意ポイントです。
BCCは良さげなyonazuno先生の日本語記事があるのでそちらをご覧ください。
終わりに
いかがでしたか?(1回言ってみたかった)
この記事はtwitterで XDPわかんないよー! という話が聞こえたのでそれに対するアンサーになればと思い書きました。簡単な資料になってしまいましたが、自分としてはそれだけパケット処理は筋肉が問われる開発なんだなぁと改めて思いました。資料が少し参考になれば幸いです。
他にも最近だと、 bpftool だけではなく xdp-toolsというツールが出てきているんですが、雑に説明しておくと、こんな感じ
- xdp-loader: XDPプログラム用のシンプルなローダー、NICに対してのマルチアタッチに対応してたりする(最新の機能で、テールコールのように複数プログラムをつけることができる。詳しくはLPC2020発表を参照)
- xdp-dump: XDPcapを入れなくてもXDPプログラムに入る前、またはXDPプログラムからの出口でdumpできるツールです。残念ながらexitの時のパラメーターは取れないけど・・・
- xdp-filter: XDPを使ったパケットフィルタしてくれる君
READMEを読むとわかると思うのでそれは興味のある人への宿題として任せます。
このような便利ツールを使うだけではなく、いっその事オレオレ便利ツールを作ってしまうのもデバッグのコツでしょう。 例えば、いくつかのebpfmapが存在しているならばパケット処理以外の理由が関わってる部分があると思います。例えばネクストホップのテーブルやベアラなどがあるでしょう。何かしらの調査の時はそれの中身をdumpするだけのツールをさくっと別に作るだけでも何が正しいのかわかるのでデバッグが捗ります。
また、デバッグとはちょっとかけ離れますが、BTFを利用したバリデーションなどをしておくとパラメーターを正しく入れるので嬉しいと思います(C言語のメタプロ補助データという噂も聞きますがw) まぁそもそもcilium/ebpfなどのアプローチとしてELFからeBPFMapを生成するときなどはBTFから情報を得てsyscallを叩いていますのでこの辺の構成に慣れておくと良いでしょう。筆者も辛い気持ちで読んでいます(げっそり)
(ところで、この辺かなりP4のP4infoみたいになってますよね・・・似てくるのだろうか。)
まぁ最近も eBPF と XDP 周りでアップデートが激しいのでそのうち、その辺も動かしてる所感とか(e.g. CO-REの導入やマルチキャスト周りとか)の駄文を書き散らしたいなと思います。
それにしても...いろいろ 無駄に キャッチしてるのにパケット処理ネタ話しできるような話を今年は発表する場所すらないんだけど、コロナは悲しいなぁ...できる時にやっておくは重要ですね 😭😭😭
では〜