SSH 本地端口转发:通过跳板机访问内网服务
内网中的 Web 服务可能监听在服务器的 127.0.0.1:50052,当前电脑既不能直接访问这台服务器,也不应该把这个服务暴露到公网。
如果当前电脑可以登录跳板机 192.0.2.10:22022,跳板机又可以访问目标服务器 198.51.100.20,就可以用 SSH 本地端口转发,把目标服务器上的服务临时映射到当前电脑。
本文中的账号和地址都是脱敏示例:跳板机使用 jump-user@192.0.2.10:22022,目标服务器使用 target-user@198.51.100.20。请替换为实际环境的值。
建立隧道:
ssh -fN \
-J jump-user@192.0.2.10:22022 \
-L 50052:127.0.0.1:50052 \
-o ExitOnForwardFailure=yes \
target-user@198.51.100.20
然后在浏览器中打开这个内网页面:
http://127.0.0.1:50052/
对浏览器来说,内网 Web 服务就在本机;对内网 Web 服务来说,请求来自目标服务器上的 SSH 连接。
完整链路
这条命令建立的是下面这条 TCP 链路:
本地 浏览器
-> 本地 127.0.0.1:50052
-> SSH 本地端口转发
-> 跳板机 192.0.2.10:22022
-> 目标服务器 198.51.100.20
-> 目标服务器 127.0.0.1:50052
-> 内网 HTTP 服务
这里有两个 127.0.0.1:50052,但它们不是同一个地址:
-L左侧的50052是当前电脑上的监听端口-L右侧的127.0.0.1:50052是目标服务器视角下的地址
127.0.0.1 永远表示“执行连接的那台机器自己”。SSH 收到本地连接后,会让目标服务器上的 SSH 服务去连接它自己的 127.0.0.1:50052。
逐项解释命令
原始命令是:
ssh -fN \
-J jump-user@192.0.2.10:22022 \
-L 50052:127.0.0.1:50052 \
-o ExitOnForwardFailure=yes \
target-user@198.51.100.20
-J
-J jump-user@192.0.2.10:22022
指定通过跳板机连接目标服务器。跳板机端口是 22022,目标服务器未指定端口,所以使用默认的 SSH 端口 22。
-J 只负责建立到目标服务器的 SSH 连接路径,不负责访问内网 Web 服务。后面的 -L 才是端口转发规则。
-L
-L 50052:127.0.0.1:50052
-L 的通用格式是:
-L [本地监听地址:]本地端口:目标地址:目标端口
所以这条规则表示:
当前电脑 127.0.0.1:50052
-> 目标服务器 127.0.0.1:50052
没有写本地监听地址时,OpenSSH 默认只在本机 loopback 地址上监听。也就是说,其他电脑不能直接连接这个转发端口。
为了把这个边界写得更明确,也可以写成:
-L 127.0.0.1:50052:127.0.0.1:50052
-N
-N 表示不在目标服务器上执行远程命令。这里 SSH 的任务只有端口转发,不需要打开 shell。
-f
-f 会在 SSH 完成认证、准备建立会话后进入后台。因此这条命令执行成功后,终端会立即返回,可以直接在浏览器中打开页面。
第一次调试时建议先不要加 -f,让 SSH 保持在前台,这样更容易看到错误:
ssh -N \
-J jump-user@192.0.2.10:22022 \
-L 50052:127.0.0.1:50052 \
-o ExitOnForwardFailure=yes \
target-user@198.51.100.20
确认隧道工作后,再使用 -fN 放到后台。
ExitOnForwardFailure
-o ExitOnForwardFailure=yes
要求 SSH 在无法建立请求的端口转发时直接退出。最常见的情况是当前电脑的 50052 已经被其他程序占用。
没有这个选项时,SSH 连接本身可能成功,但端口转发没有按预期建立,之后浏览器只会看到“连接失败”。加上它后,建立隧道的命令本身就能报告失败。
这个选项只保证转发在启动阶段成功,不保证目标服务一直在线。目标服务后来退出,或者 SSH 网络连接中断,浏览器中的页面会加载失败或断开。
先确认目标服务确实存在
在目标服务器上检查 50052 是否监听:
ss -lntp | grep 50052
如果服务只监听在目标服务器的 127.0.0.1:50052,当前的 -L 写法是正确的。
也可以从目标服务器本机测试 HTTP 接口:
curl -v 'http://127.0.0.1:50052/'
如果服务监听在目标服务器的其他地址或另一台内网机器上,右侧的目标地址应该改成“目标服务器能够访问的地址”。例如服务实际在另一台内网机器:
ssh -fN \
-J jump-user@192.0.2.10:22022 \
-L 50052:198.51.100.21:50052 \
-o ExitOnForwardFailure=yes \
target-user@198.51.100.20
此时 198.51.100.21:50052 是从 198.51.100.20 的网络视角访问的,而不是从当前电脑访问的。
确认本地转发已经建立
在 macOS 上可以查看本地监听进程:
lsof -nP -iTCP:50052 -sTCP:LISTEN
在 Linux 上也可以使用:
ss -lntp | grep 50052
看到 SSH 进程监听 127.0.0.1:50052 后,在浏览器中打开:
http://127.0.0.1:50052/
如果只是想先确认 HTTP 请求能否通过隧道,可以使用:
curl -v 'http://127.0.0.1:50052/'
如果目标是长连接或持续输出的 HTTP 接口,curl 不一定会正常结束;它的作用是确认请求已经到达服务端。查看页面时使用浏览器更合适。
本地端口被占用
如果执行命令时看到类似 bind: Address already in use,先找出占用本地端口的进程:
lsof -nP -iTCP:50052 -sTCP:LISTEN
可以停止确认无用的进程,也可以换一个本地端口。比如左侧改成 50053,右侧仍然是目标服务的 50052:
ssh -fN \
-J jump-user@192.0.2.10:22022 \
-L 50053:127.0.0.1:50052 \
-o ExitOnForwardFailure=yes \
target-user@198.51.100.20
访问地址也要随之修改:
http://127.0.0.1:50053/
-L 左侧是本地端口,右侧是远端服务端口,两者不需要相同。
隧道断开怎么办
-fN 只是把 SSH 进程放到后台,不会自动重连。对于临时访问内网服务,断开后重新执行命令通常就够了。
如果需要让 SSH 更快发现断线,可以增加 keepalive:
ssh -fN \
-J jump-user@192.0.2.10:22022 \
-L 50052:127.0.0.1:50052 \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
target-user@198.51.100.20
这只能让连接在断开后退出,不能自动重建隧道。需要长期运行时,可以再使用 autossh 或 systemd 管理重连;短时间的服务访问不必把问题复杂化。
查看后台 SSH 进程:
pgrep -af 'ssh.*198.51.100.20'
停止前先用 lsof 找到监听 50052 的准确 PID,然后执行:
kill <pid>
不要直接结束所有 SSH 进程,因为当前终端或其他业务连接也可能正在使用 SSH。
安全和性能边界
这个转发默认只在当前电脑的 127.0.0.1 上监听。不要为了让其他电脑也能访问,随意改成:
-L 0.0.0.0:50052:127.0.0.1:50052
这样会把内网 HTTP 服务暴露给当前电脑所在网络,可能绕过原本的访问控制。如果确实需要共享,应另外配置防火墙、认证和明确的监听范围。
通过隧道传输的数据会经过当前电脑、跳板机和目标服务器之间的 SSH 连接,实际访问体验取决于这条链路的带宽、延迟和稳定性。SSH 负责加密传输,但不会替内网 Web 服务解决响应速度、连接数或权限问题。
这条命令的关键思路
这条命令把三个能力组合到了一起:
-J 解决如何到达目标服务器
-L 把目标服务器上的 TCP 服务映射到本地
-fN 让只负责转发的 SSH 连接进入后台
最终,浏览器 不需要理解跳板机,也不需要知道目标服务器的内网地址。它只访问本机的 127.0.0.1:50052,剩下的路径由 SSH 完成。
