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. Multiple queries run concurrently. c-ares handles server rotation, retries, timeouts, and search-domain appending. Requires c-ares >= 1.24.0 (provided automatically by Alien::cares). HTTPS/SVCB/TLSA/DS/DNSKEY/RRSIG record parsing additionally requires c-ares >= 1.28.0; on older c-ares these 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. Note: c-ares builds without "ARES_FLAG_NO_DFLT_SVR" or "ARES_FLAG_DNS0x20" have those constants exported as 0, so combining them is a silent no-op on older c-ares. "ARES_FLAG_STAYOPEN" has no effect under EV::cares because the module hands socket lifecycle to 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 An EV::Loop instance to attach all I/O and timer watchers to. Defaults to the EV default loop. Useful for multi-loop apps (e.g. when running EV::cares inside a forked or threaded service that uses its own 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 $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) the binary fields are the raw wire-format bytes; integer fields use host byte order. "digest", "public_key", and "signature" are unmodified base64-able blobs. "signer_name" in RRSIG is the dotted owner name (uncompressed, per RFC 4034 section 3.1.7). Recursive resolvers may strip these records unless the DO (DNSSEC OK) bit is set in EDNS, which c-ares does not yet expose; you may need to query a validating resolver directly via "servers" if your default upstream doesn't return 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 its alias "ARES_NI_UDP") 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. 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_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. 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. 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, . The eg/ directory has runnable examples covering the 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.