-
-
Save jvns/1e5838a53520e45969687e2f90199770 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'socket' | |
require 'stringio' | |
def make_question_header(query_id) | |
# id, flags, num questions, num answers, num auth, num additional | |
[query_id, 0x0100, 0x0001, 0x0000, 0x0000, 0x0000].pack('nnnnnn') | |
end | |
def encode_domain_name(domain) | |
domain | |
.split(".") | |
.map { |x| x.length.chr + x } | |
.join + "\0" | |
end | |
def make_dns_query(domain, type) | |
query_id = rand(65535) | |
header = make_question_header(query_id) | |
question = encode_domain_name(domain) + [type, 1].pack('nn') | |
header + question | |
end | |
TYPES = { | |
1 => "A", | |
2 => "NS", | |
5 => "CNAME", | |
} | |
class DNSHeader | |
attr_reader :id, :flags, :num_questions, :num_answers, :num_auth, :num_additional | |
def initialize(buf) | |
hdr = buf.read(12) | |
@id, @flags, @num_questions, @num_answers, @num_auth, @num_additional = hdr.unpack('nnnnnn') | |
end | |
end | |
class DNSRecord | |
attr_reader :name, :type, :class, :ttl, :rdlength, :rdata, :parsed_rdata | |
def initialize(buf) | |
@name = read_domain_name(buf) | |
@type, @class, @ttl, @rdlength = buf.read(10).unpack('nnNn') | |
@parsed_rdata = read_rdata(buf, @rdlength) | |
end | |
def read_rdata(buf, length) | |
@type_name = TYPES[@type] || @type | |
if @type_name == "CNAME" or @type_name == "NS" | |
read_domain_name(buf) | |
elsif @type_name == "A" | |
buf.read(length).unpack('C*').join('.') | |
else | |
buf.read(length) | |
end | |
end | |
def to_s | |
"#{@name}\t\t#{@ttl}\t#{@type_name}\t#{@parsed_rdata}" | |
end | |
end | |
def read_domain_name(buf) | |
domain = [] | |
loop do | |
len = buf.read(1).unpack('C')[0] | |
break if len == 0 | |
if len & 0xc0 == 0xc0 | |
# weird case: DNS compression! | |
second_byte = buf.read(1).unpack('C')[0] | |
offset = ((len & 0x3f) << 8) + second_byte | |
old_pos = buf.pos | |
buf.pos = offset | |
domain << read_domain_name(buf) | |
buf.pos = old_pos | |
break | |
else | |
# normal case | |
domain << buf.read(len) | |
end | |
end | |
domain.join('.') | |
end | |
class DNSQuery | |
attr_reader :domain, :type, :cls | |
def initialize(buf) | |
@domain = read_domain_name(buf) | |
@type, @cls = buf.read(4).unpack('nn') | |
end | |
end | |
class DNSResponse | |
attr_reader :header, :queries, :answers, :authorities, :additionals | |
def initialize(bytes) | |
buf = StringIO.new(bytes) | |
@header = DNSHeader.new(buf) | |
@queries = (1..@header.num_questions).map { DNSQuery.new(buf) } | |
@answers = (1..@header.num_answers).map { DNSRecord.new(buf) } | |
@authorities = (1..@header.num_auth).map { DNSRecord.new(buf) } | |
@additionals = (1..@header.num_additional).map { DNSRecord.new(buf) } | |
end | |
end | |
def main | |
# connect to google dns | |
sock = UDPSocket.new | |
sock.bind('0.0.0.0', 12345) | |
sock.connect('8.8.8.8', 53) | |
# send query | |
domain = ARGV[0] | |
sock.send(make_dns_query(domain, 1), 0) | |
# receive & parse response | |
reply, _ = sock.recvfrom(1024) | |
response = DNSResponse.new(reply) | |
response.answers.each do |record| | |
puts record | |
end | |
end | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment