Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/DNS/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -256,15 +256,19 @@ 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,
questions: $this->questions,
answers: $this->answers,
authority: [],
additional: [],
authoritative: $this->header->authoritative,
authoritative: $isNodataOrNxdomain ? false : $this->header->authoritative,
truncated: false,
recursionAvailable: $this->header->recursionAvailable
);
Expand Down Expand Up @@ -299,14 +303,18 @@ 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,
questions: $this->questions,
answers: $fittingAnswers,
authority: [],
additional: [],
authoritative: $this->header->authoritative,
authoritative: $isNodataOrNxdomainTruncated ? false : $this->header->authoritative,
truncated: $needsTruncation,
recursionAvailable: $this->header->recursionAvailable
);
Expand Down
36 changes: 36 additions & 0 deletions tests/unit/DNS/MessageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}