X'masにDeepなPacket処理、いかがですか?

慶應理工 Advent Calendar 2021の14日目の記事です。

昨日の記事はこちら

nageler.hatenablog.com

詳しい..! いちばん最初のプランでは坂が無かったのか...

あと国土地理院地図・空中写真閲覧サービス、気になるサービスだ




この記事ではeXpressなDataのPath、その名もXDP!!!!についてゆるゆると書いていきたいと思います






XDPとは何者

XDPはLinuxカーネルに実装されている技術でして、高速なパケット処理を実現する方法の1つとされています。


XDPを学びたいオードリー春日「なんで高速なんだい?」


なぜ高速なのかと言いますと、XDPではNICデバイスドライバの段階でパケットを処理するプログラムを動かすことが出来るのです。NICというのはNetwork Interface Cardのことでして、フレーム(パケット)を受信するハードウエアです。このハードウエアをOS側から制御するソフトウエアがデバドラです。

図はxdp-paperより。

Linuxのパケット処理の流れとしてはNICがフレームを受信すると、メモリ上のリングバッファにその情報を格納します。フレームが置いてある受信バッファのアドレスとか長さとかの構造体の形です。同時にNICはCPUに対してハードウエア割り込みを起こします。「フレーム届いたけど!!」って教えてあげます。するとソフトウエア割り込みがスケジュールされ、先程のリングバッファを見ながらフレームが受信バッファからカーネル空間に取り出されます。パケットはsk_buffという構造体で管理します。その後TCP/IPといったプロトコルスタックの処理が走り、最終的にユーザーランドのアプリケーションからシステムコール経由で取得されます。


ポーリング(↔割り込み)とか書いてないしざっくりすぎる?なんか違うかも?ここまで書いてこの記事で一番大切なことを思い出しました。この文章は詳しい方に見て頂いたりしたわけではないので、間違った理解が含まれる可能性は大いにあります。


対象読者はXDPに出会う前の自分です。(分かりにくい)。


続けます。


今のが通常の処理なのですが、ではXDPはといいますと「フレームが受信バッファからカーネル空間に取り出されます」の前に処理をかけるのです。sk_buffを割り当てる前、プロトコルスタックに入る前、です。ユーザーランドで書いてコンパイルされてカーネル内にロードされたバイトコードがこの時点で実行されて、パケットに処理が伝わります。プロトコルスタックより先に自分の書いたプログラムでパケットを破棄したりencap/decapしたりできルンです。早くて速いって感じがする。


ではどうやってデバドラ段階で自分で書いたプログラムを動かしているのかという話ですが、ここでBPFが出てきます。BPFはLinuxカーネル内の独自の命令セットを持った仮想マシンみたいなイメージです。ユーザーランドで作ったプログラムを実行できます。カーネルモジュールが要らないのです。XDPはこのBPFによるカーネル内でのプログラム実行を利用しています。コードが書きにくい、制限がある、Verifierが存在するのも、カーネル内でコードを動かすにあたって安全を確保するためと思えば仕方ありません。


上の図はxdp-paperから拝借したのですが、Network Hardwareの直後でXDPが動いている様子が示されています。 後でコードで追ってみます。



雑に動かしてみて感じ取るXDP

簡単なコードを動かしてみて感じ取ります。


XDPのコードを試すまでの流れとしては、

  1. NICで動かしたいXDPのコードを書き、

  2. 何らかの方法でBPFのバイトコードコンパイルし、

  3. 何らかの方法でNICにアタッチする

という具合です。

例えばこのようなシンプルな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はLinuxカーネルプロトコルスタックと共存できる

あたりを感じ取っていきます。


デバイスドライバの段階でパケットに処理を入れているため、デバドラのコードを読むことになります。 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にジャンプします(後述)。likelyとか書かれてて、はいすみません...。

このlikelyな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の割り当て前に行われるということです。データコピーが走る前!すばら

大変参考になる資料たち

こんなにダラダラ書いているのにまだブラウザバックしていないあなたはもう沼にはまりかけています。触ってみたくなったこと間違いなし。(読んでいただいてありがとうございます)

自分がよく見させてもらっている資料を紹介しようと思います。

まだまだあるのですが、キリがないのでこのくらいにします



SecHack365とXDPと私

もうだいぶ長くなってしまいました。反省。

最近XDPを使ったPacket Filterを作っておりまして、その話を書こうかなーと思っていたのですが詳しいことはやめにします。

github.com

フィルタリングの設定をyamlで書いておくと、ルールに沿ったXDPプログラムを生成してくれて、ロードする、統計見る、という感じです。

良くも悪くも想像の範囲内のPacketフィルター☆って感じですが、誠意開発中です。こちらも難しいことは来年の私に任せた!

SecHack365という一年間ハッカソン(?)に参加して制作しているのですが、SecHackも非常に良いプログラムなのでどこかで書けたらよいな。





ながなが読んでいただきありがとうございました。(ありがとうございました)

普段は何をしても「いいんじゃない〰」みたいに言ってもらえるぬるま湯に浸かっているので、外部に公開するのは普通に緊張します


X'masにDeepなPacket処理、いかがですか?(また使う)