Furst/furst-proxy/README.md
nak0x 097a1bfc31 Add furst-proxy, a filtering proxy that does not decrypt traffic
Gives every browser on the machine ad and tracker blocking, including
WebKitGTK ones like surf that have no extension mechanism and so cannot run
uBlock Origin. 80,002 rules from the StevenBlack list load in 82ms and sit
in 9MB resident.

A proxy receives CONNECT doubleclick.net:443 before any TLS handshake, so a
blocked host is refused without decrypting anything. Ads and trackers are
third-party hosts, which is why host-level refusal captures nearly all of
the weight while leaving traffic sealed: no certificate authority in the
trust store, no CA private key on disk, and no visibility into a bank
session this program merely relays.

What that cannot do is cosmetic filtering and first-party ads, which need
TLS interception. That is omitted deliberately, and the README records the
reasoning and where the seam would be, rather than leaving it looking like
an oversight.

  CONNECT to a blocked host    403, refused before the handshake
  CONNECT to anything else     tunnelled bytes, untouched
  plain HTTP to a blocked host 204, so a beacon looks empty not failed
  plain HTTP otherwise         forwarded and relayed

Matching is by domain suffix, so a rule for doubleclick.net covers
stats.g.doubleclick.net; lookups walk the labels of the requested host
rather than the list. Hosts files, bare domain lists and the ||domain^
subset of Adblock syntax are accepted, while rules needing response
inspection are skipped rather than half-applied. Allow rules win at any
depth, and IP addresses are never blocked, since hosts files are full of
them as addresses.

Lists cache for a week and fall back to a stale copy when a refresh fails.
Connections are capped at 96 with a Drop guard releasing the slot even on
panic, and only the loopback interface is bound.
2026-09-06 20:29:23 +02:00

117 lines
4.4 KiB
Markdown

# furst-proxy
A filtering HTTP proxy. Point any browser at it and ad and tracker hosts stop
resolving — including in `surf` and other WebKitGTK browsers, which have no
extension mechanism and therefore no uBlock Origin.
80,000 rules, ~9MB resident, 82ms to start.
## It does not decrypt your traffic
A proxy receives `CONNECT doubleclick.net:443` **before** any TLS handshake
happens. That is enough to refuse the connection, so blocking needs no
certificate authority in your trust store and no visibility into the contents
of anything — your bank session is a sealed tunnel this program relays without
being able to read.
Ads and trackers are third-party hosts, so refusing hosts is most of the win.
What it cannot do, by construction:
- hide elements cosmetically (uBlock's `##` rules)
- block ads served from the site's own domain
- filter inside an HTTPS response
Those need TLS interception. That is a deliberate omission, not a missing
feature — see [the note below](#what-tls-interception-would-add).
## Use
```sh
furst-proxy --init # write ~/.config/furst/proxy.toml
furst-proxy --update # fetch the blocklists
furst-proxy # run in the foreground
furst-proxy --test doubleclick.net # would this be blocked?
furst-proxy --env # shell exports
furst-proxy --pac ~/.furst.pac # proxy auto-config file
```
Point a browser at `127.0.0.1:8228` for both HTTP and HTTPS:
```sh
eval "$(furst-proxy --env)" # anything honouring http_proxy
surf https://example.com/ # WebKitGTK reads the environment
```
For Firefox, set the manual proxy in *Settings → Network Settings*, or load the
PAC file. Visit **http://furst.proxy/** through the proxy for a status page:
rules loaded, requests, share blocked, bytes relayed.
## What happens to a request
| | |
|---|---|
| `CONNECT` to a blocked host | `403`, refused before the handshake |
| `CONNECT` to anything else | tunnelled bytes, untouched |
| plain HTTP to a blocked host | `204 No Content` |
| plain HTTP to anything else | forwarded, relayed |
Blocked plain-HTTP requests get `204` rather than an error, so a blocked script
or beacon looks like an empty answer instead of a failure the page retries.
## Configuration
`~/.config/furst/proxy.toml`:
```toml
port = 8228
lists = ["https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"]
# Webfont and analytics CDNs: a webfont is bytes, a layout pass and a repaint
# for no information, which is a bad trade on an old CPU.
block_heavy = true
block = ["extra.tracker.example"]
allow = ["fonts.gstatic.com"] # wins over everything
```
Lists are cached under `$XDG_CACHE_HOME/furst/lists` for a week; `--update`
refreshes them. If a refresh fails, the stale copy is used — degraded filtering
beats none. Hosts files, bare domain lists, and the `||domain^` subset of
Adblock syntax are all accepted; rules that need response inspection
(`$third-party`, paths, wildcards) are skipped rather than half-applied.
Matching is by domain suffix, so `doubleclick.net` also covers
`stats.g.doubleclick.net`. Lookups walk the labels of the requested host, so
cost is the number of dots in it, not the size of the list.
## What TLS interception would add
A `--mitm` mode would generate a local CA, mint a certificate per host, and
decrypt each connection so responses could be filtered. It would buy cosmetic
filtering and first-party ad blocking.
It was left out because the trade is poor here:
- every HTTPS page would be decrypted by this program, so a bug in it becomes a
bug in the confidentiality of everything you browse
- a CA private key on disk is a credential worth stealing
- certificate minting and validation is where proxies get subtly wrong, and
getting it wrong silently downgrades security rather than breaking loudly
- `furst-serve` already renders most pages without a browser engine, so the
remaining first-party ads are seen rarely
If you want it anyway, the seam is `handle()` in `proxy.rs`: after accepting
`CONNECT` for an unblocked host, terminate TLS with a minted certificate
instead of tunnelling.
## Limits
- Only the loopback interface is bound. This is a personal proxy, not a
network service; do not expose it.
- Plain HTTP is relayed with `Connection: close`, so no keep-alive on that
path. HTTPS tunnels are unaffected.
- At most 96 concurrent connections, so a runaway page cannot exhaust a 4GB
machine. Beyond that the proxy answers `503`.