Linux Network Namespaces: Isolated Network Stacks Without a Hypervisor

Every process on a Linux host shares the same network stack by default — the same interfaces, the same routing table, the same iptables rules, the same socket namespace. Network namespaces break that assumption. A process running in a network namespace has its own completely independent network stack: its own interfaces, routing table, ARP table, nftables ruleset, and port space. Two processes in different namespaces can both bind to port 80 without conflict.

This is the mechanism underneath Docker containers, Kubernetes pods, network function virtualisation, and most VPN implementations on Linux. Understanding namespaces directly — not through an abstraction layer — gives you both a cleaner mental model of how those systems work and a powerful tool for network testing and isolation in its own right.

The Linux VRFs post covered VRFs as a lighter-weight route isolation mechanism. This post covers namespaces, which go further: where a VRF isolates only the routing table, a namespace isolates the entire network stack.

(If you want the BGP/OSPF multi-router lab angle instead, see Linux Networking from the Ground Up — this post takes the container-runtime angle: the same primitives, applied to how Docker and Kubernetes actually build pod networking.)


What a Network Namespace Contains

Each network namespace is an independent instance of:

  • Network interfaces (each interface belongs to exactly one namespace at a time)
  • Routing tables (including policy routing rules)
  • ARP / neighbour table
  • Netfilter hooks (nftables / iptables rules)
  • Socket table (bound ports, established connections)
  • sysctl network settings (e.g. net.ipv4.ip_forward is per-namespace)
  • /proc/net/ entries

The loopback interface (lo) is created automatically in every new namespace but starts down — you need to bring it up explicitly.


Basic Namespace Operations

# Create a named namespace
ip netns add blue

# List namespaces
ip netns list

# Execute a command inside a namespace
ip netns exec blue ip link show

# Open a shell inside a namespace
ip netns exec blue bash

# Delete a namespace
ip netns delete blue

Named namespaces are stored as bind-mounts under /var/run/netns/. The name is just a handle — the namespace itself is a kernel object referenced by file descriptor.

Inside a new namespace, only the loopback exists and it is down:

ip netns exec blue ip link show
# 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN

ip netns exec blue ip link set lo up

Connecting Namespaces: veth Pairs

The standard way to connect two namespaces (or a namespace to the host) is a veth pair — a virtual Ethernet cable with one end in each namespace. Packets sent into one end emerge from the other.

# Create a veth pair
ip link add veth-blue type veth peer name veth-host

# Move one end into the namespace
ip link set veth-blue netns blue

# Configure addresses
ip addr add 10.10.0.1/24 dev veth-host
ip netns exec blue ip addr add 10.10.0.2/24 dev veth-blue

# Bring both ends up
ip link set veth-host up
ip netns exec blue ip link set veth-blue up
ip netns exec blue ip link set lo up

# Test
ping -c3 10.10.0.2
ip netns exec blue ping -c3 10.10.0.1

At this point the namespace has its own interface and can communicate with the host over the veth pair. It has no default route and no access to anything beyond the host’s veth end.


Giving a Namespace Internet Access

To give a namespace outbound internet access, the host needs to NAT the namespace’s traffic. This requires ip_forward and a masquerade rule:

# Enable IP forwarding on the host
sysctl -w net.ipv4.ip_forward=1

# Add a default route in the namespace pointing to the host veth end
ip netns exec blue ip route add default via 10.10.0.1

# NAT outbound traffic from the namespace (nftables)
nft add table ip nat
nft add chain ip nat postrouting '{ type nat hook postrouting priority 100; }'
nft add rule ip nat postrouting ip saddr 10.10.0.0/24 oif eth0 masquerade

Or with iptables if that is what is in use:

iptables -t nat -A POSTROUTING -s 10.10.0.0/24 -o eth0 -j MASQUERADE

Now the namespace has full outbound connectivity through the host:

ip netns exec blue curl -s https://ifconfig.me
# Returns the host's public IP — the namespace is NATted

Multiple Namespaces with a Bridge

For multiple namespaces that need to talk to each other and the host, use a Linux bridge:

# Create a bridge on the host
ip link add br0 type bridge
ip addr add 10.20.0.1/24 dev br0
ip link set br0 up

# Create namespace red
ip netns add red
ip link add veth-red type veth peer name veth-red-br
ip link set veth-red netns red
ip link set veth-red-br master br0
ip link set veth-red-br up
ip netns exec red ip addr add 10.20.0.2/24 dev veth-red
ip netns exec red ip route add default via 10.20.0.1
ip netns exec red ip link set veth-red up
ip netns exec red ip link set lo up

# Create namespace green
ip netns add green
ip link add veth-green type veth peer name veth-green-br
ip link set veth-green netns green
ip link set veth-green-br master br0
ip link set veth-green-br up
ip netns exec green ip addr add 10.20.0.3/24 dev veth-green
ip netns exec green ip route add default via 10.20.0.1
ip netns exec green ip link set veth-green up
ip netns exec green ip link set lo up

Now red and green can reach each other and the host through the bridge. This is exactly how Docker creates a container network — the bridge is docker0, and each container’s veth pair is plumbed in the same way.

Verify connectivity:

ip netns exec red ping -c2 10.20.0.3    # red -> green
ip netns exec green ping -c2 10.20.0.1  # green -> host

Moving Physical Interfaces Into Namespaces

Any interface can be moved into a namespace — including physical NICs:

# Move eth1 into namespace blue
ip link set eth1 netns blue

# Configure it inside the namespace
ip netns exec blue ip addr add 192.168.100.10/24 dev eth1
ip netns exec blue ip link set eth1 up
ip netns exec blue ip route add default via 192.168.100.1

Once moved, eth1 disappears from the host’s network stack entirely. The host cannot see or use it. This is useful for network appliances, out-of-band management isolation, or giving a process exclusive access to a physical NIC.

To return it to the host namespace:

# Namespace 1 is always the host (initial) namespace
ip netns exec blue ip link set eth1 netns 1

Namespace-Scoped nftables

Each namespace has its own independent nftables instance. Rules loaded in the host namespace have no effect inside a namespace, and vice versa:

# Load firewall rules that only apply inside namespace blue
ip netns exec blue nft -f - <<'EOF'
flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;
        ct state established,related accept
        iif lo accept
        tcp dport { 80, 443 } accept
    }
}
EOF

# Verify — this ruleset is isolated to blue
ip netns exec blue nft list ruleset
nft list ruleset    # host ruleset is unchanged

This per-namespace isolation is what makes container security models work — each container’s network namespace has its own firewall, and a misconfigured container rule cannot affect the host or other containers.


Network Namespaces for Testing

The most practical everyday use of namespaces for network engineers is building isolated test topologies without virtual machines. The combination of namespaces, veth pairs, and the tc netem qdisc from the tc post lets you simulate any network condition:

# Build a two-node topology: client -- [100ms latency, 1% loss] -- server
ip netns add client
ip netns add server

ip link add veth-client type veth peer name veth-server
ip link set veth-client netns client
ip link set veth-server netns server

ip netns exec client ip addr add 10.30.0.1/24 dev veth-client
ip netns exec server ip addr add 10.30.0.2/24 dev veth-server
ip netns exec client ip link set veth-client up
ip netns exec server ip link set veth-server up
ip netns exec client ip link set lo up
ip netns exec server ip link set lo up

# Apply netem impairments on the client's egress
ip netns exec client tc qdisc add dev veth-client root netem delay 100ms loss 1%

# Test
ip netns exec client ping -c10 10.30.0.2
# rtt min/avg/max/mdev = 100.xxx/100.xxx/101.xxx ms

# Run iperf3 — server in one terminal, client in another
ip netns exec server iperf3 -s &
ip netns exec client iperf3 -c 10.30.0.2 -t 10

This replaces a two-VM lab for most TCP behaviour testing. Spin it up in seconds, apply any tc netem profile, tear it down when done.


Namespaces vs VRFs — When to Use Which

From the comparison in the Linux VRFs post, the decision is usually straightforward:

NeedUse
Isolate routing tables on one host, all traffic visible to host firewallVRF
Management plane / data plane separation on a single interface setVRF
Full network stack isolation — own firewall, own ports, own interfacesNamespace
Container / pod networkingNamespace (Docker/k8s manage this for you)
Reproducible network test topologyNamespace
Process scoping (only one process uses an interface)Namespace

The practical difference: a VRF is a routing construct that lives inside the host network stack. A namespace is a separate network stack. If you need the host to be able to see and filter traffic in the isolated segment, use a VRF. If you need hard isolation — separate firewall rules, separate port space, no shared kernel state — use a namespace.


Persistent Namespaces with systemd

The ip netns commands are not persistent across reboots. For persistent namespace topology, a startup script managed by systemd is the simplest approach:

# /usr/local/bin/setup-namespaces.sh
#!/usr/bin/env bash
set -euo pipefail

ip netns add blue 2>/dev/null || true
ip link add veth-blue type veth peer name veth-host 2>/dev/null || true
ip link set veth-blue netns blue
ip addr add 10.10.0.1/24 dev veth-host 2>/dev/null || true
ip netns exec blue ip addr add 10.10.0.2/24 dev veth-blue 2>/dev/null || true
ip link set veth-host up
ip netns exec blue ip link set veth-blue up
ip netns exec blue ip link set lo up
ip netns exec blue ip route add default via 10.10.0.1 2>/dev/null || true
# /etc/systemd/system/namespaces.service
[Unit]
Description=Network namespace setup
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/setup-namespaces.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
systemctl enable --now namespaces.service

Connecting to the Series

Namespaces are the layer that ties the rest of this series together in a container or VM context. The tc netem profiles from the tc post apply inside namespaces for test topology simulation. The ss tool scoped with ip netns exec shows per-namespace socket state. The nftables rules from the nftables post are per-namespace. The ip vrf exec pattern from the VRFs post has a direct parallel in ip netns exec — the difference is the scope of isolation.

If you have ever wondered exactly what docker network create does, the answer is: it creates a Linux bridge, a network namespace per container, and a veth pair connecting each namespace to the bridge — exactly the multi-namespace bridge topology above.

  • Linux VRFs — the lighter-weight routing isolation model; read alongside this post for a clear comparison of when each is appropriate
  • Cilium — the eBPF-based Kubernetes CNI that builds directly on the namespaces, veth pairs, and bridge topology covered above to implement pod networking across nodes