class Coolio::DNSResolver

A non-blocking DNS resolver. It provides interfaces for querying both /etc/hosts and nameserves listed in /etc/resolv.conf, or nameservers of your choosing.

Presently the client only supports UDP requests against your nameservers and cannot resolve anything with records larger than 512-bytes. Also, IPv6 is not presently supported.

DNSResolver objects are one-shot. Once they resolve a domain name they automatically detach themselves from the event loop and cannot be used again.

Constants

DATAGRAM_SIZE
DNS_PORT
RETRIES
TIMEOUT

Public Class Methods

hosts(host, hostfile = Resolv::Hosts::DefaultFileName) click to toggle source

if it errs due to inability to reach the DNS server [Errno::EHOSTUNREACH], same Query /etc/hosts (or the specified hostfile) for the given host

# File lib/cool.io/dns_resolver.rb, line 43
def self.hosts(host, hostfile = Resolv::Hosts::DefaultFileName)
  hosts = {}
  File.open(hostfile) do |f|
    f.each_line do |host_entry|
      entries = host_entry.gsub(/#.*$/, '').gsub(/\s+/, ' ').split(' ')
      addr = entries.shift
      entries.each { |e| hosts[e] ||= addr }
    end
  end
  if hosts.empty?
    # On Windows, there is a case that hosts file doesn't have entry by default
    # and preferred IPv4/IPv6 behavior may be changed by registry key [1], so
    # "localhost" should be resolved by getaddrinfo.
    # (first[3] means preferred resolved IP address ::1 or 127.0.0.1)
    # [1] https://docs.microsoft.com/en-us/troubleshoot/windows-server/networking/configure-ipv6-in-windows
    require "socket"
    hosts["localhost"] = ::Socket.getaddrinfo("localhost", nil).first[3]
  end

  hosts[host]
end
new(hostname, *nameservers) click to toggle source

Create a new Coolio::Watcher descended object to resolve the given hostname. If you so desire you can also specify a list of nameservers to query. By default the resolver will use nameservers listed in /etc/resolv.conf

Calls superclass method Coolio::IOWatcher.new
# File lib/cool.io/dns_resolver.rb, line 69
def initialize(hostname, *nameservers)
  if nameservers.empty?
    nameservers = Resolv::DNS::Config.default_config_hash[:nameserver]
    raise RuntimeError, "no nameservers found" if nameservers.empty? # TODO just call resolve_failed, not raise [also handle Errno::ENOENT)]
  end

  @nameservers = nameservers
  @question = request_question hostname

  @socket = UDPSocket.new
  @timer = Timeout.new(self)

  super(@socket)
end

Public Instance Methods

attach(evloop) click to toggle source

Attach the DNSResolver to the given event loop

Calls superclass method Coolio::IOWatcher#attach
# File lib/cool.io/dns_resolver.rb, line 85
def attach(evloop)
  send_request
  @timer.attach(evloop)
  super
end
detach() click to toggle source

Detach the DNSResolver from the given event loop

Calls superclass method Coolio::IOWatcher#detach
# File lib/cool.io/dns_resolver.rb, line 92
def detach
  @timer.detach if @timer.attached?
  super
end
on_failure() click to toggle source

Called when we receive a response indicating the name didn't resolve

# File lib/cool.io/dns_resolver.rb, line 102
def on_failure; end
on_success(address) click to toggle source

Called when the name has successfully resolved to an address

# File lib/cool.io/dns_resolver.rb, line 98
def on_success(address); end
on_timeout() click to toggle source

Called if we don't receive a response, defaults to calling #on_failure

# File lib/cool.io/dns_resolver.rb, line 106
def on_timeout
  on_failure
end

Protected Instance Methods

on_readable() click to toggle source

Called by the subclass when the DNS response is available

# File lib/cool.io/dns_resolver.rb, line 125
def on_readable
  datagram = nil
  begin
    datagram = @socket.recvfrom_nonblock(DATAGRAM_SIZE).first
  rescue Errno::ECONNREFUSED
  end

  address = response_address datagram rescue nil
  address ? on_success(address) : on_failure
  detach
end
request_message() click to toggle source
# File lib/cool.io/dns_resolver.rb, line 152
def request_message
  # Standard query header
  message = [2, 1, 0].pack('nCC')

  # One entry
  qdcount = 1

  # No answer, authority, or additional records
  ancount = nscount = arcount = 0

  message << [qdcount, ancount, nscount, arcount].pack('nnnn')
  message << @question
end
request_question(hostname) click to toggle source
# File lib/cool.io/dns_resolver.rb, line 137
def request_question(hostname)
  raise ArgumentError, "hostname cannot be nil" if hostname.nil?

  # Query name
  message = hostname.split('.').map { |s| [s.size].pack('C') << s }.join + "\0"

  # Host address query
  qtype = 1

  # Internet query
  qclass = 1

  message << [qtype, qclass].pack('nn')
end
response_address(message) click to toggle source
# File lib/cool.io/dns_resolver.rb, line 166
def response_address(message)
  # Confirm the ID field
  id = message[0..1].unpack('n').first.to_i
  return unless id == 2

  # Check the QR value and confirm this message is a response
  qr = message[2..2].unpack('B1').first.to_i
  return unless qr == 1

  # Check the RCODE (lower nibble) and ensure there wasn't an error
  rcode = message[3..3].unpack('B8').first[4..7].to_i(2)
  return unless rcode == 0

  # Extract the question and answer counts
  qdcount, _ancount = message[4..7].unpack('nn').map { |n| n.to_i }

  # We only asked one question
  return unless qdcount == 1
  message.slice!(0, 12)

  # Make sure it's the same question
  return unless message[0..(@question.size-1)] == @question
  message.slice!(0, @question.size)

  # Extract the RDLENGTH
  while not message.empty?
    type = message[2..3].unpack('n').first.to_i
    rdlength = message[10..11].unpack('n').first.to_i
    rdata = message[12..(12 + rdlength - 1)]
    message.slice!(0, 12 + rdlength)

    # Only IPv4 supported
    next unless rdlength == 4

    # If we got an Internet address back, return it
    return rdata.unpack('CCCC').join('.') if type == 1
  end

  nil
end
send_request() click to toggle source

Send a request to the DNS server

# File lib/cool.io/dns_resolver.rb, line 115
def send_request
  nameserver = @nameservers.shift
  @nameservers << nameserver # rotate them
  begin
    @socket.send request_message, 0, @nameservers.first, DNS_PORT
  rescue Errno::EHOSTUNREACH # TODO figure out why it has to be wrapper here, when the other wrapper should be wrapping this one!
  end
end