visionA/docs/autoflow/04-architecture/tunnel-ws-network-requirements.md
jim800121chen dbe5d6a78e docs(architecture): WS tunnel 網路/防火牆需求文件
整理 WS tunnel 要通需要的網路設定(正式環境交接用):
- 核心:邊界反代開 WebSocket upgrade 轉發(或 L4 TCP passthrough),
  現在剝 WS upgrade header → tunnel 400
- 配套:idle timeout≥90s、wss TLS、DNS、上線前移除 demo 繞法
- Port 清單:唯一對公網開 :9527,:3800/:3801 絕不對外
- 含 HAProxy/ALB/Cloudflare/F5 等效設定 + 驗證方法 + 回傳碼對照

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 03:27:45 +08:00

21 KiB
Raw Permalink Blame History

Tunnel WebSocket 網路 / 防火牆 / 反代設定需求

文件目的:讓「公網使用者的 visionA Agent 能建立 tunnel WebSocket 連線」,需要在網路層、防火牆、反向代理上做哪些設定。 適用對象:網管 / 維運 / 部署交接人員(不需先懂 tunnel 內部協定)。 本文件與 tunnel.md 的分工tunnel.md 講 tunnel 內部協定yamux / session / 訊息格式本文件只講「WS 連線要能穿過網路各層並成功握手,每一層要開/設什麼」。 最後更新2026-07整段連線排查確認後整理供正式上線 + 交接用)


0. TL;DR給趕時間的人

正式環境要讓 tunnel 通,最關鍵的一件事

公網入口那台「邊界反向代理」必須顯式轉發 WebSocket upgradeConnection: Upgrade + Upgrade: websocket),並用 HTTP/1.1。 目前它沒設,會剝掉 upgrade header導致後端收到殘缺握手回 400 bad handshake。這是 tunnel 建不起來的唯一根因。

其餘四件事:

  1. 防火牆:放行 agent 出站 → 公網入口 :9527wss/TLS
  2. Idle timeout:邊界反代 / LB 的 idle timeout 拉長到 ≥ 90stunnel 是長連線,靠 10s 心跳維持)。
  3. TLSagent 走 wss://公網入口憑證需有效Let's Encrypt 已有)。
  4. DNSstage-9527.innovedus.com 解析到公網入口 1.34.63.223

1. WebSocket tunnel 的網路本質(先建立共識)

1.1 為什麼 tunnel 用 WebSocket

Agent 跑在使用者的內網電腦、沒有公網 IP雲端無法「主動連進去」。所以改由 agent 主動撥一條長連線出來,之後雲端所有要送給 agent 的請求都走這條反向通道。這條長連線用 WebSocket 承載(底層再跑 yamux 多工),因為 WebSocket

  • 走標準 443/HTTP(S) port穿透企業防火牆的成功率最高比自訂 TCP port 好穿);
  • 是雙向全雙工,雲端能主動往 agent 推資料;
  • 能被大多數反代 / LB 理解。

1.2 為什麼 WebSocket 對反代設定「特別敏感」

WebSocket 連線是從一個**普通 HTTP 請求「升級upgrade」**而來的。這個升級靠兩個 HTTP header

Connection: Upgrade
Upgrade: websocket

這兩個是 hop-by-hop header——依 HTTP 規範,它們只對「當前這一跳」有效,每一層反向代理都必須「主動、顯式」把它們再往下轉發。反代若沒特別設定,預設行為就是把它們吃掉不轉

結果:

agent 送Connection: Upgrade / Upgrade: websocket
   → 邊界反代沒設 WS 轉發 → 把這兩個 header 剝掉
   → 後端 remote-proxy 收到一個「沒有 upgrade 意圖」的普通 HTTP 請求
   → 無法完成 WebSocket 握手 → 回 400 Bad Requestbad handshake

這就是本專案排查到的根本卡點。實測鐵證:同一個 WS 握手agent 直連內網 192.168.0.130:9527 成功回 101 Switching Protocols;一旦改走公網邊界反代,就變 400。差別只在「中間那層有沒有轉發 upgrade header」。

白話總結:tunnel 建不起來 99% 是某一層反代把 upgrade header 吃掉了。 每多一層反代,就要多確認一次那層有沒有開 WS 轉發。

1.3 為什麼要強制 HTTP/1.1(不能 HTTP/2

Connection 這個 header 在 HTTP/2 是被禁止的h2 用不同的多工機制,沒有 hop-by-hop header 概念)。如果 agent 跟反代之間 TLS 協商ALPN談成了 h2Connection: Upgrade 這套 RFC 6455 的 WebSocket 握手方式根本無法運作。

因此:

  • Agent 端已修正TLS 握手強制 ALPN 只提供 http/1.1,避免被協商成 h2。
  • 反代端要求:面向 backend 轉發時用 proxy_http_version 1.1nginx或等效設定不要用 h2 轉發 WS。

注意:這裡指的是「承載 WebSocket 那一跳」要用 h1.1。TLS 本身照常wss = WebSocket over TLS


2. 完整連線鏈路與每一跳的要求

2.1 架構拓樸

┌─────────────┐   wss (TLS, h1.1)   ┌──────────────────────────┐   http (內網)   ┌────────────────────────┐
│ visionA     │  WS upgrade         │ 邊界反向代理(公網入口)  │  轉發           │ container 內 nginx      │
│ Agent       │ ──────────────────► │ stage-9527.innovedus.com │ ──────────────► │ 192.168.0.130:9527     │
│ (桌面程式)  │  :9527              │ 1.34.63.223:9527         │                 │ /tunnel/connect        │
│ gorilla/ws  │                     │  · LE HTTPS termination  │                 │ (Host 白名單 + WS 轉發) │
└─────────────┘                     │  · NAT → 內網 130        │                 └───────────┬────────────┘
                                     │  ⚠ 目前剝掉 WS upgrade   │                             │ http (loopback)
                                     └──────────────────────────┘                             ▼
                                        ↑ 本專案碰不到、非 repo 內                  ┌────────────────────────┐
                                        ↑ 需網管在此開 WS 轉發                      │ remote-proxy :3800     │
                                                                                    │ (tunnel WS server,     │
                                                                                    │  yamux, 10s keepalive) │
                                                                                    └────────────────────────┘

2.2 逐跳要求

從 → 到 協定 這一跳要能過,需要什麼
Agent → 邊界反代 wss (TLS+h1.1) 防火牆放行 agent 出站到 :9527DNS 解析正確;憑證有效
邊界反代 → container nginx httpNAT/內網) ⚠ 最關鍵:邊界反代必須顯式轉發 Upgrade/Connection header + 用 h1.1 + 長 read timeout。目前缺這段 → 400。
container nginx → remote-proxy httploopback 已正確設定(/tunnel/connect location 已含 WS 轉發,見 §3.2。Host 需命中白名單。
remote-proxy 內部 yamux over WS 已就緒10s 心跳 / 30s 判定掉線)。

每一跳都要「WS 通」,只要任何一層剝掉 upgrade header整條就斷。 ② 是目前唯一未達標的一跳。


3. 正式環境要做什麼(重點,給網管 / 交接)

3.1 邊界反向代理(公網入口那台,最關鍵

這台做 Let's Encrypt HTTPS termination + NAT 轉發到內網 192.168.0.130。它目前是 L7 HTTP proxy 且沒設 WS 轉發,所以剝掉 upgrade header。要修二選一

選項 A推薦在邊界反代加 WebSocket upgrade 轉發

如果這台是 nginx,針對 /tunnel/connect(或整個 domain

# http contextserver block 外)先定義 upgrade map
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 9527 ssl;
    server_name stage-9527.innovedus.com;
    # ... LE 憑證設定 ...

    location /tunnel/connect {
        proxy_pass http://192.168.0.130:9527;   # NAT 到內網 container nginx

        # ── WebSocket 轉發三要素(缺一不可)──
        proxy_http_version 1.1;                  # ① 必須 h1.1h2 不支援 hop-by-hop header
        proxy_set_header Upgrade    $http_upgrade;      # ② 顯式轉發 upgrade header
        proxy_set_header Connection $connection_upgrade; # ③ 顯式轉發 connection header

        # ── 長連線 timeouttunnel 是長連線,靠心跳維持)──
        proxy_read_timeout  86400s;
        proxy_send_timeout  86400s;

        # ── 不要 buffer WS 訊框 ──
        proxy_buffering off;

        proxy_set_header Host            $host;
        proxy_set_header X-Real-IP       $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

驗收:改完後,從外部對 wss://stage-9527.innovedus.com:9527/tunnel/connect 發 WS 握手應回 101(見 §5

選項 B如果那台不是 nginx可能是防火牆 / LB / WAF

不同設備的等效設定名稱:

設備類型 要開的等效設定 備註
HAProxy mode http + 預設即支援 WSh1.1);或直接 mode tcp 做 L4 passthrough 確認 timeout tunnel 拉長(如 timeout tunnel 1h
AWS ALB / GCP HTTPS LB 原生支援 WebSocket但要確認 idle timeout 拉長ALB 預設 60s Target group 用 HTTP/1.1
Cloudflare / CDN 開啟 WebSockets帳號設定內有開關避免走會剝 header 的 transform rule 免費/付費方案都支援 WS
F5 / Nginx Plus / 商用 WAF 需開「WebSocket profile / passthrough」部分 WAF 預設攔 upgrade 若 WAF 檢測 body長連線可能被誤殺
通用防火牆 / NAT 若只是 L4 轉發TCP passthrough天生支援 WS(它不看 HTTP header只要把 :9527 TCP 轉到內網即可 最單純、最不會出錯的做法

最保險做法:若邊界那台能做 L4 / TCP passthrough(純轉 TCP、不解 HTTPWebSocket 天生就通,因為它根本不碰 HTTP header。代價是 HTTPS termination 要往內移(改由 container 那層或另設)。是否採用取決於現有架構,需與網管確認。

3.2 Container 內 nginx已就緒僅供交接核對

Repo 內 docker/nginx.stage.conf/tunnel/connect 已正確設定,交接時只需確認未被改壞:

map $http_upgrade $connection_upgrade {   # http context
    default upgrade;
    ''      close;
}

location /tunnel/connect {
    proxy_pass http://visiona_tunnel;      # → 127.0.0.1:3800 (remote-proxy)
    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_read_timeout  86400s;
    proxy_send_timeout  86400s;
    proxy_buffering     off;
    proxy_set_header Host $host;
    # ...
}

Host 白名單注意:此 nginx 只接受 Host: stage-9527.innovedus.com;不符的 Host含直接打 IP一律 444(直接關連線)。這是 trust boundary 防護。若邊界反代轉發時改寫或遺漏 Host會被打 444——邊界反代必須帶正確的 Host header 下來。

3.3 防火牆 / Port 放行清單

Port 協定 方向 開在哪 用途 必要性
9527 TCP / TLS(wss) agent 出站 → 公網入口入站 邊界防火牆(公網側) agent 建 tunnel 的唯一對外入口 必須
9527 TCP 邊界反代 → 內網 130 內網防火牆 NAT 轉發到 container nginx 必須(內部)
3800 TCP container nginx → remote-proxy container loopback tunnel WS server面向 agent 側) 內部(同 container無需對外
3801 TCP api-server → remote-proxy 內部 remote-proxy internal HTTP面向 api-server 側) 內部(絕不可對外
3721 TCP nginx → api-server container loopback api-server 內部
3000 TCP nginx → frontend container loopback Next.js frontend 內部

關鍵放行只有一條:讓使用者端 agent 能出站連到 公網入口 :9527TCP/wss。其餘都在雲端內部 / container loopback不對公網開放。

⚠️ 安全提醒:3800tunnel WS server:3801remote-proxy internal無認證的 HTTP 轉發端點)絕對不能直接暴露到公網。它們只該從 container loopback / 內網受信任來源存取。對外一律只走 :9527 經 nginx。

3.4 Idle timeout長連線存活

Tunnel 是長連線,靠 yamux 每 10s 送一次心跳維持(連續 3 次未回 = 30s 判定掉線)。中間任何一層若 idle timeout 太短,會在心跳間隔間把連線掐斷。

建議 idle / read timeout 理由
邊界反代 / LB ≥ 90s(建議直接設 3600s 或 86400s 要大於心跳間隔且留安全邊際;雲端 LB 常見預設 60s 偏短
container nginx 86400s已設 長連線;心跳由 yamux 處理nginx 不該中途斷
remote-proxy session idle VISIONA_TUNNEL_IDLE_TIMEOUT,預設 30s 這是「判定對端失聯」的時間(= 3 次心跳未回。demo 曾調 24h 避免誤斷,但正式環境建議保留 30s 預設(配合 10s 心跳,這是正確的掉線判定值),不要盲目拉長,否則殭屍 session 不會被清

釐清:VISIONA_TUNNEL_IDLE_TIMEOUT=30s 是「多久沒心跳就判死」,不是「多久沒 API 請求就斷 tunnel」。tunnel 只要心跳正常就永遠在線。demo 當時調成 24h 是為了繞過另一個問題(心跳未被中間層正確轉發時的誤斷),正式環境把中間層 WS 轉發修對後,應回到 30s 預設

3.5 HTTPS / TLS

  • Agent 走 wss://WebSocket over TLS。公網入口的憑證Let's Encrypt必須有效、未過期、CN/SAN 涵蓋 stage-9527.innovedus.com
  • Agent 端不應在正式環境用 InsecureSkipVerify(略過憑證驗證)——那只允許 dev 自簽憑證時使用。
  • TLS termination 在公網入口那台LE 憑證container nginx 收到的是純 HTTP內網這是正常的X-Forwarded-Proto: https 已由 nginx 補回給 backend

3.6 DNS

stage-9527.innovedus.com 必須解析到公網入口 1.34.63.223。agent 的連線 URLtunnel endpoint指向此 domain。若走內網繞法見附錄則直接用 IP不經 DNS。


4. 各層設定 Checklist交接核對表

要做什麼 為什麼 誰負責 狀態
邊界反向代理(公網入口) 針對 /tunnel/connect(或整 domainWebSocket upgrade 轉發h1.1 + Upgrade/Connection header + read timeout ≥ 90s + buffering off或改 L4 TCP passthrough 這層目前剝掉 upgrade header → 400是 tunnel 建不起來的唯一根因 網管(此設備非 repo 內) 待做(最優先)
防火牆 / Port 放行 agent 出站 → 公網入口 :9527TCP/wss確認邊界反代 → 內網 130:9527 內部轉發通;:3800/:3801 不對外 agent 唯一對外入口;內部端點防護 網管 需確認
Idle timeout 邊界反代 / LB idle timeout 拉長到 ≥ 90s建議 3600s+ tunnel 長連線靠 10s 心跳維持,太短會誤斷 網管 需確認
HTTPS / TLS 公網入口 LE 憑證有效、涵蓋 domainagent 走 wss、正式不 skip verify wss 安全;憑證錯 agent 連不上 網管 / 部署 LE 已有,需核對
DNS stage-9527.innovedus.com1.34.63.223 agent 找得到入口 網管 需確認
container nginx 確認 /tunnel/connect 的 WS 轉發設定未被改壞Host 白名單接受 stage-9527...;正式上線移除 demo 用的 IP 直連 server block或加 allow 內網網段; deny all; 這層已就緒demo 繞法不可留在正式 部署 已就緒demo block 待移除)
remote-proxy 確認 :3800 tunnel port、VISIONA_TUNNEL_IDLE_TIMEOUT 回到 30s 預設 心跳/掉線判定正確 部署 idle timeout 需從 demo 的 24h 改回)
agent 端 強制 ALPN http/1.1(已修);連線 URL 指向公網 domain正式憑證不 skip verify 避免被協商成 h2 導致 WS 握手失敗 已修code 已修

5. 驗證方法(怎麼確認每一層 WS 通)

WebSocket 握手成功的標誌是 HTTP 101 Switching Protocols。用以下方式逐層測,快速定位是哪一跳斷的。

5.1 用 curl 測 WS 握手(看回傳 code

# 對「內網 container nginx」直測繞過邊界反代— 預期 101
curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  -H "Host: stage-9527.innovedus.com" \
  http://192.168.0.130:9527/tunnel/connect

# 對「公網邊界反代」測(正式路徑)— 修好前是 400修好後應為 101
curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  https://stage-9527.innovedus.com:9527/tunnel/connect

5.2 回傳 code 對照表(快速定位斷點)

收到 意義 該查哪一層
101 Switching Protocols 握手成功WS 通 這一層 OK
400 Bad Requestbad handshake upgrade header 被中間層剝掉 → 後端收到殘缺握手 邊界反代沒開 WS 轉發(本專案根因);或中間某層吃掉 header
444(連線直接被關、無回應) Host 白名單擋掉 反代沒帶正確 Host: stage-9527.innovedus.com 下來,命中 container nginx 的 default_server → 444
502 / 504 到達 nginx 但上游 remote-proxy 不通 / timeout remote-proxy:3800)沒起,或 read timeout 太短
連線逾時 / 拒絕 網路層不通 防火牆沒放行 :9527,或 DNS 解錯

5.3 用 wscat 做完整 WS 連線測試(更真實)

# npm i -g wscat
wscat -c "wss://stage-9527.innovedus.com:9527/tunnel/connect?token=<測試 token>"
# 成功:連上不立即斷;失敗:印出 400 / handshake 錯誤

5.4 用 openssl 檢查 TLS / ALPN排 h2 誤協商)

openssl s_client -connect stage-9527.innovedus.com:9527 -alpn http/1.1 -servername stage-9527.innovedus.com </dev/null 2>/dev/null | grep -i 'ALPN\|Verify'
# 確認 ALPN 談成 http/1.1、憑證 Verify return code: 0 (ok)

5.5 逐層排查建議順序

  1. 先測 §5.1 內網直連130→ 若 101證明 container nginx + remote-proxy 都 OK問題純在邊界反代。
  2. 再測 §5.1 公網邊界反代 → 若 400就是邊界反代沒轉 WS照 §3.1 修)。
  3. 若 444 → 檢查邊界反代有沒有把 Host 改成 / 帶成 stage-9527.innovedus.com
  4. 若逾時 → 回頭查防火牆 §3.3 與 DNS §3.6。

附錄 Ademo 環境的臨時繞法(正式不可用

排查期間為了讓 tunnel 先 online 做 demo採用「agent 直連內網、繞過邊界反代」的臨時 hack。記錄於此供理解正式上線一律不可沿用

A.1 demo 怎麼繞通的

繞法 做了什麼 為什麼能通
agent 直連內網 agent 連 ws://192.168.0.130:9527(不走公網 :9527 邊界反代) 直接打 container nginx跳過「會剝 header 的邊界反代」
nginx 加 IP server block docker/nginx.stage.confserver_name 192.168.0.130 的 block含完整 WS 轉發) agent 用 IP 當 Host 時能過白名單(否則打 444
VISIONA_RELAY_PUBLIC_URL 改內網 pairing exchange 回給 agent 的 relay URL 改成內網 ws://192.168.0.130:9527/... 讓 agent 拿到內網 URL 去連
VISIONA_TUNNEL_IDLE_TIMEOUT 改 24h 拉長掉線判定 繞過 demo 當時中間層心跳未正確轉發導致的誤斷

A.2 為什麼正式不能用

  • 只在同一內網可行192.168.0.130 是私有 IP公網使用者的 agent 根本連不到。
  • 明文 http無 TLSdemo 走 ws://(非 wsstunnel 流量未加密,正式環境不可接受。
  • 繞過 trust boundaryIP 直連 server block 是刻意開的後門,正式上線前必須移除,或改成 allow <內網網段>; deny all; 限制來源。
  • idle timeout 24h 掩蓋問題:正式環境把中間層 WS 轉發修對後,應回到 30s 預設,讓殭屍 session 能被正確清理。

正式上線正解:修好 §3.1 的邊界反代 WS 轉發,讓公網使用者的 agent 能經 wss://stage-9527.innovedus.com:9527/tunnel/connect 正常建立 tunnel然後移除本附錄的所有 demo 繞法。


附錄 B相關檔案 / 設定索引

項目 位置
container nginx 設定(含 /tunnel/connect WS 轉發 + demo IP block docker/nginx.stage.conf
tunnel 內部協定 / session 管理 / yamux 心跳參數 docs/autoflow/04-architecture/tunnel.md
tunnel port / idle timeout 等環境變數 visionA-backend/internal/config/config.goload.goVISIONA_TUNNEL_PORT=3800VISIONA_PROXY_INTERNAL_PORT=3801VISIONA_TUNNEL_IDLE_TIMEOUT=30sVISIONA_RELAY_PUBLIC_URL
stage 部署設定 docs/autoflow/04-architecture/stage-deployment.mddocs/autoflow/07-delivery/stage-deployment-setup.md