Why: CT124 observed "Connecting" from iPhone for hours while CT124 self-reported Connected. Root cause: DNS timeout ~7h earlier broke management gRPC, daemon didn't auto-recover after DNS returned. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
Peer 部署與維運操作
記錄新增 peer、改名、daemon 維運等實際操作的 know-how。
在 LXC/VM 裡透過 Docker 部署 NetBird 客戶端
適合 headless 伺服器(沒有 GUI、沒有真人 SSO 登入流程)。本 NetBird 部署已在 CT100、CT101 用此方法上線。
前置
- 目標主機已裝 Docker
- 有可用的 PAT(見
setup-keys-and-pat.md) - 對目標 NetBird management server 可連線
步驟
1. 為這台機器建一把 one-off setup key
TOKEN=<你的 PAT>
curl -sf -X POST -H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "CT100-SSH-JumpBox",
"type": "one-off",
"expires_in": 86400,
"revoked": false,
"auto_groups": [],
"usage_limit": 1,
"ephemeral": false
}' \
https://netbird.timmy.us.kg/api/setup-keys | jq
命名慣例:<主機名>-<用途>(例 CT100-SSH-JumpBox、CT101-RustDesk),Dashboard 上一眼看得出這把 key 給誰。
type: one-off+usage_limit: 1— 用一次就失效,最小攻擊面expires_in: 86400— 24 小時內沒用就過期ephemeral: false— 此 peer 離線後不自動清除(伺服器要持續存在)
把回傳的 key 欄位(UUID)存起來,下一步要用。
2. 在目標主機寫 docker-compose.yml
services:
netbird:
image: netbirdio/netbird:latest
container_name: netbird
restart: unless-stopped
network_mode: host
cap_add:
- NET_ADMIN
- SYS_RESOURCE
environment:
- NB_SETUP_KEY=<上一步拿到的 key UUID>
- NB_MANAGEMENT_URL=https://netbird.timmy.us.kg:443
- NB_HOSTNAME=<主機名,例 CT100>
volumes:
- /opt/netbird/data:/var/lib/netbird
labels:
- com.centurylinklabs.watchtower.enable=true
放在 /opt/netbird/docker-compose.yml(跟 NetBird server 部署在 .127 的路徑一致,維運直覺)。
關鍵設定:
network_mode: host— NetBird 需要能直接看到 host 的網路介面來做 ICEcap_add: [NET_ADMIN, SYS_RESOURCE]— 建 WireGuard 介面必要NB_HOSTNAME— peer 註冊後的顯示名稱(日後可用 API 再改)- Watchtower label — 讓 Watchtower 自動跟 upstream 更新
3. 啟動
cd /opt/netbird
docker compose pull
docker compose up -d
# 驗證
sleep 10
docker exec netbird netbird status
正常輸出應該有 Management: Connected、Relays: 2/2 Available、Peers count: N/N Connected。
同主機已跑 Tailscale kernel-mode 的衝突
症狀:NetBird 容器跑起來、status 顯示 Connected、Peers count 正確,但互 ping 完全不通(100% loss)。
原因:Tailscale 的 kernel-mode 會在 iptables-legacy 加入兩條防禦規則:
-A ts-input -s 100.64.0.0/10 ! -i tailscale0 -j DROP
-A ts-forward -s 100.64.0.0/10 -o tailscale0 -j DROP
NetBird 的 100.64.0.0/10 介面 (wt0) 送進來的封包在 ts-input 被丟棄。iptables 與 iptables-legacy 是雙軌,兩邊都要過,NetBird 自己加的 nft 規則不影響 legacy 表。
辨識: 主機 docker ps 有 tailscale 容器,且不是以 --tun=userspace-networking 模式運行,iptables-legacy -L ts-input 看得到 DROP 規則。CT100/CT101 不會中(它們是 userspace),CT124 會中(kernel)。
修復:在 iptables-legacy 的 INPUT/FORWARD 鏈最前面插入 allow:
iptables-legacy -I INPUT 1 -i wt0 -j ACCEPT
iptables-legacy -I FORWARD 1 -i wt0 -j ACCEPT
iptables-legacy -I FORWARD 1 -o wt0 -j ACCEPT
持久化(iptables-persistent 只管 nft 那一軌不會存 legacy)— 用 systemd oneshot:
cat > /usr/local/sbin/netbird-iptables-legacy-fix.sh << 'EOF'
#!/bin/sh
iptables-legacy -C INPUT -i wt0 -j ACCEPT 2>/dev/null || iptables-legacy -I INPUT 1 -i wt0 -j ACCEPT
iptables-legacy -C FORWARD -i wt0 -j ACCEPT 2>/dev/null || iptables-legacy -I FORWARD 1 -i wt0 -j ACCEPT
iptables-legacy -C FORWARD -o wt0 -j ACCEPT 2>/dev/null || iptables-legacy -I FORWARD 1 -o wt0 -j ACCEPT
EOF
chmod +x /usr/local/sbin/netbird-iptables-legacy-fix.sh
cat > /etc/systemd/system/netbird-iptables-fix.service << 'EOF'
[Unit]
Description=Allow NetBird wt0 traffic past Tailscale iptables-legacy rules
After=network.target docker.service
Wants=docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/netbird-iptables-legacy-fix.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now netbird-iptables-fix.service
LXC 容器內的 TUN 問題
理論上:NetBird 需要 /dev/net/tun 來建立 WireGuard kernel 介面。
實際上:在非特權 LXC 容器(如 Proxmox 的 unprivileged: 1)/dev/net/tun 不存在,但 NetBird client 容器透過 network_mode: host + cap_add: NET_ADMIN 能跑起來,並顯示 Interface type: Kernel(推測是透過 host namespace 繞過)。
如果未來遇到 TUN 不可用的情境:
- 選項 A:在 PVE CT config 加
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file,optional 0 0(需重啟 CT) - 選項 B:切 userspace WireGuard(參考 Tailscale 的
--tun=userspace-networking作法,但 NetBird 沒有這個開關,需要手動 patch)
Peer 改名(API 做得到,setup key 做不到)
對比:
| 資源 | API PUT name |
其他方式 |
|---|---|---|
| setup_keys | 不生效(見 setup-keys-and-pat.md) |
改 store.db + 重啟 server |
| peers | 生效,連 FQDN / dns_label 都自動同步 | — |
用法
TOKEN=<你的 PAT>
# 列出 peer IDs
curl -sf -H "Authorization: Token $TOKEN" \
https://netbird.timmy.us.kg/api/peers | jq '.[] | {id, name, hostname, ip}'
# 改名
curl -X PUT -H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "mbp-13-pro",
"ssh_enabled": false,
"login_expiration_enabled": false,
"inactivity_expiration_enabled": false,
"approval_required": false
}' \
https://netbird.timmy.us.kg/api/peers/<PEER_ID>
注意:PUT 需要帶完整的 peer 設定(不只 name),ssh_enabled 等欄位若不帶會用預設值覆蓋。先 GET 看看這個 peer 現況,再帶相同值 + 新的 name。
命名慣例
改名後的 dns_label 會是 <name>.netbird.selfhosted,建議:
- 全小寫、英數與連字號、不用空格/中文
- 語意清楚:
mbp-13-pro、iphone-15-pro、ct100、ct101 - 避免
.local等 mDNS 後綴(macOS 預設 hostname 會自動帶,改掉)
Mac daemon 卡住:launchctl 重啟
症狀
netbird status 顯示 Management: Disconnected, reason: rpc error: context canceled 或類似 RPC 錯誤。Signal 可能還是 Connected,但 peers count 歸零。
UI 上 Disconnect → Connect 通常可以恢復,但 daemon 真的卡死時 UI 也不回應。
解法
直接重啟 launchd service:
sudo launchctl kickstart -k system/netbird
⚠️ 服務名稱是 netbird,不是 io.netbird.client、com.netbird.daemon 等常見猜測。
確認方式:
ls /Library/LaunchDaemons/ | grep -i netbird
# → netbird.plist
cat /Library/LaunchDaemons/netbird.plist | grep -A 1 '<key>Label</key>'
# → <string>netbird</string>
launchd 語法 system/<Label> 所以完整 domain 是 system/netbird。
重啟後驗證
netbird status
# Management: Connected
# Peers count: N/N Connected ← 應該是 N/N 而非 0/0
其他平台
Linux systemd:
sudo systemctl restart netbird
Windows:
Restart-Service -Name "Netbird"
Docker 容器(本文部署的方式):
docker restart netbird
Management 斷線後 daemon 不自癒(DNS flap)
症狀
本部署 CT124 曾發生:從 iPhone 看 CT124 一直顯示 Connecting,但 CT124 自己跑 netbird status 顯示 Peers count: 5/5 Connected。細看:
Management: Disconnected, reason: rpc error: code = DeadlineExceeded
desc = context deadline exceeded while waiting for connections to become ready
Signal: Connected
且其他 peer 雖然顯示 Connected,但 Last WireGuard handshake: -(從未握手)、Connection type: Relayed(沒做 ICE)— 表示只是殘留的舊狀態,沒有實際流量。
根因
容器 log 往上翻會看到類似這段:
grpc: addrConn.createTransport failed to connect to {Addr: "netbird.timmy.us.kg:443", ...}.
Err: transport: Error while dialing: nbnet.NewDialer().DialContext:
dial tcp: lookup netbird.timmy.us.kg: i/o timeout
NetBird daemon 啟動後跟 management server 是 gRPC 長連線,若期間 DNS 解析短暫失敗(OpenWrt dnsmasq 抖動、上游 DNS timeout 等),daemon 的重連邏輯不一定能自癒 — 在 0.68.3 觀察到 DNS 已恢復、其他連線都好了,但 management gRPC 一直卡在失敗狀態。
後果:
- 不會收 network map 更新 → 新加入的 peer 握不到手
- 既有 peer 顯示為 Connected 但實際是前一次 session 的陰影,handshake 永不更新
- Dashboard 上該 peer 看起來像「Connecting」
辨識方式(黃金一行)
docker exec netbird netbird status | grep -E "^Management:|^Signal:"
正常:
Management: Connected
Signal: Connected
壞掉:
Management: Disconnected, reason: ...
Signal: Connected
修復
直接重啟 daemon / 容器:
# Docker 部署
docker restart netbird
# Linux systemd
systemctl restart netbird
# macOS
sudo launchctl kickstart -k system/netbird
Signal 斷線通常會自動重連,但 Management 不一定 — 看到這個症狀直接重啟是最快的,不要試著 netbird down && netbird up,那不會重建 gRPC channel。
可選:自動 health check
若部署規模變大、手動檢查不實際,可以加一個 cron / systemd timer 每 5 分鐘掃一次:
cat > /usr/local/sbin/netbird-mgmt-watchdog.sh << 'EOF'
#!/bin/sh
if docker exec netbird netbird status 2>/dev/null | grep -q "^Management: Disconnected"; then
logger -t netbird-watchdog "Management disconnected, restarting container"
docker restart netbird
fi
EOF
chmod +x /usr/local/sbin/netbird-mgmt-watchdog.sh
# 加進 cron(每 5 分鐘)
echo "*/5 * * * * root /usr/local/sbin/netbird-mgmt-watchdog.sh" > /etc/cron.d/netbird-mgmt-watchdog
目前本部署未啟用自動 watchdog,peer 數量還在可人工巡檢的範圍。
意外發現:NetBird 內建 UPNP 自動打洞
觀察 CT101 的啟動日誌:
client/internal/portforward/manager.go:162: discovered NAT gateway: UPNP (IG2-IP2)
client/internal/portforward/manager.go:204: created port mapping: 51820 -> 10935 via UPNP (IG2-IP2) (external IP: 125.229.110.50)
NetBird client 啟動時會主動向 NAT gateway 發 UPNP/IGD 請求,自動 map 一個 public port 到自己的 51820。
好處:
- 不需要手動在路由器上加 DNAT 也能讓 peer 做 P2P inbound
- 等於 NetBird 自己處理 port forwarding
前提:
- 路由器支援 UPNP / NAT-PMP 且開啟
- OpenWrt 通常需裝
miniupnpd套件並在 firewall zone 允許
本部署的 OpenWrt 有啟用 UPNP,所以 CT101 才能自動打這個洞。如果未來發現某台 peer 一直走 Relay、查 log 沒看到 UPNP 訊息,先檢查閘道器有沒有開 UPNP。
CT100 部署時意外踩的雷
CT100 原本 root SSH 密碼登入完全被擋(Permission denied),不是 root 密碼錯,而是 sshd config 有個較早的 drop-in 設了 PermitRootLogin prohibit-password。
診斷
pct exec 100 -- sshd -T | grep -iE 'permitrootlogin|passwordauthentication'
# permitrootlogin without-password ← 擋 root 密碼登入
# passwordauthentication yes
grep -l -r PermitRootLogin /etc/ssh/ 只找到 /etc/ssh/sshd_config,但該檔內有兩個 PermitRootLogin 指令(先 prohibit-password 後 yes),OpenSSH 是 first-match-wins,所以前者有效。
修復
覆蓋寫一個更晚載入的 drop-in:
pct exec 100 -- bash -c 'echo "PermitRootLogin yes" > /etc/ssh/sshd_config.d/99-allow-root.conf'
# 驗證
pct exec 100 -- sshd -T | grep permitrootlogin
# permitrootlogin yes
不需重啟 sshd:CT100 用 ssh.socket 做 systemd socket activation,每個新連線都 fork 新 sshd 重讀設定。
密碼也要重設
CT100 原本的 root 密碼忘了,從 PVE host 繞過:
ssh root@192.168.42.39
pct exec 100 -- bash -c "echo 'root:25915525' | chpasswd"