SSH 反向隧道总断,我把它交给 autossh 和 systemd 才省心
起因
单位内网有一台机器跑了个内部看板,端口 5000,只在局域网能看。外头有一台云服务器有公网 IP,我就想着用 SSH 反向隧道把它带出去,省得折腾内网穿透那套。
第一版命令很朴素:
ssh -fN -R 2222:localhost:5000 user@vps
-f 后台跑,-N 不执行远程命令,-R 的含义是:在 VPS 上开 2222 端口,收到流量就通过这条 SSH 连接转发回我内网机器的 5000。
命令执行完没报错,但我在外面 curl http://vps-ip:2222 一直是 Connection refused。
第一个坑:默认只监听 127.0.0.1
这是 -R 最容易踩的一点。不加参数时,远端开的端口只绑在 127.0.0.1 上,也就是只有 VPS 自己能访问,外网进不来。
去 VPS 上确认一下就很清楚:
ss -lntp | grep 2222
# LISTEN 0 128 127.0.0.1:2222 0.0.0.0:*
看到 127.0.0.1 就说明绑错了。改法是两边一起动:
服务端 /etc/ssh/sshd_config 加一行:
GatewayPorts clientspecified
改完 systemctl restart sshd。这里顺便说一下,GatewayPorts 有三个值,no 是默认,yes 是强制全部绑 0.0.0.0(不太安全),clientspecified 是由客户端决定绑哪个地址,我一般用这个。
然后客户端显式写上绑定地址:
ssh -fN -R 0.0.0.0:2222:localhost:5000 user@vps
再 ss -lntp 就能看到 0.0.0.0:2222 了,curl 也通了。
第二个坑:进程还在,隧道已经死了
真正麻烦的是后面。这条隧道跑个一天半天就废了,但 ps aux | grep ssh 看进程还好端端在那儿,VPS 上端口也还在 LISTEN,就是 curl 的时候一直挂着不返回,直到超时。
原因是中间的网络设备(NAT、防火墙)静默把空闲连接掐了,两边都没收到 FIN 包,于是双双以为连接还活着。TCP 层面没察觉,SSH 自己也不主动发探测包,就成了假死。
解决办法是让 SSH 自己在通道里发心跳:
ssh -fN \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-R 0.0.0.0:2222:localhost:5000 user@vps
ServerAliveInterval=30 表示 30 秒没数据就发一个探测;ServerAliveCountMax=3 表示连续 3 次没回应就认定连接已死,退出。注意这个探测是走加密通道内部的,比 TCPKeepAlive 那种空 TCP ACK 靠谱,后者经常被中间设备糊弄过去。
ExitOnForwardFailure=yes 也挺关键,默认是 no,端口被占用时 ssh 只打个警告然后继续跑,看着像成功了其实没转发。加上之后会直接失败退出,方便让外层去重启。
第三步:换成 autossh
心跳只能发现问题,不能自己重连。autossh 就是干这个的,它盯着 ssh 子进程,退出了就重新拉起来。
sudo apt install autossh
autossh -M 0 -f -N \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-R 0.0.0.0:2222:localhost:5000 user@vps
-M 0 是把 autossh 自带的监控端口关掉。它默认会额外开一对端口做回声检测,我还得在公网机上给它放行,麻烦,而且老被端口扫描扫到。既然 ssh 自带的 ServerAlive 已经能探活,就让 ssh 自己判断、autossh 只管重启。
这里还有个隐式要求:必须配好密钥登录。交互式输密码的话 autossh 重启时卡在密码输入上,永远起不来。
ssh-keygen -t ed25519
ssh-copy-id user@vps
第一次连新 VPS 还会卡在 Are you sure you want to continue connecting (yes/no),自动化场景下加一句 -o StrictHostKeyChecking=accept-new,它只接受未知主机的密钥、但已知主机指纹变了仍然拒绝,比直接 no 安全一点。
第四步:用 systemd 兜底
-f 后台 fork 出来的进程没人管,机器重启就没了。所以我最后把它写成了一个服务:
[Unit]
Description=Reverse SSH tunnel for dashboard
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=tunnel
Environment=AUTOSSH_GATETIME=0
ExecStart=/usr/bin/autossh -M 0 -N \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-o StrictHostKeyChecking=accept-new \
-i /home/tunnel/.ssh/id_ed25519 \
-R 0.0.0.0:2222:localhost:5000 user@vps
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
AUTOSSH_GATETIME=0 一定要设。autossh 默认会等首次连接成功一小段时间,超时就自杀;设成 0 是关掉这个逻辑,否则开机时网络还没起来,它先把自己干掉了。
sudo systemctl daemon-reload
sudo systemctl enable --now tunnel.service
systemctl status tunnel
journalctl -u tunnel -f
安全上补两刀
隧道专用账号我只给了端口转发权限。VPS 的 /etc/ssh/sshd_config 末尾加:
Match User tunneluser
AllowTcpForwarding remote
GatewayPorts clientspecified
PermitOpen 127.0.0.1:*
ForceCommand /bin/false
这样就算密钥泄露,对方也只能转发、拿不到 shell。
现在的状态
跑了三周没再断过。我还在 VPS 上加了个五分钟一次的探活,curl 那个端口,连续两次失败就发通知到群里——毕竟隧道自愈归自愈,内网那台机器真宕机了它也是白搭,这两件事得分开看。
- 点赞
- 收藏
- 关注作者
评论(0)