mock_dns_server 0.3.0

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 (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