I use LXC containers mainly as isolated environments when I'm writing code, but also when I just want to try the last version of a package without being obliged to break something on my current configuration. This is the first part of a serie about LXC containers. In this post, I'll share with you what I've learned about network namespaces. Everything here will be done under Debian Jessie, as root, I'm using Python 3.5 and I've pip installed pyroute2.

To keep the schemas as simple as possible, I won't mention the interface lo actually available inside the root namespace. The root namespace is the namespace available to you after a fresh boot of your Operating System. This is a small schema of my configuration showing you where we'll start: Root namespace

What we will do:

  1. Create a namespace.
  2. Create a virtual ethernet device pair.
  3. Assign one side of the veth pair to the namespace.
  4. Assign IP addresses to each side of the veth pair.
  5. Set the default route for our namespace.
  6. Attach the other side of the veth pair to a bridge.

Network namespaces

A namespace is a way of scoping a particular set of identifiers. You can use the same identifier multiple times in different namespaces. Most importantly, you can also restrict an identifier set visible to particular processes.

Fact: the set of network interfaces and routing tables are shared across your entire Operating System. Network namespaces change that fundamental assumption. With network namespaces, you can have different and separate instances of network interfaces and routing tables that operate independent of each other. Let's see how it works, using the Python package Pyroute2:

>>> from pyroute2 import IPDB, IPRoute, NetNS, netns, NSPopen
>>> from subprocess import Popen, PIPE
>>> ipdb_namespace1 = IPDB(nl=NetNS('namespace1'))
>>> nsp = NSPopen('namespace1', ['ip', 'addr'], stdout=PIPE)
>>> for l in nsp.communicate()[0].split(b'\n'):
...   print(l)
...
b'1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default '
b'    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00'
b''


We've just created a network namespace named namespace1. The ip addr command launched inside the namespace namespace1 tells us that there is only one interface, lo. Namespace1 created

Now, we need a way to link our newly created namespace, to the root namespace. To do that, virtual ethernet devices, veth pair, need to be created and configured. A veth pair works like a patch cable, connecting two sides. It consists of two virtual interfaces, one of them is assigned to the root network namespace, while the other lives within the network namespace, namespace1:

>>> ipdb_root = IPDB() # root namespace
>>> ipdb_root.create(ifname='v01', kind='veth', peer='vp01').commit()
{'linkmode': 0, 'broadcast': 'ff:ff:ff:ff:ff:ff', 'num_rx_queues': 1, 'qdisc': 'noop', 'txqlen': 1000, 'ipaddr': [], 'group': 0, 'flags': 4098, 'neighbours': [], 'mtu': 1500, 'promiscuity': 0, 'vlans': [], 'address': 'ca:a0:d7:39:57:22', 'ipdb_scope': 'system', 'kind': 'veth', 'ipdb_priority': 0, 'ifi_type': 1, 'family': 0, 'carrier': 0, 'ports': [], 'peer': 'vp01', 'num_tx_queues': 1, 'index': 10, 'ifname': 'v01', 'carrier_changes': 1, 'operstate': 'DOWN'}
>>>

Veth pair device created

Veth pair device created. Both sides of the veth pair device are actually in the root namespace. To link the root namespace to our newly created namespace namespace1, one side of the veth pair device, the peer, should be assigned to namespace1:

>>> with ipdb_root.interfaces.vp01 as v:
...   v.net_ns_fd = 'namespace1'
...
>>>

Veth peer moved

Now, let's give IP addresses to all of our interfaces, and set everybody up:

>>> with ipdb_root.interfaces.v01 as veth:
...     veth.add_ip('192.168.1.36/24')
...     veth.up()
...
{'kind': 'veth', 'promiscuity': 0, 'group': 0, 'ipdb_scope': 'system', 'peer': 'vp01', 'linkmode': 0, 'ipdb_priority': 0, 'carrier_changes': 1, 'address': 'f2:2d:aa:9d:16:da', 'mtu': 1500, 'carrier': 0, 'ifname': 'v01', 'num_tx_queues': 1, 'index': 6, 'ifi_type': 1, 'ports': [], 'vlans': [], 'ipaddr': [], 'qdisc': 'noop', 'neighbours': [], 'txqlen': 1000, 'broadcast': 'ff:ff:ff:ff:ff:ff', 'operstate': 'DOWN', 'num_rx_queues': 1, 'flags': 4098, 'family': 0}
>>>
>>> with ipdb_namespace1.interfaces.vp01 as veth:
...     veth.add_ip('192.168.1.37/24')
...     veth.up()
...
{'kind': 'veth', 'promiscuity': 0, 'group': 0, 'ipdb_scope': 'system', 'ifi_type': 1, 'linkmode': 0, 'ipdb_priority': 0, 'carrier_changes': 1, 'num_rx_queues': 1, 'address': '26:29:f4:06:8a:ea', 'mtu': 1500, 'carrier': 0, 'ifname': 'vp01', 'num_tx_queues': 1, 'index': 5, 'ports': [], 'vlans': [], 'ipaddr': [], 'qdisc': 'noop', 'neighbours': [], 'txqlen': 1000, 'broadcast': 'ff:ff:ff:ff:ff:ff', 'operstate': 'DOWN', 'flags': 4098, 'family': 0}
>>>
>>> with ipdb_namespace1.interfaces.lo as i:
...   i.up()
...
{'flags': 8, 'promiscuity': 0, 'group': 0, 'ipdb_scope': 'system', 'ifi_type': 772, 'linkmode': 0, 'ipdb_priority': 0, 'carrier_changes': 0, 'address': '00:00:00:00:00:00', 'mtu': 65536, 'carrier': 1, 'ifname': 'lo', 'num_tx_queues': 1, 'index': 1, 'ports': [], 'vlans': [], 'ipaddr': [], 'qdisc': 'noop', 'neighbours': [], 'txqlen': 0, 'broadcast': '00:00:00:00:00:00', 'operstate': 'DOWN', 'num_rx_queues': 1, 'family': 0}
>>>



At this point, we can open a shell and directly check the configuration for v01 interface:

kintanu:~# ip addr show v01
6: v01: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether f2:2d:aa:9d:16:da brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.36/24 scope global v01
       valid_lft forever preferred_lft forever
    inet6 fe80::f02d:aaff:fe9d:16da/64 scope link
       valid_lft forever preferred_lft forever


We can also, still from the shell, check the available interfaces inside namespace1:

kintanu:~# ip netns exec namespace1 ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
5: vp01: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 26:29:f4:06:8a:ea brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.37/24 scope global vpeer01
       valid_lft forever preferred_lft forever
    inet6 fe80::2429:f4ff:fe06:8aea/64 scope link
       valid_lft forever preferred_lft forever
kintanu:~#


A system uses its routing table to determine which network interface to use when sending packets to remote systems. Let's make all traffic leaving namespace1 to go through v01:

kintanu:~# ip netns exec namespace1 ip route add default via 192.168.1.36

Default route added

I told you I often use LXC containers when I'm working. For that reason, I always have a bridge configured and up. Then, all the containers are connected to the internet, via the bridge, as ports. Let me show you:

kintanu:~# ip addr show br0
4: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 3c:97:0e:65:c0:27 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.34/24 brd 192.168.1.255 scope global br0
       valid_lft forever preferred_lft forever
    inet6 fe80::3e97:eff:fe65:c027/64 scope link
       valid_lft forever preferred_lft forever
kintanu:~# brctl show
bridge name     bridge id               STP enabled     interfaces
br0             8000.3c970e65c027       no              eth0
                                                        vethLMRO1N
kintanu:~#


The br0 interface is the interface I use for going outside. In fact, br0 is a bridge. eth0 and vethLMRON are interfaces, ports, attached to that bridge. v01 should be attached to the br0 bridge, otherwise, v01 won't be able to send packets to the outside world:

kintanu:~# brctl addif br0 v01
kintanu:~# brctl show
bridge name     bridge id               STP enabled     interfaces
br0             8000.3c970e65c027       no              eth0
                                                        vethLMRO1N
                                                        v01
kintanu:~#


Veth pair device added to bridge

If everything went fine, you should be able to ping an external host from namespace1:

kintanu:~# ip netns exec namespace1 ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
From 192.168.1.34: icmp_seq=1 Redirect Host(New nexthop: 192.168.1.1)
64 bytes from 8.8.8.8: icmp_seq=1 ttl=54 time=104 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=54 time=105 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=54 time=105 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=54 time=104 ms
^C
--- 8.8.8.8 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 104.190/104.859/105.375/0.595 ms
kintanu:~#


We've just seen how to create a network namespace, and link that network namespace to the outside world. When playing with LXC containers, you will also heard about control groups, a kernel mechanism that will allow you to allocate resources among user-defined groups of tasks (processes) running on a system. That will be the topic of the next chapter.




All the diagrams were made using Asciiflow