<?php namespace App\Classes\Protocol\DNS; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; final class Query { private const LOGKEY = 'PDQ'; private string $buf; private int $class; private string $dns; private int $id; private int $type; private int $arcount; private int $qdcount; private RR $additional; private Collection $labels; // https://github.com/guyinatuxedo/dns-fuzzer/blob/master/dns.md private const header = [ // Struct of a DNS query 'id' => [0x00,'n',1], // ID 'header' => [0x01,'n',1], // Header 'qdcount' => [0x02,'n',1], // Entries in the question 'ancount' => [0x03,'n',1], // Resource Records in the answer 'nscount' => [0x04,'n',1], // Server Resource Records in the answer 'arcount' => [0x05,'n',1], // Resource Records in the addition records section ]; public function __construct(string $buf) { $this->buf = $buf; $rx_ptr = 0; // DNS Query header $header = unpack(self::unpackheader(self::header),$buf); $rx_ptr += $this->header_len(); $this->id = $header['id']; $this->qdcount = $header['qdcount']; $this->arcount = $header['arcount']; $this->header = $header['header']; // Get the domain elements $this->labels = collect(); while (($len=ord(substr($this->buf,$rx_ptr++,1))) !== 0x00) { $this->labels->push(strtolower(substr($this->buf,$rx_ptr,$len))); $rx_ptr += $len; } // Get the query type/class try { $result = unpack('ntype/nclass',substr($this->buf,$rx_ptr,4)); } catch (\Exception $e) { Log::error(sprintf('%s:! Unpack failed: Buffer: [%s] (%d), RXPTR [%d]',self::LOGKEY,hex_dump($this->buf),strlen($this->buf),$rx_ptr)); throw $e; } $rx_ptr += 4; $this->type = $result['type']; $this->class = $result['class']; $this->dns = substr($this->buf,$this->header_len(),$rx_ptr-$this->header_len()); // Do we have additional records if ($this->arcount) { // Additional records, EDNS: https://datatracker.ietf.org/doc/html/rfc6891 if (($haystack = strstr(substr($this->buf,$rx_ptr+1+10),"\x00",true)) !== FALSE) { Log::error(sprintf('%s:! DNS additional record format error?',self::LOGKEY),['buf'=>hex_dump($this->buf)]); return; } $this->additional = new RR(substr($this->buf,$rx_ptr,(strlen($haystack) === 0) ? NULL : strlen($haystack))); $rx_ptr += $this->additional->length; } if (strlen($this->buf) !== $rx_ptr) throw new \Exception(sprintf('! DNS Buffer still has [%d]: %s',strlen($this->buf)-$rx_ptr,hex_dump(substr($this->buf,$rx_ptr)))); } public function __get($key) { switch ($key) { case 'class': case 'dns': case 'id': case 'labels': case 'qdcount': case 'arcount': case 'header': case 'type': return $this->{$key}; case 'domain': return $this->labels->join('.'); } } public static function header_len() { return collect(self::header)->sum(function($item) { return $item[2]*2; }); } /** * Unpack our configured DNS header * * @param array $pack * @return string */ protected static function unpackheader(array $pack): string { return join('/', collect($pack) ->sortBy(function($k,$v) {return $k[0];}) ->transform(function($k,$v) {return $k[1].$v;}) ->values() ->toArray() ); } }