Skip to content

Commit

Permalink
Initial version.
Browse files Browse the repository at this point in the history
  • Loading branch information
Keith Bennett committed Oct 30, 2014
0 parents commit 8dee673
Show file tree
Hide file tree
Showing 42 changed files with 3,726 additions and 0 deletions.
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
*.gem
*.rbc
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp
.idea/
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source 'https://rubygems.org'

# Specify your gem's dependencies in mock_dns_server.gemspec
gemspec
24 changes: 24 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Copyright (c) 2014, Verisign, Inc.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Verisign, Inc. nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
127 changes: 127 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# MockDnsServer

A mock DNS server that can be instructed to perform actions based on
user-provided conditions, and queried for its history of inputs and outputs.
This server listens and responds on both UDP and TCP ports.

An admin interface is provided, currently in the form of Ruby methods that
can be called. In the future we will probably add an HTTP interface
to these methods. The admin methods:

* instructing the server how to respond given the characteristics of the request
* query the server for its history of inputs and outputs
* request shutdown


## Implementation


### Threads

```server.start``` launches its own thread in which it runs. This thread is terminated when ```server.close``` is called.

Admin requests (configuration, analysis, etc.) will occur on the caller's thread.


### Starting a Server

The simplest way to run a server is to call the convenience method ```Server#with_new_server```:

```ruby
Server.with_new_server(options) do |server|
# configure server with conditional actions
server.start
end
```

Options currently consist of:

Option Name|Default Value|Description
-----------|-------------|-----------|
port | 53 | port on which to listen (UDP and TCP)
timeout | 0.25 sec | timeout for IO.select call
verbose | false | Whether or not to output diagnostic messages

The code above will result in the creation of a new thread in which the server will listen
and respond indefinitely. Terminating the server is accomplished by calling server.close
in the caller's thread.


### Locking

A single mutex will be used to protect access to the rules and history objects.
All mutex handling will be done by code in this gem, so the caller does not
need to know or care that it is being done.


### Message Read Loop

The server will have the following flow of execution:

```
loop do
read a packet
attempt to parse it into a Dnsruby::Message object; if not, the message will be a string
mutex.synchronize do
action = look up rule
action.call # (perform appropriate action -- send or don't send response, etc.)
add input and output to history
end
end
```

The above loop is wrapped in an IO.select timeout loop, although currently nothing
is done at timeout time. (Closing the server is accomplished by calling server.close
on the caller's thread.)

For TCP, since the application layer requires that the transmission begin with a 2-byte
message length field (which is packed/unpacked with 'n'), this field is read first,
and the server continues reading until the entire transmission is read.

### Conditional Actions

The server can be set up with conditional actions that will control how it responds to
incoming requests. Basically, a ConditionalAction consists of a proc (usually a lambda)
that will be called to determine whether or not the action should be executed,
and another proc (also usually a lambda) defining the action that should be performed.

Only one conditional action will be performed per incoming request.
The conditions in the conditional actions are evaluated in the order with which
they were added to the server. When a condition returns true, the corresponding
action will be performed, and the message loop iteration will end.

For more information about how conditional actions are created, see the ConditionalAction,
PredicateFactory, ActionFactory, and ConditionalActionFactory classes. For how
the conditional actions are searched and performed, see the ConditionalActions class.


### History

To get a history of the server's events, call ```server.history_copy```.


## Installation

Add this line to your application's Gemfile:

gem 'mock_dns_server'

And then execute:

$ bundle

Or install it yourself as:

$ gem install mock_dns_server

## Usage

TODO: Write usage instructions here

## Contributing

1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
3 changes: 3 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## v0.1.0

* First open sourced version.
19 changes: 19 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# encoding: utf-8
require 'rubygems'
require 'bundler'
require "bundler/gem_tasks"

begin
Bundler.setup(:default, :development)
rescue Bundler::BundlerError => e
$stderr.puts e.message
$stderr.puts "Run `bundle install` to install missing gems"
exit e.status_code
end
require 'rake'

require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)

task :test => :spec
task :default => :spec
41 changes: 41 additions & 0 deletions bin/show_dig_request
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env ruby

# Stands up a Mock DNS Server to receive a dig request,
# and outputs the request to stdout using Dnsruby::Message#to_s,
# whose output differs somewhat from dig output, but has
# the same information..

require 'mock_dns_server'
require 'pry'
require 'hexdump'

MDS = MockDnsServer

def conditional_action
condition = MDS::PredicateFactory.new.always_true
action = MDS::ActionFactory.new.puts_and_echo
MDS::ConditionalAction.new(condition, action, 1)
end

def run
MDS::Server.with_new_server(host: 'localhost', port: 9999) do |server|
server.add_conditional_action(conditional_action)
server.start
puts "\nReflected message as per Dnsruby:\n\n"
output = `dig -p 9999 @localhost #{ARGV[0]} 2>&1`
puts "Reflected message as per dig:\n\n#{output}\n"
puts "Executing pry, press [Ctrl-D] to exit.\n\n"
binding.pry
raise output if $? != 0
end
end

def validate_input
if ARGV.empty?
puts "Syntax is show_dig_request \"[dig arguments]\" but without specifying host or port."
exit -1
end
end

validate_input
run
12 changes: 12 additions & 0 deletions lib/mock_dns_server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

# This is put in a lambda so as not to pollute the namespace with variables that will be useless later
->() {
file_mask = File.join(File.dirname(__FILE__), '**/*.rb')
files_to_require = Dir[file_mask]
files_to_require.each { |file| require file }
}.call


module MockDnsServer
# Your code goes here...
end
84 changes: 84 additions & 0 deletions lib/mock_dns_server/action_factory.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
require 'mock_dns_server/message_builder'

module MockDnsServer

# Creates and returns actions that will be run upon receiving incoming messages.
class ActionFactory

include MessageBuilder

# Echos the request back to the sender.
def echo
->(incoming_message, sender, context, protocol) do
context.server.send_response(sender, incoming_message, protocol)
end
end

def puts_and_echo
->(incoming_message, sender, context, protocol) do
puts "Received #{protocol.to_s.upcase} message from #{sender}:\n#{incoming_message}\n\n"
puts "Hex:\n\n"
puts "#{incoming_message.encode.hexdump}\n\n"
echo.(incoming_message, sender, context, protocol)
end
end

# Responds with the same object regardless of the request content.
def constant(constant_object)
->(_, sender, context, protocol) do
context.server.send_response(sender, constant_object, protocol)
end
end


# Sends a SOA response.
def send_soa(zone, serial, expire = nil, refresh = nil)
send_message(soa_response(
name: zone, serial: serial, expire: expire, refresh: refresh))
end


# Sends a fixed DNSRuby::Message.
def send_message(response)
->(incoming_message, sender, context, protocol) do

if [incoming_message, response].all? { |m| m.is_a?(Dnsruby::Message) }
response.header.id = incoming_message.header.id
end
if response.is_a?(Dnsruby::Message)
response.header.qr = true
end
context.server.send_response(sender, response, protocol)
end
end

# Outputs the string representation of the incoming message to stdout.
def puts_message
->(incoming_message, sender, context, protocol) do
puts incoming_message
end
end


def zone_load(serial_history)
->(incoming_message, sender, context, protocol) do

mt = MessageTransformer.new(incoming_message)
zone = mt.qname
type = mt.qtype

if serial_history.zone.downcase != zone.downcase
raise "Zones differ (history: #{serial_history.zone}, request: #{zone}"
end

if %w(AXFR IXFR).include?(type)
xfr_response = serial_history.xfr_response(incoming_message)
send_message(xfr_response).(incoming_message, sender, context, :tcp)
elsif type == 'SOA'
send_soa(zone, serial_history.high_serial).(incoming_message, sender, context, protocol)
end
end
end

end
end
42 changes: 42 additions & 0 deletions lib/mock_dns_server/conditional_action.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module MockDnsServer

class ConditionalAction

attr_accessor :condition, :action, :description, :max_uses, :use_count


# @param condition a proc/lambda that, when called with request as a param, returns
# true or false to determine whether or not the action will be executed
# @param action the code (lambda or proc) to be executed; takes incoming message,
# sender, server context, and (optionally) protocol as parameters
# and performs an action
# @param max_uses maximum number of times this action should be executed
# @return the value returned by the action, e.g. the message, or array of messages, it sent
def initialize(condition, action, max_uses)
@condition = condition
@action = action
@max_uses = max_uses
@use_count = 0
end


def increment_use_count
@use_count += 1
end

def to_s
"#{super.to_s}; condition: #{condition.to_s}, action = #{action.to_s}, max_uses = #{max_uses}"
end

def eligible_to_run?
max_not_reached = max_uses.nil? || use_count < max_uses
max_not_reached && condition.call
end

def run(request, sender, context, protocol)
# TODO: Output to history?
action.call(request, sender, context, protocol)
end

end
end
Loading

0 comments on commit 8dee673

Please sign in to comment.