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 ;
2023-10-03 12:15:21 +00:00
use App\Models\ { Address , Domain , Mailer };
2023-06-12 11:51:55 +00:00
/**
* Respond to DNS queries and provide addresses to FTN nodes .
2023-10-05 04:23:49 +00:00
* http :// ftsc . org / docs / fts - 5004.001
2023-06-12 11:51:55 +00:00
*
* 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-10-03 09:58:23 +00:00
public const DNS_TYPE_SRV = 33 ; // SRV 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
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
2023-11-12 07:14:53 +00:00
* https :// www . ietf . org / rfc / rfc2308 . txt
2023-04-22 11:30:30 +00:00
* 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-11-22 10:04:58 +00:00
* @ param bool $force_queue Not used here
2023-04-23 13:08:30 +00:00
* @ return int
* @ throws \Exception
2023-04-22 11:30:30 +00:00
*/
2023-11-22 10:04:58 +00:00
public function protocol_session ( bool $force_queue = FALSE ) : int
2023-04-22 11:30:30 +00:00
{
Log :: debug ( sprintf ( '%s:+ DNS Query' , self :: LOGKEY ));
2023-10-04 04:50:24 +00:00
try {
$this -> query = new BaseProtocol\DNS\Query ( $this -> client -> read ( 0 , 512 ));
} catch ( \Exception $e ) {
2024-06-01 02:55:44 +00:00
Log :: notice ( sprintf ( '%s:! Ignoring bad DNS query (%s)' , self :: LOGKEY , $e -> getMessage ()));
2023-10-04 04:50:24 +00:00
return FALSE ;
}
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 ) {
2024-06-01 02:55:44 +00:00
Log :: notice ( sprintf ( '%s:! We only service Internet queries [%d]' , self :: LOGKEY , $this -> query -> class ));
2023-11-12 07:14:53 +00:00
return $this -> reply ( self :: DNS_NOTIMPLEMENTED ,[], $this -> soa ());
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 ,
2023-11-12 07:14:53 +00:00
$this -> soa (),
2023-06-12 11:51:55 +00:00
[],
2023-11-28 08:57:47 +00:00
[ serialize ( $this -> domain_split ( config ( 'fido.dns_ns' ))) => self :: DNS_TYPE_NS ],
2023-06-12 11:51:55 +00:00
);
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 ,
2023-11-28 08:57:47 +00:00
[ serialize ( $this -> domain_split ( config ( 'fido.dns_ns' ))) => 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 :
2023-10-03 12:15:21 +00:00
case self :: DNS_TYPE_SRV :
case self :: DNS_TYPE_TXT :
2024-06-26 13:56:30 +00:00
Log :: debug ( 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-10-03 12:15:21 +00:00
$mailer = '' ;
// If this is a SRV record query
if ( $this -> query -> type === self :: DNS_TYPE_SRV ) {
2023-10-05 04:23:49 +00:00
if ( $labels -> skip ( 1 ) -> first () !== '_tcp' )
return $this -> reply ( self :: DNS_NAMEERR );
2023-10-03 12:15:21 +00:00
switch ( $labels -> first ()) {
case '_binkp' :
2024-11-08 12:31:21 +00:00
$mailer = Mailer :: where ( 'name' , 'BINKP' ) -> sole ();
2023-10-03 12:15:21 +00:00
break ;
2023-10-05 04:23:49 +00:00
case '_ifcico' :
2024-11-08 12:31:21 +00:00
$mailer = Mailer :: where ( 'name' , 'EMSI' ) -> sole ();
2023-10-05 04:23:49 +00:00
break ;
2023-10-03 12:15:21 +00:00
default :
return $this -> reply ( self :: DNS_NAMEERR );
}
2023-10-05 04:23:49 +00:00
$labels -> shift ( 2 );
2023-10-03 12:15:21 +00:00
}
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 ();
2024-05-24 02:34:28 +00:00
else
$p = 0 ;
2023-04-22 11:30:30 +00:00
2024-05-24 02:34:28 +00:00
// We'll assume f0
if ( ! is_null ( $f = $this -> parse ( 'f' , $labels -> first ())))
$labels -> shift ();
else
$f = 0 ;
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-09-06 00:50:18 +00:00
// If the query doesnt end with .ftn, then the remainder of the query is the domain name
if (( $labels -> search ( self :: TLD ) !== FALSE ))
$d = $labels -> shift ();
else
$d = '' ;
2023-06-12 11:51:55 +00:00
// Make sure we have a root/base domain
if ( ! $labels -> count ())
return $this -> nameerr ();
$rootdn = $labels -> join ( '.' );
2023-09-06 00:50:18 +00:00
if ( ! $d && ( $rootdn !== self :: TLD ))
$d = Domain :: where ( 'dnsdomain' , $rootdn ) -> single () ? -> name ;
2023-06-12 11:51:55 +00:00
$ao = Address :: findFTN ( sprintf ( '%d:%d/%d.%d@%s' , $z , $n , $f , $p , $d ));
// Check we have the right record
2023-10-05 01:09:56 +00:00
if (( ! $ao ) || (( $rootdn !== self :: TLD ) && (( ! $ao -> zone -> domain -> dnsdomain ) || ( $ao -> zone -> domain -> dnsdomain !== $rootdn )))) {
2023-06-12 11:51:55 +00:00
Log :: alert ( sprintf ( '%s:= No DNS record for [%d:%d/%d.%d@%s]' , self :: LOGKEY , $z , $n , $f , $p , $d ));
2024-06-26 22:20:52 +00:00
2023-06-12 11:51:55 +00:00
return $this -> nameerr ();
}
2023-04-22 11:30:30 +00:00
2023-10-03 12:15:21 +00:00
switch ( $this -> query -> type ) {
case self :: DNS_TYPE_SRV :
2023-11-17 11:03:00 +00:00
if (( $ao -> system -> address ) && ( $xx = $ao -> system -> mailers -> where ( 'id' , $mailer -> id ) -> pop ())) {
2024-06-26 22:20:52 +00:00
Log :: info ( sprintf ( '%s:= Returning [%s] for DNS query [%s]' , self :: LOGKEY , $ao -> system -> address , $ao -> ftn ));
2023-10-03 12:15:21 +00:00
return $this -> reply (
self :: DNS_NOERROR ,
[ serialize ([
2023-10-05 04:23:49 +00:00
0 , // priority
1 , // weight
2023-10-03 12:15:21 +00:00
$xx -> pivot -> port ,
$this -> domain_split ( $ao -> system -> address ),
]) => self :: DNS_TYPE_SRV ]);
} else {
2024-06-26 22:20:52 +00:00
Log :: alert ( sprintf ( '%s:! No/incomplete hostname/port details for [%d] for DNS query [%s]' , self :: LOGKEY , $ao -> system -> id , $ao -> ftn ));
2023-11-12 07:14:53 +00:00
return $this -> nodata ();
2023-10-03 12:15:21 +00:00
}
case self :: DNS_TYPE_TXT :
2023-10-05 01:09:56 +00:00
Log :: info ( sprintf ( '%s:= Returning [%s] for DNS query [%s]' , self :: LOGKEY , $ao -> system -> name , $ao -> ftn ));
2023-10-03 12:15:21 +00:00
return $this -> reply (
self :: DNS_NOERROR ,
[ serialize ( $ao -> system -> name ) => self :: DNS_TYPE_TXT ]);
default :
2024-06-26 22:20:52 +00:00
Log :: info ( sprintf ( '%s:= Returning [%s] for DNS query [%s]' , self :: LOGKEY , $ao -> system -> address ? : 'NO ADDRESS' , $ao -> ftn ));
2023-10-03 12:15:21 +00:00
2023-10-05 01:09:56 +00:00
return ( ! $ao -> system -> address )
2023-11-17 11:03:00 +00:00
? $this -> nodata ()
2023-10-05 01:09:56 +00:00
: $this -> reply (
self :: DNS_NOERROR ,
[ serialize ( $this -> domain_split ( $ao -> system -> address )) => self :: DNS_TYPE_CNAME ]);
2023-10-03 12:15:21 +00:00
}
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 :
2024-06-01 02:55:44 +00:00
Log :: notice ( sprintf ( '%s:! We dont support DNS query types [%d]' , self :: LOGKEY , $this -> query -> type ));
2023-04-23 13:08:30 +00:00
2023-11-12 07:14:53 +00:00
return $this -> reply ( self :: DNS_NOTIMPLEMENTED ,[], $this -> soa ());
2023-06-12 11:51:55 +00:00
}
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-11-12 07:14:53 +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-06-12 11:51:55 +00:00
private function nameerr () : int
2023-04-22 11:30:30 +00:00
{
2024-06-01 02:55:44 +00:00
Log :: notice ( 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-11-12 07:14:53 +00:00
return $this -> reply ( self :: DNS_NAMEERR ,[], $this -> soa ());
}
private function nodata () : int
{
2024-06-01 02:55:44 +00:00
Log :: notice ( sprintf ( '%s:! DNS query for a resource we dont manage [%s] in our zone(s)' , self :: LOGKEY , $this -> query -> domain ));
2023-11-12 07:14:53 +00:00
return $this -> reply ( self :: DNS_NOERROR ,[], $this -> soa ());
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 = [];
2024-04-12 05:29:11 +00:00
return ( preg_match ( '/^' . $prefix . '([0-9]+)+/' , $label , $m ) && ( $m [ 1 ] <= Address :: ADDRESS_FIELD_MAX ))
2023-04-22 11:30:30 +00:00
? $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 ;
}
/**
* 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-10-03 12:15:21 +00:00
case self :: DNS_TYPE_SRV :
$a .= pack ( 'nnn' , $ars [ 0 ], $ars [ 1 ], $ars [ 2 ]);
$a .= $ars [ 3 ];
break ;
case self :: DNS_TYPE_TXT :
$a .= pack ( 'C' , strlen ( $ars )) . $ars ;
break ;
2023-04-22 11:30:30 +00:00
}
$reply .= pack ( 'n' , strlen ( $a )) . $a ;
return $reply ;
}
2023-11-12 07:14:53 +00:00
private function soa () : array
{
return
[ serialize ([
2023-11-28 08:57:47 +00:00
$this -> domain_split ( config ( 'fido.dns_ns' )),
2023-11-12 07:14:53 +00:00
$this -> domain_split ( Str :: replace ( '@' , '.' , config ( 'app.mail.mail_from' , 'nobody@' . gethostname ()))),
1 , // Serial
self :: DEFAULT_TTL , // Refresh
self :: DEFAULT_TTL , // Retry
self :: DEFAULT_TTL * 7 , // Expire
self :: DEFAULT_TTL // Minimum cache
]) => self :: DNS_TYPE_SOA ];
}
2023-04-22 11:30:30 +00:00
}