mirror of
https://github.com/apple/container.git
synced 2026-09-25 07:39:36 -05:00
Resolve IPv6 address queries for container names. (#1016)
- Closes #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`.
This commit is contained in:
@@ -31,11 +31,19 @@ extension Application {
|
||||
@Option(name: .customLong("label"), help: "Set metadata for a network")
|
||||
var labels: [String] = []
|
||||
|
||||
@Option(name: .customLong("subnet"), help: "Set subnet for a network")
|
||||
var ipv4Subnet: String? = nil
|
||||
@Option(
|
||||
name: .customLong("subnet"), help: "Set subnet for a network",
|
||||
transform: {
|
||||
try CIDRv4($0)
|
||||
})
|
||||
var ipv4Subnet: CIDRv4? = nil
|
||||
|
||||
@Option(name: .customLong("subnet-v6"), help: "Set the IPv6 prefix for a network")
|
||||
var ipv6Subnet: String? = nil
|
||||
@Option(
|
||||
name: .customLong("subnet-v6"), help: "Set the IPv6 prefix for a network",
|
||||
transform: {
|
||||
try CIDRv6($0)
|
||||
})
|
||||
var ipv6Subnet: CIDRv6? = nil
|
||||
|
||||
@OptionGroup
|
||||
var global: Flags.Global
|
||||
@@ -47,9 +55,13 @@ extension Application {
|
||||
|
||||
public func run() async throws {
|
||||
let parsedLabels = Utility.parseKeyValuePairs(labels)
|
||||
let ipv4Subnet = try ipv4Subnet.map { try CIDRv4($0) }
|
||||
let ipv6Subnet = try ipv6Subnet.map { try CIDRv6($0) }
|
||||
let config = try NetworkConfiguration(id: self.name, mode: .nat, ipv4Subnet: ipv4Subnet, ipv6Subnet: ipv6Subnet, labels: parsedLabels)
|
||||
let config = try NetworkConfiguration(
|
||||
id: self.name,
|
||||
mode: .nat,
|
||||
ipv4Subnet: ipv4Subnet,
|
||||
ipv6Subnet: ipv6Subnet,
|
||||
labels: parsedLabels
|
||||
)
|
||||
let state = try await ClientNetwork.create(configuration: config)
|
||||
print(state.id)
|
||||
}
|
||||
|
||||
@@ -26,22 +26,25 @@ public struct Attachment: Codable, Sendable {
|
||||
public let ipv4Address: CIDRv4
|
||||
/// The IPv4 gateway address.
|
||||
public let ipv4Gateway: IPv4Address
|
||||
/// The CIDR address describing the interface IPv6 address, with the prefix length of the subnet.
|
||||
/// The address is nil if the IPv6 subnet could not be determined at network creation time.
|
||||
public let ipv6Address: CIDRv6?
|
||||
/// The MAC address associated with the attachment (optional).
|
||||
public let macAddress: MACAddress?
|
||||
|
||||
public init(network: String, hostname: String, ipv4Address: CIDRv4, ipv4Gateway: IPv4Address, macAddress: MACAddress? = nil) {
|
||||
public init(
|
||||
network: String,
|
||||
hostname: String,
|
||||
ipv4Address: CIDRv4,
|
||||
ipv4Gateway: IPv4Address,
|
||||
ipv6Address: CIDRv6?,
|
||||
macAddress: MACAddress?
|
||||
) {
|
||||
self.network = network
|
||||
self.hostname = hostname
|
||||
self.ipv4Address = ipv4Address
|
||||
self.ipv4Gateway = ipv4Gateway
|
||||
self.ipv6Address = ipv6Address
|
||||
self.macAddress = macAddress
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case network
|
||||
case hostname
|
||||
case ipv4Address
|
||||
case ipv4Gateway
|
||||
case macAddress
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerizationExtras
|
||||
|
||||
/// Configuration information for attaching a container network interface to a network.
|
||||
public struct AttachmentConfiguration: Codable, Sendable {
|
||||
/// The network ID associated with the attachment.
|
||||
@@ -34,9 +36,9 @@ public struct AttachmentOptions: Codable, Sendable {
|
||||
public let hostname: String
|
||||
|
||||
/// The MAC address associated with the attachment (optional).
|
||||
public let macAddress: String?
|
||||
public let macAddress: MACAddress?
|
||||
|
||||
public init(hostname: String, macAddress: String? = nil) {
|
||||
public init(hostname: String, macAddress: MACAddress? = nil) {
|
||||
self.hostname = hostname
|
||||
self.macAddress = macAddress
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ public struct NetworkStatus: Codable, Sendable {
|
||||
|
||||
/// The address allocated for the IPv6 network if no subnet was specified at
|
||||
/// creation time; otherwise, the IPv6 subnet from the configuration.
|
||||
/// The value is nil if the IPv6 subnet cannot be determined at creation time.
|
||||
public let ipv6Subnet: CIDRv6?
|
||||
|
||||
public init(
|
||||
|
||||
@@ -35,11 +35,12 @@ struct ContainerDNSHandler: DNSHandler {
|
||||
case ResourceRecordType.host:
|
||||
record = try await answerHost(question: question)
|
||||
case ResourceRecordType.host6:
|
||||
// Return NODATA (noError with empty answers) for AAAA queries ONLY if A record exists.
|
||||
// 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 try await networkService.lookup(hostname: question.name) != nil {
|
||||
let result = try await answerHost6(question: question)
|
||||
if result.record == nil && result.hostnameExists {
|
||||
// Return NODATA (noError with empty answers) when hostname exists but has no IPv6.
|
||||
// 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".
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
@@ -48,8 +49,7 @@ struct ContainerDNSHandler: DNSHandler {
|
||||
answers: []
|
||||
)
|
||||
}
|
||||
// If hostname doesn't exist, return nil which will become NXDOMAIN
|
||||
return nil
|
||||
record = result.record
|
||||
case ResourceRecordType.nameServer,
|
||||
ResourceRecordType.alias,
|
||||
ResourceRecordType.startOfAuthority,
|
||||
@@ -101,4 +101,19 @@ struct ContainerDNSHandler: DNSHandler {
|
||||
|
||||
return HostRecord<IPv4>(name: question.name, ttl: ttl, ip: ip)
|
||||
}
|
||||
|
||||
private func answerHost6(question: Question) async throws -> (record: ResourceRecord?, hostnameExists: Bool) {
|
||||
guard let ipAllocation = try await networkService.lookup(hostname: question.name) else {
|
||||
return (nil, false)
|
||||
}
|
||||
guard let ipv6Address = ipAllocation.ipv6Address else {
|
||||
return (nil, true)
|
||||
}
|
||||
let ipv6 = ipv6Address.address.description
|
||||
guard let ip = IPv6(ipv6) else {
|
||||
throw DNSResolverError.serverError("failed to parse IPv6 address: \(ipv6)")
|
||||
}
|
||||
|
||||
return (HostRecord<IPv6>(name: question.name, ttl: ttl, ip: ip), true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,16 +278,17 @@ public struct Utility {
|
||||
}
|
||||
|
||||
// attach the first network using the fqdn, and the rest using just the container ID
|
||||
return networks.enumerated().map { item in
|
||||
return try networks.enumerated().map { item in
|
||||
let macAddress = try item.element.macAddress.map { try MACAddress($0) }
|
||||
guard item.offset == 0 else {
|
||||
return AttachmentConfiguration(
|
||||
network: item.element.name,
|
||||
options: AttachmentOptions(hostname: containerId, macAddress: item.element.macAddress)
|
||||
options: AttachmentOptions(hostname: containerId, macAddress: macAddress)
|
||||
)
|
||||
}
|
||||
return AttachmentConfiguration(
|
||||
network: item.element.name,
|
||||
options: AttachmentOptions(hostname: fqdn ?? containerId, macAddress: item.element.macAddress)
|
||||
options: AttachmentOptions(hostname: fqdn ?? containerId, macAddress: macAddress)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import ContainerResource
|
||||
import ContainerXPC
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import Foundation
|
||||
|
||||
/// A client for interacting with a single network.
|
||||
@@ -47,11 +48,14 @@ extension NetworkClient {
|
||||
return state
|
||||
}
|
||||
|
||||
public func allocate(hostname: String, macAddress: String? = nil) async throws -> (attachment: Attachment, additionalData: XPCMessage?) {
|
||||
public func allocate(
|
||||
hostname: String,
|
||||
macAddress: MACAddress? = nil
|
||||
) async throws -> (attachment: Attachment, additionalData: XPCMessage?) {
|
||||
let request = XPCMessage(route: NetworkRoutes.allocate.rawValue)
|
||||
request.set(key: NetworkKeys.hostname.rawValue, value: hostname)
|
||||
if let macAddress = macAddress {
|
||||
request.set(key: NetworkKeys.macAddress.rawValue, value: macAddress)
|
||||
request.set(key: NetworkKeys.macAddress.rawValue, value: macAddress.description)
|
||||
}
|
||||
|
||||
let client = createClient()
|
||||
|
||||
@@ -42,10 +42,14 @@ actor AttachmentAllocator {
|
||||
}
|
||||
|
||||
/// Free an allocated network address by hostname.
|
||||
func deallocate(hostname: String) async throws {
|
||||
if let index = hostnames.removeValue(forKey: hostname) {
|
||||
try allocator.release(index)
|
||||
@discardableResult
|
||||
func deallocate(hostname: String) async throws -> UInt32? {
|
||||
guard let index = hostnames.removeValue(forKey: hostname) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
try allocator.release(index)
|
||||
return index
|
||||
}
|
||||
|
||||
/// If no addresses are allocated, prevent future allocations and return true.
|
||||
|
||||
@@ -26,6 +26,7 @@ public actor NetworkService: Sendable {
|
||||
private let network: any Network
|
||||
private let log: Logger?
|
||||
private var allocator: AttachmentAllocator
|
||||
private var macAddresses: [UInt32: MACAddress]
|
||||
|
||||
/// Set up a network service for the specified network.
|
||||
public init(
|
||||
@@ -41,6 +42,7 @@ public actor NetworkService: Sendable {
|
||||
|
||||
let size = Int(subnet.upper.value - subnet.lower.value - 3)
|
||||
self.allocator = try AttachmentAllocator(lower: subnet.lower.value + 2, size: size)
|
||||
self.macAddresses = [:]
|
||||
self.network = network
|
||||
self.log = log
|
||||
}
|
||||
@@ -61,16 +63,20 @@ public actor NetworkService: Sendable {
|
||||
}
|
||||
|
||||
let hostname = try message.hostname()
|
||||
let macAddress = try message.string(key: NetworkKeys.macAddress.rawValue)
|
||||
let macAddress =
|
||||
try message.string(key: NetworkKeys.macAddress.rawValue)
|
||||
.map { try MACAddress($0) }
|
||||
?? MACAddress((UInt64.random(in: 0...UInt64.max) & 0x0cff_ffff_ffff) | 0xf200_0000_0000)
|
||||
let index = try await allocator.allocate(hostname: hostname)
|
||||
let subnet = status.ipv4Subnet
|
||||
let ipv6Address = try status.ipv6Subnet
|
||||
.map { try CIDRv6(macAddress.ipv6Address(network: $0.lower), prefix: $0.prefix) }
|
||||
let ip = IPv4Address(index)
|
||||
let attachment = Attachment(
|
||||
network: state.id,
|
||||
hostname: hostname,
|
||||
ipv4Address: try CIDRv4(ip, prefix: subnet.prefix),
|
||||
ipv4Address: try CIDRv4(ip, prefix: status.ipv4Subnet.prefix),
|
||||
ipv4Gateway: status.ipv4Gateway,
|
||||
ipv6Address: ipv6Address,
|
||||
macAddress: macAddress
|
||||
)
|
||||
log?.info(
|
||||
@@ -79,7 +85,8 @@ public actor NetworkService: Sendable {
|
||||
"hostname": "\(hostname)",
|
||||
"ipv4Address": "\(attachment.ipv4Address)",
|
||||
"ipv4Gateway": "\(attachment.ipv4Gateway)",
|
||||
"macAddress": "\(macAddress?.description ?? "unspecified")",
|
||||
"ipv6Address": "\(attachment.ipv6Address?.description ?? "unavailable")",
|
||||
"macAddress": "\(attachment.macAddress?.description ?? "unspecified")",
|
||||
])
|
||||
let reply = message.reply()
|
||||
try reply.setAttachment(attachment)
|
||||
@@ -88,13 +95,16 @@ public actor NetworkService: Sendable {
|
||||
try reply.setAdditionalData(additionalData.underlying)
|
||||
}
|
||||
}
|
||||
macAddresses[index] = macAddress
|
||||
return reply
|
||||
}
|
||||
|
||||
@Sendable
|
||||
public func deallocate(_ message: XPCMessage) async throws -> XPCMessage {
|
||||
let hostname = try message.hostname()
|
||||
try await allocator.deallocate(hostname: hostname)
|
||||
if let index = try await allocator.deallocate(hostname: hostname) {
|
||||
macAddresses.removeValue(forKey: index)
|
||||
}
|
||||
log?.info("released attachments", metadata: ["hostname": "\(hostname)"])
|
||||
return message.reply()
|
||||
}
|
||||
@@ -112,14 +122,21 @@ public actor NetworkService: Sendable {
|
||||
guard let index else {
|
||||
return reply
|
||||
}
|
||||
|
||||
guard let macAddress = macAddresses[index] else {
|
||||
return reply
|
||||
}
|
||||
let address = IPv4Address(index)
|
||||
let subnet = status.ipv4Subnet
|
||||
let ipv4Address = try CIDRv4(address, prefix: subnet.prefix)
|
||||
let ipv6Address = try status.ipv6Subnet
|
||||
.map { try CIDRv6(macAddress.ipv6Address(network: $0.lower), prefix: $0.prefix) }
|
||||
let attachment = Attachment(
|
||||
network: state.id,
|
||||
hostname: hostname,
|
||||
ipv4Address: try CIDRv4(address, prefix: subnet.prefix),
|
||||
ipv4Gateway: status.ipv4Gateway
|
||||
ipv4Address: ipv4Address,
|
||||
ipv4Gateway: status.ipv4Gateway,
|
||||
ipv6Address: ipv6Address,
|
||||
macAddress: macAddress
|
||||
)
|
||||
log?.debug(
|
||||
"lookup attachment",
|
||||
|
||||
Reference in New Issue
Block a user