require 'rbconfig' require 'socket' require 'timeout' require 'net/dns/packet' require 'net/dns/resolver/timeouts' # Resolver helper method. # # Calling the resolver directly: # # puts Resolver("www.google.com").answer.size # # => 5 # # An optional block can be passed yielding the Net::DNS::Packet object. # # Resolver("www.google.com") { |packet| puts packet.size + " bytes" } # # => 484 bytes # def Resolver(name, type = Net::DNS::A, cls = Net::DNS::IN) resolver = Net::DNS::Resolver.start(name, type, cls) if block_given? yield resolver else resolver end end module Net module DNS include Logger::Severity # = Net::DNS::Resolver - DNS resolver class # # 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! # # == 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 class Error < StandardError end class NoResponseError < Error end # 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"), source_address_inet6: IPAddr.new('::'), 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(5), udp_timeout: UdpTimeout.new(5), }.freeze class << self C = Object.const_get(defined?(RbConfig) ? :RbConfig : :Config)::CONFIG # Quick resolver method. Bypass the configuration using # the defaults. # # Net::DNS::Resolver.start "www.google.com" # def start(*params) new.search(*params) end # Returns true if running on a Windows platform. # # Note. This method doesn't rely on the RUBY_PLATFORM constant # because the comparison will fail when running on JRuby. # On JRuby RUBY_PLATFORM == 'java'. def platform_windows? !!(C["host_os"] =~ /msdos|mswin|djgpp|mingw/i) end end # Creates 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 = {}) config.is_a?(Hash) or raise(ArgumentError, "Expected `config' to be a Hash") @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) || (key == :config_file) begin eval "self.#{key} = val" rescue NoMethodError raise ArgumentError, "Option #{key} not valid" end end end # Get the resolver search list, 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 ArgumentError, "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 @config[:nameservers].map(&:to_s) end alias 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| val = case x when String begin IPAddr.new(x) rescue ArgumentError nameservers_from_name(arg) return end when IPAddr x else raise ArgumentError, "Wrong argument format" end @config[:nameservers] << val end @logger.info "Nameservers list changed to value #{@config[:nameservers].inspect}" else raise ArgumentError, "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) (0..65_535).cover?(num) or raise(ArgumentError, "Wrong port number #{num}") @config[:port] = num @logger.info "Port number changed to #{num}" 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) root? or raise(ResolverPermissionError, "Are you root?") (0..65_535).cover?(num) or raise(ArgumentError, "Wrong port number #{num}") @config[:source_port] = num 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 # Get the local ipv6 address from which the resolver sends queries # def source_address_inet6 @config[:source_address_inet6].to_s end # 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) addr.respond_to?(:to_s) or raise(ArgumentError, "Wrong address argument #{addr}") begin port = rand(1024..65_023) @logger.info "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) num.positive? or raise(ArgumentError, "Interval must be positive") @config[:retry_interval] = num @logger.info "Retransmission interval changed to #{num} seconds" 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) (num.is_a?(Integer) && (num > 0)) or raise(ArgumentError, "Retry value must be a positive integer") @config[:retry_number] = num @logger.info "Retrasmissions number changed to #{num}" 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 recurse recursive? alias 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 ArgumentError, "Argument must be boolean" end end alias recurse= recursive= # Return a string representing 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| str << if (key == :log_file) || (key == :config_file) "#{key}: #{val} \t" else "#{key}: #{eval(key.to_s)} \t" end str << "\n;; " if i.even? 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 ArgumentError, "Argument must be boolean" end end # Get the state of the dns_search flag. def dns_search @config[:dns_search] end alias 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 ArgumentError, "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 usevc use_tcp? alias 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 ArgumentError, "Argument must be boolean" end end alias usevc= use_tcp= def ignore_truncated? @config[:ignore_truncated] end alias 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 ArgumentError, "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 5 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 information: 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. # # Default is 5 seconds. # # 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) @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) logger.is_a?(Logger) or raise(ArgumentError, "Argument must be an instance of Logger class") @logger = logger 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 Resolver#query method instead. # def search(name, type = Net::DNS::A, cls = Net::DNS::IN) return query(name, type, cls) if name.class == IPAddr # 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 !~ /\.$/ && @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#query # method instead. # def query(name, type = Net::DNS::A, cls = Net::DNS::IN) return send(name, type, cls) if name.class == IPAddr # If the name doesn't contain any dots then append the default domain. if name !~ /\./ && name !~ /:/ && @config[:defname] 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. # # # Executes the query with a +Packet+ object # send_packet = Net::DNS::Packet.new("host.example.com", Net::DNS::NS, Net::DNS::HS) # packet = res.query(send_packet) # # # Executes the query with a host, type and cls # packet = res.query("host.example.com") # packet = res.query("host.example.com", Net::DNS::NS) # packet = res.query("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.query(ip) # # packet = res.query("172.16.100.2") # # Use +packet.header.ancount+ or +packet.answer+ to find out if there # were any records in the answer section. # def query(argument, type = Net::DNS::A, cls = Net::DNS::IN) !@config[:nameservers].empty? or raise(Resolver::Error, "No nameservers specified!") method = :query_udp packet = if argument.is_a? Net::DNS::Packet argument else 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 = :query_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 = :query_tcp else # Finally use UDP @logger.info "Sending #{packet_size} bytes using UDP" end end if type == Net::DNS::AXFR if @raw @logger.info "AXFR query, switching to TCP over RAW socket" method = :send_raw_tcp else @logger.info "AXFR query, switching to TCP" method = :query_tcp end end ans = send(method, packet, packet_data) unless ans message = "No response from nameservers list" @logger.fatal(message) raise NoResponseError, message 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? && !ignore_truncated? @logger.warn "Packet truncated, retrying using TCP" self.use_tcp = true begin return query(argument, type, cls) ensure self.use_tcp = false end end response end # Performs a zone transfer for the zone passed as a parameter. # # It is actually only a wrapper to a send with type set as Net::DNS::AXFR, # since it is using the same infrastucture. # def axfr(name, cls = Net::DNS::IN) @logger.info "Requested AXFR transfer, zone #{name} class #{cls}" query(name, Net::DNS::AXFR, cls) 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. # # res = Net::DNS::Resolver.new # res.mx("google.com") # def mx(name, cls = Net::DNS::IN) arr = [] query(name, Net::DNS::MX, cls).answer.each do |entry| arr << entry if entry.type == 'MX' end arr.sort_by(&:preference) end private # Parses a configuration file specified as the argument. def parse_config_file if self.class.platform_windows? require 'win32/resolv' arr = Win32::Resolv.get_resolv_info self.domain = arr[0][0] self.nameservers = arr[1] else nameservers = [] IO.foreach(@config[:config_file]) do |line| line.gsub!(/\s*[;#].*/, "") next unless line =~ /\S/ case line when /^\s*domain\s+(\S+)/ self.domain = Regexp.last_match(1) when /^\s*search\s+(.*)/ self.searchlist = Regexp.last_match(1).split(" ") when /^\s*nameserver\s+(.*)/ nameservers << Regexp.last_match(1).split(" ") end end self.nameservers = nameservers.flatten end end # Parses 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 return unless ENV['RES_OPTIONS'] ENV['RES_OPTIONS'].split(" ").each do |opt| name, val = opt.split(":") begin eval("self.#{name} = #{val}") rescue NoMethodError raise ArgumentError, "Invalid ENV option #{name}" 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.chomp(".")).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 query_tcp(_packet, packet_data) ans = nil length = [packet_data.size].pack("n") @config[:nameservers].each do |ns| 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.unpack1("n") @logger.info "Receiving #{len} bytes..." if len == 0 @logger.warn "Receiving 0 length 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 ans end def query_udp(_packet, packet_data) socket4 = UDPSocket.new socket4.bind(@config[:source_address].to_s, @config[:source_port]) socket6 = UDPSocket.new(Socket::AF_INET6) socket6.bind(@config[:source_address_inet6].to_s, @config[:source_port]) ans = nil response = "" @config[:nameservers].each do |ns| @config[:udp_timeout].timeout do @logger.info "Contacting nameserver #{ns} port #{@config[:port]}" ans = if ns.ipv6? socket6.send(packet_data, 0, ns.to_s, @config[:port]) socket6.recvfrom(@config[:packet_size]) else socket4.send(packet_data, 0, ns.to_s, @config[:port]) socket4.recvfrom(@config[:packet_size]) end end break if ans rescue Timeout::Error @logger.warn "Nameserver #{ns} not responding within UDP timeout, trying next one" next end ans end # FIXME: a ? method should never raise. def valid?(name) name !~ /[^-\w\.\*]/ or raise(ArgumentError, "Invalid domain name #{name}") true end end end end