VLAN interfaces (created with ip link add link <parent> name <name> type vlan id <id>) appear in a rtnetlink RTM_GETLINK dump alongside every other interface. Unlike physical Ethernet adapters, VLANs are always virtual and carry a clear marker: the nested IFLA_LINKINFO attribute with IFLA_INFO_KIND set to "vlan". They also carry IFLA_LINK, which points to the parent interface’s ifindex — the physical or bridge device the VLAN sits on top of.
This post shows a compact C++ program that filters the rtnetlink dump for VLAN interfaces only, printing each VLAN’s name, ifindex, and parent ifindex.
How VLANs appear in rtnetlink
A VLAN interface reports the following distinguishing attributes:
| Attribute | Value | Meaning |
|---|---|---|
ifi_type | ARPHRD_ETHER (1) | VLANs are Ethernet-framed |
IFLA_LINKINFO → IFLA_INFO_KIND | "vlan" | Identifies the link as a VLAN |
IFLA_LINK | parent ifindex (e.g. 3) | The underlying interface the VLAN is stacked on |
IFLA_LINKINFO → IFLA_INFO_DATA → IFLA_VLAN_ID | VLAN ID (1–4094) | The 802.1Q tag |
The key filter is IFLA_INFO_KIND == "vlan". The IFLA_LINK attribute is not unique to VLANs (veths also use it), but it is always present for VLANs and tells you which parent device to look up.
The program
#include <iostream>
#include <cstdint>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/if_arp.h>
int main() {
int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (sock_fd < 0) { perror("socket"); return 1; }
struct sockaddr_nl sa{};
sa.nl_family = AF_NETLINK;
sa.nl_pid = getpid();
bind(sock_fd, reinterpret_cast<struct sockaddr*>(&sa), sizeof(sa));
// Request a dump of all links (AF_UNSPEC = all address families).
struct {
struct nlmsghdr nlh;
struct ifinfomsg ifm;
} req{};
req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
req.nlh.nlmsg_type = RTM_GETLINK;
req.ifm.ifi_family = AF_UNSPEC;
struct sockaddr_nl dest{};
dest.nl_family = AF_NETLINK;
struct iovec iov = { &req, req.nlh.nlmsg_len };
struct msghdr msg = { &dest, sizeof(dest), &iov, 1, nullptr, 0, 0 };
sendmsg(sock_fd, &msg, 0);
char buf[16384];
bool running = true;
while (running) {
ssize_t len = recv(sock_fd, buf, sizeof(buf), 0);
if (len < 0) break;
auto* nlh = reinterpret_cast<struct nlmsghdr*>(buf);
for (; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {
if (nlh->nlmsg_type == NLMSG_DONE) { running = false; break; }
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);
std::string name;
std::string kind;
uint32_t parent_ifindex = 0;
bool has_parent = false;
for (; RTA_OK(rta, attr_len); rta = RTA_NEXT(rta, attr_len)) {
switch (rta->rta_type) {
case IFLA_IFNAME:
name = reinterpret_cast<char*>(RTA_DATA(rta));
break;
case IFLA_LINK:
// IfIndex of the parent/underlying device.
// For a VLAN on eth0, this is eth0's ifindex.
parent_ifindex = *reinterpret_cast<uint32_t*>(RTA_DATA(rta));
has_parent = true;
break;
case IFLA_LINKINFO: {
// IFLA_LINKINFO is nested: parse its payload for
// IFLA_INFO_KIND, which holds the link type string
// (e.g. "vlan", "bridge", "veth", "wireguard").
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)
kind = reinterpret_cast<char*>(RTA_DATA(sub));
}
break;
}
}
}
// Filter: only VLAN interfaces.
if (kind != "vlan") continue;
std::cout << name << " (ifindex=" << ifi->ifi_index;
if (has_parent)
std::cout << " parent=" << parent_ifindex;
std::cout << " flags=0x" << std::hex << ifi->ifi_flags
<< std::dec << ")\n";
}
}
close(sock_fd);
return 0;
}The program walks the top-level attributes of each RTM_NEWLINK message. When it encounters IFLA_LINKINFO, it descends one level into the nested attribute list to read IFLA_INFO_KIND. Only interfaces whose kind is "vlan" are printed.
Building and running
g++ -o list_vlans list_vlans.cpp
./list_vlansNo special privileges are required — reading the link dump via rtnetlink is unprivileged.
Example output
A VLAN interface created on top of a Wi-Fi interface (wlo1, ifindex 3) with ip link add link wlo1 name v77 type vlan id 77 looks like this in the full link dump:
----------------------------------------
Interface Index: 30
Type (Family): 1
Flags (Kernel): Flags: UP BROADCAST RUNNING MULTICAST (69699)
Name: v77
TX Queue Len: 1000
State: UP
Link Mode: default (0)
MTU: * 1500
Group: 0
Promiscuity: 0
Num TX Queues: 1
Num RX Queues: 1
Carrier: yes
MAC: e6:8e:03:51:d8:ae
Broadcast: ff:ff:ff:ff:ff:ff
Stats64:
rx_packets: 7533 tx_packets: 2472
rx_bytes: 1189752 tx_bytes: 110511
rx_errors: 0 tx_errors: 0
rx_dropped: 0 tx_dropped: 0
multicast: 1870 collisions: 0
Kind: vlan
Parent IfIndex: 3
Qdisc: noqueueThe filter program extracts only the relevant fields and prints:
v77 (ifindex=30 parent=3 flags=0x11043)The parent=3 refers to wlo1’s ifindex — you can resolve it to a name with a second rtnetlink lookup or by cross-referencing the dump.
Extracting the VLAN ID
The VLAN ID (1–4094) is stored one level deeper, inside IFLA_LINKINFO → IFLA_INFO_DATA → IFLA_VLAN_ID. IFLA_INFO_DATA is itself a nested attribute list, so you need a second level of descent:
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)
kind = reinterpret_cast<char*>(RTA_DATA(sub));
// IFLA_INFO_DATA is another nest containing VLAN-specific attrs.
if (sub->rta_type == IFLA_INFO_DATA) {
int data_len = RTA_PAYLOAD(sub);
auto* d = reinterpret_cast<struct rtattr*>(RTA_DATA(sub));
for (; RTA_OK(d, data_len); d = RTA_NEXT(d, data_len)) {
if (d->rta_type == IFLA_VLAN_ID)
vlan_id = *reinterpret_cast<uint16_t*>(RTA_DATA(d));
}
}
}
break;
}IFLA_VLAN_ID is a uint16_t (values 1–4094). Add uint16_t vlan_id = 0; to your variables and include it in the output if needed.
Also see How to list all network links using rtnetlink API for the full link-dump program, How to filter physical ethernet links using rtnetlink for the complementary physical-NIC filter, and How to iterate rtnetlink rtattr attributes for the attribute-walking idiom.