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 <https://c-ares.org/> 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, <https://c-ares.org/>.

    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.

