背景

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

端口转发例子

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

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

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

ssh [email protected] -p 1122

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

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

断线重连

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

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

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

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

输入密码之后即完成免密登录配置,之后再执行ssh命令连接到服务器就不用再输入密码了。
下面使用systemctl来实现断线重连和开机自动运行。在客户端创建一个systemctl服务配置文件:

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

写入以下内容:

[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秒,然后重新执行。
    保存后运行一下:

    sudo systemctl start ssh-link

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

    $ 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 ......

    开机自启

    配置开机启动:

    $ 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, 配置以下参数:

    ClientAliveInterval 10
    ClientAliveCountMax 3

其中:
ClientAliveInterval:参数表示如果服务器连续N秒没有收到来自客户端的数据包,则服务器会向客户端发送一条消息。
ClientAliveCountMax:表示如果服务器发送了N次数据到客户端都没有收到回应时,就会认为连接已经断开,服务器会结束会话、关闭监听的端口。
上述配置表示,如果服务器连续10秒没有收到客户端的数据,就会主动发送数据给客户端。连续发送了3次数据到客户端,都没有收到回复就断开连接。这意味着,网络断开后的最长30秒内,服务器就会关闭ssh会话
保存之后需要重新sshd服务:

sudo systemctl restart sshd

客户端配置

通过上述配置,服务器就可以检测客户端是否存活。同理,也需要修改客户端的配置,让客户端可以检测服务端是否存活。
在客户端编辑配置文件/etc/ssh/ssh_config,配置以下参数:

ServerAliveInterval 10
ServerAliveCountMax 3

保存之后在客户端重启ssh:

sudo systemctl restart ssh

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

03-05 23:15