<?php

/**
 * DNS Library for handling lookups and updates.
 *
 * Copyright (c) 2020, Mike Pultz <mike@mikepultz.com>. All rights reserved.
 *
 * See LICENSE for more details.
 *
 * @category  Networking
 * @package   Net_DNS2
 * @author    Mike Pultz <mike@mikepultz.com>
 * @copyright 2020 Mike Pultz <mike@mikepultz.com>
 * @license   http://www.opensource.org/licenses/bsd-license.php  BSD License
 * @link      https://netdns2.com/
 * @since     File available since Release 0.6.0
 *
 */

/*
 * register the auto-load function
 *
 */
spl_autoload_register('Net_DNS2::autoload');

/**
 * This is the base class for the Net_DNS2_Resolver and Net_DNS2_Updater classes.
 *
 */
class Net_DNS2
{
	/*
	 * the current version of this library
	 */
	const VERSION = '1.5.3';

	/*
	 * the default path to a resolv.conf file
	 */
	const RESOLV_CONF = '/etc/resolv.conf';

	/*
	 * override options from the resolv.conf file
	 *
	 * if this is set, then certain values from the resolv.conf file will override
	 * local settings. This is disabled by default to remain backwards compatible.
	 *
	 */
	public $use_resolv_options = false;

	/*
	 * use TCP only (true/false)
	 */
	public $use_tcp = false;

	/*
	 * DNS Port to use (53)
	 */
	public $dns_port = 53;

	/*
	 * the ip/port for use as a local socket
	 */
	public $local_host = '';
	public $local_port = 0;

	/*
	 * timeout value for socket connections
	 */
	public $timeout = 5;

	/*
	 * randomize the name servers list
	 */
	public $ns_random = false;

	/*
	 * default domains
	 */
	public $domain = '';

	/*
	 * domain search list - not actually used right now
	 */
	public $search_list = [];

	/*
	 * enable cache; either "shared", "file" or "none"
	 */
	public $cache_type = 'none';

	/*
	 * file name to use for shared memory segment or file cache
	 */
	public $cache_file = '/tmp/net_dns2.cache';

	/*
	 * the max size of the cache file (in bytes)
	 */
	public $cache_size = 50000;

	/*
	 * the method to use for storing cache data; either "serialize" or "json"
	 *
	 * json is faster, but can't remember the class names (everything comes back
	 * as a "stdClass Object"; all the data is the same though. serialize is
	 * slower, but will have all the class info.
	 *
	 * defaults to 'serialize'
	 */
	public $cache_serializer = 'serialize';

	/*
	 * by default, according to RFC 1034
	 *
	 * CNAME RRs cause special action in DNS software.  When a name server
	 * fails to find a desired RR in the resource set associated with the
	 * domain name, it checks to see if the resource set consists of a CNAME
	 * record with a matching class.  If so, the name server includes the CNAME
	 * record in the response and restarts the query at the domain name
	 * specified in the data field of the CNAME record.
	 *
	 * this can cause "unexpected" behavious, since i'm sure *most* people
	 * don't know DNS does this; there may be cases where Net_DNS2 returns a
	 * positive response, even though the hostname the user looked up did not
	 * actually exist.
	 *
	 * strict_query_mode means that if the hostname that was looked up isn't
	 * actually in the answer section of the response, Net_DNS2 will return an
	 * empty answer section, instead of an answer section that could contain
	 * CNAME records.
	 *
	 */
	public $strict_query_mode = false;

	/*
	 * if we should set the recursion desired bit to 1 or 0.
	 *
	 * by default this is set to true, we want the DNS server to perform a recursive
	 * request. If set to false, the RD bit will be set to 0, and the server will
	 * not perform recursion on the request.
	 */
	public $recurse = true;

	/*
	 * request DNSSEC values, by setting the DO flag to 1; this actually makes
	 * the resolver add a OPT RR to the additional section, and sets the DO flag
	 * in this RR to 1
	 *
	 */
	public $dnssec = false;

	/*
	 * set the DNSSEC AD (Authentic Data) bit on/off; the AD bit on the request
	 * side was previously undefined, and resolvers we instructed to always clear
	 * the AD bit when sending a request.
	 *
	 * RFC6840 section 5.7 defines setting the AD bit in the query as a signal to
	 * the server that it wants the value of the AD bit, without needed to request
	 * all the DNSSEC data via the DO bit.
	 *
	 */
	public $dnssec_ad_flag = false;

	/*
	 * set the DNSSEC CD (Checking Disabled) bit on/off; turning this off, means
	 * that the DNS resolver will perform it's own signature validation- so the DNS
	 * servers simply pass through all the details.
	 *
	 */
	public $dnssec_cd_flag = false;

	/*
	 * the EDNS(0) UDP payload size to use when making DNSSEC requests
	 * see RFC 4035 section 4.1 - EDNS Support.
	 *
	 * there is some different ideas on the suggest size to supprt; but it seems to
	 * be "at least 1220 bytes, but SHOULD support 4000 bytes.
	 *
	 * we'll just support 4000
	 *
	 */
	public $dnssec_payload_size = 4000;

	/*
	 * the last exeception that was generated
	 */
	public $last_exception = null;

	/*
	 * the list of exceptions by name server
	 */
	public $last_exception_list = [];

	/*
	 * name server list
	 */
	public $nameservers = [];

	/*
	 * local sockets
	 */
	protected $sock = [ Net_DNS2_Socket::SOCK_DGRAM => [], Net_DNS2_Socket::SOCK_STREAM => [] ];

	/*
	 * the TSIG or SIG RR object for authentication
	 */
	protected $auth_signature = null;

	/*
	 * the shared memory segment id for the local cache
	 */
	protected $cache = null;

	/*
	 * internal setting for enabling cache
	 */
	protected $use_cache = false;

	/**
	 * Constructor - base constructor for the Resolver and Updater
	 *
	 * @param mixed $options array of options or null for none
	 *
	 * @throws Net_DNS2_Exception
	 * @access public
	 *
	 */
	public function __construct(array $options = null)
	{
		//
		// load any options that were provided
		//
		if (!empty($options)) {

			foreach ($options as $key => $value) {

				if ($key == 'nameservers') {

					$this->setServers($value);
				} else {

					$this->$key = $value;
				}
			}
		}

		//
		// if we're set to use the local shared memory cache, then
		// make sure it's been initialized
		//
		switch($this->cache_type) {
			case 'shared':
				if (extension_loaded('shmop')) {

					$this->cache = new Net_DNS2_Cache_Shm;
					$this->use_cache = true;
				} else {

					throw new Net_DNS2_Exception(
						'shmop library is not available for cache',
						Net_DNS2_Lookups::E_CACHE_SHM_UNAVAIL
					);
				}
				break;
			case 'file':

				$this->cache = new Net_DNS2_Cache_File;
				$this->use_cache = true;

				break;
			case 'none':
				$this->use_cache = false;
				break;
			default:

				throw new Net_DNS2_Exception(
					'un-supported cache type: ' . $this->cache_type,
					Net_DNS2_Lookups::E_CACHE_UNSUPPORTED
				);
		}
	}

	/**
	 * autoload call-back function; used to auto-load classes
	 *
	 * @param string $name the name of the class
	 *
	 * @return void
	 * @access public
	 *
	 */
	static public function autoload($name)
	{
		//
		// only auto-load our classes
		//
		if (strncmp($name, 'Net_DNS2', 8) == 0) {

			include str_replace('_', '/', $name) . '.php';
		}

		return;
	}

	/**
	 * sets the name servers to be used
	 *
	 * @param mixed $nameservers either an array of name servers, or a file name
	 *                           to parse, assuming it's in the resolv.conf format
	 *
	 * @return boolean
	 * @throws Net_DNS2_Exception
	 * @access public
	 *
	 */
	public function setServers($nameservers)
	{
		//
		// if it's an array, then use it directly
		//
		// otherwise, see if it's a path to a resolv.conf file and if so, load it
		//
		if (is_array($nameservers)) {

			$this->nameservers = $nameservers;

		} else {

			//
			// temporary list of name servers; do it this way rather than just
			// resetting the local nameservers value, just incase an exception
			// is thrown here; this way we might avoid ending up with an empty
			// namservers list.
			//
			$ns = [];

			//
			// check to see if the file is readable
			//
			if (is_readable($nameservers) === true) {

				$data = file_get_contents($nameservers);
				if ($data === false) {
					throw new Net_DNS2_Exception(
						'failed to read contents of file: ' . $nameservers,
						Net_DNS2_Lookups::E_NS_INVALID_FILE
					);
				}

				$lines = explode("\n", $data);

				foreach ($lines as $line) {

					$line = trim($line);

					//
					// ignore empty lines, and lines that are commented out
					//
					if ( (strlen($line) == 0)
					     || ($line[0] == '#')
					     || ($line[0] == ';')
					) {
						continue;
					}

					//
					// ignore lines with no spaces in them.
					//
					if (strpos($line, ' ') === false) {
						continue;
					}

					list($key, $value) = preg_split('/\s+/', $line, 2);

					$key    = trim(strtolower($key));
					$value  = trim(strtolower($value));

					switch($key) {
						case 'nameserver':

							//
							// nameserver can be a IPv4 or IPv6 address
							//
							if ( (self::isIPv4($value) == true)
							     || (self::isIPv6($value) == true)
							) {

								$ns[] = $value;
							} else {

								throw new Net_DNS2_Exception(
									'invalid nameserver entry: ' . $value,
									Net_DNS2_Lookups::E_NS_INVALID_ENTRY
								);
							}
							break;

						case 'domain':
							$this->domain = $value;
							break;

						case 'search':
							$this->search_list = preg_split('/\s+/', $value);
							break;

						case 'options':
							$this->parseOptions($value);
							break;

						default:
							;
					}
				}

				//
				// if we don't have a domain, but we have a search list, then
				// take the first entry on the search list as the domain
				//
				if ( (strlen($this->domain) == 0)
				     && (count($this->search_list) > 0)
				) {
					$this->domain = $this->search_list[0];
				}

			} else {
				throw new Net_DNS2_Exception(
					'resolver file file provided is not readable: ' . $nameservers,
					Net_DNS2_Lookups::E_NS_INVALID_FILE
				);
			}

			//
			// store the name servers locally
			//
			if (count($ns) > 0) {
				$this->nameservers = $ns;
			}
		}

		//
		// remove any duplicates; not sure if we should bother with this- if people
		// put duplicate name servers, who I am to stop them?
		//
		$this->nameservers = array_unique($this->nameservers);

		//
		// check the name servers
		//
		$this->checkServers();

		return true;
	}

	/**
	 * return the internal $sock array
	 *
	 * @return array
	 * @access public
	 */
	public function getSockets()
	{
		return $this->sock;
	}

	/**
	 * give users access to close all open sockets on the resolver object; resetting each
	 * array, calls the destructor on the Net_DNS2_Socket object, which calls the close()
	 * method on each object.
	 *
	 * @return boolean
	 * @access public
	 *
	 */
	public function closeSockets()
	{
		$this->sock[Net_DNS2_Socket::SOCK_DGRAM]    = [];
		$this->sock[Net_DNS2_Socket::SOCK_STREAM]   = [];

		return true;
	}

	/**
	 * parses the options line from a resolv.conf file; we don't support all the options
	 * yet, and using them is optional.
	 *
	 * @param string $value is the options string from the resolv.conf file.
	 *
	 * @return boolean
	 * @access private
	 *
	 */
	private function parseOptions($value)
	{
		//
		// if overrides are disabled (the default), or the options list is empty for some
		// reason, then we don't need to do any of this work.
		//
		if ( ($this->use_resolv_options == false) || (strlen($value) == 0) ) {

			return true;
		}

		$options = preg_split('/\s+/', strtolower($value));

		foreach ($options as $option) {

			//
			// override the timeout value from the resolv.conf file.
			//
			if ( (strncmp($option, 'timeout', 7) == 0) && (strpos($option, ':') !== false) ) {

				list($key, $val) = explode(':', $option);

				if ( ($val > 0) && ($val <= 30) ) {

					$this->timeout = $val;
				}

				//
				// the rotate option just enabled the ns_random option
				//
			} else if (strncmp($option, 'rotate', 6) == 0) {

				$this->ns_random = true;
			}
		}

		return true;
	}

	/**
	 * checks the list of name servers to make sure they're set
	 *
	 * @param mixed $default a path to a resolv.conf file or an array of servers.
	 *
	 * @return boolean
	 * @throws Net_DNS2_Exception
	 * @access protected
	 *
	 */
	protected function checkServers($default = null)
	{
		if (empty($this->nameservers)) {

			if (isset($default)) {

				$this->setServers($default);
			} else {

				throw new Net_DNS2_Exception(
					'empty name servers list; you must provide a list of name '.
					'servers, or the path to a resolv.conf file.',
					Net_DNS2_Lookups::E_NS_INVALID_ENTRY
				);
			}
		}

		return true;
	}

	/**
	 * adds a TSIG RR object for authentication
	 *
	 * @param string $keyname   the key name to use for the TSIG RR
	 * @param string $signature the key to sign the request.
	 * @param string $algorithm the algorithm to use
	 *
	 * @return boolean
	 * @access public
	 * @since  function available since release 1.1.0
	 *
	 */
	public function signTSIG(
		$keyname, $signature = '', $algorithm = Net_DNS2_RR_TSIG::HMAC_MD5
	) {
		//
		// if the TSIG was pre-created and passed in, then we can just used
		// it as provided.
		//
		if ($keyname instanceof Net_DNS2_RR_TSIG) {

			$this->auth_signature = $keyname;

		} else {

			//
			// otherwise create the TSIG RR, but don't add it just yet; TSIG needs
			// to be added as the last additional entry- so we'll add it just
			// before we send.
			//
			$this->auth_signature = Net_DNS2_RR::fromString(
				strtolower(trim($keyname)) .
				' TSIG '. $signature
			);

			//
			// set the algorithm to use
			//
			$this->auth_signature->algorithm = $algorithm;
		}

		return true;
	}

	/**
	 * adds a SIG RR object for authentication
	 *
	 * @param string $filename the name of a file to load the signature from.
	 *
	 * @return boolean
	 * @throws Net_DNS2_Exception
	 * @access public
	 * @since  function available since release 1.1.0
	 *
	 */
	public function signSIG0($filename)
	{
		//
		// check for OpenSSL
		//
		if (extension_loaded('openssl') === false) {

			throw new Net_DNS2_Exception(
				'the OpenSSL extension is required to use SIG(0).',
				Net_DNS2_Lookups::E_OPENSSL_UNAVAIL
			);
		}

		//
		// if the SIG was pre-created, then use it as-is
		//
		if ($filename instanceof Net_DNS2_RR_SIG) {

			$this->auth_signature = $filename;

		} else {

			//
			// otherwise, it's filename which needs to be parsed and processed.
			//
			$private = new Net_DNS2_PrivateKey($filename);

			//
			// create a new Net_DNS2_RR_SIG object
			//
			$this->auth_signature = new Net_DNS2_RR_SIG();

			//
			// reset some values
			//
			$this->auth_signature->name         = $private->signname;
			$this->auth_signature->ttl          = 0;
			$this->auth_signature->class        = 'ANY';

			//
			// these values are pulled from the private key
			//
			$this->auth_signature->algorithm    = $private->algorithm;
			$this->auth_signature->keytag       = $private->keytag;
			$this->auth_signature->signname     = $private->signname;

			//
			// these values are hard-coded for SIG0
			//
			$this->auth_signature->typecovered  = 'SIG0';
			$this->auth_signature->labels       = 0;
			$this->auth_signature->origttl      = 0;

			//
			// generate the dates
			//
			$t = time();

			$this->auth_signature->sigincep     = gmdate('YmdHis', $t);
			$this->auth_signature->sigexp       = gmdate('YmdHis', $t + 500);

			//
			// store the private key in the SIG object for later.
			//
			$this->auth_signature->private_key  = $private;
		}

		//
		// only RSA algorithms are supported for SIG(0)
		//
		switch($this->auth_signature->algorithm) {
			case Net_DNS2_Lookups::DNSSEC_ALGORITHM_RSAMD5:
			case Net_DNS2_Lookups::DNSSEC_ALGORITHM_RSASHA1:
			case Net_DNS2_Lookups::DNSSEC_ALGORITHM_RSASHA256:
			case Net_DNS2_Lookups::DNSSEC_ALGORITHM_RSASHA512:
			case Net_DNS2_Lookups::DNSSEC_ALGORITHM_DSA:
				break;
			default:
				throw new Net_DNS2_Exception(
					'only asymmetric algorithms work with SIG(0)!',
					Net_DNS2_Lookups::E_OPENSSL_INV_ALGO
				);
		}

		return true;
	}

	/**
	 * a simple function to determine if the RR type is cacheable
	 *
	 * @param string $_type the RR type string
	 *
	 * @return bool returns true/false if the RR type if cachable
	 * @access public
	 *
	 */
	public function cacheable($_type)
	{
		switch($_type) {
			case 'AXFR':
			case 'OPT':
				return false;
		}

		return true;
	}

	/**
	 * PHP doesn't support unsigned integers, but many of the RR's return
	 * unsigned values (like SOA), so there is the possibility that the
	 * value will overrun on 32bit systems, and you'll end up with a
	 * negative value.
	 *
	 * 64bit systems are not affected, as their PHP_IN_MAX value should
	 * be 64bit (ie 9223372036854775807)
	 *
	 * This function returns a negative integer value, as a string, with
	 * the correct unsigned value.
	 *
	 * @param string $_int the unsigned integer value to check
	 *
	 * @return string returns the unsigned value as a string.
	 * @access public
	 *
	 */
	public static function expandUint32($_int)
	{
		if ( ($_int < 0) && (PHP_INT_MAX == 2147483647) ) {
			return sprintf('%u', $_int);
		} else {
			return $_int;
		}
	}

	/**
	 * returns true/false if the given address is a valid IPv4 address
	 *
	 * @param string $_address the IPv4 address to check
	 *
	 * @return boolean returns true/false if the address is IPv4 address
	 * @access public
	 *
	 */
	public static function isIPv4($_address)
	{
		//
		// use filter_var() if it's available; it's faster than preg
		//
		if (extension_loaded('filter') == true) {

			if (filter_var($_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) == false) {
				return false;
			}
		} else {

			//
			// do the main check here;
			//
			if (inet_pton($_address) === false) {
				return false;
			}

			//
			// then make sure we're not a IPv6 address
			//
			if (preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $_address) == 0) {
				return false;
			}
		}

		return true;
	}

	/**
	 * returns true/false if the given address is a valid IPv6 address
	 *
	 * @param string $_address the IPv6 address to check
	 *
	 * @return boolean returns true/false if the address is IPv6 address
	 * @access public
	 *
	 */
	public static function isIPv6($_address)
	{
		//
		// use filter_var() if it's available; it's faster than preg
		//
		if (extension_loaded('filter') == true) {
			if (filter_var($_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) == false) {
				return false;
			}
		} else {

			//
			// do the main check here
			//
			if (inet_pton($_address) === false) {
				return false;
			}

			//
			// then make sure it doesn't match a IPv4 address
			//
			if (preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $_address) == 1) {
				return false;
			}
		}

		return true;
	}

	/**
	 * formats the given IPv6 address as a fully expanded IPv6 address
	 *
	 * @param string $_address the IPv6 address to expand
	 *
	 * @return string the fully expanded IPv6 address
	 * @access public
	 *
	 */
	public static function expandIPv6($_address)
	{
		$hex = unpack('H*hex', inet_pton($_address));

		return substr(preg_replace('/([A-f0-9]{4})/', "$1:", $hex['hex']), 0, -1);
	}

	/**
	 * sends a standard Net_DNS2_Packet_Request packet
	 *
	 * @param Net_DNS2_Packet $request a Net_DNS2_Packet_Request object
	 * @param boolean         $use_tcp true/false if the function should
	 *                                 use TCP for the request
	 *
	 * @return Net_DNS2_Packet_Response
	 * @throws Net_DNS2_Exception
	 * @access protected
	 *
	 */
	protected function sendPacket(Net_DNS2_Packet $request, $use_tcp)
	{
		//
		// get the data from the packet
		//
		$data = $request->get();
		if (strlen($data) < Net_DNS2_Lookups::DNS_HEADER_SIZE) {

			throw new Net_DNS2_Exception(
				'invalid or empty packet for sending!',
				Net_DNS2_Lookups::E_PACKET_INVALID,
				null,
				$request
			);
		}

		reset($this->nameservers);

		//
		// randomize the name server list if it's asked for
		//
		if ($this->ns_random == true) {

			shuffle($this->nameservers);
		}

		//
		// loop so we can handle server errors
		//
		$response = null;
		$ns = '';

		while (1) {

			//
			// grab the next DNS server
			//
			$ns = current($this->nameservers);
			next($this->nameservers);

			if ($ns === false) {

				if (is_null($this->last_exception) == false) {

					throw $this->last_exception;
				} else {

					throw new Net_DNS2_Exception(
						'every name server provided has failed',
						Net_DNS2_Lookups::E_NS_FAILED
					);
				}
			}

			//
			// if the use TCP flag (force TCP) is set, or the packet is bigger than our
			// max allowed UDP size- which is either 512, or if this is DNSSEC request,
			// then whatever the configured dnssec_payload_size is.
			//
			$max_udp_size = Net_DNS2_Lookups::DNS_MAX_UDP_SIZE;
			if ($this->dnssec == true)
			{
				$max_udp_size = $this->dnssec_payload_size;
			}

			if ( ($use_tcp == true) || (strlen($data) > $max_udp_size) ) {

				try
				{
					$response = $this->sendTCPRequest($ns, $data, ($request->question[0]->qtype == 'AXFR') ? true : false);

				} catch(Net_DNS2_Exception $e) {

					$this->last_exception = $e;
					$this->last_exception_list[$ns] = $e;

					continue;
				}

				//
				// otherwise, send it using UDP
				//
			} else {

				try
				{
					$response = $this->sendUDPRequest($ns, $data);

					//
					// check the packet header for a trucated bit; if it was truncated,
					// then re-send the request as TCP.
					//
					if ($response->header->tc == 1) {

						$response = $this->sendTCPRequest($ns, $data);
					}

				} catch(Net_DNS2_Exception $e) {

					$this->last_exception = $e;
					$this->last_exception_list[$ns] = $e;

					continue;
				}
			}

			//
			// make sure header id's match between the request and response
			//
			if ($request->header->id != $response->header->id) {

				$this->last_exception = new Net_DNS2_Exception(

					'invalid header: the request and response id do not match.',
					Net_DNS2_Lookups::E_HEADER_INVALID,
					null,
					$request,
					$response
				);

				$this->last_exception_list[$ns] = $this->last_exception;
				continue;
			}

			//
			// make sure the response is actually a response
			//
			// 0 = query, 1 = response
			//
			if ($response->header->qr != Net_DNS2_Lookups::QR_RESPONSE) {

				$this->last_exception = new Net_DNS2_Exception(

					'invalid header: the response provided is not a response packet.',
					Net_DNS2_Lookups::E_HEADER_INVALID,
					null,
					$request,
					$response
				);

				$this->last_exception_list[$ns] = $this->last_exception;
				continue;
			}

			//
			// make sure the response code in the header is ok
			//
			if ($response->header->rcode != Net_DNS2_Lookups::RCODE_NOERROR) {

				$this->last_exception = new Net_DNS2_Exception(

					'DNS request failed: ' .
					Net_DNS2_Lookups::$result_code_messages[$response->header->rcode],
					$response->header->rcode,
					null,
					$request,
					$response
				);

				$this->last_exception_list[$ns] = $this->last_exception;
				continue;
			}

			break;
		}

		return $response;
	}

	/**
	 * cleans up a failed socket and throws the given exception
	 *
	 * @param string  $_proto the protocol of the socket
	 * @param string  $_ns    the name server to use for the request
	 * @param string  $_error the error message to throw at the end of the function
	 *
	 * @throws Net_DNS2_Exception
	 * @access private
	 *
	 */
	private function generateError($_proto, $_ns, $_error)
	{
		if (isset($this->sock[$_proto][$_ns]) == false)
		{
			throw new Net_DNS2_Exception('invalid socket referenced', Net_DNS2_Lookups::E_NS_INVALID_SOCKET);
		}

		//
		// grab the last error message off the socket
		//
		$last_error = $this->sock[$_proto][$_ns]->last_error;

		//
		// remove it from the socket cache; this will call the destructor, which calls close() on the socket
		//
		unset($this->sock[$_proto][$_ns]);

		//
		// throw the error provided
		//
		throw new Net_DNS2_Exception($last_error, $_error);
	}

	/**
	 * sends a DNS request using TCP
	 *
	 * @param string  $_ns   the name server to use for the request
	 * @param string  $_data the raw DNS packet data
	 * @param boolean $_axfr if this is a zone transfer request
	 *
	 * @return Net_DNS2_Packet_Response the reponse object
	 * @throws Net_DNS2_Exception
	 * @access private
	 *
	 */
	private function sendTCPRequest($_ns, $_data, $_axfr = false)
	{
		//
		// grab the start time
		//
		$start_time = microtime(true);

		//
		// see if we already have an open socket from a previous request; if so, try to use
		// that instead of opening a new one.
		//
		if ( (!isset($this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]))
		     || (!($this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns] instanceof Net_DNS2_Socket))
		) {

			//
			// create the socket object
			//
			$this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns] = new Net_DNS2_Socket(
				Net_DNS2_Socket::SOCK_STREAM, $_ns, $this->dns_port, $this->timeout
			);

			//
			// if a local IP address / port is set, then add it
			//
			if (strlen($this->local_host) > 0) {

				$this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]->bindAddress(
					$this->local_host, $this->local_port
				);
			}

			//
			// open the socket
			//
			if ($this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]->open() === false) {

				$this->generateError(Net_DNS2_Socket::SOCK_STREAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
			}
		}

		//
		// write the data to the socket; if it fails, continue on
		// the while loop
		//
		if ($this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]->write($_data) === false) {

			$this->generateError(Net_DNS2_Socket::SOCK_STREAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
		}

		//
		// read the content, using select to wait for a response
		//
		$size = 0;
		$result = null;
		$response = null;

		//
		// handle zone transfer requests differently than other requests.
		//
		if ($_axfr == true) {

			$soa_count = 0;

			while (1) {

				//
				// read the data off the socket
				//
				$result = $this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]->read($size,
					($this->dnssec == true) ? $this->dnssec_payload_size : Net_DNS2_Lookups::DNS_MAX_UDP_SIZE);

				if ( ($result === false) || ($size < Net_DNS2_Lookups::DNS_HEADER_SIZE) ) {

					//
					// if we get an error, then keeping this socket around for a future request, could cause
					// an error- for example, https://github.com/mikepultz/netdns2/issues/61
					//
					// in this case, the connection was timing out, which once it did finally respond, left
					// data on the socket, which could be captured on a subsequent request.
					//
					// since there's no way to "reset" a socket, the only thing we can do it close it.
					//
					$this->generateError(Net_DNS2_Socket::SOCK_STREAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
				}

				//
				// parse the first chunk as a packet
				//
				$chunk = new Net_DNS2_Packet_Response($result, $size);

				//
				// if this is the first packet, then clone it directly, then
				// go through it to see if there are two SOA records
				// (indicating that it's the only packet)
				//
				if (is_null($response) == true) {

					$response = clone $chunk;

					//
					// look for a failed response; if the zone transfer
					// failed, then we don't need to do anything else at this
					// point, and we should just break out.
					//
					if ($response->header->rcode != Net_DNS2_Lookups::RCODE_NOERROR) {
						break;
					}

					//
					// go through each answer
					//
					foreach ($response->answer as $index => $rr) {

						//
						// count the SOA records
						//
						if ($rr->type == 'SOA') {
							$soa_count++;
						}
					}

					//
					// if we have 2 or more SOA records, then we're done;
					// otherwise continue out so we read the rest of the
					// packets off the socket
					//
					if ($soa_count >= 2) {
						break;
					} else {
						continue;
					}

				} else {

					//
					// go through all these answers, and look for SOA records
					//
					foreach ($chunk->answer as $index => $rr) {

						//
						// count the number of SOA records we find
						//
						if ($rr->type == 'SOA') {
							$soa_count++;
						}

						//
						// add the records to a single response object
						//
						$response->answer[] = $rr;
					}

					//
					// if we've found the second SOA record, we're done
					//
					if ($soa_count >= 2) {
						break;
					}
				}
			}

			//
			// everything other than a AXFR
			//
		} else {

			$result = $this->sock[Net_DNS2_Socket::SOCK_STREAM][$_ns]->read($size,
				($this->dnssec == true) ? $this->dnssec_payload_size : Net_DNS2_Lookups::DNS_MAX_UDP_SIZE);

			if ( ($result === false) || ($size < Net_DNS2_Lookups::DNS_HEADER_SIZE) ) {

				$this->generateError(Net_DNS2_Socket::SOCK_STREAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
			}

			//
			// create the packet object
			//
			$response = new Net_DNS2_Packet_Response($result, $size);
		}

		//
		// store the query time
		//
		$response->response_time = microtime(true) - $start_time;

		//
		// add the name server that the response came from to the response object,
		// and the socket type that was used.
		//
		$response->answer_from = $_ns;
		$response->answer_socket_type = Net_DNS2_Socket::SOCK_STREAM;

		//
		// return the Net_DNS2_Packet_Response object
		//
		return $response;
	}

	/**
	 * sends a DNS request using UDP
	 *
	 * @param string  $_ns   the name server to use for the request
	 * @param string  $_data the raw DNS packet data
	 *
	 * @return Net_DNS2_Packet_Response the reponse object
	 * @throws Net_DNS2_Exception
	 * @access private
	 *
	 */
	private function sendUDPRequest($_ns, $_data)
	{
		//
		// grab the start time
		//
		$start_time = microtime(true);

		//
		// see if we already have an open socket from a previous request; if so, try to use
		// that instead of opening a new one.
		//
		if ( (!isset($this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns]))
		     || (!($this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns] instanceof Net_DNS2_Socket))
		) {

			//
			// create the socket object
			//
			$this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns] = new Net_DNS2_Socket(
				Net_DNS2_Socket::SOCK_DGRAM, $_ns, $this->dns_port, $this->timeout
			);

			//
			// if a local IP address / port is set, then add it
			//
			if (strlen($this->local_host) > 0) {

				$this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns]->bindAddress(
					$this->local_host, $this->local_port
				);
			}

			//
			// open the socket
			//
			if ($this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns]->open() === false) {

				$this->generateError(Net_DNS2_Socket::SOCK_DGRAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
			}
		}

		//
		// write the data to the socket
		//
		if ($this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns]->write($_data) === false) {

			$this->generateError(Net_DNS2_Socket::SOCK_DGRAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
		}

		//
		// read the content, using select to wait for a response
		//
		$size = 0;

		$result = $this->sock[Net_DNS2_Socket::SOCK_DGRAM][$_ns]->read($size,
			($this->dnssec == true) ? $this->dnssec_payload_size : Net_DNS2_Lookups::DNS_MAX_UDP_SIZE);

		if (( $result === false) || ($size < Net_DNS2_Lookups::DNS_HEADER_SIZE)) {

			$this->generateError(Net_DNS2_Socket::SOCK_DGRAM, $_ns, Net_DNS2_Lookups::E_NS_SOCKET_FAILED);
		}

		//
		// create the packet object
		//
		$response = new Net_DNS2_Packet_Response($result, $size);

		//
		// store the query time
		//
		$response->response_time = microtime(true) - $start_time;

		//
		// add the name server that the response came from to the response object,
		// and the socket type that was used.
		//
		$response->answer_from = $_ns;
		$response->answer_socket_type = Net_DNS2_Socket::SOCK_DGRAM;

		//
		// return the Net_DNS2_Packet_Response object
		//
		return $response;
	}
}
