Development & Runtime
dsh-web-fetch-proxy
1499501762/dsh-web-fetch-proxy · ★ 1
DSH 插件:让内置 web_fetch 经由本地代理(Clash/Mihomo 等)出网,绕过 TUN fake-ip 假地址被 SSRF 防护拦截的问题。 / DSH plugin that routes the built-in web_fetch through a local proxy…
Manifest details
安装
dsh plugin --profile web add 'github:1499501762/dsh-web-fetch-proxy#9145719dcb041e496010bd7f6704b848e81ac41b'GitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试
For install and environment setup, plus plugin development tutorials, see DSH community docs ↗
README
Table of contents
A DSH plugin that routes the built-in
web_fetchthrough a local proxy, so TUN-mode fake-ip answers stop tripping the provider SSRF guard.
Symptom
On Windows with a TUN-mode proxy client running (Clash Verge / Mihomo, sing-box, Surge), every
web_fetch call fails:
ERROR URL hostname "raw.githubusercontent.com" resolves to a non-public IP address
DNS resolution succeeded. The address set was rejected by policy.
Root cause
- fake-ip DNS. Clash Verge Rev runs TUN mode with
dns.enhanced-mode: fake-ip,fake-ip-range: 28.0.0.1/8andfake-ip-range6: fdfe:dcba:9876::1/64. Every name resolves to a synthetic address, for example28.0.0.250andfdfe:dcba:9876::ed. - The SSRF guard.
@deepseek-ai/dsh-web-fetch-httpresolves the hostname itself and rejects the whole answer set if any address is not globally routable unicast.fdfe:dcba:9876::edsits infc00::/7, soipaddr.jsreportsuniqueLocaland throwsWEB_BLOCKED_URL. - The escape hatch the provider already has. When
@deepseek-ai/dsh-http-proxyreports a proxied route for the URL, the provider tunnels through it and lets the proxy resolve the hostname, skipping the local DNS check. That route only exists when the harness was launched withHTTP(S)_PROXYset.
A proxy plugin that only calls undici.setGlobalDispatcher() does not create that route: the
policy inside dsh-http-proxy stays empty, proxyRouteFor() keeps returning { proxied: false },
and web_fetch still resolves locally.
What this plugin does
At boot, inside the host process, it:
- finds a reachable local HTTP proxy;
- calls
installProxyFromEnvironment()from@deepseek-ai/dsh-http-proxywith a synthesized launch environment, installing a process-wide proxied policy; - verifies that
proxyRouteFor()now reportsproxied: true, and rolls back otherwise.
route before : direct (local DNS check applies)
fetch before : WEB_BLOCKED_URL URL hostname "raw.githubusercontent.com" resolves to a non-public IP address
route after : proxied via http://127.0.0.1:7897
fetch after : ok, 200, 1046 chars
Install
dsh plugin --profile web add github:1499501762/dsh-web-fetch-proxy
# or, from a local checkout:
dsh plugin --profile web add link:<absolute-path-to-this-package>
Restart DSH. A working boot logs:
[web-fetch-proxy] web_fetch now tunnels through http://127.0.0.1:7897 (source: clash-config:...); the local DNS check is bypassed.
Settings page
After a restart, Settings -> General shows a "Web Fetch proxy" row:
| Control | Effect |
|---|---|
| Auto-detect / Manual proxy / Off | Writes the proxy field of the web-fetch-proxy settings namespace; applies live, no restart |
| Proxy URL input | Manual mode; accepts http://127.0.0.1:7897 or 127.0.0.1:7897. The host validates and rejects bad values before they are stored |
| Bypass list | Appends noProxy entries; localhost, 127.0.0.1 and ::1 are always bypassed by dsh-http-proxy |
| Status line / Re-detect | Reads the host's live status (current route, source, failure reason) and can re-run discovery on demand |
The status line reports what the host actually did, for example:
Active http://127.0.0.1:7897 (via: clash-config:...)
It reads a fenced, loopback-only endpoint, POST /web-fetch-proxy/api
({"action":"status"} to read, {"action":"redetect"} to re-run discovery).
There is no "restart to apply" step: the settings namespace is
applies: live, and the host rebuilds the route fromscope.watch()- the previous policy is released first, then discovery and installation run again with the new configuration.
Configuration (cordis layer)
The page writes the same configuration. The config block in cordis.patch.yml supplies the
deployment baseline and defaults (the settings base layer), and carries the fields the page
does not expose:
- insert:
- id: web-fetch-proxy
name: dsh-web-fetch-proxy
config:
enabled: true # false disables the plugin entirely
proxy: auto # auto | off | http://host:port | host:port
noProxy: "" # extra bypass entries, comma separated
retryMs: 15000 # retry interval when nothing was found, 0 disables
maxRetries: 20
probeTimeoutMs: 400
DSH_WEB_FETCH_PROXY overrides auto-detection.
Discovery order
With proxy: auto, the first candidate that accepts a TCP connection wins:
DSH_WEB_FETCH_PROXYHTTPS_PROXY/HTTP_PROXY/ALL_PROXY(either casing)- the
mixed-portin a local Clash / Mihomo / Clash Verge config (8 known paths) - the Windows WinINET system proxy (
HKCU\...\Internet Settings) - a TCP probe of well-known ports: 7897, 7890, 7891, 7899, 10809, 10808, 1080, 2080, 20171, 8889
Coexistence with dsh-network-proxy
They do not conflict. Measured with both plugins mounted in one process:
| Situation | proxyRouteFor() |
web_fetch |
|---|---|---|
only dsh-network-proxy (direct mode) |
DIRECT | fails with WEB_BLOCKED_URL |
| plus this plugin | PROXIED 127.0.0.1:7897 |
200 |
dsh-network-proxy switched to manual via its settings watcher |
PROXIED | 200 |
dsh-network-proxy switched to direct (clears HTTP(S)_PROXY) |
PROXIED | 200 |
| three mode flips in a row | PROXIED | 200 every time |
web_fetchonly consults the policy and dispatcher held bydsh-http-proxy;setGlobalDispatcher()from another plugin cannot take it away.- Both share the undici global dispatcher (last writer wins), which affects plain
fetch()only. - Neither closes a dispatcher it did not create, so repeated mode changes are safe.
- The only oddity: the
direct/manual/systemsetting ofdsh-network-proxydoes not governweb_fetch. Point both at the same proxy to keep them consistent.
Safety
- Fail-open: no throw during module evaluation, none from
apply(), and no route is installed unless the proxy is actually accepting connections. - An existing host route is never overridden.
- Only
node:builtins are imported statically; harness packages are loaded through anchored, guarded imports (async for the proxy runtime, synchronous for the settings schema), so a harness layout change degrades the settings page instead of stopping the host from booting. - Installation is self-checked with
proxyRouteFor()and rolled back when it did not take.
Verify
npm test # unit and integration tests
npm run verify # live before/after comparison against a real URL
Coverage: proxy discovery, anchored host-module resolution, route lifecycle (including the
"a slow discovery must not overwrite a newer configuration" generation guard), the settings and
status surfaces, and a real HttpFetchProvider before/after integration test.
npm run verify runs the real HttpFetchProvider in a separate process and never touches the
running harness.
License
MIT © 2026 Tong317