net.doh
Encrypted DNS: a resolver over DNS over HTTPS and DNS over TLS with server failover and a TTL cache, plain UDP DNS as a fallback, presets, and a local stub server for other programs.
import net.doh
Ordinary DNS asks "what is the address of this name?" in plain text over UDP, so the network between you and the resolver can read every name you look up, and can change or block the answer. DNS over HTTPS (DoH) and DNS over TLS (DoT) send the same question inside an encrypted connection. This package speaks both, and plain UDP DNS too, and adds what a program needs on top: a Resolver that tries a list of servers in order, remembers answers for their TTL and answers LookupHost with a list of addresses. A Stub turns the resolver into a small local DNS server, so other programs can use it by pointing their DNS at it. To see how the pieces fit together in an app, read Building a v2ray style or VPN app.
Core ideas:
- A
Serversays how to reach one DNS server:KindDoH(HTTPS POST),KindDoHGet(HTTPS GET),KindDoT(TLS on port 853) orKindUDP(plain).Cloudflare,Google,Quad9and their DoT forms are ready made, andNewServer,NewDoTandNewPlainmake your own. - Give a server its IP addresses in
addrs. Then the program does not need to resolve the server's own name first, which matters when DNS is exactly what is blocked. Queryasks one server once.NewResolverandResolveask a list: a server that fails, times out or answers SERVFAIL or REFUSED is skipped for the next, and answers are cached (an answer for its TTL, kept between the limits ofSetTTLLimits; a "no such name" answer for a short fixed time).- Certificates are checked.
insecureturns that off for tests only.
Things that catch people out:
- Encrypted DNS hides the question from the network, not from the resolver. The resolver you pick sees every name you look up. Choose one you trust, and remember that connecting to a well known resolver address is itself visible.
- Blocked resolvers. A network can block the addresses of public DoH servers. Use several servers, keep a plain or private one as the last in the list, and see
IranPlainServersfor the plain servers used in Iran. Plain DNS is readable and can be changed on the way. - No DNSSEC. Answers are trusted as the server gave them; nothing here checks DNSSEC signatures.
- The stub answers on UDP and TCP (the same port, TCP with the two byte length prefix) and has no access control. It listens on
127.0.0.1when the host is empty; give0.0.0.0only on purpose, because an open DNS server on a public address is abused. - Free what you hold.
FreeResponse,FreeAnswerandFreeServersrelease memory; aResolverand aStublive until the program ends. - The cache and counters are shared across threads and the resolver takes its own lock, so one resolver can serve many threads.
- IPv6 resolvers work. A server address may be an IPv6 literal, over UDP, TLS or HTTPS. The built-in servers list IPv4 first;
WithIPv6,Cloudflare6,Google6andQuad96add or use the IPv6 addresses (2606:4700:4700::1111,2001:4860:4860::8888,2620:fe::fe). The local stub can listen on::1or::. - Connections are reused. DoH over HTTPS keeps its connections alive and reuses them for later queries (up to 8 idle ones, for 30 seconds); a stale one is replaced by a new connection and the query is repeated once.
ReusedConnectionscounts the reuses andCloseIdleConnectionscloses the idle ones. - Iran presets are unverified. The addresses of
Shecan,Server403,Electro,RadarandBegzarcome from community lists and could not be confirmed against the official sites of those services. Check them before relying on them.
Using a resolver
Make a Resolver from a list of servers and ask it. The second lookup of the same name comes from the cache and does not touch the network. The example uses a tiny DNS server on loopback so that it runs anywhere; with real servers use DefaultServers or your own list.
import compress
import mem
import net.dns
import net.doh
import net.udp
import sync
mut udp_port := 0
mut queries := 0
mut ready := sync.NewWaitGroup()
func record(name: str, rtype: int, ttl: int, data: str): dns.Record:
ret dns.Record { name = name, rtype = rtype, rclass = 1, ttl = ttl, data = data }
end
func answer_for(p: void*, n: int): compress.ByteBuf:
queries++
question := dns.Decode(p, n)
asked := question.questions.items.get(0)
mut records := Vector {} as Vector<dns.Record>
mut rcode := 0
if asked.name == "a.test" and asked.qtype == dns.TypeA:
records.push(record("a.test", dns.TypeA, 120, "10.0.0.1"))
records.push(record("a.test", dns.TypeA, 300, "10.0.0.2"))
else asked.name == "v6.test" and asked.qtype == dns.TypeAAAA:
records.push(record("v6.test", dns.TypeAAAA, 300, "2001:db8::5"))
else asked.name == "txt.test" and asked.qtype == dns.TypeTXT:
records.push(record("txt.test", dns.TypeTXT, 60, "hello world"))
else asked.name == "alias.test":
records.push(record("alias.test", dns.TypeCNAME, 60, "target.test"))
if asked.qtype == dns.TypeA:
records.push(record("target.test", dns.TypeA, 60, "10.9.9.9"))
end
else:
rcode = 3
end
ret doh.EncodeResponse(question.id, asked.name, asked.qtype, rcode, records)
end
func udp_server():
s := udp.Bind(0)
udp_port = udp.Port(s)
sync.Done(ready)
buf := mem.Allocate(2048 as u64)
mut from_host := ""
mut from_port := 0
until true:
n := udp.ReceiveBytes(s, buf, 2048, from_host, from_port)
if n > 0:
answer := answer_for(buf, n)
udp.SendBytes(s, from_host, from_port, answer.data, answer.len)
end
end
end
func start_mock():
sync.Add(ready, 1 as i64)
_t := spawn(udp_server)
sync.Wait(ready)
end
func mock_server(name: str): doh.Server:
mut server := doh.NewPlain(name, "127.0.0.1")
server.port = udp_port
ret server
end
func main:
start_mock()
mut servers := Vector {} as Vector<doh.Server>
servers.push(mock_server("mock"))
resolver := doh.NewResolver(servers)
addresses := doh.LookupHost(resolver, "a.test")
println "addresses:", addresses.len(), addresses.get(0)
again := doh.LookupHost(resolver, "a.test")
println again.len()
stats := doh.Stats(resolver)
println "asked the server", queries, "time(s); cache hits:", stats.hits
endFor real use, put your servers in the order you trust them and give each one its addresses:
mut servers := Vector {} as Vector<doh.Server>
servers.push(doh.Cloudflare())
servers.push(doh.GoogleDoT())
servers.push(doh.NewPlain("home", "192.168.1.1"))
resolver := doh.NewResolver(servers)
println doh.LookupHost(resolver, "example.com")
DNS when networks interfere
Three kinds of trouble are common. The network can read your lookups: use DoH or DoT. It can change answers for some names, or return nothing: encrypted DNS stops changes on the path, but not a block of the resolver's address. It can block the resolvers themselves: list several, keep addrs filled so that no lookup is needed to reach them, and keep a plain server that works in your network as the last choice. IranPlainServers lists plain servers used in Iran; their traffic is not encrypted.
Run a Stub on 127.0.0.1 and point the operating system or a program at it, and every lookup it makes goes through your resolver and its cache. When a program does name lookups itself through a proxy, as SOCKS5 and Trojan do, let the proxy resolve the name, so that the lookup is not made locally at all.
What to know
- Encrypted DNS hides the question from the network, not from the resolver you choose.
- Fill
addrs, or set a bootstrap withSetBootstrap, so that the resolver does not need DNS to find its DNS. - Failed servers are skipped; a "no such name" answer is final and cached for the short failure time.
- No DNSSEC validation. The stub has no access control; bind it to loopback.
Queryhas a timeout in milliseconds, and aResolverhasSetTimeout; keep it short enough that a blocked server does not stall your program.- No logging happens by itself.
Types
CacheEntry
struct CacheEntry| Name | Type | Description |
|---|---|---|
raw | compress.ByteBuf | The cached answer as DNS wire bytes. |
expires_ms | i64 = 0 | When the entry expires, as a time in milliseconds. |
negative | bool = false | True for a cached 'no such name' or 'no data' answer. |
Answer
struct AnswerResolve returns: the answer, and whether it came from the cache.| Name | Type | Description |
|---|---|---|
ok | bool = false | True when some server gave a usable answer, or the cache did. |
cached | bool = false | True when the answer came from the cache without asking a server. |
negative | bool = false | True when the answer says the name does not exist or has no data of that type. |
rcode | int = 0 | The DNS response code: 0 is success, 3 is a name that does not exist. |
ttl | int = 0 | How many seconds the answer may still be used, after the limits of SetTTLLimits. |
raw | compress.ByteBuf | The answer as DNS wire bytes. Free it with FreeAnswer. |
msg | dns.Message | The decoded dns.Message. |
server | str = "" | The name of the server that answered, empty for a cached answer. |
err | str = "" | Why no server could answer, when ok is false. |
import compress
import mem
import net.dns
import net.doh
import net.udp
import sync
mut udp_port := 0
mut queries := 0
mut ready := sync.NewWaitGroup()
func record(name: str, rtype: int, ttl: int, data: str): dns.Record:
ret dns.Record { name = name, rtype = rtype, rclass = 1, ttl = ttl, data = data }
end
func answer_for(p: void*, n: int): compress.ByteBuf:
queries++
question := dns.Decode(p, n)
asked := question.questions.items.get(0)
mut records := Vector {} as Vector<dns.Record>
mut rcode := 0
if asked.name == "a.test" and asked.qtype == dns.TypeA:
records.push(record("a.test", dns.TypeA, 120, "10.0.0.1"))
records.push(record("a.test", dns.TypeA, 300, "10.0.0.2"))
else asked.name == "v6.test" and asked.qtype == dns.TypeAAAA:
records.push(record("v6.test", dns.TypeAAAA, 300, "2001:db8::5"))
else asked.name == "txt.test" and asked.qtype == dns.TypeTXT:
records.push(record("txt.test", dns.TypeTXT, 60, "hello world"))
else asked.name == "alias.test":
records.push(record("alias.test", dns.TypeCNAME, 60, "target.test"))
if asked.qtype == dns.TypeA:
records.push(record("target.test", dns.TypeA, 60, "10.9.9.9"))
end
else:
rcode = 3
end
ret doh.EncodeResponse(question.id, asked.name, asked.qtype, rcode, records)
end
func udp_server():
s := udp.Bind(0)
udp_port = udp.Port(s)
sync.Done(ready)
buf := mem.Allocate(2048 as u64)
mut from_host := ""
mut from_port := 0
until true:
n := udp.ReceiveBytes(s, buf, 2048, from_host, from_port)
if n > 0:
answer := answer_for(buf, n)
udp.SendBytes(s, from_host, from_port, answer.data, answer.len)
end
end
end
func start_mock():
sync.Add(ready, 1 as i64)
_t := spawn(udp_server)
sync.Wait(ready)
end
func mock_server(name: str): doh.Server:
mut server := doh.NewPlain(name, "127.0.0.1")
server.port = udp_port
ret server
end
func first(v: Vector<str>): str:
if v.len() == 0:
ret "none"
end
ret v.get(0)
end
func main:
start_mock()
mut servers := Vector {} as Vector<doh.Server>
servers.push(mock_server("mock"))
resolver := doh.NewResolver(servers)
doh.SetTimeout(resolver, 2000)
doh.SetTTLLimits(resolver, 30, 3600, 15)
mut a := doh.ResolveAt(resolver, "a.test", dns.TypeA, 1000 as i64)
println a.ok, a.cached, a.ttl, a.negative, a.rcode, a.msg.answers.items.len(), queries
mut again := doh.ResolveAt(resolver, "a.test", dns.TypeA, 61000 as i64)
println again.cached, again.ttl, queries
mut later := doh.ResolveAt(resolver, "a.test", dns.TypeA, 200000 as i64)
println later.cached, later.ttl, queries
doh.FreeAnswer(a)
doh.FreeAnswer(again)
doh.FreeAnswer(later)
n1 := doh.ResolveAt(resolver, "nothing.test", dns.TypeA, 500000 as i64)
println n1.ok, n1.negative, n1.rcode, n1.ttl, n1.cached
n2 := doh.ResolveAt(resolver, "nothing.test", dns.TypeA, 505000 as i64)
println n2.cached, n2.negative, queries
mut live := doh.Resolve(resolver, "a.test", dns.TypeA)
println live.ok, live.cached
doh.FreeAnswer(live)
println first(doh.LookupA(resolver, "a.test")), doh.LookupA(resolver, "a.test").len()
println first(doh.LookupAAAA(resolver, "v6.test")), first(doh.LookupTXT(resolver, "txt.test"))
println doh.LookupCNAME(resolver, "alias.test"), first(doh.LookupHost(resolver, "alias.test")), first(doh.LookupHost(resolver, "5.6.7.8"))
stats := doh.Stats(resolver)
println stats.hits > 0, stats.misses > 0, stats.upstream > 0, stats.failures
println doh.CacheSize(resolver) > 0
doh.ClearCache(resolver)
println doh.CacheSize(resolver)
mut dead := Vector {} as Vector<doh.Server>
dead.push(doh.NewServer("dead", doh.KindDoH, "http://127.0.0.1:1/dns-query"))
broken := doh.NewResolver(dead)
doh.SetTimeout(broken, 300)
failed := doh.Resolve(broken, "a.test", dns.TypeA)
println failed.ok, failed.err.len() > 0, doh.Stats(broken).failures
println first(doh.LookupA(broken, "a.test"))
mut helper := Vector {} as Vector<doh.Server>
helper.push(mock_server("bootstrap"))
doh.SetBootstrap(broken, helper)
println "bootstrap set"
endResolverStats
struct ResolverStatsStats returns them.| Name | Type | Description |
|---|---|---|
hits | i64 = 0 | Questions answered from the cache. |
misses | i64 = 0 | Questions that were not in the cache. |
upstream | i64 = 0 | Servers asked in total (a failed server and its successor count twice). |
failures | i64 = 0 | Questions that no server could answer. |
The example for this item is shown with the ones above: net-doh-Resolver.salam
Resolver
struct ResolverNewResolver; it is safe to use from several threads.| Name | Type | Description |
|---|---|---|
st | State* | The internal state: servers, cache, limits and counters. |
The example for this item is shown with the ones above: net-doh-Resolver.salam
Response
struct ResponseQuery: the raw DNS message, the decoded message, how long it took and which server gave it.| Name | Type | Description |
|---|---|---|
ok | bool = false | True when a valid DNS message with the same id came back. A name that does not exist is still ok: look at the message's rcode. |
raw | compress.ByteBuf | The answer as DNS wire bytes. Free it with FreeResponse. |
msg | dns.Message | The decoded dns.Message: its questions, answers and rcode. |
rtt_ms | i64 = 0 | The round trip time in milliseconds. |
server | str = "" | The name of the server that answered. |
err | str = "" | A readable reason when ok is false, such as id mismatch or a connection error. |
import compress
import mem
import net.dns
import net.doh
import net.udp
import sync
mut udp_port := 0
mut queries := 0
mut ready := sync.NewWaitGroup()
func record(name: str, rtype: int, ttl: int, data: str): dns.Record:
ret dns.Record { name = name, rtype = rtype, rclass = 1, ttl = ttl, data = data }
end
func answer_for(p: void*, n: int): compress.ByteBuf:
queries++
question := dns.Decode(p, n)
asked := question.questions.items.get(0)
mut records := Vector {} as Vector<dns.Record>
mut rcode := 0
if asked.name == "a.test" and asked.qtype == dns.TypeA:
records.push(record("a.test", dns.TypeA, 120, "10.0.0.1"))
records.push(record("a.test", dns.TypeA, 300, "10.0.0.2"))
else asked.name == "v6.test" and asked.qtype == dns.TypeAAAA:
records.push(record("v6.test", dns.TypeAAAA, 300, "2001:db8::5"))
else asked.name == "txt.test" and asked.qtype == dns.TypeTXT:
records.push(record("txt.test", dns.TypeTXT, 60, "hello world"))
else asked.name == "alias.test":
records.push(record("alias.test", dns.TypeCNAME, 60, "target.test"))
if asked.qtype == dns.TypeA:
records.push(record("target.test", dns.TypeA, 60, "10.9.9.9"))
end
else:
rcode = 3
end
ret doh.EncodeResponse(question.id, asked.name, asked.qtype, rcode, records)
end
func udp_server():
s := udp.Bind(0)
udp_port = udp.Port(s)
sync.Done(ready)
buf := mem.Allocate(2048 as u64)
mut from_host := ""
mut from_port := 0
until true:
n := udp.ReceiveBytes(s, buf, 2048, from_host, from_port)
if n > 0:
answer := answer_for(buf, n)
udp.SendBytes(s, from_host, from_port, answer.data, answer.len)
end
end
end
func start_mock():
sync.Add(ready, 1 as i64)
_t := spawn(udp_server)
sync.Wait(ready)
end
func mock_server(name: str): doh.Server:
mut server := doh.NewPlain(name, "127.0.0.1")
server.port = udp_port
ret server
end
func main:
start_mock()
server := mock_server("mock")
println server.kind == doh.KindUDP, server.port == udp_port, server.use_tls, server.addrs.get(0)
mut r := doh.Query(server, "a.test", dns.TypeA, 2000)
println r.ok, r.server, r.msg.rcode, r.msg.answers.items.len(), r.msg.answers.items.get(0).data
println r.raw.len > 12, r.err.len()
doh.FreeResponse(r)
mut direct := doh.QueryAddr(server, "127.0.0.1", "v6.test", dns.TypeAAAA, 2000)
println direct.ok, direct.msg.answers.items.get(0).data
doh.FreeResponse(direct)
mut missing := doh.Query(server, "nothing.test", dns.TypeA, 2000)
println "missing name:", missing.ok, missing.msg.rcode, missing.msg.answers.items.len()
doh.FreeResponse(missing)
dead := doh.NewServer("dead", doh.KindDoH, "http://127.0.0.1:1/dns-query")
failed := doh.Query(dead, "a.test", dns.TypeA, 500)
println "dead server:", failed.ok, failed.err.len() > 0
endStub
struct StubResolver. Point a program's DNS at it and its lookups go through your encrypted resolver and cache. Start it with StartStub.| Name | Type | Description |
|---|---|---|
ok | bool = false | True when the port was bound. |
port | int = 0 | The UDP port it listens on, useful when you asked for 0. |
host | str = "" | The address the stub listens on. |
tcp_ok | bool = false | True when the TCP side of the stub is listening too. |
st | StubState* | The internal state. |
import compress
import mem
import net.dns
import net.doh
import net.udp
import sync
mut udp_port := 0
mut queries := 0
mut ready := sync.NewWaitGroup()
func record(name: str, rtype: int, ttl: int, data: str): dns.Record:
ret dns.Record { name = name, rtype = rtype, rclass = 1, ttl = ttl, data = data }
end
func answer_for(p: void*, n: int): compress.ByteBuf:
queries++
question := dns.Decode(p, n)
asked := question.questions.items.get(0)
mut records := Vector {} as Vector<dns.Record>
mut rcode := 0
if asked.name == "a.test" and asked.qtype == dns.TypeA:
records.push(record("a.test", dns.TypeA, 120, "10.0.0.1"))
records.push(record("a.test", dns.TypeA, 300, "10.0.0.2"))
else asked.name == "v6.test" and asked.qtype == dns.TypeAAAA:
records.push(record("v6.test", dns.TypeAAAA, 300, "2001:db8::5"))
else asked.name == "txt.test" and asked.qtype == dns.TypeTXT:
records.push(record("txt.test", dns.TypeTXT, 60, "hello world"))
else asked.name == "alias.test":
records.push(record("alias.test", dns.TypeCNAME, 60, "target.test"))
if asked.qtype == dns.TypeA:
records.push(record("target.test", dns.TypeA, 60, "10.9.9.9"))
end
else:
rcode = 3
end
ret doh.EncodeResponse(question.id, asked.name, asked.qtype, rcode, records)
end
func udp_server():
s := udp.Bind(0)
udp_port = udp.Port(s)
sync.Done(ready)
buf := mem.Allocate(2048 as u64)
mut from_host := ""
mut from_port := 0
until true:
n := udp.ReceiveBytes(s, buf, 2048, from_host, from_port)
if n > 0:
answer := answer_for(buf, n)
udp.SendBytes(s, from_host, from_port, answer.data, answer.len)
end
end
end
func start_mock():
sync.Add(ready, 1 as i64)
_t := spawn(udp_server)
sync.Wait(ready)
end
func mock_server(name: str): doh.Server:
mut server := doh.NewPlain(name, "127.0.0.1")
server.port = udp_port
ret server
end
func main:
start_mock()
mut servers := Vector {} as Vector<doh.Server>
servers.push(mock_server("mock"))
resolver := doh.NewResolver(servers)
stub := doh.StartStub(resolver, "127.0.0.1", 0)
println "stub up:", stub.ok, stub.port > 0
sock := udp.Open()
udp.SetTimeout(sock, 3000)
buf := mem.Allocate(2048 as u64)
mut from_host := ""
mut from_port := 0
each name in ["a.test", "a.test", "nothing.test"]:
mut asked := compress.NewByteBuf()
compress.BufPush(asked, 0x12)
compress.BufPush(asked, 0x34)
compress.BufPush(asked, 0x01)
compress.BufPush(asked, 0x00)
compress.BufPush(asked, 0)
compress.BufPush(asked, 1)
repeat 6:
compress.BufPush(asked, 0)
end
each label in name.split("."):
compress.BufPush(asked, label.len())
compress.BufPushBytes(asked, label as void*, label.len())
end
compress.BufPush(asked, 0)
compress.BufPush(asked, 0)
compress.BufPush(asked, 1)
compress.BufPush(asked, 0)
compress.BufPush(asked, 1)
udp.SendBytes(sock, "127.0.0.1", stub.port, asked.data, asked.len)
n := udp.ReceiveBytes(sock, buf, 2048, from_host, from_port)
reply := dns.Decode(buf, n)
println name, reply.id == 4660, reply.rcode, reply.answers.items.len(), queries
end
println "served:", doh.StubServed(stub)
doh.StopStub(stub)
udp.Close(sock)
endServer
struct ServerNewServer, NewDoT and NewPlain make them.| Name | Type | Description |
|---|---|---|
name | str = "" | A short name for logs and for server. |
kind | int = 0 | KindDoH, KindDoHGet, KindDoT or KindUDP. |
url | str = "" | The URL of a DoH endpoint, such as https://dns.google/dns-query. |
host | str = "" | The host name (for the TLS name and the Host header) or the IP. |
port | int = 0 | The port: 443 for DoH, 853 for DoT, 53 for plain DNS. |
path | str = "" | The request path of a DoH endpoint. |
use_tls | bool = true | True when the server uses TLS. It is true for DoH over https and for DoT. |
addrs | Vector | IP addresses to connect to, so that the server's own name does not have to be looked up first. This is what makes an encrypted resolver work when DNS is the thing that is blocked. |
insecure | bool = false | True to skip the certificate check. For tests only. |
import net.doh
func main:
cloudflare := doh.Cloudflare()
println cloudflare.name, cloudflare.kind == doh.KindDoH, cloudflare.url, cloudflare.host, cloudflare.port, cloudflare.path, cloudflare.use_tls
println cloudflare.addrs.get(0), cloudflare.addrs.get(1)
println doh.Google().host, doh.Google().addrs.get(0), doh.Quad9().host, doh.Quad9().addrs.get(0)
dot := doh.CloudflareDoT()
println dot.kind == doh.KindDoT, dot.host, dot.port, dot.addrs.get(0)
println doh.GoogleDoT().addrs.get(0), doh.Quad9DoT().host
custom := doh.NewServer("mine", doh.KindDoHGet, "https://dns.example.net/resolve")
println custom.kind == doh.KindDoHGet, custom.host, custom.path, custom.addrs.len()
pinned := doh.WithBootstrap(custom, "203.0.113.53")
println pinned.addrs.len(), custom.addrs.len(), pinned.addrs.get(0)
mine := doh.NewDoT("dot", "dns.example.net", "203.0.113.54")
plain := doh.NewPlain("plain", "203.0.113.55")
println mine.port, mine.use_tls, plain.port, plain.kind == doh.KindUDP, plain.use_tls
println doh.KindDoH, doh.KindDoHGet, doh.KindDoT, doh.KindUDP
mut servers := doh.DefaultServers()
println servers.len(), servers.get(0).name, servers.get(2).name
iran := doh.IranPlainServers()
println iran.len(), iran.get(0).name, iran.get(0).port, iran.get(0).addrs.get(0)
println doh.Shecan().len(), doh.Electro().get(0).name, doh.Radar().get(1).name, doh.Begzar().get(0).addrs.get(0)
doh.FreeServers(servers)
endUrl
struct Urluse_tls), host, port and path.| Name | Type | Description |
|---|---|---|
ok | bool = false | True when the URL was valid. |
use_tls | bool = true | True for https, false for http. |
host | str = "" | The host. |
port | int = 0 | The port, or 443 or 80 when the URL has none. |
path | str = "" | The path with the query, at least /. |
import compress
import net.dns
import net.doh
func main:
println doh.IsIP("1.2.3.4"), doh.IsIP("2001:db8::1"), doh.IsIP("dns.google"), doh.IsIP("1.2.3")
url := doh.ParseURL("https://dns.example.net/dns-query")
println url.ok, url.use_tls, url.host, url.port, url.path
plain := doh.ParseURL("http://127.0.0.1:8053")
println plain.ok, plain.use_tls, plain.port, plain.path
println doh.ParseURL("ftp://x").ok
mut v4 := compress.NewByteBuf()
println doh.ParseIPv4("10.0.0.7", v4), v4.len
mut v6 := compress.NewByteBuf()
println doh.ParseIPv6("2001:db8::7", v6), v6.len
println doh.ParseIPv4("10.0.0", compress.NewByteBuf())
mut records := Vector {} as Vector<dns.Record>
records.push(dns.Record { name = "example.test", rtype = dns.TypeA, rclass = 1, ttl = 300, data = "192.0.2.7" })
wire := doh.EncodeResponse(4242, "example.test", dns.TypeA, 0, records)
message := dns.Decode(wire.data, wire.len)
println message.id, message.qr, message.rcode, message.questions.items.len(), message.answers.items.len()
answer := message.answers.items.get(0)
println answer.name, answer.ttl, answer.data
nx := doh.EncodeResponse(7, "nothing.test", dns.TypeA, 3, Vector {} as Vector<dns.Record>)
println dns.Decode(nx.data, nx.len).rcode
endConstants and variables
Server kinds Kind…
kind.| Name | Value |
|---|---|
KindDoH | 0 |
KindDoHGet | 1 |
KindDoT | 2 |
KindUDP | 3 |
import compress
import mem
import net.dns
import net.doh
import net.tcp
import str
import sync
mut http_port := 0
mut methods := ""
mut ready := sync.NewWaitGroup()
func find_text(b: compress.ByteBuf, pattern: str): int:
n := pattern.len()
mut i := 0
until i + n <= b.len:
mut same := true
repeat n in j:
if compress.BufGet(b, i + j) != (pattern as u8*)[j] as int:
same = false
break
end
end
if same:
ret i
end
i++
end
ret 0 - 1
end
func text_at(b: compress.ByteBuf, off: int, n: int): str:
buf := mem.Allocate((n + 1) as u64)
mem.Copy(buf, ((b.data as i64) + (off as i64)) as void*, n as u64)
(buf as u8*)[n] = 0 as u8
ret buf as str
end
func from_base64url(s: str): compress.ByteBuf:
alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
mut out := compress.NewByteBuf()
mut acc := 0
mut bits := 0
repeat s.len() in i:
v := alphabet.find(s.substr(i, 1))
if v < 0:
break
end
acc = (acc << 6) | v
bits += 6
if bits >= 8:
bits -= 8
compress.BufPush(out, (acc >> bits) & 255)
acc = acc & ((1 << bits) - 1)
end
end
ret out
end
func serve_one(c: tcp.Conn):
mut raw := compress.NewByteBuf()
buf := mem.Allocate(4096 as u64)
mut head_end := 0 - 1
mut need := 0
until true:
n := tcp.ReadBytes(c, buf, 4096)
if n <= 0:
break
end
compress.BufPushBytes(raw, buf, n)
head_end = find_text(raw, "\r\n\r\n")
if head_end >= 0:
head := str.Lower(text_at(raw, 0, head_end))
at := head.find("content-length:")
if at >= 0:
rest := head.substr(at + 15, head.len() - at - 15)
mut line_end := rest.find("\r\n")
if line_end < 0:
line_end = rest.len()
end
need = str.Trim(rest.substr(0, line_end)).to_int()
end
if raw.len >= head_end + 4 + need:
break
end
end
end
mut wire := compress.NewByteBuf()
if text_at(raw, 0, 4) == "POST":
methods += "POST "
compress.BufPushBytes(wire, ((raw.data as i64) + (head_end as i64) + 4) as void*, need)
else:
methods += "GET "
line := text_at(raw, 0, find_text(raw, "\r\n"))
rest := line.substr(line.find("dns=") + 4, line.len())
wire = from_base64url(rest.substr(0, rest.find(" ")))
end
question := dns.Decode(wire.data, wire.len)
asked := question.questions.items.get(0)
mut records := Vector {} as Vector<dns.Record>
records.push(dns.Record { name = asked.name, rtype = dns.TypeA, rclass = 1, ttl = 300, data = "192.0.2.80" })
answer := doh.EncodeResponse(question.id, asked.name, asked.qtype, 0, records)
head := "HTTP/1.1 200 OK\r\nContent-Type: application/dns-message\r\nConnection: close\r\nContent-Length: " + answer.len + "\r\n\r\n"
tcp.Write(c, head)
tcp.WriteBytes(c, answer.data, answer.len)
tcp.Close(c)
end
func http_server():
srv := tcp.Bind(0)
http_port = tcp.Port(srv)
sync.Done(ready)
until true:
serve_one(tcp.Accept(srv))
end
end
func main:
sync.Add(ready, 1 as i64)
_t := spawn(http_server)
sync.Wait(ready)
base := "http://127.0.0.1:" + http_port + "/dns-query"
post := doh.NewServer("post", doh.KindDoH, base)
println post.use_tls, post.port == http_port, post.path
r := doh.Query(post, "web.test", dns.TypeA, 2000)
println r.ok, r.msg.answers.items.get(0).data
get := doh.NewServer("get", doh.KindDoHGet, base)
g := doh.Query(get, "web.test", dns.TypeA, 2000)
println g.ok, g.msg.answers.items.get(0).data
println methods
endFunctions
NewResolver
func NewResolver(servers: Vector<Server>): ResolverThe example for this item is shown with the ones above: net-doh-Resolver.salam
SetBootstrap
func SetBootstrap(r: Resolver, servers: Vector<Server>)addrs. They must be reachable by IP, for example plain ones.The example for this item is shown with the ones above: net-doh-Resolver.salam
SetTTLLimits
func SetTTLLimits(r: Resolver, min_ttl: int, max_ttl: int, neg_ttl: int)The example for this item is shown with the ones above: net-doh-Resolver.salam
SetTimeout
func SetTimeout(r: Resolver, timeout_ms: int)The example for this item is shown with the ones above: net-doh-Resolver.salam
Stats
func Stats(r: Resolver): ResolverStatsThe example for this item is shown with the ones above: net-doh-Resolver.salam
CacheSize
func CacheSize(r: Resolver): intThe example for this item is shown with the ones above: net-doh-Resolver.salam
ClearCache
func ClearCache(r: Resolver)The example for this item is shown with the ones above: net-doh-Resolver.salam
ResolveAt
func ResolveAt(r: Resolver, name: str, qtype: int, now_ms: i64): AnswerResolve but with the clock given in milliseconds, so the cache can be tested without waiting.The example for this item is shown with the ones above: net-doh-Resolver.salam
Resolve
func Resolve(r: Resolver, name: str, qtype: int): AnswerThe example for this item is shown with the ones above: net-doh-Resolver.salam
FreeAnswer
func FreeAnswer(a &: Answer)The example for this item is shown with the ones above: net-doh-Resolver.salam
LookupA
func LookupA(r: Resolver, name: str): Vector<str>The example for this item is shown with the ones above: net-doh-Resolver.salam
LookupAAAA
func LookupAAAA(r: Resolver, name: str): Vector<str>The example for this item is shown with the ones above: net-doh-Resolver.salam
LookupTXT
func LookupTXT(r: Resolver, name: str): Vector<str>The example for this item is shown with the ones above: net-doh-Resolver.salam
LookupCNAME
func LookupCNAME(r: Resolver, name: str): strThe example for this item is shown with the ones above: net-doh-Resolver.salam
LookupHost
func LookupHost(r: Resolver, name: str): Vector<str>The example for this item is shown with the ones above: net-doh-Resolver.salam
QueryAddr
func QueryAddr(srv: Server, addr: str, name: str, qtype: int, timeout_ms: int): ResponseThe example for this item is shown with the ones above: net-doh-Query.salam
Query
func Query(srv: Server, name: str, qtype: int, timeout_ms: int): Responseaddrs in turn, or resolving its host name when it has none. QueryAddr does one address.The example for this item is shown with the ones above: net-doh-Query.salam
FreeResponse
func FreeResponse(r &: Response)The example for this item is shown with the ones above: net-doh-Query.salam
ReusedConnections
func ReusedConnections(): i64The example for this item is shown with the ones above: net-doh-Http.salam
CloseIdleConnections
func CloseIdleConnections()The example for this item is shown with the ones above: net-doh-Http.salam
StartStub
func StartStub(r: Resolver, host: str, port: int): Stub127.0.0.1. Check ok, and tcp_ok for the TCP side.The example for this item is shown with the ones above: net-doh-Stub.salam
StubServed
func StubServed(s: Stub): i64The example for this item is shown with the ones above: net-doh-Stub.salam
StopStub
func StopStub(s: Stub)The example for this item is shown with the ones above: net-doh-Stub.salam
ParseIPv4
func ParseIPv4(text: str, out &: compress.ByteBuf): boolThe example for this item is shown with the ones above: net-doh-Parse.salam
ParseIPv6
func ParseIPv6(text: str, out &: compress.ByteBuf): boolThe example for this item is shown with the ones above: net-doh-Parse.salam
IsIP
func IsIP(host: str): boolThe example for this item is shown with the ones above: net-doh-Parse.salam
ParseURL
func ParseURL(raw: str): Urlhttp:// or https:// URL.The example for this item is shown with the ones above: net-doh-Parse.salam
NewServer
func NewServer(name: str, kind: int, url: str): Serveraddrs or use WithBootstrap so that its name need not be resolved.The example for this item is shown with the ones above: net-doh-Servers.salam
NewDoT
func NewDoT(name: str, host: str, addr: str): ServerThe example for this item is shown with the ones above: net-doh-Servers.salam
NewPlain
func NewPlain(name: str, addr: str): Serverport for another port.The example for this item is shown with the ones above: net-doh-Servers.salam
WithBootstrap
func WithBootstrap(s: Server, addr: str): ServerThe example for this item is shown with the ones above: net-doh-Servers.salam
WithIPv6
func WithIPv6(s: Server): Serverimport net.doh
func main:
v6 := doh.Cloudflare6()
println v6.addrs.get(0), doh.Google6().addrs.get(0), doh.Quad96().addrs.get(0)
both := doh.WithIPv6(doh.Cloudflare())
println both.addrs.len()
endCloudflare6
func Cloudflare6(): ServerThe example for this item is shown with the ones above: net-doh-IPv6.salam
Google6
func Google6(): ServerThe example for this item is shown with the ones above: net-doh-IPv6.salam
Quad96
func Quad96(): ServerThe example for this item is shown with the ones above: net-doh-IPv6.salam
Cloudflare
func Cloudflare(): Servercloudflare-dns.com), with its addresses 1.1.1.1 and 1.0.0.1.The example for this item is shown with the ones above: net-doh-Servers.salam
Google
func Google(): Serverdns.google), with 8.8.8.8 and 8.8.4.4.The example for this item is shown with the ones above: net-doh-Servers.salam
Quad9
func Quad9(): Serverdns.quad9.net), with 9.9.9.9 and 149.112.112.112.The example for this item is shown with the ones above: net-doh-Servers.salam
CloudflareDoT
func CloudflareDoT(): ServerThe example for this item is shown with the ones above: net-doh-Servers.salam
GoogleDoT
func GoogleDoT(): Serverimport net.doh
func main:
mut servers := Vector {} as Vector<doh.Server>
servers.push(doh.Cloudflare())
servers.push(doh.GoogleDoT())
servers.push(doh.Quad9())
resolver := doh.NewResolver(servers)
doh.SetTimeout(resolver, 3000)
answer := doh.Resolve(resolver, "example.com", 1)
println answer.ok, answer.cached, answer.server, answer.ttl
each ip in doh.LookupHost(resolver, "example.com"):
println ip
end
stub := doh.StartStub(resolver, "127.0.0.1", 5353)
println "local resolver on port", stub.port
doh.StopStub(stub)
endQuad9DoT
func Quad9DoT(): ServerThe example for this item is shown with the ones above: net-doh-Servers.salam
DefaultServers
func DefaultServers(): Vector<Server>Resolver.The example for this item is shown with the ones above: net-doh-Servers.salam
Shecan
func Shecan(): Vector<Server>The example for this item is shown with the ones above: net-doh-Servers.salam
Server403
func Server403(): Vector<Server>Electro
func Electro(): Vector<Server>The example for this item is shown with the ones above: net-doh-Servers.salam
Radar
func Radar(): Vector<Server>The example for this item is shown with the ones above: net-doh-Servers.salam
Begzar
func Begzar(): Vector<Server>The example for this item is shown with the ones above: net-doh-Servers.salam
IranPlainServers
func IranPlainServers(): Vector<Server>DefaultServers where they are reachable.The example for this item is shown with the ones above: net-doh-Servers.salam
EncodeResponse
func EncodeResponse(id: int, name: str, qtype: int, rcode: int, records: Vector<dns.Record>): compress.ByteBufThe example for this item is shown with the ones above: net-doh-Parse.salam
FreeServers
func FreeServers(v &: Vector<Server>)The example for this item is shown with the ones above: net-doh-Servers.salam