今天想介绍一个我自己写的开源项目:Fingerprint Browser。
仓库地址:https://github.com/denislov/fingerprint-browser
简单说,它是一个跑在桌面上的指纹浏览器管理器,用 Rust + GPUI Kit 写的,围绕现成的 fingerprint-chromium(指纹版 Chromium)和 Xray 工作。目前最新版本 0.3.0,Windows 安装包、Linux 归档和 Debian 包都已经发在 Releases 里,MIT 协议。

为什么要自己写一个
市面上的指纹浏览器不少,但用起来总有几件事让我别扭:
- 是云服务。 档案、Cookie、代理凭据都放在别人的服务器上,我要的是本地一个目录能装下全部东西。
- 是黑盒。 它宣称「指纹隔离」「代理生效」,但你没法验证——代理到底通没通,浏览器到底有没有真的走代理,页面看到的出口 IP 是哪一个,全靠信。
- 不透明。 后台悄悄下载内核、悄悄联网、悄悄把流量直连出去,这些恰恰是这类工具最不该做的事。
所以我想写一个我完全看得懂、跑在自己机器上、每个结论都有证据的版本。它不 fork Chromium,不去改内核,只做一件事:把「一份档案配置」编译成「一个稳定、可复现的浏览器运行环境」,然后可靠地管理它的生命周期。
它是什么,也不是什么
先说清楚边界,避免期待错位。
它是:
- 一个桌面端的多档案管理器,每个档案有独立的
user-data-dir、稳定的指纹种子和独立进程; - 一个代理编排器:每个走代理的档案配一个独立的 Xray 进程,支持 SOCKS5/HTTP 表单,也能导入 Shadowsocks、VMess、VLESS、Trojan 分享链接;
- 一套可验证的运行时:能测代理、能读回浏览器真实出口地址、能读回运行中浏览器的指纹面并对照差异。
它不是:
- 不是 Playwright / Puppeteer 那样的自动化平台;
- 没有云同步、团队协作、账号权限;
- 不做扩展市场,也不自己打 Chromium 补丁;
- 不暴露远程 CDP,不把浏览器变成别人可以远程操控的入口。
CDP 只监听回环地址,而且只用于「启动就绪、正常关闭、读回指纹」这几件本机的事。这一点是刻意的。
界面走一遍
1. 档案:一个档案 = 一套独立环境

每个档案都拥有自己的指纹种子、数据目录和浏览器进程。新建时你可以指定:
- 种子(Seed):驱动这个档案声称的所有易变指纹项,一键换新;
- 品牌 / 平台:Chrome、Edge、Opera、Vivaldi,配 Windows / macOS / Linux;
- 语言、时区、窗口尺寸;
- WebRTC 策略:禁用非代理 UDP、仅公开接口、公开与私有;
- 浏览器内核与代理:都从下拉里选,代理在启动时选定,改完从下次启动生效。
档案可以启动、停止、重启,启动过程中还能取消;正常停止会先通过 CDP 发 Browser.close,让 Cookie 和持久状态有机会落盘,然后才强制清理。删除档案时只删记录,磁盘上的浏览器数据保留——手滑删错了,数据还在。
列表支持按名称、种子、品牌、平台、内核、代理筛选。但这里有个我刻意的取舍:**筛选只影响「显示什么」,不会批量操作一个隐藏的档案。**一个被筛掉的档案该跑还是跑,详情面板回答的也还是它。多选和批量操作我不打算做,理由记在仓库文档里。
2. 代理:一个能连上的端口,不等于一个能用的代理

这是我在这个项目里最坚持的一点。
一个开着的回环端口不等于一个能用的代理。启动器最多只能知道 Xray 把本地入口绑定成功了;至于它上游凭据被拒、域名解析不了、还是根本没把任何字节送出去,这个检查一个都发现不了。结果就是:档案展示一个指纹,却被另一个地址看到——这正是这个项目想要避免的失败。
所以我做的是发一个真实的 HTTP 请求,然后报告出口端看到的地址,或者报告失败停在哪一层:引擎、配置、认证、域名解析、不可达、超时、TLS、HTTP 状态、响应不可读。测试端点故意是明文 HTTP 的(默认 http://api.ipify.org,可用 FP_BROWSER_ECHO_URL 改),因为走 TLS 的话证书问题会被误报成代理问题。
更关键的一条:**启动一个走代理的档案时,会先问代理。**按下「启动」,程序先发一次和代理页同样的真实请求,只有真的有字节从引擎回来,才把启动排进队列。代理不通,浏览器就根本不启动,行上显示「启动中」,拒绝信息会点名是哪个档案、哪条代理、卡在路径的哪一段。代价是每次启动多一个往返,换来的是「浏览器在跑、看起来很健康、其实在裸奔」这件事不会发生。
在此基础上还有两件事:
- **代理崩了,浏览器就关。**Xray 必须就绪 Chromium 才启动;运行中 Xray 崩掉,它的浏览器会被一起关掉,而不是悄悄退回直连。
- **读回浏览器自己的出口地址。**测代理只能证明代理在做什么,证明不了浏览器被指到了它。所以验证一个走代理的档案时,程序会在一个临时标签页里让浏览器自己请求一次端点,再把看到的地址读回来,并把四种结果分开:拿到地址、从未到达、没在时限内完成、返回里没有地址。「从未到达」和「没完成」是从页面状态读出来的,不是用一个超时去猜的。
3. 浏览器内核:版本决定它能说什么指纹

内核就是一个个 fingerprint-chromium 可执行文件。程序会去探测它的真实版本,并据此决定这个内核能接受哪些指纹开关:
- 未知的大版本会被拒绝;
- 不支持的指纹声明会产生警告,而不是默默忽略;
- Windows 上还会读清单和 PE 元数据。
内核页上的「下载内核」会列出 adryfish/fingerprint-chromium 发布过的东西,按新到旧排,每个系统一个按钮——本机的那颗标出来,另外两颗留给别台机器。下载是可移植版本,解压到数据目录后走和手动添加完全相同的版本探测:大版本是从二进制里读出来的,不是从 release tag 抄的。一次只下载一个,进度显示在它自己那一行。
同样的规则也适用于 Xray:设置页里有对应的卡片,从 XTLS/Xray-core 下载本机对应平台架构的构建,解压、写进设置、下次启动生效。**而且——不点就不下载。**这是整个项目的一条硬规则:程序不会替你下载任何东西。
4. 日志:这个窗口做过什么、看到什么

所有操作都有记录:启动、停止、警告、错误与读数。日志同时写进 logs/activity.log,达到 512 KiB 轮转一份备份,内存里的历史上限 500 行。「清空列表」只影响当前页面,文件里的内容一律保留。启动日志里能看到 PID、CDP 端口、SOCKS 端口、Xray PID,以及用了多少个启动参数。
5. 设置:有效值、来源,都说清楚

设置页有一个我很喜欢的设计:它不只显示「当前值」,还显示这个值是从哪来的——是配置文件、还是环境变量覆盖的。环境变量永远优先于保存的设置,界面会注名是哪一边赢了。
这里可以配置:
- 外观:深色 / 浅色。颜色是唯一一个改完立刻生效、不等下次启动的设置;
- 语言:English / 简体中文,两项都立刻作用于当前窗口;
- 关闭窗口时:每次询问、后台继续运行(窗口隐藏,托盘图标可唤回)、仅退出程序(浏览器留着跑)、退出全部。
一个目录 = 一整套安装
配置、数据库、日志、档案、导出文件,全都在同一个数据目录里,用 FP_BROWSER_DATA_DIR 或平台默认值选定。程序会对数据目录加内核级文件锁,同一目录只允许开一个窗口:第二个副本拿不到锁,会打印出「谁」占着这个目录然后以退出码 3 退出,而不是打开同一个数据库、把第一个副本正在跑的浏览器停掉。锁是内核的,所以崩溃也不会留下死锁。
--version、--help、--diagnostics 三个选项不打开窗口就能用。--diagnostics 会写出这个安装的一份报告——构建信息、有效设置及来源、它维护的每个文件的状态和权限、日志尾部——从不包含代理凭据,也从不读浏览器数据。
有多轻

这是任务管理器里的它:16.5 MB 内存,0.1% CPU,0 网络。不是随便一帧的峰值,而是它开着窗口、平时待着的常态。
之所以能这么小,是因为它从一开始就没打算把「一个浏览器」塞进自己身体里:
- 原生 Rust + GPUI Kit,不是 Electron、不是 CEF 套壳。没有打包进去的 Chromium 运行时,没有 Node 运行时,没有几百 MB 的
node_modules; - 不内嵌内核和代理引擎。
fingerprint-chromium和 Xray 是外部可执行文件,用哪个、放哪里都由你决定,管理器本身不背着它们; - 空闲就是真的空闲。不启动任何浏览器、不起任何 Xray、不建连接——只有你按下「启动」,进程才会被拉起来。关窗口选「后台继续运行」常驻在托盘时,也还是这个量级。
作为对照,一个 Electron 的空窗口往往就要一两百 MB 起步。对这一类需要长期挂在托盘里、时不时看一眼状态的小工具来说,这一层省下来的内存是实打实的。
不过有一句得说清楚:**省下的只是「管理器」这一层,不是浏览器本身。**真正开起档案之后,内存大头是 Chromium 自己的,和直接用 Chrome 差不多;而且每个档案是独立进程加独立 user-data-dir,开得越多自然涨得越多。这是隔离换来的成本,也是它本来就该有的样子——管理器不该为浏览器的开销背锅,也不该假装自己不占内存。
工程上我在意的事
这个项目维护着一份指纹开关矩阵:哪些开关在哪些大版本上可用,凭据是什么。其中有个例子我很想分享,因为它说明了「证据」和「假设」的差别。
矩阵一开始以 Chromium 144 为分界,这个分界是从兄弟项目那里继承来的,本仓库并没有测过 144 以下。后来我把一个 142 的构建拉出来实测,结果推翻了继承的结论:
| 在 142 / 144 / 148 上实测 | 142 | 144 | 148 |
|---|---|---|---|
--fingerprinting-canvas-image-data-noise 能改变 toDataURL | 是 | 是 | 是 |
--disable-spoofing=canvas|clientrects|audio 被采纳 | 否 | 是 | 是 |
也就是说,噪声开关不是 144 才有的特性,而排除开关才是。表格和测试都被修正了。150 行不到的文档背后是这类反复的实测——所以至今 145–147 仍然标注为「推断」,而不是「已知」。
其他一些细节:
- 分层清晰:Rust workspace 分成
domain、storage、runtime、application、app五个 crate。UI 不直接Command::spawn(),Storage 不认识 GPUI 类型,Domain 不依赖 SQLite / GPUI / Xray JSON / Chromium CLI。 - 启动计划不可变:所有外部进程启动前,先生成一个不可变的
LaunchPlan,之后再执行。 - 慢操作进 worker:代理测试、指纹校验、打开目录、浏览器数据复制、导入导出,都交给 worker,结果经各自的通道回到 tick 循环。
- Windows 生命周期:Chromium 和 Xray 在创建时就用
PROC_THREAD_ATTRIBUTE_JOB_LIST放进独立的 Job Object,赋值和创建是原子的,没有「先创建后赋值」的空窗。会话日志记录 PID 和原生创建时间,用GetProcessTimes校验身份,把「已退出」和「读不到」区分开。Unix 上则用独立进程组,Linux 用/proc做恢复。 - CI 门禁:
scripts/check.ps1/check.sh跑 fmt、check、测试和严格 clippy,在 Ubuntu 和 Windows 上都会跑。真实引擎的验收测试是 opt-in 的,故意和门禁分开。
现状与限制
说点实话:
- 0.3.0 已经发布,Releases 里有 Windows 安装包、Linux 归档和 Debian 包,每个都带
.sha256;发布 workflow 跑在 GitHub Actions 上。 - 程序本身不附带浏览器和代理引擎,需要你自己提供
fingerprint-chromium和 Xray——或者用界面里的一键下载。 - Windows 上的真实测试已经覆盖了档案隔离、Cookie、取消、正常关闭、浏览器/Xray 崩溃、管理器被强杀和残留日志清理;Windows 端的指纹实测和远端上游转发仍是待办。
- 强制终止无法保证 Cookie/状态一定落盘,所以正常停止路径仍然是 CDP 关闭。
- 代理隧道走 UDP 的协议(Shadowsocks、SOCKS5)如果叠加 HTTP 系统代理会丢掉 UDP,这个组合会在连接层报错,而不是静默走去直连。
怎么用起来
从源码跑(需要 rustup,它会自动安装 rust-toolchain.toml 里钉住的编译器):
FP_BROWSER_CHROMIUM_BIN=/path/chrome FP_BROWSER_XRAY_BIN=/path/xray cargo run -p appLinux 需要 libfontconfig1-dev、libfreetype-dev、libxcb1-dev、libxkbcommon-dev、libxkbcommon-x11-dev——但只有运行时才需要显示器,构建本身不用。
懒得编译的话,直接去 Releases 下载:Windows 用 -setup.exe,Debian/Ubuntu 用 .deb,其他 Linux 用 tar.gz。第一次打开如果还没有内核,空列表会明确告诉你,并带一个按钮直接去内核页下载。
最后
这个项目的定位从来不是「大而全」,而是把一件小事做扎实:让每个档案的环境可复现,让每个「代理生效了」的说法可验证,让程序在你不点按钮的时候什么都不做。
如果你也在意这些,欢迎去看一眼、提 issue,或者直接读代码——仓库里的 docs/ 目录写得比这篇博客细得多,包括架构、领域模型、生命周期、指纹矩阵和每一版的实现计划。
用一句项目文档里的话收尾吧:
把 Profile 配置编译为稳定、可复现的浏览器运行环境,并可靠管理其生命周期。