From b1c62ae22759a184b53140058cae5fedbbc3cf77 Mon Sep 17 00:00:00 2001 From: Deon George Date: Sun, 23 Apr 2023 23:08:30 +1000 Subject: [PATCH] Update SocketClient to support UDP. Change DNS queries to use SocketClient --- app/Classes/Protocol.php | 2 +- app/Classes/Protocol/DNS.php | 247 ++++++++++++----------------- app/Classes/Protocol/DNS/Query.php | 88 ++++++++++ app/Classes/Sock/SocketClient.php | 74 +++++++-- app/Classes/Sock/SocketServer.php | 15 +- 5 files changed, 262 insertions(+), 164 deletions(-) create mode 100644 app/Classes/Protocol/DNS/Query.php diff --git a/app/Classes/Protocol.php b/app/Classes/Protocol.php index 5d359de..4e272ea 100644 --- a/app/Classes/Protocol.php +++ b/app/Classes/Protocol.php @@ -383,7 +383,7 @@ abstract class Protocol * * @param SocketClient $client */ - private function setClient(SocketClient $client): void + protected function setClient(SocketClient $client): void { $this->client = $client; } diff --git a/app/Classes/Protocol/DNS.php b/app/Classes/Protocol/DNS.php index 8250308..8599e1e 100644 --- a/app/Classes/Protocol/DNS.php +++ b/app/Classes/Protocol/DNS.php @@ -2,22 +2,19 @@ namespace App\Classes\Protocol; -use App\Models\Address; -use App\Models\Domain; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; +use App\Classes\Protocol as BaseProtocol; +use App\Classes\Sock\SocketClient; use App\Http\Controllers\DomainController; +use App\Models\Address; -final class DNS +final class DNS extends BaseProtocol { private const LOGKEY = 'PD-'; - private string $rx_buf; - private array $header; - private array $remote; - private int $rx_ptr; - private \Socket $socket; + private BaseProtocol\DNS\Query $query; // DNS Response codes public const DNS_NOERROR = 0; // No error @@ -40,15 +37,43 @@ final class DNS public const DNS_TYPE_AAAA = 28; // AAAA Records - // 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 - ]; + /** + * Split a domain into a DNS domain string + * + * @param string $domain + * @return string + */ + private function domain_split(string $domain): string + { + $a = ''; + + foreach (explode('.',$domain) as $item) + $a .= pack('C',strlen($item)).$item; + + return $a; + } + + public function onConnect(SocketClient $client): ?int + { + // If our parent returns a PID, we've forked + if (! parent::onConnect($client)) { + Log::withContext(['pid'=>getmypid()]); + + $this->setClient($client); + $this->protocol_session(); + + Log::info(sprintf('%s:= onConnect - Connection closed [%s]',self::LOGKEY,$client->address_remote)); + exit(0); + } + + return NULL; + } + + protected function protocol_init(): int + { + // N/A + return 0; + } /** * Handle a DNS query @@ -61,79 +86,58 @@ final class DNS * TTL positive values of a signed 32 bit number. * UDP messages 512 octets or less * - * @param array $remote - * @param string $buf - * @param \Socket $socket - * @return void + * @return int + * @throws \Exception * -dig +noedns -t CNAME mail.dcml.au @1.1.1.1 - - ; <<>> DiG 9.10.6 <<>> +noedns -t CNAME mail.dcml.au @1.1.1.1 - ;; global options: +cmd - ;; Got answer: - ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6473 - ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 - - ;; QUESTION SECTION: - ;mail.dcml.au. IN CNAME - - ;; ANSWER SECTION: - mail.dcml.au. 300 IN CNAME l.dege.au. - - ;; Query time: 43 msec - ;; SERVER: 1.1.1.1#53(1.1.1.1) - ;; WHEN: Thu Apr 20 06:47:55 PST 2023 - ;; MSG SIZE rcvd: 51 -06:47:54.995929 IP 192.168.130.184.51502 > one.one.one.one.domain: 6473+ CNAME? mail.dcml.au. (30) - 0x0000: cc03 d9cc 88cb bcd0 7414 d055 0800 4500 ........t..U..E. - 0x0010: 003a eee2 0000 4011 466e c0a8 82b8 0101 .:....@.Fn...... - 0x0020: 0101 c92e 0035 0026 bb5f|1949 0120 0001 .....5.&._.I.... - 0x0030: 0000 0000 0000 046d 6169 6c04 6463 6d6c .......mail.dcml - 0x0040: 0261 7500 0005 0001 .au..... -06:47:55.034171 IP one.one.one.one.domain > 192.168.130.184.51502: 6473$ 1/0/0 CNAME l.dege.au. (51) - 0x0000: bcd0 7414 d055 cc03 d9cc 88cb 0800 4588 ..t..U........E. - 0x0010: 004f 514a 4000 3a11 a969 0101 0101 c0a8 .OQJ@.:..i...... - 0x0020: 82b8 0035 c92e 003b 9274|1949 81a0 0001 ...5...;.t.I.... - 0x0030: 0001 0000 0000 046d 6169 6c04 6463 6d6c .......mail.dcml - 0x0040: 0261 7500 0005 0001 c00c 0005 0001 0000 .au............. - 0x0050: 012c 0009 016c 0464 6567 65c0 16 .,...l.dege.. + * dig +noedns -t CNAME mail.dcml.au @1.1.1.1 + * + * ; <<>> DiG 9.10.6 <<>> +noedns -t CNAME mail.dcml.au @1.1.1.1 + * ;; global options: +cmd + * ;; Got answer: + * ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6473 + * ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 + * + * ;; QUESTION SECTION: + * ;mail.dcml.au. IN CNAME + * + * ;; ANSWER SECTION: + * mail.dcml.au. 300 IN CNAME l.dege.au. + * + * ;; Query time: 43 msec + * ;; SERVER: 1.1.1.1#53(1.1.1.1) + * ;; WHEN: Thu Apr 20 06:47:55 PST 2023 + * ;; MSG SIZE rcvd: 51 + * 06:47:54.995929 IP 192.168.130.184.51502 > one.one.one.one.domain: 6473+ CNAME? mail.dcml.au. (30) + * 0x0000: cc03 d9cc 88cb bcd0 7414 d055 0800 4500 ........t..U..E. + * 0x0010: 003a eee2 0000 4011 466e c0a8 82b8 0101 .:....@.Fn...... + * 0x0020: 0101 c92e 0035 0026 bb5f|1949 0120 0001 .....5.&._.I.... + * 0x0030: 0000 0000 0000 046d 6169 6c04 6463 6d6c .......mail.dcml + * 0x0040: 0261 7500 0005 0001 .au..... + * 06:47:55.034171 IP one.one.one.one.domain > 192.168.130.184.51502: 6473$ 1/0/0 CNAME l.dege.au. (51) + * 0x0000: bcd0 7414 d055 cc03 d9cc 88cb 0800 4588 ..t..U........E. + * 0x0010: 004f 514a 4000 3a11 a969 0101 0101 c0a8 .OQJ@.:..i...... + * 0x0020: 82b8 0035 c92e 003b 9274|1949 81a0 0001 ...5...;.t.I.... + * 0x0030: 0001 0000 0000 046d 6169 6c04 6463 6d6c .......mail.dcml + * 0x0040: 0261 7500 0005 0001 c00c 0005 0001 0000 .au............. + * 0x0050: 012c 0009 016c 0464 6567 65c0 16 .,...l.dege.. */ - public function onConnect(array $remote,string $buf,\Socket $socket) + public function protocol_session(): int { Log::debug(sprintf('%s:+ DNS Query',self::LOGKEY)); - $header_len = collect(self::header)->sum(function($item) { return $item[2]*2; }); - $this->rx_buf = $buf; - $this->rx_ptr = 0; - - // DNS Query header - $this->header = unpack(self::unpackheader(self::header),$buf); - $this->rx_ptr += $header_len; - - $this->remote = $remote; - $this->socket = $socket; + $this->query = new BaseProtocol\DNS\Query($this->client->read(0,512)); // If there is no query count, then its an error - if ($this->header['qdcount'] !== 1) { - Log::error(sprintf('%s:! DNS query doesnt have the right number of queries [%d]',self::LOGKEY,$this->header['qdcount'])); - $this->reply(self::DNS_FORMERR); - return; + if ($this->query->qdcount !== 1) { + Log::error(sprintf('%s:! DNS query doesnt have the right number of queries [%d]',self::LOGKEY,$this->query->qdcount)); + return $this->reply(self::DNS_FORMERR); } - // Get the query elements - $labels = collect(); - while (($len=ord($this->read(1))) !== 0x00) - $labels->push($this->read($len)); - - $this->question = substr($this->rx_buf,$header_len,$this->rx_ptr-$header_len+4); - // We need a minimum of f.n.z.d.root - if ($labels->count() < 5) { - Log::error(sprintf('%s:! DNS query for a resource we dont manage [%s]',self::LOGKEY,$labels->join('.'))); - $this->reply(self::DNS_NAMEERR); + if ($this->query->labels->count() < 5) + return $this->nameerr($this->query->labels); - return; - } + $labels = clone($this->query->labels); // First check that it is a query we can answer // First label should be p.. or f.. @@ -164,32 +168,30 @@ dig +noedns -t CNAME mail.dcml.au @1.1.1.1 if ((! $ao) || (! $ao->system->mailer_address) || (($rootdn !== 'ftn') && ((! $ao->zone->domain->dnsdomain) || ($ao->zone->domain->dnsdomain !== $d.'.'.$rootdn)))) return $this->nameerr($labels); - // Get the query type/class - $result = unpack('ntype/nclass',$x=$this->read(4)); - // If the wrong class - if ($result['class'] !== self::DNS_QUERY_IN) { - Log::error(sprintf('%s:! We only service Internet queries [%d]',self::LOGKEY,$result['class'])); - $this->reply(self::DNS_NOTIMPLEMENTED); - return; + if ($this->query->class !== self::DNS_QUERY_IN) { + Log::error(sprintf('%s:! We only service Internet queries [%d]',self::LOGKEY,$this->query->class)); + return $this->reply(self::DNS_NOTIMPLEMENTED); } // Check the class - switch ($result['type']) { + switch ($this->query->type) { case self::DNS_TYPE_CNAME: case self::DNS_TYPE_A: case self::DNS_TYPE_AAAA: Log::debug(sprintf('%s:= Returning [%s] for DNS query [%s]',self::LOGKEY,$ao->system->mailer_address,$ao->ftn)); $this->reply( self::DNS_NOERROR, - $this->question, - [serialize(explode('.',$ao->system->mailer_address)) => self::DNS_TYPE_CNAME]); + $this->query->domain, + [$this->domain_split($ao->system->mailer_address) => self::DNS_TYPE_CNAME]); break; default: - Log::error(sprintf('%s:! We dont support DNS query types [%d]',self::LOGKEY,$result['type'])); + Log::error(sprintf('%s:! We dont support DNS query types [%d]',self::LOGKEY,$this->query->type)); $this->reply(self::DNS_NOTIMPLEMENTED); } + + return self::DNS_NOERROR; } /** @@ -203,11 +205,11 @@ dig +noedns -t CNAME mail.dcml.au @1.1.1.1 return pack('n',$offset | (3 << 14)); } - private function nameerr(Collection $labels) + private function nameerr(Collection $labels): int { Log::error(sprintf('%s:! DNS query for a resource we dont manage [%s]',self::LOGKEY,$labels->join('.'))); - $this->reply(self::DNS_NAMEERR); + return $this->reply(self::DNS_NAMEERR); } /** @@ -233,6 +235,7 @@ dig +noedns -t CNAME mail.dcml.au @1.1.1.1 * @param string $question * @param array $answer * @return bool + * @throws \Exception */ private function reply(int $code,string $question='',array $answer=[]): bool { @@ -250,7 +253,7 @@ dig +noedns -t CNAME mail.dcml.au @1.1.1.1 $nscount = 0; $arcount = 0; - $reply = pack('nnnnnn',$this->header['id'],$header,$q,$r,$nscount,$arcount); + $reply = pack('nnnnnn',$this->query->id,$header,$q,$r,$nscount,$arcount); // Return the answer if ($r) { @@ -259,7 +262,7 @@ dig +noedns -t CNAME mail.dcml.au @1.1.1.1 // @todo In the case we return a CNAME and an A record, this should reference the CNAME domain when returning the A record foreach ($answer as $item => $type) - $reply .= $this->rr($this->compress(12),unserialize($item),$type,300); + $reply .= $this->rr($this->compress(12),$item,$type,300); } else { $reply .= $question; @@ -268,12 +271,12 @@ dig +noedns -t CNAME mail.dcml.au @1.1.1.1 // nscount //$reply .= $this->rr($this->domain_split('net1.fsxnet.nz'),["a","root-servers","net"],self::DNS_TYPE_NS,300); - if (! socket_sendto($this->socket,$reply,strlen($reply),0,(string)$this->remote['ip'],(int)$this->remote['port'])) { + if (! $this->client->send($reply,0)) { Log::error(sprintf('%s:! Error [%s] sending DNS reply to [%s:%d]', self::LOGKEY, socket_strerror(socket_last_error()), - $this->remote['ip'], - $this->remote['port'] + $this->client->address_remote, + $this->client->port_remote )); return FALSE; @@ -282,30 +285,6 @@ dig +noedns -t CNAME mail.dcml.au @1.1.1.1 return TRUE; } - private function domain_split(string $domain): string - { - $a = ''; - - foreach (explode('.',$domain) as $item) - $a .= pack('C',strlen($item)).$item; - - return $a; - } - - /** - * Read from a rx_buf - * - * @param int $len - * @return string - */ - private function read(int $len): string - { - $result = substr($this->rx_buf,$this->rx_ptr,$len); - $this->rx_ptr += $len; - - return $result; - } - /** * Return a DNS Resource Record * @@ -315,7 +294,7 @@ dig +noedns -t CNAME mail.dcml.au @1.1.1.1 * @param int $ttl - Time to live * @return string */ - private function rr(string $query,mixed $ars,int $type,int $ttl): string + private function rr(string $query,string $ars,int $type,int $ttl): string { // Reference the domain query in the question $reply = $query; @@ -336,36 +315,16 @@ dig +noedns -t CNAME mail.dcml.au @1.1.1.1 switch ($type) { case self::DNS_TYPE_CNAME: case self::DNS_TYPE_NS: - foreach ($ars as $item) - $a .= pack('C',strlen($item)).$item; - - $a .= "\x00"; + $a = $ars."\x00"; break; case self::DNS_TYPE_A: case self::DNS_TYPE_AAAA: - $a .= $ars; + $a = $ars; } $reply .= pack('n',strlen($a)).$a; return $reply; } - - /** - * 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() - ); - } } \ No newline at end of file diff --git a/app/Classes/Protocol/DNS/Query.php b/app/Classes/Protocol/DNS/Query.php new file mode 100644 index 0000000..bfc445f --- /dev/null +++ b/app/Classes/Protocol/DNS/Query.php @@ -0,0 +1,88 @@ + [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']; + + // Get the domain elements + $this->labels = collect(); + + while (($len=ord(substr($this->buf,$rx_ptr++,1))) !== 0x00) { + $this->labels->push(substr($this->buf,$rx_ptr,$len)); + $rx_ptr += $len; + } + + // Get the query type/class + $result = unpack('ntype/nclass',substr($this->buf,$rx_ptr,4)); + $rx_ptr += 4; + $this->type = $result['type']; + $this->class = $result['class']; + + $this->domain = substr($this->buf,$x=$this->header_len(),$rx_ptr-$x); + } + + public function __get($key) + { + switch ($key) { + case 'class': + case 'domain': + case 'id': + case 'labels': + case 'qdcount': + case 'type': + return $this->{$key}; + } + } + + 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() + ); + } +} \ No newline at end of file diff --git a/app/Classes/Sock/SocketClient.php b/app/Classes/Sock/SocketClient.php index da8ac84..c15f016 100644 --- a/app/Classes/Sock/SocketClient.php +++ b/app/Classes/Sock/SocketClient.php @@ -56,11 +56,13 @@ final class SocketClient { private string $rx_buf = ''; public function __construct (\Socket $connection) { - socket_getsockname($connection,$this->address_local,$this->port_local); - socket_getpeername($connection,$this->address_remote,$this->port_remote); - Log::info(sprintf('%s:+ Connection host [%s] on port [%d]',self::LOGKEY,$this->address_remote,$this->port_remote)); - $this->connection = $connection; + + if ($this->type === 'TCP') { + socket_getsockname($connection,$this->address_local,$this->port_local); + socket_getpeername($connection,$this->address_remote,$this->port_remote); + Log::info(sprintf('%s:+ Connection host [%s] on port [%d] (%s)',self::LOGKEY,$this->address_remote,$this->port_remote,$this->type)); + } } public function __get($key) { @@ -73,6 +75,17 @@ final class SocketClient { case 'speed': return Arr::get($this->session,$key); + case 'type': + switch ($x=socket_get_option($this->connection,SOL_SOCKET,SO_TYPE)) { + case SOCK_STREAM: + return 'TCP'; + case SOCK_DGRAM: + return 'UDP'; + + default: + return sprintf('UNKNOWN [%d]',$x); + } + default: throw new \Exception(sprintf('%s:! Unknown key [%s]:',self::LOGKEY,$key)); } @@ -226,6 +239,7 @@ final class SocketClient { /** * Create a client socket + * * @param string $address * @param int $port * @return static @@ -292,24 +306,43 @@ final class SocketClient { /** * Read data from the socket. - * If we only want 1 character, we'll return the ASCII value of the data received * * @param int $timeout * @param int $len - * @return int|string + * @param int $size + * @return string * @throws SocketException */ - public function read(int $timeout,int $len=1024) + public function read(int $timeout,int $len=1024,int $size=1024): string { if ($this->DEBUG) Log::debug(sprintf('%s:+ Start [%d] (%d)',self::LOGKEY,$len,$timeout)); + // We have data in our buffer + if ($this->rx_left >= $len) { + $result = substr($this->rx_buf,$this->rx_ptr,$len); + $this->rx_ptr += $len; + $this->rx_left -= $len; + + if ($this->rx_left === 0) { + $this->rx_buf = ''; + $this->rx_ptr = 0; + } + + return $result; + } + if ($timeout AND ($this->hasData($timeout) === 0)) return ''; $buf = ''; try { - $rc = socket_recv($this->connection,$buf, $len,MSG_DONTWAIT); + if ($this->type === 'TCP') + $rc = socket_recv($this->connection,$buf, $size,MSG_DONTWAIT); + + else { + $rc = socket_recvfrom($this->connection,$buf, $size,MSG_DONTWAIT,$this->address_remote,$this->port_remote); + } } catch (\Exception $e) { Log::error(sprintf('%s: - socket_recv Exception [%s]',self::LOGKEY,$e->getMessage())); @@ -320,8 +353,23 @@ final class SocketClient { if ($this->DEBUG) Log::debug(sprintf('%s: - Read [%d]',self::LOGKEY,$rc)); - if ($rc === FALSE) + if ($rc === FALSE) { + // If we have something in the buffer, we'll send it + if ($this->rx_left && $this->rx_left < $len) { + $return = substr($this->rx_buf,$this->rx_ptr); + + $this->rx_left = 0; + $this->rx_ptr = 0; + $this->rx_buf = ''; + + return $return; + } + throw new SocketException($x=socket_last_error($this->connection),socket_strerror($x)); + } + + $this->rx_buf .= $buf; + $this->rx_left += strlen($buf); // If our buffer is null, see if we have any out of band data. // @todo We throw an errorexception when the socket is closed by the remote I think. @@ -334,7 +382,7 @@ final class SocketClient { } } - return is_null($buf) ? '' : $buf; + return $this->read($timeout,$len,$size); } /** @@ -343,7 +391,7 @@ final class SocketClient { * * @param int $timeout * @return int - * @throws SocketException + * @throws \Exception */ public function read_ch(int $timeout): int { @@ -410,7 +458,9 @@ final class SocketClient { if (is_null($length)) $length = strlen($message); - return socket_write($this->connection,$message,$length); + return ($this->type === 'TCP') + ? socket_write($this->connection,$message,$length) + : socket_sendto($this->connection,$message,$length,0,$this->address_remote,$this->port_remote); } /** diff --git a/app/Classes/Sock/SocketServer.php b/app/Classes/Sock/SocketServer.php index 6779d96..7f7dc7b 100644 --- a/app/Classes/Sock/SocketServer.php +++ b/app/Classes/Sock/SocketServer.php @@ -138,14 +138,15 @@ final class SocketServer { private function loop_udp() { - $buf = ''; - $remote = []; - $remote['ip'] = NULL; - $remote['port'] = NULL; - while (TRUE) { - if (socket_recvfrom($this->server,$buf,512,MSG_WAITALL,$remote['ip'],$remote['port'])) - $this->handler[0]->{$this->handler[1]}($remote,$buf,$this->server); + $r = new SocketClient($this->server); + + if ($r->hasData(30)) { + $this->handler[0]->{$this->handler[1]}($r); + + // Sleep so our thread has a chance to pick up the data from our connection + usleep(50000); + } } }