From 31f896956f3c6465c14f4945027ab5402989babf Mon Sep 17 00:00:00 2001 From: eldadfux Date: Thu, 19 Feb 2026 16:51:32 +0100 Subject: [PATCH] fix --- src/DNS/Message.php | 14 ++++++++++--- tests/unit/DNS/MessageTest.php | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/DNS/Message.php b/src/DNS/Message.php index fb1eb34..906847d 100644 --- a/src/DNS/Message.php +++ b/src/DNS/Message.php @@ -256,7 +256,11 @@ private function encodeWithTruncation(int $maxSize): string return $packet; } - // Step 2: Try without authority section + // Step 2: Try without authority section. + // NODATA (NOERROR + no answers) and NXDOMAIN require SOA in authority per RFC; + // when we drop authority for size, mark as non-authoritative so validation allows it. + $isNodataOrNxdomain = ($this->header->responseCode === self::RCODE_NOERROR && $this->answers === []) + || $this->header->responseCode === self::RCODE_NXDOMAIN; $withoutAuthority = self::response( $this->header, $this->header->responseCode, @@ -264,7 +268,7 @@ private function encodeWithTruncation(int $maxSize): string answers: $this->answers, authority: [], additional: [], - authoritative: $this->header->authoritative, + authoritative: $isNodataOrNxdomain ? false : $this->header->authoritative, truncated: false, recursionAvailable: $this->header->recursionAvailable ); @@ -299,6 +303,10 @@ private function encodeWithTruncation(int $maxSize): string // Per RFC 2181 Section 9: TC is set only when required RRSet data couldn't fit $needsTruncation = count($fittingAnswers) < count($this->answers); + // When authority is empty (dropped for truncation), NODATA/NXDOMAIN must be non-authoritative + $isNodataOrNxdomainTruncated = ($this->header->responseCode === self::RCODE_NOERROR && $fittingAnswers === []) + || $this->header->responseCode === self::RCODE_NXDOMAIN; + $truncatedResponse = self::response( $this->header, $this->header->responseCode, @@ -306,7 +314,7 @@ private function encodeWithTruncation(int $maxSize): string answers: $fittingAnswers, authority: [], additional: [], - authoritative: $this->header->authoritative, + authoritative: $isNodataOrNxdomainTruncated ? false : $this->header->authoritative, truncated: $needsTruncation, recursionAvailable: $this->header->recursionAvailable ); diff --git a/tests/unit/DNS/MessageTest.php b/tests/unit/DNS/MessageTest.php index 9bb9889..2482d5a 100644 --- a/tests/unit/DNS/MessageTest.php +++ b/tests/unit/DNS/MessageTest.php @@ -455,4 +455,40 @@ public function testEncodeWithoutMaxSizeDoesNotTruncate(): void // Verify all answers are preserved $this->assertCount(5, $decoded->answers); } + + /** + * NODATA (NOERROR + no answers) with SOA in authority must be encodable when truncation + * drops the authority section; we mark as non-authoritative to satisfy validation. + */ + public function testEncodeNodataWithTruncationDroppingAuthority(): void + { + $question = new Question('empty.example.com', Record::TYPE_TXT); + $query = Message::query($question, id: 0x1234); + + $soa = new Record( + 'example.com', + Record::TYPE_SOA, + Record::CLASS_IN, + 300, + 'ns.example.com. hostmaster.example.com. 2024010101 3600 600 86400 300' + ); + + $response = Message::response( + $query->header, + Message::RCODE_NOERROR, + questions: $query->questions, + answers: [], + authority: [$soa], + additional: [], + authoritative: true + ); + + // Force truncation to drop authority (packet with question + SOA exceeds small limit) + $truncated = $response->encode(80); + $decoded = Message::decode($truncated); + + $this->assertCount(0, $decoded->answers); + $this->assertCount(0, $decoded->authority); + $this->assertFalse($decoded->header->authoritative, 'Dropped authority => non-authoritative'); + } }