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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: abc38df32ffd449faa5b893d4b608eb08c3bcc53
4
+ data.tar.gz: c088ed5ad24f4e41457bf747f36dab340b210865
5
+ SHA512:
6
+ metadata.gz: 08e6c68851d74ac431d6c492b36ff25dec0c3be893d0f58d491c8f47f8b06c905365ee8e798898de6bc0a1a68a1ea896d368cfe576c05b8a48e32f4ebf4e204a
7
+ data.tar.gz: 584ab899e561059c4f07b94342a770821bc3b5989c23d6cc635fa4041ddea8fba5b61f5c455c474b9bf3faacf6599262bb3d3aa0d5ae9bd0e20f6328f6fc2415
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mock_dns_server.gemspec
4
+ gemspec
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2014, Verisign, Inc.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ * Neither the name of Verisign, Inc. nor the
12
+ names of its contributors may be used to endorse or promote products
13
+ derived from this software without specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
19
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,127 @@
1
+ # MockDnsServer
2
+
3
+ A mock DNS server that can be instructed to perform actions based on
4
+ user-provided conditions, and queried for its history of inputs and outputs.
5
+ This server listens and responds on both UDP and TCP ports.
6
+
7
+ An admin interface is provided, currently in the form of Ruby methods that
8
+ can be called. In the future we will probably add an HTTP interface
9
+ to these methods. The admin methods:
10
+
11
+ * instructing the server how to respond given the characteristics of the request
12
+ * query the server for its history of inputs and outputs
13
+ * request shutdown
14
+
15
+
16
+ ## Implementation
17
+
18
+
19
+ ### Threads
20
+
21
+ ```server.start``` launches its own thread in which it runs. This thread is terminated when ```server.close``` is called.
22
+
23
+ Admin requests (configuration, analysis, etc.) will occur on the caller's thread.
24
+
25
+
26
+ ### Starting a Server
27
+
28
+ The simplest way to run a server is to call the convenience method ```Server#with_new_server```:
29
+
30
+ ```ruby
31
+ Server.with_new_server(options) do |server|
32
+ # configure server with conditional actions
33
+ server.start
34
+ end
35
+ ```
36
+
37
+ Options currently consist of:
38
+
39
+ Option Name|Default Value|Description
40
+ -----------|-------------|-----------|
41
+ port | 53 | port on which to listen (UDP and TCP)
42
+ timeout | 0.25 sec | timeout for IO.select call
43
+ verbose | false | Whether or not to output diagnostic messages
44
+
45
+ The code above will result in the creation of a new thread in which the server will listen
46
+ and respond indefinitely. Terminating the server is accomplished by calling server.close
47
+ in the caller's thread.
48
+
49
+
50
+ ### Locking
51
+
52
+ A single mutex will be used to protect access to the rules and history objects.
53
+ All mutex handling will be done by code in this gem, so the caller does not
54
+ need to know or care that it is being done.
55
+
56
+
57
+ ### Message Read Loop
58
+
59
+ The server will have the following flow of execution:
60
+
61
+ ```
62
+ loop do
63
+ read a packet
64
+ attempt to parse it into a Dnsruby::Message object; if not, the message will be a string
65
+ mutex.synchronize do
66
+ action = look up rule
67
+ action.call # (perform appropriate action -- send or don't send response, etc.)
68
+ add input and output to history
69
+ end
70
+ end
71
+ ```
72
+
73
+ The above loop is wrapped in an IO.select timeout loop, although currently nothing
74
+ is done at timeout time. (Closing the server is accomplished by calling server.close
75
+ on the caller's thread.)
76
+
77
+ For TCP, since the application layer requires that the transmission begin with a 2-byte
78
+ message length field (which is packed/unpacked with 'n'), this field is read first,
79
+ and the server continues reading until the entire transmission is read.
80
+
81
+ ### Conditional Actions
82
+
83
+ The server can be set up with conditional actions that will control how it responds to
84
+ incoming requests. Basically, a ConditionalAction consists of a proc (usually a lambda)
85
+ that will be called to determine whether or not the action should be executed,
86
+ and another proc (also usually a lambda) defining the action that should be performed.
87
+
88
+ Only one conditional action will be performed per incoming request.
89
+ The conditions in the conditional actions are evaluated in the order with which
90
+ they were added to the server. When a condition returns true, the corresponding
91
+ action will be performed, and the message loop iteration will end.
92
+
93
+ For more information about how conditional actions are created, see the ConditionalAction,
94
+ PredicateFactory, ActionFactory, and ConditionalActionFactory classes. For how
95
+ the conditional actions are searched and performed, see the ConditionalActions class.
96
+
97
+
98
+ ### History
99
+
100
+ To get a history of the server's events, call ```server.history_copy```.
101
+
102
+
103
+ ## Installation
104
+
105
+ Add this line to your application's Gemfile:
106
+
107
+ gem 'mock_dns_server'
108
+
109
+ And then execute:
110
+
111
+ $ bundle
112
+
113
+ Or install it yourself as:
114
+
115
+ $ gem install mock_dns_server
116
+
117
+ ## Usage
118
+
119
+ TODO: Write usage instructions here
120
+
121
+ ## Contributing
122
+
123
+ 1. Fork it
124
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
125
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
126
+ 4. Push to the branch (`git push origin my-new-feature`)
127
+ 5. Create new Pull Request
@@ -0,0 +1,3 @@
1
+ ## v0.1.0
2
+
3
+ * First open sourced version.
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+ require 'rubygems'
3
+ require 'bundler'
4
+ require "bundler/gem_tasks"
5
+
6
+ begin
7
+ Bundler.setup(:default, :development)
8
+ rescue Bundler::BundlerError => e
9
+ $stderr.puts e.message
10
+ $stderr.puts "Run `bundle install` to install missing gems"
11
+ exit e.status_code
12
+ end
13
+ require 'rake'
14
+
15
+ require 'rspec/core/rake_task'
16
+ RSpec::Core::RakeTask.new(:spec)
17
+
18
+ task :test => :spec
19
+ task :default => :spec
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Stands up a Mock DNS Server to receive a dig request,
4
+ # and outputs the request to stdout using Dnsruby::Message#to_s,
5
+ # whose output differs somewhat from dig output, but has
6
+ # the same information..
7
+
8
+ require 'mock_dns_server'
9
+ require 'pry'
10
+ require 'hexdump'
11
+
12
+ MDS = MockDnsServer
13
+
14
+ def conditional_action
15
+ condition = MDS::PredicateFactory.new.always_true
16
+ action = MDS::ActionFactory.new.puts_and_echo
17
+ MDS::ConditionalAction.new(condition, action, 1)
18
+ end
19
+
20
+ def run
21
+ MDS::Server.with_new_server(host: 'localhost', port: 9999) do |server|
22
+ server.add_conditional_action(conditional_action)
23
+ server.start
24
+ puts "\nReflected message as per Dnsruby:\n\n"
25
+ output = `dig -p 9999 @localhost #{ARGV[0]} 2>&1`
26
+ puts "Reflected message as per dig:\n\n#{output}\n"
27
+ puts "Executing pry, press [Ctrl-D] to exit.\n\n"
28
+ binding.pry
29
+ raise output if $? != 0
30
+ end
31
+ end
32
+
33
+ def validate_input
34
+ if ARGV.empty?
35
+ puts "Syntax is show_dig_request \"[dig arguments]\" but without specifying host or port."
36
+ exit -1
37
+ end
38
+ end
39
+
40
+ validate_input
41
+ run
@@ -0,0 +1,12 @@
1
+
2
+ # This is put in a lambda so as not to pollute the namespace with variables that will be useless later
3
+ ->() {
4
+ file_mask = File.join(File.dirname(__FILE__), '**/*.rb')
5
+ files_to_require = Dir[file_mask]
6
+ files_to_require.each { |file| require file }
7
+ }.call
8
+
9
+
10
+ module MockDnsServer
11
+ # Your code goes here...
12
+ end
@@ -0,0 +1,84 @@
1
+ require 'mock_dns_server/message_builder'
2
+
3
+ module MockDnsServer
4
+
5
+ # Creates and returns actions that will be run upon receiving incoming messages.
6
+ class ActionFactory
7
+
8
+ include MessageBuilder
9
+
10
+ # Echos the request back to the sender.
11
+ def echo
12
+ ->(incoming_message, sender, context, protocol) do
13
+ context.server.send_response(sender, incoming_message, protocol)
14
+ end
15
+ end
16
+
17
+ def puts_and_echo
18
+ ->(incoming_message, sender, context, protocol) do
19
+ puts "Received #{protocol.to_s.upcase} message from #{sender}:\n#{incoming_message}\n\n"
20
+ puts "Hex:\n\n"
21
+ puts "#{incoming_message.encode.hexdump}\n\n"
22
+ echo.(incoming_message, sender, context, protocol)
23
+ end
24
+ end
25
+
26
+ # Responds with the same object regardless of the request content.
27
+ def constant(constant_object)
28
+ ->(_, sender, context, protocol) do
29
+ context.server.send_response(sender, constant_object, protocol)
30
+ end
31
+ end
32
+
33
+
34
+ # Sends a SOA response.
35
+ def send_soa(zone, serial, expire = nil, refresh = nil)
36
+ send_message(soa_response(
37
+ name: zone, serial: serial, expire: expire, refresh: refresh))
38
+ end
39
+
40
+
41
+ # Sends a fixed DNSRuby::Message.
42
+ def send_message(response)
43
+ ->(incoming_message, sender, context, protocol) do
44
+
45
+ if [incoming_message, response].all? { |m| m.is_a?(Dnsruby::Message) }
46
+ response.header.id = incoming_message.header.id
47
+ end
48
+ if response.is_a?(Dnsruby::Message)
49
+ response.header.qr = true
50
+ end
51
+ context.server.send_response(sender, response, protocol)
52
+ end
53
+ end
54
+
55
+ # Outputs the string representation of the incoming message to stdout.
56
+ def puts_message
57
+ ->(incoming_message, sender, context, protocol) do
58
+ puts incoming_message
59
+ end
60
+ end
61
+
62
+
63
+ def zone_load(serial_history)
64
+ ->(incoming_message, sender, context, protocol) do
65
+
66
+ mt = MessageTransformer.new(incoming_message)
67
+ zone = mt.qname
68
+ type = mt.qtype
69
+
70
+ if serial_history.zone.downcase != zone.downcase
71
+ raise "Zones differ (history: #{serial_history.zone}, request: #{zone}"
72
+ end
73
+
74
+ if %w(AXFR IXFR).include?(type)
75
+ xfr_response = serial_history.xfr_response(incoming_message)
76
+ send_message(xfr_response).(incoming_message, sender, context, :tcp)
77
+ elsif type == 'SOA'
78
+ send_soa(zone, serial_history.high_serial).(incoming_message, sender, context, protocol)
79
+ end
80
+ end
81
+ end
82
+
83
+ end
84
+ end
@@ -0,0 +1,42 @@
1
+ module MockDnsServer
2
+
3
+ class ConditionalAction
4
+
5
+ attr_accessor :condition, :action, :description, :max_uses, :use_count
6
+
7
+
8
+ # @param condition a proc/lambda that, when called with request as a param, returns
9
+ # true or false to determine whether or not the action will be executed
10
+ # @param action the code (lambda or proc) to be executed; takes incoming message,
11
+ # sender, server context, and (optionally) protocol as parameters
12
+ # and performs an action
13
+ # @param max_uses maximum number of times this action should be executed
14
+ # @return the value returned by the action, e.g. the message, or array of messages, it sent
15
+ def initialize(condition, action, max_uses)
16
+ @condition = condition
17
+ @action = action
18
+ @max_uses = max_uses
19
+ @use_count = 0
20
+ end
21
+
22
+
23
+ def increment_use_count
24
+ @use_count += 1
25
+ end
26
+
27
+ def to_s
28
+ "#{super.to_s}; condition: #{condition.to_s}, action = #{action.to_s}, max_uses = #{max_uses}"
29
+ end
30
+
31
+ def eligible_to_run?
32
+ max_not_reached = max_uses.nil? || use_count < max_uses
33
+ max_not_reached && condition.call
34
+ end
35
+
36
+ def run(request, sender, context, protocol)
37
+ # TODO: Output to history?
38
+ action.call(request, sender, context, protocol)
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,53 @@
1
+ require 'mock_dns_server/conditional_action'
2
+ require 'mock_dns_server/action_factory'
3
+ require 'mock_dns_server/predicate_factory'
4
+
5
+ module MockDnsServer
6
+
7
+
8
+ # Provides conditional actions that may be commonly used.
9
+ class ConditionalActionFactory
10
+
11
+ attr_reader :action_factory
12
+
13
+ def initialize
14
+ @action_factory = ActionFactory.new
15
+ @predicate_factory = PredicateFactory.new
16
+ end
17
+
18
+
19
+ # Probably only used for testing, this ConditionalAction will unconditionally
20
+ # respond with the same object it receives.
21
+ def echo
22
+ ConditionalAction.new(PredicateFactory.new.always_true, action_factory.echo, 0)
23
+ end
24
+
25
+
26
+ def specified_a_response(qname, answer_string, times = 0)
27
+ predicate = @predicate_factory.a_request(qname)
28
+ response = MessageBuilder.specified_a_response(answer_string)
29
+ action = @action_factory.send_message(response)
30
+ ConditionalAction.new(predicate, action, times)
31
+ end
32
+
33
+
34
+ # Causes a SOA response with the specified serial to be sent upon receiving a SOA request.
35
+ # If zone is specified, the action will only be performed if the request specifies that zone.
36
+ def soa_response(serial, zone, times = 0)
37
+ predicate = @predicate_factory.all(@predicate_factory.qname(zone), @predicate_factory.soa)
38
+ response = MessageBuilder.soa_response(name: zone, serial: serial)
39
+ action = @action_factory.send_message(response)
40
+ ConditionalAction.new(predicate, action, times)
41
+ end
42
+
43
+
44
+ # Sets up the server to respond to SOA requests with the given SOA, and respond to AXFR
45
+ # requests with the specified data, wrapped in SOA responses.
46
+ def zone_load(serial_history, times = 0)
47
+ pr = @predicate_factory
48
+ predicate = pr.all(pr.qname(serial_history.zone), pr.zone_load)
49
+ action = ActionFactory.new.zone_load(serial_history)
50
+ ConditionalAction.new(predicate, action, times)
51
+ end
52
+ end
53
+ end