Kindling

Kindling

Get the first bytes through a censored network.

Kindling is a Go library that fetches small things, like config files, through firewalls that block everything you'd normally try. It races several circumvention techniques at once and hands your request to whichever connects first. You get back an ordinary http.Client.

One request, every route at once

Domain fronting, proxyless dialing and the AMP cache start connecting at once, and the first to connect carries the request. The Soar DNS tunnel starts only if all three fail.

Illustrative timings.

● The bootstrap problem

Before a VPN can connect, it has to phone home

Every circumvention app starts the same way: it needs a config file, a list of servers, or a login check before it can do anything else. On a censored network that first request is the easiest one to block, because it goes to a known address before any tunnel is up.

Kindling exists for that one request. It isn't a VPN and doesn't carry your traffic. It gets a few kilobytes in and out reliably, so the app that uses it can get going.

  1. App opensNo servers known yet, nothing cached on a fresh install.
  2. Fetch config · kindlingRaced over every technique that might work on this network.
  3. Pick a proxyNow the app knows where its servers are.
  4. Tunnel upNormal traffic flows through the app's own protocols.
● Transports

Four ways through, and room for yours

Each technique beats a different kind of blocking. None works everywhere, which is why kindling runs them together.

domainfrontTier 1

Domain fronting

Connects to a large CDN under an innocuous server name, then asks for the real origin inside the encrypted request. Blocking it means blocking the CDN. Kindling only counts a front as connected after a real TLS handshake, so a dead front can't win the race.

streaming okconfig refreshed remotely
smartTier 1

Proxyless dialing

The Outline SDK smart dialer connects straight to the destination and searches for a trick that gets past DNS and SNI blocking:

  • DNS over HTTPS through a dozen resolvers
  • TCP stream splitting and out-of-order segments
  • TLS record fragmentation
ampTier 1

AMP cache

Rides Google's AMP cache, a technique introduced by David Fifield, with Lantern's implementation. Censors that need Google can't block it cleanly. Request bodies are capped, so kindling routes only small requests here.

bodies ≤ 6,000 bytesno streaming
soarLast resort

Soar DNS tunnel

Soar carries the request inside DNS queries through whatever resolvers the network allows, including in Russia, where UDP is intercepted, and in China, where forged answers are injected. It's slower than the others, so kindling dials it only after every tier 1 transport has failed.

works when only DNS doesconnects in under 1 s
WithTransport(…)Tier 1 by default

Your transport

Anything that can return an http.RoundTripper can join the race. Implement the Transport interface, returning 0 from MaxLength() and RequestTimeout() for no cap and the default timeout, and add an optional Priority() int to race in a later tier. Kindling handles the racing, retries and timeouts.

● How the race works

Race the connections, send the request once

Filter

Transports that can't carry this request are skipped: a body over a transport's size cap, or a streaming request on a transport that can't stream.

Race a tier

Every transport in the first tier starts connecting at once. Transports pre-connect, so the race measures real reachability on this network.

Send once

The first transport to connect gets the request. Requests are sent one at a time, never in parallel, so a POST can't hit the server twice.

Fall back

If the whole tier fails, the next tier starts. That's how Soar stays out of the way on networks where faster routes work.

RequestConnection failsTransport error, 5xx or 403Other 4xx
GET, HEADnext transportnext transportreturned as-is
POST, PUT, DELETE…next transportreturned as-isreturned as-is
with X-Kindling-Idempotentnext transportnext transportreturned as-is
A connection that never came up carried no body, so it's always safe to try the next one. Once a body has crossed the wire, kindling replays it only when the method is safe or you set X-Kindling-Idempotent. A 403 counts as a failure because CDN fronts often send one when they refuse a country on one path only.
● Code

An http.Client that finds its own way out

go get github.com/getlantern/kindling, pick your transports, and use the client like any other.

Using kindling
k, err := kindling.NewKindling("myapp",
	kindling.WithDomainFronting(df),
	kindling.WithProxyless("raw.githubusercontent.com"),
	kindling.WithAMPCache(ampClient),
	kindling.WithDNSTunnel(soarClient), // last resort
)
if err != nil {
	return err
}

client := k.NewHTTPClient()
resp, err := client.Get(configURL)
if err != nil {
	return err
}
defer resp.Body.Close()
Bring your own
type Transport interface {
	// Return a pre-connected RoundTripper.
	NewRoundTripper(ctx context.Context,
		addr string) (http.RoundTripper, error)
	MaxLength() int                // max body, 0 = no limit
	IsStreamable() bool            // can carry text/event-stream
	Name() string
	RequestTimeout() time.Duration // 0 = kindling's default
}

// Optional: race in a later tier.
//   Priority() int
kindling.WithTransport(myTransport)

Add fuel to the fire

Got a technique that gets through somewhere? Wrap it as a Transport and open a pull request. Note any server-side pieces it needs, and every tool built on kindling gets the new route.