Windows 强制为任意 EXE 设置代理的通用方案

本文最后更新于 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) // -1,表示父进程
CREATE_NO_WINDOW = 0x08000000
)

func main() {
// 释放当前控制台(GUI 模式编译后默认没有),
// 再尝试 attach 到父进程的控制台
freeConsole.Call()
r, _, _ := attachConsole.Call(ATTACH_PARENT_PROCESS)
isCLI := r != 0
// isCLI == true → 从终端手动启动,父进程有控制台
// isCLI == false → 被 GUI 程序(如 VSCode)调用,父进程无控制台

// ── 修改这里:真实程序的路径 ──────────────────────────────────
// 方案 A:真实程序与本 EXE 同目录(改名为 xxx_real.exe)
exePath, _ := os.Executable()
dir := filepath.Dir(exePath)
real := filepath.Join(dir, "target_real.exe") // ← 改成实际文件名

// 方案 B:固定路径
// real := `C:\path\to\target.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") // Node.js 兼容
env = append(env, "https_proxy=http://192.168.1.1:10808") // Node.js 兼容
env = append(env, "NODE_TLS_REJECT_UNAUTHORIZED=0") // 忽略内网证书错误
// env = append(env, "ANY_OTHER_VAR=value") // 可继续追加任意变量
// ──────────────────────────────────────────────────────────────
// 注意:重复的 key Windows 取最后一个,所以 append 会覆盖系统中已有的同名变量

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 {
// GUI 宿主调用:透传 pipe,禁止子进程创建控制台窗口(防止弹黑框)
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
REM 把原程序改名
rename "C:\path\to\target.exe" target_real.exe

REM 把编译好的包装器放进去
copy "wrapper_build\target.exe" "C:\path\to\target.exe"

4. 验证

1
2
3
4
REM 手动调用测试(应正常显示交互界面)
target.exe

REM 检查代理是否生效(看程序能否正常联网)

实际案例: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

REM 备份原始程序
rename kilo.exe kilo_real.exe

REM 放入包装器(wrapper.go 中 real 指向 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。

Windows 强制为任意 EXE 设置代理的通用方案
https://xinhaojin.github.io/2026/04/08/Windows 强制为任意 EXE 设置代理的通用方案/
作者
xinhaojin
发布于
2026年4月8日
许可协议