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:
J Logan
2026-01-07 15:35:35 -08:00
committed by GitHub
parent 5d6c750708
commit db8932ab0f
13 changed files with 322 additions and 46 deletions
@@ -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)
}
}