dsh-web-fetch-fakeip-allow
Verifieddsh-web-fetch-fakeip-allow · v0.2.0 · MIT
DSH host plugin: let web_fetch reach domains the local Clash/mihomo TUN resolves into its fake-ip pools (IPv4 198.18.0.0/15, IPv6 2001:2::/48), while every other non-public address stays blocked.
Install
dsh plugin add dsh-web-fetch-fakeip-allow Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Readme
description: "DSH 宿主插件:让 web_fetch 不再把 Clash/mihomo 的 fake-ip 占位地址(IPv4 198.18.0.0/15 + IPv6 2001:2::/48)当成内网地址拦掉,同时保持环回/私网/链路本地/IP 字面量照旧被拒。"
dsh-web-fetch-fakeip-allow
从社区安装 ·
dsh plugin --profile <profile> add dsh-web-fetch-fakeip-allow或从源码:dsh plugin --profile <profile> add github:mimajiushi/dsh-plugins#path:/plugins/dsh-web-fetch-fakeip-allow下文里的node scripts/install.mjs是源码检出时的本地安装:把包link:进你自己的 profile。
本机开着 Clash Verge(mihomo) 的 TUN + fake-ip,被代理域名在系统 DNS 里只回
198.18.x.x这类占位地址(实测docs.godotengine.org → 198.18.0.133、github.com → 198.18.0.62),真正的解析与连接由 mihomo 完成。浏览器照常打开,但 DSH 的web_fetch会在建连前做地址预检,判定"非公网地址"直接拒绝,报URL hostname "…" resolves to a non-public IP address(WEB_BLOCKED_URL)——请求根本没发出去。本插件只放宽这一处:整组解析结果都落在显式配置的 fake-ip 段内时放行,其余一律保持原样。
它做什么
- 保留宿主 provider 的全部行为:同源重定向策略、字节/字符上限、charset 解码、超时、错误码映射,一个字都没改。
- 只替换 provider 的
resolveAddresses:先问原解析器,只有它因地址策略拒绝、且重新解析得到的每一条答案都在配置的段内时,才接受这次解析。 - 公网域名、环回(
127.0.0.1)、RFC 1918、链路本地、CGNAT、URL 里的 IP 字面量:全部保持原判,抛出的还是 provider 自己那个错误对象。 - 两族占位地址都要有对应的段:解析器同时回 A 和 AAAA 占位地址时(本机 mihomo 开了
dns.ipv6,fake-ip-range6: 2001:2::0/64),只要有一条落在段外就整组拒绝——所以默认allow同时给出198.18.0.0/15和2001:2::/48。 - 不注册新 provider、不动
web_search、不碰 DSH 安装目录或app.asar、没有客户端 UI(不需要刷新页面)。
为什么是这个做法
@deepseek-ai/dsh-web-fetch-http 里,拦截发生在 resolvePublicAddresses():
if (!isPublicIpAddress(entry.address)) throw new WebError(`URL hostname "${hostname}" resolves to a non-public IP address`, "WEB_BLOCKED_URL");
同一个 requestOnce() 里还有一条产品自带的豁免:URL 走代理分支时(proxyRouteFor(url).proxied)跳过地址检查,因为"代理自己会解析域名"。也就是说宿主本来就承认"解析结果不代表最终目的地"这件事——只是它没把 fake-ip 这种本地代理占位地址考虑进去。
被否决的两个备选:
- 自己注册一个 fetch provider:组合树里
web行的 config 已写死fetchProvider: http,新 provider 不会被选中;要生效就得改那行 config(替换 config 必须重述searchProvider等字段,脆弱),而且等于把整套传输逻辑重写或偷出来——收益远小于风险。 - harness home
.env里写HTTPS_PROXY:确实能让 provider 走代理分支从而跳过检查,但会把整个宿主的出网(含模型 API)一起塞进 Clash。作为备选记在文末,不作为默认。
安装
cd plugins/dsh-web-fetch-fakeip-allow
node --test test/logic.test.mjs test/host.test.mjs # 单测(不需要 DSH)
node scripts/install.mjs --profile desktop # 安装 + 组合复核
脚本会:把 @deepseek-ai/schemastery 链接进本包的 node_modules → 备份一次 profile manifest → dsh plugin --profile desktop add link:<本目录> → 校验 dependency / bundles / composed 三行(composed 用 dsh --profile desktop --dump-config 复现宿主启动时的组合,并打印本插件的行与 config)。
然后重启 DSH Desktop(宿主插件在启动时装载;当前会话会保留)。
验证
- 正例:
web_fetch https://docs.godotengine.org/en/stable/tutorials/scripting/nodes_and_scene_instances.html→ 返回正文(安装前稳定报resolves to a non-public IP address)。 - 反例(必须仍被拒,错误文本不变):
web_fetch http://127.0.0.1:43120/、web_fetch http://192.168.199.1/。 - 日志说明:插件的 INFO 行不会落进
%APPDATA%\DSH Desktop\logs(实测该文件只记生命周期标记与错误,info 级 0 行),所以判定以行为为准——正例返回正文 + 反例仍被拒,就同时证明"补丁已生效"和"放宽范围没有扩大"。accepted fake-ip answer for <域名> -> 198.18.0.133这条 INFO 在宿主的 logger 通道上,是否可见取决于宿主的日志级别/输出端。
实测结果(重启 DSH Desktop 后,2026-09):
| 检查 | 结果 |
|---|---|
web_fetch https://docs.godotengine.org/en/stable/tutorials/scripting/nodes_and_scene_instances.html |
HTTP 200,返回页面正文 ✅(安装前:resolves to a non-public IP address) |
web_fetch http://127.0.0.1:43120/ |
仍 WEB_BLOCKED_URL ✅ |
web_fetch http://192.168.199.1/ |
仍 WEB_BLOCKED_URL ✅ |
复发记录:双栈 fake-ip(2026-09-25)
同一条错误又回来了,而且所有域名都失败(api.github.com、www.baidu.com、example.com 一个不落)——因为 mihomo 侧开了 IPv6 假地址,旧版插件只认 IPv4:
| 证据 | 值 |
|---|---|
| mihomo 运行时 dns 段 | enhanced-mode: fake-ip、fake-ip-range: 198.18.0.1/16、ipv6: true、fake-ip-range6: 2001:2::0/64 |
dns.lookup('api.github.com', {all:true}) |
198.18.0.38 (A) + 2001:2::24 (AAAA) |
| 宿主判定 | 逐条 isPublicIpAddress;2001:2::/48 在 ipaddr.js 里是 reserved → 整组拒绝 |
| 本插件(0.1.x) | 只认 family === 4,见到 AAAA 直接放弃 → 原错误重抛 |
修复(0.2.0):allow 同时接受 IPv6 CIDR,acceptFakeIpAnswerSet 按族逐条判定,整组仍在段内才放行。
排查这类问题的三步(下次直接照做):Resolve-DnsName <域名> 看是不是 198.18.x.x / 2001:2::x;node -e "require('dns').lookup('<域名>',{all:true,order:'verbatim'},console.log)" 看有没有 AAAA;拿这个答案集跑 acceptFakeIpAnswerSet(见 test/logic.test.mjs 里的双栈用例)确认是策略还是装载问题。
不重启也能自证:scripts/probe-provider.cjs 直接把 App 里那份 @deepseek-ai/dsh-web-fetch-http 从 app.asar 里 require 出来,先看裸 provider 拒绝,再挂上本插件的补丁看它被救回——全程走宿主自己的代码,没有 mock:
cd plugins/dsh-web-fetch-fakeip-allow
$env:ELECTRON_RUN_AS_NODE=1
& "D:\software\dsh_desktop\DSH Desktop\DSH Desktop.exe" scripts\probe-provider.cjs api.github.com
2026-09-25 实测(本机 mihomo 双栈 fake-ip):
api.github.com (want: rescue)
bare provider : refused [WEB_BLOCKED_URL] URL hostname "api.github.com" resolves to a non-public IP address
with patch : 198.18.0.38 (family 4), 2001:2::24 (family 6)
localhost / 192.168.199.1 : 挂补丁后仍 refused [WEB_BLOCKED_URL] ✅
注意:这里证明的是策略与补丁已经正确;补丁要在跑着的宿主里生效仍需重启 DSH Desktop(宿主插件在启动时装载)。
配置
行内 config 在 cordis.patch.yml(随包安装,改完需要重新组合/重启):
config:
enabled: true # false = 插件完全惰性(不用卸载)
allow: # 允许放行的 CIDR —— 这就是全部放宽范围(IPv4/IPv6 都收)
- 198.18.0.0/15 # mihomo/clash 的 fake-ip-range(IPv4 池)
- 2001:2::/48 # mihomo/clash 的 fake-ip-range6(IPv6 池;本机 pin 的是 2001:2::0/64,落在其中)
log: true # 每次放行打一行日志
profile 是 patchReload: live,所以不用重启也能用用户 patch 层按 id 覆写这行 config:
%USERPROFILE%\.dsh\profiles\desktop\cordis.patch.yml(或 harness home 的 ~/.dsh/cordis.patch.yml)里加
- id: web-fetch-fakeip-allow
config:
allow:
- 198.18.0.0/15
- 10.0.0.0/8 # 举例:只有你确认要走代理的段才加
回滚
node scripts/install.mjs --profile desktop --uninstall # 然后重启 DSH Desktop
或把 cordis.patch.yml 里的 enabled 改成 false 再重新组合。
风险与边界(明知故犯的部分)
- 本插件有意放宽了一道 SSRF 预检。放宽范围被限制为"整组解析结果都落在显式配置的段内",且默认只给
198.18.0.0/15(RFC 2544 保留段,Clash/mihomo、sing-box、Surge 的 fake-ip 池)。被放行的地址在 Clash 运行时由 TUN 持有、绑定到某个域名,并不通向任意内网服务;环回/私网/链路本地/CGNAT 依旧全拦。 - 混合答案集不放行:只要有一条答案落在段外(例如
198.18.0.133+127.0.0.1),整组拒绝,抛原错误。这一条在双栈下意味着:某个族没有配段,该族就是"段外"。 - IPv6 只在配了 IPv6 段时放行:默认带
2001:2::/48(RFC 5180 benchmarking,无真实目的地)。把 v6 段从allow删掉,带 AAAA 的答案集就会整组被拒——这正是 2026-09-25 的故障形态。 - URL 里的 IP 字面量走的是 provider 的另一条检查(
isNonPublicIpLiteral),本插件不碰它——所以反例 2 依旧被拒。 - 宿主升级风险:若未来版本改了 provider 的字段名,本插件只打一条 warn(
skipped (no replaceable resolver)),行为退回"照旧拦截",不会影响启动。
备选方案(零代码)
在 harness home 的 .env(%USERPROFILE%\.dsh\.env)写 HTTPS_PROXY=http://127.0.0.1:<Clash 混合端口>:provider 会走"代理分支"从而跳过地址检查。代价是宿主所有出网(含模型 API)都改走 Clash,且必须保持 Clash 常开。
文件结构
package.json 包清单(dsh.bundle.patch 指向 cordis.patch.yml;无客户端半)
cordis.patch.yml bundle 补丁:插入 web-fetch-fakeip-allow 行并携带 config
lib/logic.js 无第三方依赖的策略核心:IPv4/IPv6 CIDR 解析 / 答案集判定 / provider 与 registry 打补丁与还原
lib/index.js 宿主插件壳:name / inject / Config(schemastery) / apply
test/logic.test.mjs 策略核心单测(含双栈 fake-ip 用例,网络与 DNS 全部注入)
test/host.test.mjs 装配单测 + loader 契约(schemastery 链接后才跑,否则 skip)
scripts/install.mjs 安装 / 卸载 / dry-run / 组合复核
scripts/probe-provider.cjs 真实 provider 探针(Electron as-node 读 app.asar,不重启宿主即可自证)