SSH实现多跳代理


背景

我们实验室有两台服务器,其中一台无法从外网访问,另一台设置了内网穿透。我平常在无法从外网访问的那台服务器上做实验,所以如果在实验室外要连接那台服务器就需要将设置了内网穿透的服务器作为跳板,通过两次ssh登进实验服务器。

最近我的实验需要联网下载包,但是服务器要联网需通过登录认证服务网页进行认证,而服务器又没有安装远程桌面,直接通过命令行访问认证网页实在是力不从心。所以我需要想办法设置代理,使得我可以通过自己电脑上的浏览器通过实验服务器访问认证网页,从而登录上网。

总结一下需求,就是网络中有A、B、C、D四个节点,其中A和B互联,B和C互联,C和D互联,如何设置使得A能访问D。

方法

考虑简单情况,如果B和D是互联的,那么就可以直接通过ssh的Socks代理实现功能,具体步骤如下:

  1. 在A运行:ssh -N -D 127.0.0.1:Aport Busername@Bhostname。其中Aport可以是A上任意闲置的端口。
  2. 在A中打开浏览器,设置浏览器的代理为socks5://127.0.0.1:Aport,然后输入D的网址,即可访问认证网页。

实际上,相当于A对D的请求被ssh代理转发到B上,由B的sshd进程访问D。所以如果B服务器要上网,通过这种方法在D上认证后能够上网的就是B。那么如何将A对D的请求转发到C上,由C访问D呢?

失败方法

我一开始考虑的方法是利用ssh的端口转发功能,端口转发实现的是将远程服务器的端口映射到本地端口。这么说是不是很抽象,然而大部分博客就是这样说的,能看懂就见鬼了。以我的需求做例子,在B上运行命令:ssh -L Bport:Dhostname:Dport Cusername@Chostname,其中Bport可以是B上任意闲置的端口,实现的功能就是可以通过访问Bport端口透过C访问D了。那么理论上我就可以通过前面所说的Socks代理方法用A访问D:

  1. 在A运行:ssh -N -D 127.0.0.1:Aport Busername@Bhostname。其中Aport可以是A上任意闲置的端口。
  2. 在A中打开浏览器,设置浏览器的代理为socks5://127.0.0.1:Aport,然后输入https://127.0.0.1:Bport,通过B代理透过C访问D。

为了测试,我在B上运行curl -I https://127.0.0.1:Bport,连是能连,但返回了证书错误,说明如果验证网页是http,方法应该是可行的。可惜认证网页是https,让我不得不去寻找其他方法。

顺带一提,ssh的端口转发功能分为两种,一种是本地端口转发,一种是远程端口转发,前面使用参数-L的方法是本地端口映射,如果要使用远程端口映射,则在C上运行命令ssh -R Bport:Dhostname:Dport Busername@Bhostname,其中Bport可以是B上任意闲置的端口,实现的功能和本地端口映射完全等价,不过本地端口转发是B上的ssh负责监听,转发给C上的sshd负责请求;远程端口转发是B上的sshd负责监听,转发给C上的ssh负责请求,ssh和sshd互换了身份,当然对于我的需求,两种方法都不满足。

成功方法

我在网上搜相关资料的时候,意外搜到一个给ssh本身添加代理的方法,顿时有了灵感。如果我能通过某种方法,使A能够直连到C,这样我就可以将C作为Socks5代理连到D了。那么如何使A直连到C呢?给ssh本身添加代理就是解决方案!具体步骤如下:

  1. 在A运行:ssh -p Bport -N -D 127.0.0.1:Aport1 Busername@Bhostname,其中Aport1可以是A上任意闲置的端口。
  2. 在A运行:ssh -o ProxyCommand='C:\Program Files\Git\mingw64\bin\connect.exe -S 127.0.0.1:Aport1 %h %p' -p Cport -N -D 127.0.0.1:Aport2 Cusername@Chostname,其中Aport2可以是A上任意和Aport1不一样的闲置端口。
  3. 在A中打开浏览器,设置浏览器的代理为socks5://127.0.0.1:Aport2,然后输入D的网址,即可访问认证网页。

注意A是Windows系统,所以给ssh添加代理的方法是使用connect.exe,Linux下使用的是Netcat,网上有很多相关资料。另外就是这个connect.exe似乎是Git自带的一个程序,我刚好电脑里有Git就用上了,不知道有没有别的办法。BportCport分别是服务器B和服务器C开放SSH的端口,如果是默认的22端口则可以不加此参数。

这个方法的意思是先建立一个B服务器的代理,然后建立一个通过B代理连接C服务器的代理,再通过C代理连接D。从A出发的对D的请求,先通过第二条ssh指令转变为往C发送,而往C发送的请求通过第一条ssh指令转变为往B发送,B的sshd收到以后往C发送,C的sshd收到以后往D发送,就实现了功能。

扩展

基于该方法,不难将其扩展到任意跳服务器的代理。比如将我需求中的两跳服务器改为三跳,即有A、B、C、D、E五个节点,A和B互联,B和C互联,C和D互联,D和E互联,怎么从A连到E。参考前面方法,只需要在A执行三个ssh代理命令即可:

ssh -p Bport -N -D 127.0.0.1:Aport1 Busername@Bhostname
ssh -o ProxyCommand='C:\Program Files\Git\mingw64\bin\connect.exe -S 127.0.0.1:Aport1 %h %p' -p Cport -N -D 127.0.0.1:Aport2 Cusername@Chostname
ssh -o ProxyCommand='C:\Program Files\Git\mingw64\bin\connect.exe -S 127.0.0.1:Aport2 %h %p' -p Dport -N -D 127.0.0.1:Aport3 Dusername@Dhostname

三条命令在不同的命令行窗口下执行。用浏览器设置Socks5代理端口为Aport3,就能访问E了。再多跳数也是如此,跳数多少就执行多少条ssh代理命令。