net/dns
Finding the address of a name and asking for any kind of DNS record.
import net.dns
DNS turns names such as salamlang.ir into IP addresses. Lookup is the simplest way and does what a browser does. The Lookup... functions ask the DNS server for one particular kind of record and are what you want for building network tools or checking the configuration of a domain.
A lookup can fail for ordinary reasons: the name does not exist, the network is down or the server is slow. None of the functions stop the program. Lookup and LookupCNAME return an empty string, and the others return an empty list, so check the result before using it.
Two different resolvers
Lookupasks the operating system, exactly as connecting by name does: the hosts file,localhost, search domains and the system cache all apply. It returns one IPv4 address.- The
Lookup...record functions send their own queries over UDP to the servers fromDefaultServers: thenameserverlines of/etc/resolv.confon Linux, macOS and BSD. The hosts file is not used. When there is no such file, which is always the case on Windows, they fall back to the public resolvers8.8.8.8and1.1.1.1, so the names you look up leave your network. Each server gets 3 seconds before the next one is tried.
What trips people up
- Large answers can be incomplete. An answer that does not fit in one UDP packet comes back with
tcset and is not retried over TCP, so very long TXT records or long lists of addresses may be cut short. QueryServerdoes not check the answer's id against the question's. On an untrusted network compareMessage.idyourself, or prefer a resolver you trust.- Everything is plain text on port 53. There is no DNSSEC validation and no DNS over TLS or HTTPS.
LookupPTRtakes IPv4 addresses only.
Resolve a name, then connect
net/tcp and net/udp take a host name or an address, but resolving the name yourself first lets you report a clear error before you try to connect. Lookup uses the operating system resolver, so localhost and the names in the hosts file work too, offline.
import net.dns
import net.tcp
mut server := tcp.Server {}
func greet():
conn := tcp.Accept(server)
_ := tcp.Write(conn, "hello, you found me")
tcp.Close(conn)
end
func connect(host: str, port: int): tcp.Conn:
address := dns.Lookup(host)
if address == "":
println "cannot resolve", host
ret tcp.Conn {}
end
println host, "is", address
ret tcp.Dial(address, port)
end
func main:
server = tcp.Bind(0)
t := spawn(greet)
conn := connect("localhost", tcp.Port(server))
if tcp.ConnOk(conn):
tcp.SetReadDeadline(conn, 2000)
println tcp.Read(conn, 1024)
tcp.Close(conn)
end
join(t)
tcp.CloseServer(server)
bad := connect("no-such-host.invalid", 80)
println "connected:", tcp.ConnOk(bad)
endThe .invalid top level domain is reserved and never exists, which makes it handy for testing the failure path.
Checking the mail setup of a domain
Mail for a domain goes to the servers in its MX records, lowest priority number first; the others are fallbacks. The domain's TXT records hold the SPF policy, which says which servers may send mail for it, and the DMARC policy lives in the TXT record of _dmarc. plus the domain. This program needs the internet, so it is only compiled here. Against the real DNS it printed:
import net.dns
func main:
domain := "gmail.com"
mx := dns.LookupMX(domain)
if mx.items.len() == 0:
println domain, "has no MX records, mail goes to its A record"
end
mut best := dns.Record {}
each r in mx.items:
println "MX", r.priority, r.data
if best.data == "" or r.priority < best.priority:
best = r
end
end
println "deliver to:", best.data, "first"
each text in dns.LookupTXT(domain):
if text.starts_with("v=spf1"):
println "SPF:", text
end
end
each text in dns.LookupTXT("_dmarc." + domain):
if text.starts_with("v=DMARC1"):
println "DMARC:", text
end
end
endMX 10 alt1.gmail-smtp-in.l.google.com
MX 20 alt2.gmail-smtp-in.l.google.com
MX 40 alt4.gmail-smtp-in.l.google.com
MX 30 alt3.gmail-smtp-in.l.google.com
MX 5 gmail-smtp-in.l.google.com
deliver to: gmail-smtp-in.l.google.com first
SPF: v=spf1 redirect=_spf.google.com
DMARC: v=DMARC1; p=none; sp=quarantine; rua=mailto:mailauth-reports@google.com
The records come back in whatever order the server chose, so sort or pick by priority yourself, as the loop does.
Asking one server directly
QueryServer skips the system resolver and asks the server you name, which is how you compare what different resolvers say or test your own DNS server. It returns the whole Message: rcode 3 means the name does not exist, and an empty message (no qr flag, id 0) means no answer came back within the timeout. Each record has a ttl, the number of seconds a cache may keep it. A name that is an alias answers with a CNAME record before the address, which is why the loop looks at rtype. This one needs the internet too.
import net.dns
func show(server: str, name: str):
m := dns.QueryServer(server, name, dns.TypeA, 2000)
if m.id == 0 and not m.qr:
println server, "did not answer"
ret
end
if m.rcode == 3:
println server, "says", name, "does not exist"
ret
end
each r in m.answers.items:
if r.rtype == dns.TypeA:
println server, name, "->", r.data, "for", r.ttl, "s"
else:
println server, name, "is an alias of", r.data
end
end
end
func main:
servers := dns.DefaultServers()
println "system servers:", servers
show(servers.get(0), "www.github.com")
show("1.1.1.1", "salamlang.ir")
show("1.1.1.1", "no-such-name.invalid")
show("192.0.2.1", "salamlang.ir")
end192.0.2.1 belongs to a range reserved for documentation, so nothing answers there and the program shows the timeout case. The addresses and TTLs you see will differ from run to run.
Speaking the protocol yourself
DNS is just small binary packets over UDP, and EncodeQuery and Decode build and read them. That is enough for a test double: the program below runs a toy DNS server on 127.0.0.1 that answers every question with no such name by setting two flag bytes of the query and sending it back. The client sends a real query with net/udp and decodes the answer, all offline.
import net.dns
import net.udp
import mem
mut server := udp.Socket {}
func answer_nxdomain():
udp.SetTimeout(server, 2000)
buf := mem.AllocateZeroed(512 as u64)
mut host := ""
mut port := 0
n := udp.ReceiveBytes(server, buf, 512, host, port)
question := dns.Decode(buf, n)
println "server: question", question.id, "for", question.questions.items[0].name
b := buf as u8*
b[2] = b[2] | (128 as u8)
b[3] = 131 as u8
_ := udp.SendBytes(server, host, port, buf, n)
mem.Free(buf)
end
func rcode_name(code: int): str:
switch code:
0: ret "NOERROR" end
2: ret "SERVFAIL" end
3: ret "NXDOMAIN" end
5: ret "REFUSED" end
end
ret "other"
end
func main:
server = udp.BindOn("127.0.0.1", 0)
t := spawn(answer_nxdomain)
query := dns.EncodeQuery("shop.example", dns.TypeA, 7)
client := udp.Open()
udp.SetTimeout(client, 2000)
_ := udp.SendBytes(client, "127.0.0.1", udp.Port(server), query.data, query.len)
reply := mem.AllocateZeroed(512 as u64)
mut host := ""
mut port := 0
n := udp.ReceiveBytes(client, reply, 512, host, port)
join(t)
m := dns.Decode(reply, n)
println "client: answer", m.id, "qr", m.qr, "ra", m.ra
println "client: rcode", m.rcode, rcode_name(m.rcode), "answers", m.answers.items.len()
mem.Free(reply)
udp.Close(client)
udp.Close(server)
endThe id of the answer matches the id of the question (7 here). A real client sends a random id and ignores any answer whose id does not match, so a stray or forged packet is not mistaken for the answer.
Which function for which job
| Job | Use |
|---|---|
| Get an address to connect to | Lookup |
| All IPv4 or IPv6 addresses of a name | LookupA, LookupAAAA |
| Where mail for a domain goes | LookupMX |
| SPF, DMARC and domain verification codes | LookupTXT |
| The real name behind an alias | LookupCNAME |
| The name of an IP address | LookupPTR |
| Ask a chosen server, see TTLs and error codes | QueryServer |
| Build or read raw DNS packets | EncodeQuery, Decode |
Types
Question
struct Question| Name | Type | Description |
|---|---|---|
name | str = "" | The domain name. |
qtype | int = 0 | The record type. |
qclass | int = 0 | The class. |
import net.dns
import compress
func main:
query := dns.EncodeQuery("salamlang.ir", dns.TypeA, 42)
println compress.BufLen(query), compress.BufToBase64(query)
m := dns.Decode(query.data, query.len)
println m.id, m.qr, m.rd, m.opcode, m.rcode, m.questions.items.len()
q := m.questions.items[0]
println q.name, q.qtype == dns.TypeA, q.qclass == dns.ClassIN
println m.answers.items.len(), m.authorities.items.len(), m.extras.items.len(), m.aa, m.tc, m.ra
println dns.TypeNS, dns.TypeCNAME, dns.TypeSOA, dns.TypePTR, dns.TypeMX, dns.TypeTXT, dns.TypeAAAA
println dns.Lookup("localhost")
println dns.DefaultServers().len() > 0
endQuestions
struct Questions| Name | Type | Description |
|---|---|---|
items | Vector | The questions. |
The example for this item is shown with the ones above: net-dns-Message.salam
Record
struct Record| Name | Type | Description |
|---|---|---|
name | str = "" | The domain name. |
rtype | int = 0 | The record type. |
rclass | int = 0 | The class. |
ttl | int = 0 | How many seconds the answer may be kept. |
priority | int = 0 | The priority of an MX record; a smaller number means a higher priority. |
data | str = "" | The record data as text: an address, a name or some text. |
import net.dns
func main:
println dns.LookupA("salamlang.ir")
println dns.LookupAAAA("salamlang.ir")
println dns.LookupCNAME("www.github.com")
mx := dns.LookupMX("gmail.com")
each m in mx.items:
println m.priority, m.data, m.ttl, m.rtype, m.rclass, m.name
end
println dns.LookupTXT("salamlang.ir")
println dns.LookupPTR("8.8.8.8")
answer := dns.QueryServer("1.1.1.1", "salamlang.ir", dns.TypeA, 2000)
println answer.answers.items.len()
endRecords
struct Records| Name | Type | Description |
|---|---|---|
items | Vector | The records. |
The example for this item is shown with the ones above: net-dns-Lookup.salam
Message
struct Message| Name | Type | Description |
|---|---|---|
id | int = 0 | The id that ties an answer to its question. |
qr | bool = false | The message is an answer. |
opcode | int = 0 | The kind of operation; zero is an ordinary question. |
aa | bool = false | The answer comes from the authoritative server. |
tc | bool = false | The answer was truncated. |
rd | bool = false | A recursive lookup was asked for. |
ra | bool = false | The server supports recursive lookups. |
rcode | int = 0 | The result code; zero means no error, 3 means the name does not exist. |
questions | Questions | The questions. |
answers | Records | The answers. |
authorities | Records | The authority records. |
extras | Records | The additional records. |
The example for this item is shown with the ones above: net-dns-Message.salam
Constants and variables
TypeA
const TypeA: i32 = 1The example for this item is shown with the ones above: net-dns-Message.salam
TypeNS
const TypeNS: i32 = 2The example for this item is shown with the ones above: net-dns-Message.salam
TypeCNAME
const TypeCNAME: i32 = 5The example for this item is shown with the ones above: net-dns-Message.salam
TypeSOA
const TypeSOA: i32 = 6The example for this item is shown with the ones above: net-dns-Message.salam
TypePTR
const TypePTR: i32 = 12The example for this item is shown with the ones above: net-dns-Message.salam
TypeMX
const TypeMX: i32 = 15The example for this item is shown with the ones above: net-dns-Message.salam
TypeTXT
const TypeTXT: i32 = 16The example for this item is shown with the ones above: net-dns-Message.salam
TypeAAAA
const TypeAAAA: i32 = 28The example for this item is shown with the ones above: net-dns-Message.salam
ClassIN
const ClassIN: i32 = 1The example for this item is shown with the ones above: net-dns-Message.salam
Functions
EncodeQuery
func EncodeQuery(name: str, qtype: int, id: int): compress.ByteBufThe example for this item is shown with the ones above: net-dns-Message.salam
Decode
func Decode(data: void*, n: int): MessageThe example for this item is shown with the ones above: net-dns-Message.salam
Lookup
func Lookup(host: str): strThe example for this item is shown with the ones above: net-dns-Message.salam
QueryServer
func QueryServer(server: str, name: str, qtype: int, timeout_ms: int): Messagetimeout_ms without an answer. The answer id is not compared with the question.The example for this item is shown with the ones above: net-dns-Lookup.salam
DefaultServers
func DefaultServers(): Vector<str>nameserver lines of /etc/resolv.conf; 8.8.8.8 and 1.1.1.1 when there are none, which is always the case on Windows.The example for this item is shown with the ones above: net-dns-Message.salam
LookupA
func LookupA(name: str): Vector<str>The example for this item is shown with the ones above: net-dns-Lookup.salam
LookupAAAA
func LookupAAAA(name: str): Vector<str>The example for this item is shown with the ones above: net-dns-Lookup.salam
LookupCNAME
func LookupCNAME(name: str): strThe example for this item is shown with the ones above: net-dns-Lookup.salam
LookupMX
func LookupMX(name: str): RecordsThe example for this item is shown with the ones above: net-dns-Lookup.salam
LookupTXT
func LookupTXT(name: str): Vector<str>The example for this item is shown with the ones above: net-dns-Lookup.salam
LookupPTR
func LookupPTR(ip: str): Vector<str>The example for this item is shown with the ones above: net-dns-Lookup.salam