SSH RemoteForward 踩坑:为什么 VS Code 自动转发会把本地代理绕死

昨天遇到一个奇怪的问题,我一连服务器,本地的代理就挂掉,国内的网站也进不去,一断开服务器,本地的网络就恢复正常。最后发现是端口转发出了问题。于是记录一下整个过程。

📚 基本概念速读

名称 定义 省流
Port(端口) 操作系统给网络程序分配的逻辑入口编号。 一台机器里不同网络服务的门牌号
SSH Secure Shell,远程登录与加密隧道工具。 远程终端 + 隧道
RemoteForward SSH 的远程端口转发,把远端某个端口的流量送回本地。 让服务器“借用”你本机端口
LocalForward SSH 的本地端口转发,把本地某个端口送到远端。 让本机访问远端服务
Proxy(代理) 代理服务,代替客户端向目标站点发请求。 中间人转发流量
VS Code Port Forwarding VS Code Remote SSH 自动探测并映射端口的功能。 帮你转发,但有时会帮倒忙

如果用更白话的话说:

  • SSH 不只是远程登录工具,本质上还能顺手带一条加密管道
  • RemoteForward 是让远端机器“借用”当前客户端上的某个端口
  • LocalForward 是让本地机器“借道”去访问远端的某个端口
  • Proxy 不是目标服务本身,而是一个代你转发请求的中间层
  • VS Code 自动端口转发 则是在远端发现服务后,顺手帮你把访问入口搬回本机

🧱 程序(program)、端口(port)、套接字(socket)到底是什么关系

  • 程序(program):真正运行中的进程(process),比如 Clash Vergecurlnpm devsshd
  • 端口(port):操作系统上的一个编号,比如 7897,可以理解成网络入口的门牌号
  • 套接字(socket):程序真正拿来监听(listen)或通信的对象,可以粗略理解成“某个程序占住了某个 IP:端口”之后形成的那个网络端点

放到这次问题里看,会更容易理解:

  • 不是“端口自己在工作”,而是某个程序(program / process)创建了套接字(socket)并监听(listen)这个端口
  • 更准确地说,通常是同一个 IP:端口 组合的监听套接字,只能被一个程序成功持有,比如 127.0.0.1:7897 正常情况下只能有一个监听者
  • 所以看到 7897 在监听,只能说明“有程序占了这个门牌号”,不能直接说明是谁在处理流量
  • lsofssnetstat 这类命令的意义,就是帮你把“端口”反查回“哪个程序持有了对应套接字”

这也是为什么排障时不能只盯着“端口号是不是一样”,而要盯着:

  • 当前到底是谁持有了这个 IP:端口 的监听套接字
  • 是不是本来该由 Clash 持有,后来却变成了 VS Codessh helper

一旦监听者变了,流量路径就会跟着变。

这也是为什么这次故障排查不能只看“7897 通不通”,还得看:

  • Clash 持有本地 7897
  • 还是 VS Code / ssh helper 持有本地 7897
  • 或者两边是否在不同阶段都试图接管这个端口

从这个角度说,RemoteForward 和 VS Code 自动端口转发,本质上都是在安排“由哪个程序持有哪个套接字,并把流量交给谁继续处理”。

🧩 网络拓扑到底长什么样

我使用的设备如下:

  • macbook pro 2020
  • win xiaoxin 14pro 2021
  • linux 实验室的高性能服务器 atlanta

当时的 SSH 配置如下:

1
2
3
4
Host Atlanta
HostName 10.16.98.45
User lzh
RemoteForward 7897 127.0.0.1:7897
  • 从 Win 或 Mac SSH 到 atlanta
  • SSH 连接建立后,会让 atlanta 打开一个本地监听端口 7897
  • 任何打到 atlanta:7897 的流量,都会沿着 SSH 隧道回到当前客户端的 127.0.0.1:7897
  • 而客户端本机的 127.0.0.1:7897,正好是 Clash Verge 的代理入口

也就是说,atlanta 并没有真正部署代理,只是“借用了当前登录机器上的本地代理”。

可以看这张链路拓扑图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+------------------------+    SSH 连接 / 隧道     +------------------------+
| Win / Mac | <-------------------> | atlanta |
| | | |
| Clash Verge :7897 | | localhost:7897 |
| 本地代理监听 | | RemoteForward 监听口 |
+-----------+------------+ +-----------+------------+
^ |
| RemoteForward 把远端 7897 的请求送回本地 | 远端程序访问代理
| v
+---------------------------------------- atlanta curl / npm

Clash :7897
-> 机场节点
-> 目标网站 / API

🔌 端口为什么会“撞车”

端口不是全局唯一的,它只在一台机器的网络命名空间里唯一。

这意味着:

  • 你的 Win 本机可以有一个 127.0.0.1:7897
  • atlanta 也可以有一个自己的 127.0.0.1:7897
  • SSH 的 RemoteForward 7897 127.0.0.1:7897,是在把这两个同号端口绑定成一条隧道

问题在于,VS Code Remote SSH 还有一套自己的端口转发系统。它会自动扫描远端监听端口,并尝试把它们转发回本地,方便你在浏览器或本机工具里访问。

如果它看到 atlanta 上有个 7897 在监听,它可能会再做一层类似这样的映射:

1
本地 7897  <---- VS Code 自动转发 ----  atlanta 7897

不过这里要注意一件事:这不是 VS Code 的默认必然行为。官方文档写得很明确,如果本地同号端口已经被占用,它通常会改绑到别的本地端口,而不是硬抢这个端口。也就是说,更常见的情况其实是:

1
本地 4123  <---- VS Code 自动转发 ----  atlanta 7897

atlanta 7897 本来又是:

1
atlanta 7897  <---- SSH RemoteForward ----  本地 127.0.0.1:7897

两层叠起来,就容易出现环路或自咬尾巴。

🔄 这次故障最可能的形成过程

按当时的现象,最合理的解释是:

  1. 我在 Win 上通过 VS Code Remote SSH 连到 atlanta
  2. SSH 按配置建立了 RemoteForward,使 atlanta:7897 -> Win本地:7897
  3. VS Code 又检测到远端 7897 正在监听,于是自动做了端口转发
  4. 按 VS Code 的正常行为,如果 Win 本地 7897 已经被 Clash 占着,它本来应该改绑到别的本地端口,而不是继续占 7897
  5. 因此,真正危险的情况更像是:VS Code 因为恢复了旧的端口映射,或者因为启动时序问题,实际又拿到了 Win 本地 7897
  6. 一旦这件事发生,“Win 本地 7897”就不再只是 Clash 的入口,还会被 VS Code 的转发链路卷进去
  7. 最终结果就是:本地代理流量绕回 SSH,再绕回远端,再被转回来,形成回环或错误接管

把这个过程单独画成“故障回环顺序图”,会更直观:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
① SSH 先建立 RemoteForward

atlanta:7897
-> SSH RemoteForward
-> Win 本地 127.0.0.1:7897
-> Clash Verge

② VS Code 发现 atlanta:7897 在监听

本地某端口
<- VS Code 自动转发 ->
atlanta:7897

③ 正常情况

本地 4123
<- VS Code 自动转发 ->
atlanta:7897

Clash 继续独占本地 7897

④ 危险情况:VS Code 因恢复旧映射或时序问题实际占回本地 7897

本地 7897
<- VS Code 自动转发 ->
atlanta:7897
-> SSH RemoteForward
-> 本地 7897

结果:同一个端口同时处在“转发入口”和“代理出口”上,链路开始自绕

当时观察到的两个现象正好支持这个推断:

  • 只要断开 VS Code SSH,Win 本地网络立刻恢复
  • 只要删掉 VS Code 里自动转发的 7897,问题立刻消失

🛠️ SSH 端口转发到底是怎么工作的

RemoteForward

RemoteForward A B:C

意思是:

  • 在远端机器监听 A
  • 收到连接后,通过 SSH 隧道送到本地的 B:C

这里的配置:

1
RemoteForward 7897 127.0.0.1:7897

等价于:

1
atlanta:7897  ->  当前客户端:127.0.0.1:7897

这里很容易混淆的一点是:

  • SSH 隧道 本身是双向承载数据的
  • RemoteForward 定义的入口方向并不是对等的

它真正表达的是:

  • 由远端程序去连接 atlanta:7897
  • SSH 再把这条连接转交给当前客户端本机的 127.0.0.1:7897

所以更准确的理解应该是:

1
2
3
4
atlanta 上的程序
-> 连接 atlanta:7897
-> SSH RemoteForward
-> 当前客户端的 127.0.0.1:7897

这条连接一旦建立,请求和响应都会在同一条被转发的 TCP 连接里双向流动。也就是说:

  • atlanta 发给 atlanta:7897 的请求,会被送到当前客户端的 7897
  • 当前客户端上的代理再去访问外网
  • 外网返回的数据,会沿着同一条连接再返回给 atlanta

但这不等于“当前客户端也能靠这条 RemoteForward 主动去访问 atlanta:7897”。RemoteForward 解决的是“让远端借用本地服务”,不是让两端在这个端口上获得对等的主动访问能力。

因此,只要在 atlanta 上配置:

1
2
export http_proxy='http://127.0.0.1:7897'
export https_proxy='http://127.0.0.1:7897'

流量其实并不是从 atlanta 直接出国,而是:

1
2
3
4
5
6
7
atlanta curl
-> atlanta:7897
-> SSH 隧道
-> Win 本地 Clash:7897
-> 机场节点
-> 目标站点
<- 响应原路返回

VS Code 自动端口转发

VS Code Remote SSH 会做另一件事:

  • 发现远端某个端口在监听
  • 自动把它映射到本机
  • 让你本机直接访问远端服务

这个功能本来是给 300080808888 这类开发服务准备的,比如远端起了 Web 服务,你本机浏览器可以直接打开。

它的核心作用其实很朴素:让“明明跑在远端”的服务,用起来像“跑在本机”一样顺手。这样你就不用每次都自己记 SSH 隧道命令,也不用手工再开一层 ssh -L

典型应用场景一般有这些:

  • 远端跑了前端开发服务器,比如 Vite、Next.js、Storybook,本机浏览器直接开 localhost
  • 远端起了 Jupyter、Gradio、Streamlit、TensorBoard 这类本地 Web 面板,本机可以直接点开预览
  • 远端临时起了一个调试 API、管理后台或文档站,不想专门手敲隧道命令
  • 容器、跳板机或云主机里的服务只监听 127.0.0.1,但你仍然想在本地 GUI 里访问它

所以它最适合的是“开发服务”和“临时调试入口”。这类服务的共同特点是:

  • 端口是给人看的
  • 访问方向通常是“本机去看远端”
  • 即使换一个本地端口也无所谓,只要能打开就行

它的探测方式,VS Code 团队在 issue 里解释过,主要有两类:

  • process:轮询远端 /proc,找哪些进程正在监听端口
  • output:扫描终端或调试输出里像 URL / 端口的内容

另外,官方文档也说明了一个关键细节:如果本地同号端口已经被占用,VS Code 往往会换一个本地端口,比如把远端 3000 映射到本地 4123

所以对这次故障,更严谨的表述应该是:

  • 不是“VS Code 一看到远端 7897 就一定会再占本地 7897
  • 而是“VS Code 会自动转发远端 7897;只有它因为恢复旧映射或时序问题真的落回本地 7897 时,才会和 RemoteForward 叠成自环”

也正因为如此,7897 这种本来就参与 SSH 隧道的代理口,确实属于“不该自动转发的端口”。

原因也不复杂:代理口和普通开发端口的角色完全不一样。

  • 开发端口通常是“给你打开页面用的”
  • 代理端口则是“给别的流量继续转发用的”

前者被 VS Code 额外包一层映射,通常只是更方便;后者如果再被 IDE 接管,就可能改变原来的流量路径,甚至把链路绕回自己。

⚠️ 为什么这类问题特别隐蔽

因为它会同时制造两种错觉:

现象 容易误判成什么 实际更可能是什么
atlanta curl 无响应 远端没网、机场挂了 远端代理口被错误转发
Win 本地机场节点看起来正常 机场没问题 但本地代理出口发生环路
断开 VS Code SSH 后马上恢复 偶发网络波动 SSH/VS Code 转发链路有问题
删除自动转发 7897 后恢复 临时玄学 端口冲突被解除

根本原因是:表面上坏的是“网络”,实际上坏的是“流量路径”

🧪 如何验证是不是端口回环

下次再遇到类似问题,可以按这个顺序排查。

在本机看 7897 到底谁在监听

Windows 可以看:

1
netstat -ano | findstr 7897

macOS / Linux 可以看:

1
2
lsof -i :7897
ss -ltnp | grep 7897

重点不是只看“有没有监听”,而是看:

  • 是 Clash 在监听
  • 还是 VS Code helper / SSH client 在监听
  • 还是两边都试图占这个端口

在远端确认转发是否存在

1
ss -ltnp | grep 7897

如果 atlanta 上有 127.0.0.1:7897 的监听,很可能就是 SSH 建出来的 RemoteForward

🚫 常见误区

误区 正解
Mac 先连上,Win 就会“占不到同一个远端端口” 不一定。RemoteForward 作用在具体 SSH 会话上,不能简单理解成一台机器上的全局唯一占坑。
看到节点正常,就说明代理没问题 不对。节点健康只说明客户端到订阅/控制面的状态正常,不代表数据流量没有回环。
curl 没输出一定是目标网站挂了 不对。端口回环、代理黑洞、DNS 被错误接管都可能表现成“卡死没输出”。
VS Code 自动转发一定是好事 只对普通开发服务是好事,对代理口、隧道口、数据库口未必。

✅ 总结

这次问题的本质,不是机场挂了,也不是服务器断网,而是远端代理口既参与了 SSH 的 RemoteForward,又被 VS Code 自动纳入端口转发候选;一旦 VS Code 因恢复旧映射或启动时序异常实际占回本地同号端口,流量路径就会自我缠绕。

如果你也在用 RemoteForward 让远端机器借用本地 Clash,最稳的策略就三条:

  • 禁掉 VS Code 对代理端口的自动转发
  • 让远端端口和本地代理端口不要同号
  • 把不同设备的远端代理端口编号分开

这样下次再遇到“本地看起来在线,远端却完全没网”的诡异故障,第一反应就不该是换节点,而该先检查端口转发链路。