gitlab-net-dns 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.gitlab-ci.yml +20 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +3 -0
  6. data/.rubocop_defaults.yml +359 -0
  7. data/.rubocop_todo.yml +207 -0
  8. data/.travis.yml +13 -0
  9. data/CHANGELOG.md +113 -0
  10. data/Gemfile +9 -0
  11. data/LICENSE.txt +56 -0
  12. data/README.md +172 -0
  13. data/Rakefile +38 -0
  14. data/THANKS.rdoc +24 -0
  15. data/bin/console +14 -0
  16. data/demo/check_soa.rb +104 -0
  17. data/demo/threads.rb +18 -0
  18. data/gitlab-net-dns.gemspec +24 -0
  19. data/lib/net/dns.rb +104 -0
  20. data/lib/net/dns/header.rb +705 -0
  21. data/lib/net/dns/names.rb +120 -0
  22. data/lib/net/dns/packet.rb +560 -0
  23. data/lib/net/dns/question.rb +185 -0
  24. data/lib/net/dns/resolver.rb +1214 -0
  25. data/lib/net/dns/resolver/socks.rb +148 -0
  26. data/lib/net/dns/resolver/timeouts.rb +70 -0
  27. data/lib/net/dns/rr.rb +356 -0
  28. data/lib/net/dns/rr/a.rb +114 -0
  29. data/lib/net/dns/rr/aaaa.rb +94 -0
  30. data/lib/net/dns/rr/classes.rb +130 -0
  31. data/lib/net/dns/rr/cname.rb +74 -0
  32. data/lib/net/dns/rr/hinfo.rb +96 -0
  33. data/lib/net/dns/rr/mr.rb +70 -0
  34. data/lib/net/dns/rr/mx.rb +82 -0
  35. data/lib/net/dns/rr/ns.rb +70 -0
  36. data/lib/net/dns/rr/null.rb +50 -0
  37. data/lib/net/dns/rr/ptr.rb +77 -0
  38. data/lib/net/dns/rr/soa.rb +75 -0
  39. data/lib/net/dns/rr/srv.rb +41 -0
  40. data/lib/net/dns/rr/txt.rb +58 -0
  41. data/lib/net/dns/rr/types.rb +191 -0
  42. data/lib/net/dns/version.rb +8 -0
  43. data/spec/fixtures/resolv.conf +4 -0
  44. data/spec/spec_helper.rb +14 -0
  45. data/spec/unit/resolver/dns_timeout_spec.rb +36 -0
  46. data/spec/unit/resolver/tcp_timeout_spec.rb +46 -0
  47. data/spec/unit/resolver/udp_timeout_spec.rb +46 -0
  48. data/test/test_helper.rb +13 -0
  49. data/test/unit/header_test.rb +164 -0
  50. data/test/unit/names_test.rb +21 -0
  51. data/test/unit/packet_test.rb +47 -0
  52. data/test/unit/question_test.rb +81 -0
  53. data/test/unit/resolver_test.rb +114 -0
  54. data/test/unit/rr/a_test.rb +106 -0
  55. data/test/unit/rr/aaaa_test.rb +102 -0
  56. data/test/unit/rr/classes_test.rb +83 -0
  57. data/test/unit/rr/cname_test.rb +90 -0
  58. data/test/unit/rr/hinfo_test.rb +111 -0
  59. data/test/unit/rr/mr_test.rb +99 -0
  60. data/test/unit/rr/mx_test.rb +106 -0
  61. data/test/unit/rr/ns_test.rb +80 -0
  62. data/test/unit/rr/types_test.rb +71 -0
  63. data/test/unit/rr_test.rb +127 -0
  64. metadata +172 -0
@@ -0,0 +1,185 @@
1
+ module Net
2
+ module DNS
3
+ #
4
+ # =Name
5
+ #
6
+ # Net::DNS::Question - DNS packet question class
7
+ #
8
+ # =Synopsis
9
+ #
10
+ # require 'net/dns/question'
11
+ #
12
+ # =Description
13
+ #
14
+ # This class represent the Question portion of a DNS packet. The number
15
+ # of question entries is stored in the +qdCount+ variable of an Header
16
+ # object.
17
+ #
18
+ # A new object can be created passing the name of the query and the type
19
+ # of answer desired, plus an optional argument containing the class:
20
+ #
21
+ # question = Net::DNS::Question.new("google.com.", Net::DNS::A)
22
+ # #=> "google.com. A IN"
23
+ #
24
+ # Alternatevly, a new object is created when processing a binary
25
+ # packet, as when an answer is received.
26
+ # To obtain the binary data from a question object you can use
27
+ # the method Question#data:
28
+ #
29
+ # question.data
30
+ # #=> "\006google\003com\000\000\001\000\001"
31
+ #
32
+ # A lot of methods were written to keep a compatibility layer with
33
+ # the Perl version of the library, as long as methods name which are
34
+ # more or less the same.
35
+ #
36
+ class Question
37
+ include Names
38
+
39
+ # Base error class.
40
+ class Error < StandardError
41
+ end
42
+
43
+ # An error in the +name+ part of a Question entry
44
+ class NameInvalid < Error
45
+ end
46
+
47
+ # +name+ part of a Question entry
48
+ attr_reader :qName
49
+ # +type+ part of a Question entry
50
+ attr_reader :qType
51
+ # +class+ part of a Question entry
52
+ attr_reader :qClass
53
+
54
+ # Creates a new Net::DNS::Question object:
55
+ #
56
+ # question = Net::DNS::Question.new("example.com")
57
+ # #=> "example.com A IN"
58
+ # question = Net::DNS::Question.new("example.com", Net::DNS::MX)
59
+ # #=> "example.com MX IN"
60
+ # question = Net::DNS::Question.new("example.com", Net::DNS::TXT, Net::DNS::HS)
61
+ # #=> "example.com TXT HS"
62
+
63
+ # If not specified, +type+ and +cls+ arguments defaults
64
+ # to Net::DNS::A and Net::DNS::IN respectively.
65
+ #
66
+ def initialize(name, type = Net::DNS::A, cls = Net::DNS::IN)
67
+ @qName = check_name name
68
+ @qType = Net::DNS::RR::Types.new(type)
69
+ @qClass = Net::DNS::RR::Classes.new(cls)
70
+ end
71
+
72
+ # Return a new Net::DNS::Question object created by
73
+ # parsing binary data, such as an answer from the
74
+ # nameserver.
75
+ #
76
+ # question = Net::DNS::Question.parse(data)
77
+ # puts "Queried for #{question.qName} type #{question.qType.to_s}"
78
+ # #=> Queried for example.com type A
79
+ #
80
+ def self.parse(arg)
81
+ o = allocate
82
+ o.send(:new_from_binary, arg.to_s)
83
+ o
84
+ end
85
+
86
+ # Outputs binary data from a Question object
87
+ #
88
+ # question.data
89
+ # #=> "\006google\003com\000\000\001\000\001"
90
+ #
91
+ def data
92
+ [pack_name(@qName), @qType.to_i, @qClass.to_i].pack("a*nn")
93
+ end
94
+
95
+ # Return the binary data of the objects, plus an offset
96
+ # and an Hash with references to compressed names. For use in
97
+ # Net::DNS::Packet compressed packet creation.
98
+ def comp_data
99
+ arr = @qName.split(".")
100
+ str = pack_name(@qName)
101
+ string = ""
102
+ names = {}
103
+ offset = Net::DNS::HFIXEDSZ
104
+ arr.size.times do |i|
105
+ x = i + 1
106
+ elem = arr[-x]
107
+ len = elem.size
108
+ string = (string.reverse + [len, elem].pack("Ca*").reverse).reverse
109
+ names[string] = offset
110
+ offset += len
111
+ end
112
+ offset += 2 * Net::DNS::INT16SZ
113
+ str += "\000"
114
+ [[str, @qType.to_i, @qClass.to_i].pack("a*nn"), offset, names]
115
+ end
116
+
117
+ #
118
+ # call-seq:
119
+ # question.inspect -> string
120
+ #
121
+ # Returns a printable version of question with nice formatting.
122
+ #
123
+ # q = Net::DNS::Question.new("google.com.", Net::DNS::A)
124
+ # q.inspect # => "google.com. IN A "
125
+ #
126
+ def inspect
127
+ len = if @qName.size > 29
128
+ @qName.size + 1
129
+ else
130
+ 29
131
+ end
132
+ [@qName, @qClass.to_s, @qType.to_s].pack("A#{len} A8 A8")
133
+ end
134
+
135
+ #
136
+ # call-seq:
137
+ # question.to_s -> string
138
+ #
139
+ # Returns a string representation of question.
140
+ # It is the same as <tt>inspect</tt>.
141
+ #
142
+ # q = Net::DNS::Question.new("google.com.", Net::DNS::A)
143
+ # q.inspect # => "google.com. IN A "
144
+ #
145
+ def to_s
146
+ inspect.to_s
147
+ end
148
+
149
+ private
150
+
151
+ def build_qName(str)
152
+ result = ""
153
+ offset = 0
154
+ loop do
155
+ len = str.unpack("@#{offset} C")[0]
156
+ break if len == 0
157
+
158
+ offset += 1
159
+ result += str[offset..offset + len - 1]
160
+ result += "."
161
+ offset += len
162
+ end
163
+ result
164
+ end
165
+
166
+ def check_name(input)
167
+ name = input.to_s.strip
168
+ if name =~ /[^\w\.\-_]/
169
+ raise NameInvalid, "Invalid Question Name `#{name}'"
170
+ end
171
+
172
+ name
173
+ end
174
+
175
+ def new_from_binary(data)
176
+ str, type, cls = data.unpack("a#{data.size - 4}nn")
177
+ @qName = build_qName(str)
178
+ @qType = Net::DNS::RR::Types.new type
179
+ @qClass = Net::DNS::RR::Classes.new cls
180
+ rescue StandardError => e
181
+ raise ArgumentError, "Invalid data: #{data.inspect}"
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,1214 @@
1
+ require 'rbconfig'
2
+ require 'socket'
3
+ require 'timeout'
4
+ require 'net/dns/packet'
5
+ require 'net/dns/resolver/timeouts'
6
+
7
+ # Resolver helper method.
8
+ #
9
+ # Calling the resolver directly:
10
+ #
11
+ # puts Resolver("www.google.com").answer.size
12
+ # # => 5
13
+ #
14
+ # An optional block can be passed yielding the Net::DNS::Packet object.
15
+ #
16
+ # Resolver("www.google.com") { |packet| puts packet.size + " bytes" }
17
+ # # => 484 bytes
18
+ #
19
+ def Resolver(name, type = Net::DNS::A, cls = Net::DNS::IN, &block)
20
+ resolver = Net::DNS::Resolver.start(name, type, cls)
21
+ if block_given?
22
+ yield resolver
23
+ else
24
+ resolver
25
+ end
26
+ end
27
+
28
+ module Net
29
+ module DNS
30
+ include Logger::Severity
31
+
32
+ # = Net::DNS::Resolver - DNS resolver class
33
+ #
34
+ # The Net::DNS::Resolver class implements a complete DNS resolver written
35
+ # in pure Ruby, without a single C line of code. It has all of the
36
+ # tipical properties of an evoluted resolver, and a bit of OO which
37
+ # comes from having used Ruby.
38
+ #
39
+ # This project started as a porting of the Net::DNS Perl module,
40
+ # written by Martin Fuhr, but turned out (in the last months) to be
41
+ # an almost complete rewriting. Well, maybe some of the features of
42
+ # the Perl version are still missing, but guys, at least this is
43
+ # readable code!
44
+ #
45
+ # == Environment
46
+ #
47
+ # The Following Environment variables can also be used to configure
48
+ # the resolver:
49
+ #
50
+ # * +RES_NAMESERVERS+: A space-separated list of nameservers to query.
51
+ #
52
+ # # Bourne Shell
53
+ # $ RES_NAMESERVERS="192.168.1.1 192.168.2.2 192.168.3.3"
54
+ # $ export RES_NAMESERVERS
55
+ #
56
+ # # C Shell
57
+ # % setenv RES_NAMESERVERS "192.168.1.1 192.168.2.2 192.168.3.3"
58
+ #
59
+ # * +RES_SEARCHLIST+: A space-separated list of domains to put in the
60
+ # search list.
61
+ #
62
+ # # Bourne Shell
63
+ # $ RES_SEARCHLIST="example.com sub1.example.com sub2.example.com"
64
+ # $ export RES_SEARCHLIST
65
+ #
66
+ # # C Shell
67
+ # % setenv RES_SEARCHLIST "example.com sub1.example.com sub2.example.com"
68
+ #
69
+ # * +LOCALDOMAIN+: The default domain.
70
+ #
71
+ # # Bourne Shell
72
+ # $ LOCALDOMAIN=example.com
73
+ # $ export LOCALDOMAIN
74
+ #
75
+ # # C Shell
76
+ # % setenv LOCALDOMAIN example.com
77
+ #
78
+ # * +RES_OPTIONS+: A space-separated list of resolver options to set.
79
+ # Options that take values are specified as option:value.
80
+ #
81
+ # # Bourne Shell
82
+ # $ RES_OPTIONS="retrans:3 retry:2 debug"
83
+ # $ export RES_OPTIONS
84
+ #
85
+ # # C Shell
86
+ # % setenv RES_OPTIONS "retrans:3 retry:2 debug"
87
+ #
88
+ class Resolver
89
+ class Error < StandardError
90
+ end
91
+
92
+ class NoResponseError < Error
93
+ end
94
+
95
+ # An hash with the defaults values of almost all the
96
+ # configuration parameters of a resolver object. See
97
+ # the description for each parameter to have an
98
+ # explanation of its usage.
99
+ Defaults = {
100
+ config_file: "/etc/resolv.conf",
101
+ log_file: $stdout,
102
+ port: 53,
103
+ searchlist: [],
104
+ nameservers: [IPAddr.new("127.0.0.1")],
105
+ domain: "",
106
+ source_port: 0,
107
+ source_address: IPAddr.new("0.0.0.0"),
108
+ source_address_inet6: IPAddr.new('::'),
109
+ retry_interval: 5,
110
+ retry_number: 4,
111
+ recursive: true,
112
+ defname: true,
113
+ dns_search: true,
114
+ use_tcp: false,
115
+ ignore_truncated: false,
116
+ packet_size: 512,
117
+ tcp_timeout: TcpTimeout.new(5),
118
+ udp_timeout: UdpTimeout.new(5),
119
+ }.freeze
120
+
121
+ class << self
122
+ C = Object.const_get(defined?(RbConfig) ? :RbConfig : :Config)::CONFIG
123
+
124
+ # Quick resolver method. Bypass the configuration using
125
+ # the defaults.
126
+ #
127
+ # Net::DNS::Resolver.start "www.google.com"
128
+ #
129
+ def start(*params)
130
+ new.search(*params)
131
+ end
132
+
133
+ # Returns true if running on a Windows platform.
134
+ #
135
+ # Note. This method doesn't rely on the RUBY_PLATFORM constant
136
+ # because the comparison will fail when running on JRuby.
137
+ # On JRuby RUBY_PLATFORM == 'java'.
138
+ def platform_windows?
139
+ !!(C["host_os"] =~ /msdos|mswin|djgpp|mingw/i)
140
+ end
141
+ end
142
+
143
+ # Creates a new resolver object.
144
+ #
145
+ # Argument +config+ can either be empty or be an hash with
146
+ # some configuration parameters. To know what each parameter
147
+ # do, look at the description of each.
148
+ # Some example:
149
+ #
150
+ # # Use the sistem defaults
151
+ # res = Net::DNS::Resolver.new
152
+ #
153
+ # # Specify a configuration file
154
+ # res = Net::DNS::Resolver.new(:config_file => '/my/dns.conf')
155
+ #
156
+ # # Set some option
157
+ # res = Net::DNS::Resolver.new(:nameservers => "172.16.1.1",
158
+ # :recursive => false,
159
+ # :retry => 10)
160
+ #
161
+ # == Config file
162
+ #
163
+ # Net::DNS::Resolver uses a config file to read the usual
164
+ # values a resolver needs, such as nameserver list and
165
+ # domain names. On UNIX systems the defaults are read from the
166
+ # following files, in the order indicated:
167
+ #
168
+ # * /etc/resolv.conf
169
+ # * $HOME/.resolv.conf
170
+ # * ./.resolv.conf
171
+ #
172
+ # The following keywords are recognized in resolver configuration files:
173
+ #
174
+ # * domain: the default domain.
175
+ # * search: a space-separated list of domains to put in the search list.
176
+ # * nameserver: a space-separated list of nameservers to query.
177
+ #
178
+ # Files except for /etc/resolv.conf must be owned by the effective userid
179
+ # running the program or they won't be read. In addition, several environment
180
+ # variables can also contain configuration information; see Environment
181
+ # in the main description for Resolver class.
182
+ #
183
+ # On Windows Systems, an attempt is made to determine the system defaults
184
+ # using the registry. This is still a work in progress; systems with many
185
+ # dynamically configured network interfaces may confuse Net::DNS.
186
+ #
187
+ # You can include a configuration file of your own when creating a resolver
188
+ # object:
189
+ #
190
+ # # Use my own configuration file
191
+ # my $res = Net::DNS::Resolver->new(config_file => '/my/dns.conf');
192
+ #
193
+ # This is supported on both UNIX and Windows. Values pulled from a custom
194
+ # configuration file override the the system's defaults, but can still be
195
+ # overridden by the other arguments to Resolver::new.
196
+ #
197
+ # Explicit arguments to Resolver::new override both the system's defaults
198
+ # and the values of the custom configuration file, if any.
199
+ #
200
+ # == Parameters
201
+ #
202
+ # The following arguments to Resolver::new are supported:
203
+ #
204
+ # * nameservers: an array reference of nameservers to query.
205
+ # * searchlist: an array reference of domains.
206
+ # * recurse
207
+ # * debug
208
+ # * domain
209
+ # * port
210
+ # * srcaddr
211
+ # * srcport
212
+ # * tcp_timeout
213
+ # * udp_timeout
214
+ # * retrans
215
+ # * retry
216
+ # * usevc
217
+ # * stayopen
218
+ # * igntc
219
+ # * defnames
220
+ # * dnsrch
221
+ # * persistent_tcp
222
+ # * persistent_udp
223
+ # * dnssec
224
+ #
225
+ # For more information on any of these options, please consult the
226
+ # method of the same name.
227
+ #
228
+ # == Disclaimer
229
+ #
230
+ # Part of the above documentation is taken from the one in the
231
+ # Net::DNS::Resolver Perl module.
232
+ #
233
+ def initialize(config = {})
234
+ config.is_a?(Hash) or
235
+ raise(ArgumentError, "Expected `config' to be a Hash")
236
+
237
+ @config = Defaults.merge config
238
+ @raw = false
239
+
240
+ # New logger facility
241
+ @logger = Logger.new(@config[:log_file])
242
+ @logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
243
+
244
+ #------------------------------------------------------------
245
+ # Resolver configuration will be set in order from:
246
+ # 1) initialize arguments
247
+ # 2) ENV variables
248
+ # 3) config file
249
+ # 4) defaults (and /etc/resolv.conf for config)
250
+ #------------------------------------------------------------
251
+
252
+ #------------------------------------------------------------
253
+ # Parsing config file
254
+ #------------------------------------------------------------
255
+ parse_config_file
256
+
257
+ #------------------------------------------------------------
258
+ # Parsing ENV variables
259
+ #------------------------------------------------------------
260
+ parse_environment_variables
261
+
262
+ #------------------------------------------------------------
263
+ # Parsing arguments
264
+ #------------------------------------------------------------
265
+ config.each do |key, val|
266
+ next if (key == :log_file) || (key == :config_file)
267
+
268
+ begin
269
+ eval "self.#{key} = val"
270
+ rescue NoMethodError
271
+ raise ArgumentError, "Option #{key} not valid"
272
+ end
273
+ end
274
+ end
275
+
276
+ # Get the resolver search list, returned as an array of entries.
277
+ #
278
+ # res.searchlist
279
+ # #=> ["example.com","a.example.com","b.example.com"]
280
+ #
281
+ def searchlist
282
+ @config[:searchlist].inspect
283
+ end
284
+
285
+ # Set the resolver searchlist.
286
+ # +arg+ can be a single string or an array of strings.
287
+ #
288
+ # res.searchstring = "example.com"
289
+ # res.searchstring = ["example.com","a.example.com","b.example.com"]
290
+ #
291
+ # Note that you can also append a new name to the searchlist.
292
+ #
293
+ # res.searchlist << "c.example.com"
294
+ # res.searchlist
295
+ # #=> ["example.com","a.example.com","b.example.com","c.example.com"]
296
+ #
297
+ # The default is an empty array.
298
+ #
299
+ def searchlist=(arg)
300
+ case arg
301
+ when String
302
+ @config[:searchlist] = [arg] if valid? arg
303
+ @logger.info "Searchlist changed to value #{@config[:searchlist].inspect}"
304
+ when Array
305
+ @config[:searchlist] = arg if arg.all? { |x| valid? x }
306
+ @logger.info "Searchlist changed to value #{@config[:searchlist].inspect}"
307
+ else
308
+ raise ArgumentError, "Wrong argument format, neither String nor Array"
309
+ end
310
+ end
311
+
312
+ # Get the list of resolver nameservers, in a dotted decimal format-
313
+ #
314
+ # res.nameservers
315
+ # #=> ["192.168.0.1","192.168.0.2"]
316
+ #
317
+ def nameservers
318
+ @config[:nameservers].map(&:to_s)
319
+ end
320
+
321
+ alias nameserver nameservers
322
+
323
+ # Set the list of resolver nameservers.
324
+ # +arg+ can be a single ip address or an array of addresses.
325
+ #
326
+ # res.nameservers = "192.168.0.1"
327
+ # res.nameservers = ["192.168.0.1","192.168.0.2"]
328
+ #
329
+ # If you want you can specify the addresses as IPAddr instances.
330
+ #
331
+ # ip = IPAddr.new("192.168.0.3")
332
+ # res.nameservers << ip
333
+ # #=> ["192.168.0.1","192.168.0.2","192.168.0.3"]
334
+ #
335
+ # The default is 127.0.0.1 (localhost)
336
+ #
337
+ def nameservers=(arg)
338
+ case arg
339
+ when String
340
+ begin
341
+ @config[:nameservers] = [IPAddr.new(arg)]
342
+ @logger.info "Nameservers list changed to value #{@config[:nameservers].inspect}"
343
+ rescue ArgumentError # arg is in the name form, not IP
344
+ nameservers_from_name(arg)
345
+ end
346
+ when IPAddr
347
+ @config[:nameservers] = [arg]
348
+ @logger.info "Nameservers list changed to value #{@config[:nameservers].inspect}"
349
+ when Array
350
+ @config[:nameservers] = []
351
+ arg.each do |x|
352
+ val = case x
353
+ when String
354
+ begin
355
+ IPAddr.new(x)
356
+ rescue ArgumentError
357
+ nameservers_from_name(arg)
358
+ return
359
+ end
360
+ when IPAddr
361
+ x
362
+ else
363
+ raise ArgumentError, "Wrong argument format"
364
+ end
365
+ @config[:nameservers] << val
366
+ end
367
+ @logger.info "Nameservers list changed to value #{@config[:nameservers].inspect}"
368
+ else
369
+ raise ArgumentError, "Wrong argument format, neither String, Array nor IPAddr"
370
+ end
371
+ end
372
+ alias_method("nameserver=", "nameservers=")
373
+
374
+ # Return a string with the default domain.
375
+ def domain
376
+ @config[:domain].inspect
377
+ end
378
+
379
+ # Set the domain for the query.
380
+ def domain=(name)
381
+ @config[:domain] = name if valid? name
382
+ end
383
+
384
+ # Return the defined size of the packet.
385
+ def packet_size
386
+ @config[:packet_size]
387
+ end
388
+
389
+ # Get the port number to which the resolver sends queries.
390
+ #
391
+ # puts "Sending queries to port #{res.port}"
392
+ #
393
+ def port
394
+ @config[:port]
395
+ end
396
+
397
+ # Set the port number to which the resolver sends queries. This can be useful
398
+ # for testing a nameserver running on a non-standard port.
399
+ #
400
+ # res.port = 10053
401
+ #
402
+ # The default is port 53.
403
+ #
404
+ def port=(num)
405
+ (0..65_535).cover?(num) or
406
+ raise(ArgumentError, "Wrong port number #{num}")
407
+
408
+ @config[:port] = num
409
+ @logger.info "Port number changed to #{num}"
410
+ end
411
+
412
+ # Get the value of the source port number.
413
+ #
414
+ # puts "Sending queries using port #{res.source_port}"
415
+ #
416
+ def source_port
417
+ @config[:source_port]
418
+ end
419
+ alias srcport source_port
420
+
421
+ # Set the local source port from which the resolver sends its queries.
422
+ #
423
+ # res.source_port = 40000
424
+ #
425
+ # Note that if you want to set a port you need root priviledges, as
426
+ # raw sockets will be used to generate packets. The class will then
427
+ # generate the exception ResolverPermissionError if you're not root.
428
+ #
429
+ # The default is 0, which means that the port will be chosen by the
430
+ # underlaying layers.
431
+ #
432
+ def source_port=(num)
433
+ root? or
434
+ raise(ResolverPermissionError, "Are you root?")
435
+ (0..65_535).cover?(num) or
436
+ raise(ArgumentError, "Wrong port number #{num}")
437
+
438
+ @config[:source_port] = num
439
+ end
440
+ alias srcport= source_port=
441
+
442
+ # Get the local address from which the resolver sends queries
443
+ #
444
+ # puts "Sending queries using source address #{res.source_address}"
445
+ #
446
+ def source_address
447
+ @config[:source_address].to_s
448
+ end
449
+ alias srcaddr source_address
450
+
451
+ # Get the local ipv6 address from which the resolver sends queries
452
+ #
453
+ def source_address_inet6
454
+ @config[:source_address_inet6].to_s
455
+ end
456
+
457
+ # Set the local source address from which the resolver sends its queries.
458
+ #
459
+ # res.source_address = "172.16.100.1"
460
+ # res.source_address = IPAddr.new("172.16.100.1")
461
+ #
462
+ # You can specify +arg+ as either a string containing the ip address
463
+ # or an instance of IPAddr class.
464
+ #
465
+ # Normally this can be used to force queries out a specific interface
466
+ # on a multi-homed host. In this case, you should of course need to
467
+ # know the addresses of the interfaces.
468
+ #
469
+ # Another way to use this option is for some kind of spoofing attacks
470
+ # towards weak nameservers, to probe the security of your network.
471
+ # This includes specifing ranged attacks such as DoS and others. For
472
+ # a paper on DNS security, checks http://www.marcoceresa.com/security/
473
+ #
474
+ # Note that if you want to set a non-binded source address you need
475
+ # root priviledges, as raw sockets will be used to generate packets.
476
+ # The class will then generate an exception if you're not root.
477
+ #
478
+ # The default is 0.0.0.0, meaning any local address (chosen on routing needs).
479
+ #
480
+ def source_address=(addr)
481
+ addr.respond_to?(:to_s) or
482
+ raise(ArgumentError, "Wrong address argument #{addr}")
483
+
484
+ begin
485
+ port = rand(1024..65_023)
486
+ @logger.info "Try to determine state of source address #{addr} with port #{port}"
487
+ a = TCPServer.new(addr.to_s, port)
488
+ rescue SystemCallError => e
489
+ case e.errno
490
+ when 98 # Port already in use!
491
+ @logger.warn "Port already in use"
492
+ retry
493
+ when 99 # Address is not valid: raw socket
494
+ @raw = true
495
+ @logger.warn "Using raw sockets"
496
+ else
497
+ raise SystemCallError, e
498
+ end
499
+ ensure
500
+ a.close
501
+ end
502
+
503
+ case addr
504
+ when String
505
+ @config[:source_address] = IPAddr.new(string)
506
+ @logger.info "Using new source address: #{@config[:source_address]}"
507
+ when IPAddr
508
+ @config[:source_address] = addr
509
+ @logger.info "Using new source address: #{@config[:source_address]}"
510
+ else
511
+ raise ArgumentError, "Unknown dest_address format"
512
+ end
513
+ end
514
+ alias srcaddr= source_address=
515
+
516
+ # Return the retrasmission interval (in seconds) the resolvers has
517
+ # been set on.
518
+ def retry_interval
519
+ @config[:retry_interval]
520
+ end
521
+ alias retrans retry_interval
522
+
523
+ # Set the retrasmission interval in seconds. Default 5 seconds.
524
+ def retry_interval=(num)
525
+ num.positive? or
526
+ raise(ArgumentError, "Interval must be positive")
527
+
528
+ @config[:retry_interval] = num
529
+ @logger.info "Retransmission interval changed to #{num} seconds"
530
+ end
531
+ alias retrans= retry_interval=
532
+
533
+ # The number of times the resolver will try a query.
534
+ #
535
+ # puts "Will try a max of #{res.retry_number} queries"
536
+ #
537
+ def retry_number
538
+ @config[:retry_number]
539
+ end
540
+
541
+ # Set the number of times the resolver will try a query.
542
+ # Default 4 times.
543
+ def retry_number=(num)
544
+ num.is_a?(Integer) && (num > 0) or
545
+ raise(ArgumentError, "Retry value must be a positive integer")
546
+
547
+ @config[:retry_number] = num
548
+ @logger.info "Retrasmissions number changed to #{num}"
549
+ end
550
+ alias_method('retry=', 'retry_number=')
551
+
552
+ # This method will return true if the resolver is configured to
553
+ # perform recursive queries.
554
+ #
555
+ # print "The resolver will perform a "
556
+ # print res.recursive? ? "" : "not "
557
+ # puts "recursive query"
558
+ #
559
+ def recursive?
560
+ @config[:recursive]
561
+ end
562
+ alias recurse recursive?
563
+ alias recursive recursive?
564
+
565
+ # Sets whether or not the resolver should perform recursive
566
+ # queries. Default is true.
567
+ #
568
+ # res.recursive = false # perform non-recursive query
569
+ #
570
+ def recursive=(bool)
571
+ case bool
572
+ when TrueClass, FalseClass
573
+ @config[:recursive] = bool
574
+ @logger.info("Recursive state changed to #{bool}")
575
+ else
576
+ raise ArgumentError, "Argument must be boolean"
577
+ end
578
+ end
579
+ alias recurse= recursive=
580
+
581
+ # Return a string representing the resolver state, suitable
582
+ # for printing on the screen.
583
+ #
584
+ # puts "Resolver state:"
585
+ # puts res.state
586
+ #
587
+ def state
588
+ str = ";; RESOLVER state:\n;; "
589
+ i = 1
590
+ @config.each do |key, val|
591
+ str << if (key == :log_file) || (key == :config_file)
592
+ "#{key}: #{val} \t"
593
+ else
594
+ "#{key}: #{eval(key.to_s)} \t"
595
+ end
596
+ str << "\n;; " if i.even?
597
+ i += 1
598
+ end
599
+ str
600
+ end
601
+ alias print state
602
+ alias inspect state
603
+
604
+ # Checks whether the +defname+ flag has been activate.
605
+ def defname?
606
+ @config[:defname]
607
+ end
608
+ alias defname defname?
609
+
610
+ # Set the flag +defname+ in a boolean state. if +defname+ is true,
611
+ # calls to Resolver#query will append the default domain to names
612
+ # that contain no dots.
613
+ # Example:
614
+ #
615
+ # # Domain example.com
616
+ # res.defname = true
617
+ # res.query("machine1")
618
+ # #=> This will perform a query for machine1.example.com
619
+ #
620
+ # Default is true.
621
+ #
622
+ def defname=(bool)
623
+ case bool
624
+ when TrueClass, FalseClass
625
+ @config[:defname] = bool
626
+ @logger.info("Defname state changed to #{bool}")
627
+ else
628
+ raise ArgumentError, "Argument must be boolean"
629
+ end
630
+ end
631
+
632
+ # Get the state of the dns_search flag.
633
+ def dns_search
634
+ @config[:dns_search]
635
+ end
636
+ alias dnsrch dns_search
637
+
638
+ # Set the flag +dns_search+ in a boolean state. If +dns_search+
639
+ # is true, when using the Resolver#search method will be applied
640
+ # the search list. Default is true.
641
+ def dns_search=(bool)
642
+ case bool
643
+ when TrueClass, FalseClass
644
+ @config[:dns_search] = bool
645
+ @logger.info("DNS search state changed to #{bool}")
646
+ else
647
+ raise ArgumentError, "Argument must be boolean"
648
+ end
649
+ end
650
+ alias_method("dnsrch=", "dns_search=")
651
+
652
+ # Get the state of the use_tcp flag.
653
+ #
654
+ def use_tcp?
655
+ @config[:use_tcp]
656
+ end
657
+ alias usevc use_tcp?
658
+ alias use_tcp use_tcp?
659
+
660
+ # If +use_tcp+ is true, the resolver will perform all queries
661
+ # using TCP virtual circuits instead of UDP datagrams, which
662
+ # is the default for the DNS protocol.
663
+ #
664
+ # res.use_tcp = true
665
+ # res.query "host.example.com"
666
+ # #=> Sending TCP segments...
667
+ #
668
+ # Default is false.
669
+ #
670
+ def use_tcp=(bool)
671
+ case bool
672
+ when TrueClass, FalseClass
673
+ @config[:use_tcp] = bool
674
+ @logger.info("Use tcp flag changed to #{bool}")
675
+ else
676
+ raise ArgumentError, "Argument must be boolean"
677
+ end
678
+ end
679
+ alias usevc= use_tcp=
680
+
681
+ def ignore_truncated?
682
+ @config[:ignore_truncated]
683
+ end
684
+ alias ignore_truncated ignore_truncated?
685
+
686
+ def ignore_truncated=(bool)
687
+ case bool
688
+ when TrueClass, FalseClass
689
+ @config[:ignore_truncated] = bool
690
+ @logger.info("Ignore truncated flag changed to #{bool}")
691
+ else
692
+ raise ArgumentError, "Argument must be boolean"
693
+ end
694
+ end
695
+
696
+ # Return an object representing the value of the stored TCP
697
+ # timeout the resolver will use in is queries. This object
698
+ # is an instance of the class +TcpTimeout+, and two methods
699
+ # are available for printing informations: TcpTimeout#to_s
700
+ # and TcpTimeout#pretty_to_s.
701
+ #
702
+ # Here's some example:
703
+ #
704
+ # puts "Timeout of #{res.tcp_timeout} seconds" # implicit to_s
705
+ # #=> Timeout of 150 seconds
706
+ #
707
+ # puts "You set a timeout of " + res.tcp_timeout.pretty_to_s
708
+ # #=> You set a timeout of 2 minutes and 30 seconds
709
+ #
710
+ # If the timeout is infinite, a string "infinite" will be returned.
711
+ #
712
+ def tcp_timeout
713
+ @config[:tcp_timeout].to_s
714
+ end
715
+
716
+ # Set the value of TCP timeout for resolver queries that
717
+ # will be performed using TCP. A value of 0 means that
718
+ # the timeout will be infinite.
719
+ # The value is stored internally as a +TcpTimeout+ object, see
720
+ # the description for Resolver#tcp_timeout
721
+ #
722
+ # Default is 5 seconds.
723
+ #
724
+ def tcp_timeout=(secs)
725
+ @config[:tcp_timeout] = TcpTimeout.new(secs)
726
+ @logger.info("New TCP timeout value: #{@config[:tcp_timeout]} seconds")
727
+ end
728
+
729
+ # Return an object representing the value of the stored UDP
730
+ # timeout the resolver will use in is queries. This object
731
+ # is an instance of the class +UdpTimeout+, and two methods
732
+ # are available for printing information: UdpTimeout#to_s
733
+ # and UdpTimeout#pretty_to_s.
734
+ #
735
+ # Here's some example:
736
+ #
737
+ # puts "Timeout of #{res.udp_timeout} seconds" # implicit to_s
738
+ # #=> Timeout of 150 seconds
739
+ #
740
+ # puts "You set a timeout of " + res.udp_timeout.pretty_to_s
741
+ # #=> You set a timeout of 2 minutes and 30 seconds
742
+ #
743
+ # If the timeout is zero, a string "not defined" will
744
+ # be returned.
745
+ #
746
+ def udp_timeout
747
+ @config[:udp_timeout].to_s
748
+ end
749
+
750
+ # Set the value of UDP timeout for resolver queries that
751
+ # will be performed using UDP. A value of 0 means that
752
+ # the timeout will not be used, and the resolver will use
753
+ # only +retry_number+ and +retry_interval+ parameters.
754
+ #
755
+ # Default is 5 seconds.
756
+ #
757
+ # The value is stored internally as a +UdpTimeout+ object, see
758
+ # the description for Resolver#udp_timeout.
759
+ #
760
+ def udp_timeout=(secs)
761
+ @config[:udp_timeout] = UdpTimeout.new(secs)
762
+ @logger.info("New UDP timeout value: #{@config[:udp_timeout]} seconds")
763
+ end
764
+
765
+ # Set a new log file for the logger facility of the resolver
766
+ # class. Could be a file descriptor too:
767
+ #
768
+ # res.log_file = $stderr
769
+ #
770
+ # Note that a new logging facility will be create, destroing
771
+ # the old one, which will then be impossibile to recover.
772
+ #
773
+ def log_file=(log)
774
+ @config[:log_file] = log
775
+ @logger = Logger.new(@config[:log_file])
776
+ @logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
777
+ end
778
+
779
+ # This one permits to have a personal logger facility to handle
780
+ # resolver messages, instead of new built-in one, which is set up
781
+ # for a +$stdout+ (or +$stderr+) use.
782
+ #
783
+ # If you want your own logging facility you can create a new instance
784
+ # of the +Logger+ class:
785
+ #
786
+ # log = Logger.new("/tmp/resolver.log","weekly",2*1024*1024)
787
+ # log.level = Logger::DEBUG
788
+ # log.progname = "ruby_resolver"
789
+ #
790
+ # and then pass it to the resolver:
791
+ #
792
+ # res.logger = log
793
+ #
794
+ # Note that this will destroy the precedent logger.
795
+ #
796
+ def logger=(logger)
797
+ logger.is_a?(Logger) or
798
+ raise(ArgumentError, "Argument must be an instance of Logger class")
799
+
800
+ @logger = logger
801
+ end
802
+
803
+ # Set the log level for the built-in logging facility.
804
+ #
805
+ # The log level can be one of the following:
806
+ #
807
+ # - +Net::DNS::DEBUG+
808
+ # - +Net::DNS::INFO+
809
+ # - +Net::DNS::WARN+
810
+ # - +Net::DNS::ERROR+
811
+ # - +Net::DNS::FATAL+
812
+ #
813
+ # Note that if the global variable $DEBUG is set (like when the
814
+ # -d switch is used at the command line) the logger level is
815
+ # automatically set at DEGUB.
816
+ #
817
+ # For further informations, see Logger documentation in the
818
+ # Ruby standard library.
819
+ #
820
+ def log_level=(level)
821
+ @logger.level = level
822
+ end
823
+
824
+ # Performs a DNS query for the given name, applying the searchlist if
825
+ # appropriate. The search algorithm is as follows:
826
+ #
827
+ # 1. If the name contains at least one dot, try it as is.
828
+ # 2. If the name doesn't end in a dot then append each item in the search
829
+ # list to the name. This is only done if +dns_search+ is true.
830
+ # 3. If the name doesn't contain any dots, try it as is.
831
+ #
832
+ # The record type and class can be omitted; they default to +A+ and +IN+.
833
+ #
834
+ # packet = res.search('mailhost')
835
+ # packet = res.search('mailhost.example.com')
836
+ # packet = res.search('example.com', Net::DNS::MX)
837
+ # packet = res.search('user.passwd.example.com', Net::DNS::TXT, Net::DNS::HS)
838
+ #
839
+ # If the name is an IP address (Ipv4 or IPv6), in the form of a string
840
+ # or a +IPAddr+ object, then an appropriate PTR query will be performed:
841
+ #
842
+ # ip = IPAddr.new("172.16.100.2")
843
+ # packet = res.search(ip)
844
+ # packet = res.search("192.168.10.254")
845
+ #
846
+ # Returns a Net::DNS::Packet object. If you need to examine the response packet
847
+ # whether it contains any answers or not, use the Resolver#query method instead.
848
+ #
849
+ def search(name, type = Net::DNS::A, cls = Net::DNS::IN)
850
+ return query(name, type, cls) if name.class == IPAddr
851
+
852
+ # If the name contains at least one dot then try it as is first.
853
+ if name.include? "."
854
+ @logger.debug "Search(#{name},#{Net::DNS::RR::Types.new(type)},#{Net::DNS::RR::Classes.new(cls)})"
855
+ ans = query(name, type, cls)
856
+ return ans if ans.header.anCount > 0
857
+ end
858
+
859
+ # If the name doesn't end in a dot then apply the search list.
860
+ if name !~ /\.$/ && @config[:dns_search]
861
+ @config[:searchlist].each do |domain|
862
+ newname = name + "." + domain
863
+ @logger.debug "Search(#{newname},#{Net::DNS::RR::Types.new(type)},#{Net::DNS::RR::Classes.new(cls)})"
864
+ ans = query(newname, type, cls)
865
+ return ans if ans.header.anCount > 0
866
+ end
867
+ end
868
+
869
+ # Finally, if the name has no dots then try it as is.
870
+ @logger.debug "Search(#{name},#{Net::DNS::RR::Types.new(type)},#{Net::DNS::RR::Classes.new(cls)})"
871
+ query(name + ".", type, cls)
872
+ end
873
+
874
+ # Performs a DNS query for the given name; the search list
875
+ # is not applied. If the name doesn't contain any dots and
876
+ # +defname+ is true then the default domain will be appended.
877
+ #
878
+ # The record type and class can be omitted; they default to +A+
879
+ # and +IN+. If the name looks like an IP address (IPv4 or IPv6),
880
+ # then an appropriate PTR query will be performed.
881
+ #
882
+ # packet = res.query('mailhost')
883
+ # packet = res.query('mailhost.example.com')
884
+ # packet = res.query('example.com', Net::DNS::MX)
885
+ # packet = res.query('user.passwd.example.com', Net::DNS::TXT, Net::DNS::HS)
886
+ #
887
+ # If the name is an IP address (Ipv4 or IPv6), in the form of a string
888
+ # or a +IPAddr+ object, then an appropriate PTR query will be performed:
889
+ #
890
+ # ip = IPAddr.new("172.16.100.2")
891
+ # packet = res.query(ip)
892
+ # packet = res.query("192.168.10.254")
893
+ #
894
+ # Returns a Net::DNS::Packet object. If you need to examine the response
895
+ # packet whether it contains any answers or not, use the Resolver#query
896
+ # method instead.
897
+ #
898
+ def query(name, type = Net::DNS::A, cls = Net::DNS::IN)
899
+ return send(name, type, cls) if name.class == IPAddr
900
+
901
+ # If the name doesn't contain any dots then append the default domain.
902
+ if name !~ /\./ && name !~ /:/ && @config[:defname]
903
+ name += "." + @config[:domain]
904
+ end
905
+
906
+ @logger.debug "Query(#{name},#{Net::DNS::RR::Types.new(type)},#{Net::DNS::RR::Classes.new(cls)})"
907
+
908
+ send(name, type, cls)
909
+ end
910
+
911
+ # Performs a DNS query for the given name. Neither the
912
+ # searchlist nor the default domain will be appended.
913
+ #
914
+ # The argument list can be either a Net::DNS::Packet object
915
+ # or a name string plus optional type and class, which if
916
+ # omitted default to +A+ and +IN+.
917
+ #
918
+ # Returns a Net::DNS::Packet object.
919
+ #
920
+ # # Executes the query with a +Packet+ object
921
+ # send_packet = Net::DNS::Packet.new("host.example.com", Net::DNS::NS, Net::DNS::HS)
922
+ # packet = res.query(send_packet)
923
+ #
924
+ # # Executes the query with a host, type and cls
925
+ # packet = res.query("host.example.com")
926
+ # packet = res.query("host.example.com", Net::DNS::NS)
927
+ # packet = res.query("host.example.com", Net::DNS::NS, Net::DNS::HS)
928
+ #
929
+ # If the name is an IP address (Ipv4 or IPv6), in the form of a string
930
+ # or a IPAddr object, then an appropriate PTR query will be performed:
931
+ #
932
+ # ip = IPAddr.new("172.16.100.2")
933
+ # packet = res.query(ip)
934
+ #
935
+ # packet = res.query("172.16.100.2")
936
+ #
937
+ # Use +packet.header.ancount+ or +packet.answer+ to find out if there
938
+ # were any records in the answer section.
939
+ #
940
+ def query(argument, type = Net::DNS::A, cls = Net::DNS::IN)
941
+ !@config[:nameservers].empty? or
942
+ raise(Resolver::Error, "No nameservers specified!")
943
+
944
+ method = :query_udp
945
+ packet = if argument.is_a? Net::DNS::Packet
946
+ argument
947
+ else
948
+ make_query_packet(argument, type, cls)
949
+ end
950
+
951
+ # Store packet_data for performance improvements,
952
+ # so methods don't keep on calling Packet#data
953
+ packet_data = packet.data
954
+ packet_size = packet_data.size
955
+
956
+ # Choose whether use TCP, UDP or RAW
957
+ if packet_size > @config[:packet_size] # Must use TCP, either plain or raw
958
+ if @raw # Use raw sockets?
959
+ @logger.info "Sending #{packet_size} bytes using TCP over RAW socket"
960
+ method = :send_raw_tcp
961
+ else
962
+ @logger.info "Sending #{packet_size} bytes using TCP"
963
+ method = :query_tcp
964
+ end
965
+ else # Packet size is inside the boundaries
966
+ if @raw # Use raw sockets?
967
+ @logger.info "Sending #{packet_size} bytes using UDP over RAW socket"
968
+ method = :send_raw_udp
969
+ elsif use_tcp? # User requested TCP
970
+ @logger.info "Sending #{packet_size} bytes using TCP"
971
+ method = :query_tcp
972
+ else # Finally use UDP
973
+ @logger.info "Sending #{packet_size} bytes using UDP"
974
+ end
975
+ end
976
+
977
+ if type == Net::DNS::AXFR
978
+ if @raw
979
+ @logger.info "AXFR query, switching to TCP over RAW socket"
980
+ method = :send_raw_tcp
981
+ else
982
+ @logger.info "AXFR query, switching to TCP"
983
+ method = :query_tcp
984
+ end
985
+ end
986
+
987
+ ans = send(method, packet, packet_data)
988
+
989
+ unless ans
990
+ message = "No response from nameservers list"
991
+ @logger.fatal(message)
992
+ raise NoResponseError, message
993
+ end
994
+
995
+ @logger.info "Received #{ans[0].size} bytes from #{ans[1][2] + ':' + ans[1][1].to_s}"
996
+ response = Net::DNS::Packet.parse(ans[0], ans[1])
997
+
998
+ if response.header.truncated? && !ignore_truncated?
999
+ @logger.warn "Packet truncated, retrying using TCP"
1000
+ self.use_tcp = true
1001
+ begin
1002
+ return query(argument, type, cls)
1003
+ ensure
1004
+ self.use_tcp = false
1005
+ end
1006
+ end
1007
+
1008
+ response
1009
+ end
1010
+
1011
+ # Performs a zone transfer for the zone passed as a parameter.
1012
+ #
1013
+ # It is actually only a wrapper to a send with type set as Net::DNS::AXFR,
1014
+ # since it is using the same infrastucture.
1015
+ #
1016
+ def axfr(name, cls = Net::DNS::IN)
1017
+ @logger.info "Requested AXFR transfer, zone #{name} class #{cls}"
1018
+ query(name, Net::DNS::AXFR, cls)
1019
+ end
1020
+
1021
+ # Performs an MX query for the domain name passed as parameter.
1022
+ #
1023
+ # It actually uses the same methods a normal Resolver query would
1024
+ # use, but automatically sort the results based on preferences
1025
+ # and returns an ordered array.
1026
+ #
1027
+ # res = Net::DNS::Resolver.new
1028
+ # res.mx("google.com")
1029
+ #
1030
+ def mx(name, cls = Net::DNS::IN)
1031
+ arr = []
1032
+ query(name, Net::DNS::MX, cls).answer.each do |entry|
1033
+ arr << entry if entry.type == 'MX'
1034
+ end
1035
+ arr.sort_by(&:preference)
1036
+ end
1037
+
1038
+ private
1039
+
1040
+ # Parses a configuration file specified as the argument.
1041
+ def parse_config_file
1042
+ if self.class.platform_windows?
1043
+ require 'win32/resolv'
1044
+ arr = Win32::Resolv.get_resolv_info
1045
+ self.domain = arr[0][0]
1046
+ self.nameservers = arr[1]
1047
+ else
1048
+ nameservers = []
1049
+ IO.foreach(@config[:config_file]) do |line|
1050
+ line.gsub!(/\s*[;#].*/, "")
1051
+ next unless line =~ /\S/
1052
+
1053
+ case line
1054
+ when /^\s*domain\s+(\S+)/
1055
+ self.domain = Regexp.last_match(1)
1056
+ when /^\s*search\s+(.*)/
1057
+ self.searchlist = Regexp.last_match(1).split(" ")
1058
+ when /^\s*nameserver\s+(.*)/
1059
+ nameservers << Regexp.last_match(1).split(" ")
1060
+ end
1061
+ end
1062
+ self.nameservers = nameservers.flatten
1063
+ end
1064
+ end
1065
+
1066
+ # Parses environment variables.
1067
+ def parse_environment_variables
1068
+ if ENV['RES_NAMESERVERS']
1069
+ self.nameservers = ENV['RES_NAMESERVERS'].split(" ")
1070
+ end
1071
+ if ENV['RES_SEARCHLIST']
1072
+ self.searchlist = ENV['RES_SEARCHLIST'].split(" ")
1073
+ end
1074
+ if ENV['LOCALDOMAIN']
1075
+ self.domain = ENV['LOCALDOMAIN']
1076
+ end
1077
+ if ENV['RES_OPTIONS']
1078
+ ENV['RES_OPTIONS'].split(" ").each do |opt|
1079
+ name, val = opt.split(":")
1080
+ begin
1081
+ eval("self.#{name} = #{val}")
1082
+ rescue NoMethodError
1083
+ raise ArgumentError, "Invalid ENV option #{name}"
1084
+ end
1085
+ end
1086
+ end
1087
+ end
1088
+
1089
+ def nameservers_from_name(arg)
1090
+ arr = []
1091
+ arg.split(" ").each do |name|
1092
+ Resolver.new.search(name).each_address do |ip|
1093
+ arr << ip
1094
+ end
1095
+ end
1096
+ @config[:nameservers] << arr
1097
+ end
1098
+
1099
+ def make_query_packet(string, type, cls)
1100
+ case string
1101
+ when IPAddr
1102
+ name = string.reverse
1103
+ type = Net::DNS::PTR
1104
+ @logger.warn "PTR query required for address #{string}, changing type to PTR"
1105
+ when /\d/ # Contains a number, try to see if it's an IP or IPv6 address
1106
+ begin
1107
+ name = IPAddr.new(string.chomp(".")).reverse
1108
+ type = Net::DNS::PTR
1109
+ rescue ArgumentError
1110
+ name = string if valid? string
1111
+ end
1112
+ else
1113
+ name = string if valid? string
1114
+ end
1115
+
1116
+ # Create the packet
1117
+ packet = Net::DNS::Packet.new(name, type, cls)
1118
+
1119
+ if packet.query?
1120
+ packet.header.recursive = @config[:recursive] ? 1 : 0
1121
+ end
1122
+
1123
+ # DNSSEC and TSIG stuff to be inserted here
1124
+
1125
+ packet
1126
+ end
1127
+
1128
+ def query_tcp(packet, packet_data)
1129
+ ans = nil
1130
+ length = [packet_data.size].pack("n")
1131
+
1132
+ @config[:nameservers].each do |ns|
1133
+ begin
1134
+ buffer = ""
1135
+ socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
1136
+ socket.bind(Socket.pack_sockaddr_in(@config[:source_port], @config[:source_address].to_s))
1137
+
1138
+ sockaddr = Socket.pack_sockaddr_in(@config[:port], ns.to_s)
1139
+
1140
+ @config[:tcp_timeout].timeout do
1141
+ socket.connect(sockaddr)
1142
+ @logger.info "Contacting nameserver #{ns} port #{@config[:port]}"
1143
+ socket.write(length + packet_data)
1144
+ ans = socket.recv(Net::DNS::INT16SZ)
1145
+ len = ans.unpack("n")[0]
1146
+
1147
+ @logger.info "Receiving #{len} bytes..."
1148
+
1149
+ if len == 0
1150
+ @logger.warn "Receiving 0 length packet from nameserver #{ns}, trying next."
1151
+ next
1152
+ end
1153
+
1154
+ while buffer.size < len
1155
+ left = len - buffer.size
1156
+ temp, from = socket.recvfrom(left)
1157
+ buffer += temp
1158
+ end
1159
+
1160
+ unless buffer.size == len
1161
+ @logger.warn "Malformed packet from nameserver #{ns}, trying next."
1162
+ next
1163
+ end
1164
+ end
1165
+ return [buffer, ["", @config[:port], ns.to_s, ns.to_s]]
1166
+ rescue TimeoutError
1167
+ @logger.warn "Nameserver #{ns} not responding within TCP timeout, trying next one"
1168
+ next
1169
+ ensure
1170
+ socket.close
1171
+ end
1172
+ end
1173
+ ans
1174
+ end
1175
+
1176
+ def query_udp(packet, packet_data)
1177
+ socket4 = UDPSocket.new
1178
+ socket4.bind(@config[:source_address].to_s, @config[:source_port])
1179
+ socket6 = UDPSocket.new(Socket::AF_INET6)
1180
+ socket6.bind(@config[:source_address_inet6].to_s, @config[:source_port])
1181
+
1182
+ ans = nil
1183
+ response = ""
1184
+ @config[:nameservers].each do |ns|
1185
+ begin
1186
+ @config[:udp_timeout].timeout do
1187
+ @logger.info "Contacting nameserver #{ns} port #{@config[:port]}"
1188
+ ans = if ns.ipv6?
1189
+ socket6.send(packet_data, 0, ns.to_s, @config[:port])
1190
+ socket6.recvfrom(@config[:packet_size])
1191
+ else
1192
+ socket4.send(packet_data, 0, ns.to_s, @config[:port])
1193
+ socket4.recvfrom(@config[:packet_size])
1194
+ end
1195
+ end
1196
+ break if ans
1197
+ rescue TimeoutError
1198
+ @logger.warn "Nameserver #{ns} not responding within UDP timeout, trying next one"
1199
+ next
1200
+ end
1201
+ end
1202
+ ans
1203
+ end
1204
+
1205
+ # FIXME: a ? method should never raise.
1206
+ def valid?(name)
1207
+ name !~ /[^-\w\.]/ or
1208
+ raise(ArgumentError, "Invalid domain name #{name}")
1209
+
1210
+ true
1211
+ end
1212
+ end
1213
+ end
1214
+ end