手っ取り早くやってみる 超XDP入門 Part2 (eBPF Map編)

概要

前回の記事では、XDPのプログラムを使って全てのパケットをDROPするプログラムを書きました。

eBPFのMapを使って、DROPしたパケットをカウントするプログラムを書いてみます。

今回も、XDP+eBPF Mapを手っ取り早く触ってみるためのお話です。
そのため、eBPFなどの細かい説明や使っていないMapの種類などの説明は省略します。

環境構築

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

セットアップについては、前回の記事を参照してください。

eBPF Map

eBPF Mapは、カーネルとユーザ空間間でデータを共有するためのデータ構造です。これにより、
XDPプログラムがリアルタイムでデータを更新し、ユーザ空間がそれを読み取ることができます。

今回は、XDPでDROPしたパケット数をカウントするプログラムを書いてみます。

eBPF Mapの種類

eBPF Mapにはいろいろな種類があります。
Linux Kernel Documentation BPF Mapに詳細が記載されています。
今回は、以下の種類を使います。

  • BPF_MAP_TYPE_HASH
    • ハッシュテーブを表現するMapです。
    • ハッシュキーと値のペアを格納ができます。

今回は、キーをパケット送信元のIPアドレスとし、値にDROPしたパケット数を格納します。

eBPF Mapの作成

eBPF Mapは、eBPFプログラムの中で以下のように定義します。

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, uint32_t);
    __type(value, uint64_t);
    __uint(max_entries, 1024);
} icmp_count_map SEC(".maps");
  • type : Mapの種類を指定します。
  • key : キーの型を指定します。今回はIPv4アドレスをキーとするため、uint32_t を指定します。
  • value : 値の型を指定します。今回はDROPしたパケット数を格納するため、uint64_t を指定します。
  • max_entries : Mapに格納できる最大エントリ数を指定します。
    • BPF_MAP_TYPE_HASHにおいてエントリ数を超える追加はエラーとなります。
  • icmp_count_map : Mapの名前を指定します。
  • SEC(".maps") : Mapを定義するためのセクションを指定します。

eBPF Mapの操作

eBPF Mapは、eBPFプログラムから以下のように操作します。

  • bpf_map_lookup_elem : Mapからキーに対応する値を取得します。
  • bpf_map_update_elem : Mapにキーと値のペアを追加します。
  • bpf_map_delete_elem : Mapからキーに対応する値を削除します。
  • bpf_map_get_next_key : Mapの次のキーを取得します。

実際にXDPプログラムを書いてみる

eBPF Mapを使ったプログラム

eBPF Mapを使ったプログラムを以下に示します。

#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>

// eBPF Mapの定義
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, uint32_t);
    __type(value, uint64_t);
    __uint(max_entries, 1024);
} icmp_count_map SEC(".maps");

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;

    // ソースIPアドレスを取り出す
    uint32_t src_ip = ip->saddr;

    // ソースIPアドレスでicmp_count_mapを検索
    uint64_t *count = bpf_map_lookup_elem(&icmp_count_map, &src_ip);

    // u64型のポインタがNULLではない場合は、カウントをインクリメント
    if (count) {
        __sync_fetch_and_add(count, 1);
    } else {
        // NULLの場合は、icmp_count_mapに追加
        bpf_map_update_elem(&icmp_count_map, &src_ip, &(uint64_t){1}, BPF_ANY);
    }  

    // ICMPパケットをドロップ
    return XDP_DROP;
}

char _license[] SEC("license") = "MIT";

ビルドとロード

xdp.cをビルドして、XDPプログラをロードします。

$ clang -O2 -target bpf -g -c xdp.c -o xdp.o
$ sudo ip link set dev eth0 xdp obj xdp.o sec xdp

ユーザ空間からMapを参照してみる

mapを参照できるようpinを設定

eBPF Mapをユーザ空間から参照するために、Mapをpinします。
これにより、ユーザ空間からMapを参照できるようになります。

  • BPFプログラムのmap idを取得

    $ sudo bpftool map
    16: hash  name icmp_count_map  flags 0x0
            key 4B  value 8B  max_entries 1024  memlock 86144B
            btf_id 25
    
    • 16 がMapのidです。
  • Mapにpinを設定

    $ sudo bpftool map pin id 16 /sys/fs/bpf/xdp/icmp_count_map
    

eBPF Mapを取得するユーザ空間プログラムを書く

eBPF Mapに格納したので、ユーザ空間からMapを参照してみます。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <linux/bpf.h>
#include <linux/if_link.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>

int main(int argc, char **argv) {
    int map_fd = bpf_obj_get("/sys/fs/bpf/xdp/icmp_count_map");
    if (map_fd < 0) {
        perror("bpf_obj_get");
        return 1;
    }

    struct bpf_map_info map_info = {};
    uint32_t info_len = sizeof(map_info);
    if (bpf_obj_get_info_by_fd(map_fd, &map_info, &info_len)) {
        perror("bpf_obj_get_info_by_fd");
        return 1;
    }

    // Mapの情報を表示
    printf("Map name: %s\n", map_info.name);
    printf("Map type: %d\n", map_info.type);
    printf("Map key size: %d\n", map_info.key_size);
    printf("Map value size: %d\n", map_info.value_size);
    printf("Map max_entries: %d\n", map_info.max_entries);

    // 全てのエントリを取得
    uint32_t key, next_key;
    uint64_t value;
    key = next_key = 0;
    while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
        key = next_key; // 更新されたキーを使用
        if (bpf_map_lookup_elem(map_fd, &key, &value) == 0) {
            // IPアドレスを文字列に変換
            char ip_str[INET_ADDRSTRLEN];
            inet_ntop(AF_INET, &key, ip_str, sizeof(ip_str));
            printf("Key: %s, Value: %lu\n", ip_str, value);
        }
        key = next_key; // 次のループの準備
    }

    return 0;
}
  • bpf_obj_get : pinしたMapのファイルディスクリプタを取得します。

    • /sys/fs/bpf/xdp/icmp_count_map にpinしたMapを指定します。
  • bpf_obj_get_info_by_fd : Mapの情報を取得します。

    • Mapの名前、型、キーのサイズ、値のサイズ、最大エントリ数を取得します。
  • bpf_map_get_next_key : Mapの次のキーを取得します。

    • key : 現在のキー
    • next_key : 次のキー
  • bpf_map_lookup_elem : Mapからキーに対応する値を取得します。

ビルドと実行

$ gcc -o map_info map_info.c -lbpf
$ sudo ./map_info
Map name: icmp_count_map
Map type: 1
Map key size: 4
Map value size: 8
Map max_entries: 1024
Key: 192.168.XX.10, Value: 320
Key: 192.168.XX.11, Value: 2307

これで、XDPプログラムでDROPしたICMPパケットの送信元IPアドレスとカウント数が表示されます。

後片付け

  • Mapのunpin

    $ sudo rm -iv /sys/fs/bpf/xdp/icmp_count_map
    
  • XDPプログラムのアンロード

    $ sudo ip link set dev eth0 xdp off
    

xdpのロードとmapを参照するプログラム

毎回ipコマンドでXDPをロードして、pinを設定してmapにアクセスするのは辛いので
常駐プログラムであれば、xdpプログラムをロードしたときにMapのファイルディスクリプタを
取得できるため、pinを設定せずにMapにアクセスすることができます。

また、プログラムを終了するときにXDPプログラムをアンロードするため、 アンロード忘れやpinの解除忘れを防ぐことができます。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <linux/bpf.h>
#include <linux/if_link.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include <unistd.h>
#include <net/if.h>

int main(int argc, char **argv) {
    struct bpf_object *obj;
    struct bpf_map *map;
    struct bpf_program *prog;
    struct bpf_link *link = NULL;
    int map_fd, prog_fd;
    int err;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s <interface>\n", argv[0]);
        return 1;
    }

    // xdpプログラムをロード
    obj = bpf_object__open_file("./xdp.o", NULL);
    if (libbpf_get_error(obj)) {
        perror("Failed to open BPF object");
        return 1;
    }

    if (bpf_object__load(obj)) {
        perror("Failed to load BPF object");
        return 1;
    }

    // プログラムを取得
    prog = bpf_object__find_program_by_name(obj, "xdp_drop_icmp");
    if (libbpf_get_error(prog)) {
        perror("Failed to find BPF program by name");
        return 1;
    }

    // Mapを取得
    map = bpf_object__find_map_by_name(obj, "icmp_count_map");
    if (!map) {
        perror("Failed to find BPF map by name");
        return 1;
    }

    map_fd = bpf_map__fd(map);
    if (map_fd < 0) {
        perror("Failed to get map fd");
        return 1;
    }

    // eth0にXDPプログラムをアタッチ
    const char* if_name = argv[1];
    int ifindex = if_nametoindex(if_name);
    if (ifindex == 0) {
        perror("Failed to get interface index");
        return 1;
    }

    link = bpf_program__attach_xdp(prog, ifindex);
    if (libbpf_get_error(link)) {
        perror("Failed to attach XDP program");
        return 1;
    }

    // 無限ループで1秒ごとにマップの状態を表示
    while (1) {
        // 区切り線
        printf("==============================\n");
        // Mapの情報を表示
        struct bpf_map_info map_info = {};
        uint32_t info_len = sizeof(map_info);
        if (bpf_obj_get_info_by_fd(map_fd, &map_info, &info_len)) {
            perror("Failed to get map info");
            return 1;
        }

        printf("Map name: %s\n", map_info.name);
        printf("Map type: %d\n", map_info.type);
        printf("Map key size: %d\n", map_info.key_size);
        printf("Map value size: %d\n", map_info.value_size);
        printf("Map max_entries: %d\n", map_info.max_entries);

        // 全てのエントリを取得
        uint32_t key, next_key;
        uint64_t value;
        key = next_key = 0;
        while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
            key = next_key; // 更新されたキーを使用
            if (bpf_map_lookup_elem(map_fd, &key, &value) == 0) {
                // IPアドレスを文字列に変換
                char ip_str[INET_ADDRSTRLEN];
                inet_ntop(AF_INET, &key, ip_str, sizeof(ip_str));
                printf("Key: %s, Value: %lu\n", ip_str, value);
            }
            key = next_key; // 次のループの準備
        }
        sleep(1);  // 1秒間隔でループ
    }

    return 0;
}
  • bpf_object__open_file : BPFオブジェクトをファイルから開きます。
    • XDPのビルド済みオブジェクトファイルを指定します。
  • bpf_object__load : BPFオブジェクトをロードします。
    • BPFオブジェクトをロードすることで、プログラムやMapを取得できるようになります。
  • bpf_object__find_program_by_name : BPFプログラムを名前で取得します。
    • XDPプログラムの名前を指定します。sec(“xdp”)で指定した直後の関数名を指定します。
  • bpf_object__find_map_by_name : BPF Mapを名前で取得します。
    • Mapの名前を指定します。
  • bpf_map__fd : Mapのファイルディスクリプタを取得します。
  • bpf_program__attach_xdp : XDPプログラムを指定したインターフェースにアタッチします。

ビルドと実行

  • ビルド

    $ gcc -o map_monitor map_monitor.c -lbpf
    
  • 実行

    ./map_monitor eth0
    Map name: icmp_count_map
    Map type: 1
    Map key size: 4
    Map value size: 8
    Map max_entries: 1024
    Key: 192.168.XX.2, Value: 4
    ==============================
    Map name: icmp_count_map
    Map type: 1
    Map key size: 4
    Map value size: 8
    Map max_entries: 1024
    Key: 192.168.XX.2, Value: 5
    ^C
    

    Ctrl+Cで終了します。

終了すると、XDPプログラムがアンロードされます。

まとめ

XDPプログラムでDROPしたパケット数をカウントするプログラムを書いてみました。
Mapを使うことで、eBPFプログラムとユーザ空間プログラム間でデータをやり取りすることがき、
XDPの応用の幅が広がると思います。

最後に補足

本当は、パフォーマンスなどを考えると、BPF_MAP_TYPE_HASHではなく、
BPF_MAP_TYPE_PERCPU_HASHを使うべきですが、今回は簡単のためにBPF_MAP_TYPE_HASHを使いました。