Update SocketClient to support UDP. Change DNS queries to use SocketClient

This commit is contained in:
2023-04-23 23:08:30 +10:00
parent 073be20ceb
commit b1c62ae227
5 changed files with 262 additions and 164 deletions

View File

@@ -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()
);
}
}