LearnThe Network StackWhat Moves on the Wire

Building a Bridge

bridge - a hands-on Linux lab on a real virtual machine.

A Linux bridge is a software switch. Three hosts are already plugged into one. Watch it learn MAC addresses, read its forwarding database, and see frames flow through it.

Three machines are plugged in and it knows nothing about them

Every lesson so far has given you two machines and one cable between them. That is simple, and it is not how any real network works. In a real network dozens or hundreds of machines share the same local segment. They need a device that can take a frame in on one port and put it out the one port that reaches the right destination.

That device is a switch. In Linux you can build one in software, and it is called a bridge.

This lab booted with three machines already plugged into a bridge and not one byte of traffic between them. The bridge is running, its ports are up, and its map of who is out there is completely blank. You are going to read that blank map, send a single ping, and watch a line appear.

The black boxes below are a practice terminal: a safe sandbox that checks only the one command each step teaches. The real VM waits at the end. There you are student on the machine bridge-lab. The bridge lives in your own shell's network stack, and the three hosts live in namespaces. A namespace is a private copy of the network stack, a separate computer as far as networking is concerned. Your progress is tracked automatically, so type commands naturally. Interface index numbers, timestamps and the bridge's own MAC will differ on your machine. Every name and address this lesson teaches will match.

What a bridge is

A Linux bridge is a virtual network switch. It connects multiple network interfaces and forwards Ethernet frames between them based on MAC addresses.

It works at Layer 2. It does not look at IP addresses at all. It reads the destination MAC in each frame and decides which port to send it out. That is the entire job.

A physical switch in a data center does exactly the same thing with 24 or 48 ports and real cables. The Linux bridge does it in software, using virtual interfaces called veth pairs instead of physical cables. A veth pair is two interfaces joined back to back: what goes into one end comes out the other.

Your lab network

This lab has a three-machine network connected through a Linux bridge:

  [host-a]  10.0.1.10  <--veth-->  [br0]  <--veth-->  10.0.1.20  [host-b]
  MAC: 02:00:0a:00:01:0a             |                 MAC: 02:00:0a:00:01:14
                                      |
                                 <--veth-->  10.0.1.30  [host-c]
                                             MAC: 02:00:0a:00:01:1e

The bridge (br0) lives in the default namespace, which is where your shell already is. That is why the bridge commands in this lesson need no namespace prefix. Three veth pairs connect the three namespaces (host-a, host-b, host-c) to it. Each namespace is a separate computer plugged into a switch port.

Each veth pair has two ends with two names. The bridge-side ends are veth-a-br, veth-b-br and veth-c-br, and they are here with you. The host-side ends are veth-a, veth-b and veth-c, and each one lives inside its namespace, invisible from your shell.

Three helpers send traffic for you: send-ping-ab pings from host-a to host-b, send-ping-ac from host-a to host-c, and send-ping-bc from host-b to host-c. cap-bridge captures on the bridge itself.

Those helper names tab-complete, and so do the interface names. Type send-ping-a and press Tab twice and the shell shows you both matches. Type cap- and Tab for cap-bridge, che and Tab for check-progress. On names this similar, letting the shell finish them removes a whole class of typo.

Build one yourself

Nothing about this switch was installed or configured by hand. Three commands built the whole thing while the lab was booting, and they are the same three you would run on any Linux box.

The first one creates the switch:

ip link add name br0 type bridge

That makes a new network interface whose type is bridge. At this moment it has no ports, no addresses and nothing plugged into it. It is an empty switch sitting in a rack.

The second one turns it on, because a bridge that is down forwards nothing:

ip link set br0 up

The third one plugs a cable in, and it runs once per port:

ip link set veth-a-br master br0

master br0 is the whole idea in two words. That interface now belongs to that bridge, and the bridge decides what happens to every frame arriving on it. Hold on to those two words. In a moment you are going to read them straight back out of the switch.

All three need root. This lab ran the first two once and the third three times, for veth-a-br, veth-b-br and veth-c-br.

Now run the first one yourself, on a name that is not taken. br1 does not exist on this machine. Before you press Enter, decide what a successful ip command prints.

sudo ip link add name br1 type bridge

prompt: student@bridge-lab:~$ answer: sudo ip link add name br1 type bridge ||| sudo ip link add br1 type bridge ||| ip link add name br1 type bridge ||| ip link add br1 type bridge output: hint: The link subcommand that adds an interface, the name you want, and the type that makes it a switch: sudo ip link add name br1 type bridge

Silence, and your prompt straight back. ip says nothing at all when it succeeds, which takes some getting used to. Run the same command a second time and it stops being quiet: the name is taken now, so you get RTNETLINK answers: File exists.

br1 is a real switch. It is down, it has no ports, and it has no MAC address of its own yet. A bridge has no burned-in address: it adopts the lowest address among the interfaces plugged into it, and nothing is plugged into this one. Hand it a port with ip link set <interface> master br1 and it takes that port's address.

Everything from here on is about the switch the lab already built, br0, which has three ports and has been waiting for traffic since boot.

Leave the three existing ports where they are. ip link set veth-a-br master br1 would move host-a off br0 and onto your empty switch, and the forwarding table you are about to fill would go with it. Creating a new bridge is safe. Re-pointing a live one is how machines get disconnected.

Count the ports

bridge link show lists every interface that is currently a port on a bridge. It reads the kernel's own state and needs no sudo at all, because reading a bridge is open to everyone.

Before you press Enter, commit to a number. How many lines should come back, and what should every one of them have in common?

bridge link show

prompt: student@bridge-lab:~$ answer: sudo bridge link show ||| sudo bridge link ||| bridge link show ||| bridge link ||| bridge link ls output: 3: veth-a-br: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding 5: veth-b-br: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding 7: veth-c-br: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding hint: The bridge tool, the link subcommand, the show verb: bridge link show

Three lines, one per host, and two things on every line are the point.

master br0 means this interface is a port on the bridge named br0. That is the relationship the whole lesson rests on: the interface belongs to the bridge, and the bridge decides what happens to frames arriving on it.

state forwarding means the port is active and passing traffic. A port can also be blocking or disabled, which you will meet in the details step.

The numbers at the front (3:, 5:, 7:) are kernel interface indexes. They count every interface on the machine, and the gaps are the host-side ends of each veth pair, which were moved into the namespaces. Yours may be different numbers. The names will not be. Listing the bridge and its ports is the first of the five things your lab grades.

The older view of the same bridge

There is a second tool for this, and it is worth meeting because you will find it in every older runbook. brctl show comes from the bridge-utils package, the original userspace tools for Linux bridging:

brctl show

It prints the bridge as a table:

bridge name     bridge id               STP enabled     interfaces
br0             8000.<br0 own MAC>      no              veth-a-br
                                                        veth-b-br
                                                        veth-c-br

The bridge name, its bridge ID, whether STP is enabled, and the member interfaces.

The bridge ID is two things joined by a dot: the bridge priority in hex (8000 is 32768, the default) and the bridge's own MAC address with the colons stripped out. That MAC is written as a placeholder above because it is different on your machine, and it cannot honestly be printed here. A Linux bridge has no burned-in address of its own. It adopts the lowest MAC among the ports plugged into it, and the ports here get random addresses at boot. Run the command and read your own value.

STP (Spanning Tree Protocol) prevents loops in networks with redundant paths. If two switches are connected by two cables, STP disables one path to avoid frames circling forever. In this lab, STP is off because there are no loops in the topology.

The table that makes a switch a switch

A physical switch keeps a table that maps MAC addresses to ports. When a frame arrives on port 1 carrying source MAC aa:bb:cc:dd:ee:ff, the switch records one fact: that MAC is reachable through port 1. Next time a frame is addressed to it, the switch sends it out port 1 instead of copying it to every port.

The Linux bridge does exactly the same thing, and its table has a name: the forwarding database, or FDB.

Nobody configures this table. It fills itself in from traffic, which is why the next few steps are the heart of the lesson.

Open the forwarding database

bridge fdb show prints the whole table. Be ready for it to look busier than you expect.

bridge fdb show

prompt: student@bridge-lab:~$ answer: sudo bridge fdb show ||| sudo bridge fdb ||| bridge fdb show ||| bridge fdb ||| bridge fdb ls output: 33:33:00:00:00:01 dev veth-a-br self permanent 01:00:5e:00:00:01 dev veth-a-br self permanent ... (the same pattern repeats, several entries per port, for veth-b-br and veth-c-br) ... hint: The bridge tool, the fdb subcommand, the show verb: bridge fdb show

A screen of entries, several per port, and almost none of them mean what you came here for.

Look at the last word on each line. permanent marks an entry the kernel added by itself: each port's own MAC address, plus the multicast addresses that port listens to. 33:33:... is IPv6 multicast and 01:00:5e:... is IPv4 multicast. None of that came from traffic, and none of it tells you where the three hosts are.

The interesting entries are the ones the bridge learned by watching frames arrive, and they are the ones without permanent on the end. Reading this table is the second of the five things your lab grades.

To filter the output and see only entries for the bridge itself, and not each port, try bridge fdb show br br0. Learned entries say master br0 and do not say permanent, so that is the pair of words to read on every line.

Commit: how much has it learned so far

The ports have been up since this lab booted. The bridge is forwarding on all three. No traffic has crossed it.

The blank map

Prove it. Filter the noise out of the table and look at what is left.

The | grep -v permanent drops every line ending in permanent, which is all the kernel-added rows, and leaves only what the bridge learned by itself.

bridge fdb show br br0 | grep -v permanent

prompt: student@bridge-lab:~$ answer: sudo bridge fdb show br br0 | grep -v permanent ||| bridge fdb show br br0 | grep -v permanent ||| bridge fdb show br br0 |grep -v permanent output: hint: The FDB for br0 only, piped through grep -v to drop the permanent lines: bridge fdb show br br0 | grep -v permanent

Nothing at all. An empty result, and your prompt straight back.

That blank screen is the honest state of a switch that has never seen traffic. Every frame sent right now would have to be flooded, because the bridge has no idea which port leads to which machine.

Keep this screen in mind. You are about to run the identical command again and it will not be blank.

Send traffic, read it again

Now give the bridge something to watch. send-ping-ab sends a ping from host-a (10.0.1.10) to host-b (10.0.1.20), and every one of those frames has to cross the bridge to get there.

send-ping-ab

Then read the same filtered table again. Nothing about the command changes. Before you press Enter, commit to a number: how many lines, and whose addresses?

bridge fdb show br br0 | grep -v permanent

prompt: student@bridge-lab:~$ answer: sudo bridge fdb show br br0 | grep -v permanent ||| bridge fdb show br br0 | grep -v permanent ||| bridge fdb show br br0 |grep -v permanent output: 02:00:0a:00:01:0a dev veth-a-br master br0 02:00:0a:00:01:14 dev veth-b-br master br0 hint: Exactly the command you ran before the ping, unchanged: bridge fdb show br br0 | grep -v permanent

Two lines where there were none, and neither of them says permanent. The bridge has learned:

It learned this by watching the source MAC address of frames that arrived on each port. When a frame from host-a arrived on veth-a-br, the bridge recorded that address against that port. When the reply from host-b arrived on veth-b-br, it recorded that one too. One ping, two lessons learned, no configuration anywhere.

The order matters and your lab grades it: the read has to come after the traffic. A table read before the ping proves nothing, because there was nothing to see. Watching the bridge learn is the third of the five things your lab grades.

Commit: a destination it has never heard of

host-c has been silent since boot, so its address is in no table anywhere. Now imagine host-a sends it a frame.

How a bridge decides where to send a frame

Every frame that arrives on a bridge port goes through the same three steps:

1. Learn. Record the source MAC and the port it arrived on in the FDB. 2. Look up. Check the FDB for the destination MAC. 3. Forward or flood.

Notice that learning happens first, on every single frame, before any decision is made. That is why a bridge fills its table from ordinary traffic and never has to ask anyone anything.

This is exactly how a physical Ethernet switch works. The bridge is a switch in software.

Milestone: you watched a switch build its map

Stop and count what you have done. You read a switch's port list, opened its forwarding table, found it blank, sent one ping, and watched two rows appear that nobody typed.

That is the mechanism every Ethernet network on earth runs on, and it is not more complicated at scale. A 48-port switch in a rack fills the same table the same way, from the same source MAC addresses, one frame at a time.

What is left is watching the frames themselves as the bridge handles them, and reading the settings that govern how it behaves.

Tap the backplane

You can capture on the bridge interface itself. That shows every frame crossing the bridge, whichever port it entered or left by.

The cap-bridge helper runs tshark on br0, and -c 4 stops it after four packets. On the real VM you start the traffic first with send-ping-ab &, where the & backgrounds it so you get your prompt back. The practice terminal keeps traffic flowing for you, so here you type only the capture.

One rule: do not ask for more packets than the traffic will produce. send-ping-ab sends three requests, so six packets cross the bridge, but tshark takes a moment to start while the ping is already running, so the first request and reply are usually gone before the capture is listening. Four is what reliably remains. Ask for ten and the capture sits there waiting for packets that are never coming, and Ctrl-C stops it and prints what it caught.

cap-bridge -c 4

prompt: student@bridge-lab:~$ answer: cap-bridge -c 4 ||| sudo tshark -i br0 -c 4 output: Running as user "root" and group "root". This could be dangerous. Capturing on 'br0' 1 0.000000000 10.0.1.10 -> 10.0.1.20 ICMP 98 Echo (ping) request id=0x0521, seq=1/256, ttl=64 2 0.000039804 10.0.1.20 -> 10.0.1.10 ICMP 98 Echo (ping) reply id=0x0521, seq=1/256, ttl=64 (request in 1) 3 1.023951120 10.0.1.10 -> 10.0.1.20 ICMP 98 Echo (ping) request id=0x0521, seq=2/512, ttl=64 4 1.023993516 10.0.1.20 -> 10.0.1.10 ICMP 98 Echo (ping) reply id=0x0521, seq=2/512, ttl=64 (request in 3) 4 packets captured hint: The bridge capture helper with a count of four: cap-bridge -c 4

The ICMP packets flowing between host-a and host-b, request and reply alternating, seen from the middle. Capturing on br0 is like plugging a monitoring cable into a switch's backplane: you see the frames as the

Practice Building a Bridge in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.

More lessons in What Moves on the Wire