Skip to content

Enable support for DNS over TLS (RFC 7858) #214

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ easily be used to create a DNS server.
* [Advanced usage](#advanced-usage)
* [UdpTransportExecutor](#udptransportexecutor)
* [TcpTransportExecutor](#tcptransportexecutor)
* [TlsTransportExecutor](#tlstransportexecutor)
* [SelectiveTransportExecutor](#selectivetransportexecutor)
* [HostsFileExecutor](#hostsfileexecutor)
* [Install](#install)
Expand Down Expand Up @@ -336,6 +337,30 @@ $executor = new CoopExecutor(
packages. Higher-level components should take advantage of the Socket
component instead of reimplementing this socket logic from scratch.

### TlsTransportExecutor
The TlsTransportExecutor builds upon TcpTransportExecutor
providing support for 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
Expand Down
88 changes: 63 additions & 25 deletions src/Query/TcpTransportExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -74,6 +74,7 @@
* 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.
*
*/
class TcpTransportExecutor implements ExecutorInterface
{
Expand All @@ -85,10 +86,10 @@ class TcpTransportExecutor implements ExecutorInterface
/**
* @var ?resource
*/
private $socket;
protected $socket;

/**
* @var Deferred[]
* @var Promise\Deferred[]
*/
private $pending = array();

Expand Down Expand Up @@ -128,7 +129,13 @@ class TcpTransportExecutor implements ExecutorInterface
private $readPending = false;

/** @var string */
private $readChunk = 0xffff;
protected $readChunk = 0xffff;

/** @var null|int */
protected $writeChunk = null;

/** @var array Connection parameters to provide to stream_context_create */
private $connectionParameters = array();

/**
* @param string $nameserver
Expand All @@ -146,6 +153,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->connectionParameters);
}

$this->nameserver = 'tcp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53);
$this->loop = $loop ?: Loop::get();
$this->parser = new Parser();
Expand All @@ -164,26 +176,28 @@ 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'
));
}

$queryData = \pack('n', $length) . $queryData;

if ($this->socket === null) {
//Setup stream context if requested ($options must be null if connectionParameters is an empty array
$context = stream_context_create((empty($this->connectionParameters) ? null : $this->connectionParameters));
// 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
));
}

// set socket to non-blocking and wait for it to become writable (connection success/rejected)
\stream_set_blocking($socket, false);
if (\function_exists('stream_set_chunk_size')) {
if ($this->readChunk !== -1 && \function_exists('stream_set_chunk_size')) {
\stream_set_chunk_size($socket, $this->readChunk); // @codeCoverageIgnore
}
$this->socket = $socket;
Expand All @@ -203,7 +217,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]);
Expand All @@ -225,7 +239,7 @@ public function handleWritable()
{
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')) {
Expand All @@ -247,7 +261,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
Expand All @@ -256,18 +270,34 @@ public function handleWritable()
$errstr = isset($m[2]) ? $m[2] : $error;
});

$written = \fwrite($this->socket, $this->writeBuffer);

\restore_error_handler();
if ($this->writeChunk !== null) {
$written = \fwrite($this->socket, $this->writeBuffer, $this->writeChunk);
} 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 {
Expand All @@ -282,9 +312,15 @@ public function handleWritable()
*/
public function handleRead()
{
// 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);
// @codeCoverageIgnoreStart
if (null === $this->socket) {
$this->closeError('Connection to DNS server ' . $this->nameserver . ' lost');
return;
}
// @codeCoverageIgnoreEnd

$chunk = @\stream_get_contents($this->socket, $this->readChunk);

if ($chunk === false || $chunk === '') {
$this->closeError('Connection to DNS server ' . $this->nameserver . ' lost');
return;
Expand Down Expand Up @@ -351,8 +387,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(
Expand Down
Loading