NAME

EV::cares - high-performance async DNS resolver using c-ares and EV

SYNOPSIS

use EV;
use EV::cares qw(:status :types :classes);

my $r = EV::cares->new(
    servers => ['8.8.8.8', '1.1.1.1'],
    timeout => 5,
    tries   => 3,
);

# simple A + AAAA resolve
$r->resolve('example.com', sub {
    my ($status, @addrs) = @_;
    if ($status == ARES_SUCCESS) {
        print "resolved: @addrs\n";
    } else {
        warn "failed: " . EV::cares::strerror($status) . "\n";
    }
});

# auto-parsed DNS search
$r->search('example.com', T_MX, sub {
    my ($status, @mx) = @_;
    printf "MX %d %s\n", $_->{priority}, $_->{host} for @mx;
});

# raw DNS query
$r->query('example.com', C_IN, T_A, sub {
    my ($status, $buf) = @_;
    # $buf is the raw DNS response packet
});

EV::run;

DESCRIPTION

EV::cares integrates the c-ares asynchronous DNS library directly with the EV event loop at the C level. Socket I/O and timer management happen entirely in XS with zero Perl-level event processing overhead.

c-ares drives server rotation, retries, timeouts, and search-domain appending. Multiple queries on the same channel run concurrently.

Requires c-ares >= 1.24 (provided by Alien::cares). HTTPS/SVCB/TLSA/DS/DNSKEY/RRSIG parsing requires c-ares >= 1.28; on older builds those types fall through to the raw response buffer.

CONSTRUCTOR

new

my $r = EV::cares->new(%opts);

All options are optional.

servers => \@addrs | "addr1,addr2,..."

DNS server addresses. Default: system resolv.conf servers.

timeout => $seconds

Per-try timeout (fractional seconds).

maxtimeout => $seconds

Maximum total timeout across all tries.

tries => $n

Number of query attempts.

ndots => $n

Threshold for treating a name as absolute (skip search suffixes).

flags => $flags

Bitmask of ARES_FLAG_* constants. Flags missing from the linked c-ares build are exported as 0 (silent no-op when combined). ARES_FLAG_STAYOPEN is a no-op here because socket lifecycle is managed by libev via ARES_OPT_SOCK_STATE_CB.

lookups => $string

Lookup order: "b" for DNS, "f" for /etc/hosts.

rotate => 1

Round-robin among servers. Silently ignored on c-ares builds where ARES_OPT_ROTATE is unavailable.

tcp_port => $port
udp_port => $port

Non-standard DNS port.

ednspsz => $bytes

EDNS0 UDP payload size.

resolvconf => $path

Path to an alternative resolv.conf.

hosts_file => $path

Path to an alternative hosts file.

udp_max_queries => $n

Max queries per UDP connection before reconnect.

qcache => $max_ttl

Enable query result cache; $max_ttl is the upper TTL bound in seconds. 0 disables the cache.

loop => $ev_loop

EV::Loop for I/O and timer watchers. Defaults to the EV default loop.

QUERY METHODS

Every query method takes a callback as the last argument. The first argument to the callback is always a status code (ARES_SUCCESS on success).

resolve

$r->resolve($name, sub { my ($status, @addrs) = @_ });

Resolves $name via ares_getaddrinfo with AF_UNSPEC, returning both IPv4 and IPv6 address strings.

resolve_ttl

$r->resolve_ttl($name, sub {
    my ($status, @records) = @_;
    # @records = ({addr, family, ttl, timeouts, [canonname]}, ...)
});

Like "resolve", but each result is a hashref carrying the per-record TTL reported by the answering nameserver. Useful for application-level caching that respects authoritative TTLs. When the resolver returned a CNAME chain, canonname holds the final canonical name. timeouts is the c-ares retry count for the underlying query.

resolve_all

$r->resolve_all(\@names, sub {
    my ($results) = @_;
    # $results->{$name} = { status => $s, addrs => [...] }
});

Convenience helper that fires one concurrent resolve() per unique name and invokes $cb once with a hashref keyed by name. Duplicate names are deduplicated before issuing queries. Calls $cb synchronously with an empty hashref if the name list is empty.

reverse_all

$r->reverse_all(\@ips, sub {
    my ($results) = @_;
    # $results->{$ip} = { status => $s, hosts => [...] }
});

Bulk reverse-DNS lookup. One reverse() per unique IP (deduplicated). Useful for log enrichment. An invalid IP in the input croaks (same as the underlying "reverse"); validate inputs upfront if your data isn't trusted.

resolve_ttl_all

$r->resolve_ttl_all(\@names, sub {
    my ($results) = @_;
    # $results->{$name} = { status => $s, records => [...] }
    # records are {addr, family, ttl, timeouts, [canonname]} hashrefs
});

Like "resolve_all", but each result entry's records contains the full hashref form (with TTL etc.) produced by "resolve_ttl".

search_all

$r->search_all(\@names, $type, sub {
    my ($results) = @_;
    # $results->{$name} = { status => $s, records => [...] }
});
$r->search_all(\@names, $type, $class, sub { ... });   # explicit class

Like "resolve_all", but issues one search() per unique name for the given record type. Class defaults to C_IN; pass an explicit class as the optional fourth argument. Each result hashref carries the same records arrayref shape that the underlying "search" returns for that type. Useful for bulk MX, TXT, or HTTPS lookups.

getaddrinfo_all

$r->getaddrinfo_all(\@nodes, $service, \%hints, sub {
    my ($results) = @_;
    # $results->{$node} = { status => $s, addrs => [...] }
});

Bulk "getaddrinfo": issues one query per unique node with the same $service/\%hints, fires the callback once with a hashref keyed by node when every query has returned. $service and \%hints may be undef. Result entries' addrs reflect whatever the underlying "getaddrinfo" would return for those hints (scalars by default, or TTL hashrefs when ttl => 1 is set).

getaddrinfo

$r->getaddrinfo($node, $service, \%hints, $cb);

Full getaddrinfo. $service and \%hints may be undef. Hint keys: family, socktype, protocol, flags (ARES_AI_*), plus ttl => 1 to receive {addr, family, ttl, timeouts, [canonname]} hashrefs instead of bare strings. canonname is included only when the answer followed a CNAME chain. Callback receives ($status, @ip_strings) by default, or @hashrefs when ttl is set.

socktype defaults to SOCK_STREAM to coalesce duplicate addresses; pass socktype => 0 only if you want a separate result entry for each socktype the resolver returns.

$r->search($name, $type, sub { my ($status, @records) = @_ });
$r->search($name, $type, $class, sub { ... });   # explicit class

DNS search (appends search domains from resolv.conf). Class defaults to C_IN; pass an explicit class (e.g. C_CHAOS for queries like version.bind) as the optional third argument. Results are auto-parsed based on $type:

T_A, T_AAAA       @ip_strings
T_NS, T_PTR       @hostnames
T_TXT             @strings
T_MX              @{ {priority, host} }
T_SRV             @{ {priority, weight, port, target} }
T_SOA             {mname, rname, serial, refresh, retry,
                   expire, minttl}
T_NAPTR           @{ {order, preference, flags, service,
                      regexp, replacement} }
T_CAA             @{ {critical, property, value} }
T_HTTPS, T_SVCB   @{ {priority, target, params => \%p} }
T_TLSA            @{ {cert_usage, selector,
                      matching_type, data} }
T_DS              @{ {key_tag, algorithm,
                      digest_type, digest} }
T_DNSKEY          @{ {flags, protocol, algorithm,
                      public_key} }
T_RRSIG           @{ {type_covered, algorithm, labels,
                      original_ttl, sig_expiration,
                      sig_inception, key_tag,
                      signer_name, signature} }
T_CNAME, T_ANY,
other             $raw_dns_response_buffer (a wire-format DNS
                  packet -- feed it to e.g. Net::DNS::Packet
                  to decode further)

For TLSA (DANE, RFC 6698), data is the raw fingerprint / certificate bytes; the integer fields are cert_usage (0..3), selector (0..1), and matching_type (0..2). TLSA parsing requires c-ares >= 1.28.

For DS / DNSKEY / RRSIG (DNSSEC, RFC 4034) binary fields are raw wire-format bytes; integer fields use host byte order. digest, public_key, and signature are unmodified base64-able blobs. signer_name is the uncompressed dotted owner name (RFC 4034 sec 3.1.7). Some recursive resolvers strip these records unless EDNS is enabled (flags => ARES_FLAG_EDNS); a validating upstream may also be required if the default refuses to forward them.

For HTTPS/SVCB, %p may contain alpn (arrayref of protocol IDs), no_default_alpn (1 if set), port (integer), ipv4hint / ipv6hint (arrayrefs of address strings), ech (opaque bytes), dohpath (string), and any unrecognized SVCB param as keyN => $bytes. Parsing requires c-ares >= 1.28; on older c-ares HTTPS/SVCB falls through to the raw buffer like unknown types.

query

$r->query($name, $class, $type, sub { my ($status, $buf) = @_ });

Raw DNS query without search-domain appending. Returns the unmodified DNS response packet.

gethostbyname

$r->gethostbyname($name, $family, sub { my ($status, @addrs) = @_ });

Legacy resolver. $family is AF_INET or AF_INET6.

reverse

$r->reverse($ip, sub { my ($status, @hostnames) = @_ });

Reverse DNS (PTR) lookup for an IPv4 or IPv6 address string.

getnameinfo

$r->getnameinfo($packed_sockaddr, $flags, sub {
    my ($status, $node, $service) = @_;
});

Full getnameinfo. $packed_sockaddr comes from "pack_sockaddr_in" in Socket or "pack_sockaddr_in6" in Socket. $flags is a bitmask of ARES_NI_* constants. Note that ARES_NI_TCP is 0 (TCP is the default); pass ARES_NI_DGRAM or equivalently ARES_NI_UDP (both denote the same value) to select datagram-mode lookups.

CHANNEL METHODS

cancel

Cancel all pending queries. Each outstanding callback fires with ARES_ECANCELLED. Safe to call from within a callback. Croaks if called on a destroyed resolver -- guard with "is_destroyed" if you may race a destroy.

set_servers

$r->set_servers('8.8.8.8', '1.1.1.1');
$r->set_servers(['8.8.8.8', '1.1.1.1:5353']);
$r->set_servers([
    { host => '1.1.1.1' },
    { host => '8.8.8.8', port => 53 },
]);

Replace the DNS server list. Accepts a flat list, an arrayref of strings (each may be "host:port"), or an arrayref of { host => ..., port => ... } hashrefs. Croaks if no server is given.

set_sortlist

$r->set_sortlist('192.168.0.0/255.255.0.0 ::1/128');

Set the address-sortlist for ordering returned addresses. See c-ares' ares_set_sortlist for the format (CIDR / netmask pairs separated by whitespace). Croaks on parse error.

servers

my $csv = $r->servers;   # "8.8.8.8,1.1.1.1"

Returns the current server list as a comma-separated string.

set_local_dev

$r->set_local_dev('eth0');

Bind outgoing queries to a network device.

set_local_ip4

$r->set_local_ip4('192.168.1.100');

Bind outgoing queries to a local IPv4 address.

set_local_ip6

$r->set_local_ip6('::1');

Bind outgoing queries to a local IPv6 address.

loop

my $loop = $r->loop;   # EV::Loop ref, or undef for default loop

Returns the EV::Loop the resolver's watchers are armed on, as passed to new(loop => ...). Returns undef when the resolver runs on EV's default loop, and also undef after destroy (the loop reference is released as part of cleanup). Read-only.

active_queries

my $n = $r->active_queries;

Returns the number of outstanding queries. Remains callable after destroy; returns 0 in that case (during interpreter global destruction the count may reflect whatever was pending, since ares_destroy is intentionally skipped on the global-destruction path).

is_busy

if ($r->is_busy) { ... }

Convenience wrapper for $r->active_queries > 0.

wait_idle

my $drained = $r->wait_idle;          # default 30s timeout
my $drained = $r->wait_idle($seconds);

Pumps the EV loop until every pending query on this resolver has fired its callback, or until the timeout elapses. Returns true if the channel drained, false on timeout. Returns immediately when the resolver is already idle. Croaks on a destroyed resolver.

Intended for mostly-synchronous scripts that issue a batch of queries and want to ensure their callbacks have run before continuing. Inside a long-running event-driven program you generally don't need this -- let the existing EV::run drive callbacks.

Must not be called recursively on the same resolver: invoking wait_idle from inside a query callback whose enclosing wait_idle is still pumping the loop can let the outer timeout timer fire during the nested pump and report a spurious timeout.

is_destroyed

if ($r->is_destroyed) { ... }

Returns 1 if destroy has been called on this resolver, 0 otherwise. Useful in long-running daemons that want to skip work without croaking on a torn-down channel. Remains callable after destroy.

next_timeout

my $secs = $r->next_timeout;

Returns the seconds until c-ares' next internal timer (e.g. retry window for an in-flight query), or -1 if no timer is pending. Useful for wiring EV::cares into custom scheduling or for diagnosing a slow upstream. Croaks on a destroyed resolver.

last_query_timeouts

my $n = $r->last_query_timeouts;

Returns the c-ares retry/timeout count of the most recently completed callback. Useful for tuning per-server timeouts; note that with multiple in-flight queries this is whichever callback fired most recently and races accordingly. Remains callable after destroy.

reinit

$r->reinit;

Re-read system DNS configuration (resolv.conf, hosts file) without destroying the channel. Useful for long-running daemons where the resolver configuration may change at runtime.

destroy

$r->destroy;

Explicitly release the c-ares channel and stop all watchers. Pending callbacks fire with ARES_EDESTRUCTION. Safe to call from within a callback or twice in a row. Also called automatically when the object is garbage-collected.

FUNCTIONS

strerror

my $msg = EV::cares::strerror($status);
my $msg = EV::cares->strerror($status);   # also works

Returns a human-readable string for a status code.

lib_version

my $ver = EV::cares::lib_version();   # e.g. "1.34.6"

Returns the c-ares library version string.

ptr_name

my $arpa = EV::cares::ptr_name('192.0.2.1');   # 1.2.0.192.in-addr.arpa
my $arpa = EV::cares::ptr_name('2001:db8::1'); # ...ip6.arpa

Pure-Perl utility that converts an IPv4 or IPv6 literal to its reverse DNS name (.in-addr.arpa or .ip6.arpa). No DNS query is issued; useful when you want to look up the reverse zone yourself via query or search.

parse_header

my $h = EV::cares::parse_header($raw_dns_response);

Decode the 12-byte DNS header. Returns a hashref with id, qr, opcode, aa, tc, rd, ra, z, ad, cd, rcode, qdcount, ancount, nscount, arcount. Useful on the raw buffer from query or unrecognized search types -- e.g. check ad for the upstream resolver's DNSSEC validation claim.

CALLBACK SAFETY

Callbacks fire from within ares_process_fd, driven by EV I/O and timer watchers. Exceptions are caught (G_EVAL) and emitted as warnings; they do not propagate to the caller.

cancel, destroy, and dropping the last reference to the resolver are all safe from inside a callback. Outstanding queries on the same channel receive ARES_ECANCELLED or ARES_EDESTRUCTION.

Local-only lookups (lookups => 'f', hosts-file matches, cached results) may complete synchronously inside the initiating method call; write your code so it tolerates that.

SECURITY

Plain DNS is unauthenticated

The default UDP/TCP transport carries no integrity or origin authentication; an on-path attacker can spoof responses. Do not treat any DNS reply as a trust anchor by itself.

DNSSEC records are parsed but not validated

This module parses DS, DNSKEY, RRSIG, and TLSA wire-format records into Perl hashrefs (see "search") so you can inspect their fields. It does not verify the cryptographic chain of trust. If your application depends on validated DNSSEC, run a validating recursive resolver and rely on the AD bit in the response header (extract via "parse_header" on a raw "query" response).

The AD bit is the upstream resolver's claim

The AD bit returned by "parse_header" reflects what the recursive resolver tells you about validation. It is not a cryptographic guarantee from this code. Use only over a trusted transport (loopback to a local validator, or DoT/DoH where supported by your platform's c-ares build).

No DoT/DoH in c-ares 1.34's CSV parser

As of c-ares 1.34 only the dns:// URI scheme is accepted by ares_set_servers_csv (which this module calls). dns+tls:// and dns+https:// URI forms are not yet supported. Track c-ares releases for upstream availability.

EXPORT TAGS

:status    ARES_SUCCESS  ARES_ENODATA  ARES_ETIMEOUT  ...
:types     T_A  T_AAAA  T_MX  T_SRV  T_TXT  T_NS  T_SOA  ...
:classes   C_IN  C_CHAOS  C_HS  C_ANY
:flags     ARES_FLAG_USEVC  ARES_FLAG_EDNS  ARES_FLAG_DNS0x20  ...
:ai        ARES_AI_CANONNAME  ARES_AI_ADDRCONFIG  ARES_AI_NOSORT  ...
:ni        ARES_NI_NOFQDN  ARES_NI_NUMERICHOST  ...
:families  AF_INET  AF_INET6  AF_UNSPEC
:all       all of the above

SEE ALSO

EV, Alien::cares, https://c-ares.org/.

The eg/ directory has runnable examples: a dig-style CLI, HTTPS/SVCB and TLSA/DANE inspection, DNSSEC zone trace, email-posture checks, MX-to-SMTP probe, log-IP enrichment, a minimal UDP DNS proxy, Mojo interop, and a Future-based parallel resolve.

AUTHOR

vividsnow

LICENSE

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.