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 as0(silent no-op when combined).ARES_FLAG_STAYOPENis a no-op here because socket lifecycle is managed by libev viaARES_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_ROTATEis 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_ttlis 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.
search
$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, andTLSAwire-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 theADbit in the response header (extract via "parse_header" on a raw "query" response). - The AD bit is the upstream resolver's claim
-
The
ADbit 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 byares_set_servers_csv(which this module calls).dns+tls://anddns+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.