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-1→172.17.1.0/24br-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 crée :
| |
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) :
| Fichier | Rôle |
|---|---|
/etc/libvirt/bridge-forward.conf | Quel bridge peut joindre quel bridge |
/usr/local/sbin/libvirt-bridge-forward | apply / flush / status (pilote nft) |
/etc/systemd/system/libvirt-bridge-forward.service | Applique au boot / au restart |
/etc/libvirt/hooks/network | Réapplique après reconstruction des règles nft par libvirt |
1. Fichier de politique — bridge-forward.conf
| |
oneway— uniquementfrom → toboth— 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 :
| |
Sur apply, le script fait à peu près :
Attendre que les chaînes
guest_crossetguest_natdeip libvirt_networkexistent.Supprimer les règles précédentes taguées avec le préfixe de commentaire
lbf-.Pour chaque ligne du conf, insert en tête de
guest_cross:1iif <from> oif <to> acceptEt insert dans
guest_nat(optionnel pour la joignabilité, utile pour garder les vraies IP source) :1ip saddr <from_cidr> ip daddr <to_cidr> returnSans ç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.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
| |
Activation et usage :
| |
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 :
| |
Le rendre exécutable (chmod +x) et lui donner le bon type SELinux :
| |
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) :
| |
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).
- Le guest route via
172.17.1.1(hôte sur le bridge). - L’hôte forward (
net.ipv4.ip_forward=1). - Le paquet passe le hook forward de
libvirt_network:guest_cross: matchiif br-network-1 oif br-network-2→ accept (notre règle).
guest_nat: matchsaddr 172.17.1.0/24 daddr 172.17.2.0/24→ return (pas de masquerade ; sans cette règle le paquet est quand même forwardé, mais SNATé).- Le forward firewalld de la zone
libvirtaccepte 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 :
| |
Exemple concret — une seule règle nft (un sens)
Pour autoriser uniquement br-network-1 → br-network-2 à la main (sans conf ni service), inserer en tête de la chaîne guest_cross de libvirt :
| |
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 :
| |