Soar
Soar carries your TCP connections inside DNS queries and answers, through recursive resolvers,
to a Soar server that you run as the authority for a
delegated zone. It is an outbound: "type": "soar" in the outbounds list. The client is the
Soar Go library from the Lantern team (github.com/getlantern/soar), used as it is.
Why use it
- It works on networks where almost everything is blocked but DNS still passes, like the other DNS tunnels (DNSTT, MasterDNS).
- It was designed against what censors and resolvers do: every resolver is a path with its own round-trip time, loss record, name and answer size limits and congestion window; losses are found fast at both ends and only the lost packets are sent again; the server holds idle queries and answers the moment it has data. Its project measures it ahead of the other DNS tunnels on lossy, rate-limited and Iran- and Russia-like networks, and it connects in about a second.
- It finds working resolvers by itself (discovery): from the device's own, a curated list for the country, and public resolvers. A resolver is used only once the server's signed answer came back through it, so hijacking resolvers are left out by the same test.
- The server connects to the destination itself: no SOCKS or SSH outbound behind it.
It is still a DNS tunnel: slow next to a direct proxy. The reference server only reaches public
addresses on ports 80 and 443 unless it is started with -allow-ports.
How it gets past filtering
The filter sees DNS queries for names under your zone, sent to ordinary resolvers, which forward them to your server. Every session starts with an X25519 key exchange signed by the server's Ed25519 key, so a forged answer cannot authenticate and is ignored; data is sealed with ChaCha20-Poly1305. Names start at 101 characters, which networks that drop long names still pass, and grow only where they go through. Where UDP/53 is intercepted, DNS over TCP is used; where TXT records are refused, AAAA records carry the answers.
It does not help when no resolver you can reach forwards queries for your zone.
How it works
- Session. The client handshakes at the first connection, then carries every connection as
a stream of one session. A session without connections ends after
session_idle, and the next connection opens a new one. - Resolvers written in the config. They go through the outbound's own
smart resolver pool, on unless
smart_poolisfalse: each is first sent a plain query (pretest_domain,pretest_record_type), and the ones that answer join the pool, up to 10 working, more when some go down. The client sees the pool as one resolver, on the loopback. Resolvers may then also betcp://, DoT (dot://host:853) or DoH (https://host/dns-query). Withsmart_pool: false, each resolver is a path of the client's own, plain UDP (and TCP, seetcp) only. auto, or no resolvers. The client's own discovery runs, the code of the Soar library: it tries the resolvers it verified on earlier runs (saved in the cache file), then the device's own (where the system lets it read them), the curated list of the country that Soar bundles (Iran, China, Russia), and global public resolvers; withsample_ranges, also addresses of the country's ranges. It keepswantworking and looks for more when some stop. Its settings aresmart_pool.discovery, the block every DNS tunnel shares; DNSTT and MasterDNS take the same candidates for theirauto. These resolvers are paths of the client's own, next to the pool when the config also writes resolvers. hiddify-core setsdiscovery.countryfrom the app's region when it is empty.- Sockets. The queries go out with the outbound's socket settings (
bind_interface,routing_mark) and the platform's protection, so on a VPN they do not loop back into the tunnel; queries to the pool on the loopback use a plain socket.detouris not supported.
TCP only. UDP is not carried.
Configuration
{
"type": "soar",
"tag": "soar-out",
"domain": "t.example.com",
"public_key": "<soar-server pubkey key>",
"resolvers": ["8.8.8.8", "1.1.1.1", "auto"],
"smart_pool": {
"enabled": true,
"discovery": { "country": "ir" }
}
}
| Field | Type | Default | Meaning |
|---|---|---|---|
domain |
string | required | The tunnel zone delegated to the server (soar-server -zone). |
public_key |
string | required | The server's Ed25519 public key, as soar-server pubkey prints it (base64; base64url and hex are read too). |
resolvers |
list of strings | auto |
Resolvers: "8.8.8.8", "1.1.1.1:5353", or "auto" for Soar's discovery. With the smart pool also tcp://, dot://host:853 and https://host/dns-query. |
smart_pool |
bool or object | on | The smart pool for the resolvers written, as described above; false turns it off. The object takes the pool's settings, among them discovery, which also steers Soar's own discovery: country, sample_ranges, want (default 16 here), concurrency (16), pace (20ms). With the pool off, write {"enabled": false, "discovery": {...}}. |
pretest_domain |
string | "www.google.com" |
With the pool: the name queried to check a resolver. |
pretest_record_type |
string | "a" |
With the pool: the record type of that query. |
tcp |
string | "auto" |
DNS over TCP: auto (for a resolver whose UDP never answers), on, only or off. off with the pool alone. |
safe_qname |
int | 101 |
Name length every resolver starts at. |
max_qname |
int | 253 |
Longest name a resolver is probed up to. |
max_answer |
int | 1232 |
Largest DNS answer probed up to, in bytes. |
hold |
duration | 500ms |
How long the server may hold an idle query. |
max_inflight |
int | 128 |
Queries outstanding over all resolvers (at most 1024). |
max_path_window |
int | 32; max_inflight with the pool alone |
Queries outstanding over one resolver. |
max_streams |
int | 64 |
Connections open at once (at most 256). |
stream_window |
int | 262144 |
Each connection's receive window, in bytes. |
send_buffer |
int | 65536 |
Each connection's send buffer, in bytes. |
session_idle |
duration | 90s |
A session without connections ends after this; keep it below the server's 2 minutes. |
connect_timeout |
duration | 90s |
How long a connection waits for the session. |
Links
Soar defines no share link. hiddify-core's ray2sing reads soar:// in two forms:
- The profile link of WhiteDNS, which exports
and imports
soar://<base64url JSON>with schemawhitedns.profile: the server'sdomainandpublic_key, itsresolversandsoarSettings(transport, discovery, country, name and answer sizes, windows, timeouts). Discovery on addsautoto the resolvers. - A URL:
soar://t.example.com?pk=<public key>&resolvers=8.8.8.8,1.1.1.1&country=ir#name, or the key before the host,soar://<public key>@t.example.com?.... The key is written in base64url, or percent-encoded in the query. Every field of the table is a parameter,country,sample_ranges,discovery_want,discovery_concurrencyanddiscovery_pacego intosmart_pool.discovery,smart_pool=0turns the pool off,discovery=1addsautoto written resolvers; no resolvers meansauto. masterdns and dnstt links take the same discovery parameters.
Notes
- Soar is pre-release: its protocol may still change, and client and server must be of versions that speak the same one.
- Anyone with the zone and the public key can use the server. Share them only with people you trust: a published zone is soon blocked.
- The pool is one path for the client, so its per-resolver tuning (name length, answer size,
window) applies to the pool as a whole. Without resolvers in the config, or with
smart_pool: false, the client tunes every resolver separately.