Skip to content

Commit db8932a

Browse files
authored
Resolve IPv6 address queries for container names. (apple#1016)
- Closes apple#1005. - Adapt everything to use MACAddress type from containerization 0.20.0. - Allocate MAC addresses for every container so that we have deterministic IPv6 link local addresses. - Add AAAA handling to ContainerDNSHandler. - NOTE: Only works on Tahoe. On Sequoia, we don't have a good way to set or determine the IPv6 network prefix when networks are created, so we can't infer the IPv6 link local addresses for AAAA responses and we instead return `NODATA`.
1 parent 5d6c750 commit db8932a

File tree

13 files changed

+322
-46
lines changed

13 files changed

+322
-46
lines changed

Package.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ let package = Package(
9191
"ContainerBuild",
9292
"ContainerAPIClient",
9393
"ContainerLog",
94+
"ContainerNetworkService",
9495
"ContainerPersistence",
9596
"ContainerPlugin",
9697
"ContainerResource",
@@ -263,6 +264,14 @@ let package = Package(
263264
],
264265
path: "Sources/Services/ContainerNetworkService/Server"
265266
),
267+
.testTarget(
268+
name: "ContainerNetworkServiceTests",
269+
dependencies: [
270+
.product(name: "Containerization", package: "containerization"),
271+
.product(name: "ContainerizationExtras", package: "containerization"),
272+
"ContainerNetworkService",
273+
]
274+
),
266275
.target(
267276
name: "ContainerNetworkServiceClient",
268277
dependencies: [

Sources/ContainerCommands/Network/NetworkCreate.swift

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,19 @@ extension Application {
3131
@Option(name: .customLong("label"), help: "Set metadata for a network")
3232
var labels: [String] = []
3333

34-
@Option(name: .customLong("subnet"), help: "Set subnet for a network")
35-
var ipv4Subnet: String? = nil
34+
@Option(
35+
name: .customLong("subnet"), help: "Set subnet for a network",
36+
transform: {
37+
try CIDRv4($0)
38+
})
39+
var ipv4Subnet: CIDRv4? = nil
3640

37-
@Option(name: .customLong("subnet-v6"), help: "Set the IPv6 prefix for a network")
38-
var ipv6Subnet: String? = nil
41+
@Option(
42+
name: .customLong("subnet-v6"), help: "Set the IPv6 prefix for a network",
43+
transform: {
44+
try CIDRv6($0)
45+
})
46+
var ipv6Subnet: CIDRv6? = nil
3947

4048
@OptionGroup
4149
var global: Flags.Global
@@ -47,9 +55,13 @@ extension Application {
4755

4856
public func run() async throws {
4957
let parsedLabels = Utility.parseKeyValuePairs(labels)
50-
let ipv4Subnet = try ipv4Subnet.map { try CIDRv4($0) }
51-
let ipv6Subnet = try ipv6Subnet.map { try CIDRv6($0) }
52-
let config = try NetworkConfiguration(id: self.name, mode: .nat, ipv4Subnet: ipv4Subnet, ipv6Subnet: ipv6Subnet, labels: parsedLabels)
58+
let config = try NetworkConfiguration(
59+
id: self.name,
60+
mode: .nat,
61+
ipv4Subnet: ipv4Subnet,
62+
ipv6Subnet: ipv6Subnet,
63+
labels: parsedLabels
64+
)
5365
let state = try await ClientNetwork.create(configuration: config)
5466
print(state.id)
5567
}

Sources/ContainerResource/Network/Attachment.swift

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,25 @@ public struct Attachment: Codable, Sendable {
2626
public let ipv4Address: CIDRv4
2727
/// The IPv4 gateway address.
2828
public let ipv4Gateway: IPv4Address
29+
/// The CIDR address describing the interface IPv6 address, with the prefix length of the subnet.
30+
/// The address is nil if the IPv6 subnet could not be determined at network creation time.
31+
public let ipv6Address: CIDRv6?
2932
/// The MAC address associated with the attachment (optional).
3033
public let macAddress: MACAddress?
3134

32-
public init(network: String, hostname: String, ipv4Address: CIDRv4, ipv4Gateway: IPv4Address, macAddress: MACAddress? = nil) {
35+
public init(
36+
network: String,
37+
hostname: String,
38+
ipv4Address: CIDRv4,
39+
ipv4Gateway: IPv4Address,
40+
ipv6Address: CIDRv6?,
41+
macAddress: MACAddress?
42+
) {
3343
self.network = network
3444
self.hostname = hostname
3545
self.ipv4Address = ipv4Address
3646
self.ipv4Gateway = ipv4Gateway
47+
self.ipv6Address = ipv6Address
3748
self.macAddress = macAddress
3849
}
39-
40-
enum CodingKeys: String, CodingKey {
41-
case network
42-
case hostname
43-
case ipv4Address
44-
case ipv4Gateway
45-
case macAddress
46-
}
4750
}

Sources/ContainerResource/Network/AttachmentConfiguration.swift

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

17+
import ContainerizationExtras
18+
1719
/// Configuration information for attaching a container network interface to a network.
1820
public struct AttachmentConfiguration: Codable, Sendable {
1921
/// The network ID associated with the attachment.
@@ -34,9 +36,9 @@ public struct AttachmentOptions: Codable, Sendable {
3436
public let hostname: String
3537

3638
/// The MAC address associated with the attachment (optional).
37-
public let macAddress: String?
39+
public let macAddress: MACAddress?
3840

39-
public init(hostname: String, macAddress: String? = nil) {
41+
public init(hostname: String, macAddress: MACAddress? = nil) {
4042
self.hostname = hostname
4143
self.macAddress = macAddress
4244
}

Sources/ContainerResource/Network/NetworkState.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public struct NetworkStatus: Codable, Sendable {
2727

2828
/// The address allocated for the IPv6 network if no subnet was specified at
2929
/// creation time; otherwise, the IPv6 subnet from the configuration.
30+
/// The value is nil if the IPv6 subnet cannot be determined at creation time.
3031
public let ipv6Subnet: CIDRv6?
3132

3233
public init(

Sources/Helpers/APIServer/ContainerDNSHandler.swift

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,12 @@ struct ContainerDNSHandler: DNSHandler {
3535
case ResourceRecordType.host:
3636
record = try await answerHost(question: question)
3737
case ResourceRecordType.host6:
38-
// Return NODATA (noError with empty answers) for AAAA queries ONLY if A record exists.
39-
// This is required because musl libc has issues when A record exists but AAAA returns NXDOMAIN.
40-
// musl treats NXDOMAIN on AAAA as "domain doesn't exist" and fails DNS resolution entirely.
41-
// NODATA correctly indicates "no IPv6 address available, but domain exists".
42-
if try await networkService.lookup(hostname: question.name) != nil {
38+
let result = try await answerHost6(question: question)
39+
if result.record == nil && result.hostnameExists {
40+
// Return NODATA (noError with empty answers) when hostname exists but has no IPv6.
41+
// This is required because musl libc has issues when A record exists but AAAA returns NXDOMAIN.
42+
// musl treats NXDOMAIN on AAAA as "domain doesn't exist" and fails DNS resolution entirely.
43+
// NODATA correctly indicates "no IPv6 address available, but domain exists".
4344
return Message(
4445
id: query.id,
4546
type: .response,
@@ -48,8 +49,7 @@ struct ContainerDNSHandler: DNSHandler {
4849
answers: []
4950
)
5051
}
51-
// If hostname doesn't exist, return nil which will become NXDOMAIN
52-
return nil
52+
record = result.record
5353
case ResourceRecordType.nameServer,
5454
ResourceRecordType.alias,
5555
ResourceRecordType.startOfAuthority,
@@ -101,4 +101,19 @@ struct ContainerDNSHandler: DNSHandler {
101101

102102
return HostRecord<IPv4>(name: question.name, ttl: ttl, ip: ip)
103103
}
104+
105+
private func answerHost6(question: Question) async throws -> (record: ResourceRecord?, hostnameExists: Bool) {
106+
guard let ipAllocation = try await networkService.lookup(hostname: question.name) else {
107+
return (nil, false)
108+
}
109+
guard let ipv6Address = ipAllocation.ipv6Address else {
110+
return (nil, true)
111+
}
112+
let ipv6 = ipv6Address.address.description
113+
guard let ip = IPv6(ipv6) else {
114+
throw DNSResolverError.serverError("failed to parse IPv6 address: \(ipv6)")
115+
}
116+
117+
return (HostRecord<IPv6>(name: question.name, ttl: ttl, ip: ip), true)
118+
}
104119
}

Sources/Services/ContainerAPIService/Client/Utility.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,16 +278,17 @@ public struct Utility {
278278
}
279279

280280
// attach the first network using the fqdn, and the rest using just the container ID
281-
return networks.enumerated().map { item in
281+
return try networks.enumerated().map { item in
282+
let macAddress = try item.element.macAddress.map { try MACAddress($0) }
282283
guard item.offset == 0 else {
283284
return AttachmentConfiguration(
284285
network: item.element.name,
285-
options: AttachmentOptions(hostname: containerId, macAddress: item.element.macAddress)
286+
options: AttachmentOptions(hostname: containerId, macAddress: macAddress)
286287
)
287288
}
288289
return AttachmentConfiguration(
289290
network: item.element.name,
290-
options: AttachmentOptions(hostname: fqdn ?? containerId, macAddress: item.element.macAddress)
291+
options: AttachmentOptions(hostname: fqdn ?? containerId, macAddress: macAddress)
291292
)
292293
}
293294
}

Sources/Services/ContainerNetworkService/Client/NetworkClient.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import ContainerResource
1818
import ContainerXPC
1919
import ContainerizationError
20+
import ContainerizationExtras
2021
import Foundation
2122

2223
/// A client for interacting with a single network.
@@ -47,11 +48,14 @@ extension NetworkClient {
4748
return state
4849
}
4950

50-
public func allocate(hostname: String, macAddress: String? = nil) async throws -> (attachment: Attachment, additionalData: XPCMessage?) {
51+
public func allocate(
52+
hostname: String,
53+
macAddress: MACAddress? = nil
54+
) async throws -> (attachment: Attachment, additionalData: XPCMessage?) {
5155
let request = XPCMessage(route: NetworkRoutes.allocate.rawValue)
5256
request.set(key: NetworkKeys.hostname.rawValue, value: hostname)
5357
if let macAddress = macAddress {
54-
request.set(key: NetworkKeys.macAddress.rawValue, value: macAddress)
58+
request.set(key: NetworkKeys.macAddress.rawValue, value: macAddress.description)
5559
}
5660

5761
let client = createClient()

Sources/Services/ContainerNetworkService/Server/AttachmentAllocator.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,14 @@ actor AttachmentAllocator {
4242
}
4343

4444
/// Free an allocated network address by hostname.
45-
func deallocate(hostname: String) async throws {
46-
if let index = hostnames.removeValue(forKey: hostname) {
47-
try allocator.release(index)
45+
@discardableResult
46+
func deallocate(hostname: String) async throws -> UInt32? {
47+
guard let index = hostnames.removeValue(forKey: hostname) else {
48+
return nil
4849
}
50+
51+
try allocator.release(index)
52+
return index
4953
}
5054

5155
/// If no addresses are allocated, prevent future allocations and return true.

Sources/Services/ContainerNetworkService/Server/NetworkService.swift

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public actor NetworkService: Sendable {
2626
private let network: any Network
2727
private let log: Logger?
2828
private var allocator: AttachmentAllocator
29+
private var macAddresses: [UInt32: MACAddress]
2930

3031
/// Set up a network service for the specified network.
3132
public init(
@@ -41,6 +42,7 @@ public actor NetworkService: Sendable {
4142

4243
let size = Int(subnet.upper.value - subnet.lower.value - 3)
4344
self.allocator = try AttachmentAllocator(lower: subnet.lower.value + 2, size: size)
45+
self.macAddresses = [:]
4446
self.network = network
4547
self.log = log
4648
}
@@ -61,16 +63,20 @@ public actor NetworkService: Sendable {
6163
}
6264

6365
let hostname = try message.hostname()
64-
let macAddress = try message.string(key: NetworkKeys.macAddress.rawValue)
66+
let macAddress =
67+
try message.string(key: NetworkKeys.macAddress.rawValue)
6568
.map { try MACAddress($0) }
69+
?? MACAddress((UInt64.random(in: 0...UInt64.max) & 0x0cff_ffff_ffff) | 0xf200_0000_0000)
6670
let index = try await allocator.allocate(hostname: hostname)
67-
let subnet = status.ipv4Subnet
71+
let ipv6Address = try status.ipv6Subnet
72+
.map { try CIDRv6(macAddress.ipv6Address(network: $0.lower), prefix: $0.prefix) }
6873
let ip = IPv4Address(index)
6974
let attachment = Attachment(
7075
network: state.id,
7176
hostname: hostname,
72-
ipv4Address: try CIDRv4(ip, prefix: subnet.prefix),
77+
ipv4Address: try CIDRv4(ip, prefix: status.ipv4Subnet.prefix),
7378
ipv4Gateway: status.ipv4Gateway,
79+
ipv6Address: ipv6Address,
7480
macAddress: macAddress
7581
)
7682
log?.info(
@@ -79,7 +85,8 @@ public actor NetworkService: Sendable {
7985
"hostname": "\(hostname)",
8086
"ipv4Address": "\(attachment.ipv4Address)",
8187
"ipv4Gateway": "\(attachment.ipv4Gateway)",
82-
"macAddress": "\(macAddress?.description ?? "unspecified")",
88+
"ipv6Address": "\(attachment.ipv6Address?.description ?? "unavailable")",
89+
"macAddress": "\(attachment.macAddress?.description ?? "unspecified")",
8390
])
8491
let reply = message.reply()
8592
try reply.setAttachment(attachment)
@@ -88,13 +95,16 @@ public actor NetworkService: Sendable {
8895
try reply.setAdditionalData(additionalData.underlying)
8996
}
9097
}
98+
macAddresses[index] = macAddress
9199
return reply
92100
}
93101

94102
@Sendable
95103
public func deallocate(_ message: XPCMessage) async throws -> XPCMessage {
96104
let hostname = try message.hostname()
97-
try await allocator.deallocate(hostname: hostname)
105+
if let index = try await allocator.deallocate(hostname: hostname) {
106+
macAddresses.removeValue(forKey: index)
107+
}
98108
log?.info("released attachments", metadata: ["hostname": "\(hostname)"])
99109
return message.reply()
100110
}
@@ -112,14 +122,21 @@ public actor NetworkService: Sendable {
112122
guard let index else {
113123
return reply
114124
}
115-
125+
guard let macAddress = macAddresses[index] else {
126+
return reply
127+
}
116128
let address = IPv4Address(index)
117129
let subnet = status.ipv4Subnet
130+
let ipv4Address = try CIDRv4(address, prefix: subnet.prefix)
131+
let ipv6Address = try status.ipv6Subnet
132+
.map { try CIDRv6(macAddress.ipv6Address(network: $0.lower), prefix: $0.prefix) }
118133
let attachment = Attachment(
119134
network: state.id,
120135
hostname: hostname,
121-
ipv4Address: try CIDRv4(address, prefix: subnet.prefix),
122-
ipv4Gateway: status.ipv4Gateway
136+
ipv4Address: ipv4Address,
137+
ipv4Gateway: status.ipv4Gateway,
138+
ipv6Address: ipv6Address,
139+
macAddress: macAddress
123140
)
124141
log?.debug(
125142
"lookup attachment",

0 commit comments

Comments
 (0)