6 minutes
Coding Netlink and Netfilter binaries
While I was working on reproducing some N-Day Linux kernel exploits I realized that (as often occurs in the low-level world) interacting with the kernel through netlink code is poorly documented.
I mean, code itself is the greatest documentation, but spending a few words on it never hurts and will save a lot of time googling around to make sense of it.
The main netfilter interface from userspace is through the nft system administration tool that is implemented using the libraries libmnl and libnftnl. We will use those libs in this guide to create sample binaries.
Specifically, we will see how to set up netlink communication and interact with the netfilter subsystem.
Pointers to correctly implement C code will be given to make the reader quickly operational.
The advantage of writing C programs directly over using nft is that you can finely control the logic and reach the right paths to trigger exploits in the kernel.
A query for netfilter in the www.cve.org archive returns 390 vulnerabilities at the time of this writing.
This is why I dedicate a blog post to this topic as a foundation for further vulnerability analysis articles.
Moreover, almost all the userspace netlink and netfilter tools use these libraries, and can be useful for a dev who wants to approach this area.
Netlink and libmnl
Netlink is a socket-based inter-process communication (IPC) mechanism used mainly between the Linux kernel and userspace processes. It was introduced as a more flexible successor to ioctl() for configuring and querying kernel subsystems: routing tables, network interfaces and addresses, and — the one we care about here — netfilter.
Being socket based, it can also be spoken directly through the raw socket, bind, sendto, recvfrom, setsockopt (etc…) system calls, crafting messages by hand per spec (see for example RFC 3549 and man 7 netlink).
This blog post won’t go deep into the protocol. For that have a look at the RFC or other dedicated resources.
The C library to interact with netlink is libmnl. Homepage is here. Repository and source code are available here.
Just:
git clone https://git.netfilter.org/libmnl/
to explore the source code.
The library is minimal and the available APIs are in the header file at include/libmnl/libmnl.h.
A bunch of useful wrappers over the standard socket system calls are available and can be used to setup the netlink protocol, ready to dialog with the kernel.
The following is a simple C program that uses the library to open a netlink socket towards the netfilter subsystem, bind it, and ask the kernel for the port ID that was assigned to us:
#include <stdio.h>
#include <stdlib.h>
#include <libmnl/libmnl.h>
int main(void)
{
// open a netlink socket to talk with netfilter
struct mnl_socket *nl;
uint32_t portid;
nl = mnl_socket_open(NETLINK_NETFILTER);
if (nl == NULL) {
perror("mnl_socket_open");
exit(EXIT_FAILURE);
}
if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
perror("mnl_socket_bind");
exit(EXIT_FAILURE);
}
portid = mnl_socket_get_portid(nl);
printf("Created portid: %u\n", portid);
// close netfilter socket
mnl_socket_close(nl);
return EXIT_SUCCESS;
}
Three calls do the work. mnl_socket_open(NETLINK_NETFILTER) is a thin wrapper
over socket(AF_NETLINK, SOCK_RAW, NETLINK_NETFILTER). mnl_socket_bind() with
MNL_SOCKET_AUTOPID lets the kernel pick our port ID (the netlink equivalent of
a source address) instead of hardcoding one. mnl_socket_get_portid() reads it
back so we can match it against replies later.
That last argument to socket() — the C “protocol” slot — is really the
netlink family, and it is what selects which netlink subsystem you talk to.
Here NETLINK_NETFILTER reaches netfilter, but the same AF_NETLINK socket also
speaks to others such as NETLINK_ROUTE (links, addresses, routes) or
NETLINK_GENERIC.
Installing the library
The development package is available for different distributions:
# Debian / Ubuntu
apt-get install -y gcc libmnl-dev
# Fedora
dnf install -y gcc libmnl-devel
# Arch Linux
pacman -Sy --noconfirm gcc libmnl
Then compile, linking against the library with -lmnl:
gcc -Wall main.c -lmnl
Testing it in Docker
You don’t need to touch your host to try this out. A minimal Dockerfile
builds and runs the program in a throwaway container:
FROM debian:12
RUN apt-get update && apt-get install -y gcc libmnl-dev libnftnl-dev nftables && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY main.c .
RUN gcc -Wall main.c -lmnl -lnftnl
CMD ["./a.out"]
Build and run it:
docker build -t netlink-demo .
docker run --rm netlink-demo
You should see the kernel-assigned port ID:
Created portid: 1
Opening and binding a netlink socket needs no special privileges, so this runs
fine in an unprivileged container. Once we start changing netfilter state in
the next sections you’ll need --cap-add=NET_ADMIN (or --privileged).
Netfilter and libnftnl
A library dedicated to the netfilter subsystem is also available and can be used to craft this kind of message interacting with the Linux firewall.
Netfilter is the packet filtering and manipulation framework built into the Linux kernel. It exposes a set of hooks along the network stack (prerouting, input, forward, output, postrouting) where packets can be inspected, accepted, dropped or mangled. The modern configuration front-end is nftables, which organizes rules into tables (containers scoped to an address family), chains (ordered lists of rules attached to a hook) and rules (the match/action statements themselves). Its nf_tables subsystem is driven entirely over netlink, which is exactly what libnftnl helps us speak.
The homepage of the library is here.
The homepage lists libmnl as a dependency: configuration of netfilter is built on top of netlink protocol.
A complete extended documentation that explains how to use the library is not available but code itself is the greatest source of truth.
Let’s have a look at the examples folder.
There is a great set of programs to add, del, get, upd netfilter objects such as tables, chains, rules, sets.
Let’s craft a simple example to create a new table. The one thing to know: any write to nf_tables must be wrapped in a batch (NFNL_MSG_BATCH_BEGIN → NFT_MSG_NEWTABLE → NFNL_MSG_BATCH_END) that the kernel applies atomically.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nf_tables.h>
#include <libmnl/libmnl.h>
#include <libnftnl/common.h>
#include <libnftnl/table.h>
int main(void)
{
char buf[MNL_SOCKET_BUFFER_SIZE];
struct mnl_socket *nl;
struct mnl_nlmsg_batch *batch;
struct nlmsghdr *nlh;
struct nftnl_table *t;
uint32_t seq = time(NULL);
// open socket
nl = mnl_socket_open(NETLINK_NETFILTER);
if (nl == NULL || mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
perror("mnl_socket");
exit(EXIT_FAILURE);
}
// alloc new table
t = nftnl_table_alloc();
nftnl_table_set_str(t, NFTNL_TABLE_NAME, "base_table");
nftnl_table_set_u32(t, NFTNL_TABLE_FAMILY, NFPROTO_INET);
// build message
batch = mnl_nlmsg_batch_start(buf, sizeof(buf));
nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
mnl_nlmsg_batch_next(batch);
nlh = nftnl_table_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch),
NFT_MSG_NEWTABLE, NFPROTO_INET,
NLM_F_CREATE | NLM_F_ACK, seq++);
nftnl_table_nlmsg_build_payload(nlh, t);
mnl_nlmsg_batch_next(batch);
// send the message
nftnl_batch_end(mnl_nlmsg_batch_current(batch), seq++);
mnl_nlmsg_batch_next(batch);
if (mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch),
mnl_nlmsg_batch_size(batch)) < 0) {
perror("mnl_socket_sendto");
exit(EXIT_FAILURE);
}
mnl_nlmsg_batch_stop(batch);
nftnl_table_free(t);
mnl_socket_close(nl);
return EXIT_SUCCESS;
}
Building and running
This program links libnftnl on top of libmnl, so compile it with both flags:
gcc -Wall main.c -lmnl -lnftnl
docker build -t netlink-demo .
# --cap-add=NET_ADMIN is needed to create table objects
docker run --cap-add=NET_ADMIN --name nftdemo netlink-demo sh -c "./a.out && nft list tables"
The program output confirms the table was created:
table inet base_table
Summary
This is a little blog post useful as a first index to further develop using netlink and (specifically) the netfilter component. Using the pointers offered by this post you can easily start implementing code.
The goal of this post is to fill the lack of proper documentation surfaced by first attempts at a web search. Lots of examples online use standard socket communication, which makes the code much more raw and verbose.
Obviously special credits are given to all the netfilter project maintainers, authors and contributors offering these great libraries.
1233 Words
2026-07-20 23:00