Skip to content
Menu

Networking9 min read

Namespaces & Virtual Interfaces

The kernel primitives that make container networking possible

Network Namespaces: Fully Isolated Network Stacks

A network namespace is a kernel construct that provides a complete, independent copy of the entire network stack. Each namespace has its own:

Per-Namespace Resources

  • Network interfaces (lo, eth0, etc.)
  • Routing table (separate ip route)
  • iptables/nftables rules (separate firewall)
  • Socket bindings (port 80 in ns1 != port 80 in ns2)
  • ARP/NDP tables
  • /proc/net view

Where Namespaces Are Used

  • Docker containers — each container gets one
  • Kubernetes pods — all containers in a pod share one
  • Network testing — simulate multi-host topologies on one machine
  • VRF isolation — separate routing domains
  • systemd-networkd — isolates network services

Note

Default namespace: The host itself runs in the initial (default) network namespace. When you run ip addr on a host, you see the default namespace's interfaces. Every process inherits its parent's namespace unless explicitly moved.

Creating and Using Namespaces

console
console

    # Create a namespace

    $ ip netns add ns1


    # List all namespaces

    $ ip netns list

    ns1


    # Execute a command inside the namespace

    $ ip netns exec ns1 ip addr

    1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN

        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00


    # Note: loopback is DOWN by default in new namespaces

    $ ip netns exec ns1 ip link set lo up


    # Enter namespace interactively (spawns a shell inside ns1)

    $ ip netns exec ns1 bash


    # Delete a namespace

    $ ip netns del ns1
  

Tip

Tip: ip netns exec uses setns(2) + unshare(2) under the hood. Docker uses the same syscalls but manages namespace lifecycle via containerd/runc. You can inspect a container's namespace with nsenter -t <PID> -n.

veth Pairs: Virtual Ethernet Cables

A veth pair is two virtual network interfaces connected like a pipe — whatever goes in one end comes out the other. They are the fundamental mechanism for connecting network namespaces together.

Namespace A
veth0
10.0.0.1/24
Namespace B
veth1
10.0.0.2/24

Creating a veth Pair

console
console

    # Create veth pair (both ends start in the default namespace)

    $ ip link add veth0 type veth peer name veth1


    # Move one end into a namespace

    $ ip link set veth1 netns ns1


    # Assign IPs and bring up interfaces

    $ ip addr add 10.0.0.1/24 dev veth0

    $ ip link set veth0 up


    $ ip netns exec ns1 ip addr add 10.0.0.2/24 dev veth1

    $ ip netns exec ns1 ip link set veth1 up


    # Test connectivity

    $ ping -c 1 10.0.0.2

    PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.

    64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.043 ms
  

Key Properties of veth Pairs

  • Always created in pairs — you cannot have one end without the other
  • Deleting one end automatically destroys the other
  • They operate at L2 — Ethernet frames pass between the two ends
  • Each end can be in a different namespace
  • Performance is near line-rate — no kernel-to-userspace copy
  • Docker creates a veth pair for every container: one end in the container ns, the other attached to docker0 bridge

Linux Bridges: L2 Switch in Software

A Linux bridge is a software Layer 2 switch. It learns MAC addresses from incoming frames and forwards traffic between attached interfaces — exactly like a physical Ethernet switch, but implemented in the kernel.

Container A
vethA-ns
172.17.0.2
br0 (bridge)
172.17.0.1
MAC learning
Container B
vethB-ns
172.17.0.3

Creating and Using a Bridge

console
console

    # Create a bridge

    $ ip link add br0 type bridge

    $ ip link set br0 up


    # Assign an IP to the bridge (acts as default gateway for containers)

    $ ip addr add 172.17.0.1/24 dev br0


    # Attach veth ends to the bridge

    $ ip link set vethA master br0

    $ ip link set vethB master br0


    # View bridge members

    $ bridge link show

    3: vethA: <BROADCAST,MULTICAST,UP> mtu 1500 master br0

    4: vethB: <BROADCAST,MULTICAST,UP> mtu 1500 master br0


    # View learned MAC addresses

    $ bridge fdb show br br0
  

How Bridge Forwarding Works

  • Learning: When a frame arrives on a port, the bridge records the source MAC → port mapping
  • Flooding: If the destination MAC is unknown, the frame is sent to all ports (except the source)
  • Forwarding: If the destination MAC is known, the frame is sent only to the correct port
  • This is identical behavior to a physical L2 switch

Note

Docker's docker0: When Docker starts, it creates a bridge called docker0 (default: 172.17.0.1/16). Each container's veth pair has one end attached to this bridge. That's how containers on the same Docker network communicate at L2.

tun/tap Devices

tun and tap are virtual network interfaces where the "other end" is a userspace program instead of a physical wire or another namespace. The program reads/writes packets via a file descriptor (/dev/net/tun).

TAP — Layer 2
  • Carries Ethernet frames
  • Appears as a full Ethernet interface (has a MAC address)
  • Used by: VM hypervisors (QEMU/KVM), OpenVPN in tap mode, bridged networking
  • Userspace program receives raw L2 frames
  • Can be attached to a bridge
TUN — Layer 3
  • Carries IP packets
  • No Ethernet framing — point-to-point
  • Used by: WireGuard, OpenVPN in tun mode, VPN tunnels
  • Userspace program receives raw IP packets
  • Cannot be bridged (no L2 headers)
  1. Application

    sends data to dest IP

  2. Kernel Routing

    routes to tun0/tap0

  3. tun/tap Device

    kernel → userspace fd

  4. VPN Process

    encrypts, encapsulates

  5. Physical NIC

    sends encrypted packet

console
console

    # Create a tun device

    $ ip tuntap add dev tun0 mode tun

    $ ip addr add 10.8.0.1/24 dev tun0

    $ ip link set tun0 up


    # Create a tap device

    $ ip tuntap add dev tap0 mode tap

    $ ip link set tap0 up

    $ ip link set tap0 master br0  # attach to bridge
  

macvlan / ipvlan

Both macvlan and ipvlan create virtual sub-interfaces from a single physical NIC, allowing multiple "virtual NICs" to share one uplink — each appearing as a distinct network identity to the rest of the network.

macvlan
  • Each sub-interface gets its own unique MAC address
  • To external network, each looks like a separate physical NIC
  • Multiple modes: bridge, vepa, private, passthru
  • Bridge mode: sub-interfaces can talk to each other directly
  • Use case: give containers direct L2 network access without NAT
console
# Create macvlan sub-interface
ip link add macvlan0 link eth0 type macvlan mode bridge
ip addr add 192.168.1.100/24 dev macvlan0
ip link set macvlan0 up
ipvlan
  • All sub-interfaces share the same MAC address as the parent
  • Distinguished by IP only — L3 multiplexing
  • Two modes: L2 (acts like bridge) and L3 (acts like router)
  • Works in environments that restrict multiple MACs (some cloud NICs, 802.1X)
  • Use case: cloud VMs where provider limits MACs per interface
console
# Create ipvlan sub-interface
ip link add ipvlan0 link eth0 type ipvlan mode l2
ip addr add 192.168.1.101/24 dev ipvlan0
ip link set ipvlan0 up
Feature macvlan ipvlan
MAC address Unique per sub-interface Shared with parent
Isolation level L2 (different MACs) L3 (same MAC, different IPs)
Cloud compatibility May not work (MAC restrictions) Works everywhere
Parent ↔ child traffic Cannot communicate directly Cannot communicate directly
Performance Excellent (no bridge overhead) Excellent (no bridge overhead)
Docker driver --driver macvlan --driver ipvlan

Warning

Limitation: With both macvlan and ipvlan, the parent interface cannot communicate with its sub-interfaces. This is by kernel design. If you need host-to-container communication, add a separate macvlan/ipvlan sub-interface for the host or use a bridge instead.

Hands-on: Build Container Networking from Scratch

This walkthrough recreates what Docker and Kubernetes CNIs do under the hood: create isolated network namespaces and connect them through a bridge with NAT for external access.

Target Topology

ns1 (Container A)
eth0: 10.200.0.2/24
gw: 10.200.0.1
br0 (Bridge)
10.200.0.1/24
ns2 (Container B)
eth0: 10.200.0.3/24
gw: 10.200.0.1
Internet
via host eth0

Step 1: Create Namespaces

console
console

    # Create two namespaces (representing two containers)

    $ ip netns add ns1

    $ ip netns add ns2


    # Bring up loopback in each

    $ ip netns exec ns1 ip link set lo up

    $ ip netns exec ns2 ip link set lo up
  

Step 2: Create the Bridge

console
console

    # Create bridge and assign it an IP (acts as default gateway)

    $ ip link add br0 type bridge

    $ ip link set br0 up

    $ ip addr add 10.200.0.1/24 dev br0
  

Step 3: Create veth Pairs and Connect

console
console

    # Create veth pairs for ns1

    $ ip link add veth-ns1 type veth peer name eth0-ns1

    $ ip link set veth-ns1 master br0       # host end → bridge

    $ ip link set veth-ns1 up

    $ ip link set eth0-ns1 netns ns1         # container end → namespace


    # Create veth pairs for ns2

    $ ip link add veth-ns2 type veth peer name eth0-ns2

    $ ip link set veth-ns2 master br0

    $ ip link set veth-ns2 up

    $ ip link set eth0-ns2 netns ns2
  

Step 4: Assign IPs Inside Namespaces

console
console

    # Configure ns1

    $ ip netns exec ns1 ip addr add 10.200.0.2/24 dev eth0-ns1

    $ ip netns exec ns1 ip link set eth0-ns1 up

    $ ip netns exec ns1 ip route add default via 10.200.0.1


    # Configure ns2

    $ ip netns exec ns2 ip addr add 10.200.0.3/24 dev eth0-ns2

    $ ip netns exec ns2 ip link set eth0-ns2 up

    $ ip netns exec ns2 ip route add default via 10.200.0.1
  

Step 5: Enable Routing + NAT

console
console

    # Enable IP forwarding on the host

    $ sysctl -w net.ipv4.ip_forward=1


    # NAT outbound traffic from containers

    $ iptables -t nat -A POSTROUTING -s 10.200.0.0/24 ! -o br0 -j MASQUERADE


    # Allow forwarded traffic

    $ iptables -A FORWARD -i br0 -j ACCEPT

    $ iptables -A FORWARD -o br0 -j ACCEPT
  

Step 6: Verify Connectivity

console
console

    # Container-to-container (via bridge, L2)

    $ ip netns exec ns1 ping -c 2 10.200.0.3

    PING 10.200.0.3 (10.200.0.3) 56(84) bytes of data.

    64 bytes from 10.200.0.3: icmp_seq=1 ttl=64 time=0.051 ms

    64 bytes from 10.200.0.3: icmp_seq=2 ttl=64 time=0.038 ms


    # Container-to-host (via bridge, L3)

    $ ip netns exec ns1 ping -c 1 10.200.0.1

    64 bytes from 10.200.0.1: icmp_seq=1 ttl=64 time=0.029 ms


    # Container-to-internet (via host NAT)

    $ ip netns exec ns1 ping -c 1 8.8.8.8

    64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=12.3 ms
  

Tip

That's it. You just built the same networking model that Docker uses. Docker's docker0 bridge, per-container veth pairs, and MASQUERADE rules are exactly this pattern. Kubernetes CNIs (Flannel, Calico, Cilium) build on this foundation with additional overlay/routing logic.

Summary: Virtual Interface Types

Interface Type Layer Purpose Primary Use Case
veth pair L2 Connect two namespaces Container networking (Docker, K8s)
bridge L2 Software switch (connect multiple interfaces) docker0, VM networking
tun L3 Userspace ↔ kernel IP packets VPNs (WireGuard, OpenVPN tun)
tap L2 Userspace ↔ kernel Ethernet frames VM networking, OpenVPN tap
macvlan L2 Multiple MACs on one physical NIC Direct L2 container access
ipvlan L3 Multiple IPs, same MAC on one NIC Cloud environments (MAC-restricted)

How These Compose in Real Systems

  1. Container

    network namespace

  2. veth pair

    pipe between ns

  3. bridge

    L2 switching

  4. iptables NAT

    MASQUERADE

  5. Physical NIC

    internet access

Solidnines — solidnines.com