先说我踩过的坑
上个月公司新项目部署到客户机房,MySQL只监听内网IP 172.16.1.10:3306,安全组不放行3306端口。运维给了个跳板机,但VPN客户端要装专属插件,证书过期后全员抓瞎,连上去延迟还高到打字都卡顿。我花了三个小时搞定SSH隧道后发现,这问题本来三分钟就能解决。
这篇文章就把SSH隧道这块讲透:本地转发、远程转发、动态转发分别解决什么问题,代码直接抄,数据直接看,坑提前告诉你。
问题场景还原
典型情况:应用服务器(内网IP 172.16.1.10,CentOS 7.9,PHP 8.3)跑在客户机房,MySQL 8.0.35只在内网监听。你本地开发机(MacOS 14.3,OpenSSH_9.6p1)需要直连MySQL做数据排查和脚本调试。
三条路:
- 走VPN:装客户端、导入证书、连上后路由表混杂,还可能断线
- 申请安全组放行3306:流程走3天,安全审计还不过
- SSH隧道:现有跳板机的22端口就够用
SSH隧道本质是:你本地和跳板机之间建立一条加密SSH连接,把本来要发给内网服务的流量,打包塞进这条连接里,到内网再解开转发出去。服务端不需要额外装任何软件,OpenSSH自带功能。
方案对比:VPN vs SSH隧道
我拿实际体验对比了下,不说空话:
| 对比项 | 公司VPN(OpenVPN 2.5.9) | SSH隧道(OpenSSH_9.6p1) |
|---|---|---|
| 客户端安装 | 需要装OpenVPN GUI + 配置文件 | 系统自带ssh命令,零安装 |
| 连接耗时 | 12-15秒(含证书校验、路由表下发) | 0.3-0.5秒 |
| 空闲断线 | 经常掉,需要重连 | 加ServerAliveInterval参数可保活 |
| 权限要求 | 需要管理员权限改路由表 | 普通用户即可,无需额外权限 |
| 网络暴露面 | 整个内网网段都通 | 只暴露你指定的那一个端口 |
| 稳定度实测 | 峰值延迟110ms,波动大 | 隧道稳定在35ms上下 |
安全性上,SSH隧道暴露面远小于VPN。VPN接入后就进入了整个内网,SSH只能转发你指定的端口。安全审计时,SSH隧道方案一次通过,VPN方案被驳回一次(理由是客户端权限过大)。
方案一:本地转发(-L)——访问内网服务
本地转发是把「本地端口 → 跳板机 → 内网目标端口」这条链路建立起来。你访问自己电脑上的端口,数据就被安全地转到了内网目标服务上。
核心命令
# 语法:ssh -L 本地端口:目标IP:目标端口 跳板机用户@跳板机IP
# 场景:本地3307端口 → 跳板机 → 172.16.1.10:3306
ssh -N -L 3307:172.16.1.10:3306 deploy@192.168.1.100 -p 22 -i ~/.ssh/id_rsa
参数说明:-N 表示不执行远程命令,只做端口转发;-p 指定跳板机SSH端口;-i 指定密钥文件。
建立后,本地验证:
# 测试连接MySQL
mysql -h 127.0.0.1 -P 3307 -u app_user -p
# 看到 MySQL 8.0.35 的版本信息就是通了
# 或者用 nc 测试端口
nc -zv 127.0.0.1 3307
压测数据:直连 vs 隧道
我用 PHP 8.3 写了PDO连接脚本,跑了1000次查询:
<?php
// test_tunnel.php —— 在本地执行
$dsn = 'mysql:host=127.0.0.1;port=3307;dbname=app_db;charset=utf8mb4';
$pdo = new PDO($dsn, 'app_user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 3,
]);
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
$pdo->query('SELECT 1');
}
$end = microtime(true);
printf("耗时: %.4f秒, 平均: %.4f毫秒/次\n", $end - $start, ($end - $start) * 1000 / 1000);
?>
跑三次取中位数,数据如下:
| 连接方式 | 总耗时(1000次) | 平均每次 | 额外开销 |
|---|---|---|---|
| 公网IP直连(同城机房) | 4.72s | 4.72ms | — |
| SSH隧道(同网段跳板机) | 5.86s | 5.86ms | 1.14ms |
| 公司VPN(OpenVPN) | 11.34s | 11.34ms | 6.62ms |
SSH隧道比VPN快了差不多一倍,多出的1ms是SSH加密和解密的CPU开销,可以忽略。如果你和跳板机不在同一个城市,这1ms会被网络延迟覆盖掉。
转发多个端口
有时要同时访问MySQL和Redis:
ssh -N -L 3307:172.16.1.10:3306 -L 6380:172.16.1.10:6379 deploy@192.168.1.100
一条命令,两个隧道。优先级上,-L的转发监听在0.0.0.0还是127.0.0.1需要注意,默认只监听127.0.0.1,安全。需要局域网其他机器共享隧道才加 -g 参数。
方案二:远程转发(-R)——把内网服务暴露到公网
场景反过来了:你的内网机器(比如家里NAS上的Web服务跑在8080端口)需要在外网被访问。跳板机是公网服务器(比如腾讯云CVM,公网IP 43.139.xx.xx)。
核心命令
# 在内网机器上执行:把内网8080端口挂到公网服务器的8080端口上
# 语法:ssh -R 远程端口:本地IP:本地端口 公网用户@公网IP
ssh -N -R 8080:127.0.0.1:8080 root@43.139.xx.xx
此时外网访问 http://43.139.xx.xx:8080 就相当于访问你家NAS的8080端口。
一个坑:MySQL远程管理
这个场景我实际用过:帮朋友排查生产库(腾讯云MySQL 8.0),本身有公网访问策略限制只允许特定IP,我本地IP老变。用 -R 解决:
# 在任意能访问MySQL的机器上执行
# 把远程MySQL(127.0.0.1:3306)挂到公网中转机的33060端口
ssh -N -R 33060:127.0.0.1:3306 root@43.139.xx.xx
然后本地连公网中转机的33060端口就行了。需要有公网中转机的SSH权限,这个一般好申请。
GatewayPorts 限制
默认情况下,-R 绑定的远程端口只监听在公网服务器的127.0.0.1上,外部访问不到。需要修改公网服务器的sshd配置:
# 在公网服务器(中转机)上修改 /etc/ssh/sshd_config
# 添加这一行
GatewayPorts yes
# 重启
sudo systemctl restart sshd
必须用root或sudo执行。这点容易漏——不配置的话,从外网访问公网IP的8080端口会直接连接拒绝。
方案三:动态转发(-D)——把跳板机变成SOCKS5代理
动态转发是建立一条SOCKS5代理通道,配合Proxifier或系统代理设置,可以让本地所有网络请求都走跳板机出去。做爬虫绕过IP限制、访问内网Web控制台都能用。
核心命令
# 本地1080端口变为SOCKS5代理
ssh -N -D 1080 deploy@192.168.1.100
使用场景示例:内网的Grafana监控面板(172.16.1.20:3000)没有外网权限,本地浏览器直接访问不了。开启动态转发后:
# 配合 curl 使用
curl --socks5-hostname 127.0.0.1:1080 http://172.16.1.20:3000/login
# 或使用 proxychains(实测 proxychains-ng 4.17)
proxychains4 mysql -h 172.16.1.10 -P 3306 -u app_user -p
要全系统代理,MacOS上系统设置里填「SOCKS代理 127.0.0.1:1080」即可。浏览器和终端流量都会走隧道。
生产级方案:SSH配置与保活
手动敲命令跑隧道,SSH一断就全断了。生产中我用这个方案:配置文件 + ControlMaster + autossh。
SSH config 配置
# ~/.ssh/config
Host jumphost
HostName 192.168.1.100
User deploy
Port 22
IdentityFile ~/.ssh/id_rsa
ServerAliveInterval 30
ServerAliveCountMax 3
ExitOnForwardFailure yes
TCPKeepAlive yes
解释:ServerAliveInterval 30 每30秒发一个心跳包;ServerAliveCountMax 3 连续收不到3次响应就断开;ExitOnForwardFailure 端口被占用时立即退出而不是静默失败;TCPKeepAlive 保持TCP连接活跃。
autossh 自动重连
# Ubuntu 22.04 安装 autossh(版本 1.4f)
sudo apt install -y autossh
# 用 autossh 替代 ssh
autossh -M 0 -N -L 3307:172.16.1.10:3306 jumphost
-M 0 表示禁用autossh自带的监控端口,改用SSH的ServerAliveInterval做健康检查。生产环境推荐这样配。
做成 systemd 服务
# /etc/systemd/system/ssh-tunnel.service
[Unit]
Description=SSH Tunnel to Production MySQL
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=deploy
ExecStart=/usr/bin/autossh -M 0 -N -L 3307:172.16.1.10:3306 jumphost
Restart=always
RestartSec=10
Environment=AUTOSSH_GATETIME=0
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now ssh-tunnel.service
sudo systemctl status ssh-tunnel.service
效果数据汇总
我把三种方案的性能实测数据汇总一下:
| 测试项 | 结果 | 备注 |
|---|---|---|
| SSH隧道 vs VPN 延迟 | 35ms vs 110ms | 同网络条件下实测 |
| MySQL查询隧道额外开销 | 1.14ms/次 | 1000次SELECT 1取中位数 |
| 大文件传输带宽损耗 | 约8% | scp 1GB文件,隧道模式对比直传 |
| SSH隧道建立耗时 | 0.4秒 | 复用连接后只需0.05秒 |
| 连接保活成功率 | 99.2% | autossh + ServerAliveInterval,运行30天统计 |
大文件传输8%的带宽损耗是我用 scp 传1GB安装包测出来的。如果是交互式操作(SQL查询、Web访问),感知不到什么差别。
避坑指南
这一节的坑都是我真金白银踩出来的。
坑1:ControlMaster复用导致服务卡死
第一次用ControlMaster配置连接复用,感觉像发现了新大陆:多个SSH会话共享一条TCP连接,建连只要0.05秒。但后来内网MySQL出过一个问题:一条慢查询把整个连接池堵死,新请求全部排队。
因为ControlMaster默认是共享连接。多个终端共用一个SSH连接,一个连接卡住,其他终端的请求全部阻塞。解决办法是改成ControlMaster auto,让系统自动决定是否复用,或者在生产环境干脆关掉连接复用。
坑2:ServerAliveInterval和KeepAlive的区别
我开始只配了 TCPKeepAlive yes,断网后SSH连接依然挂在那一两天不释放。发现 TCPKeepAlive 走的是TCP层,检测周期太长(默认2小时)。正确姿势是配 ServerAliveInterval 30,走SSH应用层心跳,30秒发一次,3次没响应就断开。这两者的区别是:TCP KeepAlive探测的是TCP连接状态,ServerAlive发的是真实的SSH数据包。
坑3:-R 远程端口绑定127.0.0.1
远程转发做了半天,外网始终连不上。查了官方文档才知道:-R 默认把远程端口绑定在127.0.0.1上,只有中转机本机能访问。必须改 GatewayPorts yes。这个配置在sshd_config里默认是no,改完要重启sshd。注意:这项配置在云服务器上要确认安全组放行了对应端口。
坑4:多级跳板
遇到过跳板机套跳板机的情况,防火墙策略极严格。用 ProxyJump 参数,一行解决:
# 本机 → 跳板机A(192.168.1.100) → 跳板机B(10.0.0.5) → 目标MySQL(10.0.0.50)
ssh -N -L 3307:10.0.0.50:3306 -J deploy@192.168.1.100 deploy@10.0.0.5
-J 参数(ProxyJump)从 OpenSSH 7.3 开始支持。自动完成两次跳转,本地直接连到最终目标。
坑5:密钥而非密码认证
有人图省事用密码登录跳板机。隧道断线后autossh自动重连,需要输入密码就断了。必须配SSH密钥认证,这不仅是安全问题,更是自动化保活的前提。生成密钥:
ssh-keygen -t ed25519 -C "deploy@work" -f ~/.ssh/id_ed25519
ssh-copy-id deploy@192.168.1.100
用ed25519算法,比RSA 2048更快且更长。
坑6:隧道跨IDC的MTU问题
一个环境从北京连上海的跳板机,隧道的 -L 端口能通,但传输大SQL文件就卡死。查了半天是MTU问题:PPPoE拨号环境MTU只有1492,隧道包太大分片丢失。解决办法是降低MTU,或者在SSH配置里加 IPQoS lowdelay 减少队列延迟。遇到类似问题先检查本机MTU。
排障命令速查
出问题先跑这些:
# 1. 确认隧道进程活着
ps aux | grep "ssh -N"
# 2. 确认本地端口在监听
lsof -i :3307
# 3. 确认隧道内层连通性
ssh jumphost "nc -zv 172.16.1.10 3306"
# 4. 打开SSH详细日志
ssh -vvv -N -L 3307:172.16.1.10:3306 jumphost 2>&1 | grep "forwarding"
# 看到 "forwarding listening on port" 就是成功
最后说一句
SSH隧道不是银弹,它解决的是「基于SSH协议能访问到的网络环境」的端口暴露问题。搞定 -L、-R、-D 这三种方向和一条配置文件的保活套路,大多数远程访问场景够用了。真要保障7×24稳定运行,还得配监控告警,我是在隧道挂掉时收到报警才发现autossh没起来的。
看清方向,用对参数,你的隧道会和你一样稳定。