public
Created — forked from peterc/dnsd.rb

Simple, scrappy UDP DNS server in Ruby (with protocol annotations)

  • Download Gist
dnsd.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
# Simple, scrappy UDP DNS server in Ruby (with protocol annotations)
# By Peter Cooper
#
# MIT license
#
# * Not advised to use in your production environment! ;-)
# * Requires Ruby 1.9
# * Supports A and CNAME records
# * See http://www.ietf.org/rfc/rfc1035.txt for protocol guidance
# * All records get the same TTL
 
require 'socket'
 
records = {
'example.com.' => '1.2.3.4',
'test.host.' => '127.0.0.2',
'test.cnames.com.' => 'example.com'
}
 
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 = {
port: 53,
ttl: 60,
records: {}
}.merge(options)
@port, @records, @ttl = options[:port], options[:records], options[:ttl]
end
def run
Socket.udp_server_loop(@port) do |data, src|
r = DNSRequest.new(self, data)
src.reply r.response(@records[r.domain])
end
end
end
 
DNSServer.new(records: records, ttl: 120).run

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.