Sous RHEL avec le backend nftables de firewalld, autoriser un réseau libvirt à joindre un autre n’est plus une affaire de firewall-cmd --direct. Libvirt installe sa propre table ip libvirt_network, et c’est elle qui refuse le trafic NEW entre bridges.

Le problème

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

  • br-network-1172.17.1.0/24
  • br-network-2172.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 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 ne matche pas guest_cross, tombe dans guest_input et est rejeté. firewalld s’exécute plus tard (filter + 10) : policies / forward de zone ne sauvent pas un paquet déjà rejeté dans libvirt_network.

L’approche

On laisse les deux bridges dans la zone libvirt (libvirt les y remettrait de toute façon). On insert des règles ACCEPT en tête de guest_cross. Cela suffit pour que les paquets soient forwardés.

En option, on évite aussi le masquerade entre les deux subnets guests dans guest_nat. Ce n’est pas nécessaire pour la connectivité : sans ça, le ping fonctionne encore, mais les règles NAT de libvirt réécrivent la source vers l’IP de l’hôte sur le bridge de sortie (172.17.2.1 au lieu de 172.17.1.3). Le conntrack répare le chemin retour, donc la conversation marche, mais en SNAT. Le bypass guest_nat conserve les vraies IP source des guests.

Pour que ce soit durable et éditable, quatre fichiers (aussi disponibles sur github.com/tinsjourney/libvirt-bridge-forward) :

FichierRôle
/etc/libvirt/bridge-forward.confQuel bridge peut joindre quel bridge
/usr/local/sbin/libvirt-bridge-forwardapply / flush / status (pilote nft)
/etc/systemd/system/libvirt-bridge-forward.serviceApplique au boot / au restart
/etc/libvirt/hooks/networkRéapplique après reconstruction des règles nft par libvirt

1. Fichier de politique — 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 ce fichier, puis on restart (ou reload) l’unité systemd. Les subnets pour le bypass NAT sont découverts via la route IPv4 kernel de chaque bridge sur l’hôte (172.17.1.0/24, 172.17.2.0/24, …).

2. Script d’application — libvirt-bridge-forward

Installé dans /usr/local/sbin/libvirt-bridge-forward.

Commandes :

1
2
3
libvirt-bridge-forward apply    # purge les règles gérées, puis applique le conf
libvirt-bridge-forward flush    # retire uniquement les règles gérées
libvirt-bridge-forward status   # affiche le conf + les règles nft actuelles

Sur apply, le script fait à peu près :

  1. Attendre que les chaînes guest_cross et guest_nat de ip libvirt_network existent.

  2. Supprimer les règles précédentes taguées avec le préfixe de commentaire lbf-.

  3. Pour chaque ligne du conf, insert en tête de guest_cross :

    1
    
    iif <from> oif <to> accept
    
  4. Et insert dans guest_nat (optionnel pour la joignabilité, utile pour garder les vraies IP source) :

    1
    
    ip saddr <from_cidr> ip daddr <to_cidr> return
    

    Sans ça, libvirt continue de masquerader 172.17.1.0/24 → !172.17.1.0/24, donc le trafic vers l’autre réseau guest est SNATé via l’hôte.

  5. Si la direction est both, faire aussi la paire inverse.

Les règles gérées sont identifiées par des commentaires du type lbf-c-br-network-1-to-br-network-2, donc le ré-apply est idempotent.

3. Unité systemd — libvirt-bridge-forward.service

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[Unit]
Description=Apply libvirt bridge-forward.conf (cross-bridge forwarding)
After=virtnetworkd.service libvirtd.service firewalld.service
Wants=virtnetworkd.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/bin/sleep 1
ExecStart=/usr/local/sbin/libvirt-bridge-forward apply
ExecReload=/usr/local/sbin/libvirt-bridge-forward apply
ExecStop=/usr/local/sbin/libvirt-bridge-forward flush

[Install]
WantedBy=multi-user.target

Activation et usage :

1
2
3
4
5
6
systemctl enable --now libvirt-bridge-forward.service

# après édition du conf :
systemctl restart libvirt-bridge-forward.service
# ou :
systemctl reload libvirt-bridge-forward.service

RemainAfterExit=yes rend restart/reload pertinents pour un service oneshot.

4. Hook réseau libvirt — /etc/libvirt/hooks/network

Quand libvirt démarre ou redémarre un réseau virtuel, il reconstruit ip libvirt_network et vos inserts disparaissent. Le hook les remet :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash
NETWORK="$1"
OPERATION="$2"
SUBOP="$3"

case "${OPERATION}/${SUBOP}" in
  started/end|restarted/end) ;;
  *) exit 0 ;;
esac

/usr/local/sbin/libvirt-bridge-forward apply || true
exit 0

Le rendre exécutable (chmod +x) et lui donner le bon type SELinux :

1
2
chmod +x /etc/libvirt/hooks/network
restorecon -vF /etc/libvirt/hooks/network

Sous RHEL avec SELinux Enforcing, virtnetworkd ne peut toujours pas exécuter le hook tant que ce booléen n’est pas activé (sinon virsh net-start échoue avec exit 126 / Permission denied, et les réseaux en autostart ne remontent pas après reboot) :

1
setsebool -P virt_hooks_unconfined on

Le hook appelle le même apply que l’unité systemd : le conf reste la seule source de vérité.

Chemin d’un paquet autorisé

Exemple : une VM sur br-network-1 (172.17.1.3) ping une VM sur br-network-2 (172.17.2.5).

  1. Le guest route via 172.17.1.1 (hôte sur le bridge).
  2. L’hôte forward (net.ipv4.ip_forward=1).
  3. Le paquet passe le hook forward de libvirt_network :
    • guest_cross : match iif br-network-1 oif br-network-2accept (notre règle).
  4. guest_nat : match saddr 172.17.1.0/24 daddr 172.17.2.0/24return (pas de masquerade ; sans cette règle le paquet est quand même forwardé, mais SNATé).
  5. Le forward firewalld de la zone libvirt accepte déjà le trafic intra-zone.

Sans l’étape 3, guest_input ferait un REJECT du flux NEW vers br-network-2. L’étape 4 ne sert qu’à conserver l’adresse source réelle.

Penser aussi à activer le forward de zone firewalld :

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

Exemple concret — une seule règle nft (un sens)

Pour autoriser uniquement br-network-1br-network-2 à la main (sans conf ni service), inserer en tête de la chaîne guest_cross de libvirt :

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

Cette seule règle suffit pour le forward dans un sens. Elle disparaît au redémarrage du réseau libvirt, sauf si on la réajoute (ou si on utilise les quatre fichiers ci-dessus).

Si la VM destination doit aussi voir la vraie IP source (pas de SNAT via l’hôte), ajouter :

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