Deploying a Lost and Found Management Website Using PVE for Business Computer Networks
用一台舊筆電打造校園 PVE 實驗平台:Tailscale 無固定 IP 存取、NAT 容器網路與防火牆隔離#
情境:實驗室想用一台筆電架 Proxmox VE,放在學校有線網路,讓多位同學各自擁有 LXC 容器做專題(前端、後端、資料庫)。學校沒有提供固定對外 IP,也不希望校內其他人連到管理介面。本文整理整個建置流程與踩過的坑。
整體架構#
同學電腦 (Tailscale)
│ 加密 VPN(NAT 穿透)
▼
┌───────────────────────────────────────────┐
│ PVE 主機(筆電,校園網路 vmbr0) │
│ - tailscale0:subnet router │
│ - PVE 防火牆:只放行 Tailscale │
│ - vmbr1 10.10.10.1/24 ── NAT ──► 校園網路 │
└───────────────┬───────────────────────────┘
│ vmbr1(內部網段 10.10.10.0/24)
┌──────────┼──────────┐
CT 110 CT 111 CT 112 ...
DB BE FE
重點設計:
- PVE 不對外開放,只能透過 Tailscale 連入,校內其他 IP 一律擋掉。
- 容器放在內部 NAT 網段
vmbr1,由 PVE 負責上網,不佔用學校的 IP。 - 每位同學一個 Tailscale tag,只能連到自己的容器。
- 容器之間再用 PVE 防火牆依角色隔離(FE → BE → DB)。
1. 安裝 PVE 與網路基礎#
PVE 9 基於 Debian 13。安裝後第一步是處理套件來源:沒有訂閱時,企業版 repo 會回 401,要移除並改用 pve-no-subscription。
# 移除 enterprise 來源後,加入 no-subscription
echo "deb http://download.proxmox.com/debian/pve trixie pve-no-subscription" \
> /etc/apt/sources.list.d/pve-no-subscription.list
apt update
幾個常見的小坑:
- 網頁介面要用
https://IP:8006,用http://只會看到Moved Permanently。 - USB 網卡若一直斷線(dmesg 出現
-71錯誤),換 USB 孔或關閉 autosuspend。 - PVE 不適合直接用 WiFi 當橋接網路,建議用有線或 USB 有線轉接。
2. 無固定 IP 的遠端存取:Tailscale#
PVE 在校園網路裡本來就有 IP,但:
- 這個 IP 可能被 DHCP 更換;
- 校內其他人也看得到;
- 沒有對外固定 IP,校外無法直連。
解法是讓 PVE 主動向外連線。Tailscale 從 PVE 往外連到協調伺服器,建立 VPN,不需要學校開任何 port。
curl -fsSL https://tailscale.com/install.sh | sh
# 開啟 IP 轉發(subnet router 需要)
echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-tailscale.conf
sysctl -p /etc/sysctl.d/99-tailscale.conf
# 宣告內部容器網段
tailscale up --advertise-routes=10.10.10.0/24
同學登入同一個 Tailnet 後,用 PVE 的 Tailscale 位址(100.x.y.z)連線:
https://100.x.y.z:8006
不管 PVE 的校園 IP 怎麼變,同學連的都是 Tailscale 位址,不受影響。
注意事項#
- 在 Tailscale 管理後台關閉 PVE 節點的 key expiry,否則金鑰過期後大家會突然連不上。
- 重開機後確認
tailscaled有自動啟動(systemctl enable --now tailscaled)。
3. 內部 NAT 網段 vmbr1#
在 PVE 上新增一個沒有實體網卡的橋接 vmbr1,容器接在這上面,再由 PVE 做 NAT 出去。
/etc/network/interfaces:
auto vmbr0
iface vmbr0 inet static
address 140.115.87.208/24 # 依你的環境修改
gateway 140.115.87.254
bridge-ports enx9cebe8185114 # 你的實體網卡
bridge-stp off
bridge-fd 0
auto vmbr1
iface vmbr1 inet static
address 10.10.10.1/24
bridge-ports none
bridge-stp off
bridge-fd 0
post-up echo 1 > /proc/sys/net/ipv4/ip_forward
post-up nft -f /etc/pve-nat.nft
post-up iptables -t raw -C PREROUTING -i fwbr+ -j CT --zone 1 2>/dev/null || iptables -t raw -I PREROUTING -i fwbr+ -j CT --zone 1
post-up iptables -t mangle -C PREROUTING -i fwbr+ -j MARK --set-xmark 0x0/0xff0000 2>/dev/null || iptables -t mangle -I PREROUTING -i fwbr+ -j MARK --set-xmark 0x0/0xff0000
post-down iptables -t mangle -D PREROUTING -i fwbr+ -j MARK --set-xmark 0x0/0xff0000
post-down iptables -t raw -D PREROUTING -i fwbr+ -j CT --zone 1
post-down nft delete table ip pvenat
#NAT internal
/etc/pve-nat.nft(這裡用 nftables,不是 iptables):
table ip pvenat
delete table ip pvenat
table ip pvenat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.10.10.0/24 oifname "vmbr0" masquerade
}
}
post-up用-C ... || -I的寫法是為了讓規則可重複執行,避免ifreload之後規則重複堆疊。
建立容器時,網路設定為 Bridge: vmbr1、IP 例如 10.10.10.110/24、Gateway 10.10.10.1,DNS 填學校 DNS 或 1.1.1.1。
4. 擋掉校園其他 IP:PVE 防火牆#
重點在「PVE 會自動把本機所在網段加入 management ipset」,所以只寫「允許 management」不夠,校內同網段的人也會被放行。必須明確 DROP 校園網段,且放在規則最上方。
/etc/pve/firewall/cluster.fw:
[OPTIONS]
enable: 1
policy_in: DROP
policy_out: ACCEPT
policy_forward: ACCEPT
[IPSET management]
100.x.y.z # 管理者的 Tailscale 位址
[RULES]
IN DROP -source 140.115.87.0/24 -p tcp -dport 22,3128,8006,5900:5999,60000:60050
IN DROP -source 140.115.87.0/24 -p icmp
IN Ping(ACCEPT) -source +management
IN Ping(ACCEPT) -i vmbr1
IN ACCEPT -i tailscale0 -p tcp -dport 8006
驗證方式:用校園 IP 連 https://PVE校園IP:8006 應逾時;用 Tailscale 位址則可正常登入。
此外,vmbr1 那條規則建議使用 Ping(ACCEPT) 而不是全放行,否則容器可以連到 PVE 主機的 22、8006。
5. 每位同學只能連自己的容器:Tailscale ACL#
為什麼不用 user 規則#
被標上 tag 的裝置沒有使用者身分,所以依 email 寫的規則對它們無效。因此改用「每位同學一個 tag」的方式。
Tailscale policy 的重點部分:
{
"tagOwners": {
"tag:bus-pve": ["autogroup:member"],
"tag:u110": ["autogroup:member"],
"tag:u111": ["autogroup:member"]
// ... 依同學數量增加
},
"autoApprovers": {
"routes": {
"10.10.10.0/24": ["tag:bus-pve"]
}
},
"grants": [
// 所有同學裝置:只能開 PVE 管理介面
{ "src": ["tag:bus-pve"], "dst": ["100.x.y.z"], "ip": ["tcp:8006"] },
// 每位同學:只能連自己的容器
{ "src": ["tag:u110"], "dst": ["10.10.10.110"], "ip": ["icmp:*", "tcp:*"] },
{ "src": ["tag:u111"], "dst": ["10.10.10.111"], "ip": ["icmp:*", "tcp:*"] }
]
}
要點:
- 同學的裝置同時帶
tag:bus-pve(能開 PVE 網頁)與自己的tag:u11x(能連自己的容器)。 tcp:*之後可以收斂成實際需要的 port。- Mac 或 Linux 客戶端需要接受路由,用
route -n get 10.10.10.110(macOS)確認走的是utun介面。
帳號權限#
網頁登入也要分權限,每位同學只能管理自己的容器:
pveum user add u110@pve
pveum passwd u110@pve
pveum acl modify /vms/110 --users u110@pve --roles PVEVMUser
再替管理者與重要帳號開啟 2FA,並使用強 root 密碼。
6. 踩坑紀錄:容器防火牆開啟後的三個問題#
容器防火牆預設是關閉的。要啟用「依角色隔離」,需要同時:
- 容器 → Firewall → Options:
Firewall = Yes - 容器網卡
net0:勾選 Firewall - 設定
policy_in = DROP
一旦勾選網卡的 Firewall,PVE 會在路徑上多插入 fwbr / fwpr / fwln 這組橋接,結果出現下面三個問題。
問題一:Tailscale 進來的 ping 沒有回應#
現象:網卡 firewall 關閉時能 ping 通,開啟後不通。tcpdump 在 vmbr1 看到來源是 10.10.10.1,但沒有回應封包。
原因:Tailscale 用 mark 決定哪些封包要做 MASQUERADE。封包進入 fwbr 橋接時,這個 mark 還在,MASQUERADE 又被觸發一次,來源被改成 Tailscale 位址,回程路徑就斷了。
修正:進入 fwbr+ 時清掉 mark:
iptables -t mangle -I PREROUTING -i fwbr+ -j MARK --set-xmark 0x0/0xff0000
問題二:容器開啟防火牆後無法上網#
現象:ping 8.8.8.8 失敗,但關掉網卡 firewall 就好了。
原因:NAT 與橋接防火牆各自追蹤連線,造成 conntrack 狀態不一致,回程封包被判定為 INVALID。
修正:這是 Proxmox wiki 記載的做法,替 fwbr+ 指定獨立的 conntrack zone:
iptables -t raw -I PREROUTING -i fwbr+ -j CT --zone 1
問題三:容器之間 ping 不通#
兩個原因疊在一起:
ICMP 規則的來源只允許
10.10.10.1,要改成10.10.10.0/24:IN Ping(ACCEPT) -source 10.10.10.0/24規則是由上往下比對。如果最上面已經有一條明確的
in DROP,後面新增的規則永遠不會被比對到。policy_in: DROP已經設定的話,這條明確 DROP 可以直接刪掉。
設定檔參考(每個容器)#
/etc/pve/firewall/110.fw:
[OPTIONS]
enable: 1
policy_in: DROP
policy_out: ACCEPT
[RULES]
IN ACCEPT -source 10.10.10.1 -p tcp -dport 22
IN Ping(ACCEPT) -source 10.10.10.0/24
IN ACCEPT -source 10.10.10.1 -p tcp -dport 3000
7. 依角色隔離(FE → BE → DB)#
目標:
| 容器 | 角色 | 允許來源 | 允許 port |
|---|---|---|---|
| .110 | DB | .111 | 5432 |
| .111 | BE | .112 | 8000 |
| .112 | FE | Tailscale 進來的使用者 | 前端服務 port |
以資料庫容器為例:
IN ACCEPT -source 10.10.10.111 -p tcp -dport 5432
再開啟 IP filter,防止容器偽造來源 IP。
連線到 PostgreSQL 的檢查清單#
如果從本機連容器裡的 PostgreSQL 失敗,逐層檢查:
服務監聽位址:
ss -tlnp | grep 5432,如果只監聽127.0.0.1,要把listen_addresses改成'*'。pg_hba.conf:經過 Tailscale subnet router 進來的來源會被 SNAT 成10.10.10.1(--snat-subnet-routes預設開啟),要允許這個位址。host all all 10.10.10.1/32 scram-sha-256容器防火牆:確認 5432 的 ACCEPT 規則在任何 DROP 之前。
Tailscale ACL:確認自己的 tag 有權限連該容器。
客戶端工具:我們遇過 Windows 版 pgAdmin 回報
access violation writing 0x0000000000000000。這是客戶端程式崩潰,不是網路問題。先用 PowerShell 驗證網路:Test-NetConnection 10.10.10.113 -Port 5432通了就是客戶端的問題,換工具或更新版本即可。
8. 服務很慢?先看 IO 壓力#
容器中的網站上線後,雖然可以連,但很慢。監控數字:CPU 2.5%、記憶體 35%、SWAP 53%、IO 壓力持續 90% 以上。
CPU 低、IO 壓力高,通常表示瓶頸在磁碟。可能的原因:
- 容器的記憶體上限太小,資料被換到 swap,而 swap 在慢速磁碟上;
- 主機本身 RAM 不足;
- 磁碟是 HDD 或 USB 儲存,隨機讀寫很慢;
- 應用本身 IO 很多,例如 dev server 的 file watcher、持續寫 log、資料庫與其他服務共用同一顆慢碟。
在 PVE 主機上診斷:
free -h
swapon --show
cat /proc/pressure/io /proc/pressure/memory
lsblk -d -o NAME,ROTA,SIZE,MODEL # ROTA=1 代表 HDD
pvesm status # 看容器 rootfs 放在哪個 storage
vmstat 1 10 # 看 si/so 與 wa
處理方向:
調高容器 Memory,並降低 Swap;
降低主機
vm.swappiness:sysctl vm.swappiness=10 echo 'vm.swappiness=10' > /etc/sysctl.d/99-swappiness.conf把容器 rootfs 搬到較快的 storage(Move Storage),或換 SSD;
前端用 production build,不要在容器裡長時間跑 dev server。
9. 對外公開網站:Cloudflare Tunnel#
上面的架構只有 Tailnet 內的人能連。如果要讓一般使用者透過網址瀏覽網站,可以用 Cloudflare Tunnel。原理和 Tailscale 類似,也是「由內往外」連線:
- 在前端容器(或專用容器)執行
cloudflared,它會主動連到 Cloudflare,建立加密長連線。 - 在 Cloudflare 設定「網址 → 內網服務」,例如
app.example.com→http://10.10.10.112:3000。 - 使用者連
app.example.com,DNS 指向 Cloudflare,請求先到 Cloudflare。 - Cloudflare 沿著那條已建立的連線把請求送進內網,回應原路回去。
好處是學校不用開 port、不需要固定 IP,還有 HTTPS 與基本防護。前提是要有自己的網域並託管在 Cloudflare。
總結#
| 需求 | 做法 |
|---|---|
| 沒有固定 IP 也能遠端管理 | Tailscale(PVE 主動向外連線) |
| 擋掉校內其他 IP | PVE 防火牆明確 DROP 校園網段,只放行 Tailscale |
| 容器不佔學校 IP | vmbr1 內部網段 + nftables NAT |
| 每人只能連自己的容器 | 每人一個 Tailscale tag + grants |
| 容器之間依角色隔離 | 容器防火牆 + IP filter |
| 對外公開網站 | Cloudflare Tunnel |
這次最大的收穫是:網路問題常常不是單一設定造成的,NAT、conntrack、橋接防火牆、VPN 的 mark 都可能互相影響。排錯時用 tcpdump 一層一層看封包到底停在哪裡,比憑猜測改規則有效得多。