本地转发与远程转发的区别
先把一句话记住,后面所有细节都从它推出来:
SSH 隧道就是一根两端都能“开口”的管子。谁监听端口,谁就是被动等着别人来连;真正去连目标服务的,永远是管子的另一头。
-L 和 -R 的区别只在于:监听端口开在客户端还是服务端,以及目标从哪一头去连。
一、共同前提:为什么能转发
SSH 连接建立后,两端之间存在一条加密的全双工通道。端口转发的本质是:
- 在某一端开一个 TCP 监听端口;
- 有人连进来后,把这个连接的字节流塞进 SSH 通道;
- 通道另一端的 ssh 进程收到后,替你去发起一条到目标的普通 TCP 连接;
- 两边数据互相搬运。
所以转发能穿透防火墙或 NAT,是因为“真正发起连接的那一端”在网络拓扑上离目标更近。
记三个关键问题,就能彻底分清 -L / -R:
| 问题 | 本地转发 -L |
远程转发 -R |
|---|---|---|
| 谁在监听端口? | SSH 客户端(你本机) | SSH 服务端(跳板机或公网机) |
| 谁去连目标 host:port? | SSH 服务端 | SSH 客户端 |
| 目标主机名在哪解析? | 服务端 | 客户端 |
| 数据方向 | 客户端 → 服务端 | 服务端 → 客户端 |
二、本地端口转发 -L(Local Forwarding)
语法
ssh -L [bind_address:]本地端口:目标主机:目标端口 user@SSH服务器
数据流
以 ssh -L 8080:db.internal:5432 me@jump.example.com 为例:
[你本机] [跳板机 jump] [db.internal]
应用/psql
│ 连 127.0.0.1:8080
▼
┌──────────────┐ ┌──────────┐
│ ssh 客户端 │ ═══════ SSH 加密通道 ═════════▶ │ sshd │ ──▶ db.internal:5432
│ 监听 :8080 │ │ (发起连接)│
└──────────────┘ └──────────┘
- 监听:你本机的 8080。
- 发起到
db.internal:5432的连接:跳板机。 db.internal这个名字是在跳板机上解析的。
最典型的用法
1. 访问内网服务(最常见)
内网有个只在内网可达的服务,你从外面连不上:
ssh -N -L 8080:10.0.3.21:80 me@jump
# 然后浏览器访问 http://127.0.0.1:8080
2. 访问只监听 127.0.0.1 的服务(数据库、管理后台)
ssh -N -L 15432:127.0.0.1:5432 me@db-host
psql -h 127.0.0.1 -p 15432 -U postgres
这里的 127.0.0.1 是相对于 db-host 自己的 127.0.0.1,不是你的本机。这是新手最容易搞错的一点。
3. 把目标换成第三方主机(以跳板机为出口)
ssh -N -L 3306:mysql.prod.internal:3306 me@jump
注意:目标不一定要在跳板机上,只要跳板机能连到就行。因此 -L 也是一种“借用某台机器的网络位置”的手段,可用于绕过你本机的网络限制。
4. Kubernetes 场景的底层就是它
kubectl port-forward svc/grafana 3000:80
# 本质类似 ssh -L 3000:podIP:80 到节点上,只是走的是 API server 的 SPDY 通道
三、远程端口转发 -R(Remote Forwarding)
语法
ssh -R [bind_address:]远端端口:目标主机:目标端口 user@SSH服务器
注意这里的“远端端口”是开在 SSH 服务器上的,“目标主机”是从你这台机器去连的。
数据流
以 ssh -R 9000:localhost:3000 me@public.example.com 为例:
[外部用户] [公网服务器 public] [你本机 dev]
┌──────────┐ ┌──────────────┐
连 public:9000 ─────▶ │ sshd │ ═════ SSH 加密通道 ═══▶ │ ssh 客户端 │ ──▶ localhost:3000
│ 监听 :9000│ │ (发起连接) │
└──────────┘ └──────────────┘
- 监听:服务器(public) 的 9000。
- 发起到
localhost:3000的连接:你本机。 localhost在你的机器上解析,指的就是你自己。
最典型的用法
1. 把本机服务暴露给外网(NAT 穿透)
你在公司内网或家里的笔记本上跑着开发服务器 :3000,没有公网 IP,路由器也做不了端口映射。但你能 SSH 出去:
ssh -N -R 9000:localhost:3000 me@my-vps
这时任何人访问 my-vps:9000(前提见下面 GatewayPorts)就等于访问你本机的 3000。连接方向是“外向内”,但 TCP 握手是由内网机器主动建立的 SSH 连接承载的,所以 NAT 拦不住。
这是 frp、ngrok、Cloudflare Tunnel 这类内网穿透工具的经典原理雏形。
2. 给同事或客户临时看本地环境
ssh -N -R 8080:localhost:8080 demo@demo-server
# 把 demo-server:8080 发给对方
3. 反向访问:从远端机器“回连”到你身边的内网
ssh -R 2222:192.168.1.50:22 me@vps
# 在 vps 上:ssh -p 2222 user@127.0.0.1 就等于连你办公室的 192.168.1.50
4. 正向运维场景:让运维从跳板机进来的流量落到你控制的一台机器
ssh -R 15432:prod-db:5432 me@jump
(目标 prod-db:5432 是从你本机去连的——所以这台机器必须在你本机可达的网络里。)
四、最容易踩的坑:-R 的监听地址
-L 默认就绑定在 127.0.0.1,你本机自己用没问题。但 -R 的默认行为是:
- ssh 客户端请求服务端监听
127.0.0.1:9000(只监听服务端的回环地址)。
也就是说:ssh -R 9000:localhost:3000 me@vps 之后,只有登录到 vps 上的人能访问 127.0.0.1:9000,外网访问 vps:9000 会连不上。
要让外界能连,需要两件事同时满足:
① 客户端指定绑定地址
ssh -R 0.0.0.0:9000:localhost:3000 me@vps
# 或老式写法(等效于上面的 bind_address)
ssh -R '*:9000:localhost:3000' me@vps
② 服务端 sshd 允许
/etc/ssh/sshd_config 中:
GatewayPorts clientspecified # 允许客户端指定;yes 表示强制绑 0.0.0.0
改完 systemctl reload sshd。如果这里是默认的 no,客户端请求 0.0.0.0 会被静默降级成 127.0.0.1,你会以为配置生效了但其实没生效——这是排查时最常见的误判。
-L 侧对应的开关是客户端侧的 -g(等价于 GatewayPorts yes 的效果,允许本机上非回环地址连入):
ssh -g -L 8080:internal:80 me@jump
# 默认只绑 127.0.0.1,加了 -g 后绑到所有地址,局域网里别人也能用你的隧道
五、一张表对比
-L 本地转发 |
-R 远程转发 |
|
|---|---|---|
| 监听位置 | 客户端(本机) | 服务端(跳板或公网机) |
| 连接发起方 | 服务端 | 客户端 |
| 目标 host 解析方 | 服务端 | 客户端 |
| 数据名义方向 | 本机 → 远端网络 | 远端网络 → 本机 |
| 打开的端口给谁用 | 本机(及 -g 后的本机网络) |
服务端(及 GatewayPorts 后的外部) |
| 典型目的 | 访问远端的资源 | 把本地资源暴露出去 |
| 默认绑定 | 127.0.0.1 |
127.0.0.1(且可能被服务端降级) |
| 关键服务端配置 | AllowTcpForwarding |
AllowTcpForwarding + GatewayPorts |
| 一句话 | “我要进去” | “我要被进来” |
记忆口诀:-L 是 Listen 在本 Local;-R 是 Remote 那边 Receive 连接。或者记“端口开在哪头,目标就在另一头”。
六、常用参数与工程实践
ssh -N -f -L 8080:internal:80 me@jump
| 参数 | 作用 |
|---|---|
-N |
不执行远程命令,只做转发(做隧道必加) |
-f |
转后台运行 |
-T |
不分配 TTY |
-g |
允许本机以外的主机连入 -L 的监听端口 |
-C |
压缩(低带宽有用,现代链路意义不大) |
-v / -vvv |
排查转发是否建立 |
ExitOnForwardFailure yes |
转发失败就退出,避免“看着连上了但隧道没通” |
ServerAliveInterval 30 |
保活,防止 NAT 表项超时静默断链 |
-o 组合 |
所有配置都能用 -o 写在命令行 |
推荐的 ~/.ssh/config 写法(比记命令行参数靠谱得多):
Host db-tunnel
HostName jump.example.com
User me
IdentityFile ~/.ssh/id_ed25519
LocalForward 15432 127.0.0.1:5432
ExitOnForwardFailure yes
ServerAliveInterval 30
ServerAliveCountMax 3
Host expose-dev
HostName my-vps.example.com
User me
RemoteForward 0.0.0.0:9000 localhost:3000
ExitOnForwardFailure yes
ServerAliveInterval 30
用法:ssh -N db-tunnel、ssh -N expose-dev。
更稳的做法:autossh
autossh -M 0 -N -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-R 0.0.0.0:9000:localhost:3000 me@vps
再配 systemd unit 就能做成开机自启的常驻穿透隧道。
七、顺带一提:-D 动态转发
不是第三种方向,而是 -L 的泛化版:不在配置里写死目标,而是开一个 SOCKS5 代理,目标由客户端的请求动态决定。
ssh -N -D 1080 me@jump
# 应用配 socks5://127.0.0.1:1080,所有流量由 jump 出口
-L 是“一对一”,-D 是“一对任意”。
八、排查清单
按顺序问自己:
ss -lntp | grep 端口在哪台机器上看到了监听? 是不是你以为的那台?绑定地址是127.0.0.1还是0.0.0.0?- 服务端
AllowTcpForwarding是不是no?(很多加固过的 sshd 会关掉,隧道会直接建不起来) -R想对外开放,GatewayPorts开了吗? 没开的话请求被静默降级到127.0.0.1。- 目标
host:port是哪一头解析、哪一头去连的? 这是 90% 的“连不上”的根因——把127.0.0.1当成了自己的本机。 - 用
-v看日志里有没有Remote connections from ... forwarded to .../Local connections to ... forwarded to ...,-vvv能看到每次连接到达和失败原因。 - 链路活着但长时间空闲后失效? 加
ServerAliveInterval,或TCPKeepAlive,或上autossh。 - 端口冲突:加
ExitOnForwardFailure yes,否则 ssh 会“成功登录”但端口其实没绑上。
九、两个场景对照,彻底固化印象
场景 A:我在咖啡厅,要连公司内网的数据库
ssh -N -L 15432:db.corp.internal:5432 me@jump.corp.com
→ 用 -L。因为资源在远端,我要“进去”。数据库名 db.corp.internal 由公司跳板机解析。
场景 B:我在家写前端,要让客户看我的本地页面
ssh -N -R 0.0.0.0:9000:localhost:3000 me@my-vps
→ 用 -R。因为资源在我这,我要“被进来”。localhost:3000 由我的笔记本解析。
同一根 SSH 通道,方向相反,命令只差一个字母——区别全在“端口开在哪头、目标从哪头连”。