xdp-tutorial packet02-rewritingをやってみる

github.com

bpf_xdp_adjust_head()はパケットの先頭ポインタをずらすことができる。 つまり、パケット長が長くしたり、パケット長を短くするときに使える。(例えば、encap処理やdecap処理)bpf_xdp_adjust_headのあるパラメータに正の値を与えると、パケット長が短くなり、負の値を与えるとパケット長が長くなる。

イメージとしてはこんな感じ。

 098 | 099 | 100 | 101 | ...         | 500
           ^                         ^
    - <--- | ---> +           - <--- | ---> +
    (start-pointer)           (end-pointer)

パケットの末尾のポインタをずらすbpf_xdp_adjust_tail()ってのもある。

Assignment1

TCP/UDPパケットのdst portを-1する。

テスト環境構築

sudo ../testenv/testenv.sh setup --name veth-packet02

コード修正

  • コード中のコメントを見ると、xdp_port_rewrite_func()に処理を実装するらしい。
  • xdp_port_rewrite_func()にはpacket01-pasingのときに実装した機能が記載されている。つまり、IP headerまでのパースにはこの関数の処理の一部をそのままコピペすればよい。
  • プロトコルごとのパース処理はpacket01-parsingのときは自前ですべて実装していたが、今回はこれが提供されている。実装はcommon/parsing_helpers.hに存在している。プロトコルごとのパース処理は原則これを利用する。※私がここで実装したものとは微妙に差異があるため注意。
--- a/packet02-rewriting/xdp_prog_kern.c
+++ b/packet02-rewriting/xdp_prog_kern.c
@@ -57,7 +57,54 @@ static __always_inline int vlan_tag_push(struct xdp_md *ctx,
 SEC("xdp")
 int xdp_port_rewrite_func(struct xdp_md *ctx)
 {
-       return XDP_PASS;
+       void *data_end = (void *)(long)ctx->data_end;
+       void *data = (void *)(long)ctx->data;
+
+       /* Default action XDP_PASS, imply everything we couldn't parse, or that
+        * we don't want to deal with, we just pass up the stack and let the
+        * kernel deal with it.
+        */
+       __u32 action = XDP_PASS; /* Default action */
+
+       /* These keep track of the next header type and iterator pointer */
+       struct hdr_cursor nh;
+       int nh_type;
+       nh.pos = data;
+
+       struct ethhdr *eth;
+
+       /* Packet parsing in steps: Get each header one at a time, aborting if
+        * parsing fails. Each helper function does sanity checking (is the
+        * header type in the packet correct?), and bounds checking.
+        */
+       nh_type = parse_ethhdr(&nh, data_end, &eth);
+
+       if (nh_type == bpf_htons(ETH_P_IPV6)) {
+               struct ipv6hdr *ip6h;
+               nh_type = parse_ip6hdr(&nh, data_end, &ip6h);
+       } else if (nh_type == bpf_htons(ETH_P_IP)) {
+               struct iphdr *iph;
+               nh_type = parse_iphdr(&nh, data_end, &iph);
+       }
+       if (nh_type == IPPROTO_TCP) {
+               struct tcphdr *tcph;
+               int hdrlen;
+               if ((hdrlen = parse_tcphdr(&nh, data_end, &tcph)) < 0) {
+                       goto out;
+               }
+               tcph->dest = bpf_htons(bpf_ntohs(tcph->dest) - 1);
+               //return XDP_PASS;
+       } else if (nh_type == IPPROTO_UDP) {
+               struct udphdr *udph;
+               int hdrlen;
+               if ((hdrlen = parse_udphdr(&nh, data_end, &udph)) < 0) {
+                       goto out;
+               }
+               udph->dest = bpf_htons(bpf_ntohs(udph->dest) - 1);
+               //return XDP_PASS;
+       }
+ out:
+       return xdp_stats_record_action(ctx, action);
 }

 /* VLAN swapper; will pop outermost VLAN tag if it exists, otherwise push a new

TEST

XDPプログラムをロードする。

make
sudo ./xdp-loader unload veth-packet02 --all
sudo ./xdp-loader load --prog-name xdp_port_rewrite_func --mode skb  veth-packet02 xdp_prog_kern.o

dst portを2000に指定してudpパケットを送信する。

sudo ../testenv/testenv.sh exec -- socat - 'udp6:[fc00:dead:cafe:1::1]:2000'

veth-packet02でパケットをキャプチャするとdst portが2000から1999に変わってることが分かる。

Assignment2

一番外側にあるVLANタグを取り外す。

テスト環境構築

sudo ../testenv/testenv.sh setup --name veth-packet02 --vlan

コード修正

vlan_tag_pop()が定義されているので、処理はここに実装する。

--- a/packet02-rewriting/xdp_prog_kern.c
+++ b/packet02-rewriting/xdp_prog_kern.c
@@ -3,6 +3,8 @@
 #include <linux/in.h>
 #include <bpf/bpf_helpers.h>
 #include <bpf/bpf_endian.h>
+#include <pcap/vlan.h>
+#include <string.h>

 // The parsing helper functions from the packet01 lesson have moved here
 #include "../common/parsing_helpers.h"
@@ -16,30 +18,38 @@
  */
 static __always_inline int vlan_tag_pop(struct xdp_md *ctx, struct ethhdr *eth)
 {
-       /*
        void *data_end = (void *)(long)ctx->data_end;
        struct ethhdr eth_cpy;
-       struct vlan_hdr *vlh;
+       struct vlan_tag *vlh;
        __be16 h_proto;
-       */
        int vlid = -1;

-       /* Check if there is a vlan tag to pop */
-
        /* Still need to do bounds checking */

        /* Save vlan ID for returning, h_proto for updating Ethernet header */
+       memcpy(&vlh, (void *)eth + sizeof(eth->h_dest) + sizeof(eth->h_source), sizeof(struct vlan_tag));
+       vlid = bpf_ntohs(vlh->vlan_tci) & 0x0fff;
+       h_proto = *(__be16 *)((void *)eth + sizeof(eth->h_dest) + sizeof(eth->h_source) + sizeof(struct vlan_tag));

        /* Make a copy of the outer Ethernet header before we cut it off */
+       memcpy(&eth_cpy, eth, sizeof(struct ethhdr));

        /* Actually adjust the head pointer */
+       if (bpf_xdp_adjust_head(ctx, sizeof(struct vlan_tag)))
+               return -1;

        /* Need to re-evaluate data *and* data_end and do new bounds checking
         * after adjusting head
         */
+       eth = (void *)(long)ctx->data;
+       data_end = (void *)(long)ctx->data_end;
+       if (eth + 1 > data_end)
+               return -1;

        /* Copy back the old Ethernet header and update the proto type */

+       memcpy(eth, &eth_cpy, sizeof(struct ethhdr));
+       eth->h_proto = h_proto;

        return vlid;
 }

TEST

XDPプログラムをロードする。

$ make
$ sudo ./xdp-loader unload veth-packet02 --all
$ sudo ./xdp-loader load --prog-name xdp_vlan_swap_func --mode skb  veth-packet02 xdp_prog_kern.o
$ sudo ../testenv/testenv.sh ping --vlan
Running ping from inside test environment:

PING fc00:dead:cafe:1001::1(fc00:dead:cafe:1001::1) 56 data bytes
64 bytes from fc00:dead:cafe:1001::1: icmp_seq=1 ttl=64 time=0.044 ms
64 bytes from fc00:dead:cafe:1001::1: icmp_seq=2 ttl=64 time=0.111 ms
^C
--- fc00:dead:cafe:1001::1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1027ms
rtt min/avg/max/mdev = 0.044/0.077/0.111/0.033 ms

veth-packet02でキャプチャすると以下のようになる。

icmp echo requestからvlanタグが消えているのが分かる。

Assignment3

VLANタグがついてなかったらVLANタグを付与する。

テスト環境構築

sudo ../testenv/testenv.sh setup --name veth-packet02

コード修正

vlan_tag_push()に処理を実装する。

--- a/packet02-rewriting/xdp_prog_kern.c
+++ b/packet02-rewriting/xdp_prog_kern.c
@@ -60,9 +60,31 @@ static __always_inline int vlan_tag_pop(struct xdp_md *ctx, struct ethhdr *eth)
 /* Pushes a new VLAN tag after the Ethernet header. Returns 0 on success,
  * -1 on failure.
  */
-static __always_inline int vlan_tag_push(struct xdp_md *ctx,
-                                        struct ethhdr *eth, int vlid)
-{
+static __always_inline int vlan_tag_push(struct xdp_md *ctx, struct ethhdr *eth,
+                                         int vlid) {
+       void *data_end = (void *)(long)ctx->data_end;
+       struct ethhdr eth_cpy;
+       struct vlan_tag vlh = {
+           .vlan_tpid = bpf_htons(ETH_P_8021Q),
+           .vlan_tci = bpf_htons(vlid),
+       };
+
+       if (eth + 1 > data_end) {
+         return -1;
+       }
+       memcpy(&eth_cpy, eth, sizeof(struct ethhdr));
+       if (bpf_xdp_adjust_head(ctx, -(int)sizeof(struct vlan_tag)))
+         return -1;
+       eth = (void *)(long)ctx->data;
+       data_end = (void *)(long)ctx->data_end;
+       if ((void *)eth + sizeof(struct ethhdr) + sizeof(struct vlan_tag) > data_end) {
+               return -1;
+       }
+       memcpy(eth, &eth_cpy, sizeof(struct ethhdr));
+       memcpy(&eth->h_proto, &vlh, sizeof(struct vlan_tag));
+       void *p = (void *)&eth + sizeof(eth->h_dest) + sizeof(eth->h_source) + sizeof(struct vlan_tag);
+       memcpy(p, &eth_cpy.h_proto, sizeof(eth_cpy.h_proto));
+
        return 0;
 }

TEST

XDPプログラムをロードする。

$ make
$ sudo ./xdp-loader unload veth-packet02 --all
$ sudo ./xdp-loader load --prog-name xdp_vlan_swap_func --mode skb  veth-packet02 xdp_prog_kern.o

pingする。

$ sudo ../testenv/testenv.sh ping

veth-packet02でキャプチャすると以下のようになる。

icmp echo requestにvlanタグが付与されているのが分かる。

補足

testenv.shコピペ用コマンドメモ

sudo ../testenv/testenv.sh setup --name veth-packet02
sudo ../testenv/testenv.sh setup --name veth-packet02 --vlan
sudo ../testenv/testenv.sh setup --name veth-packet02 --vlan --legacy-ip
sudo ../testenv/testenv.sh enter --name veth-packet02
sudo ../testenv/testenv.sh teardown

xdp-loaderコピペ用コマンドメモ

sudo ./xdp-loader load --prog-name xdp_port_rewrite_func --mode skb  veth-packet02 xdp_prog_kern.o
sudo ./xdp-loader load --prog-name xdp_vlan_swap_func --mode skb  veth-packet02 xdp_prog_kern.o
sudo ./xdp-loader unload veth-packet02 --all
sudo ./xdp-loader status

xdp-tutorial packet01-parsingをやってみる

github.com

packet01-parsingでは以下を行う。

  • 指定したICMPパケットを許可し、それ以外のICMPパケットはドロップする。
  • ICMPパケットでないものは許可。

Assignment1

parse_ethhdr()のbouds checkのバグフィックスをする。

テスト環境構築

sudo ../testenv/testenv.sh setup --name veth-packet01

コード修正

--- a/packet01-parsing/xdp_prog_kern.c
+++ b/packet01-parsing/xdp_prog_kern.c
@@ -36,7 +36,7 @@ static __always_inline int parse_ethhdr(struct hdr_cursor *nh,
        /* Byte-count bounds check; check if current pointer + size of header
         * is after data_end.
         */
-       if (nh->pos + 1 > data_end)
+       if (nh->pos + hdrsize > data_end)
                return -1;

        nh->pos += hdrsize;

Assignment2

parse_ip6hdr()を実装して、IPv6パケットをパースする。

テスト環境構築

sudo ../testenv/testenv.sh setup --name veth-packet01

コード修正

--- a/packet01-parsing/xdp_prog_kern.c
+++ b/packet01-parsing/xdp_prog_kern.c
@@ -46,11 +46,18 @@ static __always_inline int parse_ethhdr(struct hdr_cursor *nh,
 }

 /* Assignment 2: Implement and use this */
-/*static __always_inline int parse_ip6hdr(struct hdr_cursor *nh,
+static __always_inline int parse_ip6hdr(struct hdr_cursor *nh,
                                        void *data_end,
                                        struct ipv6hdr **ip6hdr)
 {
-}*/
+       struct ipv6hdr *ip6h = nh->pos;
+
+       if (ip6h + 1 > data_end)
+               return -1;
+       nh->pos = ip6h + 1;
+       *ip6hdr = ip6h;
+       return ip6h->nexthdr;
+}

 /* Assignment 3: Implement and use this */
 /*static __always_inline int parse_icmp6hdr(struct hdr_cursor *nh,
@@ -65,6 +72,7 @@ int  xdp_parser_func(struct xdp_md *ctx)
        void *data_end = (void *)(long)ctx->data_end;
        void *data = (void *)(long)ctx->data;
        struct ethhdr *eth;
+       struct ipv6hdr *ipv6;

        /* Default action XDP_PASS, imply everything we couldn't parse, or that
         * we don't want to deal with, we just pass up the stack and let the
@@ -86,6 +94,9 @@ int  xdp_parser_func(struct xdp_md *ctx)
        nh_type = parse_ethhdr(&nh, data_end, &eth);
        if (nh_type != bpf_htons(ETH_P_IPV6))
                goto out;
+       nh_type = parse_ip6hdr(&nh, data_end, &ipv6);
+       if (nh_type != IPPROTO_ICMPV6)
+               goto out;

        /* Assignment additions go below here */

Assignment3

parse_icmp6hdr()を実装して、seqenceが奇数のICMPパケットのみを許可する。

コード修正

--- a/packet01-parsing/xdp_prog_kern.c
+++ b/packet01-parsing/xdp_prog_kern.c
@@ -60,11 +60,18 @@ static __always_inline int parse_ip6hdr(struct hdr_cursor *nh,
 }

 /* Assignment 3: Implement and use this */
-/*static __always_inline int parse_icmp6hdr(struct hdr_cursor *nh,
+static __always_inline int parse_icmp6hdr(struct hdr_cursor *nh,
                                          void *data_end,
                                          struct icmp6hdr **icmp6hdr)
 {
-}*/
+       struct icmp6hdr *icmp6h = nh->pos;
+
+       if (icmp6h + 1 > data_end)
+               return -1;
+       nh->pos = icmp6h + 1;
+       *icmp6hdr = icmp6h;
+       return bpf_ntohs(icmp6h->icmp6_sequence);
+}

 SEC("xdp")
 int  xdp_parser_func(struct xdp_md *ctx)
@@ -73,6 +80,8 @@ int  xdp_parser_func(struct xdp_md *ctx)
        void *data = (void *)(long)ctx->data;
        struct ethhdr *eth;
        struct ipv6hdr *ipv6;
+       struct icmp6hdr *icmp6;
+       int icmp_seq;

        /* Default action XDP_PASS, imply everything we couldn't parse, or that
         * we don't want to deal with, we just pass up the stack and let the
@@ -97,6 +106,9 @@ int  xdp_parser_func(struct xdp_md *ctx)
        nh_type = parse_ip6hdr(&nh, data_end, &ipv6);
        if (nh_type != IPPROTO_ICMPV6)
                goto out;
+       icmp_seq = parse_icmp6hdr(&nh, data_end, &icmp6);
+       if (icmp_seq % 2 == 1)
+               goto out;

        /* Assignment additions go below here */

TEST

$ make
$ sudo ./xdp-loader unload veth-packet01 --all
$ sudo ./xdp-loader load --prog-name xdp_parser_func --mode skb  veth-packet01 xdp_prog_kern.o
$ ping -6 fc00:dead:cafe:1::2
PING fc00:dead:cafe:1::2(fc00:dead:cafe:1::2) 56 data bytes
64 bytes from fc00:dead:cafe:1::2: icmp_seq=1 ttl=64 time=0.057 ms
64 bytes from fc00:dead:cafe:1::2: icmp_seq=3 ttl=64 time=0.082 ms
64 bytes from fc00:dead:cafe:1::2: icmp_seq=5 ttl=64 time=0.075 ms
^C
--- fc00:dead:cafe:1::2 ping statistics ---
5 packets transmitted, 3 received, 40% packet loss, time 4084ms
rtt min/avg/max/mdev = 0.057/0.071/0.082/0.010 ms

奇数シーケンスのパケットのみpingが通ってる。

Assignment4

VLANをサポートさせる。 また、通常のタグパケットだけでなく、ダブルタグVLANも考慮する。

テスト環境構築

以下コマンドでvlan環境を構築する。

sudo ../testenv/testenv.sh setup --name veth-packet01 --vlan

下図のような環境が出来上がる。

試しにhost namespaceからveth0.2のアドレスに向けてpingを打ってみる。

$ ping -6 fc00:dead:cafe:2001::2 -I veth-packet01.2 -c 2
PING fc00:dead:cafe:2001::2(fc00:dead:cafe:2001::2) from fc00:dead:cafe:2001::1 veth-packet01.2: 56 data bytes
64 bytes from fc00:dead:cafe:2001::2: icmp_seq=1 ttl=64 time=0.097 ms
64 bytes from fc00:dead:cafe:2001::2: icmp_seq=2 ttl=64 time=0.099 ms

--- fc00:dead:cafe:2001::2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1008ms
rtt min/avg/max/mdev = 0.097/0.098/0.099/0.001 ms

veth-packet01.2で取得したキャプチャ

veth-packet01で取得したキャプチャ

veth-packet01.2(VLANインターフェース)でvlan tagの付与・取り外しがちゃんと行われている。

コード修正

--- a/packet01-parsing/xdp_prog_kern.c
+++ b/packet01-parsing/xdp_prog_kern.c
@@ -8,6 +8,7 @@
 #include <linux/icmpv6.h>
 #include <bpf/bpf_helpers.h>
 #include <bpf/bpf_endian.h>
+#include <pcap/vlan.h>
 /* Defines xdp_stats_map from packet04 */
 #include "../common/xdp_stats_kern_user.h"
 #include "../common/xdp_stats_kern.h"
@@ -17,6 +18,10 @@ struct hdr_cursor {
        void *pos;
 };

+static __always_inline int proto_is_vlan(__u16 h_proto)
+{
+       return !!(h_proto == bpf_htons(ETH_P_8021Q) || h_proto == bpf_htons(ETH_P_8021AD));
+}
 /* Packet parsing helpers.
  *
  * Each helper parses a packet header, including doing bounds checking, and
@@ -31,18 +36,38 @@ static __always_inline int parse_ethhdr(struct hdr_cursor *nh,
                                        struct ethhdr **ethhdr)
 {
        struct ethhdr *eth = nh->pos;
-       int hdrsize = sizeof(*eth);
+       __u16 *proto = NULL;
+       struct vlan_tag *vtag;

        /* Byte-count bounds check; check if current pointer + size of header
         * is after data_end.
         */
-       if (nh->pos + hdrsize > data_end)
+       if ((nh->pos + sizeof(eth->h_dest)) > data_end)
                return -1;
-
-       nh->pos += hdrsize;
+       nh->pos += sizeof(eth->h_dest);
+       if ((nh->pos + sizeof(eth->h_source)) > data_end)
+               return -1;
+       nh->pos += sizeof(eth->h_source);
+
+       int i = 0;
+       #pragma unroll
+       for (i = 0; i < 2; i++) {
+               if ((nh->pos + sizeof(vtag->vlan_tpid)) > data_end)
+                       return -1;
+               if (!proto_is_vlan(*(__u16 *)nh->pos))
+                       break;
+               if ((nh->pos + sizeof(struct vlan_tag)) > data_end)
+                       return -1;
+               nh->pos += sizeof(struct vlan_tag);
+       }
+
+       if ((nh->pos + sizeof(eth->h_proto)) > data_end)
+               return -1;
+       proto = (__u16 *)nh->pos;
+       nh->pos += sizeof(eth->h_proto);
        *ethhdr = eth;

-       return eth->h_proto; /* network-byte-order */
+       return *proto; /* network-byte-order */
 }

 /* Assignment 2: Implement and use this */

TEST

$ make
$ sudo ./xdp-loader unload veth-packet01 --all
$ sudo ./xdp-loader load --prog-name xdp_parser_func --mode skb  veth-packet01 xdp_prog_kern.o
$ ping -6 fc00:dead:cafe:2001::2
PING fc00:dead:cafe:2001::2(fc00:dead:cafe:2001::2) 56 data bytes
64 bytes from fc00:dead:cafe:2001::2: icmp_seq=1 ttl=64 time=0.081 ms
64 bytes from fc00:dead:cafe:2001::2: icmp_seq=3 ttl=64 time=0.092 ms
64 bytes from fc00:dead:cafe:2001::2: icmp_seq=5 ttl=64 time=0.088 ms
64 bytes from fc00:dead:cafe:2001::2: icmp_seq=7 ttl=64 time=0.091 ms
64 bytes from fc00:dead:cafe:2001::2: icmp_seq=9 ttl=64 time=0.087 ms
^C
--- fc00:dead:cafe:2001::2 ping statistics ---
9 packets transmitted, 5 received, 44.4444% packet loss, time 8180ms
rtt min/avg/max/mdev = 0.081/0.087/0.092/0.003 ms

Assignment5

IPv4をサポートさせる。

テスト環境構築

以下コマンドでipv4環境を構築する。

sudo ../testenv/testenv.sh setup --name veth-packet01 --vlan --legacy-ip

確認した所、vlan interfaceにIPアドレスアサインされてないようなので、アサインする。

$ sudo ip addr add 192.168.101.1/24 dev veth-packet01.1
$ sudo ip addr add 192.168.102.1/24 dev veth-packet01.2
$ sudo ip netns exec veth-packet01 ip addr add 192.168.101.2/24 dev veth0.1
$ sudo ip netns exec veth-packet01 ip addr add 192.168.102.2/24 dev veth0.2

試しにhost namespaceからveth0.2のアドレスに向けてpingを打ってみる。

$ ping 192.168.102.2 -c 2
PING 192.168.102.2 (192.168.102.2) 56(84) bytes of data.
64 bytes from 192.168.102.2: icmp_seq=1 ttl=64 time=0.240 ms
64 bytes from 192.168.102.2: icmp_seq=2 ttl=64 time=0.091 ms

--- 192.168.102.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.091/0.165/0.240/0.074 ms

veth-packet01.2で取得したキャプチャ

veth-packet01で取得したキャプチャ

コード修正

--- a/packet01-parsing/xdp_prog_kern.c
+++ b/packet01-parsing/xdp_prog_kern.c
@@ -4,7 +4,9 @@
 #include <linux/in.h>
 #include <linux/if_ether.h>
 #include <linux/if_packet.h>
+#include <linux/ip.h>
 #include <linux/ipv6.h>
+#include <linux/icmp.h>
 #include <linux/icmpv6.h>
 #include <bpf/bpf_helpers.h>
 #include <bpf/bpf_endian.h>
@@ -70,6 +72,22 @@ static __always_inline int parse_ethhdr(struct hdr_cursor *nh,
        return *proto; /* network-byte-order */
 }

+static __always_inline int parse_iphdr(struct hdr_cursor *nh,
+                                       void *data_end,
+                                       struct iphdr **iphdr)
+{
+       struct iphdr *iph = nh->pos;
+
+       if (iph + 1 > data_end)
+               return -1;
+       int hdrsize = iph->ihl * 4;
+       if (nh->pos + hdrsize > data_end)
+               return -1;
+       nh->pos += hdrsize;
+       *iphdr = iph;
+       return iph->protocol;
+}
+
 /* Assignment 2: Implement and use this */
 static __always_inline int parse_ip6hdr(struct hdr_cursor *nh,
                                        void *data_end,
@@ -84,6 +102,19 @@ static __always_inline int parse_ip6hdr(struct hdr_cursor *nh,
        return ip6h->nexthdr;
 }

+static __always_inline int parse_icmphdr(struct hdr_cursor *nh,
+                                         void *data_end,
+                                         struct icmphdr **icmphdr)
+{
+       struct icmphdr *icmph = nh->pos;
+
+       if (icmph + 1 > data_end)
+               return -1;
+       nh->pos = icmph + 1;
+       *icmphdr = icmph;
+       return bpf_ntohs(icmph->un.echo.sequence);
+}
+
 /* Assignment 3: Implement and use this */
 static __always_inline int parse_icmp6hdr(struct hdr_cursor *nh,
                                          void *data_end,
@@ -104,7 +135,9 @@ int  xdp_parser_func(struct xdp_md *ctx)
        void *data_end = (void *)(long)ctx->data_end;
        void *data = (void *)(long)ctx->data;
        struct ethhdr *eth;
+       struct iphdr *ip;
        struct ipv6hdr *ipv6;
+       struct icmphdr *icmp;
        struct icmp6hdr *icmp6;
        int icmp_seq;

@@ -126,12 +159,19 @@ int  xdp_parser_func(struct xdp_md *ctx)
         * header type in the packet correct?), and bounds checking.
         */
        nh_type = parse_ethhdr(&nh, data_end, &eth);
-       if (nh_type != bpf_htons(ETH_P_IPV6))
-               goto out;
-       nh_type = parse_ip6hdr(&nh, data_end, &ipv6);
-       if (nh_type != IPPROTO_ICMPV6)
+       if (nh_type == bpf_htons(ETH_P_IP)) {
+               nh_type = parse_iphdr(&nh, data_end, &ip);
+               if (nh_type != IPPROTO_ICMP)
+                       goto out;
+               icmp_seq = parse_icmphdr(&nh, data_end, &icmp);
+       } else if (nh_type == bpf_htons(ETH_P_IPV6)) {
+               nh_type = parse_ip6hdr(&nh, data_end, &ipv6);
+               if (nh_type != IPPROTO_ICMPV6)
+                       goto out;
+               icmp_seq = parse_icmp6hdr(&nh, data_end, &icmp6);
+       } else {
                goto out;
-       icmp_seq = parse_icmp6hdr(&nh, data_end, &icmp6);
+       }
        if (icmp_seq % 2 == 1)
                goto out;

TEST

$ make
$ sudo ./xdp-loader unload veth-packet01 --all
$ sudo ./xdp-loader load --prog-name xdp_parser_func --mode skb  veth-packet01 xdp_prog_kern.o
$ ping 192.168.102.2
PING 192.168.102.2 (192.168.102.2) 56(84) bytes of data.
64 bytes from 192.168.102.2: icmp_seq=1 ttl=64 time=0.204 ms
64 bytes from 192.168.102.2: icmp_seq=3 ttl=64 time=0.104 ms
64 bytes from 192.168.102.2: icmp_seq=5 ttl=64 time=0.091 ms
64 bytes from 192.168.102.2: icmp_seq=7 ttl=64 time=0.087 ms
64 bytes from 192.168.102.2: icmp_seq=9 ttl=64 time=0.083 ms
64 bytes from 192.168.102.2: icmp_seq=11 ttl=64 time=0.086 ms
--- 192.168.102.2 ping statistics ---
11 packets transmitted, 6 received, 45.4545% packet loss, time 10223ms
rtt min/avg/max/mdev = 0.083/0.109/0.204/0.042 ms
$ ping -6 fc00:dead:cafe:2001::2
PING fc00:dead:cafe:2001::2(fc00:dead:cafe:2001::2) 56 data bytes
64 bytes from fc00:dead:cafe:2001::2: icmp_seq=1 ttl=64 time=0.074 ms
64 bytes from fc00:dead:cafe:2001::2: icmp_seq=3 ttl=64 time=0.095 ms
64 bytes from fc00:dead:cafe:2001::2: icmp_seq=5 ttl=64 time=0.088 ms
64 bytes from fc00:dead:cafe:2001::2: icmp_seq=7 ttl=64 time=0.088 ms
64 bytes from fc00:dead:cafe:2001::2: icmp_seq=9 ttl=64 time=0.090 ms
--- fc00:dead:cafe:2001::2 ping statistics ---
10 packets transmitted, 5 received, 50% packet loss, time 9201ms
rtt min/avg/max/mdev = 0.074/0.087/0.095/0.007 ms

補足

testenv.shコピペ用コマンドメモ

sudo ../testenv/testenv.sh setup --name veth-packet01
sudo ../testenv/testenv.sh setup --name veth-packet01 --vlan
sudo ../testenv/testenv.sh setup --name veth-packet01 --vlan --legacy-ip

sudo ../testenv/testenv.sh enter --name veth-packet01
sudo ../testenv/testenv.sh teardown

xdp-loaderコピペ用コマンドメモ

sudo ./xdp-loader load --prog-name xdp_parser_func --mode skb  veth-packet01 xdp_prog_kern.o
sudo ./xdp-loader unload veth-packet01 --all
sudo ./xdp-loader status

xdp-tutorial basic04-pinning-mapsをやってみる

github.com

Assignment1

eBPFプログラムが再ロードされた場合、xdp_statsプログラムでそれを検知して、mapを再取得するというもの。

解いてみる

誤ったファイルディスクリプタを使っていないかbpfマップを検証する関数を実装する

問題文に書いてある通り、新しいbpfプログラムが再ロードされたら、bpfマップのidが変わるので、それを利用して、誤ったファイルディスクリプタを使っていないか検証できる。

xdp_stats.c

@@ -180,6 +180,16 @@ static bool map_collect(int fd, __u32 map_type, __u32 key, struct record *rec)
        return true;
 }

+static int is_wrong_fd(__u32 id, char * pin_dir) {
+       struct bpf_map_info info = { 0 };
+       int map_fd = open_bpf_map_file(pin_dir, "xdp_stats_map", &info);
+       if (map_fd < 0) {
+               return -1;
+       }
+       close(map_fd);
+       return id != info.id;
+}
+
 static void stats_collect(int map_fd, __u32 map_type,
                          struct stats_record *stats_rec)
 {

stats_pollのループの中でbpfマップを検証する。

stats_pollのループの中でbpfマップを検証するように修正を行う。
open_bpf_map_fileでエラーが起きた場合にはxdp_statsをEXIT_FAILで終了させている。
EXIT_FAILはcommon_difine.hで定義されていた終了値コードで、これを使わなくても良いが、既存のコードに合わせて使用している。

xdp_stats.c

@@ -191,7 +201,7 @@ static void stats_collect(int map_fd, __u32 map_type,
        }
 }

-static void stats_poll(int map_fd, __u32 map_type, int interval)
+static int stats_poll(int map_fd, __u32 map_type, int interval, __u32 id, char * pin_dir)
 {
        struct stats_record prev, record = { 0 };

@@ -203,6 +213,11 @@ static void stats_poll(int map_fd, __u32 map_type, int interval)
        usleep(1000000/4);

        while (1) {
+               int result;
+               result = is_wrong_fd(id, pin_dir);
+               if (result != 0) {
+                       return result;
+               }
                prev = record; /* struct copy */
                stats_collect(map_fd, map_type, &record);
                stats_print(&record, &prev);
@@ -247,29 +262,38 @@ int main(int argc, char **argv)
                return EXIT_FAIL_OPTION;
        }

-       stats_map_fd = open_bpf_map_file(pin_dir, "xdp_stats_map", &info);
-       if (stats_map_fd < 0) {
-               return EXIT_FAIL_BPF;
-       }
-
-       /* check map info, e.g. datarec is expected size */
-       map_expect.key_size    = sizeof(__u32);
-       map_expect.value_size  = sizeof(struct datarec);
-       map_expect.max_entries = XDP_ACTION_MAX;
-       err = check_map_fd_info(&info, &map_expect);
-       if (err) {
-               fprintf(stderr, "ERR: map via FD not compatible\n");
-               return err;
-       }
-       if (verbose) {
-               printf("\nCollecting stats from BPF map\n");
-               printf(" - BPF map (bpf_map_type:%d) id:%d name:%s"
-                      " key_size:%d value_size:%d max_entries:%d\n",
-                      info.type, info.id, info.name,
-                      info.key_size, info.value_size, info.max_entries
-                      );
+       while (1) {
+               stats_map_fd = open_bpf_map_file(pin_dir, "xdp_stats_map", &info);
+               if (stats_map_fd < 0) {
+                       return EXIT_FAIL_BPF;
+               }
+
+               /* check map info, e.g. datarec is expected size */
+               map_expect.key_size    = sizeof(__u32);
+               map_expect.value_size  = sizeof(struct datarec);
+               map_expect.max_entries = XDP_ACTION_MAX;
+               err = check_map_fd_info(&info, &map_expect);
+               if (err) {
+                       fprintf(stderr, "ERR: map via FD not compatible\n");
+                       return err;
+               }
+               if (verbose) {
+                       printf("\nCollecting stats from BPF map\n");
+                       printf(" - BPF map (bpf_map_type:%d) id:%d name:%s"
+                              " key_size:%d value_size:%d max_entries:%d\n",
+                              info.type, info.id, info.name,
+                              info.key_size, info.value_size, info.max_entries
+                              );
+               }
+
+               int result;
+               result = stats_poll(stats_map_fd, info.type, interval, info.id, pin_dir);
+               if (result == -1) {
+                       goto ERROR_CASE;
+               }
+               printf("bpf map will be reloaded.\n");
        }
-
-       stats_poll(stats_map_fd, info.type, interval);
        return EXIT_OK;
+ERROR_CASE:
+       return EXIT_FAIL;
 }

試してみる

注意点としてbasic04では以下のようにprgonameを明示的に指定する必要がある。
また、attach_modeとしてデフォルトでXDP_MODE_NATIVEが指定されている。
私の環境はXDP_MODE_NATIVEを実行できる環境ではないので、-Aを指定してSKBモードで動作させている。

$ sudo ./xdp_loader --progname=xdp_pass_func --dev=lo -A
$ sudo ./xdp_stats --dev=lo

上記を実行後、別ウィンドウでxdpプログラムを以下のように再ロードする。

$ sudo ./xdp_loader --progname=xdp_pass_func --dev=lo -A

xdp_statsの出力は以下となった。

Collecting stats from BPF map
 - BPF map (bpf_map_type:6) id:71 name:xdp_stats_map key_size:4 value_size:16 max_entries:5
XDP-action
XDP_ABORTED            0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:0.250328
XDP_DROP               0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:0.250258
XDP_PASS               2 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:0.250259
XDP_TX                 0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:0.250259
XDP_REDIRECT           0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:0.250260

XDP-action
XDP_ABORTED            0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000448
XDP_DROP               0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000450
XDP_PASS               2 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000450
XDP_TX                 0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000450
XDP_REDIRECT           0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000450

XDP-action
XDP_ABORTED            0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000432
XDP_DROP               0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000432
XDP_PASS               2 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000432
XDP_TX                 0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000432
XDP_REDIRECT           0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000432

XDP-action
XDP_ABORTED            0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000417
XDP_DROP               0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000417
XDP_PASS               2 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000417
XDP_TX                 0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000417
XDP_REDIRECT           0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000417

XDP-action
XDP_ABORTED            0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000586
XDP_DROP               0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000586
XDP_PASS               2 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000586
XDP_TX                 0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000586
XDP_REDIRECT           0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000586

XDP-action
XDP_ABORTED            0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000425
XDP_DROP               0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000425
XDP_PASS               2 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000425
XDP_TX                 0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000425
XDP_REDIRECT           0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:2.000425

bpf map will be reloaded.

Collecting stats from BPF map
 - BPF map (bpf_map_type:6) id:79 name:xdp_stats_map key_size:4 value_size:16 max_entries:5
XDP-action
XDP_ABORTED            0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:0.250216
XDP_DROP               0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:0.250216
XDP_PASS               1 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:0.250216
XDP_TX                 0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:0.250216
XDP_REDIRECT           0 pkts (         0 pps)           0 Kbytes (     0 Mbits/s) period:0.250217

bpf_loaderを再実行したタイミングでbpf map will be reloadedというログが出るのを確認できた。

Assignment2

ピンどめされたbpfマップを再利用するように修正する。
再利用のやり方は問題文が載っているページにほぼ載っている。

解いてみる

問題文が載っているページのヒントにしたがって解く

ピンどめされたbpfマップがあるとき、そのままの状態だとそれをクリアしているが、それをクリアせずに再利用させる。

xdp_loader.c

@@ -69,7 +69,7 @@ const char *pin_basedir =  "/sys/fs/bpf";
 const char *map_name    =  "xdp_stats_map";

 /* Pinning maps under /sys/fs/bpf in subdir */
-int pin_maps_in_bpf_object(struct bpf_object *bpf_obj, const char *subdir)
+int pin_maps_in_bpf_object(struct bpf_object *bpf_obj, const char *subdir, char *filename)
 {
        char map_filename[PATH_MAX];
        char pin_dir[PATH_MAX];
@@ -90,16 +90,11 @@ int pin_maps_in_bpf_object(struct bpf_object *bpf_obj, const char *subdir)

        /* Existing/previous XDP prog might not have cleaned up */
        if (access(map_filename, F_OK ) != -1 ) {
-               if (verbose)
-                       printf(" - Unpinning (remove) prev maps in %s/\n",
-                              pin_dir);
-
-               /* Basically calls unlink(3) on map_filename */
-               err = bpf_object__unpin_maps(bpf_obj, pin_dir);
-               if (err) {
-                       fprintf(stderr, "ERR: UNpinning maps in %s\n", pin_dir);
-                       return EXIT_FAIL_BPF;
-               }
+               int pinned_map_fd = bpf_obj_get(map_filename);
+               struct bpf_object *obj = bpf_object__open(filename);
+               struct bpf_map    *map = bpf_object__find_map_by_name(obj, map_name);
+               bpf_map__reuse_fd(map, pinned_map_fd);
+               bpf_object__load(obj);
        }
        if (verbose)
                printf(" - Pinning maps in %s/\n", pin_dir);
@@ -150,7 +145,7 @@ int main(int argc, char **argv)
        }

        /* Use the --dev name as subdir for exporting/pinning maps */
-       err = pin_maps_in_bpf_object(xdp_program__bpf_obj(program), cfg.ifname);
+       err = pin_maps_in_bpf_object(xdp_program__bpf_obj(program), cfg.ifname, cfg.filename);
        if (err) {
                fprintf(stderr, "ERR: pinning maps\n");
                return err;

xdp-tutorial basic03-map-counterをやってみる

github.com

Assignment 1

現状の実装にbyteカウンタの機能を追加するというもの。

解いてみる

xdp_stats_mapのエントリの構造体にbyteカウンタのフィールドを追加する

eBPFマップは以下のように定義されている。

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, __u32);
    __type(value, struct datarec);
    __uint(max_entries, XDP_ACTION_MAX);
} xdp_stats_map SEC(".maps");

エントリの構造体であるdatarecrx_bytesというbyteカウンタのフィールドを追加する。

file: basic03-map-counter/common_kern_user.h

@@ -7,6 +7,7 @@
 /* This is the data record stored in the map */
 struct datarec {
        __u64 rx_packets;
+       __u64 rx_bytes;
        /* Assignment#1: Add byte counters */
 };
packetの長さを求めて、それを上で作ったrx_bytesというフィールドに加算する。

xdp programのctxに指定されているxdp_mdというやつから、packetの内容が読める。

/* user accessible metadata for XDP packet hook
 * new fields must be added to the end of this structure
 */
struct xdp_md {
    __u32 data;
    __u32 data_end;
    __u32 data_meta;
    /* Below access go through struct xdp_rxq_info */
    __u32 ingress_ifindex; /* rxq->dev->ifindex */
    __u32 rx_queue_index;  /* rxq->queue_index  */

    __u32 egress_ifindex;  /* txq->dev->ifindex */
};

xdp_mdtaカーネルによってリマップされた後の形で、実際はxdp_buffxdp_rxq_infoの形になっているようだ。(問題文の説明にそう書いてあった。)

struct xdp_buff {
    void *data;
    void *data_end;
    void *data_meta;
    void *data_hard_start;
    unsigned long handle;
    struct xdp_rxq_info *rxq;
} __attribute__((preserve_access_index));

struct xdp_rxq_info {
    /* Structure does not need to contain all entries,
    * as "preserve_access_index" will use BTF to fix this...
    */
    struct net_device *dev;
    __u32 queue_index;
} __attribute__((preserve_access_index));

dataがパケットの先頭のアドレスでdata_endがパケットの末尾のアドレスであるようだ。
つまりはdataとdata_endの距離がパケット長になる。

ということで以下のように修正。

basic03-map-counter/xdp_prog_kern.c

@@ -25,8 +25,8 @@ struct {
 SEC("xdp")
 int  xdp_stats1_func(struct xdp_md *ctx)
 {
-       // void *data_end = (void *)(long)ctx->data_end;
-       // void *data     = (void *)(long)ctx->data;
+       void *data_end = (void *)(long)ctx->data_end;
+       void *data     = (void *)(long)ctx->data;
        struct datarec *rec;
        __u32 key = XDP_PASS; /* XDP_PASS = 2 */

@@ -39,13 +39,14 @@ int  xdp_stats1_func(struct xdp_md *ctx)
        if (!rec)
                return XDP_ABORTED;

+
        /* Multiple CPUs can access data record. Thus, the accounting needs to
         * use an atomic operation.
         */
        lock_xadd(&rec->rx_packets, 1);
-        /* Assignment#1: Add byte counters
-         * - Hint look at struct xdp_md *ctx (copied below)
-         *
+       __u64 bytes = data_end - data;
+       lock_xadd(&rec->rx_bytes, bytes);
+        /*
          * Assignment#3: Avoid the atomic operation
          * - Hint there is a map type named BPF_MAP_TYPE_PERCPU_ARRAY
          */
ユーザ空間のプログラム側でmap上のbyteカウンタの値を取得する

rx_packetsと同様にrx_bytesの値を取得するようにする。

xdp_load_and_stats.c

@@ -181,6 +187,7 @@ static bool map_collect(int fd, __u32 map_type, __u32 key, struct record *
rec)

        /* Assignment#1: Add byte counters */
        rec->total.rx_packets = value.rx_packets;
+       rec->total.rx_bytes = value.rx_bytes;
        return true;
 }
ユーザ空間のプログラム側でmap上のbyteカウンタの情報を出力する

これもrx_packetsと同様にrx_bytesの値の情報を出力する。
(コード上のコメントに従えば、Mpbsを出力させる意図があるようだが、この後の検証でそんなにたくさんのデータを流す予定はないので、bpsに変更している。)

xdp_load_and_stats.c

@@ -118,12 +118,15 @@ static void stats_print(struct stats_record *stats_rec,
        struct record *rec, *prev;
        double period;
        __u64 packets;
+       __u64 bytes;
+       __u64 bits;
        double pps; /* packets per sec */
+       double bps; /* bits per sec */

        /* Assignment#2: Print other XDP actions stats  */
        {
                char *fmt = "%-12s %'11lld pkts (%'10.0f pps)"
-                       //" %'11lld Kbytes (%'6.0f Mbits/s)"
+                       " %'11lld bytes (%'6.0f bits/s)"
                        " period:%f\n";
                const char *action = action2str(XDP_PASS);
                rec  = &stats_rec->stats[0];
@@ -135,8 +138,11 @@ static void stats_print(struct stats_record *stats_rec,

                packets = rec->total.rx_packets - prev->total.rx_packets;
                pps     = packets / period;
+               bytes   = rec->total.rx_bytes - prev->total.rx_bytes;
+               bits    = bytes << 3;
+               bps     = bits / period;

-               printf(fmt, action, rec->total.rx_packets, pps, period);
+               printf(fmt, action, rec->total.rx_packets, pps, rec->total.rx_bytes, bps, period);
        }
 }

検証

$ make
    CC       xdp_load_and_stats
    CLANG    xdp_prog_kern.o
    LLC      xdp_prog_kern.o
$ sudo ./xdp_load_and_stats -d lo
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
Success: Loaded BPF-object(xdp_prog_kern.o) and used section(xdp_stats1_func)
 - XDP prog id:211 attached on device:lo(ifindex:1)

Collecting stats from BPF map
 - BPF map (bpf_map_type:2) id:75 name:xdp_stats_map key_size:4 value_size:16 max_entries:5

XDP-action
XDP_PASS               1 pkts (         4 pps)          90 bytes (  2877 bits/s) period:0.250223
XDP_PASS               2 pkts (         0 pps)         180 bytes (   360 bits/s) period:2.000256
XDP_PASS               2 pkts (         0 pps)         180 bytes (     0 bits/s) period:2.000286
XDP_PASS               2 pkts (         0 pps)         180 bytes (     0 bits/s) period:2.000314
XDP_PASS               2 pkts (         0 pps)         180 bytes (     0 bits/s) period:2.000300
XDP_PASS               4 pkts (         1 pps)         376 bytes (   784 bits/s) period:2.000297
XDP_PASS               4 pkts (         0 pps)         376 bytes (     0 bits/s) period:2.000259
XDP_PASS               4 pkts (         0 pps)         376 bytes (     0 bits/s) period:2.000316

packetのカウンタが2pktsから4pktsに上がっている部分がある。
ここのところで実は以下の通り、pingを打った。

$ping 127.0.0.1 -c 1

この部分のbytesカウンタを見ると180から376に上がっている。 つまり、このとき、loを通過したicmpパケット合計が196 bytes(376-180)であれば、正しく実装できたということ。

ここで取得したpcapを見てみる。

requestとreplyがそれぞれ98 bytesで合計196 bytesなのでうまくいっている。

Assignment 2

user 空間側のプログラムを修正してxdp_actionごとにstatsを集計するようにするというもの。

xdp_actionとは以下のこと。

linux/bpf.h

/* User return codes for XDP prog type.
 * A valid XDP program must return one of these defined values. All other
 * return codes are reserved for future use. Unknown return codes will
 * result in packet drops and a warning via bpf_warn_invalid_xdp_action().
 */
enum xdp_action {
    XDP_ABORTED = 0,
    XDP_DROP,
    XDP_PASS,
    XDP_TX,
    XDP_REDIRECT,
};

解いてみる

xdp_stats_mapの構造を見てみる

xdp_stats_mapは以下のようになっている。

xdp_prog_kern.c

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, __u32);
    __type(value, struct datarec);
    __uint(max_entries, XDP_ACTION_MAX);
} xdp_stats_map SEC(".maps");

XDP_ACTION_MAXは以下のように定義されている。

common_kern_user.h

#ifndef XDP_ACTION_MAX
#define XDP_ACTION_MAX (XDP_REDIRECT + 1)
#endif

つまり、xdp_stats_mapはxdp_actionの種類分だけエントリがあるということ。
そして、xdp_stats_mapのkeyにはxdp_actionが指定される。

xdp_prog_kern.c

 __u32 key = XDP_PASS; /* XDP_PASS = 2 */

    /* Lookup in kernel BPF-side return pointer to actual data record */
    rec = bpf_map_lookup_elem(&xdp_stats_map, &key);

複数のエントリを扱えるように改良する

xdp_load_and_statsプログラムではxdp_stats_mapのエントリをstats_recordという構造体に入れて扱う。
stats_recordはxdp_stats_mapのエントリの生の構造体であるdata_recをラップしたものになっている。

xdp_load_and_stats.c

struct record {
    __u64 timestamp;
    struct datarec total; /* defined in common_kern_user.h */
};

struct stats_record {
    struct record stats[1];
};

stats_recordがxdp_stats_mapのエントリをxdp_action個数分保存できるように修正する。

mapからxdp_actionの個数分だけ、エントリを取り出す。

xdp_load_and_stats.c

@@ -97,7 +97,7 @@ struct record {
 };

 struct stats_record {
-       struct record stats[1]; /* Assignment#2: Hint */
+       struct record stats[XDP_ACTION_MAX];
 };

xdp_load_and_stats.c

@@ -194,10 +194,11 @@ static bool map_collect(int fd, __u32 map_type, __u32 key, struct record *rec)
 static void stats_collect(int map_fd, __u32 map_type,
                          struct stats_record *stats_rec)
 {
-       /* Assignment#2: Collect other XDP actions stats  */
-       __u32 key = XDP_PASS;
-
-       map_collect(map_fd, map_type, key, &stats_rec->stats[0]);
+       map_collect(map_fd, map_type, XDP_ABORTED, &stats_rec->stats[XDP_ABORTED]);
+       map_collect(map_fd, map_type, XDP_DROP, &stats_rec->stats[XDP_DROP]);
+       map_collect(map_fd, map_type, XDP_PASS, &stats_rec->stats[XDP_PASS]);
+       map_collect(map_fd, map_type, XDP_TX, &stats_rec->stats[XDP_TX]);
+       map_collect(map_fd, map_type, XDP_REDIRECT, &stats_rec->stats[XDP_REDIRECT]);
 }

 static void stats_poll(int map_fd, __u32 map_type, int interval)

statsの出力を修正する

xdp_load_and_stats.c

@@ -123,14 +123,14 @@ static void stats_print(struct stats_record *stats_rec,
        double pps; /* packets per sec */
        double bps; /* bits per sec */

-       /* Assignment#2: Print other XDP actions stats  */
-       {
+       int xdpact = 0;
+       for (xdpact = 0; xdpact < XDP_ACTION_MAX; xdpact++) {
                char *fmt = "%-12s %'11lld pkts (%'10.0f pps)"
                        " %'11lld bytes (%'6.0f bits/s)"
                        " period:%f\n";
-               const char *action = action2str(XDP_PASS);
-               rec  = &stats_rec->stats[0];
-               prev = &stats_prev->stats[0];
+               const char *action = action2str(xdpact);
+               rec  = &stats_rec->stats[xdpact];
+               prev = &stats_prev->stats[xdpact];

                period = calc_period(rec, prev);
                if (period == 0)

動かしてみる

xdp_actionごとにstatsが出力される。ただし、kernel側で動くプログラムにはタッチしてないので、XDP_PASSしかカウンタは上がらない。

$ sudo ./xdp_load_and_stats --dev=lo
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
Success: Loaded BPF-object(xdp_prog_kern.o) and used section(xdp_stats1_func)
 - XDP prog id:52 attached on device:lo(ifindex:1)

Collecting stats from BPF map
 - BPF map (bpf_map_type:2) id:11 name:xdp_stats_map key_size:4 value_size:16 max_entries:5

XDP-action
XDP_ABORTED            0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250372
XDP_DROP               0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250382
XDP_PASS               1 pkts (         4 pps)          90 bytes (  2876 bits/s) period:0.250382
XDP_TX                 0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250382
XDP_REDIRECT           0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250382
XDP_ABORTED            0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000472
XDP_DROP               0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000472
XDP_PASS               2 pkts (         0 pps)         180 bytes (   360 bits/s) period:2.000472
XDP_TX                 0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000472
XDP_REDIRECT           0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000472
XDP_ABORTED            0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000388
XDP_DROP               0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000387
XDP_PASS               2 pkts (         0 pps)         180 bytes (     0 bits/s) period:2.000387
XDP_TX                 0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000386
XDP_REDIRECT           0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000386

Assignment3

atomicな加算処理はコストが高いので、これをやめて、BPF_MAP_TYPE_PERCPU_ARRAYのeBPFマップを使うというもの。

atomicな加算とは何か?

__sync_fetch_and_addはコンパイラで提供される組み込み関数でこれを使うとatomicな加算が実現できる。

#ifndef lock_xadd
#define lock_xadd(ptr, val) ((void) __sync_fetch_and_add(ptr, val))
#endif

BPF_MAP_TYPE_PERCPU_ARRAYとは何か?

CPU毎にメモリ領域を割り当ててくれるBPF_MAP_TYPE_ARRAY。

CPU毎にメモリ領域がそれぞれ存在するため、値の取り出しをするときは以下のようにcpuを指定してやる必要がある。

void *bpf_map_lookup_percpu_elem(struct bpf_map *map, const void *key, u32 cpu)

docs.kernel.org

解いてみる

MAPの種類を変更する

xdp_prog_kern.c

@@ -9,7 +9,7 @@
  * - The idea is to keep stats per (enum) xdp_action
  */
 struct {
-       __uint(type, BPF_MAP_TYPE_ARRAY);
+       __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
        __type(key, __u32);
        __type(value, struct datarec);
        __uint(max_entries, XDP_ACTION_MAX);

atomicな加算をやめて、普通に加算する

xdp_prog_kern.c

@@ -43,13 +43,9 @@ int  xdp_stats1_func(struct xdp_md *ctx)
        /* Multiple CPUs can access data record. Thus, the accounting needs to
         * use an atomic operation.
         */
-       lock_xadd(&rec->rx_packets, 1);
+       rec->rx_packets++;
        __u64 bytes = data_end - data;
-       lock_xadd(&rec->rx_bytes, bytes);
-        /*
-         * Assignment#3: Avoid the atomic operation
-         * - Hint there is a map type named BPF_MAP_TYPE_PERCPU_ARRAY
-         */
+       rec->rx_bytes += bytes;

        return XDP_PASS;
 }

ユーザ空間での値の集計

(問題文でcpuごとの値の集計を行うコードが提供されているので、それをそのまま使っている。)

xdp_load_and_stats.c

@@ -158,11 +158,26 @@ void map_get_value_array(int fd, __u32 key, struct datarec *value)
 /* BPF_MAP_TYPE_PERCPU_ARRAY */
 void map_get_value_percpu_array(int fd, __u32 key, struct datarec *value)
 {
-       /* For percpu maps, userspace gets a value per possible CPU */
-       // unsigned int nr_cpus = libbpf_num_possible_cpus();
-       // struct datarec values[nr_cpus];
+       /* For percpu maps, user space gets a value per possible CPU */
+       unsigned int nr_cpus = libbpf_num_possible_cpus();
+       struct datarec values[nr_cpus];
+       __u64 sum_bytes = 0;
+       __u64 sum_pkts = 0;
+       int i;
+
+       if ((bpf_map_lookup_elem(fd, &key, values)) != 0) {
+               fprintf(stderr,
+                       "ERR: bpf_map_lookup_elem failed key:0x%X\n", key);
+               return;
+       struct datarec values[nr_cpus];
+       __u64 sum_bytes = 0;
+       __u64 sum_pkts = 0;
+       int i;
+
+       if ((bpf_map_lookup_elem(fd, &key, values)) != 0) {
+               fprintf(stderr,
+                       "ERR: bpf_map_lookup_elem failed key:0x%X\n", key);
+               return;
+       }

-       fprintf(stderr, "ERR: %s() not impl. see assignment#3", __func__);
+       /* Sum values from each CPU */
+       for (i = 0; i < nr_cpus; i++) {
+               sum_pkts  += values[i].rx_packets;
+               sum_bytes += values[i].rx_bytes;
+       }
+       value->rx_packets = sum_pkts;
+       value->rx_bytes   = sum_bytes;
 }

 static bool map_collect(int fd, __u32 map_type, __u32 key, struct record *rec)
@@ -177,7 +192,8 @@ static bool map_collect(int fd, __u32 map_type, __u32 key, struct record *
rec)
                map_get_value_array(fd, key, &value);
                break;
        case BPF_MAP_TYPE_PERCPU_ARRAY:
-               /* fall-through */
+               map_get_value_percpu_array(fd, key, &value);
+               break;
        default:
                fprintf(stderr, "ERR: Unknown map_type(%u) cannot handle\n",
                        map_type);

解説

カーネル空間

  • 適当なCPUでマップを更新する。

ユーザ空間

  • 各CPUに個別に割当らているマップが持つカウンタを全て足し合わせて、それをstatsとして表示する。

動かしてみる

$ sudo ./xdp_load_and_stats --dev=lo
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
Success: Loaded BPF-object(xdp_prog_kern.o) and used section(xdp_stats1_func)
 - XDP prog id:74 attached on device:lo(ifindex:1)

 Collecting stats from BPF map
  - BPF map (bpf_map_type:6) id:19 name:xdp_stats_map key_size:4 value_size:16 max_entries:5

  XDP-action
  XDP_ABORTED            0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250211
  XDP_DROP               0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250183
  XDP_PASS               1 pkts (         4 pps)          90 bytes (  2878 bits/s) period:0.250184
  XDP_TX                 0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250184
  XDP_REDIRECT           0 pkts (         0 pps)           0 bytes (     0 bits/s) period:0.250184
  XDP_ABORTED            0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000381
  XDP_DROP               0 pkts (         0 pps)           0 bytes (     0 bits/s) period:2.000383

vim-lsp/clangd/bear/compdb

clangd

clangdはclangのLSPサーバ。
例えば、Makefileで-Iオプションとかを使ってて、それによってエディタがincludeパスを正しく見つけられなくて、エラーを出すみたいな問題を解決できる。

vim-lsp

LSPクライアントはvim-lspをつかって、その設定はvim-lsp-settingsに任せている。

github.com

github.com

bear/compdb

# bear -- <BUILD COMMAND>
bear -- make 

上記を実行するとcompile_commands.jsonというのがカレントディレクトリに生成される。 compile_commands.jsonが見るコンパイル情報のデータベースと思えば良い。
そして、bearはmakeの実行内容を見てくれて、compile_commands.jsonを生成してくれる。 compile_commands.jsonのあるディレクトリ以下ではエディタはclangd経由で適切にincludeパス等を解決できるようになる。

ヘッダファイルについてLSPで面倒みてもらうためにはcompdbが使える。 以下を実行するとcompile_commands.jsonにヘッダファイルの情報が追記される。

compdb list | sponge compile_commands.json

github.com

github.com

uchan.hateblo.jp

ヘッダーファイル中に未定義の型がある問題

ヘッダーファイルとかだと、ヘッダーファイル単体では未定義の型とかがある場合がある(そのヘッダファイルが依存する他のヘッダーファイルをincludeしてない場合等)。 この状態で最終的なビルドでは問題なくても、vimでそういうヘッダーファイルを開くとLSPがエラーを出してしまう。 これに対するうまい解決方法がわからないので、現状だと、compile_commands.jsonの該当のヘッダーファイルに関する記述の部分にgccの-includeオプションを手動で追記して無理やり依存を解決させている。

perfがうまくいかなかったメモ

xdp_exceptionのイベントを確認したくて、perfを実行したら、以下のようにエラーに遭遇した。

sudo perf record -a -e xdp:xdp_exception sleep 4 
event syntax error: 'xdp:xdp_exception'
                     \___ unsupported tracepoint

libtraceevent is necessary for tracepoint support
Run 'perf list' for a list of valid events

 Usage: perf record [<options>] [<command>]
    or: perf record [<options>] -- <command> [<options>]

    -e, --event <event>   event selector. use 'perf list' to list available events

以下を確認したこと

  • perf listの出力にはxdp:xdp_exceptionはちゃんと載っている。
  • libtraceeventがないかと思ってみて調べたが、ちゃんとlibtraceevent-devパッケージはインストールされている。

原因わからないので、ワークアラウンドとして以下でxdp_exceptionのイベントを確認することにした。

sudo trace-cmd record -e 'xdp:xdp_exception' sleep 4
sudo trace-cmd report

valinux.hatenablog.com

XDPプログラムをCLIでロード/アンロードする方法

iproute2

ロード

sudo ip link set dev DEVICE  [ 
    { xdp | xdpgeneric | xdpdrv | xdpoffload } 
        { object FILE [ section NAME ] [ verbose ] | pinned FILE }
]

アンロード

sudo ip link set dev DEVICE xdpgeneric off

xdp-loader

xdp-loaderはxdp-toolsについてくるコマンドツールで、XDPプログラムの簡単な操作を行える。

github.com

ロード

sudo xdp-loader load [options] <ifname> <filenames>

例えば、loにhoge.oというXDPプログラムをxdpgenericモードでロードするには以下のようにする。

sudo xdp-loader load -m skb lo hoge.o

アンロード

sudo xdp-loader unload [options]  <ifname> 

例えば、loにロードされている全てのXDPプログラムをアンロードするには以下のようにする。

sudo xdp-loader unload --all lo