From a3101f2cb90de6eb813b1436804e392a54d2b8f4 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 27 Jul 2025 03:29:58 +0000 Subject: [PATCH 01/12] Fix: improve encoder functions according to RFC --- src/DNS/Server.php | 135 +++++++++++++++++++++++++++++---------------- 1 file changed, 87 insertions(+), 48 deletions(-) diff --git a/src/DNS/Server.php b/src/DNS/Server.php index 7eff073..077d369 100644 --- a/src/DNS/Server.php +++ b/src/DNS/Server.php @@ -330,100 +330,139 @@ protected function resolve(array $question): array return $this->resolver->resolve($question); } + /** + * Encode an IPv4 address (A record) according to RFC 1035. + * + * @param string $ip + * @param int $ttl + * @return string + */ protected function encodeIP(string $ip, int $ttl): string { - $result = \pack('Nn', $ttl, 4); - $binaryIP = inet_pton($ip); - if ($binaryIP === false) { + if ($binaryIP === false || strlen($binaryIP) !== 4) { throw new \Exception("Invalid IPv4 address format: {$ip}"); } - - // Append the binary IPv4 address directly - $result .= $binaryIP; - + $result = pack('Nn', $ttl, 4) . $binaryIP; return $result; } + /** + * Encode an IPv6 address (AAAA record) according to RFC 3596. + * + * @param string $ip + * @param int $ttl + * @return string + */ protected function encodeIPv6(string $ip, int $ttl): string { - $result = \pack('Nn', $ttl, 16); - $binaryIP = inet_pton($ip); - if ($binaryIP === false) { + if ($binaryIP === false || strlen($binaryIP) !== 16) { throw new \Exception("Invalid IPv6 address format: {$ip}"); } - - $result .= $binaryIP; - + $result = pack('Nn', $ttl, 16) . $binaryIP; return $result; } + /** + * Encode a domain name (CNAME, NS, PTR) according to RFC 1035. + * + * @param string $domain + * @param int $ttl + * @return string + */ protected function encodeDomain(string $domain, int $ttl): string { + $labels = explode('.', rtrim($domain, '.')); $result = ''; $totalLength = 0; - - foreach (\explode('.', $domain) as $label) { - $labelLength = \strlen($label); - $result .= \chr($labelLength); - $result .= $label; + foreach ($labels as $label) { + $labelLength = strlen($label); + if ($labelLength > 63) { + throw new \Exception("Label too long in domain: {$label}"); + } + $result .= chr($labelLength) . $label; $totalLength += 1 + $labelLength; } - - $result .= \chr(0); + $result .= chr(0); $totalLength += 1; - - $result = \pack('Nn', $ttl, $totalLength) . $result; - + $result = pack('Nn', $ttl, $totalLength) . $result; return $result; } + /** + * Encode a TXT record according to RFC 1035. + * + * @param string $text + * @param int $ttl + * @return string + */ protected function encodeText(string $text, int $ttl): string { - $textLength = \strlen($text); - $result = \pack('Nn', $ttl, 1 + $textLength) . \chr($textLength) . $text; - + $chunks = []; + $len = strlen($text); + for ($i = 0; $i < $len; $i += 255) { + $chunk = substr($text, $i, 255); + $chunks[] = chr(strlen($chunk)) . $chunk; + } + $txtData = implode('', $chunks); + $result = pack('Nn', $ttl, strlen($txtData)) . $txtData; return $result; } + /** + * Encode an MX record according to RFC 1035. + * + * @param string $domain + * @param int $ttl + * @param int $priority + * @return string + */ protected function encodeMx(string $domain, int $ttl, int $priority): string { - $result = \pack('n', $priority); + $labels = explode('.', rtrim($domain, '.')); + $result = pack('n', $priority); $totalLength = 2; - - foreach (\explode('.', $domain) as $label) { - $labelLength = \strlen($label); - $result .= \chr($labelLength); - $result .= $label; + foreach ($labels as $label) { + $labelLength = strlen($label); + if ($labelLength > 63) { + throw new \Exception("Label too long in MX domain: {$label}"); + } + $result .= chr($labelLength) . $label; $totalLength += 1 + $labelLength; } - - $result .= \chr(0); + $result .= chr(0); $totalLength += 1; - - $result = \pack('Nn', $ttl, $totalLength) . $result; - + $result = pack('Nn', $ttl, $totalLength) . $result; return $result; } + /** + * Encode an SRV record according to RFC 2782. + * + * @param string $domain + * @param int $ttl + * @param int $priority + * @param int $weight + * @param int $port + * @return string + */ protected function encodeSrv(string $domain, int $ttl, int $priority, int $weight, int $port): string { - $result = \pack('nnn', $priority, $weight, $port); + $labels = explode('.', rtrim($domain, '.')); + $result = pack('nnn', $priority, $weight, $port); $totalLength = 6; - - foreach (\explode('.', $domain) as $label) { - $labelLength = \strlen($label); - $result .= \chr($labelLength); - $result .= $label; + foreach ($labels as $label) { + $labelLength = strlen($label); + if ($labelLength > 63) { + throw new \Exception("Label too long in SRV domain: {$label}"); + } + $result .= chr($labelLength) . $label; $totalLength += 1 + $labelLength; } - - $result .= \chr(0); + $result .= chr(0); $totalLength += 1; - - $result = \pack('Nn', $ttl, $totalLength) . $result; - + $result = pack('Nn', $ttl, $totalLength) . $result; return $result; } From be5f654a9bdbefa5a4feac087805a6ad501459bb Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 27 Jul 2025 03:32:02 +0000 Subject: [PATCH 02/12] Fix zone tokenizer to handle quoted strings --- src/DNS/Zone.php | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/DNS/Zone.php b/src/DNS/Zone.php index 4b52bba..9054d48 100644 --- a/src/DNS/Zone.php +++ b/src/DNS/Zone.php @@ -360,22 +360,47 @@ protected function isComment(string $line): bool * * @return string[] */ + /** + * Tokenize a line by whitespace, handling quoted strings and escapes per RFC 1035. + * + * @return string[] + */ protected function tokenize(string $line): array { $tokens = []; $current = ''; - $inSpace = true; + $inQuote = false; + $escape = false; for ($i = 0, $len = strlen($line); $i < $len; $i++) { $c = $line[$i]; - if ($c === ' ' || $c === "\t") { - if (!$inSpace) { + if ($escape) { + $current .= $c; + $escape = false; + continue; + } + if ($c === '\\') { + $escape = true; + continue; + } + if ($inQuote) { + if ($c === '"') { + $inQuote = false; $tokens[] = $current; $current = ''; + } else { + $current .= $c; } - $inSpace = true; } else { - $current .= $c; - $inSpace = false; + if ($c === '"') { + $inQuote = true; + } elseif ($c === ' ' || $c === "\t") { + if ($current !== '') { + $tokens[] = $current; + $current = ''; + } + } else { + $current .= $c; + } } } if ($current !== '') { From bb861d0aaa2644dadd3ebbab930b499cfce46beb Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 27 Jul 2025 03:44:29 +0000 Subject: [PATCH 03/12] fix validation --- src/DNS/Server.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/DNS/Server.php b/src/DNS/Server.php index 077d369..616b86c 100644 --- a/src/DNS/Server.php +++ b/src/DNS/Server.php @@ -378,6 +378,9 @@ protected function encodeDomain(string $domain, int $ttl): string $totalLength = 0; foreach ($labels as $label) { $labelLength = strlen($label); + if ($labelLength === 0) { + throw new \Exception("Empty label in domain: '{$domain}'"); + } if ($labelLength > 63) { throw new \Exception("Label too long in domain: {$label}"); } @@ -449,11 +452,24 @@ protected function encodeMx(string $domain, int $ttl, int $priority): string */ protected function encodeSrv(string $domain, int $ttl, int $priority, int $weight, int $port): string { + // Validate SRV parameters + if ($priority < 0 || $priority > 65535) { + throw new \Exception("SRV priority out of range: {$priority}"); + } + if ($weight < 0 || $weight > 65535) { + throw new \Exception("SRV weight out of range: {$weight}"); + } + if ($port < 0 || $port > 65535) { + throw new \Exception("SRV port out of range: {$port}"); + } $labels = explode('.', rtrim($domain, '.')); $result = pack('nnn', $priority, $weight, $port); $totalLength = 6; foreach ($labels as $label) { $labelLength = strlen($label); + if ($labelLength === 0) { + throw new \Exception("Empty label in SRV domain: '{$domain}'"); + } if ($labelLength > 63) { throw new \Exception("Label too long in SRV domain: {$label}"); } From 56bc8cc3fce72a5a7c44976c9ddabdb905997b6f Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 27 Jul 2025 03:51:05 +0000 Subject: [PATCH 04/12] add test --- tests/DNS/ClientTest.php | 3 ++- tests/DNS/ServerMemory.php | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/DNS/ClientTest.php b/tests/DNS/ClientTest.php index 23f1c56..c18b786 100644 --- a/tests/DNS/ClientTest.php +++ b/tests/DNS/ClientTest.php @@ -105,9 +105,10 @@ public function testTXTRecords(): void $records = $this->client->query('dev2.appwrite.io', 'TXT'); - $this->assertCount(2, $records); + $this->assertCount(3, $records); $this->assertEquals('key with "$\'- symbols', $records[0]->getRdata()); $this->assertEquals('key with spaces', $records[1]->getRdata()); + $this->assertEquals('v=DMARC1; p=none; rua=mailto:jon@snow.got; ruf=mailto:jon@snow.got; fo=1;', $records[2]->getRdata()); $records = $this->client->query('dev3.appwrite.io', 'TXT'); $this->assertCount(0, $records); diff --git a/tests/DNS/ServerMemory.php b/tests/DNS/ServerMemory.php index 36c29ac..5a747cc 100644 --- a/tests/DNS/ServerMemory.php +++ b/tests/DNS/ServerMemory.php @@ -60,6 +60,10 @@ 'value' => 'key with spaces' ]); +$resolver->addRecord('dev2.appwrite.io', 'TXT', [ + 'value' => 'v=DMARC1; p=none; rua=mailto:jon@snow.got; ruf=mailto:jon@snow.got; fo=1;' +]); + $resolver->addRecord('dev.appwrite.io', 'CAA', [ 'value' => 'issue "letsencrypt.org"', 'ttl' => 50 From fdf0efc3cb90e42dd04f82119620383cb9b9f7a3 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 27 Jul 2025 04:54:52 +0000 Subject: [PATCH 05/12] update client --- src/DNS/Client.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/DNS/Client.php b/src/DNS/Client.php index 7eb3eb4..ba23536 100644 --- a/src/DNS/Client.php +++ b/src/DNS/Client.php @@ -226,10 +226,15 @@ private function parseRdata(string $packet, int &$offset, int $type, int $rdleng while ($offset < $end) { $len = ord($packet[$offset]); $offset++; - $txts[] = substr($packet, $offset, $len); + if ($len > 0) { + $txts[] = substr($packet, $offset, $len); + } else { + $txts[] = ''; + } $offset += $len; } - return implode(' ', $txts); + // Concatenate chunks directly, no separator + return implode('', $txts); case 33: // SRV record $priority = unpack('n', substr($packet, $offset, 2)); $weight = unpack('n', substr($packet, $offset + 2, 2)); From 665646803250d22e6714fd5f99464ed238804c9f Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 27 Jul 2025 05:15:59 +0000 Subject: [PATCH 06/12] improve client --- src/DNS/Client.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/DNS/Client.php b/src/DNS/Client.php index ba23536..51db7db 100644 --- a/src/DNS/Client.php +++ b/src/DNS/Client.php @@ -226,15 +226,16 @@ private function parseRdata(string $packet, int &$offset, int $type, int $rdleng while ($offset < $end) { $len = ord($packet[$offset]); $offset++; - if ($len > 0) { - $txts[] = substr($packet, $offset, $len); - } else { - $txts[] = ''; - } + $chunk = ($len > 0) ? substr($packet, $offset, $len) : ''; + $txts[] = $chunk; $offset += $len; } - // Concatenate chunks directly, no separator - return implode('', $txts); + // If you want to match dig output, wrap in quotes and escape embedded quotes + $txtValue = implode('', $txts); + if (strpos($txtValue, '"') !== false) { + $txtValue = str_replace('"', '\\"', $txtValue); + } + return '"' . $txtValue . '"'; case 33: // SRV record $priority = unpack('n', substr($packet, $offset, 2)); $weight = unpack('n', substr($packet, $offset + 2, 2)); From 614b63dd5864855f668bcd83460e13c87a9417e5 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 27 Jul 2025 05:19:14 +0000 Subject: [PATCH 07/12] improve client --- src/DNS/Client.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/DNS/Client.php b/src/DNS/Client.php index 51db7db..e17c340 100644 --- a/src/DNS/Client.php +++ b/src/DNS/Client.php @@ -230,12 +230,7 @@ private function parseRdata(string $packet, int &$offset, int $type, int $rdleng $txts[] = $chunk; $offset += $len; } - // If you want to match dig output, wrap in quotes and escape embedded quotes - $txtValue = implode('', $txts); - if (strpos($txtValue, '"') !== false) { - $txtValue = str_replace('"', '\\"', $txtValue); - } - return '"' . $txtValue . '"'; + return implode('', $txts); case 33: // SRV record $priority = unpack('n', substr($packet, $offset, 2)); $weight = unpack('n', substr($packet, $offset + 2, 2)); From 957e01e1f6a85578859337bbff89d877cfad6c22 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 27 Jul 2025 05:39:01 +0000 Subject: [PATCH 08/12] fix zone import/export --- src/DNS/Zone.php | 9 ++++++++- tests/DNS/ZoneTest.php | 13 +++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/DNS/Zone.php b/src/DNS/Zone.php index 9054d48..1cc6fc9 100644 --- a/src/DNS/Zone.php +++ b/src/DNS/Zone.php @@ -219,7 +219,14 @@ public function import(string $domain, string $content): array } $type = strtoupper($tokens[$pos++]); $dataParts = array_slice($tokens, $pos); - $data = implode(' ', $dataParts); + if ($type === 'TXT') { + $data = ''; + foreach ($dataParts as $part) { + $data .= $part; + } + } else { + $data = implode(' ', $dataParts); + } if ($owner !== '@' && !str_ends_with($owner, '.')) { $owner .= '.' . rtrim($this->defaultOrigin, '.'); } diff --git a/tests/DNS/ZoneTest.php b/tests/DNS/ZoneTest.php index fb0157e..a888eaa 100644 --- a/tests/DNS/ZoneTest.php +++ b/tests/DNS/ZoneTest.php @@ -337,4 +337,17 @@ public function testImportExportRoundTrip(): void $this->assertSame($records1[2]->getPriority(), $records2[2]->getPriority()); $this->assertSame($records1[2]->getRdata(), $records2[2]->getRdata()); } + + public function testImportTxtWithSpecialChars() + { + $zone = new Zone(); + $zonefile = <<import('example.com', $zonefile); + $this->assertCount(1, $records); + $rec = $records[0]; + $this->assertEquals('TXT', $rec->getTypeName()); + $this->assertEquals('v=DMARC1; p=none; rua=mailto:jon@snow.got; ruf=mailto:jon@snow.got; fo=1;', trim($rec->getRdata(), '"')); + } } From 23ff4294bd42c2ac83641d272fa44ef7e3a91956 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 27 Jul 2025 06:07:31 +0000 Subject: [PATCH 09/12] fix zone --- src/DNS/Zone.php | 54 ++++++++++++++++++++++++++++++++---------- tests/DNS/ZoneTest.php | 12 +++++++++- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/DNS/Zone.php b/src/DNS/Zone.php index 1cc6fc9..11cd951 100644 --- a/src/DNS/Zone.php +++ b/src/DNS/Zone.php @@ -178,7 +178,17 @@ public function import(string $domain, string $content): array $lastOwner = null; $logicalLines = $this->collectLogicalLines($content); foreach ($logicalLines as $rawLine) { - $line = trim($this->stripTrailingComment($rawLine)); + // For TXT records, only strip comments at #, not at semicolons + $isTxt = false; + $peekTokens = $this->tokenize($rawLine); + if (count($peekTokens) >= 4 && strtoupper($peekTokens[3]) === 'TXT') { + $isTxt = true; + } + if ($isTxt) { + $line = trim($this->stripTrailingComment($rawLine, true)); + } else { + $line = trim($this->stripTrailingComment($rawLine, false)); + } if ($line === '' || $this->isComment($line)) { continue; } @@ -272,11 +282,21 @@ public function export(string $domain, array $records): string $data = "{$r->getPriority()} {$r->getRdata()}"; } elseif ($type === 'SRV' && $r->getPriority() !== null && $r->getWeight() !== null && $r->getPort() !== null) { $data = "{$r->getPriority()} {$r->getWeight()} {$r->getPort()} {$r->getRdata()}"; + } elseif ($type === 'TXT') { + // Encode TXT records with double quotes and escape embedded quotes and backslashes + $escaped = str_replace(['\\', '"'], ['\\\\', '\\"'], $data); + $data = '"' . $escaped . '"'; } $lines[] = sprintf("%s %d %s %s %s", $owner, $ttl, $class, $type, $data); } - return implode("\n", $lines) . "\n"; + // Remove any empty lines at the end + while (!empty($lines) && trim(end($lines)) === '') { + array_pop($lines); + } + $output = implode("\n", $lines); + // Guarantee only a single newline at the end + return rtrim($output, "\n") . "\n"; } /** @@ -300,7 +320,7 @@ protected function collectLogicalLines(string $content): array $parenDepth = 0; foreach ($rawLines as $rawLine) { // Remove trailing comments from the physical line. - $line = trim($this->stripTrailingComment($rawLine)); + $line = trim($this->stripTrailingComment($rawLine, false)); if ($line === '') { continue; } @@ -343,17 +363,26 @@ protected function collectLogicalLines(string $content): array /** * Remove trailing comments (anything after '#' or ';') from a line. */ - protected function stripTrailingComment(string $line): string + + /** + * Remove trailing comments (anything after '#' or ';') from a line. + * If $txtMode is true, only strip at #, not at semicolons. + */ + protected function stripTrailingComment(string $line, bool $txtMode): string { - $hashPos = strpos($line, '#'); - if ($hashPos !== false) { - $line = substr($line, 0, $hashPos); - } - $semiPos = strpos($line, ';'); - if ($semiPos !== false) { - $line = substr($line, 0, $semiPos); + $inQuote = false; + $result = ''; + for ($i = 0, $len = strlen($line); $i < $len; $i++) { + $c = $line[$i]; + if ($c === '"') { + $inQuote = !$inQuote; + } + if (!$inQuote && ($c === '#' || (!$txtMode && $c === ';'))) { + break; + } + $result .= $c; } - return $line; + return $result; } protected function isComment(string $line): bool @@ -374,6 +403,7 @@ protected function isComment(string $line): bool */ protected function tokenize(string $line): array { + // Manual tokenizer: handles quoted strings and escapes $tokens = []; $current = ''; $inQuote = false; diff --git a/tests/DNS/ZoneTest.php b/tests/DNS/ZoneTest.php index a888eaa..c5db471 100644 --- a/tests/DNS/ZoneTest.php +++ b/tests/DNS/ZoneTest.php @@ -338,7 +338,7 @@ public function testImportExportRoundTrip(): void $this->assertSame($records1[2]->getRdata(), $records2[2]->getRdata()); } - public function testImportTxtWithSpecialChars() + public function testImportTxtWithSpecialChars(): void { $zone = new Zone(); $zonefile = <<assertEquals('TXT', $rec->getTypeName()); $this->assertEquals('v=DMARC1; p=none; rua=mailto:jon@snow.got; ruf=mailto:jon@snow.got; fo=1;', trim($rec->getRdata(), '"')); } + + public function testExportTxtWithSpecialChars(): void + { + $zone = new Zone(); + $txt = 'v=DMARC1; p=none; rua=mailto:jon@snow.got; ruf=mailto:jon@snow.got; fo=1; text="quoted"; backslash=\\'; + $rec = new Record('@', 3600, 'IN', 'TXT', $txt); + $exported = $zone->export('example.com', [$rec]); + $expected = '@ 3600 IN TXT "v=DMARC1; p=none; rua=mailto:jon@snow.got; ruf=mailto:jon@snow.got; fo=1; text=\\"quoted\\"; backslash=\\\\"' . "\n"; + $this->assertSame($expected, $exported); + } } From eddb5d8af2f94a63b940881a02866d01f2cf77b6 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 27 Jul 2025 06:17:16 +0000 Subject: [PATCH 10/12] fix reviews --- src/DNS/Zone.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/DNS/Zone.php b/src/DNS/Zone.php index 11cd951..b93abb7 100644 --- a/src/DNS/Zone.php +++ b/src/DNS/Zone.php @@ -181,8 +181,11 @@ public function import(string $domain, string $content): array // For TXT records, only strip comments at #, not at semicolons $isTxt = false; $peekTokens = $this->tokenize($rawLine); - if (count($peekTokens) >= 4 && strtoupper($peekTokens[3]) === 'TXT') { - $isTxt = true; + foreach ($peekTokens as $tok) { + if (strtoupper($tok) === 'TXT') { + $isTxt = true; + break; + } } if ($isTxt) { $line = trim($this->stripTrailingComment($rawLine, true)); @@ -374,8 +377,17 @@ protected function stripTrailingComment(string $line, bool $txtMode): string $result = ''; for ($i = 0, $len = strlen($line); $i < $len; $i++) { $c = $line[$i]; + // Check for unescaped quote if ($c === '"') { - $inQuote = !$inQuote; + $escaped = false; + $j = $i - 1; + while ($j >= 0 && $line[$j] === '\\') { + $escaped = !$escaped; + $j--; + } + if (!$escaped) { + $inQuote = !$inQuote; + } } if (!$inQuote && ($c === '#' || (!$txtMode && $c === ';'))) { break; From 7650f282c5d8e585e38dff2693bfd346d9a79d12 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 28 Jul 2025 07:51:59 +0000 Subject: [PATCH 11/12] improvements with validation and constants --- src/DNS/Server.php | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/DNS/Server.php b/src/DNS/Server.php index 616b86c..613be9a 100644 --- a/src/DNS/Server.php +++ b/src/DNS/Server.php @@ -51,7 +51,18 @@ */ class Server +// DNS protocol constants { + public const IPV4_LEN = 4; + public const IPV6_LEN = 16; + public const MAX_LABEL_LEN = 63; + public const MAX_LABELS = 127; + public const MAX_DOMAIN_NAME_LEN = 255; // RFC 1035: max length of domain name in wire format + public const MAX_PRIORITY = 65535; + public const MAX_WEIGHT = 65535; + public const MAX_PORT = 65535; + public const MAX_CAA_FLAGS = 255; + public const MAX_TXT_CHUNK = 255; protected Adapter $adapter; protected Resolver $resolver; /** @var array */ @@ -340,10 +351,10 @@ protected function resolve(array $question): array protected function encodeIP(string $ip, int $ttl): string { $binaryIP = inet_pton($ip); - if ($binaryIP === false || strlen($binaryIP) !== 4) { + if ($binaryIP === false || strlen($binaryIP) !== self::IPV4_LEN) { throw new \Exception("Invalid IPv4 address format: {$ip}"); } - $result = pack('Nn', $ttl, 4) . $binaryIP; + $result = pack('Nn', $ttl, self::IPV4_LEN) . $binaryIP; return $result; } @@ -357,10 +368,10 @@ protected function encodeIP(string $ip, int $ttl): string protected function encodeIPv6(string $ip, int $ttl): string { $binaryIP = inet_pton($ip); - if ($binaryIP === false || strlen($binaryIP) !== 16) { + if ($binaryIP === false || strlen($binaryIP) !== self::IPV6_LEN) { throw new \Exception("Invalid IPv6 address format: {$ip}"); } - $result = pack('Nn', $ttl, 16) . $binaryIP; + $result = pack('Nn', $ttl, self::IPV6_LEN) . $binaryIP; return $result; } @@ -381,7 +392,7 @@ protected function encodeDomain(string $domain, int $ttl): string if ($labelLength === 0) { throw new \Exception("Empty label in domain: '{$domain}'"); } - if ($labelLength > 63) { + if ($labelLength > self::MAX_LABEL_LEN) { throw new \Exception("Label too long in domain: {$label}"); } $result .= chr($labelLength) . $label; @@ -389,6 +400,9 @@ protected function encodeDomain(string $domain, int $ttl): string } $result .= chr(0); $totalLength += 1; + if ($totalLength > self::MAX_DOMAIN_NAME_LEN) { + throw new \Exception("Encoded domain name too long: {$domain}"); + } $result = pack('Nn', $ttl, $totalLength) . $result; return $result; } @@ -404,8 +418,8 @@ protected function encodeText(string $text, int $ttl): string { $chunks = []; $len = strlen($text); - for ($i = 0; $i < $len; $i += 255) { - $chunk = substr($text, $i, 255); + for ($i = 0; $i < $len; $i += self::MAX_TXT_CHUNK) { + $chunk = substr($text, $i, self::MAX_TXT_CHUNK); $chunks[] = chr(strlen($chunk)) . $chunk; } $txtData = implode('', $chunks); @@ -428,7 +442,7 @@ protected function encodeMx(string $domain, int $ttl, int $priority): string $totalLength = 2; foreach ($labels as $label) { $labelLength = strlen($label); - if ($labelLength > 63) { + if ($labelLength > self::MAX_LABEL_LEN) { throw new \Exception("Label too long in MX domain: {$label}"); } $result .= chr($labelLength) . $label; @@ -436,6 +450,9 @@ protected function encodeMx(string $domain, int $ttl, int $priority): string } $result .= chr(0); $totalLength += 1; + if ($totalLength > self::MAX_DOMAIN_NAME_LEN) { + throw new \Exception("Encoded MX domain name too long: {$domain}"); + } $result = pack('Nn', $ttl, $totalLength) . $result; return $result; } @@ -453,13 +470,13 @@ protected function encodeMx(string $domain, int $ttl, int $priority): string protected function encodeSrv(string $domain, int $ttl, int $priority, int $weight, int $port): string { // Validate SRV parameters - if ($priority < 0 || $priority > 65535) { + if ($priority < 0 || $priority > self::MAX_PRIORITY) { throw new \Exception("SRV priority out of range: {$priority}"); } - if ($weight < 0 || $weight > 65535) { + if ($weight < 0 || $weight > self::MAX_WEIGHT) { throw new \Exception("SRV weight out of range: {$weight}"); } - if ($port < 0 || $port > 65535) { + if ($port < 0 || $port > self::MAX_PORT) { throw new \Exception("SRV port out of range: {$port}"); } $labels = explode('.', rtrim($domain, '.')); @@ -470,7 +487,7 @@ protected function encodeSrv(string $domain, int $ttl, int $priority, int $weigh if ($labelLength === 0) { throw new \Exception("Empty label in SRV domain: '{$domain}'"); } - if ($labelLength > 63) { + if ($labelLength > self::MAX_LABEL_LEN) { throw new \Exception("Label too long in SRV domain: {$label}"); } $result .= chr($labelLength) . $label; @@ -478,6 +495,9 @@ protected function encodeSrv(string $domain, int $ttl, int $priority, int $weigh } $result .= chr(0); $totalLength += 1; + if ($totalLength > self::MAX_DOMAIN_NAME_LEN) { + throw new \Exception("Encoded SRV domain name too long: {$domain}"); + } $result = pack('Nn', $ttl, $totalLength) . $result; return $result; } @@ -515,7 +535,7 @@ protected function encodeCAA(array|string $rdata, int $ttl): string } } // Validate flags (must be 0-255) - $flags = max(0, min(255, $flags)); + $flags = max(0, min(self::MAX_CAA_FLAGS, $flags)); $tagLen = strlen($tag); $valueLen = strlen($value); $rdataBin = chr($flags) . chr($tagLen) . $tag . $value; From d8064364b7e137e5ce8b2cf8eedf82ae10d2589b Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 28 Jul 2025 07:55:22 +0000 Subject: [PATCH 12/12] relevant RFC links --- src/DNS/Server.php | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/DNS/Server.php b/src/DNS/Server.php index 613be9a..b11fe3f 100644 --- a/src/DNS/Server.php +++ b/src/DNS/Server.php @@ -10,7 +10,7 @@ use Utopia\Telemetry\Histogram; /** - * Refference about DNS packet: + * Reference about DNS packet: * * HEADER * > 16 bits identificationField (1-65535. 0 means no ID). ID provided by client. Helps to match async responses. Usage may allow DNS Cache Poisoning @@ -29,7 +29,7 @@ * > 16 bits numberOfAdditionals (0-65535) * * QUESTIONS SECTION - * > Each question contians: + * > Each question contains: * > -- dynamic-length name. Includes domain name we are looking for. Split into labels. To get domain, join labels with dot symbol. * > -- -- Following pattern repeats: * > -- -- -- 8 bits labelLength (0-255). Defines length of label. We use it in next step @@ -42,12 +42,18 @@ * ANSWERS SECTION * > Follows same pattern as questions section. * > Each answer also has (at the end): - * > -- 32 bits ttl. Time to live of the naswer + * > -- 32 bits ttl. Time to live of the answer * > -- 16 bit length. Length of the answer data. * > -- X bits data X length is length from above. Gives answer itself. Structure changes based on type. * * AUTHORITIES SECTION * ADDITIONALS SECTION + * + * RFCs: + * - RFC 1035: https://datatracker.ietf.org/doc/html/rfc1035 + * - RFC 3596: https://datatracker.ietf.org/doc/html/rfc3596 + * - RFC 6844: https://datatracker.ietf.org/doc/html/rfc6844 + * - RFC 2782: https://datatracker.ietf.org/doc/html/rfc2782 */ class Server @@ -343,6 +349,7 @@ protected function resolve(array $question): array /** * Encode an IPv4 address (A record) according to RFC 1035. + * @see https://datatracker.ietf.org/doc/html/rfc1035 * * @param string $ip * @param int $ttl @@ -360,6 +367,7 @@ protected function encodeIP(string $ip, int $ttl): string /** * Encode an IPv6 address (AAAA record) according to RFC 3596. + * @see https://datatracker.ietf.org/doc/html/rfc3596 * * @param string $ip * @param int $ttl @@ -377,6 +385,7 @@ protected function encodeIPv6(string $ip, int $ttl): string /** * Encode a domain name (CNAME, NS, PTR) according to RFC 1035. + * @see https://datatracker.ietf.org/doc/html/rfc1035 * * @param string $domain * @param int $ttl @@ -409,6 +418,7 @@ protected function encodeDomain(string $domain, int $ttl): string /** * Encode a TXT record according to RFC 1035. + * @see https://datatracker.ietf.org/doc/html/rfc1035 * * @param string $text * @param int $ttl @@ -429,6 +439,7 @@ protected function encodeText(string $text, int $ttl): string /** * Encode an MX record according to RFC 1035. + * @see https://datatracker.ietf.org/doc/html/rfc1035 * * @param string $domain * @param int $ttl @@ -459,6 +470,7 @@ protected function encodeMx(string $domain, int $ttl, int $priority): string /** * Encode an SRV record according to RFC 2782. + * @see https://datatracker.ietf.org/doc/html/rfc2782 * * @param string $domain * @param int $ttl @@ -504,6 +516,7 @@ protected function encodeSrv(string $domain, int $ttl, int $priority, int $weigh /** * Encode a CAA record according to RFC 6844. + * @see https://datatracker.ietf.org/doc/html/rfc6844 * * @param array{flags?:int,tag?:string,value?:string}|string $rdata * @param int $ttl