When you receive an RTM_NEWLINK (or any rtnetlink reply) message from a NETLINK_ROUTE socket, the fixed header (struct ifinfomsg for link messages, struct ifaddrmsg for address messages, etc.) is followed by a variable-length sequence of route attributes (struct rtattr).
Each rtattr carries a 16-bit rta_type (a protocol-specific constant such as IFLA_IFNAME or IFLA_MTU) and a payload whose length is implied by rta_len. Attributes are not all the same size, so you must walk them one by one using the kernel-provided macros.
The iteration pattern
The standard idiom is a for loop built around three macros from <linux/rtnetlink.h>:
RTA_OK(rta, remaining)— returns true whileremainingbytes still hold a valid attribute.RTA_NEXT(rta, remaining)— advancesrtato the next attribute and decrementsremaining.RTA_DATA(rta)/RTA_PAYLOAD(rta)— access the attribute’s payload bytes and its length.
The attributes start right after the fixed message header. For link messages the kernel gives you IFLA_RTA(ifi) (a typed cast of NLMSG_DATA(nlh) + sizeof(ifinfomsg)) and IFLA_PAYLOAD(nlh) for the remaining bytes; for address messages the equivalents are IFA_RTA(ifa) and IFA_PAYLOAD(nlh).
Partial example
#include <iostream>
#include <cstdint>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <net/if.h>
// ... socket setup, send RTM_GETLINK dump request, recv into buf ...
auto* nlh = reinterpret_cast<struct nlmsghdr*>(buf);
for (; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {
if (nlh->nlmsg_type != RTM_NEWLINK) continue;
auto* ifi = reinterpret_cast<struct ifinfomsg*>(NLMSG_DATA(nlh));
int attr_len = IFLA_PAYLOAD(nlh);
struct rtattr* rta = IFLA_RTA(ifi);
// Walk every rtattr in the message
for (; RTA_OK(rta, attr_len); rta = RTA_NEXT(rta, attr_len)) {
switch (rta->rta_type) {
case IFLA_IFNAME:
std::cout << "Name: "
<< reinterpret_cast<char*>(RTA_DATA(rta)) << "\n";
break;
case IFLA_MTU:
std::cout << "MTU: "
<< *reinterpret_cast<uint32_t*>(RTA_DATA(rta)) << "\n";
break;
// ...handle more IFLA_* types here...
}
}
}Note how attr_len is declared outside the inner for and is mutated in place by RTA_NEXT — it tracks the remaining bytes in the attribute block, not a loop counter. Declaring it inside the loop header or copying it into a second variable will break the iteration.
Reading typed payloads
RTA_DATA(rta) returns void*; you cast it to the type the kernel documents for that rta_type. Common cases for link attributes:
rta_type | Payload type | Notes |
|---|---|---|
IFLA_IFNAME | char[] | NUL-terminated interface name |
IFLA_MTU | uint32_t | |
IFLA_ADDRESS | unsigned char[6] | L2 address; length is RTA_PAYLOAD(rta) |
IFLA_OPERSTATE | uint8_t | IF_OPER_* value |
IFLA_STATS64 | struct rtnl_link_stats64 |
Always check RTA_PAYLOAD(rta) before reading variable-length payloads such as IFLA_ADDRESS — the length depends on the link type (Ethernet is 6 bytes, but other link layers differ).
Nested attributes
Some attributes (e.g. IFLA_LINKINFO) are themselves containers: their payload is another sequence of rtattrs. You recurse by treating RTA_DATA(rta) as the start of a new attribute block and RTA_PAYLOAD(rta) as its length:
case IFLA_LINKINFO: {
int sub_len = RTA_PAYLOAD(rta);
auto* sub = reinterpret_cast<struct rtattr*>(RTA_DATA(rta));
for (; RTA_OK(sub, sub_len); sub = RTA_NEXT(sub, sub_len)) {
if (sub->rta_type == IFLA_INFO_KIND)
std::cout << "Kind: "
<< reinterpret_cast<char*>(RTA_DATA(sub)) << "\n";
}
break;
}The same RTA_OK / RTA_NEXT pair handles every nesting level — only the starting pointer and length change.
Also see How to list all network links using rtnetlink API for a complete program that uses this idiom to dump all link attributes, How to filter physical ethernet links using rtnetlink for filtering physical wired NICs, and How to filter VLAN interfaces using rtnetlink for filtering VLAN interfaces.