python pyroute2 move WiFi interface into network namespace

172 Views Asked by At

I want to use python pyroute2 to discover a USB WiFi dongle on Linux and move it into a network namespace.

I can do this with an ethernet interface but I understand that I have to use the PHY to move it with pyroute2 if it is a wifi interface. I checked the docs and do not see an example for how to do this.

Below is what I have so far. How can I use pyroute2 to move my USB WiFi dongle into a network namespace?

# Inspect networking interfaces
import netifaces

# Setup netowrking namespaces
from pyroute2 import NetNS, NSPopen, netns, IPDB
from pyroute2 import IW
from pyroute2 import IPRoute
from pyroute2.netlink import NetlinkError

# Get a list of all the network interfaces
interfaces = netifaces.interfaces()

# Manage interfaces
ipdb = IPDB()

# Loop over all the interfaces and print their details
for iface in interfaces:
    iface_details = netifaces.ifaddresses(iface)
    if iface.find("wlxc") >= 0:

        print(f"Found USB Wi-Fi interface: {iface} with details: {iface_details}")

        # Truncate the long name
        truncated_name = iface[-8:-1]

        # Create a networking namespace
        ipdb_netns = IPDB(nl=NetNS(truncated_name))

        # Command to run
        program_command = ["ifconfig"]

        # Get the interface
        print(f"Truncated name is: {truncated_name}")
        link_iface = ipdb.interfaces[iface]
        print(f"Link interface: {link_iface} ...")

        # Look for the wireless index
        ip = IPRoute()
        iw = IW()
        index = ip.link_lookup(ifname=iface)[0]
        try:
            wifi_if = iw.get_interface_by_ifindex(index)
            print(f"Wireless interface: {wifi_if}")

            with ipdb.interfaces[iface] as if_to_move:
                print(f"Moving interface: {if_to_move} to: {truncated_name}")
                if_to_move.net_ns_fd = truncated_name
            print(f"Done moving to: {truncated_name} ...")
        except NetlinkError as e:
            if e.code == 19:  # 19 'No such device'
                print("not a wireless interface")
        finally:
            iw.close()
            ip.close()

Error:

Found USB Wi-Fi interface: wlxc87f548d9633 with details: {17: [{'addr': 'c8:7f:54:8d:96:33', 'broadcast': 'ff:ff:ff:ff:ff:ff'}]}
Truncated name is: 548d963
Link interface: {'address': 'c8:7f:54:8d:96:33', 'broadcast': 'ff:ff:ff:ff:ff:ff', 'ifname': 'wlxc87f548d9633', 'mtu': 1500, 'qdisc': 'noqueue', 'txqlen': 1000, 'operstate': 'DOWN', 'linkmode': 1, 'group': 0, 'promiscuity': 0, 'num_tx_queues': 1, 'num_rx_queues': 1, 'carrier': 0, 'carrier_changes': 1, 'proto_down': 0, 'gso_max_segs': 65535, 'gso_max_size': 65536, 'xdp': {'attrs': [('IFLA_XDP_ATTACHED', None)]}, 'carrier_up_count': 0, 'carrier_down_count': 1, 'min_mtu': 256, 'max_mtu': 2304, 'perm_address': 'c8:7f:54:8d:96:33', 'parent_dev_name': '3-3:1.0', 'parent_dev_bus_name': 'usb', 'index': 10, 'flags': 4099, 'ipdb_scope': 'system', 'ipdb_priority': 0, 'vlans': (), 'ipaddr': (), 'ports': (), 'family': 0, 'ifi_type': 1, 'state': 'up', 'neighbours': ()} ...
Wireless interface: ({'cmd': 7, 'version': 1, 'reserved': 0, 'attrs': [('NL80211_ATTR_IFINDEX', 10), ('NL80211_ATTR_IFNAME', 'wlxc87f548d9633'), ('NL80211_ATTR_WIPHY', 1), ('NL80211_ATTR_IFTYPE', 2), ('NL80211_ATTR_WDEV', 4294967297), ('NL80211_ATTR_MAC', 'c8:7f:54:8d:96:33'), ('NL80211_ATTR_GENERATION', 17), ('NL80211_ATTR_4ADDR', '00'), ('NL80211_ATTR_WIPHY_TX_POWER_LEVEL', 1600), ('NL80211_ATTR_TXQ_STATS', '08:00:01:00:00:00:00:00:08:00:02:00:00:00:00:00:08:00:03:00:00:00:00:00:08:00:04:00:00:00:00:00:08:00:05:00:00:00:00:00:08:00:06:00:00:00:00:00:08:00:08:00:00:00:00:00:08:00:09:00:00:00:00:00:08:00:0a:00:00:00:00:00')], 'header': {'length': 188, 'type': 34, 'flags': 0, 'sequence_number': 255, 'pid': 14840, 'error': None, 'target': 'localhost', 'stats': Stats(qsize=0, delta=0, delay=0)}, 'event': 'NL80211_CMD_NEW_INTERFACE'},)
Moving interface: {'address': 'c8:7f:54:8d:96:33', 'broadcast': 'ff:ff:ff:ff:ff:ff', 'ifname': 'wlxc87f548d9633', 'mtu': 1500, 'qdisc': 'noqueue', 'txqlen': 1000, 'operstate': 'DOWN', 'linkmode': 1, 'group': 0, 'promiscuity': 0, 'num_tx_queues': 1, 'num_rx_queues': 1, 'carrier': 0, 'carrier_changes': 1, 'proto_down': 0, 'gso_max_segs': 65535, 'gso_max_size': 65536, 'xdp': {'attrs': [('IFLA_XDP_ATTACHED', None)]}, 'carrier_up_count': 0, 'carrier_down_count': 1, 'min_mtu': 256, 'max_mtu': 2304, 'perm_address': 'c8:7f:54:8d:96:33', 'parent_dev_name': '3-3:1.0', 'parent_dev_bus_name': 'usb', 'index': 10, 'flags': 4099, 'ipdb_scope': 'system', 'ipdb_priority': 0, 'vlans': (), 'ipaddr': (), 'ports': (), 'family': 0, 'ifi_type': 1, 'state': 'up', 'neighbours': ()} to: 548d963
fail <class 'NoneType'>
fail <class 'NoneType'>

Updated code with suggested answer:

#!/usr/bin/python3

# Configure logging
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO, format="%(asctime)s:%(levelname)s:%(message)s")

# Inspect networking interfaces
import netifaces

# Setup netowrking namespaces
from pyroute2 import NetNS, NSPopen, netns, IPDB, IPRoute

# Get a list of all the network interfaces
interfaces = netifaces.interfaces()

# Managing network interfaces
with IPDB() as ipdb:

    # Loop over all the interfaces and logger.info their details
    for iface in interfaces:

        # Determine interface information
        iface_details = netifaces.ifaddresses(iface)
        if iface.find("wlxc") >= 0:

            logger.info(f"Found USB Wi-Fi interface: {iface} with details: {iface_details}")

            # Attempt to remove a networking namespace by the same name
            iface_truncate = iface[-4:]
            net_namespace = f"{iface_truncate}-ns"
            try:
                netns.remove(net_namespace)
            except Exception as err:
                logger.warning(f"Namespace does not exist and we could not remove it: {net_namespace} ...")

            # Create a new network namespace or identify an existing one
            logger.info(f"Trying to create network namespace: {net_namespace} ...")
            netns.create(net_namespace)

            # Get the interface index
            with IPRoute() as ip:
                index = ip.link_lookup(ifname=iface)[0]

            logger.info(f"Index of USB WiFi is: {index} ...")

            logger.info(f"Interfaces: {ipdb.interfaces[iface]} ...")

            # Move the interface to the new network namespace
            with ipdb.interfaces[index] as interf:
                interf.net_ns_fd = net_namespace
                logger.info(f"Moved {iface} to {net_namespace}")

            logger.info("Done moving interface ...")

# Close IPDB
ipdb.release()

Traceback with latest code:

2023-11-13 21:05:36,676:WARNING:Deprecation warning https://docs.pyroute2.org/ipdb_toc.html
2023-11-13 21:05:36,676:WARNING:To remove this DeprecationWarning exception, start IPDB(deprecation_warning=False, ...)
2023-11-13 21:05:36,678:INFO:Found USB Wi-Fi interface: wlxc87f548d9633 with details: {17: [{'addr': 'c8:7f:54:8d:96:33', 'broadcast': 'ff:ff:ff:ff:ff:ff'}]}
2023-11-13 21:05:36,680:INFO:Trying to create network namespace: 9633-ns ...
2023-11-13 21:05:36,686:INFO:Index of USB WiFi is: 10 ...
2023-11-13 21:05:36,700:INFO:Interfaces: {'address': 'c8:7f:54:8d:96:33', 'broadcast': 'ff:ff:ff:ff:ff:ff', 'ifname': 'wlxc87f548d9633', 'mtu': 1500, 'qdisc': 'noqueue', 'txqlen': 1000, 'operstate': 'DOWN', 'linkmode': 1, 'group': 0, 'promiscuity': 0, 'num_tx_queues': 1, 'num_rx_queues': 1, 'carrier': 0, 'carrier_changes': 1, 'proto_down': 0, 'gso_max_segs': 65535, 'gso_max_size': 65536, 'xdp': {'attrs': [('IFLA_XDP_ATTACHED', None)]}, 'carrier_up_count': 0, 'carrier_down_count': 1, 'min_mtu': 256, 'max_mtu': 2304, 'perm_address': 'c8:7f:54:8d:96:33', 'parent_dev_name': '3-3:1.0', 'parent_dev_bus_name': 'usb', 'index': 10, 'flags': 4099, 'ipdb_scope': 'system', 'ipdb_priority': 0, 'vlans': (), 'ipaddr': (), 'ports': (), 'family': 0, 'ifi_type': 1, 'state': 'up', 'neighbours': ()} ...
2023-11-13 21:05:36,700:INFO:Moved wlxc87f548d9633 to 9633-ns
fail <class 'NoneType'>
fail <class 'NoneType'>
Traceback (most recent call last):
  File "/home/user/Desktop/network-stress-test/network-stress-test.py", line 52, in <module>
    with ipdb.interfaces[index] as interf:
  File "/usr/local/lib/python3.10/dist-packages/pyroute2/ipdb/transactional.py", line 206, in __exit__
    self.commit()
  File "/usr/local/lib/python3.10/dist-packages/pyroute2/ipdb/interfaces.py", line 1136, in commit
    raise error
  File "/usr/local/lib/python3.10/dist-packages/pyroute2/ipdb/interfaces.py", line 1036, in commit
    run(nl.link, 'update', **request)
  File "/usr/local/lib/python3.10/dist-packages/pyroute2/ipdb/interfaces.py", line 515, in _run
    raise error
  File "/usr/local/lib/python3.10/dist-packages/pyroute2/ipdb/interfaces.py", line 510, in _run
    return cmd(*argv, **kwarg)
  File "/usr/local/lib/python3.10/dist-packages/pyroute2/iproute/linux.py", line 1672, in link
    ret = self.nlm_request(msg, msg_type=msg_type, msg_flags=msg_flags)
  File "/usr/local/lib/python3.10/dist-packages/pyroute2/netlink/nlsocket.py", line 870, in nlm_request
    return tuple(self._genlm_request(*argv, **kwarg))
  File "/usr/local/lib/python3.10/dist-packages/pyroute2/netlink/nlsocket.py", line 1214, in nlm_request
    for msg in self.get(
  File "/usr/local/lib/python3.10/dist-packages/pyroute2/netlink/nlsocket.py", line 873, in get
    return tuple(self._genlm_get(*argv, **kwarg))
  File "/usr/local/lib/python3.10/dist-packages/pyroute2/netlink/nlsocket.py", line 550, in get
    raise msg['header']['error']
pyroute2.netlink.exceptions.NetlinkError: (22, 'Invalid argument')
1

There are 1 best solutions below

2
VonC On BEST ANSWER

You are creating a new network namespace using IPDB(nl=NetNS(truncated_name)), and then try to move the interface to the new namespace using the with ipdb.interfaces[iface] as if_to_move: context manager, which does not seem to work.
Using if_to_move.net_ns_fd = truncated_name, assuming this would move the interface to the new namespace, might work for Ethernet interfaces. But WiFi interfaces require a different handling due to their association with a wireless PHYs (physical layer entities).

You should first identify the WiFi interface using its ifname or index, which you did (see, for instance, "How to get interface names from kernel using pyroute2 IPDB function?", but also the IPRoute module).
Then create a new network namespace or identify an existing one. And move the WiFi interface to the new network namespace.

from pyroute2 import NetNS, IPDB, IW, IPRoute

# Get a list of all the network interfaces
interfaces = netifaces.interfaces()

# Create a new network namespace or identify an existing one
net_namespace = 'my_new_namespace'
netns.create(net_namespace)

# Managing network interfaces
with IPDB() as ipdb:
    for iface in interfaces:
        if iface.startswith("wl"):
            print(f"Found USB Wi-Fi interface: {iface}")
            
            # Get the interface index
            with IPRoute() as ip:
                index = ip.link_lookup(ifname=iface)[0]

            # Move the interface to the new network namespace
            with ipdb.interfaces[iface] as i:
                i.net_ns_fd = net_namespace
                print(f"Moved {iface} to {net_namespace}")

# Close IPDB
ipdb.release()

That should move your USB WiFi dongle into the specified network namespace.

[ Updated Setup ]
┌──────────────┐       ┌───────────────────┐
│ USB WiFi     ├──────►│ New Namespace     │
│ Dongle       │       │ (my_new_namespace)│
└──────────────┘       └───────────────────┘

Instead of creating a new namespace as part of the IPDB instance, I use netns.create(net_namespace) to explicitly create or identify the namespace. That approach is clearer and separates the concerns of namespace creation and interface manipulation.

Then, I use with IPDB() as ipdb: to manage the interfaces and with ipdb.interfaces[iface] as i: to handle the interface to be moved (from "Move an interface to a namespace").


With your suggested code I get a Netlink traceback from pyroute2: pyroute2.netlink.exceptions.NetlinkError: (22, 'Invalid argument').
I do not see any errors in dmesg.
I don't see any usage of the index after ip.link_lookup, and I tried using the index there as well.
The traceback occurs after attempting to move the interface into the namespace.

The NetlinkError: (22, 'Invalid argument') error in your updated code indicates that an invalid argument is being passed during the operation of moving the interface to the new network namespace.

In your code, you are trying to move the interface using its index with ipdb.interfaces[index] as interf:. However, Pyroute2 typically expects the interface name rather than the index for such operations. Try and use the interface name instead of the index when moving it to the namespace:

with ipdb.interfaces[iface] as interf:
    interf.net_ns_fd = net_namespace

That would be:

from pyroute2 import NetNS, IPDB, netns

# Your existing code for setting up logging and getting interfaces

with IPDB() as ipdb:
    for iface in interfaces:
        if iface.find("wlxc") >= 0:
            logger.info(f"Found USB Wi-Fi interface: {iface} with details: {iface_details}")

            # Your existing code for handling network namespace

            # Move the interface to the new network namespace
            try:
                with ipdb.interfaces[iface] as interf:
                    interf.down()  # Make sure the interface is down before moving
                    interf.net_ns_fd = net_namespace
                logger.info(f"Moved {iface} to {net_namespace}")
            except Exception as e:
                logger.error(f"Error moving interface: {e}")

            # Your code for further operations

# Your code for closing IPDB

Also, make sure the namespace you are trying to move the interface to exists and is valid: after creating the namespace, do verify its existence or handle potential exceptions related to namespace creation more precisely.

from pyroute2 import NetNS, IPDB, netns
import logging

# Configure logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO, format="%(asctime)s:%(levelname)s:%(message)s")

import netifaces

# Get a list of all the network interfaces
interfaces = netifaces.interfaces()

# Managing network interfaces
with IPDB() as ipdb:
    for iface in interfaces:
        if iface.find("wlxc") >= 0:
            iface_details = netifaces.ifaddresses(iface)
            logger.info(f"Found USB Wi-Fi interface: {iface} with details: {iface_details}")

            iface_truncate = iface[-4:]
            net_namespace = f"{iface_truncate}-ns"

            # Check if namespace exists, if not, create it
            try:
                if net_namespace not in netns.listnetns():
                    logger.info(f"Creating network namespace: {net_namespace} ...")
                    netns.create(net_namespace)
                else:
                    logger.info(f"Namespace {net_namespace} already exists.")
            except Exception as e:
                logger.error(f"Error during namespace handling: {e}")
                continue  # Skip to next interface if there's an error

            # Move the interface to the new network namespace
            try:
                with ipdb.interfaces[iface] as interf:
                    interf.down()  # Ensure the interface is down before moving
                    interf.net_ns_fd = net_namespace
                    logger.info(f"Moved {iface} to {net_namespace}")
            except Exception as e:
                logger.error(f"Error moving interface: {e}")

# Close IPDB
ipdb.release()

Before attempting to create a new namespace, the code checks if it already exists using netns.listnetns(). This prevents errors related to trying to create a namespace that's already there.
This includes try-except blocks around namespace operations to handle any exceptions that might occur during the creation or verification of the namespace. And some more logging statements, for better tracking of the process, especially regarding namespace operations.