Skip to content

Commit 4dd2cde

Browse files
committed
Add DatagramTransportExecutor
1 parent 3e0dd27 commit 4dd2cde

6 files changed

Lines changed: 394 additions & 13 deletions

File tree

README.md

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ easily be used to create a DNS server.
1414
* [Caching](#caching)
1515
* [Custom cache adapter](#custom-cache-adapter)
1616
* [Advanced usage](#advanced-usage)
17+
* [DatagramTransportExecutor](#datagramtransportexecutor)
1718
* [HostsFileExecutor](#hostsfileexecutor)
1819
* [Install](#install)
1920
* [Tests](#tests)
@@ -117,13 +118,20 @@ See also the wiki for possible [cache implementations](https://github.com/reactp
117118

118119
## Advanced Usage
119120

120-
For more advanced usages one can utilize the `React\Dns\Query\Executor` directly.
121+
### DatagramTransportExecutor
122+
123+
The `DatagramTransportExecutor` can be used to
124+
send DNS queries over a datagram transport such as UDP.
125+
126+
This is the main class that sends a DNS query to your DNS server and is used
127+
internally by the `Resolver` for the actual message transport.
128+
129+
For more advanced usages one can utilize this class directly.
121130
The following example looks up the `IPv6` address for `igor.io`.
122131

123132
```php
124133
$loop = Factory::create();
125-
126-
$executor = new Executor($loop, new Parser(), new BinaryDumper(), null);
134+
$executor = new DatagramTransportExecutor($loop);
127135

128136
$executor->query(
129137
'8.8.8.8:53',
@@ -135,11 +143,41 @@ $executor->query(
135143
}, 'printf');
136144

137145
$loop->run();
138-
139146
```
140147

141148
See also the [fourth example](examples).
142149

150+
Note that this executor does not implement a timeout, so you will very likely
151+
want to use this in combination with a `TimeoutExecutor` like this:
152+
153+
```php
154+
$executor = new TimeoutExecutor(
155+
new DatagramTransportExecutor($loop),
156+
3.0,
157+
$loop
158+
);
159+
```
160+
161+
Also note that this executor uses an unreliable UDP transport and that it
162+
does not implement any retry logic, so you will likely want to use this in
163+
combination with a `RetryExecutor` like this:
164+
165+
```php
166+
$executor = new RetryExecutor(
167+
new TimeoutExecutor(
168+
new DatagramTransportExecutor($loop),
169+
3.0,
170+
$loop
171+
)
172+
);
173+
```
174+
175+
> Internally, this class uses PHP's UDP sockets and does not take advantage
176+
of [react/datagram](https://github.com/reactphp/datagram) purely for
177+
organizational reasons to avoid a cyclic dependency between the two
178+
packages. Higher-level components should take advantage of the Datagram
179+
component instead of reimplementing this socket logic from scratch.
180+
143181
### HostsFileExecutor
144182

145183
Note that the above `Executor` class always performs an actual DNS query.

examples/04-query-a-and-aaaa.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
<?php
22

33
use React\Dns\Model\Message;
4-
use React\Dns\Protocol\BinaryDumper;
5-
use React\Dns\Protocol\Parser;
6-
use React\Dns\Query\Executor;
4+
use React\Dns\Query\DatagramTransportExecutor;
75
use React\Dns\Query\Query;
86
use React\EventLoop\Factory;
97

108
require __DIR__ . '/../vendor/autoload.php';
119

1210
$loop = Factory::create();
13-
14-
$executor = new Executor($loop, new Parser(), new BinaryDumper(), null);
11+
$executor = new DatagramTransportExecutor($loop);
1512

1613
$name = isset($argv[1]) ? $argv[1] : 'www.google.com';
1714

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
namespace React\Dns\Query;
4+
5+
use React\Dns\Model\Message;
6+
use React\Dns\Protocol\BinaryDumper;
7+
use React\Dns\Protocol\Parser;
8+
use React\EventLoop\LoopInterface;
9+
use React\Promise\Deferred;
10+
11+
/**
12+
* Send DNS queries over a datagram transport such as UDP.
13+
*
14+
* This is the main class that sends a DNS query to your DNS server and is used
15+
* internally by the `Resolver` for the actual message transport.
16+
*
17+
* For more advanced usages one can utilize this class directly.
18+
* The following example looks up the `IPv6` address for `igor.io`.
19+
*
20+
* ```php
21+
* $loop = Factory::create();
22+
* $executor = new DatagramTransportExecutor($loop);
23+
*
24+
* $executor->query(
25+
* '8.8.8.8:53',
26+
* new Query($name, Message::TYPE_AAAA, Message::CLASS_IN, time())
27+
* )->then(function (Message $message) {
28+
* foreach ($message->answers as $answer) {
29+
* echo 'IPv6: ' . $answer->data . PHP_EOL;
30+
* }
31+
* }, 'printf');
32+
*
33+
* $loop->run();
34+
* ```
35+
*
36+
* See also the [fourth example](examples).
37+
*
38+
* Note that this executor does not implement a timeout, so you will very likely
39+
* want to use this in combination with a `TimeoutExecutor` like this:
40+
*
41+
* ```php
42+
* $executor = new TimeoutExecutor(
43+
* new DatagramTransportExecutor($loop),
44+
* 3.0,
45+
* $loop
46+
* );
47+
* ```
48+
*
49+
* Also note that this executor uses an unreliable UDP transport and that it
50+
* does not implement any retry logic, so you will likely want to use this in
51+
* combination with a `RetryExecutor` like this:
52+
*
53+
* ```php
54+
* $executor = new RetryExecutor(
55+
* new TimeoutExecutor(
56+
* new DatagramTransportExecutor($loop),
57+
* 3.0,
58+
* $loop
59+
* )
60+
* );
61+
* ```
62+
*
63+
* > Internally, this class uses PHP's UDP sockets and does not take advantage
64+
* of [react/datagram](https://github.com/reactphp/datagram) purely for
65+
* organizational reasons to avoid a cyclic dependency between the two
66+
* packages. Higher-level components should take advantage of the Datagram
67+
* component instead of reimplementing this socket logic from scratch.
68+
*/
69+
class DatagramTransportExecutor implements ExecutorInterface
70+
{
71+
private $loop;
72+
private $parser;
73+
private $dumper;
74+
75+
/**
76+
* @param LoopInterface $loop
77+
* @param null|Parser $parser optional/advanced: DNS protocol parser to use
78+
* @param null|BinaryDumper $dumper optional/advanced: DNS protocol dumper to use
79+
*/
80+
public function __construct(LoopInterface $loop, Parser $parser = null, BinaryDumper $dumper = null)
81+
{
82+
if ($parser === null) {
83+
$parser = new Parser();
84+
}
85+
if ($dumper === null) {
86+
$dumper = new BinaryDumper();
87+
}
88+
89+
$this->loop = $loop;
90+
$this->parser = $parser;
91+
$this->dumper = $dumper;
92+
}
93+
94+
public function query($nameserver, Query $query)
95+
{
96+
$request = Message::createRequestForQuery($query);
97+
98+
$queryData = $this->dumper->toBinary($request);
99+
if (isset($queryData[512])) {
100+
return \React\Promise\reject(new \RuntimeException(
101+
'DNS query for ' . $query->name . ' failed: Query too large for UDP transport'
102+
));
103+
}
104+
105+
// UDP connections are instant, so try connection without a loop or timeout
106+
$socket = @\stream_socket_client("udp://$nameserver", $errno, $errstr, 0);
107+
if ($socket === false) {
108+
return \React\Promise\reject(new \RuntimeException(
109+
'DNS query for ' . $query->name . ' failed: Unable to connect to DNS server (' . $errstr . ')',
110+
$errno
111+
));
112+
}
113+
114+
// set socket to non-blocking and immediately try to send (fill write buffer)
115+
\stream_set_blocking($socket, false);
116+
\fwrite($socket, $queryData);
117+
118+
$loop = $this->loop;
119+
$deferred = new Deferred(function () use ($loop, $socket, $query) {
120+
// cancellation should remove socket from loop and close socket
121+
$loop->removeReadStream($socket);
122+
\fclose($socket);
123+
124+
throw new CancellationException('DNS query for ' . $query->name . ' has been cancelled');
125+
});
126+
127+
$parser = $this->parser;
128+
$loop->addReadStream($socket, function ($socket) use ($loop, $deferred, $query, $parser) {
129+
// try to read a single data packet from the DNS server
130+
// ignoring any errors, this is uses UDP packets and not a stream of data
131+
$data = @\fread($socket, 512);
132+
133+
// we only react to the first message, so immediately remove socket from loop and close
134+
$loop->removeReadStream($socket);
135+
\fclose($socket);
136+
137+
try {
138+
$response = $parser->parseMessage($data);
139+
} catch (\Exception $e) {
140+
// reject if we received an invalid message from remote server
141+
$deferred->reject($e);
142+
return;
143+
}
144+
145+
if ($response->header->isTruncated()) {
146+
$deferred->reject(new \RuntimeException('DNS query for ' . $query->name . ' failed: The server returned a truncated result for a UDP query, but retrying via TCP is currently not supported'));
147+
return;
148+
}
149+
150+
$deferred->resolve($response);
151+
});
152+
153+
return $deferred->promise();
154+
}
155+
}

src/Query/Executor.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
use React\Stream\DuplexResourceStream;
1212
use React\Stream\Stream;
1313

14+
/**
15+
* @deprecated unused, exists for BC only
16+
* @see DatagramTransportExecutor
17+
*/
1418
class Executor implements ExecutorInterface
1519
{
1620
private $loop;

src/Resolver/Factory.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55
use React\Cache\ArrayCache;
66
use React\Cache\CacheInterface;
77
use React\Dns\Config\HostsFile;
8-
use React\Dns\Protocol\Parser;
9-
use React\Dns\Protocol\BinaryDumper;
108
use React\Dns\Query\CachedExecutor;
11-
use React\Dns\Query\Executor;
9+
use React\Dns\Query\DatagramTransportExecutor;
1210
use React\Dns\Query\ExecutorInterface;
1311
use React\Dns\Query\HostsFileExecutor;
1412
use React\Dns\Query\RecordCache;
@@ -71,7 +69,7 @@ private function decorateHostsFileExecutor(ExecutorInterface $executor)
7169
protected function createExecutor(LoopInterface $loop)
7270
{
7371
return new TimeoutExecutor(
74-
new Executor($loop, new Parser(), new BinaryDumper(), null),
72+
new DatagramTransportExecutor($loop),
7573
5.0,
7674
$loop
7775
);

0 commit comments

Comments
 (0)