Skip to content
FreeBSDDeep Dive Published Updated 6 min readViews unavailable

FreeBSD Netlink Operations: Read Routes and Interfaces Through AF_NETLINK

Use FreeBSD's AF_NETLINK API to query routes and interface state, parse multipart replies safely, and distinguish supported families from Linux assumptions.

FreeBSD includes a kernel Netlink interface for network configuration and event exchange. It gives programs a message-based socket API for operations such as querying interfaces, addresses, neighbors, routes, and VNET routing state. The interface is useful for network managers, agents, and diagnostics that need structured kernel replies rather than scraping command output, but it is not a promise that every Linux Netlink family or command exists on FreeBSD.

The FreeBSD 15.1 netlink(4) manual documents two supported families: NETLINK_ROUTE and NETLINK_GENERIC. NETLINK_ROUTE covers network configuration. NETLINK_GENERIC is a container for separately registered subsystem families. The protocol appeared in FreeBSD 13.2, so treat both headers and runtime support as versioned interfaces and check the manuals for the release you deploy.

Open the right socket and family

FreeBSD’s headers live under <netlink/>, and the route family is selected when the socket is created. A minimal read-only preflight can test whether the local kernel accepts the socket type and family:

#include <sys/types.h>
#include <sys/socket.h>
#include <netlink/netlink.h>
#include <netlink/netlink_route.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
    if (fd == -1) {
        perror("socket(AF_NETLINK, NETLINK_ROUTE)");
        return 1;
    }
    puts("NETLINK_ROUTE socket opened; no request was sent");
    close(fd);
    return 0;
}

This proves only that the process can open a route-family Netlink socket. It does not prove that the requested message is implemented, that a route query is well-formed, or that the process can change network state. The Netlink socket has a FreeBSD-specific sockaddr_nl structure; the manual notes that filling it is typically unnecessary for basic socket operations. Use the target release’s <netlink/> headers rather than copying Linux structure definitions into a FreeBSD program.

Parse the transport as a stream of messages

Netlink is message-based. Each message begins with nlmsghdr, may have a family-specific header, and can carry nested type-length-value (TLV) attributes. Headers and attributes are padded to four-byte boundaries. A single send(2) or recv(2) call may contain multiple messages, so a parser must walk the entire returned buffer and validate each declared length before reading its attributes.

The base header’s nlmsg_seq is a caller-provided request identifier. Match replies and errors to outstanding requests instead of assuming that the next message belongs to the most recent operation. NLMSG_ERROR carries a standard error result; NLM_F_ACK requests an explicit acknowledgment for a request. Multipart dumps can return a group of messages and finish with NLMSG_DONE, which also contains a result. If NETLINK_EXT_ACK is enabled, the kernel may provide an error string or the offset of a problematic attribute. Record these fields in diagnostics rather than reducing every parse or policy failure to “route not found.”

An abbreviated parser boundary check looks like this; the family-specific decoder still has to validate each TLV and its alignment:

#include <string.h>

size_t offset = 0;
while (offset < received_length) {
    if (received_length - offset < sizeof(struct nlmsghdr))
        return malformed_reply();

    const unsigned char *message = buffer + offset;
    size_t remaining = received_length - offset;
    struct nlmsghdr header;
    memcpy(&header, message, sizeof(header));
    if (header.nlmsg_len < sizeof(header) || header.nlmsg_len > remaining)
        return malformed_reply();

    size_t step = NLMSG_ALIGN(header.nlmsg_len);
    if (step > remaining && header.nlmsg_len != remaining)
        return malformed_reply();

    dispatch_one_message(message, header.nlmsg_len);
    offset += step <= remaining ? step : header.nlmsg_len;
}

The example intentionally leaves message decoding to the family layer. It copies the fixed header into aligned local storage before reading it, so the receive buffer itself need not be aligned for struct nlmsghdr. The decoder receives the original bytes plus the validated message length; it still must reject truncated attributes, handle multipart termination, and avoid assuming that unfamiliar optional attributes are fatal. Keep the receive buffer large enough for the response shape and handle receive-buffer overflow as an incomplete observation, not as a valid snapshot.

Query route state without changing it

The FreeBSD rtnetlink(4) family supports interface, address, route, nexthop, and neighbor operations. RTM_GETROUTE can fetch one route or dump routes in the current VNET. A dump returns route messages followed by the multipart terminator. The documented route filters include address family and RTA_TABLE, which selects a FIB or requests all FIBs. This is a useful starting point for a monitoring agent that needs a consistent structured representation of kernel state.

Do not interpret a route message without its address family, prefix length, table, outgoing interface, and gateway context. RTM_GETADDR reads interface addresses, while the FreeBSD 15.1 manual marks RTM_NEWADDR and RTM_DELADDR unsupported; do not infer Linux address-mutation behavior from the shared message names. For a comparison against a human-readable result, capture the Netlink response and run the equivalent read-only route or netstat query in the same VNET and FIB. A mismatch can be a parser defect, an omitted family or table filter, or a test that queried different routing contexts. It is not automatically evidence that the kernel has two contradictory route tables.

Treat a dump as a candidate snapshot only after the multipart terminator and all messages have been validated. If the kernel marks a reply with NLM_F_DUMP_INTR, or the NLMSG_DONE result reports an error, record the observation as incomplete and retry instead of publishing a partial route inventory as current state.

When an event-driven agent subscribes to route notifications, treat notifications as change hints and reconcile with a fresh dump after a restart, buffer overflow, or sequence gap. This is a conservative design rule: an event stream cannot reconstruct state the client never received. Netlink’s ability to deliver event notifications does not remove the need for a recovery snapshot and explicit handling of incomplete dumps.

Keep Linux compatibility assumptions out of production

FreeBSD’s route family is Linux-like in structure, but the release manual is the compatibility contract. It identifies supported message types and attributes and also documents FreeBSD-specific route fields. Unsupported commands can return ENOTSUP; malformed or missing TLVs can return EINVAL; insufficient privilege can return EPERM; resource exhaustion can produce ENOBUFS or ENOMEM. Feature-detect the operation you need, retain the returned error metadata, and do not assume the presence of a Linux command-line utility proves the kernel implements the corresponding Netlink message.

The generic family has a separate discovery model: family IDs are allocated dynamically, and the nlctrl control family can list registered families and supported operations. Do not hard-code an ID observed on a different boot or system. Query by family name, inspect the advertised operations, and check each operation’s required permission before issuing a modifying request.

For production diagnostics, first use a read-only NETLINK_ROUTE request and compare its result with the established FreeBSD tools. Add subscriptions only after the parser can handle multiple messages, sequence matching, multipart completion, and errors. Attempt a route or interface mutation only in a maintenance window or disposable VNET with a tested rollback. That progression separates API availability, request encoding, privileges, and actual network behavior without turning a parser experiment into an outage.

Related:

Sources:

Comments