chore: import upstream snapshot with attribution
container project - merge build / Invoke build (push) Successful in 1s
container project - merge build / Invoke build (push) Successful in 1s
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Protocol for implementing custom DNS handlers.
|
||||
public protocol DNSHandler {
|
||||
/// Attempt to answer a DNS query
|
||||
/// - Parameter query: the query message
|
||||
/// - Throws: a server failure occurred during the query
|
||||
/// - Returns: The response message for the query, or nil if the request
|
||||
/// is not within the scope of the handler.
|
||||
func answer(query: Message) async throws -> Message?
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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
|
||||
import NIOCore
|
||||
import NIOPosix
|
||||
|
||||
extension DNSServer {
|
||||
/// Handles the DNS request.
|
||||
/// - Parameters:
|
||||
/// - outbound: The NIOAsyncChannelOutboundWriter for which to respond.
|
||||
/// - packet: The request packet.
|
||||
func handle(
|
||||
outbound: NIOAsyncChannelOutboundWriter<AddressedEnvelope<ByteBuffer>>,
|
||||
packet: inout AddressedEnvelope<ByteBuffer>
|
||||
) async throws {
|
||||
// RFC 1035 §2.3.4 limits UDP DNS messages to 512 bytes. We don't implement
|
||||
// EDNS0 (RFC 6891), and this server only resolves host A/AAAA queries, so a
|
||||
// legitimate query will never approach this limit. Reject oversized packets
|
||||
// before reading to avoid allocating memory for malformed or malicious datagrams.
|
||||
let maxPacketSize = 512
|
||||
guard packet.data.readableBytes <= maxPacketSize else {
|
||||
self.log?.error("dropping oversized DNS packet: \(packet.data.readableBytes) bytes")
|
||||
return
|
||||
}
|
||||
|
||||
var data = Data()
|
||||
self.log?.debug("reading data")
|
||||
while packet.data.readableBytes > 0 {
|
||||
if let chunk = packet.data.readBytes(length: packet.data.readableBytes) {
|
||||
data.append(contentsOf: chunk)
|
||||
}
|
||||
}
|
||||
|
||||
self.log?.debug("deserializing message")
|
||||
|
||||
// always send response
|
||||
let responseData: Data
|
||||
do {
|
||||
let query = try Message(deserialize: data)
|
||||
self.log?.debug("processing query: \(query.questions)")
|
||||
|
||||
self.log?.debug("awaiting processing")
|
||||
var response =
|
||||
try await handler.answer(query: query)
|
||||
?? Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .notImplemented,
|
||||
questions: query.questions,
|
||||
answers: []
|
||||
)
|
||||
|
||||
// Only set NXDOMAIN if handler didn't explicitly set noError (NODATA response).
|
||||
// This preserves NODATA responses for AAAA queries when A record exists,
|
||||
// which prevents musl libc from treating empty AAAA as "domain doesn't exist".
|
||||
if response.answers.isEmpty && response.returnCode != .noError {
|
||||
response.returnCode = .nonExistentDomain
|
||||
}
|
||||
|
||||
self.log?.debug("serializing response")
|
||||
responseData = try response.serialize()
|
||||
} catch let error as DNSBindError {
|
||||
// Best-effort: echo the transaction ID from the first two bytes of the raw packet.
|
||||
let rawId = data.count >= 2 ? data[0..<2].withUnsafeBytes { $0.load(as: UInt16.self) } : 0
|
||||
let id = UInt16(bigEndian: rawId)
|
||||
let returnCode: ReturnCode
|
||||
switch error {
|
||||
case .unsupportedValue:
|
||||
self.log?.error("not implemented processing DNS message: \(error)")
|
||||
returnCode = .notImplemented
|
||||
default:
|
||||
self.log?.error("format error processing DNS message: \(error)")
|
||||
returnCode = .formatError
|
||||
}
|
||||
let response = Message(
|
||||
id: id,
|
||||
type: .response,
|
||||
returnCode: returnCode,
|
||||
questions: [],
|
||||
answers: []
|
||||
)
|
||||
responseData = try response.serialize()
|
||||
} catch {
|
||||
let rawId = data.count >= 2 ? data[0..<2].withUnsafeBytes { $0.load(as: UInt16.self) } : 0
|
||||
let id = UInt16(bigEndian: rawId)
|
||||
self.log?.error("error processing DNS message: \(error)")
|
||||
let response = Message(
|
||||
id: id,
|
||||
type: .response,
|
||||
returnCode: .serverFailure,
|
||||
questions: [],
|
||||
answers: []
|
||||
)
|
||||
responseData = try response.serialize()
|
||||
}
|
||||
|
||||
self.log?.debug("sending response")
|
||||
let rData = ByteBuffer(bytes: responseData)
|
||||
do {
|
||||
try await outbound.write(AddressedEnvelope(remoteAddress: packet.remoteAddress, data: rData))
|
||||
} catch {
|
||||
self.log?.error("failed to send DNS response: \(error)")
|
||||
}
|
||||
|
||||
self.log?.debug("processing done")
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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
|
||||
import Logging
|
||||
import NIOCore
|
||||
import NIOPosix
|
||||
|
||||
/// Provides a DNS server.
|
||||
/// - Parameters:
|
||||
/// - host: The host address on which to listen.
|
||||
/// - port: The port for the server to listen.
|
||||
public struct DNSServer {
|
||||
public var handler: DNSHandler
|
||||
let log: Logger?
|
||||
|
||||
public init(
|
||||
handler: DNSHandler,
|
||||
log: Logger? = nil
|
||||
) {
|
||||
self.handler = handler
|
||||
self.log = log
|
||||
}
|
||||
|
||||
public func run(host: String, port: Int) async throws {
|
||||
// TODO: TCP server
|
||||
let srv = try await DatagramBootstrap(group: NIOSingletons.posixEventLoopGroup)
|
||||
.channelOption(.socketOption(.so_reuseaddr), value: 1)
|
||||
.bind(host: host, port: port)
|
||||
.flatMapThrowing { channel in
|
||||
try NIOAsyncChannel(
|
||||
wrappingChannelSynchronously: channel,
|
||||
configuration: NIOAsyncChannel.Configuration(
|
||||
inboundType: AddressedEnvelope<ByteBuffer>.self,
|
||||
outboundType: AddressedEnvelope<ByteBuffer>.self
|
||||
)
|
||||
)
|
||||
}
|
||||
.get()
|
||||
|
||||
try await srv.executeThenClose { inbound, outbound in
|
||||
for try await var packet in inbound {
|
||||
try await self.handle(outbound: outbound, packet: &packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func run(socketPath: String) async throws {
|
||||
// TODO: TCP server
|
||||
let srv = try await DatagramBootstrap(group: NIOSingletons.posixEventLoopGroup)
|
||||
.bind(unixDomainSocketPath: socketPath, cleanupExistingSocketFile: true)
|
||||
.flatMapThrowing { channel in
|
||||
try NIOAsyncChannel(
|
||||
wrappingChannelSynchronously: channel,
|
||||
configuration: NIOAsyncChannel.Configuration(
|
||||
inboundType: AddressedEnvelope<ByteBuffer>.self,
|
||||
outboundType: AddressedEnvelope<ByteBuffer>.self
|
||||
)
|
||||
)
|
||||
}
|
||||
.get()
|
||||
|
||||
try await srv.executeThenClose { inbound, outbound in
|
||||
for try await var packet in inbound {
|
||||
log?.debug("received packet from \(packet.remoteAddress)")
|
||||
try await self.handle(outbound: outbound, packet: &packet)
|
||||
log?.debug("sent packet")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() async throws {}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Delegates a query sequentially to handlers until one provides a response.
|
||||
public struct CompositeResolver: DNSHandler {
|
||||
private let handlers: [DNSHandler]
|
||||
|
||||
public init(handlers: [DNSHandler]) {
|
||||
self.handlers = handlers
|
||||
}
|
||||
|
||||
public func answer(query: Message) async throws -> Message? {
|
||||
for handler in self.handlers {
|
||||
if let response = try await handler.answer(query: query) {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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 ContainerizationExtras
|
||||
|
||||
/// Handler that uses table lookup to resolve hostnames.
|
||||
///
|
||||
/// Keys in `hosts4` are normalized to `DNSName` on construction, so lookups
|
||||
/// are case-insensitive and trailing dots are optional.
|
||||
public struct HostTableResolver: DNSHandler {
|
||||
public let hosts4: [DNSName: IPv4Address]
|
||||
private let ttl: UInt32
|
||||
|
||||
/// Creates a resolver backed by a static IPv4 host table.
|
||||
///
|
||||
/// - Parameter hosts4: A dictionary mapping domain names to IPv4 addresses.
|
||||
/// Keys are normalized to `DNSName` (lowercased, trailing dot stripped), so
|
||||
/// `"FOO."`, `"foo."`, and `"foo"` all refer to the same entry.
|
||||
/// - Parameter ttl: The TTL in seconds to set on answer records (default is 300).
|
||||
/// - Throws: `DNSBindError.invalidName` if any key is not a valid DNS name.
|
||||
public init(hosts4: [String: IPv4Address], ttl: UInt32 = 300) throws {
|
||||
self.hosts4 = try Dictionary(uniqueKeysWithValues: hosts4.map { (try DNSName($0.key), $0.value) })
|
||||
self.ttl = ttl
|
||||
}
|
||||
|
||||
public func answer(query: Message) async throws -> Message? {
|
||||
guard let question = query.questions.first else {
|
||||
return nil
|
||||
}
|
||||
let n = question.name.hasSuffix(".") ? String(question.name.dropLast()) : question.name
|
||||
let key = try DNSName(labels: n.isEmpty ? [] : n.split(separator: ".", omittingEmptySubsequences: false).map(String.init))
|
||||
let record: ResourceRecord?
|
||||
switch question.type {
|
||||
case ResourceRecordType.host:
|
||||
record = answerHost(question: question, key: key)
|
||||
case ResourceRecordType.host6:
|
||||
// Return NODATA (noError with empty answers) for AAAA queries ONLY if A record exists.
|
||||
// This is required because musl libc has issues when A record exists but AAAA returns NXDOMAIN.
|
||||
// musl treats NXDOMAIN on AAAA as "domain doesn't exist" and fails DNS resolution entirely.
|
||||
// NODATA correctly indicates "no IPv6 address available, but domain exists".
|
||||
if hosts4[key] != nil {
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .noError,
|
||||
questions: query.questions,
|
||||
answers: []
|
||||
)
|
||||
}
|
||||
// If hostname doesn't exist, return nil which will become NXDOMAIN
|
||||
return nil
|
||||
default:
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .notImplemented,
|
||||
questions: query.questions,
|
||||
answers: []
|
||||
)
|
||||
}
|
||||
|
||||
guard let record else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .noError,
|
||||
questions: query.questions,
|
||||
answers: [record]
|
||||
)
|
||||
}
|
||||
|
||||
private func answerHost(question: Question, key: DNSName) -> ResourceRecord? {
|
||||
guard let ip = hosts4[key] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return HostRecord<IPv4Address>(name: question.name, ttl: ttl, ip: ip)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Handler that returns NXDOMAIN for all hostnames.
|
||||
public struct NxDomainResolver: DNSHandler {
|
||||
private let ttl: UInt32
|
||||
|
||||
public init(ttl: UInt32 = 300) {
|
||||
self.ttl = ttl
|
||||
}
|
||||
|
||||
public func answer(query: Message) async throws -> Message? {
|
||||
let question = query.questions[0]
|
||||
switch question.type {
|
||||
case ResourceRecordType.host:
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .nonExistentDomain,
|
||||
questions: query.questions,
|
||||
answers: []
|
||||
)
|
||||
default:
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .notImplemented,
|
||||
questions: query.questions,
|
||||
answers: []
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Pass standard queries to a delegate handler.
|
||||
public struct StandardQueryValidator: DNSHandler {
|
||||
private let handler: DNSHandler
|
||||
|
||||
/// Create the handler.
|
||||
/// - Parameter delegate: the handler that receives valid queries
|
||||
public init(handler: DNSHandler) {
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
/// Ensures the query is valid before forwarding it to the delegate.
|
||||
/// - Parameter msg: the query message
|
||||
/// - Returns: the delegate response if the query is valid, and an
|
||||
/// error response otherwise
|
||||
public func answer(query: Message) async throws -> Message? {
|
||||
// Reject response messages.
|
||||
guard query.type == .query else {
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .formatError,
|
||||
questions: query.questions
|
||||
)
|
||||
}
|
||||
|
||||
// Standard DNS servers handle only query operations.
|
||||
guard query.operationCode == .query else {
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .notImplemented,
|
||||
questions: query.questions
|
||||
)
|
||||
}
|
||||
|
||||
// Standard DNS servers only handle messages with exactly one question.
|
||||
guard query.questions.count == 1 else {
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .formatError,
|
||||
questions: query.questions
|
||||
)
|
||||
}
|
||||
|
||||
return try await handler.answer(query: query)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Errors that can occur during DNS message serialization/deserialization.
|
||||
public enum DNSBindError: Error, CustomStringConvertible {
|
||||
case marshalFailure(type: String, field: String)
|
||||
case unmarshalFailure(type: String, field: String)
|
||||
case unsupportedValue(type: String, field: String)
|
||||
case invalidName(String)
|
||||
case unexpectedOffset(type: String, expected: Int, actual: Int)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .marshalFailure(let type, let field):
|
||||
return "failed to marshal \(type).\(field)"
|
||||
case .unmarshalFailure(let type, let field):
|
||||
return "failed to unmarshal \(type).\(field)"
|
||||
case .unsupportedValue(let type, let field):
|
||||
return "unsupported value for \(type).\(field)"
|
||||
case .invalidName(let reason):
|
||||
return "invalid DNS name: \(reason)"
|
||||
case .unexpectedOffset(let type, let expected, let actual):
|
||||
return "unexpected offset serializing \(type): expected \(expected), got \(actual)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// DNS message type (query or response).
|
||||
public enum MessageType: UInt16, Sendable {
|
||||
case query = 0
|
||||
case response = 1
|
||||
}
|
||||
|
||||
/// DNS operation code (RFC 1035, 1996, 2136).
|
||||
public enum OperationCode: UInt8, Sendable {
|
||||
case query = 0 // Standard query (RFC 1035)
|
||||
case inverseQuery = 1 // Inverse query (obsolete, RFC 3425)
|
||||
case status = 2 // Server status request (RFC 1035)
|
||||
// 3 is reserved
|
||||
case notify = 4 // Zone change notification (RFC 1996)
|
||||
case update = 5 // Dynamic update (RFC 2136)
|
||||
case dso = 6 // DNS Stateful Operations (RFC 8490)
|
||||
// 7-15 reserved
|
||||
}
|
||||
|
||||
/// DNS response return codes (RFC 1035, 2136, 2845, 6895).
|
||||
public enum ReturnCode: UInt8, Sendable {
|
||||
case noError = 0 // No error
|
||||
case formatError = 1 // Format error - unable to interpret query
|
||||
case serverFailure = 2 // Server failure
|
||||
case nonExistentDomain = 3 // Name error - domain does not exist (NXDOMAIN)
|
||||
case notImplemented = 4 // Not implemented - query type not supported
|
||||
case refused = 5 // Refused - policy restriction
|
||||
case yxDomain = 6 // Name exists when it should not (RFC 2136)
|
||||
case yxRRSet = 7 // RR set exists when it should not (RFC 2136)
|
||||
case nxRRSet = 8 // RR set does not exist when it should (RFC 2136)
|
||||
case notAuthoritative = 9 // Server not authoritative (RFC 2136) / Not authorized (RFC 2845)
|
||||
case notZone = 10 // Name not in zone (RFC 2136)
|
||||
case dsoTypeNotImplemented = 11 // DSO-TYPE not implemented (RFC 8490)
|
||||
// 12-15 reserved
|
||||
case badSignature = 16 // TSIG signature failure (RFC 2845)
|
||||
case badKey = 17 // Key not recognized (RFC 2845)
|
||||
case badTime = 18 // Signature out of time window (RFC 2845)
|
||||
case badMode = 19 // Bad TKEY mode (RFC 2930)
|
||||
case badName = 20 // Duplicate key name (RFC 2930)
|
||||
case badAlgorithm = 21 // Algorithm not supported (RFC 2930)
|
||||
case badTruncation = 22 // Bad truncation (RFC 4635)
|
||||
case badCookie = 23 // Bad/missing server cookie (RFC 7873)
|
||||
}
|
||||
|
||||
/// DNS resource record types (RFC 1035, 3596, 2782, and others).
|
||||
public enum ResourceRecordType: UInt16, Sendable {
|
||||
case host = 1 // A - IPv4 address (RFC 1035)
|
||||
case nameServer = 2 // NS - Authoritative name server (RFC 1035)
|
||||
case mailDestination = 3 // MD - Mail destination (obsolete, RFC 1035)
|
||||
case mailForwarder = 4 // MF - Mail forwarder (obsolete, RFC 1035)
|
||||
case alias = 5 // CNAME - Canonical name (RFC 1035)
|
||||
case startOfAuthority = 6 // SOA - Start of authority (RFC 1035)
|
||||
case mailbox = 7 // MB - Mailbox domain name (experimental, RFC 1035)
|
||||
case mailGroup = 8 // MG - Mail group member (experimental, RFC 1035)
|
||||
case mailRename = 9 // MR - Mail rename domain name (experimental, RFC 1035)
|
||||
case null = 10 // NULL - Null RR (experimental, RFC 1035)
|
||||
case wellKnownService = 11 // WKS - Well known service (RFC 1035)
|
||||
case pointer = 12 // PTR - Domain name pointer (RFC 1035)
|
||||
case hostInfo = 13 // HINFO - Host information (RFC 1035)
|
||||
case mailInfo = 14 // MINFO - Mailbox information (RFC 1035)
|
||||
case mailExchange = 15 // MX - Mail exchange (RFC 1035)
|
||||
case text = 16 // TXT - Text strings (RFC 1035)
|
||||
case responsiblePerson = 17 // RP - Responsible person (RFC 1183)
|
||||
case afsDatabase = 18 // AFSDB - AFS database location (RFC 1183)
|
||||
case x25 = 19 // X25 - X.25 PSDN address (RFC 1183)
|
||||
case isdn = 20 // ISDN - ISDN address (RFC 1183)
|
||||
case routeThrough = 21 // RT - Route through (RFC 1183)
|
||||
case nsapAddress = 22 // NSAP - NSAP address (RFC 1706)
|
||||
case nsapPointer = 23 // NSAP-PTR - NSAP pointer (RFC 1706)
|
||||
case signature = 24 // SIG - Security signature (RFC 2535)
|
||||
case key = 25 // KEY - Security key (RFC 2535)
|
||||
case pxRecord = 26 // PX - X.400 mail mapping (RFC 2163)
|
||||
case gpos = 27 // GPOS - Geographical position (RFC 1712)
|
||||
case host6 = 28 // AAAA - IPv6 address (RFC 3596)
|
||||
case location = 29 // LOC - Location information (RFC 1876)
|
||||
case nextDomain = 30 // NXT - Next domain (obsolete, RFC 2535)
|
||||
case endpointId = 31 // EID - Endpoint identifier
|
||||
case nimrodLocator = 32 // NIMLOC - Nimrod locator
|
||||
case service = 33 // SRV - Service locator (RFC 2782)
|
||||
case atma = 34 // ATMA - ATM address
|
||||
case namingPointer = 35 // NAPTR - Naming authority pointer (RFC 3403)
|
||||
case keyExchange = 36 // KX - Key exchange (RFC 2230)
|
||||
case cert = 37 // CERT - Certificate (RFC 4398)
|
||||
case a6Record = 38 // A6 - IPv6 address (obsolete, RFC 2874)
|
||||
case dname = 39 // DNAME - Delegation name (RFC 6672)
|
||||
case sink = 40 // SINK - Kitchen sink
|
||||
case opt = 41 // OPT - EDNS option (RFC 6891)
|
||||
case apl = 42 // APL - Address prefix list (RFC 3123)
|
||||
case delegationSigner = 43 // DS - Delegation signer (RFC 4034)
|
||||
case sshFingerprint = 44 // SSHFP - SSH key fingerprint (RFC 4255)
|
||||
case ipsecKey = 45 // IPSECKEY - IPsec key (RFC 4025)
|
||||
case resourceSignature = 46 // RRSIG - Resource record signature (RFC 4034)
|
||||
case nsec = 47 // NSEC - Next secure record (RFC 4034)
|
||||
case dnsKey = 48 // DNSKEY - DNS key (RFC 4034)
|
||||
case dhcid = 49 // DHCID - DHCP identifier (RFC 4701)
|
||||
case nsec3 = 50 // NSEC3 - NSEC3 (RFC 5155)
|
||||
case nsec3Param = 51 // NSEC3PARAM - NSEC3 parameters (RFC 5155)
|
||||
case tlsa = 52 // TLSA - TLSA certificate (RFC 6698)
|
||||
case smimea = 53 // SMIMEA - S/MIME cert association (RFC 8162)
|
||||
// 54 unassigned
|
||||
case hip = 55 // HIP - Host identity protocol (RFC 8005)
|
||||
case ninfo = 56 // NINFO
|
||||
case rkey = 57 // RKEY
|
||||
case taLink = 58 // TALINK - Trust anchor link
|
||||
case cds = 59 // CDS - Child DS (RFC 7344)
|
||||
case cdnsKey = 60 // CDNSKEY - Child DNSKEY (RFC 7344)
|
||||
case openPGPKey = 61 // OPENPGPKEY - OpenPGP key (RFC 7929)
|
||||
case csync = 62 // CSYNC - Child-to-parent sync (RFC 7477)
|
||||
case zoneDigest = 63 // ZONEMD - Zone message digest (RFC 8976)
|
||||
case svcBinding = 64 // SVCB - Service binding (RFC 9460)
|
||||
case httpsBinding = 65 // HTTPS - HTTPS binding (RFC 9460)
|
||||
// 66-98 unassigned
|
||||
case spf = 99 // SPF - Sender policy framework (RFC 7208)
|
||||
case uinfo = 100 // UINFO
|
||||
case uid = 101 // UID
|
||||
case gid = 102 // GID
|
||||
case unspec = 103 // UNSPEC
|
||||
case nid = 104 // NID - Node identifier (RFC 6742)
|
||||
case l32 = 105 // L32 - Locator32 (RFC 6742)
|
||||
case l64 = 106 // L64 - Locator64 (RFC 6742)
|
||||
case lp = 107 // LP - Locator FQDN (RFC 6742)
|
||||
case eui48 = 108 // EUI48 - 48-bit MAC (RFC 7043)
|
||||
case eui64 = 109 // EUI64 - 64-bit MAC (RFC 7043)
|
||||
// 110-248 unassigned
|
||||
case tkey = 249 // TKEY - Transaction key (RFC 2930)
|
||||
case tsig = 250 // TSIG - Transaction signature (RFC 2845)
|
||||
case incrementalZoneTransfer = 251 // IXFR - Incremental zone transfer (RFC 1995)
|
||||
case standardZoneTransfer = 252 // AXFR - Full zone transfer (RFC 1035)
|
||||
case mailboxRecords = 253 // MAILB - Mailbox-related records (RFC 1035)
|
||||
case mailAgentRecords = 254 // MAILA - Mail agent RRs (obsolete, RFC 1035)
|
||||
case all = 255 // * - All records (RFC 1035)
|
||||
case uri = 256 // URI - Uniform resource identifier (RFC 7553)
|
||||
case caa = 257 // CAA - Certification authority authorization (RFC 8659)
|
||||
case avc = 258 // AVC - Application visibility and control
|
||||
case doa = 259 // DOA - Digital object architecture
|
||||
case amtRelay = 260 // AMTRELAY - Automatic multicast tunneling relay (RFC 8777)
|
||||
case resInfo = 261 // RESINFO - Resolver information
|
||||
// ...
|
||||
case ta = 32768 // TA - DNSSEC trust authorities
|
||||
case dlv = 32769 // DLV - DNSSEC lookaside validation (RFC 4431)
|
||||
}
|
||||
|
||||
/// DNS resource record class (RFC 1035).
|
||||
public enum ResourceRecordClass: UInt16, Sendable {
|
||||
case internet = 1 // IN - Internet (RFC 1035)
|
||||
// 2 unassigned
|
||||
case chaos = 3 // CH - Chaos (RFC 1035)
|
||||
case hesiod = 4 // HS - Hesiod (RFC 1035)
|
||||
// 5-253 unassigned
|
||||
case none = 254 // NONE - None (RFC 2136)
|
||||
case any = 255 // * - Any class (RFC 1035)
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 name encoded as a sequence of labels.
|
||||
///
|
||||
/// DNS names are encoded as: `[length][label][length][label]...[0]`
|
||||
/// For example, "example.com" becomes: `[7]example[3]com[0]`
|
||||
public struct DNSName: Sendable, Hashable, CustomStringConvertible {
|
||||
/// The labels that make up this name (e.g., ["example", "com"]).
|
||||
public private(set) var labels: [String]
|
||||
|
||||
/// Creates a DNS name representing the root (empty label list).
|
||||
public init() {
|
||||
self.labels = []
|
||||
}
|
||||
|
||||
/// Creates a validated DNS name from an array of labels.
|
||||
///
|
||||
/// Validates structural RFC 1035 constraints only: no empty labels, each label ≤ 63
|
||||
/// bytes, total wire length ≤ 255 bytes. Does not enforce hostname character rules.
|
||||
/// Labels are lowercased to normalize for case-insensitive DNS comparison.
|
||||
///
|
||||
/// - Throws: `DNSBindError.invalidName` if any label is empty, exceeds 63 bytes,
|
||||
/// or if the total wire representation exceeds 255 bytes.
|
||||
public init(labels: [String]) throws {
|
||||
for label in labels {
|
||||
guard !label.isEmpty else {
|
||||
throw DNSBindError.invalidName("empty label")
|
||||
}
|
||||
guard label.utf8.count <= 63 else {
|
||||
throw DNSBindError.invalidName("label too long: \"\(label)\"")
|
||||
}
|
||||
}
|
||||
let wireLength = labels.reduce(1) { $0 + 1 + $1.utf8.count }
|
||||
guard wireLength <= 255 else {
|
||||
throw DNSBindError.invalidName("name too long")
|
||||
}
|
||||
self.labels = labels.map { $0.lowercased() }
|
||||
}
|
||||
|
||||
/// Creates a validated DNS name from a dot-separated hostname string
|
||||
/// (e.g., `"example.com."` or `"example.com"`).
|
||||
///
|
||||
/// A trailing dot is accepted but not required.
|
||||
/// An empty string produces the root name without error.
|
||||
///
|
||||
/// Labels must start and end with a letter or digit (LDH hostname rule).
|
||||
/// Use `init(labels:)` directly when working with wire-decoded names that
|
||||
/// may contain non-hostname labels (e.g. service-discovery labels like `"_dns"`).
|
||||
///
|
||||
/// - Throws: `DNSBindError.invalidName` if any label violates the character rules,
|
||||
/// or if structural limits are exceeded (see `init(labels:)`).
|
||||
public init(_ hostname: String) throws {
|
||||
let normalized = hostname.hasSuffix(".") ? String(hostname.dropLast()) : hostname
|
||||
guard !normalized.isEmpty else {
|
||||
self.init()
|
||||
return
|
||||
}
|
||||
let parts = normalized.split(separator: ".", omittingEmptySubsequences: false).map { String($0) }
|
||||
let hostnameRegex = /[a-zA-Z0-9](?:[a-zA-Z0-9\-_]*[a-zA-Z0-9])?/
|
||||
for part in parts {
|
||||
guard part.wholeMatch(of: hostnameRegex) != nil else {
|
||||
throw DNSBindError.invalidName(
|
||||
"label must start and end with a letter or digit: \"\(part)\""
|
||||
)
|
||||
}
|
||||
}
|
||||
try self.init(labels: parts)
|
||||
}
|
||||
|
||||
/// The wire format size of this name in bytes.
|
||||
public var size: Int {
|
||||
// Each label: 1 byte length + label bytes, plus 1 byte for null terminator
|
||||
labels.reduce(1) { $0 + 1 + $1.utf8.count }
|
||||
}
|
||||
|
||||
/// The fully-qualified domain name with trailing dot.
|
||||
public var description: String {
|
||||
labels.joined(separator: ".") + "."
|
||||
}
|
||||
|
||||
/// The partially-qualified domain name, which is the FQDN less the trailing dot.
|
||||
public var pqdn: String {
|
||||
labels.joined(separator: ".")
|
||||
}
|
||||
|
||||
/// Serialize this name into the buffer at the given offset.
|
||||
public func appendBuffer(_ buffer: inout [UInt8], offset: Int) throws -> Int {
|
||||
let startOffset = offset
|
||||
var offset = offset
|
||||
|
||||
for label in labels {
|
||||
let bytes = Array(label.utf8)
|
||||
guard bytes.count <= 63 else {
|
||||
throw DNSBindError.marshalFailure(type: "DNSName", field: "label")
|
||||
}
|
||||
|
||||
guard let newOffset = buffer.copyIn(as: UInt8.self, value: UInt8(bytes.count), offset: offset) else {
|
||||
throw DNSBindError.marshalFailure(type: "DNSName", field: "label")
|
||||
}
|
||||
offset = newOffset
|
||||
|
||||
guard let newOffset = buffer.copyIn(buffer: bytes, offset: offset) else {
|
||||
throw DNSBindError.marshalFailure(type: "DNSName", field: "label")
|
||||
}
|
||||
offset = newOffset
|
||||
}
|
||||
|
||||
// Null terminator
|
||||
guard let newOffset = buffer.copyIn(as: UInt8.self, value: 0, offset: offset) else {
|
||||
throw DNSBindError.marshalFailure(type: "DNSName", field: "terminator")
|
||||
}
|
||||
|
||||
guard newOffset == startOffset + size else {
|
||||
throw DNSBindError.unexpectedOffset(type: "DNSName", expected: startOffset + size, actual: newOffset)
|
||||
}
|
||||
return newOffset
|
||||
}
|
||||
|
||||
/// Deserialize a name from the buffer at the given offset.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - buffer: The buffer to read from.
|
||||
/// - offset: The offset to start reading.
|
||||
/// - messageStart: The start of the DNS message (for compression pointer resolution).
|
||||
/// - Returns: The new offset after reading.
|
||||
public mutating func bindBuffer(
|
||||
_ buffer: inout [UInt8],
|
||||
offset: Int,
|
||||
messageStart: Int = 0
|
||||
) throws -> Int {
|
||||
var offset = offset
|
||||
var collectedLabels: [String] = []
|
||||
var jumped = false
|
||||
var returnOffset = offset
|
||||
var pointerHops = 0
|
||||
|
||||
while true {
|
||||
guard offset < buffer.count else {
|
||||
throw DNSBindError.unmarshalFailure(type: "DNSName", field: "name")
|
||||
}
|
||||
|
||||
let length = buffer[offset]
|
||||
|
||||
// Check for compression pointer (top 2 bits set)
|
||||
if (length & 0xC0) == 0xC0 {
|
||||
guard offset + 1 < buffer.count else {
|
||||
throw DNSBindError.unmarshalFailure(type: "DNSName", field: "pointer")
|
||||
}
|
||||
|
||||
pointerHops += 1
|
||||
guard pointerHops <= 10 else {
|
||||
throw DNSBindError.unmarshalFailure(type: "DNSName", field: "pointer")
|
||||
}
|
||||
|
||||
if !jumped {
|
||||
returnOffset = offset + 2
|
||||
}
|
||||
|
||||
// Calculate pointer offset from message start
|
||||
let pointer = Int(length & 0x3F) << 8 | Int(buffer[offset + 1])
|
||||
let pointerTarget = messageStart + pointer
|
||||
guard pointerTarget >= 0 && pointerTarget < offset && pointerTarget < buffer.count else {
|
||||
throw DNSBindError.unmarshalFailure(type: "DNSName", field: "pointer")
|
||||
}
|
||||
offset = pointerTarget
|
||||
jumped = true
|
||||
continue
|
||||
}
|
||||
|
||||
offset += 1
|
||||
|
||||
// Null terminator - end of name
|
||||
if length == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
guard offset + Int(length) <= buffer.count else {
|
||||
throw DNSBindError.unmarshalFailure(type: "DNSName", field: "label")
|
||||
}
|
||||
|
||||
let labelBytes = Array(buffer[offset..<offset + Int(length)])
|
||||
guard let label = String(bytes: labelBytes, encoding: .utf8) else {
|
||||
throw DNSBindError.unmarshalFailure(type: "DNSName", field: "label")
|
||||
}
|
||||
|
||||
collectedLabels.append(label)
|
||||
offset += Int(length)
|
||||
}
|
||||
|
||||
self = try DNSName(labels: collectedLabels)
|
||||
return jumped ? returnOffset : offset
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerizationExtras
|
||||
|
||||
/// Protocol for IP address types that can be used in DNS records.
|
||||
public protocol IPAddressProtocol: Sendable, Hashable {
|
||||
static var size: Int { get }
|
||||
static var recordType: ResourceRecordType { get }
|
||||
var bytes: [UInt8] { get }
|
||||
}
|
||||
|
||||
extension IPv4Address: IPAddressProtocol {
|
||||
public static let size = 4
|
||||
public static let recordType: ResourceRecordType = .host
|
||||
}
|
||||
|
||||
extension IPv6Address: IPAddressProtocol {
|
||||
public static let size = 16
|
||||
public static let recordType: ResourceRecordType = .host6
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 question (query).
|
||||
///
|
||||
/// All domain names in a `Question` are canonical DNS names — fully-qualified
|
||||
/// with a trailing dot (e.g. `"example.com."`). This invariant is not enforced
|
||||
/// automatically; callers are responsible for supplying canonical names.
|
||||
public struct Question: Sendable, CustomStringConvertible {
|
||||
/// The fully-qualified domain name being queried, with a trailing dot (e.g. `"example.com."`).
|
||||
public var name: String
|
||||
|
||||
/// The record type being requested.
|
||||
public var type: ResourceRecordType
|
||||
|
||||
/// The record class (usually .internet).
|
||||
public var recordClass: ResourceRecordClass
|
||||
|
||||
/// Creates a DNS question.
|
||||
///
|
||||
/// - Parameter name: The fully-qualified domain name to query, with a trailing dot
|
||||
/// (e.g. `"example.com."`). Supplying a name without a trailing dot will produce
|
||||
/// lookup mismatches against canonically-stored records.
|
||||
/// - Parameter type: The record type being requested.
|
||||
/// - Parameter recordClass: The record class (usually `.internet`).
|
||||
public init(
|
||||
name: String,
|
||||
type: ResourceRecordType = .host,
|
||||
recordClass: ResourceRecordClass = .internet
|
||||
) {
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.recordClass = recordClass
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
"\(name) \(type) \(recordClass)"
|
||||
}
|
||||
|
||||
/// Serialize this question into the buffer.
|
||||
public func appendBuffer(_ buffer: inout [UInt8], offset: Int) throws -> Int {
|
||||
let startOffset = offset
|
||||
var offset = offset
|
||||
|
||||
// Write name
|
||||
let normalized = name.hasSuffix(".") ? String(name.dropLast()) : name
|
||||
let dnsName = try DNSName(labels: normalized.isEmpty ? [] : normalized.split(separator: ".", omittingEmptySubsequences: false).map(String.init))
|
||||
offset = try dnsName.appendBuffer(&buffer, offset: offset)
|
||||
|
||||
// Write type (big-endian)
|
||||
guard let newOffset = buffer.copyIn(as: UInt16.self, value: type.rawValue.bigEndian, offset: offset) else {
|
||||
throw DNSBindError.marshalFailure(type: "Question", field: "type")
|
||||
}
|
||||
offset = newOffset
|
||||
|
||||
// Write class (big-endian)
|
||||
guard let newOffset = buffer.copyIn(as: UInt16.self, value: recordClass.rawValue.bigEndian, offset: offset) else {
|
||||
throw DNSBindError.marshalFailure(type: "Question", field: "class")
|
||||
}
|
||||
|
||||
let expectedOffset = startOffset + dnsName.size + 4
|
||||
guard newOffset == expectedOffset else {
|
||||
throw DNSBindError.unexpectedOffset(type: "Question", expected: expectedOffset, actual: newOffset)
|
||||
}
|
||||
return newOffset
|
||||
}
|
||||
|
||||
/// Deserialize a question from the buffer.
|
||||
public mutating func bindBuffer(_ buffer: inout [UInt8], offset: Int, messageStart: Int = 0) throws -> Int {
|
||||
var offset = offset
|
||||
|
||||
// Read name
|
||||
var dnsName = DNSName()
|
||||
offset = try dnsName.bindBuffer(&buffer, offset: offset, messageStart: messageStart)
|
||||
self.name = dnsName.description
|
||||
|
||||
// Read type (big-endian)
|
||||
guard let (newOffset, rawType) = buffer.copyOut(as: UInt16.self, offset: offset) else {
|
||||
throw DNSBindError.unmarshalFailure(type: "Question", field: "type")
|
||||
}
|
||||
guard let qtype = ResourceRecordType(rawValue: UInt16(bigEndian: rawType)) else {
|
||||
throw DNSBindError.unsupportedValue(type: "Question", field: "type")
|
||||
}
|
||||
self.type = qtype
|
||||
offset = newOffset
|
||||
|
||||
// Read class (big-endian)
|
||||
guard let (newOffset, rawClass) = buffer.copyOut(as: UInt16.self, offset: offset) else {
|
||||
throw DNSBindError.unmarshalFailure(type: "Question", field: "class")
|
||||
}
|
||||
guard let qclass = ResourceRecordClass(rawValue: UInt16(bigEndian: rawClass)) else {
|
||||
throw DNSBindError.unsupportedValue(type: "Question", field: "class")
|
||||
}
|
||||
self.recordClass = qclass
|
||||
|
||||
return newOffset
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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
|
||||
|
||||
/// Protocol for DNS resource records.
|
||||
public protocol ResourceRecord: Sendable {
|
||||
/// The domain name this record applies to.
|
||||
var name: String { get }
|
||||
|
||||
/// The record type.
|
||||
var type: ResourceRecordType { get }
|
||||
|
||||
/// The record class.
|
||||
var recordClass: ResourceRecordClass { get }
|
||||
|
||||
/// Time to live in seconds.
|
||||
var ttl: UInt32 { get }
|
||||
|
||||
/// Serialize this record into the buffer.
|
||||
func appendBuffer(_ buffer: inout [UInt8], offset: Int) throws -> Int
|
||||
}
|
||||
|
||||
/// A host record (A or AAAA) containing an IP address.
|
||||
public struct HostRecord<T: IPAddressProtocol>: ResourceRecord {
|
||||
public let name: String
|
||||
public let type: ResourceRecordType
|
||||
public let recordClass: ResourceRecordClass
|
||||
public let ttl: UInt32
|
||||
public let ip: T
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
ttl: UInt32 = 300,
|
||||
ip: T,
|
||||
recordClass: ResourceRecordClass = .internet
|
||||
) {
|
||||
self.name = name
|
||||
self.type = T.recordType
|
||||
self.recordClass = recordClass
|
||||
self.ttl = ttl
|
||||
self.ip = ip
|
||||
}
|
||||
|
||||
public func appendBuffer(_ buffer: inout [UInt8], offset: Int) throws -> Int {
|
||||
let startOffset = offset
|
||||
var offset = offset
|
||||
|
||||
// Write name
|
||||
let normalized = name.hasSuffix(".") ? String(name.dropLast()) : name
|
||||
let dnsName = try DNSName(labels: normalized.isEmpty ? [] : normalized.split(separator: ".", omittingEmptySubsequences: false).map(String.init))
|
||||
offset = try dnsName.appendBuffer(&buffer, offset: offset)
|
||||
|
||||
// Write type (big-endian)
|
||||
guard let newOffset = buffer.copyIn(as: UInt16.self, value: type.rawValue.bigEndian, offset: offset) else {
|
||||
throw DNSBindError.marshalFailure(type: "HostRecord", field: "type")
|
||||
}
|
||||
offset = newOffset
|
||||
|
||||
// Write class (big-endian)
|
||||
guard let newOffset = buffer.copyIn(as: UInt16.self, value: recordClass.rawValue.bigEndian, offset: offset) else {
|
||||
throw DNSBindError.marshalFailure(type: "HostRecord", field: "class")
|
||||
}
|
||||
offset = newOffset
|
||||
|
||||
// Write TTL (big-endian)
|
||||
guard let newOffset = buffer.copyIn(as: UInt32.self, value: ttl.bigEndian, offset: offset) else {
|
||||
throw DNSBindError.marshalFailure(type: "HostRecord", field: "ttl")
|
||||
}
|
||||
offset = newOffset
|
||||
|
||||
// Write rdlength (big-endian)
|
||||
let rdlength = UInt16(T.size)
|
||||
guard let newOffset = buffer.copyIn(as: UInt16.self, value: rdlength.bigEndian, offset: offset) else {
|
||||
throw DNSBindError.marshalFailure(type: "HostRecord", field: "rdlength")
|
||||
}
|
||||
offset = newOffset
|
||||
|
||||
// Write IP address bytes
|
||||
guard let newOffset = buffer.copyIn(buffer: ip.bytes, offset: offset) else {
|
||||
throw DNSBindError.marshalFailure(type: "HostRecord", field: "rdata")
|
||||
}
|
||||
|
||||
let expectedOffset = startOffset + dnsName.size + 10 + T.size
|
||||
guard newOffset == expectedOffset else {
|
||||
throw DNSBindError.unexpectedOffset(type: "HostRecord", expected: expectedOffset, actual: newOffset)
|
||||
}
|
||||
return newOffset
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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
|
||||
|
||||
// TODO: This copies some of the Bindable code from Containerization,
|
||||
// but we can't use Bindable as it presumes a fixed length record.
|
||||
// We can look at refining this later to see if we can use some common
|
||||
// bit fiddling code everywhere.
|
||||
|
||||
extension [UInt8] {
|
||||
/// Copy a value into the buffer at the given offset.
|
||||
/// - Returns: The new offset after writing, or nil if the buffer is too small.
|
||||
package mutating func copyIn<T>(as type: T.Type, value: T, offset: Int = 0) -> Int? {
|
||||
let size = MemoryLayout<T>.size
|
||||
guard self.count >= size + offset else {
|
||||
return nil
|
||||
}
|
||||
return self.withUnsafeMutableBytes {
|
||||
$0.baseAddress?.advanced(by: offset).assumingMemoryBound(to: T.self).pointee = value
|
||||
return offset + size
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy a value out of the buffer at the given offset.
|
||||
/// - Returns: A tuple of (new offset, value), or nil if the buffer is too small.
|
||||
package func copyOut<T>(as type: T.Type, offset: Int = 0) -> (Int, T)? {
|
||||
let size = MemoryLayout<T>.size
|
||||
guard self.count >= size + offset else {
|
||||
return nil
|
||||
}
|
||||
return self.withUnsafeBytes {
|
||||
guard let value = $0.baseAddress?.advanced(by: offset).assumingMemoryBound(to: T.self).pointee else {
|
||||
return nil
|
||||
}
|
||||
return (offset + size, value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy a byte array into the buffer at the given offset.
|
||||
/// - Returns: The new offset after writing, or nil if the buffer is too small.
|
||||
package mutating func copyIn(buffer: [UInt8], offset: Int = 0) -> Int? {
|
||||
guard offset + buffer.count <= self.count else {
|
||||
return nil
|
||||
}
|
||||
self[offset..<offset + buffer.count] = buffer[0..<buffer.count]
|
||||
return offset + buffer.count
|
||||
}
|
||||
|
||||
/// Copy bytes out of the buffer into another buffer.
|
||||
/// - Returns: The new offset after reading, or nil if the buffer is too small.
|
||||
package func copyOut(buffer: inout [UInt8], offset: Int = 0) -> Int? {
|
||||
guard offset + buffer.count <= self.count else {
|
||||
return nil
|
||||
}
|
||||
buffer[0..<buffer.count] = self[offset..<offset + buffer.count]
|
||||
return offset + buffer.count
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-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
|
||||
|
||||
public enum DNSResolverError: Swift.Error, CustomStringConvertible {
|
||||
case serverError(_ msg: String)
|
||||
case invalidHandlerSpec(_ spec: String)
|
||||
case unsupportedHandlerType(_ t: String)
|
||||
case invalidIP(_ v: String)
|
||||
case invalidHandlerOption(_ v: String)
|
||||
case handlerConfigError(_ msg: String)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .serverError(let msg):
|
||||
return "server error: \(msg)"
|
||||
case .invalidHandlerSpec(let msg):
|
||||
return "invalid handler spec: \(msg)"
|
||||
case .unsupportedHandlerType(let t):
|
||||
return "unsupported handler type specified: \(t)"
|
||||
case .invalidIP(let ip):
|
||||
return "invalid IP specified: \(ip)"
|
||||
case .invalidHandlerOption(let v):
|
||||
return "invalid handler option specified: \(v)"
|
||||
case .handlerConfigError(let msg):
|
||||
return "error configuring handler: \(msg)"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user