Skip to content

Commit 32eac7e

Browse files
committed
Use Sendable DNS types.
- Closes #1268. - The types we were using weren't very usable with Swift 6 structured concurrency. - Use notImplemented instead of formatError for unknown record types. - Fold case on domain names. - Reject self-referencing compression pointers. - Use pure actor for LocalhostDNSHandler now that we have sendable types.
1 parent 79ef846 commit 32eac7e

20 files changed

Lines changed: 1333 additions & 163 deletions

Package.resolved

Lines changed: 1 addition & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ let package = Package(
4747
.library(name: "TerminalProgress", targets: ["TerminalProgress"]),
4848
],
4949
dependencies: [
50-
.package(url: "https://github.com/Bouke/DNS.git", from: "1.2.0"),
5150
.package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion)),
5251
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
5352
.package(url: "https://github.com/apple/swift-collections.git", from: "1.2.0"),
@@ -56,7 +55,6 @@ let package = Package(
5655
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"),
5756
.package(url: "https://github.com/apple/swift-system.git", from: "1.4.0"),
5857
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.26.0"),
59-
.package(url: "https://github.com/orlandos-nl/DNSClient.git", from: "2.4.1"),
6058
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.20.1"),
6159
.package(url: "https://github.com/swiftlang/swift-docc-plugin.git", from: "1.1.0"),
6260
],
@@ -427,17 +425,15 @@ let package = Package(
427425
dependencies: [
428426
.product(name: "NIOCore", package: "swift-nio"),
429427
.product(name: "NIOPosix", package: "swift-nio"),
430-
.product(name: "DNSClient", package: "DNSClient"),
431-
.product(name: "DNS", package: "DNS"),
432428
.product(name: "Logging", package: "swift-log"),
429+
.product(name: "ContainerizationExtras", package: "containerization"),
433430
.product(name: "ContainerizationOS", package: "containerization"),
434431
]
435432
),
436433
.testTarget(
437434
name: "DNSServerTests",
438435
dependencies: [
439-
.product(name: "DNS", package: "DNS"),
440-
"DNSServer",
436+
"DNSServer"
441437
]
442438
),
443439
.testTarget(

Sources/DNSServer/Handlers/HostTableResolver.swift

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
// limitations under the License.
1515
//===----------------------------------------------------------------------===//
1616

17-
import DNS
17+
import ContainerizationExtras
1818

1919
/// Handler that uses table lookup to resolve hostnames.
2020
public struct HostTableResolver: DNSHandler {
21-
public let hosts4: [String: IPv4]
21+
public let hosts4: [String: IPv4Address]
2222
private let ttl: UInt32
2323

24-
public init(hosts4: [String: IPv4], ttl: UInt32 = 300) {
24+
public init(hosts4: [String: IPv4Address], ttl: UInt32 = 300) {
2525
self.hosts4 = hosts4
2626
self.ttl = ttl
2727
}
@@ -48,28 +48,11 @@ public struct HostTableResolver: DNSHandler {
4848
}
4949
// If hostname doesn't exist, return nil which will become NXDOMAIN
5050
return nil
51-
case ResourceRecordType.nameServer,
52-
ResourceRecordType.alias,
53-
ResourceRecordType.startOfAuthority,
54-
ResourceRecordType.pointer,
55-
ResourceRecordType.mailExchange,
56-
ResourceRecordType.text,
57-
ResourceRecordType.service,
58-
ResourceRecordType.incrementalZoneTransfer,
59-
ResourceRecordType.standardZoneTransfer,
60-
ResourceRecordType.all:
61-
return Message(
62-
id: query.id,
63-
type: .response,
64-
returnCode: .notImplemented,
65-
questions: query.questions,
66-
answers: []
67-
)
6851
default:
6952
return Message(
7053
id: query.id,
7154
type: .response,
72-
returnCode: .formatError,
55+
returnCode: .notImplemented,
7356
questions: query.questions,
7457
answers: []
7558
)
@@ -93,6 +76,6 @@ public struct HostTableResolver: DNSHandler {
9376
return nil
9477
}
9578

96-
return HostRecord<IPv4>(name: question.name, ttl: ttl, ip: ip)
79+
return HostRecord<IPv4Address>(name: question.name, ttl: ttl, ip: ip)
9780
}
9881
}

Sources/DNSServer/Handlers/NxDomainResolver.swift

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
// limitations under the License.
1515
//===----------------------------------------------------------------------===//
1616

17-
import DNS
18-
1917
/// Handler that returns NXDOMAIN for all hostnames.
2018
public struct NxDomainResolver: DNSHandler {
2119
private let ttl: UInt32
@@ -35,29 +33,11 @@ public struct NxDomainResolver: DNSHandler {
3533
questions: query.questions,
3634
answers: []
3735
)
38-
case ResourceRecordType.nameServer,
39-
ResourceRecordType.alias,
40-
ResourceRecordType.startOfAuthority,
41-
ResourceRecordType.pointer,
42-
ResourceRecordType.mailExchange,
43-
ResourceRecordType.text,
44-
ResourceRecordType.host6,
45-
ResourceRecordType.service,
46-
ResourceRecordType.incrementalZoneTransfer,
47-
ResourceRecordType.standardZoneTransfer,
48-
ResourceRecordType.all:
49-
return Message(
50-
id: query.id,
51-
type: .response,
52-
returnCode: .notImplemented,
53-
questions: query.questions,
54-
answers: []
55-
)
5636
default:
5737
return Message(
5838
id: query.id,
5939
type: .response,
60-
returnCode: .formatError,
40+
returnCode: .notImplemented,
6141
questions: query.questions,
6242
answers: []
6343
)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
19+
// TODO: Look for a way that we can make use of the
20+
// bit-fiddling types from ContainerizationExtras, instead
21+
// of copying them here.
22+
23+
/// Errors that can occur during DNS message serialization/deserialization.
24+
public enum DNSBindError: Error, CustomStringConvertible {
25+
case marshalFailure(type: String, field: String)
26+
case unmarshalFailure(type: String, field: String)
27+
28+
public var description: String {
29+
switch self {
30+
case .marshalFailure(let type, let field):
31+
return "failed to marshal \(type).\(field)"
32+
case .unmarshalFailure(let type, let field):
33+
return "failed to unmarshal \(type).\(field)"
34+
}
35+
}
36+
}
37+
38+
/// Protocol for types that can be serialized to/from a byte buffer.
39+
protocol Bindable: Sendable {
40+
/// The fixed size of this type in bytes, if applicable.
41+
static var size: Int { get }
42+
43+
/// Serialize this value into the buffer at the given offset.
44+
/// - Returns: The new offset after writing.
45+
func appendBuffer(_ buffer: inout [UInt8], offset: Int) throws -> Int
46+
47+
/// Deserialize this value from the buffer at the given offset.
48+
/// - Returns: The new offset after reading.
49+
mutating func bindBuffer(_ buffer: inout [UInt8], offset: Int) throws -> Int
50+
}
51+
52+
extension [UInt8] {
53+
/// Copy a value into the buffer at the given offset.
54+
/// - Returns: The new offset after writing, or nil if the buffer is too small.
55+
package mutating func copyIn<T>(as type: T.Type, value: T, offset: Int = 0) -> Int? {
56+
let size = MemoryLayout<T>.size
57+
guard self.count >= size + offset else {
58+
return nil
59+
}
60+
return self.withUnsafeMutableBytes {
61+
$0.baseAddress?.advanced(by: offset).assumingMemoryBound(to: T.self).pointee = value
62+
return offset + size
63+
}
64+
}
65+
66+
/// Copy a value out of the buffer at the given offset.
67+
/// - Returns: A tuple of (new offset, value), or nil if the buffer is too small.
68+
package func copyOut<T>(as type: T.Type, offset: Int = 0) -> (Int, T)? {
69+
let size = MemoryLayout<T>.size
70+
guard self.count >= size + offset else {
71+
return nil
72+
}
73+
return self.withUnsafeBytes {
74+
guard let value = $0.baseAddress?.advanced(by: offset).assumingMemoryBound(to: T.self).pointee else {
75+
return nil
76+
}
77+
return (offset + size, value)
78+
}
79+
}
80+
81+
/// Copy a byte array into the buffer at the given offset.
82+
/// - Returns: The new offset after writing, or nil if the buffer is too small.
83+
package mutating func copyIn(buffer: [UInt8], offset: Int = 0) -> Int? {
84+
guard offset + buffer.count <= self.count else {
85+
return nil
86+
}
87+
self[offset..<offset + buffer.count] = buffer[0..<buffer.count]
88+
return offset + buffer.count
89+
}
90+
91+
/// Copy bytes out of the buffer into another buffer.
92+
/// - Returns: The new offset after reading, or nil if the buffer is too small.
93+
package func copyOut(buffer: inout [UInt8], offset: Int = 0) -> Int? {
94+
guard offset + buffer.count <= self.count else {
95+
return nil
96+
}
97+
buffer[0..<buffer.count] = self[offset..<offset + buffer.count]
98+
return offset + buffer.count
99+
}
100+
}

0 commit comments

Comments
 (0)