std/dns
Domain names with IDNA and the comparison of RFC 4343, DNS records as typed cases, and the RFC 1035 wire format with compression and EDNS0 - values with no natives and no capability, for the transports of std/network and std/http.
The generated reference of std/dns has every public construct with its signature, its doc comment and its examples.
std/dns holds the Domain Name System as values: a DomainName read from what a person writes (IDNA, with Unicode
written as A-labels), from the presentation format of a zone file, or from the labels of a message, and compared
without regard to the case of ASCII letters; the records as a closed set of typed cases; and a Message with the wire
format of RFC 1035, compressed names and the EDNS0 record. It touches nothing - no natives, no capability - so sending
a query is a transport's: std/network over UDP and TCP, std/tls over TLS and std/http over HTTPS move the
bytes query writes and readResponse reads (see docs/design/DNS.md). It is not in the prelude.
Import#
use DomainName, DnsError, Message, Record, RecordData, RecordType, query, readResponse from "std/dns"use DomainName, Message, RecordType, query from "std/dns"
match DomainName.tryFrom("Bücher.example") {
Ok(name) => {
print name
print name.unicodeText()
const bytes = query name, RecordType.Aaaa, 4660
match Message.tryFrom(bytes) {
Ok(message) => print message.questions
Fail(problem) => print problem
}
}
Fail(problem) => print problem
}Declarations#
Names#
type DomainName with Show, Equals, Hash, TryFrom<String, DnsError> {
static root = Self([])
static fn tryFrom(text: String): Result<Self, DnsError>
static fn ofPresentation(text: String): Result<Self, DnsError>
static fn ofLabels(labels: List<Bytes>): Result<Self, DnsError>
fn labels(): List<Bytes>
fn labelCount(): Int
fn isRoot(): Bool
fn isHostName(): Bool
fn parent(): Self?
fn isSubdomain(of: Self): Bool
fn wireLength(): Int
fn show(): String
fn unicodeText(): String
fn equals(other: Self): Bool
fn hash(): Int
}A name is its labels, most specific first, each 1 to 63 bytes, at most 255 bytes on the wire. tryFrom reads a host
name as a person writes it: the lookup processing of UTS #46 as far as the tables of std/text reach, then
Punycode (RFC 3492), so Bücher.example is xn--bcher-kva.example; every label is letters, digits and hyphens, and
one trailing dot changes nothing. ofPresentation reads the presentation format of a zone file - any byte, \. and
\DDD escapes, no IDNA - for names such as _sip._tcp.example.test, and ofLabels takes the labels of a message.
== and hash() ignore the case of ASCII letters (RFC 4343), and show() keeps the case the name arrived in.
unicodeText() shows the A-labels as U-labels for a person to read. What the IDNA processing does not cover - the
mapping table outside Latin-1, NFC, the contextual and bidirectional rules - is listed in
docs/design/DNS.md section 3.
Records#
type RecordData with Show, Equals, Hash {
case A(address: Ipv4Address)
case Aaaa(address: Ipv6Address)
case Cname(target: DomainName)
case Mx(preference: Int, exchange: DomainName)
case Txt(strings: List<Bytes>)
case Srv(priority: Int, weight: Int, port: Int, target: DomainName)
case Ns(server: DomainName)
case Soa(primaryServer: DomainName, responsibleMailbox: DomainName, serial: Int, refresh: Int, retry: Int, expire: Int, minimum: Int)
case Ptr(target: DomainName)
case Caa(flags: Int, tag: String, value: Bytes)
case Svcb(priority: Int, target: DomainName, parameters: List<ServiceParameter>)
case Https(priority: Int, target: DomainName, parameters: List<ServiceParameter>)
case Unknown(recordType: RecordType, data: Bytes)
fn recordType(): RecordType
fn address(): IpAddress?
fn show(): String
}
type ServiceParameter with Show, Equals, Hash {
case Mandatory(keys: List<Int>)
case Alpn(protocols: List<String>)
case NoDefaultAlpn
case Port(port: Int)
case Ipv4Hint(addresses: List<Ipv4Address>)
case EncryptedClientHello(configurations: Bytes)
case Ipv6Hint(addresses: List<Ipv6Address>)
case Other(key: Int, value: Bytes)
fn key(): Int
fn show(): String
}
type Record with Show, Equals, Hash {
name: DomainName
recordClass: RecordClass = RecordClass.Internet
timeToLive: Int
data: RecordData
fn recordType(): RecordType
fn show(): String
}
type RecordType with Show, Equals, Hash {
case A
case Ns
case Cname
case Soa
case Ptr
case Mx
case Txt
case Aaaa
case Srv
case Opt
case Svcb
case Https
case Any
case Caa
case Other(code: Int)
static fn of(code: Int): Self
fn code(): Int
fn show(): String
fn equals(other: Self): Bool
fn hash(): Int
}
type RecordClass with Show, Equals, Hash {
case Internet
case Chaos
case Hesiod
case Any
case Other(code: Int)
static fn of(code: Int): Self
fn code(): Int
fn show(): String
fn equals(other: Self): Bool
fn hash(): Int
}The data of a record is typed by its case - addresses of std/ip, names, numbers in the range of their field -
and a type this package does not model is Unknown with its bytes. ServiceParameter types the keys of RFC 9460
(Alpn, Port, Ipv4Hint, Ipv6Hint, ...) and keeps the rest as Other(key, value). RecordType, RecordClass,
Operation and ResponseCode are closed sets with Other(code) that compare by their number, so
RecordType.Other(28) == RecordType.Aaaa. show() writes the presentation format of a zone file:
example.test. 300 IN A 192.0.2.1.
Messages#
type Message with Show, Equals, Hash, TryFrom<Bytes, DnsError> {
identifier: Int = 0
kind: MessageKind = MessageKind.Query
operation: Operation = Operation.Query
authoritative: Bool = false
truncated: Bool = false
recursionDesired: Bool = false
recursionAvailable: Bool = false
authenticated: Bool = false
checkingDisabled: Bool = false
responseCode: ResponseCode = ResponseCode.NoError
questions: List<Question> = []
answers: List<Record> = []
authority: List<Record> = []
additional: List<Record> = []
edns: Edns? = None
static fn tryFrom(bytes: Bytes): Result<Self, DnsError>
fn encoded(): Result<Bytes, DnsError>
fn answersFor(name: DomainName, recordType: RecordType): List<Record>
fn addresses(of: DomainName): List<IpAddress>
}
type MessageKind with Show, Equals, Hash {
case Query
case Response
}
type Question with Show, Equals, Hash {
name: DomainName
recordType: RecordType
recordClass: RecordClass = RecordClass.Internet
fn show(): String
}
type Edns with Show, Equals, Hash {
payloadSize: Int = 1232
version: Int = 0
dnssecOk: Bool = false
options: List<EdnsOption> = []
}
type EdnsOption with Show, Equals, Hash {
code: Int
data: Bytes
}
type Operation with Show, Equals, Hash {
case Query
case Status
case Notify
case Update
case Other(code: Int)
static fn of(code: Int): Self
fn code(): Int
fn show(): String
fn equals(other: Self): Bool
fn hash(): Int
}
type ResponseCode with Show, Equals, Hash {
case NoError
case FormatError
case ServerFailure
case NameError
case NotImplemented
case Refused
case NameExists
case RecordSetExists
case RecordSetMissing
case NotAuthoritative
case NotInZone
case BadVersion
case BadCookie
case Other(code: Int)
static fn of(code: Int): Self
fn code(): Int
fn show(): String
fn equals(other: Self): Bool
fn hash(): Int
}
fn query(name: DomainName, recordType: RecordType, identifier: Int, recursionDesired: Bool = true, dnssecOk: Bool = false): Bytes
fn readResponse(bytes: Bytes, query: Bytes): Result<Message, DnsError>
type DnsError with Show, Error {
case InvalidName(name: String, reason: String)
case Truncated(offset: Int, part: String)
case InvalidPointer(offset: Int, target: Int)
case PointerLoop(offset: Int)
case InvalidRecord(recordType: RecordType, offset: Int, reason: String)
case InvalidMessage(offset: Int, reason: String)
case Unencodable(reason: String)
case UnexpectedResponse(reason: String)
fn show(): String
}
extend DnsError with From<ReadError>Message.tryFrom reads the bytes of a message, following compression pointers with a guard against loops and pointers
outside the message; the EDNS0 record becomes edns, and its extended bits join responseCode. Bytes from the network
cannot make it panic: every problem is a DnsError that says where. encoded() compresses every name RFC 1035 allows
to compress and refuses a value its field cannot carry. query answers the bytes of a standard query with EDNS0
(1232 bytes of UDP payload offered), and readResponse reads a response and checks it against the query's bytes -
identifier, operation, question. answersFor follows the CNAME chain of the answer section, and addresses(of:) is
the same for A and AAAA. The AD bit is read into authenticated; DNSSEC validation is not part of the package.
Related#
- std/ip - the addresses an
Aand anAAAArecord hold. - std/uri -
Host.domainName()answers the registered name of a URI as aDomainName. - std/network -
resolveasks the system's resolver today;lookupover UDP and TCP will send these messages. - The standard library - the other packages.