mirror of
https://github.com/apple/container.git
synced 2026-09-18 05:35:37 +00: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:
@@ -91,6 +91,7 @@ let package = Package(
|
||||
"ContainerBuild",
|
||||
"ContainerAPIClient",
|
||||
"ContainerLog",
|
||||
"ContainerNetworkService",
|
||||
"ContainerPersistence",
|
||||
"ContainerPlugin",
|
||||
"ContainerResource",
|
||||
@@ -263,6 +264,14 @@ let package = Package(
|
||||
],
|
||||
path: "Sources/Services/ContainerNetworkService/Server"
|
||||
),
|
||||
.testTarget(
|
||||
name: "ContainerNetworkServiceTests",
|
||||
dependencies: [
|
||||
.product(name: "Containerization", package: "containerization"),
|
||||
.product(name: "ContainerizationExtras", package: "containerization"),
|
||||
"ContainerNetworkService",
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "ContainerNetworkServiceClient",
|
||||
dependencies: [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerizationExtras
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@@ -32,7 +33,7 @@ class TestCLICreateCommand: CLITest {
|
||||
|
||||
@Test func testCreateWithMACAddress() throws {
|
||||
let name = getTestName()
|
||||
let expectedMAC = "02:42:ac:11:00:03"
|
||||
let expectedMAC = try MACAddress("02:42:ac:11:00:03")
|
||||
#expect(throws: Never.self, "expected container create with MAC address to succeed") {
|
||||
try doCreate(name: name, networks: ["default,mac=\(expectedMAC)"])
|
||||
try doStart(name: name)
|
||||
@@ -43,9 +44,9 @@ class TestCLICreateCommand: CLITest {
|
||||
try waitForContainerRunning(name)
|
||||
let inspectResp = try inspectContainer(name)
|
||||
#expect(inspectResp.networks.count > 0, "expected at least one network attachment")
|
||||
let actualMAC = inspectResp.networks[0].macAddress?.description ?? "nil"
|
||||
#expect(
|
||||
inspectResp.networks[0].macAddress?.description == expectedMAC,
|
||||
"expected MAC address \(expectedMAC), got \(inspectResp.networks[0].macAddress?.description ?? "nil")"
|
||||
actualMAC == expectedMAC.description, "expected MAC address \(expectedMAC), got \(actualMAC)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import Testing
|
||||
|
||||
@testable import ContainerNetworkService
|
||||
|
||||
struct AttachmentAllocatorTest {
|
||||
@Test func testAllocateSingleHostname() async throws {
|
||||
let allocator = try AttachmentAllocator(lower: 100, size: 10)
|
||||
|
||||
let address = try await allocator.allocate(hostname: "test-host")
|
||||
|
||||
#expect(address >= 100)
|
||||
#expect(address < 110)
|
||||
}
|
||||
|
||||
@Test func testAllocateSameHostnameTwice() async throws {
|
||||
let allocator = try AttachmentAllocator(lower: 100, size: 10)
|
||||
|
||||
let address1 = try await allocator.allocate(hostname: "test-host")
|
||||
let address2 = try await allocator.allocate(hostname: "test-host")
|
||||
|
||||
#expect(address1 == address2)
|
||||
}
|
||||
|
||||
@Test func testAllocateMultipleHostnames() async throws {
|
||||
let allocator = try AttachmentAllocator(lower: 100, size: 10)
|
||||
|
||||
let address1 = try await allocator.allocate(hostname: "host1")
|
||||
let address2 = try await allocator.allocate(hostname: "host2")
|
||||
let address3 = try await allocator.allocate(hostname: "host3")
|
||||
|
||||
#expect(address1 != address2)
|
||||
#expect(address2 != address3)
|
||||
#expect(address1 != address3)
|
||||
}
|
||||
|
||||
@Test func testLookupAllocatedHostname() async throws {
|
||||
let allocator = try AttachmentAllocator(lower: 100, size: 10)
|
||||
|
||||
let allocatedAddress = try await allocator.allocate(hostname: "test-host")
|
||||
let lookedUpAddress = try await allocator.lookup(hostname: "test-host")
|
||||
|
||||
#expect(lookedUpAddress == allocatedAddress)
|
||||
}
|
||||
|
||||
@Test func testLookupNonExistentHostname() async throws {
|
||||
let allocator = try AttachmentAllocator(lower: 100, size: 10)
|
||||
|
||||
let address = try await allocator.lookup(hostname: "non-existent")
|
||||
|
||||
#expect(address == nil)
|
||||
}
|
||||
|
||||
@Test func testDeallocateAllocatedHostname() async throws {
|
||||
let allocator = try AttachmentAllocator(lower: 100, size: 10)
|
||||
|
||||
let allocatedAddress = try await allocator.allocate(hostname: "test-host")
|
||||
let deallocatedAddress = try await allocator.deallocate(hostname: "test-host")
|
||||
|
||||
#expect(deallocatedAddress == allocatedAddress)
|
||||
|
||||
// After deallocation, lookup should return nil
|
||||
let lookedUpAddress = try await allocator.lookup(hostname: "test-host")
|
||||
#expect(lookedUpAddress == nil)
|
||||
}
|
||||
|
||||
@Test func testDeallocateNonExistentHostname() async throws {
|
||||
let allocator = try AttachmentAllocator(lower: 100, size: 10)
|
||||
|
||||
let deallocatedAddress = try await allocator.deallocate(hostname: "non-existent")
|
||||
|
||||
#expect(deallocatedAddress == nil)
|
||||
}
|
||||
|
||||
@Test func testReallocateAfterDeallocation() async throws {
|
||||
let allocator = try AttachmentAllocator(lower: 100, size: 10)
|
||||
|
||||
let address1 = try await allocator.allocate(hostname: "test-host")
|
||||
let released1 = try await allocator.deallocate(hostname: "test-host")
|
||||
#expect(address1 == released1)
|
||||
let address2 = try await allocator.allocate(hostname: "test-host")
|
||||
|
||||
// After deallocation, allocating the same hostname should give a new address
|
||||
#expect(address2 >= 100)
|
||||
#expect(address2 < 110)
|
||||
}
|
||||
|
||||
@Test func testAllocateUntilFull() async throws {
|
||||
let size = 5
|
||||
let allocator = try AttachmentAllocator(lower: 100, size: size)
|
||||
|
||||
// Allocate up to the limit
|
||||
for i in 0..<size {
|
||||
_ = try await allocator.allocate(hostname: "host\(i)")
|
||||
}
|
||||
|
||||
// Attempting to allocate one more should throw
|
||||
await #expect(throws: Error.self) {
|
||||
try await allocator.allocate(hostname: "extra-host")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testDeallocateAndReallocateDifferentHostname() async throws {
|
||||
let size = 3
|
||||
let allocator = try AttachmentAllocator(lower: 100, size: size)
|
||||
|
||||
// Fill up the allocator
|
||||
let address1 = try await allocator.allocate(hostname: "host1")
|
||||
let address2 = try await allocator.allocate(hostname: "host2")
|
||||
let address3 = try await allocator.allocate(hostname: "host3")
|
||||
|
||||
// Deallocate one
|
||||
let released2 = try await allocator.deallocate(hostname: "host2")
|
||||
#expect(address2 == released2)
|
||||
|
||||
// Should be able to allocate a new hostname now
|
||||
let newAddress = try await allocator.allocate(hostname: "host4")
|
||||
#expect(newAddress >= 100)
|
||||
#expect(newAddress < 103)
|
||||
|
||||
// The three remaining allocations should all be different
|
||||
let finalAddress1 = try await allocator.lookup(hostname: "host1")
|
||||
let finalAddress3 = try await allocator.lookup(hostname: "host3")
|
||||
let finalAddress4 = try await allocator.lookup(hostname: "host4")
|
||||
|
||||
#expect(finalAddress1 == address1)
|
||||
#expect(finalAddress3 == address3)
|
||||
#expect(finalAddress4 == newAddress)
|
||||
}
|
||||
|
||||
@Test func testDisableAllocatorWhenEmpty() async throws {
|
||||
let allocator = try AttachmentAllocator(lower: 100, size: 10)
|
||||
|
||||
let disabled = await allocator.disableAllocator()
|
||||
|
||||
#expect(disabled == true)
|
||||
|
||||
// After disabling, allocation should fail
|
||||
await #expect(throws: Error.self) {
|
||||
try await allocator.allocate(hostname: "test-host")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testDisableAllocatorWhenNotEmpty() async throws {
|
||||
let allocator = try AttachmentAllocator(lower: 100, size: 10)
|
||||
|
||||
_ = try await allocator.allocate(hostname: "test-host")
|
||||
|
||||
let disabled = await allocator.disableAllocator()
|
||||
|
||||
#expect(disabled == false)
|
||||
|
||||
// Since disable failed, should still be able to allocate
|
||||
let address = try await allocator.allocate(hostname: "another-host")
|
||||
#expect(address >= 100)
|
||||
#expect(address < 110)
|
||||
}
|
||||
|
||||
@Test func testDisableAfterDeallocatingAll() async throws {
|
||||
let allocator = try AttachmentAllocator(lower: 100, size: 10)
|
||||
|
||||
_ = try await allocator.allocate(hostname: "host1")
|
||||
_ = try await allocator.allocate(hostname: "host2")
|
||||
|
||||
try await allocator.deallocate(hostname: "host1")
|
||||
try await allocator.deallocate(hostname: "host2")
|
||||
|
||||
let disabled = await allocator.disableAllocator()
|
||||
|
||||
#expect(disabled == true)
|
||||
|
||||
// After disabling, allocation should fail
|
||||
await #expect(throws: Error.self) {
|
||||
try await allocator.allocate(hostname: "test-host")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testMultipleDeallocationsOfSameHostname() async throws {
|
||||
let allocator = try AttachmentAllocator(lower: 100, size: 10)
|
||||
|
||||
let address = try await allocator.allocate(hostname: "test-host")
|
||||
|
||||
let firstDeallocate = try await allocator.deallocate(hostname: "test-host")
|
||||
#expect(firstDeallocate == address)
|
||||
|
||||
// Second deallocation should return nil since it's already deallocated
|
||||
let secondDeallocate = try await allocator.deallocate(hostname: "test-host")
|
||||
#expect(secondDeallocate == nil)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -207,7 +207,7 @@ Use the `mac` option to specify a custom MAC address for your container's networ
|
||||
- Network testing scenarios requiring predictable MAC addresses
|
||||
- Consistent network configuration across container restarts
|
||||
|
||||
The MAC address must be in the format `XX:XX:XX:XX:XX:XX` (with colons or hyphens as separators):
|
||||
The MAC address must be in the format `XX:XX:XX:XX:XX:XX` (with colons or hyphens as separators). Set the two least significant bits of the first octet to `10` (locally signed, unicast address).
|
||||
|
||||
```bash
|
||||
container run --network default,mac=02:42:ac:11:00:02 ubuntu:latest
|
||||
@@ -223,7 +223,7 @@ To verify the MAC address is set correctly, run `ip addr show` inside the contai
|
||||
valid_lft forever preferred_lft forever
|
||||
```
|
||||
|
||||
If you don't specify a MAC address, the system will auto-generate one for you.
|
||||
If you don't specify a MAC address, `container` will generate one for you. The generated address has a first nibble set to hexadecimal `f` (`fX:XX:XX:XX:XX:XX`) in case you want to minimize the very small chance of conflict between your MAC address and generated addresses.
|
||||
|
||||
## Mount your host SSH authentication socket in your container
|
||||
|
||||
|
||||
Reference in New Issue
Block a user