SoarSOA, start of authority: the first record in every DNS zone.
A DNS tunnel for networks that block everything except DNS.
Soar carries TCP connections through the recursive resolvers a censor can't afford to switch off: the ISP's, public ones, and in-country ones it finds on its own. It uses all of them at once, shrugs off forged answers and blocked UDP, and connects in under a second.
query, data rides in the nameanswer, authenticated by the serverdead or blocked resolver
187KB/s
in a Russia-like network where UDP to public resolvers is intercepted. dnstt, VayDNS and slipstream can't connect at all there; the next best manages 14.
0.2–0.9s
to connect across all nine test networks. MasterDnsVPN and CottenDNS take 2 to 49 seconds.
7of 9
emulated networks where Soar has the highest throughput. With 10% loss each way slipstream-rust edges it 71 to 69, and with two dead resolvers VayDNS, handed a good one, leads 153 to 144.
The name
Every query ends at the authority
Every DNS zone begins with an SOA record, its start of authority. Soar's server is the authoritative name server for a zone you delegate to it, so every query for a name in that zone eventually lands there, whichever resolver it passed through.
Censors can block an IP address, a TLS fingerprint, or a whole protocol. Blocking the resolvers that every phone, bank and government site depends on is much harder. Add an r to SOA and the authority soars over the wall, using the one service the censor has to leave running.
; parent zone, example.com$ORIGIN example.com.
@ IN SOA ns1.example.com. hostmaster.example.com. (
2026101001 ; serial
7200 ; refresh
900 ; retry
1209600 ; expire
60 ) ; negative TTL; hand the tunnel zone to the Soar server
t IN NS ns.t.example.com.
ns.t IN A 203.0.113.53
; a query a phone sends, base32 under the zone; 4-byte header, sealed frames, 8-byte tagaeb3k7qp4xw2mfzr…d6ty.t.example.com. IN TXT
How it works
The life of a request
A Soar client opens TCP streams the way any app would. Underneath, each stream becomes a series of DNS queries and answers, spread across every resolver that works.
Handshake
The client puts a fresh X25519 key in a query name. The server answers with its own key, a session ID, and an Ed25519 signature over both. Only the real server can sign, so a forged answer from the censor is ignored and the client keeps waiting for the genuine one.
Each resolver is a separate path with its own round-trip time, loss record, name-length and answer-size limits, and congestion window. Soar sends on all of them at once and keeps checking ones it hasn't verified yet. Working resolvers are found by discovery and vetted with the same signed handshake.
example
path rtt loss name answer cwnd
isp-a 48ms 1% 253 1232 24
isp-b 71ms 3% 101 512 11
national 92ms 2% 253 900 16
public-1 –– dead –– –– 0
Data rides in the name
Upstream data is sealed into a base32 query name. The header is 4 bytes and the authentication tag 8, which matters on networks that drop names longer than about 101 characters and leave roughly 50 bytes of room per query. Every path starts at that safe length and probes up to the full 253.
sid (2) | protected pn (2)
| seal( hold budget, answer size class
| | STREAM id=4 off=1180 len=37 … )
| tag (8)
The server holds the line
When there is nothing to send back yet, the server holds the client's query open until data arrives or the client's hold budget runs out, then answers the oldest held query first. Downstream data leaves the moment it is ready instead of waiting for the next poll.
t=0ms query #812 arrives, nothing to send: hold
t=0ms query #813 arrives: hold
t=37ms origin replies with 1.1 KB
t=37ms answer #812 (oldest first), then #813
An answer is an acknowledgement
Only the server can produce an authenticated answer, so an answer proves its query arrived and upstream needs no ACK frames. Downstream, the client acknowledges packets and also reports which queries got no answer, so the server resends exactly what was lost instead of waiting out a timer.
query ACK 640–702,704–719 LOST 703
answer STREAM id=4 off=88340 (resent 703's data,
re-cut to fit this resolver's answer size)
The tail goes twice
The last packet of a request or response is sent on two different paths. When one copy is lost, the transfer still finishes on time instead of stalling for a retransmission timeout.
pn 742 STREAM id=4 … FIN → isp-a
pn 742 STREAM id=4 … FIN → national (dup)
Design
Why it's fast where others stall
Each existing DNS tunnel does one thing well. Soar was designed from scratch against all of them, in an emulator that models what censors and resolvers actually do.
Packets, not segments
A session has one packet-number space, and stream data travels as byte ranges. A retransmission is re-cut to fit whichever resolver it goes out on, and several frames share each packet, so nothing is stuck waiting for a resolver with the right size limit.
Every resolver at once
dnstt and VayDNS use a single resolver. Soar spreads traffic over all of them, weighted by how each one is performing, and drops a dead one without the user noticing.
Loss read in runs
Resolver rate limits drop queries in runs. Random loss on a healthy path is scattered. Soar shrinks a path's window only on runs of losses, so a lossy but healthy resolver stays fully used while an overloaded one gets backed off.
Answer sizes measured, not assumed
Some resolvers silently drop answers over 512 bytes once traffic picks up, with no truncation flag. Soar starts every path at 512 bytes and probes upward with paired canary queries, and falls back to AAAA answers where a resolver refuses TXT.
Fast loss detection at both ends
Per-path packet thresholds on the client and RFC 9002-style packet and time thresholds on the server. Error answers such as truncation or REFUSED count against a path only after a full deadline, so a forged error can't push traffic off a working resolver.
Built for phones
Soar is a Go client library and server in one module, written for memory-constrained mobile clients, iOS network extensions included. An idle session goes quiet: it polls only while data is in flight, which saves battery and keeps a quiet resolver quiet.
Restricted airspace
Built for Iran, Russia and China
Each country breaks DNS in its own way. Soar handles each one directly, and the emulator reproduces each one so the fixes are measured.
R-IR · Iran
A hostile mix
The network
Resolvers that filter TXT, some that are dead, and others that are lossy, rate-limited and jittery, cap answer sizes, or drop names over 101 characters.
What Soar does
AAAA answers where TXT is refused, safe-length names by default, per-path answer sizes, and discovery of in-country resolvers from bundled lists.
105 KB/snext best 43, CottenDNS, after 17 s to connect
R-RU · Russia
UDP/53 intercepted
The network
UDP queries to public resolvers are intercepted, while TCP port 53 is left alone. The national resolver that UDP lands on is slow and rate-limited.
What Soar does
Runs paths over TCP/53 with RFC 7766 pipelining, many queries in flight on one connection, alongside whatever UDP paths still work.
187 KB/snext best 14, CottenDNS, after 18 s; dnstt, VayDNS, slipstream fail
R-CN · China
Forged answers
The network
Forged answers race the real ones on UDP/53, and domestic resolvers rate-limit each client address.
What Soar does
Forged answers can't authenticate, so they're dropped and the real answer still counts. Rate limits read as runs of loss, and the window backs off only on that path.
60 KB/snext best 43, slipstream-rust; VayDNS, MasterDnsVPN fail
Benchmarks
Eight tunnels, nine networks
Every tunnel fetches 100 KB through the same emulated recursive resolvers. Pick a network to see throughput and time to connect.
Tunnel100 KB fetch, KB/sConnect
Network
Soar
Best other
Soar vs best
How these were measured. A purpose-built emulator puts real builds of each tunnel behind emulated recursive resolvers with delay, jitter, loss in each direction, per-resolver query rate limits, answer-size caps, long-name filtering, TXT filtering, dead resolvers, blocked UDP and forged-answer injection. Throughput is for a 100 KB fetch. dnstt and VayDNS use one resolver, so they're given the best one in each network. MasterDnsVPN runs its defaults (3× duplication) and CottenDNS runs both its speed and survival presets.
These are emulator results. Field measurements from inside Iran, Russia and China come next.
Wire format
Twelve bytes of overhead upstream
Upstream travels in the query name as lower-case base32. Downstream travels in TXT character-strings, or in numbered 15-byte AAAA chunks where TXT is filtered.
Upstream data packet, in the query name
sid2 B
pn2 B, protected
hdrhold · size class
framessealed
tag8 B
The 1-byte header carries the client's hold budget in 100 ms steps and the answer size class it wants (512 + 48·k bytes).
Downstream data packet, in the answer
pn2 B, protected
held×10 ms
backlog1 B
framessealed
tag8 B
The server reports how long it held the query, so the client can separate resolver round trips from server wait, and how much data is still queued.
PADDINGPINGACK rangesLOSTSTREAM id · offset · len · FINMAX_STREAM_DATARESET_STREAMCLOSE
Forward secret
Every session starts with an ephemeral X25519 key exchange, so recorded traffic stays sealed even if the server's long-term key leaks later.
Server authenticated
The server signs the handshake transcript with Ed25519. Clients ship only its public key, so there is no shared secret for a censor to pull out of an app.
Sealed per direction
ChaCha20-Poly1305 with a separate key each way and the tag truncated to 8 bytes. Packet numbers are protected QUIC-style, so the wire carries no visible counters.
Part of kindling
The last resort that still gets through
Soar ships as a transport in kindling, Lantern's open-source library for getting an app's first requests through a censored network. Kindling races domain fronting, proxyless dialing and AMP caching against each other and uses whichever connects first.
Soar sits in kindling's last-resort tier. It's dialed only when every faster transport has failed, which is exactly the network where nothing else works. Any Go app can use the same setup with one option, kindling.WithDNSTunnel(client).
Tier 1 · raced in paralleldomain fronting · proxyless smart dialer · AMP cache · your own transports
Last resort · only if tier 1 failsSoar DNS tunnel
Get started
Run a server, ship a key
Delegate a zone to a host with an NS record, generate a key pair, and point clients at it.
Server
# install
go install github.com/getlantern/soar/cmd/soar-server@latest
# save the private key to key (keep it secret)
(umask 077; soar-server keygen | awk '$1 == "private" { print $2 }' > key)
# print the public key to ship with clients
soar-server pubkey key
# answer for the delegated zone
soar-server -zone t.example.com -key-file key -listen :53
The server dials destinations for anyone holding its public key, so by default it refuses private addresses, including its own, and allows only ports 80 and 443 (-allow-ports changes that). The client also plugs straight into kindling as its DNS-tunnel transport, which is how Lantern uses it.
Pre-release
Soar is open source under the Apache 2.0 license and built by the Lantern team, who make Lantern, a free app for getting past internet censorship. The protocol may still change and has no compatibility promise yet.
Its bundled Iran resolver and range lists come from KevinNet DNS (MIT). Issues and pull requests are welcome on GitHub.