Bridge and STP HOWTO: Linux network bridges for VMs and LANs

Updated November 2026

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:

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.

brctl commands and their replacements
Old (bridge-utils)Today (iproute2)
brctl addbr br0ip link add br0 type bridge
brctl addif br0 eth0ip link set dev eth0 master br0
brctl delif br0 eth0ip link set dev eth0 nomaster
brctl showbridge link show
brctl showmacs br0bridge fdb show br br0
brctl stp br0 onip link set dev br0 type bridge stp_state 1
brctl setbridgeprio br0 4096ip 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

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.

STP default when you create a bridge
ToolSTP by defaultSetting
ip link (kernel default)offstp_state 1
NetworkManageronbridge.stp yes|no
netplanonparameters: stp: true|false
systemd-networkdkernel 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

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

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.