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, gateway192.0.2.1.pl-peer:eth0,192.0.2.11/24, gateway192.0.2.1.pl-router:lan1andlan2belong to bridgebr-lan; the bridge has192.0.2.1/24. Routed interfacewan0has198.51.100.1/24.pl-server:eth0,198.51.100.10/24, gateway198.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 tcpdumpCreate 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
doneCreate 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 eth0Build 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
doneGive 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=1The 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_forwardThe 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 showThe 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 REACHABLEAn 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 showThis 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.10The measured result was immediate:
ping: connect: Network is unreachableThe 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.10All 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.