@logictan/dsh-fakeip-fetch
v0.1.2
Published
DSH web_fetch provider that works behind a fake-ip DNS proxy (Clash / sing-box). Narrows the upstream non-public-address guard to the configured reserved range instead of removing it.
Maintainers
Readme
@logictan/dsh-fakeip-fetch
让 DSH 的 web_fetch 在 fake-ip DNS 环境下可用。
它解决什么
本地 DNS 代理(Clash / sing-box 的 fake-ip 模式)会把每个外部域名都解析到同一个保留网段
(默认 198.18.0.0/15)。上游 @deepseek-ai/dsh-web-fetch-http 的安全守卫按地址形状判定,
把 198.18.0.0/15 归为 reserved(与 198.51.100.0/24 同类)并抛出:
Error: URL hostname "sing-box.sagernet.org" resolves to a non-public IP address结果是所有正常站点都无法抓取。但该 fake-ip 实际可达——curl --resolve <host>:443:<fake-ip>
返回 200,直连该地址同样 200。这是守卫的误判,不是真实的拦截。
它怎么修
收窄守卫,而不是移除它。 注册一个替换 provider:
- 先用上游原封不动的守卫尝试(公共单播地址);
- 仅当它抛出
WEB_BLOCKED_URL时,才用放宽后的 resolver 重试一次; - 放宽后的 resolver 只放行落在配置白名单内的地址,其余一律拒绝;
- 兜底也失败时,上报兜底自己的错误;只有兜底也以
WEB_BLOCKED_URL拒绝时, 才保留守卫的诊断。
因为严格路径优先,豁免只作用于真实守卫确实拒绝过的请求。回环、内网、链路本地与云元数据 地址在两条路径上都仍然被拒。放宽路径没有「公共单播」旁路:公共地址在严格路径上本就已被 接受,在兜底里再放行一次只会让守卫更宽。
安装
dsh plugin --profile <profile> add @logictan/dsh-fakeip-fetch本包自带 dsh.bundle.patch,安装即挂载。它的 patch 做两件事:
- 把上游
web-fetch-http行disabled: true; - 插入本插件的行
fakeip-fetch。
两者都必需:本插件故意注册上游自己的 provider id http,这样 dsh-base 里
web 行的 fetchProvider: http 一个字都不用改(本包因此永远不需要复述、也就不会随上游漂移
searchProvider 等配置)。同一个 id 注册两次会抛 WEB_DUPLICATE_PROVIDER,所以原行必须让位。
配置
| 字段 | 默认 | 说明 |
|---|---|---|
| allowedCidrs | ['198.18.0.0/15'] | 要豁免的 fake-ip 网段 |
| maxResponseBytes | 上游默认 | 透传 |
| maxBodyChars | 上游默认 | 透传 |
| timeoutMs | 上游默认 | 透传 |
| maxRedirects | 上游默认 | 透传 |
| userAgent | 上游默认 | 透传 |
传输限制直接展开上游 schema,因此默认值跟随上游,不抄进本包。
限制的校验同样归上游:本包的 patch 禁用了上游 web-fetch-http 行,那行的 apply 本来
负责校验这四个限制,所以 apply 会先把 config 交给上游的 apply 跑一遍,再丢弃它建出的
provider、只取校验结果。这不是洁癖——Node 会把超过 2^31-1 毫秒的定时器压成 1 毫秒,
未校验的 timeoutMs 会让每次抓取立即超时。委托上游可让规则与报错文本都留在上游手里。
改白名单:
- id: fakeip-fetch
name: '@logictan/dsh-fakeip-fetch'
config:
allowedCidrs:
- 198.18.0.0/15白名单条目必须是保留网段
allowedCidrs 的每个条目掩码后都必须完整落在一个 ipaddr.js 的 reserved 段内,否则
加载即失败:
| 条目 | 结果 |
|---|---|
| 198.18.0.0/15、192.0.2.0/24、240.0.0.0/4 | 接受 |
| 2001:db8::/32、2001::/23、3fff::/20 | 接受,但会削弱 NAT64 防护(见下)——实际部署不要配 IPv6 条目 |
| 198.18.0.0/5、192.0.2.7/1、192.0.0.0/1、240.0.0.0/1、2001:db8::/1 | 拒绝(掩码后越出 reserved,会覆盖真实内网/回环) |
| 0.0.0.0/0、::/0、0.0.0.0/8 | 拒绝(掩码后是 unspecified,不是 reserved) |
| 192.0.0.0/24、192.0.0.128/25 | 拒绝(reserved 但含 192.0.0.192 Oracle 云元数据、192.0.0.170/171 NAT64 探测) |
| 8.8.8.0/24、2606:4700::/32 | 拒绝(公共单播) |
| 10.0.0.0/8、192.168.0.0/16、127.0.0.0/8、169.254.0.0/16、fc00::/7 | 拒绝(真实内网) |
判据是掩码后的有效块,不是书写出来的 base:ipaddr.parseCIDR 不做掩码,所以
198.18.0.0/5 的 base 看着无害(落在默认 fake-ip 池里),实际覆盖 192.0.0.0/5,包含
192.168.0.0/16 整个私网段。少写一位(.0/5 而非 .0/15)就足以打开局域网,因此必须
先掩码再判定。
reserved 是必要条件,不是充分条件。 192.0.0.0/24 是 reserved,却装着 Oracle 云
实例元数据端点 192.0.0.192 与 NAT64/DNS64 探测地址 192.0.0.170/.171。因此含这些
地址的条目一律拒绝(192.0.0.0/25 只覆盖 .0–.127,不含 .192,仍然接受)。其余常见
元数据端点(169.254.169.254、100.100.100.200、169.254.0.23)本就落在所有 reserved
块之外,天然无法被列入。
拒绝真实内网是刻意的:豁免 10.0.0.0/8 等于让一个域名把抓取器指向局域网或云元数据端点。
安全边界(如实披露)
- 豁免的语义是「信任本机 TUN」。 resolver 只能看到 DNS 应答,无法证明该地址的可达性。 若本机 DNS 被恶意劫持,理论上可以把一个域名指向白名单网段再由 TUN 转进内网。用户自行运行 fake-ip TUN 已隐含接受这一类风险。
- 解析是全有或全无。 一个域名若同时返回 fake-ip 地址与真实内网地址,整体拒绝——攻击者 控制的记录集无法把内网目标夹带过豁免。
- 云元数据端点不可豁免。
reserved不等于安全:192.0.0.0/24是 reserved 却含 Oracle 云 元数据192.0.0.192。含已知元数据/NAT64 探测地址的allowedCidrs条目在加载时即被拒绝。 - 放宽路径只认白名单。 它不放行任何白名单之外的地址,公共单播也不例外。这是刻意的: 放宽路径只在严格路径已经拒绝之后才被走到,公共地址在那里本就被接受,再放行一次只会更宽。
- IPv6 白名单条目会削弱 NAT64 防护。 上游对 IPv6 应答有 RFC 6052 二次校验(把内嵌的
IPv4 取出再判一次),本插件注入的 resolver 会替换该步骤。放宽路径只认白名单,所以默认
白名单下 NAT64 地址(如
2001:4860:64::a9fe:a9fe,内嵌169.254.169.254)与上游一样被拒; 但若把 IPv6 保留段(如2001:db8::/32、2001::/23)写进allowedCidrs,其内嵌地址就会被 放行。实际部署不需要 IPv6 条目:fake-ip 段是 IPv4(198.18.0.0/15),而 Clash/sing-box 的 IPv6 fake-ip 段fc00::/18属 ULA,本就不在 reserved 内、无法被列入。只配 IPv4 条目即可。 - 解析结果为空或行形状不合法时报
WEB_PROVIDER_ERROR而非WEB_BLOCKED_URL,与上游resolvePublicAddresses的分界一致。这个区分是必需的:本插件只在WEB_BLOCKED_URL上重试, 把「什么都没解析到」当成策略拒绝只会把同一次空查询再跑一遍。 - 兜底失败时上报的是兜底自己的错误。 请求已经通过豁免解析、却在解析之后失败(跨域重定向、
超出体积上限、连接被拒)时,守卫的
non-public IP address与真实原因无关;照搬它会让 「插件没生效」和「站点抓不动」看起来一模一样,把排查引向一个本来正常工作的 resolver。 只有两条路径都以WEB_BLOCKED_URL拒绝同一目标时,豁免才确实不适用,此时保留守卫措辞。 - 上游的守卫仍是第一道防线;只有它拒绝后才走放宽路径。
开发
pnpm install # 在仓库根执行一次;ESM 的裸导入按文件位置解析,与 cwd 无关
node build.mjs # src/ → lib/(prepare / prepack 自动跑)
node --test tests/ # 在包目录内执行lib/ 不入版本控制。
测试与运行依赖 @deepseek-ai/dsh-web(取 WebError,错误码才不会被上游重写)、
@deepseek-ai/dsh-web-fetch-http、@deepseek-ai/schemastery 与 ipaddr.js,
由仓库根的 pnpm install 装到本包的 node_modules。ESM 的裸导入按文件位置解析,
所以换到任何目录、用绝对路径跑 node --test <包>/tests/ 都能找到依赖。
