
camofox-browser 是我最近大半年一直在维护的一个项目。简单说它不是一个从零写的浏览器而是把 Firefox 桌面版通过一系列配置、扩展和编译选项改造成一台以减少追踪、拦截垃圾脚本、严格管理站点数据为核心目标的浏览器。大多数人现在并不是不想关掉这些追踪而是根本不知道从哪里关就算关完一圈设置某天某个网站忽然登录不上又得一个个改回来。camofox 想解决的就是这个平衡问题把隐私保护和页面可用性做到开箱即用。这套方案适合三类人被广告跟踪搞烦了的普通用户、研究浏览器安全的前端开发者以及需要做多环境下页面验证的测试工程师。1. 一次页面审查让我决定重做浏览器1.1 从官网首页的 Network 面板说起事情起源于一个普通的下午。我接到反馈说公司官网的转化数据异常排查时打开浏览器开发者工具看 Network 面板发现首页一次加载居然发起了 60 多个第三方请求。它们来自广告平台、统计服务、A/B 测试脚本甚至还有一些我花了点时间才查清楚来源的埋点。也就是说用户在我们页面上的鼠标轨迹、点击热区、滚动深度在用户完全不知情的情况下被好几家服务商分别拿走了。这个页面从表面看干干净净但数据已经不在我们手里。后来我在家里打开一个母婴论坛屏幕亮起的同时就看到一条纸尿裤广告。我并没有搜过纸尿裤。这种体验相信很多人都有过它不是巧合而是第三方 Cookie 和浏览器指纹在协同工作。它们不是某一个单独服务在追踪你而是一张由多个脚本组成的网。每个脚本只拿走一小片信息汇总起来却足以拼出一份非常具体的行为画像。1.2 我拆出来的三条追踪链路为了弄清楚到底该怎么挡我把浏览器里的追踪手法拆成了三大类。第一类是第三方 Cookie。站点在加载一个跨域资源时顺手在请求里落下标识下次你访问另一个嵌入了同一广告系统的网站对方就能通过这个标识认出来。第二类是脚本采集。页面里运行一段 JavaScript读取屏幕分辨率、操作系统、安装字体、时区、浏览器语言这些信息组合成一个近乎唯一的指纹。第三类是本地存储。localStorage、IndexedDB甚至 Service Worker 里的缓存都能让网站在你下次访问时准确记得你是谁、上次做过什么。这三类手段有一个共同点它们都发生在浏览器进程内。所以理论上浏览器本身是完全可以挡住它们的不需要操作系统配合也不需要改动任何网络配置。这就是 camofox 的切入点——把全部工作限定在浏览器自己能控制的范围内通过默认配置、拦截规则和数据隔离来实现。这个边界后来成了项目最重要的设计原则之一。1.3 这个项目到底要解决什么我把目标缩小成三个词拦截、清洗、隔离。拦截是把与当前页面功能无关的第三方资源请求挡在门外清洗是让那些绕过了请求拦截、直接通过 JS API 采集的指纹数据变得不可信隔离是让网站在不同站点之间不能互相串门。这三个词听起来简单真正落地的时候每一步都要在防到什么程度和页面还能不能用之间反复拉扯。后面我会详细讲每个模块的设计以及我踩过的坑。2. 为什么我选了 Firefox 源码而不是 Chromium2.1 许可证和构建成本先把不合适的排除市面上的定制浏览器基本两条路线Chromium 和 Firefox。Chromium 生态大各类插件、内核工具都多但它有两个让我却步的问题。一是源码体积非常夸张首次构建对机器要求很高时间以小时计个人项目根本没有那么多耐心去反复编译。二是它默认的内核机制更偏向平台侧很多隐私相关行为需要反着修补工作量大到不像一个业余项目该有的样子。Firefox 这边虽然社区规模小一点但有一个设计对我这种独立维护者极其友好user.js。它允许在不修改核心源码的情况下覆盖大量内核默认行为这意味着很多隐私策略可以在配置层面直接落地而不是动辄就要改 C 重新编译。网络请求拦截、容器隔离这类能力Firefox 也都提供了稳定的扩展 API。我不否认 Chromium 能做到同等效果只是代价完全不同。个人项目最怕的就是 90% 的时间花在搭地基上选择 Firefox 完全出于务实考虑。2.2 对隐私友好的接口很多是现成的Firefox 的 webRequest API 支持 blocking 模式可以在网络请求真正发出之前修改或者取消这是做请求拦截的基础能力。它还内置了 First Party Isolation 机制打开之后第三方请求的 Cookie 会按照顶级域名隔离相当于从根上拆掉了跨站追踪的一条大腿。Firefox 的 containers API 也提供了官方级的站点数据隔离能力比自己在扩展里模拟靠谱得多。这些能力如果放在 Chromium 上不是没有而是很多必须通过更底层的方式去实现或者依赖第三方框架。对于一个需要长期维护的项目来说内核原生的能力每多一分后续升级崩溃的风险就小一分。2.3 项目的整体结构我把 camofox 的项目结构设计得很简单方便自己维护camofox-browser/ ├── config/ │ ├── user.js │ └── policies.json ├── extensions/ │ ├── content-blocker/ │ ├── fingerprint-cleaner/ │ └── container-helper/ ├── scripts/ │ ├── build.sh │ └── update-lists.py └── docs/config 目录里是内核配置文件决定浏览器默认的隐私行为extensions 目录里是按功能拆分的扩展各自独立坏了一个不至于全崩scripts 里是自动化脚本主要负责构建和规则列表的更新。这个结构让我在换电脑、重装环境的时候只需要拉一次仓库就能完全恢复整个工作台。3. 核心模块设计拦截、清洗、隔离三个维度分开做3.1 请求拦截层uBlock 风格的规则引擎拦截层的主要工作是挡住那些和当前页面功能无关的第三方请求。我复刻了一套类似 uBlock Origin 的规则逻辑使用 Firefox 的 webRequest.onBeforeRequest 在请求发出前拦截。核心代码很简单真正复杂的是规则本身browser.webRequest.onBeforeRequest.addListener( (details) { const url new URL(details.url); if (shouldBlock(url.hostname, url.pathname)) { return { cancel: true }; } return {}; }, { urls: [all_urls] }, [blocking] );wouldBlock 里跑的不是自然语言规则而是经过压缩的匹配模式。上游我用的是 EasyList、EasyPrivacy 这些公开列表删掉一部分误伤率太高的条目再加入我自己维护的补充列表。拦截的资源类型要区分 script、image、xhr、sub_frame不能一刀切否则很多网站会直接白屏。实际操作中我发现对 script 类型拦截要格外谨慎。很多页面把核心业务逻辑都写在第三方脚本里比如统计 SDK 挂掉就导致页面卡死或者某些功能按钮点击没反应。后来我加了一档宽松模式只拦截图片、字体、iframe 这类不影响核心逻辑的资源。再后来我意识到规则引擎设计得再完美也不可能穷尽所有网站的情况所以又加了白名单机制这是后话。3.2 指纹清洗层让采集到的数据不可信请求拦截只能管住发出去的请求挡不住页面里直接跑的 JS。指纹采集就是典型的例子网站让浏览器绘制一个带有特殊图形的 canvas由于不同设备、显卡、驱动对图形渲染的细微差别最终生成的像素数据哈希值是不同的这个值可以用来跨站点识别你的设备。WebGL 指纹的原理也类似GPU 型号、驱动版本会体现在渲染结果里。对于这类采集camofox 的做法是加噪声。在 canvas 相关的 API 调用时注入一层非常轻微的颜色扰动让每次生成的哈希都不同。这样即使站点采到了数据也没法拿它当稳定的标识符。同时我开启了 Firefox 的隐私相关强化配置统一时区偏置、限制部分 API 的精度让整体环境特征变得普通化不那么显眼。这里有个度的问题。清洗强度开得过高你会遇到一些奇奇怪怪的现象地图组件定位不准、在线文档协作工具提示环境异常、某些内部系统拒绝登录。所以我最后把指纹清洗做成了三档宽松档只处理最标准的 cookie标准档开启请求拦截并处理主流指纹向量严格档把最影响兼容性的抗指纹能力也打开。默认是标准档普通用户基本感知不到。3.3 数据隔离层让网站之间无法串门数据隔离要解决的问题是我在 A 网站登录了B 网站能不能通过某种方式拿到我这边的会话信息。最直接的手段就是容器标签。Firefox 的容器机制把不同网站的 Cookie、localStorage、IndexedDB 存到彼此独立的存储空间里A 容器里的登录状态B 容器完全看不见。camofox 在此基础上做了两件事。一是配置了默认容器策略普通网站都放进一个隔离默认容器不搞花活信任的网站比如邮箱、网上银行可以用专门容器保存登录状态。二是在切换容器时通过扩展 API 自动给新容器分配独立的存储标识避免因为容器 ID 复用导致的数据泄漏风险。此外还有一个自动清理逻辑每次浏览器正常退出时清掉所有非白名单站点的 Cookie、缓存和本地存储数据只保留用户明确标记为信任的网站。这个功能在最初版本里实现得很粗暴也踩了不少坑后面在踩坑部分会详细说。3.4 规则列表的更新与同步规则列表是整个拦截体系的生命线。我写了一个 Python 脚本每天拉取上游列表做去重、合并、格式压缩再剔除已经被验证为误杀的条目。更新策略不追求实时性每周发布一次规则包就够了用户侧通过扩展的网络同步功能拉取不强制重启浏览器。# scripts/update-lists.py 简化版 import urllib.request from pathlib import Path sources [ https://easylist.to/easylist/easylist.txt, https://easylist.to/easylist/easyprivacy.txt, ] rule_path Path(../config/rules.txt) rules set() for url in sources: with urllib.request.urlopen(url, timeout30) as r: for line in r: line line.decode(utf-8).strip() if line and not line.startswith(!): rules.add(line) rule_path.write_text(\n.join(sorted(rules)), encodingutf-8)脚本本身不复杂但避免了手工维护规则列表最容易犯的错误改着改着自己就搞混了不知道哪条规则是哪个列表引入的。现在我每次变更都会跑一个回归测试脚本用一组离线页面做基准确保改动不会让本来正常访问的网站出现异常。4. 实测数据与那些没想到的兼容性问题4.1 测试环境和方法我给 camofox 建了一个 30 个常用网站的测试集涵盖新闻资讯、电商、在线办公、出行、视频门户、社区论坛等类型。测试时把 camofox 和默认 Firefox、Chrome 做对比记录三类指标首次加载发出的第三方请求数量、整页加载耗时、浏览器总内存占用。网络环境固定关闭了所有无关的后台程序每个网站测三次取中位数。测试结果比我预想的乐观浏览器平均第三方请求数中位数加载耗时内存峰值大约Chrome 默认473.1s1.4GBFirefox 默认423.3s1.2GBcamofox-browser73.5s1.4GB第三方请求数量下降了 80% 以上页面加载耗时的增加在多数网站上可以忽略不计。内存占用比默认 Firefox 高了一些这主要是因为扩展和容器机制本身有开销但还在我能接受的范围内。4.2 兼容性问题的真实样本实测中最突出的问题集中在三个类型。第一类在线支付。某些支付组件会在页面加载时运行一系列环境检测脚本这些脚本有些逻辑确实和数据采集比较像被拦截后支付组件直接判定当前环境异常调不起支付框。这是标准档模式下最闹心的问题。我的解决办法是在支付域名上加入站点白名单同时保留请求拦截和指纹清洗的宽松策略只降低强度不完全关闭。第二类是地图服务。一些网站在嵌入地图时会在加载后调用 localization API 获取时区。指纹清洗层把时区统一成 UTC 后地图自动定位到错误位置。这个问题的根源在时区干扰用户在页面上的实际需求是看地图、找地址不是隐藏时区。最后我在白名单机制里加了一个精细控制项允许地图类站点读取真实时区其他指纹向量继续屏蔽。第三类是登录状态频繁失效。这个问题出现频率最高原因也好理解容器隔离之后原本依赖第三方 Cookie 做单点登录的网站会话在切换容器时就被切断了。面对这种情况我没有走放开所有第三方 Cookie的老路而是主动升级了规则策略只要网站在白名单里我就允许它在相关容器里正常保存登录态。4.3 异常站点的管理方案面对兼容性问题我的最终方案是三步走先提示、再降级、最后放行。扩展会在拦截动作发生时记录日志如果同一个域名下的页面在短时间内出现大量被拦截请求同时页面又触发了一个简单的可用性检测脚本扩展会在工具栏显示一个提示告诉用户此页面可能因为安全策略显示异常。用户点击可以进入站点管理面板直接调整拦截等级不需要打开任何配置文件。这套机制的思路是隐私保护不应该变成用户访问正常服务的阻碍。白名单不是用来绕过什么而是把选择权交回给用户。camofox 的核心目标从来不是不让网站看见你而是别让无关的第三方把你在各处的行为拼成一张完整画像。5. 构建与日常维护中踩过的坑持续更新5.1 user.js 配置在升级后被默默重置这是我遇到的第一个大坑。Firefox 每次大版本更新某些配置项的默认值会变化user.js 里的配置如果旧了就可能被忽略或者被覆盖。我最初是在本地手工改 user.js升级后发现拦截效果明显变弱检查半天发现内核把好几个关键配置项重置了。解决办法是把受管策略配置文件 policies.json 也用起来。它和 user.js 的区别在于policies.json 的优先级更高浏览器升级时不会主动改动用户自定义的受管配置。这样即使某次升级后内核默认行为有变化我的关键配置还是能兜底。现在每次有新版火狐发布我都会先跑一遍配置校验脚本把过期项找出来。5.2 自编译版本无法启用未签名扩展我在开发初期用的是火狐的开发者版本它在 about:config 里允许关闭扩展签名校验。但正式发布的自编译版本默认会要求所有扩展都必须经过签名。这个问题卡了我一个周末最后确认是只需要在构建脚本中显式设置xpinstall.signatures.requiredfalse并且发版时强制使用开发者渠道的构建基础。顺带提一句扩展签名其实是个好东西它防止恶意扩展随意注入页面代码。camofox 只在项目构建脚本里为本地开发关了签名校验面向外部使用时我建议还是走正式的签名渠道。5.3 规则列表冲突导致无限重定向有一次用户报告某个资讯网站无法访问打开就是此页面造成了无限重定向的提示。我一开始以为是网站自己的问题后来用默认浏览器打开完全正常。排查过程很折腾我先在 about:config 里临时关闭全部拦截页面恢复正常说明问题出在拦截层再逐步二分禁用规则列表最后定位到一条第三方统计脚本规则。原来是那个统计脚本在页面没加载成功时触发了页面上的一个反馈机制脚本用 history.replaceState 判断自己是否加载没加载就刷新页面刷新后又被拦截再次触发反馈陷入死循环。这个案例给我上了很好的一课规则引擎不仅要考虑拦截还要考虑页面对拦截的反应。解决方法是给这条规则加上一个特殊标记允许它在某些页面被跳过。5.4 自动清理逻辑误杀本地开发环境标准版 camofox 的自动清理策略是浏览器退出时清掉所有非信任站点的本地数据。有一次我在本机调试项目时发现 localhost 域名的登录态总是消失刷新页面就掉登录。排查半天发现自动清理策略会忽略 localhost 之外的一切唯独没把 localhost 自己加进免清理列表。开发者的本地环境确实不该被当作用户数据清掉。修复方式很简单在清理逻辑里把所有环回地址和局域网 IP 加入白名单。这个问题的价值在于提醒我自动化的清理功能虽然方便但一定要有充分的场景判断否则就是一把无差别攻击的刀。6. 项目边界与后续规划有些能力我选择不碰6.1 设计边界所有逻辑都控制在浏览器以内camofox 从第一天起就有一条明确的设计边界所有功能都运行在浏览器主体内部只管理页面资源加载和站点数据不涉及浏览器以外的任何网络设置。这个边界不是技术限制而是主动选择。原因很简单浏览器的本职工作是解析和渲染网页它能控制的应该是页面资源和本地数据一旦跨到系统网络层面项目性质就完全不同了也会带来用户无法预期的风险。做一款和网络拓扑强耦合的浏览器维护成本和安全隐患都不是一个个人项目能扛得住的。守住这条边界camofox 的功能才可能保持纯粹。6.2 做数据的主人不是做网络的影子我理解的隐私保护核心是数据控制权归用户而不是让用户隐身。camofox 不会试图隐藏你的真实身份也不承诺让你在网络世界里完全不可见。它做的事情更朴素默认状态下你的浏览行为不会被一堆与业务无关的第三方服务商拼凑成完整的画像你的登录状态和本地缓存不会默认被互联网上任何一个来路不明的脚本读取。白名单机制的目的也是如此。用户主动把某个网站加入信任名单表示我愿意在这个网站上保留登录状态、允许它的脚本运行。这个过程是透明且可撤销的。所以 camofox 的使用体验是你清楚知道自己把哪些信息授权给了谁而不是浏览器在后台偷偷替你做了决定。6.3 后续的路线规划规则列表社区化是我接下来最想做的一件事。现在的规则维护还是集中在 project owner 手里但误报和漏报永远存在如果能让更多用户提交自己遇到的异常站点案例形成一个可审核的规则提案流程整个项目的实用性会大幅提升。其次是移动端适配。手机浏览器上的追踪问题其实比桌面端更严重但移动端容器机制和扩展能力受到更多限制需要重新审视哪些模块能平移、哪些需要降级。比如指纹清洗在移动端可能不需要做得太重但请求拦截和数据隔离必须做好。最后是回归测试自动化。我已经开始搭一套基于 Playwright 的回归测试脚本把测试网站集自动化起来每次规则更新后自动跑一遍把拦截后的页面状态和默认浏览器做对比。目标是把人工排查成本降到最低。写在最后的经验维护 camofox 这段时间我最大的收获并不是学会了怎么配置一个更硬核的浏览器而是理解了产品设计里默认值的分量。99% 的用户不会去改设置他们使用的就是默认值所以把默认值设计得相对安全、相对尊重用户才是真正影响大多数人体验的地方。如果你也想做类似的隐私增强工具我建议先别急着写代码花一周时间把自己常用的浏览器打开开发者工具看看每一次访问背后到底发生了什么。当你真正看清楚那些请求列表时你会对浏览器到底是怎么工作这件事产生完全不一样的理解。