Standard library · Network

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

  • Lookup asks 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 from DefaultServers: the nameserver lines of /etc/resolv.conf on 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 resolvers 8.8.8.8 and 1.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 tc set and is not retried over TCP, so very long TXT records or long lists of addresses may be cut short.
  • QueryServer does not check the answer's id against the question's. On an untrusted network compare Message.id yourself, 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.
  • LookupPTR takes 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.

net-dns-guide-connect.salam
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)
end
Outputlocalhost is 127.0.0.1 hello, you found me cannot resolve no-such-host.invalid connected: false

The .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:

net-dns-guide-mail.salam
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
end
MX 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.

net-dns-guide-servers.salam
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")
end

192.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.

net-dns-guide-toy-server.salam
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)
end
Outputserver: question 7 for shop.example client: answer 7 qr true ra true client: rcode 3 NXDOMAIN answers 0

The 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

JobUse
Get an address to connect toLookup
All IPv4 or IPv6 addresses of a nameLookupA, LookupAAAA
Where mail for a domain goesLookupMX
SPF, DMARC and domain verification codesLookupTXT
The real name behind an aliasLookupCNAME
The name of an IP addressLookupPTR
Ask a chosen server, see TTLs and error codesQueryServer
Build or read raw DNS packetsEncodeQuery, Decode

Types

Question

struct Question
One question.
NameTypeDescription
namestr = ""The domain name.
qtypeint = 0The record type.
qclassint = 0The class.
net-dns-Message.salam
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
end
Output30 ACoBAAABAAAAAAAACXNhbGFtbGFuZwJpcgAAAQAB 42 false true 0 0 1 salamlang.ir true true 0 0 0 false false false 2 5 6 12 15 16 28 127.0.0.1 true

Questions

struct Questions
A list of questions.
NameTypeDescription
itemsVectorThe questions.

The example for this item is shown with the ones above: net-dns-Message.salam

Record

struct Record
One answer record.
NameTypeDescription
namestr = ""The domain name.
rtypeint = 0The record type.
rclassint = 0The class.
ttlint = 0How many seconds the answer may be kept.
priorityint = 0The priority of an MX record; a smaller number means a higher priority.
datastr = ""The record data as text: an address, a name or some text.
net-dns-Lookup.salam
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()
end

Records

struct Records
A list of records.
NameTypeDescription
itemsVectorThe records.

The example for this item is shown with the ones above: net-dns-Lookup.salam

Message

struct Message
One DNS message, a question or an answer.
NameTypeDescription
idint = 0The id that ties an answer to its question.
qrbool = falseThe message is an answer.
opcodeint = 0The kind of operation; zero is an ordinary question.
aabool = falseThe answer comes from the authoritative server.
tcbool = falseThe answer was truncated.
rdbool = falseA recursive lookup was asked for.
rabool = falseThe server supports recursive lookups.
rcodeint = 0The result code; zero means no error, 3 means the name does not exist.
questionsQuestionsThe questions.
answersRecordsThe answers.
authoritiesRecordsThe authority records.
extrasRecordsThe additional records.

The example for this item is shown with the ones above: net-dns-Message.salam

Constants and variables

TypeA

const TypeA: i32 = 1
An IPv4 address record.

The example for this item is shown with the ones above: net-dns-Message.salam

TypeNS

const TypeNS: i32 = 2
A name server of the domain.

The example for this item is shown with the ones above: net-dns-Message.salam

TypeCNAME

const TypeCNAME: i32 = 5
An alias.

The example for this item is shown with the ones above: net-dns-Message.salam

TypeSOA

const TypeSOA: i32 = 6
The start of authority of the domain.

The example for this item is shown with the ones above: net-dns-Message.salam

TypePTR

const TypePTR: i32 = 12
The name for an address (a reverse lookup).

The example for this item is shown with the ones above: net-dns-Message.salam

TypeMX

const TypeMX: i32 = 15
A mail server.

The example for this item is shown with the ones above: net-dns-Message.salam

TypeTXT

const TypeTXT: i32 = 16
Free text, such as SPF records.

The example for this item is shown with the ones above: net-dns-Message.salam

TypeAAAA

const TypeAAAA: i32 = 28
An IPv6 address record.

The example for this item is shown with the ones above: net-dns-Message.salam

ClassIN

const ClassIN: i32 = 1
The internet class; nearly always this one.

The 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.ByteBuf
Builds a DNS question message to send over UDP.

The example for this item is shown with the ones above: net-dns-Message.salam

Decode

func Decode(data: void*, n: int): Message
Reads a DNS message from bytes.

The example for this item is shown with the ones above: net-dns-Message.salam

Lookup

func Lookup(host: str): str
Finds one IPv4 address of a host name through the operating system resolver, hosts file included; an empty string when it fails.

The 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): Message
Asks the given server on port 53 directly, with a random id, and returns the answer message; an empty message after timeout_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>
The DNS servers from the 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 IPv4 addresses of a name.

The example for this item is shown with the ones above: net-dns-Lookup.salam

LookupAAAA

func LookupAAAA(name: str): Vector<str>
The IPv6 addresses of a name.

The example for this item is shown with the ones above: net-dns-Lookup.salam

LookupCNAME

func LookupCNAME(name: str): str
The real name behind an alias.

The example for this item is shown with the ones above: net-dns-Lookup.salam

LookupMX

func LookupMX(name: str): Records
The mail servers of a domain, with their priority.

The example for this item is shown with the ones above: net-dns-Lookup.salam

LookupTXT

func LookupTXT(name: str): Vector<str>
The text records of a domain.

The example for this item is shown with the ones above: net-dns-Lookup.salam

LookupPTR

func LookupPTR(ip: str): Vector<str>
The names of an IPv4 address; an empty list for anything else.

The example for this item is shown with the ones above: net-dns-Lookup.salam