class DNSBL::Client

Client actually handles the sending of queries to a recursive DNS server and places any replies into DNSBLResults

Constants

VERSION

Current version of the dnsbl-client gem

Attributes

first_only[W]
timeout[W]

Public Class Methods

new(config = YAML.safe_load(File.read(" click to toggle source

initialize a new DNSBL::Client object the config file automatically points to a YAML file containing the list of DNSBLs and their return codes the two-level-tlds file lists most of the two level tlds, needed for hostname to domain normalization

# File lib/dnsbl/client.rb, line 54
def initialize(config = YAML.safe_load(File.read("#{File.expand_path '../../data', __dir__}/dnsbl.yaml")),
               two_level_tldfile = "#{File.expand_path '../../data', __dir__}/two-level-tlds",
               three_level_tldfile = "#{File.expand_path '../../data', __dir__}/three-level-tlds")
  @dnsbls = config
  @timeout = 1.5
  @first_only = false
  @two_level_tld = []
  @three_level_tld = []
  File.open(two_level_tldfile).readlines.each do |l|
    @two_level_tld << l.strip
  end
  File.open(three_level_tldfile).readlines.each do |l|
    @three_level_tld << l.strip
  end
  @sockets = []
  config = Resolv::DNS::Config.new

  # let's just the first nameserver in this version of the library
  ip, port = config.nameservers.first

  sock = UDPSocket.new
  sock.connect ip, port
  @sockets << sock
  @socket_index = 0
end

Public Instance Methods

add_dnsbl(name, domain, type = 'ip', codes = { '0' => 'OK', '127.0.0.2' => 'Blacklisted' }) click to toggle source

allows the adding of a new DNSBL to the set of configured DNSBLs

# File lib/dnsbl/client.rb, line 108
def add_dnsbl(name, domain, type = 'ip', codes = { '0' => 'OK', '127.0.0.2' => 'Blacklisted' })
  @dnsbls[name] = codes
  @dnsbls[name]['domain'] = domain
  @dnsbls[name]['type'] = type
end
dnsbls() click to toggle source

returns a list of DNSBL names currently configured

# File lib/dnsbl/client.rb, line 115
def dnsbls
  @dnsbls.keys
end
lookup(loopup_item) click to toggle source

lookup performs the sending of DNS queries for the given items returns an array of DNSBLResult

# File lib/dnsbl/client.rb, line 121
def lookup(loopup_item)
  # if item is an array, use it, otherwise make it one
  items = Array loopup_item

  # place the results in the results array
  results = []
  # for each ip or hostname
  items.each do |item|
    # sent is used to determine when we have all the answers
    sent = 0
    # record the start time
    @starttime = Time.now.to_f
    # determine the type of query
    itemtype = item.match?(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) ? 'ip' : 'domain'
    if item.match?(/^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/) # rubocop: disable Layout/LineLength
      itemtype = 'ip'
    end

    # for each dnsbl that supports our type, create the DNS query packet and send it
    # rotate across our configured name servers and increment sent
    @dnsbls.each do |name, config|
      next if config['disabled']
      next unless config['type'] == itemtype

      begin
        msg = encode_query item, itemtype, config['domain'], config['apikey']
        @sockets[@socket_index].send msg, 0
        @socket_index += 1
        @socket_index %= @sockets.length
        sent += 1
      rescue StandardError => e
        puts "error for #{name}: e"
        puts e.backtrace.join("\n")
      end
    end

    # while we still expect answers
    while sent.positive?
      # wait on the socket for maximally @timeout seconds
      r, = IO.select @sockets, nil, nil, @timeout
      # if we time out, break out of the loop
      break unless r

      # for each reply, decode it and receive results, decrement the pending answers
      first_only = false
      r.each do |s|
        begin
          response = decode_response(s.recv(4096))
          results += response
        rescue StandardError => e
          puts e
          puts e.backtrace.join("\n")
        end
        sent -= 1
        if @first_only
          first_only = true
          break
        end
      end
      break if first_only
    end
  end
  results
end
nameservers=(nss = Resolv::DNS::Config.new.nameservers) click to toggle source

sets the nameservers used for performing DNS lookups in round-robin fashion

# File lib/dnsbl/client.rb, line 81
def nameservers=(nss = Resolv::DNS::Config.new.nameservers)
  @sockets.each(&:close)
  @sockets = []

  # let's just the first nameserver in this version of the library
  ip, port = nss.first

  sock = UDPSocket.new
  sock.connect ip, port
  @sockets << sock
  @socket_index = 0
end
normalize(domain) click to toggle source

Converts a hostname to the domain: e.g., www.google.com => google.com, science.somewhere.co.uk => somewhere.co.uk

# File lib/dnsbl/client.rb, line 95
def normalize(domain)
  # strip off the protocol (\w{1,20}://), the URI (/), parameters (?), port number (:), and username (.*@)
  # then split into parts via the .
  parts = domain.gsub(%r{^\w{1,20}://}, '').gsub(%r{[/?:].*}, '').gsub(/.*?@/, '').split('.')
  # grab the last two parts of the domain
  dom = parts[-2, 2].join '.'
  # if the dom is in the two_level_tld list, then use three parts
  dom = parts[-3, 3].join '.' if @two_level_tld.index dom
  dom = parts[-4, 4].join '.' if @three_level_tld.index dom
  dom
end

Private Instance Methods

decode_response(buf) click to toggle source

takes a DNS response and converts it into a DNSBLResult

# File lib/dnsbl/client.rb, line 208
def decode_response(buf)
  reply = Resolv::DNS::Message.decode buf
  results = []
  reply.each_answer do |name, _ttl, data|
    if name.to_s =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(.+)$/
      ip = [Regexp.last_match(4),
            Regexp.last_match(3),
            Regexp.last_match(2),
            Regexp.last_match(1)].join '.'
      domain = Regexp.last_match 5
      @dnsbls.each do |dnsblname, config|
        next unless data.is_a? Resolv::DNS::Resource::IN::A
        next unless domain == config['domain']

        meaning = config[data.address.to_s] || data.address.to_s
        results << DNSBLResult.new(dnsblname, ip, name.to_s, data.address.to_s, meaning, Time.now.to_f - @starttime)
        break
      end
    else
      @dnsbls.each do |dnsblname, config|
        next unless name.to_s.end_with? config['domain']

        meaning = if config['decoder']
                    send "#{config['decoder']}_decoder".to_sym, data.address.to_s
                  elsif config[data.address.to_s]
                    config[data.address.to_s]
                  else
                    data.address.to_s
                  end

        results << DNSBLResult.new(dnsblname, name.to_s.gsub(".#{config['domain']}", ''),
                                   name.to_s,
                                   data.address.to_s,
                                   meaning,
                                   Time.now.to_f - @starttime)
        break
      end
    end
  end
  results
end
encode_query(item, itemtype, domain, apikey = nil) click to toggle source

converts an ip or a hostname into the DNS query packet requires to lookup the result

# File lib/dnsbl/client.rb, line 189
def encode_query(item, itemtype, domain, apikey = nil)
  label = case itemtype
          when 'ip'
            ip = IPAddr.new item
            ip.reverse.gsub('.ip6.arpa', '').gsub('.in-addr.arpa', '')
          when 'domain'
            normalize item
          end

  lookup = "#{label}.#{domain}"
  lookup = "#{apikey}.#{lookup}" if apikey
  txid = lookup.sum
  message = Resolv::DNS::Message.new txid
  message.rd = 1
  message.add_question lookup, Resolv::DNS::Resource::IN::A
  message.encode
end
phpot_decoder(ip) click to toggle source

decodes the response from Project Honey Pot’s service

# File lib/dnsbl/client.rb, line 251
def phpot_decoder(ip)
  octets = ip.split '.'
  if octets.length != 4 || octets[0] != '127'
    'invalid response'
  elsif octets[3] == '0'
    search_engines = %w[undocumented AltaVista Ask Baidu Excite Google Looksmart Lycos MSN Yahoo Cuil InfoSeek Miscellaneous]
    sindex = octets[2].to_i
    if sindex >= search_engines.length
      'type=search engine,engine=unknown'
    else
      "type=search engine,engine=#{search_engines[sindex]}"
    end
  else
    days, threatscore, flags = octets[1, 3]
    flags = flags.to_i
    types = []
    types << 'suspicious' if (flags & 0x1) == 0x1
    types << 'harvester' if (flags & 0x2) == 0x2
    types << 'comment spammer' if (flags & 0x4) == 0x4
    types << 'reserved' if (flags & 0xf8).positive?
    type = types.join ','
    "days=#{days},score=#{threatscore},type=#{type}"
  end
end