Multi-protocol tunnel · Rust · nine transports

A tunnel that
changes shape
to get through.

Censors classify a connection from its opening handshake, within the first few hundred bytes. Spark treats that opening as the thing to get right, and gives it a repertoire deep enough that it never has to reuse one.

01 — the problem

The verdict arrives before you have said anything

A modern filter does not read your traffic. It looks at how a connection opens: the handshake, the server name, the client fingerprint, the size and timing of the first few packets. From that it assigns a category and applies a policy. For most traffic the opening verdict is the whole interaction, and it happens inside a kilobyte.

Resembling something beats resembling nothing

Traffic that looks like an ordinary allowed category gets through. Traffic that looks like no known category does not, regardless of what it actually contains.

Unclassifiable is now a category of its own

High-entropy traffic with no recognisable shape gets throttled or dropped on principle. No operator ever has to decide what it was.

So the opening is where the budget goes

Reshaping a whole stream is expensive and the cost scales with the traffic. Reshaping the opening costs a few hundred bytes once per connection, and the decision was made there anyway.

02 — the loop

The openings are searched for, not written

An opening is expressed as data: a parameter set describing what goes into the handshake, how it is framed into records, and how those bytes are timed onto the wire. Every value is a delta against a real browser handshake, so a strategy is a few integers rather than a code change, and it can be scored, signed and shipped without a client release.

01 PROPOSE Mutate candidates LLM + genetic search 02 PLAY Execute the genome two engines, one spec 03 SCORE Did it arrive? the server is the oracle 04 DEPLOY Sign and roll out parameters, not binaries WHAT REACHED A SERVER BECOMES THE NEXT GENERATION
Status. Stage 02 is built and verified. Stage 03 is partly built. Stages 01 and 04 run server-side and are not yet operating against the production fleet.
01

Propose

A genetic search, guided by a language model grounded in a corpus of documented techniques, generates candidate openings and reasons about why the last attempt failed instead of mutating at random.

Planned
02

Play

Two independent engines execute the same parameter set: a lean BoringSSL path on the Rust client, and uTLS on the established Go fleet. One strategy therefore reaches both populations.

Built
03

Score

A strategy counts as working when its traffic reaches a server. The arrival is itself the evidence, so there is no client telemetry to collect and nothing useful for a filter to forge.

In progress
04

Deploy

Winners ship as signed, versioned parameters. Because the censor keeps moving, the process has no end state, which is why it is a search rather than a configuration.

Planned
Where this actually stands. The parameter format, both execution engines, the wire-shaping layer and the fingerprint anchoring are built. A Spark handshake currently matches live Chrome on JA4, verified in CI. The outer loop that generates and scores strategies at fleet scale is server-side and is not yet running against production, and this page marks it that way throughout.

The full argument is in Opening Book →

03 — the data path

From an IP packet to a shaped handshake

Capturing a device's traffic yields raw IP packets. A transport needs a destination and a byte stream. Six stages close that gap, and each hands the next something narrower.

TUN IP packets NETSTACK TCP terminated locally FLOW stream + original intent ROUTER judges every flow PROXY through the pool, to an exit DIRECT protected socket, no tunnel PROXYLESS direct, but shaped REJECT ads, malware, phishing TRANSPORT nine, behind one interface THE OPENING the only adaptive stage Five of the six stages are deliberately unremarkable. A fault in any of them looks exactly like censorship from the outside, so each is proved on its own before they are wired together.
Read left to right, then down. The router's verdict decides whether a flow reaches a transport at all.

Rules match names, packets carry addresses

Built

By the time traffic reaches a tunnel the hostname is long gone. Spark answers DNS itself with synthetic addresses and recovers the name when the application connects to one, so routing decisions are made on what the user actually asked for.

Split tunnelling by application

Built

Per-app inclusion and exclusion, for the banking app that refuses to run over a VPN and the printer that has to stay reachable on the local network.

Two netstacks

In progress

A userspace TCP/IP stack that behaves identically everywhere, and a kernel-TCP path that hands termination to the operating system. The kernel path ships but stays off by default on every platform, each for a documented reason.

A pool that scores itself

Built

Transports are raced and ranked on the outcome of real flows rather than a synthetic probe, so a protocol that has started failing stops being selected without anyone intervening.

04 — the repertoire

Nine transports behind one interface

Everything below the router implements a single operation: dial a target, return a stream. A new wire protocol is therefore a new implementation rather than a change to the client, which is what lets the repertoire grow.

AnyTLSMultiplexed streams inside a TLS session shaped to match a real browser, with a Chrome profile verified against live Chrome on JA4.Built
Hysteria2QUIC, with congestion control tuned for lossy and heavily shaped links. The right answer when a path is bad rather than blocked.Built
ShadowsocksThe widely deployed AEAD protocol, for interoperating with existing server fleets.Built
ProxylessNo proxy and no exit hop. Un-poisoned resolution plus a shaped handshake, which defeats the block on the direct path and consumes no server bandwidth.Built
DNS tunnelA clean-slate design for networks where little else escapes. Intentionally not wire-compatible with existing DNS tunnels, whose signatures are already well known.Built
Fronted meekDomain-fronted HTTPS over infrastructure that is costly to block wholesale.Built
SamizdatHTTP/2 multiplexing with session-id injection, sharing the same handshake-shaping layer as the others.Built
WASM modulesSandboxed guest transports delivered as signed, version-floored artifacts. A new obfuscation without a client release, and contributable from outside the team.Built
BIP324Bitcoin's encrypted transport as a carrier, for traffic that resembles participation in a network few operators want to be seen blocking.Built
05 — posture

Privilege, secrets and logs

People run this on networks that are actively hostile and on devices that may be inspected. A log line can be evidence. The design treats that as a constraint on the software rather than as advice to the user.

PRIVILEGED SERVICE owns the tunnel device and the routing table netstack transports PACKETS NEVER LEAVE THIS PROCESS UNPRIVILEGED CLIENT the interface, with no special powers UI may ask for status may not read secrets CONTROL PLANE commands · status · logs a few messages a second PRIVILEGE BOUNDARY
One descriptor crosses, once. On Linux the tunnel file descriptor can be handed over at startup so the code parsing hostile packets need not run as root. Per-packet data never crosses at all.

Destinations stay out of logs

Built

Hostnames and destination addresses are redacted unless an explicit debug flag is set. This is enforced in the logging layer rather than left to whoever writes the next log statement.

Secrets travel one way

Built

Credentials are stored by the privileged side and never sent back over the control channel. The interface can ask whether a profile is configured. It cannot ask what the key is.

The caller is authenticated

Built

Connecting to the service is not the same as being allowed to command it. Peer credentials on Linux, a pipe security descriptor on Windows, a code-signing requirement on macOS.

Signed modules with a version floor

Built

Guest transports carry a signature and a minimum version. Signing alone would still let an attacker serve an older module with a known weakness, so the floor is the part doing the work.

Size is a constraint, not an afterthought

Built

Downloaded on metered connections and sometimes passed between phones by hand, so the build is tuned for size and the release profile is optimised for it. That budget also constrains the dependency tree, which is most of what keeps the attack surface reviewable.

No C in the default build

Built

Pure Rust unless a feature is explicitly enabled. The two C dependencies that exist are gated off by default, so the shipping base build does not contain them.

06 — reach

Five platforms, one core

Each operating system hands over packets differently and disagrees about who may ask for them. Only the first stage of the data path changes; the netstack, transports and routing are the same code everywhere.

LinuxKernel tun behind a privileged service, with the option of passing the descriptor to a lower-privileged process.Built
macOSA system extension owns the device. Ships signed and notarised.Built
AndroidA genuine Linux tun behind VpnService, with upstream sockets explicitly protected so they do not route back into the tunnel.Built
iOSA packet-tunnel provider rather than a kernel device, under a memory ceiling that sizes every buffer in the system. Shipping to TestFlight and in regular use.Built
WindowsWinTun, the named-pipe control transport and the service handler all build and package, and the whole workspace cross-compiles. Not yet run on a Windows host.In progress
07 — documentation

The detail behind all of this

Everything above is a summary. These go all the way down, written from the implementation, and they include the measurements that came out the wrong way.

Opening Book

Theory

Why a filter's decision lands in the opening of a flow, and what follows from treating a handshake as a repertoire instead of a fixed protocol.

the argument →

The spine

Architecture

The six stages in full, both netstacks and why neither wins outright, and the one line the design never crosses.

tun → netstack → flow → router → transport → wire

Five kinds of DNS

Resolution

DNS appears five separate times in the client. Conflating any two of them produces a resolution loop that deadlocks on the first query.

bootstrap · fake-IP · per-flow · search axis · transport

The control plane

Processes

What crosses the privilege boundary, what never does, and why the tunnel keeps forwarding when the interface dies.

pass the descriptor, not the packets

Write a module

Contribute

The guest ABI in full: four exports, the calling convention, the host crypto menu, resource limits and a worked example. A repertoire only grows if people outside the team can add to it.

contributor guide →
08 — status

What is true today

Spark is under active development. The tunnel works end to end on desktop and Android and carries real traffic. The parts that do not exist yet are marked as such here rather than described in the present tense.

Core tunnelTUN capture, both netstacks, TCP and UDP including DNS, and the transport pool with racing and health scoring.Built
Handshake shapingThe signed parameter format, both execution engines, record framing and wire timing, and browser fingerprint parity verified in CI.Built
Discovery loopThe outer loop that generates, scores and deploys strategies at fleet scale. Server-side, designed and partly built, not yet running against production.Planned
Kernel netstack defaultShips and is selectable, off by default everywhere. Each platform has a specific unmet condition, and on iOS the reason is permanent.In progress
Public sourceThe contributor guide is written and the ABI documented, but the repository is not open yet, so the guide currently describes a contract nobody outside the team can clone.Planned