docs(browser): BR-4 runbook for browser identity, UA priority, migration, CF cookies, egress
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
adc853aa64
commit
9ea0d6bed1
4 files changed
+160
-3
No files matched your search
+146
-1
@@ -365,6 +365,8 @@ camofox 1.17.0 只有 `POST /sessions/:userId/cookies`(导入),而且要另一
|
||||
| `POST /umt/sessions/{userId}/cookies` `{cookies}` | `context.addCookies`,并触发持久化插件立即落盘(与上游导入端点相同) |
|
||||
| `GET /umt/sessions/{userId}/cookies?urls=a,b` | 作用于这些地址的 Cookie:会话活着读 `context.cookies(urls)`,否则读 profile 落盘文件 |
|
||||
| `GET /umt/instance` | `{instanceId, startedAt}`:插件加载时生成,camofox 进程重启即变(BR-3 重启检测,§10) |
|
||||
| `GET /umt/identity` | 固定浏览器身份的公开部分(UA / OS / 版本,BR-4,§11.1) |
|
||||
| `PUT /umt/sessions/{userId}/proxy` `{proxy}` | 该 profile 的出口:`{server, username?, password?}` 或 `null` 直连(BR-4,§11.5) |
|
||||
|
||||
适配器先走插件;插件不在(404)再试上游导入端点(`CAMOFOX_API_KEY` 与访问凭据相同时可用,只能写不能读);都不行 → 会话照常打开,
|
||||
`credential.status = unsupported` 并说明。profile 落盘在 `/root/.camofox/profiles/<sha256(userId) 前 32 位>/storage-state.json`
|
||||
@@ -393,7 +395,8 @@ camofox 1.17.0 只有 `POST /sessions/:userId/cookies`(导入),而且要另一
|
||||
| 定时 | 会话有动作时,距上次读回(或创建)满 10 分钟,后台读回一次 |
|
||||
| refresh-login 发现 Cookie 失效 | Cookie 账号:从浏览器 profile 读回(不要求会话开着,读落盘文件),与存的不同就拿它验证一次,有效才回写并标 active |
|
||||
|
||||
读回只取作用于站点主机的、未过期的 Cookie(统计 / CDN / 第三方域名一律丢弃),与存储比较(忽略顺序),有变化才写:
|
||||
读回只取作用于站点主机的、未过期的 Cookie(统计 / CDN / 第三方域名一律丢弃),剔除 Cloudflare 的 `cf_clearance` / `__cf_bm`(仅浏览器,§11.4),
|
||||
与存储比较(忽略顺序),有变化才写(同时结束 UA 待切换,§11.3):
|
||||
Cookie 账号替换 `secret_ref`,账密账号替换(或新建)会话 Cookie;`site_account.user_info.cookieOrigin = {source: "browser", by: "browser", at}`,
|
||||
`expired` / `invalid` 的账号回到 `unknown`。来源判定:`cookieOrigin.at` 不早于 secret 最近写入(`rotated_at`,留 5 秒)才算浏览器回写,
|
||||
之后再手填或自动登录会让它变回 `manual` / `login`。
|
||||
@@ -460,3 +463,145 @@ camofox 容器重启(`docker compose up -d` 重建、OOM、手动 restart)后,
|
||||
- 真机:未对真实 camofox 容器重启核验。桌面镜像要重建后才有 `GET /umt/instance`(`docker compose build camofox && docker compose up -d camofox`);
|
||||
重建前只有被动发现(截图 / 操作时自愈),桌面模式要等一次截图或 Agent 操作才会重连。
|
||||
|
||||
|
||||
## 11. 浏览器身份、站点请求 UA 与出口(BR-4)
|
||||
|
||||
同一个站点账号的登录态在浏览器(camofox / Firefox)和 HTTP 端(Deno)之间互注(§9),所以两端要**看起来是同一台设备**:
|
||||
同一个 UA、稳定的指纹、同一个出口 IP。用户决定(2026-09-30):对外伪装 **Windows**;全实例**一套固定指纹**;
|
||||
浏览器与 HTTP **按站点统一出口**。调研见 `docs/design/browser-ua-research.md`。
|
||||
|
||||
代码:插件 `docker/browser-desktop/root/opt/camofox/plugins/umt-cookies/{index.js,identity.js}`;
|
||||
`apps/server/src/modules/sources-identity/{runtime.ts,service/user-agent.ts,service/credentials.ts,service/account.ts}`;
|
||||
`apps/server/src/modules/browser/service.ts`(`identityUserAgent` / `refreshIdentity` / `applyEgress`);迁移 `0056_browser_identity`。
|
||||
|
||||
### 11.1 固定浏览器身份(插件 `browser:launching`)
|
||||
|
||||
- **首次启动**:插件用 camoufox-js 的 `launchOptions({ os: "windows" })` 生成一份完整指纹——BrowserForge 的 Windows Firefox
|
||||
指纹(navigator / screen / 硬件)、Camoufox 的 Windows 字族表、按 Windows 取样的 WebGL(vendor / renderer / 参数)、
|
||||
字体间距 / 音频 / canvas 随机种子、`window.history.length`、canvas 抗锯齿偏移——去掉运行期字段(`humanize`、`locale:*`、
|
||||
`geolocation:*`、`timezone`、`webrtc:*`、`addons`)后落盘 **`/root/.camofox/umt-identity.json`**(compose 的持久卷 `umt-camofox`,
|
||||
与 profile 同一个卷;属主 abc、0600)。WebGL2 开关等配套 Firefox 首选项一起存。
|
||||
- **之后每次启动**(容器重启、浏览器空闲 5 分钟退出后再启动):把上游随机生成的 config 里的指纹部分整份换成身份文件里的,
|
||||
运行期字段仍取上游。浏览器进程日志 `umt identity applied {os, fingerprintId, revision}`。
|
||||
- **升级**:Camoufox 版本或 Firefox 主版本与文件里记的不同 → 只改 `navigator.*` / `headers.*` 里的版本号(与 camoufox-js 生成时
|
||||
同一个替换规则),新版本多出的指纹字段从同 OS 新样本补齐,其余一律保留;`revision + 1`,`fingerprintId` 不变
|
||||
(日志 `umt identity upgraded`)。站点看到的是"同一台电脑升级了 Firefox"。
|
||||
- **重置**(换一台"新设备",所有站点都会看到新设备):停 camofox,删 `umt-identity.json`,再启动。
|
||||
- **开关与路径**:`UMT_BROWSER_IDENTITY=off` 关掉(回到上游每次随机的 Linux 指纹);`UMT_IDENTITY_FILE` / `UMT_STATE_DIR` 改路径。
|
||||
文件损坏会改名 `.bad-<时间>` 后重新生成。
|
||||
- **`GET /umt/identity`**(访问凭据保护):`{userAgent, os, platform, firefoxVersion, camoufoxVersion, fingerprintId, revision,
|
||||
schema, createdAt, updatedAt}`,不含指纹细节。app 存进 `browser_provider.identity`(加 `fetchedAt`):打开会话时(10 分钟内读过就不读)、
|
||||
重连时、设置页探测浏览器服务时读;站点请求取它时超过 6 小时会重读,读不到 5 分钟内不再试。
|
||||
|
||||
**Windows 伪装的一致性**(camoufox-js 0.11.5 源码 + 2026-09-30 本地临时容器实测,只开本地页面):
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| UA / 请求头 | `Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0`;HTTP `User-Agent` 头同值 |
|
||||
| navigator | `platform = Win32`、`oscpu = Windows NT 10.0; Win64; x64`;并发数、语言等来自同一份 Windows 指纹 |
|
||||
| WebGL | 按 Windows 取样:`Google Inc. (Intel)` / `ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0)`(D3D11,Windows 才有) |
|
||||
| 字体 | 字族白名单 = Camoufox 的 Windows 字体表;字形来自 Camoufox 自带的 Windows 字体(`/opt/camoufox/fonts/windows`:Arial、Segoe UI、微软雅黑、宋体、等线……)。实测"微软雅黑"可用且宽度与回退字体不同,Noto / DejaVu 这类 Linux 字体不可见;中文页面以微软雅黑渲染 |
|
||||
| 跨重启 | 重启容器后 `fingerprintId`、屏幕尺寸、WebGL 不变;模拟 140 → 152 升级只改了 UA 版本 |
|
||||
|
||||
发现并修正的上游问题:camoufox-js 0.11.5 把 `FONTCONFIG_PATH` 设成 `/opt/camoufox/fontconfig/lin`(不存在;Camoufox 152 的目录是
|
||||
`linux` / `macos` / `windows`),而 fontconfig 里的字体目录写的是相对当前目录的 `fonts`(浏览器进程当前目录是 `/opt/camofox`)。
|
||||
插件把 `fontconfig/windows/fonts.conf` 复制到临时目录、字体目录改成 `/opt/camoufox/fonts` 绝对路径,再指给 `FONTCONFIG_PATH`。
|
||||
|
||||
限制:渲染仍是 Linux 上的 Firefox——字体光栅化、canvas / 音频的像素级结果与真 Windows 不同(Camoufox 用固定种子加噪,稳定但不是
|
||||
Windows 的真值);TLS / HTTP2 指纹是 Firefox 152 本身(与 OS 无关)。屏幕尺寸来自指纹(如 2560×1440),桌面模式窗口是 1280×900,
|
||||
相当于没最大化的窗口。时区跟容器 `TZ`(缺省 Asia/Shanghai);语言跟指纹(缺省 en-US),要中文设 `CAMOFOX_LOCALE`。
|
||||
指纹是进程级的:所有站点 / 账号共用这一套(用户决定;每账号一套要每账号一个浏览器进程,不做)。
|
||||
|
||||
### 11.2 站点请求 UA
|
||||
|
||||
站点请求的 UA 按账号解析,**优先级从高到低**:
|
||||
|
||||
| 来源(`SiteAccount.userAgentSource`) | 取值 | 界面 |
|
||||
| --- | --- | --- |
|
||||
| `manual` | 账号手动填的 UA(`site_account.user_agent`) | 手动填写 |
|
||||
| `pending` | 待切换:取得当前登录态时用的旧 UA(`site_account.ua_pending`,§11.3) | 待切换:… |
|
||||
| `global` | 设置 → 站点默认 → User-Agent(`settings.sites.userAgent`,非空时) | 使用设置里的全局 UA |
|
||||
| `browser` | 浏览器固定身份的 UA(`browser_provider.identity.userAgent`);读不到时用常量 `FALLBACK_SITE_USER_AGENT`(同型 Windows Firefox 152) | 跟随浏览器 |
|
||||
|
||||
- 覆盖面:站点适配器(列表 / 搜索 / 详情 / 下载 .torrent / 健康检测)、RSS、账密登录、用户数据、Cookie 失效时的浏览器读回验证——
|
||||
全部经 `SourcesRuntime.siteFetch`:请求自己没设 UA 就补上;适配器每条请求现场按"与站点凭证同一个账号"(第一个启用、未失效、非 Passkey)
|
||||
取 UA,与 Cookie 同一时刻读库,不缓存。之前适配器与 RSS 发的是 Deno 缺省 UA `Deno/2.9.7`,已不再出现
|
||||
(测试 `apps/server/test/sources-identity/user-agent.test.ts` 逐条断言)。不挂站点的请求(通用 RSS)用全局 UA / 跟随浏览器。
|
||||
- 设置里的 User-Agent **缺省留空 = 跟随浏览器**(原缺省 Windows Chrome 140 已去掉);填了会让 HTTP 与浏览器 UA 不一致,只在确有需要时用。
|
||||
- 账号"更新凭证"里的 UA 留空 = 不覆盖;Cookie 是从别的浏览器复制的、且站点校验 UA 时才填那个浏览器的 UA。
|
||||
- 不在范围:TMDB、图片代理(不带凭据的公开请求)、下载器、浏览器服务本身。
|
||||
- 升级 camofox 镜像(Firefox 大版本变)时:浏览器身份自动升级(§11.1),app 最迟 6 小时(或下次打开会话 / 重连)跟上;
|
||||
同时把 `packages/contracts/src/settings.ts` 的 `FALLBACK_SITE_USER_AGENT` 改成新版本号。
|
||||
|
||||
### 11.3 平滑迁移(待切换)
|
||||
|
||||
改 UA 一般不会让 NexusPHP 的 Cookie 失效,但前置 Cloudflare / WAF 的站点可能要求重新过挑战。所以**不立即给存量账号换 UA**:
|
||||
|
||||
- 迁移 `0056_browser_identity`:Cookie / 账密账号(有凭证、没手动 UA)记 `ua_pending` = 取得当前登录态时的 UA——设置里改过全局 UA
|
||||
就是那个值,否则是旧缺省 Chrome 140;API Key / Passkey 账号与网页登录态无关,直接跟随浏览器;设置里的 UA 仍是旧缺省值时删掉
|
||||
(= 跟随浏览器),改过的保留为全局 UA。
|
||||
- 待切换期间,这个账号的所有站点请求(包括原来发 Deno UA 的适配器 / RSS)都用旧 UA,与登录态一致。
|
||||
- **切换时机**(Cookie 与 UA 同一事务写,下一条站点请求两者一起生效):
|
||||
1. 重新登录:账密账号 refresh-login 走 takelogin 时用切换后的 UA,拿到会话 Cookie 后清 `ua_pending`;
|
||||
2. 浏览器回写 Cookie(§9.2 的交还 / 关闭 / 定时 / refresh-login 读回):浏览器是用自己的 UA 取得 / 使用这份登录态的,回写时清 `ua_pending`;
|
||||
Cookie 没变但浏览器不在登录页、正用着这份登录态,也清;
|
||||
3. 换主凭证(填新 Cookie / 新密码):新登录态,直接跟随浏览器。
|
||||
- Cookie 账号如果一直不开原站视图、也不换 Cookie,会一直待切换(界面写"待切换:下次刷新登录,或在原站视图里用过后改为跟随浏览器")。
|
||||
刷新登录对 Cookie 账号只校验,不切换。
|
||||
|
||||
### 11.4 Cloudflare Cookie 仅浏览器
|
||||
|
||||
`cf_clearance`、`__cf_bm`(及挑战过程中的 `cf_chl_*`)与取得它的浏览器绑定(UA + IP + TLS 指纹),Deno 的 TLS 指纹与 Firefox 必然不同,
|
||||
HTTP 端拿去用多半无效还可能触发风控。所以:
|
||||
|
||||
- 浏览器回写账号时剔除这些 Cookie,只在 `site_account.user_info.browserOnlyCookies` 记名字(不含值);它们留在浏览器 profile 里照常用;
|
||||
- 比较"有没有变化"时也不算它们(CF Cookie 轮换不会触发回写);refresh-login 从浏览器读回的 Cookie 同样剔除;
|
||||
- 站点账号页技术信息里显示"仅浏览器使用的 Cookie";
|
||||
- 用户手填的 Cookie 里带 `cf_clearance` 不处理(用户自己的选择)。
|
||||
|
||||
### 11.5 出口 IP 按站点统一
|
||||
|
||||
实测(本地临时容器):**Firefox 会用容器的 `HTTP_PROXY` / `HTTPS_PROXY` 环境变量**(Firefox 缺省"使用系统代理",Linux 下没有
|
||||
GSettings 就读环境变量)。所以本机栈此前的状态是:`docker-compose.override.yml` 给 camofox 设了 `HTTP_PROXY=http://172.17.0.1:7891`,
|
||||
浏览器**所有**站点都走代理;HTTP 端只有开了"使用代理"的站点走代理——同一账号两端出口 IP 不同。
|
||||
|
||||
现在(`UMT_BROWSER_PROXY_MODE=site`,缺省):
|
||||
|
||||
- 插件在启动时设 `network.proxy.type = 0`:浏览器**不再用**容器环境变量里的代理,缺省直连;
|
||||
- app 在打开会话 / 重连前调 `PUT /umt/sessions/{userId}/proxy`:站点开了"使用代理"且设置了代理地址 → 该站点 profile 的
|
||||
BrowserContext 走同一个代理(Playwright 按上下文代理,Firefox / Juggler 原生支持;URL 里的账号口令拆成 username / password);
|
||||
否则 `null` 直连。插件把这张表存在 `/root/.camofox/umt-proxies.json`(0600,代理带口令时含口令),camofox 重启后照用;
|
||||
- 该 profile 已有活动上下文、出口不同 → 插件先关掉它(持久化插件先落盘登录状态),下一次操作按新出口重建;
|
||||
- 实测:直连 profile 访问测试域名走不到环境变量里的代理;代理 profile 的请求到了指定代理;改出口时活动上下文被重建。
|
||||
|
||||
限制与注意:
|
||||
|
||||
- **代理地址要让 app 与 camofox 两个容器都能连**(如本机的 socat 中继 `http://172.17.0.1:7891` 可以;`http://127.0.0.1:7890` 在
|
||||
camofox 容器里连不到)。
|
||||
- 站点"使用代理"开关或代理地址改了,已打开的浏览器会话在**下一次打开 / 重连**时才按新出口;要立即生效就关掉会话重开。
|
||||
- 匿名会话与 Agent 打开的站点会话同样按站点出口(浏览器会话都属于某个站点)。
|
||||
- 旧镜像 / 插件没有出口接口:app 只记日志,浏览器照旧按环境变量出网。`UMT_BROWSER_PROXY_MODE=env` 可退回旧行为。
|
||||
- SOCKS 代理的 DNS 解析位置未核验(HTTP 代理由代理端解析,与 Deno 的代理客户端一致)。
|
||||
- 本机栈重建 camofox 后,憨憨(未开代理)的浏览器会话会从"经代理"变为**直连**,与 HTTP 端一致;override 里 camofox 的
|
||||
`HTTP_PROXY` 可以留着(Firefox 不再用它),也可以删掉。
|
||||
|
||||
### 11.6 上线步骤与核验
|
||||
|
||||
需要重建 **app**(代码 + 迁移 0056)和 **camofox**(插件):
|
||||
|
||||
```sh
|
||||
docker compose --env-file .env.deploy build app camofox
|
||||
docker compose --env-file .env.deploy up -d app camofox
|
||||
docker compose exec camofox curl -s -H "Authorization: Bearer $CAMOFOX_ACCESS_KEY" http://127.0.0.1:9377/umt/identity
|
||||
```
|
||||
|
||||
camofox 镜像只有 `COPY root/ /` 与其后改插件清单的一层变化;前面的 apt 层与从 `camofox-browser:1.17.0` 复制 node / camofox /
|
||||
Camoufox 浏览器包的层都走构建缓存,**不会重新下载 Camoufox(约 630 MB)**。2026-09-30 在本机用临时标签实测:带
|
||||
`--build-arg APT_MIRROR=mirrors.ustc.edu.cn`(与 `.env.deploy` 相同)时 apt / 复制层全部 `CACHED`,只重跑插件两层(< 1 秒)。
|
||||
注意**构建参数要与上次一致**(用同一个 `--env-file .env.deploy`):不带 `APT_MIRROR` 构建会让 apt 层缓存失效,重新 apt 安装(浏览器包
|
||||
仍从本地 `camofox-browser:1.17.0` 复制,不从网上下)。首次启动生成身份,之后 profile 卷里的登录状态照常可用。
|
||||
|
||||
核验:`apps/server/test/sources-identity/user-agent.test.ts`(每条出站请求的 UA、优先级、待切换、CF Cookie)、
|
||||
`migration-browser-identity.test.ts`、`apps/server/src/modules/browser/__tests__/browser-identity.test.ts`(出口下发、身份存库)、
|
||||
`packages/adapters/src/browser/umt-plugin.test.ts`(插件纯函数、适配器接口);真机:本地临时容器(`--network none`,只开容器内本地
|
||||
页面)核验了 §11.1 表格与 §11.5 出口行为,未对真实站点发请求。
|
||||
Reference in new issue
Block a user