# dsh-fakeip-compat 中文 | [English](README.md) > 版本 **1.0.0** · 已在 DeepSeek Harness `0.2.1-rc.5` 的 Desktop 与 CLI profile 上实测。 为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的 `web_fetch` 提供 FakeIP 兼容层,并在「设置 → 通用设置」里带一行自己的配置界面。 当本机 DNS 返回 FakeIP 段里的占位地址时,Harness 会在发出任何字节之前就拒绝这次抓取: ```text Error: URL hostname "raw.githubusercontent.com" resolves to a non-public IP address ``` 本插件以同一个 provider id 重新注册 `http` 抓取器,只在原有判定之前多一条规则;也可以选择用 DNS-over-HTTPS 复核真实目标地址。连接固定、同源重定向、字节与字符上限、字符集解码这些行为一律沿用 Harness 原实现。 `compat` 这个后缀就是重点:除了目标判定,其他都不变。provider id、行 id、组合方式都是 DeepSeek Harness 原本就有的那套,因此可以直接插进已经在用 `fetchProvider: http` 的 profile,不需要改动别处。 --- ## 目录 - [问题在哪](#问题在哪) - [实现方式](#实现方式) - [真实地址校验](#真实地址校验) - [环境要求](#环境要求) - [安装](#安装) - [配置](#配置) - [验证](#验证) - [Desktop 相关说明](#desktop-相关说明) - [排错](#排错) - [目录结构](#目录结构) - [许可](#许可) --- ## 问题在哪 `@deepseek-ai/dsh-web-fetch-http` 解析域名一次,并要求**每一个**结果都是全局可达单播地址(内部用 `ipaddr.js` 的 `range() === 'unicast'`)。FakeIP 解析器返回的地址落在保留段或唯一本地段里,于是每次请求都被拒: | 解析结果 | `ipaddr.js` 判定 | 结果 | |---|---|---| | `198.18.0.0/15` | `reserved` | 视为 non-public,拒绝 | | `fc00::/18` | `uniqueLocal` | 视为 non-public,拒绝 | 凡是解析到这类网段的域名,agent 一律抓不到。 ## 实现方式 在原有判定之前插入一条规则: ```text 目标落在 fakeipRanges 内 -> 放行 allowNonPublic === true -> 放行(整体跳过校验) 其余情况 -> 仍是原来的 public-unicast 判定 ``` 私网、回环以及 `169.254.169.254` 元数据地址默认仍然拦截。 几个值得知道的取舍: - **零依赖。** IPv4/IPv6 解析、CIDR 匹配与 non-public 段表都在 `lib/ip.js` 里。profile 里安装的 bundle 只能解析自己的 `node_modules`,因此插件不能依赖外部包。 - **能复用就复用官方传输层。** 自定义 resolver 作为 Harness 自己的 `HttpFetchProvider` 的第二个构造参数传入,连接固定、重定向、上限与解码都是官方实现,而不是复制品。 - **另有 Node 原生兜底。** 官方模块解析不到时,等价的 `node:http`/`node:https` 传输接管:连接固定与上限仍生效,但绕过 Harness 的代理路由。`runtime.implementation` 会说明当前是哪一种。 - **官方行是被禁用,不是被替换。** bundle patch 关掉 `web-fetch-http` 只是因为两个 provider 不能共用 id `http`。 ## 真实地址校验 FakeIP 答案本身不携带任何关于真实目标的信息:解析器只给一个占位地址,代理随后再去解析域名。因此一个被指向内网的域名,看起来和任何普通占位地址完全一样。 打开校验后,会在建立连接之前向一个独立的 JSON DoH 端点询问真实答案: | `verifyRealAddress` | 行为 | |---|---| | `off`(默认) | 完全不查询,零额外延迟 | | `warn` | 查询,把非公网的真实答案写进日志,然后照常放行 | | `enforce` | 查询,真实答案落在非公网时拒绝本次抓取 | 校验开启时,`dohFailurePolicy` 决定「查不出来」意味着什么:`allow`(默认)或 `deny`。只有确实没拿到可用答案才算查不出来;拿到公网地址就是通过。 **为什么默认关闭。** 校验不是免费的,这些取舍值得明说: - **两套 DNS 视角可能不一致。** 分流或策略型解析器返回的答案与公共端点本来就可能不同。`enforce` 会把这种差异变成抓取失败,因此它适合「唯一的分流就是代理」的家庭网络,不适合策略复杂的部署。 - **查不到不等于不允许。** 把「问不到」当成「不准」会让一次 DNS 抖动变成 `web_fetch` 全面不可用,所以失败策略单列,且默认 `allow`。 - **它会泄露访问意图。** 每个被校验的域名都对该 DoH 运营方可见,而这恰恰是配置本地解析器的人可能不想要的。 - **字面量地址不参与校验。** IP 字面量是调用方自己写的,而且主要路径已经覆盖:字面量直接参与分类,DNS rebinding 也赢不了,因为连接被固定在已校验的地址集上。 所以校验只堵一个具体的口子——**域名**的真实答案是内网地址——代价是可用性、延迟与隐私。默认关闭;只有当这个口子正是你在意的东西时才打开。 ## 环境要求 | 项目 | 值 | |---|---| | DeepSeek Harness | `0.2.1-rc.5`(已实测),Desktop 与 CLI profile 皆可 | | Node.js | 20 及以上 | | 组合要求 | 必须挂载 `web`;配置行还需 `webServer` 与 `configEditor` | 没有 `webServer` 或 `configEditor`(例如纯 TUI profile)时,抓取器照常工作;配置行会提示配置路由不可用,此时改用 `cordis.patch.yml` 手写行配置。 ## 安装 ```text plugin_manager { action: "install_bundle", target: "<本仓库的绝对路径>" } ``` 已安装时覆盖会被拒绝(`already-installed`),先移除再安装: ```text plugin_manager { action: "remove_bundle", target: "dsh-fakeip-compat" } plugin_manager { action: "install_bundle", target: "<本仓库的绝对路径>" } ``` bundle patch 做两件事: ```yaml - id: web-fetch-http # 仅为止重复注册 "http" provider id disabled: true - insert: - id: fakeip-compat name: 'dsh-fakeip-compat' config: fakeipRanges: ['198.18.0.0/15'] allowNonPublic: false verifyRealAddress: 'off' ``` 也可以从 GitHub 用 tarball 安装:`package.json` 在 `dsh.install` 里记下了正式来源(tarball 优先),不需要自己拼 URL。 ### 需要重启吗 - **Host 半体**(抓取器与配置路由):安装返回 `application: applied` 即生效;保存配置后 Loader 自动 reconcile 该行,无需重启。 - **Client 半体**(设置行):浏览器模块图在启动时组合。首次安装、或改动 `client.js` 之后,需要重启 DSH 才会出现。 ## 配置 设置 → 通用设置 → 下拉到底 → **FakeIP 抓取兼容**: - **FakeIP 网段**:chip 列表。输入 CIDR 后回车即添加,重复项自动忽略,点 `×` 删除单条。列表为空表示所有 non-public 目标仍被拦截。 - **取消 non-public IP 限制**:对应 `allowNonPublic` 复选框。 - **真实地址校验**:`off` / `warn` / `enforce`,以及 DoH 端点与失败策略。 - **保存**:把行配置写进 profile 的 `cordis.patch.yml`,Loader 随即 reconcile 该行,抓取器按新设置重载。 读写链路: ```text 浏览器 → GET|POST /ext/fakeip-compat/config → configEditor.edit(entry, …) → ~/.dsh/profiles//cordis.patch.yml → Loader reconcile 该行 → 抓取器按新配置重载 ``` ### 行配置 | 字段 | 默认值 | 说明 | |---|---|---| | `fakeipRanges` | `['198.18.0.0/15']` | 视为合法目的地的 CIDR 列表,可多条,IPv4/IPv6 混合 | | `allowNonPublic` | `false` | 彻底取消目标校验,私网与回环地址一并放行 | | `verifyRealAddress` | `'off'` | `off` / `warn` / `enforce`,语义见上 | | `dohEndpoint` | `'https://dns.google/resolve'` | 校验开启时使用的 JSON DoH 端点,仅支持 https | | `dohTimeoutMs` | `4000` | 单次 DoH 查询的超时 | | `dohFailurePolicy` | `'allow'` | 校验开启时「查不出来」的含义 | | `maxResponseBytes` | `5000000` | 响应体字节上限 | | `maxBodyChars` | `100000` | 解码后字符上限 | | `timeoutMs` | `30000` | 抓取超时 | | `maxRedirects` | `5` | 同源重定向跳数上限 | | `userAgent` | `deepseek-harness/…` | 请求 `User-Agent` | ```yaml - id: fakeip-compat name: 'dsh-fakeip-compat' config: fakeipRanges: - '198.18.0.0/15' - 'fc00::/18' allowNonPublic: false verifyRealAddress: 'warn' dohEndpoint: 'https://dns.google/resolve' dohFailurePolicy: 'allow' ``` ## 验证 **1. 配置路由。** 端口换成当前 GUI 实际使用的: ```powershell Invoke-WebRequest -Uri 'http://127.0.0.1:/ext/fakeip-compat/config' -UseBasicParsing | Select-Object -ExpandProperty Content ``` 期望 `HTTP 200`、`rowId: fakeip-compat`、解析后的 `config`,以及 `runtime` 块: ```json { "rowId": "fakeip-compat", "config": { "fakeipRanges": ["198.18.0.0/15"], "allowNonPublic": false, "verifyRealAddress": "off" }, "runtime": { "desktop": true, "electron": "44.7.0", "node": "24.21.0", "dshVersion": "0.2.1-rc.5", "implementation": "shipped-http", "verifyRealAddress": "off", "shippedPath": "…/dsh-web-fetch-http/lib/index.js", "resolvedFrom": "…/dsh/package.json" } } ``` `runtime` 仅供诊断,刻意不在设置行里渲染。 **2. 设置行。** ```text cordis_inspect_query { platform: "client", provider: "Slots", method: "listSubTree", input: { root: "settings.general.item" } } ``` `occupants` 里应出现 `{ "id": "fakeip-compat", "order": 101, "active": true }`。 **3. 抓取。** 以前必失败的地址现在能取回正文: ```text web_fetch { url: "https://raw.githubusercontent.com/nodejs/node/main/README.md" } ``` **4. 离线自测。** 不碰 DNS、不联网: ```bash node scripts/selftest.mjs ``` 覆盖 CIDR 边界(`198.18.0.0` 与 `198.19.255.254`)、`fc00::/18` 内外、`::ffff:` 映射、私网/回环/元数据仍拒绝、非法 CIDR 报错、四个校验字段的配置校验,以及 URL 策略。 ## Desktop 相关说明 Desktop 是 Electron 应用,对 profile 里安装的 bundle 有三处影响: | 项目 | 值 | |---|---| | 宿主安装树 | `<...>/resources/app/dsh/`(部分发行形态在 `app.asar` 内) | | Profile 目录 | `~/.dsh/profiles/` | | Web GUI | `http://127.0.0.1:<随机端口>`,每次重启都会变 | | 客户端 bundle 热更新 | 无;改动 `client.js` 需重启 | 官方抓取器按以下顺序探测: 1. 裸 `import('@deepseek-ai/dsh-web-fetch-http')` 2. `process.resourcesPath/app/dsh/package.json`(**Desktop 主路径**) 3. `process.resourcesPath/app/package.json`(`asar` 打包形态) 4. `process.argv[1]`(CLI 启动点) 5. `dirname(process.execPath)/resources/app/dsh/package.json` 6. `process.cwd()/package.json` 7. `$DSH_PROFILE_DIR/package.json` 全都没命中时由 Node 原生兜底接管:抓取仍可用、上限仍生效,但会绕过 Harness 的代理路由。`runtime.implementation` 可区分两者。 ## 排错 | 现象 | 原因 | 处理 | |---|---|---| | `WEB_DUPLICATE_PROVIDER` | `web-fetch-http` 行没被禁用 | 确认 patch 里的 `- id: web-fetch-http` / `disabled: true` | | 仍报 `WEB_BLOCKED_URL` | 该网段没进 `fakeipRanges` | 错误文本带具体地址,把该网段加进列表 | | `WEB_BLOCKED_URL` 提到 `verifyRealAddress is 'enforce'` | 真实答案非公网且模式为 `enforce` | 改成 `warn`,或把该域名排除在外 | | 打开校验后所有抓取都失败 | DoH 端点不可达且 `dohFailurePolicy` 为 `deny` | 修端点,或把策略设为 `allow` | | 配置行提示配置路由不可用,`(HTTP 404)` | `/ext/fakeip-compat/config` 从未注册:没有 `webServer`,或本行先于它激活 | 插件已通过 `ctx.inject` 等待 `webServer`;仍复现则重启 DSH | | `… (HTTP 503)` | 组合里没有 `configEditor` | 纯 TUI profile 没有设置行,改用 `cordis.patch.yml` 手写 | | `… row is not present in this profile` | 行 id 不是 `fakeip-compat` | 改回默认 id | | 保存报 `is overridden by a home patch or command-line overlay` | 更高优先级的 patch 锁定了该行 config | 改 `$DSH_HOME` 层或 `--patch` 覆盖层 | | 设置里始终没有这一行 | 客户端半体没进模块图 | 确认 `dsh.client.inject` 里的每个包在当前版本都存在,然后重启 DSH | | `runtime.implementation` 为 `node-native` | 官方 `HttpFetchProvider` 没解析到 | 按上面的探测列表补路径;抓取可用,代理路由失效 | ## 目录结构 ```text dsh-fakeip-compat/ ├── .github/workflows/ # CI:语法检查、离线自测、清单校验 ├── package.json # 名称、版本、main、exports、dsh.bundle + dsh.client ├── cordis.patch.yml # bundle patch:禁用官方行 + 插入本行 ├── client.js # Client 半体:window.__ModuleLoader__.load + settings.general.item ├── lib/ │ ├── index.js # Host 半体:provider、解析探测、配置路由、runtime 事实 │ ├── ip.js # IPv4/IPv6 解析、CIDR 匹配、段表、分类器 │ ├── resolver.js # 一次解析、逐条判定、可选真实地址校验、中止竞速 │ ├── doh.js # 可选的 JSON DoH 真实地址校验 │ ├── provider.js # node:http/https 兜底传输 │ ├── config.js # DEFAULTS / normalizeConfig / ROW_ID │ └── config-route.js # GET/POST /ext/fakeip-compat/config ├── locale/{en,zh}.json # 插件管理器里的标题与描述 ├── icon.svg # 插件图标 ├── scripts/selftest.mjs # 离线自测 ├── CHANGELOG.md # 版本记录 └── LICENSE ``` ## 卸载 ```text plugin_manager { action: "remove_bundle", target: "dsh-fakeip-compat" } ``` 移除 bundle 的同时,`web-fetch-http` 的覆盖也一并失效,官方行为恢复,FakeIP 目标重新被拒。 ## 许可 [MIT](LICENSE)