After even more research I've come to the conclusion that my goal isn't achievable using /sys/class/net/.... Luckily, it is achievable.
Mounting the Correctly Namespace'd /sys/class/net
Thanks to Danila Kiver's answer to "Switching into a network namespace does not change /sys/class/net?" all I need to do is to mount sysfs in some place in order to get the correctly (network) namespace'd view to sysfs and its class/net/ branch.
The following Python example scans for network namespaces, and then lists all network interfaces in a specific network namespace, marking each physical network interface with [PHY]. Please note that this script needs root/admin cap, especially due to the mount.
import psutil
import nsenter
from ctypes import c_char_p, c_ulong
import ctypes.util
import os
import tempfile
# https://stackoverflow.com/a/29156997
libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)
libc.mount.argtypes = (c_char_p, c_char_p, c_char_p, c_ulong, c_char_p)
libc.umount.argtypes = (c_char_p, )
netns_index = dict()
for process in psutil.process_iter():
netns_ref = '/proc/%d/ns/net' % process.pid
try:
netns_id = os.stat(netns_ref).st_ino
if netns_id not in netns_index:
netns_index[netns_id] = netns_ref
except PermissionError:
pass
with tempfile.TemporaryDirectory() as temp_mnt:
for netns_id, netns_ref in netns_index.items():
with nsenter.Namespace(netns_ref, 'net'):
print('net:[%d]' % netns_id)
if libc.mount('sysfs'.encode('ascii'),
temp_mnt.encode('ascii'),
'sysfs'.encode('ascii'),
0,
''.encode('ascii')) >= 0:
for nif_name in sorted(os.listdir('%s/class/net' % temp_mnt)):
nif_path = os.readlink('%s/class/net/%s' % (temp_mnt, nif_name))
phys_nif = not nif_path.startswith('../../devices/virtual/')
print(' %s %s' % (nif_name, '[PHY]' if phys_nif else ''))
libc.umount(temp_mnt.encode('ascii'))
Workaround Without /sys/class/net
However, the NETLINK interface to the Linux kernel provides the required information, otherwise the ip link command wouldn't be able to tell the kind of interface.
The key here is the IFLA_LINKINFO attribute that is returned when asking the kernel for a list of network links (that is, network interfaces). Inside it is another attribute called IFLA_INFO_KIND, it is veth in case of an veth network interface, or bridge in case of a Linux Kernel bridge.
Please note that IFLA_LINKINFO is an optional attribute; for instance, the loopback, ethernet, and wifi network interfaces don't offer IFLA_LINKINFO.
This information can be easily gotten in Python using the famous pyroute2 netlink library. pyroute2 deals with all the nasty NETLINK stuff, install it easily via pip3. This example simply iterates over all network interfaces visible in the current network namespace, giving their names, interface index, and the IFLA_LINKINFO value, if present.
from pyroute2 import IPRoute
netw = IPRoute()
for link in netw.get_links():
ifindex = link['index']
ifname = link.get_attr('IFLA_IFNAME')
linkinfo = link.get_attr('IFLA_LINKINFO')
if linkinfo is not None:
linktype = linkinfo.get_attr('IFLA_INFO_KIND')
else:
linktype = 'None'
print('{0}: #{1} {2}'.format(ifname, ifindex, linktype))