require 'net/dns/names'
require 'net/dns/header'
require 'net/dns/question'
require 'net/dns/rr'
module Net
module DNS
#
# = Net::DNS::Packet
#
# The Net::DNS::Packet class represents an entire DNS packet,
# divided in his main section:
#
# * Header (instance of Net::DNS::Header)
# * Question (array of Net::DNS::Question objects)
# * Answer, Authority, Additional (each formed by an array of Net::DNS::RR
# objects)
#
# You can use this class whenever you need to create a DNS packet, whether
# in an user application, in a resolver instance (have a look, for instance,
# at the Net::DNS::Resolver#send method) or for a nameserver.
#
# For example:
#
# # Create a packet
# packet = Net::DNS::Packet.new("www.example.com")
# mx = Net::DNS::Packet.new("example.com", Net::DNS::MX)
#
# # Getting packet binary data, suitable for network transmission
# data = packet.data
#
# A packet object can be created from binary data too, like an
# answer packet just received from a network stream:
#
# packet = Net::DNS::Packet::parse(data)
#
# Each part of a packet can be gotten by the right accessors:
#
# header = packet.header # Instance of Net::DNS::Header class
# question = packet.question # Instance of Net::DNS::Question class
#
# # Iterate over additional RRs
# packet.additional.each do |rr|
# puts "Got an #{rr.type} record"
# end
#
# Some iterators have been written to easy the access of those RRs,
# which are often the most important. So instead of doing:
#
# packet.answer.each do |rr|
# if rr.type == Net::DNS::RR::Types::A
# # do something with +rr.address+
# end
# end
#
# we can do:
#
# packet.each_address do |ip|
# # do something with +ip+
# end
#
# Be sure you don't miss all the iterators in the class documentation.
#
# == Logging facility
#
# Logger can be set by using logger= to set the logger to any object that implements
# the necessary functions. If no logger is set then no logging is performed.
#
# Logger level will be set to Logger::Debug if $DEBUG variable is set.
#
class Packet
include Names
# Base error class.
class Error < StandardError
end
# Generic Packet Error.
class PacketError < Error
end
attr_reader :header, :question, :answer, :authority, :additional
attr_reader :answerfrom, :answersize
@@logger = nil
# Creates a new instance of Net::DNS::Packet class. Arguments are the
# canonical name of the resource, an optional type field and an optional
# class field. If the arguments are omitted, no question is added to the new packet;
# type and class default to +A+ and +IN+ if a name is given.
#
# packet = Net::DNS::Packet.new
# packet = Net::DNS::Packet.new("www.example.com")
# packet = Net::DNS::Packet.new("example.com", Net::DNS::MX)
# packet = Net::DNS::Packet.new("example.com", Net::DNS::TXT, Net::DNS::CH)
#
# This class no longer instantiate object from binary data coming from
# network streams. Please use Net::DNS::Packet.parse instead.
def initialize(name = nil, type = Net::DNS::A, cls = Net::DNS::IN)
default_qdcount = 0
@question = []
if not name.nil?
default_qdcount = 1
@question = [Net::DNS::Question.new(name, type, cls)]
end
@header = Net::DNS::Header.new(:qdCount => default_qdcount)
@answer = []
@authority = []
@additional = []
end
# Checks if the packet is a QUERY packet
def query?
@header.query?
end
def self.logger= logger
if logger.respond_to?(:warn) && logger.respond_to?(:debug) && logger.respond_to?(:info)
@@logger = logger
else
raise ArgumentError, "Invalid logger provided to #{self.class}"
end
end
def warn *args
if @@logger
@@logger.warn *args
end
end
def debug *args
if @@logger
@@logger.debug *args
end
end
def info *args
if @@logger
@@logger.info *args
end
end
# Returns the packet object in binary data, suitable
# for sending across a network stream.
#
# packet_data = packet.data
# puts "Packet is #{packet_data.size} bytes long"
#
def data
qdcount=ancount=nscount=arcount=0
data = @header.data
headerlength = data.length
@question.each do |question|
data += question.data
qdcount += 1
end
@answer.each do |rr|
data += rr.data#(data.length)
ancount += 1
end
@authority.each do |rr|
data += rr.data#(data.length)
nscount += 1
end
@additional.each do |rr|
data += rr.data#(data.length)
arcount += 1
end
@header.qdCount = qdcount
@header.anCount = ancount
@header.nsCount = nscount
@header.arCount = arcount
@header.data + data[Net::DNS::HFIXEDSZ..data.size]
end
# Same as Net::DNS::Packet#data, but implements name compression
# (see RFC1025) for a considerable save of bytes.
#
# packet = Net::DNS::Packet.new("www.example.com")
# puts "Size normal is #{packet.data.size} bytes"
# puts "Size compressed is #{packet.data_comp.size} bytes"
#
def data_comp
offset = 0
compnames = {}
qdcount=ancount=nscount=arcount=0
data = @header.data
headerlength = data.length
@question.each do |question|
str,offset,names = question.data
data += str
compnames.update(names)
qdcount += 1
end
@answer.each do |rr|
str,offset,names = rr.data(offset,compnames)
data += str
compnames.update(names)
ancount += 1
end
@authority.each do |rr|
str,offset,names = rr.data(offset,compnames)
data += str
compnames.update(names)
nscount += 1
end
@additional.each do |rr|
str,offset,names = rr.data(offset,compnames)
data += str
compnames.update(names)
arcount += 1
end
@header.qdCount = qdcount
@header.anCount = ancount
@header.nsCount = nscount
@header.arCount = arcount
@header.data + data[Net::DNS::HFIXEDSZ..data.size]
end
# Returns a string containing a human-readable representation
# of this Net::DNS::Packet instance.
def inspect
retval = ""
if @answerfrom != "0.0.0.0:0" and @answerfrom
retval += ";; Answer received from #@answerfrom (#{@answersize} bytes)\n;;\n"
end
retval += ";; HEADER SECTION\n"
retval += @header.inspect
retval += "\n"
section = (@header.opCode == "UPDATE") ? "ZONE" : "QUESTION"
retval += ";; #{section} SECTION (#{@header.qdCount} record#{@header.qdCount == 1 ? '' : 's'}):\n"
@question.each do |qr|
retval += ";; " + qr.inspect + "\n"
end
unless @answer.size == 0
retval += "\n"
section = (@header.opCode == "UPDATE") ? "PREREQUISITE" : "ANSWER"
retval += ";; #{section} SECTION (#{@header.anCount} record#{@header.anCount == 1 ? '' : 's'}):\n"
@answer.each do |rr|
retval += rr.inspect + "\n"
end
end
unless @authority.size == 0
retval += "\n"
section = (@header.opCode == "UPDATE") ? "UPDATE" : "AUTHORITY"
retval += ";; #{section} SECTION (#{@header.nsCount} record#{@header.nsCount == 1 ? '' : 's'}):\n"
@authority.each do |rr|
retval += rr.inspect + "\n"
end
end
unless @additional.size == 0
retval += "\n"
retval += ";; ADDITIONAL SECTION (#{@header.arCount} record#{@header.arCount == 1 ? '' : 's'}):\n"
@additional.each do |rr|
retval += rr.inspect + "\n"
end
end
retval
end
alias_method :to_s, :inspect
# Delegates to Net::DNS::Header#truncated?.
def truncated?
@header.truncated?
end
# Assigns a Net::DNS::Header object
# to this Net::DNS::Packet instance.
def header=(object)
if object.kind_of? Net::DNS::Header
@header = object
else
raise ArgumentError, "Argument must be a Net::DNS::Header object"
end
end
# Assigns a Net::DNS::Question object
# to this Net::DNS::Packet instance.
def question=(object)
case object
when Array
if object.all? {|x| x.kind_of? Net::DNS::Question}
@question = object
else
raise ArgumentError, "Some of the elements is not an Net::DNS::Question object"
end
when Net::DNS::Question
@question = [object]
else
raise ArgumentError, "Invalid argument, not a Question object nor an array of objects"
end
end
# Assigns one or an array of Net::DNS::RR objects
# to the answer section of this Net::DNS::Packet instance.
def answer=(object)
case object
when Array
if object.all? {|x| x.kind_of? Net::DNS::RR}
@answer = object
else
raise ArgumentError, "Some of the elements is not an Net::DNS::RR object"
end
when Net::DNS::RR
@answer = [object]
else
raise ArgumentError, "Invalid argument, not a RR object nor an array of objects"
end
end
# Assigns one or an array of Net::DNS::RR objects
# to the additional section of this Net::DNS::Packet instance.
def additional=(object)
case object
when Array
if object.all? {|x| x.kind_of? Net::DNS::RR}
@additional = object
else
raise ArgumentError, "Some of the elements is not an Net::DNS::RR object"
end
when Net::DNS::RR
@additional = [object]
else
raise ArgumentError, "Invalid argument, not a RR object nor an array of objects"
end
end
# Assigns one or an array of Net::DNS::RR objects
# to the authority section of this Net::DNS::Packet instance.
def authority=(object)
case object
when Array
if object.all? {|x| x.kind_of? Net::DNS::RR}
@authority = object
else
raise ArgumentError, "Some of the elements is not an Net::DNS::RR object"
end
when Net::DNS::RR
@authority = [object]
else
raise ArgumentError, "Invalid argument, not a RR object nor an array of objects"
end
end
# Filters the elements in the +answer+ section based on the class given
def elements(type = nil)
if type
@answer.select {|elem| elem.kind_of? type}
else
@answer
end
end
# Iterates every address in the +answer+ section
# of this Net::DNS::Packet instance.
#
# packet.each_address do |ip|
# ping ip.to_s
# end
#
# As you can see in the documentation for the Net::DNS::RR::A class,
# the address returned is an instance of IPAddr class.
def each_address(&block)
elements(Net::DNS::RR::A).map(&:address).each(&block)
end
# Iterates every nameserver in the +answer+ section
# of this Net::DNS::Packet instance.
#
# packet.each_nameserver do |ns|
# puts "Nameserver found: #{ns}"
# end
#
def each_nameserver(&block)
elements(Net::DNS::RR::NS).map(&:nsdname).each(&block)
end
# Iterates every exchange record in the +answer+ section
# of this Net::DNS::Packet instance.
#
# packet.each_mx do |pref,name|
# puts "Mail exchange #{name} has preference #{pref}"
# end
#
def each_mx(&block)
elements(Net::DNS::RR::MX).map{|elem| [elem.preference, elem.exchange]}.each(&block)
end
# Iterates every canonical name in the +answer+ section
# of this Net::DNS::Packet instance.
#
# packet.each_cname do |cname|
# puts "Canonical name: #{cname}"
# end
#
def each_cname(&block)
elements(Net::DNS::RR::CNAME).map(&:cname).each(&block)
end
# Iterates every pointer in the +answer+ section
# of this Net::DNS::Packet instance.
#
# packet.each_ptr do |ptr|
# puts "Pointer for resource: #{ptr}"
# end
#
def each_ptr(&block)
elements(Net::DNS::RR::PTR).map(&:ptrdname).each(&block)
end
# Returns the packet size in bytes.
#
# Resolver("www.google.com") do |packet|
# puts packet.size + " bytes"}
# end
# # => 484 bytes
#
def size
data.size
end
# Checks whether the query returned a NXDOMAIN error,
# meaning the queried domain name doesn't exist.
#
# %w[a.com google.com ibm.com d.com].each do |domain|
# response = Net::DNS::Resolver.new.send(domain)
# puts "#{domain} doesn't exist" if response.nxdomain?
# end
# # => a.com doesn't exist
# # => d.com doesn't exist
#
def nxdomain?
header.rCode.code == Net::DNS::Header::RCode::NAME
end
# Creates a new instance of Net::DNS::Packet class from binary data,
# taken out from a network stream. For example:
#
# # udp_socket is an UDPSocket waiting for a response
# ans = udp_socket.recvfrom(1500)
# packet = Net::DNS::Packet::parse(ans)
#
# An optional +from+ argument can be used to specify the information
# of the sender. If data is passed as is from a Socket#recvfrom call,
# the method will accept it.
#
# Be sure that your network data is clean from any UDP/TCP header,
# especially when using RAW sockets.
#
def self.parse(*args)
o = allocate
o.send(:new_from_data, *args)
o
end
private
# New packet from binary data
def new_from_data(data, from = nil)
unless from
if data.kind_of? Array
data, from = data
else
from = [0, 0, "0.0.0.0", "unknown"]
end
end
@answerfrom = from[2] + ":" + from[1].to_s
@answersize = data.size
#------------------------------------------------------------
# Header section
#------------------------------------------------------------
offset = Net::DNS::HFIXEDSZ
@header = Net::DNS::Header.parse(data[0..offset-1])
debug ";; HEADER SECTION"
debug @header.inspect
#------------------------------------------------------------
# Question section
#------------------------------------------------------------
section = @header.opCode == "UPDATE" ? "ZONE" : "QUESTION"
debug ";; #{section} SECTION (#{@header.qdCount} record#{@header.qdCount == 1 ? '': 's'})"
@question = []
@header.qdCount.times do
qobj,offset = parse_question(data,offset)
@question << qobj
debug ";; #{qobj.inspect}"
end
#------------------------------------------------------------
# Answer/prerequisite section
#------------------------------------------------------------
section = @header.opCode == "UPDATE" ? "PREREQUISITE" : "ANSWER"
debug ";; #{section} SECTION (#{@header.qdCount} record#{@header.qdCount == 1 ? '': 's'})"
@answer = []
@header.anCount.times do
begin
rrobj,offset = Net::DNS::RR.parse_packet(data,offset)
@answer << rrobj
debug rrobj.inspect
rescue NameError => e
warn "Net::DNS unsupported record type: #{e.message}"
end
end
#------------------------------------------------------------
# Authority/update section
#------------------------------------------------------------
section = @header.opCode == "UPDATE" ? "UPDATE" : "AUTHORITY"
debug ";; #{section} SECTION (#{@header.nsCount} record#{@header.nsCount == 1 ? '': 's'})"
@authority = []
@header.nsCount.times do
begin
rrobj,offset = Net::DNS::RR.parse_packet(data,offset)
@authority << rrobj
debug rrobj.inspect
rescue NameError => e
warn "Net::DNS unsupported record type: #{e.message}"
end
end
#------------------------------------------------------------
# Additional section
#------------------------------------------------------------
debug ";; ADDITIONAL SECTION (#{@header.arCount} record#{@header.arCount == 1 ? '': 's'})"
@additional = []
@header.arCount.times do
begin
rrobj,offset = Net::DNS::RR.parse_packet(data,offset)
@additional << rrobj
debug rrobj.inspect
rescue NameError => e
warn "Net::DNS unsupported record type: #{e.message}"
end
end
end
# Parse question section
def parse_question(data,offset)
size = (dn_expand(data, offset)[1] - offset) + (2 * Net::DNS::INT16SZ)
return [Net::DNS::Question.parse(data[offset, size]), offset + size]
rescue StandardError => e
raise PacketError, "Caught exception, maybe packet malformed => #{e.message}"
end
end
end
end