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
.
- Specify the pinned Map at
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 keynext_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.