Skip to content

Instantly share code, notes, and snippets.

@relistan
Forked from peterc/dnsd.rb
Last active December 9, 2015 23:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save relistan/4345409 to your computer and use it in GitHub Desktop.
Save relistan/4345409 to your computer and use it in GitHub Desktop.
A Ruby 1.8.7 (for MagLev) port of Peter Cooper's DNS server example.
# Simple, scrappy UDP DNS server in Ruby (with protocol annotations)
# By Peter Cooper
# Ruby 1.8.7/MagLev version by Karl Matthias
#
# MIT license
#
# * Not advised to use in your production environment! ;-)
# * Supports A and CNAME records
# * See http://www.ietf.org/rfc/rfc1035.txt for protocol guidance
# * All records get the same TTL
require 'socket'
class DNSRequest
attr_reader :server, :data, :domain
def initialize(server, data)
@server = server
@data = data
extract_domain
end
def extract_domain
@domain = ''
# Check "Opcode" of question header for valid question
if @data[2].ord & 120 == 0
# Read QNAME section of question section
# DNS header section is 12 bytes long, so data starts at offset 12
idx = 12
len = @data[idx].ord
# Strings are rendered as a byte containing length, then text.. repeat until length of 0
until len == 0 do
@domain += @data[idx + 1, len] + '.'
idx += len + 1
len = @data[idx].ord
end
end
end
def response(val)
return empty_response if domain.empty? || !val
cname = val =~ /[a-z]/
# Valid response header
response = "#{data[0,2]}\x81\x00#{data[4,2] * 2}\x00\x00\x00\x00"
# Append original question section
response += data[12..-1]
# Use pointer to refer to domain name in question section
response += "\xc0\x0c"
# Set response type accordingly
response += cname ? "\x00\x05" : "\x00\x01"
# Set response class (IN)
response += "\x00\x01"
# TTL in seconds
response += [server.ttl].pack("N")
# Calculate RDATA - we need its length in advance
if cname
rdata = val.split('.').collect { |a| a.length.chr + a }.join + "\x00"
else
# Append IP address as four 8 bit unsigned bytes
rdata = val.split('.').collect(&:to_i).pack("C*")
end
# RDATA is 4 bytes
response += [rdata.length].pack("n")
response += rdata
end
def empty_response
# Empty response header
# [id * 2, flags, NXDOMAIN, qd count * 2, an count * 2, ns count * 2, ar count * 2]
response = "#{data[0,2]}\x81\x03#{data[4,2]}\x00\x00\x00\x00\x00\x00"
# Append original question section
response += data[12..-1]
end
end
class DNSServer
attr_reader :port, :ttl
attr_accessor :records
def initialize(options = {})
options = {
:address => '127.0.0.1',
:port => 53,
:ttl => 60,
:records => {}
}.merge(options)
@address, @port, @records, @ttl = options[:address], options[:port], options[:records], options[:ttl]
end
def run
BasicSocket.do_not_reverse_lookup = true
socket = UDPSocket.new
socket.bind(@address, @port)
loop do
ready = IO.select([socket], nil, nil, 1)
if ready
message = ready[0][0].recvfrom(512) # Pull down up to 512 bytes (DNS max packet size)
data = message[0] # The data we received
_, port, _, host = message[1] # The addrinfo record for the remote host
puts "Request from: #{host}:#{port}"
r = DNSRequest.new(self, data)
puts "Responding to: #{r.domain} with: #{@records[r.domain]}"
socket.send(r.response(@records[r.domain]), 0, host, port)
end
end
end
end
records = {
'example.com.' => '1.2.3.4',
'test.host.' => '127.0.0.2',
'test.cnames.com.' => 'example.com'
}
DNSServer.new(:records => records, :ttl => 120, :port => 1053).run
@relistan
Copy link
Author

UDP DNS Server for 1.8.7/Maglev

I wanted to try some experiments with this with the Maglev storage backend, so this has been ported to Ruby 1.8.7 semantics, with my own non-blocking server loop instead of 1.9's udp_server_loop.

This is capable of serving about 250 requests per second or ~15000 RPM on a MagLev 1.0.0 VM running on a 2.53Ghz Intel Core 2 Duo (2009) MacBook Pro under OS X 10.6.8.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment