From 41e60fe1b3296d92d54354a839a6ab5c53a03ee1 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 9 Jul 2025 07:43:50 +0200 Subject: [PATCH 1/3] [3.0] Introduce `Options` class representing supported `resolv.conf` options The two current supported options, `attempts` and `timeout`, have their min (assumed to be `1`), max, and default value as specified on https://man7.org/linux/man-pages/man5/resolv.conf.5.html set. --- src/Config/Options.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/Config/Options.php diff --git a/src/Config/Options.php b/src/Config/Options.php new file mode 100644 index 00000000..b80afae9 --- /dev/null +++ b/src/Config/Options.php @@ -0,0 +1,15 @@ + + */ + public $attempts = 2; + /** + * @var int<1, 30> + */ + public $timeout = 5; +} From 4df19de22cb9c8d45afcd11debc757c443f3e80a Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 9 Jul 2025 07:44:53 +0200 Subject: [PATCH 2/3] [3.0] Parse `resolv.conf` into `Options` class Adding parsing logic to the `Config` class that respects values as specified on https://man7.org/linux/man-pages/man5/resolv.conf.5.html in such a way we can easily expand the list to add `search`, `ndots`, and more in follow up PRs. As per https://man7.org/linux/man-pages/man5/resolv.conf.5.html both values are silently capped and are not allowed to go below 1. --- src/Config/Config.php | 32 ++++++++++++++++++++++++++++++++ tests/Config/ConfigTest.php | 2 ++ tests/Fixtures/etc/resolv.conf | 1 + 3 files changed, 35 insertions(+) diff --git a/src/Config/Config.php b/src/Config/Config.php index 0b19e9af..5d55b097 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -97,6 +97,29 @@ public static function loadResolvConfBlocking($path = null) } } + $matches = []; + preg_match_all('/^options.*\s*$/m', $contents, $matches); + if (isset($matches[0][0])) { + $options = preg_split('/\s+/', trim($matches[0][0])); + array_shift($options); + + foreach ($options as $option) { + $value = null; + if (strpos($option, ':') !== false) { + [$option, $value] = explode(':', $option, 2); + } + + switch ($option) { + case 'attempts': + $config->options->attempts = ((int) $value) > 5 ? 5 : (int) $value; + break; + case 'timeout': + $config->options->timeout = ((int) $value) > 30 ? 30 : (int) $value; + break; + } + } + } + return $config; } @@ -134,4 +157,13 @@ public static function loadWmicBlocking($command = null) } public $nameservers = []; + + /** + * @var Options + */ + public $options; + + public function __construct() { + $this->options = new Options(); + } } diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php index 46d2f53d..097c556f 100644 --- a/tests/Config/ConfigTest.php +++ b/tests/Config/ConfigTest.php @@ -30,6 +30,8 @@ public function testLoadsFromExplicitPath() $config = Config::loadResolvConfBlocking(__DIR__ . '/../Fixtures/etc/resolv.conf'); $this->assertEquals(['8.8.8.8'], $config->nameservers); + $this->assertEquals(4, $config->options->attempts); + $this->assertEquals(29, $config->options->timeout); } public function testLoadThrowsWhenPathIsInvalid() diff --git a/tests/Fixtures/etc/resolv.conf b/tests/Fixtures/etc/resolv.conf index cae093a8..f11cde9f 100644 --- a/tests/Fixtures/etc/resolv.conf +++ b/tests/Fixtures/etc/resolv.conf @@ -1 +1,2 @@ nameserver 8.8.8.8 +options timeout:29 trust-ad attempts:4 From 766ba63ffc9e32fe5ad881cf4dc365bf64f73011 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 9 Jul 2025 07:50:32 +0200 Subject: [PATCH 3/3] [3.0] Utilize the new `Options` class in the executors While setting up the executors we will now use the `Options` class to pass in formally hardcoded values. This won't impact current behavior unless `resolv.conf` other than default values. A small note is added to the readme documenting the behavior introduced in this PR. --- README.md | 4 +++ src/Resolver/Factory.php | 59 +++++++++++++++++++++++----------------- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 73ea2fb6..1544ca56 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,10 @@ as above if none can be found. Ideally, this method should thus be executed only once before the loop starts and not repeatedly while it is running. +> Also note that loading the system config will also read attempts, and + timeout from the options in resolv.conf and configures the retry and + timeout executors with the values it finds there. + But there's more. ## Caching diff --git a/src/Resolver/Factory.php b/src/Resolver/Factory.php index eedebe1a..e05f59ed 100644 --- a/src/Resolver/Factory.php +++ b/src/Resolver/Factory.php @@ -6,6 +6,7 @@ use React\Cache\CacheInterface; use React\Dns\Config\Config; use React\Dns\Config\HostsFile; +use React\Dns\Config\Options; use React\Dns\Query\CachingExecutor; use React\Dns\Query\CoopExecutor; use React\Dns\Query\ExecutorInterface; @@ -125,26 +126,24 @@ private function createExecutor($nameserver, LoopInterface $loop) if ($tertiary !== false) { // 3 DNS servers given => nest first with fallback for second and third - return new CoopExecutor( - new RetryExecutor( + return $this->createTopLevelDecoratingExectors( + new FallbackExecutor( + $this->createSingleExecutor($primary, $nameserver->options, $loop), new FallbackExecutor( - $this->createSingleExecutor($primary, $loop), - new FallbackExecutor( - $this->createSingleExecutor($secondary, $loop), - $this->createSingleExecutor($tertiary, $loop) - ) + $this->createSingleExecutor($secondary, $nameserver->options, $loop), + $this->createSingleExecutor($tertiary, $nameserver->options, $loop) ) - ) + ), + $nameserver ); } elseif ($secondary !== false) { // 2 DNS servers given => fallback from first to second - return new CoopExecutor( - new RetryExecutor( - new FallbackExecutor( - $this->createSingleExecutor($primary, $loop), - $this->createSingleExecutor($secondary, $loop) - ) - ) + return $this->createTopLevelDecoratingExectors( + new FallbackExecutor( + $this->createSingleExecutor($primary, $nameserver->options, $loop), + $this->createSingleExecutor($secondary, $nameserver->options, $loop) + ), + $nameserver ); } else { // 1 DNS server given => use single executor @@ -152,27 +151,35 @@ private function createExecutor($nameserver, LoopInterface $loop) } } - return new CoopExecutor(new RetryExecutor($this->createSingleExecutor($nameserver, $loop))); + return $this->createTopLevelDecoratingExectors($this->createSingleExecutor($nameserver, new Options(), $loop), $nameserver); + } + + private function createTopLevelDecoratingExectors(ExecutorInterface $executor, $nameserver) + { + $executor = new RetryExecutor($executor, (is_string($nameserver) ? new Options() : $nameserver->options)->attempts); + + return new CoopExecutor($executor); } /** * @param string $nameserver + * @param Options $options * @param LoopInterface $loop * @return ExecutorInterface * @throws \InvalidArgumentException for invalid DNS server address */ - private function createSingleExecutor($nameserver, LoopInterface $loop) + private function createSingleExecutor($nameserver, Options $options, LoopInterface $loop) { $parts = \parse_url($nameserver); if (isset($parts['scheme']) && $parts['scheme'] === 'tcp') { - $executor = $this->createTcpExecutor($nameserver, $loop); + $executor = $this->createTcpExecutor($nameserver, $options->timeout, $loop); } elseif (isset($parts['scheme']) && $parts['scheme'] === 'udp') { - $executor = $this->createUdpExecutor($nameserver, $loop); + $executor = $this->createUdpExecutor($nameserver, $options->timeout, $loop); } else { $executor = new SelectiveTransportExecutor( - $this->createUdpExecutor($nameserver, $loop), - $this->createTcpExecutor($nameserver, $loop) + $this->createUdpExecutor($nameserver, $options->timeout, $loop), + $this->createTcpExecutor($nameserver, $options->timeout, $loop) ); } @@ -181,33 +188,35 @@ private function createSingleExecutor($nameserver, LoopInterface $loop) /** * @param string $nameserver + * @param int $timeout * @param LoopInterface $loop * @return TimeoutExecutor * @throws \InvalidArgumentException for invalid DNS server address */ - private function createTcpExecutor($nameserver, LoopInterface $loop) + private function createTcpExecutor($nameserver, int $timeout, LoopInterface $loop) { return new TimeoutExecutor( new TcpTransportExecutor($nameserver, $loop), - 5.0, + $timeout, $loop ); } /** * @param string $nameserver + * @param int $timeout * @param LoopInterface $loop * @return TimeoutExecutor * @throws \InvalidArgumentException for invalid DNS server address */ - private function createUdpExecutor($nameserver, LoopInterface $loop) + private function createUdpExecutor($nameserver, int $timeout, LoopInterface $loop) { return new TimeoutExecutor( new UdpTransportExecutor( $nameserver, $loop ), - 5.0, + $timeout, $loop ); }