/lib/net/dns/resolver.rb
Ruby | 1285 lines | 628 code | 109 blank | 548 comment | 61 complexity | 9ba6031f16b5ce5710b558a9b1c317f0 MD5 | raw file
Possible License(s): BSD-3-Clause, Apache-2.0, LGPL-2.1, GPL-2.0, MIT
- # -*- 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 ResolverError, "No nameservers specified!"
- end
- method = :send_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 = :send_tcp
- end
- answers = []
- soa = 0
- self.old_send(method, packet, packet_data) do |ans|
- @logger.info "Received #{ans[0].size} bytes from #{ans[1][2]+":"+ans[1][1].to_s}"
- begin
- response = Net::DNS::Packet.parse(ans[0],ans[1])
- if response.answer[0].type == "SOA"
- soa += 1
- if soa >= 2
- break
- end
- end
- answers << response
- rescue NameError => e
- @logger.warn "Error parsing axfr response: #{e.message}"
- end
- end
- if answers.empty?
- @logger.fatal "No response from nameservers list: aborting"
- raise NoResponseError
- 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
- 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)
- got_something = false
- loop do
- buffer = ""
- ans = socket.recv(Net::DNS::INT16SZ)
- if ans.size == 0
- if got_something
- break #Proper exit from loop
- else
- @logger.warn "Connection reset to nameserver #{ns}, trying next."
- throw "next nameserver"
- end
- end
- got_something = true
- 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
- if block_given?
- yield [buffer,["",@config[:port],ns.to_s,ns.to_s]]
- else
- return [buffer,["",@config[:port],ns.to_s,ns.to_s]]
- end
- end
- end
- end
- rescue Timeout::Error
- @logger.warn "Nameserver #{ns} not responding within TCP timeout, trying next one"
- next
- ensure
- socket.close
- end
- end
- return nil
- 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 ResolverError < ArgumentError # :nodoc:
- end
- 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