lib/celluloid/io/dns_resolver.rb
require "ipaddr"
require "resolv"
module Celluloid
module IO
# Asynchronous DNS resolver using Celluloid::IO::UDPSocket
class DNSResolver
# Maximum UDP packet we'll accept
MAX_PACKET_SIZE = 512
DNS_PORT = 53
@mutex = Mutex.new
@identifier = 1
def self.generate_id
@mutex.synchronize { @identifier = (@identifier + 1) & 0xFFFF }
end
def self.nameservers
Resolv::DNS::Config.default_config_hash[:nameserver]
end
def initialize
# early return for edge case when there are no nameservers configured
# but we still want to be able to static lookups using #resolve_hostname
@nameservers = self.class.nameservers or return
@server = IPAddr.new(@nameservers.sample)
# The non-blocking secret sauce is here, as this is actually a
# Celluloid::IO::UDPSocket
@socket = UDPSocket.new(@server.family)
end
def resolve(hostname)
if host = resolve_hostname(hostname)
unless ip_address = resolve_host(host)
fail Resolv::ResolvError, "invalid entry in hosts file: #{host}"
end
return ip_address
end
query = build_query(hostname)
@socket.send query.encode, 0, @server.to_s, DNS_PORT
data, _ = @socket.recvfrom(MAX_PACKET_SIZE)
response = Resolv::DNS::Message.decode(data)
addrs = []
# The answer might include IN::CNAME entries so filters them out
# to include IN::A & IN::AAAA entries only.
response.each_answer { |name, ttl, value| addrs << value.address if value.respond_to?(:address) }
return if addrs.empty?
return addrs.first if addrs.size == 1
addrs
end
private
def resolve_hostname(hostname)
# Resolv::Hosts#getaddresses pushes onto a stack
# so since we want the first occurance, simply
# pop off the stack.
resolv.getaddresses(hostname).pop
rescue
end
def resolv
@resolv ||= Resolv::Hosts.new
end
def build_query(hostname)
Resolv::DNS::Message.new.tap do |query|
query.id = self.class.generate_id
query.rd = 1
query.add_question hostname, Resolv::DNS::Resource::IN::A
end
end
def resolve_host(host)
resolve_ip(Resolv::IPv4, host) || get_address(host) || resolve_ip(Resolv::IPv6, host)
end
def resolve_ip(klass, host)
klass.create(host)
rescue ArgumentError
end
private
def get_address(host)
Resolv::Hosts.new(host).getaddress
rescue
end
end
end
end