自家 Go 工具发出去之后 strings 一扒全是明文,Garble 能盖住哪一层?

发出去的 Go CLI 被对方扔进 strings,包路径和报错文案还躺在明文里。有人问混淆工具能挡到哪一层。命令行小工具、内部分发的 CLI,或者要卖给客户的可执行文件,用 go build 打完包丢出去,对方再跑 nm,函数名也还在。有人说「Go 编译型语言还要混淆?」——其实普通产物里留给反射、栈跟踪和调试的信息本来就多。下面只谈保护自己的构建产物,不讨论怎么拆别人的程序。

对照 burrowers/garble 仓库 README 的现行写法:它包一层 Go 工具链,常用入口是 garble build(也支持 test / run / reverse)。文档写明需要 Go 1.27 或更新。本机 $PATH 里的 go 决定版本,换工具链可以用 GOTOOLCHAIN。

它实际动了什么

README 把机制写得很直:

  • 标识符和包路径换成短 base64 哈希
  • 位置信息换成短哈希文件名
  • 去掉构建信息、模块信息和调试信息(对应 runtime.Version、debug.ReadBuildInfo 一类入口)
  • 加 -literals 时再搅字符串字面量和 //go:embed 进去的文件内容
  • 加 -tiny 时再多砍一截:位置信息直接删掉,不少 panic / fatal 打印路径也拿掉,符号名进一步省略。文档自称整体体积大约能再小 15% 量级

默认构建是确定性的,方便缓存,也方便事后用 garble reverse 把栈跟踪还原。需要固定或轮换随机性时,可以用 -seed。-seed=random 每次换种子,但丢了种子就没法 reverse。

构建耗时 README 写的是大约两倍于普通 go build(先做一遍类型检查向的构建,再做混淆构建)。增量构建仍走 Go 的构建缓存。Garble 自己还有 GARBLE_CACHE。

边界比旗标更要紧

混淆不是授权系统,也不是加密。README 在 embed 一节写得很白:可执行文件里带着解码所需信息,内容仍可被恢复,只是抬高阅读成本。把 API 密钥、客户许可证串硬编码进二进制,再指望 Garble 一挡就万事大吉,方向就错了——密钥该走环境变量、密钥管理或远端下发,别押在混淆上。

Caveats 里几条日常容易踩:

  • 导出方法目前不会混淆(接口实现可能要靠这些名字)
  • 不支持「只混淆某几个包」的官方白名单。遇到怪问题更建议去提 bug,而不是自己抠排除列表
  • Go plugin 当前不支持
  • runtime.GOROOT、debug.ReadBuildInfo 在混淆产物里不可用,可能波及时区加载一类依赖构建信息的路径
  • 即便符号糊了,运行时仍可被调试。-tiny 会让崩溃现场更难读,不等于别人不能 attach

-literals 本身也可逆(仓库 issue 里也有讨论),只是费功夫。常量表达式里的字面量编译期就定了,混不了。超大 embed 会走更简单的运行时解码,打开 embed.FS 还会多分配临时缓冲——性能账要自己量。

适合放哪一层

私有源码、需要付费才能拿到功能的桌面/CLI 分发,或者只是不想让反病毒误报把未剥离的 Go 二进制当成可疑样本——README 把这些都写成常见动机。-tiny 更像体积优化附带的信息裁剪,和 Android shrink/obfuscate 那类「瘦包」思路接近,别把它理解成「安全产品」。

落地时比较稳的姿势:发布流水线把 go build 换成 garble build,需要可读栈时保留 seed 与源码,线上告警用 garble reverse 还原。密钥和授权逻辑单独设计。别把「过了 strings 扫一眼不舒服」当成完整的 IP 保护方案。

你们发 Go 二进制时,是只 strip 符号就收工,还是已经把 Garble 编进 CI 了?卡在 -literals 的性能,还是 -tiny 之后线上 panic 太难看?