共计 10138 个字符,预计需要花费 26 分钟才能阅读完成。
文章目录
这一切, 始于一个 异想天开 (说白了就是想偷懒) 的念头: 我手头只有一台 WSL2 Linux, 却想要一个 Windows 程序——能不能 不折腾 Windows、不装 Visual Studio, 直接在 Linux 上把它编译、打包出来?
抱着「试试又不亏」的心态, 我把这个偷懒的想法丢给了 Claude Code, 让它和我结对开干。没想到一路边写代码、边踩坑、边验证, 真把它做成了: 把 kcptun 做成 Clash 客户端的内置插件, 并在纯 Linux 上交叉编译出了 Windows 的
.exe安装包。这篇文章, 就是这次「人机搭档」从异想天开到落地的完整复盘——架构设计、代码改造、交叉编译、6 个真实踩坑, 一个不少。
一、缘起:我想要什么
我有两个诉求:
- 用 kcptun(基于 KCP/UDP 的隧道)给我的网络代理「加速」——它能在丢包、高延迟的链路上显著改善 TCP 体验。
- 我日常用 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 持有 → 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 上实测通过。