//===----------------------------------------------------------------------===// // Copyright © 2026 Apple Inc. and the container project authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// import Foundation /// A DNS message (query or response). /// /// Wire format (RFC 1035): /// ``` /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | ID | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// |QR| Opcode |AA|TC|RD|RA| Z | RCODE | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | QDCOUNT | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | ANCOUNT | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | NSCOUNT | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// | ARCOUNT | /// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ /// ``` public struct Message: Sendable { /// Header size in bytes. public static let headerSize = 12 /// Transaction ID. public var id: UInt16 /// Message type (query or response). public var type: MessageType /// Operation code. public var operationCode: OperationCode /// Authoritative answer flag. public var authoritativeAnswer: Bool /// Truncation flag. public var truncation: Bool /// Recursion desired flag. public var recursionDesired: Bool /// Recursion available flag. public var recursionAvailable: Bool /// Response code. public var returnCode: ReturnCode /// Questions in this message. public var questions: [Question] /// Answer resource records. public var answers: [ResourceRecord] /// Authority resource records. public var authorities: [ResourceRecord] /// Additional resource records. public var additional: [ResourceRecord] /// Creates a new DNS message. public init( id: UInt16 = 0, type: MessageType = .query, operationCode: OperationCode = .query, authoritativeAnswer: Bool = false, truncation: Bool = false, recursionDesired: Bool = false, recursionAvailable: Bool = false, returnCode: ReturnCode = .noError, questions: [Question] = [], answers: [ResourceRecord] = [], authorities: [ResourceRecord] = [], additional: [ResourceRecord] = [] ) { self.id = id self.type = type self.operationCode = operationCode self.authoritativeAnswer = authoritativeAnswer self.truncation = truncation self.recursionDesired = recursionDesired self.recursionAvailable = recursionAvailable self.returnCode = returnCode self.questions = questions self.answers = answers self.authorities = authorities self.additional = additional } /// Deserialize a DNS message from raw data. public init(deserialize data: Data) throws { var buffer = Array(data) var offset = 0 // Read ID guard let (newOffset, rawId) = buffer.copyOut(as: UInt16.self, offset: offset) else { throw DNSBindError.unmarshalFailure(type: "Message", field: "id") } self.id = UInt16(bigEndian: rawId) offset = newOffset // Read flags guard let (newOffset, rawFlags) = buffer.copyOut(as: UInt16.self, offset: offset) else { throw DNSBindError.unmarshalFailure(type: "Message", field: "flags") } let flags = UInt16(bigEndian: rawFlags) offset = newOffset // Parse flags self.type = (flags & 0x8000) != 0 ? .response : .query guard let opCode = OperationCode(rawValue: UInt8((flags >> 11) & 0x0F)) else { throw DNSBindError.unsupportedValue(type: "Message", field: "opcode") } self.operationCode = opCode self.authoritativeAnswer = (flags & 0x0400) != 0 self.truncation = (flags & 0x0200) != 0 self.recursionDesired = (flags & 0x0100) != 0 self.recursionAvailable = (flags & 0x0080) != 0 guard let returnCode = ReturnCode(rawValue: UInt8(flags & 0x000F)) else { throw DNSBindError.unsupportedValue(type: "Message", field: "rcode") } self.returnCode = returnCode // Read counts guard let (newOffset, rawQdCount) = buffer.copyOut(as: UInt16.self, offset: offset) else { throw DNSBindError.unmarshalFailure(type: "Message", field: "qdcount") } let qdCount = UInt16(bigEndian: rawQdCount) offset = newOffset guard let (newOffset, rawAnCount) = buffer.copyOut(as: UInt16.self, offset: offset) else { throw DNSBindError.unmarshalFailure(type: "Message", field: "ancount") } let anCount = UInt16(bigEndian: rawAnCount) offset = newOffset guard let (newOffset, rawNsCount) = buffer.copyOut(as: UInt16.self, offset: offset) else { throw DNSBindError.unmarshalFailure(type: "Message", field: "nscount") } // nsCount not used for now, but we need to read past it _ = UInt16(bigEndian: rawNsCount) offset = newOffset guard let (newOffset, rawArCount) = buffer.copyOut(as: UInt16.self, offset: offset) else { throw DNSBindError.unmarshalFailure(type: "Message", field: "arcount") } // arCount not used for now, but we need to read past it _ = UInt16(bigEndian: rawArCount) offset = newOffset // Read questions self.questions = [] for _ in 0.. Data { // Calculate exact buffer size. var bufferSize = Self.headerSize for question in questions { // name + type + class let n = question.name.hasSuffix(".") ? String(question.name.dropLast()) : question.name bufferSize += (try DNSName(labels: n.isEmpty ? [] : n.split(separator: ".", omittingEmptySubsequences: false).map(String.init))).size + 4 } for answer in answers { // name + type + class + ttl + rdlen + rdata let n = answer.name.hasSuffix(".") ? String(answer.name.dropLast()) : answer.name let rdataSize = answer.type == .host ? 4 : 16 bufferSize += (try DNSName(labels: n.isEmpty ? [] : n.split(separator: ".", omittingEmptySubsequences: false).map(String.init))).size + 10 + rdataSize } var buffer = [UInt8](repeating: 0, count: bufferSize) var offset = 0 // Write ID guard let newOffset = buffer.copyIn(as: UInt16.self, value: id.bigEndian, offset: offset) else { throw DNSBindError.marshalFailure(type: "Message", field: "id") } offset = newOffset // Build and write flags var flags: UInt16 = 0 flags |= type == .response ? 0x8000 : 0 flags |= UInt16(operationCode.rawValue) << 11 flags |= authoritativeAnswer ? 0x0400 : 0 flags |= truncation ? 0x0200 : 0 flags |= recursionDesired ? 0x0100 : 0 flags |= recursionAvailable ? 0x0080 : 0 flags |= UInt16(returnCode.rawValue) guard let newOffset = buffer.copyIn(as: UInt16.self, value: flags.bigEndian, offset: offset) else { throw DNSBindError.marshalFailure(type: "Message", field: "flags") } offset = newOffset // Write counts guard questions.count <= UInt16.max else { throw DNSBindError.marshalFailure(type: "Message", field: "qdcount") } guard answers.count <= UInt16.max else { throw DNSBindError.marshalFailure(type: "Message", field: "ancount") } guard authorities.count <= UInt16.max else { throw DNSBindError.marshalFailure(type: "Message", field: "nscount") } guard additional.count <= UInt16.max else { throw DNSBindError.marshalFailure(type: "Message", field: "arcount") } guard let newOffset = buffer.copyIn(as: UInt16.self, value: UInt16(questions.count).bigEndian, offset: offset) else { throw DNSBindError.marshalFailure(type: "Message", field: "qdcount") } offset = newOffset guard let newOffset = buffer.copyIn(as: UInt16.self, value: UInt16(answers.count).bigEndian, offset: offset) else { throw DNSBindError.marshalFailure(type: "Message", field: "ancount") } offset = newOffset guard let newOffset = buffer.copyIn(as: UInt16.self, value: UInt16(authorities.count).bigEndian, offset: offset) else { throw DNSBindError.marshalFailure(type: "Message", field: "nscount") } offset = newOffset guard let newOffset = buffer.copyIn(as: UInt16.self, value: UInt16(additional.count).bigEndian, offset: offset) else { throw DNSBindError.marshalFailure(type: "Message", field: "arcount") } offset = newOffset // Write questions for question in questions { offset = try question.appendBuffer(&buffer, offset: offset) } // Write answers for answer in answers { offset = try answer.appendBuffer(&buffer, offset: offset) } // Write authorities for authority in authorities { offset = try authority.appendBuffer(&buffer, offset: offset) } // Write additional for record in additional { offset = try record.appendBuffer(&buffer, offset: offset) } guard offset == bufferSize else { throw DNSBindError.unexpectedOffset(type: "Message", expected: bufferSize, actual: offset) } return Data(buffer[0..