2023-04-22 11:30:30 +00:00
< ? php
namespace App\Classes\Protocol ;
use Illuminate\Support\Facades\Log ;
2023-06-12 11:51:55 +00:00
use Illuminate\Support\Str ;
2023-04-22 11:30:30 +00:00
2023-04-23 13:08:30 +00:00
use App\Classes\Protocol as BaseProtocol ;
use App\Classes\Sock\SocketClient ;
2023-04-22 11:30:30 +00:00
use App\Http\Controllers\DomainController ;
2023-06-12 11:51:55 +00:00
use App\Models\ { Address , Domain };
/**
* Respond to DNS queries and provide addresses to FTN nodes .
*
* This implementation doesnt support EDNS nor DNSSEC .
*
* If using bind , the following configuration is required :
* options {
* validate - except
* {
* " ftn " ;
* };
* };
*
* and optionally
* server < IP ADDRESS > {
* edns no ;
* };
*/
2023-04-23 13:08:30 +00:00
final class DNS extends BaseProtocol
2023-04-22 11:30:30 +00:00
{
private const LOGKEY = 'PD-' ;
2023-07-02 13:40:08 +00:00
/* CONSTS */
public const PORT = 53 ;
2023-06-12 11:51:55 +00:00
private const DEFAULT_TTL = 86400 ;
private const TLD = 'ftn' ;
2023-04-23 13:08:30 +00:00
private BaseProtocol\DNS\Query $query ;
2023-04-22 11:30:30 +00:00
// 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
2023-06-12 11:51:55 +00:00
public const DNS_TYPE_OPT = 41 ; // OPT Records
public const DNS_TYPE_DS = 43 ; // DS Records (Delegation signer RFC 4034)
2023-04-23 13:08:30 +00:00
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 ;
}
2023-04-22 11:30:30 +00:00
/**
* 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
*
2023-04-23 13:08:30 +00:00
* @ return int
* @ throws \Exception
2023-04-22 11:30:30 +00:00
*/
2023-04-23 13:08:30 +00:00
public function protocol_session () : int
2023-04-22 11:30:30 +00:00
{
Log :: debug ( sprintf ( '%s:+ DNS Query' , self :: LOGKEY ));
2023-04-23 13:08:30 +00:00
$this -> query = new BaseProtocol\DNS\Query ( $this -> client -> read ( 0 , 512 ));
2023-04-22 11:30:30 +00:00
2023-06-12 11:51:55 +00:00
Log :: info ( sprintf ( '%s:= DNS Query from [%s] for [%s]' , self :: LOGKEY , $this -> client -> address_remote , $this -> query -> domain ));
// If the wrong class
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 );
2023-04-22 11:30:30 +00:00
}
2023-06-12 11:51:55 +00:00
$dos = Domain :: select ([ 'id' , 'name' , 'dnsdomain' ]) -> active ();
$ourdomains = $dos
-> pluck ( 'name' )
-> transform ( function ( $item ) { return sprintf ( '%s.%s' , $item , self :: TLD ); })
-> merge ( $dos -> pluck ( 'dnsdomain' ))
-> merge ([ self :: TLD ])
-> filter ()
-> sortBy ( function ( $item ) { return substr_count ( $item , '.' ); })
-> reverse ();
$query_domain = $this -> query -> domain ;
// If the query is not for our domains, return NAMEERR
if (( $do = $ourdomains -> search ( function ( $item ) use ( $query_domain ) { return preg_match ( " / ${ item } $ / " , $query_domain ); })) === FALSE ) {
Log :: alert ( sprintf ( '%s:= DNS Query not for our domains' , self :: LOGKEY ));
return $this -> nameerr ();
}
2023-04-22 11:30:30 +00:00
2023-06-12 11:51:55 +00:00
// Action on the query type
switch ( $this -> query -> type ) {
// Return the SOA/NS records
case self :: DNS_TYPE_SOA :
Log :: info ( sprintf ( '%s:= Returning SOA for [%s]' , self :: LOGKEY , $this -> query -> domain ));
2023-04-22 11:30:30 +00:00
2023-06-12 11:51:55 +00:00
return $this -> reply (
self :: DNS_NOERROR ,
[ serialize ([
$this -> domain_split ( gethostname ()),
$this -> domain_split ( Str :: replace ( '@' , '.' , config ( 'app.mail.mail_from' , 'nobody@' . gethostname ()))),
1 ,
self :: DEFAULT_TTL ,
self :: DEFAULT_TTL ,
self :: DEFAULT_TTL ,
self :: DEFAULT_TTL
]) => self :: DNS_TYPE_SOA ],
[],
[ serialize ( $this -> domain_split ( gethostname ())) => self :: DNS_TYPE_NS ],
);
2023-04-22 11:30:30 +00:00
2023-06-12 11:51:55 +00:00
case self :: DNS_TYPE_NS :
Log :: info ( sprintf ( '%s:= Returning NS for [%s]' , self :: LOGKEY , $this -> query -> domain ));
2023-04-22 11:30:30 +00:00
2023-06-12 11:51:55 +00:00
return $this -> reply (
self :: DNS_NOERROR ,
[ serialize ( $this -> domain_split ( gethostname ())) => self :: DNS_TYPE_NS ]);
2023-04-22 11:30:30 +00:00
2023-06-12 11:51:55 +00:00
// Respond to A/AAAA/CNAME queries, with value or NAMEERR
case self :: DNS_TYPE_CNAME :
case self :: DNS_TYPE_A :
case self :: DNS_TYPE_AAAA :
Log :: info ( sprintf ( '%s:= Looking for record [%s] for [%s]' , self :: LOGKEY , $this -> query -> type , $this -> query -> domain ));
2023-04-22 11:30:30 +00:00
2023-06-12 11:51:55 +00:00
$labels = clone ( $this -> query -> labels );
2023-04-22 11:30:30 +00:00
2023-06-12 11:51:55 +00:00
// 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 ();
2023-04-22 11:30:30 +00:00
2023-06-12 11:51:55 +00:00
if ( is_null ( $f = $this -> parse ( 'f' , $labels -> shift ())))
return $this -> nameerr ();
2023-04-22 11:30:30 +00:00
2023-06-12 11:51:55 +00:00
if ( is_null ( $n = $this -> parse ( 'n' , $labels -> shift ())))
return $this -> nameerr ();
2023-04-22 11:30:30 +00:00
2023-06-12 11:51:55 +00:00
if ( is_null ( $z = $this -> parse ( 'z' , $labels -> shift ())))
return $this -> nameerr ();
2023-04-22 11:30:30 +00:00
2023-06-12 11:51:55 +00:00
if ( is_null ( $d = $labels -> shift ()))
return $this -> nameerr ();
// Make sure we have a root/base domain
if ( ! $labels -> count ())
return $this -> nameerr ();
$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 !== self :: TLD ) && (( ! $ao -> zone -> domain -> dnsdomain ) || ( $ao -> zone -> domain -> dnsdomain !== $d . '.' . $rootdn )))) {
Log :: alert ( sprintf ( '%s:= No DNS record for [%d:%d/%d.%d@%s]' , self :: LOGKEY , $z , $n , $f , $p , $d ));
return $this -> nameerr ();
}
2023-04-22 11:30:30 +00:00
Log :: debug ( sprintf ( '%s:= Returning [%s] for DNS query [%s]' , self :: LOGKEY , $ao -> system -> mailer_address , $ao -> ftn ));
2023-06-12 11:51:55 +00:00
return $this -> reply (
2023-04-22 11:30:30 +00:00
self :: DNS_NOERROR ,
2023-06-12 11:51:55 +00:00
[ serialize ( $this -> domain_split ( $ao -> system -> mailer_address )) => self :: DNS_TYPE_CNAME ]);
2023-04-22 11:30:30 +00:00
2023-06-12 11:51:55 +00:00
// Other attributes return NOTIMPL
2023-04-22 11:30:30 +00:00
default :
2023-04-23 13:08:30 +00:00
Log :: error ( sprintf ( '%s:! We dont support DNS query types [%d]' , self :: LOGKEY , $this -> query -> type ));
2023-06-12 11:51:55 +00:00
return $this -> reply ( self :: DNS_NOTIMPLEMENTED );
}
2023-04-22 11:30:30 +00:00
}
/**
* 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 ));
}
2023-06-12 11:51:55 +00:00
private function nameerr () : int
2023-04-22 11:30:30 +00:00
{
2023-06-12 11:51:55 +00:00
Log :: error ( sprintf ( '%s:! DNS query for a resource we dont manage [%s]' , self :: LOGKEY , $this -> query -> domain ));
2023-04-22 11:30:30 +00:00
2023-04-23 13:08:30 +00:00
return $this -> reply ( self :: DNS_NAMEERR );
2023-04-22 11:30:30 +00:00
}
/**
* 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 array $answer
2023-06-12 11:51:55 +00:00
* @ param array $authority
2023-07-02 13:40:08 +00:00
* @ param array $additional
2023-04-22 11:30:30 +00:00
* @ return bool
2023-04-23 13:08:30 +00:00
* @ throws \Exception
2023-04-22 11:30:30 +00:00
*/
2023-06-12 11:51:55 +00:00
private function reply ( int $code , array $answer = [], array $authority = [], array $additional = []) : bool
2023-04-22 11:30:30 +00:00
{
$header = ( 1 << 15 ); // 1b: Query/Response
2023-06-12 11:51:55 +00:00
$header |= ( $this -> query -> header & 0xf ) << 11 ; // 4b: Opcode
$header |= ( 1 << 10 ); // 1b: Authoritative Answer
2023-04-22 11:30:30 +00:00
$header |= ( 0 << 9 ); // 1b: Truncated
2023-06-12 11:51:55 +00:00
$header |= ((( $this -> query -> header >> 8 ) & 1 ) << 8 ); // 1b: Recursion Desired (in queries)
2023-04-22 11:30:30 +00:00
$header |= ( 0 << 7 ); // 1b: Recursion Available (in responses)
$header |= ( 0 << 4 ); // 3b: Zero (future, should be zero)
2023-06-12 11:51:55 +00:00
$header |= ( $code & 0xf ); // 4b: Result Code
2023-04-22 11:30:30 +00:00
2023-06-12 11:51:55 +00:00
$q = $this -> query -> dns ? 1 : 0 ;
2023-04-22 11:30:30 +00:00
$r = count ( $answer );
2023-06-12 11:51:55 +00:00
$nscount = count ( $authority );
$arcount = count ( $additional );
2023-04-22 11:30:30 +00:00
2023-04-23 13:08:30 +00:00
$reply = pack ( 'nnnnnn' , $this -> query -> id , $header , $q , $r , $nscount , $arcount );
2023-04-22 11:30:30 +00:00
// Return the answer
if ( $r ) {
// Question
2023-06-12 11:51:55 +00:00
$reply .= $this -> query -> dns ;
2023-04-22 11:30:30 +00:00
// @todo In the case we return a CNAME and an A record, this should reference the CNAME domain when returning the A record
2023-06-12 11:51:55 +00:00
foreach ( $answer as $item => $type ) {
$rr = $this -> rr ( $this -> compress ( 12 ), unserialize ( $item ), $type , self :: DEFAULT_TTL );
$reply .= $rr ;
}
2023-04-22 11:30:30 +00:00
} else {
2023-06-12 11:51:55 +00:00
$reply .= $this -> query -> dns ;
2023-04-22 11:30:30 +00:00
}
2023-06-12 11:51:55 +00:00
foreach ( $authority as $item => $type ) {
$rr = $this -> rr ( $this -> compress ( 12 ), unserialize ( $item ), $type , self :: DEFAULT_TTL );
$reply .= $rr ;
}
foreach ( $additional as $item => $type ) {
$rr = $this -> rr ( $this -> compress ( 12 ), unserialize ( $item ), $type , self :: DEFAULT_TTL );
$reply .= $rr ;
}
2023-04-22 11:30:30 +00:00
2023-04-23 13:08:30 +00:00
if ( ! $this -> client -> send ( $reply , 0 )) {
2023-04-22 11:30:30 +00:00
Log :: error ( sprintf ( '%s:! Error [%s] sending DNS reply to [%s:%d]' ,
self :: LOGKEY ,
socket_strerror ( socket_last_error ()),
2023-04-23 13:08:30 +00:00
$this -> client -> address_remote ,
$this -> client -> port_remote
2023-04-22 11:30:30 +00:00
));
return FALSE ;
}
return TRUE ;
}
2023-06-12 11:51:55 +00:00
/**
* 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 ;
$a .= " \x00 " ;
return $a ;
}
2023-04-22 11:30:30 +00:00
/**
* 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
*/
2023-06-12 11:51:55 +00:00
private function rr ( string $query , mixed $ars , int $type , int $ttl ) : string
2023-04-22 11:30:30 +00:00
{
// Reference the domain query in the question
$reply = $query ;
// Record Type
$reply .= pack ( 'n' , $type );
// Internet
$reply .= pack ( 'n' , self :: DNS_QUERY_IN );
// TTL
2023-06-12 11:51:55 +00:00
$reply .= pack ( 'N' , $ttl );
2023-04-22 11:30:30 +00:00
// Answer
$a = '' ;
switch ( $type ) {
case self :: DNS_TYPE_NS :
2023-06-12 11:51:55 +00:00
case self :: DNS_TYPE_CNAME :
2023-04-22 11:30:30 +00:00
case self :: DNS_TYPE_A :
case self :: DNS_TYPE_AAAA :
2023-06-12 11:51:55 +00:00
$a .= $ars ;
break ;
case self :: DNS_TYPE_SOA :
$a .= $ars [ 0 ];
$a .= $ars [ 1 ];
$a .= pack ( 'NNNNN' , $ars [ 2 ], $ars [ 3 ], $ars [ 4 ], $ars [ 5 ], $ars [ 6 ]);
break ;
2023-04-22 11:30:30 +00:00
}
$reply .= pack ( 'n' , strlen ( $a )) . $a ;
return $reply ;
}
}