在 Virt-manager 為了能讓 VM 上網踩到的坑
我弄好 Virt-manager 後,我迫不及待安裝 Windows VM 讓我有 Photoshop,因為我不想要有未授權的提示和潛在問題 (非企業版會有浮水印,Enterprise 則是會用到一半關機),所以我需要聯網授權。
就是這件事,讓我踩了一堆坑,還記得我在安裝 Virt-manager 結尾說的嗎?
virbr0這個 NAT 界面是個下限極低,但上限極高的存在…
完整結語可以回去看前一篇我講了什麼,這裡是典型的 2 個服務運作正常,搭載一起結果不正常的狀態,分別是 libvirt default network 和 ufw 的耦合失敗,這裡我先說明 Default network 的部分。
Libvirt Default Network
libvirt 的 Default network 是 virtnetworkd 相關服務啟動後能用的預設選項,界面開啟後會在防火牆自動建立 NAT 相關規則,並且啟動時建立 virbr0 的界面。
整個 default 啟動時大致上會開啟這些東西: 建立 virbr0 -> 設定 NAT 規則 -> 設定 net.ipv4.ip_forward=1 -> fork dnsmasq (輕量 DNS forwarder 和 DHCP Server) 實例到 virbr0 。
補充:
virbr0的 dnsmasq 實例和dnsmasq.service不是同一個事情,前者是綁定界面的,後者是獨立服務。
所以你會看到 default 啟動後dnsmasq.service還是 inactive ,兩者也不能同時存在。
Default Network DHCP
還記得前一篇文有放出 libvirt 可選依賴的圖嗎?
有一個選項就是 dnsmasq,描述就是「Required for default NAT」之類的描述,跟 iptables-nat 一樣。
補充:近幾個月的 Arch news 宣佈
iptables-nft正式更名為iptables,所以這個依賴才沒打勾,不然 CachyOS 預設安裝 ufw 就會有iptables,然後 iptables 現在預設都是走 nftables 當 backend。
這裡我提到了 dnsmasq 作為 DHCP 套件以及簡單提到了 ufw 了吧?這會直接影響到網路功能 Debug。
回憶 ufw
還記得我在 CachyOS 遷移心得 1 有簡短提到 ufw 和預設開啟的事情嗎?
我有預料到他會讓部分服務失常,如果需要使用服務要麼關防火牆,要麼寫規則,因為 KDE Connect 讓我發現 ufw 預設開啟,只是我沒想到到了 Virt-manager 這件事會變得如此慘烈。
因為我鮮少安裝過 Virt-manager ,加上 Virt-manager 在 CachyOS 上不像是 Ivon 在 Ubuntu 上安裝 Virt-manager 一樣這麼順利,所以我以為會有什麼服務不會開。
結果一路排查下來,就是防火牆搞鬼,預設 DROP 讓我吃盡苦頭,還好有 Claude 可以讓我大幅加速排錯路程,不然我可能都不知道 Virt-manager 和 Libvirt 是怎麼發 DHCP 的,畢竟我也沒看到 ISC DHCP Server (況且這個套件也在被 Kea DHCP 取代),不是 Router 或專門管 IP 的機器那這類 DHCP 套件也過重。
不修復 default 的上網法
如果你只是想快點讓 VM 可以正常連上網路,那這裡有 3 個解法:
這裡先講最難的,也就是 SR-IOV,他理論上可以切分 vNIC,相當於讓 VM 可以直接拿到網卡,在 User Space 上操作,但是 SR-IOV 設定非常複雜,也需要主機板有好的 IOMMU 分組,跳過。
再來講 Macvtap ,Macvtap 可以掛在實體網卡上、透過 rx_handler 在 L2 攔截收發,並且在實體網路上有獨立的 MAC Address,但也因為 Macvtap 的架構和特性,導致 Host 和 Guest 不能互相網路通訊,雖然有辦法解決,但不在這裡的討論範圍。
最後一個最簡單,那就是直接把防火牆關了,最省事,但是這跟大部分人臨時要玩 Minecraft 互相多人連線,發現連不通就把防火牆關了一樣,治標不治本,CachyOS 也是我難得看到預設開啟防火牆的 Distro,我並不想辜負如此漂亮的實作,即使我家的威脅模型會因此威脅到我電腦的幾率不高。
所以這三種方式都有共同的點,那就是跳過正確設定防火牆的步驟,忽略掉實際的正確流程,前兩個是跳過防火牆擋流量,最後一個是直接門戶大開。
對於想讓 Virt-manager 的網路功能全部正常運作,那只是治標不治本。
接下來我來說明怎麼交叉測試證明是防火牆的問題,以及如何正確的設定規則
ufw 規則測試
這裡測試 ufw 的方式很簡單,打開系統設定,用 KDE Plasma 的防火牆界面去控制 INPUT 和啟動狀態就好,大致會有三種狀況:
- Default ufw settings
- Input allowed
- Disable firewall
第一種情況不用多解釋,收不到 DHCP,IP 也出不去,如果收的到 DHCP,網路也出得去就不會有這篇文章了,在 Windows 上打開 CMD 輸入 ipconfig 你會看到 APIPA 生成的 169 開頭的 IP 位址。
第二種我把預設的 Input DROP 改成 Input allow 後,可以看到他能收到 IP,DHCP 功能正常,但是看到 Windows VM 右下角還是斷網狀態

第三種我直接把防火牆關閉後,不止可以拿到 IP ,還可以自由訪問 Internet,所以是防火牆設定引起 NAT 問題是實錘了

如何設定防火牆
既然要維持安全性的前提下讓 Default Network 可以正常運作,那就需要正確設定防火牆。
以我的能力,其實最理想的方法叫做 uninstall ufw,甚至 iptables 也可以刪掉,因為 Libvirt 的 firewall 後端也可以走 nftables ,我現在也沒安裝 Docker ,根本沒套件依賴 iptables。
但是 ufw 畢竟是 CachyOS 裝的,直接卸載這樣 plasma-firewall 會失效,除非裝回來或者換 firewalld。
雖然聽說 firewalld 跟 Virt-manager 搭配得很好,但是只要設定得當那用哪個工具都無所謂,我也不想破壞系統完整性,所以我會基於 CachyOS 預設規則下設定。
首先,我要說的是接下來的手段 plasma-firewall 都無法設定,所以我會直接操控底下的 ufw 甚至是 nftables。
Input 設定
這裡除了預設把 Input DROP 以外,Allow Input 後 Guest 依舊無法訪問網際網路,所以還有一個 Forward 是被拒絕或者 DROP 的,在 plasma-firewall 把設定還原後,用 ufw 驗證猜想:
~
❯ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip
恩沒有錯,除了 Input 以外,Forward 也要處理
iptables 和 plasma-firewall 看到的 Input / Output / Forward 分別對應 ufw 的 incoming / outgoing / routed
那我們首先讓 virbr0 的 Host 和 Guest 之間的連線可以正常通訊,這裡有兩條路線:
- Host 和 Guest 暢通無阻。
- Guest 只能有限的訪問 Host 的服務。
你可能會問這有什麼差?有的,但這取決於你的 VM 是做什麼的,假如你是要做高危資安實驗,但又需要聯網,那顯然方向二可以最大程度的避免橫向,畢竟你不能保証就算是在 Windows 感染的病毒,若具備橫向感染的能力不會橫向到 Linux 甚至 MacOS。
只是這種高危情況如果可以在執行病毒時不連網,那我更建議在開始執行前,直接掛到沒有設定 IP 的 Bridge 界面,或者直接在 Virt-manager 把 VM 的網卡 Shutdown。
如果你只是想讓 Guest 可以正常跟 Host 通訊,那直接允許 virbr0 的流量就可以:
sudo ufw allow in on virbr0
但如果你比較注重資安,或者有更高安全性需求,想以比較嚴格的形式來設定防火牆,就看你 Host 本身有哪些服務並且要讓 Guest 能訪問,以 DHCP 為例:
sudo ufw allow in on virbr0 to any port 67 proto udp
如果你還有其他服務假設在 Host 上,可以用 ss -tulpn 來看你開的服務對應什麼 Port,比如 KDE Connect 是 TCP 和 UDP 的 1716:
~
❯ ss -tulpn | grep kde
udp UNCONN 0 0 *:1716 *:* users:(("kdeconnectd",pid=1885,fd=18))
tcp LISTEN 0 50 *:1716 *:* users:(("kdeconnectd",pid=1885,fd=19))
但如果是 Guest 有架設服務,然後 Host 要連線與訪問,那不需要設定防火牆,原因後面細講。
如果你想要更嚴格的 Input 和 Output 嚴格掌握,Output 也走 Default Drop,因為不是預設設定,加上設定會變得很繁瑣,所以這裡不討論。
Forward 設定
因為 NAT 機制依賴 Forward ,前面解決了 Host 和 Guest 的連線問題,所以接下來也要來解決 Forward 的機制。
一樣,如果你只是想快點讓 Guest 可以訪問實體網路,你只需要 All forward 就好,比 Input 寬鬆一點,畢竟 Input 全開基本等於關防火牆,Forward 能影響的範圍比較有限。
這時候直接修改 /etc/default/ufw,找到 DEFAULT_FORWARD_POLICY="DROP" 改成 DEFAULT_FORWARD_POLICY="ACCEPT" 然後 sudo ufw reload 即可。
我果然還是想嚴謹一點!
也是,現在新電腦只要是中高階的主機板都是雙網卡 (Ethernet + WiFi),而且實際流量不只有網卡和 VM ,可能還會有 VPN 之類的,我也認為這才是最佳實踐。
雖然 Virt-manager 有說 default 是使用 NAT 的界面,但如果不是網路專業的應該很難理解 NAT 的機制和在做什麼,給沒背景的人快速說明一下。
NAT 在 Router 上,會有兩個界面,一個是連接內部網路,一個是連接外部網路的界面,內部網路會設定 Default gateway 指向到 Router,把對外網的請求送給 Router 轉送。
NAT 會做的事情就是轉送的時候把來自內部的封包改成「由 Router 請求」的封包,在實作上就是會改動封包的 Source IP,實作細節不展開。
所以我們現在要區分「內網網卡」和「外網網卡」這件事。
在 Virt-manager 上,Guest 的網卡選擇虛擬網卡 default 後,Guest 的網卡會連到 Host 的 virbr0 (virbr0 會在 default 運作後自動開啟),所以可以確定內網網卡就是 virbr0。
那外網網卡呢?這裡假設你不開 VPN ,那你的電腦怎麼上網的?沒錯,就是那些實體網卡,有 VPN 情況會比較複雜,各家 VPN 界面也不一樣,這裡不展開。
所以我們要做的事情就是要允許從 virbr0 轉發到這些實體網卡的連線,怎麼做?我的主機板有一張 RTL8125 (Ethernet) 和 MT7925 (WiFi) 網卡,那我假設我兩個界面都會使用 (我大多時候只用有線網路),那規則要新增兩條。
但在此之前,我們要先確認我們的網卡在 Linux 上的名稱,用 ip a 就可以得到:
~
❯ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: enp9s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 10:ff:e0:6f:90:90 brd ff:ff:ff:ff:ff:ff
altname enx10ffe06f9090
inet 192.168.***.***/24 brd 192.168.***.255 scope global dynamic noprefixroute enp9s0
valid_lft 594681sec preferred_lft 594681sec
inet6 2***7/64 scope global dynamic noprefixroute
valid_lft 1209597sec preferred_lft 604797sec
inet6 2***a2/128 scope global dynamic noprefixroute
valid_lft 1175226sec preferred_lft 570426sec
inet6 fe80::***5/64 scope link noprefixroute
valid_lft forever preferred_lft forever
3: wlan0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether e2:0b:1d:17:e4:fb brd ff:ff:ff:ff:ff:ff permaddr 3c:0a:f3:65:1a:a3
沒錯,分別是 enp9s0 和 wlan0 (無線網卡基本都是 wlanN),那就依序允許 virbr0 -> enp9s0 和 virbr0 -> wlan0 的流量
sudo ufw route allow in on virbr0 out on enp9s0
sudo ufw route allow in on virbr0 out on wlan0
這裡 in 就是指封包進入的網卡,out 就是出去的網卡。
如果你只是想讓 Guest 可以正常上網,做到這步就可以了,原因跟前面 Input 不用特別設定允許 Host 訪問 Guest 服務就能通一樣。
我會在為什麼 ufw 在 Input 和 Forward 預設 Drop 的情況下可以上網?這段說明原因。
那你甚至想要在有 NAT 的情況下,把服務開在 VM 上並且讓實體網路的設備可以訪問,你會需要額外設定允許從外網進入內網的流量,從寬來說直接把 virbr0 和實體網卡在指令上的位置對調即可
# 以 enp9s0 為例
sudo ufw route allow in on enp9s0 out on virbr0
但 Forward 真正可以嚴謹的地方也在這裡,因為你可以限定哪些服務的請求可以進來, 語法就是上面的語法後面加上 proto $PROTO to any port $PORT
以 Web Server 為例子,Protocol 走的是 TCP,Port 預設走 80 和 443
# HTTP
sudo ufw route allow in on enp9s0 out on virbr0 to any port 80
# HTTPS
sudo ufw route allow in on enp9s0 out on virbr0 to any port 443
如果你還想進一步限制 IP 就是多一個 from $ADDR 或 from $SUBNET,這裡不再舉例。
補充:因為是 NAT 的關係,Guest IP 在網路上會變成 Host 的 IP,所以還要做 Port-forward 才可以公開服務,這篇文章以 Guest 使用 default 網路可以上網為主,Port-forward 這裡不討論。
需要注意的是,如果你有做 bridge,並且類似 PVE 那種,Guest 的 NIC 橋接 vmbr0 就可以上網,那你會有一個實體網卡會變成沒有 IP 並且接上 bridge 界面。
如果那個網卡是你的主要上網界面,那你需要把規則上,那些實體網卡改成 bridge 界面,以我的網卡和 PVE 的 vmbr0 為例,就是把 enp9s0 換成 vmbr0 ,以上面單純開放 virbr0 -> enp9s0 為例:
sudo ufw route allow in on virbr0 out on vmbr0
查看規則
設定完後就可以查看規則,查看 ufw 規則的指令如下:
# 顯示 ufw 當前運做的詳細狀態
sudo ufw status verbose
# 顯示 ufw 以設定的規則 + 編號,方便調整規則
sudo ufw status numbered
那我是以相對嚴格的設定,可以讓 Guest 正常上網,但也沒有額外做其他服務允許以及開放服務,我這套規則屬於最低上網要求:
~
❯ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip
To Action From
-- ------ ----
67/udp on virbr0 ALLOW IN Anywhere
53/udp on virbr0 ALLOW IN Anywhere
67/udp (v6) on virbr0 ALLOW IN Anywhere (v6)
53/udp (v6) on virbr0 ALLOW IN Anywhere (v6)
Anywhere on enp9s0 ALLOW FWD Anywhere on virbr0
Anywhere (v6) on enp9s0 ALLOW FWD Anywhere (v6) on virbr0
對應的規則:
# Input
sudo ufw allow in on virbr0 to any port 67 proto udp
sudo ufw allow in on virbr0 to any port 53 proto udp
# Forward
sudo ufw route allow in on virbr0 out on enp9s0
因為 dnsmasq 會開啟 DHCP 和 DNS 服務 (在 Windows ipconfig 可以看到 DNS Server 的 IP 是 virbr0 的 IP 位址),所以要允許 DHCP 和 DNS 服務。
隨著 DNS 的發展,現在有些情況 DNS 會走 53 TCP ,如果想避免特殊情況沒有 DNS 服務,可以多 Allow 一個 53 tcp。
這裡附上 VM 順利上網的截圖:

然後現在你知道 sudo ufw status verbose 可以看到詳細的 ufw 規則了,我那時候還不熟 ufw,所以看到這裡恭喜你,你的除錯路徑應該可以少一步。
為什麼 ufw 在 Input 和 Forward 預設 Drop 的情況下可以上網?
你可能會覺得很奇怪,為什麼 Host 明明預設 Input drop ,但是我們用 CachyOS 別說 Guest 開服務,Host 能正常訪問,甚至不影響我們訪問網際網路?
前面 Input 我也說過,如果是 Host 訪問 Guest 的服務,我們不用特別設定防火牆的規則,這是跟剛剛訪問網際網路是同一個問題。
我們知道 CachyOS 預設 Firewall 規則是只有 Output allow 對吧?這解釋了流量能出去的問題,但還沒解釋 Input Drop 不影響的問題。
答案就在 /etc/ufw/before.rules 裡面,我們看他的內容,裡面有底下這段:
# quickly process packets for which we already have a connection
-A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-output -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-forward -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
這是很標準的 iptables-save 輸出格式,ufw 預設允許所有 Established (已建立連線)和 Related (有關聯連線,訪問是一個 Port ,傳輸是另外一個 Port,FTP 就是典型例子)
這裡要說 Netfilter 的第三個連線狀態了,也就是 New (新連線),這東西最粗暴的說,就是建立服務的第一個封包,一旦 Server 接收到這個封包,就會回傳另外一個封包,這時回復用的封包就可以算 Established 。
所以這就是為什麼 ufw 在 Default: deny (incoming), allow (outgoing), deny (routed) 的情況下,完全不影響你訪問網路,一旦你要開始用 KDE Connect 這類會監聽 Port 的程式,或者用 Virt-manager 才會出事情。
不過你如果一上來就能看懂這個規則,那其實你也不需要 ufw ;
那如果你是還不會這種底層防火牆的人,我的建議是 iptables 非必要不要碰,你真的要學,從 nftables 開始學,他才是後繼者。
只是這 5 年無論 Docker 還是 ufw,一堆套件還是依賴 iptables,砍 iptables 最激進的發行版是 Debian,大概 5 年前推出的 Debian Bullseye (11) 就不再預裝 iptables,需要手動安裝或者有依賴才會被裝回來。
補充:Debian 當時也有意把 firewalld 推成預設的 firewall wrapper,原因之一是 libvirt 這類 daemon 已經原生整合 firewalld,剛好呼應前面提到 firewalld 跟 Virt-manager 很搭。