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.

  • std/ip - the addresses an A and an AAAA record hold.
  • std/uri - Host.domainName() answers the registered name of a URI as a DomainName.
  • std/network - resolve asks the system's resolver today; lookup over UDP and TCP will send these messages.
  • The standard library - the other packages.
Esc