Tether’s HAL layer includes a platform-agnostic NetworkInterfaceEnumerator that uses rtnetlink to list all network interfaces on a Linux system and classify them by type — physical Ethernet, Wi-Fi, bridge, veth, VLAN, tunnel, etc. This post shows how to use it in a standalone project with Tether as a git submodule, compiling only the single source file needed and linking zero Tether libraries.
The API
The enumerator lives in <tether/hal/NetworkInterfaceEnumerator.hpp> and provides:
enumerateNetworkInterfaces()— returns astd::vector<NetworkInterface>of all interfacesgetPhysicalEthernetInterfaces()— filters to physical wired Ethernet adapters onlygetInterfacesByType(InterfaceType type)— filters by any type (VLAN, bridge, veth, tunnel, …)getInterfacesByCategory(InterfaceCategory category)— filters to Physical or Virtual
Each NetworkInterface struct carries the interface name, ifindex, ARP hardware type, link kind (from IFLA_INFO_KIND), MAC address, MTU, flags, carrier state, wireless flag, parent ifindex, and a classified InterfaceType + InterfaceCategory.
Project setup
1. Add Tether as a submodule
mkdir my-project && cd my-project
git init
git submodule add https://github.com/ulikoehler/Tether.git third_party/Tether
git submodule update --init --recursiveThe --recursive flag is needed because Tether itself has submodules (the enumerator only needs magic_enum, but recursive init is simplest).
2. CMakeLists.txt
The key insight is that LinuxNetworkInterfaceEnumerator.cpp is self-contained: it only depends on HALTypes.hpp (for the MacAddress type) and magic_enum (a header-only library). It does not depend on tether_common, tether_hal, or any other Tether library. So we compile just that one file directly:
cmake_minimum_required(VERSION 3.16)
project(network_iface_demo CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Tether submodule path
set(TETHER_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/third_party/Tether)
# Only the single source file we need — no Tether libraries linked.
add_executable(list_interfaces
main.cpp
${TETHER_ROOT}/src/hal/LinuxNetworkInterfaceEnumerator.cpp
)
target_include_directories(list_interfaces PRIVATE
${TETHER_ROOT}/include
${TETHER_ROOT}/include/tether
${TETHER_ROOT}/dependencies/magic_enum/include
)No add_subdirectory(third_party/Tether), no target_link_libraries(... tether_hal), no EtherCAT, no Drogon, no Klipper — just one .cpp file and three include paths.
3. The example program
#include <tether/hal/NetworkInterfaceEnumerator.hpp>
#include <format>
#include <iostream>
using namespace EtherCAT::HAL;
int main() {
// --- List all physical wired Ethernet interfaces ---
std::cout << "Physical Ethernet interfaces:\n";
for (const auto& iface : getPhysicalEthernetInterfaces()) {
std::cout << std::format(" {} (ifindex={}, mtu={})\n",
iface.name, iface.ifindex, iface.mtu);
}
// --- List all VLAN interfaces ---
std::cout << "\nVLAN interfaces:\n";
for (const auto& iface : getInterfacesByType(InterfaceType::Vlan)) {
std::cout << std::format(" {} (ifindex={}", iface.name, iface.ifindex);
if (iface.parentIfindex)
std::cout << std::format(", parent={}", *iface.parentIfindex);
std::cout << ")\n";
}
// --- List all interfaces with their classification ---
std::cout << "\nAll interfaces:\n";
for (const auto& iface : enumerateNetworkInterfaces()) {
std::cout << std::format(" {:<20} {:<10} {}\n",
iface.name,
interfaceTypeToString(iface.type),
interfaceCategoryToString(iface.category));
}
return 0;
}4. Build and run
cmake -B build
cmake --build build
./build/list_interfacesExample output
On a host with a USB Ethernet adapter, Wi-Fi, Docker bridges, veth pairs, and WireGuard tunnels:
Physical Ethernet interfaces:
enx8e4f6eac342f (ifindex=467, mtu=1500)
VLAN interfaces:
All interfaces:
lo Loopback Virtual
wlo1 Wireless Physical
virbr0 Bridge Virtual
docker0 Bridge Virtual
br-4986fb404ec2 Bridge Virtual
veth822ece7 Veth Virtual
veth89db15f Veth Virtual
ztno5qdlwc Tunnel Virtual
MyWireguard Tunnel Virtual
tailscale0 Tunnel Virtual
enx8e4f6eac342f Ethernet PhysicalThe getPhysicalEthernetInterfaces() filter correctly excludes the Wi-Fi adapter (wlo1), all bridges, veths, and tunnels, returning only the USB Ethernet adapter. The VLAN list is empty because no VLAN interfaces exist on this host — see How to filter VLAN interfaces using rtnetlink for what a VLAN entry looks like.
How the classification works
The enumerator applies three layers of classification:
ARP hardware type (
ifi_type):ARPHRD_ETHER(1) for Ethernet/Wi-Fi/bridges/veths,ARPHRD_LOOPBACK(772) for loopback,ARPHRD_NONE(65534) for tunnels.IFLA_INFO_KINDfrom the nestedIFLA_LINKINFOattribute:"bridge","veth","vlan","wireguard","tun", etc. Physical interfaces have noIFLA_LINKINFOat all./sys/class/net/<name>/wirelessexistence: distinguishes Wi-Fi from wired Ethernet, since both reportARPHRD_ETHERwith noIFLA_LINKINFO.
The InterfaceCategory (Physical vs. Virtual) is derived from the InterfaceType: Ethernet and Wireless are Physical; everything else is Virtual.
Available InterfaceType values
| Type | Kind string | Description |
|---|---|---|
Loopback | — | Loopback interface (lo) |
Ethernet | — | Physical wired Ethernet adapter |
Wireless | — | Wi-Fi adapter (detected via sysfs) |
Bridge | bridge | Software bridge |
Veth | veth | Virtual ethernet pair endpoint |
Vlan | vlan | 802.1Q VLAN interface |
Bond | bond | Link aggregation |
Macvlan | macvlan / macvtap | MAC-based virtual interface |
Tunnel | wireguard / tun / tap / gre / … | L3 tunnel |
Other | (unknown kind) | Virtual interface of unrecognized kind |
Unknown | — | Could not be classified |
Also see How to list all network links using rtnetlink API for the raw rtnetlink approach without Tether, How to filter physical ethernet links using rtnetlink for the underlying filtering logic, and How to iterate rtnetlink rtattr attributes for the attribute-walking idiom.