mock_dns_server 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +24 -0
  5. data/README.md +127 -0
  6. data/RELEASE_NOTES.md +3 -0
  7. data/Rakefile +19 -0
  8. data/bin/show_dig_request +41 -0
  9. data/lib/mock_dns_server.rb +12 -0
  10. data/lib/mock_dns_server/action_factory.rb +84 -0
  11. data/lib/mock_dns_server/conditional_action.rb +42 -0
  12. data/lib/mock_dns_server/conditional_action_factory.rb +53 -0
  13. data/lib/mock_dns_server/conditional_actions.rb +73 -0
  14. data/lib/mock_dns_server/dnsruby_monkey_patch.rb +19 -0
  15. data/lib/mock_dns_server/history.rb +84 -0
  16. data/lib/mock_dns_server/history_inspections.rb +58 -0
  17. data/lib/mock_dns_server/ip_address_dispenser.rb +34 -0
  18. data/lib/mock_dns_server/message_builder.rb +199 -0
  19. data/lib/mock_dns_server/message_helper.rb +86 -0
  20. data/lib/mock_dns_server/message_transformer.rb +74 -0
  21. data/lib/mock_dns_server/predicate_factory.rb +108 -0
  22. data/lib/mock_dns_server/serial_history.rb +385 -0
  23. data/lib/mock_dns_server/serial_number.rb +129 -0
  24. data/lib/mock_dns_server/serial_transaction.rb +46 -0
  25. data/lib/mock_dns_server/server.rb +422 -0
  26. data/lib/mock_dns_server/server_context.rb +57 -0
  27. data/lib/mock_dns_server/server_thread.rb +13 -0
  28. data/lib/mock_dns_server/version.rb +3 -0
  29. data/mock_dns_server.gemspec +32 -0
  30. data/spec/mock_dns_server/conditions_factory_spec.rb +58 -0
  31. data/spec/mock_dns_server/history_inspections_spec.rb +84 -0
  32. data/spec/mock_dns_server/history_spec.rb +65 -0
  33. data/spec/mock_dns_server/ip_address_dispenser_spec.rb +30 -0
  34. data/spec/mock_dns_server/message_builder_spec.rb +18 -0
  35. data/spec/mock_dns_server/predicate_factory_spec.rb +147 -0
  36. data/spec/mock_dns_server/serial_history_spec.rb +385 -0
  37. data/spec/mock_dns_server/serial_number_spec.rb +119 -0
  38. data/spec/mock_dns_server/serial_transaction_spec.rb +37 -0
  39. data/spec/mock_dns_server/server_context_spec.rb +20 -0
  40. data/spec/mock_dns_server/server_spec.rb +411 -0
  41. data/spec/mock_dns_server/socket_research_spec.rb +59 -0
  42. data/spec/spec_helper.rb +44 -0
  43. data/todo.txt +0 -0
  44. metadata +212 -0
@@ -0,0 +1,129 @@
1
+ # Handles serial values, adjusting correctly for wraparound.
2
+ #
3
+ # From the DNS and Bind book, p. 139:
4
+ #
5
+ # "The DNS serial number is a
6
+ # 32-bit unsigned integer whose value ranges from 0 to 4,294,967,295. The serial number
7
+ # uses sequence space arithmetic, which means that for any serial number, half the
8
+ # numbers in the number space (2,147,483,647 numbers) are less than the serial number,
9
+ # and half the numbers are larger.""
10
+
11
+ class SerialNumber
12
+
13
+
14
+ MIN_VALUE = 0
15
+ MAX_VALUE = 0xFFFF_FFFF # (4_294_967_295)
16
+ MAX_DIFFERENCE = 0x8000_0000 # (2_147_483_648)
17
+
18
+ attr_accessor :value
19
+
20
+ def initialize(value)
21
+ self.class.validate(value)
22
+ @value = value
23
+ end
24
+
25
+
26
+ # Call this when you have an object that may be either a SerialNumber
27
+ # or a Fixnum/Bignum, and you want to ensure that you have
28
+ # a SerialNumber.
29
+ def self.object(thing)
30
+ if thing.is_a?(SerialNumber)
31
+ thing
32
+ elsif thing.nil?
33
+ nil
34
+ else
35
+ SerialNumber.new(thing)
36
+ end
37
+ end
38
+
39
+
40
+ def self.validate(value)
41
+ if value < MIN_VALUE || value > MAX_VALUE
42
+ raise "Invalid value (#{value}), must be between #{MIN_VALUE} and #{MAX_VALUE}."
43
+ end
44
+ end
45
+
46
+
47
+ def self.compare_values(value_1, value_2)
48
+ distance = (value_1 - value_2).abs
49
+
50
+ if distance == 0
51
+ 0
52
+ elsif distance < MAX_DIFFERENCE
53
+ value_1 - value_2
54
+ elsif distance > MAX_DIFFERENCE
55
+ value_2 - value_1
56
+ else # distance == MAX_DIFFERENCE
57
+ raise "Cannot compare 2 numbers whose difference is exactly #{MAX_DIFFERENCE.to_s(16)} (#{value_1}, #{value_2})."
58
+ end
59
+ end
60
+
61
+
62
+ def self.compare(sss1, sss2)
63
+ compare_values(sss1.value, sss2.value)
64
+ end
65
+
66
+
67
+ def self.next_serial_value(value)
68
+ validate(value)
69
+ value == MAX_VALUE ? 0 : value + 1
70
+ end
71
+
72
+
73
+ def next_serial
74
+ self.class.new(self.class.next_serial_value(value))
75
+ end
76
+
77
+
78
+ def <=>(other)
79
+ self.class.compare(self, other)
80
+ end
81
+
82
+
83
+ def >(other)
84
+ self.<=>(other) > 0
85
+ end
86
+
87
+
88
+ def <(other)
89
+ self.<=>(other) < 0
90
+ end
91
+
92
+
93
+ def >=(other)
94
+ self.<=>(other) >= 0
95
+ end
96
+
97
+
98
+ def <=(other)
99
+ self.<=>(other) <= 0
100
+ end
101
+
102
+
103
+ def ==(other)
104
+ self.class == other.class &&
105
+ self.value == other.value
106
+ end
107
+
108
+
109
+ def hash
110
+ value.hash
111
+ end
112
+
113
+ def eql?(other)
114
+ self.==(other)
115
+ end
116
+
117
+ # Can be used to normalize an object that may be a Fixnum or a SerialNumber to an int:
118
+ def to_i
119
+ value
120
+ end
121
+
122
+
123
+ def to_s
124
+ "#{self.class}: value = #{value}"
125
+ end
126
+
127
+ end
128
+
129
+
@@ -0,0 +1,46 @@
1
+ module MockDnsServer
2
+
3
+ # Manages RR additions and deletions for a given serial.
4
+ class SerialTransaction
5
+
6
+ # serial is the starting serial, i.e. the serial to which
7
+ # the additions and changes will be applied to get to
8
+ # the next serial value.
9
+ attr_accessor :serial, :additions, :deletions, :zone
10
+
11
+ # An object containing serial change information
12
+ #
13
+ # @param zone the zone for which this data applies
14
+ # @param serial a number from 0 to 2^32 - 1, or a SerialNumber instance
15
+ # @param deletions a single RR or an array of RR's representing deletions
16
+ # @param additions a single RR or an array of RR's representing additions
17
+ def initialize(zone, serial, deletions = [], additions = [])
18
+ @zone = zone
19
+ @serial = SerialNumber.object(serial)
20
+ @deletions = Array(deletions)
21
+ @additions = Array(additions)
22
+ end
23
+
24
+
25
+ # Returns an array of records corresponding to a serial change of 1,
26
+ # including delimiting SOA records, suitable for inclusion in an
27
+ # IXFR response.
28
+ def ixfr_records(start_serial)
29
+ records = []
30
+ records << MessageBuilder.soa_answer(name: zone, serial: start_serial)
31
+ deletions.each { |record| records << record }
32
+ records << MessageBuilder.soa_answer(name: zone, serial: serial)
33
+ additions.each { |record| records << record }
34
+ #require 'awesome_print'; puts ''; ap records; puts ''
35
+ records
36
+ end
37
+
38
+
39
+ def to_s
40
+ s = "Changes to serial #{serial}:\n"
41
+ deletions.each { |d| s << "- #{d}\n" }
42
+ additions.each { |a| s << "+ #{a}\n" }
43
+ s
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,422 @@
1
+ require 'socket'
2
+ require 'forwardable'
3
+ require 'thread_safe'
4
+
5
+ require 'mock_dns_server/message_helper'
6
+ require 'mock_dns_server/server_context'
7
+ require 'mock_dns_server/server_thread'
8
+
9
+
10
+ module MockDnsServer
11
+
12
+ # Starts a UDP and TCP server that listens for DNS and/or other messages.
13
+ class Server
14
+
15
+ extend Forwardable
16
+ def_delegators :@context, :host, :port, :conditional_actions, :timeout_secs, :verbose
17
+
18
+ # Do we want serials to be attributes of the server, or configured in conditional actions?
19
+
20
+ attr_reader :context, :sockets, :serials, :control_queue
21
+
22
+ DEFAULT_PORT = 53
23
+ DEFAULT_TIMEOUT = 1.0
24
+
25
+
26
+ def initialize(options = {})
27
+
28
+ @closed = false
29
+ defaults = {
30
+ port: DEFAULT_PORT,
31
+ timeout_secs: DEFAULT_TIMEOUT,
32
+ verbose: false
33
+ }
34
+ options = defaults.merge(options)
35
+
36
+ @context = ServerContext.new(self, options)
37
+
38
+ self.class.open_servers << self
39
+ create_sockets
40
+ end
41
+
42
+
43
+ # Creates a server, executes the passed block, and then closes the server.
44
+ def create_sockets
45
+ @tcp_listener_socket = TCPServer.new(host, port)
46
+ @tcp_listener_socket.setsockopt(:SOCKET, :REUSEADDR, true)
47
+
48
+ @udp_socket = UDPSocket.new
49
+ @udp_socket.bind(host, port)
50
+
51
+ @control_queue = SizedQueue.new(1000)
52
+
53
+ @sockets = [@tcp_listener_socket, @udp_socket]
54
+ end
55
+
56
+
57
+ # Closes the sockets and exits the server thread if it has already been created.
58
+ def close
59
+ return if closed?
60
+ puts "Closing #{self}..." if verbose
61
+ @closed = true
62
+
63
+ sockets.each { |socket| socket.close unless socket.closed? }
64
+
65
+ self.class.open_servers.delete(self)
66
+
67
+ if @server_thread
68
+ @server_thread.exit
69
+ @server_thread.join
70
+ @server_thread = nil
71
+ end
72
+ end
73
+
74
+
75
+ def closed?
76
+ @closed
77
+ end
78
+
79
+
80
+ # Determines whether the server has reached the point in its lifetime when it is ready,
81
+ # but it may still be true after the server is closed. Intended to be used during server
82
+ # startup and not thereafter.
83
+ def ready?
84
+ !! @ready
85
+ end
86
+
87
+
88
+ def add_conditional_action(conditional_action)
89
+ conditional_actions.add(conditional_action)
90
+ end
91
+
92
+
93
+ def add_conditional_actions(conditional_actions)
94
+ conditional_actions.each { |ca| add_conditional_action(ca) }
95
+ end
96
+
97
+
98
+ def record_receipt(request, sender, protocol)
99
+ history.add_incoming(request, sender, protocol)
100
+ end
101
+
102
+
103
+ def handle_request(request, sender, protocol)
104
+
105
+ request = MessageHelper.to_dns_message(request)
106
+
107
+ context.with_mutex do
108
+ if block_given?
109
+ yield(request, sender, protocol)
110
+ else
111
+ record_receipt(request, sender, protocol)
112
+ context.conditional_actions.respond_to(request, sender, protocol)
113
+ end
114
+ end
115
+ end
116
+
117
+
118
+ def conditional_action_count
119
+ context.conditional_actions.size
120
+ end
121
+
122
+
123
+ # Starts this server and returns the new thread in which it will run.
124
+ # If a block is passed, it will be passed in turn to handle_request
125
+ # to be executed (on the server's thread).
126
+ def start(&block)
127
+ raise "Server already started." if @server_thread
128
+
129
+ puts "Starting server on host #{host}:#{port}..." if verbose
130
+ @server_thread = ServerThread.new do
131
+ begin
132
+
133
+ Thread.current.server = self
134
+
135
+ loop do
136
+ unless @control_queue.empty?
137
+ action = @control_queue.pop
138
+ action.()
139
+ end
140
+
141
+ @ready = true
142
+ reads, _, errors = IO.select(sockets, nil, sockets, timeout_secs)
143
+
144
+ error_occurred = errors && errors.first # errors.first will be nil on timeout
145
+ if error_occurred
146
+ puts errors if verbose
147
+ break
148
+ end
149
+
150
+ if reads
151
+ reads.each do |read_socket|
152
+ handle_read(block, read_socket)
153
+ #if conditional_actions.empty?
154
+ # puts "No more conditional actions. Closing server..." if verbose
155
+ # break
156
+ #end
157
+ end
158
+ else
159
+ # TODO: This is where we can put things to do periodically when the server is not busy
160
+ end
161
+ end
162
+ rescue => e
163
+ self.close
164
+ # Errno::EBADF is raised when the server object is closed normally,
165
+ # so we don't want to report it. All other errors should be reported.
166
+ raise e unless e.is_a?(Errno::EBADF)
167
+ end
168
+ end
169
+ self # for chaining, especially with wait_until_ready
170
+ end
171
+
172
+
173
+ # Handles the receiving of a single message.
174
+ def handle_read(block, read_socket)
175
+
176
+ request = nil
177
+ sender = nil
178
+ protocol = nil
179
+
180
+ if read_socket == @tcp_listener_socket
181
+ sockets << @tcp_listener_socket.accept
182
+ puts "Got new TCP socket: #{sockets.last}" if verbose
183
+
184
+ elsif read_socket == @udp_socket
185
+ protocol = :udp
186
+ request, sender = udp_recvfrom_with_timeout(read_socket)
187
+ request = MessageHelper.to_dns_message(request)
188
+ puts "Got incoming message from UDP socket:\n#{request}\n" if verbose
189
+
190
+ else # it must be a spawned TCP read socket
191
+ if read_socket.eof? # we're here because it closed on the client side
192
+ sockets.delete(read_socket)
193
+ puts "received EOF from socket #{read_socket}...deleted it from listener list." if verbose
194
+ else # read from it
195
+ protocol = :tcp
196
+
197
+ # Read message size:
198
+ request = MessageHelper.read_tcp_message(read_socket)
199
+ sender = read_socket
200
+
201
+ if verbose
202
+ if request.nil? || request == ''
203
+ puts "Got no request."
204
+ else
205
+ puts "Got incoming message from TCP socket:\n#{request}\n"
206
+ end
207
+ end
208
+ end
209
+ end
210
+ handle_request(request, sender, protocol, &block) if request
211
+ end
212
+
213
+
214
+ def send_tcp_response(socket, content)
215
+ socket.write(MessageHelper.tcp_message_package_for_write(content))
216
+ end
217
+
218
+
219
+ def send_udp_response(sender, content)
220
+ send_data = MessageHelper.udp_message_package_for_write(content)
221
+ _, client_port, ip_addr, _ = sender
222
+ @udp_socket.send(send_data, 0, ip_addr, client_port)
223
+ end
224
+
225
+
226
+ def send_response(sender, content, protocol)
227
+ if protocol == :tcp
228
+ send_tcp_response(sender, content)
229
+ elsif protocol == :udp
230
+ send_udp_response(sender, content)
231
+ end
232
+ end
233
+
234
+
235
+ def history_copy
236
+ copy = nil
237
+ context.with_mutex { copy = history.copy }
238
+ copy
239
+ end
240
+
241
+
242
+ def history
243
+ context.history
244
+ end; private :history
245
+
246
+
247
+ def occurred?(inspection)
248
+ history.occurred?(inspection)
249
+ end
250
+
251
+
252
+ def conditional_actions
253
+ context.conditional_actions
254
+ end; private :conditional_actions
255
+
256
+
257
+ def is_dns_packet?(packet, protocol)
258
+ raise "protocol must be :tcp or :udp" unless [:tcp, :udp].include?(protocol)
259
+
260
+ encoded_message = :udp ? packet : packet[2..-1]
261
+ message = MessageHelper.to_dns_message(encoded_message)
262
+ message.is_a?(Dnsruby::Message)
263
+ end
264
+
265
+
266
+ # @param message_options - name (zone), serial, mname
267
+ def send_notify(zts_host, zts_port, message_options, notify_message_override = nil, wait_for_response = true)
268
+ notify_message = notify_message_override ? notify_message_override : MessageBuilder::notify_message(message_options)
269
+
270
+ socket = UDPSocket.new
271
+
272
+ puts "Sending notify message to host #{zts_host}, port #{zts_port}" if verbose
273
+ socket.send(notify_message.encode, 0, zts_host, zts_port)
274
+
275
+ if wait_for_response
276
+ response_wire_data, _ = udp_recvfrom_with_timeout(socket)
277
+ response = MessageHelper.to_dns_message(response_wire_data)
278
+ context.with_mutex { history.add_notify_response(response, zts_host, zts_port, :udp) }
279
+ response
280
+ else
281
+ nil
282
+ end
283
+ end
284
+
285
+
286
+ def udp_recvfrom_with_timeout(udp_socket, timeout_secs = 10, max_data_size = 10_000) # TODO: better default max?
287
+ request = nil
288
+ sender = nil
289
+
290
+ recv_thread = Thread.new do
291
+ request, sender = udp_socket.recvfrom(max_data_size)
292
+ end
293
+ timeout_expired = recv_thread.join(timeout_secs).nil?
294
+ if timeout_expired
295
+ recv_thread.exit
296
+ raise "Response not received from UDP socket."
297
+ end
298
+ [request, sender]
299
+ end
300
+
301
+ # For an already initialized server, perform the passed block and ensure that the server
302
+ # will be closed, even if an error is raised.
303
+ def do_then_close
304
+ begin
305
+ start
306
+ yield
307
+ ensure
308
+ close
309
+ end
310
+ end
311
+
312
+
313
+ # Waits until the server is ready, sleeping between calls to ready.
314
+ # @return elapsed time until ready
315
+ def wait_until_ready(sleep_duration = 0.000_02)
316
+
317
+ if Thread.current == @server_thread
318
+ raise "This method must not be called in the server's thread."
319
+ end
320
+
321
+ start = Time.now
322
+ sleep(sleep_duration) until ready?
323
+ duration = Time.now - start
324
+ duration
325
+ end
326
+
327
+
328
+ # Sets up the SOA and records to serve on IXFR/AXFR queries.
329
+ # mname is set to "default.#{zone}
330
+ #
331
+ # @param options hash containing the following keys:
332
+ # zone
333
+ # serial (SOA)
334
+ # dns_records array of RR's
335
+ # times times for the action to be performed before removal (optional, defaults to forever)
336
+ # zts_hosts array of ZTS hosts
337
+ # zts_port (optional, defaults to 53)
338
+ #
339
+ def load_zone(options)
340
+
341
+ validate_options = ->() do
342
+ required_options = [:zone, :serial_history]
343
+ missing_options = required_options.select { |o| options[o].nil? }
344
+ unless missing_options.empty?
345
+ raise "Options required for load_zone were missing: #{missing_options.join(', ')}."
346
+ end
347
+ end
348
+
349
+ validate_options.()
350
+
351
+ serial_history = options[:serial_history]
352
+ zone = serial_history.zone
353
+ zts_hosts = Array(options[:zts_hosts])
354
+ zts_port = options[:zts_port] || 53
355
+ times = options[:times] || 0
356
+ mname = "default.#{zone}"
357
+
358
+ cond_action = ConditionalActionFactory.new.zone_load(serial_history, times)
359
+ conditional_actions.add(cond_action)
360
+
361
+ notify_options = { name: zone, serial: serial_history.high_serial, mname: mname }
362
+ zts_hosts.each do |zts_host|
363
+ send_notify(zts_host, zts_port, notify_options)
364
+ end
365
+ end
366
+
367
+
368
+ def to_s
369
+ "#{self.class.name}: host: #{host}, port: #{port}, ready: #{ready?}, closed: #{closed?}"
370
+ end
371
+
372
+
373
+ def self.open_servers
374
+ @servers ||= ThreadSafe::Array.new
375
+ end
376
+
377
+
378
+ # Creates a new server, yields to the passed block, then closes the server.
379
+ def self.with_new_server(options = {})
380
+ begin
381
+ server = self.new(options)
382
+ yield(server)
383
+ ensure
384
+ server.close if server
385
+ end
386
+ nil # don't want to return server because it should no longer be used
387
+ end
388
+
389
+
390
+ def self.close_all_servers
391
+ open_servers.clone.each { |server| server.close }
392
+ end
393
+
394
+
395
+ def self.kill_all_servers
396
+ threads_needing_exit = ServerThread.all.select { |thread| ['sleep', 'run'].include?(thread.status) }
397
+
398
+ threads_needing_exit.each do |thread|
399
+ server = thread.server
400
+ # If we can get a handle on the server, close it; else, just exit the thread.
401
+ if server
402
+ server.close
403
+ raise "Sockets not closed." unless server.closed?
404
+ else
405
+ raise "Could not get server reference"
406
+ end
407
+ thread.join
408
+ end
409
+ end
410
+
411
+
412
+ # Returns the IP addresses (as strings) of the host on which this is running
413
+ # that are eligible to be used for a Server instance. Eligibility is defined
414
+ # as IPV4, not loopback, and not multicast.
415
+ def self.eligible_interfaces
416
+ addrinfos = Socket.ip_address_list.select do |intf|
417
+ intf.ipv4? && !intf.ipv4_loopback? && !intf.ipv4_multicast?
418
+ end
419
+ addrinfos.map(&:ip_address)
420
+ end
421
+ end
422
+ end