title: "White Proxy" date: "2026-06-01" description: "A pure-Go, cross-platform proxy + scanner + DPI-desync suite for defeating TLS-based censorship."
White Proxy
A pure-Go, cross-platform proxy + scanner + DPI-desync suite for defeating TLS-based censorship.
This document is a whitepaper, not a man page. It explains the adversary we model, the network-level primitives we exploit, and the system design that turns those primitives into a single daemon you can point a browser at. The goal is that an engineer who has never seen the source can reconstruct the system from this text alone. The code is the authority on details; this is the authority on why.
White Proxy runs as a long-lived daemon that listens on 127.0.0.1:7080 and speaks SOCKS5 and HTTP-CONNECT on that one port. Behind it sit four cooperating engines:
- a routing engine that races traffic across a continuously re-verified pool of "clean" edge IPs and caches the winner per destination;
- an MMDF (MITM + Domain-Fronting) engine that terminates client TLS locally and re-handshakes outward under a clean front SNI;
- a raw-socket DPI-desync relay that injects kernel-rejected decoy ClientHellos to flip the censor's verdict; and
- a set of scanners that mine the clean IPs, SNIs, and proxies the other three consume.
Everything is one statically-linked binary. No Python, no sidecar, no pip install, no files beside the binary — the ASN databases, the default SNI config, and the sub-server UI are all go:embed-ed.
Table of contents
- The adversary (threat model)
- Circumvention primitives
- System architecture
- The routing engine
- MMDF — MITM + domain fronting
- The DPI-desync relay
- The scanners
- The subscription server
- Building and running
- Connecting clients
- Data, config, and on-disk state
- Measurement methodology
- Ethics and scope
1. The adversary (threat model)
Every design choice below follows from a small set of measured facts about how the censor's DPI is built. These were established by hundreds of controlled probes from a residential vantage inside the censored network (see §12). They have held across multiple regime shifts, so we treat them as architectural rather than incidental.
1.1 Two-lane network
Traffic to the open internet is policy-routed into one of two lanes:
- Lane A (the intercept lane). Destination prefixes the censor cares about are routed through a national-CDN ingress, past a DPI inspector, and out via a state-controlled egress pool (often a foreign transit tunnel). This lane completes TCP — you reach the destination — but the DPI watches the TLS handshake.
- Lane B (the default lane). Everything else has its SYNs dropped at the edge. The destination is simply unreachable.
The practical tell: a destination that is "TCP-unreachable" is on Lane B; one that is "TCP-OK but TLS-RST" is on Lane A and was decided against by the inspector.
1.2 The verdict is (destination-IP, SNI) — nothing else
On Lane A the DPI's allow/deny decision depends only on the destination IP and the SNI string in the TLS ClientHello. Cipher suites, extension order, GREASE, key shares, ALPN, TLS version, and post-quantum key shares are not inspected. This is why fingerprint mimicry (uTLS) is unnecessary for the verdict — though we still use realistic fingerprints for other reasons.
Two prefix policies coexist:
- tuple-strict — default-deny; only an exact
(IP, service-domain)pair is admitted (historically: Cloudflare customer prefixes, GitHub API edge). - allowlist-fleet — default-allow on the prefix minus a small explicit blocklist (historically: Google edge).
Under the current relaxed regime the two classes have partly merged — most mainstream SNIs now admit on formerly tuple-strict prefixes — but a political denylist (twitter.com, x.com, facebook, instagram, telegram.org, youtube.com, signal.org, pages.dev, …) is RST'd on every prefix.
1.3 The block mechanism is RST-injection mid-handshake
When the verdict is "deny," the censor injects a TCP RST during the TLS handshake — it does not silently drop. The RST's source TTL is consistent with a middlebox ~10 hops in (national-CDN intercept), not with the real destination.
1.4 No post-handshake re-inspection
This is the load-bearing fact. Once a TLS handshake completes, the censor stops inspecting the flow. The inner carrier protocol (HTTP/2, WebSocket, VMess, Trojan, gRPC) needs no further evasion. If you can get the handshake through once, the rest of the session rides free.
1.5 The path preserves TTL and forwards bad-checksum packets
- TTL is preserved end-to-end. NAT layers do not normalize it. A packet sent with TTL=6 expires at the DPI middlebox; a packet with TTL=250 reaches the destination. This makes TTL-staggered injection possible.
- Bad-checksum TCP segments leave the host and are dropped in-path, not repaired. The DPI sees them on the wire before the drop point. (On Linux you must disable NIC checksum offload —
ethtool -K <iface> tx-checksumming off— or the NIC fixes the checksum before it leaves the box.)
1.6 Other measured properties
- SNI matching is byte-equality against a lowercased allow-list, case-sensitive.
HCAPTCHA.COM≠hcaptcha.com. (A fingerprint, not a usable bypass.) - A present-but-empty SNI extension triggers a parser bailout (timeout), not a clean allow. An absent SNI extension is a valid ClientHello and is allowed.
- The censor reassembles TCP segments before matching — naive SNI fragmentation does not work.
- PSK resumption cannot replay a denylisted SNI — the RST lands before any session ticket is issued.
- DNS is hijacked selectively. Plaintext UDP/53 works for neutral domains but returns a sinkhole address (
10.10.34.x) for sensitive ones. Direct DoH (port 443 with the resolver's real SNI) is RST'd. Only fronted DoH survives (see §2.4).
1.7 The IP × SNI class model
Folding the above together yields the model the whole system is built around. Every foreign IP and every SNI falls into a class:
| IP class | Behavior | |---|---| | dead | TCP unreachable (Lane B). | | gray | Caps the connection after a small initial burst unless the SNI is whitelisted. Almost every foreign IP — including essentially all CDN anycast — is gray. | | clean | Not in the gray classifier; streams regardless of SNI. Vanishingly rare (≈1 per full prefix sweep; usually a stale prefix announcement the classifier missed). |
| SNI class | Behavior |
|---|---|
| white | On the censor's whitelist; whitens any gray connection (e.g. *.cloudflare.com, many cheaply-registered "nim-baha" domains). |
| black | RST'd at handshake (the political denylist). |
| grey | Neither; only serves on a clean IP (e.g. your own custom worker domain). |
The takeaway that drives the design: chasing clean IPs is a losing game (the clean × grey quadrant is structurally ~1-in-tens-of-thousands). The high-leverage move is SNI-side — pair any gray IP with a white SNI, or spoof a white SNI onto the wire so the censor whitens the flow. That is exactly what the desync relay and MMDF do.
2. Circumvention primitives
These are the network-level tricks the system composes. Each is a direct exploitation of a fact from §1.
2.1 Bare-IP TLS (omit the SNI extension)
The only universal carrier against a tuple-strict white-IP. Send a ClientHello with the SNI extension completely absent (not present-but-empty — that hits the parser-bailout timeout of §1.6). Servers accept it: Cloudflare's Universal SSL falls back to the prefix default cert, Google edges complete the handshake. In Go this is tls.Config{ServerName: ""} dialed to an IP literal. Useless on allowlist-fleet prefixes (where SNIs are allowed anyway) and on SNI-routed backends (you land on the wrong vhost).
2.2 bad_csum + cover-SNI (the workhorse spoof)
This is the primitive that makes denylisted destinations reachable. It exploits §1.3 ∧ §1.4 ∧ §1.5 simultaneously:
Client Censor DPI Destination
│ │ │
│─── TCP Handshake ──────┼────────────────────────>│ (Not spoofed)
│<── SYN-ACK ────────────┼────────────────────────│
│ │ │
│─── Decoy ClientHello ─>│ Cover SNI: hcaptcha │ Bad checksum
│ (bad checksum) │ Verdict: ALLOW │ (never reaches dest)
│ │ │
│─── Real ClientHello ───┼── (ignored, already >│ Blocked SNI
│ (real SNI) │ allowed) │
│<── ServerHello ────────┼────────────────────────│ Handshake complete
Per-attempt success is destination-dependent (≈90%+ on some Google edges, a few percent on busy CF anycast where the kernel's retransmit timer races the front's decode_error alert). Low rates are amortized by brute-force parallelism (§2.5).
Cover ClientHello size constraint: the decoy's TLS record must be ≤ the path MSS (~1460 B). Oversized covers (notably post-quantum Chrome with an ML-KEM key share, ~1751 B) are silently truncated on a raw-socket send, the censor sees a partial record, and the spoof fails 100%. Keep cover fingerprints under the MSS.
2.3 badcsum_ttl6 (checksum + low TTL)
A sharper variant of §2.2: make the decoy both bad-checksum and TTL=6. The TTL=6 ensures the decoy expires at the DPI middlebox (internal hop ≤6) rather than relying solely on the checksum drop. Raises the censor-flip rate toward 100% on stubborn CF anycast. Long post-decoy sleeps (>50 ms) are harmful — fire 3–5 decoys back-to-back, then the real ClientHello with no delay.
The TTL knob is orthogonal to the rejection method. Any (method, TTL) pair is valid; badcsum_ttl6 == (wrong_checksum, TTL=6).
2.4 Fronted DoH (the only working name resolution)
Because plaintext DNS is sinkholed and direct DoH is RST'd (§1.6), the system never uses the OS resolver. It resolves names by sending a DoH query to 1.1.1.1:443 wrapped in TLS with a spoofed www.microsoft.com SNI and an HTTP Host: cloudflare-dns.com header (with InsecureSkipVerify, since the cert won't match the fake SNI). The censor sees a benign Microsoft SNI; once past the SNI wall the genuine Cloudflare resolver answers truthfully. Bogon/sinkhole answers are dropped; clean answers are cached briefly. On total DoH failure the system errors rather than dial a poisoned address.
2.5 Brute-force parallelism
When per-attempt spoof rates are low, fire many connection attempts in parallel and let "first one through wins." §1.4 is what makes this safe to compose — a winner survives even though dozens of losing attempts were RST'd on adjacent 4-tuples.
2.6 MMDF fake-SNI fronting
A higher-level primitive built on §2.4. Instead of spoofing a decoy packet, terminate the client's TLS locally and open a fresh outbound TLS connection to the real destination backend (resolved via fronted DoH) but with the ServerName rewritten to a clean front (www.google.com, www.microsoft.com). The client's original Host: header travels inside the encrypted tunnel, so the backend serves the correct content while the censor only ever saw the clean front SNI. Detailed in §5.
3. System architecture
3.1 Hard rules (the sandbox)
The system is built on a few non-negotiable invariants. Reproducing them is reproducing the architecture:
- The daemon owns all long-lived state. The proxy listener, routing engine, MMDF CA + profile registry, scan job manager, relay process, sub server, and config snapshot all live in one daemon process. The TUI and CLI are thin clients holding zero authoritative state.
- All IPC is JSON-RPC 2.0 over an AF_UNIX socket (Linux/macOS native; Windows 10 1803+ supports AF_UNIX, so no named pipes). Streaming responses (event subscriptions, scan tails) are supported.
- Package boundaries are the API. Cross-component calls go through stable library packages; internal packages are toolchain-private.
- Scanners are libraries, not subprocesses. The daemon imports them directly and runs them as in-process pipelines — there is no stdin/JSONL wire protocol.
- One binary. A single executable dispatches to every subcommand based on
argv[1]. - Embed, don't ship beside. ASN DBs, default SNI config, and the sub-server HTML are baked in with
go:embed. - Cancellation is honored everywhere. Every long-running goroutine threads a
context.Contextand tears down child processes (relay, masscan, nmap) on cancel. - No telemetry, ever. This runs in hostile networks; nothing phones home.
3.2 The single binary and its subcommands
argv[1] selects an entry point. With no args, the TUI opens (auto-spawning a detached daemon if no socket answers).
| Subcommand | Purpose |
|---|---|
| daemon | The long-lived daemon (RPC over AF_UNIX). |
| tui | Interactive bubbletea TUI (default with no args). |
| ctl | kubectl-style client: status, down, config, events, route, scan, sub, relay. |
| scan | Proxy / SOCKS5 / HTTP discovery scanner. |
| whitescan | White-IP TLS-admit scanner. |
| snicheck / snicheck-tui | SNI accessibility probe (CLI / TUI). |
| routeverify | Long-running stdin/stdout route-verify worker. |
| relay | Standalone SNI-desync TCP relay. |
The non-daemon cmd/* entry points are packages exposing a Run() that the dispatcher calls by name — there is exactly one main().
3.3 Process topology
┌──────────────┐ ┌──────────────┐
│ TUI client │ │ ctl client │
└──────┬───────┘ └──────┬───────┘
│ JSON-RPC 2.0 over AF_UNIX
└───────────┬────────┘
▼
┌──────────────────────────────────────────┐
│ daemon │
│ ┌────────────┐ ┌─────────┐ ┌────────┐ │
│ │ proxy │ │ MMDF │ │ scan │ │
│ │ listener │ │ manager │ │ jobs │ │
│ │ :7080 │ └─────────┘ └────────┘ │
│ └─────┬──────┘ ┌─────────┐ ┌────────┐ │
│ ▼ │ sub srv │ │ relay │ │
│ ┌────────────┐ │ :7070 │ │ :7777 │ │
│ │ route │ └─────────┘ └────────┘ │
│ │ engine │ │
│ └────────────┘ │
└──────────────────────────────────────────┘
│ │ │
edge IP pool front edges raw sockets
(scan_*.json) (fronted DoH) (AF_PACKET/WinDivert)
A client request enters the proxy listener, which decides — per connection — whether to hand it to the MMDF manager (matched front domain), the route engine (everything else), or treat it as native. The desync relay is a separate listener (:7777) that clients can chain in front of their own VLESS/Trojan client; it is not in the default proxy path.
3.4 Per-user paths (XDG-style)
Nothing is cwd-relative. Three dirs, created lazily:
- Config (
~/.config/whiteproxy) — JSON config, MMDF profile overrides. - Cache (
~/.cache/whiteproxy) — the AF_UNIX socket, pidfile. - Data (
~/.local/share/whiteproxy) — scan results, IP caches, MMDF CA + leaf certs, decision logs.
4. The routing engine
The routing engine answers one question per destination host: which endpoint do I dial, and with what SNI? Its output is a Decision: either FamilyNative (dial the real host directly) or FamilyWhite (dial a specific ip:port from the pool, with a chosen SNI).
4.1 The decision pipeline
For each new host the engine evaluates, in order:
- Policy overrides — operator-set force-native / force-white / per-host IP bans.
- Prior match — a Clash-format
ir.txtrule list pins.irand other domains to native routing. - Cache hit — an L1/L2 cache (below). On hit, return the cached endpoint immediately (sticky per host).
- Scan-admit lookup — find candidate IPs from the scanned pool, in priority order:
- direct — IPs whose scan record admitted this exact host;
- related — IPs admitting a parent suffix;
- family — CDN siblings (e.g. any Google edge for a Google host);
- blind — a random sample of the pool.
- Wave-loop race (below) across the shortlist.
- First success wins and is promoted into the cache; total failure falls back to native.
4.2 The wave-loop race
Rather than fire the whole pool at once, the engine runs a small number of graduated waves (default 3). Each wave:
- takes a fresh shortlist (direct/family IPs first, then random blind candidates — default 24 per blind wave);
- dials each candidate with a short timeout (≈1.5 s connect, ≈1.5 s TLS), optionally followed by a lightweight HTTP probe (≈1 s) used as a geofence check;
- bounded concurrency (default 24), returns on the first verified winner (
K=1); - a TLS-level verification (cert chain, hostname, ALPN) gates a "winner" — a TCP connect alone is not enough.
Per-host race locks prevent two concurrent races for the same host. For geoblock-sensitive hosts the shortlist is restricted to IPs whose exit country (from the ASN DB + an exit-country probe) is known non-IR — an Iran-exit clean IP is useless for a geoblocked service.
4.3 The cache (the L1/L2 model)
A single sticky cache with escalating TTLs, so a destination that keeps working keeps getting cheaper:
| Tier | When set | TTL |
|---|---|---|
| Hot (L1) | immediately after a fresh race | ~60 s |
| Cold bump (L2) | each cache hit | +5 min, capped at 30 min |
| Promote | on a confirmed-good session close | +1 h, capped at 24 h |
| Negative | on MarkBad (a route failed in use) | evict the (host, endpoint) pair for ~90 s |
When a cached entry is within a refresh threshold (~4 s of expiry) of running out, the engine kicks off an async re-race so the next request never blocks. The cache is intentionally sticky per host — the same endpoint is reused until MarkBad evicts it, because stateful sessions break under per-request endpoint rotation.
4.4 Background liveness
A background sweep re-probes a random sample of cached (IP, SNI) pairs every few minutes and marks dead IPs out of the pool. Because of the ghost-cooldown effect (an IP that just served you may "starve" on re-dial for many minutes; see §1.7), a single-touch probe has a high false-alive rate — so confirmation matters more than raw speed here.
5. MMDF — MITM + domain fronting
MMDF (Man-in-the-Middle + Domain Fronting) is the engine for domains where IP routing alone fails but a clean front SNI succeeds. It is a native-Go TLS-MITM proxy — no xray sidecar — so the listener, cert chain, upstream dial, SNI substitution, and inner-Host rewriting all live in one process.
5.1 The CA and leaf certs
The user installs a persistent root CA into their browser trust store once. Per connection, MMDF mints a leaf certificate for the requested inner SNI on demand, signed by the root, sharing one in-process ECDSA private key; leaves are cached in memory and on disk for warm restarts. Clients verify the signature, not key identity, so per-SNI minting is cheap. Regenerating the CA breaks every client's trust — it must be stable.
5.2 The profile registry
A profile says which inner domains map to which front:
type Profile struct {
Name string
Domains []string // suffix match: host == d OR host endswith "."+d
FrontSNI string // outbound TLS SNI; "" = use the client's SNI
FrontHost string // host/IP to resolve for the dial; "" = use the inner host
ForceALPN []string // when set, only these ALPN values are offered outbound
Enabled bool
}
Profiles are matched by suffix, first-hit-wins, so ordering is load-bearing (specific domains before catch-alls; do not alphabetize). Two modes:
- fake-SNI (
FrontHost == "") — dial the client's real backend (resolved via fronted DoH) but rewrite the outbound SNI to a clean name. The leaf SAN check on the outbound is skipped (InsecureSkipVerify). This is what makes YouTube/Gemini/Instagram work: the real backend, keyed on the innerHost, serves the content, and the censor saw only the clean SNI. Most profiles use this. - true-front (
FrontHostset) — dial a known front edge that routes on the innerHost(used for shared edges like Fastly).
ForceALPN exists for backend pickiness, not censorship — some media backends are flaky on HTTP/2, so the outbound (and, mirrored, the client-facing inbound) is pinned to http/1.1. Skipping that mirror is a real bug: the browser offers [h2, http/1.1], picks h2, and an h2 preface hits an http/1.1-only backend → 400.
A representative default set: a google-video profile (*.googlevideo.com, front www.google.com, http/1.1, pinned edges), a broad google profile (front www.google.com, h2), a fastly true-front, and a meta profile (Facebook/WhatsApp/Instagram, front www.microsoft.com). The list is data: it ships embedded but can be replaced at runtime by dropping a mmdf_profiles.json override in the config dir and reloading — no rebuild.
5.3 Per-connection flow
- Client connects to the MMDF path (CONNECT-style).
- MMDF reads the client ClientHello and extracts the inner SNI.
- Suffix-match the inner SNI against the registry → a
Profile. - Mint/lookup a leaf cert for the inner SNI; terminate the client TLS with it.
- Resolve the dial target via fronted DoH (§2.4), never the OS resolver. fake-SNI resolves the real inner host; true-front resolves
FrontHost. - Open an outbound TLS handshake to the resolved IP with
ServerName = Profile.FrontSNI(andForceALPNif set). - Pipe plaintext between the two TLS sessions — the inner HTTP request,
Hostheader and all, rides inside the front's tunnel.
An excludes anti-match list (applied before registry lookup) lets specific hosts skip MMDF and fall back to IP-pin routing with their real SNI — kept for future regime needs.
6. The DPI-desync relay
The desync relay implements §2.2 / §2.3 as a standalone TCP proxy (default :7777). A client (your VLESS/Trojan client, or anything that speaks TLS) connects to it; the relay forwards bytes to a configured front IP, injecting decoy ClientHellos to flip the censor's verdict before replaying the real handshake. Putting the relay in front of a tunnel client both bypasses the SNI denylist on the first handshake and warms the censor's per-flow state so post-handshake shaping never engages (§1.4 / §1.7).
6.1 Per-connection sequence
- Accept the client TLS session.
- Dial the upstream front with a normal
connect()— the 3WHS is not spoofed. - Sniff the post-3WHS seq/ack off the wire (the decoy must look like a legitimate continuation).
- Inject N decoy ClientHellos (default 3): cover SNI = the admitted name for the front IP; rejection method + TTL as configured.
- Replay the real ClientHello immediately, no sleep.
- Race across pool members in parallel; first landed flow wins.
6.2 Rejection methods
The method decides how the decoy is kernel-rejected. Names mirror sing-box's spoof feature for config portability:
| Method | What's wrong on the decoy | Notes |
| --- | --- | --- |
| wrong_checksum | invalid TCP or IP checksum | The workhorse; requires NIC tx-checksum offload disabled on Linux or the NIC repairs it. |
| wrong_sequence | seq far outside the window | Cheap (no checksum recalc), reliably dropped on any interface. |
| wrong_acknowledgment | invalid ack | Symmetric variant. |
| wrong_md5_sig | bogus TCP-MD5 (RFC 2385) option | Silently path-dropped in the target network. |
| wrong_timestamp | malformed/out-of-window TCP timestamp | Robust where offload can't be disabled. |
The kernel's rejection is pure TCP-layer — win rates are flat across SNIs, confirming SNI content plays no role in the drop (only in the censor's verdict).
6.3 Per-platform mechanism
The sniff and inject backends are the only platform-specific code:
- Linux — sniff via
AF_PACKET(SOCK_RAW,ETH_P_ALL); inject viasocket(AF_INET, SOCK_RAW, IPPROTO_TCP)(the kernel writes the IP header). NeedsCAP_NET_RAW(root) and offload disabled. - Windows — sniff and inject via the WinDivert kernel driver at the WFP layer (modern Windows has no raw-socket bad-checksum path). Needs Administrator; the driver ships bundled.
- macOS — BPF (
/dev/bpf*) sniff + BSD raw socket inject (the user must write the IP header). Built but runtime-gated:Start/SelfTestreturnErrNotSupportedpending entitlement and offload-behavior validation. - All other platforms — a stub returns
ErrNotSupportedso the daemon'srelay.*RPCs compile everywhere.
A loopback self-test sweep measures per-method × per-SNI success so the relay can auto-pick the best method for the current network.
7. The scanners
Scanners are in-process library pipelines that mine the inputs the routing/MMDF/relay engines consume. They share a worker-pool engine with a cooperative pause gate, panic recovery, a stall watchdog, and a set of speed profiles (Throttled / Balanced / Aggressive / Turbo) that bound concurrency to the link. Results are written to siloed output files so one scanner's output never silently reclassifies another's.
| Scanner | What it does | Output |
| --- | --- | --- |
| white | White-IP admit scanner. Multi-stage: TCP liveness → uTLS cert probe → strict cert verify → honeypot filter → exit-country probe. | scan_*.json: {ip, port, sni, verdict ∈ accept/soft/reject/dead, exit_country} |
| proxy | Open-proxy discovery: TCP connect → SOCKS5/HTTP fingerprint → deep-validate (tunnel to a known endpoint, verify a real response). | {ip, port, protocol, latency, …} |
| sni | SNI accessibility probe: DNS pipeline + per-IP TLS/HTTP probing. | {sni, profile_tag, verdict, trail} |
| route | Route verification: re-checks an (IP, SNI) pair completes TLS. | {ip, port, sni, tls_ok, http_ok, latency} |
| speed | Latency + throughput over a found proxy. | SpeedResult |
| targets | Expands IP/CIDR/ASN scopes into concrete endpoints. | endpoint stream |
| cache | Persistent alive/dead IP cache with TTLs. | alive_cache.json |
| common | The shared connect/fingerprint/validate engine + worker-pool orchestration. | — |
A verify pass is mandatory for the white/route scanners because of ghost-cooldown (§4.4): single-touch alive rates overstate truth by ~100×.
8. The subscription server
An in-daemon HTTP server (default :7070) that publishes the scanned/verified endpoints as ready-to-import client subscriptions. It auto-loads the latest scan results on daemon start and serves:
GET /sub/v2ray— base64 VLESS listGET /sub/singbox— a full sing-box JSON configGET /sub/clash— Clash/Mihomo YAML (with dialer-proxy chaining)GET /api/status— JSON statsPOST /api/template— add/remove a config template (loopback-only)GET /— a minimal HTML landing page (embedded)
The store manages templates, the white-IP pool, and fallback proxies; all mutations round-trip through sub.* RPCs on the daemon.
9. Building and running
make # → build/whiteproxy (single binary)
make whiteproxy-linux # cross-builds
make whiteproxy-darwin
make whiteproxy-windows
make whiteproxy-all-os
make wp-test # tests / vet / fmt / tidy: wp-test|wp-vet|wp-fmt|wp-tidy
./build/whiteproxy # TUI (auto-spawns the daemon)
./build/whiteproxy daemon # daemon alone (default --proxy-listen=127.0.0.1:7080)
./build/whiteproxy daemon --proxy-listen="" # daemon, listener disabled
./build/whiteproxy ctl status # kubectl-style client
./build/whiteproxy ctl scan ... # status, down, events, scan.*, route.*, sub.*, relay.*, config.*
./build/whiteproxy help # full subcommand list
The TUI has two modes (toggle with x):
- white mode — tabs: Scan / Monitor / Routing / Sub / Inspect / Settings.
- desync mode (Linux/Windows live, macOS gated) — tabs: Control / Test / Relay.
Root/Administrator is needed only for the desync relay (raw sockets / WinDivert). Everything else runs unprivileged.
10. Connecting clients
The proxy multiplexes protocols on the single port :7080 by sniffing the client's first bytes:
| Client speaks | Configure as |
| --- | --- |
| SOCKS5 | socks5://127.0.0.1:7080 |
| HTTP CONNECT | http://127.0.0.1:7080 |
- V2Ray / Xray / sing-box — set the outbound to
socksorhttpat127.0.0.1:7080. White Proxy becomes the upstream that does the actual escape; your existing Worker/VLESS config is unchanged. - Browser — any standard HTTP/HTTPS/SOCKS5 proxy field; a per-profile Firefox proxy is the least invasive.
- Tunnel client + relay chain — point your VLESS/Trojan client's outbound at the desync relay (
127.0.0.1:7777) to get the spoof primitive in front of it (§6).
For MMDF to work, install the local CA (TUI MMDF pane, or proxy.mmdf.installCA) and restart the browser; without it, MMDF domains fall back to the routing path.
11. Data, config, and on-disk state
All persistent state lives under the per-user Data dir; config under the Config dir; the socket and pidfile under Cache. Key artifacts:
| Artifact | Purpose |
| --- | --- |
| scan_*.json | White-IP scan results — the routing pool. |
| alive_cache.json | Alive/dead IP cache with TTLs. |
| ir.txt | Clash-format prior pinning native-routed domains. |
| mmdf_ca.crt / mmdf_ca.key | The persistent MMDF root CA. |
| MMDF leaf cache | Per-SNI leaf certs (in-memory + on-disk). |
| mmdf_profiles.json (config dir) | Optional runtime profile override. |
| decisions-YYYY-MM-DD.jsonl | Per-connection routing decision log (tailed by the Monitor tab). |
Config is a JSON snapshot owned by the daemon; clients read and mutate it via config.* RPCs. Numeric fields are clamped to safe ranges on load so a hand-edited config can't put the engine in a degenerate state.
12. Measurement methodology
Every claim in §1 is empirical, not assumed. The reproducible method:
- Vantage matters absolutely. Probes must originate inside the censored network on the residential path — not a VPN, not a datacenter. A single probe on the wrong path corrupts the model. In this project the residential path is reached through a network namespace that pins probes to the residential interface while a VPN stays up on the host for unrelated traffic.
- Distinguish the lanes. Always separate "TCP unreachable" (Lane B) from "TCP-OK but TLS-RST" (Lane A verdict). They look similar to a browser and mean opposite things.
- Control your variables. To prove the verdict is
(IP, SNI)-only (§1.2), hold the IP fixed and sweep SNIs, then hold the SNI fixed and sweep fingerprints. To prove no post-handshake re-inspection (§1.4), complete a handshake via a spoof and then push sustained traffic. - Verify, don't trust a single touch. Ghost-cooldown (§4.4) makes single-shot results unreliable; every alive/clean claim needs a confirmation pass minutes later.
- Preflight on a neutral, known-reachable host before a sweep, and never use a hijacked anycast (e.g.
1.1.1.1) as a liveness oracle.
The project keeps an indexed research vault (research/) where each finding is a numbered experiment with a one-line conclusion and a link to its raw results; the cross-cutting state (NETWORK_STATE.md, INVARIANTS.md, PRIMITIVES.md) is rewritten as the regime shifts.
13. Ethics and scope
White Proxy exists to restore access for users whose open internet is filtered, throttled, or actively attacked. Use it on networks you own or are authorized to use. High-rate scanning of third-party infrastructure can trigger abuse complaints and may be illegal in some jurisdictions — understand your local rules before pointing a scanner at the public internet.
The MMDF engine performs a deliberate TLS interception of your own traffic on your own machine. Do not install the local CA on devices you do not own. The system contains no telemetry and never reports targets, IPs, or usage off-host — by design, because it is built for hostile-network contexts where such a leak is dangerous.