My home network has a setup with powerline communications and wireless devices, I’ve always been interested in monitoring devices connecting to my network. Unfortunately, my existing router (Asus RT-AC1200G+) had limitations when it came to detecting and reporting new MAC addresses in real time. This pushed me to explore alternatives. Enter OpenWRT and the power of eBPF!

First, OpenWRT, a customizable Linux-based firmware for routers, allowed me to take full control of my network’s behavior. By leveraging eBPF, I was able to monitor incoming traffic, detect new MAC addresses, and log events to a syslog server for visibility.

Here’s a high-level view of the solution I built:

Router Setup with OpenWRT: I installed OpenWRT on my rasbperry Pi

eBPF Program for MAC Address Detection: I wrote an eBPF program that hooks into the networking stack (via XDP) to inspect incoming packets and extract source MAC addresses. It compares each MAC address against a hash map of known devices. If a new address is detected, it logs an event. (Motivation by BPF Performance Book)

Syslog Integration: Detected events are sent to a syslog server running on a separate machine in my network, which consolidates logs for analysis.

I generally WFH, so I did not want to mess with my original setup. I did considered to flash OpenWRT on my router, but rather I picked a Raspberry Pi (Model 3B) for this task, and acting as a router.

Not going to go into details on how to install OpenWRT in a raspberry Pi, otherwise this is going to be waaaay toooooo loooooong, but is quite straight forward, see Official OpenWRT site for details

  1. Set up OpenWRT
  • Logging to your newly flashed OpenWRT system, update it and install the relevant tools
opkg update
opkg install bpftools libbpf
  1. Writing the eBPF Program
  • This eBPF program inspects packets at the XDP layer. Here’s the code:
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>

struct bpf_map_def SEC("maps") mac_seen_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = ETH_ALEN, // MAC address size (6 bytes)
    .value_size = sizeof(__u8), // Dummy value
    .max_entries = 1024, // Max number of MACs to track
};

SEC("xdp")
int detect_new_mac(struct xdp_md *ctx) {
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    // Parse Ethernet header
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end) {
        return XDP_PASS;
    }

    // Check if the source MAC is already known
    __u8 *value;
    value = bpf_map_lookup_elem(&mac_seen_map, eth->h_source);
    if (!value) {
        // New MAC detected, log it
        __u8 dummy = 1;
        bpf_map_update_elem(&mac_seen_map, eth->h_source, &dummy, BPF_ANY);

        // Log MAC to syslog via printk
        bpf_printk("New MAC detected: %02x:%02x:%02x:%02x:%02x:%02x\n",
                   eth->h_source[0], eth->h_source[1], eth->h_source[2],
                   eth->h_source[3], eth->h_source[4], eth->h_source[5]);
    }

    return XDP_PASS; // Continue normal packet processing
    char _license[] SEC("license") = "GPL";
}

A bit of context of the script, if you are not familiar with eBPF, this could be a bit harsh to get put on your head…

First, let’s define a Hash Map to store the MACs that have already been seen (bpf_map_def), SEC(“maps”): Marks this structure as a BPF map, which the kernel loads during program initialization, particularely useful as we are storing this in memory for performance reasons :) Imagine dumping onto a MySQL :P

I did set a max of 1024 as .max_entries as I think this is more than enough for this purpose. Obviously, I do not expect more than 100 MACs or… maybe yes ? 😱

struct bpf_map_def SEC("maps") mac_seen_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = ETH_ALEN, // MAC address size (6 bytes)
    .value_size = sizeof(__u8), // Dummy value
    .max_entries = 1024, // Max number of MACs to track
};

Once inside the main program, SEC(“xdp”) declares this function as an XDP program, which attaches to the data path for processing packets early in the kernel. The entry point is detect_new_mac struct xdp_md gives you metadata about the packet being processed, including pointers to the packet data and its size.

SEC("xdp")
int detect_new_mac(struct xdp_md *ctx) {

These pointers are critical for bounds checking to ensure the eBPF program doesn’t read beyond the packet’s allocated memory, which would be unsafe and cause the program to fail verification, lovely segfault

void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;

the nex block casts the packet data to an Ethernet header (struct ethhdr) to access its fields.

We must ensure that the eth header is whitin the packet bounds, if not, the program exists with XDP_PASS to let the kernel continue processing the packet normally, to finally pass it to the kernel networking stack.

struct ethhdr *eth = data;
  if ((void *)(eth + 1) > data_end) {
  	return XDP_PASS;
}

We check for existing mac address, bpf_map_lookup_elem seeks up the source MAC address (eth->h_source - pointer to 6-byte source MAC in the Ether header) in the mac_seen_map

__u8 *value;
value = bpf_map_lookup_elem(&mac_seen_map, eth->h_source);
if (!value) 

Later, we store the New MAC Address. create a dummy value (1 byte) to store in the map. The actual value is irrelevant in this use case. bpf_map_update_elem: Adds the new MAC address (eth->h_source) and its dummy value to the mac_seen_map. BPF_ANY: Overwrite existing entries if the key already exists.

 __u8 dummy = 1;
 bpf_map_update_elem(&mac_seen_map, eth->h_source, &dummy, BPF_ANY);

Logging the MAC address, logs the detected MAC address to the kernel’s trace buffer. Each byte of the MAC address is formatted as hexadecimal (%02x).

bpf_printk("New MAC detected: %02x:%02x:%02x:%02x:%02x:%02x\n",
    eth->h_source[0], eth->h_source[1], eth->h_source[2],
    eth->h_source[3], eth->h_source[4], eth->h_source[5]);

Lastly, If a new MAC is detected, it’s logged, and the program continues processing. XDP_PASS allows the packet to pass through the rest of the networking stack.

return XDP_PASS; // Continue normal packet processing
  1. Compile

That was a tough one, to compile and deploy the eBPF program for OpenWRT, you’ll need to cross-compile the eBPF program and its loader for the architecture of your OpenWRT device.

You need the toolchain https://openwrt.org/docs/guide-developer/toolchain/start and match with your architecture on your alternative system.

tar -xjf openwrt-toolchain-<arch>-<version>.tar.bz2
cd openwrt-toolchain-<arch>-<version>

Then, install the dependenices (I use a Debian amd64)

sudo apt update
sudo apt install clang llvm gcc libbpf-dev make

Set the Cross-Compiler Path: Export the toolchain binaries:

export STAGING_DIR=/path/to/toolchain/staging_dir
export PATH=$STAGING_DIR/toolchain-*/bin:$PATH

Compile the program!

clang -O2 -target bpf -D__KERNEL__ -D__BPF_TRACING__ -Wall -Wno-unused-value -Wno-pointer-sign \
    -Wno-compare-distinct-pointer-types -c mac_detector.c -o mac_detector.o

Compile the loader!

gcc -o loader loader.c -lbpf

And do not forget to transfer the objects :)

scp mac_detector.o loader root@<openwrt-ip>:/tmp/
  1. Load the eBPF Program
  • I used bpftool to lead the eBPF program like this:
owrt# bpftool prog loader mac_detector.o /sys/fs/bpf/detect_new_mac
  • Then attach it to the relevant network interface
    owrt# ip link set dev eth0 xdp obj /sys/fs/bpf/detect_new_mac
  1. Integrating with the Syslog Server The eBPF program uses bpf_printk to log messages. These messages are picked up by the kernel and can be forwarded to a syslog server. I configured OpenWRT’s syslog to send kernel logs to a central server (on another Pi)
  • Install and Configure rsyslog

    owrt# opkg install rsyslog

  • Do not forget the config on the service, edit /etc/rsyslog.confg

    *.* @10.10.10.50:514

  • (Replace 10.10.10.50 with the IP address of your syslog server.), Obviosly :)

And you should be done…

Validate with dmesg…

[ 1685587200.123456] eBPF program loaded successfully: ID=42
[ 1685587200.223456] XDP program attached to interface eth0
[ 1685587205.345678] Packet received: src=10.10.10.36 dst=192.168.1.1
[ 1685587210.456789] New MAC address detected: MAC=aa:bb:cc:dd:ee:ff
[ 1685587215.567890] Event logged for MAC address: aa:bb:cc:dd:ee:ff

Each time a new MAC address appeared on the network, an entry like this was logged to the syslog server:

Jun 01 18:05:23 router kernel: New MAC detected: aa:bb:cc:dd:ee:ff

Over time, I had a consolidated log of every new device joining my network, which was useful for identifying unauthorized devices or debugging network issues, which make me a bit less paranoid. Also, this small excersice helped me out to understand how can I use eBPF for monitoring, which was really interesting.

Errors I dealt with (Hopefully it helps you if unlucky)

Error:

error: unknown target triple 'bpf', please use -triple or -march

Cause: This happens if the clang toolchain is missing or improperly configured for the bpf target.

Solution: Ensure that clang is installed and configured correctly. Use this command to explicitly set the target:

clang -target bpf -c mac_detector.c -o mac_detector.o

Error:

Failed to load eBPF object

The eBPF program has syntax or logic errors. Kernel features required by the program (e.g., CONFIG_BPF) are not enabled.

Solution:

  • Verify the kernel configuration supports eBPF:
zgrep CONFIG_BPF /proc/config.gz

Debug the eBPF program with bpftool:

bpftool prog load mac_detector.o /sys/fs/bpf/mac_detector
bpftool prog show

Error:

Program type not supported

Cause: The kernel does not support the specified eBPF program type, such as XDP.

Solution: Ensure the kernel version is correct, or upgrade/downgrade the program to use supported features.