iT邦幫忙

0

寫在 30 天走進 OpenClaw 之後:它曾經有兩套協定,為什麼現在只剩一套

  • 分享至 

  • xImage
  •  

開場故事

這一篇原本的題目是「兩種對話,兩種距離 —— WebSocket protocol 與 bridge protocol」。

我在 docs/gateway/ 底下看到兩個檔名:protocol.md 和 bridge-protocol.md,直覺就是「同一個系統有兩套協定,一定有原因,這題可以寫」。

然後我打開 bridge-protocol.md,第一段是一個警告框:

📄 文件:docs/gateway/bridge-protocol.md

The TCP bridge has been **removed**. Current OpenClaw builds do not ship the
bridge listener, and `bridge.*` config keys are no longer in the schema.
This page is historical reference only.

題目當場作廢。

我去原始碼確認了一下,確實乾淨:

$ ls src/gateway/server-bridge.ts
ls: src/gateway/server-bridge.ts: No such file or directory

$ ls src/gateway/ | grep -i bridge
(沒有輸出)

那份文件自己都說了,allowlist 的執行邏輯「lived in src/gateway/server-bridge.ts (removed)」。

但我決定還是寫這一篇,而且比原本的題目更值得寫。

因為一個被刪掉的東西,會留下一個很有價值的問題:

它當初為了解決什麼而存在?那些問題現在跑到哪裡去了?

這比「兩套協定有什麼不同」有意思多了。一個系統刪掉一整條傳輸層,代表它想通了某件事。

今天要解的問題

  • 那個 TCP bridge 當初是為了什麼而存在?
  • 它被刪掉之後,原本扛的責任落到誰身上?
  • 現在唯一那套 WebSocket 協定,handshake 到底在確認什麼?
  • 從兩套變一套,付出的代價是什麼?

架構總覽

先看 bridge 當初存在的理由。文件自己列了四條:

📄 文件:docs/gateway/bridge-protocol.md

## Why it existed

- **Security boundary**: exposed a small allowlist instead of the full gateway API surface.
- **Pairing + node identity**: node admission was owned by the gateway and tied to a per-node token.
- **Discovery UX**: nodes could discover gateways via Bonjour on LAN, or connect directly over a tailnet.
- **Loopback WS**: the full WS control plane stayed local unless tunneled via SSH.

這四條講的其實是同一件焦慮:

我不敢讓外面的裝置直接碰那個完整的 WS API。

上一篇講過,gateway 光 server-methods/ 就有 174 個檔案,等於一百多種操作。那是給 CLI、給 macOS app 用的完整控制面。

現在想像一支放在客廳的 Android 手機,當成 node 接進來。你會願意讓它拿到那一百多種操作嗎?

不會。所以早期的答案是:另外開一個門。

  • 走不同的傳輸(TCP JSONL,預設 port 18790,WS 走 18789)
  • 只開一小撮允許的方法
  • WS 控制面維持 loopback,除非你自己用 SSH 開隧道

兩個 port、兩套協定、兩種信任等級。用「距離」換「安全」。

那四條理由,現在各自去了哪裡

這是我覺得最有意思的部分。四個責任沒有消失,它們被搬到四個不同的地方。

一、安全邊界 → 搬進 handshake 的 role 和 scope

以前靠「你走哪個 port」決定你能做什麼。現在靠你在 handshake 時宣告自己是誰。

📄 文件:docs/gateway/protocol.md

{
  "type": "req",
  "method": "connect",
  "params": {
    "minProtocol": 4,
    "maxProtocol": 4,
    "client": { "id": "cli", "version": "1.2.3", "platform": "macos", "mode": "operator" },
    "role": "operator",
    "scopes": ["operator.read", "operator.write"],
    "caps": [],
    "commands": [],
    "permissions": {}
  }
}

role 和 scopes 取代了「哪個 port」。

而且注意 caps 和 commands 這兩個欄位——一個 node 連進來的時候,是自己申報「我有鏡頭、我能錄螢幕」。它不是拿到一份通用能力清單,而是宣告自己能提供什麼。

方向整個反過來了:以前是 gateway 決定「這個門後面能做什麼」,現在是連線者宣告「我是誰、我要什麼、我能提供什麼」,由 gateway 審。

二、配對與裝置身分 → 搬進同一個 connect frame

以前是一套獨立的四步握手:hello → NOT_PAIRED → pair-request → pair-ok。

現在配對資訊直接長在 connect 裡:

"auth": { "token": "…" },
"device": {
  "id": "device_fingerprint",
  "publicKey": "…",
  "signature": "…",
  "signedAt": 1737264000000
}

從 token 變成公鑰 + 簽章 + 簽署時間。

而且 gateway 會先發一個挑戰:

{ "type": "event", "event": "connect.challenge", "payload": { "nonce": "…", "ts": … } }

先給 nonce,你簽回來。這是標準的防重放做法,而以前那個 per-node token 是做不到的——token 被側錄就等於身分被拿走。

安全等級其實是提升的。少一套協定,不代表少一層防護。

三、Discovery → 搬成一個 plugin

這個轉向我覺得最漂亮。

📄 文件:docs/gateway/bonjour.md

the bundled `bonjour` plugin owns LAN advertising, auto-starting on macOS hosts
and opt-in on Linux, Windows, and containerized gateway deployments.

Bonjour 還在,但它不再是傳輸層的一部分,而是一個內建 plugin。

這件事完全呼應我在 18th 鐵人賽講 plugin 角色那篇的結論:plugin 是會改變系統運作方式的模組,不是內容包。

「在區網上廣播自己」是一個可選的、依平台而異的行為——macOS 自動開、Linux 和容器環境要自己選。這種東西本來就不該焊死在核心的傳輸層裡。

而且文件講得很誠實:

Discovery is best-effort and does **not** replace SSH or Tailnet-based connectivity.

它只是方便,不是連線方案。這也是原本的 bridge 混淆掉的事——把「找得到」和「連得上」綁在同一層。

四、Loopback 限制 → 交給網路層

原本 WS 預設只聽 loopback(127.0.0.1:18789),要遠端就得自己開 SSH 隧道。

現在這件事交給 Tailscale、SSH、trusted proxy 這些本來就該做這件事的東西。

docs/gateway/ 底下有 tailscale.md、trusted-proxy-auth.md、remote.md、multi-tenant-hosting.md——整整一組文件在講「怎麼安全地讓它被遠端連到」。

這比「自己做一個比較安全的 TCP 協定」務實太多。

現在那唯一一套協定長什麼樣

既然只剩一套,它的規矩就要更嚴。幾個值得記的細節:

第一個 frame 一定要是 connect。 不能先做別的事。

握手前的 frame 有大小上限。 原始碼裡就是一個常數:

📄 原始碼:src/gateway/server-constants.ts:5

export const MAX_PREAUTH_PAYLOAD_BYTES = 64 * 1024;

64 KiB。在你還沒證明自己是誰之前,只能送這麼多——這是一道很便宜但很有效的防線,擋掉「未認證的連線灌爆記憶體」這種攻擊。

握手之後改看 hello-ok.policy.maxPayload,由伺服器告訴你新的額度。認證前後兩套限制,這個分界很乾淨。

frame 只有三種形狀:

Request:  {type:"req",   id, method, params}
Response: {type:"res",   id, ok, payload|error}
Event:    {type:"event", event, payload, seq?, stateVersion?}

會產生副作用的方法需要冪等鍵。 因為 WS 會斷線重連,重送是常態——這跟我在講重試那篇談的是同一個世界觀:失敗和重複不是例外,是預設會發生的事。

還有一個我很喜歡的細節。超大 frame 和緩慢的輸出緩衝會發出 payload.large 診斷事件,而文件特別說明那個事件裡有什麼、沒有什麼:

These events carry `surface`, byte sizes, limits, and a safe reason code, never
message bodies, attachment contents, raw frame bytes, tokens, cookies, or secrets.

診斷事件本身也是一個洩漏面。 會特地寫下「這裡面絕對不會有什麼」的專案不多。

設計取捨

  • 好處是只有一套協定要維護、要測、要寫文件、要出 SDK
  • 好處是安全邊界從「你走哪個 port」變成「你宣告什麼 role 和 scope」,更細也更好審
  • 好處是身分驗證從 token 升級成 nonce + 簽章,防重放
  • 好處是 discovery 變成 plugin,可以依平台開關,不綁核心
  • 代價是那一百多種方法現在全部在同一個表面上,scope 的檢查一旦有漏,後果比以前大
  • 代價是所有 client 都得實作完整的 handshake,不能再靠「走那個簡單的 port」偷懶
  • 代價是舊的 node 全部要改寫,沒有相容層

最後一條其實是這個決定最貴的部分。但從結果看,他們換到的是一個只有一個地方需要做對的架構。

兩套協定意味著兩套認證、兩套 scope 檢查、兩套速率限制——而且它們一定會不同步。真正的風險從來不是「協定不夠多」,是兩條路的安全等級悄悄長得不一樣。

今天的結論

  • OpenClaw 以前有兩套傳輸:WS 控制面(18789)和 TCP JSONL bridge(18790),bridge 已完全移除
  • bridge 存在的四個理由——安全邊界、配對、discovery、loopback 限制——一個都沒消失,只是各自搬家
  • 安全邊界搬進 handshake 的 role / scopes;配對搬進同一個 connect frame,並升級成 nonce + 簽章
  • discovery 搬成一個 bundled plugin,依平台開關;遠端連線交給 Tailscale / SSH / trusted proxy
  • 現在唯一那套協定:第一個 frame 必須是 connect,認證前 64 KiB 上限,三種 frame 形狀,副作用方法要冪等鍵
  • 刪掉一條路的代價是所有舊 client 要改寫,換到的是「只有一個地方需要做對」

下一步

既然一台主機只能有一個 gateway,而且它掛了所有東西一起掛,那就有兩個很現實的問題:

它怎麼確保自己是唯一的?重開之後,跑到一半的工作怎麼辦?

下一篇講那三層鎖,還有一份「什麼東西活得過重啟」的清單。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言