Bridge and STP HOWTO: Linux network bridges for VMs and LANs
A Linux bridge is a software Ethernet switch inside the kernel. Add a physical network card and some virtual machine or container interfaces to it, and they all sit on the same LAN as if they were plugged into one switch. Spanning Tree Protocol (STP) stops the network from melting down when bridges are connected in a loop. This page shows how to build, inspect and save a bridge with today's tools.
What a bridge does
Like a hardware switch, the bridge learns which MAC address lives behind which port and keeps that in its forwarding database (FDB). Frames for a known address go only to that port; broadcasts and unknown addresses go to all ports. The bridge itself works at the Ethernet level. An IP address, if you want the host to be reachable, goes on the bridge device (br0), not on the cards inside it.
Typical uses today:
- Putting virtual machines (KVM/libvirt, Proxmox, GNOME Boxes) directly on the LAN, so they get addresses from the same DHCP server as real machines.
- Connecting containers: each container gets a virtual Ethernet (veth) pair with one end in a bridge. Docker's
docker0is a bridge of this kind. - Joining two network cards into one LAN segment, for example a small transparent firewall.
Wi-Fi cannot be bridged this way. A wireless interface in normal client (station) mode cannot be added to a Linux bridge. If the host reaches the network over Wi-Fi, give VMs a NAT network instead (libvirt's default network on virbr0) or route.
Create a bridge with ip
The ip and bridge commands from iproute2 replace the old brctl. Changes made this way are lost at reboot; use them to learn and test, then save the setup with one of the tools further down.
Do this from a console, not over SSH through the same card. When enp3s0 joins the bridge, its IP address stops working until you move the address to br0. Your SSH session drops in between.
sudo ip link add br0 type bridge
sudo ip link set dev enp3s0 master br0
sudo ip link set dev br0 up
sudo ip addr flush dev enp3s0
sudo ip addr add 192.0.2.10/24 dev br0
sudo ip route add default via 192.0.2.1
Look at the result:
$ ip -br link show type bridge
br0 UP 3a:c4:3c:53:68:10 <BROADCAST,MULTICAST,UP,LOWER_UP>
$ bridge link show
2: enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 4
$ bridge fdb show br br0 dynamic
The last command lists the MAC addresses the bridge has learned. To take a port out again: sudo ip link set dev enp3s0 nomaster. To delete the bridge: sudo ip link del br0.
| Old (bridge-utils) | Today (iproute2) |
|---|---|
brctl addbr br0 | ip link add br0 type bridge |
brctl addif br0 eth0 | ip link set dev eth0 master br0 |
brctl delif br0 eth0 | ip link set dev eth0 nomaster |
brctl show | bridge link show |
brctl showmacs br0 | bridge fdb show br br0 |
brctl stp br0 on | ip link set dev br0 type bridge stp_state 1 |
brctl setbridgeprio br0 4096 | ip link set dev br0 type bridge priority 4096 |
Spanning Tree Protocol
If two bridges (or a bridge and a switch) are connected by more than one path, broadcast frames circle forever and the network stops. STP prevents this. The bridges exchange small messages (BPDUs), elect one root bridge (lowest priority value, then lowest MAC address wins), and put every redundant port into a blocking state. If a link fails, a blocked port takes over.
A port with STP on goes through these states: listening and learning (no user traffic yet), then forwarding. Blocking ports only process BPDUs; disabled ports take no part.
Turn it on and tune it
sudo ip link set dev br0 type bridge stp_state 1 priority 4096
ip -d link show br0 | grep -o 'stp_state [0-9]*\|priority [0-9]*\|forward_delay [0-9]*'
bridge link show
Give the bridge you want as root the lowest priority (0–65535, default 32768). Port path costs and port priorities decide which redundant port blocks: sudo bridge link set dev enp4s0 cost 100.
The forward delay is how long a port stays in each of listening and learning; the default is 15 seconds, so a new port waits about 30 seconds before it passes traffic. ip takes and shows this timer in hundredths of a second: forward_delay 1500 is the 15-second default, and sudo ip link set dev br0 type bridge forward_delay 400 sets 4 seconds (2 seconds is the minimum). NetworkManager, networkd and netplan take plain seconds.
When to use STP
- Turn it on whenever a loop is possible: two uplinks from one bridge, two hosts bridging the same two networks, or a lab where people plug cables in freely.
- Leave it off on a typical VM host with one uplink cable, where the 30-second wait only delays DHCP in guests after every restart. If the upstream switch runs STP, it still protects the LAN.
The kernel's built-in STP is the original 802.1D protocol. For the faster Rapid STP (RSTP), the kernel hands STP to a user-space daemon such as mstpd through the /sbin/bridge-stp helper when one is installed.
| Tool | STP by default | Setting |
|---|---|---|
ip link (kernel default) | off | stp_state 1 |
| NetworkManager | on | bridge.stp yes|no |
| netplan | on | parameters: stp: true|false |
| systemd-networkd | kernel default (off) | STP=yes in [Bridge] |
Save the bridge
NetworkManager (Fedora, RHEL, desktops)
sudo nmcli connection add type bridge con-name br0 ifname br0 \
ipv4.method auto bridge.stp no
sudo nmcli connection add type ethernet con-name br0-port1 ifname enp3s0 \
controller br0 port-type bridge
sudo nmcli connection up br0
nmcli device status
For a static address, add ipv4.method manual ipv4.addresses 192.0.2.10/24 ipv4.gateway 192.0.2.1 to the bridge profile. controller and port-type are the current names; older NetworkManager releases use master br0 slave-type bridge instead. Activating the port profile replaces the card's old profile, so run this from a console.
systemd-networkd
Three files in /etc/systemd/network/:
# 25-br0.netdev
[NetDev]
Name=br0
Kind=bridge
[Bridge]
STP=yes
ForwardDelaySec=4
# 25-br0-port.network
[Match]
Name=enp3s0
[Network]
Bridge=br0
# 25-br0.network
[Match]
Name=br0
[Network]
DHCP=yes
sudo networkctl reload
networkctl status br0
netplan (Ubuntu)
/etc/netplan/60-bridge.yaml:
network:
version: 2
renderer: networkd
ethernets:
enp3s0:
dhcp4: false
bridges:
br0:
interfaces: [enp3s0]
dhcp4: true
parameters:
stp: false
Apply it with sudo netplan try: it rolls back after 120 seconds unless you confirm, which saves you from a lock-out. With STP on, raise the wait with --timeout, because STP can take over a minute to settle.
Debian ifupdown
Older Debian setups describe bridges in /etc/network/interfaces with a bridge_ports line (needs the bridge-utils package for the helper scripts). New installs are easier to manage with one of the tools above.
Bridges for virtual machines
With libvirt, a guest joins the host bridge through an interface of type bridge. Edit the guest with sudo virsh edit vmname and use:
<interface type='bridge'>
<source bridge='br0'/>
<model type='virtio'/>
</interface>
When the guest starts, libvirt creates a tap device and adds it to br0; bridge link show lists it. Compare this with libvirt's default network: virbr0 is also a bridge, but it has no physical card in it, and guests reach the outside through NAT.
VLANs on a bridge
A VLAN-aware bridge can carry several VLANs over one trunk port and put each guest in its own VLAN:
sudo ip link set dev br0 type bridge vlan_filtering 1
sudo bridge vlan add dev enp3s0 vid 10
sudo bridge vlan add dev vnet0 vid 10 pvid untagged
sudo bridge vlan del dev vnet0 vid 1
bridge vlan show
pvid untagged means the guest sends and receives plain frames, and the bridge tags them with VLAN 10 on the trunk. Every port starts as an untagged member of VLAN 1, so remove VLAN 1 from the guest port, or the guest also sees the untagged VLAN 1 traffic. libvirt creates a new vnetN device each time a guest starts, so these per-port commands must be repeated then (for example from a libvirt hook script).
Filtering bridged traffic
Use the nftables bridge family to filter frames passing through a bridge. This example stops an untrusted guest on port vnet0 from answering DHCP requests for the whole LAN:
table bridge vmfilter {
chain forward {
type filter hook forward priority filter; policy accept;
iifname "vnet0" udp sport 67 udp dport 68 drop
}
}
The old way, loading br_netfilter so that iptables sees bridged packets, is a legacy feature (Docker and some Kubernetes setups still load it; check with lsmod | grep br_netfilter). nftables and ebtables work without it, and leaving it out avoids surprises where host firewall rules suddenly apply to VM traffic.
Common mistakes
- IP address left on the card. Once a card is a bridge port, its own address is useless. Put the address on
br0. - Locking yourself out. Build the bridge from a console, or use
netplan try. - Guests time out on DHCP. STP with the default 15-second forward delay holds new ports for about 30 seconds. Shorten the delay or turn STP off where no loop is possible.
- Two tools managing one card. If NetworkManager and networkd (or ifupdown) both configure
enp3s0, the bridge breaks at the next restart.
The classic 2001 BRIDGE-STP-HOWTO, written for the 2.2/2.4 bridge patches and brctl, is kept by the LDP at tldp.org.
Sources
- Ethernet Bridging, kernel.org documentation
- ip-link(8): bridge type options, man7.org
- bridge(8), man7.org
- Bridge settings (defaults for stp, forward-delay, priority), NetworkManager
- nm-settings-nmcli(5): controller and port-type, NetworkManager
- systemd.netdev(5): [Bridge] section, freedesktop.org
- systemd.network(5): Bridge=, freedesktop.org
- Netplan YAML reference: bridges, netplan.readthedocs.io
- netplan try, netplan.readthedocs.io
- Networking, libvirt wiki
- NetworkConfiguration (bridge example), Debian Wiki
The ip, bridge and nft commands on this page were tested in a network namespace with iproute2 6.15, kernel 6.12 and nftables 1.1.3.