VLANs and 802.1Q: build a switch, split it, and watch the tags
0 / 10 steps
The goal
Build a VLAN-aware switch and three “PCs” inside one Linux machine, put two PCs in VLAN 10 and one in VLAN 20, and prove the VLAN 20 PC can’t be reached even when it sits on the same subnet. Then add a router on a trunk port, route between the VLANs, and read the real 802.1Q tags on the wire with tcpdump.
Everything here lives inside your own Linux machine. The “PCs” are network namespaces and the cables are virtual (veth pairs), so nothing touches your real network. Step 10 removes all of it.
Why this lab. VLANs are on every Network+ and Security+ exam and in every office network, but most people only ever see them as a number in a switch menu. Building one yourself shows what a VLAN actually is: a label on each frame that the switch uses to decide who can hear whom. Once you have watched the tag change on the wire, “access port”, “trunk” and “router on a stick” stop being vocabulary.
A VLAN is one switch pretending to be two switches. Sneaky. I approve. — Nyx
Before you start. You need an Ubuntu machine you control: the VM from Lab 00, or Ubuntu in WSL2 on Windows. The ip and bridge commands come with Ubuntu; install tcpdump with sudo apt install tcpdump. Every command below needs root, so start a root shell once with sudo -i (the prompt changes to #). If the first ping in step 5 fails on a machine that also runs Docker, see the note in that step.
Steps
01Build the switch
Create a Linux bridge and turn on VLAN filtering. Without that flag a bridge is a dumb switch that ignores VLANs.
# ip link add br0 type bridge vlan_filtering 1 # ip link set br0 up
No output means it worked. ip -br link show br0 should list br0.
Why this matters. This is the same job a managed office switch does. The only difference is that this switch lives in your kernel.
02Plug in three PCs
Each “PC” is a network namespace (its own private network stack). A veth pair is the cable: one end, eth0, goes inside the PC; the other end, pcN-sw, plugs into the switch.
# for n in pc1 pc2 pc3; do ip netns add $n ip link add $n-sw type veth peer name eth0 netns $n ip link set $n-sw master br0 up ip -n $n link set lo up ip -n $n link set eth0 up done # ip -br link show master br0
You should see three ports on the switch, all UP: pc1-sw, pc2-sw, pc3-sw.
03Put each port in a VLAN
Every port starts in VLAN 1. Take it out, then make pc1 and pc2 access ports in VLAN 10 and pc3 an access port in VLAN 20.
# bridge vlan del dev pc1-sw vid 1; bridge vlan add dev pc1-sw vid 10 pvid untagged # bridge vlan del dev pc2-sw vid 1; bridge vlan add dev pc2-sw vid 10 pvid untagged # bridge vlan del dev pc3-sw vid 1; bridge vlan add dev pc3-sw vid 20 pvid untagged # bridge vlan show
Our run printed:
port vlan-id br0 1 PVID Egress Untagged pc1-sw 10 PVID Egress Untagged pc2-sw 10 PVID Egress Untagged pc3-sw 20 PVID Egress Untagged
Reading it. PVID means “frames arriving untagged on this port belong to this VLAN”. Egress Untagged means the switch strips the tag before sending frames out of this port, so the PC never sees a tag. That pair is exactly what an access port is.
04Give all three PCs the same subnet
On purpose, all three get addresses in 10.0.10.0/24, including pc3 in VLAN 20.
# ip -n pc1 addr add 10.0.10.1/24 dev eth0 # ip -n pc2 addr add 10.0.10.2/24 dev eth0 # ip -n pc3 addr add 10.0.10.3/24 dev eth0
Why the same subnet? People often think a VLAN is “just a different subnet”. The next step proves it isn’t: the VLAN boundary works at Layer 2, before IP addresses even come into it.
05Prove the VLANs are separate
Ping from pc1 to pc2 (same VLAN), then pc1 to pc3 (other VLAN, same subnet).
# ip netns exec pc1 ping -c 2 -W 1 10.0.10.2 # ip netns exec pc1 ping -c 2 -W 1 10.0.10.3
In our run the first ping got 2 received, 0% packet loss. The second got 0 received, 100% packet loss. pc1 asks “who has 10.0.10.3?” (ARP) as a broadcast, but the switch only floods that broadcast inside VLAN 10, so pc3 never hears the question.
Same street name, different town. The mail never arrives. — Nyx
If the FIRST ping fails too. On machines that also run Docker, Docker’s firewall rules can drop traffic crossing a Linux bridge. Check iptables -S FORWARD; if the policy is DROP, allow this lab’s bridge for now with iptables -I FORWARD -i br0 -o br0 -j ACCEPT, and remove that rule in step 10 by running the same command with -D instead of -I. (Our test machine didn’t need this.)
06Add a router on a trunk port
A trunk carries several VLANs over one cable, each frame tagged with its VLAN number. Plug a router into the switch on a trunk that carries VLANs 10 and 20 (tagged), and give the router one virtual interface per VLAN. This setup is called “router on a stick”.
# ip netns add router # ip link add trunk-sw type veth peer name eth0 netns router # ip link set trunk-sw master br0 up # bridge vlan del dev trunk-sw vid 1 # bridge vlan add dev trunk-sw vid 10 # bridge vlan add dev trunk-sw vid 20 # ip -n router link set lo up # ip -n router link set eth0 up # ip -n router link add link eth0 name eth0.10 type vlan id 10 # ip -n router link add link eth0 name eth0.20 type vlan id 20 # ip -n router addr add 10.0.10.254/24 dev eth0.10 # ip -n router addr add 10.0.20.254/24 dev eth0.20 # ip -n router link set eth0.10 up # ip -n router link set eth0.20 up # ip netns exec router sysctl -w net.ipv4.ip_forward=1
bridge vlan show dev trunk-sw now lists VLANs 10 and 20 with no PVID and no “Egress Untagged”: frames leave this port with their tag on.
Why this matters. eth0.10 and eth0.20 are 802.1Q sub-interfaces. They add and remove the tag on the router’s side, the same way a router or firewall connects to a trunk on a real network.
07Give VLAN 20 its own subnet and set gateways
Routing works between subnets, so move pc3 to 10.0.20.0/24 and point every PC at the router as its default gateway.
# ip -n pc3 addr flush dev eth0 # ip -n pc3 addr add 10.0.20.3/24 dev eth0 # ip -n pc1 route add default via 10.0.10.254 # ip -n pc2 route add default via 10.0.10.254 # ip -n pc3 route add default via 10.0.20.254
One VLAN, one subnet, one gateway: that pairing is how almost every office network is laid out.
08Watch the 802.1Q tags on the trunk
Start a capture on the trunk port, then ping from pc1 (VLAN 10) to pc3 (VLAN 20).
# tcpdump -e -n -l -i trunk-sw -c 4 vlan and icmp & # ip netns exec pc1 ping -c 2 10.0.20.3
The ping works, with ttl=63 (one hop through the router). Our capture (MAC addresses shortened):
2e:df.. > 86:89.., ethertype 802.1Q (0x8100): vlan 10, 10.0.10.1 > 10.0.20.3: ICMP echo request 86:89.. > d6:63.., ethertype 802.1Q (0x8100): vlan 20, 10.0.10.1 > 10.0.20.3: ICMP echo request d6:63.. > 86:89.., ethertype 802.1Q (0x8100): vlan 20, 10.0.20.3 > 10.0.10.1: ICMP echo reply 86:89.. > 2e:df.., ethertype 802.1Q (0x8100): vlan 10, 10.0.20.3 > 10.0.10.1: ICMP echo reply
Reading it. The same ping crosses the trunk twice. It goes up to the router tagged vlan 10, and the router sends it back down tagged vlan 20 toward pc3. The reply makes the same trip in reverse. 0x8100 is the EtherType that means “an 802.1Q tag follows”. The capture on pc1’s own port would show no tags at all, because access ports strip them.
09Prune the trunk (and why attackers love lazy trunks)
A trunk should carry only the VLANs it needs. Remove VLAN 20 from the trunk and try again, then put it back.
# bridge vlan del dev trunk-sw vid 20 # ip netns exec pc1 ping -c 2 -W 1 10.0.20.3 # bridge vlan add dev trunk-sw vid 20
With VLAN 20 pruned, our ping got 100% packet loss: the router can no longer reach VLAN 20 at all.
The attacker’s view. On real switches, two classic “VLAN hopping” tricks need a careless trunk. Switch spoofing: a laptop pretends to be a switch and gets an auto-negotiated trunk carrying every VLAN. Double tagging: a frame carries two tags and abuses the native VLAN (the one a trunk sends untagged) to slip one tag off and land in another VLAN.
The defender’s move. Turn off automatic trunking on user ports and set them to access mode. Prune every trunk to the VLANs it needs, as you just did. Use an otherwise unused VLAN as the native VLAN, never one users sit on. Put unused ports in a “parking” VLAN and shut them down.
10Clean up
Deleting a namespace removes its end of each cable (and the switch end goes with it), so this clears the whole lab.
# for n in pc1 pc2 pc3 router; do ip netns del $n; done # ip link del br0 # exit
ip netns list should print nothing from this lab, and exit leaves the root shell.
What you learned
- access port — one VLAN (the PVID), tag stripped on the way out; the PC never sees it
- VLAN = Layer 2 boundary — same subnet doesn’t matter; broadcasts (like ARP) stay inside the VLAN
- trunk / 802.1Q — many VLANs on one link, each frame tagged; EtherType 0x8100
- router on a stick — one link, one sub-interface per VLAN, routing between them
- trunk hygiene — prune trunks, no auto-trunking on user ports, unused native VLAN
Tried it? Tell us
Nova built this lab from Steven’s ideas, and we ran every step once before publishing. If a step broke, behaved differently for you, or you know a better way, tell us. Ideas for the next lab are welcome too — no promises on timing.
NextRouting basics: build two routers, break them, fix them→
Go deeper
- ARP lab — what happens inside one broadcast domain, before VLANs split it
- Subnetting and DHCP with your own IP address — the subnet half of “one VLAN, one subnet”
- DHCP explained — why DHCP broadcasts stay inside a VLAN unless a relay forwards them
- Networking glossary — VLAN, trunk, broadcast and friends in plain words
Original walkthrough. Every command was run end to end on Ubuntu 24.04 (WSL2, kernel 6.6, iproute2 6.1) on 27 Oct 2026; the outputs shown are from that run. Real switches (Cisco, Aruba, UniFi) use their own commands for the same ideas. If something doesn’t match what your machine does, tell me and I will fix it.