Autoriser un réseau libvirt à en joindre un autre n’est plus une affaire de firewall-cmd --direct. C’est le pare-feu NAT de libvirt qui refuse le trafic NEW entre bridges. La forme des règles change selon la version de RHEL — et les outils aussi.

Le problème

Deux réseaux NAT libvirt isolés, par exemple :

  • br-network-1 → 172.17.1.0/24
  • br-network-2 → 172.17.2.0/24

Chaque VM joint l’hôte et l’extérieur (via NAT). Un ping d’un réseau vers l’autre échoue avec Destination Port Unreachable depuis l’IP du bridge hôte.

firewalld peut déjà afficher forward: yes sur la zone libvirt. Ce n’est pas suffisant : libvirt rejette le paquet avant.

Deux backends, deux chaînes d’outils

HôtePare-feu libvirtOù vivent les règles inter-bridges
RHEL 10+nftables natifip libvirt_network → guest_cross / guest_nat
RHEL 9backend iptables (iptables-nft)ip filter → LIBVIRT_FWX, ip nat → LIBVIRT_PRT

Repérage sur l’hôte :

1
2
nft list tables | grep libvirt_network && echo "RHEL 10+ nftables natif"
iptables -L LIBVIRT_FWX -n &>/dev/null && echo "RHEL 9 iptables-nft"

Sous RHEL 9, il n’y a en général pas de table ip libvirt / ip libvirt_network. Les chaînes apparaissent dans les tables filter / nat gérées par iptables-nft.

Scripts empaquetés (même fichier de conf) : github.com/tinsjourney/libvirt-bridge-forward

ScriptInstallCible
libvirt-bridge-forward./install.shRHEL 10+
libvirt-bridge-forward-iptables./install-iptables.shRHEL 9

Fichier de politique commun — bridge-forward.conf

1
2
# <from_bridge>  <to_bridge>  <oneway|both>
br-network-1  br-network-2  both
  • oneway — uniquement from → to
  • both — les deux sens

On édite, puis on restart l’unité systemd correspondante. Les subnets pour le bypass NAT viennent de la route IPv4 kernel de chaque bridge sur l’hôte.

RHEL 10+ — nftables natif

Libvirt crée :

1
2
3
4
table ip libvirt_network
  chain guest_cross   # n'accepte que iif==oif (même bridge)
  chain guest_input   # NEW entrant vers un bridge = REJECT
  chain guest_nat     # masquerade guest → ailleurs

Un paquet inter-bridges rate guest_cross, tombe dans guest_input et meurt. firewalld s’exécute plus tard (filter + 10) : le forward de zone ne peut pas aider.

Approche

On laisse les bridges dans la zone libvirt. On insert un ACCEPT en tête de guest_cross (nécessaire au forward). En option, un return dans guest_nat pour ne pas masquerader le guest↔guest (sinon le pair voit l’IP hôte sur le bridge de sortie ; le conntrack fait quand même marcher le ping).

FichierRôle
/etc/libvirt/bridge-forward.confPolitique
/usr/local/sbin/libvirt-bridge-forwardapply / flush / status
/etc/systemd/system/libvirt-bridge-forward.serviceBoot / restart
/etc/libvirt/hooks/networkRéapplique après reconstruction des règles nft
1
2
sudo ./install.sh
systemctl restart libvirt-bridge-forward.service

Sur apply, le script attend guest_cross / guest_nat, purge les règles lbf-*, puis insert les paires forward + bypass NAT du conf.

Chemin d’un paquet

  1. Le guest route via l’IP hôte du bridge (172.17.1.1).
  2. L’hôte forward (net.ipv4.ip_forward=1).
  3. guest_cross : iif br-network-1 oif br-network-2 → accept.
  4. guest_nat : saddr 172.17.1.0/24 daddr 172.17.2.0/24 → return (optionnel ; évite le SNAT).
  5. Le forward firewalld de la zone libvirt accepte le trafic intra-zone.

RHEL 9 — iptables-nft (LIBVIRT_*)

Le backend iptables de libvirt crée des chaînes visibles via iptables-nft, par exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
table ip filter
  chain FORWARD
    jump LIBVIRT_FWX
    jump LIBVIRT_FWI
    jump LIBVIRT_FWO
  chain LIBVIRT_FWX
    iifname "virbr0" oifname "virbr0" accept   # même bridge seulement

table ip nat
  chain POSTROUTING
    jump LIBVIRT_PRT
  chain LIBVIRT_PRT
    ... masquerade pour chaque subnet guest ...

LIBVIRT_FWX joue le rôle de guest_cross. Préférer iptables pour inserer les règles (ces tables sont gérées par iptables-nft ; un nft brut sur ip nat affiche do not touch).

FichierRôle
/etc/libvirt/bridge-forward.confMême format de politique
/usr/local/sbin/libvirt-bridge-forward-iptablesapply / flush / status
/etc/systemd/system/libvirt-bridge-forward-iptables.serviceBoot / restart
/etc/libvirt/hooks/network (depuis hooks/network-iptables)Réapplique après démarrage réseau
1
2
sudo ./install-iptables.sh
systemctl restart libvirt-bridge-forward-iptables.service

Sur apply, les règles sont inserées en tête de LIBVIRT_FWX et LIBVIRT_PRT avec le préfixe de commentaire lbf-.

Hook réseau libvirt + SELinux

Au démarrage d’un réseau virtuel, libvirt reconstruit son pare-feu et les inserts manuels disparaissent. Le hook rappelle apply.

1
2
3
chmod +x /etc/libvirt/hooks/network
restorecon -vF /etc/libvirt/hooks/network
setsebool -P virt_hooks_unconfined on

Sans virt_hooks_unconfined, virtnetworkd échoue le hook avec exit 126 / Permission denied, et les réseaux en autostart ne remontent pas après reboot.

Activer aussi le forward de zone firewalld :

1
2
firewall-cmd --permanent --zone=libvirt --add-forward
firewall-cmd --reload

Exemples concrets — forward dans un sens à la main

RHEL 10+ (nftables natif) :

1
2
nft insert rule ip libvirt_network guest_cross \
  iif "br-network-1" oif "br-network-2" counter accept

IP source réelle en option :

1
2
nft insert rule ip libvirt_network guest_nat \
  ip saddr 172.17.1.0/24 ip daddr 172.17.2.0/24 counter return

RHEL 9 (iptables-nft) :

1
iptables -I LIBVIRT_FWX 1 -i br-network-1 -o br-network-2 -j ACCEPT

IP source réelle en option :

1
iptables -t nat -I LIBVIRT_PRT 1 -s 172.17.1.0/24 -d 172.17.2.0/24 -j RETURN

Ces one-liners disparaissent au redémarrage du réseau libvirt, sauf si on utilise les scripts et le hook du dépôt.