この記事を作った動機

 libvirt の仕様を理解していなかった結果、大量の時間を溶かすことがあったので記録したい。具体的には、仮想マシンに対してRDPのためにポート転送を設定しようとしたが、libvirtが作成する、defaultネットワークに属している、virbr0ブリッジはnftablesをいじっても言うことを聞かないほど、ガチガチに守られていて使うべきではないという結論に至った。

 今回はそこに至るまでの経緯と、実際に機能する事を確かめた対処方法について記録する。機能する対処方法の概要としては、libvirt以外に、手動でブリッジを専用に作成し、それに対してfirewalldでNATやDNATを構成し、仮想マシンにそのブリッジを使うように設定する事をする。

機能した対処方法

仮定

  • 仮想マシンを動かしているホストマシンは、firewalldのpublicゾーンを使用している
  • ホストマシンは、ブリッジに対して192.168.150.1/24のIPを持つ
  • 仮想マシンのIPは手動で192.168.150.25/24を割り当て、ゲートウェイにはホストマシンに向けて192.168.150.1と設定する
  • sysctl -w net.ipv4.ip_forward=1が設定されており、カーネルが異なるインタフェース間のパケットの行き来を許可していること

ブリッジを作成

sudo brctl addbr virt
sudo ip link set virt up
sudo ip addr add 192.168.150.1/24 dev virt

仮想マシンを作成したブリッジを使うように設定

ブリッジを指定する様子

firewalld でNATとDNATを構成する

# ゾーンとポリシー(NAT用)を作成
sudo firewall-cmd --new-zone=virt --permanent
sudo firewall-cmd --zone=virt --add-interface=virt --permanent
sudo firewall-cmd --zone=virt --new-policy=nat --permanent
sudo firewall-cmd --policy=nat --add-ingress-zone=virt --permanent
sudo firewall-cmd --policy=nat --add-egress-zone=public --permanent
sudo firewall-cmd --policy=nat --add-masquerade --permanent

# ポート転送設定(DNAT)
sudo firewall-cmd --zone=public --add-forward-port=port=33890:proto=tcp:toport=3389:toaddr=192.168.150.25 --permanent
sudo firewall-cmd --zone=public --add-forward-port=port=33890:proto=udp:toport=3389:toaddr=192.168.150.25 --permanent

# NATが機能するようにマスカレードと転送を設定
sudo firewall-cmd --zone=public --add-masquerade --permanent
sudo firewall-cmd --zone=virt --add-forward --permanent
sudo systemctl restart firewalld

問題至るまでの経緯

今までの構成を見直す

 今まで仮想マシンへのRDPアクセスは、無理やりVPN経路を通じて同じLAN内に仮想マシンを参加させることで動かしていた。接続が不安定であり定期的にVPNが接続できなくなったり、ネットワークトポロジ的にも循環経路を形成しかねないように見受けられた。VPNを介さずにホストマシンを直接介させることで、仮想マシンに対してRDPを行うために、今回は仮想マシンのRDPポートである、3389に対してポート転送を行う事を考えた。

ポート転送できず苦しむ

 ポート転送するに際して、既に仮想マシンが、virbr0のブリッジに属していることに気づく。firewalldによって、ポート転送をそれに対して設定すれば、動くだろうと思い色々試行錯誤したもののどれもうまく行かず。最終的にnftablesで最優先に設定した設定をpreroutingやforwardチェーンに入れると、パケットはそのルールを反映するものの、意地でもvirbr0ではtcpdump -i virbr0 port 3389としてもパケットが一切見られなかった。あれこれして、ネットで色々当たった結果、以下のサイトの記述にとり、重要な事実としてlibvirtのデフォルトで作成されるブリッジについて重要な事実を知る。

The problem is in the KVM (libvirt) which when starting the "network" virbr0 injects rules (probably via firewalld) which prevents (first match principle) any attempt to port forwarding by conventional ways using firewall-cmd. The KVM injected (libvirt) rules are at the top of FORWARD and POSTROUTING, before regular firewalld ruleset. Since the netfilter operates on first match principle, these rules.

ブリッジを手動で作成する考えに至るまで

 同じく以下の記事にして、以下の部分とlibvirtが生成したブリッジには触りたくないということとVirt-managerにあるネットワークの設定項目で任意のブリッジを手動で設定できる項目があることから、今回解決とした策に至った。

I have found another solution, which does not require any extra code. This was tested on Fedora 41; I am not sure about CentOS 7 which is rather old by this time.

All the commands in these instructions are to be run as root.

Edit the libvirt network (virsh net-edit default usually), find , change to . Then either just reboot your host or restart the network, usually like this:

virsh net-destroy default
virsh net-start default
At this point your guests have lost communication with the "outside world" because you have disabled libvirt's NAT, but now you add your own NAT.

First, take a look at your zones with firewall-cmd --get-active-zones. Here's what I got:

# firewall-cmd --get-active-zones
FedoraServer (default)
 interfaces: ens18
libvirt
 interfaces: virbr0
Your public zone can have a different name, adjust the rest of the commands accordingly (I think it's usually "public" in RHEL and derivatives). If you don't see virbr0 in the libvirt zone, add it: firewall-cmd --zone libvirt --add-interface virbr0. If virbr0 is in some other zone, use that zone name instead. But it should not be in your public zone.

Now we create the new NAT:

firewall-cmd --permanent --new-policy libvirt-nat
firewall-cmd --permanent --policy libvirt-nat --set-target ACCEPT
firewall-cmd --permanent --policy libvirt-nat --add-ingress-zone libvirt
firewall-cmd --permanent --policy libvirt-nat --add-egress-zone FedoraServer
firewall-cmd --permanent --policy libvirt-nat --add-masquerade
firewall-cmd --reload
Your guests should have their connectivity restored, and with this NAT your port forwarding is rather simple, for example:

firewall-cmd --permanent --zone FedoraServer --add-forward-port=port=8080:proto=tcp:toport=8080:toaddr=192.168.122.117
firewall-cmd --reload
Replace the port number and internal address as necessary.

There is a special case: when your guests have real IPv6 addresses and you have routing of these addresses configured through your host already . In this case you might want to allow the outside world to connect to your guests. If - and that is an if - you want to do so, add another policy:

firewall-cmd --permanent --new-policy libvirt-inbound
firewall-cmd --permanent --policy libvirt-inbound --set-target ACCEPT
firewall-cmd --permanent --policy libvirt-inbound --add-egress-zone libvirt
firewall-cmd --permanent --policy libvirt-inbound --add-ingress-zone FedoraServer
firewall-cmd --reload
Sometimes a cloud provider will give you IPv6 addresses but not route them. There is still a way to use them like this, it is something called "proxying NDP", but it is beyond the scope of this answer.

使った画像とか

参考にしたサイトとか