Michael Zamot / Writing

Network namespaces and VRFs: separate routing on one Linux host

How Linux keeps routing separate on one machine: VRFs for separate routing tables, network namespaces for separate network stacks, and veth pairs to connect them.

A router often serves more than one customer, and customers don’t coordinate their address plans. Two of them can both use 10.0.0.0/24, and the router has to keep their traffic and their routes completely apart. Service providers solve this with VRFs, and the same need appears on any Linux host that runs containers, virtual machines or several services that must not see each other’s networks.

Linux has two mechanisms for it. A VRF gives you separate routing tables on one shared network stack. A network namespace gives you a separate network stack altogether. They solve related problems at different depths, and they are often combined. This post explains both, shows what each one isolates, and connects them with veth pairs.

How they work

The routing table. Every Linux system makes forwarding decisions from a routing table, the one you see with ip route. By default there is one, called the main table, and all interfaces and routes share it. That is the root of the overlapping-address problem: two connected networks with the same prefix end up as two competing routes in one table.

VRF. A VRF (virtual routing and forwarding instance) is a separate routing table together with the interfaces that belong to it. On Linux a VRF is a device you create with a table number: ip link add red type vrf table 10. An interface is attached to it by making it a slave of the VRF device. From then on, traffic arriving on that interface is routed using the VRF’s table, and the interface’s connected routes live in that table, not in the main one. Everything else stays shared: the interfaces are all visible to everyone, and so are the firewall rules and the kernel settings.

Network namespace. A network namespace is a complete, independent copy of the network stack. It has its own interfaces, its own routing tables, its own firewall rules and its own kernel settings. An interface lives in exactly one namespace at a time, and a process sees only the interfaces of the namespace it runs in. Containers are built on this.

veth pair. A veth pair is a virtual Ethernet cable with two ends: a frame sent into one end comes out of the other. The two ends can be placed in different namespaces, which makes a veth pair the standard way to connect them.

VRFNetwork namespace
Routing tablesSeparate per VRFSeparate per namespace
InterfacesAll visible everywhere; each one belongs to one VRFOnly the namespace’s own interfaces are visible
Kernel settings (sysctl)Shared: global settings apply to every VRFSeparate per namespace
Firewall rulesSharedSeparate per namespace
Run a command inside itip vrf exec NAME …ip netns exec NAME …

The lab

A Linux router connects two customers, red1 and blue1, and both use the address 10.0.0.2/24:

   red1 ────── router ────── blue1
  10.0.0.2    eth1  eth2     10.0.0.2
NodeInterfaceIPv4 addressConnected to
routereth1—red1
routereth2—blue1
red1eth110.0.0.2/24router
blue1eth110.0.0.2/24router

The router’s own addresses are added during the walkthrough. The nodes are containers deployed with containerlab. You need containerlab and podman. They run a small image built from the official Alpine Linux image with the iproute2 package, which provides the ip command. The lab contains its two-line Containerfile, and you build the image once:

sudo podman build -t localhost/nsvrf-node:latest image/

Get the files from the lab repository and cd into namespaces-vrf-basics, then deploy:

sudo containerlab --runtime podman deploy -t topology.clab.yml

The commands in the rest of the post run on the router, in a shell opened with:

sudo podman exec -it clab-nsvrf-router sh

Overlapping addresses without VRFs

Give the router the address 10.0.0.1/24 on both interfaces, one toward each customer:

ip addr add 10.0.0.1/24 dev eth1
ip addr add 10.0.0.1/24 dev eth2
ip route
default via 172.20.20.1 dev eth0 proto static metric 100 
10.0.0.0/24 dev eth1 proto kernel scope link src 10.0.0.1 
10.0.0.0/24 dev eth2 proto kernel scope link src 10.0.0.1 
172.20.20.0/24 dev eth0 proto kernel scope link src 172.20.20.4 

Linux accepts it, and now the table holds two routes for the same prefix. Ping 10.0.0.2 and look at which neighbor answered:

ping -c 2 10.0.0.2
ip neigh show 10.0.0.2
--- 10.0.0.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.061/0.062/0.063 ms
10.0.0.2 dev eth1 lladdr aa:c1:ab:38:4e:d6 REACHABLE 

The ping works, but only through eth1: the neighbor entry is on eth1, with a MAC address that belongs to red1. The lookup picks the first matching route every time, so with one routing table the router can only ever reach one of the two customers at 10.0.0.2. Remove the addresses before going on:

ip addr flush dev eth1
ip addr flush dev eth2
ip neigh flush all

Separate routing tables with VRFs

Create one VRF per customer, attach each interface to its VRF, and only then assign the addresses:

ip link add red type vrf table 10
ip link add blue type vrf table 20
ip link set red up
ip link set blue up
ip link set eth1 master red
ip link set eth2 master blue
ip addr add 10.0.0.1/24 dev eth1
ip addr add 10.0.0.1/24 dev eth2
ip -br link show type vrf
ip -br addr show | grep -E "^eth[12]"
red              UP             f6:22:32:fe:e1:69 <NOARP,MASTER,UP,LOWER_UP> 
blue             UP             de:85:ba:a0:eb:10 <NOARP,MASTER,UP,LOWER_UP> 
eth2@if764       UP             10.0.0.1/24 
eth1@if765       UP             10.0.0.1/24 

red and blue are VRF devices, tied to routing tables 10 and 20. Both interfaces now have the same address. Look at the routing tables:

ip route
ip route show vrf red
ip route show vrf blue
default via 172.20.20.1 dev eth0 proto static metric 100 
172.20.20.0/24 dev eth0 proto kernel scope link src 172.20.20.4 

10.0.0.0/24 dev eth1 proto kernel scope link src 10.0.0.1 

10.0.0.0/24 dev eth2 proto kernel scope link src 10.0.0.1 

The main table has no 10.0.0.0/24 any more. Each VRF has its own copy, pointing at its own interface, so there is no conflict. A rule in the kernel sends lookups for VRF interfaces to the VRF tables:

ip rule
0:	from all lookup local
1000:	from all lookup [l3mdev-table]
32766:	from all lookup main
32767:	from all lookup default

The l3mdev-table rule is what makes it work: for traffic tied to a VRF, the kernel consults the VRF’s table.

Using a VRF

A plain ping from the router now fails, because the main table has no route to 10.0.0.2. To use a VRF you say which one, either by binding a command to it or by running it inside it:

ping -c 1 -W 1 10.0.0.2
ping -c 2 -I red 10.0.0.2
ip vrf exec blue ping -c 2 10.0.0.2
--- 10.0.0.2 ping statistics ---
1 packets transmitted, 0 packets received, 100% packet loss

--- 10.0.0.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.051/0.055/0.060 ms

--- 10.0.0.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.055/0.066/0.078 ms

The same destination address now reaches two different machines, depending on the VRF. The neighbor tables prove it:

ip neigh show vrf red
ip neigh show vrf blue
10.0.0.2 dev eth1 lladdr aa:c1:ab:38:4e:d6 DELAY 
10.0.0.2 dev eth2 lladdr aa:c1:ab:b2:ac:46 REACHABLE 

The same IP address, 10.0.0.2, has a different MAC address in each VRF, and they are the two clients’ addresses: red1’s eth1 is aa:c1:ab:38:4e:d6 and blue1’s is aa:c1:ab:b2:ac:46. Each client can also reach its gateway, 10.0.0.1, which is a different interface on the router for each of them.

Separate network stacks with namespaces

A namespace goes further than a VRF. Create one, and connect it to the router’s default namespace with a veth pair, keeping one end here and moving the other end into the namespace:

ip netns add ns1
ip link add veth0 type veth peer name veth1
ip link set veth1 netns ns1
ip addr add 172.16.0.1/30 dev veth0
ip link set veth0 up
ip -n ns1 addr add 172.16.0.2/30 dev veth1
ip -n ns1 link set veth1 up
ip -n ns1 link set lo up
ip netns list
ping -c 2 172.16.0.2
ns1 (id: 3)

--- 172.16.0.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.052/0.056/0.061 ms

ip -n ns1 … runs an ip command inside the namespace. The two ends of the veth pair sit on a /30 link, one in each namespace, and the router can reach the one inside ns1 like any neighbor. Compare what each namespace can see:

ip -n ns1 -br link
ip -br link | awk '{print $1,$2}'
ip -n ns1 route
lo               UNKNOWN        00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP> 
veth1@if6        UP             3e:cb:78:ba:57:92 <BROADCAST,MULTICAST,UP,LOWER_UP> 

lo UNKNOWN
eth0@if761 UP
red UP
blue UP
veth0@if5 UP
eth2@if764 UP
eth1@if765 UP

172.16.0.0/30 dev veth1 proto kernel scope link src 172.16.0.2 

Inside ns1 there are only two interfaces, lo and veth1, and a single route. The router’s eth1, eth2 and the VRFs are invisible there, while the default namespace sees everything, including veth0. A VRF could not hide interfaces like this. The namespace cannot reach the customers either:

ip netns exec ns1 ping -c 1 -W 1 10.0.0.2
ping: sendto: Network unreachable

The kernel settings are separate too. Here IPv4 forwarding is on in the router and off in the namespace, set independently:

ip netns exec ns1 sysctl -w net.ipv4.ip_forward=0
sysctl -n net.ipv4.ip_forward
ip netns exec ns1 sysctl -n net.ipv4.ip_forward
net.ipv4.ip_forward = 0
1
0

A VRF inside a namespace

The two mechanisms work together. A namespace is a full network stack, so it can have VRFs of its own. Create one inside ns1 and move the veth end into it:

ip -n ns1 addr flush dev veth1
ip -n ns1 link add cust type vrf table 100
ip -n ns1 link set cust up
ip -n ns1 link set veth1 master cust
ip -n ns1 addr add 172.16.0.2/30 dev veth1
ip -n ns1 route show vrf cust
ip -n ns1 route
ip -n ns1 -br link show type vrf
ping -c 1 -W 1 172.16.0.2
172.16.0.0/30 dev veth1 proto kernel scope link src 172.16.0.2 

cust             UP             1e:be:7d:e3:29:9a <NOARP,MASTER,UP,LOWER_UP> 
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.090/0.090/0.090 ms

Inside ns1, the connected route has moved into the cust VRF, and the namespace’s own main table is empty. The ping from the router still works, because the interface is unchanged. The two layers are independent: the namespace isolates the stack, and the VRF separates the routing tables inside it.

Which to use

Use a VRF when one system must keep several routing domains apart while sharing everything else, as a router does for its customers. Use a namespace when you want a separate network stack, with its own interfaces, firewall and settings, as containers and isolated services need. A veth pair connects them, and nothing stops you from combining them.

Tear it down

sudo containerlab --runtime podman destroy -t topology.clab.yml --cleanup
← All writing