XDP パート 2: RHEL 8でeXpress Data Path(XDP)マップを使用する

この記事はRed Hat DeveloperUsing eXpress Data Path (XDP) maps in RHEL 8: Part 2 を、許可をうけて翻訳したものです。

::: By Paolo Abeni December 17, 2018 :::

XDPに飛び込もう

XDPに関するこのシリーズの最初のパートでは、XDPを紹介し、最も単純な例について説明しました。それでは、いくつかのより高度なeBPF機能(マップ)といくつかの一般的な落とし穴を探りながら、もうすこし高度なことをしてみましょう。

XDPはRed Hat Enterprise Linux 8で利用可能で、今すぐダウンロードして実行することができます。

車輪を再発明する(しない)

サンプルにパケット解析を追加するところから始めます。このような作業を簡単にするために、XDPプログラムのincludeセクションに以下を追加して、一般的なネットワークプロトコルのカーネル定義を再利用します。

#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/if_vlan.h>
#include <linux/ip.h>

XDPコンテキストを介してパケットの内容にアクセスする必要があります。その定義を見てみましょう。

struct xdp_md {
    __u32 data;
    __u32 data_end;
    __u32 data_meta;
    / *以下へのアクセスはstruct xdp_rxq_infoを経由する* /
    __u32 ingress_ifindex; /* rxq->dev->ifindex */
    __u32 rx_queue_index; /* rxq->queue_index */
};

パケットの内容は、ctx->datactx->data_endの間にあります。それで、構文解析コードを追加して、パケットのアドレスを使ってみましょう。この例では、IPv4宛先アドレスがゼロのパケットをドロップします。

/ * IPv4パケットを解析してSRC、DST IP、およびプロトコルを取得する* /
static inline int parse_ipv4(void *data, __u64 nh_off, void *data_end, __be32 *src, __be32 *dest)
{
    struct iphdr *iph = data + nh_off;

    *src = iph->saddr;
    *dest = iph->daddr;
    return iph->protocol;
}

SEC("prog")
int xdp_drop(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    struct ethhdr *eth = data;
    __be32 dest_ip, src_ip;
    __u16 h_proto;
    __u64 nh_off;
    int ipproto;

    nh_off = sizeof(*eth);

    /* parse vlan */
    h_proto = eth->h_proto;
    if (h_proto == __constant_htons(ETH_P_8021Q) ||
        h_proto == __constant_htons(ETH_P_8021AD)) {
        struct vlan_hdr *vhdr;

        vhdr = data + nh_off;
        nh_off += sizeof(struct vlan_hdr);
        h_proto = vhdr->h_vlan_encapsulated_proto;
    }
    if (h_proto != __constant_htons(ETH_P_IP))
        goto pass;

    ipproto = parse_ipv4(data, nh_off, data_end, &src_ip, &dest_ip);
    if (!dst_ip)
        return XDP_DROP;

pass:
    return XDP_PASS;
}

ベリファイアにひっかかる

上記のコードは問題なくコンパイルできますが、iprouteを使用してロードしようとすると、驚くことになります。

Prog section 'prog' rejected:Permission denied (13)!
- Type:6
- Instructions:19 (0 over limit)
- License:

Verifier analysis:

0:(61) r1 = *(u32 *)(r1 +0)
1:(71) r2 = *(u8 *)(r1 +13)
invalid access to packet, off=13 size=1, R1(id=0,off=0,r=0)
R1 offset is outside of the packet

Error fetching program/map!

ベリファイアの検証を通過できません。このベリファイアのエラーメッセージは「パケットの最初の数バイトにアクセスしているために発生したかもしれない」と誤解を招くかもしれません。各イーサネットフレームは少なくとも64バイトの長さでなければならないことを私達は知っています、従って、私達は私達がパケットペイロードの中の有効なオフセットにアクセスしていることがわかっています。

代わりに、ベリファイアは明示的なチェックのみに依存します。パケット内のオフセットにアクセスしたり操作したりする前に、そのオフセットがパケット本体の内側にあることを条件式でチェックする必要があります。この例では、各ヘッダーにアクセスする前に、次のようなパッチを追加して、ヘッダーの末尾がパケットの末尾より手前にあることを確認する必要があります。

@@ -17,6 +17,9 @@ static inline int parse_ipv4(void *data, __u64 nh_off, void *data_end,
  {
      struct iphdr *iph = data + nh_off;

+     if (iph + 1 > data_end)
+         return 0;
+
      *src = iph->saddr;
      *dest = iph->daddr;
      return iph->protocol;
@@ -34,6 +37,8 @@ int xdp_drop(struct xdp_md *ctx)
      int ipproto;

      nh_off = sizeof(*eth);
+     if (data + nh_off > data_end)
+         goto pass;

      /* parse vlan */
      h_proto = eth->h_proto;
@@ -43,6 +48,8 @@ int xdp_drop(struct xdp_md *ctx)

      vhdr = data + nh_off;
      nh_off += sizeof(struct vlan_hdr);
+     if (data + nh_off > data_end)
+         goto pass;
      h_proto = vhdr->h_vlan_encapsulated_proto;
      }
      if (h_proto != __constant_htons(ETH_P_IP))

ベリファイアも今回は納得でしょう!

カスタムXDPローダー

私たちはすでにパート1でマップについて話しました。実際にそれらを使用する方法を見てみましょう。XDPプログラムを拡張して、ユーザーが実行時にドロップするアドレスを指定でき、関連する統計を読み取ることができるようにします。

最初のステップとして、iproute2ツールをカスタムローダープログラムに置き換える必要があります。これは、このツールではマップの操作ができないためです。XDPプログラムをロードするために使用されるコードは、次のようになります。

#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <error.h>

// [ ... ]
    struct bpf_prog_load_attr prog_load_attr = {
        .prog_type = BPF_PROG_TYPE_XDP,
        .file = "xdp_drop_kern.o",
    };
// [ ... ]
    if (bpf_prog_load_xattr(&prog_load_attr, &obj, &prog_fd))
        error(1, errno, "can't load %s", prog_load_attr.file);

    ifindex = if_nametoindex(dev_name);
    if (!ifindex)
        error(1, errno, "unknown interface %s\n", dev_name);
    if (bpf_set_link_xdp_fd(ifindex, prog_fd, 0) < 0)
        error(1, errno "can't attach to interface %s:%d: "
              "%d:%s\n", dev_name, ifindex, errno,
              strerror(errno));
// [ ... ]
    // cleaning-up
    bpf_set_link_xdp_fd(ifindex, -1, 0);

我々はLinuxカーネルソースにバンドルされているlibbpfヘルパーライブラリを使用しています。 bpf_prog_load_xattr()は、prog_load_attr引数で指定されたeBPFプログラムをロードします。指定されたオブジェクトのすべてのELFセクションを解析して、すべての関連情報を抽出し、それをobj状況データに入れます。見つかった各プログラム(textセクション)は、新しく割り当てられたファイル記述子(prog_fd)を介してカーネル内にロードされます。

このファイル記述子は、後でbpf_set_link_xdp_fd()関数を介して、ロードされたプログラムを選択されたデバイスに接続するために使用されます。最後の引数では、既存のXDPプログラムがある場合はそれを置き換えるためのフラグ、ドライバレベルのXDPフックを使用するためのフラグなど、いくつかのフラグを指定できます。デフォルトでは:

  • ドライバレベルのフックを使用しようとし、その後一般的なものにフォールバックします。
  • 指定したデバイスにXDPプログラムがすでにインストールされている場合は失敗します。

最後に、プログラムの終了時に呼び出される最後のヘルパー関数は、XDPプログラムをNICから切り離し、関連するすべてのカーネルリソースを解放します。

ユーザー空間とのやり取り

それでは、面白い部分、マップに移りましょう。ユーザー空間とeBPFプログラムの間で共有されるすべてのデータ構造は「マップ」と呼ばれますが、実際にはハッシュマップ、配列、キューなど、さまざまな種類があります。通常、「シンプル」と「CPUごと」の2種類があります。CPUごとのマップでは、各エントリはすべてのローカルで利用可能なCPUに対して複製されます。カーネル内部では、各CPUはそのプライベートコピーにのみアクセスします。CPUごとのマップは、あらゆる種類の競合関連の問題を回避します。eBPFプログラムがパケットごとにデータエントリを変更しなければならない場合は、これが推奨されます。

マップデータは、ユーザー空間とeBPFプログラムの両方からアクセスされます。両側に含まれるヘッダーファイルにデータ型定義を追加すると便利です。この例では、マップを使用してフィルタリングする送信元アドレスを指定し、指定した各アドレスについてドロップされたバイト数とパケット数をカウントします。このようなマップをプログラムに追加するには、以下のような記述が必要です。

    // in xdp_drop_common.h
    struct stats_entry {
        __u64 packets;
        __u64 bytes;
    };

    // in xdp_drop_kern.c
    #include "xdp_drop_common.h"
    // [ ... ]
    /* forwarding map */
    struct bpf_map_def SEC("maps") egress_map = {
        .type = BPF_MAP_TYPE_PERCPU_HASH,
        .key_size = sizeof(__be32),
        .value_size = sizeof(struct stats_entry),
        .max_entries = 100,
    };

    // in xdp_drop_user.c
    struct bpf_map *map;
    int map_fd;
    // [ ... ]
    map = bpf_object__find_map_by_name(obj, "drop_map");
    if (!map)
        error(1, errno, "can't load drop_map");
    map_fd = bpf_map__fd(map);
    if (map_fd < 0)
        error(1, errno, "can't get drop_map fd");

このマップは実際にはCPUごとのハッシュテーブルであり、定義にはキーと値のサイズだけしか含まれていません。カーネルには割り当てとルックアップの実行、およびエントリの更新を行うために必要なそれらの情報だけがあればよいためです。マップ定義には、そのようなマップ内で許可されている最大エントリ数も含まれています。ハッシュマップは最初は空で、上記の制限を越えて挿入しようとすると失敗します。配列は指定された制限に等しい固定サイズを持ちます。ユーザー空間は、指定されたファイル記述子を介してマップにアクセスできます。libbpfヘルパー関数を使用するのはここでは少し複雑すぎるように見えるかもしれませんが、eBPFプログラムが複数のマップを公開するときには本当に役に立ちます。

これでユーザースペースとeBPFのインタラクションを追加する準備が整いました。

    // in xdp_drop_kern.c
    struct stats_entry entry;
    // [ ... ]
    stats = bpf_map_lookup_elem(&drop_map, &src_ip);
    if (!stats)
        goto pass;

    stats->packets++
    stats->bytes += ctx->data_end - ctx->data;
    return XDP_DROP;

    // in xdp_drop_user.c
    // [ ... ]
    memset(&entry, 0, sizeof(entry));
    if (bpf_map_update_elem(map_fd, &saddr, entry, BPF_ANY))
        error(1, errno, "can't add address %s\n", argv[i]);
    // [ ... ]
    if (bpf_map_lookup_elem(map_fd, &ipv4_addr, &entry))
        error(1, errnom "no stats for rule %x %x\n",
              ipv4_addr);
    printf("addr %x drop %ld:%ld\n", ipv4_addr,
           entry.packets, entry.bytes);

これで、eBPFプログラムは、送信元IPアドレスがdrop_mapハッシュテーブルで見つかった場合にのみパケットをドロップし、関連する統計情報を更新します。ユーザースペースプログラムは、そのようなマップをゼロ化された統計で埋め、(定期的に)そのようなエントリを調べ、eBPFプログラムによって報告された統計を出力します。

簡潔にするために、ソースアドレス[list]をフェッチしたり、正常終了するための定型的なユーザースペースコードは省略しています。それができれば、私たちはビルドと実行の準備が整っています。

マップに関する注意事項

現在のコードでは、ユーザースペースプログラムのランダムなクラッシュからeBPFフィルターに明らかに何も効果がないことに至るまで、期待外れでしょう。ユーザースペースプログラムが異常終了した場合、XDPプログラムはネットワークデバイスに接続されたままになり、後で実行開始時に失敗します。このような場合、ユーザーは手動でiprouteを使ってXDPプログラムをデタッチする必要があります。

ip link set dev <NIC> xdp off

いくつかの幸運な場合には、現在のコードはほとんど完璧に動作し、シャットダウン時にXDPプログラムを切り離すことに失敗することがあります。

あなたはすでに問題がどこにあるのかを推測しているかもしれませんが、xdp_drop_kernに以下を追加することによってXDP / eBPFデバッグ機能を使用してプログラムのステータスをダンプしましょう。

@@ -33,6 +33,13 @@ static inline int parse_ipv4(void *data, __u64 nh_off, void *data_end,
      return iph->protocol;
  }

+     #define bpf_printk(fmt, ...) \
+     ({ \
+         char ____fmt[] = fmt; \
+         bpf_trace_printk(____fmt, sizeof(____fmt), \
+         ##__VA_ARGS__); \
+     })
+
      SEC("prog")
      int xdp_drop(struct xdp_md *ctx)
      {
@@ -45,6 +52,8 @@ int xdp_drop(struct xdp_md *ctx)
      __u64 nh_off;
      int ipproto;

+     bpf_printk("xdp_drop\n");
+
      nh_off = sizeof(*eth);
      if (data + nh_off > data_end)
          goto pass;
@@ -72,6 +81,8 @@ int xdp_drop(struct xdp_md *ctx)
      if (!stats)
          goto pass;

+     bpf_printk("xdp_drop pkts %lld:%lld\n", stats->packets, stats->bytes);
+
      stats->packets++;
      stats->bytes += ctx->data_end - ctx->data;
      return XDP_DROP;

それから例をもう一度実行して、以下のファイルにメッセージが表示されるのを確認します。

/sys/kernel/debug/tracing/trace

eBPFヘルパー関数は各入力パケットに対して正しく呼び出されます。

運が良ければ、ユーザースペースによって作成されたマップエントリに関連付けられた統計情報が破損しているように見えることがあります。たとえば、エントリ作成後に最初のパケットを受信した場合でもかなりランダムな値が含まれます。

我々はCPUごとのマップを使っていますので、エントリを設定するとき、カーネルは指定されたデータアドレスから 可能な最大CPU個数分の値を読み取り、それぞれをカーネルマップ内の対応するCPUごとの値へコピーします。この例ではユーザー空間プログラムスタックにあるstats変数以外のデータにアクセスする場合にこれが発生します。

さらに、ユーザースペースプロセスがマップからエントリを読み込もうとすると、カーネルは同じ量のデータを指定されたアドレスにコピーし、再びスタック上のデータにアクセスして、上記のランダムな動作を引き起こします。

解決策は、次のような記述をおこない、マップエントリに十分なストレージを割り当てることです。

    int nr_cpus = sysconf(_SC_NPROCESSORS_CONF);
     struct stats_entry *entry;
// [ ... ]
    entry = calloc(nr_cpus, sizeof(struct stats_entry));
    if (!entry)
        error(1, 0, "can't allocate entry\n");

そして、マップを読むときは、すべての値を調べて集計します。

    struct stats_entry all = { 0, 0};

    if (bpf_map_lookup_elem(map_fd, &ipv4_addr, entry))
        error(1, errno, "no stats for address %x\n",
              ipv4_addr);

    for (j = 0; j < nr_cpus; j++) {
        all.packets += entry[j].packets;
        all.bytes += entry[j].bytes;
    }

これで、私たちのIPフィルタアプリケーションは完成です。

この先は

この記事では、XDP / eBPFによって提供される機能のいくつかを取り上げましたが、もっと多くの機能があります。たとえば、さまざまなタスクに使用する準備ができているeBPFヘルパー関数が他にもたくさんあります。変更後のパケットチェックサムの更新、パケットの転送などです。

良い出発点は、Linuxカーネルソース内のこのヘッダです。これには、実装されたヘルパー関数の公式ドキュメントが含まれています。

include/uapi/linux/bpf.h

さらに、カーネルソース内のsamples/bpf/ディレクトリには、さらに複雑なXDPの例がいくつか含まれています。ただし、そこを見る前に関連する背景が必要になります。

例の完全なソースコードは、次の場所にあります。

https://github.com/altoor/xdp_walkthrough_examples

Happy hacking!

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。