手っ取り早くやってみる 超XDP入門
サーバ ネットワーク プログラミング
Lastmod: 2024-09-17
Published: 2024-07-24

概要

XDPを手っ取り早く触ってみるためのお話です。
そのため、eBPFなどの細かい説明は省略します。

XDPは、Linuxカーネルのネットワーキングスタックの最も初期の段階でパケットを処理するためのフレームワークであり、eBPFを使ってNIC(ネットワークインターフェースカード)に直接プログラムを挿入することができます。

Linuxカーネルのネットワーキングスタックの最も初期の段階でパケットを操作するため、iptablesなどのフィルタより高速に処理ができます。

ざっくりと、XDPのプログラムをeBPFを使ってNICのインターフェイスにアタッチできるという認識でOKです。

環境構築

Ubuntu 22.04を前提にしています。
XDPでNICの通信を制御するため、VMなどの仮想マシンで試すことをお勧めします。

  • 必要パッケージのインストール

    $ sudo apt install build-essential clang llvm gcc-multilib libbpf-dev
    

eBPF(XDP)のビルドはgccではなくclangを使う、またebpf用のヘッダファイルが必要であるため、libbpf-devをインストールします。

gcc-multilibはあとでasm/types.hを使うのに必要です。

早速やってみよう パケット全部DROP

インターフェイスの確認

  • まず、インターフェイスに通信が来ていることを見てみます

    $ sudo tcpdump -i eht0 -n
    

通常、スイッチなどにつながっていればARPなどが飛んでくるはずです。
何も通信が発生しない場合は外部からpingなどを打ってパケットを確認してください。

XDPプログラムの準備

  • XDPで全ての通信をDROPする

    • xdp.c
    #include <linux/bpf.h>
    #include <bpf/bpf_helpers.h>
    
    SEC("xdp")
    int xdp_drop(struct xdp_md *ctx) {
        return XDP_DROP;
    }
    
    char _license[] SEC("license") = "MIT";
    
  • #include <linux/bpf.h>

    • LinuxのBPF (Berkeley Packet Filter) 関連の機能を提供するヘッダーファイル
  • #include <bpf/bpf_helpers.h>

    • eBPFプログラムを書くために必要なヘルパー関数を提供するヘッダーファイル
  • SEC("xdp")

    • 次に定義される関数がどのタイプのeBPFプログラムであるかを指定するマクロ
    • “xdp"を指定することで、次のに定義される関数がXDPプログラムである
  • int xdp_drop(struct xdp_md *ctx)

    • パケットを受信する度にxdp_dropが呼び出されます。
      今回はreturn XDP_DROP;とし全てのパケットをDROPするプログラム
  • char _license[] SEC("license") = "MIT";

    • eBPFプログラムは、実行されるためにライセンス指定が必要
    • GPL由来の関数を実行する場合はGPLじゃないとエラーになる

ビルド

  • ビルド

    $ clang -O2 -target bpf -c xdp.c -o xdp.o
    
  • targetにbpfを指定してビルドすることで、eBPF用のオブジェクトとしてビルドされます。

インターフェイスへのアタッチ

  • インターフェイスへのアタッチ

    $ sudo ip link set dev eth0 xdp obj xdp.o sec xdp
    

パケットの確認

$ sudo tcpdump -i eth0 -n

ARP、ICMPなどのパケットが到達しなくなったはずです。
XDPはtcpdumpより先に処理されるため、tcpdumpでパケットを観測できなくなります

XDPのデタッチ

$ sudo ip link set dev eth0 xdp off

これで全てのパケットをDROPするXDPプログラムがデタッチされ、
通信ができるようになったはずです。

もう少し深く見てみる (ICMPを落とす)

さすがに全パケットDROPでは ip link set down dev eth0 と大差なく
応用がないので、XDPを使ってICMPだけを落としてみましょう。

  • xdp.c
    #include <linux/bpf.h>
    #include <bpf/bpf_helpers.h>
    #include <linux/if_ether.h>
    #include <linux/ip.h>
    #include <linux/icmp.h>
    #include <arpa/inet.h>
    
    SEC("xdp")
    int xdp_drop_icmp(struct xdp_md *ctx) {
        void *data_end = (void *)(unsigned long)ctx->data_end;
        void *data = (void *)(unsigned long)ctx->data;
    
        // Ethernetヘッダのポインタを取得
        struct ethhdr *eth = data;
        if ((void *)eth + sizeof(*eth) > data_end)
            return XDP_PASS;
    
        // EthernetフレームがIPパケットを含んでいるか確認
        if (eth->h_proto != htons(ETH_P_IP))
            return XDP_PASS;
    
        // IPヘッダのポインタを取得
        struct iphdr *ip = data + sizeof(*eth);
        if ((void *)ip + sizeof(*ip) > data_end)
            return XDP_PASS;
    
        // プロトコルがICMPであるか確認
        if (ip->protocol != IPPROTO_ICMP)
            return XDP_PASS;
    
        // ICMPヘッダのポインタを取得
        struct icmphdr *icmp = (void *)ip + sizeof(*ip);
        if ((void *)icmp + sizeof(*icmp) > data_end)
            return XDP_PASS;
    
        // ICMPパケットをドロップ
        return XDP_DROP;
    }
    
    char _license[] SEC("license") = "MIT";
    

ctxからパケットのポインタなどが取り出せるのでそれを使って処理を行います。

  • data
    • パケットデータへのポインタ
  • data_end
    • dataの終わりのポインタ
struct ethhdr *eth = data;
if ((void *)eth + sizeof(*eth) > data_end)
    return XDP_PASS;

Ethernetヘッダにdataポインタを代入します。
ethのポインタ+ethヘッダサイズがdata_endより大きい場合は
パケットサイズがEthernetヘッダより小さいので処理せずKernelに渡します。

if (eth->h_proto != htons(ETH_P_IP))
    return XDP_PASS;

ethヘッダを確認し、h_protoがETH_P_IP(IPv4プロトコル 0x0800)であることを確認しています。htonsはホストのエンディアン(ホストバイトオーダー)から通信パケットのエンディアン(ネットワークバイトオーダー)に変換するマクロです。htons(host to network short)

Etherパケットに含まれるプロトコルがIPv4ではない場合はXDP_PASSとしKernelに渡します。

struct iphdr *ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end)
    return XDP_PASS;

ipヘッダのポインタを取得しています。 data(最初のポインタ) + Ethernetのヘッダサイズを足してEthernetヘッダの終わりを
ipヘッダの先頭ポインタとします。

この際にipヘッダのポインタからiphdrのサイズをポインタがdata_endより小さい場合は
ipヘッダではないのでXDP_PASSをしてkernelに渡します。

if (ip->protocol != IPPROTO_ICMP)
    return XDP_PASS;

ipパケットに含まれるプロトコルがIPPROTO_ICMP(1)であることを確認しています。
ICPM以外のプロトコルはXDP_PASSとしてkernelに渡します。

struct icmphdr *icmp = (void *)ip + sizeof(*ip);
if ((void *)icmp + sizeof(*icmp) > data_end)
    return XDP_PASS;

icmpヘッダのポインタを取得しています。
ipヘッダの先頭ポインタからipヘッダサイズ分進めたポインタを取得します。
また、ここでも同様にdata_endを超えていないかチェックします。

ここまででXDP_PASSされなかったパケットはICMPであるため、XDP_DROPで
パケットを落とすことで目的が達成できます。

※ IPPROTO_ICMPの時点で落としてしまってもいいかも。

最後に

C言語で書かないといけないので、ちょっぴり難しく感じられるが、
やってみると以外とシンプルなことで実現できたと思います。

次のステップ (2024/09/17 追記)