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 会把每次解析决策打到实例控制台
| 字段 | 默认 | 说明 |
|---|---|---|
mode | auto | 见上表 |
tunnelCidrs | 198.18.0.0/15 | 代理用来合成答案的网段;不确定就看你代理的 fake-ip-range |
dohEndpoints | 阿里 / 腾讯 / Cloudflare / Google | 按顺序尝试,第一个给出公网地址的胜出 |
dohTimeoutMs | 4000 | 单个 DoH 查询预算 |
cacheTtlMs | 60000 | DoH 结果缓存,避免一次页面抓取打多次查询 |
preferDoh | false | true = 所有域名都先走 DoH(等价于全局 doh 模式,但保留系统 DNS 兜底) |
retryWithDoh | true | 连接失败后是否用 DoH 重试一次 |
log | false | 解析决策日志 |
也可以用环境变量临时改: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字段(探测不到就原样放过并记一条日志)。