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

12 KiB
Raw Permalink Blame History

Peer 部署與維運操作

記錄新增 peer、改名、daemon 維運等實際操作的 know-how。

在 LXC/VM 裡透過 Docker 部署 NetBird 客戶端

適合 headless 伺服器(沒有 GUI、沒有真人 SSO 登入流程)。本 NetBird 部署已在 CT100CT101 用此方法上線。

前置

  • 目標主機已裝 Docker
  • 有可用的 PATsetup-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-JumpBoxCT101-RustDeskDashboard 上一眼看得出這把 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 的網路介面來做 ICE
  • cap_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: ConnectedRelays: 2/2 AvailablePeers count: N/N Connected

同主機已跑 Tailscale kernel-mode 的衝突

症狀NetBird 容器跑起來、status 顯示 ConnectedPeers 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 pstailscale 容器,且不是以 --tun=userspace-networking 模式運行,iptables-legacy -L ts-input 看得到 DROP 規則。CT100/CT101 不會中(它們是 userspaceCT124 會中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 設定(不只 namessh_enabled 等欄位若不帶會用預設值覆蓋。先 GET 看看這個 peer 現況,再帶相同值 + 新的 name。

命名慣例

改名後的 dns_label 會是 <name>.netbird.selfhosted,建議:

  • 全小寫、英數與連字號、不用空格/中文
  • 語意清楚:mbp-13-proiphone-15-proct100ct101
  • 避免 .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.clientcom.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

目前本部署未啟用自動 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

診斷

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-passwordyesOpenSSH 是 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

不需重啟 sshdCT100 用 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"