← Back to home@baldovinmarques391-design

dsh-web-fetch-vpn

修复 VPN 下 web_fetch 被误判非公网地址

Stars
0
Language
JavaScript
Created
Oct 17, 2026
Updated
Oct 17, 2026

Introduction

dsh-web-fetch-vpn

让 DSH 的 web_fetch 在开着 TUN / fake-IP 代理(Clash、Surge、sing-box 等)时照常工作。

现象

VPN 一开,web_fetch 对任何网址都报:

网页获取 · Error: URL hostname "api-docs.deepseek.com" resolves to a non-public IP address

关掉 VPN 立刻正常。web_search 不受影响(它走 DeepSeek API,不做本机地址校验)。

根因

@deepseek-ai/dsh-web-fetch-http 是一条有意的 SSRF 防护:先把域名解析一次,只要答案里出现非公网单播地址就整组拒绝,再把校验过的地址钉进连接,防止解析与连接之间被换地址。

TUN 模式 + fake-IP 的代理会拦截 DNS,对任何域名都返回 198.18.0.0/15 里的合成地址(例如 api-docs.deepseek.com → 198.18.0.240)。这个地址不是目的地,隧道会在下一次 TCP connect 时把它路由到真实服务器 —— 但它在地址分类上属于「保留段」,于是被上面的防护全部拒掉,浏览器能力在 VPN 期间整体消失。

做法

插件只替换「解析策略」这一步,其余(跳转、字节上限、编码解码、系统代理路由、错误码)仍然由官方 provider 负责:

情况处理
系统 DNS 返回的地址全是公网原样通过,和官方行为完全一致
答案全在 tunnelCidrs(默认 198.18.0.0/15)视为可连接:隧道才是这些地址的真实目的地
答案里混有真正的私网 / 回环 / 链路本地仍然拦截(防护没有被削弱)
答案是 0.0.0.0、被污染或被 DNS 广告过滤拦掉回退到 DNS-over-HTTPS,钉住真实公网地址
钉住 fake-IP 后连接失败(隧道没接管这个域名)对该主机强制 DoH 再重试一次,成功后 2 分钟内记住

它不注册新 provider,web.fetchProvider: http 保持不变 —— 因此关掉这个 bundle 就等于还原官方行为,不需要改任何配置。

模式

mode行为
auto(默认)接受隧道网段;连接失败时用 DoH 重试一次
passthrough接受隧道网段,但从不重试(失败即失败)
doh跳过系统 DNS,直接用 DoH 解析真实地址(最快,但会绕过内网 split-horizon DNS)
system完全保持官方规则,用于对比排查

配置

配置在插件自己的 cordis.patch.yml 里(也可以在 profile 的 cordis.patch.yml 里按 row id web-fetch-vpn 覆盖):

- insert:
    - id: web-fetch-vpn
      name: dsh-web-fetch-vpn
      config:
        mode: auto
        tunnelCidrs:
          - 198.18.0.0/15      # Clash / Surge / sing-box 默认 fake-IP 段
        dohEndpoints:
          - https://dns.alidns.com/resolve
          - https://doh.pub/dns-query
          - https://cloudflare-dns.com/dns-query
          - https://dns.google/resolve
        dohTimeoutMs: 4000
        retryWithDoh: true
        log: false             # true 会把每次解析决策打到实例控制台
字段默认说明
modeauto见上表
tunnelCidrs198.18.0.0/15代理用来合成答案的网段;不确定就看你代理的 fake-ip-range
dohEndpoints阿里 / 腾讯 / Cloudflare / Google按顺序尝试,第一个给出公网地址的胜出
dohTimeoutMs4000单个 DoH 查询预算
cacheTtlMs60000DoH 结果缓存,避免一次页面抓取打多次查询
preferDohfalsetrue = 所有域名都先走 DoH(等价于全局 doh 模式,但保留系统 DNS 兜底)
retryWithDohtrue连接失败后是否用 DoH 重试一次
logfalse解析决策日志

也可以用环境变量临时改:DSH_WEB_FETCH_VPN_MODE=system、DSH_WEB_FETCH_VPN_LOG=1。

验证

# 纯策略测试(不联网部分也能跑:加 --offline)
node test/selftest.mjs

# 端到端:同一台机器上先复现官方报错,再用本插件的策略真的抓下页面
node test/e2e-live.mjs https://api-docs.deepseek.com/

test/e2e-live.mjs 需要一个 DSH 运行时;用 DSH_RUNTIME 指过去,例如 $env:DSH_RUNTIME = 'D:\path\to\dsh\runtime'。

安装 / 卸载

安装就是「往 profile 里加一个 bundle」:

dsh plugin --profile web add <path-to-this-checkout>

也可以在插件页里安装/开关。装好即生效:base bundle 的 @deepseek-ai/dsh-hmr 监听 profile manifest,dsh.profile.bundles 一变就重合成并当场装载,不需要重启。

例外:改 package.json 的 exports / files(例如新增 locale/*.json)必须重启。 Node 按进程缓存 package.json 的 exports,运行中的实例看不到新增的导出;而插件页文案 走 app-boot 的 readPluginMeta(),它要靠 exports 才能解析到 locale/en.json。

卸载:把 bundle 从 dsh.profile.bundles 里去掉(或用插件页的开关关掉),保存即热生效;关掉即恢复官方行为,无需改其它配置。

边界

  • 不碰 web_search:搜索走 API,本来就不受 fake-IP 影响。
  • 系统代理模式(HTTP_PROXY/HTTPS_PROXY)本来就不做本机解析,插件不会介入。
  • doh / preferDoh 会用公网 DNS 的答案,可能绕过内网 split-horizon 域名;默认 auto 只在系统答案不可用时才这么做。
  • 纯 Node 内置模块实现(node:net 的 BlockList、node:dns、全局 fetch),没有 npm 依赖,也不锁定 DSH 版本;唯一依赖的运行时契约是官方 provider 实例上的 resolveAddresses 字段(探测不到就原样放过并记一条日志)。