利用 SSH 隧道将内网 MySQL 端口暴露给外网

在测试环境中,内网机器往往无法被外网直接访问,但内网机器本身可以主动连接公网服务器。利用 SSH 的反向端口转发功能,可以将内网 MySQL(3306)端口稳定地暴露给外网使用。

场景说明

1
2
3
4
5
6
7
外网机器 A
│ 连接 B:3306

公网服务器 B ← 内网机器 C 主动 SSH 连接 B,建立反向隧道
│ 隧道内转发

内网机器 C :3306B 无法主动连 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"

# 将公钥推送到 B
ssh-copy-id user@B的公网IP

方案一:基础反向端口转发

第一步:修改公网服务器 B 的 SSH 配置

编辑 /etc/ssh/sshd_config,添加或修改以下配置:

1
2
GatewayPorts yes       # 允许远程转发绑定 0.0.0.0,外网机器才能访问
AllowTcpForwarding yes # 允许 TCP 转发

重启 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 上检查监听状态:

1
2
ss -tlnp | grep 3306
# 期望输出:0.0.0.0:3306(若为 127.0.0.1:3306 则需检查 GatewayPorts 配置)

在外网机器 A 上连接测试:

1
2
3
mysql -h B的公网IP -P 3306 -u youruser -p
# 或用 telnet 测试连通性
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
# /etc/ssh/sshd_config 添加
Port 22
Port 443
1
2
3
4
sudo systemctl restart sshd

# 防火墙放行 443
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 上的 stunnelclient 模式)
TLS 加密,走 443 出口

B 上的 stunnelserver 模式)
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 有流量特征检测,需完全伪装 ⭐⭐⭐

测试环境一般方案二 + 方案三组合即可满足需求。


常见问题排查

隧道建立了但外网连不上?

1
2
3
# 检查 B 上 3306 绑定的地址
ss -tlnp | grep 3306
# 必须是 0.0.0.0:3306,否则检查 GatewayPorts yes

autossh 无报错但隧道断了?

1
2
3
4
5
# 查看 autossh 日志
journalctl -u ssh-tunnel-mysql -f

# 手动测试 SSH 连通性
ssh -p 22 -N -R 0.0.0.0:3306:localhost:3306 user@B的公网IP

MySQL 拒绝远程连接?

1
2
3
4
5
6
7
# 检查 MySQL 是否允许远程账号
mysql -u root -p -e "SELECT user, host FROM mysql.user WHERE user='youruser';"
# host 必须是 '%' 或 B 的 IP,而不是 'localhost'

# 授权
GRANT ALL PRIVILEGES ON yourdb.* TO 'youruser'@'%' IDENTIFIED BY 'password';
FLUSH PRIVILEGES;

安全建议

  • 使用密钥认证,禁用密码登录(PasswordAuthentication no
  • 防火墙限制来源 IP,只允许特定 IP 访问 B 的 3306 端口
  • 使用非标准映射端口,如将 3306 映射到 B 的 33306,减少端口扫描风险
  • MySQL 账号最小权限,远程账号只授予必要数据库的必要操作权限
  • 长期生产环境建议迁移到 WireGuard VPN,更安全稳定