概要
前回の記事では、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を使いました。