Files
netbird-selfhosted/peer-deployment-ops.md
timmy dc070bdff8 docs: add Tailscale kernel-mode iptables-legacy collision note
Discovered while deploying NetBird on CT124 (which already runs
Tailscale in kernel mode). Tailscale's ts-input/ts-forward chains
drop all 100.64.0.0/10 traffic not coming from tailscale0, and since
NetBird uses 100.71.0.0/16 (within that CGNAT range), its wt0
traffic gets silently dropped.

Captured:
- Symptom recognition (Connected in status but 100% ping loss)
- Distinguishing when it hits (kernel-mode Tailscale vs userspace)
- Fix: iptables-legacy -I INPUT/FORWARD allow rules on wt0
- Persistence: systemd oneshot + /usr/local/sbin script
  (iptables-persistent doesn't cover the legacy table)
2026-04-18 16:29:54 +08:00

303 lines
9.7 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
```
## 意外發現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"
```