SSH 端口转发是那种人们从 Stack Overflow 上抄来、跑通一次、却从没真正搞懂的功能——于是下次再用到时,又回到了瞎猜 -L、-R、-D 该用哪个的状态。这三个参数并不难,只是名字起得糟。下面给你一个让它们一目了然的思维模型、每种的示例,以及决定这条隧道能不能撑过一个下午的那些参数和陷阱。
一个问题就能挑对参数
每条 SSH 隧道都会在一台机器上开一个监听端口,并悄悄把打到这个端口的流量送往另一台机器能够访问的某处。所以唯一的问题是:你要连接的那个端口,开在哪一侧?
-L(本地)——端口开在你的机器上。你在本地连接,流量从服务器一侧出来。-R(远程)——端口开在服务器上。有人连到那里,流量从你这一侧出来。-D(动态)——端口开在你的机器上,但它是一个 SOCKS 代理:目标是逐个连接临时决定的,而不是事先固定好的。
其余都是细节。把“端口开在哪一侧”记在脑子里,你就再也不会抓错参数了。
本地转发(-L)——把远程服务拉到本地
最常见的一种。一个数据库、一个内部仪表盘、一个指标界面——某个绑定在服务器 127.0.0.1 上、或者只有服务器能访问的机器上的东西。你想让它出现在你的笔记本上,就像它本来就在本地一样。
ssh -L 5432:localhost:5432 deploy@db.example.com
从左到右读:在我的机器上打开 5432,并把到达的流量转发到 localhost:5432——这个地址以服务器的视角来看。现在你笔记本上的 psql -h 127.0.0.1 就能连上服务器的 Postgres(英文)了——走的是加密的 SSH 连接,完全不必把数据库暴露到网络上。
中间那个主机是在服务器一侧解析的,这正是它强大的地方:
# reach a database that only the bastion can see
ssh -L 5432:10.0.0.9:5432 deploy@bastion.example.com
这里的 10.0.0.9 是一个你笔记本路由不到的私有地址——但跳板机能路由到。隧道把这道鸿沟接了起来。
远程转发(-R)——把你的机器暴露到服务器一侧
它是镜像版,也是人们觉得方向反了的那一个(它有自己的深度讲解——SSH 反向隧道)。端口开在服务器上,对它的连接会从你这一端出来。两种经典用法:
# let the server (or its network) reach a dev server running on your laptop
ssh -R 8080:localhost:3000 deploy@app.example.com
现在打到服务器 localhost:8080 的一切,都会被引到你笔记本的 localhost:3000——方便你把正在做的东西展示给在那台机器上的同事,或者让某个远程服务回调到只有你在跑的东西上。
这也是连上一台困在 NAT 之后、无法入站访问的机器的诀窍:让它向一台你控制的服务器开一条向外的反向隧道,你再借道这台服务器连进去。那台无法接受连接的机器,改为主动发起出站连接。
有一个坑:默认情况下服务器会把转发端口绑定到 localhost,所以只有服务器自己能用它。要让服务器所在网络里的其他机器也能访问它,服务器的 sshd_config 需要 GatewayPorts yes(或 clientspecified)——这是服务器端的设置,不是你能从客户端强行指定的。
动态转发(-D)——一个参数搞定 SOCKS 代理
-L 转发到一个固定的目标。-D 则改为开一个 SOCKS 代理,所以目标是按每个请求临时选定的:
ssh -D 1080 deploy@jump.example.com
把你的浏览器(或 curl --socks5 127.0.0.1:1080)指向 127.0.0.1:1080,每个请求就都会从服务器那一侧出去。这是浏览内部网络的快捷方式,也可以用来测试某个服务从数据中心 IP 看过去是什么样,或者在不必为每台主机都写一条 -L 的情况下连上一小批内部主机。
让它真正好用的那些参数
一条裸隧道命令还会给你开一个你并不想要的 shell,而且你一关它就断。实际用的时候:
ssh -N -L 5432:localhost:5432 deploy@db.example.com
-N——不执行远程命令。你要的只是隧道,不是 shell。-f——连上之后把进程放到后台(和-N搭配用)。-o ExitOnForwardFailure=yes——如果端口无法绑定(已被占用、权限不足),就明确报错,而不是照样连上、却悄无声息地什么都没转发。- 保活——没有
ServerAliveInterval 30,一条长期隧道会死于空闲超时。加上它,否则你一停下不用,隧道就断。
在 ssh_config 里把它固定下来
凡是你会用 -L/-R/-D 传的东西,都有对应的配置指令,所以一个保存好的主机能带着它的隧道一起走:
Host db-tunnel
HostName db.example.com
User deploy
LocalForward 5432 localhost:5432
ServerAliveInterval 30
RequestTTY no
现在 ssh -N db-tunnel 就能把数据库带到 127.0.0.1:5432,不用再记任何参数。RemoteForward 和 DynamicForward 也是同样的用法。(关于配置文件本身,更多内容见 ~/.ssh/config 指南(英文)。)
三个陷阱
- 绑定地址很关键。
-L 5432:...只监听127.0.0.1。要让你局域网里的其他设备也能用这条隧道,就在前面加一个绑定地址:-L 0.0.0.0:5432:...——并且要明白,你刚刚已经把它暴露给了本地网络。 - “Address already in use”(地址已被占用)意味着本地端口被占了(常常是上一条没退出的隧道)。换一个本地端口,或者把旧的那条杀掉;
ExitOnForwardFailure能把这种无声的失败变成你真的会注意到的错误。 - 中间那个主机是在对端解析的。
-L a:localhost:b里的localhost指的是服务器的 localhost,不是你的。把这一点搞反,是端口转发里最常见的那个错误。
当隧道无法绑定
如果命令以 bind: Address already in use 或 bind: Permission denied 失败,说明监听器根本没开起来——照着这份短清单排查。已被占用:有东西占着端口,往往是昨天那条被你忘掉的隧道——ss -tlnp 能点出是哪个进程,然后换一个端口,或把旧的杀掉。本地的权限不足通常意味着是特权端口:任何低于 1024 的端口都需要 root,所以改监听 8443 而不是 443。在远程一端(-R),服务器有一票否决权:sshd_config 里的 AllowTcpForwarding no 会直接挡掉转发,而没有 GatewayPorts 时,无论你请求绑定哪个地址,远程监听器都只绑定到 localhost。在加固过的 RHEL 系主机上,即便配置允许,SELinux 也可能拒绝 sshd 执行绑定——在怪你的命令之前,先查一下审计日志。
当手动摆弄隧道变得烦人
转发是一个很棒的基本功,每个开发者都该把这三个参数烂熟于心。摩擦出现在后面:你留着一堆 ssh -N -L ... 命令(英文)的便签,每次笔记本休眠之后都要重跑一遍,你忘了哪个本地端口对应哪个服务,而且你一眼看不出某条隧道到底还在不在。
这时候,一个带像样隧道管理器的客户端就能帮上忙。Termalin 把本地、远程和动态转发作为保存好的配置挂在主机上——点一下就能启动,看清哪些在运行,并让它们随连接一起重新建立,而不必再敲一遍命令。同一份主机列表(连同它的密钥、跳板主机和保活)会在你的各台机器之间端到端加密同步,而且每台服务器都会在首次连接时得到验证。命令行教会你隧道是怎么工作的;而一个客户端,让你不必每天早上都手动把它们重建一遍。
不管用哪种方式,那个模型就是全部窍门:先定下端口开在哪一侧,参数剩下的部分自己就写出来了。
Termalin 是一款免费、跨平台的 SSH 客户端,内置隧道、SFTP 和密钥管理器——立即下载,或了解它如何安全地处理你的密钥。