通过代理服务器ssh连接内网服务器

经常遇到的场景是,公司或学校的服务器都不能直接在外网进行访问,但是存在一个代理服务器,这时候就可以利用代理服务器来访问内网服务器。

使用跳板机访问内网服务器的场景和姿势都很多,我这里只介绍我碰到的场景。

网络状态

场景为,有客户端pc A,代理服务器P,目标服务器T,我们的最终需求是直接ssh到T上。

1
2
3
+------+       +----------+      +----------+
| pc A | <---> | Server P | <--> | Server T |
+------+ +----------+ +----------+

目前我们可以做到的是,先在A上ssh连接到代理服务器P,然后继续ssh连接到T,可以简单描述为在A上执行下面的过程。

1
2
ssh userP@hostnameP -p portP
ssh userT@hostnameT -p portT

这种最朴素的方法需要两步ssh,但是能不能一步ssh就可以连接到T呢?

首先要问,为什么要一步ssh到T?

第一,这样可以简化ssh到目标服务器的过程,不需要两步ssh。第二,有些基于ssh的服务无法两步ssh,例如使用FileZilla等通过ssh浏览文件的软件,还有vscode Remote SSH 这样的插件,都需要一步ssh到目标服务器。

笔者面临的更复杂的场景

1
2
3
+------+       +------------+       +----------+       +----------+
| pc A | <---> | Firewall F | <---> | Server P | <---> | Server T |
+------+ +------------+ +----------+ +----------+

其中Firewall F是一台网关服务器,而当执行ssh userP@hostnameF -p portF时,会连接到P,这说明服务器P的ssh监听端口portP已经被转发到了网关F的portF上,但是笔者所在的网络中,这里的网关服务器不是用的简单的端口转发,而是使用了一个程序监听的portF,验证身份后再将该tcp的连接的内容转发到P的portP上。因此很多简单的方法都失效了。但是我在服务器P有一个端口portM,上可以建立从服务器P的端口portM到Firewall的连接,这是我最终能够实现目标的关键。

好了,下面介绍几种我尝试过的方法。前几种都是在描述的简单的场景下有用的方法,最后一种方法是对我的场景也有用的方法。

使用ssh的Proxy Jump功能

其实目前大部分系统自带的OpenSSH软件已经有了Proxy Jump功能,它exactly实现了我们第一个场景的需求。

在使用命令行直接ssh时,可以使用-J参数来使用该功能。下面的命令描述了简单的使用方法,只要这样一行ssh命令就可以进行把服务器P当作ssh跳板的功能。

1
ssh -J userP@hostnameP:portP userT@hostnameT:portT

也可以在~/.ssh/config中定义host来快捷地进行ssh访问

1
2
3
4
5
6
7
8
9
10
11
12
13
# ssh config file
# Server P
Host serverP
HostName hostnameP
User userP
Port portP

# Server T
Host serverT
HostName hostnameT
User userT
Port portT
ProxyJump serverP

这样只要使用

1
ssh serverT

就可以ssh到server T上了。另外这里只用了一步跳转,其实还可以进行更多步跳转,如下面的~/.ssh/config设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ssh config file
# Server P
Host serverP
HostName hostnameP
User userP
Port portP

# Server Q
Host serverQ
HostName hostnameQ
User userQ
Port portQ
ProxyJump serverP

# Server T
Host serverT
HostName hostnameT
User userT
Port portT
ProxyJump serverQ

这样执行ssh serverT后,会发生2步跳转,先经过服务器P,再经过服务器Q,最后到服务器T

使用ssh的ProxyCommand功能

老旧的OpenSSH程序是不支持ProxyJump功能的,这时候可以使用OpenSSH的ProxyCommand功能,也可以达到一样的效果。

1
ssh -o ProxyCommand="ssh -W %h:%p userP@hostnameP:portP" userT@hostnameT:portT

上面的一行命令也可以通过Server P进行跳转ssh

当然,这种方法也是可以通过~/.ssh/config配置简单设置的。

使用ssh的-tt参数

如果OpenSSH老到连ProxyCommand都不支持,那么还可以通过-tt参数实现。

1
ssh -tt userP@hostnameP:portP ssh -tt userT@hostnameT:portT

-tt可以在ssh建立后立刻执行下一命令

使用端口转发

如果不是像笔者一样属于第二个情景,即ServerP以外还有一个防火墙,那么前面描述的方法已经完全够用了。但是笔者的情景必须要使用另外的方法。

之前提到,在笔者的场景中,服务器P上有额外的端口portM,而我们可以将Server T的portT端口转发到Server P的portM上,这样我们访问hostnameP:portM就相当于访问hostnameT:portT

端口转发有多种实现方法,笔者在网关服务器F上没有操作权限,但是可以在ssh时使用LocalForward功能进行端口转发

例如,我们可以将Server P的端口portM转发到pc A的本地端口portA上,只需要在pc A上运行

1
ssh -L portA:localhost:portM userP@hostnameF:portF

这样一来,我在pc A上访问localhost:portA就相当于访问hostnameP:portM,下一步就是将hostnameT:portT转发到hostnameP:portM上。

不过要注意的是,LocalForward转发默认转发后只在本地监听端口,例如现在进行了一步转发后,pc A的sshd进程会监听localhost:portA,但是别的计算机是不能访问hostnameA:portA的。

但是由于我们需要转发hostnameP:portM,意味着portM是对其他ip也都可见的端口,那么就需要使用-g (GatewayPorts)选项,这代表着会监听所有监听来自所有ip的对portM的请求。

在Server P上运行

1
ssh -L portM:localhost:portT -g userT@hostnameT:portT

就可以在Server P上将hostnameT:portT转发到hostnameP:portM上,且整个内网都可以访问hostnameP:portM

这样一来,链条就打通了,首先在Server P上将hostnameT:portT转发到hostnameP:portM上,然后在pc A上将hostnameP:portM转发到localhost:portA上,之后只需要在pc A上运行

1
ssh userT@localhost:portA

就可以ssh到Server T上了

上面的命令都可以写入~/.ssh/config中简化配置,在pc A上,等效的config配置为

1
2
3
4
5
6
7
8
9
10
11
12
13
Host serverP
HostName hostnameF
Port portF
User userP
LocalForward portA localhost:portM

Host serverT
HostName localhost
Port portA
User userT
# You'd better use RSA key to verify, e.g., append the content of ~/.ssh/id_rsa.pub to ~/.ssh/authorized_keys in Server T
# IdentityFile ~/.ssh/id_rsa
# IdentitiesOnly yes

在Server P上,config配置为

1
2
3
4
5
6
Host serverT
HostName hostnameT
Port portT
User userT
LocalForward portM localhost:portT
GatewayPorts yes

配置完成后,在Server P上运行

1
ssh serverT

在pc A上运行下面的命令进行端口转发

1
ssh serverP

再在pc A上运行

1
ssh serverT

就可以直接ssh到Server T了,配置同样可以直接在vscode Remote SSH中使用。相对于使用vim写代码,个人更习惯通过vscode在serverT上写代码。

如果你的情境中Server P和Server T是一体的,即只是存在Firewall F和Server T,那么只需要把原Server P上的config配置放在Server T上,并在Server T上运行ssh serverT,在pc A的config设置也都改成server T的相关配置即可。

另外,笔者的环境下,使用vscode Remote SSH时必须在pc A上指定IdentifyFile文件进行ssh serverT