所有页面
本页内容

在反向代理后运行中继

反向代理是你自己运行的一台 Web 服务器,它接收来自浏览器的连接,再转交给中继。以下情况需要用到它:你想为局域网或服务器上的中继提供 HTTPS;中继要放在你已有站点的某个子路径下(https://example.com/oar/);或者浏览器和中继之间隔着隧道的边缘节点(例如 Cloudflare)。位于中继所在机器上的代理无需任何设置即可工作。位于另一台机器上的代理需要两到三项设置,见下文。有五个 config.json 键用来向中继说明代理的情况,每个键解决一个具体问题;随后会逐一说明。

代理必须做什么 #

中继的页面、API 和实时连接都使用同一个端口(除非你改过 port,否则是 3333)。代理必须:

  • 传递 WebSocket 升级。 应用会与中继保持一个打开的 Socket.IO 连接,路径为 /socket.io/(有前缀时为 <prefix>/socket.io/)。如果升级无法通过,页面能加载,但永远不会显示实时更新。
  • 不缓冲长连接,也不让它们超时。 只要页面开着,这个套接字就一直保持打开,各轮的内容在进行时实时流式传输过来。请使用较长的读取超时和空闲超时,并且不要缓冲响应。
  • 传递 Host 请求头,或发送 X-Forwarded-Host。中继据此构建分享链接的地址,并且 0.9.11 新增 只接受来自该地址上页面的实时连接(参见 allowedOrigins)。X-Forwarded-Host 和 X-Forwarded-Proto 只有来自中继信任的代理时才有效。
  • 发送 X-Forwarded-Proto(当代理终结 TLS 时),让中继知道浏览器使用的是 HTTPS(参见 Cookie 与 HTTPS);并发送 X-Forwarded-For,让中继知道客户端的地址。
  • 保持路径不变。 不要重写路径。如果你把中继放在某个前缀下提供,中继会自行去掉前缀(参见 remotePath)。

你不需要缓存规则。中继会为它的页面、service worker、manifest 和 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 或其他任何代理,请查阅它们自己的文档,了解如何传递升级和设置超时,并对照上面的列表检查。关于隧道,参见 SSH 隧道和 Cloudflare Tunnel;关于访问令牌,参见访问与登录。

位于另一台机器上的代理 0.9.11 新增 #

内置隧道和中继所在机器上的代理都通过 127.0.0.1 连接中继,默认的 trustProxy 会信任它们发来的转发请求头。位于另一台主机、另一个容器或虚拟机中的代理则是另一个对端。在你信任它之前,中继会忽略它的 X-Forwarded-For、X-Forwarded-Host 和 X-Forwarded-Proto,因此:

  • 所有访客都显示为代理的地址:他们共用同一个登录锁定,每条访问日志记录也都是同一个地址;
  • 新的分享链接以 http:// 和代理发送的主机开头;
  • 登录 cookie 不会被标记为 Secure。

在配置文件中设置三个键,然后重启中继:

{
  "localhostOnly": false,
  "trustProxy": "192.0.2.10",
  "allowedOrigins": ["https://relay.example.com"]
}
  • localhostOnly: false 让中继不只在环回地址上监听。请让该端口只对代理开放,对其他一切保持关闭。
  • trustProxy 是代理的地址、所在子网,或中继前面的代理数量(一个代理就是 1)。参见下文。
  • 只有当代理重写 Host 请求头且不发送 X-Forwarded-Host 时,才需要 allowedOrigins。参见下文。

位于中继所在机器上的代理既不需要 trustProxy,也不需要 localhostOnly: false;只有在它重写 Host 时才需要 allowedOrigins。如果使用 URL 前缀,还要设置 remotePath。

向中继说明代理情况的键 #

这五个键都在 config.json 的表格中。修改后请重启中继。

trustProxy #

默认值 "loopback"。它决定中继信任谁发来的 X-Forwarded-* 请求头。使用默认值时,只信任从同一台机器连接的代理,这正适合在中继旁边运行的 Caddy、nginx 或 cloudflared。

中继会从受信任代理的请求头中获取:

  • 请求是否通过 HTTPS 到达,这决定登录 cookie 是否带 Secure 标志,以及新分享链接使用的协议;
  • 浏览器访问的主机(X-Forwarded-Host),用于新分享链接和实时连接的来源检查;
  • 客户端的地址,用于登录锁定、分享视图的在线人数限制以及分享链接的访问日志。该日志中的国家(cf-ipcountry)也只从受信任的代理读取。

如果你的代理运行在另一台机器上,中继看到的是一个非本地的对端,会忽略这些请求头。请给 trustProxy 设置 Express 的 trust proxy 所接受的值:跳数(1)、地址或子网("10.0.0.0/8")、它们组成的列表,或 "loopback"。

仅在前面有代理时

只有当到达中继的每个连接都确实经过你的代理时,才把 trustProxy 设置得比 "loopback" 更宽。否则,任何能访问该端口的人都可以发送伪造的 X-Forwarded-Proto。

0.9.11 新增 所有对转发请求头的使用都遵循 trustProxy。在 0.9.10 中,分享视图的在线人数限制和新分享链接的地址会读取任何客户端发来的这些请求头。

allowedOrigins 0.9.11 新增 #

默认值 []。实时连接(Socket.IO)只接受来自中继自身来源的浏览器握手:浏览器发送的 Origin 必须带有浏览器访问的主机,即受信任的代理发送了 X-Forwarded-Host 时取其值,否则取 Host。其他站点上的页面无法借用你的登录 cookie 打开实时连接。不发送 Origin 的客户端(例如脚本)不受影响。

如果代理把 Host 请求头改写成中继的内部地址,又不发送 X-Forwarded-Host,这个比较就会失败。这时请列出公开来源,与浏览器显示的完全一致,包括协议、主机以及任何非默认端口:

"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 #

默认值 ""。当代理把中继放在某个子路径下提供时,把它设为该前缀,例如 "/oar"。开头缺少的斜杠会自动补上,末尾的斜杠会被去掉。

设置后,中继会:

  • 从每个传入请求中去掉前缀,因此代理传递路径时要带着前缀,不要去掉它;
  • 把前缀加入登录 cookie 的路径、套接字路径(/oar/socket.io/)以及文件和分享链接的地址中。

不设置的话,位于前缀下的中继找不到自己的文件和套接字,因为页面会在站点根路径请求 /socket.io/ 和 /api/...。请带末尾斜杠打开中继(https://example.com/oar/)。

已安装的应用归属于安装时所用的地址,因此位于前缀下的中继是一个独立的应用。参见安装为应用。

publicHostnames #

默认值 []。列出指向中继本身的主机名,例如 ["relay.example.com"]。中继只在一处用到它们:预览通道会拒绝使用这些主机名之一(或 localhost、127.0.0.1)的 previews.publicBaseUrl,这样被预览的应用就绝不会与中继共享登录 cookie。如果你用一个新主机名把中继放在代理后面,并且使用预览,请把该主机名加到这里。

tunnelMarkerHeaders #

默认值 []。你的代理或隧道边缘节点会给它转发的每个请求添加的请求头名称。中继用它们实现下面的会话 worker 路径保护。Cloudflare 添加的 cf-ray 始终在列表中。值可以是数组,也可以是逗号分隔的字符串。环境变量 COPILOT_TUNNEL_MARKER_HEADERS(逗号分隔)可以添加更多名称;0.9.11 新增 两个列表会合并,因此 tunnelMarkerHeaders: [] 不再能让该变量失效。参见环境变量。

状态点

tunnelMarkerHeaders 不会让状态点变成琥珀色。只有在中继自己托管的 Cloudflare 隧道处于连接状态时,状态点才是琥珀色(参见对话菜单)。你自己运行的代理永远不会让它变成琥珀色。

会话 worker 的路径保护 #

每个智能体会话都运行在中继所在机器上的一个 worker 中,worker 通过 /api/session-worker/ws(有前缀时为 <prefix>/api/session-worker/ws)上的 WebSocket 与中继通信。代理或隧道会把其主机名下的每条路径都转发给中继,因此如果没有保护,这条路径就能从互联网访问。只有本地 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,不带这些请求头中的任何一个,因此不受影响。分享链接通过隧道照常可用:路径保护只针对 worker 路径,而不看请求是否匿名。

Cloudflare 会自动添加 cf-ray,Caddy 会自动添加 X-Forwarded-For,因此路径保护无需设置就能与它们配合使用;使用 nginx 时,请用 proxy_set_header 发送 X-Forwarded-For。完全不添加任何内容的转发器(例如 SSH 反向隧道或裸 TCP 代理)看起来就像本地进程;这时该路径只受访问令牌保护。使用这类转发器时,请让它添加一个带固定非空值的请求头,例如 X-Oar-Edge: 1,并列出它的名称:"tunnelMarkerHeaders": ["x-oar-edge"]。或者在中继前面直接拒绝这条路径。

只有中继所在机器能使用的端点 #

0.9.11 新增 有两项只响应来自中继所在机器、且未经任何代理转发的请求,规则与路径保护相同:

  • 上面所说的会话 worker 套接字;
  • 执行图片操作(POST /api/image-operations/…/execute),由中继自己的 worker 调用。其他请求会得到 403 Image operation execution is reachable from this machine only(图像操作执行仅可从本机访问)。

其他一切都只需要访问令牌,不论请求来自哪里。这包括 🌄 Restart web relay(重启网页中继)、用重启横幅上的 Cancel(取消)按钮撤回排队中的重启,以及 🚮 Empty queue(清空队列):任何已登录的浏览器都可以使用它们,通过局域网或经由任何代理、隧道也一样。

在 0.9.10 及更早版本中,这三项本应只限中继所在机器使用,但检查会放行经由同一台机器上的隧道或代理的所有请求,却会拒绝直接的局域网连接,而页面仍显示成功。如果不希望远程用户重启中继或清空队列,请让代理拒绝 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。

  • 代理在同一台机器上终结 TLS。 代理发送 X-Forwarded-Proto: https,默认的 trustProxy 信任它,cookie 带 Secure。
  • 代理在另一台机器上终结 TLS。 按上文设置 trustProxy。在你设置之前,cookie 没有 Secure 标志,但仍然可用。
  • 混合配置。 如果你还在局域网上通过明文 http 直接打开中继,那个请求不安全,因此它的 cookie 没有 Secure 标志;经代理的 HTTPS 请求则会带上该标志。浏览器按地址分别保存 cookie。

检查设置 #

  1. 在浏览器中打开经代理的地址。会出现登录页或对话列表。
  2. 查看状态点:它会变成绿色,并且在你开始一轮时页面会显示实时更新。如果一直处于重新连接状态,说明 WebSocket 没有通过。
  3. 使用前缀时,带末尾斜杠打开地址。页面、样式和实时连接都能加载,并且 cookie 的路径是该前缀(在浏览器的开发者工具中查看)。
  4. 创建一个分享链接(分享)。它的地址使用公开主机名和 https,如果设置了前缀也会包含前缀。
  5. 从外部测试路径保护:请求 https://relay.example.com/api/session-worker/ws。你应该看到 403,并在中继日志中看到一行 [tunnel-guard]。如果得到其他响应,说明你的代理既没有添加标记请求头,也没有添加 X-Forwarded-For。
  6. 代理位于另一台机器上时:从两台设备登录,然后在 Settings → Sharing(设置 → 分享)中检查分享链接的访问日志显示的是查看者的地址,而不是代理的地址。如果显示的是代理的地址,说明 trustProxy 与代理不匹配。

无法正常工作时 #

你看到的现象 可能的原因
页面能加载,但一直在重新连接,或从不更新 代理没有在 /socket.io/ 上传递 WebSocket 升级,或关闭了空闲连接。
页面能加载,但没有任何实时更新,日志中出现 Refused a handshake from origin 代理重写了 Host 请求头。请在 allowedOrigins 中列出公开来源,或让代理原样传递 Host。
所有人登录时同时出现 Too many failed sign-in attempts(登录失败次数过多) 中继不信任位于另一台机器上的代理,因此所有访客共用代理的地址及其登录锁定。请设置 trustProxy。
使用前缀时,页面空白或缺少文件 未设置 remotePath,或代理去掉了前缀。代理必须保留前缀传递。
使用前缀时,一再要求你重新登录 cookie 的路径是 remotePath 前缀,因此浏览器只在该前缀下发送它。请在该前缀下、带末尾斜杠打开中继。
分享链接或文件链接的地址错误或缺少前缀 未设置 remotePath,或代理没有发送 Host 或 X-Forwarded-Host / X-Forwarded-Proto,或中继不信任该代理。
代理位于另一台机器上时,分享链接以 http:// 开头,或 cookie 没有 Secure 标志 trustProxy 仍是 "loopback"。
通过你的代理访问 worker 路径时,返回的不是 403 代理既没有添加标记请求头,也没有添加 X-Forwarded-For。参见路径保护。
状态点从不变成琥珀色 这是预期行为:只有中继自己托管的 Cloudflare 隧道会让它变色。
预览在新主机名上拒绝启动 把中继的主机名加到 publicHostnames。

关于首次登录和访问令牌,参见首次运行。