# Security model

This note explains what the fake-IP fallback protects, what it assumes, and
where its limits are. The short version: the built-in guard stays in charge;
the plugin only answers a narrow question for names the guard already rejected.

## Assets

- **Local services**: loopback admin panels, development servers, databases on
  the host.
- **Private networks**: RFC 1918, CGNAT, link-local, ULA, and on-link IPv6.
- **Cloud metadata endpoints**: link-local and provider-specific addresses in
  the same private space.
- **The tool contract**: `web_fetch` should fetch public web content, not act as
  a network pivot.

The plugin does not hold credentials. Account handling belongs to other
plugins; see the Balance Pill project for that side of the stack.

## Adversary and failure model

The fallback can be influenced by:

1. **The requested hostname**, which may come from a model, a user, or a fetched
   page.
2. **The DNS answer**, which may be poisoned, split-horizon, or produced by a
   misconfigured forwarder.
3. **The plugin config**, which a user can edit, including a dangerously broad
   synthetic range.

The fallback is designed to fail closed for all three. It does not assume the
DNS answer is honest, and it does not trust a configured range blindly.

## Invariants

| # | Invariant | Enforced by |
| --- | --- | --- |
| 1 | The original validator always runs first. | `installFakeIpResolver` awaits `state.original`; the fallback is reachable only on `WEB_BLOCKED_URL`. |
| 2 | IP literals never reach the fallback. | `hostnameEligibility` classifies the hostname before any DNS call. |
| 3 | Local and non-DNS names never reach the fallback. | `localhost`, single-label names, and the reserved suffix list. |
| 4 | The complete answer set must be synthetic. | `resolveFakeIpAddresses` checks every A/AAAA entry; one real or unparsable entry refuses the set. |
| 5 | Synthetic answers must be inside a configured range. | `matchFakeRange` against `policy.cidrs`. |
| 6 | A configured range must not overlap special-use space. | `rangeOverlapsSpecialUse` at policy construction; unsafe entries are logged and excluded. |
| 7 | A refused destination keeps the provider's original error and code. | `explainBlockedAddress` appends a hint to the original `WebError`. |
| 8 | Cancellation stays cancellation. | The wrapper rethrows the abort reason instead of turning it into a policy failure. |
| 9 | The provider still pins the address set. | The fallback returns addresses; the provider's existing pin and TLS logic is untouched. |

## Why the special-use check is mandatory

Without it, the policy would trust the config as much as the DNS answer. A user
could write:

```yaml
cidrs:
  - 127.0.0.0/8
```

Then a hostile or careless DNS answer for `example.com` pointing at `127.0.0.1`
would be accepted, and `web_fetch` would connect to a loopback service. The
same applies to `10.0.0.0/8`, `169.254.0.0/16`, `fc00::/7`, `fe80::/10`, and
every other special-use block.

The check rejects a range if it **overlaps** a special-use block, not merely if
it is contained by one. `0.0.0.0/0`, `::/0`, and a range that happens to include
`127.0.0.1` are all refused. The default ranges (`198.18.0.0/15` and
`2001:2::/48`) are benchmark space and do not overlap the list.

An IPv4-mapped IPv6 range such as `::ffff:198.18.0.0/112` overlaps the mapped
prefix `::ffff:0:0/96` and is therefore refused. Use native IPv4/IPv6 ranges
for fake-IP blocks.

## Refused ranges

`SPECIAL_USE_CIDRS` in [`lib/policy.js`](../lib/policy.js) is the authoritative
list. It covers:

- IPv4: this-network, private, CGNAT, loopback, link-local, IETF protocol
  assignments, TEST-NET-1/2/3, 6to4 relay anycast, multicast, reserved, and the
  broadcast address.
- IPv6: unspecified/loopback/compatible, IPv4-mapped, NAT64, discard-only,
  Teredo, ORCHID, documentation, 6to4, multicast, unique-local, and link-local.

The list is intentionally broader than "private". Documentation and benchmark
ranges are not local, but accepting them as fake-IP blocks would make the
configuration ambiguous and could hide a misconfigured forwarder. The only
benchmark range the plugin accepts by default is the one fake-IP forwarders
actually use.

## What the plugin cannot protect

- **Forwarder policy.** A fake-IP forwarder can map a public name to a private
  destination inside its own routing rules. The plugin ensures the application
  only sends traffic to the synthetic address; it cannot inspect what the TUN
  does afterward. Audit the forwarder's rules separately.
- **Explicit proxy path.** When an HTTP(S) proxy is configured, the provider
  lets the proxy resolve the destination. The proxy's own SSRF policy applies;
  this plugin is not in that path.
- **A compromised local resolver.** If the resolver returns a synthetic address
  for a name and the TUN routes it to an internal service, the plugin cannot
  tell. That is the forwarder behaving as configured; the plugin's job is only
  to avoid accepting private answers directly.
- **Other HTTP clients.** `web_search`, model requests, MCP servers, and
  plugins with their own clients do not use this provider and are out of scope.
- **Provider internals.** The wrapper depends on `resolveAddresses` being an own
  property of the provider instance. A future Harness release could change it;
  the plugin fails closed (retries, then logs that it is inactive) rather than
  silently accepting everything.

## Fail-closed behavior

- Invalid config syntax → that entry is excluded; defaults apply if nothing
  safe remains.
- Unsafe config → the entry is excluded with a warning; defaults apply if
  nothing safe remains.
- No safe range at all → `apply` logs and returns without installing the
  wrapper.
- Provider not found within the retry window → logs and leaves the original
  guard active.
- DNS lookup failure, empty answer, mixed answer, local name, IP literal,
  cancellation → the original `WEB_BLOCKED_URL` error is rethrown (cancellation
  stays cancellation).

## Auditing the build

- `test/policy.test.mjs` covers IPv4/IPv6 parsing, CIDR overlap, special-use
  rejection, hostname eligibility, mixed answers, and abort propagation.
- `test/plugin.test.mjs` covers the wrapper install/uninstall lifecycle, the
  original-error passthrough, and the unsafe-config warning path.
- `test/manifest.test.mjs` checks that the shipped patch enables the policy and
  that the package carries the policy module.

The policy module has no imports, so the entire decision surface can be read in
one file.

## Reporting

Report a bypass privately through GitHub Security Advisories:

<https://github.com/Jayve/dsh-fakeip-http/security/advisories/new>

Please include the URL, DNS answer, config, Harness version, and observed
connection target. Do not include live credentials or private session text.
