diff --git a/README.md b/README.md index 6bc53f97..18ef95bd 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ easily be used to create a DNS server. * [Advanced usage](#advanced-usage) * [UdpTransportExecutor](#udptransportexecutor) * [TcpTransportExecutor](#tcptransportexecutor) + * [DNS over TLS](#dns-over-tls-dot) * [SelectiveTransportExecutor](#selectivetransportexecutor) * [HostsFileExecutor](#hostsfileexecutor) * [Install](#install) @@ -336,6 +337,27 @@ $executor = new CoopExecutor( packages. Higher-level components should take advantage of the Socket component instead of reimplementing this socket logic from scratch. +#### DNS over TLS (DoT) +DoT provides secure DNS lookups over Transport Layer Security (TLS). +The tls:// scheme must be provided when configuring nameservers to +enable DoT communication to a TLS supporting DNS server. +The port 853 is used by default. + +```php +$executor = new TcpTransportExecutor('tls://8.8.8.8'); +```` + +> Note: To ensure security and privacy, DoT resolvers typically only support + TLS 1.2 and above. DoT is not supported on legacy PHP < 5.6 and HHVM + +##### TLS Configuration +[SSL Context parameters](https://www.php.net/manual/en/context.ssl.php) can be set appending passing query parameters to the nameserver URI in the format `wrapper[parameter]=value`. + +```php +// Verify that the 8.8.8.8 resolver's certificate CN matches dns.google +$executor = new TcpTransportExecutor('tls://8.8.8.8?ssl[peer_name]=dns.google'); +```` + ### SelectiveTransportExecutor The `SelectiveTransportExecutor` class can be used to diff --git a/src/Query/TcpTransportExecutor.php b/src/Query/TcpTransportExecutor.php index bfaedbae..fbd0bea9 100644 --- a/src/Query/TcpTransportExecutor.php +++ b/src/Query/TcpTransportExecutor.php @@ -7,7 +7,7 @@ use React\Dns\Protocol\Parser; use React\EventLoop\Loop; use React\EventLoop\LoopInterface; -use React\Promise\Deferred; +use React\Promise; /** * Send DNS queries over a TCP/IP stream transport. @@ -74,6 +74,9 @@ * organizational reasons to avoid a cyclic dependency between the two * packages. Higher-level components should take advantage of the Socket * component instead of reimplementing this socket logic from scratch. + * + * Support for DNS over TLS can be enabled via specifying the nameserver with scheme tls:// + * @link https://tools.ietf.org/html/rfc7858 */ class TcpTransportExecutor implements ExecutorInterface { @@ -88,7 +91,7 @@ class TcpTransportExecutor implements ExecutorInterface private $socket; /** - * @var Deferred[] + * @var Promise\Deferred[] */ private $pending = array(); @@ -97,6 +100,12 @@ class TcpTransportExecutor implements ExecutorInterface */ private $names = array(); + /** @var bool */ + private $tls = false; + + /** @var bool */ + private $cryptoEnabled = false; + /** * Maximum idle time when socket is current unused (i.e. no pending queries outstanding) * @@ -130,6 +139,8 @@ class TcpTransportExecutor implements ExecutorInterface /** @var string */ private $readChunk = 0xffff; + private $connection_parameters = array(); + /** * @param string $nameserver * @param ?LoopInterface $loop @@ -142,11 +153,17 @@ public function __construct($nameserver, LoopInterface $loop = null) } $parts = \parse_url((\strpos($nameserver, '://') === false ? 'tcp://' : '') . $nameserver); - if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'tcp' || @\inet_pton(\trim($parts['host'], '[]')) === false) { + if (!isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('tcp','tls'), true) || @\inet_pton(\trim($parts['host'], '[]')) === false) { throw new \InvalidArgumentException('Invalid nameserver address given'); } - $this->nameserver = 'tcp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53); + //Parse any connection parameters to be supplied to stream_context_create() + if (isset($parts['query'])) { + parse_str($parts['query'], $this->connection_parameters); + } + + $this->tls = $parts['scheme'] === 'tls'; + $this->nameserver = 'tcp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : ($this->tls ? 853 : 53)); $this->loop = $loop ?: Loop::get(); $this->parser = new Parser(); $this->dumper = new BinaryDumper(); @@ -164,7 +181,7 @@ public function query(Query $query) $queryData = $this->dumper->toBinary($request); $length = \strlen($queryData); if ($length > 0xffff) { - return \React\Promise\reject(new \RuntimeException( + return Promise\reject(new \RuntimeException( 'DNS query for ' . $query->describe() . ' failed: Query too large for TCP transport' )); } @@ -172,10 +189,28 @@ public function query(Query $query) $queryData = \pack('n', $length) . $queryData; if ($this->socket === null) { + //Setup TLS context if requested + $cOption = array(); + if ($this->tls) { + if (!\function_exists('stream_socket_enable_crypto') || defined('HHVM_VERSION') || \PHP_VERSION_ID < 50600) { + return Promise\reject(new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8 or PHP < 5.6?)')); // @codeCoverageIgnore + } + // Setup sane defaults for SSL to ensure secure connection to the DNS server + $cOption['ssl'] = array( + 'verify_peer' => true, + 'verify_peer_name' => true, + 'allow_self_signed' => false, + ); + } + $cOption = array_merge($cOption, $this->connection_parameters); + if (empty($cOption)) { + $cOption = null; + } + $context = stream_context_create($cOption); // create async TCP/IP connection (may take a while) - $socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0, \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT); + $socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0, \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT, $context); if ($socket === false) { - return \React\Promise\reject(new \RuntimeException( + return Promise\reject(new \RuntimeException( 'DNS query for ' . $query->describe() . ' failed: Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')', $errno )); @@ -203,7 +238,7 @@ public function query(Query $query) $names =& $this->names; $that = $this; - $deferred = new Deferred(function () use ($that, &$names, $request) { + $deferred = new Promise\Deferred(function () use ($that, &$names, $request) { // remove from list of pending names, but remember pending query $name = $names[$request->id]; unset($names[$request->id]); @@ -223,9 +258,51 @@ public function query(Query $query) */ public function handleWritable() { + if ($this->tls && false === $this->cryptoEnabled) { + $error = null; + \set_error_handler(function ($_, $errstr) use (&$error) { + $error = \str_replace(array("\r", "\n"), ' ', $errstr); + + // remove useless function name from error message + if (($pos = \strpos($error, "): ")) !== false) { + $error = \substr($error, $pos + 3); + } + }); + + $method = \STREAM_CRYPTO_METHOD_TLS_CLIENT; + if (\PHP_VERSION_ID < 70200 && \PHP_VERSION_ID >= 50600) { + $method |= \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; // @codeCoverageIgnore + } + + $result = \stream_socket_enable_crypto($this->socket, true, $method); + + \restore_error_handler(); + + if (true === $result) { + $this->cryptoEnabled = true; + } elseif (false === $result) { + if (\feof($this->socket) || $error === null) { + // EOF or failed without error => connection closed during handshake + $this->closeError( + 'Connection lost during TLS handshake (ECONNRESET)', + \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 104 + ); + } else { + // handshake failed with error message + $this->closeError( + $error + ); + } + return; + } else { + // need more data, will retry + return; + } + } + if ($this->readPending === false) { $name = @\stream_socket_get_name($this->socket, true); - if ($name === false) { + if (!is_string($name)) { //PHP: false, HHVM: null on error // Connection failed? Check socket error if available for underlying errno/errstr. // @codeCoverageIgnoreStart if (\function_exists('socket_import_stream')) { @@ -247,7 +324,7 @@ public function handleWritable() } $errno = 0; - $errstr = ''; + $errstr = null; \set_error_handler(function ($_, $error) use (&$errno, &$errstr) { // Match errstr from PHP's warning message. // fwrite(): Send of 327712 bytes failed with errno=32 Broken pipe @@ -256,18 +333,42 @@ public function handleWritable() $errstr = isset($m[2]) ? $m[2] : $error; }); - $written = \fwrite($this->socket, $this->writeBuffer); - - \restore_error_handler(); + // PHP < 7.1.4 (and PHP < 7.0.18) suffers from a bug when writing big + // chunks of data over TLS streams at once. + // We try to work around this by limiting the write chunk size to 8192 + // bytes for older PHP versions only. + // This is only a work-around and has a noticable performance penalty on + // affected versions. Please update your PHP version. + // This applies only to configured TLS connections + // See https://github.com/reactphp/socket/issues/105 + if ($this->tls && (\PHP_VERSION_ID < 70018 || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70104))) { + $written = \fwrite($this->socket, $this->writeBuffer, 8192); // @codeCoverageIgnore + } else { + $written = \fwrite($this->socket, $this->writeBuffer); + } - if ($written === false || $written === 0) { - $this->closeError( - 'Unable to send query to DNS server ' . $this->nameserver . ' (' . $errstr . ')', - $errno - ); - return; + // Only report errors if *nothing* could be sent and an error has been raised, or we are unable to retrieve the remote socket name (connection dead) [HHVM]. + // Ignore non-fatal warnings if *some* data could be sent. + // Any hard (permanent) error will fail to send any data at all. + // Sending excessive amounts of data will only flush *some* data and then + // report a temporary error (EAGAIN) which we do not raise here in order + // to keep the stream open for further tries to write. + // Should this turn out to be a permanent error later, it will eventually + // send *nothing* and we can detect this. + if (($written === false || $written === 0)) { + $name = @\stream_socket_get_name($this->socket, true); + if (!is_string($name) || $errstr !== null) { + \restore_error_handler(); + $this->closeError( + 'Unable to send query to DNS server ' . $this->nameserver . ' (' . $errstr . ')', + $errno + ); + return; + } } + \restore_error_handler(); + if (isset($this->writeBuffer[$written])) { $this->writeBuffer = \substr($this->writeBuffer, $written); } else { @@ -282,9 +383,30 @@ public function handleWritable() */ public function handleRead() { + // @codeCoverageIgnoreStart + if (null === $this->socket) { + $this->closeError('Connection to DNS server ' . $this->nameserver . ' lost'); + return; + } + // @codeCoverageIgnoreEnd + // read one chunk of data from the DNS server // any error is fatal, this is a stream of TCP/IP data - $chunk = @\fread($this->socket, $this->readChunk); + // PHP < 7.3.3 (and PHP < 7.2.15) suffers from a bug where feof() might + // block with 100% CPU usage on fragmented TLS records. + // We try to work around this by always consuming the complete receive + // buffer at once to avoid stale data in TLS buffers. This is known to + // work around high CPU usage for well-behaving peers, but this may + // cause very large data chunks for high throughput scenarios. The buggy + // behavior can still be triggered due to network I/O buffers or + // malicious peers on affected versions, upgrading is highly recommended. + // @link https://bugs.php.net/bug.php?id=77390 + if ($this->tls && (\PHP_VERSION_ID < 70215 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70303))) { + $chunk = @\stream_get_contents($this->socket, -1); // @codeCoverageIgnore + } else { + $chunk = @\stream_get_contents($this->socket, $this->readChunk); + } + if ($chunk === false || $chunk === '') { $this->closeError('Connection to DNS server ' . $this->nameserver . ' lost'); return; @@ -351,8 +473,10 @@ public function closeError($reason, $code = 0) $this->idleTimer = null; } - @\fclose($this->socket); - $this->socket = null; + if (null !== $this->socket) { + @\fclose($this->socket); + $this->socket = null; + } foreach ($this->names as $id => $name) { $this->pending[$id]->reject(new \RuntimeException( diff --git a/src/Resolver/Factory.php b/src/Resolver/Factory.php index 5fe608cb..018725bd 100644 --- a/src/Resolver/Factory.php +++ b/src/Resolver/Factory.php @@ -165,7 +165,7 @@ private function createSingleExecutor($nameserver, LoopInterface $loop) { $parts = \parse_url($nameserver); - if (isset($parts['scheme']) && $parts['scheme'] === 'tcp') { + if (isset($parts['scheme']) && in_array($parts['scheme'], array('tcp','tls'), true)) { $executor = $this->createTcpExecutor($nameserver, $loop); } elseif (isset($parts['scheme']) && $parts['scheme'] === 'udp') { $executor = $this->createUdpExecutor($nameserver, $loop); diff --git a/tests/FunctionalResolverTest.php b/tests/FunctionalResolverTest.php index 9cb05615..19ac3420 100644 --- a/tests/FunctionalResolverTest.php +++ b/tests/FunctionalResolverTest.php @@ -75,6 +75,66 @@ public function testResolveGoogleOverTcpResolves() $this->loop->run(); } + /** + * @group internet + */ + public function testResolveGoogleOverTlsResolves() + { + if (defined('HHVM_VERSION') || \PHP_VERSION_ID < 50600) { + $this->markTestSkipped('DNS over TLS not supported on legacy PHP'); + } + + $factory = new Factory(); + $this->resolver = $factory->create('tls://8.8.8.8?socket[tcp_nodelay]=true', $this->loop); + + $promise = $this->resolver->resolve('google.com'); + $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); + + $this->loop->run(); + } + + /** + * @group internet + */ + public function testAttemptTlsOnNonTlsPortRejects() + { + if (defined('HHVM_VERSION') || \PHP_VERSION_ID < 50600) { + $this->markTestSkipped('DNS over TLS not supported on legacy PHP'); + } + + $factory = new Factory(); + $this->resolver = $factory->create('tls://8.8.8.8:53', $this->loop); + + $promise = $this->resolver->resolve('google.com'); + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + + $this->loop->run(); + } + + /** + * @group internet + */ + public function testUnsupportedLegacyPhpOverTlsRejectsWithBadMethodCall() + { + if (!(defined('HHVM_VERSION') || \PHP_VERSION_ID < 50600)) { + $this->markTestSkipped('Tests not relevant to recent PHP versions'); + } + + $factory = new Factory(); + $this->resolver = $factory->create('tls://8.8.8.8', $this->loop); + + $promise = $this->resolver->resolve('google.com'); + $exception = null; + $promise->then($this->expectCallableNever(), function ($reason) use (&$exception) { + $exception = $reason; + }); + + /** @var \BadMethodCallException $exception */ + $this->assertInstanceOf('BadMethodCallException', $exception); + + $this->loop->run(); + } + /** * @group internet */ diff --git a/tests/Query/TcpTransportExecutorTest.php b/tests/Query/TcpTransportExecutorTest.php index 860ad0dc..38e4b8d0 100644 --- a/tests/Query/TcpTransportExecutorTest.php +++ b/tests/Query/TcpTransportExecutorTest.php @@ -403,7 +403,7 @@ public function testQueryRejectsWhenClientKeepsSendingWhenServerClosesSocketWith $this->assertNull($error); $exception = null; - $promise->then(null, function ($reason) use (&$exception) { + $promise->then($this->expectCallableNever(), function ($reason) use (&$exception) { $exception = $reason; }); @@ -413,6 +413,7 @@ public function testQueryRejectsWhenClientKeepsSendingWhenServerClosesSocketWith 'Unable to send query to DNS server tcp://' . $address . ' (', defined('SOCKET_EPIPE') && !defined('HHVM_VERSION') ? (PHP_OS !== 'Darwin' || $writePending ? SOCKET_EPIPE : SOCKET_EPROTOTYPE) : null ); + $this->assertNotNull($exception, 'Promise did not reject with an Exception'); throw $exception; } @@ -938,4 +939,272 @@ public function testQueryAgainAfterPreviousQueryResolvedWillReuseSocketAndCancel // trigger second query $executor->query($query); } + + public function testQueryRejectsWhenTlsCannotBeEstablished() + { + if (defined('HHVM_VERSION') || \PHP_VERSION_ID < 50600) { + $this->markTestSkipped('DNS over TLS not supported on legacy PHP'); + } + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $server = \stream_socket_server('tcp://127.0.0.1:0'); + $address = \stream_socket_get_name($server, false); + $executor = new TcpTransportExecutor('tls://' . $address, $loop); + + $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); + + $exception = null; + $executor->query($query)->then( + null, + function ($e) use (&$exception) { + $exception = $e; + } + ); + + $ref = new \ReflectionProperty($executor, 'writePending'); + $ref->setAccessible(true); + while($ref->getValue($executor)) { + //Call handleWritable as many times as required to perform the attempted TLS handshake + $executor->handleWritable(); + @\stream_socket_accept($server,0); + } + + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertContains($exception->getMessage(), array( + 'DNS query for google.com (A) failed: Connection lost during TLS handshake (ECONNRESET)', + 'DNS query for google.com (A) failed: SSL: Undefined error: 0', + )); + } + + public function testQueryRejectsWhenTlsClosedDuringHandshake() + { + if (defined('HHVM_VERSION') || \PHP_VERSION_ID < 50600) { + $this->markTestSkipped('DNS over TLS not supported on legacy PHP'); + } + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $server = \stream_socket_server('tcp://127.0.0.1:0'); + $address = \stream_socket_get_name($server, false); + $executor = new TcpTransportExecutor('tls://' . $address, $loop); + + $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); + + $exception = null; + $executor->query($query)->then( + null, + function ($e) use (&$exception) { + $exception = $e; + } + ); + + $ref = new \ReflectionProperty($executor, 'writePending'); + $ref->setAccessible(true); + while($ref->getValue($executor)) { + //Call handleWritable as many times as required to perform the attempted TLS handshake + $executor->handleWritable(); + $client = @\stream_socket_accept($server,0); + if (false !== $client) { + fclose($client); + } + } + + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertContains($exception->getMessage(), array( + 'DNS query for google.com (A) failed: Connection lost during TLS handshake (ECONNRESET)', + 'DNS query for google.com (A) failed: SSL: Undefined error: 0', + )); + } + + public function testQueryRejectsWhenTlsCertificateVerificationFails() + { + if (defined('HHVM_VERSION') || \PHP_VERSION_ID < 50600) { + $this->markTestSkipped('DNS over TLS not supported on legacy PHP'); + } + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + // Connect to self-signed.badssl.com https://github.com/chromium/badssl.com + $executor = new TcpTransportExecutor('tls://104.154.89.105:443', $loop); + + $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); + + $executor->query($query)->then( + null, + function ($e) use (&$exception) { + $exception = $e; + } + ); + + $ref = new \ReflectionProperty($executor, 'writePending'); + $ref->setAccessible(true); + while($ref->getValue($executor)) { + //Call handleWritable as many times as required to perform the TLS handshake + $executor->handleWritable(); + } + + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertStringStartsWith('DNS query for google.com (A) failed: SSL operation failed with code ', $exception->getMessage()); + if (method_exists($this, 'assertStringContainsString')) { + $this->assertStringContainsString('certificate verify failed', $exception->getMessage()); + } else { + $this->assertContains('certificate verify failed', $exception->getMessage()); + } + } + + public function testCryptoEnabledAfterConnectingToTlsDnsServer() + { + if (defined('HHVM_VERSION') || \PHP_VERSION_ID < 50600) { + $this->markTestSkipped('DNS over TLS not supported on legacy PHP'); + } + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $executor = new TcpTransportExecutor('tls://8.8.8.8', $loop); + + $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); + + $executor->query($query); + + $ref = new \ReflectionProperty($executor, 'writePending'); + $ref->setAccessible(true); + while($ref->getValue($executor)) { + //Call handleWritable as many times as required to perform the TLS handshake + $executor->handleWritable(); + } + + $ref = new \ReflectionProperty($executor, 'cryptoEnabled'); + $ref->setAccessible(true); + $this->assertTrue($ref->getValue($executor)); + } + + public function testCryptoEnabledWithPeerFingerprintMatch() + { + if (defined('HHVM_VERSION') || \PHP_VERSION_ID < 50600) { + $this->markTestSkipped('DNS over TLS not supported on legacy PHP'); + } + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + //1.1.1.1 used here. Google 8.8.8.8 uses two different certs so fingerprint match can fail + $fingerprint = '099d03214d1414a5325db61090e73ddb94f37d72'; + $executor = new TcpTransportExecutor('tls://1.1.1.1?ssl[peer_fingerprint]=' . $fingerprint, $loop); + + $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); + + $executor->query($query); + + $ref = new \ReflectionProperty($executor, 'writePending'); + $ref->setAccessible(true); + while($ref->getValue($executor)) { + //Call handleWritable as many times as required to perform the TLS handshake + $executor->handleWritable(); + } + + $ref = new \ReflectionProperty($executor, 'cryptoEnabled'); + $ref->setAccessible(true); + $this->assertTrue($ref->getValue($executor)); + } + + public function testCryptoFailureWithPeerFingerprintMismatch() + { + if (defined('HHVM_VERSION') || \PHP_VERSION_ID < 50600) { + $this->markTestSkipped('DNS over TLS not supported on legacy PHP'); + } + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $invalid_fingerprint = sha1('invalid'); + $executor = new TcpTransportExecutor('tls://8.8.8.8?ssl[peer_fingerprint]=' . $invalid_fingerprint, $loop); + + $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); + + $exception = null; + $executor->query($query)->then( + null, + function ($e) use (&$exception) { + $exception = $e; + } + ); + + $ref = new \ReflectionProperty($executor, 'writePending'); + $ref->setAccessible(true); + while($ref->getValue($executor)) { + //Call handleWritable as many times as required to perform the TLS handshake + $executor->handleWritable(); + } + + $ref = new \ReflectionProperty($executor, 'cryptoEnabled'); + $ref->setAccessible(true); + $this->assertFalse($ref->getValue($executor)); + + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('DNS query for google.com (A) failed: peer_fingerprint match failure', $exception->getMessage()); + } + + public function testCryptoEnabledWithPeerNameVerified() + { + if (defined('HHVM_VERSION') || \PHP_VERSION_ID < 50600) { + $this->markTestSkipped('DNS over TLS not supported on legacy PHP'); + } + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $executor = new TcpTransportExecutor('tls://8.8.8.8?ssl[peer_name]=dns.google', $loop); + + $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); + + $executor->query($query); + + $ref = new \ReflectionProperty($executor, 'writePending'); + $ref->setAccessible(true); + while($ref->getValue($executor)) { + //Call handleWritable as many times as required to perform the TLS handshake + $executor->handleWritable(); + } + + $ref = new \ReflectionProperty($executor, 'cryptoEnabled'); + $ref->setAccessible(true); + $this->assertTrue($ref->getValue($executor)); + } + + public function testCryptoFailureWithPeerNameVerified() + { + if (defined('HHVM_VERSION') || \PHP_VERSION_ID < 50600) { + $this->markTestSkipped('DNS over TLS not supported on legacy PHP'); + } + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $executor = new TcpTransportExecutor('tls://8.8.8.8?ssl[peer_name]=notgoogle', $loop); + + $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); + + $executor->query($query)->then( + null, + function ($e) use (&$exception) { + $exception = $e; + } + ); + + $ref = new \ReflectionProperty($executor, 'writePending'); + $ref->setAccessible(true); + while($ref->getValue($executor)) { + //Call handleWritable as many times as required to perform the TLS handshake + $executor->handleWritable(); + } + + $ref = new \ReflectionProperty($executor, 'cryptoEnabled'); + $ref->setAccessible(true); + $this->assertFalse($ref->getValue($executor)); + + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('DNS query for google.com (A) failed: Peer certificate CN=`dns.google\' did not match expected CN=`notgoogle\'', $exception->getMessage()); + } }