Skip to content
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

Google DNS over HTTPS resolver #85

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
11 changes: 6 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@
}
],
"require": {
"react/socket": "^0.8.10",
"react/datagram": "^1.4",
"php": "~7.1",
"ext-curl": "*",
"ext-json": "~1.0",
"ext-simplexml": "*",
"symfony/event-dispatcher": "~4.0",
"psr/log": "^1.0"
"php": "~7.1",
"psr/log": "^1.0",
"react/datagram": "^1.4",
"react/socket": "^0.8.10",
"symfony/event-dispatcher": "~4.0"
},
"autoload": {
"psr-4": {
Expand Down
12 changes: 12 additions & 0 deletions docs/Google-DNS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Google DNS

Resolver class `GoogleDns.php` uses Google's DNS-over-HTTPS service to
resolve records. GoogleDNS resolver could be used as a drop in replacement
instead of `SystemResolver` to avoid eavesdropping on DNS requests.

Upon DNS query server will issue HTTPS request to Google service and obtain
information on query, information will further be delivered to client in form of DNS response.

Resolver at the moment supports `A` and `AAAA` type records.

For more information refer to: https://developers.google.com/speed/public-dns/docs/dns-over-https
14 changes: 14 additions & 0 deletions example/google-dns-example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

require_once __DIR__.'/../vendor/autoload.php';

$stackableResolver = new yswery\DNS\Resolver\StackableResolver([
new yswery\DNS\Resolver\GoogleDnsResolver()
]);

$eventDispatcher = new \Symfony\Component\EventDispatcher\EventDispatcher();
$eventDispatcher->addSubscriber(new \yswery\DNS\EchoLogger());

$server = new yswery\DNS\Server($stackableResolver, $eventDispatcher);

$server->start();
130 changes: 130 additions & 0 deletions src/Resolver/GoogleDnsResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

namespace yswery\DNS\Resolver;

use yswery\DNS\RecordTypeEnum;
use yswery\DNS\ResourceRecord;

class GoogleDnsResolver extends AbstractResolver
{
private const API_ENDPOINT = 'https://dns.google.com/resolve';

private const ANSWER_FIELD_NAME = 'Answer';

private const NAME_QUERY_PARAM = 'name';
private const TYPE_QUERY_PARAM = 'type';
private const TTL_FIELD_NAME = 'TTL';
private const DATA_FIELD_NAME = 'data';

/** @var int */
private $defaultTtl;

public function __construct($defaultTtl = 300)
{
$this->allowRecursion = true;
$this->isAuthoritative = true;
$this->defaultTtl = $defaultTtl;
}

/**
* @param ResourceRecord[] $queries
*
* @return ResourceRecord[]
*/
public function getAnswer(array $queries): array
{
$answers = [];
foreach ($queries as $query) {
$response = $this->request($query->getName(), $query->getType());
$answers[] = $this->createAnswer($query, $response);
}

return array_merge(...$answers);
}

/**
* @param ResourceRecord $query
*
* @param array|null $response
*
* @return ResourceRecord[]
*/
public function createAnswer(ResourceRecord $query, ?array $response): array
{
$answers = [];

if (!is_array($response)) {
return [$this->getEmptyAnswer($query)];
}

if (!isset($response[self::ANSWER_FIELD_NAME]) || empty($response[self::ANSWER_FIELD_NAME])) {
return [$this->getEmptyAnswer($query)];
}

foreach ($response[self::ANSWER_FIELD_NAME] as $item) {
$answer = $this->getEmptyAnswer($query);

$answer->setTtl($item[self::TTL_FIELD_NAME] ?? $this->defaultTtl);

if ($query->getType() === RecordTypeEnum::TYPE_A && isset($item[self::DATA_FIELD_NAME])) {
$answer->setRdata($item[self::DATA_FIELD_NAME]);
}

if ($query->getType() === RecordTypeEnum::TYPE_AAAA && isset($item[self::DATA_FIELD_NAME])) {
$answer->setRdata($item[self::DATA_FIELD_NAME]);
}

$answers[] = $answer;
}

return $answers;
}

/**
* @param string $name
* @param string $type
*
* @return array|null
*/
private function request(string $name, string $type): ?array
{
$session = curl_init();

$query = [
self::NAME_QUERY_PARAM => $name,
self::TYPE_QUERY_PARAM => $type,
];

$url = self::API_ENDPOINT.'?'.http_build_query($query);

curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
curl_setopt($session, CURLOPT_URL, $url);

$response = curl_exec($session);

curl_close($session);

$response = json_decode($response, true);

if (json_last_error() !== JSON_ERROR_NONE) {
return null;
}

return $response;
}

/**
* @param ResourceRecord $query
*
* @return ResourceRecord
*/
private function getEmptyAnswer(ResourceRecord $query): ResourceRecord
{
$answer = new ResourceRecord();
$answer->setName($query->getName());
$answer->setType($query->getType());
$answer->setTtl($this->defaultTtl);

return $answer;
}
}
2 changes: 1 addition & 1 deletion src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public function start(): void
* @param string $address
* @param SocketInterface $socket
*/
public function onMessage(string $message, string $address, SocketInterface $socket)
public function onMessage(string $message, string $address, SocketInterface $socket): void
{
try {
$this->dispatcher->dispatch(Events::MESSAGE, new MessageEvent($socket, $address, $message));
Expand Down
79 changes: 79 additions & 0 deletions src/Tests/Resolver/GoogleDnsResolveTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace yswery\DNS\Tests\Resolver;

use PHPUnit\Framework\TestCase;
use yswery\DNS\RecordTypeEnum;
use yswery\DNS\Resolver\GoogleDnsResolver;
use yswery\DNS\ResourceRecord;

class GoogleDnsResolveTest extends TestCase
{
private const EXAMPLE_QUERY = 'apple.com.';

/**
* @var GoogleDnsResolver
*/
protected $resolver;

/**
* @var array
*/
protected $successResponse;

/**
* @var array
*/
protected $failureResponse;

public function setUp()
{
$this->resolver = new GoogleDnsResolver(300);

$this->failureResponse = json_decode(
file_get_contents(__DIR__.'/../Resources/google-dns-query-failure.json'),
true
);
$this->successResponse = json_decode(
file_get_contents(__DIR__.'/../Resources/google-dns-query-success.json'),
true
);
}

public function testRecordResolve(): void
{
$query = (new ResourceRecord())
->setName(self::EXAMPLE_QUERY)
->setType(RecordTypeEnum::TYPE_A)
->setQuestion(true);

$answers = $this->resolver->createAnswer($query, $this->successResponse);

static::assertArrayHasKey(0, $answers);

$answer = $answers[0];

static::assertEquals('apple.com.', $answer->getName());
static::assertEquals(RecordTypeEnum::TYPE_A, $answer->getType());
static::assertEquals(3599, $answer->getTtl());
static::assertEquals('17.178.96.59', $answer->getRdata());
}

public function testRecordFailedToResolve() {
$query = (new ResourceRecord())
->setName(self::EXAMPLE_QUERY)
->setType(RecordTypeEnum::TYPE_A)
->setQuestion(true);

$answers = $this->resolver->createAnswer($query, $this->failureResponse);

static::assertArrayHasKey(0, $answers);

$answer = $answers[0];

static::assertEquals(self::EXAMPLE_QUERY, $answer->getName());
static::assertEquals(RecordTypeEnum::TYPE_A, $answer->getType());
static::assertEquals(300, $answer->getTtl());
static::assertEquals(null, $answer->getRdata());
}
}
16 changes: 16 additions & 0 deletions src/Tests/Resources/google-dns-query-failure.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"Status": 2,
"TC": false,
"RD": true,
"RA": true,
"AD": false,
"CD": false,
"Question":
[
{
"name": "dnssec-failed.org.",
"type": 1
}
],
"Comment": "DNSSEC validation failure. Please check http://dnsviz.net/d/dnssec-failed.org/dnssec/."
}
38 changes: 38 additions & 0 deletions src/Tests/Resources/google-dns-query-success.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"Status": 0,
"TC": false,
"RD": true,
"RA": true,
"AD": false,
"CD": false,
"Question":
[
{
"name": "apple.com.",
"type": 1
}
],
"Answer":
[
{
"name": "apple.com.",
"type": 1,
"TTL": 3599,
"data": "17.178.96.59"
},
{
"name": "apple.com.",
"type": 1,
"TTL": 3599,
"data": "17.172.224.47"
},
{
"name": "apple.com.",
"type": 1,
"TTL": 3599,
"data": "17.142.160.59"
}
],
"Additional": [ ],
"edns_client_subnet": "12.34.56.78/0"
}