std/dns
import std/dns to use it. Source: src/std/dns.iyi at v0.18.0.
Why iyi has its own: A name is turned into an address by libc's getaddrinfo everywhere else, and on Linux that is libc.so, the NSS modules it loads by name at run time (libnss_files, libnss_dns, and whatever else /etc/nsswitch.conf lists) and libresolv's state. That is the dependency floor SPEC.md III.9 keeps a program off, and the reason std/socket read numeric addresses and localhost only. A stub resolver is small: it asks the name servers the machine is configured with, over UDP and then TCP, and reads what they answer. This module is the part of it that is arithmetic over bytes and text - building a query, reading an answer, reading /etc/resolv.conf and /etc/hosts - and std/socket's Resolver is the part that talks to the network. They are two modules because the talking needs std/socket and std/socket needs this to connect by name; an import cycle is refused (R-1).
What an answer is trusted with: The answer to a query is read only if it carries the query's 16-bit ID, is a response, and repeats the question asked - the name (in any case), the type and the class; anything else on the port is somebody else's and is ignored, which is what makes a blind forger guess both the ID and the port the resolver asked from. A name inside an answer is read with its compression pointers held to RFC 1035's rule that a pointer refers to an earlier occurrence: each pointer must point before the place the previous one led to, so a loop - a pointer to itself, or two pointing at each other - and a pointer past the end are refused as malformed rather than followed. A CNAME chain is followed at most DnsMessage::MAX_CNAMES links, which ends a loop of aliases the same way.
What it does not do: no EDNS(0), so a UDP answer is at most 512 bytes and a larger one comes back truncated and is asked again over TCP; no cache, so every lookup asks; no DNSSEC validation; no reverse lookups.
DnsErrorstruct
Why a name was not resolved. One type with a question per answer, as SocketError has, so a caller tells a name that does not exist (not_found?, NXDOMAIN - authoritative, not worth asking again) from a server that failed (server_failure?, SERVFAIL - maybe worth asking again later) without reading a sentence.
pub struct DnsError
# The same failure, said about *name*: a failure found while reading
# bytes knows no name, and the resolver that asked does.
def about(name : String, server : String) : DnsError
def detail : String
def initialize(kind : Int32, name : String, server : String = "", detail : String = "")
# Not a host name at all, so nothing was asked.
def invalid_name? : ::Bool
def kind : Int32
# An answer that carried the query's ID and question and could not be
# read: a compression loop, a pointer past the end, a record running off
# the message, an alias chain that does not end.
def malformed? : ::Bool
# The name as the caller wrote it.
def name : String
# The name exists and has no address of the kind asked for (NOERROR
# with no records, "NODATA").
def no_address? : ::Bool
# NXDOMAIN: the name does not exist.
def not_found? : ::Bool
def reason : String
# REFUSED, FORMERR or NOTIMP: the server would not answer this query.
def refused? : ::Bool
# The server whose answer this is, `address:port`; empty when no server
# was asked.
def server : String
# SERVFAIL from every server asked.
def server_failure? : ::Bool
# No server answered within the timeout, on any attempt.
def timed_out? : ::Bool
# The server could not be reached: nothing listens on its port (an ICMP
# refusal), no route to it, or the TCP connection the truncated answer
# needed failed.
def unreachable? : ::Bool
endDnsMessagestruct
A query, and the reading of an answer to one.
pub struct DnsMessage
# The addresses of *type* this answer gives *name*, following CNAMEs:
# the records for the end of the chain, the name the chain ended at, and
# how many links it took. No addresses and a different name is a chain
# that left this answer before it ended, which the resolver asks about
# next. A chain of more than *budget* links is malformed.
def addresses(name : String, type : Int32, budget : Int32 = MAX_CNAMES) : Tuple(Array(Bytes), String, Int32) | DnsError
def answers : Array(DnsRecord)
# Whether this is the answer to query *id* for *name* and *type*: a
# response, a standard query's, with the ID, and repeating the question.
# A refusal of the query (SERVFAIL, REFUSED, FORMERR, NOTIMP) may come
# without the question, as some servers send it; an answer that says
# anything about the name - NOERROR or NXDOMAIN - must carry it.
def answers?(id : UInt16, name : String, type : Int32) : ::Bool
def flags : Int32
def id : UInt16
# The ID in a datagram's first two bytes, or -1 when it is too short to
# be a message: what the resolver reads before it reads anything else.
def self.id_of(packet : Bytes) : Int32
def initialize(id : UInt16, flags : Int32, question_name : String, question_type : Int32, question_class : Int32, answers : Array(DnsRecord))
def self.malformed(why : String) : DnsError
def opcode : Int32
# The message in *packet*: its header, its one question, and - unless it
# is truncated - its answer section. Anything that does not fit the
# bytes is `DnsError` with `malformed?`, never a read past them.
def self.parse(packet : Bytes) : DnsMessage | DnsError
# The query for *name*'s records of *type*, class IN, with recursion
# desired: a 12-byte header and one question. *name* is a host name
# (`DnsName.valid?`); a trailing dot is dropped.
def self.query(id : UInt16, name : String, type : Int32) : Bytes
def question_class : Int32
# The question the answer repeats; empty with `question_type` 0 when it
# repeats none.
def question_name : String
def question_type : Int32
def rcode : Int32
# The name at *at* in *packet*, and the offset just past it where it
# sits; nil when it cannot be read. Labels are 1 to 63 bytes, a label may
# not hold a `.` (it would read as two), the name is at most 255 bytes on
# the wire, and each compression pointer must point before the place the
# name was last read from - RFC 1035 4.1.4's "prior occurrence" - so the
# positions strictly fall and a loop cannot be written.
def self.read_name(packet : Bytes, at : Int32) : Tuple(String, Int32) | ::Nil
def response? : ::Bool
# TC: the answer did not fit in the datagram and is to be asked again
# over TCP. Its records are not read: what is there is a fragment.
def truncated? : ::Bool
endDnsNamestruct
What a host name may be, and the names a short one stands for.
pub struct DnsName
# The fully qualified names a lookup of *name* asks for, in order, as
# resolv.conf(5) says: a name ending in `.` is only itself; one with at
# least *ndots* dots is tried as it is first and then under each search
# domain; one with fewer is tried under each search domain first and as
# it is last. A candidate longer than a name can be is left out.
def self.candidates(name : String, search : Array(String), ndots : Int32) : Array(String)
def initialize
# *name* without the trailing dot that makes it absolute.
def self.relative(name : String) : String
# Two names are the same name in any case (RFC 4343), with or without
# the trailing dot.
def self.same?(a : String, b : String) : ::Bool
# A host name as RFC 1123 and glibc's `res_hnok` read one: labels of 1 to
# 63 letters, digits, `-` and `_`, no label starting with `-`, at most
# 253 characters, one trailing dot allowed. The last label must not be
# all digits (RFC 3696 2: no top-level domain is), so `999.1.1.1` and
# `1.2.3` stay what the numeric parser refuses rather than becoming
# questions to a name server.
def self.valid?(name : String) : ::Bool
# Why *name* is not a host name, or nil when it is.
def self.why(name : String) : String | ::Nil
endDnsRecordstruct
One resource record from an answer section.
pub struct DnsRecord
# The record's data as it came: four bytes for an A record, sixteen for
# an AAAA, the encoded name for a CNAME.
def data : Bytes
def initialize(name : String, type : Int32, record_class : Int32, ttl : UInt32, data : Bytes, target : String)
def name : String
def record_class : Int32
# A CNAME's target, read with compression; empty for any other type.
def target : String
def ttl : UInt32
def type : Int32
endHostsFilestruct
The hosts file: an address, then the names it answers for.
pub struct HostsFile
def initialize
# The addresses *text* gives *name*, as written and in the file's order:
# every line whose names include it, in any case and with or without a
# trailing dot. A line with an address and no name says nothing.
def self.lookup(text : String, name : String) : Array(String)
endResolvConfstruct
/etc/resolv.conf, as resolv.conf(5) describes it and glibc reads it.
pub struct ResolvConf
def attempts : Int32
def initialize(nameservers : Array(String) = [] of String, search : Array(String) = [] of String, ndots : Int32 = 1, timeout_ms : Int32 = 5000, attempts : Int32 = 2)
# Each an address as written - IPv4, or IPv6 without a zone - optionally
# with a port (`192.0.2.1:5353`, `[2001:db8::1]:5353`), which
# resolv.conf itself never has and a resolver built in a program may.
def nameservers : Array(String)
def ndots : Int32
# The keywords glibc reads: `nameserver`, `domain`, `search` (the later
# of `domain` and `search` wins, as there) and `options` with `ndots:`,
# `timeout:` (seconds) and `attempts:`, each held to glibc's bounds - 15,
# 30 and 5. A `#` or `;` starts a comment. Anything else is ignored, as
# glibc ignores it: a file other programs read too is not this module's
# to refuse.
def self.parse(text : String) : ResolvConf
def search : Array(String)
def timeout_ms : Int32
endError for DnsErrorimpl
impl Error for DnsError
def message : String
end
DNS: the wire format, the resolver's configuration file and the hosts file, read by iyi itself.