Quick Start Guide to XDP: Part 2 (eBPF Map)
Server Network Programming
Published: 2024-09-17

Overview

In the previous article, I wrote a program using XDP to DROP all packets.

This time, I’ll write a program that counts the number of packets that were DROPPED using eBPF Maps.

Again, this discussion is for those looking to quickly get hands-on with XDP + eBPF Maps.
Therefore, detailed explanations of eBPF and types of Maps not being used will be omitted.

Environment Setup

This guide assumes an Ubuntu 22.04 setup.
It is recommended to test in a virtual machine, such as a VM, to control NIC communication with XDP.

Please refer to the previous article for setup instructions.

eBPF Map

eBPF Maps are data structures used to share data between the kernel and user space. This allows the XDP program to update data in real-time, which can be read by user space.

This time, I will write a program to count the number of packets DROPPED by XDP.

Types of eBPF Maps

There are various types of eBPF Maps.
Details can be found in the Linux Kernel Documentation BPF Map.
This time, the following type will be used.

  • BPF_MAP_TYPE_HASH
    • This Map represents a hash table.
    • It can store pairs of hash keys and values.

In this case, the key will be the source IP address of the packet, and the value will store the number of DROPPED packets.

Creating an eBPF Map

The eBPF Map is defined within the eBPF program as follows:

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: Specifies the type of the Map.
  • key: Specifies the type of the key. In this case, since the key is an IPv4 address, uint32_t is specified.
  • value: Specifies the type of the value. Here, uint64_t is specified to store the number of DROPPED packets.
  • max_entries: Specifies the maximum number of entries that can be stored in the Map.
    • Exceeding the number of entries in BPF_MAP_TYPE_HASH results in an error.
  • icmp_count_map: Specifies the name of the Map.
  • SEC(".maps"): Specifies the section to define the Map.

Operations on eBPF Maps

eBPF Maps are operated on from within the eBPF program as follows:

  • bpf_map_lookup_elem: Retrieves the value that corresponds to a key from the Map.
  • bpf_map_update_elem: Adds a key-value pair to the Map.
  • bpf_map_delete_elem: Deletes the value corresponding to a key from the Map.
  • bpf_map_get_next_key: Retrieves the next key from the Map.

Writing the XDP Program

Program Using eBPF Map

Here is a program that uses 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>

// Definition of 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;

    // Get the pointer to the Ethernet header
    struct ethhdr *eth = data;
    if ((void *)eth + sizeof(*eth) > data_end)
        return XDP_PASS;

    // Check if the Ethernet frame contains an IP packet
    if (eth->h_proto != htons(ETH_P_IP))
        return XDP_PASS;

    // Get the pointer to the IP header
    struct iphdr *ip = data + sizeof(*eth);
    if ((void *)ip + sizeof(*ip) > data_end)
        return XDP_PASS;

    // Check if the protocol is ICMP
    if (ip->protocol != IPPROTO_ICMP)
        return XDP_PASS;

    // Get the pointer to the ICMP header
    struct icmphdr *icmp = (void *)ip + sizeof(*ip);
    if ((void *)icmp + sizeof(*icmp) > data_end)
        return XDP_PASS;

    // Extract the source IP address
    uint32_t src_ip = ip->saddr;

    // Search icmp_count_map by source IP address
    uint64_t *count = bpf_map_lookup_elem(&icmp_count_map, &src_ip);

    // Increment the count if the pointer to u64 is not NULL
    if (count) {
        __sync_fetch_and_add(count, 1);
    } else {
        // If NULL, add to icmp_count_map
        bpf_map_update_elem(&icmp_count_map, &src_ip, &(uint64_t){1}, BPF_ANY);
    }  

    // Drop ICMP packets
    return XDP_DROP;
}

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

Build and Load

Build the xdp.c and load the XDP program:

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

Accessing Map from User Space

Setting Pin to Access the Map

To access the eBPF Map from user space, we need to pin the Map.
This allows the Map to be referenced from user space.

  • Get the map id of the BPF program

    $ sudo bpftool map
    16: hash  name icmp_count_map  flags 0x0
            key 4B  value 8B  max_entries 1024  memlock 86144B
            btf_id 25
    
    • 16 is the ID of the Map.
  • Set the pin on the Map

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

Writing a User Space Program to Get the eBPF Map

Now that the Map has been pinned, let’s reference it from user space.

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

    // Display Map information
    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);

    // Retrieve all 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; // Use the updated key
        if (bpf_map_lookup_elem(map_fd, &key, &value) == 0) {
            // Convert IP address to string
            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; // Prepare for the next loop
    }

    return 0;
}
  • bpf_obj_get: Retrieves the file descriptor of the pinned Map.

    • Specify the pinned Map at /sys/fs/bpf/xdp/icmp_count_map.
  • bpf_obj_get_info_by_fd: Retrieves information about the Map.

    • Gathers the Map’s name, type, key size, value size, and maximum entries.
  • bpf_map_get_next_key: Retrieves the next key from the Map.

    • key: current key
    • next_key: next key
  • bpf_map_lookup_elem: Retrieves the value corresponding to a key from the Map.

Build and Run

$ 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

This will display the source IP addresses and counts of the ICMP packets dropped by the XDP program.

Cleanup

  • Unpin the Map

    $ sudo rm -iv /sys/fs/bpf/xdp/icmp_count_map
    
  • Unload the XDP program

    $ sudo ip link set dev eth0 xdp off
    

Loading XDP and Referencing Map in a Persistent Program

It can be cumbersome to load the XDP program every time with the ip command, and then set pins to access the Map.
If using a persistent program, you can retrieve the file descriptor of the Map when loading the XDP program, allowing access to the Map without pinning.

Also, when exiting the program, it can unload the XDP program to prevent forgetting to unload or unpin.

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

    // Load the XDP program
    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;
    }

    // Get the program
    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;
    }

    // Get the 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;
    }

    // Attach the XDP program to eth0
    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;
    }

    // Infinite loop to display the state of the map every second
    while (1) {
        // Separator line
        printf("==============================\n");
        // Display Map information
        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);

        // Retrieve all 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; // Use updated key
            if (bpf_map_lookup_elem(map_fd, &key, &value) == 0) {
                // Convert IP address to string
                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; // Prepare for the next loop
        }
        sleep(1);  // Wait 1 second before looping again
    }

    return 0;
}
  • bpf_object__open_file: Opens the BPF object from a file.
    • Specify the pre-built XDP object file.
  • bpf_object__load: Loads the BPF object.
    • This allows for obtaining programs and Maps.
  • bpf_object__find_program_by_name: Retrieves the BPF program by name.
    • Specify the name of the XDP program as defined by sec(“xdp”) followed by the function name.
  • bpf_object__find_map_by_name: Retrieves the BPF Map by name.
    • You specify the name of the Map.
  • bpf_map__fd: Retrieves the file descriptor of the Map.
  • bpf_program__attach_xdp: Attaches the XDP program to the specified interface.

Build and Run

  • Build

    $ gcc -o map_monitor map_monitor.c -lbpf
    
  • Run

    ./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
    

    Exit with Ctrl+C.

When you exit, the XDP program will be unloaded.

Conclusion

I have written a program that counts the number of packets dropped by the XDP program.
By using Maps, it allows for data exchange between eBPF programs and user space programs, broadening the potential applications of XDP.

Final Note

In terms of performance, using BPF_MAP_TYPE_PERCPU_HASH would be ideal, but for simplicity, BPF_MAP_TYPE_HASH was used this time.