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.
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.
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.
High-entropy traffic with no recognisable shape gets throttled or dropped on principle. No operator ever has to decide what it was.
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.
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.
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.
PlannedTwo 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.
BuiltA 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 progressWinners 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.
PlannedCapturing 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 six stages in full, both netstacks and why neither wins outright, and the one line the design never crosses.
DNS appears five separate times in the client. Conflating any two of them produces a resolution loop that deadlocks on the first query.
What crosses the privilege boundary, what never does, and why the tunnel keeps forwarding when the interface dies.
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.
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.