From 907552b1c7c033b4de11397cea87f005c6bbc9f6 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sat, 3 Oct 2026 00:18:36 +0000 Subject: [PATCH] Resolve hosts with getaddrinfo so hosts-file IPv6 entries are found Guard::resolve() combined gethostbynamel(), which is IPv4 only, with a DNS AAAA query, which skips the hosts file. IPv6 addresses from the hosts file, such as ::1 for localhost, were never found, so safe mode couldn't reach a server listening only on them. Use getaddrinfo via socket_addrinfo_lookup() when ext-sockets is available, falling back to the previous lookups otherwise. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/p3k/HTTP/Guard.php | 12 ++++++++++++ tests/GuardTest.php | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/p3k/HTTP/Guard.php b/src/p3k/HTTP/Guard.php index 38dbfff..1902501 100644 --- a/src/p3k/HTTP/Guard.php +++ b/src/p3k/HTTP/Guard.php @@ -91,6 +91,18 @@ class Guard { /** Every IPv4 and IPv6 address the system resolver knows for a host. */ public static function resolve($host) { + // getaddrinfo, as curl itself uses, sees both address families and the + // hosts file. Without ext-sockets, fall back to asking for each family + // separately, which misses IPv6 entries in the hosts file such as ::1. + if(function_exists('socket_addrinfo_lookup')) { + $addresses = []; + foreach(@socket_addrinfo_lookup($host, null, ['ai_socktype' => SOCK_STREAM]) ?: [] as $info) { + $address = socket_addrinfo_explain($info)['ai_addr']; + $addresses[] = $address['sin6_addr'] ?? $address['sin_addr']; + } + return array_values(array_unique($addresses)); + } + $addresses = gethostbynamel($host) ?: []; $records = @dns_get_record($host, DNS_AAAA); foreach($records ?: [] as $record) { diff --git a/tests/GuardTest.php b/tests/GuardTest.php index efed7c7..b86811d 100644 --- a/tests/GuardTest.php +++ b/tests/GuardTest.php @@ -64,6 +64,25 @@ class GuardTest extends TestCase { $this->assertSame(8080, $result['port']); } + // Uses the real system resolver: localhost is in every hosts file, and the + // IPv6 entry is only found through getaddrinfo, not a DNS AAAA query. + public function testSystemResolverReadsTheHostsFile() { + if(!function_exists('socket_addrinfo_lookup')) + $this->markTestSkipped('ext-sockets is not available'); + + $expected = []; + foreach(socket_addrinfo_lookup('localhost', null, ['ai_socktype' => SOCK_STREAM]) as $info) { + $address = socket_addrinfo_explain($info)['ai_addr']; + $expected[] = $address['sin6_addr'] ?? $address['sin_addr']; + } + + $addresses = Guard::resolve('localhost'); + $this->assertContains('127.0.0.1', $addresses); + foreach(array_unique($expected) as $address) + $this->assertContains($address, $addresses); + $this->assertSame([], Guard::resolve('nonexistent.invalid')); + } + public function testUnresolvableHost() { $this->assertSame('dns_error', self::guard()->check('https://nowhere.example/')['error']); }