dsh-web-fetch-fakeip
已验证dsh-web-fetch-fakeip · v0.2.0 · MIT
fake-ip-aware WebFetchProvider for the DeepSeek Harness web seam — keeps the web_fetch tool working under mihomo/Clash fake-ip DNS without disabling fake-ip or setting proxy environment variables.
安装
dsh plugin add dsh-web-fetch-fakeip 用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。
源码
标签
作者
说明文档
description: "面向 DeepSeek Harness web 接缝(ctx.web)的 fake-ip 感知 WebFetchProvider:在 mihomo/Clash TUN 模式 + fake-ip DNS 下让 web_fetch 工具继续可用,无需关闭 fake-ip,也无需设置任何代理环境变量。" kind: "package-reference"
dsh-web-fetch-fakeip
English | 中文
@deepseek-ai/dsh-web-fetch-http 的直接替代品:当本机 DNS 处于 fake-ip
模式、且网络走透明代理(TUN)时,让 web_fetch 工具继续可用——fake-ip 保持
开启,且不需要设置 http_proxy / https_proxy / all_proxy。
摘要
dsh-web-fetch-http 会自行解析每个主机名,并要求所有应答都是公网 unicast
地址,否则拒绝请求。而在 fake-ip 模式下,mihomo 与 Clash 会用
dns.fake-ip-range(默认 198.18.0.1/16)中的占位地址应答每一个主机名。
ipaddr.js 把 198.18.0.0/15 归类为 reserved(RFC 2544 基准测试段),因此
原版 provider 会拒绝所有主机名:
Error: URL hostname "example.com" resolves to a non-public IP address
curl 等工具不受影响,因为它们根本不做这项校验:它们把包发往占位地址,TUN
网卡截走并交给代理,由代理解析真实源站。原版 provider 的代理逃生通道同样无效,
因为 proxyRouteFor() 只读 http_proxy / https_proxy / all_proxy,而 TUN
方案一个都不需要设。
本插件完整复用原版 provider 的传输层,只替换目的地判定策略。
目录
快速开始
安装到 profile
本包已发布到 npm。裸安装解析到 latest:
dsh plugin --profile web add dsh-web-fetch-fakeip
从 DSH 0.1.2-rc.1 到 0.2.x 的所有线,这条命令都是对的。上一版 latest
(0.1.1)会被 DSH 0.2.0-rc.2 的兼容性门禁跳过;若你在该 DSH 版本上且早于
本次发布就已安装,执行一次更新即可:
dsh plugin --profile web update dsh-web-fetch-fakeip
dsh plugin 会把参数原样转发给 profile 内的 pnpm,因此任何 npm 描述符都可以
——精确版本([email protected])、dist-tag(...@next)、git URL
或本地路径。
dist-tag 语义:latest 跟踪当前 DSH 线。版本号带预发布后缀(-rc.1、
-alpha.2)的发布走 next,因此 latest 只会移向刻意切出的版本。插件的
minor 版本即它支持的 DSH minor 线——0.2.x 支持 DSH 0.2.*——详见
版本策略。
也可以从本地 checkout 安装:
# 在包含本仓库的目录下执行。相对路径以你当前所在目录为基准,而非 profile。
dsh plugin --profile web add /path/to/dsh-web-fetch-fakeip
由于本包在 package.json 中声明了 dsh.bundle,dsh plugin 会自动把它加入
dsh.profile.bundles,并把它的 cordis.patch.yml 作为一层
应用:禁用原版 web-fetch-http 行,插入本 provider。
重启 profile(或等 patchReload: live 监听器生效)后,web_fetch 即可恢复。
手动安装
若不想安装为包,可把目录复制进 profile,再用 patch 行指向文件:
cp -r dsh-web-fetch-fakeip "$DSH_HOME/profiles/web/plugins/"
# $DSH_HOME/profiles/web/cordis.patch.yml
- id: web-fetch-http
disabled: true
- insert:
- id: web-fetch-fakeip
name: './plugins/dsh-web-fetch-fakeip/src/index.js'
patch 会整体替换目标行的 config;匹配不到任何行的 patch 只会告警并被跳过,
所以即使原版行不存在,上面的 disabled: true 也是安全的。
验证
# 应打印出组合结果:web-fetch-http 已禁用,web-fetch-fakeip 已插入。
dsh --profile web --dump-config | grep -A3 fakeip
随后让 agent 抓取一个页面,或直接跑真实网络测试:
npm run test:live
安装或卸载 bundle 后必须重启。 profile 的 patchReload: live 监听器只会
重新读取 cordis.patch.yml;而 dsh.profile.bundles 列表只在启动时读取一次。
安装 bundle 会立刻把它写入该列表(因此 --dump-config 能看到),但正在运行的
host 会一直沿用启动时的层栈,直到重启。
卸载
dsh plugin --profile web remove dsh-web-fetch-fakeip
dsh plugin 会在 pnpm 结束后核对 dsh.profile.bundles,因此该 bundle 层会随
依赖一起离开层栈。之后重启 profile。原版 web-fetch-http 行会自动恢复——那条
禁用来自本 bundle 的 patch,所以移除 bundle 也就移除了禁用。
更新
dsh plugin --profile web update dsh-web-fetch-fakeip
不带 ref 的 github: 说明符跟踪仓库的默认分支,pnpm 在安装/更新时才解析它——
因此更新是显式的,绝不会静默发生。
DSH 升级后插件不再加载时
自 DSH 0.2.0-rc.2 起,若某个 bundle 的 peerDependencies 不包含当前运行的 DSH
版本,harness 会拒绝加载该 bundle,并在启动时提示一次:
dsh: skipping profile bundle "dsh-web-fetch-fakeip": Error: Plugin
[email protected] is incompatible with dsh 0.2.0-rc.2: ...
此时 web_fetch 工具的表现与本插件存在之前完全一样——fake-ip DNS 下每个主机名
都以 resolves to a non-public IP address 失败。这不是崩溃:bundle 被跳过,原版
provider 接管了。
正确的修法是更新:
dsh plugin --profile web update dsh-web-fetch-fakeip
peer 范围采用的是有界区间(>=0.1.2-rc.1 <0.3.0-0),而不是逐一罗列已发布的
预发布版本,因此新的 0.2.x 或 0.2.0 正式版不会再触发该门禁。0.3.0 线则
有意仍会触发:那是一个尚未经过任何验证的兼容性声明,harness 要求明确决策是
对的。
也可以用 dsh plugin allow-version 授予精确版本豁免。那是"你确知接缝未变"时的
逃生通道,而不是修复方案。对本插件而言,诚实的答案就是更新——出问题的是声明,
不是代码。
示例配置
常见情形:mihomo/Clash 默认网段
无需任何配置——每个字段的默认值都与原版 provider 一致。随包提供的
cordis.patch.yml 就是这样做的:
- id: web-fetch-http
disabled: true
- insert:
- id: web-fetch-fakeip
name: 'dsh-web-fetch-fakeip'
显式写出的版本见
examples/mihomo-default.patch.yml。
非默认的 dns.fake-ip-range
把 fakeIpRanges 改成代理实际写入的网段,否则 fake-ip 主机名仍会失败:
- id: web-fetch-http
disabled: true
- insert:
- id: web-fetch-fakeip
name: 'dsh-web-fetch-fakeip'
config:
fakeIpRanges:
- 10.18.0.0/16
- 198.18.0.0/15
允许配置多个网段;只要一组应答中的每个地址都落在某个已配置网段内,该组应答
就被接受。这个例子加上收紧的传输限制见
examples/custom-range.patch.yml。
查询代理的实际网段
| 代理 | 配置键 |
|---|---|
| mihomo / Clash / Clash Verge | dns.fake-ip-range |
| sing-box | dns.fakeip.range |
对 GUI 客户端而言,实际生效值在生成的运行时配置里,而不是订阅配置里——到
客户端自己的配置目录中查找上表中的键,并确认 enhanced-mode 为 fake-ip:
dns:
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
scripts/diagnose.mjs 会直接读回 DNS 的实际应答,因此你不必找到那个文件也能知道
该配置什么。
配置项
每个字段的默认值都与原版 provider 一致,因此不带 config: 挂载本插件时,改变的
只有一件事:目的地如何判定。
| 字段 | 默认值 | 含义 |
|---|---|---|
fakeIpRanges |
['198.18.0.0/15'] |
可作为 fake-ip 应答接受的占位地址段 |
maxResponseBytes |
5000000 |
响应体最大字节数 |
maxBodyChars |
100000 |
解码后正文最大字符数 |
timeoutMs |
30000 |
抓取超时——资源兜底,不是工具预算 |
maxRedirects |
5 |
同源重定向最大跳数(0 表示不跟随) |
userAgent |
deepseek-harness/0.0.1 (+https://github.com/deepseek-ai) |
每个请求发送的 User-Agent |
非法值会在插件构造时直接报错,而不是构造出一个限制值荒谬的 provider——与原版 provider 一致。URL 长度上限固定为 2048 字符。
为什么默认是 198.18.0.0/15 而不是 198.18.0.1/16
mihomo 的文档默认值是 198.18.0.1/16,它是 198.18.0.0/15 的子集。较宽的
网段正是 RFC 2544 基准测试段整体,因此默认值同时覆盖两种写法以及部分配置使用的
相邻网段。若你希望更严格,收窄到 /16 是安全的。
改变与不改变的行为
| 目的地 | 行为 |
|---|---|
| 主机名,其应答全部是 fake-ip 占位地址 | 接受,原样固定连接;连接抵达 TUN 网卡,由代理解析真实源站 |
| 主机名,解析为公网 unicast | 接受,与原版 provider 完全一致 |
| 主机名,解析为内网 / 环回 / 保留地址 | 拒绝(WEB_BLOCKED_URL),与原版完全一致 |
| 主机名,应答混合(占位地址 + 任何真实地址) | 拒绝——绝不部分接受 |
IP 字面量且落在占位网段内(http://198.18.0.100/) |
拒绝——字面量本身指明了目的地,绝不走 fake-ip 路径 |
IP 字面量且为内网/环回/链路本地(127.0.0.1、192.168.1.1、169.254.169.254、10.0.0.1、[::1]) |
拒绝 |
IP 字面量且为公网(1.1.1.1) |
经正常校验路径接受 |
其余行为全部原样继承自原版 provider:仅同源重定向、字节与字符上限、
Content-Type 分类、按 Content-Type 头解码字符集、拒绝二进制类型与含凭据的
URL,以及相同的 WebError 错误码(WEB_INVALID_URL、WEB_BLOCKED_URL、
WEB_FETCH_TOO_LARGE、WEB_FETCH_TIMEOUT、WEB_REDIRECT_BLOCKED、
WEB_UNSUPPORTED_CONTENT_TYPE、WEB_ABORTED、WEB_PROVIDER_ERROR)。
诊断你的环境
先运行随包诊断脚本——它能把三种从报错信息看起来一样的情形区分开。作为 bundle 安装时,请通过包来解析其路径,这样无论 pnpm 把它放在哪里都能用:
# 在 profile 目录下执行(即安装了本 bundle 的那个 profile)。
node --input-type=module -e "import('dsh-web-fetch-fakeip/scripts/diagnose.mjs')"
也可以直接运行文件——在源码 checkout 里,或经由安装后的路径:
node scripts/diagnose.mjs
node scripts/diagnose.mjs example.com api.github.com
它会打印每个主机名的应答、其 ipaddr.js 归类,以及已配置网段是否覆盖,最后给出
结论:
- fake-ip 且已覆盖 —— 本插件就是正确的解法;若仍失败,问题在目的地判定之后 (代理路由)。
- 保留地址但未覆盖 —— 把
fakeIpRanges改成上面报告的网段。 - 未见 fake-ip —— 本机解析出的是真实地址,原版 provider 本就正确,本插件对 这些主机没有任何改变。
确认组合后的配置树里确实包含替换行:
dsh --profile web --dump-config | grep -B1 -A4 'fakeip\|web-fetch-http'
工作原理
HttpFetchProvider 的构造函数接受第二个参数——自定义 resolver,这就是全部的接缝。
本插件传入一个理解 fake-ip 的 resolver,其余一切交给原版类:
import { HttpFetchProvider, LOCAL_FETCH_PROVIDER_ID } from '@deepseek-ai/dsh-web-fetch-http'
const inner = new HttpFetchProvider(limits, createFakeIpResolver(ranges))
ctx.web.registerFetchProvider({
id: LOCAL_FETCH_PROVIDER_ID,
available: () => true,
fetch: (request, signal) => inner.fetch(request, signal),
})
resolver 对每组应答的判定:
- 解析一次(或直接采用字面量)。
- 拒绝畸形条目(
WEB_PROVIDER_ERROR)。 - 若主机名不是字面量,且每个应答都是落在已配置网段内的占位地址,则原样 接受该组应答。
- 否则要求每个应答都是公网 unicast,与原先完全一致。
第 3 步刻意做成全有或全无。占位地址本身不携带目的地,只有本机 TUN 网卡能路由它, 因此接受它并不会打开通往内网服务的通路。而混合应答会被拒绝,否则一条 DNS 应答就能靠夹带一个占位地址来放宽策略,把内网地址一并送进来。
源码导览
| 文件 | 职责 |
|---|---|
src/index.js |
插件入口:注册 provider,并再导出 API |
src/config.js |
配置 schema、与原版一致的默认值、限制值校验 |
src/resolver.js |
目的地策略:地址归类、网段匹配、resolver 判定 |
cordis.patch.yml |
bundle patch:禁用原版行,插入本行 |
scripts/diagnose.mjs |
报告 DNS 实际应答以及配置是否覆盖 |
scripts/check-package.mjs |
当发布包缺少必需文件或夹带禁止文件时失败 |
scripts/release-control.mjs |
发布门禁:标签/版本校验、npm 幂等检查、CHANGELOG 说明提取、GitHub Release 同步 |
test/resolver.test.js |
离线单元测试(不联网) |
test/compat.test.js |
用 harness 的兼容性门禁校验声明的 DSH peer 范围,并断言 apply() 在真实接缝上仍能注册 |
test/release-control.test.js |
发布门禁的离线测试 |
test/transport.live.js |
可选的真实网络测试 |
为什么必须禁用原版行
两个 provider 注册的是同一个 fetch-provider id(http),而 web 接缝会以
WEB_DUPLICATE_PROVIDER 拒绝重复注册。禁用该行并不会移除那个包——本插件正是从
它导入 HttpFetchProvider。沿用同一个 id 意味着 ctx.web.fetch() 在既有的
fetchProvider: http 配置下就会选中本 provider,无需改动 web 服务行。
开发
需要 Node 22+(在 Node 24 上开发)。
npm test # 离线单元测试——不联网、不查 DNS
npm run test:live # 可选的真实网络测试——需要可用的 DNS 与出网
npm run check # 语法检查 + 离线测试
npm run pack:check # 校验发布包内容
npm run verify # check + pack:check——发布工作流所依据的门禁
离线测试注入 resolver,因此不触碰网络即可断言目的地策略。真实网络测试同时断言 两件事:真实主机名即使解析到占位地址也能抓取成功,且每个内网目的地仍然被 拒绝。在没有 fake-ip DNS 的主机上,fake-ip 相关断言会自动跳过,因此该套件在任何 环境都有意义。
test/compat.test.js 覆盖的正是其他套件看不见的失效模式:它从 package.json
重新推导 harness 的兼容性判定——与启动时 evaluatePluginCompatibility 所做的是
同一个决策——因此,一个会导致整个 bundle 被跳过的 peer 范围会在这里、离线地失败,
而不是以"provider 静默消失"的形式暴露出来。它同时断言 CI 矩阵钉住的每个 DSH 版本
都满足声明的范围,这正是防止两者漂移的机制。
两套测试都驱动真实入口而非它的副本:apply() 会注册进真实的 Context +
WebRuntime(ctx.web),并通过 ctx.web.fetch() 发起抓取。因此注册层面的错误
——id 写错,或根本没注册——会让测试失败,而不是被手工搭建的 provider 桩掩盖过去。
devDependencies 跟踪本插件声明支持的最新 DSH 线(0.2.0-rc.2),因此本地套件
跑的就是当前 harness 实际携带的闭包。
pack:check 的存在理由:files 白名单写错在开发期是看不见的——整个工作树都在——
只有消费者安装之后才会暴露。本仓库已经踩过一次,因此该检查在每次 CI 都跑,
而不只在发布时跑。
在 profile 之外开发时,node_modules 需要能解析到 harness 包。把它指向安装的共享
依赖闭包:
# Windows(junction,无需管理员权限)
New-Item -ItemType Junction -Path node_modules -Target "$env:USERPROFILE\.dsh\profiles\node_modules"
# POSIX
ln -s "$HOME/.dsh/profiles/node_modules" node_modules
node_modules 已被 gitignore。
版本策略
插件的 minor 版本即它支持的 DSH 线:插件 0.M.x 支持 DSH 0.M.*,声明的
DSH peer 范围上界恒为 <0.(M+1).0-0。因此 0.2.0 声明
>=0.1.2-rc.1 <0.3.0-0,而 DSH 0.3 线将作为插件 0.3.0 采纳。完整理由见
CHANGELOG;test/compat.test.js 会从 manifest
自身的 minor 版本推导出期望上界,因此这条规则不会漂移。
由于范围是有界的而非开放式的,同一条 minor 线内的 DSH 补丁版或预发布无需 插件发布——但新的 DSH minor 线需要,且在它发布之前门禁会跳过本 bundle。 这是刻意的:采纳一条未经测试的线应当是一个明确决策。
持续集成
| 工作流 | 触发 | 用途 |
|---|---|---|
ci.yml |
推送到 main、pull request |
覆盖受支持 DSH 版本的测试矩阵、打包校验、真实网络通道 |
release.yml |
推送 v* 标签 |
经 Trusted Publishing(OIDC)发布到 npm,随后创建 GitHub Release |
测试矩阵显式钉住每个 DSH 版本,而不是用范围解析。@deepseek-ai/dsh-* 各子包
在 npm 上的 latest 是陈旧的(0.0.1-rc.x),尽管 @deepseek-ai/dsh 主包本身把
当前版本同时发到 latest 和 next——所以用范围会解析到错误的线。
发布流程与一次性的 npm 配置见 RELEASING.md。
安全说明
本插件之所以存在,是因为 fake-ip DNS 下原版校验无法被满足;因此值得追问的是这次 放宽的代价是什么。
放宽范围严格等于占位网段。 fake-ip 下由代理执行真实解析,因此上游可达性取决于 代理自身的路由规则,而非本模块。两个推论:
- 不要让代理规则把内网网段暴露给模型的抓取工具。若某条规则把 RFC1918 或环回 流量路由进代理,主机名就可能抵达内网服务——这个风险由代理配置引入,而非本插件, 但正是本插件让抓取得以成功,因此值得一并审计。
- IP 字面量永远不是占位地址。 它指明了调用方选定的目的地,把它交给本机代理
恰好会抵达校验想要挡住的环回或内网服务。这就是为什么
http://127.0.0.1/仍然被 拦截,而一个解析进占位网段的主机名却不会。
若需要更严格的姿态,把 fakeIpRanges 收窄到代理的确切网段(例如
198.18.0.0/16),而不是使用较宽的默认值。
已知限制
- 非默认网段必须手工配置。 插件无法自动探测代理的
dns.fake-ip-range;scripts/diagnose.mjs会告诉你该填什么。 - 仅解码文本内容 —— 继承自原版 provider。
text/html、application/xhtml+xml、text/*以及 JSON/XML 家族会被解码;缺少Content-Type或二进制类型会抛WEB_UNSUPPORTED_CONTENT_TYPE。 - 字符集仅取自
Content-Type头(默认 UTF-8)—— 同样是继承行为;HTML 中的<meta charset>声明会被忽略。 - 必须禁用原版 provider。 两个 provider 不能共用
http这个 id。 - 未来的 DSH 线需要更新 peer 范围。 声明的范围止于
0.3.0,因此0.3.x的 harness 会跳过本 bundle,直到范围被放宽。这是刻意的——未经测试的大版本线 应当是一个明确决策,而不是想当然——但这也意味着 DSH 升级后,web_fetch可能需要先更新插件才能恢复。