由于访问大学的计算资源需要连接学校内网。但虚拟专用网需要的安全凭证过于麻烦,需要密码+一次性密码双重认证。于是我利用了在大学里的一台能访问公网的服务器(无需公网IP!)向家中服务器和其他内网服务器搭建了几条SSH隧道,实现了公网通过SSH访问内网服务器。

网络拓扑

                                Internet
                                   |
                        +----------------------+
                        |  Public Server       |
                        |  IP: 125.239.204.25  |
                        +----------------------+
                                   |
                                   |
                          +-----------------+
                          |     Firewall    |
                          | (outbound only) |
                          +-----------------+
                                   |
                           University Network
                                   |
                  +---------------------------------+
                  |                                 |
       +----------------------+         +----------------------+
       | Server V             |         | Server A             |
       | IP: 130.216.0.2      |---------| IP: 130.216.0.3      |
       | Public Access        |         | Public Access        |
       +----------------------+         +----------------------+
                  |                                 |
       +----------------------+         +----------------------+
       | Server C1            |         | Server C2            |
       | IP: 130.216.4.2      |---------| IP: 130.216.0.3      |
       | (No Internet Access) |         | (No Internet Access) |
       +----------------------+         +----------------------+

其中,Server V代表一台DGX Station V100,Server A代表一台DGX Station A100它们都接入了学校内网并且在防火墙下允许访问公网;Server C1C2代表Cluster 1和2,它们接入了学校内网但不允许访问公网。一台具有公网IP的服务器作为桥梁来建立SSH隧道。

SSH隧道

所谓隧道,是指经过SSH身份认证之后建立的一条持久化TCP连接。这条隧道不仅可以传输终端命令,也可以传输网页等一切基于TCP协议的数据。

生活中的隧道没有正反,但SSH隧道分为正向隧道和反向隧道。SSH隧道本身是双向的,类似于生活中的隧道,但我们规定:发出命令的方向为正向,返回结果的方向为反向。

正向隧道

假如一条隧道由发出命令的一方建立,那么这条隧道就叫做正向隧道。在SSH命令中用参数-L表示。

例如,想要在Server V的8955端口和Server C1的22端口之间搭建一条正向隧道,使得访问Server V的8955端口就像访问Server C1的22端口一样,就应该在Server V的终端使用如下命令:

ssh -N -L 8955:localhost:22 <credential>

<credential>指在~/.ssh/config中配置对方的的登录凭据。也可以使用<user>@<hostname>的形式。注意,这个凭据仅用于建立SSH隧道的身份认证用。在使用SSH隧道时,可以为任何用户。

在这条命令中,-N选项代表在执行命令后不进入目标机器的终端。-L选项表示建立正向隧道,它的参数是这条隧道的正向表示,即从发出命令的端口(8955)传输数据到接收命令的机器和端口(localhost:22)。

反向隧道

假如一条隧道由接收并执行命令的一方建立,那么这条隧道就叫做反向隧道。在SSH命令中用参数-R表示。

例如,想要在公网服务器的8933端口和防火墙之下的Server V的22端口之间搭建一条反向隧道,使得访问公网服务器的8933端口就像访问Server V的22端口一样,就应该在Server V的终端使用如下命令:

ssh -N -R 8933:localhost:22 <credential>

<credential>指在~/.ssh/config中配置对方的的登录凭据。也可以使用<user>@<hostname>的形式。注意,这个凭据仅用于建立SSH隧道的身份认证用。在使用SSH隧道时,可以为任何用户。

在这条命令中,-N选项代表在执行命令后不进入目标机器的终端。-R选项表示建立反向隧道,它的参数是这条隧道的正向表示,即从发出命令的端口(8933)传输数据到接收命令的机器和端口(localhost:22)。

建立SSH隧道实现内网服务器公网访问

根据正向隧道和反向隧道的原理,我们可以:

  • Server V上配置一条到公网服务器的反向隧道,实现公网访问Server V

  • Server V上配置数条到内网服务器的正向隧道,实现一台服务器代表所有内网服务器

  • Server V上配置数条到公网服务器的反向隧道,实现公网访问所有内网服务器

基于以上,即可实现访问公网服务器的不同SSH 端口来连接到不同的内网服务器。

但是,还有一些杂项需要注意:

开启SSH服务器作为网关配置

默认的sshd配置中,在建立一条隧道之后,如果使用netstat -tuln命令来查看端口占用的话,会发现SSH服务仅监听了本机传入的TCP连接:

tcp        0      0 127.0.0.1:8955          localhost:*               LISTEN

这是因为:sshd配置中,GatewayPorts默认是关闭的。这会使得建立隧道时,仅允许本机连接该端口。使用管理员权限修改sshd配置文件:

vim /etc/ssh/sshd_config

找到GatewayPorts一行,将注释去除并将no改为yes即可。

然后重新加载SSH服务:

sudo systemctl reload sshd

或者,重启SSH服务:

sudo systemctl restart sshd

使用autossh保持连接活跃

一旦出现网络波动或者sshd进程被清理,隧道就永久性地断掉了,而autossh可以自动断线重连,保持隧道的健壮。

autosshssh的使用方法完全对等,仅需将程序名称改为autossh,例如:

autossh -N -R 8933:localhost:22 <credential>

同时,还可以加入ssh客户端配置,使得连接保持活跃,无需断线重连:

autossh -M 0 -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" -N -R 8933:localhost:22 <credential>

使用脚本一键开启所有所需隧道

综上,针对整个网络拓扑,我们可以有这样一个脚本,仅需在Server V上运行一次,就可以在公网访问所有内网服务器:

#!/bin/bash

nohup autossh -M 0 -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" -N -R 8933:localhost:22 <public-credential> > autossh_public.log 2>&1 &

nohup autossh -M 0 -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" -N -L 8944:localhost:22 <Server-V-credential> > autossh_serverv.log 2>&1 &

nohup autossh -M 0 -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" -N -R 8944:localhost:8944 <public-credential> > autossh_public_8944.log 2>&1 &

nohup autossh -M 0 -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" -N -L 8955:localhost:22 uoa > autossh_serverc1.log 2>&1 &

nohup autossh -M 0 -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" -N -R 8955:localhost:8955 <public-credential> > autossh_public_8955.log 2>&1 &

nohup autossh -M 0 -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" -N -L 8966:localhost:22 uoa2 > autossh_serverc2.log 2>&1 &

nohup autossh -M 0 -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" -N -R 8966:localhost:8966 <public-credential> > autossh_public_8966.log 2>&1 &

关闭服务

当不需要SSH隧道时,可能需要将其关闭。但是由于进程运行在后台,且nohup命令使得即使退出终端也不会结束进程,我们需要获取autosshPID手动关闭。

使用ps命令查看autossh所有进程的详细信息:

ps aux | grep autossh

使用pgrep查看autossh的所有PID:

pgrep autossh

若要一键杀掉所有autossh的进程,可以结合xargskill命令:

pgrep autossh | xargs kill