delphij's Chaos

选择chaos这个词是因为~~实在很难找到一个更合适的词来形容这儿了……

20 Jun 2022

作弊条:SSH 的 ProxyJump 跳板服务

问题

有些环境中,SSH 服务器可能无法从 Internet 直接访问(例如,SSH 服务器可能使用的是一个私有 IP 地址,或是 Internet 服务提供商没有提供 IPv6 服务,而 SSH 服务器只提供 IPv6 服务)。

考虑到 SSH 已经进行了相互认证(连接时客户端会验证服务器的公钥是否与已知公钥,例如 ~/.ssh/known_hosts, 或是通过 DNSsec 发布的 SSHFP RR 匹配;服务器端则会验证用户是否能证明自己拥有与授权公钥对应的私钥), 因此比较常见的解决方法便是使用 VPN、在防火墙上穿孔,或是使用代理服务器。

由于 SSH 自身也提供了许多转发功能,因此如果中间的跳板服务器也提供 SSH 服务, 便可以使用这些跳转服务器直接作为代理服务器来用。与前面那些传统方法相比, 这样做的优点是避免了安装额外的软件,也不需要特别指定端口。

ssh -W (“netcat mode”)

OpenSSH 提供了一个 -W 选项。该选项的作用是让 SSH 服务器打开到某一特定 IP/端口 的 TCP 连接, 并将标准输入输出与该连接绑定,这与 nc(1) 类似。 此功能最早在 2010 年由 OpenSSH 5.4 引入。

传统上,OpenSSH 支持一个名为 ProxyCommand 的选项,该选项可以让系统在连接时在本地运行一个命令。 故而,我们可以把 ProxyCommand 写作 ssh -W %h:%p username@<跳板服务器> 来告诉 ssh(1) 在连接服务器时,首先连接 username@<跳板服务器> 并用其作为代理服务器来提供服务,如下图所示。

+-------+       +-------+       +--------------------+
| 客户机 |------>| 跳板机 |------>| 另一网络上的SSH服务器 |
+-------+       +-------+       +--------------------+

不过,对于比较复杂的网络来说,有可能需要使用多层跳板。ProxyCommand 仍然可以满足此需求, 但写起来会复杂不少。考虑到使用 ssh -W 是一种常见情形,在 OpenSSH 7.5 中引入了一个新的选项 ProxyJump,它可以直接指定多个跳板机,并在每一跳使用不同的用户名/端口来进行连接。 由于该功能大致上时 ProxyCommand 的子集,因此它和 ProxyCommand 同时在配置中出现时, 只有后出现的那一个生效。

根据条件选择是否使用跳板

使用跳板机可能并非在所有情况下都适合,例如如果跳板机完成的功能是接受 IPv4 的 SSH 连接请求, 并提供到 IPv6 网络的连接服务,那么在一个有 IPv6 服务的地方可能就无所谓是否使用跳板机了。

OpenSSH 的 Host 配置只支持做模式匹配(例如: *.example.com)而无法灵活地判断当前网络环境。 自然,我们可以使用脚本去让系统使用不同的 ~/.ssh/config 文件,但这种外科手术式的做法并不美观。 OpenSSH 还提供了一个 Match 配置,除了模式匹配之外, 它还支持使用一个可执行文件来判断当前主机是否匹配,这样就可以做更加灵活的判断了。

下面是一个简单的此类脚本,其原理是通过调用某个支持 GET 方法的 API 来判断是否可以直接连通内网并据此返回是否使用跳板的结果:

#!/bin/sh

HASH=$(fetch -T 1 -qo - http://api.internal.example.com/reachable 2>/dev/null | sha512)
if [ ${HASH} = "fa706818d7cb88e278eed38b528269eeffce175ccb118fc9665b547e4742758b6d1da65786da56a5cc755b1f86789d89e2e7ff31b228b111420d9424252a74c1" ]; then
        # 签名匹配:网络可直达,返回 1 表示不应使用跳板
	exit 1
else
        # 其他情况:网络不可达,或者 API 在 1 秒内无响应,使用跳板
	exit 0
fi

或者,下面的脚本可以判断是否能够连上 IPv6 网络:

#!/bin/sh

# 解析即将连接的域名的AAAA记录
AAAA_COUNT=$(dig "$1" AAAA +short | wc -l)

# 如果没有IPv6地址,则无需使用跳板
[ 0 = "${AAAA_COUNT}" ] && exit 1

# 如果访问 Google 的 generate_204 超时或出错,则表示很可能 IPv6 不通,需要使用代理
# 注意此做法假定使用的是正常的互联网,如果不是请换成其他可用的 API。
curl -6 --connect-timeout 0.5 https://www.gstatic.com/generate_204 > /dev/null 2>&1 || exit 0

# 反之,直接连接
exit 1

与之对应的 ssh_config~/.ssh/config 配置如下:

Match host 10.*,*.internal.example.com exec "/usr/local/bin/need_proxy %h"
	ProxyJump trampoline.example.com

其中 trampoline.example.com 是一台在 Internet 上可达的 SSH 服务器作为跳板。 第一个脚本不需要 %h 参数,其他可以使用的参数请参见 ssh_config(5)