数据库端口不想对公网开,但本地又要用图形工具连;内网的服务想临时给外面看一眼;出差时想让所有流量走回自己的服务器——这几件事都不用装额外软件,SSH 自带的端口转发就能做。
本文讲清楚三种转发方式的区别、参数怎么记、以及那些一看就懂但一用就错的地方。
一、先理解一件事:转发的是「连接」,不是「端口」
很多教程一上来就给命令,看完照抄能跑,但换个场景就不会改了。原因是没想清楚数据往哪边流。
SSH 转发的本质是:在一台机器上开个监听端口,谁连上来,就把这条连接通过已建立的 SSH 加密通道,转到另一端去替他连目标地址。
所以每次写命令,只需要回答三个问题:
- 在哪台机器上监听?(决定用
-L还是-R) - 监听哪个端口?
- 连接最终要送到哪个地址、哪个端口?——从哪一端的视角看?
第三个问题是所有困惑的来源,后面会反复强调。
二、本地转发 -L:把远端的服务搬到本地
这是最常用的一种。典型场景:数据库只监听 127.0.0.1,不对公网开放,但你要用本地的图形工具连它。
ssh -L 13306:127.0.0.1:3306 root@db-server
参数怎么读
-L 13306:127.0.0.1:3306
└─┬─┘ └────┬────┘ └┬─┘
│ │ └─ 目标端口
│ └─ 目标地址(★ 从 db-server 的视角看 ★)
└─ 在你本机监听的端口
中间那个地址是站在服务器上看的,这是最容易搞错的地方。127.0.0.1 在这里指的不是你的电脑,而是 db-server 自己。
数据怎么走
你的电脑 db-server
mysql -h 127.0.0.1 -P 13306
│
└─→ 本机 13306(ssh 在监听)
│
│ ═══ SSH 加密隧道(走 22 端口)═══
│
└─→ 127.0.0.1:3306(MySQL)
关键在于:MySQL 看到的连接来自它自己的 127.0.0.1,因为最后一跳是 db-server 内部发起的。所以数据库完全不用对外开放端口,防火墙只需要放行 22。
用法
# 终端 1:开隧道
ssh -L 13306:127.0.0.1:3306 root@db-server
# 终端 2:在本机连,就像连本地库一样
mysql -h 127.0.0.1 -P 13306 -u app_user -p
为什么本地端口用 13306 而不是 3306
如果你本机也装了 MySQL,3306 已被占用,隧道会起不来(bind: Address already in use)。换个不冲突的端口最省事,13306、23306 都行。
目标地址不一定是 127.0.0.1
如果数据库在另一台内网机器上,SSH 的这台只是跳板:
ssh -L 13306:10.0.0.20:3306 root@jump-server
# └─ 内网数据库地址,从 jump-server 的视角能访问到就行
这就是「堡垒机」的典型用法:只有跳板机能 SSH,数据库藏在内网,你从家里照样能连。
三、远程转发 -R:把本地的服务送到远端
方向反过来。典型场景:你本地跑着一个开发中的服务,想让服务器(或通过服务器让别人)访问它。
ssh -R 8080:127.0.0.1:3000 root@remote-server
参数怎么读
-R 8080:127.0.0.1:3000
└─┬┘ └────┬────┘ └┬─┘
│ │ └─ 目标端口
│ └─ 目标地址(★ 这次是从你本机的视角看 ★)
└─ 在 remote-server 上监听的端口
注意视角又变了:-R 的时候,监听端在远端,目标地址从本机视角看。
数据怎么走
你的电脑 remote-server
本地 3000(你的开发服务) 别人 curl localhost:8080
↑ │
└─ 127.0.0.1:3000 ←────────────────────────┘
═══ SSH 加密隧道 ═══
一个坑:默认只有服务器自己能访问
-R 开出来的端口默认只绑在服务器的 127.0.0.1 上,也就是说只有登录到那台服务器的人能用,外网访问不到。
想让外网也能访问,需要两步:
# 1. 服务器的 /etc/ssh/sshd_config 里加上
GatewayPorts yes
# 然后 systemctl restart sshd
# 2. 客户端明确绑到 0.0.0.0
ssh -R 0.0.0.0:8080:127.0.0.1:3000 root@remote-server
开 GatewayPorts 前想清楚:这等于允许任何能 SSH 进来的人,把服务器变成一个对外的入口。多人共用的机器上要谨慎。
四、动态转发 -D:把 SSH 变成 SOCKS 代理
ssh -D 1080 root@remote-server
这个不指定目标地址——它在本地开一个 SOCKS5 代理,任何支持 SOCKS 的程序把流量交给它,都会从服务器那端出去。
# 用它访问
curl --socks5 127.0.0.1:1080 https://example.com
# git 也支持
git config --global http.proxy socks5://127.0.0.1:1080
浏览器配置 SOCKS5 代理 127.0.0.1:1080 之后,所有网页请求都从服务器出口走。
和 -L 的区别:-L 是一个端口对一个目标,-D 是一个端口对任意目标,由客户端在协议里告诉代理要连谁。
五、常用参数组合
-N:不要远程终端
直接 ssh -L ... 会在开隧道的同时给你一个远程 shell。如果只想要隧道:
ssh -N -L 13306:127.0.0.1:3306 root@db-server
-N 的意思是「不执行远程命令」,于是它就挂在那儿只做转发,不给 shell。
-f:丢到后台
ssh -fN -L 13306:127.0.0.1:3306 root@db-server
连终端都不占。关闭的时候按命令特征杀掉:
pkill -f "13306:127.0.0.1:3306"
多条隧道一次开
-L 可以重复写:
ssh -fN \
-L 13306:127.0.0.1:3306 \
-L 16379:127.0.0.1:6379 \
-L 27017:127.0.0.1:27017 \
root@db-server
一条 SSH 连接同时转发 MySQL、Redis、MongoDB。
让隧道别断
长时间不用会被中间的防火墙或 NAT 掐掉。加心跳:
ssh -fN -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-L 13306:127.0.0.1:3306 root@db-server
每 30 秒发一次心跳,连续 3 次没响应才判定断开。
写进 ssh config,一劳永逸
~/.ssh/config:
Host db-tunnel
HostName db-server.example.com
User root
LocalForward 13306 127.0.0.1:3306
LocalForward 16379 127.0.0.1:6379
ServerAliveInterval 30
ExitOnForwardFailure yes
以后只要:
ssh -fN db-tunnel
ExitOnForwardFailure yes 很有用——端口被占用时直接失败退出,而不是「SSH 连上了但转发没成」这种最难排查的半吊子状态。
六、几个高频踩坑
1. 连的时候写了 localhost
mysql -h localhost -P 13306 # 连不上
mysql -h 127.0.0.1 -P 13306 # 正确
MySQL 客户端对 localhost 会走 unix socket 文件,根本不走 TCP,自然也就绕过了隧道。必须写 127.0.0.1 强制走 TCP。
同理,-P(大写)是端口,-p(小写)是密码,写错了会得到莫名其妙的报错。
2. 搞反了中间那个地址的视角
再强调一次:
| 监听在哪 | 中间的目标地址从谁的视角看 | |
|---|---|---|
-L |
本地 | 远端服务器 |
-R |
远端服务器 | 本地 |
记忆方法:目标地址永远是「隧道出口那一端」要去连的地址。-L 的出口在服务器,-R 的出口在本地。
3. 以为隧道断了,其实是端口没释放
隧道进程被 kill 之后偶尔端口还占着,再开会报 Address already in use。查一下谁在占:
lsof -i :13306
4. 服务器禁用了转发
有些机器出于安全考虑在 sshd_config 里关掉了转发:
AllowTcpForwarding no
这时候 -L 会静默失败或者报 administratively prohibited。需要服务器管理员放开。
七、什么时候该用隧道,什么时候该用别的
隧道好用,但不是所有场景的最优解。
适合用隧道:
- 临时的、交互式的访问——比如用图形工具连生产库查个数据
- 开发调试时访问内网服务
- 不想为了一次性的需求去改防火墙规则
不适合用隧道:
-
常驻服务之间的连接。比如备份机上的定时任务要每天连数据库,靠一条挂着的 SSH 隧道很脆弱——断了没人知道,任务就静默失败了。这种场景更适合用防火墙规则放行特定源 IP:
ufw allow from 10.0.0.5 to any port 3306 -
高吞吐的数据传输。所有流量都经过 SSH 加解密,大批量导数据时会成为瓶颈。
一个实用的判断标准:这条连接是「人在用」还是「机器在用」? 人用的走隧道,机器长期用的走防火墙白名单。
小结
-L本地转发:把远端服务搬到本地,最常用,中间的地址从服务器视角看-R远程转发:把本地服务送到远端,中间的地址从本地视角看,默认只有服务器自己能访问-D动态转发:变成 SOCKS5 代理,一个端口通向任意目标-fN是日常最实用的组合:后台运行、不要 shell- 写进
~/.ssh/config加上ExitOnForwardFailure yes,比每次敲长命令可靠
数据库、Redis 这类服务的正确姿势是:端口只绑 127.0.0.1,人要用就开隧道,机器要用就配防火墙白名单。关于数据库账号本身该怎么分离、授权和限制来源,可以接着看 MySQL 用户最佳实践:按站分离、改密码、改授权、限制 IP。