我為 Proxmox VE 寫了一個 Port Forward Manager:從一個 IPv4、nftables 到開機恢復

原本只是覺得每次替 VM 新增 Port Forwarding 都要去改 nftables,實在有點麻煩,所以想寫一個自己用的 Web Panel。結果做到後來,花最多時間的不是前端,而是怎麼讓規則可以被安全地套用、確認、回滾,以及在整台 PVE 重開機之後恢復。

這篇文章想完整記錄 PVE Port Forward Manager 從想法、架構到實際踩坑的過程。它不是「一鍵搞定所有 PVE 網路問題」的工具,目前也還在 Beta Candidate 階段;但我覺得開發過程中碰到的問題,值得留下一份紀錄。

一、為什麼想寫這個?

我平常會在 Proxmox VE 上開各種 VM 和 LXC Container,跑 Web 服務、實驗環境,以及一些自己需要的服務。這些機器大多在私人網段裡,但我手上只有一個主要用來對外提供服務的 IPv4。

當我想讓外面連到某台 VM 的 SSH,或把一個 Web 服務開放出去時,就需要在 Host 設定 DNAT/Port Forwarding。單獨寫一條 nftables 規則其實不難,真正麻煩的是日後維護:哪個外部 Port 對應哪台 VM?TCP 和 UDP 有沒有都加?規則是不是已經載入 Kernel?上次測試留下的設定還在不在?

有時候改完之後看起來沒問題,過一陣子或重開機卻又得回來排查。每次都靠翻指令、找檔案,對自己的 Homelab 來說未免太累。

我當時也想過直接另外架一台 OpenWrt、OPNsense 或 pfSense 當閘道器。這當然是很成熟的做法,但那意味著要重新規劃流量要經過哪台機器、VM 的 Gateway,以及額外的管理和故障點。我當時不想把整個 PVE 網路重新設計成路由器專案,只想把既有 Host 上那幾條轉發規則管理好。

所以第一個想法很單純:能不能讓我在瀏覽器裡看到所有 Port Forward 規則,新增或修改後先預覽,再由程式去處理 nftables?

但很快我就想到另一個有點尷尬的問題:會用 nftables 的人可能覺得手動寫就好;不會用的人,又未必知道網段、Bridge、Gateway 應該怎麼填。 因此這個專案後來的重點,不只是「有 GUI」,而是盡量把高風險操作變成看得懂、能確認、出錯時有恢復機會的流程。

PVE Port Forward Manager Dashboard(匿名化的 Mock 示範畫面)

圖:PVE Port Forward Manager 管理首頁,使用本地 Mock 與示例網路資料產生,不是正式 PVE 環境的操作截圖。查看 GitHub 原圖。

二、我想解決的問題,以及不想做的事情

PVE Port Forward Manager 主要針對「一個對外 IPv4、Host 後面有多台私人網段 VM/CT」這種比較明確的情境。

我希望它能集中管理 TCP/UDP 單一 Port 的轉發、檢查設定、呈現實際 Host 狀態,並讓套用規則前後都有清楚的操作步驟。比起管理幾十種網路功能,我更想先把這件事情做好。

相對地,我沒有打算讓它管理 DHCP、DNS、VM 生命週期、VPN、整台 PVE Firewall,或是自動幫使用者修改 Bridge 與路由。那些都是不同層次的問題。把所有功能堆進去,未必會讓這個小工具更安全。

三、架構怎麼設計?為什麼不直接在 PVE Host 跑 Web?

最初它比較像一個前端小工具,後來才逐漸加入 FastAPI、Host Helper、狀態管理和測試。

目前大致分成四層:

flowchart TB
    B["Browser / 管理者"]
    subgraph LXC["Unprivileged LXC"]
        UI["Web UI"] --> API["FastAPI"]
    end
    SSH["Restricted SSH<br/>固定指令 / JSON"]
    subgraph HOST["Proxmox VE Host"]
        H["Root-owned Host Helper"]
        N["Kernel nftables<br/>table ip pve_portfw"]
        H -.->|驗證與套用規則| N
    end
    B -->|SSH Tunnel / 私人通道| UI
    API --> SSH
    SSH --> H
    C["外部 TCP / UDP 封包"] --> N
    N -->|DNAT / Forward| VM["私人網段 VM / CT"]

管理者透過 LXC 裡的 Web UI 與 FastAPI 呼叫 Host Helper;虛線表示 Helper 驗證並修改 nftables 規則。實際 DNAT 封包由 PVE Host 的 Kernel 轉送到 VM/CT,不經過 Web UI、FastAPI 或 Host Helper。 圖表超出螢幕寬度時,可以在圖內左右滑動。

我刻意不把整個 Web Panel 以 Root 權限放在 Host 上。前端和 API 放在 Unprivileged LXC;真正有權限修改 Host nftables 的,是額外安裝、由 Root 擁有的 Helper。

CT 透過專用 SSH Key 和受限制的命令去呼叫 Helper,而且新的 Helper Protocol v2 接收的是結構化規則,不是從 Web 端傳進來的一整份任意 nft Script。Host 會重新驗證資料和設定,再自行產生 nftables 規則。

從專案目錄也能看出這個分工:app/main.py 提供 FastAPI 路由、app/apply.py 管理 CT 端的操作狀態、app/static/ 是原生 JavaScript 與 CSS 介面;portfw/policy.py 統一規則的驗證與產生方式,而 portfw/host.py 負責 Host 端的實際 nftables 交易、Snapshot、Rollback 和 Recovery。scripts/ 則放安裝器與受限制的 Helper 入口。

我沒有另外導入大型前端框架。這個工具的 UI 範圍不算大,直接用 HTML、CSS 和 JavaScript,對我來說比較容易追蹤每個按鈕到底呼叫了哪個 API;相對地,前端狀態變多之後,也得更小心處理草稿、確認狀態和非同步更新。

這種切法確實讓安裝比單一 Bash 腳本複雜,但我認為管理介面與最高權限的防火牆操作之間,應該有清楚的界線。

另外,程式只管理自己的 **table ip pve_portfw**,不會執行 nft flush ruleset 去清除 Host 全部規則。這可以縮小它修改的範圍,但不代表能保證不受其他 nftables Chain、PVE Firewall 或上游防火牆影響。

一條規則實際上在做什麼?

舉一個與我實際環境無關、使用文件示例位址的例子:

1
2
3
4
5
External: 203.0.113.10:2222/TCP
│
DNAT on PVE
│
Internal: 10.42.0.100:22/TCP

Web Panel 儲存的是「外部 Port、協定、目的 VM IP、目的 Port」這些資料,再由 Host Helper 轉換成 nftables 所需要的規則。下面只是概念性的單條 DNAT 範例,並不是完整可直接套用的 nftables Table:

1
2
# 範例位址,非我的實際環境
iifname "vmbr0" ip daddr 203.0.113.10 tcp dport 2222 dnat to 10.42.0.100:22

實際產生的 Table 還會有 prerouting、postrouting、forward 等 Chain。程式也會替設定的內部網段產生 outbound masquerade;這不只作用於單一轉發目標,因此不能把它誤認成只修改某條 DNAT。

要特別注意:規則成功產生,不代表從外網一定連得到 VM。 VM 裡有沒有服務在監聽、回程 Gateway 是否正確、其他防火牆會不會 Drop,都還需要另外檢查。

四、真正麻煩的不是新增規則,而是「現在到底是哪個狀態」

當第一版可以增刪規則之後,我開始遇到一些比 UI 難處理的問題。

例如:按下 Save,只代表 JSON 已經儲存;Dry Run 通過,只代表設定與語法檢查成功;按下 Apply,才是要求 Host 真正修改 Kernel 裡的 Table。如果中途 SSH 斷線,前端甚至不一定知道 Host 有沒有成功。

檢查早期程式碼時,也發現 Dry Run 與待確認資料可能混在一起、Confirm 可能讀到 Apply 之後才修改的草稿,以及 Rollback 失敗卻不能準確表達狀態等問題。

因此新版把操作流程整理成:

flowchart TB
    D["Draft / 編輯草稿"] --> S["Save / 儲存設定"]
    S --> T["Dry Run / 驗證設定與語法"]
    T --> A["Apply / 套用到 Host"]
    A --> P["Pending / 等待確認"]
    P -->|期限內確認| C["Confirm / 驗證剛套用的版本"]
    C --> F["保存 Confirmed Snapshot"]
    P -->|手動回滾| R["Rollback / 嘗試還原先前 Snapshot"]
    P -->|逾時仍未確認 / Host Watchdog| R

其中有三個我覺得特別重要的設計。

第一,Confirm 的是剛剛「真正套用的版本」,不是目前編輯器裡的草稿。 使用者可能按下 Apply 後又修改了文字或規則,如果拿最新草稿當 Confirmed Snapshot,下一次回滾或開機恢復就可能拿錯設定。

第二,Host 必須自己確認規則是否存在且符合預期。 這次使用 nftables 的 JSON 輸出計算規則 Fingerprint,再區分 Saved、Pending、Confirmed 和目前的 Active State。這讓 Dashboard 不會因為資料庫裡有三條規則,就直接宣稱 Host 正在轉發三條。

第三,回滾不能只靠瀏覽器保持開啟。 新版將未確認 Apply 的 Deadline 和 Snapshot 保存在 Host,由 systemd Watchdog 負責檢查。即使 CT、Browser 或 SSH 連線發生問題,也不應讓回滾完全依賴 Web Session。

目前設定的確認期限是 120 秒;這是回滾設計的目標時間,不是任何負載與故障下都能保證的硬即時期限。實機的完整計時與故障情境仍有待繼續驗證。

五、最有感的一次踩坑:Host 重開後規則不見了

最近把新版部署到自己的 PVE 後,我先測試了一般 Apply/Confirm,也測試了 VM 重開機。這些看起來都正常。

結果我重開的是整台 PVE Host,問題就來了。

當時 PVE 管理介面還能存取,外部和內部 Bridge 也都顯示 UP,但我在 Host 上執行:

1
nft list table ip pve_portfw

得到的卻是:

1
Error: No such file or directory

我最初還懷疑是不是 VM、CT 或 Bridge 啟動需要時間。但再檢查 Host Helper,發現一件有意思的事:

1
2
3
4
5
confirmedRules   = 有資料
confirmedNft = 有資料
tablePresent = false
tableVerified = false
recoveryEnabled = false

也就是說,磁碟上保存了最後確認的規則,但 Kernel 裡根本沒有相對應的 nftables Table。

當時 Watchdog 也曾回報 No expired apply.。後來才真正弄清楚:Watchdog 是檢查尚未 Confirm 的交易有沒有逾時;它不是每次開機都會替你恢復最後確認規則的服務。

新版其實已經做了獨立的 pve-portfw-recover.service,但為了避免安裝時擅自修改正式 Host 的網路,它被設計成 Opt-in,不會在安裝或 Confirm 時自動啟用。

所以這一次確定的問題,不是 nftables Renderer 寫壞了,而是我部署後沒有啟用既有的 Boot Recovery Service。這也提醒我,功能「程式已經寫好」跟「正式環境已經啟用」完全是兩回事。

後來我先備份,確認保存的 Snapshot 與設定一致,檢查 Recovery 對現有相符 Table 不會重複修改,才手動啟用它。再次由我自己安排 Host Reboot 後,當次開機的服務日誌出現:

1
Restored verified last confirmed table.

Host Helper 也回報 Table 存在且驗證成功,三條預期的 TCP/UDP 規則重新出現。這證明在我的環境裡,至少有一次完整 Host Reboot 成功恢復了最後確認的 nftables Table。

但我不會把這個結果直接等同於所有外部 TCP/UDP 連線、各種 PVE Firewall 重載順序,以及其他 PVE 版本都已經驗證過。這些是接下來還需要測的東西。

六、既然已經有人寫類似工具,為什麼還要做?

我後來有去看其他人的做法。其實 PVE 的 Port Forwarding 並不是沒有人處理過;反而有不少選擇,而且某些功能還比我這個專案完整。

直接寫 nftables/iptables

這是最自由、依賴也最少的方式。熟悉 Linux 網路的人可以直接把規則與 systemd/持久化設定管理好,沒有必要為了幾條 NAT 再架一個 Web Panel。

但對我而言,困擾的不是「不知道怎麼寫 DNAT」,而是規則數量增加之後,希望能更方便地知道哪條規則正在運作、哪次修改尚未確認,以及修改出錯時要怎麼恢復。

PVE NAT 管理腳本

例如 GitHub 上的 odup/pve-nat-port-forward,提供 CLI 選單、規則管理、衝突偵測、來源 IP 白名單等功能,還把保留客戶端原始 IP 作為設計重點。

另外 eurosatofficial/proxmox-nat-router 更著重在讓 PVE Host 承擔 Router 的角色,支援 Hairpin NAT、持久化規則與 Default-deny Forwarding Policy。

這些工具各有適合的環境。我這個專案目前沒有來源 IP 白名單,也沒有 Hairpin NAT,並不能說在功能完整度上勝過它們。

我的差異主要是使用情境與操作流程:我想要的是獨立的瀏覽器管理介面、和 PVE Host 隔離的權限架構,以及對 Apply/Confirm/Rollback/Confirmed Snapshot 清楚可見的狀態管理。

OPNsense、pfSense 這類完整防火牆

OPNsense 和 pfSense 當然提供成熟的 NAT/Port Forward 管理介面,能處理的網路需求也比我這個工具廣得多。

如果本來就需要獨立路由器、完整 Firewall Policy、VPN 或更複雜的網路分區,我會優先考慮它們,而不是讓 PVE Port Forward Manager 硬做不屬於自己範圍的事情。

我的專案適合的是另一種情況:已經有一套正在使用的 PVE Host 網路架構,只需要少量、可追蹤、可回復的 IPv4 Port Forwarding 管理流程。

所以我比較願意說這是一個「定位不同的小工具」,而不是取代所有 NAT 管理方式的新方案。

七、目前還有哪些問題?

這次新版確實改善了不少地方,但我也不想把尚未做好的事情藏起來。

首先,安裝仍然有門檻。使用者要理解 Unprivileged LXC、Host Helper、受限制的 SSH Key、Host Key Pinning、Bridge 與 Gateway。對熟悉 PVE 的人來說可以接受,但對第一次玩 Homelab 的人未必友善。

第二,Web Panel 目前沒有獨立登入或 RBAC。因此它不能直接暴露在公網;現階段應透過 SSH Tunnel 或已經做好身分驗證的私人存取路徑使用。這也是為什麼我不會把它包裝成可以隨便開 Port 給所有人用的管理平台。

第三,支援範圍有限。目前主要處理 IPv4、TCP/UDP 與單一 Port,還沒有 IPv6 NAT、Port Range、Hairpin NAT 或進階來源限制。程式也不會自動改 VM 的 Gateway、DHCP 或整台 PVE 的路由。

第四,測試還不夠全面。專案目前紀錄的測試包含 Windows/WSL 的 pytest、獨立 Network Namespace 裡的真實 nft 檢查,以及桌面、手機版的 Browser Workflow。GitHub CI 也已經跑過 Python 3.11/3.12/3.14 和瀏覽器測試。

不過,這些仍不能代替不同 PVE 版本上的全新安裝、真實外網流量、完整 120 秒 Watchdog、重開機途中遭遇 Firewall 重載等實機驗證。尤其是新安裝 CT 的 systemd/權限流程,我還想再做完整的 Smoke Test。

八、接下來打算更新什麼?

我想先把重點放在可靠性和安裝體驗,而不是一直增加新功能。

短期會優先測全新 Unprivileged CT 安裝、Host/CT 升級相容性,並改善文件與 Preflight,讓第一次使用的人至少知道自己卡在哪一步。也想在 Dashboard 清楚提醒 Boot Recovery 尚未啟用,而不是等人真的重開機才發現。

中期會安排更完整的 Rollback/Reboot 測試,包含 CT 沒啟動、Apply 尚未 Confirm 就重開、Bridge 比預期晚啟動,以及其他防火牆服務重新載入的情況。備份留存與版本鎖定也還有改善空間。

長期才考慮 IPv6、Port Range、Hairpin NAT、來源 IP 限制等功能。這些目前都只是方向,不是已經完成或保證會推出的功能。我希望它一直維持「小而聚焦」,而不是變成第二套路由器管理平台。

九、如果你也在用 PVE,想邀請你幫我試試看

目前 PVE Port Forward Manager 還是 Beta Candidate,不是正式穩定版。我的實際環境能使用,不代表每一種 Bridge、PVE Firewall、VM Gateway 或 Host 版本都適用。

如果你剛好也在用 Proxmox VE,尤其是只有一個對外 IPv4、後面有幾台私人網段 VM/CT 的情況,很歡迎你先看看程式碼與安裝文件。有閒置的測試環境、能保留 Host Console 存取方式的話,也很希望你幫我試試。

我最想知道的不是單純「跑得起來」,而是你覺得哪個步驟難懂、哪個畫面不知道要按什麼、哪些錯誤訊息沒有幫助,以及你原本是怎麼管理 Port Forwarding 的。

遇到問題可以到 GitHub Issues 告訴我:使用的 PVE 版本、網路架構概要(請匿名化)、操作步驟、預期和實際結果,以及去識別化後的錯誤紀錄。請不要貼真實 Public IP、管理介面位址、SSH 私鑰或未遮蔽的設定檔。

也歡迎直接跟我說:「這個設計不合理」「這裡其實有更好的做法」「我根本不會想用這個」。對一個還在改善中的開源專案來說,這種回饋可能比只收到一顆 Star 更有幫助。

最後:做得出來,和做得讓人放心,是兩回事

回頭看,這個專案一開始真的只是想省去自己反覆編輯 NAT 規則的時間。後來才慢慢理解,真正困難的不是讓網頁能修改 Port,而是規則套用後能不能被正確驗證、操作中斷後怎麼辦、Host 重開又能不能恢復。

這次也有使用 AI 協助開發、整理程式和補測試;但需求怎麼定義、架構如何取捨,以及正式 PVE Host 上哪些動作能做、哪些動作不能亂做,最後仍然需要自己判斷和驗證。

現在它還有很多地方可以改,我也不覺得一個小工具需要假裝自己已經什麼都會。至少這次我從一個單純的 GUI 想法,真正碰到了 Linux 權限隔離、nftables 狀態、systemd 啟動與失敗恢復這些問題。

如果之後真的有人願意把它裝在自己的測試環境裡,並提出我沒有想到的問題,對我來說就是這個開源專案很有價值的下一步。

專案: GitHub — PVE Port Forward Manager
目前公開程式碼: main 分支
回報問題/提出建議: GitHub Issues


我為 Proxmox VE 寫了一個 Port Forward Manager:從一個 IPv4、nftables 到開機恢復
https://blog.fishbaby1011.com/post/pve-port-forward-manager/
作者
fishbaby1011
發布於
2026年10月12日
許可協議