White Proxy

A pure-Go, cross-platform proxy + scanner + DPI-desync suite for defeating TLS-based censorship.

During the 5-month internet blackout in Iran, White Proxy connected at least 500 Iranians to the open internet. This document is a whitepaper explaining the adversary model, the network-level primitives exploited, and the system design that turns those primitives into a single daemon you can point a browser at.


Table of Contents

  1. The Adversary (Threat Model)
  2. Circumvention Primitives
  3. System Architecture
  4. The Routing Engine
  5. MMDF — MITM + Domain Fronting
  6. The DPI-Desync Relay
  7. The Scanners
  8. The Subscription Server
  9. Building and Running
  10. Connecting Clients
  11. Data, Config, and On-Disk State
  12. Measurement Methodology
  13. 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.4Verified by 400+ long-lived sessions (1h+ p99) on every measured prefix — see §12 for the methodology. This is also why the relay's only job is the first handshake.

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.COMhcaptcha.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 ClassBehavior
deadTCP unreachable (Lane B).
grayCaps the connection after a small initial burst unless the SNI is whitelisted. Almost every foreign IP — including essentially all CDN anycast — is gray.
cleanNot in the gray classifier; streams regardless of SNI. Vanishingly rare (≈1 per full prefix sweep; usually a stale prefix announcement the classifier missed).
SNI ClassBehavior
whiteOn the censor's whitelist; whitens any gray connection (e.g. *.cloudflare.com, many cheaply-registered "nim-baha" domains).
blackRST'd at handshake (the political denylist).
greyNeither; 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:

  1. Open a normal TCP connection to the destination IP (the 3-way handshake is not spoofed).
  2. Sniff the post-handshake sequence/ack numbers off the wire.
  3. Inject one or more decoy TLS ClientHellos that look like a legitimate continuation of the flow, but are deliberately kernel-rejected — bad TCP/IP checksum (wrong_checksum), or a sequence number far outside the window (wrong_sequence), etc. The decoy carries a cover SNI: a value known to be admitted for this destination IP (e.g. www.hcaptcha.com on a CF customer prefix, www.google.com on a Google edge).
  4. The DPI sees the cover SNI on the wire and commits to "allow." The decoy never reaches the destination (the path drops the bad-checksum / out-of-window packet per §1.5).
  5. Send the real ClientHello with the actual (denylisted) SNI. The censor has already decided and does not re-inspect (§1.4). The handshake completes. §2.2Success rate on stubborn CF anycast: ~93% baseline, ~99% with badcsum_ttl6 (3–5 decoys, no inter-decoy sleep). The kernel's drop of the decoy is the mechanism; the DPI's verdict flip is the goal.

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. (Classic cross-domain fronting — inner Host ≠ outer SNI on the same edge — is dead at Cloudflare's modern edge, which enforces Host↔SNI consistency. The fake-SNI variant survives because we dial the real backend, not the front's own edge.)


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:

  1. 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.
  2. 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.
  3. Package boundaries are the API. Cross-component calls go through stable library packages; internal packages are toolchain-private.
  4. Scanners are libraries, not subprocesses. The daemon imports them directly and runs them as in-process pipelines — there is no stdin/JSONL wire protocol.
  5. One binary. A single executable dispatches to every subcommand based on argv[1].
  6. Embed, don't ship beside. ASN DBs, default SNI config, and the sub-server HTML are baked in with go:embed.
  7. Cancellation is honored everywhere. Every long-running goroutine threads a context.Context and tears down child processes (relay, masscan, nmap) on cancel.
  8. 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).

SubcommandPurpose
daemonThe long-lived daemon (RPC over AF_UNIX).
tuiInteractive bubbletea TUI (default with no args).
ctlkubectl-style client: status, down, config, events, route, scan, sub, relay.
scanProxy / SOCKS5 / HTTP discovery scanner.
whitescanWhite-IP TLS-admit scanner.
snicheck / snicheck-tuiSNI accessibility probe (CLI / TUI).
routeverifyLong-running stdin/stdout route-verify worker.
relayStandalone 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

Process topology and request routing
Clientbrowser, TUI, CLI, or tunnel client
WhiteProxy daemonowns config, RPC, scans, cache, CA, and listeners
per-connection decision

Default proxy path

Unmatched traffic flows through the route engine.

  1. Proxy listener
  2. Route engine
  3. Native / white endpoint

Fronting path

Matched profiles are handled by MMDF with rewritten SNI.

  1. Proxy listener
  2. MMDF manager
  3. Destination backend

Optional relay path

External tunnel clients may chain through the desync relay.

  1. Client tunnel
  2. Desync relay :7777
  3. Front IP

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:

  1. Policy overrides — operator-set force-native / force-white / per-host IP bans.
  2. Prior match — a Clash-format ir.txt rule list pins .ir and other domains to native routing.
  3. Cache hit — an L1/L2 cache (below). On hit, return the cached endpoint immediately (sticky per host).
  4. 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.
  5. Wave-loop race (below) across the shortlist.
  6. 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:

TierWhen SetTTL
Hot (L1)immediately after a fresh race~60 s
Cold bump (L2)each cache hit+5 min, capped at 30 min
Promoteon a confirmed-good session close+1 h, capped at 24 h
Negativeon 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 inner Host, serves the content, and the censor saw only the clean SNI. Most profiles use this.
  • true-front (FrontHost set) — dial a known front edge that routes on the inner Host (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

  1. Client connects to the MMDF path (CONNECT-style).
  2. MMDF reads the client ClientHello and extracts the inner SNI.
  3. Suffix-match the inner SNI against the registry → a Profile.
  4. Mint/lookup a leaf cert for the inner SNI; terminate the client TLS with it.
  5. Resolve the dial target via fronted DoH (§2.4), never the OS resolver. fake-SNI resolves the real inner host; true-front resolves FrontHost.
  6. Open an outbound TLS handshake to the resolved IP with ServerName = Profile.FrontSNI (and ForceALPN if set).
  7. Pipe plaintext between the two TLS sessions — the inner HTTP request, Host header 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

  1. Accept the client TLS session.
  2. Dial the upstream front with a normal connect() — the 3WHS is not spoofed.
  3. Sniff the post-3WHS seq/ack off the wire (the decoy must look like a legitimate continuation).
  4. Inject N decoy ClientHellos (default 3): cover SNI = the admitted name for the front IP; rejection method + TTL as configured.
  5. Replay the real ClientHello immediately, no sleep.
  6. Race across pool members in parallel; first landed flow wins.

The visualization below renders a small swarm of packets approaching the DPI filter (the vertical bar) from the left. In its default state the filter is blocking — packets on the decoy lane get rejected at the kernel per §6.2, and the real ClientHello is not yet through. Hover (or focus) the canvas: the cover-SNI decoys land, the DPI's verdict flips to "allow," and the real ClientHello passes through. This is the load-bearing trick — the DPI's verdict, not the kernel's drop, is what we are steering.

6.2 Rejection Methods

The method decides how the decoy is kernel-rejected. Names mirror sing-box's spoof feature for config portability:

MethodWhat's Wrong on the DecoyNotes
wrong_checksuminvalid TCP or IP checksumThe workhorse; requires NIC tx-checksum offload disabled on Linux or the NIC repairs it.
wrong_sequenceseq far outside the windowCheap (no checksum recalc), reliably dropped on any interface.
wrong_acknowledgmentinvalid ackSymmetric variant.
wrong_md5_sigbogus TCP-MD5 (RFC 2385) optionSilently path-dropped in the target network.
wrong_timestampmalformed/out-of-window TCP timestampRobust 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 via socket(AF_INET, SOCK_RAW, IPPROTO_TCP) (the kernel writes the IP header). Needs CAP_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/SelfTest return ErrNotSupported pending entitlement and offload-behavior validation.
  • All other platforms — a stub returns ErrNotSupported so the daemon's relay.* 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.

ScannerWhat It DoesOutput
whiteThe white-IP admit scanner. Stage 1 TCP liveness → stage 2 uTLS cert probe + cluster hint → stage 3 strict cert verify across an SNI panel → honeypot filter → exit-country probe for geoblock-sensitive admits.scan_*.json: {ip, port, sni, verdict ∈ accept/soft/reject/dead, exit_country}
proxyOpen-proxy discovery: TCP connect → SOCKS5/HTTP fingerprint → deep-validate (tunnel to a known endpoint, verify a real response, catch honeypots/MITM).{ip, port, protocol, latency, …}
sniSNI accessibility probe: a DNS pipeline plus per-IP TLS+HTTP probing, aggregated to a per-SNI verdict (accessible / blocked / challenge / honeypot).{sni, profile_tag, verdict, trail}
routeRoute verification: re-checks an (IP, SNI) pair completes TLS (and optional HTTP) exactly as the scanner saw — the engine's confirmation oracle.{ip, port, sni, tls_ok, http_ok, latency}
speedLatency + up/down throughput over a found proxy; flags Telegram/Psiphon capability.SpeedResult
targetsExpands ip / ip:port / CIDR / AS<n> scopes into concrete ip:port endpoints (capped; default port 443).endpoint stream
cachePersistent alive/dead IP cache with TTLs, so re-scans skip known-state IPs.alive_cache.json
commonThe 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 list;
  • GET /sub/singbox — a full sing-box JSON config;
  • GET /sub/clash — Clash/Mihomo YAML (with dialer-proxy chaining);
  • GET /api/status — JSON stats;
  • POST /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 SpeaksConfigure As
SOCKS5socks5://127.0.0.1:7080
HTTP CONNECThttp://127.0.0.1:7080
  • V2Ray / Xray / sing-box — set the outbound to socks or http at 127.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:

ArtifactPurpose
scan_*.jsonWhite-IP scan results — the routing pool.
alive_cache.jsonAlive/dead IP cache with TTLs.
ir.txtClash-format prior pinning native-routed domains.
mmdf_ca.crt / mmdf_ca.keyThe persistent MMDF root CA.
MMDF leaf cachePer-SNI leaf certs (in-memory + on-disk).
mmdf_profiles.json (config dir)Optional runtime profile override.
decisions-YYYY-MM-DD.jsonlPer-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.