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 Verge、curl、npm dev、sshd - 端口(port):操作系统上的一个编号,比如
7897,可以理解成网络入口的门牌号 - 套接字(socket):程序真正拿来监听(listen)或通信的对象,可以粗略理解成“某个程序占住了某个 IP:端口”之后形成的那个网络端点
放到这次问题里看,会更容易理解:
- 不是“端口自己在工作”,而是某个程序(program / process)创建了套接字(socket)并监听(listen)这个端口
- 更准确地说,通常是同一个
IP:端口组合的监听套接字,只能被一个程序成功持有,比如127.0.0.1:7897正常情况下只能有一个监听者 - 所以看到
7897在监听,只能说明“有程序占了这个门牌号”,不能直接说明是谁在处理流量 lsof、ss、netstat这类命令的意义,就是帮你把“端口”反查回“哪个程序持有了对应套接字”
这也是为什么排障时不能只盯着“端口号是不是一样”,而要盯着:
- 当前到底是谁持有了这个
IP:端口的监听套接字 - 是不是本来该由
Clash持有,后来却变成了VS Code或sshhelper
一旦监听者变了,流量路径就会跟着变。
这也是为什么这次故障排查不能只看“7897 通不通”,还得看:
- 是
Clash持有本地7897 - 还是
VS Code/sshhelper 持有本地7897 - 或者两边是否在不同阶段都试图接管这个端口
从这个角度说,RemoteForward 和 VS Code 自动端口转发,本质上都是在安排“由哪个程序持有哪个套接字,并把流量交给谁继续处理”。
🧩 网络拓扑到底长什么样
我使用的设备如下:
- macbook pro 2020
- win xiaoxin 14pro 2021
- linux 实验室的高性能服务器 atlanta
当时的 SSH 配置如下:
1 | Host Atlanta |
- 从 Win 或 Mac SSH 到
atlanta - SSH 连接建立后,会让
atlanta打开一个本地监听端口7897 - 任何打到
atlanta:7897的流量,都会沿着 SSH 隧道回到当前客户端的127.0.0.1:7897 - 而客户端本机的
127.0.0.1:7897,正好是 Clash Verge 的代理入口
也就是说,atlanta 并没有真正部署代理,只是“借用了当前登录机器上的本地代理”。
可以看这张链路拓扑图:
1 | +------------------------+ SSH 连接 / 隧道 +------------------------+ |
🔌 端口为什么会“撞车”
端口不是全局唯一的,它只在一台机器的网络命名空间里唯一。
这意味着:
- 你的 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 |
两层叠起来,就容易出现环路或自咬尾巴。
🔄 这次故障最可能的形成过程
按当时的现象,最合理的解释是:
- 我在 Win 上通过 VS Code Remote SSH 连到
atlanta - SSH 按配置建立了
RemoteForward,使atlanta:7897 -> Win本地:7897 - VS Code 又检测到远端
7897正在监听,于是自动做了端口转发 - 按 VS Code 的正常行为,如果 Win 本地
7897已经被 Clash 占着,它本来应该改绑到别的本地端口,而不是继续占7897 - 因此,真正危险的情况更像是:VS Code 因为恢复了旧的端口映射,或者因为启动时序问题,实际又拿到了 Win 本地
7897 - 一旦这件事发生,“Win 本地
7897”就不再只是 Clash 的入口,还会被 VS Code 的转发链路卷进去 - 最终结果就是:本地代理流量绕回 SSH,再绕回远端,再被转回来,形成回环或错误接管
把这个过程单独画成“故障回环顺序图”,会更直观:
1 | ① SSH 先建立 RemoteForward |
当时观察到的两个现象正好支持这个推断:
- 只要断开 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 | atlanta 上的程序 |
这条连接一旦建立,请求和响应都会在同一条被转发的 TCP 连接里双向流动。也就是说:
atlanta发给atlanta:7897的请求,会被送到当前客户端的7897- 当前客户端上的代理再去访问外网
- 外网返回的数据,会沿着同一条连接再返回给
atlanta
但这不等于“当前客户端也能靠这条 RemoteForward 主动去访问 atlanta:7897”。RemoteForward 解决的是“让远端借用本地服务”,不是让两端在这个端口上获得对等的主动访问能力。
因此,只要在 atlanta 上配置:
1 | export http_proxy='http://127.0.0.1:7897' |
流量其实并不是从 atlanta 直接出国,而是:
1 | atlanta curl |
VS Code 自动端口转发
VS Code Remote SSH 会做另一件事:
- 发现远端某个端口在监听
- 自动把它映射到本机
- 让你本机直接访问远端服务
这个功能本来是给 3000、8080、8888 这类开发服务准备的,比如远端起了 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 | lsof -i :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 对代理端口的自动转发
- 让远端端口和本地代理端口不要同号
- 把不同设备的远端代理端口编号分开
这样下次再遇到“本地看起来在线,远端却完全没网”的诡异故障,第一反应就不该是换节点,而该先检查端口转发链路。