Configuring Network Bridge and TAP Interface for KVM on Linux — Hands-On from the Homelab

Virtualization tutorial - IT technology blog
Virtualization tutorial - IT technology blog

The Real Problem When Running KVM Without a Bridge

I run a homelab with Proxmox VE managing 12 VMs and containers — a playground to test everything before pushing to production. Before switching to Proxmox, I used bare KVM on Ubuntu Server 22.04. The first headache: after creating a VM, virt-manager showed it was using NAT, but the VMs couldn’t ping each other and couldn’t be reached via SSH from outside. Diagnosing which ports are actually listening is much easier once you’re comfortable with netstat and ss for analyzing network connections on Linux.

The root cause: KVM defaults to NAT mode through the virtual bridge virbr0 created by libvirt — defaulting to the 192.168.122.0/24 subnet, completely isolated from your LAN. Fine for personal labs, but it puts VMs behind NAT. To give VMs a real IP on the LAN, you need to create a network bridge attached to the physical NIC and have VMs use a TAP interface to connect through it.

Bridge and TAP — Straight to the Point

KVM documentation usually explains these two concepts separately, so when things go wrong it’s hard to know which layer to debug. Let me consolidate them here before diving into the config.

Network Bridge (br0)

A bridge acts like a virtual Layer 2 switch. Add the physical NIC (enp3s0) to the bridge, and the IP now lives on br0 — no longer on enp3s0. The bridge accepts traffic from both the physical network and any virtual interfaces attached to it. VMs connected to the bridge get an IP from the real DHCP router — exactly like a computer plugged into a switch with a network cable.

TAP Interface

TAP is a virtual network interface operating at Layer 2 (Ethernet frames). When QEMU/KVM boots a VM, it creates a TAP interface (tap0, vnet0…) on the host. One end of the TAP is attached to the bridge; the other end is the virtual NIC visible to the guest OS.

Data flow: Guest NIC → TAP → Bridge → Physical NIC → Router/LAN.

Why not use TUN? TUN operates at Layer 3 (IP packets), but VMs need Layer 2 to handle ARP, broadcasts, and VLAN tagging. TAP is what meets that requirement.

Practical Configuration — Step by Step

Step 1: Identify the Physical Network Interface

ip link show
# Find the interface with an active connection, e.g.: enp3s0 or eth0

ip addr show enp3s0
# Note the current IP, subnet mask, and gateway

Warning before you proceed: once you add enp3s0 to the bridge, that interface loses its IP immediately — the IP moves to br0. If you’re connected via SSH over enp3s0, the connection will drop the moment you run the command. The safest approach is to work directly on the console, or write a script with all the commands and run it in one shot via nohup sh bridge-setup.sh & — then reconnect via SSH using the new IP on br0. A reliable alternative: run the commands inside a tmux session — if the SSH connection drops, you can reconnect and the session stays alive.

Step 2: Create the Bridge via Netplan

Ubuntu Server uses Netplan by default — this is the persistent method, surviving reboots without losing configuration. Back up first:

sudo cp /etc/netplan/00-installer-config.yaml /etc/netplan/00-installer-config.yaml.bak

DHCP configuration:

# /etc/netplan/00-installer-config.yaml
network:
  version: 2
  renderer: networkd
  ethernets:
    enp3s0:
      dhcp4: no        # Disable DHCP on the physical interface
      dhcp6: no
  bridges:
    br0:
      interfaces: [enp3s0]   # Attach enp3s0 to the bridge
      dhcp4: yes             # Bridge gets its IP from DHCP
      parameters:
        stp: false           # Disable Spanning Tree (usually not needed in a lab)
        forward-delay: 0

Or use a static IP if your router has no DHCP or you need a fixed address:

  bridges:
    br0:
      interfaces: [enp3s0]
      addresses: [192.168.1.100/24]
      routes:
        - to: default
          via: 192.168.1.1
      nameservers:
        addresses: [1.1.1.1, 8.8.8.8]
      parameters:
        stp: false
        forward-delay: 0

Apply the configuration:

sudo netplan apply

# Verify the bridge was created
ip addr show br0
bridge link show

Step 3: Manually Create a TAP Interface (to understand the mechanics)

libvirt handles this step automatically when booting a VM. But doing it manually once builds a solid mental model — and makes debugging network issues much faster:

# Create the TAP interface
sudo ip tuntap add tap0 mode tap

# Bring up the interface
sudo ip link set tap0 up

# Attach TAP to the bridge
sudo ip link set tap0 master br0

# Verify — tap0 should appear in the list
bridge link show br0

Remove after testing:

sudo ip link set tap0 nomaster
sudo ip tuntap del tap0 mode tap

Step 4: Configure libvirt to Use the Bridge

This is my most-used approach — letting libvirt manage TAP interfaces automatically:

# Create an XML file defining the network
cat << 'EOF' > /tmp/br0-network.xml
<network>
  <name>br0-network</name>
  <forward mode="bridge"/>
  <bridge name="br0"/>
</network>
EOF

# Register the network with libvirt
virsh net-define /tmp/br0-network.xml
virsh net-start br0-network
virsh net-autostart br0-network

# Verify
virsh net-list --all

Attach the VM’s NIC to the bridge. Option one — edit via virsh edit:

virsh edit vm-name

# Find the <interface type='network'> section and change it to:
# <interface type='bridge'>
#   <source bridge='br0'/>
#   <model type='virtio'/>
# </interface>

Or attach directly to a running VM:

virsh attach-interface --domain vm-name \
  --type bridge \
  --source br0 \
  --model virtio \
  --config --live

Step 5: Start the VM and Verify

virsh start vm-name
virsh console vm-name   # Or use virt-viewer

# Inside the guest OS:
ip addr show     # Should show an IP in the 192.168.1.x range
ping 192.168.1.1 # Ping the gateway

# From another machine on the LAN:
ping <VM IP>       # Should be reachable
ssh user@<VM IP>   # SSH directly without going through NAT

Debugging When the Bridge Isn’t Working

The four issues I’ve hit most often — and quick fixes:

  • VM not getting an IP via DHCP: Run bridge fdb show br0 — the TAP interface must appear there. If it doesn’t, attach it manually: ip link set vnetX master br0. libvirt sometimes creates a TAP but forgets to attach it to the bridge after a host restart.
  • Lost SSH after running netplan apply: The IP has moved to br0. Get the new IP with ip addr show br0 directly on the console, then reconnect via SSH.
  • Packets dropped even though the bridge looks correct: libvirt adds iptables FORWARD rules. Check with iptables -L FORWARD -n -v. If the policy is DROP, add a rule: iptables -I FORWARD -i br0 -j ACCEPT.
  • Bridge not forwarding packets despite correct iptables rules: Check sysctl net.bridge.bridge-nf-call-iptables. A value of 1 means the bridge calls iptables for every packet — try setting it to 0: sysctl -w net.bridge.bridge-nf-call-iptables=0.

Wrap-Up

Bridge + TAP is the foundational networking architecture used by most mainstream hypervisors today — Proxmox VE, OpenStack Nova, and libvirt on Debian all rely on this same principle under the hood; they just differ in how much of the configuration is automated.

Ever since I internalized the Guest NIC → TAP → Bridge → Physical NIC flow, I’ve been able to resolve most VM network tickets in under 10 minutes. Issues almost always come down to one of three points: the TAP isn’t attached to the bridge, iptables is blocking FORWARD traffic, or sysctl bridge-nf is interfering.

If you want to go deeper, two paths worth exploring: macvtap — the VM uses the physical NIC’s MAC layer directly, bypassing the bridge entirely for lower latency; or SR-IOV — the physical NIC creates Virtual Functions assigned directly to the VM, delivering near bare-metal performance. If you want to strengthen the host side first, network bonding on Linux pairs naturally with bridging — bond multiple physical NICs for redundancy, then attach the bond interface to br0. For a typical homelab, bridge + TAP is more than enough and the easiest to debug.

Share: