mirror of
https://github.com/apple/container.git
synced 2026-07-13 13:07:06 +00:00
4a8a8a6f89
- Closes #1268. - The types we were using weren't very usable with Swift 6 structured concurrency. - Implements just the subset of records that we use. - Use notImplemented instead of formatError for unknown record types. - Use pure actor for LocalhostDNSHandler now that we have sendable types. - Use DNSName as key for table lookups in LocalhostDNSHandler and HostTableResolver. - Utilize dot-suffixed domain names everywhere in the lookup chain. - Huge thanks to @manojmahapatra and @katiewasnothere for their diligent and patient review :D
284 lines
11 KiB
Swift
284 lines
11 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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..<qdCount {
|
|
var question = Question(name: "")
|
|
offset = try question.bindBuffer(&buffer, offset: offset, messageStart: 0)
|
|
self.questions.append(question)
|
|
}
|
|
|
|
// Read answers (simplified - skip for now as we only need to parse queries)
|
|
self.answers = []
|
|
self.authorities = []
|
|
self.additional = []
|
|
|
|
// Skip answer parsing for now - we primarily receive queries and send responses
|
|
_ = anCount
|
|
}
|
|
|
|
/// Serialize this message to raw data.
|
|
public func serialize() throws -> 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..<offset])
|
|
}
|
|
}
|