691 lines
25 KiB
Swift
691 lines
25 KiB
Swift
//===----------------------------------------------------------------------===//
|
||
// 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))
|
||
}
|
||
}
|
||
}
|
||
}
|