通过 SSH 隧道访问中继
中继可以向你的某台服务器打开一个 SSH 连接,并请该服务器把一个端口转发回中继。之后你通过服务器的地址访问中继,家里没有任何程序对互联网监听。ssh 进程由中继自己启动、监视和重启。
如果你已经有一台带公网地址、可以 SSH 登录的服务器,就选这种方式。如果你没有服务器,内置的 Cloudflare Tunnel 不需要服务器。
你需要什么 #
- 中继主机的
PATH中有ssh程序(或者用command指定另一个,见下文)。 - 服务器上的一个 SSH 登录,用密钥登录且不弹出任何提示。中继以批处理模式运行
ssh,因此无法输入密码或密钥口令。带口令的密钥只有在 SSH agent 持有它时才能用。先在中继主机上测试:ssh relay@relay.必须不询问任何内容就能登录。example. com - 服务器上一个供隧道使用的空闲端口(
remote)。Port - 服务器上有程序通过 HTTPS 提供该端口:Web 服务器或反向代理,例如 Caddy 或 nginx。隧道传输的是明文 HTTP,所以没有它的话,一旦端口可以访问,你的令牌和对话就会未经加密地穿过互联网。参见在反向代理后运行中继。
中继不需要任何特殊的 sshd 设置。remote 为 loopback 时,它请求转发时不带绑定地址,因此由服务器自己的 Gateway 设置决定端口在哪里开放。在默认值 no 下,端口只在服务器本机开放,反向代理是唯一面向互联网的东西。设为 public 时,中继请求所有网络接口(*:),而这个请求只有在服务器的 sshd 允许时才生效(Gateway 设为 clientspecified 或 yes)。
设置 #
在 config. 中添加一个 ssh 块(参见文件位置),然后用 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://,用你的访问令牌登录。中继的启动横幅会打印一行 Remote:,oar url 会打印一行 Tunnel URL: 并附带二维码。两者都是 https:// 加上 host 的值,不带端口,也不检查隧道是否已连通,因此只有当你的代理在标准端口上提供 HTTPS 时它们才正确。
sshTunnel 的键 #
所有键都位于 ssh 块中。本表与 config.json 中的同一份列表一致。
| 键 | 类型 | 默认值 | 作用 |
|---|---|---|---|
mode |
"disabled" 或 "managed" |
"disabled" |
managed 让中继启动并重启隧道。 |
user |
字符串 | 无 | 服务器上的 SSH 用户。托管模式下必填。 |
host |
字符串 | 无 | 服务器的主机名或地址。托管模式下必填。 |
remote |
数字 | 无 | 在服务器上开放的端口,1 到 65535。托管模式下必填。 |
remote |
"loopback" 或 "public" |
"loopback" |
loopback 请求端口时不带绑定地址(由服务器的 Gateway 决定,参见你需要什么);public 请求所有网络接口。其他任何值都按 loopback 处理。与 localhost 无关。 |
identity |
字符串 | 无 | 私钥文件;~ 会被展开。不设时,ssh 使用你的 agent 及其默认密钥。 |
command |
字符串 | ssh |
SSH 程序,也用于服务器上的清理。只写名称时在 PATH 中查找;带斜杠的路径按原样使用,相对路径从中继的程序文件夹算起。 |
required |
布尔值 | false |
隧道未连接期间,中继不从队列中取消息。参见必需隧道。 |
auto |
布尔值 | true |
远程端口已被占用时,在服务器上运行清理并重试。只有 false 会关闭它。 |
reclaim |
布尔值 | false |
作为该清理的一部分,同时结束你自己在服务器上的空闲 SSH 会话。 |
remote |
字符串 | 内置 | 你自己的清理命令,在服务器上代替内置命令运行。 |
旧写法 "enabled": true 仍然有效,表示 "mode": "managed",但仅限 mode 未设为 disabled 或 managed 时。
中继把服务器的 remote 转发到它自己在 127. 上的端口(除非你改过 port,否则是 3333)。设为 public 时,转发以 *: 的形式请求,否则以 remote 的形式请求。
以下设置是固定的,没有对应的键:中继运行 ssh -N,并带上 Server、Server、Batch、Strict 和 Exit。连续三次检查(间隔 30 秒)都没有回应时,ssh 会放弃该连接。
如果缺少必填值(托管模式下的 user、host 或有效的 remote),中继照常启动,在日志中记下缺的是哪一个,并让隧道保持关闭。
运行方式 #
启动。 中继开始监听后就启动隧道。ssh 运行满 1.2 秒且服务器没有拒绝转发时,中继就认为隧道已连接。日志随后显示 Tunnel up to user@host remote port N。
重连。 ssh 退出后,中继会等待一段逐渐增长的时间再启动它:5、10、20、40 秒,然后是 60 秒,此后一直保持 60 秒。一个持续超过 30 秒的连接断开后,计数重新开始。停止中继会同时停止隧道和重试。
主机密钥。 中继主机从未见过的服务器,会在第一次连接时被接受并记住。此后主机密钥一旦改变,ssh 就会拒绝连接,直到你从 known_ 中删除旧条目。
必需隧道 #
设置 required: true 后,队列从启动起一直暂停到隧道连接上为止,之后每次隧道断开也会再次暂停。你发送的消息在队列中等待,隧道连通后再运行。如果没有隧道,中继对你就毫无用处,就使用这个选项。因缺少某个值而无法启动的隧道同样会让队列保持暂停。
远程端口被占用时 #
如果服务器报告端口无法绑定,通常是因为之前某个隧道遗留的会话还在,而服务器还没发现它已经失效。除非 auto 为 false,中继会再连接服务器一次,并在那里运行清理命令。
这些选项会在你的服务器上运行命令
清理以你的 SSH 登录身份运行,并会结束进程。它会结束该用户下任何监听远程端口的进程,不管它是不是遗留的隧道。启用 reclaim 时,它还会结束该用户的每个空闲 SSH 会话,其中也包括同一登录在其他端口上的其他 ssh -N -R 隧道。remote 会运行你写的任何内容。在让这些选项保持开启之前,先读懂每一步做了什么。
内置清理按以下顺序执行:
- 检查端口是否被占用(用
ss,没有则读取内核的监听套接字列表)。如果没有被占用,就停止并报告成功。 - 找出监听该端口的进程(用
lsof,没有则用fuser,取存在的那个),向它们发送终止信号(kill),然后等待一秒。你的登录无权发送信号的进程会留下。如果端口此时已空闲,就报告成功。 - 仅在
reclaim且端口仍被占用时:向你的用户下每个没有子进程的Stale Ssh Sessions: true sshd会话发送终止信号,然后等待一秒。这些正是ssh -N -R留下的会话。运行着 shell、编辑器远程服务器或其他任何程序的会话有子进程,会被跳过,运行清理本身的会话也会被跳过。 - 如果端口仍被占用,清理以退出码 3 退出,中继在日志中记为
is still held。
设置了 remote 时,你的命令会代替以上所有步骤运行,reclaim 不再起作用。中继通过第二个 ssh 登录、用同一个密钥、以批处理模式把它作为 sh -lc '<command>' 运行。端口空闲时它必须以 0 退出,端口仍被占用时以其他退出码退出。
清理释放了端口后,中继在一秒后重试,最多连续三次。清理失败后,或者 auto 关闭时,中继使用上文的常规等待时间。
查看隧道状态 #
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 ssh对象,包含Tunnel connected、connected、Since reconnect、Attempts last、Error blocking、required、mode、valid、host、remote和Port remote。请带上你的令牌获取它。Bind Mode
ssh 结束后,last 为 exit:<code>;服务器拒绝端口后为 remote-;配置块不完整时为 invalid- 或缺少值的那条消息;隧道连通时为空。
安全 #
隧道一旦连通,中继就能从互联网访问,而陌生人与你的智能体(它们会在你的机器上运行命令)之间只隔着访问令牌。因此:
- 让
remote保持为Bind loopback,并在服务器上用 HTTPS 挡在前面。设为public时,任何能访问服务器隧道端口的人都能通过明文 HTTP 访问中继。 - 访问令牌是唯一的一把锁。参见访问令牌与谁能访问中继和 OAR 暴露什么。
- 发往中继内部会话 worker 路径的请求,如果带有 Cloudflare 标记请求头
cf-或你在ray tunnel中列出的请求头,中继会拒绝。普通反向代理不会添加其中任何一个,所以如果你的代理会添加某个你希望拦截的请求头,请把它列出来。参见 config.json。Marker Headers
疑难解答 #
先看日志中的 [ssh- 行。以 stderr: 开头的行是 ssh 自己打印的内容。
| 你看到的 | 含义 | 怎么做 |
|---|---|---|
ssh(host 同理) |
缺少必填值。隧道保持关闭。 | 设置它,然后运行 oar restart。 |
ssh |
端口缺失,或不是 1 到 65535 之间的数字。 | 改正后重启。 |
Tunnel mode disabled; running direct relay only. |
mode 不是 managed。 |
设置 "mode": "managed"。 |
stderr: ... Permission denied (publickey) |
服务器没有接受你的密钥。批处理模式下不会提示输入密码。 | 设置 identity,并在中继主机上手动测试同样的 ssh user@host。 |
Process exited (code= 反复出现 |
ssh 无法连接或登录。 |
手动运行日志中记录的 ssh 命令,阅读它的错误信息。 |
stderr: ... remote port forwarding failed for listen port N,随后是 Attempting remote reclaim ... |
服务器上的端口已被占用。 | 等待回收完成。如果最后显示 is still held,自己释放该端口,或换一个 remote。 |
Remote reclaim finished: listen port N is still held (code= |
清理未能释放端口。 | 在服务器上找出占用者,尝试 reclaim,或设置 remote。 |
stderr: ... Host key verification failed 或 REMOTE HOST IDENTIFICATION HAS CHANGED |
服务器的密钥与保存的不一致。 | 如果这一变化在预期之中,从 known_ 中删除旧条目。如果不在预期之中,不要连接。 |
Reconnecting in 60s (attempt N) 一再出现,隧道始终没有显示 Tunnel up |
每次连接都失败。 | 阅读每条重连消息之前的 stderr: 行。 |
隧道显示 Tunnel up,但该地址没有响应 |
中继这一侧正常,服务器那一侧有问题。 | 检查反向代理是否在运行并指向 remote,以及 remote 是否与代理访问它的方式相符。 |
| 隧道连接、断开、再重连,循环往复 | 服务器或网络上有东西结束了连接。 | 日志中有退出码和 ssh 的消息。检查服务器的 sshd 日志。 |
| 消息一直在排队 | required 为 true,且隧道未连接。 |
修好隧道,或把 required 设为 false。 |