0%

搭建稳定的ssh隧道

使用ssh可以创建反向连接、内网穿透、端口转发等,但网络是不稳定的,连接断开时有发生。通过断线检测+断线重连+开机自启可以创建一个稳定的ssh隧道。

背景

网上有现成的工具autossh专门用来建立稳定的ssh连接,不过经过测试效果不好,故障率较高(可能是没有正确配置导致)。另一方面,在windows上安装autossh比较麻烦,需要自己编译。在Linux使用下面的方案可以不用额外安装其它软件。

端口转发例子

以内网穿透为例,假设内网中有一台树莓派(以下简称客户端),想借助一台有外网IP的服务器(以下简称服务器)创建端口转发,以实现在外网访问到内网树莓派的ssh服务。
在树莓派上执行:

1
ssh -NT -R 1122:127.0.0.1:22 用户名@服务器IP

执行之后即可以在服务器上登录到树莓派的ssh:

1
ssh pi@127.0.0.1 -p 1122

由于TCP连接是不稳定的,所以基于TCP的ssh端口转发连接也是不稳定的。如果网络断开,可能提示:

1
Connection to xxx.xxx.xxx.xxx closed by remote host.

断线重连

断线重连很简单,ssh命令退出后,重新执行命令建立连接即可。伪代码如下:

1
2
3
while(true){
`ssh -NT -R 1122:127.0.0.1:22 用户名@服务器IP`
}

不过执行ssh命令需要输入密码,首先要配置ssh免密登录,在客户端执行:

1
2
$ ssh-keygen
$ ssh-copy-id 用户名@服务器IP

输入密码之后即完成免密登录配置,之后再执行ssh命令连接到服务器就不用再输入密码了。

下面使用systemctl来实现断线重连和开机自动运行。在客户端创建一个systemctl服务配置文件:

1
sudo vi /usr/lib/systemd/system/ssh-link.service

写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=ssh port forwarding service.

[Service]
Type=simple
ExecStart= /bin/sh -c 'ssh -NT -R 1122:127.0.0.1:22 用户名@服务器IP'
Restart=always
RestartSec=10
User=pi
Group=pi

[Install]
WantedBy=multi-user.target

其中:

  • UserGroup为执行ssh-keygen命令的用户和用户组。
  • Restart=always表示ssh命令退出后,等待RestartSec=10秒,然后重新执行。

保存后运行一下:

1
sudo systemctl start ssh-link

查看运行状态,正常情况如下:

1
2
3
4
5
$ sudo systemctl status ssh-link
● ssh-link.service
Loaded: loaded (/usr/lib/systemd/system/ssh-link.service; bad; vendor preset: enabled)
Active: active (running) since Sun 2020-11-08 23:00:33 CST; 3s ago
......

开机自启

配置开机启动:

1
2
$ sudo systemctl enable ssh-link 
Created symlink /etc/systemd/system/multi-user.target.wants/ssh-link.service → /usr/lib/systemd/system/ssh-link.service.

此时可以重启客户端,正常情况下,重启之后会自动建立ssh端口转发连接。

心跳检测

前面说到,ssh命令退出后,systemctl会重新执行ssh命令以建立连接。但有些特殊情况下,连接实际上断开了,但ssh命令没有结束。

例如服务器突然断电/网线被拔掉,服务器没有发送TCP reset包,所以客户端不知道连接断开,也就不会退出ssh命令。
同理,客户端突然断电,服务器也不知道客户端“挂了”。如果客户端随后重新联网并创建ssh端口转发,可能会提示服务器端口已被占用(因为服务器上之前的ssh会话还保持着)。

实际上,TCP连接是有心跳检测机制的,即TCP KeepAlive,不过它默认2小时发送一次心跳包,这实在是太长了。

服务器ssh配置

在服务器上编辑sshd配置文件/etc/ssh/sshd_config, 配置以下参数:

1
2
ClientAliveInterval 10
ClientAliveCountMax 3

其中:
ClientAliveInterval:参数表示如果服务器连续N秒没有收到来自客户端的数据包,则服务器会向客户端发送一条消息。
ClientAliveCountMax:表示如果服务器发送了N次数据到客户端都没有收到回应时,就会认为连接已经断开,服务器会结束会话、关闭监听的端口。

上述配置表示,如果服务器连续10秒没有收到客户端的数据,就会主动发送数据给客户端。连续发送了3次数据到客户端,都没有收到回复就断开连接。这意味着,网络断开后的最长30秒内,服务器就会关闭ssh会话

保存之后需要重新sshd服务:

1
sudo systemctl restart sshd

客户端配置

通过上述配置,服务器就可以检测客户端是否存活。同理,也需要修改客户端的配置,让客户端可以检测服务端是否存活。

在客户端编辑配置文件/etc/ssh/ssh_config,配置以下参数:

1
2
3
ServerAliveInterval 10
ServerAliveCountMax 3
ExitOnForwardFailure yes

ExitOnForwardFailure表示端口转发失败时退出ssh。因为有可能服务器上要监听的端口被占用了,可能导致转发失败,这时候自动退出,systemctl才会重新执行ssh。如果不退出,systemctl就会认为ssh转发成功了,导致ssh进程存在,但是转发通道实际上没有建立。
保存之后在客户端重启ssh:

1
sudo systemctl restart ssh

经过上述配置后,一个稳定的ssh端口转发连接就建立起来了(已经经过数月的实际测试,断线后会自动重连)。