gitlab-net-dns 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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