Browse Source

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) <noreply@anthropic.com>
main
Aaron Parecki 1 day ago
parent
commit
907552b1c7
2 changed files with 31 additions and 0 deletions
  1. +12
    -0
      src/p3k/HTTP/Guard.php
  2. +19
    -0
      tests/GuardTest.php

+ 12
- 0
src/p3k/HTTP/Guard.php View File

@ -91,6 +91,18 @@ class Guard {
/** Every IPv4 and IPv6 address the system resolver knows for a host. */ /** Every IPv4 and IPv6 address the system resolver knows for a host. */
public static function resolve($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) ?: []; $addresses = gethostbynamel($host) ?: [];
$records = @dns_get_record($host, DNS_AAAA); $records = @dns_get_record($host, DNS_AAAA);
foreach($records ?: [] as $record) { foreach($records ?: [] as $record) {

+ 19
- 0
tests/GuardTest.php View File

@ -64,6 +64,25 @@ class GuardTest extends TestCase {
$this->assertSame(8080, $result['port']); $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() { public function testUnresolvableHost() {
$this->assertSame('dns_error', self::guard()->check('https://nowhere.example/')['error']); $this->assertSame('dns_error', self::guard()->check('https://nowhere.example/')['error']);
} }

Loading…
Cancel
Save