正在阅读用 MBP 和 Ubuntu 理解 SSH:本地转发、反向转发与动态转发
香港天气加载中…
QWeather ↗

用 MBP 和 Ubuntu 理解 SSH:本地转发、反向转发与动态转发

围绕 MBP 10.9.42.98 与 Ubuntu 10.9.1.52,逐条拆解 -L、-R、-D:入口在哪、谁连接目标、SOCKS 如何提交地址,以及怎样用 curl 验证而不被缓存与代理配置误导。

先看全局:入口在哪里,谁连接目标

本地转发和反向转发,首先区别在于监听入口放在哪一侧;动态转发进一步允许应用为每条新连接指定目标。先把“入口”“SSH 连接两端”“最终目标”分开看,再读命令中的地址与端口。

本文的三条隧道命令都在 MacBook Pro M5(MBP)本机执行,SSH 连接的服务器都是 Ubuntu。这里用“本地端口转发”“远程/反向端口转发”来称呼 -L 与 -R。

方式监听入口谁发起到目标的连接本次实验的效果
-L 本地转发MBP 的 127.0.0.1:18000UbuntuMBP 读取 Ubuntu 的网页
-R 反向转发Ubuntu 的 18000,监听地址受服务端配置约束MBPUbuntu 读取 MBP 的网页
-D 动态转发MBP 的 SOCKS 入口 127.0.0.1:1080UbuntuMBP 的应用通过 Ubuntu 访问每条连接指定的目标

三种方式建立后的数据都可以双向传输。“反向”不表示响应只能单向流动,也不表示 Ubuntu 必须反过来主动建立到 MBP 的 SSH 登录连接。

依据:[1]。

实验准备:用两张不同的网页辨认来源

本次设备信息来自学习讨论:

机器地址本次角色
MacBook Pro M510.9.42.98执行隧道命令的 SSH 客户端;也是其中一台 HTTP 服务主机
Ubuntu10.9.1.52SSH 服务端,登录用户 lizhao1;也是一台 HTTP 服务主机

假定 MBP 可以正常 SSH 登录 Ubuntu,两台机器有 Python 3 和 curl,示例中的监听端口没有被其他程序占用。SSH 使用默认 22 端口;如果实际端口不同,在 ssh 命令中增加 -p 实际端口。

① Ubuntu 服务终端:先从 MBP 登录 Ubuntu

MBP 本机 · 登录 Ubuntu
ssh lizhao1@10.9.1.52

看到 Ubuntu 的 shell 后,执行下面的命令,并保持这个服务终端运行:

Ubuntu · 启动测试网页
ssh_demo_dir=$(mktemp -d)
printf '%s\n' '<h1>Hello from Ubuntu: 10.9.1.52</h1>' > "$ssh_demo_dir/index.html"
python3 -m http.server 8000 --bind 127.0.0.1 --directory "$ssh_demo_dir"

② MBP 服务终端:另开一个 MBP 本机终端

在这个终端直接执行,不先登录 Ubuntu:

MBP 本机 · 启动测试网页
ssh_demo_dir=$(mktemp -d)
printf '%s\n' '<h1>Hello from MacBook Pro M5: 10.9.42.98</h1>' > "$ssh_demo_dir/index.html"
python3 -m http.server 8000 --bind 127.0.0.1 --directory "$ssh_demo_dir"

两台机器都使用 8000 不会相互占用:端口属于各自机器。两个服务均绑定回环地址,只接收各自本机发起的连接;这正好用来观察 SSH 转发的作用。临时目录只提供实验页面,避免把当前工作目录作为网页目录。

③ 先验证服务本身,再建立隧道

在对应机器的另一个终端中分别运行:

分别在 MBP 和 Ubuntu · 本机检查
curl -q --noproxy '*' --max-time 3 http://127.0.0.1:8000/
执行位置预期响应正文
MBP<h1>Hello from MacBook Pro M5: 10.9.42.98</h1>
Ubuntu<h1>Hello from Ubuntu: 10.9.1.52</h1>

接下来额外保留一个隧道终端,三条隧道命令都在 MBP 本机运行。按顺序实验时,完成一节后,在隧道终端按 Ctrl+C 停掉该隧道;两台机器的 HTTP 服务可以一直保留到全部实验结束。验证终端是否在 MBP 或 Ubuntu,以各节标注为准。

依据:[1]、[5]。

本地转发 -L:让 MBP 读取 Ubuntu

目标:在 MBP 访问本机 18000 端口,读到 Ubuntu 的 8000 端口提供的网页。

① 在 MBP 本机的隧道终端执行

MBP 本机 · 建立 -L 隧道
ssh -N -o ExitOnForwardFailure=yes \
  -L 127.0.0.1:18000:127.0.0.1:8000 \
  lizhao1@10.9.1.52

-N 表示不执行远程命令。连接成功后,终端保持在前台且通常没有输出;不要为了“返回提示符”立即关闭它,最终是否可用仍要靠下面的请求验证。

② 在另一个 MBP 本机终端验证

MBP 本机 · 验证 -L
curl -q --noproxy '*' --max-time 3 http://127.0.0.1:18000/

预期正文:<h1>Hello from Ubuntu: 10.9.1.52</h1>。同时,Ubuntu 的 Python 服务终端应出现这次 GET 请求的日志。

③ 把两个 127.0.0.1 分开读

命令片段在这个例子里的含义
-L 127.0.0.1:18000在 MBP 的回环地址监听 18000,供 MBP 的程序连接
:127.0.0.1:8000让 Ubuntu 连接它自己的回环地址上的 8000
lizhao1@10.9.1.52MBP 登录的 SSH 服务端是 Ubuntu
  1. MBP 的 curl 连接 MBP 的 18000。
  2. MBP 的 SSH 客户端通过现有 SSH 连接向 Ubuntu 请求转发。
  3. Ubuntu 发起到 Ubuntu 自己 127.0.0.1:8000 的 TCP 连接。
  4. 网页内容经过这条转发连接返回给 MBP。

完整写法是 -L [监听地址:]监听端口:目标地址:目标端口。目标地址也可以是 Ubuntu 能到达的第三台机器;-L 并不限定只能访问 SSH 服务器自身。

观察隧道是否必要:在隧道终端按 Ctrl+C 后,再从 MBP 发起同一条新的 curl 请求。在没有其他程序接管 18000 的条件下,它应失败;Ubuntu 的 HTTP 服务此时仍可继续运行。

依据:[1]。

反向转发 -R:让 Ubuntu 读取 MBP

目标:在 Ubuntu 访问它的 18000 端口,读到 MBP 的 8000 端口提供的网页。确认上一节的隧道已停止,并保留两台机器的 HTTP 服务。

① 仍然在 MBP 本机的隧道终端执行

MBP 本机 · 建立 -R 隧道
ssh -N -o ExitOnForwardFailure=yes \
  -R 127.0.0.1:18000:127.0.0.1:8000 \
  lizhao1@10.9.1.52

② 登录 Ubuntu,再在 Ubuntu 中验证

另开一个 MBP 终端,运行 ssh lizhao1@10.9.1.52。进入 Ubuntu 的 shell 后执行:

Ubuntu · 验证 -R
curl -q --noproxy '*' --max-time 3 http://127.0.0.1:18000/

预期正文:<h1>Hello from MacBook Pro M5: 10.9.42.98</h1>。同时,MBP 的 Python 服务终端应出现这次 GET 请求的日志。

命令片段在这个例子里的含义
-R 127.0.0.1:18000请求在 Ubuntu 的回环地址监听 18000;最终监听范围由 Ubuntu 的 sshd 配置约束
:127.0.0.1:8000让 MBP 连接 MBP 自己的回环地址上的 8000
lizhao1@10.9.1.52SSH 连接依然由 MBP 主动连接 Ubuntu
  1. Ubuntu 的 curl 连接 Ubuntu 的 18000。
  2. Ubuntu 的 SSH 服务端把这次连接通过现有 SSH 通道交给 MBP。
  3. MBP 的 SSH 客户端连接 MBP 自己的 127.0.0.1:8000。
  4. MBP 的网页内容沿这条连接返回给 Ubuntu。

在 Ubuntu sshd 默认 GatewayPorts=no 时,远端入口只允许 Ubuntu 本机访问。本实验通过 Ubuntu 本机验证,不需要为了外部设备访问而开启 GatewayPorts。若服务端已经改为 yes,它会强制通配地址监听,详见后面的监听范围表。

目标也可以是 MBP 能访问的其他机器。完成这一节后停止隧道;如果验证终端还在 Ubuntu shell,先用 exit 返回 MBP,再继续动态转发实验。

依据:[1]、[2]。

动态转发 -D:同一个入口,连接不同目标

-L 的每个监听端口对应命令中预先指定的目标。-D 在本地创建 SOCKS 代理入口,由应用在建立每条代理连接时提供目标地址和端口。

① 在 MBP 本机建立动态转发

MBP 本机 · 建立 -D SOCKS 入口
ssh -N -o ExitOnForwardFailure=yes \
  -D 127.0.0.1:1080 \
  lizhao1@10.9.1.52

这个命令指定入口 1080 和 SSH 服务端 Ubuntu,没有写死最终目标。MBP 上的 ssh 程序本身承担 SOCKS 代理角色,无须额外启动一个 SOCKS 服务程序。

② 同样的 URL,比较直接访问与代理访问

确认 Ubuntu 的 8000 服务仍在运行。先在 MBP 本机直接访问:

MBP 本机 · 直接访问本机 8000
curl -q --noproxy '*' --max-time 3 http://127.0.0.1:8000/

预期读到 Hello from MacBook Pro M5。

然后仍在 MBP 本机,明确通过刚才的 SOCKS 入口访问同一个 URL:

MBP 本机 · 经 SOCKS 访问 Ubuntu 的 8000
curl -q --noproxy "" \
  --proxy socks5h://127.0.0.1:1080 \
  --max-time 10 \
  http://127.0.0.1:8000/

这次预期读到 Hello from Ubuntu。代理参数中的 127.0.0.1:1080 属于 MBP;URL 中的目标由 Ubuntu 发起连接,所以它连接的是 Ubuntu 自己的 127.0.0.1:8000。

③ 保留同一条 SSH 隧道,只改变目标 URL

如果 Ubuntu 的网络和策略允许访问外网,可以在 MBP 运行:

MBP 本机 · 经同一个 SOCKS 入口访问另一个目标
curl -q --noproxy "" \
  --proxy socks5h://127.0.0.1:1080 \
  --max-time 10 \
  https://example.com/

这次目标变为 example.com:443,入口仍是 MBP 的 1080,SSH 服务端仍是 Ubuntu。Ubuntu 能否连接这个目标,是此次访问能否成功的必要条件。

socks5h 会把目标域名交给代理链路处理;在这套 SSH 转发中由 Ubuntu 侧解析。socks5 则由 curl 本地解析目标域名后提交地址。前一个 localhost 实验使用 IP 字面量,本身没有验证 DNS 解析位置的差异。

浏览器需要设置 SOCKS 代理 127.0.0.1:1080,再访问实际网站。直接把 http://127.0.0.1:1080/ 当成网页地址,会发送 HTTP 请求,而这个端口首先需要 SOCKS 协议交互。

依据:[1]、[4]。

SOCKS 的作用:应用把目标告诉 SSH

“动态转发是不是多调用一次 SOCKS,让 SOCKS 指定目标端口?”这个理解抓住了新增的目标协商过程,但可以更精确:

以 curl 使用 socks5h 访问 https://example.com/ 为例:

步骤发生的事
1 · 连接代理入口MBP 的 curl 连接 MBP 的 127.0.0.1:1080
2 · SOCKS 握手与请求curl 按 SOCKS5 进行协商,然后提交 CONNECT 请求,携带目标 example.com 和端口 443
3 · 交给 SSH 转发MBP 的 ssh 解析请求,通过现有 SSH 连接要求 Ubuntu 建立相应的目标连接
4 · Ubuntu 连接目标Ubuntu 按请求连接 example.com:443
5 · 传输业务数据连接建立后,curl 与目标网站的数据通过该转发连接双向传输

这里的“一次”应理解为每条新的被代理 TCP 连接开始时的一段 SOCKS 交互,它可以包含多条协议消息。同一条连接中的后续数据沿已确定的连接传输;不会因为发送了一个新的数据包就重新选择目标。一条保持连接的 HTTP 会话也不一定为每个请求重新进行 SOCKS 握手。

-D 支持 SOCKS4/5 的 TCP 连接转发。它不会自动接管所有程序,也不能据此推断 UDP、QUIC 或 ping 会走这条隧道。应用需要明确使用代理,目标还要满足 Ubuntu 的网络可达性及 SSH 转发策略。

SSH 保护的是 MBP 与 Ubuntu 之间的链路。推广到第三方目标时,Ubuntu 到目标这一段的应用数据是否加密,取决于应用协议;例如 HTTPS 使用 TLS 保护客户端与网站之间的数据。

依据:[1]、[6]、[8]。

互换机器后,本地与远程也随之互换

在“目标服务分别位于这两台机器”的前提下,下面四种理解都成立:

在哪台机器运行 ssh方式入口在哪可以实现的访问
MBP,连接 Ubuntu-LMBPMBP 访问 Ubuntu 的指定服务
MBP,连接 Ubuntu-RUbuntuUbuntu 访问 MBP 的指定服务
Ubuntu,连接 MBP-LUbuntuUbuntu 访问 MBP 的指定服务
Ubuntu,连接 MBP-RMBPMBP 访问 Ubuntu 的指定服务

同样实现“MBP 访问 Ubuntu”,可以选择 MBP 发起 -L,也可以选择 Ubuntu 发起 -R。两种方式要求的 SSH 登录方向不同:前者需要 MBP 能登录 Ubuntu,后者需要 Ubuntu 能登录 MBP。已经证明前者可用,并不能直接证明后者也可用。

所以“本地/反向”确实是一组相对概念,但参照物是发起当前 SSH 会话的客户端。端口转发提供的是指定的网络服务访问,不代表获得对方所有资源的访问能力。

本次三条主实验命令都没有出现 MBP 的 10.9.42.98,因为它本身就是 SSH 发起方,本机监听或本机目标又使用了回环地址;地址没有显式写出,不等于 MBP 没有参与。

依据:[1]。

用 curl 验证:排除浏览器缓存,保留证据边界

新的 curl 请求可以避开浏览器本地缓存造成的观察干扰,但不能保证排除所有缓存。普通 curl 调用不读取 Chrome 的响应缓存;CDN、反向代理、服务端应用缓存仍可能返回以前保存的内容。

在本次直连 -L 或 -R 入口的实验中,可以增加详细连接日志:

-L 在 MBP;-R 在 Ubuntu · 查看连接过程
curl -q --noproxy '*' -v --max-time 3 http://127.0.0.1:18000/
参数本例中的用途
-q作为 curl 的第一个选项,跳过默认 curlrc;它不是关闭 HTTP 缓存的开关
--noproxy '*'直接访问转发入口,不受 curl 代理配置影响;动态代理实验改用空字符串
-v展示连接与协议过程;它不是禁用缓存的选项
--max-time 3给本机演示请求设置时间上限;超时本身不足以唯一确定故障原因

哪些证据组合起来才有说服力?

  1. 请求在正确机器上执行,并使用正确入口。
  2. 响应正文能辨认真实服务来源:Ubuntu 或 MBP。
  3. 对应的 Python 服务终端出现这次新 GET 的日志。
  4. 停止对应隧道后,新的入口请求失败,而服务本机的 8000 检查仍能成功。

每次独立启动 curl,都不会复用上一次 curl 进程中的连接。这不意味着已有 SSH 连接也被重新建立:新的被转发连接可以继续使用同一条 SSH 会话。

对普通互联网 HTTP 请求,如需请求缓存先验证再复用,可以发送:

普通 HTTP 缓存验证请求
curl -i -H 'Cache-Control: no-cache' https://example.com/

no-cache 的含义是请求先验证再复用,不能视为保证回源重新生成内容;请求缓存指令是否被遵守也需要实际观察。no-store 不会自动让已经存储的响应失效。新的网络请求、缓存验证、应用重新计算,是三个不同的事件。

依据:[4]、[7]。

监听范围、失败处理与实验收尾

本实验为什么不要求修改 GatewayPorts?

-R 的验证请求由 Ubuntu 本机发出,不需要让其他机器直接访问 Ubuntu 的转发入口。Ubuntu 的 sshd_config 中,GatewayPorts 的几种取值含义是:

服务端 GatewayPorts远端转发监听范围
no(默认)限制为回环地址,只供服务端本机访问
yes强制绑定通配地址;不能把命令中的回环地址当作最终监听范围的保证
clientspecified允许 SSH 客户端选择监听地址

如果将来要让第三台机器直接访问这个远端入口,还要同时考虑绑定地址、路由和防火墙等实际条件。本实验不需要为这个目标额外开放监听范围。

参数能保证什么?

参数或现象准确解释
-N只建立连接和转发,不执行远程命令
-p指定外层 SSH 服务端的端口,不是最终 HTTP 目标端口
-f认证后进入后台;初次实验保留前台,便于看到错误并停止
-C启用压缩;效果取决于链路和数据,不能认为总是更快
ExitOnForwardFailure=yes请求的转发无法建立时退出,例如入口监听失败;不检查最终 HTTP 服务是否健康,也不因随后某次目标连接失败而必然退出
SSH 终端保持无输出可能是 -N 正常运行状态;需要业务请求确认转发真正可用

按失败发生的位置排查

观察结果下一步检查
服务本机访问 8000 就失败先检查 Python 服务是否运行、是否端口占用、是否在预期机器上执行
SSH 报认证或连接错误确认 MBP 到 Ubuntu 的 SSH 端口、用户和登录凭据
转发入口建立失败看终端原始错误,检查端口占用及服务端是否允许该方向的转发
入口能连接,但目标访问失败检查真正发起目标连接的那台机器是否能访问目标服务
动态转发意外读到 MBP 页面确认 curl 是否真的配置 SOCKS,以及是否错误使用了 --noproxy '*' 或被 NO_PROXY 排除
请求超时结合 SSH 日志、curl -v 和服务端日志判断;不单凭超时断言唯一原因

服务端还可能通过 AllowTcpForwarding、DisableForwarding、PermitOpen、PermitListen 等配置限制转发;能登录成功不等于所有转发都被允许。

停止实验:在当前隧道终端按 Ctrl+C;在两个 Python 服务终端分别按 Ctrl+C。需要退出 Ubuntu shell 时使用 exit。由 mktemp 创建的两个实验目录可以在确认路径后按需清理。

依据:[1]、[2]、[3]。

复盘问题与官方资料

这些问题用于自行检查理解;文章发布不代表已经在设备上实测通过,也不代表已经完成复盘。

在 MBP 执行 -L 与 -R 时,右侧的 127.0.0.1 分别指谁?

-L 由 Ubuntu 连接目标,因此右侧回环地址属于 Ubuntu;-R 由 MBP 连接目标,因此右侧回环地址属于 MBP。

为什么动态转发没有在 ssh 命令中写目标端口?

应用会在每条新代理连接开始时,通过 SOCKS 请求提交目标地址和端口。MBP 的 SSH 客户端据此请求 Ubuntu 建立目标连接。

能否用 --noproxy '*' 来验证 -D?

它会让 curl 对所有目标绕过代理,因此不能用来验证该 SOCKS 路径。使用 --noproxy 空字符串,并显式指定 socks5h 代理。

已经拿到一个网页,是否就证明了预期隧道工作?

还要核对执行机器、入口、代理配置与页面来源,并观察真正服务端的新请求日志。错误地绕过代理,也可能返回另一台机器上的正常网页。

官方资料

  1. OpenSSH ssh(1):-L、-R、-D、-N、-p、-f、-C 的行为与转发方向。
  2. OpenSSH sshd_config(5):GatewayPorts 与服务端转发限制。
  3. OpenSSH ssh_config(5):ExitOnForwardFailure 的保证范围。
  4. curl 官方手册:--noproxy、--proxy、socks5h、-q、连接复用与详细日志。
  5. Python http.server:本机演示 HTTP 服务的启动、绑定和目录参数。
  6. RFC 1928:SOCKS5:握手、CONNECT 请求、目标地址与端口字段。
  7. RFC 9111:HTTP Caching:缓存验证与请求缓存指令的边界。
  8. RFC 4254:SSH Connection Protocol:转发通道与目标连接机制。

回到学习地图,沿概念关系复盘 ↗

SSH端口转发SOCKS网络基础curl

整理于 2026-10-19 · 基于本次学习讨论与官方协议文档。设备信息由用户提供,实验输出均标为预期结果;命令需在对应机器执行验证。

MORE IN DEVELOPMENT3.1 数据结构分类:逻辑关系、存储方式与 Python 容器