DNS

composer require php-standard-library/dns

The DNS component provides async DNS resolution with connection pooling, EDNS0, DNS-over-TLS, DNS-over-HTTPS, system configuration detection, hosts file support, and search domain expansion. All resolvers are non-blocking and composable via the decorator pattern.

Basic Usage

SystemResolver mirrors your operating system's DNS behavior. It can be used as a default parameter value since it requires no arguments.

use Psl\DNS;
use Psl\DNS\Record\ARecord;
use Psl\DNS\Record\RecordType;

// System resolver mirrors your OS DNS settings
$resolver = new DNS\SystemResolver();

// Query A records
$response = $resolver->query('example.com', RecordType::A);

// Check the result
if ($response->code->isSuccess()) {
    foreach ($response->getAnswerRecords(ARecord::class) as $record) {
        $record->address; // Psl\IP\Address
    }
}

By default, SystemResolver uses plain UDP with TCP fallback for each nameserver discovered from the OS. You can encrypt all system DNS traffic with DNS-over-TLS by disabling UDP and passing a TLS-enabled connector. With udp: false, all queries go directly over TCP — no plaintext UDP is sent. Combined with a TLS connector, this turns all system nameserver communication into DNS-over-TLS (DoT, RFC 7858) while still using the OS-configured nameserver addresses.

use Psl\DNS;
use Psl\TCP;
use Psl\TLS;

// Encrypt all system DNS traffic with DNS-over-TLS.
// udp: false ensures no plaintext UDP queries are sent.
// The TLS connector wraps every TCP connection in TLS (DoT, RFC 7858).
// Nameserver addresses are still read from the OS configuration.
$resolver = new DNS\SystemResolver(
    udp: false,
    connector: new TLS\TCPConnector(new TCP\Connector(), new TLS\Connector(new TLS\ClientConfiguration())),
);

Resolver Types

Resolvers can be composed for different strategies: direct UDP/TCP, fallback on truncation, racing multiple nameservers, DNS-over-TLS, or DNS-over-HTTPS.

use Psl\DNS;
use Psl\TCP;
use Psl\TLS;

// UDP resolver (standard DNS, port 53)
$udp = new DNS\UDPResolver('8.8.8.8');

// TCP resolver with connection pooling
$tcp = new DNS\TCPResolver('8.8.8.8');

// DNS-over-TLS (port 853)
$dot = new DNS\TCPResolver(
    '8.8.8.8',
    port: 853,
    connector: new TLS\TCPConnector(new TCP\Connector(), new TLS\Connector(new TLS\ClientConfiguration())),
);

// DNS-over-HTTPS (RFC 8484)
$doh = new DNS\HTTPSResolver('https://1.1.1.1/dns-query');

// UDP with automatic TCP fallback on truncated responses
$fallback = new DNS\FallbackResolver([$udp, $tcp]);

// Race multiple nameservers, return the fastest response
$racing = new DNS\RacingResolver([
    new DNS\UDPResolver('8.8.8.8'),
    new DNS\UDPResolver('1.1.1.1'),
]);

Record Types

Over 20 DNS record types are supported. Responses provide typed access via getAnswerRecords() and getFirstAnswerRecord().

use Psl\DNS;
use Psl\DNS\Record;

$resolver = new DNS\SystemResolver();

// Query different record types
$a = $resolver->query('example.com', Record\RecordType::A);
$aaaa = $resolver->query('example.com', Record\RecordType::AAAA);
$mx = $resolver->query('example.com', Record\RecordType::MX);
$txt = $resolver->query('example.com', Record\RecordType::TXT);
$srv = $resolver->query('_http._tcp.example.com', Record\RecordType::SRV);

// Extract typed records from a response
foreach ($a->getAnswerRecords(Record\ARecord::class) as $record) {
    $record->name; // "example.com"
    $record->duration; // TTL as Duration
    $record->address; // Psl\IP\Address
}

foreach ($mx->getAnswerRecords(Record\MXRecord::class) as $record) {
    $record->preference; // int
    $record->exchange; // "mail.example.com"
}

// Reverse lookup
$ptr = $resolver->reverseQuery(\Psl\IP\Address::parse('8.8.8.8'));
foreach ($ptr->getAnswerRecords(Record\PTRRecord::class) as $record) {
    $record->target; // "dns.google"
}

Caching

CachedResolver wraps any resolver with TTL-aware caching. Cache TTL is derived from the minimum record TTL in each response.

use Psl\Cache;
use Psl\DNS;

// Wrap any resolver with caching
$resolver = new DNS\CachedResolver(new DNS\SystemResolver(), new Cache\LocalStore());

// First query hits the network
$response = $resolver->query('example.com', DNS\Record\RecordType::A);

// Second query returns from cache (TTL-aware)
$cached = $resolver->query('example.com', DNS\Record\RecordType::A);

Split-Horizon DNS

SplitHorizonResolver routes queries to different resolvers based on the domain name. Domain matching is suffix-based on domain boundaries.

use Psl\DNS;

// Route queries for internal domains to a private nameserver,
// everything else to a public resolver
$resolver = new DNS\SplitHorizonResolver(routes: [
    new DNS\Route(domains: ['corp.internal', 'dev.internal'], resolver: new DNS\UDPResolver('10.0.0.53')),
], default: new DNS\SystemResolver());

// Uses the private nameserver (matches corp.internal)
$resolver->query('db.corp.internal', DNS\Record\RecordType::A);

// Uses the system resolver (no matching route)
$resolver->query('example.com', DNS\Record\RecordType::A);

Search Domains

SearchDomainResolver expands short names by appending search domains. Names with sufficient dots (configurable via $numberOfDots) are queried as-is.

use Psl\DNS;
use Psl\DNS\Record\RecordType;

// Expand short names by appending search domains
$resolver = new DNS\SearchDomainResolver(
    inner: new DNS\SystemResolver(),
    searchDomains: ['example.com', 'corp.example.com'],
);

// Queries "db.example.com", then "db.corp.example.com" if NXDOMAIN
$resolver->query('db', RecordType::A);

// Fully qualified names skip expansion
$resolver->query('google.com', RecordType::A);

System Configuration

Load your OS DNS settings and hosts file directly. Both use async process execution for non-blocking reads.

use Psl\DNS\System\HostsFile\HostsFile;
use Psl\DNS\System\Settings;

// Load system DNS settings (nameservers, search domains)
$settings = Settings::load();
foreach ($settings->nameservers as $ns) {
    $ns->host; // "8.8.8.8"
    $ns->port; // 53
    $ns->forDomains; // [] for global, ["corp.internal"] for scoped
}

$settings->searchDomains; // ["example.com", "corp.example.com"]

// Load and query the hosts file
$hosts = HostsFile::load();
$addresses = $hosts->lookup('localhost');

// [Address("127.0.0.1"), Address("::1")]

EDNS0 Options

Pass EDNS0 options to any query for features like DNS cookies, client subnet hints, padding, and NSID.

use Psl\DNS;
use Psl\DNS\EDNS;
use Psl\DNS\Record\RecordType;
use Psl\IP;
use Psl\SecureRandom;

$resolver = new DNS\SystemResolver();

// Query with EDNS0 options
$response = $resolver->query('example.com', RecordType::A, ednsOptions: [
    new EDNS\CookieOption(clientCookie: SecureRandom\bytes(8)),
    new EDNS\ECSOption(IP\Address::parse('203.0.113.0'), sourcePrefixLength: 24, scopePrefixLength: 0),
    new EDNS\PaddingOption(128),
    new EDNS\NSIDOption(),
]);

DNS-over-HTTPS

HTTPSResolver sends queries over HTTPS (RFC 8484) using the HTTP client. HTTP/2 multiplexing allows concurrent queries to share a single connection. Pass your own ClientInterface to share connection pools across resolvers.

use Psl\Cache;
use Psl\DNS;
use Psl\HTTP\Client;

// Default client — creates one internally
$resolver = new DNS\HTTPSResolver('https://1.1.1.1/dns-query');

// Bring your own client for shared connection pooling
$client = new Client\Client();
$cloudflare = new DNS\HTTPSResolver('https://1.1.1.1/dns-query', $client);
$google = new DNS\HTTPSResolver('https://dns.google/dns-query', $client);

// Race two DoH providers
$racing = new DNS\RacingResolver([$cloudflare, $google]);

// Compose with caching
$cache = new Cache\LocalStore();
$cached = new DNS\CachedResolver($racing, $cache);

$response = $cached->query('example.com', DNS\Record\RecordType::A);

Blocking System Resolver

BlockingSystemResolver wraps PHP's built-in dns_get_record() for synchronous DNS resolution with zero overhead. No event loop, no fibers, no connection pooling. Useful as a lightweight fallback behind a StaticResolver, or in environments where async DNS is unnecessary.

Supports A, AAAA, CNAME, MX, NS, PTR, SOA, SRV, TXT, CAA, and NAPTR record types. EDNS0 options are ignored. Queries for unsupported types return NXDOMAIN.

use Psl\DNS;
use Psl\DNS\Record\RecordType;

// BlockingSystemResolver wraps PHP's dns_get_record() for synchronous,
// zero-overhead DNS resolution. No event loop, no fibers, no connection pooling.
$resolver = new DNS\BlockingSystemResolver();

// Query A records
$response = $resolver->query('example.com', RecordType::A);
foreach ($response->answers as $record) {
    /** @var DNS\Record\ARecord $record */
    $record->address->toString();
    $record->duration->getSeconds();
}

// Query MX records
$response = $resolver->query('example.com', RecordType::MX);
foreach ($response->answers as $record) {
    /** @var DNS\Record\MXRecord $record */
    $record->exchange;
    $record->preference;
}

// Non-existent domain returns NXDOMAIN, never throws
$response = $resolver->query('nonexistent.invalid', RecordType::A);
$response->code; // ResponseCode::NonExistentDomain

// Reverse lookup via PTR
$response = $resolver->reverseQuery(Psl\IP\Address::parse('8.8.8.8'));
foreach ($response->answers as $record) {
    /** @var DNS\Record\PTRRecord $record */
    $record->target; // "dns.google"
}

HTTP Client Integration

DNS\HTTP\Connector is a connector decorator that resolves hostnames via any DNS resolver before delegating to an inner HTTP client connector. This enables fully async DNS resolution, DNS-over-TLS, DNS-over-HTTPS, DNSSEC validation, or custom resolution strategies for all HTTP requests.

When the origin host is already an IP address, no DNS query is performed. For HTTPS connections, the original hostname is preserved for TLS SNI so certificate validation works correctly even though the TCP connection targets the resolved IP.

use Psl\Cache;
use Psl\DNS;
use Psl\HTTP\Client;
use Psl\HTTP\Message;
use Psl\URL;

// Build a resolver chain: cached async UDP resolution with TCP fallback on truncation.
$resolver = new DNS\CachedResolver(new DNS\FallbackResolver([
    new DNS\UDPResolver(host: '1.1.1.1'),
    new DNS\TCPResolver(host: '1.1.1.1'),
]), new Cache\LocalStore());

// Wrap the pooled connector with DNS resolution.
// All HTTP requests will resolve hostnames through the resolver above,
// benefiting from caching, async I/O, and custom nameserver selection.
$connector = new DNS\HTTP\Connector(new Client\Connection\PooledConnector(), $resolver);

$client = new Client\Client(connector: $connector);

$tx = $client->send(new Message\Request(method: 'GET', url: URL\parse('https://example.com')));

$tx->response->status;
$tx->response->body?->readAll();

Kitchen Sink

Compose static entries, hosts file, search domains, split-horizon routing, multiple racing nameservers with DNS-over-TLS or DNS-over-HTTPS, TCP fallback, and caching into a single resolver that uses every feature.

use Psl\Cache;
use Psl\DateTime\Duration;
use Psl\DNS;
use Psl\DNS\Record\ARecord;
use Psl\DNS\Record\RecordType;
use Psl\DNS\System\HostsFile\HostsFile;
use Psl\IO;
use Psl\IP\Address;
use Psl\TCP;
use Psl\TLS;

$cache = new Cache\LocalStore();

// 1. Static entries for internal services (never hit the network)
$static = new DNS\StaticResolver([
    'db.internal' => [
        RecordType::A->value => [
            new ARecord('db.internal', Duration::hours(999), Address::parse('10.0.0.5')),
        ],
    ],
    'cache.internal' => [
        RecordType::A->value => [
            new ARecord('cache.internal', Duration::hours(999), Address::parse('10.0.0.6')),
        ],
    ],
]);

// 2. Internal nameserver for corporate domains (split-horizon)
$internalDns = new DNS\FallbackResolver([
    new DNS\UDPResolver('10.0.0.53'),
    new DNS\TCPResolver('10.0.0.53'),
]);

// 3. Race multiple public nameservers for fastest response
$public = new DNS\RacingResolver([
    // Cloudflare DNS-over-TLS
    new DNS\FallbackResolver([
        new DNS\UDPResolver('1.1.1.1', dnssec: true),
        new DNS\TCPResolver(
            '1.1.1.1',
            port: 853,
            dnssec: true,
            connector: new TLS\TCPConnector(new TCP\Connector(), new TLS\Connector(new TLS\ClientConfiguration())),
        ),
    ]),
    // Google DNS-over-TLS
    new DNS\FallbackResolver([
        new DNS\UDPResolver('8.8.8.8', dnssec: true),
        new DNS\TCPResolver(
            '8.8.8.8',
            port: 853,
            dnssec: true,
            connector: new TLS\TCPConnector(new TCP\Connector(), new TLS\Connector(new TLS\ClientConfiguration())),
        ),
    ]),
    // Cloudflare secondary (plain TCP fallback)
    new DNS\FallbackResolver([
        new DNS\UDPResolver('1.0.0.1', dnssec: true),
        new DNS\TCPResolver('1.0.0.1', dnssec: true),
    ]),
    // Google secondary (plain TCP fallback)
    new DNS\FallbackResolver([
        new DNS\UDPResolver('8.8.4.4', dnssec: true),
        new DNS\TCPResolver('8.8.4.4', dnssec: true),
    ]),
]);

// 4. Static overrides first, then public nameservers
$base = new DNS\FallbackResolver([$static, $public]);

// 5. Route corporate domains to internal DNS, everything else to public
$routed = new DNS\SplitHorizonResolver(routes: [
    new DNS\Route(['corp.internal', 'dev.internal'], $internalDns),
], default: $base);

// 6. Expand short names using search domains
$searched = new DNS\SearchDomainResolver($routed, ['corp.internal', 'dev.internal']);

// 7. Check the hosts file before any network query
$hostsAware = new DNS\HostsFileResolver($searched, HostsFile::load());

// 8. Cache everything on top
$resolver = new DNS\CachedResolver($hostsAware, $cache);

// 9. Profit!
$response = $resolver->query('php-standard-library.dev', RecordType::A);

IO\write_line(
    'php-standard-library.dev -> %s',
    $response->getFirstAnswerRecord(ARecord::class)?->address->toString() ?? '<unknown>',
);

Supported Record Types

Type RFC Description
A RFC 1035 IPv4 address
AAAA RFC 3596 IPv6 address
NS RFC 1035 Authoritative nameserver
CNAME RFC 1035 Canonical name alias
MX RFC 1035 Mail exchange
TXT RFC 1035 Text records (SPF, DKIM, etc.)
SRV RFC 2782 Service location
SOA RFC 1035 Start of authority
PTR RFC 1035 Reverse lookup pointer
CAA RFC 8659 Certificate authority authorization
SSHFP RFC 4255 SSH fingerprint
TLSA RFC 6698 TLS certificate association (DANE)
SVCB RFC 9460 Service binding
HTTPS RFC 9460 HTTPS service binding
LOC RFC 1876 Geographic location
NAPTR RFC 3403 Naming authority pointer
DS RFC 4034 DNSSEC delegation signer
DNSKEY RFC 4034 DNSSEC public key
RRSIG RFC 4034 DNSSEC signature
NSEC RFC 4034 DNSSEC authenticated denial
NSEC3 RFC 5155 DNSSEC hashed denial

System Configuration Sources

OS Source Method
Linux/FreeBSD /etc/resolv.conf cat via Process
macOS scutil --dns Process (includes split DNS, custom ports)
Windows ipconfig /all Process
All platforms hosts file Process

RFC Compliance

RFC Coverage
RFC 1035 DNS wire format, A, NS, CNAME, MX, TXT, SOA, PTR records
RFC 2782 SRV records
RFC 3403 NAPTR records
RFC 3596 AAAA records
RFC 4034 DNSSEC record types (DS, DNSKEY, RRSIG, NSEC)
RFC 4255 SSHFP records
RFC 5155 NSEC3 and NSEC3PARAM records
RFC 6698 TLSA records (DANE)
RFC 6891 EDNS0 (OPT record, extended RCODE, UDP payload size)
RFC 7858 DNS-over-TLS via TLS client configuration
RFC 8484 DNS-over-HTTPS via HTTP client
RFC 7871 EDNS Client Subnet option
RFC 7873 DNS Cookies option
RFC 7830 EDNS TCP Keepalive option
RFC 8467 EDNS Padding option
RFC 8659 CAA records
RFC 9460 SVCB and HTTPS records

See src/Psl/DNS/ for the full API.