Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 1 addition & 19 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 2 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ let package = Package(
.library(name: "TerminalProgress", targets: ["TerminalProgress"]),
],
dependencies: [
.package(url: "https://github.com/Bouke/DNS.git", from: "1.2.0"),
.package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion)),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.2.0"),
Expand All @@ -56,7 +55,6 @@ let package = Package(
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"),
.package(url: "https://github.com/apple/swift-system.git", from: "1.4.0"),
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.26.0"),
.package(url: "https://github.com/orlandos-nl/DNSClient.git", from: "2.4.1"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.20.1"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin.git", from: "1.1.0"),
],
Expand Down Expand Up @@ -427,17 +425,15 @@ let package = Package(
dependencies: [
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "DNSClient", package: "DNSClient"),
.product(name: "DNS", package: "DNS"),
.product(name: "Logging", package: "swift-log"),
.product(name: "ContainerizationExtras", package: "containerization"),
.product(name: "ContainerizationOS", package: "containerization"),
]
),
.testTarget(
name: "DNSServerTests",
dependencies: [
.product(name: "DNS", package: "DNS"),
"DNSServer",
"DNSServer"
]
),
.testTarget(
Expand Down
42 changes: 17 additions & 25 deletions Sources/DNSServer/Handlers/HostTableResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,24 @@
// limitations under the License.
//===----------------------------------------------------------------------===//

import DNS
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: [String: IPv4]
public let hosts4: [DNSName: IPv4Address]
private let ttl: UInt32

public init(hosts4: [String: IPv4], ttl: UInt32 = 300) {
self.hosts4 = hosts4
/// 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).
public init(hosts4: [String: IPv4Address], ttl: UInt32 = 300) {
self.hosts4 = Dictionary(uniqueKeysWithValues: hosts4.map { (DNSName($0.key), $0.value) })
self.ttl = ttl
}

Expand All @@ -37,7 +46,7 @@ public struct HostTableResolver: DNSHandler {
// 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[question.name] != nil {
if hosts4[DNSName(question.name)] != nil {
return Message(
id: query.id,
type: .response,
Expand All @@ -48,28 +57,11 @@ public struct HostTableResolver: DNSHandler {
}
// If hostname doesn't exist, return nil which will become NXDOMAIN
return nil
case ResourceRecordType.nameServer,
ResourceRecordType.alias,
ResourceRecordType.startOfAuthority,
ResourceRecordType.pointer,
ResourceRecordType.mailExchange,
ResourceRecordType.text,
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,
returnCode: .notImplemented,
questions: query.questions,
answers: []
)
Expand All @@ -89,10 +81,10 @@ public struct HostTableResolver: DNSHandler {
}

private func answerHost(question: Question) -> ResourceRecord? {
guard let ip = hosts4[question.name] else {
guard let ip = hosts4[DNSName(question.name)] else {
return nil
}

return HostRecord<IPv4>(name: question.name, ttl: ttl, ip: ip)
return HostRecord<IPv4Address>(name: question.name, ttl: ttl, ip: ip)
}
}
22 changes: 1 addition & 21 deletions Sources/DNSServer/Handlers/NxDomainResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
// limitations under the License.
//===----------------------------------------------------------------------===//

import DNS

/// Handler that returns NXDOMAIN for all hostnames.
public struct NxDomainResolver: DNSHandler {
private let ttl: UInt32
Expand All @@ -35,29 +33,11 @@ public struct NxDomainResolver: DNSHandler {
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,
returnCode: .notImplemented,
questions: query.questions,
answers: []
)
Expand Down
100 changes: 100 additions & 0 deletions Sources/DNSServer/Records/Bindable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//===----------------------------------------------------------------------===//
// 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: Look for a way that we can make use of the
// bit-fiddling types from ContainerizationExtras, instead
// of copying them here.

/// 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)

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)"
}
}
}

/// Protocol for types that can be serialized to/from a byte buffer.
protocol Bindable: Sendable {
/// The fixed size of this type in bytes, if applicable.
static var size: Int { get }

/// Serialize this value into the buffer at the given offset.
/// - Returns: The new offset after writing.
func appendBuffer(_ buffer: inout [UInt8], offset: Int) throws -> Int

/// Deserialize this value from the buffer at the given offset.
/// - Returns: The new offset after reading.
mutating func bindBuffer(_ buffer: inout [UInt8], offset: Int) throws -> Int
}

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
}
}
Loading