GitHub

Async DNS client for mruby, built on c-ares.

Supports getaddrinfo, getnameinfo, and arbitrary DNS record queries (A, AAAA, MX, CNAME, TLSA, HTTPS, SRV, TXT, SOA, NS, CAA, and any other type your c-ares version knows about) with full answer/authority/additional section parsing.

Requirements

c-ares >= 1.28.0 (for the ares_dns_record/ares_query_dnsrec APIs) is built from source automatically (via the deps/c-ares git submodule and CMake). You need:

  • CMake (for building c-ares)
  • A C/C++ compiler (gcc or clang)
  • A 64-bit mruby build (MRB_INT_BIT >= 64)

The gem builds c-ares from source automatically via CMake using the git submodule under deps/c-ares — no system installation required. Make sure to clone with --recursive or run git submodule update --init to fetch it.

Installation

Add to your build_config.rb:

MRuby::Build.new do |conf|
  # ...
  conf.gem mgem: 'mruby-c-ares'
end

Dependencies

  • mruby-socket (for Addrinfo and Socket::AF_* constants)
  • mruby-c-ext-helpers

Usage

Quick Start with Ares.run

Ares.run provides a self-contained event loop using IO.select — no external poller needed:

Ares.run do |dns|
  dns.query("example.com", :A) do |timeouts, answers, extra|
    answers.each { |rr| puts "#{rr[:name]} => #{rr[:a_addr]}" }
  end
  dns.query("example.com", :AAAA) do |timeouts, answers, extra|
    answers.each { |rr| puts "#{rr[:name]} => #{rr[:aaaa_addr]}" }
  end
  dns.query("example.com", :MX) do |timeouts, answers, extra|
    answers.each { |rr| puts "pri=#{rr[:mx_preference]} host=#{rr[:mx_exchange]}" }
  end
  dns.getaddrinfo("www.ruby-lang.org", 443) do |timeouts, cnames, addrinfos, error|
    if error
      puts "Error: #{error}"
    else
      puts "CNAMEs: #{cnames.inspect}"
      addrinfos.each { |ai| puts ai.inspect }
    end
  end
  dns.getnameinfo(Socket::AF_INET, "185.199.111.153", 443) do |timeouts, name, service, error|
    puts "Reverse: #{name} / #{service}" unless error
  end
end

Manual Event Loop

For integration with io_uring, epoll, or any custom poller, create the Ares context directly. The block receives socket state changes — you must arrange to poll (or stop polling) each socket accordingly:

ares = Ares.new do |socket, readable, writable|
  # readable/writable are both false when the socket should be removed
  my_poller.update(socket, readable, writable)
end
ares.getaddrinfo("example.com", "https") do |timeouts, cnames, addrinfos, error|
  puts addrinfos.inspect
end
while ares.active_queries > 0
  my_poller.wait(ares.timeout) do |fd, r, w|
    ares.process_fd(r ? fd : -1, w ? fd : -1)
  end
end

See examples/io_uring.rb for a complete io_uring integration.

API Reference

Ares.new(options = Ares::Options.new, &block) → Ares

Creates a new resolver context.

block {|socket, readable, writable| ... } — called by c-ares whenever socket polling state changes. When both readable and writable are false, stop polling that socket.

options can be an Ares::Options object or a Hash with any of the following keys:

Key Type Description
:flags Integer Bitwise OR of Ares::FLAG_* constants
:timeout Integer Query timeout in milliseconds
:tries Integer Number of query attempts
:ndots Integer Threshold for FQDN detection
:domains Array Search domain list (passed as splat to domains_set)
:ednspsz Integer EDNS UDP payload size
:resolvconf_path String Path to resolv.conf
:hosts_path String Path to hosts file
:udp_max_queries Integer Max queries per UDP connection
:maxtimeout Integer Maximum timeout in milliseconds
:qcache_max_ttl Integer Query cache max TTL (seconds)

Available options depend on your c-ares version. Call Ares::Options::AVAILABLE_OPTIONS to see which are supported.

For details on what each option does: https://c-ares.org/docs/ares_init_options.html

Ares.run(options = Ares::Options.new) { |dns| ... } → Ares

Convenience method that creates a fresh resolver with IO.select-based polling, yields it to the block, then runs the event loop until all queries complete (including retries and timeouts). No external poller required. options accepts the same values as Ares.new.

ares.query(name, type, class = :IN) { |timeouts, answers, extra_or_error| ... }

Performs a DNS query using the c-ares dnsrec API.

  • name — domain name to query (String)
  • type — record type as a Symbol (:A, :AAAA, :MX, :CNAME, :TXT, :SRV, :SOA, :NS, :CAA, :TLSA, :HTTPS, :SVCB, etc.). All types known to your c-ares version are available via Ares::RecType.
  • class — DNS class, defaults to :IN. Available classes in Ares::DnsClass.

Callback arguments:

  • timeouts — number of timeouts that occurred during the query
  • answers — Array of Hashes on success, nil on error. Each hash contains :name, :type (Symbol), :class (Symbol), :ttl, plus type-specific fields (e.g. :a_addr, :aaaa_addr, :mx_preference, :mx_exchange, :txt_data, :srv_target, etc.). Field names come from Ares::RRFieldMap.
  • extra_or_error — on success, a Hash with :authority and :additional arrays (same format as answers). On error, an Ares::Error exception (not raised, just passed).

Returns the query id (Integer). If the query cannot even be enqueued, the error is delivered through the callback like any other DNS error.

ares.search(name, type, class = :IN) { |timeouts, answers, extra_or_error| ... }

Like query, but applies the configured search domains and ndots rules (see ares_search), trying each candidate name in order until one yields a result.

ares.getaddrinfo(name, service, flags = 0, family = AF_UNSPEC, socktype = 0, protocol = 0) { |timeouts, cnames, addrinfos, error| ... }

Resolves a hostname to addresses, similar to POSIX getaddrinfo.

  • name — hostname to resolve (String, or nil)
  • service — service name (String), port number (Integer), or nil
  • flags, family, socktype, protocol — optional hints (see Ares::AI_* constants)

Callback arguments:

  • timeouts — number of timeouts
  • cnames — Array of CNAME strings, or nil
  • addrinfos — Array of Addrinfo objects, or nil
  • errorAres::Error on failure, nil on success

ares.getnameinfo(af, ip_address = nil, port = 0, flags = 0) { |timeouts, name, service, error| ... }

Reverse DNS lookup.

  • af — address family (Socket::AF_INET or Socket::AF_INET6)
  • ip_address — IP address string to reverse-lookup (optional)
  • port — port number to reverse-lookup (optional)
  • flags — bitwise OR of Ares::NI_* constants. LOOKUPHOST/LOOKUPSERVICE flags are set automatically based on which of ip_address/port you provide.

Callback arguments:

  • timeouts — number of timeouts
  • name — resolved hostname (String or nil)
  • service — resolved service name (String or nil)
  • errorAres::Error on failure, nil on success

ares.timeout(max = nil) → Float

Returns the number of seconds until the next timeout fires. Without max, returns 0.0 when no queries are pending. With max, the result is clamped to at most max (and is exactly max when idle) — suitable for passing straight to a poller.

ares.active_queries → Integer

Number of in-flight queries. Use active_queries > 0 as the loop condition of a custom event loop.

ares.process_fd(read_fd, write_fd)

Notify c-ares that a socket is ready for reading/writing. Pass -1 for a direction that isn't ready.

ares.process(readable_ios, writable_ios)

Notify c-ares using arrays of IO objects (as returned by IO.select). Builds fd_sets internally.

ares.servers_ports_csv = "ip[:port][%iface],..."

Set DNS servers. See https://c-ares.org/docs/ares_set_servers_ports_csv.html

ares.local_ip4 = "1.2.3.4"

Set the local IPv4 address for outgoing queries.

ares.local_ip6 = "::1"

Set the local IPv6 address for outgoing queries.

Constants

All c-ares ARES_* constants are mapped directly — ARES_AI_NUMERICSERV becomes Ares::AI_NUMERICSERV, ARES_FLAG_USEVC becomes Ares::FLAG_USEVC, etc.

Status codes are mapped to exception subclasses under Ares::. For example, ARES_ENOTFOUND corresponds to Ares::ENOTFOUND < Ares::Error.

Record types and DNS classes are available as symbol-keyed hashes:

  • Ares::RecType{ A: 1, AAAA: 28, MX: 15, ... }
  • Ares::DnsClass{ IN: 1, ... }
  • Ares::RecTypeInverse / Ares::DnsClassInverse — reverse mappings (integer → symbol)

RR field names: Ares::RRFieldMap maps c-ares ARES_RR_* integer keys to symbols. OPT/SVCB parameter names: Ares::RROptParamMap.

Error Handling

Usage errors (wrong argument types, missing blocks) raise exceptions immediately.

DNS resolution errors are passed to the callback as an Ares::Error subclass — they are not raised. Check the error argument in your callback:

ares.query("nonexistent.invalid", :A) do |timeouts, answers, extra_or_error|
  if answers.nil?
    puts "DNS error: #{extra_or_error.message}"  # => "Domain name not found"
    puts extra_or_error.class                     # => Ares::ENOTFOUND
  end
end

License

MIT — see LICENSE.

Read the original on github.com ↗