Files
netbird-selfhosted/peer-deployment-ops.md
Timmy cb1b5f4bf2 docs: NetBird management DNS flap leaves daemon stuck (CT124 case)
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>
2026-04-18 23:26:02 +08:00

391 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 不會中(它們是 userspaceCT124 會中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
```
目前本部署未啟用自動 watchdogpeer 數量還在可人工巡檢的範圍。
## 意外發現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"
```