dsh-fakeip-compat
中文 | English
Version 1.0.0 · verified against DeepSeek Harness
0.2.1-rc.5on both Desktop and CLI profiles.
A FakeIP-compatibility layer for web_fetch in DeepSeek Harness, with a General-settings row for its own configuration.
When a local DNS answers with placeholder addresses from a FakeIP range, the harness refuses the fetch before a single byte is sent:
Error: URL hostname "raw.githubusercontent.com" resolves to a non-public IP address
This plugin re-registers the http fetch provider with one extra rule ahead of that judgement, and can optionally re-check the real destination through DNS-over-HTTPS. Connection pinning, same-origin redirects, byte and character caps, and charset decoding stay exactly as the harness already does them.
The compat suffix is the point: nothing else changes. The provider id, the row id and the composition are the ones DeepSeek Harness already ships, so this drops into a profile that selects fetchProvider: http without touching anything else.
Table of contents
- The problem
- How it works
- Real-address verification
- Requirements
- Install
- Configure
- Verify
- Notes on DeepSeek Harness Desktop
- Troubleshooting
- Project layout
- License
The problem
@deepseek-ai/dsh-web-fetch-http resolves a hostname once and requires every answer to be a globally reachable unicast address (internally ipaddr.js's range() === 'unicast'). A FakeIP resolver answers with addresses from reserved or unique-local blocks, so every request is refused:
| Answer | ipaddr.js range |
Outcome |
|---|---|---|
198.18.0.0/15 |
reserved |
treated as non-public, rejected |
fc00::/18 |
uniqueLocal |
treated as non-public, rejected |
Any hostname resolving into such a range cannot be fetched by the agent at all.
How it works
One rule is inserted ahead of the stock judgement:
destination inside fakeipRanges -> allow
allowNonPublic === true -> allow (the check is skipped entirely)
anything else -> the unchanged public-unicast check
Private networks, loopback and the 169.254.169.254 metadata address stay blocked by default.
Design choices worth knowing about:
- Zero dependencies. IPv4/IPv6 parsing, CIDR matching and the non-public block table live in
lib/ip.js. A profile-installed bundle can only resolve its ownnode_modules, so the plugin must not need one. - The stock transport is reused when reachable. The resolver is passed as the second constructor argument of the harness's own
HttpFetchProvider, so pinning, redirects, caps and decoding are the shipped implementation rather than a copy. - A Node-native fallback exists. If the shipped module cannot be resolved, an equivalent
node:http/node:httpstransport takes over: pinning and caps still apply, the harness proxy router does not.runtime.implementationreports which one is live. - The shipped row is disabled, not replaced. The bundle patch turns off
web-fetch-httponly because two providers cannot share the idhttp.
Real-address verification
A FakeIP answer says nothing about the real destination: the resolver hands out a placeholder and the proxy resolves the name afterwards. A hostname pointed at a private network therefore arrives looking exactly like any other placeholder.
Turning on verification asks an independent JSON DoH endpoint for the real answer before the connection is made:
verifyRealAddress |
Behaviour |
|---|---|
off (default) |
no query at all, zero added latency |
warn |
query, log a non-public real answer, then allow the fetch |
enforce |
query, refuse the fetch when the real answer is non-public |
dohFailurePolicy decides what an unverifiable destination means while verification is on: allow (default) or deny. Only queries that produced no usable answer count as unverifiable; a query that answered with public addresses is a pass.
Why it is off by default. Verification is not free, and the honest tradeoffs are worth stating:
- The two DNS views can disagree. A split-horizon or policy-based resolver legitimately returns different answers than a public endpoint. In
enforcemode that disagreement becomes a failed fetch, soenforcesuits a home network whose only split is the proxy, not a policy-heavy deployment. - A failed query is not a verdict. Treating "could not ask" as "not allowed" turns a DNS outage into a broken
web_fetch, which is why the failure policy is separate and defaults toallow. - It leaks the browsing intent. Every verified hostname is visible to the DoH operator, which a user who set up a local resolver may specifically not want.
- Literal addresses are never verified. An IP literal is stated by the caller, and the main paths are already covered: literals are classified directly, and DNS rebinding cannot win because the connection is pinned to the address set that was validated.
So verification closes one specific gap — a name whose real answer is a private address — and pays for it in availability, latency and privacy. Off by default; turn it on when that gap is the thing you care about.
Requirements
| Item | Value |
|---|---|
| DeepSeek Harness | 0.2.1-rc.5 (verified), Desktop and CLI profiles alike |
| Node.js | 20 or newer |
| Composition | must mount web; the settings row additionally needs webServer and configEditor |
Without webServer or configEditor (for example a pure TUI profile) the fetch provider still works; the settings row reports that the configuration route is unavailable, and the row config is written by hand.
Install
plugin_manager { action: "install_bundle", target: "<absolute path to this repository>" }
Reinstalling over an existing copy is refused (already-installed); remove first, then install:
plugin_manager { action: "remove_bundle", target: "dsh-fakeip-compat" }
plugin_manager { action: "install_bundle", target: "<absolute path to this repository>" }
The bundle patch does two things:
- id: web-fetch-http # disabled only to avoid a duplicate "http" provider id
disabled: true
- insert:
- id: fakeip-compat
name: 'dsh-fakeip-compat'
config:
fakeipRanges: ['198.18.0.0/15']
allowNonPublic: false
verifyRealAddress: 'off'
A tarball install from GitHub works too: package.json records the canonical sources under dsh.install (tarball preferred), so the URL never has to be guessed.
Does it need a restart?
- Host half (fetch provider and configuration route): live as soon as the install reports
application: applied. A saved config reconciles the row through the Loader, so no restart is needed. - Client half (the settings row): the browser module graph is composed at startup. A first install, or any edit to
client.js, needs a restart before the row appears.
Configure
Settings → General → scroll to the bottom → FakeIP fetch compatibility:
- FakeIP ranges — a chip list. Type a CIDR block and press Enter to add it; duplicates are ignored,
×removes one. An empty list means every non-public destination stays blocked. - Allow every non-public destination — the
allowNonPubliccheckbox. - Real-address verification —
off/warn/enforce, plus the DoH endpoint and the failure policy. - Save — writes the row config into the profile's
cordis.patch.yml; the Loader reconciles the row and the provider reloads with the new settings.
Write path:
browser → GET|POST /ext/fakeip-compat/config
→ configEditor.edit(entry, …)
→ ~/.dsh/profiles/<profile>/cordis.patch.yml
→ Loader reconciles the row
→ provider reloads with the new config
Row config
| Field | Default | Meaning |
|---|---|---|
fakeipRanges |
['198.18.0.0/15'] |
CIDR blocks accepted as destinations. Multiple entries, IPv4 and IPv6 mixed. |
allowNonPublic |
false |
Drop the destination check entirely, private and loopback addresses included. |
verifyRealAddress |
'off' |
off / warn / enforce, as described above. |
dohEndpoint |
'https://dns.google/resolve' |
JSON DoH endpoint used while verification is on. HTTPS only. |
dohTimeoutMs |
4000 |
Per-query deadline for the DoH lookup. |
dohFailurePolicy |
'allow' |
What an unverifiable destination means while verification is on. |
maxResponseBytes |
5000000 |
Response body byte cap. |
maxBodyChars |
100000 |
Decoded character cap. |
timeoutMs |
30000 |
Fetch timeout. |
maxRedirects |
5 |
Same-origin redirect hop cap. |
userAgent |
deepseek-harness/… |
Request User-Agent. |
- id: fakeip-compat
name: 'dsh-fakeip-compat'
config:
fakeipRanges:
- '198.18.0.0/15'
- 'fc00::/18'
allowNonPublic: false
verifyRealAddress: 'warn'
dohEndpoint: 'https://dns.google/resolve'
dohFailurePolicy: 'allow'
Verify
1. Configuration route. Replace the port with the one the running GUI uses:
Invoke-WebRequest -Uri 'http://127.0.0.1:<PORT>/ext/fakeip-compat/config' -UseBasicParsing | Select-Object -ExpandProperty Content
Expect HTTP 200, rowId: fakeip-compat, the resolved config, and a runtime block:
{
"rowId": "fakeip-compat",
"config": { "fakeipRanges": ["198.18.0.0/15"], "allowNonPublic": false, "verifyRealAddress": "off" },
"runtime": {
"desktop": true,
"electron": "44.7.0",
"node": "24.21.0",
"dshVersion": "0.2.1-rc.5",
"implementation": "shipped-http",
"verifyRealAddress": "off",
"shippedPath": "…/dsh-web-fetch-http/lib/index.js",
"resolvedFrom": "…/dsh/package.json"
}
}
runtime is diagnostic only; it is deliberately not rendered in the settings row.
2. Settings row.
cordis_inspect_query { platform: "client", provider: "Slots", method: "listSubTree", input: { root: "settings.general.item" } }
occupants should contain { "id": "fakeip-compat", "order": 101, "active": true }.
3. Fetch. A URL that used to fail now returns a body:
web_fetch { url: "https://raw.githubusercontent.com/nodejs/node/main/README.md" }
4. Offline self-test. No DNS, no network:
node scripts/selftest.mjs
Covers CIDR boundaries (198.18.0.0 and 198.19.255.254), fc00::/18 inside and outside, ::ffff: mapping, private/loopback/metadata still refused, malformed CIDR rejected, config validation of every verification field, and URL policy.
Notes on DeepSeek Harness Desktop
Desktop is an Electron application, which changes three things for a profile-installed bundle:
| Item | Value |
|---|---|
| Harness tree | <...>/resources/app/dsh/ (inside app.asar for some distributions) |
| Profile directory | ~/.dsh/profiles/<profile> |
| Web GUI | http://127.0.0.1:<random port> — the port changes on every restart |
| Client bundle hot reload | none; editing client.js needs a restart |
The shipped provider is located by probing, in order:
- a bare
import('@deepseek-ai/dsh-web-fetch-http') process.resourcesPath/app/dsh/package.json(the Desktop main path)process.resourcesPath/app/package.json(packagedasarlayout)process.argv[1](CLI entry)dirname(process.execPath)/resources/app/dsh/package.jsonprocess.cwd()/package.json$DSH_PROFILE_DIR/package.json
If none resolves, the Node-native fallback takes over: fetching still works and the caps still apply, but the harness proxy router is bypassed. runtime.implementation tells the two apart.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
WEB_DUPLICATE_PROVIDER |
the web-fetch-http row was not disabled |
check - id: web-fetch-http / disabled: true in the patch |
still WEB_BLOCKED_URL |
the range is not in fakeipRanges |
the error names the address; add that block to the list |
WEB_BLOCKED_URL mentioning verifyRealAddress is 'enforce' |
the real answer is non-public and mode is enforce |
switch to warn, or drop that host from scope |
| every fetch fails once verification is on | the DoH endpoint is unreachable and dohFailurePolicy is deny |
fix the endpoint, or set the policy to allow |
row says the configuration route is unavailable, (HTTP 404) |
/ext/fakeip-compat/config was never registered: no webServer, or this row activated before it |
the plugin waits for webServer via ctx.inject; if it persists, restart DSH |
… (HTTP 503) |
no configEditor in the composition |
a TUI-only profile has no settings row; write cordis.patch.yml by hand |
… row is not present in this profile |
the row id is not fakeip-compat |
restore the default id |
save reports is overridden by a home patch or command-line overlay |
a higher-priority patch pins that row's config | edit the $DSH_HOME layer or the --patch overlay instead |
| the row never appears in Settings | the client half never entered the module graph | confirm every dsh.client.inject package exists in this version, then restart DSH |
runtime.implementation is node-native |
the shipped HttpFetchProvider was not resolved |
extend the probe list above; fetching works, proxy routing does not |
Project layout
dsh-fakeip-compat/
├── .github/workflows/ # CI: syntax check, offline self-test, manifest validation
├── package.json # name, version, main, exports, dsh.bundle + dsh.client
├── cordis.patch.yml # bundle patch: disable the shipped row, insert this one
├── client.js # Client half: window.__ModuleLoader__.load + settings.general.item
├── lib/
│ ├── index.js # Host half: provider, resolution probing, config route, runtime facts
│ ├── ip.js # IPv4/IPv6 parsing, CIDR matching, block table, classifier
│ ├── resolver.js # one resolve, per-answer verdict, optional verification, abort racing
│ ├── doh.js # optional JSON DoH verification of the real destination
│ ├── provider.js # node:http/https fallback transport
│ ├── config.js # DEFAULTS / normalizeConfig / ROW_ID
│ └── config-route.js # GET/POST /ext/fakeip-compat/config
├── locale/{en,zh}.json # Plugin Manager title and description
├── icon.svg # Plugin icon
├── scripts/selftest.mjs # Offline self-test
├── CHANGELOG.md # Release history
└── LICENSE
Uninstall
plugin_manager { action: "remove_bundle", target: "dsh-fakeip-compat" }
Removing the bundle drops the web-fetch-http override with it, so the shipped behaviour returns and FakeIP destinations are refused again.
No comments yet. Be the first to write one.