mirror of
https://github.com/apple/container.git
synced 2026-08-24 10:05:43 -05:00
Fix container DNS resolution broken by AAAA/IPv6 NXDOMAIN handling (#786)
- In Alpine Linux containers (commonly used as Docker base images),
standard DNS resolution is provided by **musl**, a lightweight C
standard library (libc). Musl implements DNS lookups via
`getaddrinfo()`, which queries AAAA (IPv6) records first.
- Problem: DNS did not work correctly **inside containers**. Any
system command attempting to resolve hostnames
(e.g., `ping dynamodb-admin`) **failed** when the DNS server
responded NXDOMAIN for AAAA records, even if A (IPv4) records
existed. Explicitly forcing IPv4 (`ping -4dynamodb-admin`) worked
correctly, showing the issue is specific to musl’s IPv6-first behavior.
- Consequence: In IPv4-only environments, Alpine-based containers
cannot resolve hostnames using standard tools or libraries.
Applications relying on `getaddrinfo()` fail with ENOTFOUND,
breaking networking and inter-container communication.
- Root cause: Following RFC 8305 / RFC 6724, musl treats NXDOMAIN
for AAAA as “hostname does not exist” and does not fallback to A
(IPv4) records.
- Fix: The Apple Container DNS engine now behaves as follows:
* If an **A record exists**, AAAA queries return **NOERROR with empty
answer (NODATA)**.
* If neither **A nor AAAA** exist, NXDOMAIN is returned.
This ensures that Alpine-based containers in IPv4-only networks can
correctly resolve hostnames inside containers without modifying
container images or application code.
This commit is contained in:
@@ -55,8 +55,10 @@ extension DNSServer {
|
||||
answers: []
|
||||
)
|
||||
|
||||
// no responses
|
||||
if response.answers.isEmpty {
|
||||
// Only set NXDOMAIN if handler didn't explicitly set noError (NODATA response).
|
||||
// This preserves NODATA responses for AAAA queries when A record exists,
|
||||
// which prevents musl libc from treating empty AAAA as "domain doesn't exist".
|
||||
if response.answers.isEmpty && response.returnCode != .noError {
|
||||
response.returnCode = .nonExistentDomain
|
||||
}
|
||||
|
||||
|
||||
@@ -32,13 +32,28 @@ public struct HostTableResolver: DNSHandler {
|
||||
switch question.type {
|
||||
case ResourceRecordType.host:
|
||||
record = 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 hosts4[question.name] != nil {
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .noError,
|
||||
questions: query.questions,
|
||||
answers: []
|
||||
)
|
||||
}
|
||||
// If hostname doesn't exist, return nil which will become NXDOMAIN
|
||||
return nil
|
||||
case ResourceRecordType.nameServer,
|
||||
ResourceRecordType.alias,
|
||||
ResourceRecordType.startOfAuthority,
|
||||
ResourceRecordType.pointer,
|
||||
ResourceRecordType.mailExchange,
|
||||
ResourceRecordType.text,
|
||||
ResourceRecordType.host6,
|
||||
ResourceRecordType.service,
|
||||
ResourceRecordType.incrementalZoneTransfer,
|
||||
ResourceRecordType.standardZoneTransfer,
|
||||
|
||||
@@ -34,13 +34,28 @@ struct ContainerDNSHandler: DNSHandler {
|
||||
switch question.type {
|
||||
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 {
|
||||
return Message(
|
||||
id: query.id,
|
||||
type: .response,
|
||||
returnCode: .noError,
|
||||
questions: query.questions,
|
||||
answers: []
|
||||
)
|
||||
}
|
||||
// If hostname doesn't exist, return nil which will become NXDOMAIN
|
||||
return nil
|
||||
case ResourceRecordType.nameServer,
|
||||
ResourceRecordType.alias,
|
||||
ResourceRecordType.startOfAuthority,
|
||||
ResourceRecordType.pointer,
|
||||
ResourceRecordType.mailExchange,
|
||||
ResourceRecordType.text,
|
||||
ResourceRecordType.host6,
|
||||
ResourceRecordType.service,
|
||||
ResourceRecordType.incrementalZoneTransfer,
|
||||
ResourceRecordType.standardZoneTransfer,
|
||||
|
||||
@@ -30,7 +30,7 @@ struct HostTableResolverTest {
|
||||
id: UInt16(1),
|
||||
type: .query,
|
||||
questions: [
|
||||
Question(name: "foo", type: .host6)
|
||||
Question(name: "foo", type: .mailExchange)
|
||||
])
|
||||
|
||||
let response = try await handler.answer(query: query)
|
||||
@@ -42,6 +42,49 @@ struct HostTableResolverTest {
|
||||
#expect(0 == response?.answers.count)
|
||||
}
|
||||
|
||||
@Test func testAAAAQueryReturnsNoDataWhenARecordExists() async throws {
|
||||
guard let ip = IPv4("1.2.3.4") else {
|
||||
throw DNSResolverError.serverError("cannot create IP address in test")
|
||||
}
|
||||
let handler = HostTableResolver(hosts4: ["foo": ip])
|
||||
|
||||
let query = Message(
|
||||
id: UInt16(1),
|
||||
type: .query,
|
||||
questions: [
|
||||
Question(name: "foo", type: .host6)
|
||||
])
|
||||
|
||||
let response = try await handler.answer(query: query)
|
||||
|
||||
// AAAA queries should return NODATA (noError with empty answers) when A record exists
|
||||
// to avoid musl libc issues where NXDOMAIN causes complete DNS resolution failure
|
||||
#expect(.noError == response?.returnCode)
|
||||
#expect(1 == response?.id)
|
||||
#expect(.response == response?.type)
|
||||
#expect(1 == response?.questions.count)
|
||||
#expect(0 == response?.answers.count)
|
||||
}
|
||||
|
||||
@Test func testAAAAQueryReturnsNilWhenHostDoesNotExist() async throws {
|
||||
guard let ip = IPv4("1.2.3.4") else {
|
||||
throw DNSResolverError.serverError("cannot create IP address in test")
|
||||
}
|
||||
let handler = HostTableResolver(hosts4: ["foo": ip])
|
||||
|
||||
let query = Message(
|
||||
id: UInt16(1),
|
||||
type: .query,
|
||||
questions: [
|
||||
Question(name: "bar", type: .host6)
|
||||
])
|
||||
|
||||
let response = try await handler.answer(query: query)
|
||||
|
||||
// AAAA queries for non-existent hosts should return nil (which becomes NXDOMAIN)
|
||||
#expect(nil == response)
|
||||
}
|
||||
|
||||
@Test func testHostNotPresent() async throws {
|
||||
guard let ip = IPv4("1.2.3.4") else {
|
||||
throw DNSResolverError.serverError("cannot create IP address in test")
|
||||
|
||||
Reference in New Issue
Block a user