Skip to content

Instantly share code, notes, and snippets.

@peterc
Created December 2, 2011 23:47
Show Gist options
  • Star 91 You must be signed in to star a gist
  • Fork 28 You must be signed in to fork a gist
  • Save peterc/1425383 to your computer and use it in GitHub Desktop.
Save peterc/1425383 to your computer and use it in GitHub Desktop.
Simple, scrappy UDP DNS server in Ruby (with protocol annotations)
# 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
@squarism
Copy link

@johnae very cool

@djbender
Copy link

I like the brevity of this!

@tarcieri
Copy link

Am I the only one who looks at this and goes hax? I'm looking for DNS server components which map to the primitives in the Resolv module. This is more like barely scraping the packets...

@ioquatix
Copy link

@tarcieri You might like to try out RubyDNS then.

@peterc Nice work :D

@grtewr
Copy link

grtewr commented Jul 22, 2019

hello
i get this error ................

ru.rb:90:in empty_response': incompatible character encodings: ASCII-8BIT and UTF-8 (Encoding::CompatibilityError) from ru.rb:52:in response'
from ru.rb:114:in block in run' from /usr/lib/ruby/2.3.0/socket.rb:976:in block in udp_server_recv'
from /usr/lib/ruby/2.3.0/socket.rb:966:in each' from /usr/lib/ruby/2.3.0/socket.rb:966:in udp_server_recv'
from /usr/lib/ruby/2.3.0/socket.rb:995:in block in udp_server_loop_on' from /usr/lib/ruby/2.3.0/socket.rb:993:in loop'
from /usr/lib/ruby/2.3.0/socket.rb:993:in udp_server_loop_on' from /usr/lib/ruby/2.3.0/socket.rb:1021:in block in udp_server_loop'
from /usr/lib/ruby/2.3.0/socket.rb:937:in udp_server_sockets' from /usr/lib/ruby/2.3.0/socket.rb:1020:in udp_server_loop'
from ru.rb:112:in run' from ru.rb:119:in

'

...........................
why ??

@peterc
Copy link
Author

peterc commented Jul 22, 2019

You need to deal with character encoding mismatches in more modern versions of Ruby. I'd need to grab the code and fix it up myself to say what to do but basically need to use the String#b method in a couple of places to keep things as binary.

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