-
-
Save peterc/1425383 to your computer and use it in GitHub Desktop.
# 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 |
Amazing. I wonder what awesome things would happen if you kept going with this. DNS views and slave support as modules? The hex binary stuff here is pretty rad, I have never gone that low.
Sweet.
Anthony Eden talked about using DNS to do gem versioning. Wonder if this could help with that...
Just wanted to point out that there is a ruby dns library out there already which I've used to build a small dns-server for internal use. It's here: https://github.com/ioquatix/rubydns
@johnae very cool
I like the brevity of this!
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...
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 ??
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.
Pretty good!