Class: Dnsruby::Resolver

Inherits:
Object
  • Object
show all
Defined in:
lib/Dnsruby/Resolver.rb

Overview

Description

This class uses a set of SingleResolvers to perform queries with retries across multiple nameservers.

The retry policy is a combination of the Net::DNS and dnsjava approach, and has the option of :

  • A total timeout for the query (defaults to 0, meaning “no total timeout”)

  • A retransmission system that targets the namervers concurrently once the first query round is

complete, but in which the total time per query round is split between the number of nameservers 
targetted for the first round. and total time for query round is doubled for each query round

Note that, if a total timeout is specified, then that will apply regardless of the retry policy 
(i.e. it may cut retries short).

Note also that these timeouts are distinct from the SingleResolver's packet_timeout

Methods

Synchronous

These methods raise an exception or return a response message with rcode==NOERROR

  • Dnsruby::Resolver#send_message(msg)

  • Dnsruby::Resolver#query(name [, type [, klass]])

Asynchronous

These methods use a response queue to return the response and the error

  • Dnsruby::Resolver#send_async(msg, query_id, response_queue)

Constant Summary collapse

DefaultQueryTimeout =
0
DefaultPacketTimeout =
10
DefaultRetryTimes =
4
DefaultRetryDelay =
5
DefaultPort =
53
DefaultUDPSize =
512

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args) ⇒ Resolver

Create a new Resolver object. If no parameters are passed in, then the default system configuration will be used. Otherwise, a Hash may be passed in with the following optional elements :

  • :port

  • :use_tcp

  • :tsig_key

  • :ignore_truncation

  • :src_address

  • :src_port

  • :persistent_tcp

  • :persistent_udp

  • :recurse

  • :udp_size

  • :config_info - see Config

  • :nameserver - can be either a String or an array of Strings

  • :packet_timeout

  • :query_timeout

  • :retry_times

  • :retry_delay



467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
# File 'lib/Dnsruby/Resolver.rb', line 467

def initialize(*args)
  reset_attributes
  
  # Process args

  if (args.length==1)
    if (args[0].class == Hash)
      args[0].keys.each do |key|
        begin
          if (key == :config_info)
            @config.set_config_info(args[0][:config_info])
          elsif (key==:nameserver)
            set_config_nameserver(args[0][:nameserver])
          else
            send(key.to_s+"=", args[0][key])
          end
        rescue Exception
          TheLog.error("Argument #{key} not valid\n")
        end
      end
    elsif (args[0].class == Config)
      # also accepts a Config object from Dnsruby::Resolv

      @config = args[0]
    end
  else
    #@TODO@ ?

  end
  if (@single_resolvers==[])
    add_config_nameservers
  end
  update
end

Instance Attribute Details

#configObject (readonly)

The current Config



83
84
85
# File 'lib/Dnsruby/Resolver.rb', line 83

def config
  @config
end

#ignore_truncationObject

Should truncation be ignored? i.e. the TC bit is ignored and thus the resolver will not requery over TCP if TC is set



64
65
66
# File 'lib/Dnsruby/Resolver.rb', line 64

def ignore_truncation
  @ignore_truncation
end

#packet_timeoutObject

The timeout for any individual packet. This is the timeout used by SingleResolver



89
90
91
# File 'lib/Dnsruby/Resolver.rb', line 89

def packet_timeout
  @packet_timeout
end

#persistent_tcpObject

Should TCP queries be sent on a persistent socket?



72
73
74
# File 'lib/Dnsruby/Resolver.rb', line 72

def persistent_tcp
  @persistent_tcp
end

#persistent_udpObject

Should UDP queries be sent on a persistent socket?



74
75
76
# File 'lib/Dnsruby/Resolver.rb', line 74

def persistent_udp
  @persistent_udp
end

#portObject

The port to send queries to on the resolver



54
55
56
# File 'lib/Dnsruby/Resolver.rb', line 54

def port
  @port
end

#query_timeoutObject

Note that this timeout represents the total time a query may run for - multiple packets can be sent to multiple nameservers in this time. This is distinct from the SingleResolver per-packet timeout The query_timeout is not required - it will default to 0, which means “do not use query_timeout”. If this is the case then the timeout will be dictated by the retry_times and retry_delay attributes



96
97
98
# File 'lib/Dnsruby/Resolver.rb', line 96

def query_timeout
  @query_timeout
end

#recurseObject

Should the Recursion Desired bit be set?



77
78
79
# File 'lib/Dnsruby/Resolver.rb', line 77

def recurse
  @recurse
end

#retry_delayObject

The query will be tried across nameservers retry_times times, with a delay of retry_delay seconds between each retry. The first time round, retry_delay will be divided by the number of nameservers being targetted, and a new nameserver will be queried with the resultant delay.



101
102
103
# File 'lib/Dnsruby/Resolver.rb', line 101

def retry_delay
  @retry_delay
end

#retry_timesObject

The query will be tried across nameservers retry_times times, with a delay of retry_delay seconds between each retry. The first time round, retry_delay will be divided by the number of nameservers being targetted, and a new nameserver will be queried with the resultant delay.



101
102
103
# File 'lib/Dnsruby/Resolver.rb', line 101

def retry_times
  @retry_times
end

#single_resolversObject (readonly)

The array of SingleResolvers used for sending query messages



86
87
88
# File 'lib/Dnsruby/Resolver.rb', line 86

def single_resolvers
  @single_resolvers
end

#src_addressObject

The source address to send queries from



67
68
69
# File 'lib/Dnsruby/Resolver.rb', line 67

def src_address
  @src_address
end

#src_portObject

The source port to send queries from



69
70
71
# File 'lib/Dnsruby/Resolver.rb', line 69

def src_port
  @src_port
end

#tsig_keyObject

Returns the value of attribute tsig_key.



60
61
62
# File 'lib/Dnsruby/Resolver.rb', line 60

def tsig_key
  @tsig_key
end

#udp_sizeObject

The maximum UDP size to be used



80
81
82
# File 'lib/Dnsruby/Resolver.rb', line 80

def udp_size
  @udp_size
end

#use_tcpObject

Should TCP be used as a transport rather than UDP?



57
58
59
# File 'lib/Dnsruby/Resolver.rb', line 57

def use_tcp
  @use_tcp
end

Instance Method Details

#add_config_nameserversObject



499
500
501
502
503
504
# File 'lib/Dnsruby/Resolver.rb', line 499

def add_config_nameservers
  # Add the Config nameservers

  @config.nameserver.each do |ns|
    @single_resolvers.push(SingleResolver.new({:server=>ns}))
  end
end

#add_resolver(single) ⇒ Object

Add a new SingleResolver to the list of resolvers this Resolver object will query.



553
554
555
# File 'lib/Dnsruby/Resolver.rb', line 553

def add_resolver(single)
  @single_resolvers.push(single)
end

#closeObject

Close the Resolver. Unfinished queries are terminated with OtherResolError.



274
275
276
277
278
279
280
281
# File 'lib/Dnsruby/Resolver.rb', line 274

def close
  @mutex.synchronize {
    @query_list.each do |client_query_id, values|
      msg, client_queue, q, outstanding = values
      send_result_and_close(client_queue, client_query_id, q, nil, OtherResolvError.new("Resolver closing!"))
    end
  }
end

#generate_timeoutsObject

:nodoc: all



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/Dnsruby/Resolver.rb', line 239

def generate_timeouts() #:nodoc: all

  # Create the timeouts for the query from the retry_times and retry_delay attributes. 

  # These are created at the same time in case the parameters change during the life of the query.

  # 

  # These should be absolute, rather than relative

  # The first value should be Time.now

  time_now = Time.now
  timeouts={}
  #These should be be pegged to the single_resolver they are targetting :

  #  e.g. timeouts[timeout1]=nameserver

  retry_delay = @retry_delay
  @retry_times.times do |retry_count|
    if (retry_count>0)
      retry_delay *= 2
    end
    servers=[]
    @single_resolvers.each do |r| servers.push(r.server) end
    @single_resolvers.each_index do |i|
      res= @single_resolvers[i]
      offset = (i*@retry_delay.to_f/@single_resolvers.length)
      if (retry_count==0)
        timeouts[time_now+offset]=[res, retry_count]
      else
        if (timeouts.has_key?(time_now+retry_delay+offset))
          TheLog.error("Duplicate timeout key!")
          raise RuntimeError.new("Duplicate timeout key!")
        end
        timeouts[time_now+retry_delay+offset]=[res, retry_count]
      end
    end
  end
  return timeouts      
end

#handle_error_response(select_queue, query_id, error, response) ⇒ Object

:nodoc: all



385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/Dnsruby/Resolver.rb', line 385

def handle_error_response(select_queue, query_id, error, response) #:nodoc: all

  #Handle an error

  @mutex.synchronize{
    TheLog.debug("handling error #{error.class}, #{error}")
    # Check what sort of error it was :

    resolver, msg, client_query_id, retry_count = query_id
    msg, client_queue, select_queue, outstanding = @query_list[client_query_id]
    if (error.kind_of?(ResolvTimeout))
      #   - if it was a timeout, then check which number it was, and how many retries are expected on that server

      #       - if it was the last retry, on the last server, then return a timeout to the client (and clean up)

      #       - otherwise, continue

      # Do we have any more packets to send to this resolver?

      timeouts = @timeouts[client_query_id]
      if (outstanding.empty? && timeouts[1].values.empty?)
        TheLog.debug("Sending timeout to client")
        send_result_and_close(client_queue, client_query_id, select_queue, response, error)
      end
    elsif (error.kind_of?NXDomain)
      #   - if it was an NXDomain, then return that to the client, and stop all new queries (and clean up)

      send_result_and_close(client_queue, client_query_id, select_queue, response, error)
    else
      #   - if it was any other error, then remove that server from the list for that query

      #   If a Too Many Open Files error, then don't remove, but let retry work.

      timeouts = @timeouts[client_query_id]
      if (!(error.to_s=~/Errno::EMFILE/))
        TheLog.debug("Removing #{resolver.server} from resolver list for this query")
        timeouts[1].each do |key, value|
          res = value[0]
          if (res == resolver)
            timeouts[1].delete(key)
          end
        end
      else
        TheLog.debug("NOT Removing #{resolver.server} due to Errno::EMFILE")          
      end
      #        - if it was the last server, then return an error to the client (and clean up)

      if (outstanding.empty? && timeouts[1].values.empty?)
        #          if (outstanding.empty?)

        TheLog.debug("Sending error to client")
        send_result_and_close(client_queue, client_query_id, select_queue, response, error)
      end
    end
    #@TODO@ If we're still sending packets for this query, but none are outstanding, then 

    #jumpstart the next query?

  }
end

#handle_queue_event(queue, id) ⇒ Object

This method is called by the SelectThread (in the select thread) when the queue has a new item on it. The queue interface is used to separate producer/consumer threads, but we’re using it here in one thread. It’s probably a good idea to create a new “worker thread” to take items from the select thread queue and call this method in the worker thread.

Time to process a new queue event.



347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/Dnsruby/Resolver.rb', line 347

def handle_queue_event(queue, id) #:nodoc: all

  # If we get a callback for an ID we don't know about, don't worry -

  # just ignore it. It may be for a query we've already completed.

  # 

  # So, get the next response from the queue (presuming there is one!)

  if (queue.empty?)
    TheLog.fatal("Queue empty in handle_queue_event!")
    raise RuntimeError.new("Severe internal error - Queue empty in handle_queue_event")
  end
  event_id, response, error = queue.pop
  # We should remove this packet from the list of outstanding packets for this query

  resolver, msg, client_query_id, retry_count = id
  if (id != event_id)
    TheLog.error("Serious internal error!! #{id} expected, #{event_id} received")
    raise RuntimeError.new("Serious internal error!! #{id} expected, #{event_id} received")
  end
  @mutex.synchronize{
    if (@query_list[client_query_id]==nil)
      TheLog.debug("Ignoring response for dead query")
      return
    end
    msg, client_queue, select_queue, outstanding = @query_list[client_query_id]
    if (!outstanding.include?id)
      TheLog.error("Query id not on outstanding list! #{outstanding.length} items. #{id} not on #{outstanding}")
      raise RuntimeError.new("Query id not on outstanding!")
    end
    outstanding.delete(id)
  }
  #      if (event.kind_of?(Exception))

  if (error != nil)
    handle_error_response(queue, event_id, error, response)
  else # if (event.kind_of?(Message))

    handle_response(queue, event_id, response)
    #      else

    #        TheLog.error("Random object #{event.class} returned through queue to Resolver")

  end
end

#handle_response(select_queue, query_id, response) ⇒ Object

:nodoc: all



432
433
434
435
436
437
438
439
440
441
442
443
444
# File 'lib/Dnsruby/Resolver.rb', line 432

def handle_response(select_queue, query_id, response) #:nodoc: all

  # Handle a good response

  TheLog.debug("Handling good response")
  resolver, msg, client_query_id, retry_count = query_id
  @mutex.synchronize{
    query, client_queue, s_queue, outstanding = @query_list[client_query_id]
    if (s_queue != select_queue)
      TheLog.error("Serious internal error : expected select queue #{s_queue}, got #{select_queue}")
      raise RuntimeError.new("Serious internal error : expected select queue #{s_queue}, got #{select_queue}")
    end
    send_result_and_close(client_queue, client_query_id, select_queue, response, nil)
  }
end

#nameserver=(n) ⇒ Object



557
558
559
560
561
# File 'lib/Dnsruby/Resolver.rb', line 557

def nameserver=(n)
  @single_resolvers=[]
  set_config_nameserver(n)
  add_config_nameservers
end

#query(name, type = Types.A, klass = Classes.IN) ⇒ Object

Query for a n. If a valid Message is received, then it is returned to the caller. Otherwise an exception (a Dnsruby::ResolvError or Dnsruby::ResolvTimeout) is raised.

require 'Dnsruby'
res = Dnsruby::Resolver.new
response = res.query("example.com") # defaults to Types.A, Classes.IN
response = res.query("example.com", Types.MX)
response = res.query("208.77.188.166") # IPv4 address so PTR query will be made
response = res.query("208.77.188.166", Types.PTR)


116
117
118
119
120
121
# File 'lib/Dnsruby/Resolver.rb', line 116

def query(name, type=Types.A, klass=Classes.IN)
  msg = Message.new
  msg.header.rd = 1
  msg.add_question(name, type, klass)
  return send_message(msg)
end

#reset_attributesObject

:nodoc: all



514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
# File 'lib/Dnsruby/Resolver.rb', line 514

def reset_attributes #:nodoc: all

  # data structures

  @mutex=Mutex.new
  @query_list = {}
  
  # Attributes

  @timeouts = {}
  @query_timeout = DefaultQueryTimeout
  @retry_delay = DefaultRetryDelay
  @retry_times = DefaultRetryTimes
  @packet_timeout = DefaultPacketTimeout
  @port = DefaultPort
  @udp_size = DefaultUDPSize
  @use_tcp = false
  @tsig_key = nil
  @ignore_truncation = false
  @config = Config.new()
  @src_addr        = '0.0.0.0'
  @src_port        = 0
  @recurse = true
  @persistent_udp = false
  @persistent_tcp = false
  @single_resolvers=[]
end

#send_async(msg, client_query_id, client_queue) ⇒ Object

Asynchronously sends a DNS packet (Dnsruby::Message). The client must pass in the Message to be sent, a client_query_id to identify the message and a client_queue (of class Queue) to pass the response back in.

A tuple of (query_id, response_message, exception) will be added to the client_queue.

example :

require 'Dnsruby'
res = Dnsruby::Resolver.new
query_id = 10 # can be any object you like
query_queue = Queue.new
res.send_async(Message.new("example.com", Types.MX), query_id,  query_queue)
query_id += 1
res.send_async(Message.new("example.com", Types.A), query_id,  query_queue)
# ...do a load of other stuff here...
2.times do 
  response_id, response, exception = query_queue.pop
  # You can check the ID to see which query has been answered
  if (exception == nil)
      # deal with good response
  else
      # deal with problem
  end
end


189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/Dnsruby/Resolver.rb', line 189

def send_async(msg, client_query_id, client_queue)
  # This is the whole point of the Resolver class.

  # We want to use multiple SingleResolvers to run a query.

  # So we kick off a system with select_thread where we send

  # a query with a queue, but log ourselves as observers for that

  # queue. When a new response is pushed on to the queue, then the

  # select thread will call this class' handler method IN THAT THREAD.

  # When the final response is known, this class then sticks it in

  # to the client queue.

  
  q = Queue.new
  
  if (!client_queue.kind_of?Queue)
    TheLog.error("Wrong type for client_queue in Resolver#send_async")
    client_queue.push([client_query_id, ArgumentError.new("Wrong type of client_queue passed to Dnsruby::Resolver#send_async - should have been Queue, was #{client_queue.class}")])
    return
  end
  
  if (!msg.kind_of?Message)
    TheLog.error("Wrong type for msg in Resolver#send_async")
    client_queue.push([client_query_id, ArgumentError.new("Wrong type of msg passed to Dnsruby::Resolver#send_async - should have been Message, was #{msg.class}")])
    return
  end
  
  tick_needed=false
  # add to our data structures

  @mutex.synchronize{
    tick_needed = true if @query_list.empty?
    if (@query_list.has_key?client_query_id)
      TheLog.error("Duplicate query id requested (#{client_query_id}")
      client_queue.push([client_query_id, ArgumentError.new("Client query ID already in use")])
      return
    end
    outstanding = []
    @query_list[client_query_id]=[msg, client_queue, q, outstanding]
    
    query_timeout = Time.now+@query_timeout
    if (@query_timeout == 0)
      query_timeout = Time.now+31536000 # a year from now

    end
    @timeouts[client_query_id]=[query_timeout, generate_timeouts()]
  }
  
  # Now do querying stuff using SingleResolver

  # All this will be handled by the tick method (if we have 0 as the first timeout)

  st = SelectThread.instance
  st.add_observer(q, self)
  tick if tick_needed
end

#send_message(message) ⇒ Object

Send a message, and wait for the response. If a valid Message is received, then it is returned to the caller. Otherwise an exception (a Dnsruby::ResolvError or Dnsruby::ResolvTimeout) is raised.

send_async is called internally.

example :

require 'Dnsruby'
res = Dnsruby::Resolver.new
begin
response = res.send_message(Message.new("example.com", Types.MX))
rescue ResolvError
  # ...
rescue ResolvTimeout
  # ...
end


139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/Dnsruby/Resolver.rb', line 139

def send_message(message)
  TheLog.debug("Resolver : sending message")
  q = Queue.new
  send_async(message, q, q)
  id, result, error = q.pop
  TheLog.debug("Resolver : result received")
  if (error != nil)
    raise error
  else
    return result
  end
  #      case result

  #      when Exception

  #        # Pass them on

  #        raise result

  #      when Message

  #        return result

  #      else

  #        TheLog.error("Unknown result returned : #{result}")

  #        raise ResolvError.new("Unknown error, return : #{result}")

  #      end 

end

#send_result_and_close(client_queue, client_query_id, select_queue, msg, error) ⇒ Object

MUST BE CALLED IN A SYNCHRONIZED BLOCK!

Send the result back to the client, and close the socket for that query by removing the query from the select thread.



287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/Dnsruby/Resolver.rb', line 287

def send_result_and_close(client_queue, client_query_id, select_queue, msg, error) #:nodoc: all

  TheLog.debug("Sending result #{error} to client")
  # We might still get some callbacks, which we should ignore

  st = SelectThread.instance
  st.remove_observer(select_queue, self)
  #      @mutex.synchronize{

  # Remove the query from all of the data structures

  @timeouts.delete(client_query_id)
  @query_list.delete(client_query_id)
  #      }

  # Return the response to the client

  client_queue.push([client_query_id, msg, error])
end

#set_config_nameserver(n) ⇒ Object



506
507
508
509
510
511
512
# File 'lib/Dnsruby/Resolver.rb', line 506

def set_config_nameserver(n)
  if (n).kind_of?String
    @config.nameserver=[n]
  else
    @config.nameserver=n
  end
end

#tickObject

This method is called ten times a second from the select loop, in the select thread. It should arguably be called from another worker thread… Each tick, we check if any timeouts have occurred. If so, we take the appropriate action : Return a timeout to the client, or send a new query



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/Dnsruby/Resolver.rb', line 305

def tick #:nodoc: all

  # Handle the tick

  # Do we have any retries due to be sent yet?

  @mutex.synchronize{
    time_now = Time.now
    @timeouts.keys.each do |client_query_id|
      msg, client_queue, select_queue, outstanding = @query_list[client_query_id]
      query_timeout, timeouts = @timeouts[client_query_id]
      if (query_timeout < Time.now)
        #Time the query out

        send_result_and_close(client_queue, client_query_id, select_queue, nil, ResolvTimeout.new("Query timed out"))
        next
      end
      timeouts_done = []
      timeouts.keys.sort.each do |timeout|
        if (timeout < time_now)
          # Send the next query

          res, retry_count = timeouts[timeout]
          id = [res, msg, client_query_id, retry_count]
          TheLog.debug("Sending msg to #{res.server}")
          # We should keep a list of the queries which are outstanding

          outstanding.push(id)
          timeouts_done.push(timeout)
          timeouts.delete(timeout)
          res.send_async(msg, id, select_queue)
        else
          break
        end
      end
      timeouts_done.each do |t|
        timeouts.delete(t)
      end
    end
  }
end

#updateObject

:nodoc: all



539
540
541
542
543
544
545
546
547
548
549
# File 'lib/Dnsruby/Resolver.rb', line 539

def update #:nodoc: all

  #Update any resolvers we have with the latest config

  @single_resolvers.each do |res|
    [:port, :use_tcp, :tsig_key, :ignore_truncation, :packet_timeout, 
      :src_address, :src_port, :persistent_tcp, :persistent_udp, :recurse, 
      :udp_size].each do |param|
      
      res.send(param.to_s+"=", instance_variable_get("@"+param.to_s))
    end
  end
end