VS Code 远程开发链路优化:Tailscale SOCKS5 代理 + SSH 连接复用

2025年11月13日| Ruichen Zhou| 约 20 分钟阅读

历史路径说明:本文记录 2025 年底 VSCode Remote 链路的演进过程(V1→V4),文中 SSH config 为当时的终版,现已被统一方案取代,见多设备命名统一SSH 密钥网格

这篇记录我为了让 VSCode Remote 在跨国环境下可用,对网络链路做的几轮调整。

场景:

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

中间经历了几个阶段:

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

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

网络环境和设备清单与桥接 AP 那篇文章一致,这里不再重复;本文只补充和远程开发链路直接相关的角色与命名。

主要设备

  • Linux 工作站(位于中国大陆)

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

    • 红米 AX6(OpenWrt)
    • 24/7 在线
    • 已接入 Tailscale 网络
    • 作为本文最终版代理的承载设备
    • Tailscale IP: 100.100.1.4
  • MacBook(便携设备,德国)

    • MacBook Air,在宿舍和工位之间通勤使用
    • VSCode Remote + SSH 的客户端
    • 调试各种链路的主力设备
  • Windows 电脑(位于德国学生宿舍)

    • 长期位于德国学生宿舍
    • 与 OpenWrt 路由器在同一个局域网
    • 中间阶段曾作为临时代理机使用
    • Tailscale IP: 100.100.1.6

说明:所有设备已经在同一个 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 经常在连接/初始化阶段卡住;
  • 操作稍微密集一点,就会出现卡顿、断连;
  • 事后看,即使加保活配置也只能做到不断线,解决不了卡。

这时我意识到,光能连上撑不起日常开发:延迟和抖动同样要紧。


三、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 服务

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

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

  1. Tailscale 自带 SOCKS5(当时 Windows 上不可用)

    tailscaled 本身支持 --socks5-server 参数,可以直接开一个 SOCKS5 代理。但当时 Windows 版客户端无法通过 tailscale CLI 启用这个功能,只能放弃。

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

    Clash Verge 的设计偏向作为客户端用远端节点上网,尝试后没有按预期在本机监听固定端口,排除。

  3. v2rayN(最终选择)

    最后换用了 v2rayN,对它的期待很简单:在本地监听一个 SOCKS5 端口,收到流量后原样直连出去。 配置思路是:

    • 入站:一个 SOCKS 入站,监听本地端口(比如 10800);
    • 出站:一个 freedom 直连出站,不走任何远端节点;
    • 目标是 Tailnet 内地址时,由 Windows 的路由把包交给 Tailscale 虚拟网卡发出。

    不使用任何订阅,只做本地转发。

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

3.2 在 MacBook 上通过 SOCKS5 间接 SSH 到工作站

假设:

  • Windows 电脑在 Tailscale 上的 IP:100.100.1.6
  • v2rayN 在 Windows 电脑上监听的端口:10800

在 MacBook 上可以用 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 是 Linux 工作站的 Tailscale IP
  • 55905 是工作站上 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 的卡顿情况有所缓解
  • 但有一个致命缺点:必须保证 Windows 电脑始终开机,并且 v2rayN 在后台运行。这在宿舍日常环境里并不现实

因此,Windows 电脑一旦关机,代理就随之失效,它只能作为过渡方案。


四、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_<>_<>.tgz
tar xzf tailscale_<>_<>.tgz

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

然后在路由器上登录 Tailscale,加入已有网络(可以用 tailscale up 或事先生成 auth key)。自建控制服务器的用户、密钥和客户端登录方法见Headscale 教程

4.2 利用 tailscaled 自带 SOCKS5 功能

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

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

这行命令会:

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

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

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

标准做法是为 tailscaled 写 /etc/init.d/ 脚本,但这台定制固件上调试成本不低,我选了更简单的 /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 代理。路由器在 tailnet 中的 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
  # 工作站的 Tailscale IP
  HostName 100.100.1.1
  User ruichen
  # 工作站上的 SSH 自定义端口
  Port 55905

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


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

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

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

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

这份配置的效果是:

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

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

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

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

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

对于 VSCode 来说,这种第二次及之后的连接行为好处明显:

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

六、常见问题与排查思路

这里单独列一些过程中遇到的问题,便于以后查阅。

6.1 怀疑流量没走代理,仍直连工作站

现象: VSCode 仍然卡顿,怀疑 SSH 流量没走路由器的 SOCKS5。

注意:不能用 tailscale ping 判断:它测的是 Tailscale 的直连路径,代理生效时它照样显示 via DERP,两者无关。

可能原因:

  • SSH 没走 SOCKS5,而是 MacBook 直连工作站;
  • ~/.ssh/config 中的 ProxyCommand 被其它配置覆盖、或书写错误。

排查:

  • 临时加上 -vvv 参数,看 SSH 的调试输出,确认是否执行了 ProxyCommand
  • 在 MacBook 上用 lsof -nP -iTCP:1080netstat -an | grep 1080 查看本机是否建立了到 100.100.1.4:1080 的连接。

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

现象:

  • MacBook 无法连接 100.100.1.4:1080
  • Tailscale 控制台里看不到路由器在线。

排查:

  • 在路由器上查看日志文件(例如 /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,最终生效的配置就两处:路由器上一条 tailscaled --socks5-server=0.0.0.0:1080(写进 rc.local 自启),以及 MacBook 上 ~/.ssh/configHost workstationProxyCommand 加全局的保活与 ControlMaster 复用。链路固定为 MacBook → 路由器 → 工作站,日常只需 ssh workstation

后续再加节点(家里的 NAS、GPU 服务器),思路一样:先把拓扑画清楚,再决定每台设备干什么,而不是上来就堆命令。

评论