本地转发与远程转发的区别

本地转发与远程转发的区别

先把一句话记住,后面所有细节都从它推出来:

SSH 隧道就是一根两端都能“开口”的管子。谁监听端口,谁就是被动等着别人来连;真正去连目标服务的,永远是管子的另一头。

-L 和 -R 的区别只在于:监听端口开在客户端还是服务端,以及目标从哪一头去连。


一、共同前提:为什么能转发

SSH 连接建立后,两端之间存在一条加密的全双工通道。端口转发的本质是:

  1. 在某一端开一个 TCP 监听端口;
  2. 有人连进来后,把这个连接的字节流塞进 SSH 通道;
  3. 通道另一端的 ssh 进程收到后,替你去发起一条到目标的普通 TCP 连接;
  4. 两边数据互相搬运。

所以转发能穿透防火墙或 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 是“一对任意”。


八、排查清单

按顺序问自己:

  1. ss -lntp | grep 端口 在哪台机器上看到了监听? 是不是你以为的那台?绑定地址是 127.0.0.1 还是 0.0.0.0?
  2. 服务端 AllowTcpForwarding 是不是 no?(很多加固过的 sshd 会关掉,隧道会直接建不起来)
  3. -R 想对外开放,GatewayPorts 开了吗? 没开的话请求被静默降级到 127.0.0.1。
  4. 目标 host:port 是哪一头解析、哪一头去连的? 这是 90% 的“连不上”的根因——把 127.0.0.1 当成了自己的本机。
  5. 用 -v 看日志里有没有 Remote connections from ... forwarded to ... / Local connections to ... forwarded to ...,-vvv 能看到每次连接到达和失败原因。
  6. 链路活着但长时间空闲后失效? 加 ServerAliveInterval,或 TCPKeepAlive,或上 autossh。
  7. 端口冲突:加 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 通道,方向相反,命令只差一个字母——区别全在“端口开在哪头、目标从哪头连”。