When Docker’s Firewall Blocks Your Virt-Manager VM
Table of Contents
Resolving Network Forwarding Conflicts Between Libvirt and Docker#
1. Introduction#
On a Linux development host, it is common to run both virtual machines managed by libvirt/KVM and containers managed by Docker. While powerful, this combination can lead to a networking conflict that isolates virtual machines from the internet. This article provides a technical root cause analysis of this issue and presents a robust, persistent solution using firewalld.
The primary symptom is that a libvirt VM, configured with the default NAT network, can be reached from the host but cannot access external networks. A key diagnostic indicator is the ability of the VM to resolve public DNS names while failing to receive responses from ping or other network requests.
2. Root Cause Analysis#
The issue stems from the interaction between how Docker and libvirt manage the host’s netfilter (iptables) framework, specifically the FORWARD chain.
-
Libvirt’s Networking Model: By default,
libvirtcreates a virtual bridge (virbr0) and uses a NAT-based network (192.168.122.0/24). For a VM to access the internet, the host’s kernel must be configured to forward packets from thevirbr0interface to the primary network interface. This process also requires a correspondingiptablesPOSTROUTINGrule to apply Source Network Address Translation (SNAT), typically via theMASQUERADEtarget. -
Docker’s Network Configuration: For security and container isolation, the Docker daemon aggressively manages the host’s firewall. Upon startup, it inserts a number of rules into
iptables. Critically, it sets the default policy of theFORWARDchain toDROP. It then inserts rules to selectively accept traffic related to its own managed networks (e.g.,DOCKER-USER,DOCKER-FORWARDchains).
The Conflict: Docker’s DROP policy on the FORWARD chain is evaluated before libvirt’s less-specific rules. Consequently, packets originating from the virbr0 interface, which are not explicitly matched by a higher-priority ACCEPT rule, are dropped by the default policy. Standard firewalld zone-based rules or policies can fail to take precedence over the rules Docker injects directly into the top of the chain.
The solution requires two components:
- Explicit Forwarding Rules: We must inject high-priority rules to explicitly
ACCEPTtraffic to and from thevirbr0interface, ensuring these rules are evaluated before Docker’s chains. - Masquerading: We must ensure that NAT is correctly enabled for the
libvirtzone to handle the source IP address translation.
3. Solution Implementation#
This solution uses firewalld to manage the configuration persistently and correctly.
Step 3.1: Configure Explicit Packet Forwarding Rules#
We will use firewalld’s --direct interface to insert iptables rules at a specific priority in the FORWARD chain. A priority of 0 places them at the top, guaranteeing they are processed first.
# Add a permanent rule to accept traffic originating from the virbr0 interface for forwarding.
sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -i virbr0 -j ACCEPT
# Add a permanent rule to accept return traffic destined for the virbr0 interface
# for established or related connections.
sudo firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -o virbr0 -m state --state RELATED,ESTABLISHED -j ACCEPT
-i virbr0: Specifies the input interface.-o virbr0: Specifies the output interface.-m state --state RELATED,ESTABLISHED: This is a stateful rule that only matches packets that are part of an existing connection, which is essential for return traffic.-j ACCEPT: Specifies the target; in this case, to accept the packet.
Step 3.2: Enable Network Address Translation (NAT)#
The libvirt zone in firewalld must be configured to masquerade outgoing traffic. This will rewrite the source IP of packets from the VM to the host’s IP address.
# Enable masquerading on the libvirt zone permanently.
sudo firewall-cmd --permanent --zone=libvirt --add-masquerade
Step 3.3: Apply Configuration#
The permanent rules must be applied to the running firewall configuration.
# Reload firewalld to apply all permanent changes.
sudo firewall-cmd --reload
4. Verification#
After applying the configuration, verify that the rules are correctly implemented.
1. Inspect the iptables FORWARD Chain:
Confirm that the new rules are at the top of the chain.
sudo iptables -L FORWARD -n --line-numbers
Expected Output:
Chain FORWARD (policy DROP)
num target prot opt source destination
1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
2 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
3 DOCKER-USER all -- 0.0.0.0/0 0.0.0.0/0
...
2. Verify Masquerading Status:
Confirm that masquerading is enabled for the libvirt zone.
sudo firewall-cmd --zone=libvirt --query-masquerade
Expected Output:
yes
3. Test VM Connectivity: Restart the virtual machine to ensure it obtains a fresh network lease, then test external connectivity.
# From inside the VM
ping 8.8.8.8
The ping should now succeed, confirming that the network conflict has been resolved. This configuration is persistent across reboots and service restarts.
Conclusion#
I hope you liked blog post on troubleshooting things. Stay tuned for more such articles. 🫡