数据库端口不想对公网开,但本地又要用图形工具连;内网的服务想临时给外面看一眼;出差时想让所有流量走回自己的服务器——这几件事都不用装额外软件,SSH 自带的端口转发就能做。

本文讲清楚三种转发方式的区别、参数怎么记、以及那些一看就懂但一用就错的地方。


一、先理解一件事:转发的是「连接」,不是「端口」

很多教程一上来就给命令,看完照抄能跑,但换个场景就不会改了。原因是没想清楚数据往哪边流。

SSH 转发的本质是:在一台机器上开个监听端口,谁连上来,就把这条连接通过已建立的 SSH 加密通道,转到另一端去替他连目标地址。

所以每次写命令,只需要回答三个问题:

  1. 在哪台机器上监听?(决定用 -L 还是 -R
  2. 监听哪个端口?
  3. 连接最终要送到哪个地址、哪个端口?——从哪一端的视角看?

第三个问题是所有困惑的来源,后面会反复强调。


二、本地转发 -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)。换个不冲突的端口最省事,1330623306 都行。

目标地址不一定是 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