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
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 重连。
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 进程永远活着。
3.5 整体架构图
下面这张图展示了整个反向隧道的工作流程:
流程很清晰:内网机器上的 autossh 主动连跳板机,建好隧道。外网用户访问跳板机的 9000 端口,数据通过隧道直达内网服务的 8080 端口。整个过程对用户透明,他感觉就像在访问一个公网服务。
3.6 避坑指南
最后分享几个我踩过的坑:
- 端口冲突:跳板机上的端口可能被其他服务占用。建隧道前先
netstat -tlnp | grep 端口号检查一下。 - 防火墙拦截:跳板机的 iptables 或云平台安全组没放行对应端口。记得加规则。
- SSH 密钥权限:systemd 跑 autossh 时用的用户,必须有权限读取私钥。私钥权限必须是 600。
- 日志排查:隧道建不起来先看
journalctl -u reverse-tunnel.service -f,错误信息一般都在那里。