Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I open sockets in multiple network namespaces from my Python code?

I am running some application in multiple network namespace. And I need to create socket connection to the loopback address + a specific port in each of the name space. Note that the "specific port" is the same across all network namespaces. Is there a way I can create a socket connection like this in python?

Appreciate any pointer!

like image 781
jackiesyu Avatar asked Mar 04 '15 03:03

jackiesyu


1 Answers

This was a fun problem.

Update: I liked it so much that I packed up the solution as an installable Python module, available from https://github.com/larsks/python-netns.

You can access another network namespace through the use of the setns() system call. This call isn't exposed natively by Python, so in order to use it you would either (a) need to find a third-party module that wraps it, or (b) use something like the ctypes module to make it available in your Python code.

Using the second option (ctypes), I came up with this code:

#!/usr/bin/python

import argparse
import os
import select
import socket
import subprocess

# Python doesn't expose the `setns()` function manually, so
# we'll use the `ctypes` module to make it available.
from ctypes import cdll
libc = cdll.LoadLibrary('libc.so.6')
setns = libc.setns


# This is just a convenience function that will return the path
# to an appropriate namespace descriptor, give either a path,
# a network namespace name, or a pid.
def get_ns_path(nspath=None, nsname=None, nspid=None):
    if nsname:
        nspath = '/var/run/netns/%s' % nsname
    elif nspid:
        nspath = '/proc/%d/ns/net' % nspid

    return nspath

# This is a context manager that on enter assigns the process to an
# alternate network namespace (specified by name, filesystem path, or pid)
# and then re-assigns the process to its original network namespace on
# exit.
class Namespace (object):
    def __init__(self, nsname=None, nspath=None, nspid=None):
        self.mypath = get_ns_path(nspid=os.getpid())
        self.targetpath = get_ns_path(nspath,
                                  nsname=nsname,
                                  nspid=nspid)

        if not self.targetpath:
            raise ValueError('invalid namespace')

    def __enter__(self):
        # before entering a new namespace, we open a file descriptor
        # in the current namespace that we will use to restore
        # our namespace on exit.
        self.myns = open(self.mypath)
        with open(self.targetpath) as fd:
            setns(fd.fileno(), 0)

    def __exit__(self, *args):
        setns(self.myns.fileno(), 0)
        self.myns.close()


# This is a wrapper for socket.socket() that creates the socket inside the
# specified network namespace.
def nssocket(ns, *args):
    with Namespace(nsname=ns):
        s = socket.socket(*args)
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        return s


def main():
    # Create a socket inside the 'red' namespace
    red = nssocket('red')
    red.bind(('0.0.0.0', 7777))
    red.listen(10)

    # Create a socket inside the 'blue' namespace
    blue = nssocket('blue')
    blue.bind(('0.0.0.0', 7777))
    blue.listen(10)

    poll = select.poll()
    poll.register(red, select.POLLIN)
    poll.register(blue, select.POLLIN)

    sockets = {
        red.fileno(): {
            'socket': red,
            'label': 'red',
        },
        blue.fileno(): {
            'socket': blue,
            'label': 'blue',
        }
    }

    while True:
        events = poll.poll()

        for fd, event in events:
            sock = sockets[fd]['socket']
            label = sockets[fd]['label']

            if sock in [red, blue]:
                newsock, client = sock.accept()
                sockets[newsock.fileno()] = {
                    'socket': newsock,
                    'label': label,
                    'client': client,
                }

                poll.register(newsock, select.POLLIN)
            elif event & select.POLLIN:
                data = sock.recv(1024)
                if not data:
                    print 'closing fd %d (%s)' % (fd, label)
                    poll.unregister(sock)
                    sock.close()
                    continue
                print 'DATA %s [%d]: %s' % (label, fd, data)


if __name__ == '__main__':
    main()

Prior to running this code, I created two network namespaces:

# ip netns add red
# ip netns add blue

I added an interface inside of each namespace, so that the final configuration looked like this:

# ip netns exec red ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
816: virt-0-0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default 
    link/ether f2:9b:6a:fd:87:77 brd ff:ff:ff:ff:ff:ff
    inet 192.168.115.2/24 scope global virt-0-0
       valid_lft forever preferred_lft forever
    inet6 fe80::f09b:6aff:fefd:8777/64 scope link 
       valid_lft forever preferred_lft forever

# ip netns exec blue ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
817: virt-1-0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default 
    link/ether 82:94:6a:1b:13:16 brd ff:ff:ff:ff:ff:ff
    inet 192.168.113.2/24 scope global virt-1-0
       valid_lft forever preferred_lft forever
    inet6 fe80::8094:6aff:fe1b:1316/64 scope link 
       valid_lft forever preferred_lft forever

Running the code (as root, because you need to be root in order to make use of the setns call), I can connect to either 192.168.115.2:7777 (the red namespace) or 192.168.113.2:7777 (the blue namespace) and things work as expected.

like image 124
larsks Avatar answered Sep 18 '22 11:09

larsks