Skip to content

Commit 745cc18

Browse files
authored
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.
1 parent 13a2f1a commit 745cc18

File tree

4 files changed

+80
-5
lines changed

4 files changed

+80
-5
lines changed

Sources/DNSServer/DNSServer+Handle.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ extension DNSServer {
5555
answers: []
5656
)
5757

58-
// no responses
59-
if response.answers.isEmpty {
58+
// Only set NXDOMAIN if handler didn't explicitly set noError (NODATA response).
59+
// This preserves NODATA responses for AAAA queries when A record exists,
60+
// which prevents musl libc from treating empty AAAA as "domain doesn't exist".
61+
if response.answers.isEmpty && response.returnCode != .noError {
6062
response.returnCode = .nonExistentDomain
6163
}
6264

Sources/DNSServer/Handlers/HostTableResolver.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,28 @@ public struct HostTableResolver: DNSHandler {
3232
switch question.type {
3333
case ResourceRecordType.host:
3434
record = answerHost(question: question)
35+
case ResourceRecordType.host6:
36+
// Return NODATA (noError with empty answers) for AAAA queries ONLY if A record exists.
37+
// This is required because musl libc has issues when A record exists but AAAA returns NXDOMAIN.
38+
// musl treats NXDOMAIN on AAAA as "domain doesn't exist" and fails DNS resolution entirely.
39+
// NODATA correctly indicates "no IPv6 address available, but domain exists".
40+
if hosts4[question.name] != nil {
41+
return Message(
42+
id: query.id,
43+
type: .response,
44+
returnCode: .noError,
45+
questions: query.questions,
46+
answers: []
47+
)
48+
}
49+
// If hostname doesn't exist, return nil which will become NXDOMAIN
50+
return nil
3551
case ResourceRecordType.nameServer,
3652
ResourceRecordType.alias,
3753
ResourceRecordType.startOfAuthority,
3854
ResourceRecordType.pointer,
3955
ResourceRecordType.mailExchange,
4056
ResourceRecordType.text,
41-
ResourceRecordType.host6,
4257
ResourceRecordType.service,
4358
ResourceRecordType.incrementalZoneTransfer,
4459
ResourceRecordType.standardZoneTransfer,

Sources/Helpers/APIServer/ContainerDNSHandler.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,28 @@ struct ContainerDNSHandler: DNSHandler {
3434
switch question.type {
3535
case ResourceRecordType.host:
3636
record = try await answerHost(question: question)
37+
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 {
43+
return Message(
44+
id: query.id,
45+
type: .response,
46+
returnCode: .noError,
47+
questions: query.questions,
48+
answers: []
49+
)
50+
}
51+
// If hostname doesn't exist, return nil which will become NXDOMAIN
52+
return nil
3753
case ResourceRecordType.nameServer,
3854
ResourceRecordType.alias,
3955
ResourceRecordType.startOfAuthority,
4056
ResourceRecordType.pointer,
4157
ResourceRecordType.mailExchange,
4258
ResourceRecordType.text,
43-
ResourceRecordType.host6,
4459
ResourceRecordType.service,
4560
ResourceRecordType.incrementalZoneTransfer,
4661
ResourceRecordType.standardZoneTransfer,

Tests/DNSServerTests/HostTableResolverTest.swift

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ struct HostTableResolverTest {
3030
id: UInt16(1),
3131
type: .query,
3232
questions: [
33-
Question(name: "foo", type: .host6)
33+
Question(name: "foo", type: .mailExchange)
3434
])
3535

3636
let response = try await handler.answer(query: query)
@@ -42,6 +42,49 @@ struct HostTableResolverTest {
4242
#expect(0 == response?.answers.count)
4343
}
4444

45+
@Test func testAAAAQueryReturnsNoDataWhenARecordExists() async throws {
46+
guard let ip = IPv4("1.2.3.4") else {
47+
throw DNSResolverError.serverError("cannot create IP address in test")
48+
}
49+
let handler = HostTableResolver(hosts4: ["foo": ip])
50+
51+
let query = Message(
52+
id: UInt16(1),
53+
type: .query,
54+
questions: [
55+
Question(name: "foo", type: .host6)
56+
])
57+
58+
let response = try await handler.answer(query: query)
59+
60+
// AAAA queries should return NODATA (noError with empty answers) when A record exists
61+
// to avoid musl libc issues where NXDOMAIN causes complete DNS resolution failure
62+
#expect(.noError == response?.returnCode)
63+
#expect(1 == response?.id)
64+
#expect(.response == response?.type)
65+
#expect(1 == response?.questions.count)
66+
#expect(0 == response?.answers.count)
67+
}
68+
69+
@Test func testAAAAQueryReturnsNilWhenHostDoesNotExist() async throws {
70+
guard let ip = IPv4("1.2.3.4") else {
71+
throw DNSResolverError.serverError("cannot create IP address in test")
72+
}
73+
let handler = HostTableResolver(hosts4: ["foo": ip])
74+
75+
let query = Message(
76+
id: UInt16(1),
77+
type: .query,
78+
questions: [
79+
Question(name: "bar", type: .host6)
80+
])
81+
82+
let response = try await handler.answer(query: query)
83+
84+
// AAAA queries for non-existent hosts should return nil (which becomes NXDOMAIN)
85+
#expect(nil == response)
86+
}
87+
4588
@Test func testHostNotPresent() async throws {
4689
guard let ip = IPv4("1.2.3.4") else {
4790
throw DNSResolverError.serverError("cannot create IP address in test")

0 commit comments

Comments
 (0)