Unix & Linux
networking network-namespaces veth
Updated Fri, 20 May 2022 10:27:16 GMT

How to find the network namespace of a veth peer ifindex?


Task

I need to unambiguously and without "holistic" guessing find the peer network interface of a veth end in another network namespace.

Theory ./. Reality

Albeit a lot of documentation and also answers here on SO assume that the ifindex indices of network interfaces are globally unique per host across network namespaces, this doesn't hold in many cases: ifindex/iflink are ambiguous. Even the loopback already shows the contrary, having an ifindex of 1 in any network namespace. Also, depending on the container environment, ifindex numbers get reused in different namespaces. Which makes tracing veth wiring a nightmare, espcially with lots of containers and a host bridge with veth peers all ending in @if3 or so...

Example: link-netnsid is 0

Spin up a Docker container instance, just to get a new veth pair connecting from the host network namespace to the new container network namespace...

$ sudo docker run -it debian /bin/bash

Now, in the host network namespace list the network interfaces (I've left out those interfaces that are of no interest to this question):

$ ip link show
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
...
4: docker0:  mtu 1500 qdisc noqueue state UP mode DEFAULT group default 
    link/ether 02:42:34:23:81:f0 brd ff:ff:ff:ff:ff:ff
...
16: vethfc8d91e@if15:  mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default 
    link/ether da:4c:f7:50:09:e2 brd ff:ff:ff:ff:ff:ff link-netnsid 0

As you can see, while the iflink is unambiguous, but the link-netnsid is 0, despite the peer end sitting in a different network namespace.

For reference, check the netnsid in the unnamed network namespace of the container:

$ sudo lsns -t net
        NS TYPE NPROCS   PID USER  COMMAND
...
...
4026532469 net       1 29616 root  /bin/bash
$ sudo nsenter -t 29616 -n ip link show
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
15: eth0@if16:  mtu 1500 qdisc noqueue state UP mode DEFAULT group default 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0

So, for both veth ends ip link show (and RTNETLINK fwif) tells us they're in the same network namespace with netnsid 0. Which is either wrong or correct under the assumptions that link-netnsids are local as opposed to global. I could not find any documentation that make it explicit what scope link-netnsids are supposed to have.

/sys/class/net/... NOT to the Rescue?

I've looked into /sys/class/net/if/... but can only find the ifindex and iflink elements; these are well documented. "ip link show" also only seems to show the peer ifindex in form of the (in)famous "@if#" notation. Or did I miss some additional network namespace element?

Bottom Line/Question

Are there any syscalls that allow retrieving the missing network namespace information for the peer end of a veth pair?




Solution

Many thanks to @A.B who filled in some missing pieces for me, especially regarding the semantics of netnsids. His PoC is very instructive. However, the crucial missing piece in his PoC is how to correlate a local netnsid to its globally unique network namespace inode number, because only then we can unambiguously connect the correct corresponding veth pairs.

To summarize and give a small Python example how to gather the information programmatically without having to rely on ip netns and its need to mount things: RTNETLINK actually returns the netnsid when querying for network interfaces. It's the IFLA_LINK_NETNSID attribute, which only appears in a link's info when needed. If it's not there, then it isn't needed -- and we must assume that the peer index refers to a namespace-local network interface.

The important lesson to take home is that a netnsid/IFLA_LINK_NETSID is only locally defined within the network namespace where you got it when asking RTNETLINK for link information. A netnsid with the same value gotten in a different network namespace might identify a different peer namespace, so be careful to not use the netnsid outside its namespace. But which uniquely identifyable network namespace (inode number) map to which netnsid?

As it turns out, a very recent version of lsns as of March 2018 is well capable to show the correct netnsid next to its network namespace inode number! So there is a way to map local netnsids to namespace inodes, but it is actually backwards! And it's more an oracle (with a lowercase ell) than a lookup: RTM_GETNSID needs a network namespace identifier either as a PID or FD (to the network namespace) and then returns the netnsid. See /u5m1p4/retrieving-the-netnsid-of-a-network-namespace-in-python for an example of how to ask the Linux network namespace oracle.

In consequence, you need to enumerate the available network namespaces (via /proc and/or /var/run/netns), then for a given veth network interface attach to the network namespace where you found it, ask for the netnsids of all the network namespaces you enumerated at the beginning (because you never know Beforehand which is which), and finally map the netnsid of the veth peer to the namespace inode number per the local map you created in step 3 after attaching to the veth's namespace.

import psutil
import os
import pyroute2
from pyroute2.netlink import rtnl, NLM_F_REQUEST
from pyroute2.netlink.rtnl import nsidmsg
from nsenter import Namespace
# phase I: gather network namespaces from /proc/[0-9]*/ns/net
netns = dict()
for proc in psutil.process_iter():
    netnsref= '/proc/{}/ns/net'.format(proc.pid)
    netnsid = os.stat(netnsref).st_ino
    if netnsid not in netns:
        netns[netnsid] = netnsref
# phase II: ask kernel "oracle" about the local IDs for the
# network namespaces we've discovered in phase I, doing this
# from all discovered network namespaces
for id, ref in netns.items():
    with Namespace(ref, 'net'):
        print('inside net:[{}]...'.format(id))
        ipr = pyroute2.IPRoute()
        for netnsid, netnsref in netns.items():
            with open(netnsref, 'r') as netnsf:
                req = nsidmsg.nsidmsg()
                req['attrs'] = [('NETNSA_FD', netnsf.fileno())]
                resp = ipr.nlm_request(req, rtnl.RTM_GETNSID, NLM_F_REQUEST)
                local_nsid = dict(resp[0]['attrs'])['NETNSA_NSID']
            if local_nsid != 2**32-1:
                print('  net:[{}] <--> nsid {}'.format(netnsid, local_nsid))




Comments (4)

  • +0 – nice tool update. tried it and works as expected, giving in one line the netns and the nsid. There's still one thing it won't solve: it requires a process. Fine for classic containers, won't help for a namespace running nothing (eg: an isolated switch in its own net namespace, conntected to other net namespaces, with its own firewall rules etc, but zero process running (and kept existing by a mountpoint)). So there's still an other step to do in this case (running a process with the mountpoint as reference: nsenter --net=/var/run/netns/router sleep 10 & lsns -t net -n -o NS,NETNSID -p $! ) — May 05, 2018 at 20:00  
  • +0 – If I get the RTM_GETNSID to work correctly, then it is possible to get the netnsid by namespace fd. When there's no process then it should be still discoverable through /var/run/netns — May 05, 2018 at 20:44  
  • +0 – Sweet ... isonlated switches ... never thought about it. — May 05, 2018 at 20:45  
  • +0 – My lxkns discovery engine github.com/thediveo/lxkns picks up also namespaces that are only bind-mounted, such as mentioned above. It even picks up namespace bind-mounts in other mount namespaces. — Feb 04, 2022 at 20:59  


External Links

External links referenced by this document:

Linked Articles

Local articles referenced by this article: