Skip to content

Commit

Permalink
Support change stream context parameters via the param URI component
Browse files Browse the repository at this point in the history
Add in more test cases
  • Loading branch information
lucasnetau committed Aug 4, 2022
1 parent 79ce57c commit 454bd66
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 8 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,14 @@ $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
Expand Down
31 changes: 24 additions & 7 deletions src/Query/TcpTransportExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ class TcpTransportExecutor implements ExecutorInterface
/** @var string */
private $readChunk = 0xffff;

private $connection_parameters = array();

/**
* @param string $nameserver
* @param ?LoopInterface $loop
Expand All @@ -155,6 +157,11 @@ public function __construct($nameserver, LoopInterface $loop = null)
throw new \InvalidArgumentException('Invalid nameserver address given');
}

//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();
Expand Down Expand Up @@ -183,19 +190,22 @@ public function query(Query $query)

if ($this->socket === null) {
//Setup TLS context if requested
$cOption = null;
$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
}
$cOption = array(
'ssl'=> array(
'verify_peer' => true,
'verify_peer_name' => true,
'allow_self_signed' => false,
)
// 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, $context);
Expand Down Expand Up @@ -373,6 +383,13 @@ 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
// PHP < 7.3.3 (and PHP < 7.2.15) suffers from a bug where feof() might
Expand Down
2 changes: 1 addition & 1 deletion tests/FunctionalResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public function testResolveGoogleOverTlsResolves()
}

$factory = new Factory();
$this->resolver = $factory->create('tls://8.8.8.8', $this->loop);
$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());
Expand Down
195 changes: 195 additions & 0 deletions tests/Query/TcpTransportExecutorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,80 @@ function ($e) use (&$exception) {
));
}

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());
$this->assertStringContainsString('certificate verify failed', $exception->getMessage());
}

public function testCryptoEnabledAfterConnectingToTlsDnsServer()
{
if (defined('HHVM_VERSION') || \PHP_VERSION_ID < 50600) {
Expand All @@ -990,6 +1064,62 @@ public function testCryptoEnabledAfterConnectingToTlsDnsServer()

$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) {
Expand All @@ -1004,8 +1134,73 @@ function ($e) use (&$exception) {
$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());
}
}

0 comments on commit 454bd66

Please sign in to comment.