Added DNS server
This commit is contained in:
parent
25c3041c67
commit
b1d522d8cc
371
app/Classes/Protocol/DNS.php
Normal file
371
app/Classes/Protocol/DNS.php
Normal file
@ -0,0 +1,371 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Protocol;
|
||||
|
||||
use App\Models\Address;
|
||||
use App\Models\Domain;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Http\Controllers\DomainController;
|
||||
|
||||
final class DNS
|
||||
{
|
||||
private const LOGKEY = 'PD-';
|
||||
|
||||
private string $rx_buf;
|
||||
private array $header;
|
||||
private array $remote;
|
||||
private int $rx_ptr;
|
||||
private \Socket $socket;
|
||||
|
||||
// DNS Response codes
|
||||
public const DNS_NOERROR = 0; // No error
|
||||
public const DNS_FORMERR = 1; // Format Error - The dns server could not read / understand the query
|
||||
public const DNS_SERVFAIL = 2; // Server Failure - There was a dns error with the dns server
|
||||
public const DNS_NAMEERR = 3; // Name Error - This specifies that the domain name in the query does not exist, it is only valid from an authoritative server
|
||||
public const DNS_NOTIMPLEMENTED = 4;// Not implemented - The requested query is not supported by the dns server
|
||||
public const DNS_REFUSED = 5; // Refused - The dns server refuses to process the dns query
|
||||
|
||||
// DNS Query Classes
|
||||
public const DNS_QUERY_IN = 1; // Internet (this is the main one that is used)
|
||||
|
||||
// DNS Query Types
|
||||
public const DNS_TYPE_A = 1; // A Records
|
||||
public const DNS_TYPE_NS = 2; // NS Records
|
||||
public const DNS_TYPE_CNAME = 5; // CNAME Records
|
||||
public const DNS_TYPE_SOA = 6; // SOA Records
|
||||
public const DNS_TYPE_MX = 15; // MX Records
|
||||
public const DNS_TYPE_TXT = 16; // TXT Records
|
||||
|
||||
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
|
||||
];
|
||||
|
||||
/**
|
||||
* 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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
*
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user