From d2c8de567ad3a7cff665750fecdb1734e624de68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 26 Mar 2026 17:33:12 +0100 Subject: [PATCH 1/2] Fix soa zone answer --- src/DNS/Zone/Resolver.php | 24 ++++++++++++++++ tests/unit/DNS/Zone/ResolverTest.php | 43 ++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/DNS/Zone/Resolver.php b/src/DNS/Zone/Resolver.php index ef1646b..cbcffd1 100644 --- a/src/DNS/Zone/Resolver.php +++ b/src/DNS/Zone/Resolver.php @@ -42,6 +42,18 @@ public static function lookup(Message $query, Zone $zone): Message $records = self::selectBestRecords($query, $zone); if (empty($records)) { + // SOA is stored separately; if querying SOA at the zone apex, return it + if ($question->type === Record::TYPE_SOA && $question->name === $zone->name) { + return Message::response( + header: $query->header, + responseCode: Message::RCODE_NOERROR, + questions: $query->questions, + answers: [$zone->soa], + authoritative: true, + recursionAvailable: false + ); + } + return Message::response( header: $query->header, responseCode: Message::RCODE_NXDOMAIN, @@ -157,6 +169,18 @@ private static function handleExactMatch(array $records, Message $query, Zone $z $isAuthoritative = $zone->isAuthoritative($question->name); if ($isAuthoritative) { + // SOA is stored separately in Zone; handle SOA queries at the zone apex + if ($question->type === Record::TYPE_SOA && $question->name === $zone->name) { + return Message::response( + header: $query->header, + responseCode: Message::RCODE_NOERROR, + questions: $query->questions, + answers: [$zone->soa], + authoritative: true, + recursionAvailable: false + ); + } + // Path E1: Exact match of type $exactTypeRecords = array_filter( $records, diff --git a/tests/unit/DNS/Zone/ResolverTest.php b/tests/unit/DNS/Zone/ResolverTest.php index 0146a09..3cf5c2e 100644 --- a/tests/unit/DNS/Zone/ResolverTest.php +++ b/tests/unit/DNS/Zone/ResolverTest.php @@ -465,6 +465,49 @@ public function testLookupReturnsApexAAAARecord(): void $this->assertSame($aaaaRecord, $response->answers[0]); } + public function testLookupReturnsSoaAnswerForApexSoaQueryWithRecords(): void + { + $soa = new Record( + 'example.com', + Record::TYPE_SOA, + ttl: 300, + rdata: 'ns1.appwrite.zone. team@appwrite.io. 1761705275 3600 600 86400 300' + ); + $aRecord = new Record('example.com', Record::TYPE_A, ttl: 3600, rdata: '1.1.1.1'); + $zone = new Zone('example.com', [$aRecord], $soa); + + $question = new Question('example.com', Record::TYPE_SOA); + $query = Message::query($question); + $response = Resolver::lookup($query, $zone); + + $this->assertSame(Message::RCODE_NOERROR, $response->header->responseCode); + $this->assertCount(1, $response->answers); + $this->assertSame($soa, $response->answers[0]); + $this->assertTrue($response->header->authoritative); + $this->assertFalse($response->header->recursionAvailable); + } + + public function testLookupReturnsSoaAnswerForApexSoaQueryWithNoRecords(): void + { + $soa = new Record( + 'example.com', + Record::TYPE_SOA, + ttl: 300, + rdata: 'ns1.appwrite.zone. team@appwrite.io. 1761705275 3600 600 86400 300' + ); + $zone = new Zone('example.com', [], $soa); + + $question = new Question('example.com', Record::TYPE_SOA); + $query = Message::query($question); + $response = Resolver::lookup($query, $zone); + + $this->assertSame(Message::RCODE_NOERROR, $response->header->responseCode); + $this->assertCount(1, $response->answers); + $this->assertSame($soa, $response->answers[0]); + $this->assertTrue($response->header->authoritative); + $this->assertFalse($response->header->recursionAvailable); + } + public function testLookupReturnsSoaInAuthorityForApexNonNSQuery(): void { $soa = new Record( From f2851dfa6c24837946b10275a0b193deaaf997f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 26 Mar 2026 17:53:55 +0100 Subject: [PATCH 2/2] Fix tests --- src/DNS/Zone/Resolver.php | 33 +++++++++++++++++---------------- tests/e2e/DNS/ClientTest.php | 16 ++++++++-------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/DNS/Zone/Resolver.php b/src/DNS/Zone/Resolver.php index cbcffd1..6385795 100644 --- a/src/DNS/Zone/Resolver.php +++ b/src/DNS/Zone/Resolver.php @@ -44,14 +44,7 @@ public static function lookup(Message $query, Zone $zone): Message if (empty($records)) { // SOA is stored separately; if querying SOA at the zone apex, return it if ($question->type === Record::TYPE_SOA && $question->name === $zone->name) { - return Message::response( - header: $query->header, - responseCode: Message::RCODE_NOERROR, - questions: $query->questions, - answers: [$zone->soa], - authoritative: true, - recursionAvailable: false - ); + return self::soaApexResponse($query, $zone); } return Message::response( @@ -171,14 +164,7 @@ private static function handleExactMatch(array $records, Message $query, Zone $z if ($isAuthoritative) { // SOA is stored separately in Zone; handle SOA queries at the zone apex if ($question->type === Record::TYPE_SOA && $question->name === $zone->name) { - return Message::response( - header: $query->header, - responseCode: Message::RCODE_NOERROR, - questions: $query->questions, - answers: [$zone->soa], - authoritative: true, - recursionAvailable: false - ); + return self::soaApexResponse($query, $zone); } // Path E1: Exact match of type @@ -239,6 +225,21 @@ private static function handleExactMatch(array $records, Message $query, Zone $z } } + /** + * Build an authoritative SOA answer for the zone apex. + */ + private static function soaApexResponse(Message $query, Zone $zone): Message + { + return Message::response( + header: $query->header, + responseCode: Message::RCODE_NOERROR, + questions: $query->questions, + answers: [$zone->soa], + authoritative: true, + recursionAvailable: false + ); + } + /** * Randomize RRSet order for load balancing. * diff --git a/tests/e2e/DNS/ClientTest.php b/tests/e2e/DNS/ClientTest.php index 7213f4e..ed7c6fb 100644 --- a/tests/e2e/DNS/ClientTest.php +++ b/tests/e2e/DNS/ClientTest.php @@ -213,16 +213,16 @@ public function testSoaRecords(): void $response = $client->query(Message::query( new Question('appwrite.io', Record::TYPE_SOA) )); - $this->assertCount(0, $response->answers); + $this->assertCount(0, $response->authority); - $authority = $response->authority; - $this->assertCount(1, $authority); - $this->assertSame('appwrite.io', $authority[0]->name); - $this->assertSame(Record::CLASS_IN, $authority[0]->class); - $this->assertSame(30, $authority[0]->ttl); - $this->assertSame(Record::TYPE_SOA, $authority[0]->type); + $answers = $response->answers; + $this->assertCount(1, $answers); + $this->assertSame('appwrite.io', $answers[0]->name); + $this->assertSame(Record::CLASS_IN, $answers[0]->class); + $this->assertSame(30, $answers[0]->ttl); + $this->assertSame(Record::TYPE_SOA, $answers[0]->type); - $rdata = $authority[0]->rdata; + $rdata = $answers[0]->rdata; $this->assertStringContainsString('ns1.appwrite.zone', $rdata); $this->assertStringContainsString('team.appwrite.io', $rdata); $this->assertStringContainsString('1 7200 1800 1209600 3600', $rdata);