Network namespaces are a copy of the network stack that can run inside of your machine. They have all sorts of interesting technical reasons for existing. They are also excellent tools for prototyping networking applications.
Here we’ll walk through creating two namespaces on a Linux machine and sending traffic through them. We’ll then conclude with an example of using network namespaces to test the performance of a simple TCP proxy.
We’ll start by creating a topology that looks something like this:
left right
+----------------+ +----------------+
| | bridge | |
| 10.0.1.1/24 | | | 10.0.0.1/24 |
| +---+ | +----+ |
| | +--------+----------+ | |
| +---+ | +----+ |
| | | | |
| | | | |
+----------------+ +----------------+
Here we have two namespaces, left and right, connected by veths to a bridge. You can think of a veth as a virtual Ethernet cable and a bridge as a virtual switch. One way to imagine this setup is two computers plugged into an Ethernet switch.
Each namespace is responsible for the CIDR 10.0.1.1/24
and 10.0.0.1/24
respectively. This means that they are responsible for traffic destined for those CIDRS and personally have the addresses 10.0.1.1
and 10.0.0.1
respectively.
By the end of this our goal is that right should be able to ping left by running ping 10.0.1.1
.
In the interest of having good form we’ll organize our shell script to create these namespaces into two functions setup
and teardown
. The setup function will be responsible for creating the topology and the teardown function for destroying it.
We can start by creating an outline of this script as follows:
#!/bin/bash
function setup() {
echo "setup"
}
function teardown() {
echo "teardown"
}
case $1 in
up)
setup
;;
down)
teardown
;;
*)
echo "usage: $0 <up/down>"
;;
esac
We can now invoke our script as follows:
$ bash demo.sh
usage: demo.sh <up/down>
$ bash demo.sh up
setup
$ bash demo.sh down
teardown
Now we can start filling in our setup
function and creating some namespaces.
We can create our namespaces as follows:
ip netns add left
ip netns add right
With namespaces on the system we can inspect them by running ip netns exec left bash
. Let’s enter the left namespace and look at what links have been configured.
$ sudo ip netns exec left bash
$ ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: gre0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/gre 0.0.0.0 brd 0.0.0.0
3: gretap0@NONE: <BROADCAST,MULTICAST> mtu 1462 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
4: erspan0@NONE: <BROADCAST,MULTICAST> mtu 1450 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
An astute observer will notice that the looback interface is not up. This can be verified by running ping 127.0.0.1
from inside the namespace.
Our first task inside the namespace is to fix that. Using the ip netns exec
command every time we want to do something inside of a namespace is a little tedious so as an alternative we can use the ip -n
command to execute an ip
command inside of namespace.
For example, to set our loopback interfaces to up from the root namespace:
Now ping 127.0.0.1
works as expected. Nice.
Our setup function now looks like this:
function setup() {
ip netns add left
ip netns add right
ip -n left link set lo up
ip -n right link set lo up
}
Adding some logic to delete namespaces our teardown function looks like this:
We’ve now created our namespaces but they have no way of communicating with each other. Let’s start by creating some virtual Ethernet cables and plugging them into the namespaces.
Create them:
ip link add left_outer type veth peer name left_inner
ip link add right_outer type veth peer name right_inner
Plug them in:
ip link set left_inner netns left
ip link set right_inner netns right
Turn them on:
ip link set dev left_outer up
ip link set dev right_outer up
ip -n left link set dev left_inner up
ip -n right link set dev right_inner up
These commands may seem daunting but they’re really reasonably straightforward. Rather than try and explain the details and get something wrong, I’ll point you towards man ip-link
if you’re curious about the technical details.
Now that we have our cables, let’s create our bridge.
ip link add br0 type bridge
ip link set br0 up
With our bridge created we can plug our Ethernet cables into it.
ip link set left_outer master br0
ip link set right_outer master br0
Nice.
Our setup and teardown functions now look like this:
function setup() {
ip netns add left
ip netns add right
ip -n left link set lo up
ip -n right link set lo up
ip link add left_outer type veth peer name left_inner
ip link add right_outer type veth peer name right_inner
ip link set left_inner netns left
ip link set right_inner netns right
ip link set dev left_outer up
ip link set dev right_outer up
ip link add br0 type bridge
ip link set br0 up
ip link set left_outer master br0
ip link set right_outer master br0
}
At this point all of our cables are plugged in but trying to run a ping command from one of the namespaces to the other one will result in an error connect: Network is unreachable
.
This might make some sense because we have said literally nothing about ip addresses thus far. Let’s go ahead and add some addresses to the Ethernet cables that we plugged into our namespaces.
ip -n left addr add 10.0.1.1/24 dev left_inner
ip -n right addr add 10.0.0.1/24 dev right_inner
With those addresses in place namespaces can ping their own addresses but can not reach the other namespace.
$ ip netns exec right bash
$ ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.025 ms
$ ping 10.0.1.1
connect: Network is unreachable
The reason they can not reach each other is that we have yet to inform them that they can reach those addresses via their ethernet cables. We can tell them as such with some ip route
commands.
ip -n left route add 10.0.0.0/24 dev left_inner
ip -n right route add 10.0.1.0/24 dev right_inner
With those routes in place our pings work!
$ sudo bash demo.sh up
$ sudo ip netns exec left bash
$ ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.079 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.052 ms
C-c C-c
--- 10.0.0.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 9ms
rtt min/avg/max/mdev = 0.052/0.065/0.079/0.015 ms
$ ping 10.0.1.1
PING 10.0.1.1 (10.0.1.1) 56(84) bytes of data.
64 bytes from 10.0.1.1: icmp_seq=1 ttl=64 time=0.023 ms
64 bytes from 10.0.1.1: icmp_seq=2 ttl=64 time=0.043 ms
C-c C-c
--- 10.0.1.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 5ms
rtt min/avg/max/mdev = 0.023/0.033/0.043/0.010 ms
Nice!
Our final setup and teardown functions look like this:
function setup() {
ip netns add left
ip netns add right
ip -n left link set lo up
ip -n right link set lo up
ip link add left_outer type veth peer name left_inner
ip link add right_outer type veth peer name right_inner
ip link set left_inner netns left
ip link set right_inner netns right
ip link set dev left_outer up
ip link set dev right_outer up
ip -n left link set dev left_inner up
ip -n right link set dev right_inner up
ip link add br0 type bridge
ip link set br0 up
ip link set left_outer master br0
ip link set right_outer master br0
ip -n left addr add 10.0.1.1/24 dev left_inner
ip -n right addr add 10.0.0.1/24 dev right_inner
ip -n left route add 10.0.0.0/24 dev left_inner
ip -n right route add 10.0.1.0/24 dev right_inner
}
With these tools in your toolbelt you can use network namespaces to prototype increasingly complex network topologies without using something heavy like docker containers or actual virtual machines.
For an example of this see this script that I use to benchmark the performance of a little TCP proxy.