docs: add peer deployment ops guide

Document the operational patterns established while adding CT100 and
CT101 as peers via Docker, plus incidental fixes:

- Docker compose template for headless peer containers
- One-off setup key naming convention and API creation
- Peer rename via API PUT (works, unlike setup keys)
- Mac daemon kickstart via 'sudo launchctl kickstart -k system/netbird'
  (service name is 'netbird', not 'io.netbird.client')
- UPNP auto-port-mapping discovery in NetBird client
- CT100 PermitRootLogin drop-in override (first-match-wins sshd quirk)
This commit is contained in:
2026-04-18 15:22:33 +08:00
parent c5a8757da4
commit aaece30ca6
3 changed files with 254 additions and 1 deletions

249
peer-deployment-ops.md Normal file
View File

@@ -0,0 +1,249 @@
# 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`
### 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
```
## 意外發現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"
```