VSCode 远程开发链路的完整优化过程

2025年11月13日 | Ruichen Zhou

VSCode 远程开发链路的完整优化过程

这篇文章记录的是我为了让 VSCode Remote 在跨国环境下尽量接近本地体验,对网络链路做的一轮“事无巨细”的调整。

场景很简单:

  • 一台工作站在中国,作为主要开发环境;
  • 我在德国上学,需要在笔记本上长期远程开发;
  • 远程开发主要依赖 VSCode Remote + SSH。

中间经历了几个阶段:

  1. 直接用 Tailscale,结果长期走 DERP 中继,延迟在 250ms 左右且不稳定;
  2. 临时用宿舍 Windows 电脑做 SOCKS5 代理,强制把流量走一条稳定的「C → 宿舍 → 中国」路径;
  3. 把代理迁移到 24/7 在线的 OpenWrt 路由器上,用 tailscaled 自带 SOCKS5;
  4. 最后通过 ~/.ssh/config 把 VSCode/SSH 的体验调到比较舒适的状态。

文章里会尽量交代清楚:每一步为什么做、做完之后具体有什么变化。


一、设备命名与网络拓扑说明

一、设备命名与网络拓扑说明

为了和前几篇文章保持一致,这里统一一下设备命名(后续文章也会沿用这一套):

  • 设备 A:Linux 工作站(中国大陆)

    • Linuxmint 系统,主要开发环境;
    • 安装了 Tailscale;
    • 对外只开放一个自定义 SSH 端口,例如 55905
  • 设备 B:OpenWrt 路由器(德国学生宿舍)

    • 红米 AX6,刷了 AE86Wrt(定制 OpenWrt 系统);
    • 24/7 在线;
    • 已接入 Tailscale 网络;
    • 作为后面”最终版”代理的承载设备。
  • 设备 C:MacBook Air(德国)

    • 便携开发设备,在宿舍和工位之间通勤使用;
    • VSCode Remote + SSH 的客户端;
    • 同时也是调试各种链路的主力设备。
  • 设备 D:Windows 电脑(德国学生宿舍)

    • 长期位于德国学生宿舍;
    • 与设备 B 在同一个局域网;
    • 中间阶段曾作为临时代理机使用。

Tailscale 网络中,各设备有各自的虚拟 IP,例如:

  • 设备 A:100.100.1.1
  • 设备 B:100.100.1.4
  • 设备 D:100.100.1.6

实际 IP 不重要,重点是:所有设备已经在同一个 Tailscale 网络里,可以互相 ping 通。

后文为了便于阅读,会使用自然称呼(如”工作站”、“路由器”、“MacBook”、“Windows电脑”)而不是生硬的代号。


二、V1.0:只依赖 Tailscale 自己的连通性(DERP 模式)

最开始,我的思路很朴素:既然所有设备都装了 Tailscale,那我直接从 MacBook用 Tailscale 去连工作站就好了。

在 MacBook 上测试:

# 在 MacBook 上执行
tailscale ping 100.100.1.1

输出类似:

pong from workstation (100.100.1.1) via DERP(hk1) in 248ms
pong from workstation (100.100.1.1) via DERP(hk1) in 247ms
...
direct connection not established

关键信息有两点:

  • via DERP(hk1):说明当前流量是通过 Tailscale 在香港的 DERP 中继节点转发的,并没有打通点对点;
  • ~250ms 延迟:对于 SSH 只敲命令而言还勉强能用,但对于 VSCode Remote 这种大量小交互、频繁文件同步的场景,体验非常差。

实际体验:

  • VSCode Remote 经常在连接/初始化阶段卡住;
  • 操作稍微密集一点,就会出现卡顿、断连;
  • 即使用了 ServerAliveInterval 这种保活配置,也只是“不断线但很卡”。

这时我意识到,仅仅“能连上”不足以支撑日常开发:链路质量(延迟、抖动)同样关键


三、V2.0:用宿舍 Windows 电脑做临时 SOCKS5 代理

在梳理网络拓扑时,我发现了一个有用的事实:

  • MacBook ↔ Windows 电脑:Tailscale 能打通直连,延迟很低;
  • Windows 电脑 ↔ 工作站:Tailscale 也能打通直连,延迟相对稳定。

换句话说,MacBook ↔ Windows 电脑 ↔ 工作站 这条路径,比直接 MacBook ↔ 工作站 的 DERP 中继要靠谱得多。

只要我把 SSH 流量”强制”走 MacBook → Windows 电脑 → 工作站 这条路径,就能绕开 DERP,走两段 P2P:

  1. MacBook → Windows 电脑:德国境内;
  2. Windows 电脑 → 工作站:德国宿舍 → 中国(跨国,但相对稳定)。

这就需要在 Windows 电脑上提供一个 SOCKS5 代理端口,然后在 MacBook 的 SSH 配置里使用 ProxyCommand

3.1 在 Windows 上寻找可用的 SOCKS5 服务

设备 D 是 Windows 电脑,已经安装了几种代理相关工具,但实际可用的不多。

几个尝试过程简单列一下:

  1. Tailscale 内置 SOCKS5(失败)

    Tailscale 在 Linux/macOS 上支持 tailscale set --sockshost,可以直接开启一个 SOCKS5 代理。但在 Windows PowerShell 下,这个命令不可用,运行后只会提示参数不支持。 结论:Windows 版本 Tailscale 暂时不支持这个功能

  2. Clash Verge(不适合当前场景)

    宿舍电脑上安装了 Clash Verge,理论上也能对外提供代理。但它是“订阅优先”的设计,界面偏向于“作为客户端用远端节点上网”,而不是“在本地开一个通用 SOCKS5 给别人用”。 实际尝试时,它并没有按预期在本机监听一个稳定端口,对我这个单纯想要“开一个裸 SOCKS5 转发 Tailscale 流量”的场景不太友好。

  3. v2rayN(最终选择)

    最后我换用了 v2rayN,对它的期待非常简单:在本地开启一个 SOCKS5 端口,把所有流量转发到 Tailscale 虚拟网卡上。 配置思路是:

    • 创建一个最小化的 socks5 出站;
    • 入站则直接监听本地端口(比如 10800);
    • 不使用任何订阅,只用本地转发。

    启动后,注意了一点:系统里可能已经有其它 xray.exe 进程占住了默认端口(1080),需要通过任务管理器或 netstat 检查,清理冲突进程,最后确定一个干净的端口,例如 10800

3.2 在 C 上通过 SOCKS5 间接 SSH 到 A

假设:

  • 设备 D 在 Tailscale 上的 IP:100.100.1.6
  • v2rayN 在 D 上监听的端口:10800

在 C 上可以用 nc 先测一下代理端口是否可达:

nc -vz 100.100.1.6 10800
# Connection to 100.100.1.6 port 10800 succeeded!

然后用 ProxyCommand 做一次 SSH 尝试:

ssh -p 55905 \
  -o ProxyCommand="nc -X 5 -x 100.100.1.6:10800 %h %p" \
  ruichen@100.100.1.1

这里:

  • 100.100.1.1 是设备 A 的 Tailscale IP;
  • 55905 是 A 上 SSH 的监听端口;
  • nc -X 5 -x 100.100.1.6:10800 %h %p 表示:通过 SOCKS5 代理 100.100.1.6:10800 去访问目标 %h:%p

V2.0 的效果:

  • 延迟明显比直接走 DERP 要舒服得多,VSCode Remote 的卡顿情况有所缓解;
  • 但有一个致命缺点:必须保证 D 这台 Windows 电脑始终开机,并且 v2rayN 在后台运行。这在宿舍日常环境里并不现实。

因此,这一版只能算临时解决方案。


四、V3.0:用 OpenWrt 路由器提供 24/7 的 Tailscale SOCKS5

真正”常驻在线”的是 OpenWrt 路由器——宿舍的红米 AX6。它一直通电、功耗低、不用担心有人把它关机。

于是,把代理服务从 Windows 电脑挪到路由器上,就很自然了:

MacBook 始终通过 路由器的 SOCKS5 去访问工作站。

4.1 安装 Tailscale:绕过坏掉的 opkg 源

路由器使用的是基于 OpenWrt 的 AE86Wrt 固件。理想状态下,我只需要:

opkg update
opkg install tailscale tailscaled

但现实是:

  • opkg update 报了大量 404 / 签名错误,源已经不再维护;
  • 内置的应用商店(如 iStore)也无法使用;
  • 从 OpenWrt 官方去下载对应架构的 .ipk,也频繁遇到 404。

在确认这是“固件本身的源配置已经老旧”的问题后,我选择了另一条路:直接用 Tailscale 官方提供的静态编译二进制

大致步骤:

# 1. 下载 tailscale 的 arm64/arm 静态包(根据路由器 CPU 架构选择)
cd /tmp
wget https://pkgs.tailscale.com/stable/tailscale_..._mipsle.tgz
tar xzf tailscale_..._mipsle.tgz

# 2. 把二进制挪到 PATH 下
mv tailscale tailscaled /usr/bin/
chmod +x /usr/bin/tailscale /usr/bin/tailscaled

然后在路由器上登录 Tailscale,加入已有网络(可以用 tailscale up 或事先生成 auth key)。

4.2 利用 tailscaled 自带 SOCKS5 功能

很多教程会建议在路由器上额外安装 microsocks 之类的软件,但 tailscaled 本身就支持开启 SOCKS5:

tailscaled \
  --state=/var/lib/tailscale/tailscaled.state \
  --socks5-server=0.0.0.0:1080

这行命令会:

  • 启动 tailscaled 并读取/保存状态到指定路径;
  • 在所有网口上监听一个 SOCKS5 端口 1080

只要 B 加入了 Tailscale 网络,C 就能访问 B 的 100.100.1.4:1080(前提是防火墙允许)。

4.3 用 rc.local 做一个”够用”的开机自启

在理想世界里,我们应该为 tailscaled 写一个完整的 /etc/init.d/tailscaled 脚本。但在这台定制固件路由器上,init 脚本调试成本不低。最后我选了一个更简单的方案:直接用 /etc/rc.local

示意:

# /etc/rc.local 中,exit 0 之前加入一行
nohup /usr/bin/tailscaled \
  --state=/var/lib/tailscale/tailscaled.state \
  --socks5-server=0.0.0.0:1080 \
  >/var/log/tailscaled.log 2>&1 &

exit 0

这样每次路由器重启时:

  • tailscaled 会自动拉起;
  • SOCKS5 端口 1080 会自动监听;
  • 日志写到 /var/log/tailscaled.log,方便排查。

最终,路由器成为一个24/7 在线的 Tailscale SOCKS5 代理,并在 Tailscale 管理面板中将路由器的 IP 固定为 100.100.1.4,方便在 SSH 配置里引用。


五、V4.0:通过 SSH 配置把”复杂链路”封装到一个 Host 名里

到这一步,底层链路已经稳定了:

MacBook →(Tailscale)→ 路由器(OpenWrt)→(Tailscale)→ 工作站

接下来要解决的是:把复杂度藏起来,让 VSCode/SSH 只感知到一个稳定的”workstation”

需求有三个:

  1. 我不想每次都手写 ProxyCommand=...
  2. 无论在办公室还是宿舍,VSCode 和 Copilot 看见的都是同一个 Host workstation,以便共享历史、缓存和上下文;
  3. 尽量利用 SSH 的连接复用和保活机制,减少 VSCode 频繁重连的开销。

5.1 最终版 ~/.ssh/config

在 MacBook 上,我最后整理出这样一份配置:

# ================================================================
#  工作站(中国)- Linux 主机
# ================================================================
Host workstation
  HostName 100.100.1.1      # 工作站的 Tailscale IP
  User ruichen
  Port 55905                # 工作站上的 SSH 自定义端口

  # 核心:所有连接都通过路由器(OpenWrt)的 SOCKS5
  ProxyCommand nc -X 5 -x 100.100.1.4:1080 %h %p

### 5.1 最终版 `~/.ssh/config`

在 C 上,我最后整理出这样一份配置:

```sshconfig
# ================================================================
#  A(中国)- 4090 工作站
# ================================================================
Host workstation
  HostName 100.100.1.1      # 设备 A 的 Tailscale IP
  User ruichen
  Port 55905                # A 上的 SSH 自定义端口

  # 核心:所有连接都通过 B(OpenWrt 路由器)的 SOCKS5
  ProxyCommand nc -X 5 -x 100.100.1.4:1080 %h %p


# ================================================================
#  全局设置,作用于所有 Host(包括 workstation)
# ================================================================
Host *
  # 身份认证
  IdentityFile ~/.ssh/id_ed25519

  # --- 1. 保活设置:尽量“不断线” ---
  ServerAliveInterval 30        # 每 30 秒发一个心跳包
  ServerAliveCountMax 999999    # 理论上允许非常多次失败
  TCPKeepAlive yes

  # --- 2. 性能:连接复用 ---
  Compression yes               # 适度压缩,小带宽环境下有帮助

  # 启用 SSH 的连接复用(ControlMaster 模式)
  ControlMaster auto
  ControlPersist yes            # master 连接在后台长期保持
  ControlPath ~/.ssh/cm-%r@%h:%p

这份配置的效果是:

  • 日常使用时只需要敲 ssh workstation
  • VSCode Remote 里也只声明一个 Host:workstation
  • 所有连接都统一走 C → B:1080 → A:55905,不会因为在宿舍/办公室而改变。

5.2 连接复用带来的体验差异

启用 ControlMaster / ControlPersist 后,SSH 的行为会发生一个很“微妙但重要”的变化:

  1. 第一次连接时,SSH 会建立一个“master”连接,并显示完整的 MOTD 等信息:

    ssh workstation
    # 显示欢迎信息、GPU 状态、登录提示等
  2. 再次连接时,只要 master 还在,新的连接会直接复用已有通道,不再重新认证,也不会重新打印 MOTD:

    ssh workstation
    # 几乎瞬间进入 shell,只有一行类似:
    # Last login: Fri Nov 14 03:29:00 2025 from 100.100.1.6

对于 VSCode 来说,这种“第二次及之后”的连接行为非常友好:

  • 建立连接更快;
  • 多个终端/扩展可以共享同一条底层 SSH 通道;
  • Copilot / 终端历史等,也都建立在同一个 workstation 身份之上。

六、常见问题与排查思路

这里单独列一些过程中的坑,便于以后查阅。

6.1 仍然走 DERP,而不是走 B 的 SOCKS5

现象:

  • tailscale ping 显示仍然 via DERP(hk1)
  • ssh workstation 看起来“勉强能用”。

可能原因:

  • SSH 没走 SOCKS5,而是直接从 C → A 的 Tailscale 连接;
  • ~/.ssh/config 中的 ProxyCommand 被其它配置覆盖、或书写错误。

排查:

  • 临时加上 -vvv 参数,看 SSH 的调试输出,确认是否执行了 ProxyCommand
  • ss -tnp 查看 C 本机是否建立了到 100.100.1.4:1080 的连接。

6.2 路由器上的 tailscaled 没有正确启动

现象:

  • C 无法连接 100.100.1.4:1080
  • Tailscale 控制台里看不到 B 在线。

排查:

  • 在 B 上查看日志文件(例如 /var/log/tailscaled.log);
  • 使用 ps | grep tailscaled 确认进程是否存活;
  • 手动执行一次启动命令,排除参数问题后再写入 rc.local

6.3 代理端口被防火墙拦住

在部分路由器固件中,LAN→Tailscale 或本机端口的访问会受到防火墙规则限制。

排查:

  • 使用 netstat -lnp | grep 1080 确认 tailscaled 在监听;
  • 确认防火墙中允许来自 LAN/Tailscale 网段访问本机 1080 端口;
  • 临时放开相关规则测试,如果可行再收紧到合理范围。

七、总结与反思:把复杂度留给网络,把接口留给自己

这次从 V1.0 到 V4.0 的调整,回头看下来,技术难度其实不算高,更像是一系列“把事情想清楚”的过程:

  1. 先承认物理现实:跨国延迟无法消灭

    250ms 的 RTT 是客观存在的,即便链路优化到极致,也不会变成 10ms。 真正能优化的是:减少 DERP 中继、减少额外跳数、减少重复握手

  2. 识别出网络中的“稳定节点”,让它承担关键角色

    在我的场景里,宿舍的 OpenWrt 路由器(B)就是那个几乎永远在线的节点。 把 SOCKS5 放在 B 上,而不是时开时关的 Windows 电脑(D),能显著降低“链路突然消失”的概率。

  3. 尽量用现有工具的原生能力,而不是叠加新组件

    tailscaled 自带 SOCKS5,比在上面再套一层 microsocks/v2ray 更简单、可控。 同理,SSH 自带的 ProxyCommandControlMaster,足以处理大部分 VSCode 场景。

这篇文章更多是对这条链路的“验收报告”。后续如果再增加新的节点(比如家里的 NAS、实验用 GPU 服务器),我会沿用同样的思路:先把网络拓扑画清楚,再决定每台设备承担什么职责,而不是一上来就堆命令。