慶應理工 Advent Calendar 2021の14日目の記事です。
昨日の記事はこちら
この記事ではeXpressなDataのPath、その名もXDP!!!!についてゆるゆると書いていきたいと思います
XDPとは何者
XDPはLinuxカーネルに実装されている技術でして、高速なパケット処理を実現する方法の1つとされています。
なぜ高速なのかと言いますと、XDPではNICのデバイスドライバの段階でパケットを処理するプログラムを動かすことが出来るのです。NICというのはNetwork Interface Cardのことでして、フレーム(パケット)を受信するハードウエアです。このハードウエアをOS側から制御するソフトウエアがデバドラです。

図はxdp-paperより。
通常のLinuxのパケット処理の流れとしてはNICがフレームを受信すると、メモリ上のリングバッファにその情報を格納します。フレームが置いてある受信バッファのアドレスとか長さとかの構造体の形です。同時にNICはCPUに対してハードウエア割り込みを起こします。「フレーム届いたけど!!」って教えてあげます。するとソフトウエア割り込みがスケジュールされ、先程のリングバッファを見ながらフレームが受信バッファからカーネル空間に取り出されます。パケットはsk_buffという構造体で管理します。その後TCP/IPといったプロトコルスタックの処理が走り、最終的にユーザーランドのアプリケーションからシステムコール経由で取得されます。
たぶん。
今のが通常の処理なのですが、ではXDPはといいますと「フレームが受信バッファからカーネル空間に取り出されます」の前に処理をかけるのです。sk_buffを割り当てる前、プロトコルスタックに入る前、です。ユーザーランドで書いてコンパイルされてカーネル内にロードされたバイトコードがこの時点で実行されて、パケットに処理が伝わります。プロトコルスタックより先に自分の書いたプログラムでパケットを破棄したりencap/decapしたりできます。
ではどうやってデバドラ段階で自分で書いたプログラムを動かしているのかという話ですが、ここでBPFが出てきます。BPFはLinuxカーネル内の独自の命令セットを持った仮想マシンみたいなイメージです。ユーザーランドで作ったプログラムを実行できます。カーネルモジュールが要らないのです。XDPはこのBPFによるカーネル内でのプログラム実行を利用しています。コードが書きにくい、制限がある、Verifierが存在するのも、カーネル内でコードを動かすにあたって安全を確保するためと思えば仕方ありません。
上の図はxdp-paperから拝借したのですが、Network Hardwareの直後でXDPが動いている様子が示されています。
後でコードで追ってみます。
雑に動かしてみて感じ取るXDP
簡単なコードを動かしてみて感じ取ります。
XDPのコードを試すまでの流れとしては、
という具合です。
例えばこのようなシンプルなXDPのコードを用意します。受信したパケットをすべてDROPします。返り値がパケットの運命を決めるのです。
SEC("xdp_drop") int xdp_simple(struct xdp_md *ctx) { return XDP_DROP; }
これをコンパイルします。今回はclangを使います。
clang -O2 -target bpf -c xdp_test.c -o xdp_test.o
-target bpfとか便利な時代すぎ。大きなコードだったらオプションをもう少し設定する。
これでNICにアタッチするBPFのバイトコードが生成されたので、カーネルランドに持っていきます。iproute2がある程度対応しているので今回はそれを使います。BCC(BPF Compiler Collection)もあります。自分でも書けます。
ip link set dev eth1 xdpgeneric obj xdp_test.o sec xdp_drop
完成。すべてDROPするのでpingも通らなくなります。ちなみにxdpgenericというのはデバドラ対応してなくてもXDP試せる仕組みです。
デバドラコードから感じ取るXDP
そろそろ中身をちょっと覗いてみたくなりましたね。
XDPの処理が実装されているLinuxカーネルのコードを読んで、XDPの特徴とされている部分を感じ取ります。具体的には
sk_buff割り当て前に処理を入れる(ためデータコピーが不要)
あたりを感じ取っていきます。
デバイスドライバの段階でパケットに処理を入れているため、デバドラのコードを読むことになります。
XDP対応ドライバのリストを眺めます。(他に見るべきところがあるかもしれない)。
virtio_netのデバドラコードを雑に見てみます。linux/drivers/net/virtio_net.c。
receive_small()関数を覗きます。
まずは!xdp_enabledなXDPでないパターン。
if (likely(!vi->xdp_enabled)) { xdp_prog = NULL; goto skip_xdp; }
名前通りskip_xdpにジャンプします(後述)。
このif文を通り抜けた稀なxdp_enabledな状態がお目当てのロジックです。
act = bpf_prog_run_xdp(xdp_prog, &xdp);
来ました。ここで先程書いたXDPプログラムが実行されます。返り値にXDP_DROPを設定しましたがここに入るのですね。
switch (act) { (略) case XDP_DROP: goto err_xdp;
XDPプログラムの返り値によるswitch文が始まります。XDP_DROPの場合は関数としてはジャンプした先で下のように最終的にNULLを返します。
err_xdp: rcu_read_unlock(); stats->xdp_drops++; err_len: stats->drops++; put_page(page); xdp_xmit: return NULL;
XDP_REDIRECT XDP_TXも然る処理の後同じパスに入ってNULLを返します。
呼び出し元のreceive_buf()関数を見てみると、
(略)
skb = receive_small(dev, vi, rq, buf, ctx, len, xdp_xmit, stats);
if (unlikely(!skb))
return;
NULLで返した場合はそこでreturnしています。returnしなかった場合にnetif_receive_skb()みたいなのを呼び出してプロトコルスタックの処理に入るのかなとか思ったのですが見つけられなかったので退散します。
続いてXDPプログラムの返り値がXDP_PASSの場合。
case XDP_PASS: /* Recalculate length in case bpf program changed it */ (略) break;
このbreakのあと、!xdp_enabledと同じskip_xdpに入ります。つまりPASSを設定した場合はXDPのない通常の処理と同じフローをたどるのです!!このあと普通にプロトコルスタックに送られるのです!!
XDPがLinuxカーネルと共存すると言われる一つの要素です。
skip_xdp: (略) skb = build_skb(buf, buflen); (略) err: return skb;
skip_xdpを見てみると、この中で初めてsk_buffが割り当てられています。__build_skb()のコメントにも
Before IO, driver allocates only data buffer where NIC put incoming frame * Driver should add room at head (NET_SKB_PAD) and * MUST add room at tail (SKB_DATA_ALIGN(skb_shared_info))
* After IO, driver calls build_skb(), to allocate sk_buff and populate it * before giving packet to stack.
* RX rings only contains data buffers, not full skbs.
と書いてあります。逆を返すと、XDPの処理はsk_buffの割り当て前に行われるということです。データコピーが走る前!
大変参考になる資料たち
ダラダラ書いてしまいました。読んでいただいてありがとうございます
自分がよく見させてもらっている資料を紹介して締めたいと思います。
xdp-tutorial: 懇切丁寧。NetDevで使われたtutorialっぽい。
linux/samples/bpf: サンプルコード。私は自我を出して書くとVerifier通らないのでサンプルに従う。
今日から始めるXDPと取り巻く環境について - お腹.ヘッタ。: takemioさんはtwitterもblogも非常に参考になる...
SKBパケット選抜総選挙 〜 僕たちは誰について行けばいい? 〜 /osc21do - Speaker Deck: XDPに出会ったきっかけ
まだまだあるのですが、キリがないのでこのくらいにします
SecHack365
もうだいぶ長くなってしまいました。反省。
最近XDPを使ったPacket Filterを作っておりまして、その話を書こうかなーと思っていたのですが詳しいことはやめにします。
フィルタリングの設定をyamlで書いておくと、ルールに沿ったXDPプログラムを生成してくれて、ロードする、統計見る、という感じです。
SecHack365という一年間ハッカソン(?)に参加して制作しているのですが、SecHackも非常に良いプログラムなのでどこかで書けたらよいな。
ながなが読んでいただきありがとうございました。(ありがとうございました)
普段は何をしても「いいんじゃない〰」みたいに言ってもらえるぬるま湯に浸かっているので、外部に公開するのは普通に緊張します
X'masにDeepなPacket処理、いかがですか?(また使う)