From db4079e4b3180d651db192f05ad32ecf7144590a Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 30 Dec 2025 03:26:27 +0000 Subject: [PATCH 01/19] Add TCP support to DNS server and client - Updated README to reflect TCP support on port 5300 - Modified docker-compose to expose TCP port 5300 - Enhanced Native adapter to handle TCP connections - Updated Swoole adapter for TCP support - Improved Client class to support TCP queries - Added unit test for TCP queries in ClientTest --- README.md | 6 +- docker-compose.yml | 1 + src/DNS/Adapter/Native.php | 181 ++++++++++++++++++++++++++++++++--- src/DNS/Adapter/Swoole.php | 45 +++++++-- src/DNS/Client.php | 112 ++++++++++++++++++++-- src/DNS/Validator/DNS.php | 3 +- tests/e2e/DNS/ClientTest.php | 15 +++ 7 files changed, 331 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 9d84389..477e90f 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,8 @@ $adapter = new Native('0.0.0.0', 5300); $zone = new Zone( name: 'example.test', records: [ - new Record(name: 'example.test', type: Record::TYPE_A, rdata: '127.0.0.1', ttl: 60), +The server listens on UDP and TCP port `5300` (RFC 5966) and answers queries for `example.test` from the in-memory zone. Implement the [`Utopia\DNS\Resolver`](src/DNS/Resolver.php) interface to serve records from databases, APIs, or other stores. new Record(name: 'www.example.test', type: Record::TYPE_CNAME, rdata: 'example.test', ttl: 60), - new Record(name: 'example.test', type: Record::TYPE_TXT, rdata: '"demo record"', ttl: 60), - ], - soa: new Record( - name: 'example.test', type: Record::TYPE_SOA, rdata: 'ns1.example.test hostmaster.example.test 1 7200 1800 1209600 3600', ttl: 60 diff --git a/docker-compose.yml b/docker-compose.yml index abbcd4c..7c79bf5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,5 +15,6 @@ services: - dns ports: - '5300:5300/udp' + - '5300:5300/tcp' networks: dns: diff --git a/src/DNS/Adapter/Native.php b/src/DNS/Adapter/Native.php index 82b0d1e..0694ad2 100644 --- a/src/DNS/Adapter/Native.php +++ b/src/DNS/Adapter/Native.php @@ -8,7 +8,15 @@ class Native extends Adapter { - protected Socket $server; + protected Socket $udpServer; + + protected ?Socket $tcpServer = null; + + /** @var array */ + protected array $tcpClients = []; + + /** @var array */ + protected array $tcpBuffers = []; /** @var callable(string $buffer, string $ip, int $port): string */ protected mixed $onPacket; @@ -19,20 +27,33 @@ class Native extends Adapter protected string $host; protected int $port; + protected bool $enableTcp; + /** * @param string $host * @param int $port */ - public function __construct(string $host = '0.0.0.0', int $port = 8053) + public function __construct(string $host = '0.0.0.0', int $port = 8053, bool $enableTcp = true) { $this->host = $host; $this->port = $port; + $this->enableTcp = $enableTcp; $server = \socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); if (!$server) { throw new Exception('Could not start server.'); } - $this->server = $server; + $this->udpServer = $server; + + if ($this->enableTcp) { + $tcp = \socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + if (!$tcp) { + throw new Exception('Could not start TCP server.'); + } + + socket_set_option($tcp, SOL_SOCKET, SO_REUSEADDR, 1); + $this->tcpServer = $tcp; + } } /** @@ -60,26 +81,87 @@ public function onPacket(callable $callback): void */ public function start(): void { - if (socket_bind($this->server, $this->host, $this->port) == false) { + if (socket_bind($this->udpServer, $this->host, $this->port) == false) { throw new Exception('Could not bind server to a server.'); } + if ($this->tcpServer) { + if (socket_bind($this->tcpServer, $this->host, $this->port) == false) { + throw new Exception('Could not bind TCP server.'); + } + + if (socket_listen($this->tcpServer, 128) == false) { + throw new Exception('Could not listen on TCP server.'); + } + + socket_set_nonblock($this->tcpServer); + } + foreach ($this->onWorkerStart as $callback) { \call_user_func($callback, 0); } /** @phpstan-ignore-next-line */ while (1) { - $buf = ''; - $ip = ''; - $port = null; - $len = socket_recvfrom($this->server, $buf, 1024 * 4, 0, $ip, $port); + $readSockets = [$this->udpServer]; + + if ($this->tcpServer) { + $readSockets[] = $this->tcpServer; + } - if ($len > 0) { - $answer = call_user_func($this->onPacket, $buf, $ip, $port); + foreach ($this->tcpClients as $client) { + $readSockets[] = $client; + } - if (socket_sendto($this->server, $answer, strlen($answer), 0, $ip, $port) === false) { - printf('Error in socket\n'); + $write = []; + $except = []; + + $changed = socket_select($readSockets, $write, $except, null); + + if ($changed === false) { + continue; + } + + foreach ($readSockets as $socket) { + if ($socket === $this->udpServer) { + $buf = ''; + $ip = ''; + $port = null; + $len = socket_recvfrom($this->udpServer, $buf, 1024 * 4, 0, $ip, $port); + + if ($len > 0) { + $answer = call_user_func($this->onPacket, $buf, $ip, $port); + + if ($answer === '') { + continue; + } + + if (socket_sendto($this->udpServer, $answer, strlen($answer), 0, $ip, $port) === false) { + printf("Error sending UDP response\n"); + } + } + + continue; + } + + if ($this->tcpServer !== null && $socket === $this->tcpServer) { + $client = @socket_accept($this->tcpServer); + + if ($client instanceof Socket) { + socket_set_option($client, SOL_SOCKET, SO_KEEPALIVE, 1); + socket_set_option($client, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 5, 'usec' => 0]); + socket_set_option($client, SOL_SOCKET, SO_SNDTIMEO, ['sec' => 5, 'usec' => 0]); + + $id = spl_object_id($client); + $this->tcpClients[$id] = $client; + $this->tcpBuffers[$id] = ''; + } + + continue; + } + + if ($socket instanceof Socket) { + $this->handleTcpClient($socket); } } } @@ -94,4 +176,79 @@ public function getName(): string { return 'native'; } + + protected function handleTcpClient(Socket $client): void + { + $clientId = spl_object_id($client); + + $chunk = @socket_read($client, 8192, PHP_BINARY_READ); + + if ($chunk === '' || $chunk === false) { + $error = socket_last_error($client); + + if ($chunk === '' || !in_array($error, [SOCKET_EAGAIN, SOCKET_EWOULDBLOCK], true)) { + $this->closeTcpClient($client); + } + + return; + } + + $this->tcpBuffers[$clientId] = ($this->tcpBuffers[$clientId] ?? '') . $chunk; + + while (strlen($this->tcpBuffers[$clientId]) >= 2) { + $length = unpack('nlen', substr($this->tcpBuffers[$clientId], 0, 2)); + $payloadLength = $length['len'] ?? 0; + + if ($payloadLength === 0) { + $this->closeTcpClient($client); + return; + } + + if (strlen($this->tcpBuffers[$clientId]) < ($payloadLength + 2)) { + return; + } + + $message = substr($this->tcpBuffers[$clientId], 2, $payloadLength); + $this->tcpBuffers[$clientId] = substr($this->tcpBuffers[$clientId], $payloadLength + 2); + + $ip = ''; + $port = 0; + socket_getpeername($client, $ip, $port); + + $answer = call_user_func($this->onPacket, $message, $ip, $port); + + if ($answer === '') { + continue; + } + + $this->sendTcpResponse($client, $answer); + } + } + + protected function sendTcpResponse(Socket $client, string $payload): void + { + $frame = pack('n', strlen($payload)) . $payload; + $total = strlen($frame); + $sent = 0; + + while ($sent < $total) { + $written = @socket_write($client, substr($frame, $sent)); + + if ($written === false) { + $this->closeTcpClient($client); + return; + } + + $sent += $written; + } + } + + protected function closeTcpClient(Socket $client): void + { + $id = spl_object_id($client); + + unset($this->tcpClients[$id], $this->tcpBuffers[$id]); + + @socket_close($client); + } } diff --git a/src/DNS/Adapter/Swoole.php b/src/DNS/Adapter/Swoole.php index 6f76281..74c6377 100644 --- a/src/DNS/Adapter/Swoole.php +++ b/src/DNS/Adapter/Swoole.php @@ -5,11 +5,14 @@ use Swoole\Runtime; use Utopia\DNS\Adapter; use Swoole\Server; +use Swoole\Server\Port; class Swoole extends Adapter { protected Server $server; + protected ?Port $udpPort = null; + /** @var callable(string $buffer, string $ip, int $port): string */ protected mixed $onPacket; @@ -20,7 +23,17 @@ public function __construct(string $host = '0.0.0.0', int $port = 53) { $this->host = $host; $this->port = $port; - $this->server = new Server($this->host, $this->port, SWOOLE_PROCESS, SWOOLE_SOCK_UDP); + $this->server = new Server($this->host, $this->port, SWOOLE_PROCESS, SWOOLE_SOCK_TCP); + + $this->server->set([ + 'open_length_check' => true, + 'package_length_type' => 'n', + 'package_length_offset' => 0, + 'package_body_offset' => 2, + 'package_max_length' => 65537, + ]); + + $this->udpPort = $this->server->addlistener($this->host, $this->port, SWOOLE_SOCK_UDP); } /** @@ -43,17 +56,35 @@ public function onPacket(callable $callback): void { $this->onPacket = $callback; - $this->server->on('Packet', function ($server, $data, $clientInfo) { - $ip = $clientInfo['address'] ?? ''; - $port = $clientInfo['port'] ?? ''; - $answer = \call_user_func($this->onPacket, $data, $ip, $port); + if ($this->udpPort instanceof Port) { + $this->udpPort->on('Packet', function ($server, $data, $clientInfo) { + $ip = $clientInfo['address'] ?? ''; + $port = (int) ($clientInfo['port'] ?? 0); + $answer = \call_user_func($this->onPacket, $data, $ip, $port); + + // Swoole UDP sockets reject zero-length payloads; skip responding instead. + if ($answer === '') { + return; + } + + $server->sendto($ip, $port, $answer); + }); + } + + $this->server->on('Receive', function (Server $server, int $fd, int $reactorId, string $data) { + $info = $server->getClientInfo($fd, $reactorId) ?: []; + $ip = $info['remote_ip'] ?? ''; + $port = $info['remote_port'] ?? 0; + + $payload = substr($data, 2); // strip 2-byte length prefix + $answer = \call_user_func($this->onPacket, $payload, $ip, $port); - // Swoole UDP sockets reject zero-length payloads; skip responding instead. if ($answer === '') { return; } - $server->sendto($ip, $port, $answer); + $frame = pack('n', strlen($answer)) . $answer; + $server->send($fd, $frame); }); } diff --git a/src/DNS/Client.php b/src/DNS/Client.php index 75acb3c..4b8b56b 100644 --- a/src/DNS/Client.php +++ b/src/DNS/Client.php @@ -7,13 +7,15 @@ class Client { - /** @var \Socket */ - protected $socket; + /** @var \Socket|null */ + protected $socket = null; protected string $server; protected int $port; protected int $timeout; - public function __construct(string $server = '127.0.0.1', int $port = 53, int $timeout = 5) + protected bool $useTcp; + + public function __construct(string $server = '127.0.0.1', int $port = 53, int $timeout = 5, bool $useTcp = false) { $validator = new IP(IP::ALL); // IPv4 + IPv6 if (!$validator->isValid($server)) { @@ -23,6 +25,11 @@ public function __construct(string $server = '127.0.0.1', int $port = 53, int $t $this->server = $server; $this->port = $port; $this->timeout = $timeout; + $this->useTcp = $useTcp; + + if ($this->useTcp) { + return; + } $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); @@ -43,6 +50,10 @@ public function __construct(string $server = '127.0.0.1', int $port = 53, int $t */ public function query(Message $message): Message { + if ($this->useTcp) { + return $this->queryTcp($message); + } + $packet = $message->encode(); if (socket_sendto($this->socket, $packet, strlen($packet), 0, $this->server, $this->port) === false) { throw new Exception('Failed to send data: ' . socket_strerror(socket_last_error($this->socket))); @@ -64,11 +75,100 @@ public function query(Message $message): Message throw new Exception("Empty response received from $this->server:$this->port"); } - $response = Message::decode($data); - if ($response->header->id !== $message->header->id) { - throw new Exception("Mismatched DNS transaction ID. Expected {$message->header->id}, got {$response->header->id}"); + return $this->decodeResponse($message, $data); + } + + protected function queryTcp(Message $message): Message + { + $targetHost = $this->formatTcpHost($this->server); + $uri = "tcp://{$targetHost}:{$this->port}"; + + $socket = @stream_socket_client($uri, $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT); + + if ($socket === false) { + throw new Exception("Failed to connect to {$this->server}:{$this->port} over TCP: $errstr ($errno)"); + } + + try { + stream_set_timeout($socket, $this->timeout); + + $packet = $message->encode(); + $frame = pack('n', strlen($packet)) . $packet; + + $written = fwrite($socket, $frame); + + if ($written === false || $written < strlen($frame)) { + throw new Exception('Failed to send full TCP DNS query.'); + } + + $lengthBytes = $this->readBytes($socket, 2); + + if (strlen($lengthBytes) !== 2) { + throw new Exception('Failed to read DNS TCP length prefix.'); + } + + $length = unpack('nlen', $lengthBytes)['len'] ?? 0; + + if ($length === 0) { + throw new Exception('Received empty DNS TCP response.'); + } + + $payload = $this->readBytes($socket, $length); + + if (strlen($payload) !== $length) { + throw new Exception('Incomplete DNS TCP response received.'); + } + + return $this->decodeResponse($message, $payload); + } finally { + fclose($socket); + } + } + + protected function decodeResponse(Message $query, string $payload): Message + { + $response = Message::decode($payload); + + if ($response->header->id !== $query->header->id) { + throw new Exception("Mismatched DNS transaction ID. Expected {$query->header->id}, got {$response->header->id}"); } return $response; } + + protected function readBytes(mixed $socket, int $length): string + { + $data = ''; + + while (strlen($data) < $length) { + $chunk = fread($socket, $length - strlen($data)); + + if ($chunk === false) { + break; + } + + if ($chunk === '') { + $meta = stream_get_meta_data($socket); + + if (!empty($meta['timed_out'])) { + break; + } + + continue; + } + + $data .= $chunk; + } + + return $data; + } + + protected function formatTcpHost(string $host): string + { + if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) { + return '[' . $host . ']'; + } + + return $host; + } } diff --git a/src/DNS/Validator/DNS.php b/src/DNS/Validator/DNS.php index ded9829..14a6bcc 100644 --- a/src/DNS/Validator/DNS.php +++ b/src/DNS/Validator/DNS.php @@ -58,7 +58,7 @@ public function getDescription(): string $records = implode("', '", $this->records); - $countVerbose = match($this->count) { + $countVerbose = match ($this->count) { 1 => 'one', 2 => 'two', 3 => 'three', @@ -117,7 +117,6 @@ public function isValid(mixed $value): bool $query = array_filter($answers, function ($record) { return $record->type === $this->type; }); - } catch (\Exception $e) { $this->reason = self::FAILURE_REASON_QUERY; return false; diff --git a/tests/e2e/DNS/ClientTest.php b/tests/e2e/DNS/ClientTest.php index e2e6731..653b46d 100644 --- a/tests/e2e/DNS/ClientTest.php +++ b/tests/e2e/DNS/ClientTest.php @@ -12,6 +12,21 @@ final class ClientTest extends TestCase { public const int PORT = 5300; + public function testTcpQueries(): void + { + $client = new Client('127.0.0.1', self::PORT, 5, true); + + $response = $client->query(Message::query( + new Question('dev2.appwrite.io', Record::TYPE_A) + )); + + $records = $response->answers; + + $this->assertCount(2, $records); + $this->assertSame('dev2.appwrite.io', $records[0]->name); + $this->assertSame(Record::TYPE_A, $records[0]->type); + } + public function testARecords(): void { $client = new Client('127.0.0.1', self::PORT); From 7cf12be533e3fcb442cac78ee5521c44f11b94e9 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 30 Dec 2025 03:31:11 +0000 Subject: [PATCH 02/19] Fix codeql analysis --- src/DNS/Adapter/Native.php | 5 ++--- src/DNS/Client.php | 12 +++++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/DNS/Adapter/Native.php b/src/DNS/Adapter/Native.php index 0694ad2..eccaaa7 100644 --- a/src/DNS/Adapter/Native.php +++ b/src/DNS/Adapter/Native.php @@ -160,9 +160,8 @@ public function start(): void continue; } - if ($socket instanceof Socket) { - $this->handleTcpClient($socket); - } + // Remaining readable sockets are TCP clients. + $this->handleTcpClient($socket); } } } diff --git a/src/DNS/Client.php b/src/DNS/Client.php index 4b8b56b..e94c829 100644 --- a/src/DNS/Client.php +++ b/src/DNS/Client.php @@ -54,6 +54,10 @@ public function query(Message $message): Message return $this->queryTcp($message); } + if (!$this->socket instanceof \Socket) { + throw new Exception('UDP socket not initialized.'); + } + $packet = $message->encode(); if (socket_sendto($this->socket, $packet, strlen($packet), 0, $this->server, $this->port) === false) { throw new Exception('Failed to send data: ' . socket_strerror(socket_last_error($this->socket))); @@ -141,7 +145,13 @@ protected function readBytes(mixed $socket, int $length): string $data = ''; while (strlen($data) < $length) { - $chunk = fread($socket, $length - strlen($data)); + $remaining = $length - strlen($data); + + if ($remaining <= 0) { + break; + } + + $chunk = fread($socket, max(1, $remaining)); if ($chunk === false) { break; From 31234841dec7ad3dcc75c2eb23356ce606d0952b Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 30 Dec 2025 03:33:37 +0000 Subject: [PATCH 03/19] Fix guard agains fread zero --- src/DNS/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DNS/Client.php b/src/DNS/Client.php index e94c829..0cbfeb0 100644 --- a/src/DNS/Client.php +++ b/src/DNS/Client.php @@ -151,7 +151,7 @@ protected function readBytes(mixed $socket, int $length): string break; } - $chunk = fread($socket, max(1, $remaining)); + $chunk = fread($socket, $remaining); if ($chunk === false) { break; From 8382a0d41958573281771d110ea9c770fc44714d Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 30 Dec 2025 09:22:04 +0545 Subject: [PATCH 04/19] Update src/DNS/Client.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/DNS/Client.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DNS/Client.php b/src/DNS/Client.php index 0cbfeb0..fa70679 100644 --- a/src/DNS/Client.php +++ b/src/DNS/Client.php @@ -151,7 +151,7 @@ protected function readBytes(mixed $socket, int $length): string break; } - $chunk = fread($socket, $remaining); + $chunk = fread($socket, max(1, $remaining)); if ($chunk === false) { break; @@ -160,7 +160,7 @@ protected function readBytes(mixed $socket, int $length): string if ($chunk === '') { $meta = stream_get_meta_data($socket); - if (!empty($meta['timed_out'])) { + if (!empty($meta['timed_out']) || !empty($meta['eof'])) { break; } From 04d2dc3f0d52fc4bc179c7f6315debc19e007e7a Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 30 Dec 2025 03:38:34 +0000 Subject: [PATCH 05/19] Fix socket errors --- src/DNS/Adapter/Native.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/DNS/Adapter/Native.php b/src/DNS/Adapter/Native.php index eccaaa7..c4ab0aa 100644 --- a/src/DNS/Adapter/Native.php +++ b/src/DNS/Adapter/Native.php @@ -234,6 +234,14 @@ protected function sendTcpResponse(Socket $client, string $payload): void $written = @socket_write($client, substr($frame, $sent)); if ($written === false) { + $error = socket_last_error($client); + + if (in_array($error, [SOCKET_EAGAIN, SOCKET_EWOULDBLOCK], true)) { + socket_clear_error($client); + usleep(1000); + continue; + } + $this->closeTcpClient($client); return; } From f8c7b016f5003bb4b346d1aa4f445f5a03f48690 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 01:27:41 +0000 Subject: [PATCH 06/19] set socket non blocking --- src/DNS/Adapter/Native.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/DNS/Adapter/Native.php b/src/DNS/Adapter/Native.php index c4ab0aa..dd71844 100644 --- a/src/DNS/Adapter/Native.php +++ b/src/DNS/Adapter/Native.php @@ -148,6 +148,11 @@ public function start(): void $client = @socket_accept($this->tcpServer); if ($client instanceof Socket) { + if (@socket_set_nonblock($client) === false) { + @socket_close($client); + continue; + } + socket_set_option($client, SOL_SOCKET, SO_KEEPALIVE, 1); socket_set_option($client, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 5, 'usec' => 0]); socket_set_option($client, SOL_SOCKET, SO_SNDTIMEO, ['sec' => 5, 'usec' => 0]); From 9149af94b461c54f905deaa4ba9d7f86c2824a47 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 01:29:56 +0000 Subject: [PATCH 07/19] Fix: set max tcp clients limit --- src/DNS/Adapter/Native.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/DNS/Adapter/Native.php b/src/DNS/Adapter/Native.php index dd71844..590519a 100644 --- a/src/DNS/Adapter/Native.php +++ b/src/DNS/Adapter/Native.php @@ -29,15 +29,20 @@ class Native extends Adapter protected bool $enableTcp; + protected int $maxTcpClients; + /** * @param string $host * @param int $port + * @param bool $enableTcp + * @param int $maxTcpClients */ - public function __construct(string $host = '0.0.0.0', int $port = 8053, bool $enableTcp = true) + public function __construct(string $host = '0.0.0.0', int $port = 8053, bool $enableTcp = true, int $maxTcpClients = 100) { $this->host = $host; $this->port = $port; $this->enableTcp = $enableTcp; + $this->maxTcpClients = $maxTcpClients; $server = \socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); if (!$server) { @@ -148,6 +153,11 @@ public function start(): void $client = @socket_accept($this->tcpServer); if ($client instanceof Socket) { + if (count($this->tcpClients) >= $this->maxTcpClients) { + @socket_close($client); + continue; + } + if (@socket_set_nonblock($client) === false) { @socket_close($client); continue; From 0508f2b182ce7beb2c26d4455fd7248dc7877b46 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 01:31:40 +0000 Subject: [PATCH 08/19] make tcp secondary in swoole adapter --- src/DNS/Adapter/Swoole.php | 77 ++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/src/DNS/Adapter/Swoole.php b/src/DNS/Adapter/Swoole.php index 74c6377..60d2af9 100644 --- a/src/DNS/Adapter/Swoole.php +++ b/src/DNS/Adapter/Swoole.php @@ -11,7 +11,7 @@ class Swoole extends Adapter { protected Server $server; - protected ?Port $udpPort = null; + protected ?Port $tcpPort = null; /** @var callable(string $buffer, string $ip, int $port): string */ protected mixed $onPacket; @@ -19,21 +19,26 @@ class Swoole extends Adapter protected string $host; protected int $port; - public function __construct(string $host = '0.0.0.0', int $port = 53) + protected bool $enableTcp; + + public function __construct(string $host = '0.0.0.0', int $port = 53, bool $enableTcp = true) { $this->host = $host; $this->port = $port; - $this->server = new Server($this->host, $this->port, SWOOLE_PROCESS, SWOOLE_SOCK_TCP); - - $this->server->set([ - 'open_length_check' => true, - 'package_length_type' => 'n', - 'package_length_offset' => 0, - 'package_body_offset' => 2, - 'package_max_length' => 65537, - ]); - - $this->udpPort = $this->server->addlistener($this->host, $this->port, SWOOLE_SOCK_UDP); + $this->enableTcp = $enableTcp; + $this->server = new Server($this->host, $this->port, SWOOLE_PROCESS, SWOOLE_SOCK_UDP); + + if ($this->enableTcp) { + $this->tcpPort = $this->server->addlistener($this->host, $this->port, SWOOLE_SOCK_TCP); + + $this->tcpPort->set([ + 'open_length_check' => true, + 'package_length_type' => 'n', + 'package_length_offset' => 0, + 'package_body_offset' => 2, + 'package_max_length' => 65537, + ]); + } } /** @@ -56,36 +61,36 @@ public function onPacket(callable $callback): void { $this->onPacket = $callback; - if ($this->udpPort instanceof Port) { - $this->udpPort->on('Packet', function ($server, $data, $clientInfo) { - $ip = $clientInfo['address'] ?? ''; - $port = (int) ($clientInfo['port'] ?? 0); - $answer = \call_user_func($this->onPacket, $data, $ip, $port); + $this->server->on('Packet', function ($server, $data, $clientInfo) { + $ip = $clientInfo['address'] ?? ''; + $port = (int) ($clientInfo['port'] ?? 0); + $answer = \call_user_func($this->onPacket, $data, $ip, $port); + + // Swoole UDP sockets reject zero-length payloads; skip responding instead. + if ($answer === '') { + return; + } + + $server->sendto($ip, $port, $answer); + }); + + if ($this->tcpPort instanceof Port) { + $this->tcpPort->on('Receive', function (Server $server, int $fd, int $reactorId, string $data) { + $info = $server->getClientInfo($fd, $reactorId) ?: []; + $ip = $info['remote_ip'] ?? ''; + $port = $info['remote_port'] ?? 0; + + $payload = substr($data, 2); // strip 2-byte length prefix + $answer = \call_user_func($this->onPacket, $payload, $ip, $port); - // Swoole UDP sockets reject zero-length payloads; skip responding instead. if ($answer === '') { return; } - $server->sendto($ip, $port, $answer); + $frame = pack('n', strlen($answer)) . $answer; + $server->send($fd, $frame); }); } - - $this->server->on('Receive', function (Server $server, int $fd, int $reactorId, string $data) { - $info = $server->getClientInfo($fd, $reactorId) ?: []; - $ip = $info['remote_ip'] ?? ''; - $port = $info['remote_port'] ?? 0; - - $payload = substr($data, 2); // strip 2-byte length prefix - $answer = \call_user_func($this->onPacket, $payload, $ip, $port); - - if ($answer === '') { - return; - } - - $frame = pack('n', strlen($answer)) . $answer; - $server->send($fd, $frame); - }); } /** From e81821c326e948d591d95f6cc4ea496d09f11d58 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 01:45:57 +0000 Subject: [PATCH 09/19] max buffer and framesize for tcp --- src/DNS/Adapter/Native.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/DNS/Adapter/Native.php b/src/DNS/Adapter/Native.php index 590519a..c2b21d5 100644 --- a/src/DNS/Adapter/Native.php +++ b/src/DNS/Adapter/Native.php @@ -31,6 +31,10 @@ class Native extends Adapter protected int $maxTcpClients; + protected int $maxTcpBufferSize = 16384; + + protected int $maxTcpFrameSize = 8192; + /** * @param string $host * @param int $port @@ -207,13 +211,23 @@ protected function handleTcpClient(Socket $client): void return; } + $currentBufferSize = strlen($this->tcpBuffers[$clientId] ?? ''); + $chunkSize = strlen($chunk); + + if ($currentBufferSize + $chunkSize > $this->maxTcpBufferSize) { + printf("TCP buffer size limit exceeded for client %d\n", $clientId); + $this->closeTcpClient($client); + return; + } + $this->tcpBuffers[$clientId] = ($this->tcpBuffers[$clientId] ?? '') . $chunk; while (strlen($this->tcpBuffers[$clientId]) >= 2) { $length = unpack('nlen', substr($this->tcpBuffers[$clientId], 0, 2)); $payloadLength = $length['len'] ?? 0; - if ($payloadLength === 0) { + if ($payloadLength === 0 || $payloadLength > $this->maxTcpFrameSize) { + printf("Invalid TCP frame size %d for client %d\n", $payloadLength, $clientId); $this->closeTcpClient($client); return; } From ec57a42d9626a2cbdadf7265bb1ec30d609c5b40 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 01:50:58 +0000 Subject: [PATCH 10/19] handle truncation --- src/DNS/Adapter/Native.php | 30 ++++++++++++++++++++++++++++++ src/DNS/Adapter/Swoole.php | 30 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/DNS/Adapter/Native.php b/src/DNS/Adapter/Native.php index c2b21d5..99c8cea 100644 --- a/src/DNS/Adapter/Native.php +++ b/src/DNS/Adapter/Native.php @@ -5,6 +5,7 @@ use Exception; use Socket; use Utopia\DNS\Adapter; +use Utopia\DNS\Message; class Native extends Adapter { @@ -35,6 +36,8 @@ class Native extends Adapter protected int $maxTcpFrameSize = 8192; + protected int $maxUdpSize = 512; + /** * @param string $host * @param int $port @@ -145,6 +148,10 @@ public function start(): void continue; } + if (strlen($answer) > $this->maxUdpSize) { + $answer = $this->truncateResponse($answer); + } + if (socket_sendto($this->udpServer, $answer, strlen($answer), 0, $ip, $port) === false) { printf("Error sending UDP response\n"); } @@ -287,4 +294,27 @@ protected function closeTcpClient(Socket $client): void @socket_close($client); } + + protected function truncateResponse(string $encodedResponse): string + { + try { + $message = Message::decode($encodedResponse); + + $truncatedMessage = Message::response( + $message->header, + $message->header->responseCode, + questions: $message->questions, + answers: [], + authority: [], + additional: [], + authoritative: $message->header->authoritative, + truncated: true, + recursionAvailable: $message->header->recursionAvailable + ); + + return $truncatedMessage->encode(); + } catch (\Throwable $e) { + return $encodedResponse; + } + } } diff --git a/src/DNS/Adapter/Swoole.php b/src/DNS/Adapter/Swoole.php index 60d2af9..cb383e3 100644 --- a/src/DNS/Adapter/Swoole.php +++ b/src/DNS/Adapter/Swoole.php @@ -6,6 +6,7 @@ use Utopia\DNS\Adapter; use Swoole\Server; use Swoole\Server\Port; +use Utopia\DNS\Message; class Swoole extends Adapter { @@ -21,6 +22,8 @@ class Swoole extends Adapter protected bool $enableTcp; + protected int $maxUdpSize = 512; + public function __construct(string $host = '0.0.0.0', int $port = 53, bool $enableTcp = true) { $this->host = $host; @@ -71,6 +74,10 @@ public function onPacket(callable $callback): void return; } + if (strlen($answer) > $this->maxUdpSize) { + $answer = $this->truncateResponse($answer); + } + $server->sendto($ip, $port, $answer); }); @@ -111,4 +118,27 @@ public function getName(): string { return 'swoole'; } + + protected function truncateResponse(string $encodedResponse): string + { + try { + $message = Message::decode($encodedResponse); + + $truncatedMessage = Message::response( + $message->header, + $message->header->responseCode, + questions: $message->questions, + answers: [], + authority: [], + additional: [], + authoritative: $message->header->authoritative, + truncated: true, + recursionAvailable: $message->header->recursionAvailable + ); + + return $truncatedMessage->encode(); + } catch (\Throwable $e) { + return $encodedResponse; + } + } } From 95085a62d84c29bbb8443a4861e8d61b6e020415 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 01:53:07 +0000 Subject: [PATCH 11/19] fix correct --- src/DNS/Adapter/Swoole.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DNS/Adapter/Swoole.php b/src/DNS/Adapter/Swoole.php index cb383e3..59c02df 100644 --- a/src/DNS/Adapter/Swoole.php +++ b/src/DNS/Adapter/Swoole.php @@ -32,7 +32,7 @@ public function __construct(string $host = '0.0.0.0', int $port = 53, bool $enab $this->server = new Server($this->host, $this->port, SWOOLE_PROCESS, SWOOLE_SOCK_UDP); if ($this->enableTcp) { - $this->tcpPort = $this->server->addlistener($this->host, $this->port, SWOOLE_SOCK_TCP); + $this->tcpPort = $this->server->addListener($this->host, $this->port, SWOOLE_SOCK_TCP); $this->tcpPort->set([ 'open_length_check' => true, From 54aae66fe321d4f8a4d7a8d03d3c4dd8b0209dc9 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 01:57:37 +0000 Subject: [PATCH 12/19] truncation test --- tests/e2e/DNS/ClientTest.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/e2e/DNS/ClientTest.php b/tests/e2e/DNS/ClientTest.php index 653b46d..afaf57c 100644 --- a/tests/e2e/DNS/ClientTest.php +++ b/tests/e2e/DNS/ClientTest.php @@ -7,6 +7,7 @@ use Utopia\DNS\Message; use Utopia\DNS\Message\Question; use Utopia\DNS\Message\Record; +use Utopia\DNS\Adapter\Native; final class ClientTest extends TestCase { @@ -270,4 +271,34 @@ public function testInvalidServer(): void $this->fail('IPv6 threw unexpected error'); } } + + public function testUdpTruncationSetsTcFlag(): void + { + $native = new Native('127.0.0.1', 0, true); + + $question = new Question('example.com', Record::TYPE_TXT); + $query = Message::query($question, id: 0x1234); + + $answer = new Record('example.com', Record::TYPE_TXT, str_repeat('a', 600), Record::CLASS_IN, 60); + $response = Message::response( + $query->header, + Message::RCODE_NOERROR, + questions: $query->questions, + answers: [$answer], + authority: [], + additional: [] + ); + + $truncate = new \ReflectionMethod($native, 'truncateResponse'); + $truncate->setAccessible(true); + + $truncated = $truncate->invoke($native, $response->encode()); + $decoded = Message::decode($truncated); + + $this->assertTrue($decoded->header->truncated, 'TC flag should be set on truncated UDP response'); + $this->assertCount(0, $decoded->answers, 'Answers should be cleared when truncated'); + $this->assertCount(0, $decoded->authority, 'Authority should be cleared when truncated'); + $this->assertCount(0, $decoded->additional, 'Additional should be cleared when truncated'); + $this->assertSame($query->questions[0]->name, $decoded->questions[0]->name); + } } From c55427091a3b94cf5b1b9a471a4225849b0471ce Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 02:12:38 +0000 Subject: [PATCH 13/19] Fix param order --- tests/e2e/DNS/ClientTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/DNS/ClientTest.php b/tests/e2e/DNS/ClientTest.php index afaf57c..9310efd 100644 --- a/tests/e2e/DNS/ClientTest.php +++ b/tests/e2e/DNS/ClientTest.php @@ -279,7 +279,7 @@ public function testUdpTruncationSetsTcFlag(): void $question = new Question('example.com', Record::TYPE_TXT); $query = Message::query($question, id: 0x1234); - $answer = new Record('example.com', Record::TYPE_TXT, str_repeat('a', 600), Record::CLASS_IN, 60); + $answer = new Record('example.com', Record::TYPE_TXT, Record::CLASS_IN, 60, str_repeat('a', 600)); $response = Message::response( $query->header, Message::RCODE_NOERROR, From 2b31c9e2e11dc0853a81e9bca45c6990e9a1c9ee Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 02:14:46 +0000 Subject: [PATCH 14/19] fix truncation test --- tests/e2e/DNS/ClientTest.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/e2e/DNS/ClientTest.php b/tests/e2e/DNS/ClientTest.php index 9310efd..89a205f 100644 --- a/tests/e2e/DNS/ClientTest.php +++ b/tests/e2e/DNS/ClientTest.php @@ -276,15 +276,19 @@ public function testUdpTruncationSetsTcFlag(): void { $native = new Native('127.0.0.1', 0, true); - $question = new Question('example.com', Record::TYPE_TXT); + $question = new Question('example.com', Record::TYPE_A); $query = Message::query($question, id: 0x1234); - $answer = new Record('example.com', Record::TYPE_TXT, Record::CLASS_IN, 60, str_repeat('a', 600)); + $answers = []; + for ($i = 0; $i < 100; $i++) { + $answers[] = new Record('example.com', Record::TYPE_A, Record::CLASS_IN, 60, '192.168.' . ($i % 256) . '.' . ($i % 256)); + } + $response = Message::response( $query->header, Message::RCODE_NOERROR, questions: $query->questions, - answers: [$answer], + answers: $answers, authority: [], additional: [] ); From 5dd83ce63ceedf9de694b959973853c82f057321 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 11:27:19 +0000 Subject: [PATCH 15/19] review improvements to truncation --- src/DNS/Adapter.php | 4 +- src/DNS/Adapter/Native.php | 52 +++--------------- src/DNS/Adapter/Swoole.php | 59 ++++---------------- src/DNS/Message.php | 19 ++++++- src/DNS/Server.php | 6 +- tests/e2e/DNS/ClientTest.php | 56 +++++++++---------- tests/resources/zone-valid-localhost.txt | 10 +++- tests/unit/DNS/MessageTest.php | 70 ++++++++++++++++++++++++ 8 files changed, 146 insertions(+), 130 deletions(-) diff --git a/src/DNS/Adapter.php b/src/DNS/Adapter.php index f0199c5..86e5eb0 100644 --- a/src/DNS/Adapter.php +++ b/src/DNS/Adapter.php @@ -15,8 +15,8 @@ abstract public function onWorkerStart(callable $callback): void; /** * Packet handler * - * @param callable(string $buffer, string $ip, int $port): string $callback - * @phpstan-param callable(string $buffer, string $ip, int $port):string $callback + * @param callable(string $buffer, string $ip, int $port, ?int $maxResponseSize): string $callback + * @phpstan-param callable(string $buffer, string $ip, int $port, ?int $maxResponseSize):string $callback */ abstract public function onPacket(callable $callback): void; diff --git a/src/DNS/Adapter/Native.php b/src/DNS/Adapter/Native.php index 99c8cea..5c6b45f 100644 --- a/src/DNS/Adapter/Native.php +++ b/src/DNS/Adapter/Native.php @@ -5,7 +5,6 @@ use Exception; use Socket; use Utopia\DNS\Adapter; -use Utopia\DNS\Message; class Native extends Adapter { @@ -19,7 +18,7 @@ class Native extends Adapter /** @var array */ protected array $tcpBuffers = []; - /** @var callable(string $buffer, string $ip, int $port): string */ + /** @var callable(string $buffer, string $ip, int $port, ?int $maxResponseSize): string */ protected mixed $onPacket; /** @var list */ @@ -36,8 +35,6 @@ class Native extends Adapter protected int $maxTcpFrameSize = 8192; - protected int $maxUdpSize = 512; - /** * @param string $host * @param int $port @@ -81,7 +78,7 @@ public function onWorkerStart(callable $callback): void /** * @param callable $callback - * @phpstan-param callable(string $buffer, string $ip, int $port):string $callback + * @phpstan-param callable(string $buffer, string $ip, int $port, ?int $maxResponseSize):string $callback */ public function onPacket(callable $callback): void { @@ -142,18 +139,10 @@ public function start(): void $len = socket_recvfrom($this->udpServer, $buf, 1024 * 4, 0, $ip, $port); if ($len > 0) { - $answer = call_user_func($this->onPacket, $buf, $ip, $port); - - if ($answer === '') { - continue; - } - - if (strlen($answer) > $this->maxUdpSize) { - $answer = $this->truncateResponse($answer); - } + $answer = call_user_func($this->onPacket, $buf, $ip, $port, 512); - if (socket_sendto($this->udpServer, $answer, strlen($answer), 0, $ip, $port) === false) { - printf("Error sending UDP response\n"); + if ($answer !== '') { + socket_sendto($this->udpServer, $answer, strlen($answer), 0, $ip, $port); } } @@ -250,13 +239,11 @@ protected function handleTcpClient(Socket $client): void $port = 0; socket_getpeername($client, $ip, $port); - $answer = call_user_func($this->onPacket, $message, $ip, $port); + $answer = call_user_func($this->onPacket, $message, $ip, $port, null); - if ($answer === '') { - continue; + if ($answer !== '') { + $this->sendTcpResponse($client, $answer); } - - $this->sendTcpResponse($client, $answer); } } @@ -294,27 +281,4 @@ protected function closeTcpClient(Socket $client): void @socket_close($client); } - - protected function truncateResponse(string $encodedResponse): string - { - try { - $message = Message::decode($encodedResponse); - - $truncatedMessage = Message::response( - $message->header, - $message->header->responseCode, - questions: $message->questions, - answers: [], - authority: [], - additional: [], - authoritative: $message->header->authoritative, - truncated: true, - recursionAvailable: $message->header->recursionAvailable - ); - - return $truncatedMessage->encode(); - } catch (\Throwable $e) { - return $encodedResponse; - } - } } diff --git a/src/DNS/Adapter/Swoole.php b/src/DNS/Adapter/Swoole.php index 59c02df..3f43efd 100644 --- a/src/DNS/Adapter/Swoole.php +++ b/src/DNS/Adapter/Swoole.php @@ -6,7 +6,6 @@ use Utopia\DNS\Adapter; use Swoole\Server; use Swoole\Server\Port; -use Utopia\DNS\Message; class Swoole extends Adapter { @@ -14,7 +13,7 @@ class Swoole extends Adapter protected ?Port $tcpPort = null; - /** @var callable(string $buffer, string $ip, int $port): string */ + /** @var callable(string $buffer, string $ip, int $port, ?int $maxResponseSize): string */ protected mixed $onPacket; protected string $host; @@ -22,8 +21,6 @@ class Swoole extends Adapter protected bool $enableTcp; - protected int $maxUdpSize = 512; - public function __construct(string $host = '0.0.0.0', int $port = 53, bool $enableTcp = true) { $this->host = $host; @@ -58,44 +55,31 @@ public function onWorkerStart(callable $callback): void /** * @param callable $callback - * @phpstan-param callable(string $buffer, string $ip, int $port):string $callback + * @phpstan-param callable(string $buffer, string $ip, int $port, ?int $maxResponseSize):string $callback */ public function onPacket(callable $callback): void { $this->onPacket = $callback; + // UDP handler - enforces 512-byte limit per RFC 1035 $this->server->on('Packet', function ($server, $data, $clientInfo) { - $ip = $clientInfo['address'] ?? ''; - $port = (int) ($clientInfo['port'] ?? 0); - $answer = \call_user_func($this->onPacket, $data, $ip, $port); - - // Swoole UDP sockets reject zero-length payloads; skip responding instead. - if ($answer === '') { - return; - } + $response = \call_user_func($this->onPacket, $data, $clientInfo['address'] ?? '', (int) ($clientInfo['port'] ?? 0), 512); - if (strlen($answer) > $this->maxUdpSize) { - $answer = $this->truncateResponse($answer); + if ($response !== '') { + $server->sendto($clientInfo['address'], $clientInfo['port'], $response); } - - $server->sendto($ip, $port, $answer); }); + // TCP handler - supports larger responses with length-prefixed framing per RFC 5966 if ($this->tcpPort instanceof Port) { $this->tcpPort->on('Receive', function (Server $server, int $fd, int $reactorId, string $data) { $info = $server->getClientInfo($fd, $reactorId) ?: []; - $ip = $info['remote_ip'] ?? ''; - $port = $info['remote_port'] ?? 0; - $payload = substr($data, 2); // strip 2-byte length prefix - $answer = \call_user_func($this->onPacket, $payload, $ip, $port); + $response = \call_user_func($this->onPacket, $payload, $info['remote_ip'] ?? '', $info['remote_port'] ?? 0, null); - if ($answer === '') { - return; + if ($response !== '') { + $server->send($fd, pack('n', strlen($response)) . $response); } - - $frame = pack('n', strlen($answer)) . $answer; - $server->send($fd, $frame); }); } } @@ -118,27 +102,4 @@ public function getName(): string { return 'swoole'; } - - protected function truncateResponse(string $encodedResponse): string - { - try { - $message = Message::decode($encodedResponse); - - $truncatedMessage = Message::response( - $message->header, - $message->header->responseCode, - questions: $message->questions, - answers: [], - authority: [], - additional: [], - authoritative: $message->header->authoritative, - truncated: true, - recursionAvailable: $message->header->recursionAvailable - ); - - return $truncatedMessage->encode(); - } catch (\Throwable $e) { - return $encodedResponse; - } - } } diff --git a/src/DNS/Message.php b/src/DNS/Message.php index 0d74123..26febee 100644 --- a/src/DNS/Message.php +++ b/src/DNS/Message.php @@ -183,7 +183,7 @@ public static function decode(string $packet): self return new self($header, $questions, $answers, $authority, $additional); } - public function encode(): string + public function encode(?int $maxSize = null): string { $packet = $this->header->encode(); @@ -203,6 +203,23 @@ public function encode(): string $packet .= $additional->encode($packet); } + // Apply truncation if size limit is set and exceeded + if ($maxSize !== null && strlen($packet) > $maxSize) { + $truncated = self::response( + $this->header, + $this->header->responseCode, + questions: $this->questions, + answers: [], + authority: [], + additional: [], + authoritative: $this->header->authoritative, + truncated: true, + recursionAvailable: $this->header->recursionAvailable + ); + + return $truncated->encode(); + } + return $packet; } } diff --git a/src/DNS/Server.php b/src/DNS/Server.php index 16f60aa..6d6fe60 100644 --- a/src/DNS/Server.php +++ b/src/DNS/Server.php @@ -165,10 +165,11 @@ protected function handleError(Throwable $error): void * @param string $buffer * @param string $ip * @param int $port + * @param int|null $maxResponseSize * * @return string */ - protected function onPacket(string $buffer, string $ip, int $port): string + protected function onPacket(string $buffer, string $ip, int $port, ?int $maxResponseSize = null): string { $startTime = microtime(true); Console::info("[PACKET] Received packet of " . strlen($buffer) . " bytes from $ip:$port"); @@ -252,7 +253,7 @@ protected function onPacket(string $buffer, string $ip, int $port): string // 3. Encode response $encodeStart = microtime(true); try { - return $response->encode(); + return $response->encode($maxResponseSize); } catch (Throwable $e) { Console::error("[ERROR] Failed to encode message: " . $e->getMessage()); Console::error("[ERROR] Packet dump: " . bin2hex($buffer)); @@ -297,7 +298,6 @@ public function start(): void Console::success('[DNS] Server is ready to accept connections'); - /** @phpstan-var \Closure(string $buffer, string $ip, int $port):string $onPacket */ $onPacket = $this->onPacket(...); $this->adapter->onPacket($onPacket); $this->adapter->start(); diff --git a/tests/e2e/DNS/ClientTest.php b/tests/e2e/DNS/ClientTest.php index 89a205f..800fe9b 100644 --- a/tests/e2e/DNS/ClientTest.php +++ b/tests/e2e/DNS/ClientTest.php @@ -7,7 +7,6 @@ use Utopia\DNS\Message; use Utopia\DNS\Message\Question; use Utopia\DNS\Message\Record; -use Utopia\DNS\Adapter\Native; final class ClientTest extends TestCase { @@ -272,37 +271,34 @@ public function testInvalidServer(): void } } - public function testUdpTruncationSetsTcFlag(): void + public function testTcpFallbackAfterUdpTruncation(): void { - $native = new Native('127.0.0.1', 0, true); - - $question = new Question('example.com', Record::TYPE_A); - $query = Message::query($question, id: 0x1234); + $client = new Client('127.0.0.1', self::PORT); - $answers = []; - for ($i = 0; $i < 100; $i++) { - $answers[] = new Record('example.com', Record::TYPE_A, Record::CLASS_IN, 60, '192.168.' . ($i % 256) . '.' . ($i % 256)); + // Query for a TXT record that has large response (would trigger truncation over UDP) + $question = new Question('large.localhost', Record::TYPE_TXT); + $query = Message::query($question); + + // First try UDP - should get TC flag if response is large + $udpResponse = $client->query($query); + + // If truncated, retry with TCP + if ($udpResponse->header->truncated) { + $tcpClient = new Client('127.0.0.1', self::PORT, useTcp: true); + $tcpResponse = $tcpClient->query($query); + + // TCP response should not be truncated + $this->assertFalse($tcpResponse->header->truncated, 'TCP response should not be truncated'); + + // TCP response should have more data than UDP + $this->assertGreaterThan( + count($udpResponse->answers), + count($tcpResponse->answers), + 'TCP should return more answers than truncated UDP' + ); + } else { + // If not truncated, that's fine - the response fit in UDP + $this->addToAssertionCount(1); } - - $response = Message::response( - $query->header, - Message::RCODE_NOERROR, - questions: $query->questions, - answers: $answers, - authority: [], - additional: [] - ); - - $truncate = new \ReflectionMethod($native, 'truncateResponse'); - $truncate->setAccessible(true); - - $truncated = $truncate->invoke($native, $response->encode()); - $decoded = Message::decode($truncated); - - $this->assertTrue($decoded->header->truncated, 'TC flag should be set on truncated UDP response'); - $this->assertCount(0, $decoded->answers, 'Answers should be cleared when truncated'); - $this->assertCount(0, $decoded->authority, 'Authority should be cleared when truncated'); - $this->assertCount(0, $decoded->additional, 'Additional should be cleared when truncated'); - $this->assertSame($query->questions[0]->name, $decoded->questions[0]->name); } } diff --git a/tests/resources/zone-valid-localhost.txt b/tests/resources/zone-valid-localhost.txt index cc0309f..3fd3e07 100644 --- a/tests/resources/zone-valid-localhost.txt +++ b/tests/resources/zone-valid-localhost.txt @@ -8,4 +8,12 @@ $ORIGIN localhost. ) @ 86400 IN NS @ @ 86400 IN A 127.0.0.1 -@ 86400 IN AAAA ::1 \ No newline at end of file +@ 86400 IN AAAA ::1 +large 300 IN TXT "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" +large 300 IN TXT "Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat" +large 300 IN TXT "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur" +large 300 IN TXT "Excepteur sint occaecat cupidatat non proident sunt in culpa qui officia deserunt mollit anim id est laborum" +large 300 IN TXT "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium totam rem aperiam" +large 300 IN TXT "Eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo" +large 300 IN TXT "Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit sed quia consequuntur magni dolores" +large 300 IN TXT "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet consectetur adipisci velit" \ No newline at end of file diff --git a/tests/unit/DNS/MessageTest.php b/tests/unit/DNS/MessageTest.php index 07af773..e98ba91 100644 --- a/tests/unit/DNS/MessageTest.php +++ b/tests/unit/DNS/MessageTest.php @@ -290,4 +290,74 @@ public function testDecodeNxDomainWithAuthority(): void $this->assertSame(900, $soa->ttl); $this->assertSame('ns1.example.com hostmaster.example.com 1 3600 900 604800 300', $soa->rdata); } + + public function testEncodeTruncatesWhenExceedingMaxSize(): void + { + $question = new Question('example.com', Record::TYPE_A); + $query = Message::query($question, id: 0x1234); + + // Create a response with many answers that will exceed 512 bytes + $answers = []; + for ($i = 0; $i < 100; $i++) { + $answers[] = new Record('example.com', Record::TYPE_A, Record::CLASS_IN, 60, '192.168.' . ($i % 256) . '.' . ($i % 256)); + } + + $response = Message::response( + $query->header, + Message::RCODE_NOERROR, + questions: $query->questions, + answers: $answers, + authority: [], + additional: [] + ); + + // Encode with 512-byte limit (UDP max per RFC 1035) + $truncated = $response->encode(512); + $decoded = Message::decode($truncated); + + // Verify TC flag is set + $this->assertTrue($decoded->header->truncated, 'TC flag should be set on truncated response'); + + // Verify sections are cleared when truncated + $this->assertCount(0, $decoded->answers, 'Answers should be cleared when truncated'); + $this->assertCount(0, $decoded->authority, 'Authority should be cleared when truncated'); + $this->assertCount(0, $decoded->additional, 'Additional should be cleared when truncated'); + + // Verify question is preserved + $this->assertCount(1, $decoded->questions); + $this->assertSame($query->questions[0]->name, $decoded->questions[0]->name); + + // Verify truncated packet is within size limit + $this->assertLessThanOrEqual(512, strlen($truncated)); + } + + public function testEncodeWithoutMaxSizeDoesNotTruncate(): void + { + $question = new Question('example.com', Record::TYPE_A); + $query = Message::query($question, id: 0x1234); + + $answers = []; + for ($i = 0; $i < 5; $i++) { + $answers[] = new Record('example.com', Record::TYPE_A, Record::CLASS_IN, 60, '192.168.1.' . $i); + } + + $response = Message::response( + $query->header, + Message::RCODE_NOERROR, + questions: $query->questions, + answers: $answers, + authority: [], + additional: [] + ); + + // Encode without size limit + $encoded = $response->encode(); + $decoded = Message::decode($encoded); + + // Verify TC flag is NOT set + $this->assertFalse($decoded->header->truncated, 'TC flag should not be set on non-truncated response'); + + // Verify all answers are preserved + $this->assertCount(5, $decoded->answers); + } } From b2d0515abf5804086c5f2d36886d532bad433525 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 11:47:44 +0000 Subject: [PATCH 16/19] Fix phpstan issues --- composer.json | 2 +- src/DNS/Adapter/Native.php | 17 +++++++------ src/DNS/Adapter/Swoole.php | 49 ++++++++++++++++++++++++++------------ src/DNS/Client.php | 15 +++++++++--- 4 files changed, 57 insertions(+), 26 deletions(-) diff --git a/composer.json b/composer.json index 9ba2d5b..2f2a37d 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "scripts": { "lint": "./vendor/bin/pint --test --config pint.json", "format": "./vendor/bin/pint --config pint.json", - "check": "./vendor/bin/phpstan analyse --level 8 -c phpstan.neon src tests", + "check": "./vendor/bin/phpstan analyse --level max -c phpstan.neon src tests", "test": "./vendor/bin/phpunit --configuration phpunit.xml" }, "authors": [ diff --git a/src/DNS/Adapter/Native.php b/src/DNS/Adapter/Native.php index 5c6b45f..8dca45d 100644 --- a/src/DNS/Adapter/Native.php +++ b/src/DNS/Adapter/Native.php @@ -135,10 +135,10 @@ public function start(): void if ($socket === $this->udpServer) { $buf = ''; $ip = ''; - $port = null; + $port = 0; $len = socket_recvfrom($this->udpServer, $buf, 1024 * 4, 0, $ip, $port); - if ($len > 0) { + if ($len > 0 && is_string($buf) && is_string($ip) && is_int($port)) { $answer = call_user_func($this->onPacket, $buf, $ip, $port, 512); if ($answer !== '') { @@ -220,9 +220,10 @@ protected function handleTcpClient(Socket $client): void while (strlen($this->tcpBuffers[$clientId]) >= 2) { $length = unpack('nlen', substr($this->tcpBuffers[$clientId], 0, 2)); - $payloadLength = $length['len'] ?? 0; + $unpacked = unpack('n', substr($this->tcpBuffers[$clientId], 0, 2)); + $payloadLength = is_array($unpacked) ? ($unpacked[1] ?? 0) : 0; - if ($payloadLength === 0 || $payloadLength > $this->maxTcpFrameSize) { + if ($payloadLength > $this->maxTcpFrameSize) { printf("Invalid TCP frame size %d for client %d\n", $payloadLength, $clientId); $this->closeTcpClient($client); return; @@ -239,10 +240,12 @@ protected function handleTcpClient(Socket $client): void $port = 0; socket_getpeername($client, $ip, $port); - $answer = call_user_func($this->onPacket, $message, $ip, $port, null); + if (is_string($ip) && is_int($port)) { + $answer = call_user_func($this->onPacket, $message, $ip, $port, null); - if ($answer !== '') { - $this->sendTcpResponse($client, $answer); + if ($answer !== '') { + $this->sendTcpResponse($client, $answer); + } } } } diff --git a/src/DNS/Adapter/Swoole.php b/src/DNS/Adapter/Swoole.php index 3f43efd..cb9eb79 100644 --- a/src/DNS/Adapter/Swoole.php +++ b/src/DNS/Adapter/Swoole.php @@ -29,15 +29,18 @@ public function __construct(string $host = '0.0.0.0', int $port = 53, bool $enab $this->server = new Server($this->host, $this->port, SWOOLE_PROCESS, SWOOLE_SOCK_UDP); if ($this->enableTcp) { - $this->tcpPort = $this->server->addListener($this->host, $this->port, SWOOLE_SOCK_TCP); - - $this->tcpPort->set([ - 'open_length_check' => true, - 'package_length_type' => 'n', - 'package_length_offset' => 0, - 'package_body_offset' => 2, - 'package_max_length' => 65537, - ]); + $port = $this->server->addListener($this->host, $this->port, SWOOLE_SOCK_TCP); + + if ($port instanceof Port) { + $this->tcpPort = $port; + $this->tcpPort->set([ + 'open_length_check' => true, + 'package_length_type' => 'n', + 'package_length_offset' => 0, + 'package_body_offset' => 2, + 'package_max_length' => 65537, + ]); + } } } @@ -49,7 +52,9 @@ public function __construct(string $host = '0.0.0.0', int $port = 53, bool $enab public function onWorkerStart(callable $callback): void { $this->server->on('WorkerStart', function ($server, $workerId) use ($callback) { - \call_user_func($callback, $workerId); + if (is_int($workerId)) { + \call_user_func($callback, $workerId); + } }); } @@ -63,19 +68,33 @@ public function onPacket(callable $callback): void // UDP handler - enforces 512-byte limit per RFC 1035 $this->server->on('Packet', function ($server, $data, $clientInfo) { - $response = \call_user_func($this->onPacket, $data, $clientInfo['address'] ?? '', (int) ($clientInfo['port'] ?? 0), 512); + if (!is_string($data) || !is_array($clientInfo)) { + return; + } + + $ip = is_string($clientInfo['address'] ?? null) ? $clientInfo['address'] : ''; + $port = is_int($clientInfo['port'] ?? null) ? $clientInfo['port'] : 0; - if ($response !== '') { - $server->sendto($clientInfo['address'], $clientInfo['port'], $response); + $response = \call_user_func($this->onPacket, $data, $ip, $port, 512); + + if ($response !== '' && $server instanceof Server) { + $server->sendto($ip, $port, $response); } }); // TCP handler - supports larger responses with length-prefixed framing per RFC 5966 if ($this->tcpPort instanceof Port) { $this->tcpPort->on('Receive', function (Server $server, int $fd, int $reactorId, string $data) { - $info = $server->getClientInfo($fd, $reactorId) ?: []; + $info = $server->getClientInfo($fd, $reactorId); + if (!is_array($info)) { + return; + } + $payload = substr($data, 2); // strip 2-byte length prefix - $response = \call_user_func($this->onPacket, $payload, $info['remote_ip'] ?? '', $info['remote_port'] ?? 0, null); + $ip = is_string($info['remote_ip'] ?? null) ? $info['remote_ip'] : ''; + $port = is_int($info['remote_port'] ?? null) ? $info['remote_port'] : 0; + + $response = \call_user_func($this->onPacket, $payload, $ip, $port, null); if ($response !== '') { $server->send($fd, pack('n', strlen($response)) . $response); diff --git a/src/DNS/Client.php b/src/DNS/Client.php index fa70679..6118bca 100644 --- a/src/DNS/Client.php +++ b/src/DNS/Client.php @@ -75,7 +75,7 @@ public function query(Message $message): Message throw new Exception("Failed to receive data from $this->server: $errorMessage (Error code: $error)"); } - if (empty($data)) { + if (empty($data) || !is_string($data)) { throw new Exception("Empty response received from $this->server:$this->port"); } @@ -87,10 +87,14 @@ protected function queryTcp(Message $message): Message $targetHost = $this->formatTcpHost($this->server); $uri = "tcp://{$targetHost}:{$this->port}"; + $errno = 0; + $errstr = ''; $socket = @stream_socket_client($uri, $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT); if ($socket === false) { - throw new Exception("Failed to connect to {$this->server}:{$this->port} over TCP: $errstr ($errno)"); + $errCode = is_int($errno) ? $errno : 0; + $errMsg = is_string($errstr) ? $errstr : 'Unknown error'; + throw new Exception("Failed to connect to {$this->server}:{$this->port} over TCP: $errMsg ($errCode)"); } try { @@ -111,7 +115,8 @@ protected function queryTcp(Message $message): Message throw new Exception('Failed to read DNS TCP length prefix.'); } - $length = unpack('nlen', $lengthBytes)['len'] ?? 0; + $unpacked = unpack('nlen', $lengthBytes); + $length = is_array($unpacked) && isset($unpacked['len']) ? (int) $unpacked['len'] : 0; if ($length === 0) { throw new Exception('Received empty DNS TCP response.'); @@ -142,6 +147,10 @@ protected function decodeResponse(Message $query, string $payload): Message protected function readBytes(mixed $socket, int $length): string { + if (!is_resource($socket)) { + return ''; + } + $data = ''; while (strlen($data) < $length) { From e0df2385e8e1b0e3aa175ae7a9286e6dc0815472 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 12:01:10 +0000 Subject: [PATCH 17/19] Fix codeql --- composer.json | 2 +- src/DNS/Adapter/Native.php | 31 ++++++++----------------------- src/DNS/Adapter/Swoole.php | 15 +++++---------- src/DNS/Client.php | 23 ++++++++--------------- 4 files changed, 22 insertions(+), 49 deletions(-) diff --git a/composer.json b/composer.json index 2f2a37d..9ba2d5b 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "scripts": { "lint": "./vendor/bin/pint --test --config pint.json", "format": "./vendor/bin/pint --config pint.json", - "check": "./vendor/bin/phpstan analyse --level max -c phpstan.neon src tests", + "check": "./vendor/bin/phpstan analyse --level 8 -c phpstan.neon src tests", "test": "./vendor/bin/phpunit --configuration phpunit.xml" }, "authors": [ diff --git a/src/DNS/Adapter/Native.php b/src/DNS/Adapter/Native.php index 8dca45d..addf45a 100644 --- a/src/DNS/Adapter/Native.php +++ b/src/DNS/Adapter/Native.php @@ -24,29 +24,14 @@ class Native extends Adapter /** @var list */ protected array $onWorkerStart = []; - protected string $host; - protected int $port; - - protected bool $enableTcp; - - protected int $maxTcpClients; - - protected int $maxTcpBufferSize = 16384; - - protected int $maxTcpFrameSize = 8192; - - /** - * @param string $host - * @param int $port - * @param bool $enableTcp - * @param int $maxTcpClients - */ - public function __construct(string $host = '0.0.0.0', int $port = 8053, bool $enableTcp = true, int $maxTcpClients = 100) - { - $this->host = $host; - $this->port = $port; - $this->enableTcp = $enableTcp; - $this->maxTcpClients = $maxTcpClients; + public function __construct( + protected string $host = '0.0.0.0', + protected int $port = 8053, + protected bool $enableTcp = true, + protected int $maxTcpClients = 100, + protected int $maxTcpBufferSize = 16384, + protected int $maxTcpFrameSize = 8192 + ) { $server = \socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); if (!$server) { diff --git a/src/DNS/Adapter/Swoole.php b/src/DNS/Adapter/Swoole.php index cb9eb79..dd940f9 100644 --- a/src/DNS/Adapter/Swoole.php +++ b/src/DNS/Adapter/Swoole.php @@ -16,16 +16,11 @@ class Swoole extends Adapter /** @var callable(string $buffer, string $ip, int $port, ?int $maxResponseSize): string */ protected mixed $onPacket; - protected string $host; - protected int $port; - - protected bool $enableTcp; - - public function __construct(string $host = '0.0.0.0', int $port = 53, bool $enableTcp = true) - { - $this->host = $host; - $this->port = $port; - $this->enableTcp = $enableTcp; + public function __construct( + protected string $host = '0.0.0.0', + protected int $port = 53, + protected bool $enableTcp = true + ) { $this->server = new Server($this->host, $this->port, SWOOLE_PROCESS, SWOOLE_SOCK_UDP); if ($this->enableTcp) { diff --git a/src/DNS/Client.php b/src/DNS/Client.php index 6118bca..7d8961f 100644 --- a/src/DNS/Client.php +++ b/src/DNS/Client.php @@ -7,26 +7,19 @@ class Client { - /** @var \Socket|null */ - protected $socket = null; - protected string $server; - protected int $port; - protected int $timeout; - - protected bool $useTcp; - - public function __construct(string $server = '127.0.0.1', int $port = 53, int $timeout = 5, bool $useTcp = false) - { + public function __construct( + protected string $server = '127.0.0.1', + protected int $port = 53, + protected int $timeout = 5, + protected bool $useTcp = false, + /** @var \Socket|null */ + protected ?\Socket $socket = null + ) { $validator = new IP(IP::ALL); // IPv4 + IPv6 if (!$validator->isValid($server)) { throw new Exception('Server must be an IP address.'); } - $this->server = $server; - $this->port = $port; - $this->timeout = $timeout; - $this->useTcp = $useTcp; - if ($this->useTcp) { return; } From 791d32d01fa57a4657b8f6eb7dce0468586f55ca Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 12:01:44 +0000 Subject: [PATCH 18/19] Fix phpstan level max --- src/DNS/Adapter/Native.php | 3 +-- src/DNS/Client.php | 2 +- src/DNS/Message/Header.php | 25 ++++++++++++++++++------ src/DNS/Message/Question.php | 4 ++-- src/DNS/Message/Record.php | 37 +++++++++++++++++++++++------------- 5 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/DNS/Adapter/Native.php b/src/DNS/Adapter/Native.php index addf45a..535208e 100644 --- a/src/DNS/Adapter/Native.php +++ b/src/DNS/Adapter/Native.php @@ -204,9 +204,8 @@ protected function handleTcpClient(Socket $client): void $this->tcpBuffers[$clientId] = ($this->tcpBuffers[$clientId] ?? '') . $chunk; while (strlen($this->tcpBuffers[$clientId]) >= 2) { - $length = unpack('nlen', substr($this->tcpBuffers[$clientId], 0, 2)); $unpacked = unpack('n', substr($this->tcpBuffers[$clientId], 0, 2)); - $payloadLength = is_array($unpacked) ? ($unpacked[1] ?? 0) : 0; + $payloadLength = (is_array($unpacked) && array_key_exists(1, $unpacked) && is_int($unpacked[1])) ? $unpacked[1] : 0; if ($payloadLength > $this->maxTcpFrameSize) { printf("Invalid TCP frame size %d for client %d\n", $payloadLength, $clientId); diff --git a/src/DNS/Client.php b/src/DNS/Client.php index 7d8961f..df7edbb 100644 --- a/src/DNS/Client.php +++ b/src/DNS/Client.php @@ -109,7 +109,7 @@ protected function queryTcp(Message $message): Message } $unpacked = unpack('nlen', $lengthBytes); - $length = is_array($unpacked) && isset($unpacked['len']) ? (int) $unpacked['len'] : 0; + $length = (is_array($unpacked) && isset($unpacked['len']) && is_int($unpacked['len'])) ? $unpacked['len'] : 0; if ($length === 0) { throw new Exception('Received empty DNS TCP response.'); diff --git a/src/DNS/Message/Header.php b/src/DNS/Message/Header.php index 66a4220..a60d293 100644 --- a/src/DNS/Message/Header.php +++ b/src/DNS/Message/Header.php @@ -39,14 +39,27 @@ public static function decode(string $data, int $offset = 0): self $chunk = substr($data, $offset, self::LENGTH); $values = unpack('nid/nflags/nqdcount/nancount/nnscount/narcount', $chunk); - if (!is_array($values)) { + if (!is_array($values) + || !isset($values['id'], $values['flags'], $values['qdcount'], $values['ancount'], $values['nscount'], $values['arcount']) + || !is_int($values['id']) + || !is_int($values['flags']) + || !is_int($values['qdcount']) + || !is_int($values['ancount']) + || !is_int($values['nscount']) + || !is_int($values['arcount']) + ) { throw new DecodingException('Failed to unpack DNS header'); } + $id = $values['id']; $flags = $values['flags']; + $qdcount = $values['qdcount']; + $ancount = $values['ancount']; + $nscount = $values['nscount']; + $arcount = $values['arcount']; return new self( - id: $values['id'], + id: $id, isResponse: (bool) (($flags >> 15) & 0x1), opcode: ($flags >> 11) & 0xF, authoritative: (bool) (($flags >> 10) & 0x1), @@ -54,10 +67,10 @@ public static function decode(string $data, int $offset = 0): self recursionDesired: (bool) (($flags >> 8) & 0x1), recursionAvailable: (bool) (($flags >> 7) & 0x1), responseCode: $flags & 0xF, - questionCount: $values['qdcount'], - answerCount: $values['ancount'], - authorityCount: $values['nscount'], - additionalCount: $values['arcount'] + questionCount: $qdcount, + answerCount: $ancount, + authorityCount: $nscount, + additionalCount: $arcount ); } diff --git a/src/DNS/Message/Question.php b/src/DNS/Message/Question.php index e928067..6cdfff1 100644 --- a/src/DNS/Message/Question.php +++ b/src/DNS/Message/Question.php @@ -29,14 +29,14 @@ public static function decode(string $data, int &$offset = 0): self } $typeData = unpack('ntype', substr($data, $offset, 2)); - if (!is_array($typeData) || !array_key_exists('type', $typeData)) { + if (!is_array($typeData) || !array_key_exists('type', $typeData) || !is_int($typeData['type'])) { throw new DecodingException('Failed to unpack question type'); } $type = $typeData['type']; $offset += 2; $classData = unpack('nclass', substr($data, $offset, 2)); - if (!is_array($classData) || !array_key_exists('class', $classData)) { + if (!is_array($classData) || !array_key_exists('class', $classData) || !is_int($classData['class'])) { throw new DecodingException('Failed to unpack question class'); } $class = $classData['class']; diff --git a/src/DNS/Message/Record.php b/src/DNS/Message/Record.php index 650ab4e..c869329 100644 --- a/src/DNS/Message/Record.php +++ b/src/DNS/Message/Record.php @@ -144,28 +144,28 @@ public static function decode(string $data, int &$offset): self throw new DecodingException('Truncated RR header'); } $typeData = unpack('ntype', substr($data, $offset, 2)); - if (!is_array($typeData) || !array_key_exists('type', $typeData)) { + if (!is_array($typeData) || !array_key_exists('type', $typeData) || !is_int($typeData['type'])) { throw new DecodingException('Failed to unpack record type'); } $type = $typeData['type']; $offset += 2; $classData = unpack('nclass', substr($data, $offset, 2)); - if (!is_array($classData) || !array_key_exists('class', $classData)) { + if (!is_array($classData) || !array_key_exists('class', $classData) || !is_int($classData['class'])) { throw new DecodingException('Failed to unpack record class'); } $class = $classData['class']; $offset += 2; $ttlData = unpack('Nttl', substr($data, $offset, 4)); - if (!is_array($ttlData) || !array_key_exists('ttl', $ttlData)) { + if (!is_array($ttlData) || !array_key_exists('ttl', $ttlData) || !is_int($ttlData['ttl'])) { throw new DecodingException('Failed to unpack record TTL'); } $ttl = $ttlData['ttl']; $offset += 4; $rdLengthData = unpack('nlength', substr($data, $offset, 2)); - if (!is_array($rdLengthData) || !array_key_exists('length', $rdLengthData)) { + if (!is_array($rdLengthData) || !array_key_exists('length', $rdLengthData) || !is_int($rdLengthData['length'])) { throw new DecodingException('Failed to unpack record length'); } $rdlength = $rdLengthData['length']; @@ -216,7 +216,7 @@ public static function decode(string $data, int &$offset): self throw new DecodingException('Invalid MX RDATA length: ' . strlen($rdataRaw)); } $priorityData = unpack('npriority', substr($rdataRaw, 0, 2)); - if (!is_array($priorityData) || !array_key_exists('priority', $priorityData)) { + if (!is_array($priorityData) || !array_key_exists('priority', $priorityData) || !is_int($priorityData['priority'])) { throw new DecodingException('Failed to unpack MX priority'); } $priority = $priorityData['priority']; @@ -231,13 +231,13 @@ public static function decode(string $data, int &$offset): self $priorityData = unpack('npriority', substr($rdataRaw, 0, 2)); $weightData = unpack('nweight', substr($rdataRaw, 2, 2)); $portData = unpack('nport', substr($rdataRaw, 4, 2)); - if (!is_array($priorityData) || !array_key_exists('priority', $priorityData)) { + if (!is_array($priorityData) || !array_key_exists('priority', $priorityData) || !is_int($priorityData['priority'])) { throw new DecodingException('Failed to unpack SRV priority'); } - if (!is_array($weightData) || !array_key_exists('weight', $weightData)) { + if (!is_array($weightData) || !array_key_exists('weight', $weightData) || !is_int($weightData['weight'])) { throw new DecodingException('Failed to unpack SRV weight'); } - if (!is_array($portData) || !array_key_exists('port', $portData)) { + if (!is_array($portData) || !array_key_exists('port', $portData) || !is_int($portData['port'])) { throw new DecodingException('Failed to unpack SRV port'); } $priority = $priorityData['priority']; @@ -258,12 +258,23 @@ public static function decode(string $data, int &$offset): self } $fields = unpack('Nserial/Nrefresh/Nretry/Nexpire/Nminimum', $timingData); - if (!is_array($fields)) { + if (!is_array($fields) + || !isset($fields['serial'], $fields['refresh'], $fields['retry'], $fields['expire'], $fields['minimum']) + || !is_int($fields['serial']) + || !is_int($fields['refresh']) + || !is_int($fields['retry']) + || !is_int($fields['expire']) + || !is_int($fields['minimum']) + ) { throw new DecodingException('Unable to unpack SOA timings'); } // Convert signed to unsigned for serial $serial = $fields['serial']; + $refresh = $fields['refresh']; + $retry = $fields['retry']; + $expire = $fields['expire']; + $minimum = $fields['minimum']; if ($serial < 0) { $serial += 4294967296; } @@ -273,10 +284,10 @@ public static function decode(string $data, int &$offset): self $mname, $rname, $serial, - $fields['refresh'], - $fields['retry'], - $fields['expire'], - $fields['minimum'] + $refresh, + $retry, + $expire, + $minimum ); break; From 35057e707e11fa62d5232a75bc1dc69e76fa6ec9 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 31 Dec 2025 12:02:11 +0000 Subject: [PATCH 19/19] phpstan max --- composer.json | 2 +- src/DNS/Message/Header.php | 3 ++- src/DNS/Message/Record.php | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 9ba2d5b..2f2a37d 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "scripts": { "lint": "./vendor/bin/pint --test --config pint.json", "format": "./vendor/bin/pint --config pint.json", - "check": "./vendor/bin/phpstan analyse --level 8 -c phpstan.neon src tests", + "check": "./vendor/bin/phpstan analyse --level max -c phpstan.neon src tests", "test": "./vendor/bin/phpunit --configuration phpunit.xml" }, "authors": [ diff --git a/src/DNS/Message/Header.php b/src/DNS/Message/Header.php index a60d293..733434b 100644 --- a/src/DNS/Message/Header.php +++ b/src/DNS/Message/Header.php @@ -39,7 +39,8 @@ public static function decode(string $data, int $offset = 0): self $chunk = substr($data, $offset, self::LENGTH); $values = unpack('nid/nflags/nqdcount/nancount/nnscount/narcount', $chunk); - if (!is_array($values) + if ( + !is_array($values) || !isset($values['id'], $values['flags'], $values['qdcount'], $values['ancount'], $values['nscount'], $values['arcount']) || !is_int($values['id']) || !is_int($values['flags']) diff --git a/src/DNS/Message/Record.php b/src/DNS/Message/Record.php index c869329..2618e7b 100644 --- a/src/DNS/Message/Record.php +++ b/src/DNS/Message/Record.php @@ -258,7 +258,8 @@ public static function decode(string $data, int &$offset): self } $fields = unpack('Nserial/Nrefresh/Nretry/Nexpire/Nminimum', $timingData); - if (!is_array($fields) + if ( + !is_array($fields) || !isset($fields['serial'], $fields['refresh'], $fields['retry'], $fields['expire'], $fields['minimum']) || !is_int($fields['serial']) || !is_int($fields['refresh']) @@ -444,7 +445,7 @@ private function encodeRdata(string $packet): string } return pack('nnn', $priority, $weight, $port) . - Domain::encode($this->rdata); + Domain::encode($this->rdata); case self::TYPE_TXT: $len = strlen($this->rdata);