Build a Linux Network Lab with Namespaces, veth, and Bridges

Build a bridged LAN and routed server path on one Linux VM. Follow ARP, inspect the routes, and break and repair one missing route.

Linux network namespace lab: a client and peer on 192.0.2.0/24 connect through a Linux bridge and router to a server on 198.51.100.0/24.

Two hosts on the same subnet should talk directly. Put the destination on another subnet and the host needs a router. A small namespace lab makes that difference visible: you can inspect the routes, watch ARP, and remove one route without touching your normal network connection.

This lab puts a client, a second local host, a Linux router, and a server on one Linux VM. The router contains a bridge on its LAN side. We'll prove the bridged path, prove the routed path, then break and repair the client's route to the server.

What the lab contains

A network namespace has its own network devices, addresses, routes, and firewall rules. The processes still use the VM's kernel; these are separate network stacks, not four virtual machines. The ip-netns manual describes the namespace operations.

  • pl-client: eth0, 192.0.2.10/24, gateway 192.0.2.1.
  • pl-peer: eth0, 192.0.2.11/24, gateway 192.0.2.1.
  • pl-router: lan1 and lan2 belong to bridge br-lan; the bridge has 192.0.2.1/24. Routed interface wan0 has 198.51.100.1/24.
  • pl-server: eth0, 198.51.100.10/24, gateway 198.51.100.1.

Each veth pair acts as a virtual cable. One end lives in a host namespace; the other lives in the router namespace. The client and peer share a broadcast domain through br-lan. The server sits on a separate routed subnet. Both address ranges are reserved for documentation.

Tested with: Ubuntu 26.04.1 LTS, kernel 7.0.0-38-generic, iproute2 6.19.0, tcpdump 4.99.6. The commands need a Linux VM with network-namespace support and sudo. Containerlab and Cisco images are not required for this foundation. If the command grammar is unfamiliar, keep the Linux ip reference nearby.

Create the namespaces and virtual cables

Run the commands on the Linux VM. Use a fresh lab: check ip netns list first, and stop if any of the four pl- names already belongs to something you want to keep. On Ubuntu, the required tools are available with:

sudo apt-get install iproute2 iputils-ping tcpdump

Create the four stacks and bring up their loopback interfaces:

for ns in pl-client pl-peer pl-router pl-server; do
  sudo ip netns add "$ns"
  sudo ip -n "$ns" link set lo up
done

Create the three veth pairs, move the ends, and give each interface a name that tells you where it belongs:

sudo ip link add plc0 type veth peer name plr1
sudo ip link set plc0 netns pl-client
sudo ip link set plr1 netns pl-router
sudo ip -n pl-client link set plc0 name eth0
sudo ip -n pl-router link set plr1 name lan1

sudo ip link add plp0 type veth peer name plr2
sudo ip link set plp0 netns pl-peer
sudo ip link set plr2 netns pl-router
sudo ip -n pl-peer link set plp0 name eth0
sudo ip -n pl-router link set plr2 name lan2

sudo ip link add plrw type veth peer name pls0
sudo ip link set plrw netns pl-router
sudo ip link set pls0 netns pl-server
sudo ip -n pl-router link set plrw name wan0
sudo ip -n pl-server link set pls0 name eth0

Build the bridge and routed subnet

Create the LAN bridge inside the router namespace. Put its IP address on br-lan, not on the member ports:

sudo ip -n pl-router link add br-lan type bridge
sudo ip -n pl-router link set lan1 master br-lan
sudo ip -n pl-router link set lan2 master br-lan
sudo ip -n pl-router addr add 192.0.2.1/24 dev br-lan
sudo ip -n pl-router addr add 198.51.100.1/24 dev wan0

sudo ip -n pl-client addr add 192.0.2.10/24 dev eth0
sudo ip -n pl-peer addr add 192.0.2.11/24 dev eth0
sudo ip -n pl-server addr add 198.51.100.10/24 dev eth0

for dev in lan1 lan2 br-lan wan0; do
  sudo ip -n pl-router link set "$dev" up
done
for ns in pl-client pl-peer pl-server; do
  sudo ip -n "$ns" link set eth0 up
done

Give each host a default route and enable IPv4 forwarding inside pl-router:

sudo ip -n pl-client route add default via 192.0.2.1
sudo ip -n pl-peer route add default via 192.0.2.1
sudo ip -n pl-server route add default via 198.51.100.1
sudo ip netns exec pl-router sysctl -w net.ipv4.ip_forward=1

The namespace prefix matters. Running that sysctl without ip netns exec pl-router changes the VM's own forwarding setting instead. This lab only needs forwarding in the router stack.

Check the address and route state before sending traffic:

sudo ip -n pl-router -br address
sudo ip -n pl-client route show
sudo ip -n pl-server route show
sudo ip netns exec pl-router sysctl net.ipv4.ip_forward

The client's route listing should contain a directly connected 192.0.2.0/24 route and default via 192.0.2.1 dev eth0. The server needs its return route through 198.51.100.1; forwarding the request alone does not complete a ping.

Watch a frame cross the bridge

Open a second terminal and capture on the router's client-facing port:

sudo ip netns exec pl-router tcpdump --immediate-mode -eni lan1 'arp or icmp'

In the first terminal, clear only the client's lab neighbor entries, then ping the peer:

sudo ip -n pl-client neigh flush dev eth0
sudo ip netns exec pl-client ping -n -c 4 192.0.2.11
sudo ip netns exec pl-router bridge fdb show br br-lan
sudo ip -n pl-client neigh show

The capture begins with an ARP request for 192.0.2.11. Because the peer is on the same subnet, the client resolves the peer's MAC directly. It does not ask its default gateway to route the packet. In the test, all four probes received replies.

The forwarding database learned the client MAC on lan1 and the peer MAC on lan2. These are excerpts from the measured run; your dynamically assigned MAC addresses will differ:

ee:69:7d:bf:3a:5b dev lan1 master br-lan
ee:fe:3e:5f:e4:b6 dev lan2 master br-lan

192.0.2.11 dev eth0 lladdr ee:fe:3e:5f:e4:b6 REACHABLE

An initially unknown destination can be flooded, and broadcast ARP reaches both bridge ports. After learning, known unicast traffic goes toward the learned destination port. Capture on lan2 as well if you want to compare the bridge's ingress and egress views. Stop each foreground capture with Ctrl+C. The neighbor-state guide explains the ARP cache states.

Follow a packet through the router

Now capture the server-facing interface in a second terminal:

sudo ip netns exec pl-router tcpdump --immediate-mode -eni wan0 'arp or icmp'

From the client, inspect the lookup and ping the server:

sudo ip -n pl-client route get 198.51.100.10
sudo ip netns exec pl-client ping -n -c 4 198.51.100.10
sudo ip -n pl-client neigh show

This destination is outside 192.0.2.0/24. The client sends the Ethernet frame to the gateway's MAC, while the IP destination remains 198.51.100.10. The router forwards it out wan0, resolving the server's MAC on the other subnet. Captures on lan1 and wan0 show the same IP source and destination, with different Ethernet addresses and a decremented IP TTL on the forwarded request. There is no NAT configured in this lab.

All four server probes succeeded in the baseline. The client had neighbor entries for the local peer and gateway, not for the remote server. That is the useful distinction between the neighbor table and the routing table: one resolves a local next hop; the other chooses where to send the packet.

Remove one route and diagnose the failure

Delete only the client's default route:

sudo ip -n pl-client route del default
sudo ip netns exec pl-client ping -n -c 2 198.51.100.10

The measured result was immediate:

ping: connect: Network is unreachable

The failure happens at the client's route lookup. It has not established that a packet was dropped by the bridge, router, or server. The directly connected peer remains reachable because its subnet route still exists.

Restore the missing route and repeat the same server probe:

sudo ip -n pl-client route add default via 192.0.2.1
sudo ip netns exec pl-client ping -n -c 4 198.51.100.10

All four recovery probes succeeded. For the routing mechanics behind this check, see ip route: Managing the Linux Routing Table.

Use the lab for the next failure

You now have a healthy bridged LAN and routed server path. Keep those namespace names and addresses for the tc netem experiments: they add delay, loss, and reordering at pl-router's wan0 interface. The same path also supports latency measurements under iPerf3 load. When you're ready to break something on purpose, trace a dropped packet through nftables or find a duplicate IP with arping; both build small namespace topologies the same way. For the surrounding tool references, use the Linux networking hub. This setup exercises Linux bridging and routing on one kernel. Its value is that the routes, interfaces, and capture points are small enough to inspect when one thing fails. Two kernel behaviors that look like silent loss also build on it: Linux rp_filter: diagnose asymmetric routing packet drops and IPv6 Packet Too Big: why TCP uploads stall.

Read next