所有页面
本页内容

通过 SSH 隧道访问中继

中继可以向你的某台服务器打开一个 SSH 连接,并请该服务器把一个端口转发回中继。之后你通过服务器的地址访问中继,家里没有任何程序对互联网监听。ssh 进程由中继自己启动、监视和重启。

如果你已经有一台带公网地址、可以 SSH 登录的服务器,就选这种方式。如果你没有服务器,内置的 Cloudflare Tunnel 不需要服务器。

你需要什么 #

  • 中继主机的 PATH 中有 ssh 程序(或者用 command 指定另一个,见下文)。
  • 服务器上的一个 SSH 登录,用密钥登录且不弹出任何提示。中继以批处理模式运行 ssh,因此无法输入密码或密钥口令。带口令的密钥只有在 SSH agent 持有它时才能用。先在中继主机上测试:ssh relay@relay.example.com 必须不询问任何内容就能登录。
  • 服务器上一个供隧道使用的空闲端口(remotePort)。
  • 服务器上有程序通过 HTTPS 提供该端口:Web 服务器或反向代理,例如 Caddy 或 nginx。隧道传输的是明文 HTTP,所以没有它的话,一旦端口可以访问,你的令牌和对话就会未经加密地穿过互联网。参见在反向代理后运行中继。

中继不需要任何特殊的 sshd 设置。remoteBind 为 loopback 时,它请求转发时不带绑定地址,因此由服务器自己的 GatewayPorts 设置决定端口在哪里开放。在默认值 no 下,端口只在服务器本机开放,反向代理是唯一面向互联网的东西。设为 public 时,中继请求所有网络接口(*:),而这个请求只有在服务器的 sshd 允许时才生效(GatewayPorts 设为 clientspecified 或 yes)。

设置 #

在 config.json 中添加一个 sshTunnel 块(参见文件位置),然后用 oar restart 重启中继。

"sshTunnel": {
  "mode": "managed",
  "required": false,
  "user": "relay",
  "host": "relay.example.com",
  "remotePort": 4444,
  "remoteBind": "loopback",
  "identityFile": "~/.ssh/id_ed25519"
}

在服务器上,把 HTTPS 转到隧道端口。以 Caddy 为例:

relay.example.com {
    reverse_proxy localhost:4444
}

在浏览器中打开 https://relay.example.com/,用你的访问令牌登录。中继的启动横幅会打印一行 Remote:,oar url 会打印一行 Tunnel URL: 并附带二维码。两者都是 https:// 加上 host 的值,不带端口,也不检查隧道是否已连通,因此只有当你的代理在标准端口上提供 HTTPS 时它们才正确。

sshTunnel 的键 #

所有键都位于 sshTunnel 块中。本表与 config.json 中的同一份列表一致。

键 类型 默认值 作用
mode "disabled" 或 "managed" "disabled" managed 让中继启动并重启隧道。
user 字符串 无 服务器上的 SSH 用户。托管模式下必填。
host 字符串 无 服务器的主机名或地址。托管模式下必填。
remotePort 数字 无 在服务器上开放的端口,1 到 65535。托管模式下必填。
remoteBind "loopback" 或 "public" "loopback" loopback 请求端口时不带绑定地址(由服务器的 GatewayPorts 决定,参见你需要什么);public 请求所有网络接口。其他任何值都按 loopback 处理。与 localhostOnly 无关。
identityFile 字符串 无 私钥文件;~ 会被展开。不设时,ssh 使用你的 agent 及其默认密钥。
command 字符串 ssh SSH 程序,也用于服务器上的清理。只写名称时在 PATH 中查找;带斜杠的路径按原样使用,相对路径从中继的程序文件夹算起。
required 布尔值 false 隧道未连接期间,中继不从队列中取消息。参见必需隧道。
autoReclaimPort 布尔值 true 远程端口已被占用时,在服务器上运行清理并重试。只有 false 会关闭它。
reclaimStaleSshSessions 布尔值 false 作为该清理的一部分,同时结束你自己在服务器上的空闲 SSH 会话。
remoteCleanupCommand 字符串 内置 你自己的清理命令,在服务器上代替内置命令运行。

旧写法 "enabled": true 仍然有效,表示 "mode": "managed",但仅限 mode 未设为 disabled 或 managed 时。

中继把服务器的 remotePort 转发到它自己在 127.0.0.1 上的端口(除非你改过 port,否则是 3333)。设为 public 时,转发以 *:remotePort 的形式请求,否则以 remotePort 的形式请求。

以下设置是固定的,没有对应的键:中继运行 ssh -N,并带上 ServerAliveInterval=30、ServerAliveCountMax=3、BatchMode=yes、StrictHostKeyChecking=accept-new 和 ExitOnForwardFailure=yes。连续三次检查(间隔 30 秒)都没有回应时,ssh 会放弃该连接。

如果缺少必填值(托管模式下的 user、host 或有效的 remotePort),中继照常启动,在日志中记下缺的是哪一个,并让隧道保持关闭。

运行方式 #

启动。 中继开始监听后就启动隧道。ssh 运行满 1.2 秒且服务器没有拒绝转发时,中继就认为隧道已连接。日志随后显示 Tunnel up to user@host remote port N。

重连。 ssh 退出后,中继会等待一段逐渐增长的时间再启动它:5、10、20、40 秒,然后是 60 秒,此后一直保持 60 秒。一个持续超过 30 秒的连接断开后,计数重新开始。停止中继会同时停止隧道和重试。

主机密钥。 中继主机从未见过的服务器,会在第一次连接时被接受并记住。此后主机密钥一旦改变,ssh 就会拒绝连接,直到你从 known_hosts 中删除旧条目。

必需隧道 #

设置 required: true 后,队列从启动起一直暂停到隧道连接上为止,之后每次隧道断开也会再次暂停。你发送的消息在队列中等待,隧道连通后再运行。如果没有隧道,中继对你就毫无用处,就使用这个选项。因缺少某个值而无法启动的隧道同样会让队列保持暂停。

远程端口被占用时 #

如果服务器报告端口无法绑定,通常是因为之前某个隧道遗留的会话还在,而服务器还没发现它已经失效。除非 autoReclaimPort 为 false,中继会再连接服务器一次,并在那里运行清理命令。

这些选项会在你的服务器上运行命令

清理以你的 SSH 登录身份运行,并会结束进程。它会结束该用户下任何监听远程端口的进程,不管它是不是遗留的隧道。启用 reclaimStaleSshSessions 时,它还会结束该用户的每个空闲 SSH 会话,其中也包括同一登录在其他端口上的其他 ssh -N -R 隧道。remoteCleanupCommand 会运行你写的任何内容。在让这些选项保持开启之前,先读懂每一步做了什么。

内置清理按以下顺序执行:

  1. 检查端口是否被占用(用 ss,没有则读取内核的监听套接字列表)。如果没有被占用,就停止并报告成功。
  2. 找出监听该端口的进程(用 lsof,没有则用 fuser,取存在的那个),向它们发送终止信号(kill),然后等待一秒。你的登录无权发送信号的进程会留下。如果端口此时已空闲,就报告成功。
  3. 仅在 reclaimStaleSshSessions: true 且端口仍被占用时:向你的用户下每个没有子进程的 sshd 会话发送终止信号,然后等待一秒。这些正是 ssh -N -R 留下的会话。运行着 shell、编辑器远程服务器或其他任何程序的会话有子进程,会被跳过,运行清理本身的会话也会被跳过。
  4. 如果端口仍被占用,清理以退出码 3 退出,中继在日志中记为 is still held。

设置了 remoteCleanupCommand 时,你的命令会代替以上所有步骤运行,reclaimStaleSshSessions 不再起作用。中继通过第二个 ssh 登录、用同一个密钥、以批处理模式把它作为 sh -lc '<command>' 运行。端口空闲时它必须以 0 退出,端口仍被占用时以其他退出码退出。

清理释放了端口后,中继在一秒后重试,最多连续三次。清理失败后,或者 autoReclaimPort 关闭时,中继使用上文的常规等待时间。

查看隧道状态 #

Cloudflare 隧道会显示的那些地方不会显示 SSH 隧道:oar status 和 oar doctor 只报告 Cloudflare 隧道,状态点也不会因 SSH 隧道变成琥珀色(参见状态点)。请改看以下位置:

  • 中继的日志。 每行都以 [ssh-tunnel] 开头。其中有中继运行的 ssh 命令行、ssh 打印的内容、Tunnel up to ...、Reconnecting in Ns (attempt N)... 以及回收端口的各个步骤。日志的位置取决于中继的运行方式;参见文件与文件夹。
  • 启动横幅:配置了托管隧道时,其中有一行 Remote: 和一行 Tunnel: mode=loopback(或 public)。它们显示的是配置,而不是隧道是否已连通。oar url 以 Tunnel URL: 打印同一个地址。
  • /api/status:其中有一个 sshTunnel 对象,包含 connected、connectedSince、reconnectAttempts、lastError、blocking、required、mode、valid、host、remotePort 和 remoteBindMode。请带上你的令牌获取它。

ssh 结束后,lastError 为 exit:<code>;服务器拒绝端口后为 remote-forward-bind-failed:<code>;配置块不完整时为 invalid-config 或缺少值的那条消息;隧道连通时为空。

安全 #

隧道一旦连通,中继就能从互联网访问,而陌生人与你的智能体(它们会在你的机器上运行命令)之间只隔着访问令牌。因此:

  • 让 remoteBind 保持为 loopback,并在服务器上用 HTTPS 挡在前面。设为 public 时,任何能访问服务器隧道端口的人都能通过明文 HTTP 访问中继。
  • 访问令牌是唯一的一把锁。参见访问令牌与谁能访问中继和 OAR 暴露什么。
  • 发往中继内部会话 worker 路径的请求,如果带有 Cloudflare 标记请求头 cf-ray 或你在 tunnelMarkerHeaders 中列出的请求头,中继会拒绝。普通反向代理不会添加其中任何一个,所以如果你的代理会添加某个你希望拦截的请求头,请把它列出来。参见 config.json。

疑难解答 #

先看日志中的 [ssh-tunnel] 行。以 stderr: 开头的行是 ssh 自己打印的内容。

你看到的 含义 怎么做
sshTunnel.user is required when sshTunnel.mode is "managed"(host 同理) 缺少必填值。隧道保持关闭。 设置它,然后运行 oar restart。
sshTunnel.remotePort must be a positive integer when sshTunnel.mode is "managed" 端口缺失,或不是 1 到 65535 之间的数字。 改正后重启。
Tunnel mode disabled; running direct relay only. mode 不是 managed。 设置 "mode": "managed"。
stderr: ... Permission denied (publickey) 服务器没有接受你的密钥。批处理模式下不会提示输入密码。 设置 identityFile,并在中继主机上手动测试同样的 ssh user@host。
Process exited (code=N). Scheduling reconnect... 反复出现 ssh 无法连接或登录。 手动运行日志中记录的 ssh 命令,阅读它的错误信息。
stderr: ... remote port forwarding failed for listen port N,随后是 Attempting remote reclaim ... 服务器上的端口已被占用。 等待回收完成。如果最后显示 is still held,自己释放该端口,或换一个 remotePort。
Remote reclaim finished: listen port N is still held (code=3) 清理未能释放端口。 在服务器上找出占用者,尝试 reclaimStaleSshSessions: true,或设置 remoteCleanupCommand。
stderr: ... Host key verification failed 或 REMOTE HOST IDENTIFICATION HAS CHANGED 服务器的密钥与保存的不一致。 如果这一变化在预期之中,从 known_hosts 中删除旧条目。如果不在预期之中,不要连接。
Reconnecting in 60s (attempt N) 一再出现,隧道始终没有显示 Tunnel up 每次连接都失败。 阅读每条重连消息之前的 stderr: 行。
隧道显示 Tunnel up,但该地址没有响应 中继这一侧正常,服务器那一侧有问题。 检查反向代理是否在运行并指向 remotePort,以及 remoteBind 是否与代理访问它的方式相符。
隧道连接、断开、再重连,循环往复 服务器或网络上有东西结束了连接。 日志中有退出码和 ssh 的消息。检查服务器的 sshd 日志。
消息一直在排队 required 为 true,且隧道未连接。 修好隧道,或把 required 设为 false。