在测试环境中,内网机器往往无法被外网直接访问,但内网机器本身可以主动连接公网服务器。利用 SSH 的反向端口转发功能,可以将内网 MySQL(3306)端口稳定地暴露给外网使用。
场景说明
1 2 3 4 5 6 7
| 外网机器 A │ 连接 B:3306 ▼ 公网服务器 B ← 内网机器 C 主动 SSH 连接 B,建立反向隧道 │ 隧道内转发 ▼ 内网机器 C :3306(B 无法主动连 C,但 C 可以连 B)
|
核心思路:C 主动连接 B 建立 SSH 隧道,B 不需要能访问 C。
前置准备
- 公网服务器 B:有公网 IP,安装了 OpenSSH Server
- 内网机器 C:运行 MySQL,能访问 B 的 SSH 端口
- SSH 密钥认证:推荐配置免密登录(避免隧道重建时需要输入密码)
配置 SSH 密钥(在 C 上执行)
1 2 3 4 5
| ssh-keygen -t rsa -b 4096 -C "tunnel@machine-c"
ssh-copy-id user@B的公网IP
|
方案一:基础反向端口转发
第一步:修改公网服务器 B 的 SSH 配置
编辑 /etc/ssh/sshd_config,添加或修改以下配置:
1 2
| GatewayPorts yes AllowTcpForwarding yes
|
重启 sshd 使配置生效:
1
| sudo systemctl restart sshd
|
第二步:在内网机器 C 上建立隧道
安装 autossh(自动断线重连):
1
| sudo apt install autossh
|
建立后台持久隧道:
1 2 3 4 5 6 7 8
| autossh -M 0 -f -N \ -R 0.0.0.0:3306:localhost:3306 \ -o "ServerAliveInterval=30" \ -o "ServerAliveCountMax=3" \ -o "ExitOnForwardFailure=yes" \ -o "ConnectTimeout=10" \ -i ~/.ssh/id_rsa \ user@B的公网IP
|
参数说明:
| 参数 |
说明 |
-M 0 |
禁用 autossh 内置心跳,改用 SSH 原生 keepalive |
-f |
后台运行 |
-N |
不执行远程命令,只做端口转发 |
-R 0.0.0.0:3306:localhost:3306 |
将 B 的 3306 绑定到所有网卡,转发到 C 的 3306 |
第三步:验证隧道是否成功
在 B 上检查监听状态:
在外网机器 A 上连接测试:
1 2 3
| mysql -h B的公网IP -P 3306 -u youruser -p
telnet B的公网IP 3306
|
方案二:systemd 自启服务(生产推荐)
手动启动隧道在机器重启后会丢失,建议配置为系统服务。
在内网机器 C 上创建服务文件:
1
| sudo vim /etc/systemd/system/ssh-tunnel-mysql.service
|
写入以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| [Unit] Description=SSH Reverse Tunnel for MySQL 3306 After=network.target network-online.target Wants=network-online.target
[Service] User=your_user ExecStart=/usr/bin/autossh -M 0 -N \ -R 0.0.0.0:3306:localhost:3306 \ -o "ServerAliveInterval=30" \ -o "ServerAliveCountMax=3" \ -o "ExitOnForwardFailure=yes" \ -o "StrictHostKeyChecking=no" \ -o "ConnectTimeout=10" \ -i /home/your_user/.ssh/id_rsa \ user@B的公网IP Restart=always RestartSec=10
[Install] WantedBy=multi-user.target
|
启用并启动服务:
1 2 3 4 5 6
| sudo systemctl daemon-reload sudo systemctl enable ssh-tunnel-mysql sudo systemctl start ssh-tunnel-mysql
sudo systemctl status ssh-tunnel-mysql
|
方案三:换用 443 端口(绕过端口限制)
部分网络环境会封锁非标准端口,将 SSH 连接改走 443 端口(HTTPS)可提升连通性。
第一步:让 B 的 sshd 额外监听 443
1 2 3 4
| sudo systemctl restart sshd
sudo ufw allow 443/tcp
|
第二步:C 上指定 443 端口建立隧道
1 2 3 4 5 6 7 8
| autossh -M 0 -f -N \ -R 0.0.0.0:3306:localhost:3306 \ -p 443 \ -o "ServerAliveInterval=30" \ -o "ServerAliveCountMax=3" \ -o "ExitOnForwardFailure=yes" \ -i ~/.ssh/id_rsa \ user@B的公网IP
|
或写入 ~/.ssh/config 简化命令:
1 2 3 4 5 6 7
| Host tunnel-server HostName B的公网IP User user Port 443 IdentityFile ~/.ssh/id_rsa ServerAliveInterval 30 ServerAliveCountMax 3
|
之后只需:
1
| autossh -M 0 -f -N -R 0.0.0.0:3306:localhost:3306 tunnel-server
|
方案四:stunnel 包裹 TLS(流量特征隐藏)
如果需要让 SSH 流量看起来与普通 HTTPS 无异,可以用 stunnel 再套一层 TLS。
安装说明
B 和 C 两台机器都需要安装 stunnel,各自承担不同角色:
- B(服务端):stunnel 监听 443 端口,将收到的 TLS 流量解密后转发给本机 sshd(22 端口)
- C(客户端):stunnel 以 client 模式运行,在本机开放 2222 端口,C 上的 SSH 连接本地 2222,流量经 stunnel 加密后发往 B:443
完整流向如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| C 上的 autossh │ 连接 localhost:2222 ▼ C 上的 stunnel(client 模式) │ TLS 加密,走 443 出口 ▼ B 上的 stunnel(server 模式) │ TLS 解密,转发到本机 22 ▼ B 上的 sshd :22 │ SSH 反向隧道建立 ▼ 外网机器 A 访问 B:3306 → 转发到 C:3306
|
公网服务器 B(服务端配置)
安装并生成证书:
1 2 3 4
| sudo apt install stunnel4 sudo openssl req -new -x509 -days 3650 -nodes \ -out /etc/stunnel/stunnel.pem \ -keyout /etc/stunnel/stunnel.pem
|
配置文件 /etc/stunnel/stunnel.conf:
1 2 3 4
| [ssh] accept = 443 connect = 127.0.0.1:22 cert = /etc/stunnel/stunnel.pem
|
启动服务:
1 2
| sudo systemctl enable stunnel4 sudo systemctl start stunnel4
|
内网机器 C(客户端配置)
安装 stunnel:
1
| sudo apt install stunnel4
|
配置文件 /etc/stunnel/stunnel.conf:
1 2 3 4 5
| client = yes
[ssh] accept = 127.0.0.1:2222 connect = B的公网IP:443
|
启动 stunnel 后,SSH 连本地 2222 端口,流量经 stunnel 加密走 443 到达 B:
1 2 3 4 5 6 7
| sudo systemctl start stunnel4
autossh -M 0 -f -N \ -R 0.0.0.0:3306:localhost:3306 \ -p 2222 \ [email protected]
|
方案对比与选择建议
| 方案 |
适用场景 |
复杂度 |
| 方案一:基础反向转发 |
网络环境宽松,测试用途 |
⭐ |
| 方案二:systemd 服务 |
需要开机自启、长期稳定运行 |
⭐⭐ |
| 方案三:443 端口 |
出口封锁了非标准端口 |
⭐⭐ |
| 方案四:stunnel TLS |
有流量特征检测,需完全伪装 |
⭐⭐⭐ |
测试环境一般方案二 + 方案三组合即可满足需求。
常见问题排查
隧道建立了但外网连不上?
autossh 无报错但隧道断了?
1 2 3 4 5
| journalctl -u ssh-tunnel-mysql -f
ssh -p 22 -N -R 0.0.0.0:3306:localhost:3306 user@B的公网IP
|
MySQL 拒绝远程连接?
1 2 3 4 5 6 7
| mysql -u root -p -e "SELECT user, host FROM mysql.user WHERE user='youruser';"
GRANT ALL PRIVILEGES ON yourdb.* TO 'youruser'@'%' IDENTIFIED BY 'password'; FLUSH PRIVILEGES;
|
安全建议
- 使用密钥认证,禁用密码登录(
PasswordAuthentication no)
- 防火墙限制来源 IP,只允许特定 IP 访问 B 的 3306 端口
- 使用非标准映射端口,如将 3306 映射到 B 的 33306,减少端口扫描风险
- MySQL 账号最小权限,远程账号只授予必要数据库的必要操作权限
- 长期生产环境建议迁移到 WireGuard VPN,更安全稳定