所有頁面
本頁內容

在反向 proxy 後面執行中繼

反向 proxy 是你自己的一台網頁伺服器,它接收來自瀏覽器的連線,再把連線交給中繼。以下情況需要它:你想為區域網路或伺服器上的中繼加上 HTTPS;中繼要放在你已經在經營的網站的子路徑下(https://example.com/oar/);或瀏覽器和中繼之間隔著 tunnel 的邊緣節點(例如 Cloudflare)。中繼本機上的 proxy 不需要任何設定就能運作。另一台機器上的 proxy 需要兩到三項設定,見下方。有五個 config.json key 會告訴中繼有關 proxy 的事,每一個都解決一個特定的問題;之後會逐一說明。

Proxy 必須做的事 #

中繼的頁面、API 和即時連線都使用同一個連接埠(除非你改了 port,否則是 3333)。proxy 必須:

  • 傳遞 WebSocket 升級。 App 會對中繼保持一條開啟的 Socket.IO 連線,路徑為 /socket.io/(有前綴時為 <prefix>/socket.io/)。如果升級無法通過,頁面會載入,但永遠不會顯示即時更新。
  • 不要緩衝長時間的連線,也不要讓它們逾時。 只要頁面開著,這個 socket 就會保持開啟,回合的內容會在發生時即時串流過去。請使用較長的讀取和閒置逾時,並且不要緩衝回應。
  • 傳遞 Host 標頭,或傳送 X-Forwarded-Host。中繼會用它組出分享連結的位址,而且 0.9.11 新增 只接受來自該位址上頁面的即時連線(見 allowedOrigins)。X-Forwarded-Host 和 X-Forwarded-Proto 只有來自中繼信任的 proxy 時才算數。
  • 在 proxy 終止 TLS 時傳送 X-Forwarded-Proto,讓中繼知道瀏覽器用的是 HTTPS(見 Cookie 與 HTTPS),並傳送 X-Forwarded-For,讓它知道用戶端的位址。
  • 保持路徑原樣。 不要改寫它。如果你在前綴下提供中繼,中繼會自己移除前綴(見 remotePath)。

你不需要快取規則。中繼會為它的頁面、service worker、資訊清單和 API 傳送 Cache-Control: no-store,也會傳送 CDN-Cache-Control: no-store 和 Cloudflare-CDN-Cache-Control: no-store。不要新增快取 /api/shared/* 的規則:分享的檢視會輪詢新內容,而被快取的回應會讓觀看者看到舊的快照。

以下是適用於同一台機器上中繼的最簡 Caddy 範例。Caddy 會自行傳遞 WebSocket 升級,並加上 X-Forwarded-Proto:

relay.example.com {
    reverse_proxy 127.0.0.1:3333
}

如果是 nginx 或其他 proxy,請對照上面的清單,查看它自己的文件,了解如何傳遞升級和設定逾時。關於 tunnel,見 SSH tunnel 和 Cloudflare Tunnel;關於存取 token,見存取。

位於另一台機器上的 proxy 0.9.11 新增 #

內建的 tunnel 和中繼本機上的 proxy 都是透過 127.0.0.1 連到中繼,預設的 trustProxy 會相信它們轉送的標頭。位於另一台主機、另一個容器或 VM 中的 proxy,則是不同的對端。在你信任它之前,中繼會忽略它的 X-Forwarded-For、X-Forwarded-Host 和 X-Forwarded-Proto,因此:

  • 每個訪客的位址都是 proxy 的位址:所有人共用同一個登入鎖定,每筆存取紀錄項目中也都是同一個位址;
  • 新的分享連結會以 http:// 和 proxy 送來的主機開頭;
  • 登入 cookie 不會標上 Secure。

在設定檔中設定三個 key,然後重新啟動中繼:

{
  "localhostOnly": false,
  "trustProxy": "192.0.2.10",
  "allowedOrigins": ["https://relay.example.com"]
}
  • localhostOnly: false 讓中繼在迴路位址以外也監聽。除了 proxy 之外,請對其他一切關閉這個連接埠。
  • trustProxy 是 proxy 的位址、它的子網路,或中繼前面的 proxy 數量(一個時為 1)。見下方。
  • allowedOrigins 只有在 proxy 改寫 Host 標頭且沒有傳送 X-Forwarded-Host 時才需要。見下方。

中繼本機上的 proxy 既不需要 trustProxy,也不需要 localhostOnly: false,只有在它改寫 Host 時才需要 allowedOrigins。在網址前綴下,也要設定 remotePath。

告訴中繼有關 proxy 的 key #

這五個 key 都列在 config.json 的表格中。變更後請重新啟動中繼。

trustProxy #

預設為 "loopback"。它決定中繼相信誰的 X-Forwarded-* 標頭。使用預設值時,只有從同一台機器連線的 proxy 會被相信,這對在中繼旁邊執行的 Caddy、nginx 或 cloudflared 來說正好合適。

中繼會從受信任 proxy 的標頭取得:

  • 請求是否透過 HTTPS 而來,這會決定登入 cookie 是否帶有 Secure 旗標,以及新分享連結的協定;
  • 瀏覽器所連的主機(X-Forwarded-Host),用於新的分享連結,以及即時連線的來源檢查;
  • 用戶端的位址,用於登入鎖定、分享檢視的線上狀態限制,以及分享連結的存取紀錄。該紀錄中的國家(cf-ipcountry)也只會從受信任的 proxy 讀取。

如果你的 proxy 在另一台機器上執行,中繼會看到一個非本機的對端,並忽略這些標頭。請給 trustProxy 一個 Express 的 trust proxy 所接受的值:跳數(1)、位址或子網路("10.0.0.0/8")、由它們組成的清單,或 "loopback"。

只在前面有 proxy 時

只有在每一條連到中繼的連線真的都經過你的 proxy 時,才把 trustProxy 設得比 "loopback" 更寬。否則任何能連到這個連接埠的人,都能送出偽造的 X-Forwarded-Proto。

0.9.11 新增 每次使用轉送標頭都會依循 trustProxy。在 0.9.10 中,分享檢視的線上狀態限制和新分享連結的位址,會從任何用戶端讀取這些標頭。

allowedOrigins 0.9.11 新增 #

預設為 []。即時連線(Socket.IO)只接受來自中繼自身來源的瀏覽器交握:瀏覽器送出的 Origin 必須帶有瀏覽器所連的主機;當受信任的 proxy 送出 X-Forwarded-Host 時就是它,否則是 Host。其他網站上的頁面無法用你的登入 cookie 開啟即時連線。不送出 Origin 的用戶端(例如腳本)不受影響。

會把 Host 標頭改寫成中繼內部位址、且沒有送出 X-Forwarded-Host 的 proxy,會破壞這個比對。這時請列出公開的來源,完全照瀏覽器顯示的樣子,包含協定、主機,以及預設以外的任何連接埠:

"allowedOrigins": ["https://relay.example.com"]

沒有它的話,頁面會載入,你也能登入,但沒有任何東西會即時更新,而且中繼會針對每個來源記錄一次:

[socket] Refused a handshake from origin https://relay.example.com (host 127.0.0.1:3333); list it in allowedOrigins if a proxy rewrites the Host header

remotePath #

預設為 ""。當 proxy 在子路徑下提供中繼時,把它設為該前綴,例如 "/oar"。開頭的斜線會自動補上,結尾的斜線會被移除。

設定之後,中繼會:

  • 從每個進來的請求拿掉前綴,所以 proxy 傳遞路徑時要帶著前綴,不要移除它;
  • 把前綴放進登入 cookie 的路徑、socket 路徑(/oar/socket.io/),以及檔案和分享連結的位址。

沒有設定時,位於前綴下的中繼找不到自己的檔案和 socket,因為頁面會在網站的根目錄要求 /socket.io/ 和 /api/...。開啟中繼時請加上結尾的斜線(https://example.com/oar/)。

已安裝的 App 屬於它安裝時所用的位址,所以位於前綴下的中繼是一個獨立的 App。見安裝成 App。

publicHostnames #

預設為 []。列出連到中繼本身的主機名稱,例如 ["relay.example.com"]。中繼只在一個地方使用它們:預覽通道會拒絕位於這些名稱(或 localhost、127.0.0.1)上的 previews.publicBaseUrl,這樣被預覽的 App 就永遠不會和中繼共用登入 cookie。如果你把中繼放在使用新名稱的 proxy 後面,並且使用預覽,請把那個名稱加到這裡。

tunnelMarkerHeaders #

預設為 []。你的 proxy 或 tunnel 的邊緣節點會加到它轉送的每個請求上的標頭名稱。中繼會把它們用於下方的 session worker 路徑保護。Cloudflare 加上的 cf-ray 一律在清單上。值可以是陣列,也可以是以逗號分隔的字串。環境變數 COPILOT_TUNNEL_MARKER_HEADERS(以逗號分隔)會加入更多名稱;0.9.11 新增 兩份清單會合併,所以 tunnelMarkerHeaders: [] 不再會關閉這個變數。見環境變數。

狀態點

tunnelMarkerHeaders 不會讓狀態點變成琥珀色。只有在中繼自己受管理的 Cloudflare tunnel 已連線時,狀態點才會是琥珀色(見對話選單)。你自己執行的 proxy 永遠不會讓它變成琥珀色。

Session worker 路徑保護 #

每個 agent session 都在中繼所在機器上的一個 worker 中執行,worker 透過 /api/session-worker/ws(有前綴時為 <prefix>/api/session-worker/ws)上的 WebSocket 和中繼溝通。proxy 或 tunnel 會把它主機名稱下的每個路徑都轉送給中繼,所以如果沒有保護,這個路徑就能從網際網路連到。只有本機的 worker 應該使用它。

0.9.11 新增 只有當請求來自中繼本機、且沒有經過任何轉送時,保護才會讓對該路徑的請求通過:

  • 連線的對端是迴路位址(127.0.0.1 或 ::1);
  • 它沒有帶任何標記標頭:cf-ray,或 tunnelMarkerHeaders 中的某一個;
  • 它沒有帶 X-Forwarded-For、Forwarded 或 X-Real-IP。

其他一切都會在檢查登入之前就被拒絕:

  • 一般請求會得到 403,內容為 {"error":"Session worker endpoints are reachable from this machine only"};
  • WebSocket 升級會得到 403 Forbidden,連線也會被關閉。

每次拒絕都會記錄為 [tunnel-guard] Blocked session-worker path …: not local (…),並指出是什麼讓這個請求露了餡。本機的 worker 直接連到 127.0.0.1,不帶這些標頭,因此不受影響。分享連結透過 tunnel 仍能正常運作:保護只針對 worker 路徑,而不是針對匿名的請求。

Cloudflare 會加上 cf-ray,Caddy 也會自行加上 X-Forwarded-For,所以保護不需任何設定就能搭配它們運作;使用 nginx 時,請用 proxy_set_header 傳送 X-Forwarded-For。完全不加任何東西的轉送器,例如 SSH 反向 tunnel 或單純的 TCP proxy,看起來就像本機的處理程序;這時該路徑只受存取 token 保護。使用這類轉送器時,請讓它加上一個帶有固定非空值的標頭,例如 X-Oar-Edge: 1,並列出它的名稱:"tunnelMarkerHeaders": ["x-oar-edge"]。或者在中繼前面就拒絕這個路徑。

只有中繼所在機器能使用的端點 #

0.9.11 新增 有兩樣東西只回應來自中繼本機、且沒有經過任何 proxy 轉送的請求,規則與路徑保護相同:

  • 上面提到的 session worker socket;
  • 執行影像操作(POST /api/image-operations/…/execute),由中繼自己的 worker 呼叫。其他一切都會得到 403 Image operation execution is reachable from this machine only(影像操作執行僅能從本機存取)。

其他所有東西都只需要存取 token,不論請求來自何處。這包括 🌄 Restart web relay(重新啟動網頁中繼)、用橫幅上的 Cancel(取消)按鈕撤回排入佇列的重新啟動,以及 🚮 Empty queue(清空佇列):任何已登入的瀏覽器都能使用它們,透過區域網路、任何 proxy 或 tunnel 也一樣。

到 0.9.10 為止,這三項原本只應供中繼所在的機器使用,但檢查會讓經由 tunnel 或同一台機器上 proxy 的一切通過,卻拒絕直接的區域網路連線,同時頁面還回報成功。如果遠端使用者不應該能重新啟動中繼或清空它的佇列,請讓 proxy 拒絕 POST /api/relay/shutdown、POST /api/relay/shutdown/cancel 和 POST /api/queue/empty。在中繼所在的機器上,oar restart 一律有效。

Cookie 與 HTTPS #

登入 cookie 是 HttpOnly 和 SameSite=Lax,有效期 30 天,路徑為 /(或 remotePath 前綴)。當中繼認為請求是安全的,cookie 就會帶有 Secure。

  • proxy 在同一台機器上終止 TLS。 proxy 會送出 X-Forwarded-Proto: https,預設的 trustProxy 會相信它,cookie 就會是 Secure。
  • proxy 在另一台機器上終止 TLS。 如上所述設定 trustProxy。在你設定之前,cookie 沒有 Secure 旗標,但仍然可以運作。
  • 混合的設定。 如果你也在區域網路上直接以純 http 開啟中繼,那個請求不是安全的,所以它的 cookie 沒有 Secure 旗標;經由 proxy 的 HTTPS 請求則會得到這個旗標。瀏覽器會依位址分別保存 cookie。

檢查設定 #

  1. 在瀏覽器中開啟經由 proxy 的位址。登入畫面或對話清單會出現。
  2. 查看狀態點:它會變成綠色,而且當你開始一個回合時,頁面會顯示即時更新。如果一直處於重新連線的狀態,表示 WebSocket 無法通過。
  3. 在前綴下,用結尾的斜線開啟位址。頁面、它的樣式和即時連線都會載入,而且 cookie 的路徑就是該前綴(在瀏覽器的開發人員工具中查看)。
  4. 建立一個分享連結(分享)。它的位址使用公開的主機名稱和 https,如果你設定了前綴,也會包含前綴。
  5. 從外部測試保護:請求 https://relay.example.com/api/session-worker/ws。你應該會看到 403,中繼的記錄中也會有一行 [tunnel-guard]。如果你得到其他回應,表示你的 proxy 既沒有加上標記標頭,也沒有加上 X-Forwarded-For。
  6. 在另一台機器上的 proxy 後面:從兩台裝置登入,並在 Settings → Sharing(設定 → 分享)中檢查分享連結的存取紀錄顯示的是觀看者的位址,而不是 proxy 的位址。如果顯示的是 proxy 的位址,表示 trustProxy 與 proxy 不相符。

無法正常運作時 #

你看到的情況 可能的原因
頁面載入了,但一直在重新連線,或從不更新 proxy 沒有在 /socket.io/ 上傳遞 WebSocket 升級,或會關閉閒置的連線。
頁面載入了,但沒有任何東西即時更新,而且記錄中出現 Refused a handshake from origin proxy 改寫了 Host 標頭。請在 allowedOrigins 中列出公開的來源,或讓 proxy 原樣傳遞 Host。
登入時所有人同時看到 Too many failed sign-in attempts(登入失敗次數過多) 中繼不信任另一台機器上的 proxy,所以所有訪客共用它的位址和它的鎖定。請設定 trustProxy。
在前綴下,頁面空白或缺少檔案 沒有設定 remotePath,或 proxy 移除了前綴。proxy 必須傳遞前綴。
在前綴下,一再被要求重新登入 cookie 的路徑是 remotePath 前綴,所以瀏覽器只會在該前綴下送出它。請在該前綴下開啟中繼,並加上結尾的斜線。
分享連結或檔案連結的位址錯誤,或沒有前綴 沒有設定 remotePath,或 proxy 沒有送出 Host 或 X-Forwarded-Host / X-Forwarded-Proto,或中繼不信任該 proxy。
在另一台機器上的 proxy 後面,分享連結以 http:// 開頭,或 cookie 沒有 Secure 旗標 trustProxy 仍然是 "loopback"。
透過你的 proxy 時,worker 路徑的回應不是 403 proxy 既沒有加上標記標頭,也沒有加上 X-Forwarded-For。見路徑保護。
狀態點從不變成琥珀色 這是預期的行為:只有中繼自己受管理的 Cloudflare tunnel 才會讓它變色。
預覽拒絕在新名稱上啟動 把中繼的主機名稱加到 publicHostnames。

關於首次登入和存取 token,見首次執行。