kindling

package module
v0.0.0-...-9a360f6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 11, 2026 License: Apache-2.0 Imports: 18 Imported by: 4

README

Kindling

Library using a series of redundant techniques to send and receive small amounts of data through censoring firewalls. This is ideal for accessing things like configuration files during the bootrapping phase as circumvention tools first start. Kindling is intended to be used by any circumvention tool written in Go that need to reliably fetch configuration data on startup. It is also designed to be easy for any developer to add a new technique that other tools may benefit from.

The techniques integrated include:

  1. Domain fronting.
  2. Proxyless dialing from the Outline SDK that generally bypasses DNS-based and SNI-based blocking (i.e. works particularly well for broadly used services with a lot of IPs that are not IP-blocked)
  3. DNS tunneling via DNSTT
  4. AMP caching also via David Fifield with a Lantern implementation.

The idea is to continually add more techniques as they become available such that all tools have access to the most robust library possible for getting on the network quickly and reliably.

Transport racing and priorities

Kindling races the configured transports against each other and returns the first usable response. Transports race in priority tiers: every transport in the default tier connects in parallel, and a lower-priority tier is dialed only once every transport in the higher-priority tiers has failed to produce a usable response.

DNS tunneling (WithDNSTunnel) is registered as a last resort. It keeps working under heavy censorship but is slow and low-throughput, so it is only dialed when the faster transports (domain fronting, proxyless dialing, AMP caching) are all blocked. Custom transports added via WithTransport default to the top tier; a transport can opt into a later tier by implementing Priority() int (higher numbers race later).

Example

cfg, _ := domainfront.ParseConfigFromFile("fronted.yaml.gz")
df, _ := domainfront.New(ctx, cfg,
    domainfront.WithConfigURL("https://raw.githubusercontent.com/getlantern/fronted/refs/heads/main/fronted.yaml.gz"),
)
defer df.Close()

k, _ := kindling.NewKindling(
    "myapp",
    kindling.WithDomainFronting(df),
    kindling.WithProxyless("raw.githubusercontent.com"),
    kindling.WithDNSTunnel(newDNSTT()),
    kindling.WithAMPCache(ampClient),
)
httpClient := k.NewHTTPClient()

You can also dynamically add transports that provide a simple Transport interface:

// Transport provides the basic interface that any transport must implement to be used by Kindling.
type Transport interface {
	// NewRoundTripper creates a new http.RoundTripper that uses this transport. As much as possible
	// the RoundTripper should be pre-connected when it is returned, as otherwise it can take too
	// much time away from other transports. In other words, Kindling parallelizes the connection
	// of the transports, but the actual sending of the request is done serially to avoid
	// issues with non-idempotent requests.
	NewRoundTripper(ctx context.Context, addr string) (http.RoundTripper, error)

	// MaxLength returns the maximum length of data that can be sent using this transport, if any.
	// A value of 0 means there is no limit.
	MaxLength() int

	// Name returns the name of the transport for logging and debugging purposes.
	Name() string
}

You can then use this as follows:

k := kindling.NewKindling(
	"myapp",
    kindling.WithTransport(myCoolTransport),
	kindling.WithTransport(myCoolerTransport),
)
httpClient := k.NewHTTPClient()

I want to add fuel to the fire (aka a new bootrapping technique!). What do I do?

All you really need to do is to return an http.RoundTripper from whatever library you're adding. Then you simply need to add a method in kindling.go to allow callers to configure the new method. For DNS tunneling, for example, that method is as follows:

func WithDNSTunnel(d dnstt.DNSTT) Option {
	return newOption(func(k *kindling) {
		log.Info("Setting DNS tunnel")
		if d == nil {
			log.Error("DNSTT instance is nil")
			return
		}
		k.roundTripperGenerators = append(k.roundTripperGenerators, namedDialer("dnstt", d.NewRoundTripper))
	})
}

It is also important to document any steps that kindling users must take in order to make the technique operational, if any. Does it require server-side components, for example?

Otherwise, just open a pull request, and we'll take it for a spin and will integrate it as soon as possible.

Documentation

Index

Constants

View Source
const IdempotentHeader = "X-Kindling-Idempotent"

IdempotentHeader is an opt-in marker callers can set on a request to declare it idempotent regardless of HTTP method. raceTransport will then apply the same retry-across-transports behavior to that request that it normally reserves for GET/HEAD: transport-level errors and 5xx responses fall back to the next connected transport.

Use this for POST endpoints that are semantically read-only or otherwise safe to replay (e.g., a config-fetch endpoint that returns the same payload regardless of how many times it's called). The header is harmless to leak to the origin — kindling doesn't strip it before sending.

Any non-empty value enables the override. Recommended value is "1".

Variables

This section is empty.

Functions

func NewSmartHTTPTransport

func NewSmartHTTPTransport(logWriter io.Writer, domains ...string) (*http.Transport, error)

NewSmartHTTPTransport creates an http.Transport using the Outline SDK smart dialer for the given domains. This is a standalone utility that does not require a Kindling instance. The smart dialer's connection attempts go via the stdlib net.Dialer; use NewSmartHTTPTransportWithDialer when those attempts need to bypass an active VPN TUN.

func NewSmartHTTPTransportWithConfig

func NewSmartHTTPTransportWithConfig(
	logWriter io.Writer,
	config []byte,
	stream transport.StreamDialer,
	packet transport.PacketDialer,
	domains ...string,
) (*http.Transport, error)

NewSmartHTTPTransportWithConfig is NewSmartHTTPTransportWithDialer plus an override for the YAML strategy config. nil reads the embedded default, which lists `system: {}` first under DNS — incompatible with non-default stream dialers, since outline-sdk's smart strategy then requires the base dialer to be *transport.TCPDialer. Pass a config that omits `system: {}` (using DoH/DoT entries) to route every probe through your stream dialer. A non-nil but empty config is rejected, matching WithSmartDialerConfig.

func NewSmartHTTPTransportWithDialer

func NewSmartHTTPTransportWithDialer(
	logWriter io.Writer,
	stream transport.StreamDialer,
	packet transport.PacketDialer,
	domains ...string,
) (*http.Transport, error)

NewSmartHTTPTransportWithDialer is NewSmartHTTPTransport plus an override for the base stream / packet dialers the smart strategy probes against. nil dialers fall back to the stdlib-backed defaults. The motivating case is radiance's bypass dialer, which routes connection attempts around its own VPN TUN so kindling traffic doesn't loop through the tunnel.

Types

type Kindling

type Kindling interface {
	// NewHTTPClient returns an HTTP client whose transport races all configured
	// circumvention transports in parallel.
	NewHTTPClient() *http.Client

	// ReplaceTransport swaps the round-tripper generator for the named transport,
	// preserving its MaxLength and IsStreamable properties.
	ReplaceTransport(name TransportName, rt func(ctx context.Context, addr string) (http.RoundTripper, error)) error
}

Kindling creates HTTP clients that race requests across multiple censorship circumvention transports, returning the first successful response.

func NewKindling

func NewKindling(name string, options ...Option) (Kindling, error)

NewKindling creates a Kindling instance with the given application name and options. Returns an error if any option fails synchronously (e.g. a nil transport argument). Deferred initialization failures (such as a failed smart dialer in WithProxyless) are logged as warnings and are only fatal when they leave Kindling with no usable transports.

type Option

type Option func(*kindling) error

Option configures a Kindling instance. Options are applied in the order provided — specify WithLogWriter first to capture logs from subsequent transport initialization.

func WithAMPCache

func WithAMPCache(c amp.Client) Option

WithAMPCache adds AMP caching via the provided amp.Client. AMP has a 6000-byte request body limit and does not support streaming.

func WithDNSTunnel

func WithDNSTunnel(d dnstt.DNSTT) Option

WithDNSTunnel adds DNS tunneling via the provided dnstt.DNSTT. DNS tunneling is raced only as a last resort: it keeps working under heavy censorship but is slow and low-throughput, so the race transport reaches for it only after every faster transport has failed to produce a usable response.

func WithDomainFronting

func WithDomainFronting(c *domainfront.Client) Option

WithDomainFronting adds domain fronting via the provided domainfront.Client. Each race attempt obtains a pre-connected one-shot RoundTripper via NewConnectedRoundTripper, so the race transport blocks on a real TLS handshake to a working front (not on a cached wrapper that "connects" instantly and always wins the race).

func WithLogWriter

func WithLogWriter(w io.Writer) Option

WithLogWriter sets the log output destination. By default, logs go to os.Stdout. Specify this first to capture initialization logs from other options like WithProxyless.

func WithPacketDialer

func WithPacketDialer(d transport.PacketDialer) Option

WithPacketDialer is the UDP counterpart to WithStreamDialer. The smart strategy uses UDP for DNS probes that drive strategy selection.

func WithPanicListener

func WithPanicListener(fn func(string)) Option

WithPanicListener sets a callback invoked when a transport goroutine panics.

func WithProxyless

func WithProxyless(domains ...string) Option

WithProxyless enables direct access using the Outline SDK smart dialer, which bypasses DNS-based and SNI-based blocking. The smart dialer is constructed after every other option has run, so WithStreamDialer / WithPacketDialer take effect regardless of the order callers pass them to NewKindling.

func WithSmartDialerConfig

func WithSmartDialerConfig(cfg []byte) Option

WithSmartDialerConfig replaces the embedded smart_dialer_config.yml that drives the Outline SDK strategy probe. Callers that pass a custom StreamDialer typically need this too: the default config lists `system: {}` first under DNS, which makes the strategy fall back to the OS resolver — and outline-sdk then requires the base StreamDialer to be exactly *transport.TCPDialer, rejecting any other type. Supplying a config that omits `system: {}` (using DoH/DoT entries instead) routes every probe through the custom dialer.

func WithStreamDialer

func WithStreamDialer(d transport.StreamDialer) Option

WithStreamDialer overrides the TCP dialer used by smart-dialer-based transports (WithProxyless). When unset, the smart dialer uses Outline SDK's default transport.TCPDialer, which dials via the stdlib net.Dialer and so follows the host's routing table — sending packets through any active VPN TUN. Callers that need their connection attempts to bypass a VPN tunnel they themselves serve (radiance is the motivating case) should pass an alternative here.

func WithTransport

func WithTransport(t Transport) Option

WithTransport adds a custom Transport implementation.

type Transport

type Transport interface {
	// NewRoundTripper creates a pre-connected http.RoundTripper. Implementations
	// should complete the connection before returning so that the race transport
	// can try requests serially without paying connection latency.
	NewRoundTripper(ctx context.Context, addr string) (http.RoundTripper, error)

	// MaxLength returns the maximum request body size this transport supports.
	// Zero means no limit.
	MaxLength() int

	// IsStreamable reports whether this transport supports streaming responses
	// (e.g. text/event-stream).
	IsStreamable() bool

	// Name identifies this transport for logging and debugging.
	Name() string

	// RequestTimeout returns the maximum time a single request is allowed to
	// spend on this transport. Zero means the race transport picks a default
	// (80 s for requests without a body, 3 min for requests with a body).
	RequestTimeout() time.Duration
}

Transport defines a censorship circumvention transport that can be used by Kindling.

type TransportName

type TransportName string

TransportName identifies a built-in transport. Custom transports added via WithTransport may use any string for their Name(); the constants below cover the names assigned to the transports configured by WithDomainFronting, WithDNSTunnel, WithAMPCache, and WithProxyless.

const (
	TransportDomainfront TransportName = "domainfront"
	TransportDNSTunnel   TransportName = "dnstt"
	TransportAMP         TransportName = "amp"
	TransportSmart       TransportName = "smart"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL