本文最后更新于 2026年4月8日 上午
Windows 强制为任意 EXE 设置代理的通用方案
背景与适用场景
某些程序(尤其是 Node.js 打包的 CLI 工具)不走系统代理,也不读 VSCode 等宿主程序的代理设置,导致在内网环境中无法联网。
常见场景:
- VSCode 插件内置的 CLI 工具(如 kilocode、claude code等)
- Node.js 打包的命令行工具
- 任何启动时需要特定环境变量但自身不提供配置项的 EXE
解决思路:用一个 Go 编译的轻量 EXE 作为「代理启动器」,在启动真实程序前注入代理环境变量,并处理好有无控制台窗口的问题。
核心问题与解法
为什么不用 bat/cmd 包装?
bat 转成的 exe 本质上还是启动一个 cmd.exe 子进程,存在以下问题:
argv[0] 不正确,部分程序会用它来推断自己的安装路径,导致报错
- 无法控制子进程的控制台窗口创建行为,必然弹出黑框
为什么用 Go?
- 编译产物是真正的原生 EXE,行为与正常程序无异
- 可以精确控制子进程的启动标志(
CREATE_NO_WINDOW)
- 可以通过
AttachConsole 判断当前是否处于交互式终端环境
- 无运行时依赖,单文件分发
通用模板代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| package main
import ( "os" "os/exec" "path/filepath" "syscall" )
var ( kernel32 = syscall.NewLazyDLL("kernel32.dll") attachConsole = kernel32.NewProc("AttachConsole") freeConsole = kernel32.NewProc("FreeConsole") )
const ( ATTACH_PARENT_PROCESS = ^uintptr(0) CREATE_NO_WINDOW = 0x08000000 )
func main() { freeConsole.Call() r, _, _ := attachConsole.Call(ATTACH_PARENT_PROCESS) isCLI := r != 0
exePath, _ := os.Executable() dir := filepath.Dir(exePath) real := filepath.Join(dir, "target_real.exe")
env := os.Environ() env = append(env, "HTTP_PROXY=http://192.168.1.1:10808") env = append(env, "HTTPS_PROXY=http://192.168.1.1:10808") env = append(env, "http_proxy=http://192.168.1.1:10808") env = append(env, "https_proxy=http://192.168.1.1:10808") env = append(env, "NODE_TLS_REJECT_UNAUTHORIZED=0")
cmd := exec.Command(real, os.Args[1:]...) cmd.Env = env
if isCLI { conin, _ := os.OpenFile("CONIN$", os.O_RDWR, 0) conout, _ := os.OpenFile("CONOUT$", os.O_RDWR, 0) cmd.Stdin = conin cmd.Stdout = conout cmd.Stderr = conout cmd.SysProcAttr = &syscall.SysProcAttr{} defer conin.Close() defer conout.Close() } else { cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.SysProcAttr = &syscall.SysProcAttr{ CreationFlags: CREATE_NO_WINDOW, } }
if err := cmd.Run(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { os.Exit(exitErr.ExitCode()) } os.Exit(1) } }
|
使用步骤
1. 准备文件
将上面的代码保存为 wrapper.go,修改两处标注了「修改这里」的地方:
target_real.exe:改为真实程序的文件名
- 代理地址:改为你的实际代理地址
2. 编译
1
| go build -ldflags="-H=windowsgui" -o target.exe wrapper.go
|
-H=windowsgui 是关键,让包装器本身不创建控制台窗口。
3. 部署(以同目录替换为例)
1 2 3 4 5
| rename "C:\path\to\target.exe" target_real.exe
copy "wrapper_build\target.exe" "C:\path\to\target.exe"
|
4. 验证
实际案例:kilocode VSCode 插件
kilocode 插件会在 VSCode 内部调用其内置的 kilo.exe CLI,该程序基于 Node.js 打包,不读取 VSCode 的代理设置。
目标路径:
1
| C:\Users\<用户名>\.vscode\extensions\kilocode.kilo-code-<版本>-win32-x64\bin\
|
部署:
1 2 3 4 5 6 7
| cd C:\Users\jxh\.vscode\extensions\kilocode.kilo-code-7.1.20-win32-x64\bin
rename kilo.exe kilo_real.exe
copy "C:\kilo_wrapper\kilo.exe" kilo.exe
|
⚠️ 插件更新后会覆盖 kilo.exe,需重新部署。建议将部署命令保存为 deploy.bat。
工作原理说明
CLI 模式 vs 宿主调用模式的判断
通过 AttachConsole(-1) 尝试附加到父进程的控制台:
| 调用方式 |
父进程 |
AttachConsole 结果 |
行为 |
| 用户在 cmd/PowerShell 中手动运行 |
cmd.exe(有控制台) |
✅ 成功 |
绑定控制台,正常交互 |
| VSCode 插件内部调用 |
VSCode(Electron GUI,无控制台) |
❌ 失败 |
静默运行,不弹黑框 |
环境变量覆盖机制
os.Environ() 获取当前所有环境变量后,append 追加新值。Windows 在存在重复 key 时取列表中最后一个,因此 append 的值会可靠地覆盖系统中已有的同名变量。
CREATE_NO_WINDOW 的作用
这是 Windows CreateProcess 的一个标志位,告诉操作系统在启动子进程时不要为其创建控制台窗口。如果不加这个标志,即使包装器本身是 GUI 模式,子进程(控制台程序)启动时 Windows 也会自动给它弹出一个新的黑框。
局限性
- 仅适用于读取环境变量代理的程序。少数程序完全忽略环境变量,走系统 WinINet 代理或硬编码网络层,此方案无效,需要改用透明代理工具(如 Proxifier、Clash TUN 模式)。
- 插件更新会覆盖包装器,需要重新部署。
- 仅限 Windows,Linux/macOS 下可用 shell 脚本 +
exec "$@" 实现类似效果,无需 Go。