diff --git a/LibreNMS/Data/Source/Net/AddrInfoResolver.php b/LibreNMS/Data/Source/Net/AddrInfoResolver.php index 88549601774361a31649676105a479f27de18737..78f4ca46d8b42e056f2e6fa270238beb509348b6 100644 --- a/LibreNMS/Data/Source/Net/AddrInfoResolver.php +++ b/LibreNMS/Data/Source/Net/AddrInfoResolver.php @@ -1,4 +1,5 @@ describe(); - if (!array_key_exists($cacheKey, $this->cache)) { - $hints = match($query->type) { + if (! array_key_exists($cacheKey, $this->cache)) { + $hints = match ($query->type) { Message::TYPE_A => ['ai_family' => AF_INET, 'ai_socktype' => SOCK_DGRAM], Message::TYPE_AAAA => ['ai_family' => AF_INET6, 'ai_socktype' => SOCK_DGRAM], default => ['ai_socktype' => SOCK_DGRAM], @@ -69,7 +70,6 @@ class AddrInfoResolver implements ExecutorInterface // Call $resolve with the result $resolve($message); - } catch (\Throwable $e) { // Call $reject if there's an error $reject($e); diff --git a/LibreNMS/Data/Source/Net/Connection.php b/LibreNMS/Data/Source/Net/Connection.php index 27d6e932425a20022990885d957c1e11ab00d96a..24e517b7ee8aa71d72dc9b5232385e98a74e470d 100644 --- a/LibreNMS/Data/Source/Net/Connection.php +++ b/LibreNMS/Data/Source/Net/Connection.php @@ -1,4 +1,5 @@ $connectorClass * @param UdpCodec $codec * @return string|null The first successfully connected IP address, or null if all fail. + * * @throws Throwable */ - public function connect(array $ips, int $port, string $connectorClass, UdpCodec $codec): ?string { - Log::info("Starting Happy Eyeballs connection attempt to " . count($ips) . " IPs."); + public function connect(array $ips, int $port, string $connectorClass, UdpCodec $codec): ?string + { + Log::info('Starting Happy Eyeballs connection attempt to ' . count($ips) . ' IPs.'); $startTime = microtime(true); // initiate fibers and connectors for each IP @@ -63,12 +67,12 @@ class ConnectionFinder { } // The Event Loop - while (!empty($this->connections) && $this->connectedIp === null) { - /** @var Socket[] $read **/ + while (! empty($this->connections) && $this->connectedIp === null) { + /** @var Socket[] $read * */ $read = []; - /** @var Socket[] $write **/ + /** @var Socket[] $write * */ $write = []; - /** @var Socket[] $except **/ + /** @var Socket[] $except * */ $except = []; foreach ($this->connections as $connection) { @@ -83,7 +87,7 @@ class ConnectionFinder { } if ($numChanged === false) { - Log::error("Socket select error: " . socket_strerror(socket_last_error())); + Log::error('Socket select error: ' . socket_strerror(socket_last_error())); break; } elseif ($numChanged > 0) { Log::debug("Socket select returned $numChanged sockets ready."); @@ -96,13 +100,13 @@ class ConnectionFinder { try { Log::debug("Resuming fiber for $readyConnection->connector"); $result = $readyConnection->fiber->resume(true); - Log::debug("Fiber resume result: " . var_export($result, true)); + Log::debug('Fiber resume result: ' . var_export($result, true)); if ($result) { $this->connectedIp ??= $readyConnection->ip; $this->forgetConnection($readyConnection); } } catch (Throwable $e) { - Log::error("Exception in fiber resume: " . $e->getMessage()); + Log::error('Exception in fiber resume: ' . $e->getMessage()); $this->forgetConnection($readyConnection); } } @@ -110,7 +114,7 @@ class ConnectionFinder { } // Check for overall time limit if ((microtime(true) - $startTime) * 1000 >= self::TIMEOUT_MS) { - Log::debug("Global timeout reached. Terminating connection attempts."); + Log::debug('Global timeout reached. Terminating connection attempts.'); break; } } @@ -119,14 +123,14 @@ class ConnectionFinder { foreach ($this->connections as $connection) { if ($connection->fiber->isSuspended()) { $result = $connection->fiber->resume(false); - Log::debug("Fiber resume result: " . var_export($result, true)); + Log::debug('Fiber resume result: ' . var_export($result, true)); } } return $this->connectedIp; } - protected function getConnection(string|Socket $search): Connection|null + protected function getConnection(string|Socket $search): ?Connection { if (is_string($search)) { return $this->connections[$search] ?? null; @@ -161,11 +165,11 @@ class ConnectionFinder { if ($connector->isServiceAvailable()) { Log::info("Successfully connected to the first available IP: $ip"); Fiber::suspend($ip); // return IP to the caller + return; } } } while ($ready === true); - } catch (Throwable $e) { throw $e; } finally { diff --git a/LibreNMS/Data/Source/Net/Service/BaseConnector.php b/LibreNMS/Data/Source/Net/Service/BaseConnector.php index 7cacdf17c4998bf2f657aed4d1cffdfece76cf4e..ec6a4d456ea25267859dc16a7748ba74f045ac56 100644 --- a/LibreNMS/Data/Source/Net/Service/BaseConnector.php +++ b/LibreNMS/Data/Source/Net/Service/BaseConnector.php @@ -1,4 +1,5 @@ fiber = new Fiber(fn() => $this->fiberWorker()); + ) { + $this->fiber = new Fiber(fn () => $this->fiberWorker()); } final protected function createSocket(int $type, int $protocol): void @@ -55,7 +55,7 @@ abstract class BaseConnector implements ServiceConnector, Stringable $socket = socket_create($domain, $type, $protocol); if ($socket === false) { - throw new \RuntimeException("Failed to create socket for $this->ip:$this->port ".socket_strerror(socket_last_error())); + throw new \RuntimeException("Failed to create socket for $this->ip:$this->port " . socket_strerror(socket_last_error())); } $this->socket = $socket; @@ -123,11 +123,11 @@ abstract class BaseConnector implements ServiceConnector, Stringable if ($this->isServiceAvailable()) { Log::info("Successfully connected to the first available IP: $this->ip"); Fiber::suspend($this->ip); + return; } } } while ($ready === true); - } catch (Throwable $e) { throw $e; } finally { diff --git a/LibreNMS/Data/Source/Net/Service/DnsCodec.php b/LibreNMS/Data/Source/Net/Service/DnsCodec.php index a92e95557f9791eac646e82de3440f324786297c..9c1aca94227a595b637e955dd5a78c88949030d1 100644 --- a/LibreNMS/Data/Source/Net/Service/DnsCodec.php +++ b/LibreNMS/Data/Source/Net/Service/DnsCodec.php @@ -1,4 +1,5 @@ buildGetRequestPdu(); $message = $version . $community . $pdu; + return $this->encodeSequence($message); } @@ -97,6 +99,7 @@ class SnmpCodec implements UdpCodec $pdu = $this->buildGetRequestPdu(); $message = $version . $community . $pdu; + return $this->encodeSequence($message); } @@ -135,6 +138,7 @@ class SnmpCodec implements UdpCodec $scopedPdu = $this->encodeSequence($contextEngineId . $contextName . $pdu); $message = $version . $headerData . $msgSecurityParameters . $scopedPdu; + return $this->encodeSequence($message); } @@ -156,7 +160,7 @@ class SnmpCodec implements UdpCodec $pduContent = $requestId . $errorStatus . $errorIndex . $varbindList; // PDU type: GetRequest (0xa0) - return chr(0xa0) . $this->encodeLength(strlen($pduContent)) . $pduContent; + return chr(0xA0) . $this->encodeLength(strlen($pduContent)) . $pduContent; } /** @@ -175,7 +179,7 @@ class SnmpCodec implements UdpCodec $pduContent = $requestId . $errorStatus . $errorIndex . $varbindList; // PDU type: GetRequest (0xa0) - return chr(0xa0) . $this->encodeLength(strlen($pduContent)) . $pduContent; + return chr(0xA0) . $this->encodeLength(strlen($pduContent)) . $pduContent; } /** diff --git a/LibreNMS/Data/Source/Net/Service/TcpConnector.php b/LibreNMS/Data/Source/Net/Service/TcpConnector.php index 30b7cfd1498de88ba43a5acee1686830850e89af..0c3358ea657d5d1498943cb29a3274acb19d3ec8 100644 --- a/LibreNMS/Data/Source/Net/Service/TcpConnector.php +++ b/LibreNMS/Data/Source/Net/Service/TcpConnector.php @@ -1,4 +1,5 @@ socket, SOL_SOCKET, SO_ERROR); $peerIp = null; - if($soError === 0 && socket_getpeername($this->socket, $peerIp) && $peerIp === $this->ip) { + if ($soError === 0 && socket_getpeername($this->socket, $peerIp) && $peerIp === $this->ip) { return true; } diff --git a/LibreNMS/Data/Source/Net/Service/UdpCodec.php b/LibreNMS/Data/Source/Net/Service/UdpCodec.php index 3d5b0b195588c2b4aa7f1de1a21c077e83d89824..a02596b1cf783fd283bac2c6a2a4f43283fc642e 100644 --- a/LibreNMS/Data/Source/Net/Service/UdpCodec.php +++ b/LibreNMS/Data/Source/Net/Service/UdpCodec.php @@ -1,4 +1,5 @@ requestMessage->validateResponse($response)) { Log::info("Received valid UDP response from $this->ip"); + return true; } } diff --git a/LibreNMS/Data/Source/Net/UdpHappyEyeballsConnector.php b/LibreNMS/Data/Source/Net/UdpHappyEyeballsConnector.php index c72199b0ade32e894fced9c9050e4694534cd1cd..8f620cf1d86b302da7dd97d9c83fddf007257c98 100644 --- a/LibreNMS/Data/Source/Net/UdpHappyEyeballsConnector.php +++ b/LibreNMS/Data/Source/Net/UdpHappyEyeballsConnector.php @@ -1,4 +1,5 @@ resolver && $mode !== 'ipv6_only' - ? $this->resolver->resolveAll($hostname, \React\Dns\Model\Message::TYPE_A)->then(null, function() { return []; }) + ? $this->resolver->resolveAll($hostname, \React\Dns\Model\Message::TYPE_A)->then(null, function () { + return []; + }) : \React\Promise\resolve([]); $ipv6Promise = $this->resolver && $mode !== 'ipv4_only' - ? $this->resolver->resolveAll($hostname, \React\Dns\Model\Message::TYPE_AAAA)->then(null, function() { return []; }) + ? $this->resolver->resolveAll($hostname, \React\Dns\Model\Message::TYPE_AAAA)->then(null, function () { + return []; + }) : \React\Promise\resolve([]); \React\Promise\all([$ipv4Promise, $ipv6Promise])->then( @@ -76,6 +81,7 @@ class UdpHappyEyeballsConnector // Make sure we have at least one address if (empty($ipv4Addresses) && empty($ipv6Addresses)) { $deferred->reject(new \RuntimeException("DNS resolution failed: No addresses found for $hostname")); + return; } @@ -108,7 +114,8 @@ class UdpHappyEyeballsConnector $addresses = $this->interleaveAddresses($ipv6Addresses, $ipv4Addresses); if (empty($addresses)) { - $deferred->reject(new \RuntimeException("No addresses to connect to")); + $deferred->reject(new \RuntimeException('No addresses to connect to')); + return; } @@ -124,7 +131,7 @@ class UdpHappyEyeballsConnector $this->connectToAddress($address, $port, $requestMessage)->then( function ($result) use ($deferred, $cleanup, &$resolved) { - if (!$resolved) { + if (! $resolved) { $cleanup(); $deferred->resolve($result); } @@ -133,17 +140,17 @@ class UdpHappyEyeballsConnector $attempts[$index]['error'] = $error; // Check if all attempts have failed - if (!$resolved && count($attempts) === count($addresses)) { + if (! $resolved && count($attempts) === count($addresses)) { $allFailed = true; foreach ($attempts as $attempt) { - if (!isset($attempt['error'])) { + if (! isset($attempt['error'])) { $allFailed = false; break; } } if ($allFailed) { $cleanup(); - $deferred->reject(new \RuntimeException("All connection attempts failed")); + $deferred->reject(new \RuntimeException('All connection attempts failed')); } } } @@ -162,9 +169,9 @@ class UdpHappyEyeballsConnector // Overall timeout $timers[] = Loop::addTimer($this->timeout, function () use ($deferred, $cleanup, &$resolved) { - if (!$resolved) { + if (! $resolved) { $cleanup(); - $deferred->reject(new \RuntimeException("Connection timeout")); + $deferred->reject(new \RuntimeException('Connection timeout')); } }); } @@ -181,7 +188,7 @@ class UdpHappyEyeballsConnector $resolved = false; $client->on('message', function ($message) use ($client, $deferred, $requestMessage, &$timer, &$resolved) { - if (!$resolved && $requestMessage->validateResponse($message)) { + if (! $resolved && $requestMessage->validateResponse($message)) { $resolved = true; if ($timer) { Loop::cancelTimer($timer); @@ -189,7 +196,7 @@ class UdpHappyEyeballsConnector $deferred->resolve([ 'socket' => $client, 'response' => $message, - 'address' => $client->getRemoteAddress() + 'address' => $client->getRemoteAddress(), ]); } }); @@ -199,10 +206,10 @@ class UdpHappyEyeballsConnector // Timeout for this specific connection $timer = Loop::addTimer($this->responseTimeout, function () use ($client, $deferred, &$resolved) { - if (!$resolved) { + if (! $resolved) { $resolved = true; $client->close(); - $deferred->reject(new \RuntimeException("UDP request timeout")); + $deferred->reject(new \RuntimeException('UDP request timeout')); } }); }, @@ -227,7 +234,6 @@ class UdpHappyEyeballsConnector default => [$ipv6, $ipv4], }; - for ($i = 0; $i < $maxCount; $i++) { if (isset($first[$i])) { $result[] = $first[$i]; @@ -237,7 +243,7 @@ class UdpHappyEyeballsConnector } } - Log::debug("Resolved addresses: " . implode(", ", $result)); + Log::debug('Resolved addresses: ' . implode(', ', $result)); return $result; } diff --git a/LibreNMS/Util/Dns.php b/LibreNMS/Util/Dns.php index a22a398d44c62c3feec0c8100160140c2834a661..97921ab1b9d5210d1df9892a3843c99005cdde44 100644 --- a/LibreNMS/Util/Dns.php +++ b/LibreNMS/Util/Dns.php @@ -104,6 +104,7 @@ class Dns implements Geocoder, ResolverInterface /** * Resolve IPs for hostname IPv4 and IPv6. Respecting the order for dns.resolution_mode + * * @return array{'ipv4': string[], 'ipv6': string[]} */ public function resolveIPs(string $hostname, ?string $preferredIpFirst = null): array diff --git a/app/Console/Commands/TestHappyEyeballs.php b/app/Console/Commands/TestHappyEyeballs.php index 785810648116f27ccac3ccfb702bcc44dcc9c5c3..d238e6c61e1d167684ab3d731cf922c3d58a5fe5 100644 --- a/app/Console/Commands/TestHappyEyeballs.php +++ b/app/Console/Commands/TestHappyEyeballs.php @@ -8,30 +8,28 @@ use Illuminate\Support\Arr; use LibreNMS\Data\Source\Net\AddrInfoResolver; use LibreNMS\Data\Source\Net\ConnectionFinder; use LibreNMS\Data\Source\Net\Service\DnsCodec; -use LibreNMS\Data\Source\Net\Service\IcmpConnector; use LibreNMS\Data\Source\Net\Service\NtpCodec; -use LibreNMS\Data\Source\Net\Service\NtpConnector; use LibreNMS\Data\Source\Net\Service\SnmpCodec; -use LibreNMS\Data\Source\Net\Service\SnmpConnector; use LibreNMS\Data\Source\Net\Service\TcpConnector; use LibreNMS\Data\Source\Net\Service\UdpCodec; use LibreNMS\Data\Source\Net\Service\UdpConnector; use LibreNMS\Data\Source\Net\UdpHappyEyeballsConnector; use LibreNMS\Util\Dns; use React\Dns\Resolver\Resolver; + use function React\Async\await; class TestHappyEyeballs extends LnmsCommand { protected $signature = 'test:happy-eyeballs {hostname?} {service?} {--port=} {--fibers}'; - public function handle(Dns $dns): int { $this->configureOutputOptions(); if ($this->option('fibers')) { $this->info('Using Custom Fibers'); + return $this->fibers($dns); } @@ -43,7 +41,7 @@ class TestHappyEyeballs extends LnmsCommand if ($codec === null) { $result = $this->reactTcp($hostname, $port); - $this->info("Elapsed time: " . (microtime(true) - $start_time) . " seconds"); + $this->info('Elapsed time: ' . (microtime(true) - $start_time) . ' seconds'); return $result; } @@ -53,16 +51,16 @@ class TestHappyEyeballs extends LnmsCommand await($connector->connect($hostname, $port, $codec)->then( function ($result) { - echo "connected via: " . $result['address'] . "\n"; - echo "Response length: " . strlen($result['response']) . " bytes\n"; + echo 'connected via: ' . $result['address'] . "\n"; + echo 'Response length: ' . strlen($result['response']) . " bytes\n"; $result['socket']->close(); }, function ($error) { - echo "connection failed: " . $error->getMessage() . "\n"; + echo 'connection failed: ' . $error->getMessage() . "\n"; } )); - $this->info("Elapsed time: " . (microtime(true) - $start_time) . " seconds"); + $this->info('Elapsed time: ' . (microtime(true) - $start_time) . ' seconds'); return 0; } @@ -74,7 +72,7 @@ class TestHappyEyeballs extends LnmsCommand $dnsConnector = new \React\Socket\HappyEyeBallsConnector(null, $tcpConnector, $reactDns); $dnsConnector->connect("$hostname:$port")->then(function (\React\Socket\ConnectionInterface $connection) { - $this->info("Connected to: " . $connection->getRemoteAddress()); + $this->info('Connected to: ' . $connection->getRemoteAddress()); $connection->end(); }); @@ -110,14 +108,14 @@ class TestHappyEyeballs extends LnmsCommand return 0; } - $this->error("Failed to connect to any IP address within the timeout."); + $this->error('Failed to connect to any IP address within the timeout.'); return 1; } private function resolvePort(): int { - $default_port = match($this->argument('service')) { + $default_port = match ($this->argument('service')) { 'ntp' => 123, 'dns' => 53, 'snmp' => 161, @@ -129,7 +127,7 @@ class TestHappyEyeballs extends LnmsCommand private function resolveUdpCodec(): ?UdpCodec { - return match($this->argument('service')) { + return match ($this->argument('service')) { 'ntp' => new NtpCodec(), 'dns' => new DnsCodec(), 'snmp' => $this->createSnmpCodec(), @@ -155,7 +153,7 @@ class TestHappyEyeballs extends LnmsCommand ); } } catch (\Exception $e) { - $this->error("Error: " . $e->getMessage()); + $this->error('Error: ' . $e->getMessage()); } } diff --git a/app/Models/Device.php b/app/Models/Device.php index 58da61fcc6d1acfd351e599079faf1c3d6d1c4bd..5f350a0afcf6623efe2ef92a5bad41afe348c39c 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -139,7 +139,6 @@ class Device extends BaseModel return $resolvedIps ?: [$this->hostname]; } - public function ipFamily(): AddressFamily { return str_ends_with($this->transport ?? '', '6') ? AddressFamily::IPv6 : AddressFamily::IPv4; diff --git a/tests/Unit/Util/DnsTest.php b/tests/Unit/Util/DnsTest.php index 5a27238e4ee7622d3d35907cf5836dda0f9ed1ac..efccb947056c0ff7f996cb99477d22014f30e685 100644 --- a/tests/Unit/Util/DnsTest.php +++ b/tests/Unit/Util/DnsTest.php @@ -323,6 +323,7 @@ class DnsTest extends TestCase $this->assertNull($result); } + public function testResolveIpsReturnsEmptyArrayOnLookupFailure(): void { $this->mockConfig('os'); @@ -647,19 +648,19 @@ class DnsTest extends TestCase foreach ($ips as $ip) { if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $records[] = [ - 'ai_family' => AF_INET, - 'ai_socktype' => SOCK_STREAM, - 'ai_protocol' => 6, - 'ai_addr' => ['sin_addr' => $ip, 'sin_port' => 0], - 'ai_flags' => 0, + 'ai_family' => AF_INET, + 'ai_socktype' => SOCK_STREAM, + 'ai_protocol' => 6, + 'ai_addr' => ['sin_addr' => $ip, 'sin_port' => 0], + 'ai_flags' => 0, ]; } else { $records[] = [ - 'ai_family' => AF_INET6, - 'ai_socktype' => SOCK_STREAM, - 'ai_protocol' => 6, - 'ai_addr' => ['sin6_addr' => $ip, 'sin6_port' => 0], - 'ai_flags' => 0, + 'ai_family' => AF_INET6, + 'ai_socktype' => SOCK_STREAM, + 'ai_protocol' => 6, + 'ai_addr' => ['sin6_addr' => $ip, 'sin6_port' => 0], + 'ai_flags' => 0, ]; } }