这一切,始于一个异想天开(说白了就是想偷懒)的念头:我手头只有一台 WSL2 Linux,却想要一个 Windows 程序——能不能不折腾 Windows、不装 Visual Studio,直接在 Linux 上把它编译、打包出来?
抱着「试试又不亏」的心态,我把这个偷懒的想法丢给了 Claude Code,让它和我结对开干。没想到一路边写代码、边踩坑、边验证,真把它做成了:把 kcptun 做成 Clash 客户端的内置插件,并在纯 Linux 上交叉编译出了 Windows 的 .exe 安装包。
这篇文章,就是这次「人机搭档」从异想天开到落地的完整复盘——架构设计、代码改造、交叉编译、6 个真实踩坑,一个不少。
一、缘起:我想要什么
我有两个诉求:
- 1. 用 kcptun(基于 KCP/UDP 的隧道)给我的网络代理「加速」——它能在丢包、高延迟的链路上显著改善 TCP 体验。
- 2. 我日常用 Clash Verge Rev(一个基于 Tauri 的 Clash/Mihomo 图形客户端)。我希望把 kcptun 作为「插件」内置进去,最终编译出 Windows 版,开箱即用地实现
kcptun + clash 加速。
难点在第 3 步:Clash Verge Rev 是 Tauri 应用,Windows 包要用 windows-msvc 工具链 + NSIS 打包,按常理得在 Windows 机器或 CI 上构建。而我手头只有一台 Linux——这也正是「偷懒」念头的来源:我实在不想为了出个包再去开 Windows、装一堆环境。
于是我干脆把整个目标——从写代码、踩坑到出包——丢给 Claude Code 一起结对推进:它负责啃文档、写实现、跑验证、定位报错,我负责拍板方向和验收。边做边记录,就有了这篇文章。结论先放这里——这件事在 Linux 上完全可以做到,最终产物:
clash-verge.exe 78 MB PE32+ executable (GUI) x86-64
Clash Verge_2.5.2_x64-setup.exe 53 MB Nullsoft (NSIS) 安装包
kcptun-client-...-windows-msvc.exe 13 MB 随安装包打包的加速 sidecar
二、先搞懂原理:kcptun + clash 是怎么加速的
很多人以为「加速器」很玄,其实 kcptun 的本质很朴素:把一段 TCP 流量塞进 KCP/UDP 隧道转发。KCP 用更激进的重传/纠删策略,在丢包链路上比裸 TCP 更稳。
它分 client 和 server 两端:
- • client:本地监听一个 TCP 端口,把进来的连接经 KCP/UDP 发往远端。
- • server:在远端收 KCP,解出来再转发给「真实目标」(你的真实代理服务端)。
和 Clash 叠加时,正确的流量路径是这样的(我们采用最简单可靠的「单上游」拓扑):
┌─────────────── 本机 (Windows: Clash Verge Rev) ───────────────┐
│ │
│ 应用流量 → mihomo 内核 │
│ (选中的节点 server 指向 127.0.0.1:LPORT) │
│ │ │
│ ▼ │
│ kcptun-client (sidecar) │
│ 监听 127.0.0.1:LPORT │
└────────────────────│───────────────────────────────────────────┘
│ KCP / UDP 隧道(抗丢包)
▼
┌─────────────── 远端 (Linux VPS) ───────────────────────────────┐
│ kcptun-server 监听 :QPORT/udp │
│ │ -t 指向真实代理 │
│ ▼ │
│ 真实代理服务端 (ss / vmess / trojan ...) → 互联网 │
└────────────────────────────────────────────────────────────────┘
三个关键约束(后面代码都围绕它们):
- • kcptun client 要在 mihomo 之前启动,保证本地端口先就绪;
- • 两端的
key / crypt / mode / datashard / parityshard 必须完全一致,否则握手或解密失败; - • 想加速哪个节点,就把那个 Clash 节点的
server 改成 127.0.0.1:LPORT(手动接线,透明且不怕订阅更新覆盖)。
三、第一步:Linux 下的 kcptun C/S 二进制
kcptun 是纯 Go 项目(github.com/xtaci/kcptun不过该作者已经不维护了,我在gitee上fork了一份,见文章末尾),client 和 server 各是一个 main:
# 本机编译
go build -mod=vendor -o client ./client
go build -mod=vendor -o server ./server
别急着说「通了」——要防假阳性
测试隧道连通时,我特意没有只看「curl 返回 200」就收工。第一次测试我用了被占用的端口,结果 curl 命中了本机另一个无关服务,假装「通了」。这是测试里最容易骗自己的坑。
正确做法:用唯一防伪标记。我让 target 只提供一个内容为随机 TOKEN 的文件,经隧道取回后断言内容一致,并用 ss 确认监听端口确实属于我们的进程:
TOKEN="KCPTUN_OK_$$_${RANDOM}"; echo "$TOKEN" > webroot/marker.txt
python3 -m http.server 45001 --bind 127.0.0.1 & # 真实目标
./server -l :45002 -t 127.0.0.1:45001 -key k -crypt aes -mode fast &
./client -l :45003 -r 127.0.0.1:45002 -key k -crypt aes -mode fast &
# 经隧道取回,必须等于 TOKEN 才算通
curl -s --retry 20 --retry-connrefused http://127.0.0.1:45003/marker.txt
只有取回的内容 == TOKEN,且 ss -ltnp 显示 45003 属于 client 进程,才判定 PASS。这一步帮我把一个假阳性当场否决掉了。
四、第二步:把 kcptun 变成 Clash 的「插件」
4.1 关键:Tauri 的 sidecar 机制
Clash Verge Rev 用 Tauri v2。它管理 mihomo 内核的方式,正是我们要复刻的「sidecar」模式:
- • 在
tauri.conf.json 的 bundle.externalBin 里登记一个外部二进制名; - • 打包时 Tauri 会按目标平台三元组自动找文件(如
kcptun-client-x86_64-pc-windows-msvc.exe)并打进安装包; - • 运行时用
app.shell().sidecar("名字").args([...]).spawn() 把它拉起来,拿到子进程句柄,守护其日志,退出时 kill。
所以「把 kcptun 做成插件」= 复刻这套机制,加一个 kcptun-client 的 sidecar + 一个进程管理器 + 前端开关。
4.2 架构:改动落在哪
clash-verge-rev/
├─ scripts/prebuild.mjs ← 新增 task:从 kcptun 源码交叉编译 client sidecar
├─ src-tauri/
│ ├─ tauri.conf.json ← externalBin 增加 "sidecar/kcptun-client"
│ └─ src/
│ ├─ core/kcptun.rs ← 【新】KcptunManager:拉起/守护/kill
│ ├─ cmd/kcptun.rs ← 【新】IPC 命令 restart/stop/get_running
│ ├─ config/verge.rs ← IVerge 增加 kcptun_* 配置字段
│ ├─ utils/resolve/mod.rs ← 启动钩子:在 mihomo 之前起 kcptun
│ └─ feat/window.rs ← 退出清理:随应用一起 kill,避免孤儿进程
└─ src/ (前端)
├─ components/setting/setting-kcptun.tsx ← 【新】设置区(开关+状态)
├─ components/setting/mods/kcptun-viewer.tsx ← 【新】配置对话框
├─ services/cmds.ts / types/global.d.ts ← IPC 封装 + 类型
└─ pages/settings.tsx ← 挂载
4.3 进程管理器(Rust 核心)
完全对齐内核管理器的写法:全局 AppHandle → .shell().sidecar().args().spawn() → 异步消费事件 → 用 ArcSwapOption<CommandChild> 持有 → kill 停止。核心片段:
pub async fn start(&self) -> anyhow::Result<()> {
self.stop().await; // 幂等:先停旧的
let verge = Config::verge().await.data_arc();
if verge.enable_kcptun != Some(true) { return Ok(()); } // 没开就跳过
// 仅监听本地回环,避免对外暴露
let local_addr = format!("127.0.0.1:{}", verge.kcptun_local_port.unwrap_or(12948));
let args = vec![
"-l".into(), local_addr,
"-r".into(), /* 远端地址 */,
"-key".into(), /* 密钥 */,
"-crypt".into(), /* aes */,
"-mode".into(), /* fast */,
];
let app = Handle::app_handle();
let (mut rx, child) = app.shell().sidecar("kcptun-client")?.args(args).spawn()?;
self.child.store(Some(Arc::new(child))); // 持有句柄
AsyncHandler::spawn(move || async move { // 守护日志/退出
while let Some(ev) = rx.recv().await {
match ev {
CommandEvent::Stdout(l) | CommandEvent::Stderr(l) =>
log::info!(target: "app", "[kcptun] {}", String::from_utf8_lossy(&l)),
CommandEvent::Terminated(_) => break,
_ => {}
}
}
});
Ok(())
}
启动顺序很关键——在 resolve 的初始化里,把 init_kcptun() 放在 init_core_manager()(启动 mihomo)之前;退出时在 clean_async() 里加一个停止任务,和系统代理、内核停止一起并行清理。
4.4 让 prebuild「从源码」生成 sidecar
Clash Verge Rev 的 mihomo 是从 GitHub Release 下载的。kcptun 是我们自己的源码,所以更优雅的做法是按目标三元组直接 go build(纯 Go,零 CGO,跨平台无障碍):
async function resolveKcptun() {
const goos = { win32:'windows', darwin:'darwin', linux:'linux' }[platform]
const goarch= { x64:'amd64', arm64:'arm64', /* ... */ }[arch]
const out = `kcptun-client-${SIDECAR_HOST}${platform==='win32' ? '.exe' : ''}`
execSync(`go build -mod=vendor -trimpath -ldflags "-s -w" -o "${out}" ./client`, {
cwd: KCPTUN_SRC, // 默认 ../kcptun,可用 KCPTUN_SRC 覆盖
env: { ...process.env, CGO_ENABLED:'0', GOOS:goos, GOARCH:goarch },
})
}
一个隐蔽的坑:src-tauri/sidecar/ 被 gitignore。我做编译验证时放过假的 mihomo 占位文件,结果发现——真实 prebuild 会因「文件已存在」跳过下载真内核,从而毒化构建。验证完一定要把占位清掉。
五、第三步(高潮):在 Linux 上交叉编译 Windows 程序
这是我最兴奋的部分。先说为什么能行:
交叉编译的本质,是用「能生成目标平台机器码的编译器 + 目标平台的头文件/库」。Windows(MSVC ABI)需要的是:
- •
cargo-xwin:它会自动从微软下载 Windows SDK 与 CRT(头文件 + 导入库),无需真 Windows; - •
clang-cl:clang 以「cl.exe 兼容模式」运行,充当 MSVC 编译器; - •
lld-link:LLVM 的链接器,充当 link.exe;llvm-lib 当 lib.exe,llvm-rc 当 rc.exe; - •
makensis:在 Linux 上就能跑的 NSIS,负责把程序打成安装包。
凑齐这套,Linux 就能产出原生的 Windows .exe。
5.0 先把资源给够:8G / 4 核会让你以为「卡死了」
这是我最该提前知道的一个坑,血泪教训放在最前面。
一开始我给 WSL 只分了 8G 内存、4 核,结果交叉编译跑了将近半小时几乎没有任何输出,我一度以为它卡死了、命令写错了,反复检查也找不到问题。
原因其实不难理解:Tauri 应用的 Rust 依赖树非常庞大(几百个 crate,还有 webview2、reqwest、rustls 这些大块头)。8G 内存极易被吃满,触发 swap 抖动,CPU 大量时间耗在换页上,外在表现就是「一动不动」;4 核又让本就吃力的编译雪上加霜。
后来我把 WSL 加到 48 核 / 39G,同一个工程用 fast-release 7 分钟就编完了——差距是数量级的。
经验值:至少 16G 内存 + 8 核以上,越多越好。WSL 调整资源的方法,是在 Windows 用户目录建/改 C:\Users\<你的用户名>\.wslconfig:
[wsl2]
memory=24GB
processors=16
swap=8GB
保存后在 PowerShell 执行 wsl --shutdown,重新打开 WSL 即生效。
👉 一句忠告:如果编译「半天没动静」,先别怀疑命令,先看 free -h 和 nproc——十有八九是内存不够在疯狂换页,而不是真的卡住。
5.1 一次性装好工具链
# Rust 目标 + 交叉编译器(自动下载 Windows SDK)
rustup target add x86_64-pc-windows-msvc
cargo install --locked cargo-xwin
# clang 系工具链 + NSIS
sudo apt-get install -y clang lld llvm nsis
# cargo-xwin 需要名为 clang-cl 的可执行文件(clang 同一个二进制,换名即切换行为)
sudo ln -sf /usr/bin/clang /usr/local/bin/clang-cl
5.2 准备 sidecar 与前端
pnpm install
# 这一步会下载真实 mihomo,并用上面的 resolveKcptun 把 kcptun client 交叉编译出来
KCPTUN_SRC=/path/to/kcptun pnpm run prebuild x86_64-pc-windows-msvc
5.3 编译 + 打包
KCPTUN_SRC=/path/to/kcptun CARGO_BUILD_JOBS=$(nproc) \
pnpm tauri build --runner cargo-xwin \
--target x86_64-pc-windows-msvc \
-- --profile fast-release
--runner cargo-xwin 让 Tauri 用 cargo-xwin 代替 cargo 来跨平台编译;--profile fast-release 是个加速编译的 profile(下面解释)。产物在:
target/x86_64-pc-windows-msvc/fast-release/clash-verge.exe # 可执行体
target/x86_64-pc-windows-msvc/fast-release/bundle/nsis/*_x64-setup.exe # 安装包
5.4 我踩的三个坑(以及怎么填的)
这条路不是一帆风顺,但每个错误都很「讲道理」:
坑 1:failed to find tool "clang-cl"
cargo-xwin 把某个 C 依赖交给 clang-cl 编译,但系统只有 clang 没有 clang-cl。clang-cl 其实就是 clang 换个名字运行,所以:
sudo ln -sf /usr/bin/clang /usr/local/bin/clang-cl
# clang-cl --version 会显示 Target: x86_64-pc-windows-msvc,说明它已进入 MSVC 模式
坑 2:Plugin not found, cannot call SimpleSC::ExistsService
NSIS 模板用 SimpleSC 插件管理 Windows 服务。把 prebuild 下载好的插件丢进系统 NSIS 插件目录即可:
sudo cp Local/NSIS/SimpleSC.dll /usr/share/nsis/Plugins/x86-unicode/
坑 3:打包成功了,命令却返回非 0
日志里能看到 Finished 1 bundle at: ...setup.exe——安装包已经生成。报错是它之后的一步:自动更新签名缺私钥(TAURI_SIGNING_PRIVATE_KEY)。个人构建忽略即可,或把 createUpdaterArtifacts 关掉。
5.5 关于「多核狂飙」:fast-release vs release
这台机器加到 48 核 / 39G 后,我特意没用默认的 release。原因在 profile:
| Profile | codegen-units | LTO | 多核利用 | 用途 |
|---|
release(默认) | 1 | thin | 主 crate 基本单线程,慢 | 分发(优化好) |
fast-release | 64 | 关闭 | 吃满 48 核,快 | 快速验证 |
codegen-units = 1 意味着最后那个大 crate 几乎不能并行,核再多也用不上。换成 fast-release(codegen-units = 64)后,整包从头编译只用了 7 分钟,后续增量更短。代价是不优化(体积大、运行稍慢),所以:自己验证用 fast-release,对外分发用 release 或 CI。
六、怎么确认「真的成功了」
光有个 .exe 不够,得证明我的 kcptun 代码真的编进去了。两招:
# 1) 确认是 Windows 程序
file clash-verge.exe
# → PE32+ executable (GUI) x86-64, for MS Windows
# 2) 从二进制里捞 kcptun 痕迹
strings clash-verge.exe | grep -iE "kcptun|restart_kcptun|kcptun_remote_addr"
第二招捞到了:IPC 命令 restart_kcptun / stop_kcptun / get_kcptun_running、配置字段 kcptun_remote_addr / kcptun_key / ...、externalBin sidecar/kcptun-client、运行期日志前缀 [kcptun]——集成确确实实进了 Windows 二进制,不是嘴上说说。
此外每一层都跑了真实验证:cargo check 通过、前端 tsc --noEmit 通过、ESLint/cargo fmt 通过、隧道端到端用防伪 TOKEN 通过、部署脚本 DRY_RUN 通过。
七、从零复现(完整清单)
# ── 前提:kcptun 与 clash-verge-rev 两个仓库相邻放置 ──
# 0) 工具链(一次性)
rustup target add x86_64-pc-windows-msvc
cargo install --locked cargo-xwin
sudo apt-get install -y clang lld llvm nsis
sudo ln -sf /usr/bin/clang /usr/local/bin/clang-cl
# 1) 验证 kcptun 隧道(可选,本机)
cd kcptun && go build -mod=vendor -o client ./client && go build -mod=vendor -o server ./server
# 2) 进入 clash-verge-rev,装依赖 + 准备 sidecar(含交叉编译 kcptun)
cd ../clash-verge-rev && pnpm install
KCPTUN_SRC=$PWD/../kcptun pnpm run prebuild x86_64-pc-windows-msvc
# 3) NSIS 服务插件(prebuild 已下载,放到系统目录)
sudo cp Local/NSIS/SimpleSC.dll /usr/share/nsis/Plugins/x86-unicode/
# 4) 交叉编译 + 打包 Windows 安装包
KCPTUN_SRC=$PWD/../kcptun CARGO_BUILD_JOBS=$(nproc) \
pnpm tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc -- --profile fast-release
# 5) 取产物
ls target/x86_64-pc-windows-msvc/fast-release/bundle/nsis/*setup.exe
想要正式分发包?把 --profile fast-release 去掉(用默认 release),并配好 TAURI_SIGNING_PRIVATE_KEY,或干脆交给 GitHub Actions 的 windows-latest 原生构建。
八、踩坑清单(快速回顾)
| 现象 | 根因 | 解法 |
|---|
| 编译近半小时无动静、像卡死 | WSL 只分了 8G/4 核,内存耗尽疯狂 swap | .wslconfig 给到 16G/8 核以上,wsl --shutdown 生效 |
| 测试「通了」其实是假的 | 端口被无关服务占用 | 用唯一 TOKEN + ss 验证进程归属 |
| prebuild 跳过下载真内核 | sidecar 占位文件污染 | 验证后清掉占位 |
failed to find tool "clang-cl" | 只有 clang | ln -sf clang clang-cl |
SimpleSC::ExistsService 插件缺失 | NSIS 插件不在搜索路径 | 拷 SimpleSC.dll 到 Plugins/x86-unicode/ |
| 打包后返回非 0 | updater 签名缺私钥 | 个人构建忽略/关 createUpdaterArtifacts |
| 48 核用不满 | release 的 codegen-units=1 | 验证期改用 fast-release |
九、原理小结:为什么 Linux 能造 Windows 包
把上面串起来,一句话:编译的本质是「翻译」,而翻译不需要你站在目标国家。
- • 机器码:
clang-cl/rustc 直接生成 x86_64 的 Windows(MSVC ABI)机器码; - • 头文件/库:
cargo-xwin 替你下载并摆好微软的 SDK 与 CRT; - • 链接:
lld-link 按 Windows PE 格式把目标文件链成 .exe; - • 资源/安装包:
llvm-rc 处理图标等资源,makensis 打出 NSIS 安装器。
整条链路里没有任何一步必须在 Windows 上完成。这也是为什么——一台 Linux,就能产出能在 Windows 上双击安装运行的程序。
十、结语
这次尝试最打动我的,不是「省了一台 Windows」,而是它把「交叉编译」这件看似高深的事,拆成了一组可理解、可复现、能讲清原理的步骤。希望这篇记录,能让更多朋友也体会到这种「原来还能这样」的快乐。
更有意思的是这篇文章本身的来历:它起于一个「不想折腾 Windows」的偷懒念头,靠着和 Claude Code 一来一回地试错、验证、复盘,最后连这篇教程都是顺手记下来的。有时候,一个偷懒的异想天开,配上一个肯陪你死磕到底的搭档,反而能把你带到原本没想过的地方。 如果你也有过类似「这个能不能更省事一点」的念头,别急着否定它——动手试试,说不定下一篇实战记录就是你写的。
如果你也想试,建议从最小闭环开始:先把 kcptun 的隧道在本机用防伪 TOKEN 跑通,再一层层往上叠。每通过一层都用真实命令验证一次——别让自己被假阳性骗了,这是我今晚最大的体会之一。
相关仓库:
- • kcptun:https://gitee.com/smithwhere/kcptun.git
- • Clash Verge Rev:https://github.com/clash-verge-rev/clash-verge-rev.git
- • 交叉编译工具 cargo-xwin:https://github.com/rust-cross/cargo-xwin
本文所有命令均在 Ubuntu 24.04 / WSL2 上实测通过。
想要文章中编译好的二进制可以进群艾特群主获取