> ## Content Index
> Fetch the complete content index at: https://www.pinglabz.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Build a Linux Network Lab with Namespaces, veth, and Bridges
- URL: https://www.pinglabz.com/linux-namespace-network-lab/
- Published: 2026-10-13T06:25:03.000Z
- Updated: 2026-10-13T06:25:03.000Z
- Description: Build a bridged LAN and routed server path on one Linux VM. Follow ARP, inspect the routes, and break and repair one missing route.
- Author: Jaime
- Tags: Linux, Troubleshooting

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](https://man7.org/linux/man-pages/man8/ip-netns.8.html?ref=pinglabz.com) 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](https://www.pinglabz.com/linux-ip-command/) 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:

```bash
sudo apt-get install iproute2 iputils-ping tcpdump
```

Create the four stacks and bring up their loopback interfaces:

```bash
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:

```bash
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:

```bash
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`:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```text
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](https://www.pinglabz.com/ip-neigh-arp-linux/) explains the ARP cache states.

## Follow a packet through the router

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

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

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

```bash
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:

```bash
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:

```text
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:

```bash
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](https://www.pinglabz.com/ip-route-linux/).

## 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](https://www.pinglabz.com/tc-netem-delay-loss-reordering/): they add delay, loss, and reordering at `pl-router`'s `wan0` interface. The same path also supports [latency measurements under iPerf3 load](https://www.pinglabz.com/iperf3-latency-under-load/). When you're ready to break something on purpose, [trace a dropped packet through nftables](https://www.pinglabz.com/nftables-monitor-trace-packet-drop/) or [find a duplicate IP with arping](https://www.pinglabz.com/arping-duplicate-ip-proxy-arp-lab/); both build small namespace topologies the same way. For the surrounding tool references, use the [Linux networking hub](https://www.pinglabz.com/linux-networking-commands/). 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](https://www.pinglabz.com/linux-rp-filter-asymmetric-routing-lab/) and [IPv6 Packet Too Big: why TCP uploads stall](https://www.pinglabz.com/ipv6-packet-too-big-tcp-stall-lab/).