Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4de2a15
Use Sendable DNS types.
jglogan Mar 12, 2026
f716800
Additional PR feedback - domain name handling refinement.
jglogan Mar 13, 2026
c778d70
Additional PR feedback.
jglogan Mar 13, 2026
a1b6325
Rename API parameter to avoid breaking change.
jglogan Mar 16, 2026
ea1e492
Add default ttl to docc comment.
jglogan Mar 16, 2026
a108c64
Fix case folding in HostTableResolver.
jglogan Mar 16, 2026
5ac4c31
Test all record processing offsets, add compression ptr test.
jglogan Mar 18, 2026
f13d26e
Merge branch 'main' into dns-records
jglogan Mar 18, 2026
e8a75b5
Validate domain names in DNSName init.
jglogan Mar 18, 2026
04a628a
Don't default to a value for an invalid opcode or error code.
jglogan Mar 18, 2026
2cf5b1d
Adds compression pointer hop limit.
jglogan Mar 18, 2026
3fc5044
Range checks on question/answer counts.
jglogan Mar 18, 2026
a9d42c5
Return serverFailure error on errors other than data binding.
jglogan Mar 18, 2026
600183b
Layered input validation, miscellaneous fixes.
jglogan Mar 19, 2026
0046f87
Split Bindable.swift into separate files.
jglogan Mar 19, 2026
044ca99
security: bound packet rx size
jglogan Mar 19, 2026
5547f78
Prevent crash if handlers not protected by the validating handler.
jglogan Mar 19, 2026
a5429fd
Log failure to send response on NIO channel.
jglogan Mar 19, 2026
3e6a760
Eliminate a force-unwrap.
jglogan Mar 19, 2026
6d4ac74
Tighten the link compression pointer check.
jglogan Mar 19, 2026
c7d5d27
Multiple LocalhostDNSHandler fixes.
jglogan Mar 19, 2026
7cb521b
Key LocalhostDNSHandler on DNSName.
jglogan Mar 19, 2026
282440a
Merge remote-tracking branch 'upstream/main' into dns-records
jglogan Mar 19, 2026
69ad281
Prevent direct writes to DNSName label list.
jglogan Mar 19, 2026
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
36 changes: 14 additions & 22 deletions Sources/DNSServer/Handlers/HostTableResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,23 @@
// limitations under the License.
//===----------------------------------------------------------------------===//

import DNS
import ContainerizationExtras

/// Handler that uses table lookup to resolve hostnames.
///
/// All keys in `hosts4` must be canonical DNS names — fully-qualified with a
/// trailing dot (e.g. `"example.com."`). This matches the canonical form used
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

/// by `Question.name` when decoded from the wire.
public struct HostTableResolver: DNSHandler {
public let hosts4: [String: IPv4]
public let hosts4: [String: IPv4Address]
private let ttl: UInt32

public init(hosts4: [String: IPv4], ttl: UInt32 = 300) {
/// Creates a resolver backed by a static IPv4 host table.
///
/// - Parameter hosts4: A dictionary mapping fully-qualified domain names (with trailing dot)
/// to IPv4 addresses. Keys without a trailing dot will not match wire-decoded queries.
/// - Parameter ttl: The TTL in seconds to set on answer records.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: The TTL in seconds to set on answer records. Defaults to 300.

public init(hosts4: [String: IPv4Address], ttl: UInt32 = 300) {
self.hosts4 = hosts4
self.ttl = ttl
}
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,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

summary: all unhandled question types in handlers should return .notImplemented

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 @@ -93,6 +85,6 @@ public struct HostTableResolver: DNSHandler {
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
Loading