3. SSH 反向隧道实战:把内网机器“推”到公网

说实话,搞远程访问这么多年,我遇到最多的场景就是:客户的内网服务器没有公网 IP,但你又必须从外网连上去做维护。你想想看,总不能每次都让现场同事开 TeamViewer 吧?

SSH 反向隧道就是干这个的。它让内网机器主动连到你的公网跳板机,然后在跳板机上开一个监听端口。你连跳板机的这个端口,就等于连到了内网机器。说白了,就是让内网机器“反向”把门打开。

核心思想: 内网 → 公网(主动建连),外网 → 公网端口 → 隧道 → 内网服务。

3.1 SSH -R 命令详解

命令格式其实不复杂:

ssh -R [远程绑定地址:]远程端口:目标地址:目标端口 跳板机用户@跳板机IP

拆开来看:

  • -R:告诉 SSH 我要建反向隧道。
  • 远程绑定地址:默认只监听 127.0.0.1,想对外暴露就写 0.0.0.0。
  • 远程端口:在跳板机上开哪个端口等你连。
  • 目标地址:目标端口:内网机器上真正要访问的服务。

举个例子。内网机器上跑了 Jenkins,端口 8080。我想从外网访问它:

# 在内网机器上执行
ssh -R 9000:localhost:8080 user@your-bastion.com

这条命令的意思是:你连上跳板机后,让跳板机监听自己的 9000 端口。任何发到 9000 端口的数据,都通过 SSH 隧道转发回内网机器的 8080 端口。

我在项目中遇到过一个问题:默认情况下,跳板机上的 9000 端口只监听 127.0.0.1。也就是说,只有跳板机自己能连。如果你想让其他机器也能连,得加 GatewayPorts:

# 跳板机 /etc/ssh/sshd_config 里设置
GatewayPorts yes

# 然后重启 sshd
sudo systemctl restart sshd

# 内网机器重新建隧道,绑定 0.0.0.0
ssh -R 0.0.0.0:9000:localhost:8080 user@your-bastion.com
安全警告: 开启 GatewayPorts 意味着跳板机上的端口会暴露到公网。一定要配合防火墙白名单,只允许特定 IP 访问这些端口。我曾经见过有人直接开 0.0.0.0 然后被扫到,后果很严重。

3.2 内网穿透的典型场景

我整理了几个最常见的用法,你大概率会遇到:

场景 内网命令 外网访问方式
暴露 SSH 服务 ssh -R 2222:localhost:22 user@bastion ssh user@bastion -p 2222
暴露 Web 服务 ssh -R 8080:localhost:80 user@bastion 浏览器访问 http://bastion:8080
暴露数据库 ssh -R 3306:localhost:3306 user@bastion mysql -h bastion -P 3306
穿透多层内网 ssh -R 9090:10.0.1.5:9090 user@bastion 直接连 bastion:9090

注意最后那个“多层内网”的场景。有时候内网机器不能直接访问目标服务,但可以通过它做跳板。比如内网机器 A 能访问 10.0.1.5:9090,那就在 A 上建隧道,把 10.0.1.5:9090 映射出来。

3.3 autossh:断了自动重连

SSH 连接说断就断。网络抖动、机器重启、NAT 超时……随便一个原因隧道就掉了。你总不能 24 小时盯着吧?

autossh 就是干这个的。它会监控 SSH 进程,一旦发现连接断了,自动帮你重连。

安装很简单:

# Ubuntu/Debian
sudo apt install autossh

# CentOS/RHEL
sudo yum install autossh

用法和 SSH 几乎一样,就是把 ssh 换成 autossh:

autossh -M 0 -R 9000:localhost:8080 user@your-bastion.com

-M 0 的意思是关闭 autossh 自带的监控端口,改用 SSH 的保活机制。我个人习惯用这个参数,省心。

为了让隧道更稳定,我建议在 SSH 配置里加上保活参数:

# 在 ~/.ssh/config 里添加
Host bastion
    HostName your-bastion.com
    User user
    ServerAliveInterval 30
    ServerAliveCountMax 3
    ExitOnForwardFailure yes
  • ServerAliveInterval 30:每 30 秒发一个心跳包。
  • ServerAliveCountMax 3:连续 3 次心跳没回应就断开。
  • ExitOnForwardFailure yes:端口转发失败就直接退出,让 autossh 重连。
我的经验: 有些运营商的 NAT 超时时间很短,可能 5 分钟就断了。把 ServerAliveInterval 设成 15 秒,基本能稳住。但别设太短,1 秒一次的话,日志会刷得飞起。

3.4 systemd 服务化部署

autossh 虽然能自动重连,但机器重启后还得手动启动。生产环境必须做成系统服务。

下面是我一直在用的 systemd 服务单元:

sudo vim /etc/systemd/system/reverse-tunnel.service
[Unit]
Description=SSH Reverse Tunnel Service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=autossh-user
ExecStart=/usr/bin/autossh -M 0 -N -R 9000:localhost:8080 user@your-bastion.com
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=10
StartLimitInterval=0

[Install]
WantedBy=multi-user.target

几个关键点:

  • After=network-online.target:等网络就绪再启动,避免开机时网络没通就启动失败。
  • Restart=always:挂了就重启,配合 RestartSec=10,每 10 秒重试一次。
  • StartLimitInterval=0:不限制重启次数,防止频繁重启后被 systemd 限制。
  • -N:不执行远程命令,只建隧道,省资源。

然后启用并启动:

sudo systemctl daemon-reload
sudo systemctl enable reverse-tunnel.service
sudo systemctl start reverse-tunnel.service

查看状态:

sudo systemctl status reverse-tunnel.service

如果一切正常,你会看到 active (running)。试试断开网络再恢复,autossh 会自动重连,systemd 会确保 autossh 进程永远活着。

注意: 不要在 systemd 服务里直接用 root 跑 SSH。创建一个专用系统用户,权限最小化。我曾经见过有人用 root 跑隧道,结果跳板机被攻破后内网全部沦陷。

3.5 整体架构图

下面这张图展示了整个反向隧道的工作流程:

SSH 反向隧道整体架构 内网环境 内网服务 localhost:8080 SSH 客户端 autossh + systemd 公网环境 跳板机 端口 9000 外网用户 ssh bastion -p 9000 SSH 反向隧道 数据流:外网 → 跳板机:9000 → 隧道 → 内网服务:8080

流程很清晰:内网机器上的 autossh 主动连跳板机,建好隧道。外网用户访问跳板机的 9000 端口,数据通过隧道直达内网服务的 8080 端口。整个过程对用户透明,他感觉就像在访问一个公网服务。

3.6 避坑指南

最后分享几个我踩过的坑:

  • 端口冲突:跳板机上的端口可能被其他服务占用。建隧道前先 netstat -tlnp | grep 端口号 检查一下。
  • 防火墙拦截:跳板机的 iptables 或云平台安全组没放行对应端口。记得加规则。
  • SSH 密钥权限:systemd 跑 autossh 时用的用户,必须有权限读取私钥。私钥权限必须是 600。
  • 日志排查:隧道建不起来先看 journalctl -u reverse-tunnel.service -f,错误信息一般都在那里。