ip netns is what container networking actually is. No daemon required.
Published by RodHat

Docker’s networking model is not magic. It is a network namespace, a veth pair, a bridge, some iptables rules, and NAT. That’s the entire list. You can assemble all of these with ip from the command line — no daemon, no image pull, no docker network ls gobbledygook — and get isolated network stacks in under three seconds.
If you understand what ip netns actually does, you understand container networking. If you don’t, you’re debugging Docker issues by reading Docker documentation, which is a miserable way to spend an afternoon.
What a network namespace isolates
A network namespace gives a process its own:
- Network interfaces (the new namespace starts with only
lo, state DOWN, unconnected) - Routing table (empty except
lo) - iptables/nftables chains (empty)
- Socket table —
ss -tnpinside the namespace shows only sockets in that namespace - ARP and neighbor tables
Processes in different namespaces cannot see each other’s sockets, cannot bind the same port on the same IP without coordination, and have independent firewall state. The kernel maintains all of this per-namespace.
What it does not isolate: CPU, memory, PIDs (unless you also create a PID namespace), or the filesystem. Network namespaces are narrowly scoped. That’s a feature.
Create and enter a namespace
ip netns add testnet
ip netns list
testnet
Run a command inside it:
ip netns exec testnet ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
Only lo, state DOWN. No routes, no external interfaces, no connection to anything. A process exec’d inside this namespace cannot reach the network.
Bring lo up:
ip netns exec testnet ip link set lo up
ip netns exec testnet ping 127.0.0.1 -c1
Loopback works. Still no external connectivity.
Connect the namespace to the outside world with a veth pair
A veth (virtual ethernet) pair is two connected virtual NICs — think of it as a pipe where both ends look like network interfaces. Create one with one end in the host namespace and one end in testnet:
# Create the pair
ip link add veth0 type veth peer name veth1
# Move veth1 into the testnet namespace
ip link set veth1 netns testnet
Now the host has veth0 and testnet has veth1. They’re connected at the link layer — anything sent into one end comes out the other.
Assign addresses and bring both up:
# Host side
ip addr add 192.168.100.1/24 dev veth0
ip link set veth0 up
# Namespace side
ip netns exec testnet ip addr add 192.168.100.2/24 dev veth1
ip netns exec testnet ip link set veth1 up
Test:
ping 192.168.100.2 -c3
PING 192.168.100.2 (192.168.100.2) 56(84) bytes of data.
64 bytes from 192.168.100.2: icmp_seq=1 ttl=64 time=0.038 ms
64 bytes from 192.168.100.2: icmp_seq=2 ttl=64 time=0.041 ms
To give the namespace external internet access, add a default route and NAT:
ip netns exec testnet ip route add default via 192.168.100.1
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -j MASQUERADE
This is exactly what Docker does when you start a container with --network bridge. The daemon creates the namespace, creates a veth pair, connects the host-side veth to the docker0 bridge (so multiple containers share one bridge interface), adds a MASQUERADE rule. That’s the whole trick. The “bridge” step just lets N containers share one host-side interface rather than having N separate veth interfaces dangling off the host.
Run a process inside the namespace
ip netns exec testnet bash
That bash session — and every process it forks — is in testnet. Run ss -tnp inside and you see only sockets in this namespace. Bind :8080 inside, and the host sees nothing on that port unless you explicitly route traffic to it. Two different processes in two different namespaces can bind :8080 simultaneously on the same kernel with no conflict.
For a one-off isolation without persisting a named namespace, unshare(1):
unshare --net bash
Spawns a bash in a fresh anonymous network namespace. No ip netns add, no cleanup — the namespace evaporates when the process exits. Useful for quick isolation; not useful when you need to attach interfaces or manage the namespace from the host simultaneously.
Use case: test firewall rules without touching production
The most practical reason I use network namespaces outside of container work: validating iptables rule changes before applying them to a live box.
ip netns add fwtest
ip link add fw0 type veth peer name fw1
ip link set fw1 netns fwtest
ip link set fw0 up
ip netns exec fwtest ip link set fw1 up
# Now test your proposed rules in isolation
ip netns exec fwtest iptables -A INPUT -p tcp --dport 22 -j ACCEPT
ip netns exec fwtest iptables -A INPUT -j DROP
# Verify from inside
ip netns exec fwtest iptables -L INPUT -n -v
The namespace has fully independent iptables state. Test rules against processes running inside it. Same for nftables — each namespace has its own nftables chains. When you’re confident the rules are correct, translate them to the host. No rollback drama because nothing on the production box changed.
Use case: simulate a multi-hop network topology on one box
Three namespaces, two veth pairs, and you have a simulated router between two subnets:
ip netns add left
ip netns add router
ip netns add right
ip link add lr0 type veth peer name lr1
ip link add rr0 type veth peer name rr1
ip link set lr0 netns left
ip link set lr1 netns router
ip link set rr0 netns router
ip link set rr1 netns right
# left: 10.0.1.1/24, routes through router
ip netns exec left ip addr add 10.0.1.1/24 dev lr0
ip netns exec left ip link set lr0 up
ip netns exec left ip route add default via 10.0.1.254
# router: two interfaces, forwarding enabled
ip netns exec router ip addr add 10.0.1.254/24 dev lr1
ip netns exec router ip addr add 10.0.2.254/24 dev rr0
ip netns exec router ip link set lr1 up
ip netns exec router ip link set rr0 up
ip netns exec router sysctl -w net.ipv4.ip_forward=1
# right: 10.0.2.1/24, routes through router
ip netns exec right ip addr add 10.0.2.1/24 dev rr1
ip netns exec right ip link set rr1 up
ip netns exec right ip route add default via 10.0.2.254
Now left can ping right through router. Install firewall rules on router, test routing daemons, simulate link failures by downing interfaces — all on one box, no VMs.
Add tc netem on a veth to simulate a degraded link:
ip netns exec router tc qdisc add dev lr1 root netem delay 50ms loss 1%
50ms latency and 1% packet loss on the left-to-router link. Run your application against the simulated topology, observe behavior, remove the netem qdisc when done:
ip netns exec router tc qdisc del dev lr1 root
Policy routing via ip rule applies per-namespace — each namespace has its own routing table set and rule database. A namespace with a custom ip rule for PBR is fully independent from the host’s routing policy.
Enter a running container’s network namespace
Containers are just processes with namespaces. Their network namespace is a file descriptor at /proc/$pid/ns/net. Enter it without stopping the process:
# Get the container's PID
pid=$(docker inspect --format '{{.State.Pid}}' mycontainer)
# Enter the network namespace
nsenter --net=/proc/$pid/ns/net bash
Inside that shell, ss -tnp, ip addr, ip route, and iptables -L all operate on the container’s network stack, not the host’s. Useful when the container is missing ss or ip — you run the host’s tools inside the container’s namespace.
The full namespace-entering workflow, including mount, PID, and UTS namespaces, is at nsenter — entering Linux namespaces without stopping the process.
Cleanup
Named namespaces persist until explicitly deleted:
ip netns del testnet
ip netns del fwtest
ip netns del left
ip netns del router
ip netns del right
Deleting a namespace cleans up the associated veth interfaces and routes automatically. If you moved a real physical interface into a namespace (not a veth — an actual NIC), move it back to the host first or you’ll lose it:
# Move eth1 back to the init namespace (PID 1)
ip netns exec stranded ip link set eth1 netns 1
The kernel implementation of veth, per-namespace routing tables, and how the netns file descriptor works under the hood is in The Linux Programming Interface — Kerrisk covers network namespaces in the namespaces chapters, and the treatment of how ip netns exec is implemented (via setns(2)) is the clearest published explanation I’ve seen outside of the kernel source. For the BSD equivalent: vnet jails give you isolated network stacks with a cleaner mental model — no dangling file descriptors, lifetime tied to the jail. Rod’s opinions on vnet vs Linux netns are a separate conversation.