metasploit-framework/lib/net/dns/resolver.rb

1318 lines
43 KiB
Ruby
Raw Normal View History

# -*- coding: binary -*-
#
# $Id: Resolver.rb,v 1.11 2006/07/30 16:55:35 bluemonk Exp $
#
require 'socket'
require 'timeout'
require 'ipaddr'
require 'logger'
require 'net/dns/packet'
require 'net/dns/resolver/timeouts'
alias old_send send
module Net # :nodoc:
module DNS
include Logger::Severity
# =Name
#
# Net::DNS::Resolver - DNS resolver class
#
# =Synopsis
#
# require 'net/dns/resolver'
#
# =Description
#
# The Net::DNS::Resolver class implements a complete DNS resolver written
# in pure Ruby, without a single C line of code. It has all of the
# tipical properties of an evoluted resolver, and a bit of OO which
# comes from having used Ruby.
#
# This project started as a porting of the Net::DNS Perl module,
# written by Martin Fuhr, but turned out (in the last months) to be
# an almost complete rewriting. Well, maybe some of the features of
# the Perl version are still missing, but guys, at least this is
# readable code!
#
# FIXME
#
# =Environment
#
# The Following Environment variables can also be used to configure
# the resolver:
#
# * +RES_NAMESERVERS+: A space-separated list of nameservers to query.
#
# # Bourne Shell
# $ RES_NAMESERVERS="192.168.1.1 192.168.2.2 192.168.3.3"
# $ export RES_NAMESERVERS
#
# # C Shell
# % setenv RES_NAMESERVERS "192.168.1.1 192.168.2.2 192.168.3.3"
#
# * +RES_SEARCHLIST+: A space-separated list of domains to put in the
# search list.
#
# # Bourne Shell
# $ RES_SEARCHLIST="example.com sub1.example.com sub2.example.com"
# $ export RES_SEARCHLIST
#
# # C Shell
# % setenv RES_SEARCHLIST "example.com sub1.example.com sub2.example.com"
#
# * +LOCALDOMAIN+: The default domain.
#
# # Bourne Shell
# $ LOCALDOMAIN=example.com
# $ export LOCALDOMAIN
#
# # C Shell
# % setenv LOCALDOMAIN example.com
#
# * +RES_OPTIONS+: A space-separated list of resolver options to set.
# Options that take values are specified as option:value.
#
# # Bourne Shell
# $ RES_OPTIONS="retrans:3 retry:2 debug"
# $ export RES_OPTIONS
#
# # C Shell
# % setenv RES_OPTIONS "retrans:3 retry:2 debug"
#
class Resolver
# An hash with the defaults values of almost all the
# configuration parameters of a resolver object. See
# the description for each parameter to have an
# explanation of its usage.
Defaults = {
:config_file => "/etc/resolv.conf",
:log_file => $stdout,
:port => 53,
:searchlist => [],
:nameservers => [IPAddr.new("127.0.0.1")],
:domain => "",
:source_port => 0,
:source_address => IPAddr.new("0.0.0.0"),
:retry_interval => 5,
:retry_number => 4,
:recursive => true,
:defname => true,
:dns_search => true,
:use_tcp => false,
:ignore_truncated => false,
:packet_size => 512,
:tcp_timeout => TcpTimeout.new(120),
:udp_timeout => UdpTimeout.new(0)}
# Create a new resolver object.
#
# Argument +config+ can either be empty or be an hash with
# some configuration parameters. To know what each parameter
# do, look at the description of each.
# Some example:
#
# # Use the sistem defaults
# res = Net::DNS::Resolver.new
#
# # Specify a configuration file
# res = Net::DNS::Resolver.new(:config_file => '/my/dns.conf')
#
# # Set some option
# res = Net::DNS::Resolver.new(:nameservers => "172.16.1.1",
# :recursive => false,
# :retry => 10)
#
# ===Config file
#
# Net::DNS::Resolver uses a config file to read the usual
# values a resolver needs, such as nameserver list and
# domain names. On UNIX systems the defaults are read from the
# following files, in the order indicated:
#
# * /etc/resolv.conf
# * $HOME/.resolv.conf
# * ./.resolv.conf
#
# The following keywords are recognized in resolver configuration files:
#
# * domain: the default domain.
# * search: a space-separated list of domains to put in the search list.
# * nameserver: a space-separated list of nameservers to query.
#
# Files except for /etc/resolv.conf must be owned by the effective userid
# running the program or they won't be read. In addition, several environment
# variables can also contain configuration information; see Environment
# in the main description for Resolver class.
#
# On Windows Systems, an attempt is made to determine the system defaults
# using the registry. This is still a work in progress; systems with many
# dynamically configured network interfaces may confuse Net::DNS.
#
# You can include a configuration file of your own when creating a resolver
# object:
#
# # Use my own configuration file
# my $res = Net::DNS::Resolver->new(config_file => '/my/dns.conf');
#
# This is supported on both UNIX and Windows. Values pulled from a custom
# configuration file override the the system's defaults, but can still be
# overridden by the other arguments to Resolver::new.
#
# Explicit arguments to Resolver::new override both the system's defaults
# and the values of the custom configuration file, if any.
#
# ===Parameters
#
# The following arguments to Resolver::new are supported:
#
# - nameservers: an array reference of nameservers to query.
# - searchlist: an array reference of domains.
# - recurse
# - debug
# - domain
# - port
# - srcaddr
# - srcport
# - tcp_timeout
# - udp_timeout
# - retrans
# - retry
# - usevc
# - stayopen
# - igntc
# - defnames
# - dnsrch
# - persistent_tcp
# - persistent_udp
# - dnssec
#
# For more information on any of these options, please consult the
# method of the same name.
#
# ===Disclaimer
#
# Part of the above documentation is taken from the one in the
# Net::DNS::Resolver Perl module.
#
def initialize(config = {})
raise ResolverArgumentError, "Argument has to be Hash" unless config.kind_of? Hash
# config.key_downcase!
@config = Defaults.merge config
@raw = false
# New logger facility
@logger = Logger.new(@config[:log_file])
@logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
#------------------------------------------------------------
# Resolver configuration will be set in order from:
# 1) initialize arguments
# 2) ENV variables
# 3) config file
# 4) defaults (and /etc/resolv.conf for config)
#------------------------------------------------------------
#------------------------------------------------------------
# Parsing config file
#------------------------------------------------------------
parse_config_file
#------------------------------------------------------------
# Parsing ENV variables
#------------------------------------------------------------
parse_environment_variables
#------------------------------------------------------------
# Parsing arguments
#------------------------------------------------------------
config.each do |key,val|
next if key == :log_file or key == :config_file
begin
eval "self.#{key.to_s} = val"
rescue NoMethodError
raise ResolverArgumentError, "Option #{key} not valid"
end
end
end
# Get the resolver searchlist, returned as an array of entries
#
# res.searchlist
# #=> ["example.com","a.example.com","b.example.com"]
#
def searchlist
@config[:searchlist].inspect
end
# Set the resolver searchlist.
# +arg+ can be a single string or an array of strings
#
# res.searchstring = "example.com"
# res.searchstring = ["example.com","a.example.com","b.example.com"]
#
# Note that you can also append a new name to the searchlist
#
# res.searchlist << "c.example.com"
# res.searchlist
# #=> ["example.com","a.example.com","b.example.com","c.example.com"]
#
# The default is an empty array
#
def searchlist=(arg)
case arg
when String
@config[:searchlist] = [arg] if valid? arg
@logger.info "Searchlist changed to value #{@config[:searchlist].inspect}"
when Array
@config[:searchlist] = arg if arg.all? {|x| valid? x}
@logger.info "Searchlist changed to value #{@config[:searchlist].inspect}"
else
raise ResolverArgumentError, "Wrong argument format, neither String nor Array"
end
end
# Get the list of resolver nameservers, in a dotted decimal format
#
# res.nameservers
# #=> ["192.168.0.1","192.168.0.2"]
#
def nameservers
arr = []
@config[:nameservers].each do |x|
arr << x.to_s
end
arr
end
alias_method :nameserver, :nameservers
# Set the list of resolver nameservers
# +arg+ can be a single ip address or an array of addresses
#
# res.nameservers = "192.168.0.1"
# res.nameservers = ["192.168.0.1","192.168.0.2"]
#
# If you want you can specify the addresses as IPAddr instances
#
# ip = IPAddr.new("192.168.0.3")
# res.nameservers << ip
# #=> ["192.168.0.1","192.168.0.2","192.168.0.3"]
#
# The default is 127.0.0.1 (localhost)
#
def nameservers=(arg)
case arg
when String
begin
@config[:nameservers] = [IPAddr.new(arg)]
@logger.info "Nameservers list changed to value #{@config[:nameservers].inspect}"
rescue ArgumentError # arg is in the name form, not IP
nameservers_from_name(arg)
end
when IPAddr
@config[:nameservers] = [arg]
@logger.info "Nameservers list changed to value #{@config[:nameservers].inspect}"
when Array
@config[:nameservers] = []
arg.each do |x|
@config[:nameservers] << case x
when String
begin
IPAddr.new(x)
rescue ArgumentError
nameservers_from_name(arg)
return
end
when IPAddr
x
else
raise ResolverArgumentError, "Wrong argument format"
end
end
@logger.info "Nameservers list changed to value #{@config[:nameservers].inspect}"
else
raise ResolverArgumentError, "Wrong argument format, neither String, Array nor IPAddr"
end
end
alias_method("nameserver=","nameservers=")
# Return a string with the default domain
#
def domain
@config[:domain].inspect
end
# Set the domain for the query
#
def domain=(name)
@config[:domain] = name if valid? name
end
# Return the defined size of the packet
#
def packet_size
@config[:packet_size]
end
# Get the port number to which the resolver sends queries.
#
# puts "Sending queries to port #{res.port}"
#
def port
@config[:port]
end
# Set the port number to which the resolver sends queries. This can be useful
# for testing a nameserver running on a non-standard port.
#
# res.port = 10053
#
# The default is port 53.
#
def port=(num)
if (0..65535).include? num
@config[:port] = num
@logger.info "Port number changed to #{num}"
else
raise ResolverArgumentError, "Wrong port number #{num}"
end
end
# Get the value of the source port number
#
# puts "Sending queries using port #{res.source_port}"
#
def source_port
@config[:source_port]
end
alias srcport source_port
# Set the local source port from which the resolver sends its queries.
#
# res.source_port = 40000
#
# Note that if you want to set a port you need root priviledges, as
# raw sockets will be used to generate packets. The class will then
# generate the exception ResolverPermissionError if you're not root.
#
# The default is 0, which means that the port will be chosen by the
# underlaying layers.
#
def source_port=(num)
unless root?
raise ResolverPermissionError, "Are you root?"
end
if (0..65535).include?(num)
@config[:source_port] = num
else
raise ResolverArgumentError, "Wrong port number #{num}"
end
end
alias srcport= source_port=
# Get the local address from which the resolver sends queries
#
# puts "Sending queries using source address #{res.source_address}"
#
def source_address
@config[:source_address].to_s
end
alias srcaddr source_address
# Set the local source address from which the resolver sends its
# queries.
#
# res.source_address = "172.16.100.1"
# res.source_address = IPAddr.new("172.16.100.1")
#
# You can specify +arg+ as either a string containing the ip address
# or an instance of IPAddr class.
#
# Normally this can be used to force queries out a specific interface
# on a multi-homed host. In this case, you should of course need to
# know the addresses of the interfaces.
#
# Another way to use this option is for some kind of spoofing attacks
# towards weak nameservers, to probe the security of your network.
# This includes specifing ranged attacks such as DoS and others. For
# a paper on DNS security, checks http://www.marcoceresa.com/security/
#
# Note that if you want to set a non-binded source address you need
# root priviledges, as raw sockets will be used to generate packets.
# The class will then generate an exception if you're not root.
#
# The default is 0.0.0.0, meaning any local address (chosen on routing
# needs).
#
def source_address=(addr)
unless addr.respond_to? :to_s
raise ResolverArgumentError, "Wrong address argument #{addr}"
end
begin
port = rand(64000)+1024
@logger.warn "Try to determine state of source address #{addr} with port #{port}"
a = TCPServer.new(addr.to_s,port)
rescue SystemCallError => e
case e.errno
when 98 # Port already in use!
@logger.warn "Port already in use"
retry
when 99 # Address is not valid: raw socket
@raw = true
@logger.warn "Using raw sockets"
else
raise SystemCallError, e
end
ensure
a.close
end
case addr
when String
@config[:source_address] = IPAddr.new(string)
@logger.info "Using new source address: #{@config[:source_address]}"
when IPAddr
@config[:source_address] = addr
@logger.info "Using new source address: #{@config[:source_address]}"
else
raise ArgumentError, "Unknown dest_address format"
end
end
alias srcaddr= source_address=
# Return the retrasmission interval (in seconds) the resolvers has
# been set on
#
def retry_interval
@config[:retry_interval]
end
alias retrans retry_interval
# Set the retrasmission interval in seconds. Default 5 seconds
#
def retry_interval=(num)
if num > 0
@config[:retry_interval] = num
@logger.info "Retransmission interval changed to #{num} seconds"
else
raise ResolverArgumentError, "Interval must be positive"
end
end
alias retrans= retry_interval=
# The number of times the resolver will try a query
#
# puts "Will try a max of #{res.retry_number} queries"
#
def retry_number
@config[:retry_number]
end
# Set the number of times the resolver will try a query.
# Default 4 times
#
def retry_number=(num)
if num.kind_of? Integer and num > 0
@config[:retry_number] = num
@logger.info "Retrasmissions number changed to #{num}"
else
raise ResolverArgumentError, "Retry value must be a positive integer"
end
end
alias_method('retry=', 'retry_number=')
# This method will return true if the resolver is configured to
# perform recursive queries.
#
# print "The resolver will perform a "
# print res.recursive? ? "" : "not "
# puts "recursive query"
#
def recursive?
@config[:recursive]
end
alias_method :recurse, :recursive?
alias_method :recursive, :recursive?
# Sets whether or not the resolver should perform recursive
# queries. Default is true.
#
# res.recursive = false # perform non-recursive query
#
def recursive=(bool)
case bool
when TrueClass,FalseClass
@config[:recursive] = bool
@logger.info("Recursive state changed to #{bool}")
else
raise ResolverArgumentError, "Argument must be boolean"
end
end
alias_method :recurse=, :recursive=
# Return a string rapresenting the resolver state, suitable
# for printing on the screen.
#
# puts "Resolver state:"
# puts res.state
#
def state
str = ";; RESOLVER state:\n;; "
i = 1
@config.each do |key,val|
if key == :log_file or key == :config_file
str << "#{key}: #{val} \t"
else
str << "#{key}: #{eval(key.to_s)} \t"
end
str << "\n;; " if i % 2 == 0
i += 1
end
str
end
alias print state
alias inspect state
# Checks whether the +defname+ flag has been activate.
def defname?
@config[:defname]
end
alias defname defname?
# Set the flag +defname+ in a boolean state. if +defname+ is true,
# calls to Resolver#query will append the default domain to names
# that contain no dots.
# Example:
#
# # Domain example.com
# res.defname = true
# res.query("machine1")
# #=> This will perform a query for machine1.example.com
#
# Default is true.
#
def defname=(bool)
case bool
when TrueClass,FalseClass
@config[:defname] = bool
@logger.info("Defname state changed to #{bool}")
else
raise ResolverArgumentError, "Argument must be boolean"
end
end
# Get the state of the dns_search flag
def dns_search
@config[:dns_search]
end
alias_method :dnsrch, :dns_search
# Set the flag +dns_search+ in a boolean state. If +dns_search+
# is true, when using the Resolver#search method will be applied
# the search list. Default is true.
#
def dns_search=(bool)
case bool
when TrueClass,FalseClass
@config[:dns_search] = bool
@logger.info("DNS search state changed to #{bool}")
else
raise ResolverArgumentError, "Argument must be boolean"
end
end
alias_method("dnsrch=","dns_search=")
# Get the state of the use_tcp flag.
#
def use_tcp?
@config[:use_tcp]
end
alias_method :usevc, :use_tcp?
alias_method :use_tcp, :use_tcp?
# If +use_tcp+ is true, the resolver will perform all queries
# using TCP virtual circuits instead of UDP datagrams, which
# is the default for the DNS protocol.
#
# res.use_tcp = true
# res.query "host.example.com"
# #=> Sending TCP segments...
#
# Default is false.
#
def use_tcp=(bool)
case bool
when TrueClass,FalseClass
@config[:use_tcp] = bool
@logger.info("Use tcp flag changed to #{bool}")
else
raise ResolverArgumentError, "Argument must be boolean"
end
end
alias usevc= use_tcp=
def ignore_truncated?
@config[:ignore_truncated]
end
alias_method :ignore_truncated, :ignore_truncated?
def ignore_truncated=(bool)
case bool
when TrueClass,FalseClass
@config[:ignore_truncated] = bool
@logger.info("Ignore truncated flag changed to #{bool}")
else
raise ResolverArgumentError, "Argument must be boolean"
end
end
# Return an object representing the value of the stored TCP
# timeout the resolver will use in is queries. This object
# is an instance of the class +TcpTimeout+, and two methods
# are available for printing informations: TcpTimeout#to_s
# and TcpTimeout#pretty_to_s.
#
# Here's some example:
#
# puts "Timeout of #{res.tcp_timeout} seconds" # implicit to_s
# #=> Timeout of 150 seconds
#
# puts "You set a timeout of " + res.tcp_timeout.pretty_to_s
# #=> You set a timeout of 2 minutes and 30 seconds
#
# If the timeout is infinite, a string "infinite" will
# be returned.
#
def tcp_timeout
@config[:tcp_timeout].to_s
end
# Set the value of TCP timeout for resolver queries that
# will be performed using TCP. A value of 0 means that
# the timeout will be infinite.
# The value is stored internally as a +TcpTimeout+ object, see
# the description for Resolver#tcp_timeout
#
# Default is 120 seconds
def tcp_timeout=(secs)
@config[:tcp_timeout] = TcpTimeout.new(secs)
@logger.info("New TCP timeout value: #{@config[:tcp_timeout]} seconds")
end
# Return an object representing the value of the stored UDP
# timeout the resolver will use in is queries. This object
# is an instance of the class +UdpTimeout+, and two methods
# are available for printing informations: UdpTimeout#to_s
# and UdpTimeout#pretty_to_s.
#
# Here's some example:
#
# puts "Timeout of #{res.udp_timeout} seconds" # implicit to_s
# #=> Timeout of 150 seconds
#
# puts "You set a timeout of " + res.udp_timeout.pretty_to_s
# #=> You set a timeout of 2 minutes and 30 seconds
#
# If the timeout is zero, a string "not defined" will
# be returned.
#
def udp_timeout
@config[:udp_timeout].to_s
end
# Set the value of UDP timeout for resolver queries that
# will be performed using UDP. A value of 0 means that
# the timeout will not be used, and the resolver will use
# only +retry_number+ and +retry_interval+ parameters.
# That is the default.
#
# The value is stored internally as a +UdpTimeout+ object, see
# the description for Resolver#udp_timeout
#
def udp_timeout=(secs)
@config[:udp_timeout] = UdpTimeout.new(secs)
@logger.info("New UDP timeout value: #{@config[:udp_timeout]} seconds")
end
# Set a new log file for the logger facility of the resolver
# class. Could be a file descriptor too:
#
# res.log_file = $stderr
#
# Note that a new logging facility will be create, destroing
# the old one, which will then be impossibile to recover.
#
def log_file=(log)
@logger.close
@config[:log_file] = log
@logger = Logger.new(@config[:log_file])
@logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
end
# This one permits to have a personal logger facility to handle
# resolver messages, instead of new built-in one, which is set up
# for a +$stdout+ (or +$stderr+) use.
#
# If you want your own logging facility you can create a new instance
# of the +Logger+ class:
#
# log = Logger.new("/tmp/resolver.log","weekly",2*1024*1024)
# log.level = Logger::DEBUG
# log.progname = "ruby_resolver"
#
# and then pass it to the resolver:
#
# res.logger = log
#
# Note that this will destroy the precedent logger.
#
def logger=(logger)
if logger.kind_of? Logger
@logger.close
@logger = logger
else
raise ResolverArgumentError, "Argument must be an instance of Logger class"
end
end
# Set the log level for the built-in logging facility.
#
# The log level can be one of the following:
#
# - +Net::DNS::DEBUG+
# - +Net::DNS::INFO+
# - +Net::DNS::WARN+
# - +Net::DNS::ERROR+
# - +Net::DNS::FATAL+
#
# Note that if the global variable $DEBUG is set (like when the
# -d switch is used at the command line) the logger level is
# automatically set at DEGUB.
#
# For further informations, see Logger documentation in the
# Ruby standard library.
#
def log_level=(level)
@logger.level = level
end
# Performs a DNS query for the given name, applying the searchlist if
# appropriate. The search algorithm is as follows:
#
# 1. If the name contains at least one dot, try it as is.
# 2. If the name doesn't end in a dot then append each item in the search
# list to the name. This is only done if +dns_search+ is true.
# 3. If the name doesn't contain any dots, try it as is.
#
# The record type and class can be omitted; they default to +A+ and +IN+.
#
# packet = res.search('mailhost')
# packet = res.search('mailhost.example.com')
# packet = res.search('example.com', Net::DNS::MX)
# packet = res.search('user.passwd.example.com', Net::DNS::TXT, Net::DNS::HS)
#
# If the name is an IP address (Ipv4 or IPv6), in the form of a string
# or a +IPAddr+ object, then an appropriate PTR query will be performed:
#
# ip = IPAddr.new("172.16.100.2")
# packet = res.search(ip)
# packet = res.search("192.168.10.254")
#
# Returns a Net::DNS::Packet object. If you need to examine the response packet
# whether it contains any answers or not, use the send() method instead.
#
def search(name,type=Net::DNS::A,cls=Net::DNS::IN)
# If the name contains at least one dot then try it as is first.
if name.include? "."
@logger.debug "Search(#{name},#{Net::DNS::RR::Types.new(type)},#{Net::DNS::RR::Classes.new(cls)})"
ans = query(name,type,cls)
return ans if ans.header.anCount > 0
end
# If the name doesn't end in a dot then apply the search list.
if name !~ /\.$/ and @config[:dns_search]
@config[:searchlist].each do |domain|
newname = name + "." + domain
@logger.debug "Search(#{newname},#{Net::DNS::RR::Types.new(type)},#{Net::DNS::RR::Classes.new(cls)})"
ans = query(newname,type,cls)
return ans if ans.header.anCount > 0
end
end
# Finally, if the name has no dots then try it as is.
@logger.debug "Search(#{name},#{Net::DNS::RR::Types.new(type)},#{Net::DNS::RR::Classes.new(cls)})"
query(name+".",type,cls)
end
# Performs a DNS query for the given name; the search list
# is not applied. If the name doesn't contain any dots and
# +defname+ is true then the default domain will be appended.
#
# The record type and class can be omitted; they default to +A+
# and +IN+. If the name looks like an IP address (IPv4 or IPv6),
# then an appropriate PTR query will be performed.
#
# packet = res.query('mailhost')
# packet = res.query('mailhost.example.com')
# packet = res.query('example.com', Net::DNS::MX)
# packet = res.query('user.passwd.example.com', Net::DNS::TXT, Net::DNS::HS)
#
# If the name is an IP address (Ipv4 or IPv6), in the form of a string
# or a +IPAddr+ object, then an appropriate PTR query will be performed:
#
# ip = IPAddr.new("172.16.100.2")
# packet = res.query(ip)
# packet = res.query("192.168.10.254")
#
# Returns a Net::DNS::Packet object. If you need to examine the response
# packet whether it contains any answers or not, use the Resolver#send
# method instead.
#
def query(name,type=Net::DNS::A,cls=Net::DNS::IN)
# If the name doesn't contain any dots then append the default domain.
if name !~ /\./ and name !~ /:/ and @config[:defnames]
name += "." + @config[:domain]
end
@logger.debug "Query(#{name},#{Net::DNS::RR::Types.new(type)},#{Net::DNS::RR::Classes.new(cls)})"
send(name,type,cls)
end
# Performs a DNS query for the given name. Neither the
# searchlist nor the default domain will be appended.
#
# The argument list can be either a Net::DNS::Packet object
# or a name string plus optional type and class, which if
# omitted default to +A+ and +IN+.
#
# Returns a Net::DNS::Packet object.
#
# # Sending a +Packet+ object
# send_packet = Net::DNS::Packet.new("host.example.com",Net::DNS::NS,Net::DNS::HS)
# packet = res.send(send_packet)
#
# # Performing a query
# packet = res.send("host.example.com")
# packet = res.send("host.example.com",Net::DNS::NS)
# packet = res.send("host.example.com",Net::DNS::NS,Net::DNS::HS)
#
# If the name is an IP address (Ipv4 or IPv6), in the form of a string
# or a IPAddr object, then an appropriate PTR query will be performed:
#
# ip = IPAddr.new("172.16.100.2")
# packet = res.send(ip)
# packet = res.send("192.168.10.254")
#
# Use +packet.header.ancount+ or +packet.answer+ to find out if there
# were any records in the answer section.
#
def send(argument,type=Net::DNS::A,cls=Net::DNS::IN)
if @config[:nameservers].size == 0
raise ResolverError, "No nameservers specified!"
end
method = :send_udp
if argument.kind_of? Net::DNS::Packet
packet = argument
else
packet = make_query_packet(argument,type,cls)
end
# Store packet_data for performance improvements,
# so methods don't keep on calling Packet#data
packet_data = packet.data
packet_size = packet_data.size
# Choose whether use TCP, UDP or RAW
if packet_size > @config[:packet_size] # Must use TCP, either plain or raw
if @raw # Use raw sockets?
@logger.info "Sending #{packet_size} bytes using TCP over RAW socket"
method = :send_raw_tcp
else
@logger.info "Sending #{packet_size} bytes using TCP"
method = :send_tcp
end
else # Packet size is inside the boundaries
if @raw # Use raw sockets?
@logger.info "Sending #{packet_size} bytes using UDP over RAW socket"
method = :send_raw_udp
elsif use_tcp? # User requested TCP
@logger.info "Sending #{packet_size} bytes using TCP"
method = :send_tcp
else # Finally use UDP
@logger.info "Sending #{packet_size} bytes using UDP"
end
end
if type == Net::DNS::AXFR
if @raw
@logger.warn "AXFR query, switching to TCP over RAW socket"
method = :send_raw_tcp
else
@logger.warn "AXFR query, switching to TCP"
method = :send_tcp
end
end
ans = self.old_send(method,packet,packet_data)
unless ans
@logger.fatal "No response from nameservers list: aborting"
raise NoResponseError
end
@logger.info "Received #{ans[0].size} bytes from #{ans[1][2]+":"+ans[1][1].to_s}"
response = Net::DNS::Packet.parse(ans[0],ans[1])
if response.header.truncated? and not ignore_truncated?
@logger.warn "Packet truncated, retrying using TCP"
self.use_tcp = true
begin
return send(argument,type,cls)
ensure
self.use_tcp = false
end
end
return response
end
#
# Performs a zone transfer for the zone passed as a parameter.
#
# Returns a list of Net::DNS::Packet (not answers!)
#
def axfr(name,cls=Net::DNS::IN)
@logger.info "Requested AXFR transfer, zone #{name} class #{cls}"
if @config[:nameservers].size == 0
raise Resolver::Error, "No nameservers specified!"
end
method = :query_tcp
packet = make_query_packet(name, Net::DNS::AXFR, cls)
# Store packet_data for performance improvements,
# so methods don't keep on calling Packet#data
packet_data = packet.data
packet_size = packet_data.size
if @raw
@logger.warn "AXFR query, switching to TCP over RAW socket"
method = :send_raw_tcp
else
@logger.warn "AXFR query, switching to TCP"
method = :query_tcp
end
answers = []
length = [packet_data.size].pack("n")
@config[:nameservers].each do |ns|
begin
socket = Socket.new(Socket::AF_INET,Socket::SOCK_STREAM,0)
socket.bind(Socket.pack_sockaddr_in(@config[:source_port],@config[:source_address].to_s))
sockaddr = Socket.pack_sockaddr_in(@config[:port],ns.to_s)
@config[:tcp_timeout].timeout do
catch "next nameserver" do
socket.connect(sockaddr)
@logger.info "Contacting nameserver #{ns} port #{@config[:port]}"
socket.write(length+packet_data)
soa = 0
while soa < 2 do
buffer = ""
ans = socket.recv(Net::DNS::INT16SZ)
if ans.size == 0
if soa == 1
break #No records in zone
else
@logger.warn "Couldn't recv from nameserver #{ns}, trying next."
throw "next nameserver"
end
end
len = ans.unpack("n")[0]
@logger.info "Receiving #{len} bytes..."
if len == 0
@logger.warn "Receiving 0 length packet from nameserver #{ns}, trying next."
throw "next nameserver"
end
while (buffer.size < len)
left = len - buffer.size
temp,from = socket.recvfrom(left)
buffer += temp
end
unless buffer.size == len
@logger.warn "Malformed packet from nameserver #{ns}, trying next."
throw "next nameserver"
end
@logger.info "Received #{buffer.size} bytes from #{ns.to_s}:#{@config[:port].to_s}"
begin
response = Net::DNS::Packet.parse(buffer,["",@config[:port],ns.to_s,ns.to_s])
if response.answer[0].type == "SOA"
soa += 1
end
answers << response
rescue NameError => e
@logger.warn "Error parsing axfr response: #{e.message}"
end
end
if soa == 2
answers.pop #Remove duplicate SOA
end
end
end
rescue TimeoutError
@logger.warn "Nameserver #{ns} not responding within TCP timeout, trying next one"
answers = []
next
ensure
socket.close
end
end
unless answers
message = "No response from nameservers list"
@logger.fatal(message)
raise NoResponseError, message
end
return answers
end
#
# Performs an MX query for the domain name passed as parameter.
#
# It actually uses the same methods a normal Resolver query would
# use, but automatically sort the results based on preferences
# and returns an ordered array.
#
# Example:
#
# res = Net::DNS::Resolver.new
# res.mx("google.com")
#
def mx(name,cls=Net::DNS::IN)
arr = []
send(name, Net::DNS::MX, cls).answer.each do |entry|
arr << entry if entry.type == 'MX'
end
return arr.sort_by {|a| a.preference}
end
private
# Parse a configuration file specified as the argument.
#
def parse_config_file
if RUBY_PLATFORM =~ /mswin32|cygwin|mingw|bccwin/
require 'win32/resolv'
arr = Win32::Resolv.get_resolv_info
self.domain = arr[0]
self.nameservers = arr[1]
else
IO.foreach(@config[:config_file]) do |line|
line.gsub!(/\s*[;#].*/,"")
next unless line =~ /\S/
case line
when /^\s*domain\s+(\S+)/
self.domain = $1
when /^\s*search\s+(.*)/
self.searchlist = $1.split(" ")
when /^\s*nameserver\s+(.*)/
self.nameservers = $1.split(" ")
end
end
end
end
# Parse environment variables
def parse_environment_variables
if ENV['RES_NAMESERVERS']
self.nameservers = ENV['RES_NAMESERVERS'].split(" ")
end
if ENV['RES_SEARCHLIST']
self.searchlist = ENV['RES_SEARCHLIST'].split(" ")
end
if ENV['LOCALDOMAIN']
self.domain = ENV['LOCALDOMAIN']
end
if ENV['RES_OPTIONS']
ENV['RES_OPTIONS'].split(" ").each do |opt|
name,val = opt.split(":")
begin
eval("self.#{name} = #{val}")
rescue NoMethodError
raise ResolverArgumentError, "Invalid ENV option #{name}"
end
end
end
end
def nameservers_from_name(arg)
arr = []
arg.split(" ").each do |name|
Resolver.new.search(name).each_address do |ip|
arr << ip
end
end
@config[:nameservers] << arr
end
def make_query_packet(string,type,cls)
case string
when IPAddr
name = string.reverse
type = Net::DNS::PTR
@logger.warn "PTR query required for address #{string}, changing type to PTR"
when /\d/ # Contains a number, try to see if it's an IP or IPv6 address
begin
name = IPAddr.new(string).reverse
type = Net::DNS::PTR
rescue ArgumentError
name = string if valid? string
end
else
name = string if valid? string
end
# Create the packet
packet = Net::DNS::Packet.new(name,type,cls)
if packet.query?
packet.header.recursive = @config[:recursive] ? 1 : 0
end
# DNSSEC and TSIG stuff to be inserted here
packet
end
def send_tcp(packet,packet_data)
ans = nil
length = [packet_data.size].pack("n")
@config[:nameservers].each do |ns|
begin
buffer = ""
socket = Socket.new(Socket::AF_INET,Socket::SOCK_STREAM,0)
socket.bind(Socket.pack_sockaddr_in(@config[:source_port],@config[:source_address].to_s))
sockaddr = Socket.pack_sockaddr_in(@config[:port],ns.to_s)
@config[:tcp_timeout].timeout do
socket.connect(sockaddr)
@logger.info "Contacting nameserver #{ns} port #{@config[:port]}"
socket.write(length+packet_data)
ans = socket.recv(Net::DNS::INT16SZ)
len = ans.unpack("n")[0]
@logger.info "Receiving #{len} bytes..."
if len == 0
@logger.warn "Receiving 0 lenght packet from nameserver #{ns}, trying next."
next
end
while (buffer.size < len)
left = len - buffer.size
temp,from = socket.recvfrom(left)
buffer += temp
end
unless buffer.size == len
@logger.warn "Malformed packet from nameserver #{ns}, trying next."
next
end
end
return [buffer,["",@config[:port],ns.to_s,ns.to_s]]
rescue Timeout::Error
@logger.warn "Nameserver #{ns} not responding within TCP timeout, trying next one"
next
ensure
socket.close
end
end
end
def send_udp(packet,packet_data)
socket = UDPSocket.new
socket.bind(@config[:source_address].to_s,@config[:source_port])
ans = nil
response = ""
@config[:nameservers].each do |ns|
begin
@config[:udp_timeout].timeout do
@logger.info "Contacting nameserver #{ns} port #{@config[:port]}"
socket.send(packet_data,0,ns.to_s,@config[:port])
ans = socket.recvfrom(@config[:packet_size])
end
break if ans
rescue Timeout::Error
@logger.warn "Nameserver #{ns} not responding within UDP timeout, trying next one"
next
end
end
ans
end
def valid?(name)
if name =~ /[^-\w\.]/
raise ResolverArgumentError, "Invalid domain name #{name}"
else
true
end
end
end # class Resolver
end # module DNS
end # module Net
class ResolverArgumentError < ArgumentError # :nodoc:
end
class NoResponseError < StandardError # :nodoc:
end
module ExtendHash # :nodoc:
# Returns an hash with all the
# keys turned into downcase
#
# hsh = {"Test" => 1, "FooBar" => 2}
# hsh.key_downcase!
# #=> {"test"=>1,"foobar"=>2}
#
def key_downcase!
hsh = Hash.new
self.each do |key,val|
hsh[key.downcase] = val
end
self.replace(hsh)
end
end
class Hash # :nodoc:
include ExtendHash
end