Initial commit

This commit is contained in:
Kathryn Baldauf
2025-06-03 15:23:07 -07:00
commit d5f30b8e3e
225 changed files with 27705 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// 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?
}
+84
View File
@@ -0,0 +1,84 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// 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 {
let chunkSize = 512
var data = Data()
self.log?.debug("reading data")
while packet.data.readableBytes > 0 {
if let chunk = packet.data.readBytes(length: min(chunkSize, packet.data.readableBytes)) {
data.append(contentsOf: chunk)
}
}
self.log?.debug("deserializing message")
let query = try Message(deserialize: data)
self.log?.debug("processing query: \(query.questions)")
// always send response
let responseData: Data
do {
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: []
)
// no responses
if response.answers.isEmpty {
response.returnCode = .nonExistentDomain
}
self.log?.debug("serializing response")
responseData = try response.serialize()
} catch {
self.log?.error("error processing message from \(query): \(error)")
let response = Message(
id: query.id,
type: .response,
returnCode: .notImplemented,
questions: query.questions,
answers: []
)
responseData = try response.serialize()
}
self.log?.debug("sending response for \(query.id)")
let rData = ByteBuffer(bytes: responseData)
try? await outbound.write(AddressedEnvelope(remoteAddress: packet.remoteAddress, data: rData))
self.log?.debug("processing done")
}
}
+86
View File
@@ -0,0 +1,86 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// 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 Apple Inc. and the container project authors. All rights reserved.
//
// 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,83 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// 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 DNS
/// Handler that uses table lookup to resolve hostnames.
public struct HostTableResolver: DNSHandler {
public let hosts4: [String: IPv4]
private let ttl: UInt32
public init(hosts4: [String: IPv4], ttl: UInt32 = 300) {
self.hosts4 = hosts4
self.ttl = ttl
}
public func answer(query: Message) async throws -> Message? {
let question = query.questions[0]
let record: ResourceRecord?
switch question.type {
case ResourceRecordType.host:
record = answerHost(question: question)
case ResourceRecordType.nameServer,
ResourceRecordType.alias,
ResourceRecordType.startOfAuthority,
ResourceRecordType.pointer,
ResourceRecordType.mailExchange,
ResourceRecordType.text,
ResourceRecordType.host6,
ResourceRecordType.service,
ResourceRecordType.incrementalZoneTransfer,
ResourceRecordType.standardZoneTransfer,
ResourceRecordType.all:
return Message(
id: query.id,
type: .response,
returnCode: .notImplemented,
questions: query.questions,
answers: []
)
default:
return Message(
id: query.id,
type: .response,
returnCode: .formatError,
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) -> ResourceRecord? {
guard let ip = hosts4[question.name] else {
return nil
}
return HostRecord<IPv4>(name: question.name, ttl: ttl, ip: ip)
}
}
@@ -0,0 +1,66 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// 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 DNS
/// 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: []
)
case ResourceRecordType.nameServer,
ResourceRecordType.alias,
ResourceRecordType.startOfAuthority,
ResourceRecordType.pointer,
ResourceRecordType.mailExchange,
ResourceRecordType.text,
ResourceRecordType.host6,
ResourceRecordType.service,
ResourceRecordType.incrementalZoneTransfer,
ResourceRecordType.standardZoneTransfer,
ResourceRecordType.all:
return Message(
id: query.id,
type: .response,
returnCode: .notImplemented,
questions: query.questions,
answers: []
)
default:
return Message(
id: query.id,
type: .response,
returnCode: .formatError,
questions: query.questions,
answers: []
)
}
}
}
@@ -0,0 +1,64 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// 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)
}
}
+53
View File
@@ -0,0 +1,53 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// 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 DNS
import Foundation
public typealias Message = DNS.Message
public typealias ResourceRecord = DNS.ResourceRecord
public typealias HostRecord = DNS.HostRecord
public typealias IPv4 = DNS.IPv4
public typealias IPv6 = DNS.IPv6
public typealias ReturnCode = DNS.ReturnCode
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)"
}
}
}