Proposal for swift-dns-types

In line with the existing swift-http-types and the workgroup vision statement, I'd like to propose the creation of an official "swift-dns-types" package. This would focus on faithfully implementing the formats described in RFC 1035 and its (many) updates.

There is a variety of existing work that could stand to benefit from common currency types, as with HTTP: swift-async-dns-resolver is an obvious example. DNS has the benefit of being vastly simpler than HTTP, of course, particularly as support for specific record types may be implemented incrementally.

Initial work could focus on implementing the basic RR format, with subsequent work focusing on the message format. Considerations such as transports and master files may be considered out of scope in a similar fashion to the swift-http-types package.

2 Likes

I'd like to mention that although I haven't been able to work on GitHub - swift-dns/swift-dns: A high-performance Swift DNS library built on top of SwiftNIO; aiming to provide DNS client, resolver and server implementations. ¡ GitHub much lately, it does already have a good implementation of all DNS types in the DNSModels.

My memory is getting a bit rusty regarding it, but I know the impls are somewhat complete (e.g. contains DNSSEC as well), and that I have added a bunch of tests that run over packets I've captured via Wireshark. There are also integration tests that run against a real DNS server (was it Google's or Cloudflare's?, can't recall).
There are some benchmarks as well.
I also had plans for more test coverage and perhaps using existing test suites or such (example, Bonjour Conformance Test for mDNS), but did not get to that yet (mDNS specifically is not implemented anyway).

This is also related to: Requirements for IP address and port APIs - #3 by MahdiBM

swift-dns is where I noticed we lack such basic types and started putting those types together. swift-endpoint stuff used to be part of swift-dns before I decouple them. See [Pitch] Standard Network Address types if you're curious about the history.

1 Like

I think keeping currency types distinct is important (being able to write complex HTTP code without actually touching a server implementation has been a revelation), but that'd certainly be a good starting point.[1]

The important part, of course, is that there's widespread community buy-in. This is a bit of a waste of time otherwise.


  1. I see a few issues with `Record`, like using the wrong TTL type, but it's largely what I was envisioning. â†Šī¸Ž

1 Like

Right. I can likely find some time to decouple the types from swift-dns. Or if someone else wants to do it, swift-dns's types should give a good head start.

RFC 1035 - Domain names - implementation and specification says:

TTL             positive values of a signed 32 bit number.

I had not noticed that. swift-dns uses UInt32 which I guess is just as incorrect as Int32.
Users could pass values in range of UInt32.max (4294967295) to Int32.max (2147483647) which are invalid. In case of Int32, users can pass <0 values and that'll be incorrect as well.
In a case like this I'd rather introduce a TTL type that encapsulates a 32 bit int (signed or not) and provide correctness guarantees through that type.

The spec is quite clear: I'm not sure why it's like this, but it'd make more sense to just use Int32, perhaps with a precondition check.

I mean it's not a big deal, we can come into some conclusion, but positive values of a signed 32 bit number is not what Int32 is, which is all values of a signed 32 bit number.

I know in Swift there is a tendency to just precondition out-of-bounds values, and I don't particularly dislike it, just that I like to be as explicit as possible with API design, and that's why I prefer a dedicated TLL type.
I think Int32+precondition would do just fine, but considering that ttl is one of those "dynamic" values which could mean different things in different contexts of DNS e.g. in OPT Record TTL Field, a dedicated TTL type could make the APIs even more explicit and clear.

2 Likes

Int32 is a signed 32 bit number, so that's what should be stored. For all I know the sign bit could be overloaded by some future specification to mean something else. Short of a compile-time check, a precondition is the idiomatic way to implement this sort of condition: Swift itself has ample precedent in the form of using Int for Collection.count (for some reason).

That does complicate things a bit. An enum with associated values is my immediate thought, but I believe that'd require extra bits to discriminate between cases. Perhaps a TTL associated type with a Record protocol?

Or maybe we're overthinking it: we could have the base Record be as described in RFC 1035, with more specialized forms like that wrapping it in a RawRepresentable that provides additional fields with read accessors.

Maybe a combination thereof? Make a Record protocol that requires Int32, with BaseRecord implementing it and other implementations like OptRecord adding read accessors? I'm not sure if that would allow compiling down to the exact bit layout in the spec when used as a generic, though, which I feel is nonnegotiable.


This is the sort of sharp edge that makes it worth separating into its own package.

What I'm thinking about is something like this:

struct TTL {
    let value: Int32

    init? (value: Int32) { /* return nil if negative*/ }
}

/// For something like DoH, not in swift-dns currently
/// Essentially the type can take care of itself for all operations, instead of we having to handle an `Int32` everywhere we use it.
extension TTL: Codable {
    init(from decoder: any Decoder) throws { /*throw if negative*/ }
}

struct OPTTTLFieldOrWhatever {
    let underlyingValue: TTL

    var fieldOne: Int_ { /* get/set field, likely via some bitwise operations*/ }
    /// .....
}

Rough Idea.

Currently, in some cases, these "combined" fields (e.g. one Int32, but multiple different inner values) are eagerly decoded, and in some cases not. I'd prefer to make it always not eagerly decode, and provide get/set accessors to do the work, like in the example above.

Prime example of this in swift-dns is the DNS header impl:

Click for Header.Bytes3And4

These are simple bitwise operations but they do still have tests.

Don't mind the custom operators such as &<<<, we can likely get rid of them.

/// Represents Bytes 16 to 31 in the DNS header.
    /// That is, QR, OPCODE, AA, TC, RD, RA, ZZ, AD, CD and RCODE.
    public struct Bytes3And4: Sendable {
        /// private
        public var rawValue: UInt16

        /// TODO: check whether `truncatingIfNeeded` has a positive impact on performance
        /// Compared to just using init().

        public var messageType: MessageType {
            get {
                MessageType(rawValue: UInt8(truncatingIfNeeded: self.rawValue >> 15))!
            }
            set {
                /// clear the 15th bit then set it to the new value
                self.rawValue =
                    (self.rawValue & 0b01111111_11111111)
                    | UInt16(truncatingIfNeeded: newValue.rawValue) &<<< 15
            }
        }
        public var opCode: OPCode {
            get {
                OPCode(rawValue: UInt8((rawValue >> 11) & 0xF))!
            }
            set {
                self.rawValue =
                    (self.rawValue & 0b10000111_11111111)
                    | UInt16(truncatingIfNeeded: newValue.rawValue) &<<< 11
            }
        }
        public var authoritative: Bool {
            get {
                (rawValue & 0b00000100_00000000) == 0b00000100_00000000
            }
            set {
                switch newValue {
                case true: rawValue = (rawValue | 0b00000100_00000000)
                case false: rawValue = (rawValue & 0b11111011_11111111)
                }
            }
        }
        public var truncation: Bool {
            get {
                (rawValue & 0b00000010_00000000) == 0b00000010_00000000
            }
            set {
                switch newValue {
                case true: rawValue = (rawValue | 0b00000010_00000000)
                case false: rawValue = (rawValue & 0b11111101_11111111)
                }
            }
        }
        public var recursionDesired: Bool {
            get {
                (rawValue & 0b00000001_00000000) == 0b00000001_00000000
            }
            set {
                switch newValue {
                case true: rawValue = (rawValue | 0b00000001_00000000)
                case false: rawValue = (rawValue & 0b11111110_11111111)
                }
            }
        }
        public var recursionAvailable: Bool {
            get {
                (rawValue & 0b00000000_10000000) == 0b00000000_10000000
            }
            set {
                switch newValue {
                case true: rawValue = (rawValue | 0b00000000_10000000)
                case false: rawValue = (rawValue & 0b11111111_01111111)
                }
            }
        }
        public var authenticData: Bool {
            get {
                (rawValue & 0b00000000_00100000) == 0b00000000_00100000
            }
            set {
                switch newValue {
                case true: rawValue = (rawValue | 0b00000000_00100000)
                case false: rawValue = (rawValue & 0b11111111_11011111)
                }
            }
        }
        public var checkingDisabled: Bool {
            get {
                (rawValue & 0b00000000_00010000) == 0b00000000_00010000
            }
            set {
                switch newValue {
                case true: rawValue = (rawValue | 0b00000000_00010000)
                case false: rawValue = (rawValue & 0b11111111_11101111)
                }
            }
        }
        public var responseCode: ResponseCode {
            get {
                ResponseCode(rawValue & 0b00000000_00001111)
            }
            set {
                self.rawValue = (self.rawValue & 0b11111111_11110000) | newValue.rawValue
            }
        }

        public init(rawValue: UInt16) {
            self.rawValue = rawValue
        }
    }

For reference:

Click for current EDNS impl

EDNS.swift

/// Edns implements the higher level concepts for working with extended dns as it is used to create or be
/// created from OPT record data.
@available(SwiftStdlib 5.1, *)
public struct EDNS: Sendable {
    /// EDNS flags
    ///
    /// <https://www.rfc-editor.org/rfc/rfc6891#section-6.1.4>
    public struct Flags: Sendable {
        /// DNSSEC OK bit as defined by RFC 3225
        public var dnssecOk: Bool
        /// Remaining bits in the flags field
        ///
        /// Note that the most significant bit in this value is represented by the `dnssec_ok` field.
        /// As such, it will be zero when decoding and will not be encoded.
        ///
        /// Unless you have a specific need to set this value, we recommend leaving this as zero.
        public var z: UInt16

        var rawValue: UInt16 {
            switch self.dnssecOk {
            case true:
                return 0x8000 | self.z
            case false:
                return 0x7FFF & self.z
            }
        }

        public init(dnssecOk: Bool, z: UInt16) {
            self.dnssecOk = dnssecOk
            self.z = z
        }
    }

    // high 8 bits that make up the 12 bit total field when included with the 4bit rcode from the
    // header (from TTL)
    public var rcodeHigh: UInt8
    // Indicates the implementation level of the setter. (from TTL)
    public var version: UInt8
    public var flags: Flags
    // max payload size, minimum of 512, (from RR CLASS)
    public var maxPayload: UInt16
    public var options: OPT

    var ttl: UInt32 {
        (UInt32(self.rcodeHigh) &<<< 24)
            | (UInt32(self.version) &<<< 16)
            | UInt32(self.flags.rawValue)
    }

    public init(rcodeHigh: UInt8, version: UInt8, flags: Flags, maxPayload: UInt16, options: OPT) {
        self.rcodeHigh = rcodeHigh
        self.version = version
        self.flags = flags
        self.maxPayload = maxPayload
        self.options = options
    }
}

@available(SwiftStdlib 5.1, *)
extension EDNS {
    package init(fromOPTRecord record: consuming Record) {
        assert(record.rdata.recordType == .OPT)
        self.rcodeHigh = UInt8(truncatingIfNeeded: (record.ttl & 0xFF00_0000) >> 24)
        self.version = UInt8(truncatingIfNeeded: (record.ttl & 0x00FF_0000) >> 16)
        self.flags = Flags(from: record.ttl)
        self.maxPayload = record.dnsClass.rawValue
        self.options = OPT(fromOPTRData: record.rdata)
    }
}

@available(SwiftStdlib 5.1, *)
extension EDNS {
    package func toRecord() -> Record {
        Record(
            nameLabels: DomainName.root,
            dnsClass: DNSClass(forOPT: self.maxPayload),
            ttl: self.ttl,
            rdata: RData.OPT(self.options)
        )
    }
}

@available(SwiftStdlib 5.1, *)
extension EDNS.Flags {
    package init(from ttl: UInt32) {
        let first16bits = UInt16(truncatingIfNeeded: ttl & 0x0000_FFFF)
        self.dnssecOk = (first16bits & 0x8000) == 0x8000
        self.z = first16bits & 0x7FFF
    }
}

About using protocols, it'd be fine to use protocols if we really need to, but generally I'm not in favor of using protocols mostly to make sure there is no performance hit by users using any MyProtocol.

For a specification like DNS which we already know all the cases, we can just use enums, specially since we now have @nonexhaustive enums since Swift 6.3.
Then If we want to allow custom values or "unknown" values to be passed, we can have one last enum case which simply just allows the base type to be passed.
The ability to pass custom values can be helpful for users that want to experiment ahead with incoming RFCs, which won't be uncommon, but at the same time one could simply just fork the repo and add stuff as well.

I guess I have already implemented all these so my preference is clear in the code (with the caveat that my preference might have had some changes over the past ~year which I haven't worked on swift-dns too much.).

One way I've found nice to provide wrapper types is using dynamicMemberLookup:

Click for SpecializedMessage impl

Specialized Types.swift

/// A ``Message`` that provides convenient access to its ``answers`` by taking care of unwrapping them to ``SpecializedRecords``.
///
/// This type implements ``@dynamicMemberLookup`` over the ``message``, then shadows ``message.answers`` which
/// is of type ``[Record]``, by providing a ``answers`` property which is of the specialized type ``SpecializedRecords<RDataType>``.
@available(SwiftStdlib 5.1, *)
@dynamicMemberLookup
public struct SpecializedMessage<RDataType: RDataConvertible>: Sendable {
    public var message: Message

    /// TODO: can do more than just `answers`?

    /// Use `message.answers` if you want to access the raw records, or if you want to modify them.
    public var answers: SpecializedRecords<RDataType> {
        SpecializedRecords(records: self.message.answers)
    }

    public subscript<T>(dynamicMember member: KeyPath<Message, T>) -> T {
        /// FIXME: use `read`/`modify` accessors?
        get {
            self.message[keyPath: member]
        }
    }

    public subscript<T>(dynamicMember member: WritableKeyPath<Message, T>) -> T {
        /// FIXME: use `read`/`modify` accessors?
        get {
            self.message[keyPath: member]
        }
        set {
            self.message[keyPath: member] = newValue
        }
    }

    public init(message: Message) {
        self.message = message
    }
}

/// A lazy sequence of ``Record``s that correspond to the given ``RDataType``.
///
/// For example you might use the dns resolver to resolve A records for `www.example.com.`.
/// In this case, `www.example.com.` only has CNAME records and no A records.
/// The upstream dns resolvers usually respond with 1-2 few CNAME records followed by 2+ A records.
///
/// What this type does is it filters out the CNAME records and only returns the A records to
/// make the end user's life easier as they never requested CNAME records anyway.
@available(SwiftStdlib 5.1, *)
public struct SpecializedRecords<RDataType: RDataConvertible>: Sendable {
    public let records: TinyFastSequence<Record>

    public init(records: TinyFastSequence<Record>) {
        self.records = records
    }
}

@available(SwiftStdlib 5.1, *)
extension SpecializedRecords: Sequence {
    /// Complexity: O(n)
    @inlinable
    public var undeterminedCount: Int {
        self.count
    }

    /// A ``Record`` that provides convenient access to its ``rdata`` by taking care of unwrapping it to ``RDataType``.
    ///
    /// This type implements ``@dynamicMemberLookup`` over the ``record``, then shadows ``record.rdata`` which
    /// is of type ``RData``, by providing a ``rdata`` property which is of the specialized type ``RDataType``.
    ///
    /// This type guarantees that `rdata` and `record.rdata` are the same.
    /// That's the reason why both properties are marked as `private(set)`:
    /// So we don't have to manage syncing `rdata` and `record.rdata` manually.
    @dynamicMemberLookup
    public struct Element: Sendable {
        public let record: Record
        public var rdata: RDataType {
            try! RDataType(rdata: record.rdata)
        }

        public subscript<T>(dynamicMember member: KeyPath<Record, T>) -> T {
            _read {
                yield self.record[keyPath: member]
            }
        }

        public init(record: Record) throws(FromRDataTypeMismatchError<RDataType>) {
            self.record = record
            /// Test once to ensure the RData is expected
            _ = try RDataType(rdata: record.rdata)
        }
    }

    @inlinable
    public func makeIterator() -> Iterator {
        Iterator(base: self)
    }

    /// An iterator over a ``SpecializedRecords`` that filters out records that
    /// don't correspond to ``RDataType``.
    public struct Iterator: Sendable, IteratorProtocol {
        @usableFromInline
        var baseIterator: TinyFastSequence<Record>.Iterator

        @inlinable
        init(base: SpecializedRecords<RDataType>) {
            self.baseIterator = base.records.makeIterator()
        }

        @inlinable
        public mutating func next() -> Element? {
            while true {
                guard let record = self.baseIterator.next() else {
                    return nil
                }

                /// If the record is of the wrong type, we skip it and continue to the next record.
                /// For example we could be getting both `CNAME` and `A` record for a domain name that
                /// is `CNAME`ed to another domain name with `A` records.
                ///
                /// So here if the query is a A query, we simply ignore the `CNAME`s.
                if let element = try? Element(record: record) {
                    return element
                } else {
                    /// Got a bad record. Skip and continue to the next record.
                    continue
                }
            }
        }
    }
}

...
/// A ``Record`` that provides convenient access to its ``rdata`` by taking care of unwrapping it to ``RDataType``.
    ///
    /// This type implements ``@dynamicMemberLookup`` over the ``record``, then shadows ``record.rdata`` which
    /// is of type ``RData``, by providing a ``rdata`` property which is of the specialized type ``RDataType``.
    ///
    /// This type guarantees that `rdata` and `record.rdata` are the same.
    /// That's the reason why both properties are marked as `private(set)`:
    /// So we don't have to manage syncing `rdata` and `record.rdata` manually.
    @dynamicMemberLookup
    public struct Element: Sendable {
        public let record: Record
        public var rdata: RDataType {
            try! RDataType(rdata: record.rdata)
        }

        public subscript<T>(dynamicMember member: KeyPath<Record, T>) -> T {
            _read {
                yield self.record[keyPath: member]
            }
        }

        public init(record: Record) throws(FromRDataTypeMismatchError<RDataType>) {
            self.record = record
            /// Test once to ensure the RData is expected
            _ = try RDataType(rdata: record.rdata)
        }
    }

Notice that record itself has a rdata member. This type is simply shadowing rdata of record with a explicitly-typed rdata, compared to record.rdata which is an enum with all the cases.

struct TTL: RawRepresentable {
  let rawValue: Int32
  
  init?(rawValue: RawValue) {
    guard rawValue >= 0 else { return nil }
    self.rawValue = rawValue
  }
}

I do think RawRepresentable is sorely underappreciated for its ability to immediately communicate semantics.

There will always be a performance hit for using existentials, and that should be strongly discouraged (in all Swift code, really). I think this should actually be designed around minimizing runtime overhead of any sort, with an eye towards embedded systems: any abstractions should get flattened at compile-time.

Swift Collections has a BitSet for this, I believe.


DNS record types are added at a rather brisk cadence, and it is not realistic to implement every nuance immediately: the package should be designed with the expectation that it will never be comprehensive, especially in early versions.

In fact, I think it's worth seriously considering whether record types should be interpreted in this package at all. swift-http-types does not attempt to provide more logic around HTTP status codes than simply mapping them to error messages, and for good reason. Once the core currency types are implemented in this package, other packages can build upon them.

1 Like

Oh, and TTL could probably stand to implement DurationProtocol as well.

1 Like

Lightweight, decoupled libraries for DNS types, network address types, and so forth would be very useful to us. Right now we have to carry around our own network address and DNS type implementations.

Network address types? Aren't IP addresses just unsigned integers?

enum IPAddress {
  case v4(UInt32)
  case v6(UInt128)
}

I suppose a solid LosslessStringConvertible implementation might be a bit thorny. And that'd be an awfully memory-inefficient IPv4 representation, so an integer generic would probably be a better approach.

At any rate, I think DNS, specifically the basic primitives, is a more urgent need.

Is it? Or is it [UInt8] (unsigned char[16]for in6_addr)?

For me what's important is an address type that can parse and output string representations (RFC 5952 for IPv6 output), supports literate address arithmetic and range checking, and bridges efficiently to C types. The internal representation should be whatever is space and performance efficient, and best suits the use cases.

As you say, this discussion is about DNS and I don't want to distract from that. Within the context of a DNS library where AAAA records only contain the address bytes, an enum address should be fine. Those UInt values have to come from somewhere though, right?

DNS itself does not require any conception of a network address. This is actually a good example of why I think swift-dns-types should avoid attempting to interpret RDATA entirely: it often requires careful domain-tailored logic.

Implementing an understanding of an AAAA record might be pretty easy, but what about an OPENPGPKEY record? Better to leave it out: if necessary, distinct packages could build upon swift-dns-types for each record. Here's an example of what that could look like.

public import struct DNSTypes.Record

public struct AAAARecord: RawRepresentable {
  let rawValue: Record

  /// The IPv6 address contained in the record.
  ///
  /// See [Section 2.2 of RFC 3596](https://www.rfc-editor.org/info/rfc3596/#section-2.2).
  var address: UInt128 { // or custom IPv6 type defined elsewhere
    get { rawValue.data.load(as: UInt128.self,  endianness: .bigEndian) }
    set { rawValue.data.store(newValue, as: UInt128.self, endianness: .bigEndian) }
  }
  
  init?(rawValue: RawValue) {
    guard rawValue.type == .aaaa else { return nil }
    self.rawValue = rawValue
  }
}

When using swift-nio, you could simply use a slice of the same ByteBuffer of the DNS packet since ByteBuffer is a pointer+readerIndex+writerIndex and you can use its readSlice/moveReader/WriterIndex functions to modify those indices so the ByteBuffer starts pointing at only those 4/16 bytes of the IP address you want. This would be a standard usecase, not "hacking", to be clear.

Choosing [UInt8] as the underlying type would mean you're doing 1 heap allocation for storing each IP address, which would be a clear performance loss.

Using an integer type is generally a good choice though for multiple reasons. There is a chance that the "ByteBuffer" way can, in specific-contexts / known-usecases win against this "parse-into-uint" way, but for a general purpose implementation parsing into integers is most likely the best way for compatibility since there will be instances where you want to initialize an ip address from outside a DNS packet context in which case you have no "ByteBuffer" without actually committing to allocate one. On the other hand, parsing/writing a dns-format IP address into a uint is trivial:

struct IPv6Address {
    var address: UInt128
    /// Initialize an `IPv6Address` by parsing the 16 bytes representing it.
    public init?(parsing span: Span<UInt8>) {
        guard span.count >= 16 else {
            return nil
        }

        /// You could also do UInt128() << shift and skip this `_low`, `_high` initializer but might 
        /// have to satisfy the compiler to not timeout due to the expression being too big.
        self.address =
            UInt128(
                _low: UInt64(span[8]) << 56
                    | UInt64(span[9]) << 48
                    | UInt64(span[10]) << 40
                    | UInt64(span[11]) << 32
                    | UInt64(span[12]) << 24
                    | UInt64(span[13]) << 16
                    | UInt64(span[14]) << 8
                    | UInt64(span[15]),
                _high: UInt64(span[0]) << 56
                    | UInt64(span[1]) << 48
                    | UInt64(span[2]) << 40
                    | UInt64(span[3]) << 32
                    | UInt64(span[4]) << 24
                    | UInt64(span[5]) << 16
                    | UInt64(span[6]) << 8
                    | UInt64(span[7])
            )
    }
}

It's clear what exactly is going on in here (copy 16 bytes to UInt128) so the compiler can easily make short code of this.

For C introp there could simply be functions like this (from swift-endpoint):

@available(SwiftStdlib 5.1, *)
extension IPv6Address {
    /// Calls `body` with a pointer to a null-terminated C string of this address's textual
    /// representation. For example `IPv6Address(0x2001, 0x0DB8, 0, 0, 0, 0, 0, 1)`
    /// results in the C string `"2001:db8::1"`.
    ///
    /// Unlike `description`, the textual representation is **not** enclosed in square brackets,
    /// because that is the presentation format expected by C APIs, which reject the bracketed form.
    ///
    /// Parameters:
    /// - `body`: A closure that allows access to a `Span<CChar>` of the address's textual representation.
    ///    You can use `span.withUnsafeBufferPointer { $0.baseAddress! /*UnsafePointer<CChar>*/ }` on the
    ///    span if you need to, for C interoperability.
    /// - Returns: The result of the closure.
    @inlinable
    public func withCString<Result>(
        _ body: (Span<CChar>) throws -> Result
    ) rethrows -> Result {
        try unsafe self.makeDescription(
            enclosingInSquareBrackets: false
        ) { (maxWriteableBytes, writeBytes) in
            try withUnsafeTemporaryAllocation(
                of: UInt8.self,
                capacity: maxWriteableBytes
            ) { buffer in
                let count = unsafe writeBytes(buffer)
                /// We're counting on our own `makeDescription`'s underlying impl to never actually
                /// write as many bytes as it has requested so we don't need 1 more byte of alloc
                /// for the null terminator. That's always true right now since the impl needs extra
                /// headroom for speculative writes it performs.
                assert(count < buffer.count)
                unsafe buffer[count] = 0
                return try unsafe buffer.withMemoryRebound(to: CChar.self) { cBuffer in
                    let range = unsafe ClosedRange<Int>(uncheckedBounds: (0, count))
                    let limitedSpan = unsafe cBuffer.span.extracting(unchecked: range)
                    return try body(limitedSpan)
                }
            }
        }
    }

    /// Initialize an IPv6 address from a null-terminated C string of its textual representation.
    /// For example `"2001:db8:1111::"` will parse into `2001:DB8:1111:0:0:0:0:0`,
    /// or in other words `0x2001_0DB8_1111_0000_0000_0000_0000_0000`.
    /// Can also parse IPv4-mapped IPv6 addresses in format `"::FFFF:204.152.189.116"`.
    ///
    /// This is useful for interoperability with C APIs that produce null-terminated strings.
    ///
    /// Parameters:
    /// - `cString`: A pointer to a null-terminated C string of the address's textual representation.
    @inlinable
    public init?(cString: UnsafePointer<CChar>) {
        let length = unsafe UTF8._nullCodeUnitOffset(in: cString)
        let buffer = unsafe UnsafeBufferPointer(start: cString, count: length)
        let result = unsafe buffer.withMemoryRebound(to: UInt8.self) {
            IPv6Address(textualRepresentation: unsafe $0.span)
        }
        guard let result else {
            return nil
        }
        self = result
    }
}
Why withUnsafeTemporaryAllocation?

it does stack allocation as opposed to using e.g. a [UInt8]'+array.withUnsafeBufferPointer which is a heap alloc. It has its own downsides though, like the need for a closure since the compiler needs to know where the the stack space is no longer needed and to ensure correctness (like inserting a stack canary). Or the usual "will I run out of stack space" concerns which don't apply in cases like this where it's guaranteed that you only need less than 50 bytes.)

Being this "lazy" about the decodings doesn't look necessary to me. The DNS data structure layouts and size characteristics are well defined and never change so we can simply not be concerned.

There are however a bunch of instances where we would want to ensure we're not being strict since DNS is evolving every year.

A simple example would be (notice the unknown case, in addition to @nonexhaustive):

/// Operation code for queries, updates, and responses
///
/// [RFC 1035, DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION, November 1987](https://tools.ietf.org/html/rfc1035)
///
/// ```text
/// OPCODE          A four bit field that specifies kind of query in this
///                 message.  This value is set by the originator of a query
///                 and copied into the response.  The values are:
///
///                 0               a standard query (QUERY)
///
///                 1               an inverse query (IQUERY)
///
///                 2               a server status request (STATUS)
///
///                 3-15            reserved for future use
/// ```
/// Some OPCodes are defined in later RFCs, and some are deprecated.
@nonexhaustive
public enum OPCode: Sendable {
    /// Query request [RFC 1035](https://tools.ietf.org/html/rfc1035)
    case Query
    /// Status message [RFC 1035](https://tools.ietf.org/html/rfc1035)
    case Status
    /// Notify of change [RFC 1996](https://tools.ietf.org/html/rfc1996)
    case Notify
    /// Update message [RFC 2136](https://tools.ietf.org/html/rfc2136)
    case Update
    /// DNS Stateful Operations message [RFC 8499](https://tools.ietf.org/html/rfc8499)
    case DSO
    /// Any other opcode
    case unknown(UInt8)
}

extension OPCode: RawRepresentable {
    public init?(rawValue: UInt8) {
        switch rawValue {
        case 0: self = .Query
        case 2: self = .Status
        case 4: self = .Notify
        case 5: self = .Update
        case 6: self = .DSO
        case 1, 3, 7...15: self = .unknown(rawValue)
        default: return nil
        }
    }

    public var rawValue: UInt8 {
        switch self {
        case .Query: return 0
        case .Status: return 2
        case .Notify: return 4
        case .Update: return 5
        case .DSO: return 6
        case .unknown(let value): return value
        }
    }
}

Furthermore, in some cases you can't know where the field even is before decoding the field behind it (at least partially).
For example if you want to decode field2, but field1 is a length-prefixed string or an array of multiple values. Both cases exist in DNS; former case on a regular basis.

Why would that matter? The usual scenario is sequential streaming, not random access.

I feel you're contradicting yourself here. My point is that some parts of DNS are fundamental, and those should be separated out in the interest of catering to those who do not need other parts. This has the advantage of making serialization and deserialization as fast as possible as well.

How about this? I believe it's very important to make the memory layout line up with the specification. This isn't perfect, but it's a start.

protocol Record: RawRepresentable where RawValue == Record.RawValue { }

public enum Record {
  // @frozen
  public struct RawValue {
    public var name: Domain
    public var type: `Type`
    public var `class`: Class
    public var ttl: Int32
    public var data: Data
  }
}

extension Record.RawValue {
  // I'll come back to this later, this is just a starting point
  // @frozen
  public indirect enum Domain {
    case label(String, Domain)
    case root
  }

  @frozen
  public struct `Type`: RawRepresentable {
    public let rawValue: UInt16
  }

  @frozen
  public struct Class: RawRepresentable {
    public let rawValue: UInt16
  }

  // @frozen
  public struct Data {
    public private(set) var rdLength: UInt16
    public var rdData: UniqueArray<UInt8> // Maybe a RawSpan?
  }
}

extension Record.RawValue.`Type` {
  public static let a = Self(rawValue: 1)
  public static let ns = Self(rawValue: 2)
  [â€Ļ]
  public static let ta = Self(rawValue: 32768)
  public static let dlv = Self(rawValue: 32769)
}

extension Record.RawValue.Class {
  public static let in = Self(rawValue: 1)
  public static let cs = Self(rawValue: 2)
  public static let ch = Self(rawValue: 3)
  public static let hs = Self(rawValue: 4)
}

DNS query/response is usually just 1 packet. In some cases there can be truncation/need for multiple packets, and in other relatively rare cases, some DNS requests/responses actually are supposed to be multiple packets at least (e.g. moving zones, IIRC).

So generally, true streaming will be nice to have, but it's not as big of a deal as opposed to for example in HTTP.

I think what could happen for usual responses/queries, reasonably, is that:

  • For decoding, we can decode the header first, and the user can query decoding of the rest via a handle if needed. Either in streaming or in sequential fashion.
  • There could be a func to just "decode all" for easier use.
  • The reason is, most queries are again just 1 packet. In a server/client, you can decode the header and match it against your known database or such to decide what to do next. For example if via a client, you've received a DNS message with header's id set to a known A query, then you can just decode the response. In other cases you might want to stream it.

What happens with the cost to decoding the whole packet?

    1. It can get pretty complicated and require very custom logic (on the side of a dns client/server library) to manually make that happen.
    • For example EDNS is always at the end of a packet.
    • Or if you want to decode "answers", you need to have already decoded "queries".
    • Or for "name-servers", you need to already have decoded "answers".
    • So essentially, you'd need partial decoding at least, in most cases. To at least decode each element in queries, answers etc... so you can go to decode next.
    • This adds a lot of complexity for partial decoding, and in any case, if you've done the partial decoding, then you may as well just decode the whole thing which could add almost no overhead. See below.
    1. We need to ensure the cost to decode is minimal, and it's possible to do:
    • If there is a value to be represented by a Swift string or by an array of bytes, don't decode it.
      • Instead simply store the ByteBuffer, with modified reader/writer indices (as mentioned in above posts).
    • I think I might have forgot 1 more thing, but basically that's it.
    • This way decoding is very cheap so we don't need to add code complexity.
    • Essentially we need to ensure some things, such as extra heap allocs (thus, more ByteBuffer usage), don't happen, and then we can just decode sequentially and know the overhead is minimal.