Troubleshooting tales — How to connect two QEMU guests via a bridge interface
Recently, I wanted to study how DRBD worked closely and to accomplish it I always thought that being able to set a break-point and walk-through the routines to be the fastest way of learning anything.
DRBD runs mostly in kernel-space which left me with no alternatives but to run 2 QEMU guests — one as the primary and another as the secondary resources — both attached to a GDB session for debugging…. What could go wrong, right?
Network set up
Creating Bridge — Host Level
sudo ip link add br0 type bridge
sudo ip addr add 192.168.0.1/24 dev br0
sudo ip link set br0 up
Launching QEMU guests — Host Level
PS: I won’t add the kernel/GDB parameters in here — makes it easier for readers
# Guest 1
qemu-system-x86_64 \
-M pc \
-kernel ~/workspace/linux/build/arch/x86_64/boot/bzImage \
-initrd ~/workspace/minimal_linux_fs/images/rootfs.cpio \
-append "console=tty1 console=ttyS0" \
-net nic,model=virtio -net user \
-netdev bridge,id=hn1 \
# note that MAC addr is different
-device virtio-net,netdev=hn1,mac=e6:c8:ff:09:76:99 \
-m 1024 -d guest_errors -nographic
# Guest 2
qemu-system-x86_64 \
-M pc \
-kernel ~/workspace/linux/build/arch/x86_64/boot/bzImage \
-initrd ~/workspace/minimal_linux_fs/images/rootfs.cpio \
-append "console=tty1 console=ttyS0" \
-net nic,model=virtio -net user \
-netdev bridge,id=hn1 \
# note that MAC addr is different
-device virtio-net,netdev=hn1,mac=e6:c8:ff:09:76:9c \
-m 1024 -d guest_errors -nographic
Configuring QEMU guest interfaces — Guest Level
# Guest 1
ip addr add 192.168.0.8/24 dev eth1
ip link set eth1 ip
# Guest 2
ip addr add 192.168.0.16/24 dev eth1
ip link set eth1 ip
Checking if everything is working as expected
On the host-level I check if I can access both 192.168.0.8
and 192.168.0.16
— all seems to be in order here
[paulo@fedora ~]$ ping -W 1 -c 1 192.168.0.8
PING 192.168.0.8 (192.168.0.8) 56(84) bytes of data.
64 bytes from 192.168.0.8: icmp_seq=1 ttl=64 time=0.776 ms
--- 192.168.0.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.776/0.776/0.776/0.000 ms
[paulo@fedora ~]$ ping -W 1 -c 1 192.168.0.16
PING 192.168.0.16 (192.168.0.16) 56(84) bytes of data.
64 bytes from 192.168.0.16: icmp_seq=1 ttl=64 time=1.40 ms
--- 192.168.0.16 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.396/1.396/1.396/0.000 ms
On the guest-level something looks funny. I can ping the gateway (bridge) 192.168.0.1
but I can’t ping the other guest.
# Guest 1
ping -W 1 -c 1 192.168.0.16
PING 192.168.0.16 (192.168.0.16): 56 data bytes
--- 192.168.0.16 ping statistics ---
1 packets transmitted, 0 packets received, 100% packet loss
# Guest 2
ping -W 1 -c 1 192.168.0.8
PING 192.168.0.8 (192.168.0.8): 56 data bytes
--- 192.168.0.8 ping statistics ---
1 packets transmitted, 0 packets received, 100% packet loss
Surely this must be some iptables/nftables configuration, right? So let’s check what info we can use to troubleshoot this
wireshark
wireshark doesn’t say much apart from the fact that no response was found for the ping request
nftables
nftable caught my attention because the forward policy is set to drop by default — add that to the fact that we have no rules for our bridge br0
and you might have a solution, right?
# Zero counters
[root@fedora paulo] iptables -Z
# Checking iptables/nftables rules
[root@fedora paulo] nft list table filter
table ip filter {
chain DOCKER {
}
chain DOCKER-ISOLATION-STAGE-1 {
iifname "docker0" oifname != "docker0" counter packets 0 bytes 0 jump DOCKER-ISOLATION-STAGE-2
counter packets 0 bytes 0 return
}
chain DOCKER-ISOLATION-STAGE-2 {
oifname "docker0" counter packets 0 bytes 0 drop
counter packets 0 bytes 0 return
}
chain FORWARD {
# That looks funny... (policy drop)
type filter hook forward priority filter; policy drop;
counter packets 0 bytes 0 jump DOCKER-USER
counter packets 0 bytes 0 jump DOCKER-ISOLATION-STAGE-1
oifname "docker0" ct state related,established counter packets 0 bytes 0 accept
oifname "docker0" counter packets 0 bytes 0 jump DOCKER
iifname "docker0" oifname != "docker0" counter packets 0 bytes 0 accept
iifname "docker0" oifname "docker0" counter packets 0 bytes 0 accept
}
chain DOCKER-USER {
counter packets 0 bytes 0 return
}
chain INPUT {
type filter hook input priority filter; policy accept;
}
chain OUTPUT {
type filter hook output priority filter; policy accept;
}
}
For now I’m only using ICMP so this simple rule should suffice
# add rule for interface br0
[root@fedora paulo] nft add rule ip filter FORWARD \
iifname "br0" oifname "br0" accept
# Checking iptables/nftables rules
[root@fedora paulo] nft list table filter
table ip filter {
chain DOCKER {
}
chain DOCKER-ISOLATION-STAGE-1 {
iifname "docker0" oifname != "docker0" counter packets 0 bytes 0 jump DOCKER-ISOLATION-STAGE-2
counter packets 0 bytes 0 return
}
chain DOCKER-ISOLATION-STAGE-2 {
oifname "docker0" counter packets 0 bytes 0 drop
counter packets 0 bytes 0 return
}
chain FORWARD {
type filter hook forward priority filter; policy drop;
counter packets 0 bytes 0 jump DOCKER-USER
counter packets 0 bytes 0 jump DOCKER-ISOLATION-STAGE-1
oifname "docker0" ct state related,established counter packets 0 bytes 0 accept
oifname "docker0" counter packets 0 bytes 0 jump DOCKER
iifname "docker0" oifname != "docker0" counter packets 0 bytes 0 accept
iifname "docker0" oifname "docker0" counter packets 0 bytes 0 accept
# There she is!
iifname "br0" oifname "br0" accept
}
chain DOCKER-USER {
counter packets 0 bytes 0 return
}
chain INPUT {
type filter hook input priority filter; policy accept;
}
chain OUTPUT {
type filter hook output priority filter; policy accept;
}
}
Let’s try that again
For some reason that didn’t do the trick…
# Guest 1
ping -W 1 -c 1 192.168.0.16
PING 192.168.0.16 (192.168.0.16): 56 data bytes
--- 192.168.0.16 ping statistics ---
1 packets transmitted, 0 packets received, 100% packet loss
# Guest 2
ping -W 1 -c 1 192.168.0.8
PING 192.168.0.8 (192.168.0.8): 56 data bytes
--- 192.168.0.8 ping statistics ---
1 packets transmitted, 0 packets received, 100% packet loss
I wondered if the nftables rule was even hit in the first place. That’s when things became very confusing to me.
3 things happened and they can not all be true at the same time
- the rule #7 of the FORWARD chain shows that both
pkts
andbytes
are set to0
— so it wasn’t hit - the default policy of the chain counter shows
DROP 0 packets, 0 bytes
— so it wasn’t dropped - targets
DOCKER-USER
andDOCKER-ISOLATION-STAGE-1
seem to have hit theRETURN
action — so it couldn’t have “dropped” the packets
[root@fedora paulo]# iptables -L -v -n --line-numbers
Chain INPUT (policy ACCEPT 26 packets, 10784 bytes)
num pkts bytes target prot opt in out source destination
Chain FORWARD (policy DROP 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 1 84 DOCKER-USER 0 -- * * 0.0.0.0/0 0.0.0.0/0
2 1 84 DOCKER-ISOLATION-STAGE-1 0 -- * * 0.0.0.0/0 0.0.0.0/0
3 0 0 ACCEPT 0 -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
4 0 0 DOCKER 0 -- * docker0 0.0.0.0/0 0.0.0.0/0
5 0 0 ACCEPT 0 -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
6 0 0 ACCEPT 0 -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
7 0 0 ACCEPT 0 -- br0 br0 0.0.0.0/0 0.0.0.0/0
Chain OUTPUT (policy ACCEPT 3 packets, 324 bytes)
num pkts bytes target prot opt in out source destination
Chain DOCKER (1 references)
num pkts bytes target prot opt in out source destination
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 DOCKER-ISOLATION-STAGE-2 0 -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
2 1 84 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-ISOLATION-STAGE-2 (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 DROP 0 -- * docker0 0.0.0.0/0 0.0.0.0/0
2 0 0 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-USER (1 references)
num pkts bytes target prot opt in out source destination
1 1 84 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0
Looking at wireshark confirmed to me that there was something else blocking the packet — note that now there is a Destination unreachable
packet from 192.168.0.1
(br0) to 192.168.0.16
(Guest VM 2)
My only card up my sleeve was to take a look at the program that configure that nftables/iptables rules in the kernel — that is a program called firewalld
# Stop firewalld - which despite what people think, it doesn't stop
# the "firewall" as the rule already configured in the kernel remain so
# by default
systemctl stop firewalld.service
# Running firewalld with logging verbosity
firewalld --nofork --debug 1
While I couldn’t find any log entries that could indicate why my ICMP
packets where being dropped, I saw something that caught my attention
2024-02-03 10:37:09 DEBUG1: zone.getInterfaces('docker')
2024-02-03 10:37:09 DEBUG1: zone.addInterface('docker', 'docker0')
2024-02-03 10:37:09 DEBUG1: Setting zone of interface 'docker0' to 'docker'
2024-02-03 10:37:09 DEBUG1: Applying policy (zone_docker_HOST) derived from zone 'docker'
2024-02-03 10:37:09 DEBUG1: Applying policy (zone_HOST_docker) derived from zone 'docker'
2024-02-03 10:37:09 DEBUG1: Applying policy (zone_ANY_docker) derived from zone 'docker'
2024-02-03 10:37:09 DEBUG1: Applying policy (zone_docker_ANY) derived from zone 'docker'
2024-02-03 10:37:09 DEBUG1: zone.InterfaceAdded('docker', 'docker0')
The reason why this is interesting is because docker0
is also a bridge interface and after some quick tests I could verify that ICMP
packets from docker containers going through that bridge worked just fine.
That being said, the only difference is that docker0
is part of a zone called docker
and that zone has forward: yes
# List all zones
[root@fedora paulo] firewall-cmd --get-zones
block dmz drop external home internal libvirt public trusted work
# List all active zones
[root@fedora paulo] firewall-cmd --get-active-zones
FedoraWorkstation (default)
interfaces: wlo1
docker
interfaces: docker0
# Inspect docker zone
[root@fedora paulo] firewall-cmd --zone=docker --list-all
docker (active)
target: ACCEPT
ingress-priority: 0
egress-priority: 0
icmp-block-inversion: no
interfaces: docker0
sources:
services:
ports:
protocols:
forward: yes
masquerade: no
forward-ports:
source-ports:
icmp-blocks:
rich rules:
My options are:
- Create a new zone for DRBD
- Make use of one of the inactive zones that have
forward: yes
The zone called trusted
seem to fit the criteria as it has pretty much the same config that docker
zone has
[root@fedora paulo] firewall-cmd --zone=trusted --list-all
trusted
target: ACCEPT
ingress-priority: 0
egress-priority: 0
icmp-block-inversion: no
interfaces:
sources:
services:
ports:
protocols:
forward: yes
masquerade: no
forward-ports:
source-ports:
icmp-blocks:
rich rules:
Add “br0” to “trusted” zone
# Add interface
[root@fedora paulo] firewall-cmd --zone=trusted --add-interface=br0
success
# List active zones
[root@fedora paulo] firewall-cmd --get-active-zones
FedoraWorkstation (default)
interfaces: wlo1
docker
interfaces: docker0
trusted
interfaces: br0 # There she is!!!
Testing everything (again)
# Guest 1
ping -W 1 -c 1 192.168.0.16
PING 192.168.0.16 (192.168.0.16): 56 data bytes
64 bytes from 192.168.0.16: seq=0 ttl=64 time=1.386 ms
--- 192.168.0.16 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 1.386/1.386/1.386 ms
# Guest 2
ping -W 1 -c 1 192.168.0.8
PING 192.168.0.8 (192.168.0.8): 56 data bytes
64 bytes from 192.168.0.8: seq=0 ttl=64 time=1.362 ms
--- 192.168.0.8 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 1.362/1.362/1.362 ms
Conclusion
That was tough one to debug. To be honest it took me much longer than I care to admit.
Clearly, there is something that could be changed either at the nft
, iptables
or even at the kernel log messages to indicate that something else was also acting on the packet filtering. As is, it was pretty misleading for me — Most likely for someone with a network engineering background that is “obvious” in hindsight but this is probably the exemption rather than the rule.
Hope that this has helped you get your QEMU guests communicating ;)
Acknowledgment
While my blog post was mostly focused on the troubleshooting part, the actual network topology was gotten from this blog post. In his case he didn’t face the iptables/zone issues — mostly likely due to his distro default settings. I see our blog posts being complementary.