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>
391 lines
12 KiB
Markdown
391 lines
12 KiB
Markdown
# 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
|
||
|
||
```bash
|
||
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
|
||
|
||
```yaml
|
||
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 的網路介面來做 ICE
|
||
- `cap_add: [NET_ADMIN, SYS_RESOURCE]` — 建 WireGuard 介面必要
|
||
- `NB_HOSTNAME` — peer 註冊後的顯示名稱(日後可用 API 再改)
|
||
- Watchtower label — 讓 Watchtower 自動跟 upstream 更新
|
||
|
||
#### 3. 啟動
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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 都自動同步 | — |
|
||
|
||
### 用法
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
sudo launchctl kickstart -k system/netbird
|
||
```
|
||
|
||
⚠️ 服務名稱是 **`netbird`**,**不是** `io.netbird.client`、`com.netbird.daemon` 等常見猜測。
|
||
確認方式:
|
||
|
||
```bash
|
||
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`。
|
||
|
||
### 重啟後驗證
|
||
|
||
```bash
|
||
netbird status
|
||
# Management: Connected
|
||
# Peers count: N/N Connected ← 應該是 N/N 而非 0/0
|
||
```
|
||
|
||
## 其他平台
|
||
|
||
Linux systemd:
|
||
|
||
```bash
|
||
sudo systemctl restart netbird
|
||
```
|
||
|
||
Windows:
|
||
```
|
||
Restart-Service -Name "Netbird"
|
||
```
|
||
|
||
Docker 容器(本文部署的方式):
|
||
```bash
|
||
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」
|
||
|
||
### 辨識方式(黃金一行)
|
||
|
||
```bash
|
||
docker exec netbird netbird status | grep -E "^Management:|^Signal:"
|
||
```
|
||
|
||
正常:
|
||
|
||
```
|
||
Management: Connected
|
||
Signal: Connected
|
||
```
|
||
|
||
壞掉:
|
||
|
||
```
|
||
Management: Disconnected, reason: ...
|
||
Signal: Connected
|
||
```
|
||
|
||
### 修復
|
||
|
||
直接重啟 daemon / 容器:
|
||
|
||
```bash
|
||
# 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 分鐘掃一次:
|
||
|
||
```bash
|
||
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`。
|
||
|
||
### 診斷
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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 繞過:
|
||
|
||
```bash
|
||
ssh root@192.168.42.39
|
||
pct exec 100 -- bash -c "echo 'root:25915525' | chpasswd"
|
||
```
|