From b1d522d8cc04f58cd64eeed23f6525cf8e6e4bab Mon Sep 17 00:00:00 2001 From: Deon George Date: Sat, 22 Apr 2023 21:30:30 +1000 Subject: [PATCH] Added DNS server --- app/Classes/Protocol/DNS.php | 371 +++++++++++++++++++++++++++ app/Classes/Sock/SocketServer.php | 49 +++- app/Console/Commands/ServerStart.php | 14 +- app/Models/Setup.php | 5 +- 4 files changed, 430 insertions(+), 9 deletions(-) create mode 100644 app/Classes/Protocol/DNS.php diff --git a/app/Classes/Protocol/DNS.php b/app/Classes/Protocol/DNS.php new file mode 100644 index 0000000..8250308 --- /dev/null +++ b/app/Classes/Protocol/DNS.php @@ -0,0 +1,371 @@ + [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 + ]; + + /** + * Handle a DNS query + * + * https://www.ietf.org/rfc/rfc1035.txt + * https://github.com/guyinatuxedo/dns-fuzzer/blob/master/dns.md + * + * labels 63 octets or less + * names 255 octets or less + * 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 + * +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) + { + 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; + + // 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; + } + + // 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); + + return; + } + + // First check that it is a query we can answer + // First label should be p.. or f.. + if (! is_null($p=$this->parse('p',$labels->first()))) + $labels->shift(); + + if (is_null($f=$this->parse('f',$labels->shift()))) + return $this->nameerr($labels); + + if (is_null($n=$this->parse('n',$labels->shift()))) + return $this->nameerr($labels); + + if (is_null($z=$this->parse('z',$labels->shift()))) + return $this->nameerr($labels); + + if (is_null($d=$labels->shift())) + return $this->nameerr($labels); + + // Make sure we have a root/base domain + if (! $labels->count()) + return $this->nameerr($labels); + + $rootdn = $labels->join('.'); + + $ao = Address::findFTN(sprintf('%d:%d/%d.%d@%s',$z,$n,$f,$p,$d)); + + // Check we have the right record + 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; + } + + // Check the class + switch ($result['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]); + break; + + default: + Log::error(sprintf('%s:! We dont support DNS query types [%d]',self::LOGKEY,$result['type'])); + $this->reply(self::DNS_NOTIMPLEMENTED); + } + } + + /** + * Return a compression string for a specific offset + * + * @param int $offset + * @return string + */ + private function compress(int $offset): string + { + return pack('n',$offset | (3 << 14)); + } + + private function nameerr(Collection $labels) + { + Log::error(sprintf('%s:! DNS query for a resource we dont manage [%s]',self::LOGKEY,$labels->join('.'))); + + $this->reply(self::DNS_NAMEERR); + } + + /** + * Parse a label for a fido address nibble + * + * @param string $prefix + * @param string $label + * @return string|null + */ + private function parse(string $prefix,string $label): ?string + { + $m = []; + + return (preg_match('/^'.$prefix.'([0-9]+)+/',$label,$m) && ($m[1] <= DomainController::NUMBER_MAX)) + ? $m[1] + : NULL; + } + + /** + * Return a DNS response + * + * @param int $code + * @param string $question + * @param array $answer + * @return bool + */ + private function reply(int $code,string $question='',array $answer=[]): bool + { + $header = (1 << 15); // 1b: Query/Response + $header |= (0 << 11); // 4b: Opcode + $header |= (0 << 10); // 1b: Authoritative Answer + $header |= (0 << 9); // 1b: Truncated + $header |= (0 << 8); // 1b: Recursion Desired (in queries) + $header |= (0 << 7); // 1b: Recursion Available (in responses) + $header |= (0 << 4); // 3b: Zero (future, should be zero) + $header |= $code; // 4b: Result Code + + $q = $question ? 1 : 0; + $r = count($answer); + $nscount = 0; + $arcount = 0; + + $reply = pack('nnnnnn',$this->header['id'],$header,$q,$r,$nscount,$arcount); + + // Return the answer + if ($r) { + // Question + $reply .= $question; + + // @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); + + } else { + $reply .= $question; + } + + // 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'])) { + 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'] + )); + + return FALSE; + } + + 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 + * + * @param string $query - Domain in the query + * @param mixed $ars - Answer resources + * @param int $type - Resource type + * @param int $ttl - Time to live + * @return string + */ + private function rr(string $query,mixed $ars,int $type,int $ttl): string + { + // Reference the domain query in the question + $reply = $query; + + // Record Type + $reply .= pack('n',$type); + + // Internet + $reply .= pack('n',self::DNS_QUERY_IN); + + $reply .= pack('n',0); + + // TTL + $reply .= pack('n',$ttl); + + // Answer + $a = ''; + switch ($type) { + case self::DNS_TYPE_CNAME: + case self::DNS_TYPE_NS: + foreach ($ars as $item) + $a .= pack('C',strlen($item)).$item; + + $a .= "\x00"; + break; + + case self::DNS_TYPE_A: + case self::DNS_TYPE_AAAA: + $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/Sock/SocketServer.php b/app/Classes/Sock/SocketServer.php index 4b1519c..6779d96 100644 --- a/app/Classes/Sock/SocketServer.php +++ b/app/Classes/Sock/SocketServer.php @@ -12,12 +12,15 @@ final class SocketServer { private int $port; // The Port to bind to private int $backlog = 5; // Number of incoming connections queued + private int $type; // Socket type + private array $handler; // The class and method that will handle our connection - public function __construct(int $port,string $bind='0.0.0.0') + public function __construct(int $port,string $bind='0.0.0.0',int $type=SOCK_STREAM) { $this->bind = $bind; $this->port = $port; + $this->type = $type; $this->_init(); } @@ -49,7 +52,18 @@ final class SocketServer { if (! extension_loaded('pcntl')) throw new SocketException(SocketException::CANT_ACCEPT,'Missing pcntl extension'); - $this->server = socket_create(AF_INET|AF_INET6,SOCK_STREAM,SOL_TCP); + switch ($this->type) { + case SOCK_STREAM: + $this->server = socket_create(AF_INET|AF_INET6,$this->type,SOL_TCP); + break; + + case SOCK_DGRAM: + $this->server = socket_create(AF_INET|AF_INET6,$this->type,SOL_UDP); + break; + + default: + throw new \Exception('Unknown socket_type:'.$this->type); + } if ($this->server === FALSE) throw new SocketException(SocketException::CANT_CREATE_SOCKET,socket_strerror(socket_last_error())); @@ -78,12 +92,22 @@ final class SocketServer { if (! $this->handler) throw new SocketException(SocketException::CANT_LISTEN,'Handler not set.'); - if (socket_listen($this->server,$this->backlog) === FALSE) - throw new SocketException(SocketException::CANT_LISTEN,socket_strerror(socket_last_error($this->server))); + if (in_array($this->type,[SOCK_STREAM,SOCK_SEQPACKET])) + if (socket_listen($this->server,$this->backlog) === FALSE) + throw new SocketException(SocketException::CANT_LISTEN,socket_strerror(socket_last_error($this->server))); Log::info(sprintf('%s:- Listening on [%s:%d]',self::LOGKEY,$this->bind,$this->port)); - $this->loop(); + switch ($this->type) { + case SOCK_STREAM: + $this->loop_tcp(); + break; + + case SOCK_DGRAM: + $this->loop_udp(); + break; + } + socket_close($this->server); Log::info(sprintf('%s:= Closed [%s:%d]',self::LOGKEY,$this->bind,$this->port)); @@ -94,7 +118,7 @@ final class SocketServer { * * @throws SocketException */ - private function loop() + private function loop_tcp() { while (TRUE) { if (($accept = socket_accept($this->server)) === FALSE) @@ -112,6 +136,19 @@ 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); + } + } + /** * Set our connection handler Class and Method * diff --git a/app/Console/Commands/ServerStart.php b/app/Console/Commands/ServerStart.php index b075b45..ed92c22 100644 --- a/app/Console/Commands/ServerStart.php +++ b/app/Console/Commands/ServerStart.php @@ -5,7 +5,7 @@ namespace App\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\Log; -use App\Classes\Protocol\{Binkp,EMSI}; +use App\Classes\Protocol\{Binkp,DNS,EMSI}; use App\Classes\Sock\SocketException; use App\Classes\Sock\SocketServer; use App\Models\Setup; @@ -45,6 +45,7 @@ class ServerStart extends Command $start->put('binkp',[ 'address'=>Setup::BINKP_BIND, 'port'=>Setup::BINKP_PORT, + 'proto'=>SOCK_STREAM, 'class'=>new Binkp($o), ]); @@ -52,9 +53,18 @@ class ServerStart extends Command $start->put('emsi',[ 'address'=>Setup::EMSI_BIND, 'port'=>Setup::EMSI_PORT, + 'proto'=>SOCK_STREAM, 'class'=>new EMSI($o), ]); + //if ($o->optionGet(Setup::O_DNS)) + $start->put('dns',[ + 'address'=>Setup::DNS_BIND, + 'port'=>Setup::DNS_PORT, + 'proto'=>SOCK_DGRAM, + 'class'=>new DNS(), + ]); + $children = collect(); Log::debug(sprintf('%s: # Servers [%d]',self::LOGKEY,$start->count())); @@ -79,7 +89,7 @@ class ServerStart extends Command Log::withContext(['pid'=>getmypid()]); Log::info(sprintf('%s: - Started [%s]',self::LOGKEY,$item)); - $server = new SocketServer($config['port'],$config['address']); + $server = new SocketServer($config['port'],$config['address'],$config['proto']); $server->setConnectionHandler([$config['class'],'onConnect']); try { diff --git a/app/Models/Setup.php b/app/Models/Setup.php index a7e811e..1d66a0d 100644 --- a/app/Models/Setup.php +++ b/app/Models/Setup.php @@ -34,10 +34,13 @@ class Setup extends Model public const BINKP_BIND = '::'; public const EMSI_PORT = 60179; public const EMSI_BIND = self::BINKP_BIND; + public const DNS_PORT = 53; + public const DNS_BIND = '::'; public const O_BINKP = 1<<1; /* Listen for BINKD connections */ public const O_EMSI = 1<<2; /* Listen for EMSI connections */ - public const O_HIDEAKA = 1<<3; /* Hide AKAs to different Zones */ + public const O_DNS = 1<<3; /* List for DNS */ + public const O_HIDEAKA = 1<<4; /* Hide AKAs to different Zones */ public const PRODUCT_NAME = 'Clearing Houz'; public const PRODUCT_ID = 0xAB8D;