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(forAddrinfoandSocket::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 viaAres::RecType. - class — DNS class, defaults to
:IN. Available classes inAres::DnsClass.
Callback arguments:
timeouts— number of timeouts that occurred during the queryanswers— Array of Hashes on success,nilon 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 fromAres::RRFieldMap.extra_or_error— on success, a Hash with:authorityand:additionalarrays (same format as answers). On error, anAres::Errorexception (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 timeoutscnames— Array of CNAME strings, orniladdrinfos— Array ofAddrinfoobjects, ornilerror—Ares::Erroron failure,nilon success
ares.getnameinfo(af, ip_address = nil, port = 0, flags = 0) { |timeouts, name, service, error| ... }
Reverse DNS lookup.
- af — address family (
Socket::AF_INETorSocket::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/LOOKUPSERVICEflags are set automatically based on which of ip_address/port you provide.
Callback arguments:
timeouts— number of timeoutsname— resolved hostname (String or nil)service— resolved service name (String or nil)error—Ares::Erroron failure,nilon 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.