//===----------------------------------------------------------------------===// // 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 ContainerizationExtras import Foundation import Testing @testable import DNSServer @Suite("DNS Records Tests") struct RecordsTests { // MARK: - DNSName Tests @Suite("DNSName") struct DNSNameTests { @Test("Create from string") func createFromString() throws { let name = try DNSName("example.com") #expect(name.labels == ["example", "com"]) } @Test("Create from string with trailing dot") func createFromStringTrailingDot() throws { let name = try DNSName("example.com.") #expect(name.labels == ["example", "com"]) } @Test("Description includes trailing dot") func descriptionTrailingDot() throws { let name = try DNSName("example.com") #expect(name.description == "example.com.") } @Test("DNS name with newline should throw") func DNSNameWithNewLine() throws { #expect(throws: DNSBindError.self) { _ = try DNSName("foo.com\npwned") } } @Test("DNS name with newline should throw") func DNSNameWithPathTraversal() throws { #expect(throws: DNSBindError.self) { _ = try DNSName("../pwned") } #expect(throws: DNSBindError.self) { _ = try DNSName("myhost./../../pwned") } } @Test("Root domain") func rootDomain() throws { let name = try DNSName("") #expect(name.labels == []) #expect(name.description == ".") } @Test("Size calculation") func sizeCalculation() throws { let name = try DNSName("example.com") // [7]example[3]com[0] = 1 + 7 + 1 + 3 + 1 = 13 #expect(name.size == 13) } @Test("Serialize and deserialize") func serializeDeserialize() throws { let original = try DNSName("test.example.com") var buffer = [UInt8](repeating: 0, count: 64) let endOffset = try original.appendBuffer(&buffer, offset: 0) var parsed = DNSName() let readOffset = try parsed.bindBuffer(&buffer, offset: 0) // [4]test[7]example[3]com[0] = 5+8+4+1 = 18 #expect(endOffset == 18) #expect(readOffset == endOffset) #expect(parsed.labels == original.labels) } @Test("Serialize subdomain") func serializeSubdomain() throws { let name = try DNSName("a.b.c.d.example.com") var buffer = [UInt8](repeating: 0, count: 64) let endOffset = try name.appendBuffer(&buffer, offset: 0) var parsed = DNSName() let readOffset = try parsed.bindBuffer(&buffer, offset: 0) // [1]a[1]b[1]c[1]d[7]example[3]com[0] = 2+2+2+2+8+4+1 = 21 #expect(endOffset == 21) #expect(readOffset == endOffset) #expect(parsed.labels == ["a", "b", "c", "d", "example", "com"]) } @Test("Reject label too long") func rejectLabelTooLong() { let longLabel = String(repeating: "a", count: 64) #expect(throws: DNSBindError.self) { _ = try DNSName(longLabel + ".com") } } @Test("Reject embedded carriage return") func rejectEmbeddedCarriageReturn() { #expect(throws: DNSBindError.self) { _ = try DNSName("foo\r.com") } } @Test("Reject embedded newline") func rejectEmbeddedNewline() { #expect(throws: DNSBindError.self) { _ = try DNSName("foo\n.com") } } @Test("Reject embedded null byte") func rejectEmbeddedNullByte() { #expect(throws: DNSBindError.self) { _ = try DNSName("foo\0.com") } } @Test("Reject empty label") func rejectEmptyLabel() { #expect(throws: DNSBindError.self) { _ = try DNSName("foo..com") } } @Test("Reject name too long") func rejectNameTooLong() { // 9 labels * (1 + 30) bytes + 1 null = 280 bytes > 255 let label = String(repeating: "a", count: 30) let name = Array(repeating: label, count: 9).joined(separator: ".") #expect(throws: DNSBindError.self) { _ = try DNSName(name) } } @Test("Reject leading hyphen") func rejectLeadingHyphen() { #expect(throws: DNSBindError.self) { _ = try DNSName("-foo.com") } } @Test("Reject trailing hyphen") func rejectTrailingHyphen() { #expect(throws: DNSBindError.self) { _ = try DNSName("foo-.com") } } @Test("Reject leading underscore") func rejectLeadingUnderscore() { #expect(throws: DNSBindError.self) { _ = try DNSName("_foo.com") } } @Test("Reject trailing underscore") func rejectTrailingUnderscore() { #expect(throws: DNSBindError.self) { _ = try DNSName("foo_.com") } } @Test("Accept service labels via init(labels:)") func acceptServiceLabels() throws { let name = try DNSName(labels: ["_dns-sd", "_udp", "local"]) #expect(name.labels == ["_dns-sd", "_udp", "local"]) } @Test("Lowercase labels on init") func lowercaseLabelsOnInit() throws { let name = try DNSName("EXAMPLE.COM") #expect(name.labels == ["example", "com"]) } @Test("Lowercase labels on init with trailing dot") func lowercaseLabelsOnInitTrailingDot() throws { let name = try DNSName("Example.Com.") #expect(name.labels == ["example", "com"]) } @Test("Lowercase labels from wire format") func lowercaseLabelsFromWire() throws { // Wire-encode "EXAMPLE.COM" with uppercase bytes, then decode let upper = try DNSName(labels: ["EXAMPLE", "COM"]) var buffer = [UInt8](repeating: 0, count: 64) let endOffset = try upper.appendBuffer(&buffer, offset: 0) var parsed = DNSName() let readOffset = try parsed.bindBuffer(&buffer, offset: 0) // [7]example[3]com[0] = 8+4+1 = 13 #expect(endOffset == 13) #expect(readOffset == endOffset) #expect(parsed.labels == ["example", "com"]) } @Test("Follow valid compression pointer") func followCompressionPointer() throws { // Build a buffer with two names: // offset 0: "example.com." — [7]example[3]com[0] (13 bytes) // offset 13: "test." — [4]test 0xC0 0x00 ( 7 bytes) // The pointer 0xC0 0x00 points back to offset 0. var buffer: [UInt8] = [ 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, // [7]example 0x03, 0x63, 0x6f, 0x6d, // [3]com 0x00, // null terminator 0x04, 0x74, 0x65, 0x73, 0x74, // [4]test 0xC0, 0x00, // pointer to offset 0 ] var name = DNSName() let readOffset = try name.bindBuffer(&buffer, offset: 13) // Pointer bytes are at offset 18–19; returnOffset = 18 + 2 = 20 #expect(readOffset == 20) #expect(name.labels == ["test", "example", "com"]) } @Test("Reject forward compression pointer") func rejectForwardCompressionPointer() throws { // Craft a packet with a forward compression pointer at offset 12 pointing to offset 20 // Header (12 bytes) + pointer bytes var buffer = [UInt8](repeating: 0, count: 32) // At offset 0: compression pointer to offset 20 (forward) buffer[0] = 0xC0 buffer[1] = 0x14 // points to offset 20, which is > 0 #expect(throws: DNSBindError.self) { var b = buffer var name = DNSName() _ = try name.bindBuffer(&b, offset: 0) } } @Test("Reject self-referential compression pointer") func rejectSelfReferentialCompressionPointer() throws { var buffer = [UInt8](repeating: 0, count: 16) // At offset 0: compression pointer pointing back to offset 0 (same location) buffer[0] = 0xC0 buffer[1] = 0x00 // points to offset 0 == current offset, not prior #expect(throws: DNSBindError.self) { var b = buffer var name = DNSName() _ = try name.bindBuffer(&b, offset: 0) } } @Test("Reject compression pointer hop limit exceeded") func rejectCompressionPointerHopLimit() throws { // Build a chain of backward pointers: // offset 0: [1]a[0] — terminal name (3 bytes) // offset 3: 0xC0 0x00 — pointer → 0 // offset 5: 0xC0 0x03 — pointer → 3 // ...each entry points to the one before it... // offset 23: 0xC0 0x15 — pointer → 21 // offset 25: 0xC0 0x17 — pointer → 23 // // Reading from offset 25 follows 11 hops (25→23→21→...→3→0), // which exceeds the limit of 10. var buffer: [UInt8] = [ 0x01, 0x61, 0x00, // offset 0: [1]a[0] 0xC0, 0x00, // offset 3: → 0 0xC0, 0x03, // offset 5: → 3 0xC0, 0x05, // offset 7: → 5 0xC0, 0x07, // offset 9: → 7 0xC0, 0x09, // offset 11: → 9 0xC0, 0x0B, // offset 13: → 11 0xC0, 0x0D, // offset 15: → 13 0xC0, 0x0F, // offset 17: → 15 0xC0, 0x11, // offset 19: → 17 0xC0, 0x13, // offset 21: → 19 0xC0, 0x15, // offset 23: → 21 0xC0, 0x17, // offset 25: → 23 ] #expect(throws: DNSBindError.self) { var name = DNSName() _ = try name.bindBuffer(&buffer, offset: 25) } } } // MARK: - Question Tests @Suite("Question") struct QuestionTests { @Test("Create question") func create() { let q = Question(name: "example.com.", type: .host, recordClass: .internet) #expect(q.name == "example.com.") #expect(q.type == .host) #expect(q.recordClass == .internet) } @Test("Serialize and deserialize A record question") func serializeDeserializeA() throws { let original = Question(name: "example.com.", type: .host, recordClass: .internet) var buffer = [UInt8](repeating: 0, count: 64) let endOffset = try original.appendBuffer(&buffer, offset: 0) var parsed = Question(name: "") let readOffset = try parsed.bindBuffer(&buffer, offset: 0) // name([7]example[3]com[0]=13) + type(2) + class(2) = 17 #expect(endOffset == 17) #expect(readOffset == endOffset) #expect(parsed.type == .host) #expect(parsed.recordClass == .internet) } @Test("Serialize and deserialize AAAA record question") func serializeDeserializeAAAA() throws { let original = Question(name: "example.com.", type: .host6, recordClass: .internet) var buffer = [UInt8](repeating: 0, count: 64) let endOffset = try original.appendBuffer(&buffer, offset: 0) var parsed = Question(name: "") let readOffset = try parsed.bindBuffer(&buffer, offset: 0) // name([7]example[3]com[0]=13) + type(2) + class(2) = 17 #expect(endOffset == 17) #expect(readOffset == endOffset) #expect(parsed.type == .host6) } } // MARK: - HostRecord Tests @Suite("HostRecord") struct HostRecordTests { @Test("Create A record") func createARecord() throws { let ip = try IPv4Address("192.168.1.1") let record = HostRecord(name: "example.com.", ttl: 300, ip: ip) #expect(record.name == "example.com.") #expect(record.type == .host) #expect(record.ttl == 300) #expect(record.ip == ip) } @Test("Create AAAA record") func createAAAARecord() throws { let ip = try IPv6Address("::1") let record = HostRecord(name: "example.com.", ttl: 600, ip: ip) #expect(record.name == "example.com.") #expect(record.type == .host6) #expect(record.ttl == 600) } @Test("Serialize A record") func serializeARecord() throws { let ip = try IPv4Address("10.0.0.1") let record = HostRecord(name: "test.com.", ttl: 300, ip: ip) var buffer = [UInt8](repeating: 0, count: 64) let endOffset = try record.appendBuffer(&buffer, offset: 0) // name([4]test[3]com[0]=10) + type(2) + class(2) + ttl(4) + rdlen(2) + rdata(4) = 24 #expect(endOffset == 24) // Verify IP bytes at the end #expect(buffer[endOffset - 4] == 10) #expect(buffer[endOffset - 3] == 0) #expect(buffer[endOffset - 2] == 0) #expect(buffer[endOffset - 1] == 1) } @Test("Serialize AAAA record") func serializeAAAARecord() throws { let ip = try IPv6Address("::1") let record = HostRecord(name: "test.com.", ttl: 300, ip: ip) var buffer = [UInt8](repeating: 0, count: 64) let endOffset = try record.appendBuffer(&buffer, offset: 0) // name([4]test[3]com[0]=10) + type(2) + class(2) + ttl(4) + rdlen(2) + rdata(16) = 36 #expect(endOffset == 36) #expect(buffer[endOffset - 1] == 1) } } // MARK: - Message Tests @Suite("Message") struct MessageTests { @Test("Create query message") func createQuery() { let msg = Message( id: 0x1234, type: .query, questions: [Question(name: "example.com.", type: .host)] ) #expect(msg.id == 0x1234) #expect(msg.type == .query) #expect(msg.questions.count == 1) } @Test("Create response message") func createResponse() throws { let ip = try IPv4Address("192.168.1.1") let msg = Message( id: 0x1234, type: .response, returnCode: .noError, questions: [Question(name: "example.com.", type: .host)], answers: [HostRecord(name: "example.com.", ttl: 300, ip: ip)] ) #expect(msg.type == .response) #expect(msg.returnCode == .noError) #expect(msg.answers.count == 1) } @Test("Serialize and deserialize query") func serializeDeserializeQuery() throws { let original = Message( id: 0xABCD, type: .query, recursionDesired: true, questions: [Question(name: "example.com.", type: .host)] ) let data = try original.serialize() let parsed = try Message(deserialize: data) #expect(parsed.id == 0xABCD) #expect(parsed.type == .query) #expect(parsed.recursionDesired == true) #expect(parsed.questions.count == 1) #expect(parsed.questions[0].type == .host) } @Test("Serialize response with answer") func serializeResponse() throws { let ip = try IPv4Address("10.0.0.1") let msg = Message( id: 0x1234, type: .response, authoritativeAnswer: true, returnCode: .noError, questions: [Question(name: "test.com.", type: .host)], answers: [HostRecord(name: "test.com.", ttl: 300, ip: ip)] ) let data = try msg.serialize() // Verify we can at least parse the header back let parsed = try Message(deserialize: data) #expect(parsed.id == 0x1234) #expect(parsed.type == .response) #expect(parsed.authoritativeAnswer == true) #expect(parsed.returnCode == .noError) } @Test("Serialize NXDOMAIN response") func serializeNxdomain() throws { let msg = Message( id: 0x1234, type: .response, returnCode: .nonExistentDomain, questions: [Question(name: "unknown.com.", type: .host)], answers: [] ) let data = try msg.serialize() let parsed = try Message(deserialize: data) #expect(parsed.returnCode == .nonExistentDomain) #expect(parsed.answers.count == 0) } @Test("Serialize NODATA response (empty answers with noError)") func serializeNodata() throws { let msg = Message( id: 0x1234, type: .response, returnCode: .noError, questions: [Question(name: "example.com.", type: .host6)], answers: [] ) let data = try msg.serialize() let parsed = try Message(deserialize: data) #expect(parsed.returnCode == .noError) #expect(parsed.answers.count == 0) } @Test("Multiple questions") func multipleQuestions() throws { let msg = Message( id: 0x1234, type: .query, questions: [ Question(name: "a.com.", type: .host), Question(name: "b.com.", type: .host6), ] ) let data = try msg.serialize() let parsed = try Message(deserialize: data) #expect(parsed.questions.count == 2) #expect(parsed.questions[0].type == .host) #expect(parsed.questions[1].type == .host6) } @Test("Reject too many questions") func rejectTooManyQuestions() { let questions = Array(repeating: Question(name: "a.com.", type: .host), count: Int(UInt16.max) + 1) let msg = Message(id: 0, type: .query, questions: questions) #expect(throws: DNSBindError.self) { _ = try msg.serialize() } } @Test("Reject too many answers") func rejectTooManyAnswers() throws { let ip = try IPv4Address("1.2.3.4") let answers = Array(repeating: HostRecord(name: "a.com.", ttl: 0, ip: ip), count: Int(UInt16.max) + 1) let msg = Message(id: 0, type: .response, answers: answers) #expect(throws: DNSBindError.self) { _ = try msg.serialize() } } } // MARK: - Wire Format Tests @Suite("Wire Format") struct WireFormatTests { @Test("Parse real DNS query bytes") func parseRealQuery() throws { // A minimal DNS query for "example.com" A record // Header: ID=0x1234, QR=0, OPCODE=0, RD=1, QDCOUNT=1 let queryBytes: [UInt8] = [ 0x12, 0x34, // ID 0x01, 0x00, // Flags: RD=1 0x00, 0x01, // QDCOUNT=1 0x00, 0x00, // ANCOUNT=0 0x00, 0x00, // NSCOUNT=0 0x00, 0x00, // ARCOUNT=0 // Question: example.com A IN 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, // "example" 0x03, 0x63, 0x6f, 0x6d, // "com" 0x00, // null terminator 0x00, 0x01, // QTYPE=A 0x00, 0x01, // QCLASS=IN ] let msg = try Message(deserialize: Data(queryBytes)) #expect(msg.id == 0x1234) #expect(msg.type == .query) #expect(msg.recursionDesired == true) #expect(msg.questions.count == 1) #expect(msg.questions[0].type == .host) #expect(msg.questions[0].recordClass == .internet) } @Test("Roundtrip preserves data") func roundtrip() throws { let ip = try IPv4Address("1.2.3.4") let original = Message( id: 0xBEEF, type: .response, operationCode: .query, authoritativeAnswer: true, truncation: false, recursionDesired: true, recursionAvailable: true, returnCode: .noError, questions: [Question(name: "test.example.com.", type: .host)], answers: [HostRecord(name: "test.example.com.", ttl: 3600, ip: ip)] ) let data = try original.serialize() let parsed = try Message(deserialize: data) #expect(parsed.id == original.id) #expect(parsed.type == original.type) #expect(parsed.authoritativeAnswer == original.authoritativeAnswer) #expect(parsed.truncation == original.truncation) #expect(parsed.recursionDesired == original.recursionDesired) #expect(parsed.recursionAvailable == original.recursionAvailable) #expect(parsed.returnCode == original.returnCode) #expect(parsed.questions.count == original.questions.count) } @Test("Reject unknown opcode") func rejectUnknownOpcode() { // Opcode occupies bits 14–11 of the flags word. Value 3 is reserved. // Flags: 0x18 0x00 = QR=0, OPCODE=3, all other bits clear. let bytes: [UInt8] = [ 0x00, 0x01, // ID 0x18, 0x00, // Flags: OPCODE=3 (reserved) 0x00, 0x00, // QDCOUNT=0 0x00, 0x00, // ANCOUNT=0 0x00, 0x00, // NSCOUNT=0 0x00, 0x00, // ARCOUNT=0 ] #expect(throws: DNSBindError.self) { _ = try Message(deserialize: Data(bytes)) } } @Test("Reject unknown RCODE") func rejectUnknownRcode() { // RCODE occupies bits 3–0 of the flags word. Value 12 is reserved. // Flags: 0x00 0x0C = QR=0, OPCODE=0, RCODE=12. let bytes: [UInt8] = [ 0x00, 0x01, // ID 0x00, 0x0C, // Flags: RCODE=12 (reserved) 0x00, 0x00, // QDCOUNT=0 0x00, 0x00, // ANCOUNT=0 0x00, 0x00, // NSCOUNT=0 0x00, 0x00, // ARCOUNT=0 ] #expect(throws: DNSBindError.self) { _ = try Message(deserialize: Data(bytes)) } } @Test("Reject unknown query type") func rejectUnknownQueryType() { // Type 54 is unassigned in the IANA DNS parameters registry. let bytes: [UInt8] = [ 0x00, 0x01, // ID 0x00, 0x00, // Flags: standard query 0x00, 0x01, // QDCOUNT=1 0x00, 0x00, // ANCOUNT=0 0x00, 0x00, // NSCOUNT=0 0x00, 0x00, // ARCOUNT=0 0x01, 0x61, 0x00, // name: [1]a[0] 0x00, 0x36, // QTYPE=54 (unassigned) 0x00, 0x01, // QCLASS=IN ] #expect(throws: DNSBindError.self) { _ = try Message(deserialize: Data(bytes)) } } @Test("Reject unknown record class") func rejectUnknownRecordClass() { // Class 2 is unassigned in the IANA DNS parameters registry. let bytes: [UInt8] = [ 0x00, 0x01, // ID 0x00, 0x00, // Flags: standard query 0x00, 0x01, // QDCOUNT=1 0x00, 0x00, // ANCOUNT=0 0x00, 0x00, // NSCOUNT=0 0x00, 0x00, // ARCOUNT=0 0x01, 0x61, 0x00, // name: [1]a[0] 0x00, 0x01, // QTYPE=A 0x00, 0x02, // QCLASS=2 (unassigned) ] #expect(throws: DNSBindError.self) { _ = try Message(deserialize: Data(bytes)) } } } }