diff --git a/Sources/DNSServer/DNSServer+Handle.swift b/Sources/DNSServer/DNSServer+Handle.swift index 22bad2cb..2748f0d9 100644 --- a/Sources/DNSServer/DNSServer+Handle.swift +++ b/Sources/DNSServer/DNSServer+Handle.swift @@ -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 } diff --git a/Sources/DNSServer/Handlers/HostTableResolver.swift b/Sources/DNSServer/Handlers/HostTableResolver.swift index cff776ee..9806b5ca 100644 --- a/Sources/DNSServer/Handlers/HostTableResolver.swift +++ b/Sources/DNSServer/Handlers/HostTableResolver.swift @@ -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, diff --git a/Sources/Helpers/APIServer/ContainerDNSHandler.swift b/Sources/Helpers/APIServer/ContainerDNSHandler.swift index f26ec3dc..0104c135 100644 --- a/Sources/Helpers/APIServer/ContainerDNSHandler.swift +++ b/Sources/Helpers/APIServer/ContainerDNSHandler.swift @@ -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, diff --git a/Tests/DNSServerTests/HostTableResolverTest.swift b/Tests/DNSServerTests/HostTableResolverTest.swift index db68c409..b0f77f9a 100644 --- a/Tests/DNSServerTests/HostTableResolverTest.swift +++ b/Tests/DNSServerTests/HostTableResolverTest.swift @@ -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")