├── .rspec ├── .gitignore ├── Gemfile ├── spec ├── fixtures │ └── resolv.conf ├── spec_helper.rb ├── unit │ └── resolver │ │ ├── dns_timeout_spec.rb │ │ ├── tcp_timeout_spec.rb │ │ └── udp_timeout_spec.rb └── resolver_spec.rb ├── .travis.yml ├── lib └── net │ ├── dns │ ├── version.rb │ ├── rr │ │ ├── spf.rb │ │ ├── srv.rb │ │ ├── null.rb │ │ ├── txt.rb │ │ ├── mr.rb │ │ ├── ns.rb │ │ ├── ptr.rb │ │ ├── cname.rb │ │ ├── mx.rb │ │ ├── soa.rb │ │ ├── aaaa.rb │ │ ├── hinfo.rb │ │ ├── a.rb │ │ ├── classes.rb │ │ └── types.rb │ ├── core_ext.rb │ ├── resolver │ │ ├── timeouts.rb │ │ └── socks.rb │ ├── names.rb │ ├── question.rb │ ├── rr.rb │ ├── packet.rb │ ├── header.rb │ └── resolver.rb │ └── dns.rb ├── demo ├── threads.rb └── check_soa.rb ├── THANKS.rdoc ├── Rakefile ├── BSDL ├── net-dns.gemspec ├── LICENSE ├── CHANGELOG.md └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --colour -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Bundler 2 | .bundle 3 | pkg/* 4 | Gemfile.lock 5 | 6 | # YARD 7 | .yardoc 8 | yardoc/ 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in whois.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /spec/fixtures/resolv.conf: -------------------------------------------------------------------------------- 1 | search corporate.thoughtworks.com 2 | nameserver 192.168.1.1 3 | nameserver 192.168.1.2 4 | nameserver 192.168.1.3 192.168.1.4 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 1.9.2 5 | - 1.9.3 6 | - 2.0.0 7 | - 2.1.0 8 | - ruby-head 9 | 10 | notifications: 11 | recipients: 12 | - mordocai@mordocai.net 13 | -------------------------------------------------------------------------------- /lib/net/dns/version.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module DNS 3 | module Version 4 | 5 | MAJOR = 0 6 | MINOR = 8 7 | PATCH = 0 8 | BUILD = nil 9 | 10 | STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join(".") 11 | end 12 | 13 | VERSION = Version::STRING 14 | 15 | end 16 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'net/dns' 3 | 4 | unless defined?(SPEC_ROOT) 5 | SPEC_ROOT = File.expand_path("../", __FILE__) 6 | end 7 | 8 | # Requires supporting ruby files with custom matchers and macros, etc, 9 | # in spec/support/ and its subdirectories. 10 | Dir[File.join(SPEC_ROOT, "support/**/*.rb")].each { |f| require f } 11 | 12 | RSpec.configure do |config| 13 | # config.mock_with :mocha 14 | # config.syntax = :expect 15 | config.order = :random 16 | end 17 | -------------------------------------------------------------------------------- /demo/threads.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' if "#{RUBY_VERSION}" < "1.9.0" 2 | require 'net/dns' 3 | 4 | a = ["ibm.com", "sun.com", "redhat.com"] 5 | 6 | threads = [] 7 | 8 | for dom in a 9 | threads << Thread.new(dom) do |domain| 10 | res = Net::DNS::Resolver.new 11 | res.query(domain, Net::DNS::NS).each_nameserver do |ns| 12 | puts "Domain #{domain} has nameserver #{ns}" 13 | end 14 | puts "" 15 | end 16 | end 17 | 18 | threads.each do |t| 19 | t.join 20 | end 21 | 22 | 23 | -------------------------------------------------------------------------------- /lib/net/dns/rr/spf.rb: -------------------------------------------------------------------------------- 1 | module Net # :nodoc: 2 | module DNS 3 | class RR 4 | 5 | #------------------------------------------------------------ 6 | # RR type SPF 7 | #------------------------------------------------------------ 8 | class SPF < TXT 9 | 10 | def spf 11 | txt 12 | end 13 | 14 | private 15 | 16 | def set_type 17 | @type = Net::DNS::RR::Types.new("SPF") 18 | end 19 | 20 | end 21 | 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /THANKS.rdoc: -------------------------------------------------------------------------------- 1 | Many thanks to these wonderful people: 2 | 3 | - Paul Barry for his excellent article on May07 issue of LinuxJournal, for the kind words and for bug reports 4 | - Dan Janowski (and his SRV RR) 5 | - Joshua Abraham 6 | - Ben April 7 | - Gene Rogers 8 | - Nicholas Veeser 9 | - Gildas Cherruel 10 | - Alex Dalitz 11 | - Cory Wright 12 | - Nicolas Pugnant 13 | - Andrea Peltrin 14 | - Giovanni Corriga 15 | - Luca Russo 16 | - Pierguido Lambri 17 | - Andrea Pampuri 18 | - _koo_ 19 | - Oyku Gencay 20 | - Eric Liedtke 21 | - Justin Mercier 22 | - Daniel Berger 23 | and all #ruby-lang people 24 | 25 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | require 'rspec' 4 | 5 | $:.unshift(File.dirname(__FILE__) + '/lib') 6 | require 'net/dns' 7 | 8 | 9 | task :default => [:spec, :gem] 10 | 11 | require 'rake' 12 | require 'rspec/core/rake_task' 13 | 14 | RSpec::Core::RakeTask.new(:spec) do |t| 15 | t.pattern = FileList['spec/**/*_spec.rb'] 16 | t.rspec_opts = ["-r ./spec/spec_helper.rb"] 17 | end 18 | 19 | require 'rubygems/package_task' 20 | Gem::PackageTask.new(Gem::Specification.load("net-dns.gemspec")).define 21 | 22 | require 'yard' 23 | YARD::Rake::YardocTask.new(:yardoc) do |y| 24 | y.options = ["--output-dir", "yardoc"] 25 | end 26 | 27 | namespace :yardoc do 28 | task :clobber do 29 | rm_r "yardoc" rescue nil 30 | end 31 | end 32 | 33 | task :clobber => "yardoc:clobber" 34 | 35 | 36 | desc "Open an irb session preloaded with this library" 37 | task :console do 38 | sh "irb -rubygems -I lib -r net/dns.rb" 39 | end 40 | -------------------------------------------------------------------------------- /spec/unit/resolver/dns_timeout_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'net/dns/resolver/timeouts' 3 | 4 | describe Net::DNS::Resolver::DnsTimeout do 5 | 6 | subject { described_class.new(10) } 7 | 8 | describe "#initialize" do 9 | it "returns an instance of DnsTimeout" do 10 | expect(subject.class).to be(described_class) 11 | end 12 | 13 | it "sets timeout" do 14 | expect(described_class.new(0).seconds).to eq(0) 15 | expect(described_class.new(10).seconds).to eq(10) 16 | end 17 | 18 | it "raises ArgumentError when timeout is invalid" do 19 | expect { described_class.new(nil) }.to raise_error(ArgumentError) 20 | expect { described_class.new("") }.to raise_error(ArgumentError) 21 | expect { described_class.new("foo") }.to raise_error(ArgumentError) 22 | expect { described_class.new(-1) }.to raise_error(ArgumentError) 23 | end 24 | end 25 | 26 | describe "#to_s" do 27 | it "returns the seconds" do 28 | expect(subject.to_s).to eq("10") 29 | end 30 | end 31 | 32 | describe "#timeout" do 33 | it "requires a block" do 34 | expect { subject.timeout }.to raise_error(LocalJumpError) 35 | end 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lib/net/dns/rr/srv.rb: -------------------------------------------------------------------------------- 1 | module Net # :nodoc: 2 | module DNS 3 | class RR 4 | 5 | #------------------------------------------------------------ 6 | # RR type SRV 7 | #------------------------------------------------------------ 8 | class SRV < RR 9 | 10 | attr_reader :priority, :weight, :port, :host 11 | 12 | private 13 | 14 | def build_pack 15 | str = "" 16 | end 17 | 18 | def subclass_new_from_binary(data,offset) 19 | off_end = offset + @rdlength 20 | @priority, @weight, @port = data.unpack("@#{offset} n n n") 21 | offset+=6 22 | 23 | @host=[] 24 | while offset < off_end 25 | len = data.unpack("@#{offset} C")[0] 26 | offset += 1 27 | str = data[offset..offset+len-1] 28 | offset += len 29 | @host << str 30 | end 31 | @host=@host.join(".") 32 | offset 33 | end 34 | 35 | private 36 | 37 | def set_type 38 | @type = Net::DNS::RR::Types.new("SRV") 39 | end 40 | 41 | end 42 | end 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/net/dns/core_ext.rb: -------------------------------------------------------------------------------- 1 | module Net # :nodoc: 2 | module DNS 3 | 4 | module HashKeys # :nodoc: 5 | 6 | # Returns an hash with all the 7 | # keys turned into downcase 8 | # 9 | # hsh = {"Test" => 1, "FooBar" => 2} 10 | # hsh.downcase_keys! 11 | # #=> {"test"=>1,"foobar"=>2} 12 | # 13 | def downcase_keys! 14 | hsh = Hash.new 15 | self.each do |key,val| 16 | hsh[key.downcase] = val 17 | end 18 | self.replace(hsh) 19 | end 20 | 21 | end 22 | 23 | module HashOperators # :nodoc: 24 | 25 | # Performs a sort of group difference 26 | # operation on hashes or arrays 27 | # 28 | # a = {:a=>1,:b=>2,:c=>3} 29 | # b = {:a=>1,:b=>2} 30 | # c = [:a,:c] 31 | # a-b #=> {:c=>3} 32 | # a-c #=> {:b=>2} 33 | # 34 | def -(other) 35 | case other 36 | when Hash 37 | delete_if { |k,v| other.has_key?(k) } 38 | when Array 39 | delete_if { |k,v| other.include?(k) } 40 | end 41 | end 42 | 43 | end 44 | 45 | end 46 | end 47 | 48 | 49 | class Hash # :nodoc: 50 | include Net::DNS::HashKeys 51 | include Net::DNS::HashOperators 52 | end -------------------------------------------------------------------------------- /lib/net/dns/rr/null.rb: -------------------------------------------------------------------------------- 1 | module Net # :nodoc: 2 | module DNS 3 | class RR 4 | 5 | #------------------------------------------------------------ 6 | # RR type NULL 7 | #------------------------------------------------------------ 8 | class NULL < RR 9 | attr_reader :null 10 | 11 | private 12 | 13 | def build_pack 14 | @null_pack = @null 15 | @rdlength = @null_pack.size 16 | end 17 | 18 | def get_data 19 | @null_pack 20 | end 21 | 22 | def get_inspect 23 | "#@null" 24 | end 25 | 26 | def subclass_new_from_hash(args) 27 | if args.has_key? :null 28 | @null = args[:null] 29 | else 30 | raise ArgumentError, ":null field is mandatory but missing" 31 | end 32 | end 33 | 34 | def subclass_new_from_string(str) 35 | @null = str.strip 36 | end 37 | 38 | def subclass_new_from_binary(data,offset) 39 | @null = data[offset..offset+@rdlength] 40 | return offset + @rdlength 41 | end 42 | 43 | private 44 | 45 | def set_type 46 | @type = Net::DNS::RR::Types.new("NULL") 47 | end 48 | 49 | end 50 | 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Christopher Carpenter, Marco Ceresa, and Simone Carletti 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /spec/unit/resolver/tcp_timeout_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'net/dns/resolver/timeouts' 3 | 4 | describe Net::DNS::Resolver::TcpTimeout do 5 | 6 | subject { described_class.new(10) } 7 | 8 | it "inherits from DnsTimeout" do 9 | expect(described_class.ancestors).to include(Net::DNS::Resolver::DnsTimeout) 10 | end 11 | 12 | describe "#initialize" do 13 | it "returns an instance of TcpTimeout" do 14 | expect(subject.class).to be(described_class) 15 | end 16 | 17 | it "sets timeout" do 18 | expect(described_class.new(0).seconds).to eq(0) 19 | expect(described_class.new(10).seconds).to eq(10) 20 | end 21 | 22 | it "raises ArgumentError when timeout is invalid" do 23 | expect { described_class.new(nil) }.to raise_error(ArgumentError) 24 | expect { described_class.new("") }.to raise_error(ArgumentError) 25 | expect { described_class.new("foo") }.to raise_error(ArgumentError) 26 | expect { described_class.new(-1) }.to raise_error(ArgumentError) 27 | end 28 | end 29 | 30 | describe "#to_s" do 31 | it "returns infinite when seconds is 0" do 32 | expect(described_class.new(0).to_s).to eq("infinite") 33 | end 34 | 35 | it "returns the seconds" do 36 | expect(subject.to_s).to eq("10") 37 | end 38 | end 39 | 40 | describe "#pretty_to_s" do 41 | it "returns a more verbose version" do 42 | expect(described_class.new(30).pretty_to_s).to eq("30 seconds") 43 | expect(described_class.new(90).pretty_to_s).to eq("1 minutes and 30 seconds") 44 | expect(described_class.new(3690).pretty_to_s).to eq("1 hours, 1 minutes and 30 seconds") 45 | end 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /spec/unit/resolver/udp_timeout_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'net/dns/resolver/timeouts' 3 | 4 | describe Net::DNS::Resolver::UdpTimeout do 5 | 6 | subject { described_class.new(10) } 7 | 8 | it "inherits from DnsTimeout" do 9 | expect(described_class.ancestors).to include(Net::DNS::Resolver::DnsTimeout) 10 | end 11 | 12 | describe "#initialize" do 13 | it "returns an instance of TcpTimeout" do 14 | expect(subject.class).to be(described_class) 15 | end 16 | 17 | it "sets timeout" do 18 | expect(described_class.new(0).seconds).to eq(0) 19 | expect(described_class.new(10).seconds).to eq(10) 20 | end 21 | 22 | it "raises ArgumentError when timeout is invalid" do 23 | expect { described_class.new(nil) }.to raise_error(ArgumentError) 24 | expect { described_class.new("") }.to raise_error(ArgumentError) 25 | expect { described_class.new("foo") }.to raise_error(ArgumentError) 26 | expect { described_class.new(-1) }.to raise_error(ArgumentError) 27 | end 28 | end 29 | 30 | describe "#to_s" do 31 | it "returns infinite when seconds is 0" do 32 | expect(described_class.new(0).to_s).to eq("not defined") 33 | end 34 | 35 | it "returns the seconds" do 36 | expect(subject.to_s).to eq("10") 37 | end 38 | end 39 | 40 | describe "#pretty_to_s" do 41 | it "returns a more verbose version" do 42 | expect(described_class.new(30).pretty_to_s).to eq("30 seconds") 43 | expect(described_class.new(90).pretty_to_s).to eq("1 minutes and 30 seconds") 44 | expect(described_class.new(3690).pretty_to_s).to eq("1 hours, 1 minutes and 30 seconds") 45 | end 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /lib/net/dns/rr/txt.rb: -------------------------------------------------------------------------------- 1 | module Net # :nodoc: 2 | module DNS 3 | class RR 4 | 5 | #------------------------------------------------------------ 6 | # RR type TXT 7 | #------------------------------------------------------------ 8 | class TXT < RR 9 | attr_reader :txt 10 | 11 | private 12 | 13 | def build_pack 14 | str = "" 15 | @txt.split(" ").each do |txt| 16 | str += [txt.length,txt].pack("C a*") 17 | end 18 | @txt_pack = str 19 | @rdlength = @txt_pack.size 20 | end 21 | 22 | def get_inspect 23 | "\"#{@txt}\"" 24 | end 25 | 26 | def get_data 27 | @txt_pack 28 | end 29 | 30 | def subclass_new_from_hash(args) 31 | if args.has_key? :txt 32 | @txt = args[:txt].strip 33 | else 34 | raise ArgumentError, ":txt field is mandatory but missing" 35 | end 36 | end 37 | 38 | def subclass_new_from_string(str) 39 | @txt = str.strip 40 | if @txt[0] == '"' and @txt[-1] == '"' 41 | @txt = @txt[1, @txt.length-2] 42 | else 43 | raise ArgumentError, "TXT RR data must be enclosed in \"quotes\"" 44 | end 45 | end 46 | 47 | def subclass_new_from_binary(data,offset) 48 | off_end = offset + @rdlength 49 | rs = [] 50 | while offset < off_end 51 | len = data.unpack("@#{offset} C")[0] 52 | offset += 1 53 | str = data[offset..offset+len-1] 54 | offset += len 55 | rs << str 56 | end 57 | @txt = rs.join(" ") 58 | return offset 59 | end 60 | 61 | private 62 | 63 | def set_type 64 | @type = Net::DNS::RR::Types.new("TXT") 65 | end 66 | 67 | end 68 | 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/net/dns/rr/mr.rb: -------------------------------------------------------------------------------- 1 | module Net # :nodoc: 2 | module DNS 3 | 4 | class RR 5 | 6 | # 7 | # = Mail Rename Record (MR) 8 | # 9 | # Class for DNS MR resource records. 10 | # 11 | class MR < RR 12 | 13 | # Gets the newname value. 14 | # 15 | # Returns a String. 16 | def newname 17 | @newname 18 | end 19 | 20 | # Gets the standardized value for this record, 21 | # represented by the value of newname. 22 | # 23 | # Returns a String. 24 | def value 25 | newname.to_s 26 | end 27 | 28 | 29 | private 30 | 31 | def subclass_new_from_hash(options) 32 | if options.has_key?(:newname) 33 | @newname = check_name(options[:newname]) 34 | else 35 | raise ArgumentError, ":newname field is mandatory" 36 | end 37 | end 38 | 39 | def subclass_new_from_string(str) 40 | @newname = check_name(str) 41 | end 42 | 43 | def subclass_new_from_binary(data, offset) 44 | @newname = dn_expand(data,offset) 45 | offset 46 | end 47 | 48 | 49 | def set_type 50 | @type = Net::DNS::RR::Types.new("MR") 51 | end 52 | 53 | def get_inspect 54 | value 55 | end 56 | 57 | 58 | def check_name(input) 59 | name = input.to_s 60 | unless name =~ /(\w\.?)+\s*$/ 61 | raise ArgumentError, "Invalid Domain Name `#{name}'" 62 | end 63 | name 64 | end 65 | 66 | def build_pack 67 | @newname_pack = pack_name(@newname) 68 | @rdlength = @newname_pack.size 69 | end 70 | 71 | def get_data 72 | @newname_pack 73 | end 74 | 75 | end 76 | 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/net/dns/rr/ns.rb: -------------------------------------------------------------------------------- 1 | module Net # :nodoc: 2 | module DNS 3 | class RR 4 | 5 | # 6 | # = Name Server Record (NS) 7 | # 8 | # Class for DNS NS resource records. 9 | # 10 | class NS < RR 11 | 12 | # Gets the name server value. 13 | # 14 | # Returns a String. 15 | def nsdname 16 | @nsdname 17 | end 18 | 19 | # Gets the standardized value for this record, 20 | # represented by the value of nsdname. 21 | # 22 | # Returns a String. 23 | def value 24 | nsdname.to_s 25 | end 26 | 27 | 28 | private 29 | 30 | def subclass_new_from_hash(options) 31 | if options.has_key?(:nsdname) 32 | @nsdname = check_name(options[:nsdname]) 33 | else 34 | raise ArgumentError, ":nsdname field is mandatory" 35 | end 36 | end 37 | 38 | def subclass_new_from_string(str) 39 | @nsdname = check_name(str) 40 | end 41 | 42 | def subclass_new_from_binary(data, offset) 43 | @nsdname, offset = dn_expand(data, offset) 44 | offset 45 | end 46 | 47 | 48 | def set_type 49 | @type = Net::DNS::RR::Types.new("NS") 50 | end 51 | 52 | def get_inspect 53 | value 54 | end 55 | 56 | 57 | def check_name(input) 58 | name = input.to_s 59 | unless name =~ /(\w\.?)+\s*$/ and name =~ /[a-zA-Z]/ 60 | raise ArgumentError, "Invalid Name Server `#{name}'" 61 | end 62 | name 63 | end 64 | 65 | def build_pack 66 | @nsdname_pack = pack_name(@nsdname) 67 | @rdlength = @nsdname_pack.size 68 | end 69 | 70 | def get_data 71 | @nsdname_pack 72 | end 73 | 74 | end 75 | 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/net/dns/rr/ptr.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module DNS 3 | class RR 4 | 5 | # 6 | # = Pointer Record (PTR) 7 | # 8 | # Class for DNS Pointer (PTR) resource records. 9 | # 10 | # Pointer records are the opposite of A and AAAA RRs 11 | # and are used in Reverse Map zone files to map 12 | # an IP address (IPv4 or IPv6) to a host name. 13 | # 14 | class PTR < RR 15 | 16 | # Gets the PTR value. 17 | # 18 | # Returns a String. 19 | def ptrdname 20 | @ptrdname.to_s 21 | end 22 | 23 | alias_method :ptr, :ptrdname 24 | 25 | # Gets the standardized value for this record, 26 | # represented by the value of ptrdname. 27 | # 28 | # Returns a String. 29 | def value 30 | ptrdname.to_s 31 | end 32 | 33 | 34 | private 35 | 36 | def build_pack 37 | @ptrdname_pack = pack_name(@ptrdname) 38 | @rdlength = @ptrdname_pack.size 39 | end 40 | 41 | def get_data 42 | @ptrdname_pack 43 | end 44 | 45 | def subclass_new_from_hash(args) 46 | if args.has_key?(:ptrdname) or args.has_key?(:ptr) 47 | @ptrdname = args[:ptrdname] 48 | else 49 | raise ArgumentError, ":ptrdname or :ptr field is mandatory" 50 | end 51 | end 52 | 53 | def subclass_new_from_string(str) 54 | @ptrdname = check_name(str) 55 | end 56 | 57 | def subclass_new_from_binary(data, offset) 58 | @ptrdname, offset = dn_expand(data, offset) 59 | offset 60 | end 61 | 62 | private 63 | 64 | def set_type 65 | @type = Net::DNS::RR::Types.new("PTR") 66 | end 67 | 68 | def get_inspect 69 | value 70 | end 71 | 72 | 73 | def check_name(input) 74 | IPAddr.new(str) 75 | rescue 76 | raise ArgumentError, "Invalid PTR Section `#{input}'" 77 | end 78 | 79 | end 80 | 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/net/dns/resolver/timeouts.rb: -------------------------------------------------------------------------------- 1 | require 'timeout' 2 | 3 | module Net # :nodoc: 4 | module DNS 5 | class Resolver 6 | 7 | class DnsTimeout 8 | 9 | attr_reader :seconds 10 | 11 | 12 | def initialize(seconds) 13 | if seconds.is_a?(Numeric) && seconds >= 0 14 | @seconds = seconds 15 | else 16 | raise ArgumentError, "Invalid value for tcp timeout" 17 | end 18 | end 19 | 20 | # Returns a string representation of the timeout corresponding 21 | # to the number of @seconds. 22 | def to_s 23 | @seconds == 0 ? @output.to_s : @seconds.to_s 24 | end 25 | 26 | def pretty_to_s 27 | transform(@seconds) 28 | end 29 | 30 | # Executes the method's block. If the block execution terminates before +sec+ 31 | # seconds has passed, it returns true. If not, it terminates the execution 32 | # and raises Timeout::Error. 33 | # If @seconds is 0 or nil, no timeout is set. 34 | def timeout(&block) 35 | raise LocalJumpError, "no block given" unless block_given? 36 | Timeout.timeout(@seconds, &block) 37 | end 38 | 39 | 40 | private 41 | 42 | def transform(secs) 43 | case secs 44 | when 0 45 | to_s 46 | when 1..59 47 | "#{secs} seconds" 48 | when 60..3559 49 | "#{secs / 60} minutes and #{secs % 60} seconds" 50 | else 51 | hours = secs / 3600 52 | secs -= (hours * 3600) 53 | "#{hours} hours, #{secs / 60} minutes and #{secs % 60} seconds" 54 | end 55 | end 56 | 57 | end 58 | 59 | class TcpTimeout < DnsTimeout 60 | def initialize(seconds) 61 | @output = "infinite" 62 | super 63 | end 64 | end 65 | 66 | class UdpTimeout < DnsTimeout 67 | def initialize(seconds) 68 | @output = "not defined" 69 | super 70 | end 71 | end 72 | 73 | end 74 | end 75 | end -------------------------------------------------------------------------------- /lib/net/dns/rr/cname.rb: -------------------------------------------------------------------------------- 1 | module Net # :nodoc: 2 | module DNS 3 | class RR 4 | 5 | # 6 | # = Canonical Name Record (CNAME) 7 | # 8 | # Class for DNS CNAME resource records. 9 | # 10 | # A CNAME record maps an alias or nickname to the real or Canonical name 11 | # which may lie outside the current zone. 12 | # Canonical means expected or real name. 13 | # 14 | class CNAME < RR 15 | 16 | # Gets the canonical name value. 17 | # 18 | # Returns a String. 19 | def cname 20 | @cname 21 | end 22 | 23 | # Gets the standardized value for this record, 24 | # represented by the value of cname. 25 | # 26 | # Returns a String. 27 | def value 28 | cname.to_s 29 | end 30 | 31 | 32 | private 33 | 34 | def subclass_new_from_hash(options) 35 | if options.has_key?(:cname) 36 | @cname = check_name(options[:cname]) 37 | else 38 | raise ArgumentError, ":cname field is mandatory" 39 | end 40 | end 41 | 42 | def subclass_new_from_string(str) 43 | @cname = check_name(str) 44 | end 45 | 46 | def subclass_new_from_binary(data, offset) 47 | @cname, offset = dn_expand(data, offset) 48 | offset 49 | end 50 | 51 | 52 | def set_type 53 | @type = Net::DNS::RR::Types.new("CNAME") 54 | end 55 | 56 | def get_inspect 57 | value 58 | end 59 | 60 | 61 | def check_name(input) 62 | name = input.to_s 63 | unless name =~ /(\w\.?)+\s*$/ and name =~ /[a-zA-Z]/ 64 | raise ArgumentError, "Invalid Canonical Name `#{name}'" 65 | end 66 | name 67 | end 68 | 69 | def build_pack 70 | @cname_pack = pack_name(@cname) 71 | @rdlength = @cname_pack.size 72 | end 73 | 74 | def get_data 75 | @cname_pack 76 | end 77 | 78 | end 79 | 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/net/dns.rb: -------------------------------------------------------------------------------- 1 | require 'net/dns/core_ext' 2 | require 'net/dns/version' 3 | require 'net/dns/resolver' 4 | 5 | module Net 6 | module DNS 7 | 8 | # Packet size in bytes 9 | PACKETSZ = 512 10 | 11 | # Size of the header 12 | HFIXEDSZ = 12 13 | 14 | # Size of the question portion (type and class) 15 | QFIXEDSZ = 4 16 | 17 | # Size of an RR portion (type,class,lenght and ttl) 18 | RRFIXEDSZ = 10 19 | 20 | # Size of an int 32 bit 21 | INT32SZ = 4 22 | 23 | # Size of a short int 24 | INT16SZ = 2 25 | 26 | 27 | module QueryTypes 28 | 29 | SIGZERO = 0 30 | A = 1 31 | NS = 2 32 | MD = 3 33 | MF = 4 34 | CNAME = 5 35 | SOA = 6 36 | MB = 7 37 | MG = 8 38 | MR = 9 39 | NULL = 10 40 | WKS = 11 41 | PTR = 12 42 | HINFO = 13 43 | MINFO = 14 44 | MX = 15 45 | TXT = 16 46 | RP = 17 47 | AFSDB = 18 48 | X25 = 19 49 | ISDN = 20 50 | RT = 21 51 | NSAP = 22 52 | NSAPPTR = 23 53 | SIG = 24 54 | KEY = 25 55 | PX = 26 56 | GPOS = 27 57 | AAAA = 28 58 | LOC = 29 59 | NXT = 30 60 | EID = 31 61 | NIMLOC = 32 62 | SRV = 33 63 | ATMA = 34 64 | NAPTR = 35 65 | KX = 36 66 | CERT = 37 67 | DNAME = 39 68 | OPT = 41 69 | DS = 43 70 | SSHFP = 44 71 | RRSIG = 46 72 | NSEC = 47 73 | DNSKEY = 48 74 | SPF = 99 75 | UINFO = 100 76 | UID = 101 77 | GID = 102 78 | UNSPEC = 103 79 | TKEY = 249 80 | TSIG = 250 81 | IXFR = 251 82 | AXFR = 252 83 | MAILB = 253 84 | MAILA = 254 85 | ANY = 255 86 | 87 | end 88 | 89 | module QueryClasses 90 | 91 | # Internet class 92 | IN = 1 93 | 94 | # Chaos class 95 | CH = 3 96 | 97 | # Hesiod class 98 | HS = 4 99 | 100 | # None class 101 | NONE = 254 102 | 103 | # Any class 104 | ANY = 255 105 | 106 | end 107 | 108 | include QueryTypes 109 | include QueryClasses 110 | 111 | end 112 | 113 | end 114 | -------------------------------------------------------------------------------- /lib/net/dns/rr/mx.rb: -------------------------------------------------------------------------------- 1 | module Net # :nodoc: 2 | module DNS 3 | class RR 4 | 5 | # 6 | # = Mail Exchange Record (MX) 7 | # 8 | # Class for DNS MX resource records. 9 | # 10 | # A MX record specifies the name and relative preference of mail servers 11 | # (mail exchangers in the DNS jargon) for the zone. 12 | # The MX RR is used by SMTP (Mail) Agents to route mail for the domain. 13 | # 14 | class MX < RR 15 | 16 | # Gets the preference value. 17 | # 18 | # Returns an Integer. 19 | def preference 20 | @preference 21 | end 22 | 23 | # Gets the exchange value. 24 | # 25 | # Returns a String. 26 | def exchange 27 | @exchange 28 | end 29 | 30 | # Gets the standardized value for this record, 31 | # represented by the value of preference and exchange. 32 | # 33 | # Returns a String. 34 | def value 35 | "#{preference} #{exchange}" 36 | end 37 | 38 | 39 | private 40 | 41 | def subclass_new_from_hash(options) 42 | if options.has_key?(:preference) && options.has_key?(:exchange) 43 | @preference = options[:preference].to_i 44 | @exchange = options[:exchange] 45 | else 46 | raise ArgumentError, ":preference and :exchange fields are mandatory" 47 | end 48 | end 49 | 50 | def subclass_new_from_string(str) 51 | @preference, @exchange = check_mx(str) 52 | end 53 | 54 | def subclass_new_from_binary(data, offset) 55 | @preference = data.unpack("@#{offset} n")[0] 56 | offset += 2 57 | @exchange, offset = dn_expand(data, offset) 58 | offset 59 | end 60 | 61 | 62 | def set_type 63 | @type = Net::DNS::RR::Types.new("MX") 64 | end 65 | 66 | def get_inspect 67 | value 68 | end 69 | 70 | 71 | def check_mx(input) 72 | str = input.to_s 73 | unless str.strip =~ /^(\d+)\s+(\S+)$/ 74 | raise ArgumentError, "Invalid MX section `#{str}'" 75 | end 76 | [$1.to_i, $2] 77 | end 78 | 79 | def build_pack 80 | @mx_pack = [@preference].pack("n") + pack_name(@exchange) 81 | @rdlength = @mx_pack.size 82 | end 83 | 84 | def get_data 85 | @mx_pack 86 | end 87 | 88 | end 89 | 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /net-dns.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "net-dns2" 5 | s.version = "0.8.4" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Marco Ceresa", "Simone Carletti", "Christopher Carpenter"] 9 | s.date = "2014-05-12" 10 | s.description = "Net::DNS is a pure Ruby DNS library, with a clean OO interface and an extensible API. The net-dns2 ruby gem is an actively maintained fork of the original net-dns." 11 | s.email = "mordocai@mordocai.net" 12 | s.files = [ 13 | ".gitignore", ".travis.yml", "CHANGELOG.md", "Gemfile", "README.md", 14 | "Rakefile", "THANKS.rdoc", "demo/check_soa.rb", "demo/threads.rb", 15 | "lib/net/dns.rb", "lib/net/dns/core_ext.rb", "lib/net/dns/header.rb", 16 | "lib/net/dns/names.rb", "lib/net/dns/packet.rb", "lib/net/dns/question.rb", 17 | "lib/net/dns/resolver.rb", "lib/net/dns/resolver/socks.rb", 18 | "lib/net/dns/resolver/timeouts.rb", "lib/net/dns/rr.rb", 19 | "lib/net/dns/rr/a.rb", "lib/net/dns/rr/aaaa.rb", "lib/net/dns/rr/classes.rb", 20 | "lib/net/dns/rr/cname.rb", "lib/net/dns/rr/hinfo.rb", "lib/net/dns/rr/mr.rb", 21 | "lib/net/dns/rr/mx.rb", "lib/net/dns/rr/ns.rb", "lib/net/dns/rr/null.rb", 22 | "lib/net/dns/rr/ptr.rb", "lib/net/dns/rr/soa.rb", "lib/net/dns/rr/spf.rb", 23 | "lib/net/dns/rr/srv.rb", "lib/net/dns/rr/txt.rb", "lib/net/dns/rr/types.rb", 24 | "lib/net/dns/version.rb", "net-dns.gemspec" 25 | ] 26 | s.homepage = "http://github.com/mordocai/net-dns" 27 | s.require_paths = ["lib"] 28 | s.summary = "Pure Ruby DNS library, fork with fixes." 29 | # s.test_files = [ 30 | # "spec/fixtures/resolv.conf", "spec/resolver_spec.rb", "spec/spec_helper.rb", 31 | # "spec/unit/resolver/dns_timeout_spec.rb", "spec/unit/tcp_timeout_spec.rb", 32 | # "spec/unit/udp_timeout_spec.rb" 33 | # ] 34 | s.required_ruby_version = Gem::Requirement.new(">= 1.9.2") 35 | s.license = 'Ruby' 36 | 37 | if s.respond_to? :specification_version then 38 | s.specification_version = 4 39 | 40 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 41 | s.add_development_dependency(%q, ["~> 10.0"]) 42 | s.add_development_dependency(%q, [">= 0"]) 43 | s.add_development_dependency(%q, [">= 0"]) 44 | else 45 | s.add_dependency(%q, ["~> 10.0"]) 46 | s.add_dependency(%q, [">= 0"]) 47 | s.add_dependency(%q, [">= 0"]) 48 | end 49 | else 50 | s.add_dependency(%q, ["~> 10.0"]) 51 | s.add_dependency(%q, [">= 0"]) 52 | s.add_dependency(%q, [">= 0"]) 53 | end 54 | 55 | s.add_runtime_dependency "packetfu" 56 | end 57 | -------------------------------------------------------------------------------- /lib/net/dns/rr/soa.rb: -------------------------------------------------------------------------------- 1 | module Net # :nodoc: 2 | module DNS 3 | class RR 4 | 5 | #------------------------------------------------------------ 6 | # RR type SOA 7 | #------------------------------------------------------------ 8 | class SOA < RR 9 | attr_reader :mname, :rname, :serial, :refresh, :retry, :expire, :minimum 10 | 11 | private 12 | 13 | def build_pack 14 | @soa_pack = pack_name(@mname) 15 | @soa_pack += pack_name(@rname) 16 | @soa_pack += [@serial,@refresh,@retry,@expire,@minimum].pack("N5") 17 | @rdlength = @soa_pack.size 18 | end 19 | 20 | def get_data 21 | @soa_pack 22 | end 23 | 24 | def get_inspect 25 | "#@mname #@rname #@serial #@refresh #@retry #@expire #@minimum" 26 | end 27 | 28 | def subclass_new_from_hash(args) 29 | if args.has_key? :rdata 30 | subclass_new_from_string(args[:rdata]) 31 | else 32 | [:mname,:rname,:serial,:refresh,:retry,:expire,:minimum].each do |key| 33 | raise ArgumentError, "Missing field :#{key}" unless args.has_key? key 34 | end 35 | @mname = args[:mname] if valid? args[:mname] 36 | @rname = args[:rname] if rname? args[:rname] 37 | @serial = args[:serial] if number? args[:serial] 38 | @refresh = args[:refresh] if number? args[:refresh] 39 | @retry = args[:retry] if number? args[:retry] 40 | @expire = args[:expire] if number? args[:expire] 41 | @minimum = args[:minimum] if number? args[:minimum] 42 | end 43 | end 44 | 45 | def number?(num) 46 | if num.kind_of? Integer and num > 0 47 | true 48 | else 49 | false 50 | end 51 | end 52 | 53 | def rname?(name) 54 | mailbox, domain = name.split(/(?String or an IPAddr object. 21 | # 22 | # Examples 23 | # 24 | # a.address = "192.168.0.1" 25 | # a.address = IPAddr.new("10.0.0.1") 26 | # 27 | # Returns the new allocated instance of IPAddr. 28 | def address=(string_or_ipaddr) 29 | @address = check_address(string_or_ipaddr) 30 | build_pack 31 | @address 32 | end 33 | 34 | # Gets the standardized value for this record, 35 | # represented by the value of address. 36 | # 37 | # Returns a String. 38 | def value 39 | address.to_s 40 | end 41 | 42 | 43 | private 44 | 45 | def subclass_new_from_hash(options) 46 | if options.has_key?(:address) 47 | @address = check_address(options[:address]) 48 | else 49 | raise ArgumentError, ":address field is mandatory" 50 | end 51 | end 52 | 53 | def subclass_new_from_string(str) 54 | @address = check_address(str) 55 | end 56 | 57 | def subclass_new_from_binary(data, offset) 58 | tokens = data.unpack("@#{offset} n8") 59 | @address = IPAddr.new(sprintf("%x:%x:%x:%x:%x:%x:%x:%x", *tokens)) 60 | offset + 16 61 | end 62 | 63 | 64 | def set_type 65 | @type = Net::DNS::RR::Types.new("AAAA") 66 | end 67 | 68 | def get_inspect 69 | value 70 | end 71 | 72 | 73 | def check_address(input) 74 | address = case input 75 | when IPAddr 76 | input 77 | when String 78 | IPAddr.new(input) 79 | else 80 | raise ArgumentError, "Invalid IP address `#{input}'" 81 | end 82 | 83 | if !address.ipv6? 84 | raise(ArgumentError, "Must specify an IPv6 address") 85 | end 86 | 87 | address 88 | end 89 | 90 | def build_pack 91 | @address_pack = @address.hton 92 | @rdlength = @address_pack.size 93 | end 94 | 95 | def get_data 96 | @address_pack 97 | end 98 | 99 | end 100 | 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Original license statement by https://github.com/bluemonk/net-dns (pre-fork) stated that the code was 2 | "distributed under the same license Ruby is". 3 | 4 | I have interpreted that as meaning it is under the "Ruby License" which, at the time of this writing, 5 | is reproduced below (taken from https://www.ruby-lang.org/en/about/license.txt). 6 | 7 | Net::DNS is copyrighted free software by Christopher Carpenter (current maintainer), 8 | Marco Ceresa, and Simone Carletti(weppos@weppos.net). 9 | 10 | You can redistribute it and/or modify it under either the terms of the 11 | 2-clause BSDL (see the file BSDL), or the conditions below: 12 | 13 | 1. You may make and give away verbatim copies of the source form of the 14 | software without restriction, provided that you duplicate all of the 15 | original copyright notices and associated disclaimers. 16 | 17 | 2. You may modify your copy of the software in any way, provided that 18 | you do at least ONE of the following: 19 | 20 | a) place your modifications in the Public Domain or otherwise 21 | make them Freely Available, such as by posting said 22 | modifications to Usenet or an equivalent medium, or by allowing 23 | the author to include your modifications in the software. 24 | 25 | b) use the modified software only within your corporation or 26 | organization. 27 | 28 | c) give non-standard binaries non-standard names, with 29 | instructions on where to get the original software distribution. 30 | 31 | d) make other distribution arrangements with the author. 32 | 33 | 3. You may distribute the software in object code or binary form, 34 | provided that you do at least ONE of the following: 35 | 36 | a) distribute the binaries and library files of the software, 37 | together with instructions (in the manual page or equivalent) 38 | on where to get the original distribution. 39 | 40 | b) accompany the distribution with the machine-readable source of 41 | the software. 42 | 43 | c) give non-standard binaries non-standard names, with 44 | instructions on where to get the original software distribution. 45 | 46 | d) make other distribution arrangements with the author. 47 | 48 | 4. You may modify and include the part of the software into any other 49 | software (possibly commercial). But some files in the distribution 50 | are not written by the author, so that they are not under these terms. 51 | 52 | For the list of those files and their copying conditions, see the 53 | file LEGAL. 54 | 55 | 5. The scripts and library files supplied as input to or produced as 56 | output from the software do not automatically fall under the 57 | copyright of the software, but belong to whomever generated them, 58 | and may be sold commercially, and may be aggregated with this 59 | software. 60 | 61 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 62 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 63 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 64 | PURPOSE. 65 | -------------------------------------------------------------------------------- /lib/net/dns/rr/hinfo.rb: -------------------------------------------------------------------------------- 1 | module Net # :nodoc: 2 | module DNS 3 | class RR 4 | 5 | # 6 | # = System Information Record (HINFO) 7 | # 8 | # Class for DNS HINFO resource records. 9 | # 10 | # Allows definition of the Hardware type and Operating System (OS) in use at a host. 11 | # For security reasons these records are rarely used on public servers. 12 | # If a space exists in the field it must be enclosed in quotes. 13 | # Single space between CPU and OS parameters. 14 | # 15 | class HINFO < RR 16 | 17 | # Gets the CPU value. 18 | # 19 | # Returns a String. 20 | def cpu 21 | @cpu 22 | end 23 | 24 | # Gets the OS value. 25 | # 26 | # Returns a String. 27 | def os 28 | @os 29 | end 30 | 31 | # Gets the standardized value for this record, 32 | # represented by the value of cpu and os. 33 | # 34 | # Returns a String. 35 | def value 36 | %Q{"#{cpu}" "#{os}"} 37 | end 38 | 39 | 40 | # Gets a list of all the attributes for this record. 41 | # 42 | # Returns an Array of values. 43 | def to_a 44 | [nil, nil, cls.to_s, type.to_s, value] 45 | end 46 | 47 | 48 | private 49 | 50 | def subclass_new_from_hash(options) 51 | if options.has_key?(:cpu) && options.has_key?(:os) 52 | @cpu = options[:cpu] 53 | @os = options[:os] 54 | else 55 | raise ArgumentError, ":cpu and :os fields are mandatory" 56 | end 57 | end 58 | 59 | def subclass_new_from_string(str) 60 | @cpu, @os = check_hinfo(str) 61 | end 62 | 63 | def subclass_new_from_binary(data, offset) 64 | len = data.unpack("@#{offset} C").first 65 | offset += 1 66 | @cpu = data[offset..(offset + len)] 67 | offset += len 68 | 69 | len = data.unpack("@#{offset} C").first 70 | offset += 1 71 | @os = data[offset..(offset + len)] 72 | offset += len 73 | end 74 | 75 | 76 | def set_type 77 | @type = Net::DNS::RR::Types.new("HINFO") 78 | end 79 | 80 | def get_inspect 81 | value 82 | end 83 | 84 | 85 | def check_hinfo(input) 86 | if input.to_s.strip =~ /^(?:["']?(.*?)["']?)\s+(?:["']?(.*?)["']?)$/ 87 | [$1, $2] 88 | else 89 | raise ArgumentError, "Invalid HINFO Section `#{input}'" 90 | end 91 | end 92 | 93 | def build_pack 94 | @hinfo_pack = "" 95 | @hinfo_pack += [cpu.size].pack("C") + cpu 96 | @hinfo_pack += [os.size ].pack("C") + os 97 | @rdlength = @hinfo_pack.size 98 | end 99 | 100 | def get_data 101 | @hinfo_pack 102 | end 103 | 104 | end 105 | 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/resolver_spec.rb: -------------------------------------------------------------------------------- 1 | require 'net/dns' 2 | 3 | describe Net::DNS::Resolver do 4 | before :each do 5 | @resolv = Net::DNS::Resolver.new 6 | end 7 | 8 | describe "packet_size=" do 9 | it "can set packet_size as integer" do 10 | @resolv.packet_size = 2048 11 | @resolv.packet_size.should eq 2048 12 | end 13 | 14 | it "can set packet_size as string" do 15 | @resolv.packet_size = "2048" 16 | @resolv.packet_size.should eq 2048 17 | end 18 | 19 | it "can set packet_size as arbitrary class" do 20 | class Blah 21 | def to_i 22 | 2048 23 | end 24 | end 25 | x = Blah.new 26 | @resolv.packet_size = x 27 | @resolv.packet_size.should eq 2048 28 | end 29 | end 30 | 31 | describe "nameservers=" do 32 | let(:dns_ipaddr) {IPAddr.new("8.8.8.8")} 33 | let(:dns_string) {"8.8.8.8"} 34 | 35 | it "can set nameserver as IPAddr" do 36 | @resolv.nameserver = dns_ipaddr 37 | @resolv.nameserver.should eq [dns_string] 38 | end 39 | 40 | it "can set nameserver as String ip" do 41 | @resolv.nameserver = "8.8.8.8" 42 | @resolv.nameserver.should eq [dns_string] 43 | end 44 | 45 | it "can set nameserver as String domain" do 46 | @resolv.nameserver = "google-public-dns-a.google.com" 47 | @resolv.nameserver.should eq [dns_string] 48 | end 49 | 50 | it "can set nameserver as Array" do 51 | @resolv.nameservers = [ 52 | "google-public-dns-a.google.com", 53 | dns_ipaddr, 54 | "8.8.8.8" 55 | ] 56 | 57 | @resolv.nameserver.should eq [ 58 | dns_string, 59 | dns_string, 60 | dns_string 61 | ] 62 | end 63 | 64 | it "can set nameserver as Arbitrary to_a object" do 65 | class Blah 66 | def to_a 67 | [ 68 | "google-public-dns-a.google.com", 69 | "8.8.8.8" 70 | ] 71 | end 72 | end 73 | @resolv.nameservers = Blah.new 74 | @resolv.nameserver.should eq [ 75 | dns_string, 76 | dns_string 77 | ] 78 | end 79 | 80 | it "can set nameserver as Arbitrary map object" do 81 | class Blah 82 | def initialize 83 | @array = [ 84 | "google-public-dns-a.google.com", 85 | "8.8.8.8" 86 | ] 87 | end 88 | 89 | def map &block 90 | ret = [] 91 | @array.each do |item| 92 | ret << block.call(item) 93 | end 94 | ret 95 | end 96 | end 97 | 98 | @resolv.nameservers = Blah.new 99 | @resolv.nameserver.should eq [ 100 | dns_string, 101 | dns_string 102 | ] 103 | end 104 | end 105 | 106 | it "returns TXT records properly" do 107 | result = Net::DNS::Resolver.new.query "google.com", Net::DNS::TXT 108 | result.answer.first.should be_a Net::DNS::RR::TXT 109 | end 110 | 111 | context "packet_size set" do 112 | before :all do 113 | @resolv = Net::DNS::Resolver.new({packet_size: 2048}) 114 | end 115 | 116 | it "can query" do 117 | @resolv.query "google.com" 118 | end 119 | end 120 | 121 | context "nameservers set" do 122 | before :all do 123 | @resolv = Net::DNS::Resolver.new({nameservers: ["8.8.8.8", "8.8.4.4"]}) 124 | end 125 | 126 | it "can query" do 127 | @resolv.query "google.com" 128 | end 129 | 130 | #TODO: Add tests that ensure that the correct nameservers are used for lookup 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/net/dns/rr/a.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module DNS 3 | class RR 4 | 5 | # 6 | # = IPv4 Address Record (A) 7 | # 8 | # Class for DNS IPv4 Address (A) resource records. 9 | # 10 | # The resource data is an IPv4 (i.e. 32 bit long) address, 11 | # hold in the instance variable +address+. 12 | # 13 | # a = Net::DNS::RR::A.new("localhost.movie.edu. 360 IN A 127.0.0.1") 14 | # 15 | # a = Net::DNS::RR::A.new(:name => "localhost.movie.edu.", 16 | # :ttl => 360, 17 | # :cls => Net::DNS::IN, 18 | # :type => Net::DNS::A, 19 | # :address => "127.0.0.1" ) 20 | # 21 | # When computing binary data to transmit the RR, the RDATA section is an 22 | # Internet address expressed as four decimal numbers separated by dots 23 | # without any embedded space (e.g. "10.2.0.52" or "192.0.5.6"). 24 | # 25 | class A < RR 26 | 27 | # Gets the current IPv4 address for this record. 28 | # 29 | # Returns an instance of IPAddr. 30 | def address 31 | @address 32 | end 33 | 34 | # Assigns a new IPv4 address to this record, which can be in the 35 | # form of a String or an IPAddr object. 36 | # 37 | # Examples 38 | # 39 | # a.address = "192.168.0.1" 40 | # a.address = IPAddr.new("10.0.0.1") 41 | # 42 | # Returns the new allocated instance of IPAddr. 43 | def address=(string_or_ipaddr) 44 | @address = check_address(string_or_ipaddr) 45 | build_pack 46 | @address 47 | end 48 | 49 | # Gets the standardized value for this record, 50 | # represented by the value of address. 51 | # 52 | # Returns a String. 53 | def value 54 | address.to_s 55 | end 56 | 57 | 58 | private 59 | 60 | def subclass_new_from_hash(options) 61 | if options.has_key?(:address) 62 | @address = check_address(options[:address]) 63 | elsif options.has_key?(:rdata) 64 | @address = check_address(options[:rdata]) 65 | else 66 | raise ArgumentError, ":address or :rdata field is mandatory" 67 | end 68 | end 69 | 70 | def subclass_new_from_string(str) 71 | @address = check_address(str) 72 | end 73 | 74 | def subclass_new_from_binary(data, offset) 75 | a, b, c, d = data.unpack("@#{offset} CCCC") 76 | @address = IPAddr.new("#{a}.#{b}.#{c}.#{d}") 77 | offset + 4 78 | end 79 | 80 | 81 | def set_type 82 | @type = Net::DNS::RR::Types.new("A") 83 | end 84 | 85 | def get_inspect 86 | value 87 | end 88 | 89 | 90 | def check_address(input) 91 | address = case input 92 | when IPAddr 93 | input 94 | when Integer # Address in numeric form 95 | IPAddr.new(input, Socket::AF_INET) # We know we are IPv4 96 | when String 97 | IPAddr.new(input) 98 | else 99 | raise ArgumentError, "Invalid IP address `#{input}'" 100 | end 101 | 102 | if !address.ipv4? 103 | raise(ArgumentError, "Must specify an IPv4 address") 104 | end 105 | 106 | address 107 | end 108 | 109 | def build_pack 110 | @address_pack = @address.hton 111 | @rdlength = @address_pack.size 112 | end 113 | 114 | def get_data 115 | @address_pack 116 | end 117 | 118 | end 119 | 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /demo/check_soa.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' if "#{RUBY_VERSION}" < "1.9.0" 4 | require 'net/dns' 5 | 6 | 7 | #------------------------------------------------------------------------------ 8 | # Get the domain from the command line. 9 | #------------------------------------------------------------------------------ 10 | 11 | raise ArgumentError, "Usage: check_soa.rb domain\n" unless ARGV.size == 1 12 | 13 | domain = ARGV[0] 14 | 15 | #------------------------------------------------------------------------------ 16 | # Find all the nameservers for the domain. 17 | #------------------------------------------------------------------------------ 18 | 19 | res = Net::DNS::Resolver.new(:defname => false, :retry => 2) 20 | 21 | ns_req = res.query(domain, Net::DNS::NS) 22 | unless ns_req and ns_req.header.anCount > 0 23 | raise ArgumentError, "No nameservers found for domain: #{res.errorstring}" 24 | end 25 | 26 | 27 | # Send out non-recursive queries 28 | res.recurse = false 29 | # Do not buffer standard out 30 | #| = 1; 31 | 32 | 33 | #------------------------------------------------------------------------------ 34 | # Check the SOA record on each nameserver. 35 | #------------------------------------------------------------------------------ 36 | 37 | ns_req.each_nameserver do |ns| 38 | 39 | #---------------------------------------------------------------------- 40 | # Set the resolver to query this nameserver. 41 | #---------------------------------------------------------------------- 42 | 43 | # In order to lookup the IP(s) of the nameserver, we need a Resolver 44 | # object that is set to our local, recursive nameserver. So we create 45 | # a new object just to do that. 46 | 47 | local_res = Net::DNS::Resolver.new 48 | 49 | a_req = local_res.query(ns, Net::DNS::A) 50 | 51 | 52 | unless a_req 53 | puts "Can not find address for ns: " + res.errorstring + "\n" 54 | next 55 | end 56 | 57 | 58 | a_req.each_address do |ip| 59 | 60 | #---------------------------------------------------------------------- 61 | # Ask this IP. 62 | #---------------------------------------------------------------------- 63 | res.nameservers=ip 64 | 65 | print "#{ns} (#{ip}): " 66 | 67 | #---------------------------------------------------------------------- 68 | # Get the SOA record. 69 | #---------------------------------------------------------------------- 70 | 71 | soa_req = res.send(domain, Net::DNS::SOA, Net::DNS::IN) 72 | 73 | if soa_req == nil 74 | puts res.errorstring, "\n" 75 | next 76 | end 77 | 78 | #---------------------------------------------------------------------- 79 | # Is this nameserver authoritative for the domain? 80 | #---------------------------------------------------------------------- 81 | 82 | unless soa_req.header.auth? 83 | print "isn't authoritative for domain\n" 84 | next 85 | end 86 | 87 | #---------------------------------------------------------------------- 88 | # We should have received exactly one answer. 89 | #---------------------------------------------------------------------- 90 | 91 | unless soa_req.header.anCount == 1 92 | print "expected 1 answer, got " + soa_req.header.anCount.to_s + "\n" 93 | next 94 | end 95 | 96 | #---------------------------------------------------------------------- 97 | # Did we receive an SOA record? 98 | #---------------------------------------------------------------------- 99 | 100 | unless soa_req.answer[0].class == Net::DNS::RR::SOA 101 | print "expected SOA, got " + Net::DNS::RR::RRTypes.to_str(soa_req.answer[0].type) + "\n" 102 | next 103 | end 104 | 105 | #---------------------------------------------------------------------- 106 | # Print the serial number. 107 | #---------------------------------------------------------------------- 108 | 109 | print "has serial number " + soa_req.answer[0].serial.to_s + "\n" 110 | 111 | end 112 | end 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /lib/net/dns/names.rb: -------------------------------------------------------------------------------- 1 | module Net # :nodoc: 2 | module DNS 3 | 4 | module Names 5 | 6 | # Base error class. 7 | class Error < StandardError 8 | end 9 | 10 | # Generic Names Error. 11 | class ExpandError < Error 12 | end 13 | 14 | 15 | INT16SZ = 2 16 | 17 | # Expand a compressed name in a DNS Packet object. Please 18 | # see RFC1035 for an explanation of how the compression 19 | # in DNS packets works, how may it be useful and how should 20 | # be handled. 21 | # 22 | # This method accept two parameters: a raw packet data and an 23 | # offset, which indicates the point in the packet in which the 24 | # parsing has arrived. 25 | # 26 | def dn_expand(packet,offset) 27 | name = "" 28 | packetlen = packet.size 29 | while true 30 | raise ExpandError, "Offset is greater than packet length!" if packetlen < (offset+1) 31 | len = packet.unpack("@#{offset} C")[0] 32 | 33 | if len == 0 34 | offset += 1 35 | break 36 | elsif (len & 0xC0) == 0xC0 37 | raise ExpandError, "Packet ended before offset expand" if packetlen < (offset+INT16SZ) 38 | ptr = packet.unpack("@#{offset} n")[0] 39 | ptr &= 0x3FFF 40 | name2 = dn_expand(packet,ptr)[0] 41 | raise ExpandError, "Packet is malformed!" if name2 == nil 42 | name += name2 43 | offset += INT16SZ 44 | break 45 | else 46 | offset += 1 47 | raise ExpandError, "No expansion found" if packetlen < (offset+len) 48 | elem = packet[offset..offset+len-1] 49 | name += "#{elem}." 50 | offset += len 51 | end 52 | end 53 | [name, offset] # name.chomp(".") if trailing dot has to be omitted 54 | end 55 | 56 | def pack_name(name) 57 | if name.size > 255 58 | raise ArgumentError, "Name may not exceed 255 chars" 59 | end 60 | arr = name.split(".") 61 | str = "" 62 | arr.each do |elem| 63 | if elem.size > 63 64 | raise ArgumentError, "Label may not exceed 63 chars" 65 | end 66 | str += [elem.size,elem].pack("Ca*") 67 | end 68 | str += [0].pack("C") 69 | str 70 | end 71 | 72 | def names_array(name) 73 | arr = name.split(".") 74 | ar = [] 75 | string = "" 76 | arr.size.times do |i| 77 | x = i+1 78 | elem = arr[-x] 79 | len = elem.size 80 | string = ((string.reverse)+([len,elem].pack("Ca*")).reverse).reverse 81 | ar.unshift(string) 82 | end 83 | return ar 84 | end 85 | 86 | def dn_comp(name,offset,compnames) 87 | names = {} 88 | ptr = 0 89 | str = "" 90 | arr = names_array(name) 91 | arr.each do |entry| 92 | if compnames.has_key?(entry) 93 | ptr = 0xC000 | compnames[entry] 94 | str += [ptr].pack("n") 95 | offset += INT16SZ 96 | break 97 | else 98 | len = entry.unpack("C")[0] 99 | elem = entry[1..len] 100 | str += [len,elem].pack("Ca*") 101 | names.update({"#{entry}" => offset}) 102 | offset += len 103 | end 104 | end 105 | return str,offset,names 106 | end 107 | 108 | def valid?(name) 109 | return false if name.length < 1 or name.length > 255 110 | 111 | return true if name == '.' # the root domain is the only valid domain that begins with a dot 112 | 113 | parts = name.split('.', -1) 114 | parts.delete_at(parts.length-1) if parts.last.empty? # the domain may end with a dot 115 | 116 | parts.each do |part| 117 | if not part =~ /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/i or part.length < 1 or part.length > 63 118 | return false 119 | end 120 | end 121 | 122 | return true 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/net/dns/rr/classes.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module DNS 3 | 4 | class RR 5 | 6 | # 7 | # = Net::DNS::Classes 8 | # 9 | # This is an auxiliary class to handle Net::DNS::RR 10 | # class field in a DNS packet. 11 | # 12 | class Classes 13 | 14 | # Hash with the values of each RR class stored with the 15 | # respective id number. 16 | CLASSES = { 17 | 'IN' => 1, # RFC 1035 18 | 'CH' => 3, # RFC 1035 19 | 'HS' => 4, # RFC 1035 20 | 'NONE' => 254, # RFC 2136 21 | 'ANY' => 255, # RFC 1035 22 | } 23 | 24 | # The default value when class is nil in Resource Records 25 | @@default = CLASSES["IN"] 26 | 27 | 28 | # Creates a new object representing an RR class. Performs some 29 | # checks on the argument validity too. Il +cls+ is +nil+, the 30 | # default value is +ANY+ or the one set with Classes.default= 31 | def initialize(cls) 32 | case cls 33 | when String 34 | initialize_from_str(cls) 35 | when Fixnum 36 | initialize_from_num(cls) 37 | when nil 38 | initialize_from_num(@@default) 39 | end 40 | 41 | if @str.nil? || @num.nil? 42 | raise ArgumentError, "Unable to create a `Classes' from `#{cls}'" 43 | end 44 | end 45 | 46 | # Returns the class in number format 47 | # (default for normal use) 48 | # 49 | # FIXME: inspect must return a String. 50 | # 51 | def inspect 52 | @num 53 | end 54 | 55 | # Returns the class in string format, 56 | # ex. "IN" or "CH" or such a string. 57 | def to_s 58 | @str.to_s 59 | end 60 | 61 | # Returns the class in numeric format, 62 | # usable by the pack methods for data transfers. 63 | def to_i 64 | @num.to_i 65 | end 66 | 67 | 68 | 69 | # Be able to control the default class to assign when 70 | # cls argument is +nil+. Default to +IN+ 71 | def self.default=(str) 72 | if CLASSES[str] 73 | @@default = CLASSES[str] 74 | else 75 | raise ArgumentError, "Unknown class `#{str}'" 76 | end 77 | end 78 | 79 | # Returns whether cls is a valid RR class. 80 | # 81 | # Net::DNS::RR::Classes.valid?("IN") 82 | # # => true 83 | # Net::DNS::RR::Classes.valid?(1) 84 | # # => true 85 | # Net::DNS::RR::Classes.valid?("Q") 86 | # # => false 87 | # Net::DNS::RR::Classes.valid?(256) 88 | # # => false 89 | # Net::DNS::RR::Classes.valid?(Hash.new) 90 | # # => ArgumentError 91 | # 92 | # FIXME: valid? should never raise. 93 | # 94 | # ==== Raises 95 | # ArgumentError:: if cls isn't either a String or a Fixnum 96 | # 97 | def self.valid?(cls) 98 | case cls 99 | when String 100 | CLASSES.has_key?(cls) 101 | when Fixnum 102 | CLASSES.invert.has_key?(cls) 103 | else 104 | raise ArgumentError, "Wrong cls class: #{cls.class}" 105 | end 106 | end 107 | 108 | # Gives in output the keys from the +Classes+ hash 109 | # in a format suited for regexps 110 | def self.regexp 111 | CLASSES.keys.sort.join("|") 112 | end 113 | 114 | 115 | private 116 | 117 | # Initialize a new instance from a Class name. 118 | def initialize_from_str(str) 119 | key = str.to_s.upcase 120 | @num, @str = CLASSES[key], key 121 | end 122 | 123 | # Initialize a new instance from the Class value. 124 | def initialize_from_num(num) 125 | key = num.to_i 126 | @num, @str = key, CLASSES.invert[key] 127 | end 128 | 129 | end 130 | 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/net/dns/resolver/socks.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'ipaddr' 3 | 4 | class RawSocket # :nodoc: 5 | 6 | @@id_arr = [] 7 | 8 | def initialize(src_addr,dest_addr) 9 | 10 | # Define socket 11 | begin 12 | @socket = Socket.new PF_INET, SOCK_RAW, IPPROTO_RAW 13 | rescue SystemCallError => e 14 | raise SystemCallError, "You must be root to use raw sockets! #{e}" 15 | end 16 | 17 | @socket.setsockopt IPPROTO_IP, IP_HDRINCL, 1 18 | 19 | # Checks addresses 20 | @src_addr = check_addr src_addr 21 | @dest_addr = check_addr dest_addr 22 | 23 | # Source and destination port are zero 24 | @src_port = 0 25 | @dest_port = 0 26 | 27 | # Set correct protocol version in the header 28 | @version = @dest_addr.ipv4? ? "0100" : "0110" 29 | 30 | # Total lenght: must be overridden by subclasses 31 | @tot_lenght = 20 32 | 33 | # Protocol: must be overridden by subclasses 34 | @protocol = 1 # ICMP by default 35 | 36 | # Generate a new id 37 | # @id = genID 38 | @id = 1234 39 | 40 | # Generate peer sockaddr 41 | @to = Socket.pack_sockaddr_in @dest_port, @dest_addr.to_s 42 | end 43 | 44 | def send(payload = '') 45 | packet = make_ip_header([[ @version+'0101', 'B8' ], # version, hlen 46 | [ 0, 'C' ], # tos 47 | [ @tot_lenght + payload.size, 'n' ], # total len 48 | [ @id, 'n' ], # id 49 | [ 0, 'n' ], # flags, offset 50 | [ 64, 'C' ], # ttl 51 | [ @protocol, 'C' ], # protocol 52 | [ 0, 'n' ], # checksum 53 | [ @src_addr.to_i, 'N' ], # source 54 | [ @dest_addr.to_i, 'N' ], # destination 55 | ]) 56 | packet << make_transport_header(payload.size) 57 | packet << [payload].pack("a*") 58 | @socket.send(packet,0,@to) 59 | end 60 | 61 | private 62 | 63 | def check_addr addr 64 | case addr 65 | when String 66 | IPAddr.new(addr) 67 | when IPAddr 68 | addr 69 | else 70 | raise ArgumentError, "Wrong address format: #{addr}" 71 | end 72 | end 73 | 74 | def check_port port 75 | if (1..65535).include? port and port.kind_of? Integer 76 | port 77 | else 78 | raise ArgumentError, "Port #{port} not valid" 79 | end 80 | end 81 | 82 | def genID 83 | while (@@id_arr.include?(q = rand(65535))) 84 | end 85 | @@id_arr.push(q) 86 | q 87 | end 88 | 89 | def ipchecksum(data) 90 | checksum = data.unpack("n*").inject(0) { |s, x| s + x } 91 | ((checksum >> 16) + (checksum & 0xffff)) ^ 0xffff 92 | end 93 | 94 | def make_ip_header(parts) 95 | template = '' 96 | data = [] 97 | parts.each do |part| 98 | data += part[0..-2] 99 | template << part[-1] 100 | end 101 | data_str = data.pack(template) 102 | checksum = ipchecksum(data_str) 103 | data[-3] = checksum 104 | data.pack(template) 105 | end 106 | 107 | def make_transport_header 108 | "" 109 | end 110 | 111 | end 112 | 113 | class UdpRawSocket < RawSocket # :nodoc: 114 | 115 | def initialize(src_addr,src_port,dest_addr,dest_port) 116 | 117 | super(src_addr,dest_addr) 118 | 119 | # Check ports 120 | @src_port = check_port src_port 121 | @dest_port = check_port dest_port 122 | 123 | # Total lenght: must be overridden by subclasses 124 | @tot_lenght = 20 + 8 # 8 bytes => UDP Header 125 | 126 | # Protocol: must be overridden by subclasses 127 | @protocol = 17 # UDP protocol 128 | 129 | @to = Socket.pack_sockaddr_in @dest_port, @dest_addr.to_s 130 | end 131 | 132 | private 133 | 134 | def make_udp_header(parts) 135 | template = '' 136 | data = [] 137 | parts.each do |part| 138 | data += part[0..-2] 139 | template << part[-1] 140 | end 141 | data.pack(template) 142 | end 143 | 144 | def make_transport_header(pay_size) 145 | make_udp_header([ 146 | [ @src_port, 'n'], # source port 147 | [ @dest_port, 'n' ], # destination port 148 | [ 8 + pay_size, 'n' ], # len 149 | [ 0, 'n' ] # checksum (mandatory) 150 | ]) 151 | end 152 | 153 | end 154 | 155 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Release 0.8.3 4 | - FIXED: Packet size cannot be set issue #1 5 | - FIXED: Now raise exception if raw sockets are requested and we are not 6 | running as root issue #23 7 | - FIXED: Rake can build gem, travis file updated to test up to 8 | ruby-head issue #30 9 | - FIXED: Merged in pending bluemonk pull requests issue #29, remaining 10 | fixes are from these 11 | - FIXED: Cannot get TXT record issue #13 12 | https://github.com/bluemonk/net-dns/pull/42 13 | - FIXED: Various fixes by neoraider 14 | https://github.com/bluemonk/net-dns/pull/36 15 | - FIXED: query_tcp fix by nbaum 16 | https://github.com/bluemonk/net-dns/pull/63 17 | - FIXED: Text fixes by chrislundquist 18 | https://github.com/bluemonk/net-dns/pull/52 19 | https://github.com/bluemonk/net-dns/pull/51 20 | - CHANGED: Support SPF records by winebarrel 21 | https://github.com/bluemonk/net-dns/pull/57 22 | - CHANGED: Simplified nameserver settings 23 | https://github.com/bluemonk/net-dns/pull/60 24 | 25 | ## Release 0.8.2 26 | - FIXED: Exception when using new raw support issue #2 27 | - FIXED: Gemspec file has wrong emails 28 | - FIXED: Removed debug statement from raw support 29 | 30 | ## Release 0.8.1 31 | - First release from fork by Christopher Carpenter(mordocai@mordocai.net) 32 | - FIXED: Multiple bugs with Resolver::source_address= preventing it from 33 | executing. 34 | - FIXED: Implemented send_raw_tcp and send_raw_udp 35 | - CHANGED: Added interface config parameter for Net::DNS::Resolver, 36 | currently only used by send_raw* 37 | - CHANGED: Added TODO file for known things that I need to do. 38 | 39 | ## Release 0.8.0 40 | 41 | - FIXED: undefined local variable or method `source_address_inet6' (GH-40). [Thanks @simsicon] 42 | 43 | - FIXED: Fixed bug on parsing multiple nameservers on different lines (GH-45). [Thanks @nicholasren] 44 | 45 | - CHANGED: Dropped duplicate query ID filter. Query ID is now randomically generated but it's not guaranteed to be unique (GH-39). [Thanks @ebroder] 46 | 47 | - CHANGED: require 'net/dns' is now the preferred way to load the library (GH-37). [Thanks @johnroa] 48 | 49 | - CHANGED: Removed setup.rb installation script. 50 | 51 | 52 | ## Release 0.7.1 53 | 54 | - FIXED: Invalid file permissions on several files (GH-35) [Thanks @jamespharaoh] 55 | 56 | 57 | ## Release 0.7.0 58 | 59 | - ADDED: Added (experimental) Support for HINFO record. 60 | 61 | - FIXED: Use Net::DNS::Resolver::Error class (not ResolverError, which does not exist). 62 | 63 | - FIXED: Cleaned up require dependency and recursive require statements. 64 | 65 | - FIXED: Use RbConfig instead of obsolete and deprecated Config (GH-28, GH-33) [Thanks @shadowbq, @eik3] 66 | 67 | - FIXED: SRV record not required by Net::DNS::RR (GH-27) [Thanks @sebastian] 68 | 69 | - FIXED: Resolver now supports IPv6 (GH-32) [Thanks @jamesotron] 70 | 71 | - FIXED: Net::DNS::RR::PTR references an invalid parameter (GH-19) [Thanks @dd23] 72 | 73 | - FIXED: Net::DNS::Question changes input arguments (GH-7) [Thanks @gfarfl] 74 | 75 | - CHANGED: Refactoring unit test to follow most used Ruby conventions. 76 | 77 | - CHANGED: Rewritten and simplified Net::DNS::Classes. Improved test harness. 78 | 79 | - CHANGED: Removed Jeweler development dependency. 80 | 81 | - CHANGED: The library is now compatible with Bundler. 82 | 83 | - CHANGED: Minimum supported Ruby version changed to Ruby 1.8.7. 84 | 85 | - CHANGED: Rescue NameError so unsupported record types only result in a warning. 86 | 87 | - CHANGED: Renamed Net::DNS::Resolver#send to Net::DNS::Resolver#query to avoid overriding default meaning of send method. 88 | 89 | 90 | ## Release 0.6.1 91 | 92 | - ADDED: Net::DNS::Packet#to_s method (alias of #inspect) 93 | 94 | - FIXED: typo in lib/net/dns/rr/ptr.rb [Thanks Chris Lundquist] 95 | 96 | - FIXED: warning: method redefined; discarding old inspect (GH-3) [Thanks Kevin Baker] 97 | 98 | - FIXED: issue with rescue ArgumentError (GH-5) and with IPAddr handling (GH-6) 99 | 100 | 101 | ## Release 0.6.0 102 | 103 | *WARNING:- If you are upgrading from a previous minor release, check out the Compatibility issue list below. 104 | 105 | - FIXED: Added missing #to_s method to Net::DNS::Question. 106 | 107 | - FIXED: Compatibility with Ruby 1.9 108 | 109 | - FIXED: Types regexp order issue 110 | 111 | - CHANGED: Refactoring unit test to follow most used Ruby conventions 112 | 113 | - CHANGED: default timeout is now 5 seconds for both UDP and TCP 114 | 115 | - CHANGED: Moved main dns.rb file to lib/net folder as default for GEMs. In this way it can be autoloaded when the gem is required. 116 | 117 | ### Compatibility issues 118 | 119 | - CHANGED: RR#set_stype scope is now private to prevent invalid usage. 120 | 121 | - CHANGED: DnsTimeout#timeout now raises LocalJumpError instead of DnsTimeoutArgumentError when block is missing. 122 | 123 | - CHANGED: Renamed Net::DNS::RR::Types::Types to Net::DNS::RR::Types::TYPES to follow Ruby coding standards. 124 | 125 | 126 | ## Release 0.4 127 | 128 | - many bug fixes (thanks guys!) 129 | - a whole new class Net::DNS::Header::RCode 130 | - new methods in Net::DNS::Resolver class to do AXFR queries 131 | - a new SRV resource record written by Dan Janowski 132 | - more documentation written and corrected 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Net::DNS 2 | [![Build Status](https://travis-ci.org/mordocai/net-dns.svg?branch=master)](https://travis-ci.org/mordocai/net-dns) 3 | 4 | https://rubygems.org/gems/net-dns2 5 | 6 | Net::DNS is a DNS library written in pure Ruby. It started as a port of Perl Net::DNS module, but it evolved in time into a full Ruby library. 7 | 8 | 9 | ## Features 10 | 11 | - Complete OO interface 12 | - Clean and intuitive API 13 | - Modular and flexible 14 | - Source address spoofing (via source_address= on Net::DNS::Resolver) 15 | 16 | 17 | ## Requirements 18 | 19 | * Ruby >= 1.8.7 20 | 21 | 22 | ## Installation 23 | 24 | The best way to install this library is via [RubyGems](https://rubygems.org/). 25 | 26 | $ gem install net-dns2 27 | 28 | You might need administrator privileges on your system to install the gem. 29 | 30 | 31 | ## API Documentation 32 | 33 | Visit the page http://rdoc.info/gems/net-dns2 34 | 35 | 36 | ## Trivial resolver 37 | 38 | The simplest way to use the library is to invoke the Resolver() method: 39 | 40 | require 'rubygems' 41 | require 'net/dns' 42 | p Resolver("www.google.com") 43 | 44 | The output is compatible with BIND zone files and it's the same you would get with the dig utility. 45 | 46 | ;; Answer received from localhost:53 (212 bytes) 47 | ;; 48 | ;; HEADER SECTION 49 | ;; id = 64075 50 | ;; qr = 1 opCode: QUERY aa = 0 tc = 0 rd = 1 51 | ;; ra = 1 ad = 0 cd = 0 rcode = NoError 52 | ;; qdCount = 1 anCount = 3 nsCount = 4 arCount = 4 53 | 54 | ;; QUESTION SECTION (1 record): 55 | ;; google.com. IN A 56 | 57 | ;; ANSWER SECTION (3 records): 58 | google.com. 212 IN A 74.125.45.100 59 | google.com. 212 IN A 74.125.67.100 60 | google.com. 212 IN A 209.85.171.100 61 | 62 | ;; AUTHORITY SECTION (4 records): 63 | google.com. 345512 IN NS ns1.google.com. 64 | google.com. 345512 IN NS ns4.google.com. 65 | google.com. 345512 IN NS ns2.google.com. 66 | google.com. 345512 IN NS ns3.google.com. 67 | 68 | ;; ADDITIONAL SECTION (4 records): 69 | ns1.google.com. 170275 IN A 216.239.32.10 70 | ns2.google.com. 170275 IN A 216.239.34.10 71 | ns3.google.com. 170275 IN A 216.239.36.10 72 | ns4.google.com. 170275 IN A 216.239.38.10 73 | 74 | An optional block can be passed yielding the Net::DNS::Packet object 75 | 76 | Resolver("www.google.com") { |packet| puts packet.size + " bytes" } 77 | # => 484 bytes 78 | 79 | Same for Net::DNS::Resolver.start(): 80 | 81 | Net::DNS::Resolver.start("google.com").answer.size 82 | # => 5 83 | 84 | As optional parameters, +TYPE+ and +CLASS+ can be specified. 85 | 86 | p Net::DNS::Resolver.start("google.com", Net::DNS::MX) 87 | 88 | ;; Answer received from localhost:53 (316 bytes) 89 | ;; 90 | ;; HEADER SECTION 91 | ;; id = 59980 92 | ;; qr = 1 opCode: QUERY aa = 0 tc = 0 rd = 1 93 | ;; ra = 1 ad = 0 cd = 0 rcode = NoError 94 | ;; qdCount = 1 anCount = 4 nsCount = 4 arCount = 8 95 | 96 | ;; QUESTION SECTION (1 record): 97 | ;; google.com. IN MX 98 | 99 | ;; ANSWER SECTION (4 records): 100 | google.com. 10800 IN MX 10 smtp2.google.com. 101 | google.com. 10800 IN MX 10 smtp3.google.com. 102 | google.com. 10800 IN MX 10 smtp4.google.com. 103 | google.com. 10800 IN MX 10 smtp1.google.com. 104 | 105 | 106 | ## Handling the response packet 107 | 108 | The method Net::DNS::Resolver.start is a wrapper around Resolver.new. It returns a new Net::DNS::Packet object. 109 | 110 | A DNS packet is divided into 5 sections: 111 | 112 | - header section # => a Net::DNS::Header object 113 | - question section # => a Net::DNS::Question object 114 | - answer section # => an Array of Net::DNS::RR objects 115 | - authority section # => an Array of Net::DNS::RR objects 116 | - additional section # => an Array of Net::DNS::RR objects 117 | 118 | You can access each section by calling the attribute with the same name on a Packet object: 119 | 120 | packet = Net::DNS::Resolver.start("google.com") 121 | 122 | header = packet.header 123 | answer = packet.answer 124 | 125 | puts "The packet is #{packet.data.size} bytes" 126 | puts "It contains #{header.anCount} answer entries" 127 | 128 | answer.any? {|ans| p ans} 129 | 130 | The output is 131 | 132 | The packet is 378 bytes 133 | It contains 3 answer entries 134 | google.com. 244 IN A 74.125.45.100 135 | google.com. 244 IN A 74.125.67.100 136 | google.com. 244 IN A 209.85.171.100 137 | 138 | A better way to handle the answer section is to use the iterators directly on a Packet object: 139 | 140 | packet.each_address do |ip| 141 | puts "#{ip} is alive" if Ping.pingecho(ip.to_s, 10, 80) 142 | end 143 | 144 | Gives: 145 | 146 | 74.125.45.100 is alive 147 | 74.125.67.100 is alive 148 | 209.85.171.100 is alive 149 | 150 | 151 | ## License 152 | 153 | See License 154 | 155 | ## Authors 156 | 157 | - Marco Ceresa (@bluemonk) 158 | - Simone Carletti (@weppos) 159 | - Christopher Carpenter (@mordocai) 160 | -------------------------------------------------------------------------------- /lib/net/dns/question.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module DNS 3 | 4 | # 5 | # =Name 6 | # 7 | # Net::DNS::Question - DNS packet question class 8 | # 9 | # =Synopsis 10 | # 11 | # require 'net/dns/question' 12 | # 13 | # =Description 14 | # 15 | # This class represent the Question portion of a DNS packet. The number 16 | # of question entries is stored in the +qdCount+ variable of an Header 17 | # object. 18 | # 19 | # A new object can be created passing the name of the query and the type 20 | # of answer desired, plus an optional argument containing the class: 21 | # 22 | # question = Net::DNS::Question.new("google.com.", Net::DNS::A) 23 | # #=> "google.com. A IN" 24 | # 25 | # Alternatevly, a new object is created when processing a binary 26 | # packet, as when an answer is received. 27 | # To obtain the binary data from a question object you can use 28 | # the method Question#data: 29 | # 30 | # question.data 31 | # #=> "\006google\003com\000\000\001\000\001" 32 | # 33 | # A lot of methods were written to keep a compatibility layer with 34 | # the Perl version of the library, as long as methods name which are 35 | # more or less the same. 36 | # 37 | class Question 38 | include Names 39 | 40 | # Base error class. 41 | class Error < StandardError 42 | end 43 | 44 | # An error in the +name+ part of a Question entry 45 | class NameInvalid < Error 46 | end 47 | 48 | # +name+ part of a Question entry 49 | attr_reader :qName 50 | # +type+ part of a Question entry 51 | attr_reader :qType 52 | # +class+ part of a Question entry 53 | attr_reader :qClass 54 | 55 | # Creates a new Net::DNS::Question object: 56 | # 57 | # question = Net::DNS::Question.new("example.com") 58 | # #=> "example.com A IN" 59 | # question = Net::DNS::Question.new("example.com", Net::DNS::MX) 60 | # #=> "example.com MX IN" 61 | # question = Net::DNS::Question.new("example.com", Net::DNS::TXT, Net::DNS::HS) 62 | # #=> "example.com TXT HS" 63 | 64 | # If not specified, +type+ and +cls+ arguments defaults 65 | # to Net::DNS::A and Net::DNS::IN respectively. 66 | # 67 | def initialize(name, type = Net::DNS::A, cls = Net::DNS::IN) 68 | @qName = check_name name 69 | @qType = Net::DNS::RR::Types.new(type) 70 | @qClass = Net::DNS::RR::Classes.new(cls) 71 | end 72 | 73 | # Return a new Net::DNS::Question object created by 74 | # parsing binary data, such as an answer from the 75 | # nameserver. 76 | # 77 | # question = Net::DNS::Question.parse(data) 78 | # puts "Queried for #{question.qName} type #{question.qType.to_s}" 79 | # #=> Queried for example.com type A 80 | # 81 | def self.parse(arg) 82 | o = allocate 83 | o.send(:new_from_binary, arg.to_s) 84 | o 85 | end 86 | 87 | # Outputs binary data from a Question object 88 | # 89 | # question.data 90 | # #=> "\006google\003com\000\000\001\000\001" 91 | # 92 | def data 93 | [pack_name(@qName),@qType.to_i,@qClass.to_i].pack("a*nn") 94 | end 95 | 96 | # Return the binary data of the objects, plus an offset 97 | # and an Hash with references to compressed names. For use in 98 | # Net::DNS::Packet compressed packet creation. 99 | def comp_data 100 | arr = @qName.split(".") 101 | str = pack_name(@qName) 102 | string = "" 103 | names = {} 104 | offset = Net::DNS::HFIXEDSZ 105 | arr.size.times do |i| 106 | x = i+1 107 | elem = arr[-x] 108 | len = elem.size 109 | string = ((string.reverse)+([len,elem].pack("Ca*")).reverse).reverse 110 | names[string] = offset 111 | offset += len 112 | end 113 | offset += 2 * Net::DNS::INT16SZ 114 | str += "\000" 115 | [[str,@qType.to_i,@qClass.to_i].pack("a*nn"),offset,names] 116 | end 117 | 118 | 119 | # 120 | # call-seq: 121 | # question.inspect -> string 122 | # 123 | # Returns a printable version of question with nice formatting. 124 | # 125 | # q = Net::DNS::Question.new("google.com.", Net::DNS::A) 126 | # q.inspect # => "google.com. IN A " 127 | # 128 | def inspect 129 | if @qName.size > 29 then 130 | len = @qName.size + 1 131 | else 132 | len = 29 133 | end 134 | [@qName, @qClass.to_s, @qType.to_s].pack("A#{len} A8 A8") 135 | end 136 | 137 | # 138 | # call-seq: 139 | # question.to_s -> string 140 | # 141 | # Returns a string representation of question. 142 | # It is the same as inspect. 143 | # 144 | # q = Net::DNS::Question.new("google.com.", Net::DNS::A) 145 | # q.inspect # => "google.com. IN A " 146 | # 147 | def to_s 148 | "#{self.inspect}" 149 | end 150 | 151 | 152 | private 153 | 154 | def build_qName(str) 155 | result = "" 156 | offset = 0 157 | loop do 158 | len = str.unpack("@#{offset} C")[0] 159 | break if len == 0 160 | offset += 1 161 | result += str[offset..offset+len-1] 162 | result += "." 163 | offset += len 164 | end 165 | result 166 | end 167 | 168 | def check_name(input) 169 | name = input.to_s.strip 170 | if name =~ /[^\w\.\-_]/ 171 | raise NameInvalid, "Invalid Question Name `#{name}'" 172 | end 173 | name 174 | end 175 | 176 | def new_from_binary(data) 177 | str,type,cls = data.unpack("a#{data.size - 4}nn") 178 | @qName = build_qName(str) 179 | @qType = Net::DNS::RR::Types.new type 180 | @qClass = Net::DNS::RR::Classes.new cls 181 | rescue StandardError => e 182 | raise ArgumentError, "Invalid data: #{data.inspect}" 183 | end 184 | 185 | end 186 | 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /lib/net/dns/rr/types.rb: -------------------------------------------------------------------------------- 1 | module Net # :nodoc: 2 | module DNS 3 | 4 | class RR 5 | 6 | # This is an auxiliary class to handle RR type field in a DNS packet. 7 | class Types 8 | 9 | TYPES = { 10 | 'SIGZERO' => 0, # RFC2931 consider this a pseudo type 11 | 'A' => 1, # RFC 1035, Section 3.4.1 12 | 'NS' => 2, # RFC 1035, Section 3.3.11 13 | 'MD' => 3, # RFC 1035, Section 3.3.4 (obsolete) 14 | 'MF' => 4, # RFC 1035, Section 3.3.5 (obsolete) 15 | 'CNAME' => 5, # RFC 1035, Section 3.3.1 16 | 'SOA' => 6, # RFC 1035, Section 3.3.13 17 | 'MB' => 7, # RFC 1035, Section 3.3.3 18 | 'MG' => 8, # RFC 1035, Section 3.3.6 19 | 'MR' => 9, # RFC 1035, Section 3.3.8 20 | 'NULL' => 10, # RFC 1035, Section 3.3.10 21 | 'WKS' => 11, # RFC 1035, Section 3.4.2 (deprecated) 22 | 'PTR' => 12, # RFC 1035, Section 3.3.12 23 | 'HINFO' => 13, # RFC 1035, Section 3.3.2 24 | 'MINFO' => 14, # RFC 1035, Section 3.3.7 25 | 'MX' => 15, # RFC 1035, Section 3.3.9 26 | 'TXT' => 16, # RFC 1035, Section 3.3.14 27 | 'RP' => 17, # RFC 1183, Section 2.2 28 | 'AFSDB' => 18, # RFC 1183, Section 1 29 | 'X25' => 19, # RFC 1183, Section 3.1 30 | 'ISDN' => 20, # RFC 1183, Section 3.2 31 | 'RT' => 21, # RFC 1183, Section 3.3 32 | 'NSAP' => 22, # RFC 1706, Section 5 33 | 'NSAP_PTR' => 23, # RFC 1348 (obsolete) 34 | # The following 2 RRs are impemented in Net::DNS::SEC, TODO 35 | 'SIG' => 24, # RFC 2535, Section 4.1 36 | 'KEY' => 25, # RFC 2535, Section 3.1 37 | 'PX' => 26, # RFC 2163, 38 | 'GPOS' => 27, # RFC 1712 (obsolete) 39 | 'AAAA' => 28, # RFC 1886, Section 2.1 40 | 'LOC' => 29, # RFC 1876 41 | # The following RR is implemented in Net::DNS::SEC, TODO 42 | 'NXT' => 30, # RFC 2535, Section 5.2 43 | 'EID' => 31, # draft-ietf-nimrod-dns-xx.txt 44 | 'NIMLOC' => 32, # draft-ietf-nimrod-dns-xx.txt 45 | 'SRV' => 33, # RFC 2052 46 | 'ATMA' => 34, # ??? 47 | 'NAPTR' => 35, # RFC 2168 48 | 'KX' => 36, # RFC 2230 49 | 'CERT' => 37, # RFC 2538 50 | 'DNAME' => 39, # RFC 2672 51 | 'OPT' => 41, # RFC 2671 52 | # The following 4 RRs are implemented in Net::DNS::SEC TODO 53 | 'DS' => 43, # draft-ietf-dnsext-delegation-signer 54 | 'SSHFP' => 44, # draft-ietf-secsh-dns (No RFC # yet at time of coding) 55 | 'RRSIG' => 46, # draft-ietf-dnsext-dnssec-2535typecode-change 56 | 'NSEC' => 47, # draft-ietf-dnsext-dnssec-2535typecode-change 57 | 'DNSKEY' => 48, # draft-ietf-dnsext-dnssec-2535typecode-change 58 | 'SPF' => 99, # RFC 4408 59 | 'UINFO' => 100, # non-standard 60 | 'UID' => 101, # non-standard 61 | 'GID' => 102, # non-standard 62 | 'UNSPEC' => 103, # non-standard 63 | 'TKEY' => 249, # RFC 2930 64 | 'TSIG' => 250, # RFC 2931 65 | 'IXFR' => 251, # RFC 1995 66 | 'AXFR' => 252, # RFC 1035 67 | 'MAILB' => 253, # RFC 1035 (MB, MG, MR) 68 | 'MAILA' => 254, # RFC 1035 (obsolete - see MX) 69 | 'ANY' => 255, # RFC 1035 70 | } 71 | 72 | # The default value when type is nil in Resource Records 73 | @@default = TYPES["A"] 74 | 75 | # Be able to control the default type to assign when 76 | # type is +nil+. Default to +A+ 77 | def self.default=(str) 78 | if TYPES.has_key? str 79 | @@default = TYPES[str] 80 | else 81 | raise ArgumentError, "Unknown type #{str}" 82 | end 83 | end 84 | 85 | # Checks whether +type+ is a valid RR type. 86 | def self.valid?(type) 87 | case type 88 | when String 89 | TYPES.has_key?(type) 90 | when Fixnum 91 | TYPES.invert.has_key?(type) 92 | else 93 | raise ArgumentError, "Wrong type class: #{type.class}" 94 | end 95 | end 96 | 97 | # Returns the type in string format, as "A" or "NS", 98 | # given the numeric value 99 | def self.to_str(type) 100 | case type 101 | when Fixnum 102 | if TYPES.invert.has_key? type 103 | TYPES.invert[type] 104 | else 105 | raise ArgumentError, "Unknown type number #{type}" 106 | end 107 | else 108 | raise ArgumentError, "Wrong type class: #{type.class}" 109 | end 110 | end 111 | 112 | # Gives in output the keys from the +Types+ hash 113 | # in a format suited for regexps 114 | def self.regexp 115 | # Longest ones go first, so the regex engine will match AAAA before A. 116 | TYPES.keys.sort { |a,b| b.length <=> a.length }.join("|") 117 | end 118 | 119 | # Creates a new object representing an RR type. Performs some 120 | # checks on the argument validity too. Il +type+ is +nil+, the 121 | # default value is +ANY+ or the one set with Types.default= 122 | def initialize(type) 123 | case type 124 | when String 125 | # type in the form "A" or "NS" 126 | new_from_string(type.upcase) 127 | when Fixnum 128 | # type in numeric form 129 | new_from_num(type) 130 | when nil 131 | # default type, control with Types.default= 132 | @str = TYPES.invert[@@default] 133 | @num = @@default 134 | else 135 | raise ArgumentError, "Wrong type class: #{type.class}" 136 | end 137 | end 138 | 139 | # Returns the type in number format 140 | # (default for normal use) 141 | def inspect 142 | @num 143 | end 144 | 145 | # Returns the type in string format, 146 | # i.d. "A" or "NS" or such a string. 147 | def to_s 148 | @str 149 | end 150 | 151 | # Returns the type in numeric format, 152 | # usable by the pack methods for data transfers 153 | def to_i 154 | @num.to_i 155 | end 156 | 157 | def to_str 158 | @num.to_s 159 | end 160 | 161 | 162 | private 163 | 164 | # Constructor for string data type. 165 | def new_from_string(type) 166 | case type 167 | when /^TYPE\\d+/ 168 | # TODO!!! 169 | else 170 | # String with name of type 171 | if TYPES.has_key? type 172 | @str = type 173 | @num = TYPES[type] 174 | else 175 | raise ArgumentError, "Unknown type #{type}" 176 | end 177 | end 178 | end 179 | 180 | # Contructor for numeric data type. 181 | def new_from_num(type) 182 | if TYPES.invert.has_key? type 183 | @num = type 184 | @str = TYPES.invert[type] 185 | else 186 | raise ArgumentError, "Unkown type number #{type}" 187 | end 188 | end 189 | 190 | end 191 | 192 | end 193 | end 194 | end 195 | -------------------------------------------------------------------------------- /lib/net/dns/rr.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddr' 2 | require 'net/dns/names' 3 | require 'net/dns/rr/types' 4 | require 'net/dns/rr/classes' 5 | 6 | %w(a aaaa cname hinfo mr mx ns ptr soa srv txt spf).each do |file| 7 | require "net/dns/rr/#{file}" 8 | end 9 | 10 | module Net 11 | module DNS 12 | 13 | # 14 | # = Net::DNS::RR - DNS Resource Record class 15 | # 16 | # The Net::DNS::RR is the base class for DNS Resource 17 | # Record (RR) objects. A RR is a pack of data that represents 18 | # resources for a DNS zone. The form in which this data is 19 | # shows can be drawed as follow: 20 | # 21 | # "name ttl class type data" 22 | # 23 | # The +name+ is the name of the resource, like an canonical 24 | # name for an +A+ record (internet ip address). The +ttl+ is the 25 | # time to live, expressed in seconds. +type+ and +class+ are 26 | # respectively the type of resource (+A+ for ip addresses, +NS+ 27 | # for nameservers, and so on) and the class, which is almost 28 | # always +IN+, the Internet class. At the end, +data+ is the 29 | # value associated to the name for that particular type of 30 | # resource record. An example: 31 | # 32 | # # A record for IP address 33 | # "www.example.com 86400 IN A 172.16.100.1" 34 | # 35 | # # NS record for name server 36 | # "www.example.com 86400 IN NS ns.example.com" 37 | # 38 | # A new RR object can be created in 2 ways: passing a string 39 | # such the ones above, or specifying each field as the pair 40 | # of an hash. See the Net::DNS::RR.new method for details. 41 | # 42 | class RR 43 | include Names 44 | 45 | 46 | # Base error class. 47 | class Error < StandardError 48 | end 49 | 50 | # Error in parsing binary data, maybe from a malformed packet. 51 | class DataError < Error 52 | end 53 | 54 | 55 | # Regexp matching an RR string 56 | RR_REGEXP = Regexp.new("^\\s*(\\S+)\\s*(\\d+)?\\s+(" + 57 | Net::DNS::RR::Classes.regexp + 58 | "|CLASS\\d+)?\\s*(" + 59 | Net::DNS::RR::Types.regexp + 60 | "|TYPE\\d+)?\\s*(.*)$", Regexp::IGNORECASE) 61 | 62 | # Dimension of the sum of class, type, TTL and rdlength fields in a 63 | # RR portion of the packet, in bytes 64 | RRFIXEDSZ = 10 65 | 66 | 67 | # Create a new instance of Net::DNS::RR class, or an instance of 68 | # any of the subclass of the appropriate type. 69 | # 70 | # Argument can be a string or an hash. With a sting, we can pass 71 | # a RR resource record in the canonical format: 72 | # 73 | # a = Net::DNS::RR.new("foo.example.com. 86400 A 10.1.2.3") 74 | # mx = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.") 75 | # cname = Net::DNS::RR.new("www.example.com 300 IN CNAME www1.example.com") 76 | # txt = Net::DNS::RR.new('baz.example.com 3600 HS TXT "text record"') 77 | # 78 | # Incidentally, +a+, +mx+, +cname+ and +txt+ objects will be instances of 79 | # respectively Net::DNS::RR::A, Net::DNS::RR::MX, Net::DNS::RR::CNAME and 80 | # Net::DNS::RR::TXT classes. 81 | # 82 | # The name and RR data are required; all other informations are optional. 83 | # If omitted, the +TTL+ defaults to 10800, +type+ default to +A+ and the RR class 84 | # defaults to +IN+. Omitting the optional fields is useful for creating the 85 | # empty RDATA sections required for certain dynamic update operations. 86 | # All names must be fully qualified. The trailing dot (.) is optional. 87 | # 88 | # The preferred method is however passing an hash with keys and values: 89 | # 90 | # rr = Net::DNS::RR.new( 91 | # :name => "foo.example.com", 92 | # :ttl => 86400, 93 | # :cls => "IN", 94 | # :type => "A", 95 | # :address => "10.1.2.3" 96 | # ) 97 | # 98 | # rr = Net::DNS::RR.new( 99 | # :name => "foo.example.com", 100 | # :rdata => "10.1.2.3" 101 | # ) 102 | # 103 | # Name and data are required; all the others fields are optionals like 104 | # we've seen before. The data field can be specified either with the 105 | # right name of the resource (+:address+ in the example above) or with 106 | # the generic key +:rdata+. Consult documentation to find the exact name 107 | # for the resource in each subclass. 108 | # 109 | def initialize(arg) 110 | instance = case arg 111 | when String 112 | new_from_string(arg) 113 | when Hash 114 | new_from_hash(arg) 115 | else 116 | raise ArgumentError, "Invalid argument, must be a RR string or an hash of values" 117 | end 118 | 119 | if @type.to_s == "ANY" 120 | @cls = Net::DNS::RR::Classes.new("IN") 121 | end 122 | 123 | build_pack 124 | set_type 125 | 126 | instance 127 | end 128 | 129 | # Return a new RR object of the correct type (like Net::DNS::RR::A 130 | # if the type is A) from a binary string, usually obtained from 131 | # network stream. 132 | # 133 | # This method is used when parsing a binary packet by the Packet 134 | # class. 135 | # 136 | def RR.parse(data) 137 | o = allocate 138 | obj, offset = o.send(:new_from_binary, data, 0) 139 | obj 140 | end 141 | 142 | # Same as RR.parse, but takes an entire packet binary data to 143 | # perform name expansion. Default when analizing a packet 144 | # just received from a network stream. 145 | # 146 | # Return an instance of appropriate class and the offset 147 | # pointing at the end of the data parsed. 148 | # 149 | def RR.parse_packet(data, offset) 150 | o = allocate 151 | o.send(:new_from_binary, data, offset) 152 | end 153 | 154 | def name 155 | @name 156 | end 157 | 158 | def ttl 159 | @ttl 160 | end 161 | 162 | # Type accessor 163 | def type 164 | @type.to_s 165 | end 166 | 167 | # Class accessor 168 | def cls 169 | @cls.to_s 170 | end 171 | 172 | 173 | def value 174 | get_inspect 175 | end 176 | 177 | # Data belonging to that appropriate class, 178 | # not to be used (use real accessors instead) 179 | def rdata 180 | @rdata 181 | end 182 | 183 | # Return the RR object in binary data format, suitable 184 | # for using in network streams. 185 | # 186 | # raw_data = rr.data 187 | # puts "RR is #{raw_data.size} bytes long" 188 | # 189 | def data 190 | str = pack_name(@name) 191 | str + [@type.to_i, @cls.to_i, ttl, @rdlength].pack("n2 N n") + get_data 192 | end 193 | 194 | # Return the RR object in binary data format, suitable 195 | # for using in network streams, with names compressed. 196 | # Must pass as arguments the offset inside the packet 197 | # and an hash of compressed names. 198 | # 199 | # This method is to be used in other classes and is 200 | # not intended for user space programs. 201 | # 202 | # TO FIX in one of the future releases 203 | # 204 | def comp_data(offset,compnames) 205 | str, offset, names = dn_comp(@name, offset, compnames) 206 | str += [@type.to_i, @cls.to_i, ttl, @rdlength].pack("n2 N n") 207 | offset += Net::DNS::RRFIXEDSZ 208 | [str, offset, names] 209 | end 210 | 211 | 212 | # Returns a human readable representation of this record. 213 | # The value is always a String. 214 | # 215 | # mx = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.") 216 | # #=> example.com. 7200 IN MX 10 mailhost.example.com. 217 | # 218 | def inspect 219 | to_s 220 | end 221 | 222 | # Returns a String representation of this record. 223 | # 224 | # mx = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.") 225 | # mx.to_s 226 | # #=> "example.com. 7200 IN MX 10 mailhost.example.com." 227 | # 228 | def to_s 229 | items = to_a.map { |e| e.to_s } 230 | if @name.size < 24 231 | items.pack("A24 A8 A8 A8 A*") 232 | else 233 | items.join(" ") 234 | end.to_s 235 | end 236 | 237 | # Returns an Array with all the attributes for this record. 238 | # 239 | # mx = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.") 240 | # mx.to_a 241 | # #=> ["example.com.", 7200, "IN", "MX", "10 mailhost.example.com."] 242 | # 243 | def to_a 244 | [name, ttl, cls.to_s, type.to_s, value] 245 | end 246 | 247 | 248 | private 249 | 250 | def new_from_string(rrstring) 251 | unless rrstring =~ RR_REGEXP 252 | raise ArgumentError, 253 | "Format error for RR string (maybe CLASS and TYPE not valid?)" 254 | end 255 | 256 | # Name of RR - mandatory 257 | begin 258 | @name = $1.downcase 259 | rescue NoMethodError 260 | raise ArgumentError, "Missing name field in RR string #{rrstring}" 261 | end 262 | 263 | # Time to live for RR, default 3 hours 264 | @ttl = $2 ? $2.to_i : 10800 265 | 266 | # RR class, default to IN 267 | @cls = Net::DNS::RR::Classes.new $3 268 | 269 | # RR type, default to A 270 | @type = Net::DNS::RR::Types.new $4 271 | 272 | # All the rest is data 273 | @rdata = $5 ? $5.strip : "" 274 | 275 | if self.class == Net::DNS::RR 276 | Net::DNS::RR.const_get(@type.to_s).new(rrstring) 277 | else 278 | subclass_new_from_string(@rdata) 279 | self.class 280 | end 281 | end 282 | 283 | def new_from_hash(args) 284 | # Name field is mandatory 285 | unless args.has_key? :name 286 | raise ArgumentError, ":name field is mandatory" 287 | end 288 | 289 | @name = args[:name].downcase 290 | @ttl = args[:ttl] ? args[:ttl].to_i : 10800 # Default 3 hours 291 | @type = Net::DNS::RR::Types.new args[:type] 292 | @cls = Net::DNS::RR::Classes.new args[:cls] 293 | 294 | @rdata = args[:rdata] ? args[:rdata].strip : "" 295 | @rdlength = args[:rdlength] || @rdata.size 296 | 297 | if self.class == Net::DNS::RR 298 | Net::DNS::RR.const_get(@type.to_s).new(args) 299 | else 300 | hash = args - [:name, :ttl, :type, :cls] 301 | if hash.has_key? :rdata 302 | subclass_new_from_string(hash[:rdata]) 303 | else 304 | subclass_new_from_hash(hash) 305 | end 306 | self.class 307 | end 308 | end 309 | 310 | def new_from_binary(data,offset) 311 | if self.class == Net::DNS::RR 312 | temp = dn_expand(data,offset)[1] 313 | type = Net::DNS::RR::Types.new data.unpack("@#{temp} n")[0] 314 | (eval "Net::DNS::RR::#{type}").parse_packet(data,offset) 315 | else 316 | @name,offset = dn_expand(data,offset) 317 | rrtype,cls,@ttl,@rdlength = data.unpack("@#{offset} n2 N n") 318 | @type = Net::DNS::RR::Types.new rrtype 319 | @cls = Net::DNS::RR::Classes.new cls 320 | offset += RRFIXEDSZ 321 | offset = subclass_new_from_binary(data,offset) 322 | build_pack 323 | set_type 324 | [self, offset] 325 | end 326 | end 327 | 328 | # Methods to be overridden by subclasses 329 | def subclass_new_from_array(arr) 330 | end 331 | def subclass_new_from_string(str) 332 | end 333 | def subclass_new_from_hash(hash) 334 | end 335 | def subclass_new_from_binary(data, offset) 336 | end 337 | def build_pack 338 | end 339 | def get_inspect 340 | @rdata 341 | end 342 | def get_data 343 | @rdata 344 | end 345 | 346 | def set_type 347 | # TODO: Here we should probably 348 | # raise NotImplementedError 349 | # if we want the method to be implemented in any subclass. 350 | end 351 | 352 | 353 | def self.new(*args) 354 | o = allocate 355 | obj = o.send(:initialize,*args) 356 | if self == Net::DNS::RR 357 | obj 358 | else 359 | o 360 | end 361 | end 362 | 363 | end 364 | 365 | end 366 | end 367 | -------------------------------------------------------------------------------- /lib/net/dns/packet.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | require 'net/dns/names' 3 | require 'net/dns/header' 4 | require 'net/dns/question' 5 | require 'net/dns/rr' 6 | 7 | module Net 8 | module DNS 9 | 10 | # 11 | # = Net::DNS::Packet 12 | # 13 | # The Net::DNS::Packet class represents an entire DNS packet, 14 | # divided in his main section: 15 | # 16 | # * Header (instance of Net::DNS::Header) 17 | # * Question (array of Net::DNS::Question objects) 18 | # * Answer, Authority, Additional (each formed by an array of Net::DNS::RR 19 | # objects) 20 | # 21 | # You can use this class whenever you need to create a DNS packet, whether 22 | # in an user application, in a resolver instance (have a look, for instance, 23 | # at the Net::DNS::Resolver#send method) or for a nameserver. 24 | # 25 | # For example: 26 | # 27 | # # Create a packet 28 | # packet = Net::DNS::Packet.new("www.example.com") 29 | # mx = Net::DNS::Packet.new("example.com", Net::DNS::MX) 30 | # 31 | # # Getting packet binary data, suitable for network transmission 32 | # data = packet.data 33 | # 34 | # A packet object can be created from binary data too, like an 35 | # answer packet just received from a network stream: 36 | # 37 | # packet = Net::DNS::Packet::parse(data) 38 | # 39 | # Each part of a packet can be gotten by the right accessors: 40 | # 41 | # header = packet.header # Instance of Net::DNS::Header class 42 | # question = packet.question # Instance of Net::DNS::Question class 43 | # 44 | # # Iterate over additional RRs 45 | # packet.additional.each do |rr| 46 | # puts "Got an #{rr.type} record" 47 | # end 48 | # 49 | # Some iterators have been written to easy the access of those RRs, 50 | # which are often the most important. So instead of doing: 51 | # 52 | # packet.answer.each do |rr| 53 | # if rr.type == Net::DNS::RR::Types::A 54 | # # do something with +rr.address+ 55 | # end 56 | # end 57 | # 58 | # we can do: 59 | # 60 | # packet.each_address do |ip| 61 | # # do something with +ip+ 62 | # end 63 | # 64 | # Be sure you don't miss all the iterators in the class documentation. 65 | # 66 | # == Logging facility 67 | # 68 | # As Net::DNS::Resolver class, Net::DNS::Packet class has its own logging 69 | # facility too. It work in the same way the other one do, so you can 70 | # maybe want to override it or change the file descriptor. 71 | # 72 | # packet = Net::DNS::Packet.new("www.example.com") 73 | # packet.logger = $stderr 74 | # 75 | # # or even 76 | # packet.logger = Logger.new("/tmp/packet.log") 77 | # 78 | # If the Net::DNS::Packet class is directly instantiated by the Net::DNS::Resolver 79 | # class, like the great majority of the time, it will use the same logger facility. 80 | # 81 | # Logger level will be set to Logger::Debug if $DEBUG variable is set. 82 | # 83 | class Packet 84 | include Names 85 | 86 | # Base error class. 87 | class Error < StandardError 88 | end 89 | 90 | # Generic Packet Error. 91 | class PacketError < Error 92 | end 93 | 94 | 95 | attr_reader :header, :question, :answer, :authority, :additional 96 | attr_reader :answerfrom, :answersize 97 | 98 | # Creates a new instance of Net::DNS::Packet class. Arguments are the 99 | # canonical name of the resource, an optional type field and an optional 100 | # class field. If the arguments are omitted, no question is added to the new packet; 101 | # type and class default to +A+ and +IN+ if a name is given. 102 | # 103 | # packet = Net::DNS::Packet.new 104 | # packet = Net::DNS::Packet.new("www.example.com") 105 | # packet = Net::DNS::Packet.new("example.com", Net::DNS::MX) 106 | # packet = Net::DNS::Packet.new("example.com", Net::DNS::TXT, Net::DNS::CH) 107 | # 108 | # This class no longer instantiate object from binary data coming from 109 | # network streams. Please use Net::DNS::Packet.parse instead. 110 | def initialize(name = nil, type = Net::DNS::A, cls = Net::DNS::IN) 111 | default_qdcount = 0 112 | @question = [] 113 | 114 | if not name.nil? 115 | default_qdcount = 1 116 | @question = [Net::DNS::Question.new(name, type, cls)] 117 | end 118 | 119 | @header = Net::DNS::Header.new(:qdCount => default_qdcount) 120 | @answer = [] 121 | @authority = [] 122 | @additional = [] 123 | @logger = Logger.new $stdout 124 | @logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN 125 | end 126 | 127 | 128 | # Checks if the packet is a QUERY packet 129 | def query? 130 | @header.query? 131 | end 132 | 133 | # Returns the packet object in binary data, suitable 134 | # for sending across a network stream. 135 | # 136 | # packet_data = packet.data 137 | # puts "Packet is #{packet_data.size} bytes long" 138 | # 139 | def data 140 | qdcount=ancount=nscount=arcount=0 141 | data = @header.data 142 | headerlength = data.length 143 | 144 | @question.each do |question| 145 | data += question.data 146 | qdcount += 1 147 | end 148 | @answer.each do |rr| 149 | data += rr.data#(data.length) 150 | ancount += 1 151 | end 152 | @authority.each do |rr| 153 | data += rr.data#(data.length) 154 | nscount += 1 155 | end 156 | @additional.each do |rr| 157 | data += rr.data#(data.length) 158 | arcount += 1 159 | end 160 | 161 | @header.qdCount = qdcount 162 | @header.anCount = ancount 163 | @header.nsCount = nscount 164 | @header.arCount = arcount 165 | 166 | @header.data + data[Net::DNS::HFIXEDSZ..data.size] 167 | end 168 | 169 | # Same as Net::DNS::Packet#data, but implements name compression 170 | # (see RFC1025) for a considerable save of bytes. 171 | # 172 | # packet = Net::DNS::Packet.new("www.example.com") 173 | # puts "Size normal is #{packet.data.size} bytes" 174 | # puts "Size compressed is #{packet.data_comp.size} bytes" 175 | # 176 | def data_comp 177 | offset = 0 178 | compnames = {} 179 | qdcount=ancount=nscount=arcount=0 180 | data = @header.data 181 | headerlength = data.length 182 | 183 | @question.each do |question| 184 | str,offset,names = question.data 185 | data += str 186 | compnames.update(names) 187 | qdcount += 1 188 | end 189 | 190 | @answer.each do |rr| 191 | str,offset,names = rr.data(offset,compnames) 192 | data += str 193 | compnames.update(names) 194 | ancount += 1 195 | end 196 | 197 | @authority.each do |rr| 198 | str,offset,names = rr.data(offset,compnames) 199 | data += str 200 | compnames.update(names) 201 | nscount += 1 202 | end 203 | 204 | @additional.each do |rr| 205 | str,offset,names = rr.data(offset,compnames) 206 | data += str 207 | compnames.update(names) 208 | arcount += 1 209 | end 210 | 211 | @header.qdCount = qdcount 212 | @header.anCount = ancount 213 | @header.nsCount = nscount 214 | @header.arCount = arcount 215 | 216 | @header.data + data[Net::DNS::HFIXEDSZ..data.size] 217 | end 218 | 219 | # Returns a string containing a human-readable representation 220 | # of this Net::DNS::Packet instance. 221 | def inspect 222 | retval = "" 223 | if @answerfrom != "0.0.0.0:0" and @answerfrom 224 | retval += ";; Answer received from #@answerfrom (#{@answersize} bytes)\n;;\n" 225 | end 226 | 227 | retval += ";; HEADER SECTION\n" 228 | retval += @header.inspect 229 | 230 | retval += "\n" 231 | section = (@header.opCode == "UPDATE") ? "ZONE" : "QUESTION" 232 | retval += ";; #{section} SECTION (#{@header.qdCount} record#{@header.qdCount == 1 ? '' : 's'}):\n" 233 | @question.each do |qr| 234 | retval += ";; " + qr.inspect + "\n" 235 | end 236 | 237 | unless @answer.size == 0 238 | retval += "\n" 239 | section = (@header.opCode == "UPDATE") ? "PREREQUISITE" : "ANSWER" 240 | retval += ";; #{section} SECTION (#{@header.anCount} record#{@header.anCount == 1 ? '' : 's'}):\n" 241 | @answer.each do |rr| 242 | retval += rr.inspect + "\n" 243 | end 244 | end 245 | 246 | unless @authority.size == 0 247 | retval += "\n" 248 | section = (@header.opCode == "UPDATE") ? "UPDATE" : "AUTHORITY" 249 | retval += ";; #{section} SECTION (#{@header.nsCount} record#{@header.nsCount == 1 ? '' : 's'}):\n" 250 | @authority.each do |rr| 251 | retval += rr.inspect + "\n" 252 | end 253 | end 254 | 255 | unless @additional.size == 0 256 | retval += "\n" 257 | retval += ";; ADDITIONAL SECTION (#{@header.arCount} record#{@header.arCount == 1 ? '' : 's'}):\n" 258 | @additional.each do |rr| 259 | retval += rr.inspect + "\n" 260 | end 261 | end 262 | 263 | retval 264 | end 265 | alias_method :to_s, :inspect 266 | 267 | # Delegates to Net::DNS::Header#truncated?. 268 | def truncated? 269 | @header.truncated? 270 | end 271 | 272 | # Assigns a Net::DNS::Header object 273 | # to this Net::DNS::Packet instance. 274 | def header=(object) 275 | if object.kind_of? Net::DNS::Header 276 | @header = object 277 | else 278 | raise ArgumentError, "Argument must be a Net::DNS::Header object" 279 | end 280 | end 281 | 282 | # Assigns a Net::DNS::Question object 283 | # to this Net::DNS::Packet instance. 284 | def question=(object) 285 | case object 286 | when Array 287 | if object.all? {|x| x.kind_of? Net::DNS::Question} 288 | @question = object 289 | else 290 | raise ArgumentError, "Some of the elements is not an Net::DNS::Question object" 291 | end 292 | when Net::DNS::Question 293 | @question = [object] 294 | else 295 | raise ArgumentError, "Invalid argument, not a Question object nor an array of objects" 296 | end 297 | end 298 | 299 | # Assigns one or an array of Net::DNS::RR objects 300 | # to the answer section of this Net::DNS::Packet instance. 301 | def answer=(object) 302 | case object 303 | when Array 304 | if object.all? {|x| x.kind_of? Net::DNS::RR} 305 | @answer = object 306 | else 307 | raise ArgumentError, "Some of the elements is not an Net::DNS::RR object" 308 | end 309 | when Net::DNS::RR 310 | @answer = [object] 311 | else 312 | raise ArgumentError, "Invalid argument, not a RR object nor an array of objects" 313 | end 314 | end 315 | 316 | # Assigns one or an array of Net::DNS::RR objects 317 | # to the additional section of this Net::DNS::Packet instance. 318 | def additional=(object) 319 | case object 320 | when Array 321 | if object.all? {|x| x.kind_of? Net::DNS::RR} 322 | @additional = object 323 | else 324 | raise ArgumentError, "Some of the elements is not an Net::DNS::RR object" 325 | end 326 | when Net::DNS::RR 327 | @additional = [object] 328 | else 329 | raise ArgumentError, "Invalid argument, not a RR object nor an array of objects" 330 | end 331 | end 332 | 333 | # Assigns one or an array of Net::DNS::RR objects 334 | # to the authority section of this Net::DNS::Packet instance. 335 | def authority=(object) 336 | case object 337 | when Array 338 | if object.all? {|x| x.kind_of? Net::DNS::RR} 339 | @authority = object 340 | else 341 | raise ArgumentError, "Some of the elements is not an Net::DNS::RR object" 342 | end 343 | when Net::DNS::RR 344 | @authority = [object] 345 | else 346 | raise ArgumentError, "Invalid argument, not a RR object nor an array of objects" 347 | end 348 | end 349 | 350 | # Filters the elements in the +answer+ section based on the class given 351 | def elements(*types) 352 | if types.any? 353 | @answer.select do |elem| 354 | types.any? { |type| elem.kind_of?(type) } 355 | end 356 | else 357 | @answer 358 | end 359 | end 360 | 361 | # Iterates every address in the +answer+ section 362 | # of this Net::DNS::Packet instance. 363 | # 364 | # packet.each_address do |ip| 365 | # ping ip.to_s 366 | # end 367 | # 368 | # As you can see in the documentation for the Net::DNS::RR::A class, 369 | # the address returned is an instance of IPAddr class. 370 | def each_address(&block) 371 | elements(Net::DNS::RR::A, Net::DNS::RR::AAAA).map(&:address).each(&block) 372 | end 373 | 374 | # Iterates every nameserver in the +answer+ section 375 | # of this Net::DNS::Packet instance. 376 | # 377 | # packet.each_nameserver do |ns| 378 | # puts "Nameserver found: #{ns}" 379 | # end 380 | # 381 | def each_nameserver(&block) 382 | elements(Net::DNS::RR::NS).map(&:nsdname).each(&block) 383 | end 384 | 385 | # Iterates every exchange record in the +answer+ section 386 | # of this Net::DNS::Packet instance. 387 | # 388 | # packet.each_mx do |pref,name| 389 | # puts "Mail exchange #{name} has preference #{pref}" 390 | # end 391 | # 392 | def each_mx(&block) 393 | elements(Net::DNS::RR::MX).map{|elem| [elem.preference, elem.exchange]}.each(&block) 394 | end 395 | 396 | # Iterates every canonical name in the +answer+ section 397 | # of this Net::DNS::Packet instance. 398 | # 399 | # packet.each_cname do |cname| 400 | # puts "Canonical name: #{cname}" 401 | # end 402 | # 403 | def each_cname(&block) 404 | elements(Net::DNS::RR::CNAME).map(&:cname).each(&block) 405 | end 406 | 407 | # Iterates every pointer in the +answer+ section 408 | # of this Net::DNS::Packet instance. 409 | # 410 | # packet.each_ptr do |ptr| 411 | # puts "Pointer for resource: #{ptr}" 412 | # end 413 | # 414 | def each_ptr(&block) 415 | elements(Net::DNS::RR::PTR).map(&:ptrdname).each(&block) 416 | end 417 | 418 | # Returns the packet size in bytes. 419 | # 420 | # Resolver("www.google.com") do |packet| 421 | # puts packet.size + " bytes"} 422 | # end 423 | # # => 484 bytes 424 | # 425 | def size 426 | data.size 427 | end 428 | 429 | # Checks whether the query returned a NXDOMAIN error, 430 | # meaning the queried domain name doesn't exist. 431 | # 432 | # %w[a.com google.com ibm.com d.com].each do |domain| 433 | # response = Net::DNS::Resolver.new.send(domain) 434 | # puts "#{domain} doesn't exist" if response.nxdomain? 435 | # end 436 | # # => a.com doesn't exist 437 | # # => d.com doesn't exist 438 | # 439 | def nxdomain? 440 | header.rCode.code == Net::DNS::Header::RCode::NAME 441 | end 442 | 443 | 444 | # Creates a new instance of Net::DNS::Packet class from binary data, 445 | # taken out from a network stream. For example: 446 | # 447 | # # udp_socket is an UDPSocket waiting for a response 448 | # ans = udp_socket.recvfrom(1500) 449 | # packet = Net::DNS::Packet::parse(ans) 450 | # 451 | # An optional +from+ argument can be used to specify the information 452 | # of the sender. If data is passed as is from a Socket#recvfrom call, 453 | # the method will accept it. 454 | # 455 | # Be sure that your network data is clean from any UDP/TCP header, 456 | # especially when using RAW sockets. 457 | # 458 | def self.parse(*args) 459 | o = allocate 460 | o.send(:new_from_data, *args) 461 | o 462 | end 463 | 464 | private 465 | 466 | # New packet from binary data 467 | def new_from_data(data, from = nil) 468 | unless from 469 | if data.kind_of? Array 470 | data, from = data 471 | else 472 | from = [0, 0, "0.0.0.0", "unknown"] 473 | end 474 | end 475 | 476 | @answerfrom = from[2] + ":" + from[1].to_s 477 | @answersize = data.size 478 | @logger = Logger.new $stdout 479 | @logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN 480 | 481 | #------------------------------------------------------------ 482 | # Header section 483 | #------------------------------------------------------------ 484 | offset = Net::DNS::HFIXEDSZ 485 | @header = Net::DNS::Header.parse(data[0..offset-1]) 486 | 487 | @logger.debug ";; HEADER SECTION" 488 | @logger.debug @header.inspect 489 | 490 | #------------------------------------------------------------ 491 | # Question section 492 | #------------------------------------------------------------ 493 | section = @header.opCode == "UPDATE" ? "ZONE" : "QUESTION" 494 | @logger.debug ";; #{section} SECTION (#{@header.qdCount} record#{@header.qdCount == 1 ? '': 's'})" 495 | 496 | @question = [] 497 | @header.qdCount.times do 498 | qobj,offset = parse_question(data,offset) 499 | @question << qobj 500 | @logger.debug ";; #{qobj.inspect}" 501 | end 502 | 503 | #------------------------------------------------------------ 504 | # Answer/prerequisite section 505 | #------------------------------------------------------------ 506 | section = @header.opCode == "UPDATE" ? "PREREQUISITE" : "ANSWER" 507 | @logger.debug ";; #{section} SECTION (#{@header.qdCount} record#{@header.qdCount == 1 ? '': 's'})" 508 | 509 | @answer = [] 510 | @header.anCount.times do 511 | begin 512 | rrobj,offset = Net::DNS::RR.parse_packet(data,offset) 513 | @answer << rrobj 514 | @logger.debug rrobj.inspect 515 | rescue NameError => e 516 | warn "Net::DNS unsupported record type: #{e.message}" 517 | end 518 | end 519 | 520 | #------------------------------------------------------------ 521 | # Authority/update section 522 | #------------------------------------------------------------ 523 | section = @header.opCode == "UPDATE" ? "UPDATE" : "AUTHORITY" 524 | @logger.debug ";; #{section} SECTION (#{@header.nsCount} record#{@header.nsCount == 1 ? '': 's'})" 525 | 526 | @authority = [] 527 | @header.nsCount.times do 528 | begin 529 | rrobj,offset = Net::DNS::RR.parse_packet(data,offset) 530 | @authority << rrobj 531 | @logger.debug rrobj.inspect 532 | rescue NameError => e 533 | warn "Net::DNS unsupported record type: #{e.message}" 534 | end 535 | end 536 | 537 | #------------------------------------------------------------ 538 | # Additional section 539 | #------------------------------------------------------------ 540 | @logger.debug ";; ADDITIONAL SECTION (#{@header.arCount} record#{@header.arCount == 1 ? '': 's'})" 541 | 542 | @additional = [] 543 | @header.arCount.times do 544 | begin 545 | rrobj,offset = Net::DNS::RR.parse_packet(data,offset) 546 | @additional << rrobj 547 | @logger.debug rrobj.inspect 548 | rescue NameError => e 549 | warn "Net::DNS unsupported record type: #{e.message}" 550 | end 551 | end 552 | 553 | end 554 | 555 | 556 | # Parse question section 557 | def parse_question(data,offset) 558 | size = (dn_expand(data, offset)[1] - offset) + (2 * Net::DNS::INT16SZ) 559 | return [Net::DNS::Question.parse(data[offset, size]), offset + size] 560 | rescue StandardError => e 561 | raise PacketError, "Caught exception, maybe packet malformed => #{e.message}" 562 | end 563 | 564 | end 565 | 566 | end 567 | end 568 | -------------------------------------------------------------------------------- /lib/net/dns/header.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module DNS 3 | 4 | # DNS packet header class 5 | # 6 | # The Net::DNS::Header class represents the header portion of a 7 | # DNS packet. An Header object is created whenever a new packet 8 | # is parsed or as user request. 9 | # 10 | # header = Net::DNS::Header.new 11 | # # ;; id = 18123 12 | # # ;; qr = 0 opCode: 0 aa = 0 tc = 0 rd = 1 13 | # # ;; ra = 0 ad = 0 cd = 0 rcode = 0 14 | # # ;; qdCount = 1 anCount = 0 nsCount = 0 arCount = 0 15 | # 16 | # header.format 17 | # # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 18 | # # | 18123 | 19 | # # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 20 | # # |0| 0 |0|0|1|0|0| 0 | 0 | 21 | # # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 22 | # # | 1 | 23 | # # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 24 | # # | 0 | 25 | # # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 26 | # # | 0 | 27 | # # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 28 | # # | 0 | 29 | # # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 30 | # 31 | # # packet is an instance of Net::DNS::Packet 32 | # header = packet.header 33 | # puts "Answer is #{header.auth? ? '' : 'non'} authoritative" 34 | # 35 | # A lot of methods were written to keep a compatibility layer with 36 | # the Perl version of the library, as long as methods name which are 37 | # more or less the same. 38 | # 39 | class Header 40 | 41 | # A wrong +count+ parameter has been passed. 42 | class WrongCountError < ArgumentError 43 | end 44 | 45 | # A wrong +recursive+ parameter has been passed. 46 | class WrongRecursiveError < ArgumentError 47 | end 48 | 49 | # An invalid +opCode+ has been specified. 50 | class WrongOpcodeError < ArgumentError 51 | end 52 | 53 | # Base error class. 54 | class Error < StandardError 55 | end 56 | 57 | 58 | # DNS Header RCode handling class 59 | # 60 | # It should be used internally by Net::DNS::Header class. However, it's still 61 | # possible to instantiate it directly. 62 | # 63 | # require 'net/dns/header' 64 | # rcode = Net::DNS::Header::RCode.new 0 65 | # 66 | # The RCode class represents the RCode field in the Header portion of a 67 | # DNS packet. This field (called Response Code) is used to get informations 68 | # about the status of a DNS operation, such as a query or an update. These 69 | # are the values in the original Mockapetris's standard (RFC1035): 70 | # 71 | # * 0 No error condition 72 | # * 1 Format error - The name server was unable to interpret 73 | # the query. 74 | # * 2 Server failure - The name server was 75 | # unable to process this query due to a 76 | # problem with the name server. 77 | # * 3 Name Error - Meaningful only for 78 | # responses from an authoritative name 79 | # server, this code means that the 80 | # domain name referenced in the query does 81 | # not exist. 82 | # * 4 Not Implemented - The name server does 83 | # not support the requested kind of query. 84 | # * 5 Refused - The name server refuses to 85 | # perform the specified operation for 86 | # policy reasons. For example, a name 87 | # server may not wish to provide the 88 | # information to the particular requester, 89 | # or a name server may not wish to perform 90 | # a particular operation (e.g., zone 91 | # transfer) for particular data. 92 | # * 6-15 Reserved for future use. 93 | # 94 | # In the next DNS RFCs, codes 6-15 has been assigned to the following 95 | # errors: 96 | # 97 | # * 6 YXDomain 98 | # * 7 YXRRSet 99 | # * 8 NXRRSet 100 | # * 9 NotAuth 101 | # * 10 NotZone 102 | # 103 | # More RCodes has to come for TSIGs and other operations. 104 | # 105 | class RCode 106 | 107 | # Constant for +rcode+ Response Code No Error 108 | NOERROR = 0 109 | # Constant for +rcode+ Response Code Format Error 110 | FORMAT = 1 111 | # Constant for +rcode+ Response Code Server Format Error 112 | SERVER = 2 113 | # Constant for +rcode+ Response Code Name Error 114 | NAME = 3 115 | # Constant for +rcode+ Response Code Not Implemented Error 116 | NOTIMPLEMENTED = 4 117 | # Constant for +rcode+ Response Code Refused Error 118 | REFUSED = 5 119 | 120 | 121 | 122 | RCodeType = %w[NoError FormErr ServFail NXDomain NotImp 123 | Refused YXDomain YXRRSet NXRRSet NotAuth NotZone] 124 | 125 | RCodeErrorString = ["No errors", 126 | "The name server was unable to interpret the query", 127 | "The name server was unable to process this query due to problem with the name server", 128 | "Domain name referenced in the query does not exists", 129 | "The name server does not support the requested kind of query", 130 | "The name server refuses to perform the specified operation for policy reasons", 131 | "", 132 | "", 133 | "", 134 | "", 135 | ""] 136 | 137 | attr_reader :code, :type, :explanation 138 | 139 | def initialize(code) 140 | if (0..10).include? code 141 | @code = code 142 | @type = RCodeType[code] 143 | @explanation = RCodeErrorString[code] 144 | else 145 | raise ArgumentError, "RCode `#{code}' out of range" 146 | end 147 | end 148 | 149 | def to_s 150 | @code.to_s 151 | end 152 | end 153 | 154 | 155 | # Listing: http://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-5 156 | # Constant for +opCode+ query 157 | QUERY = 0 158 | # Constant for +opCode+ iquery 159 | IQUERY = 1 160 | # Constant for +opCode+ status 161 | STATUS = 2 162 | # Constant for +opCode+ notify 163 | NOTIFY = 4 164 | # Array with given strings 165 | OPARR = %w[QUERY IQUERY STATUS UNASSIGNED NOTIFY] 166 | 167 | # Reader for +id+ attribute 168 | attr_reader :id 169 | # Reader for the operational code 170 | attr_reader :opCode 171 | # Reader for the rCode instance 172 | attr_reader :rCode 173 | # Reader for question section entries number 174 | attr_reader :qdCount 175 | # Reader for answer section entries number 176 | attr_reader :anCount 177 | # Reader for authority section entries number 178 | attr_reader :nsCount 179 | # Reader for addictional section entries number 180 | attr_reader :arCount 181 | 182 | # Creates a new Net::DNS::Header object with the desired values, 183 | # which can be specified as an Hash argument. When called without 184 | # arguments, defaults are used. If a data string is passed, values 185 | # are taken from parsing the string. 186 | # 187 | # Examples: 188 | # 189 | # # Create a new Net::DNS::Header object 190 | # header = Net::DNS::Header.new 191 | # 192 | # # Create a new Net::DNS::Header object passing values 193 | # header = Net::DNS::Header.new(:opCode => 1, :rd => 0) 194 | # 195 | # # Create a new Net::DNS::Header object with binary data 196 | # header = Net::DNS::Header.new(data) 197 | # 198 | # Default values are: 199 | # 200 | # :id => auto generated 201 | # :qr => 0 # Query response flag 202 | # :aa => 0 # Authoritative answer flag 203 | # :tc => 0 # Truncated packet flag 204 | # :ra => 0 # Recursiond available flag 205 | # :rCode => 0 # Response code (status of the query) 206 | # :opCode => 0 # Operational code (purpose of the query) 207 | # :cd => 0 # Checking disable flag 208 | # :ad => 0 # Only relevant in DNSSEC context 209 | # :rd => 1 # Recursion desired flag 210 | # :qdCount => 1 # Number of questions in the dns packet 211 | # :anCount => 0 # Number of answer RRs in the dns packet 212 | # :nsCount => 0 # Number of authoritative RRs in the dns packet 213 | # :arCount => 0 # Number of additional RRs in the dns packet 214 | # 215 | # See also each option for a detailed explanation of usage. 216 | # 217 | def initialize(arg = {}) 218 | if arg.kind_of? Hash 219 | new_from_hash(arg) 220 | else 221 | raise ArgumentError, "Wrong argument class `#{arg.class}'" 222 | end 223 | end 224 | 225 | # Creates a new Net::DNS::Header object from binary data, which is 226 | # passed as a string object as argument. 227 | # The configurations parameters are taken from parsing the string. 228 | # 229 | # Example: 230 | # 231 | # # Create a new Net::DNS::Header object with binary data 232 | # header = Net::DNS::Header.new(data) 233 | # 234 | # header.auth? 235 | # #=> "true" if it comes from authoritative name server 236 | # 237 | def self.parse(arg) 238 | if arg.kind_of? String 239 | o = allocate 240 | o.send(:new_from_binary, arg) 241 | o 242 | else 243 | raise ArgumentError, "Wrong argument class `#{arg.class}'" 244 | end 245 | end 246 | 247 | # Inspect method, prints out all the options and relative values. 248 | # 249 | # p Net::DNS::Header.new 250 | # # ;; id = 18123 251 | # # ;; qr = 0 opCode: 0 aa = 0 tc = 0 rd = 1 252 | # # ;; ra = 0 ad = 0 cd = 0 rcode = 0 253 | # # ;; qdCount = 1 anCount = 0 nsCount = 0 arCount = 0 254 | # 255 | # This method will maybe be changed in the future to a more pretty 256 | # way of display output. 257 | # 258 | def inspect 259 | ";; id = #@id\n" + 260 | if false # @opCode == "UPDATE" 261 | #do stuff 262 | else 263 | ";; qr = #@qr\t" + 264 | "opCode: #{opCode_str}\t" + 265 | "aa = #@aa\t" + 266 | "tc = #@tc\t" + 267 | "rd = #@rd\n" + 268 | ";; ra = #@ra\t" + 269 | "ad = #@ad\t" + 270 | "cd = #@cd\t" + 271 | "rcode = #{@rCode.type}\n" + 272 | ";; qdCount = #@qdCount\t"+ 273 | "anCount = #@anCount\t"+ 274 | "nsCount = #@nsCount\t"+ 275 | "arCount = #@arCount\n" 276 | end 277 | end 278 | 279 | # The Net::DNS::Header#format method prints out the header 280 | # in a special ascii representation of data, in a way 281 | # similar to those often found on RFCs. 282 | # 283 | # p Net::DNS::Header.new.format 284 | # # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 285 | # # | 18123 | 286 | # # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 287 | # # |0| 0 |0|0|1|0|0| 0 | 0 | 288 | # # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 289 | # # | 1 | 290 | # # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 291 | # # | 0 | 292 | # # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 293 | # # | 0 | 294 | # # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 295 | # # | 0 | 296 | # # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 297 | # 298 | # This can be very usefull for didactical purpouses :) 299 | # 300 | def format 301 | del = ("+-" * 16) + "+\n" 302 | len = del.length 303 | str = del + "|" + @id.to_s.center(len-3) + "|\n" 304 | str += del + "|" + @qr.to_s 305 | str += "|" + @opCode.to_s.center(7) 306 | str += "|" + @aa.to_s 307 | str += "|" + @tc.to_s 308 | str += "|" + @rd.to_s 309 | str += "|" + @ra.to_s 310 | str += "|" + @ad.to_s 311 | str += "|" + @cd.to_s.center(3) 312 | str += "|" + @rCode.to_s.center(7) + "|\n" 313 | str += del + "|" + @qdCount.to_s.center(len-3) + "|\n" 314 | str += del + "|" + @anCount.to_s.center(len-3) + "|\n" 315 | str += del + "|" + @nsCount.to_s.center(len-3) + "|\n" 316 | str += del + "|" + @arCount.to_s.center(len-3) + "|\n" + del 317 | str 318 | end 319 | 320 | # Returns the header data in binary format, appropriate 321 | # for use in a DNS query packet. 322 | # 323 | # hdata = header.data 324 | # puts "Header is #{hdata.size} bytes" 325 | # 326 | def data 327 | arr = [] 328 | arr.push(@id) 329 | arr.push((@qr<<7)|(@opCode<<3)|(@aa<<2)|(@tc<<1)|@rd) 330 | arr.push((@ra<<7)|(@ad<<5)|(@cd<<4)|@rCode.code) 331 | arr.push(@qdCount) 332 | arr.push(@anCount) 333 | arr.push(@nsCount) 334 | arr.push(@arCount) 335 | arr.pack("n C2 n4") 336 | end 337 | 338 | # Set the ID for the current header. Useful when 339 | # performing security tests. 340 | # 341 | def id=(val) 342 | if (0..65535).include? val 343 | @id = val 344 | else 345 | raise ArgumentError, "ID `#{val}' out of range" 346 | end 347 | end 348 | 349 | # Checks whether the header is a query (+qr+ bit set to 0) 350 | # 351 | def query? 352 | @qr == 0 353 | end 354 | 355 | # Set the +qr+ query response flag to be either +true+ or 356 | # +false+. You can also use the values 0 and 1. This flag 357 | # indicates if the DNS packet contains a query or an answer, 358 | # so it should be set to +true+ in DNS answer packets. 359 | # If +qr+ is +true+, the packet is a response. 360 | # 361 | def qr=(val) 362 | case val 363 | when true 364 | @qr = 1 365 | when false 366 | @qr = 0 367 | when 0,1 368 | @qr = val 369 | else 370 | raise ArgumentError, ":qr must be true(or 1) or false(or 0)" 371 | end 372 | end 373 | 374 | # Checks whether the header is a response 375 | # (+qr+ bit set to 1) 376 | # 377 | def response? 378 | @qr == 1 379 | end 380 | 381 | # Returns a string representation of the +opCode+ 382 | # 383 | # puts "Packet is a #{header.opCode_str}" 384 | # #=> Packet is a QUERY 385 | # 386 | def opCode_str 387 | OPARR[@opCode] 388 | end 389 | 390 | # Set the +opCode+ variable to a new value. This fields indicates 391 | # the type of the question present in the DNS packet; +val+ can be 392 | # one of the values QUERY, IQUERY or STATUS. 393 | # 394 | # * QUERY is the standard DNS query 395 | # * IQUERY is the inverse query 396 | # * STATUS is used to query the nameserver for its status 397 | # * NOTIFY is to notify a slave that it should check for an updated SOA on the master 398 | # 399 | # Example: 400 | # 401 | # include Net::DNS 402 | # header = Header.new 403 | # header.opCode = Header::STATUS 404 | # 405 | def opCode=(val) 406 | if [0, 1, 2, 4].include? val 407 | @opCode = val 408 | else 409 | raise WrongOpcodeError, "Wrong opCode value (#{val}), must be QUERY, IQUERY, STATUS, or NOTIFY" 410 | end 411 | end 412 | 413 | # Checks whether the response is authoritative 414 | # 415 | # if header.auth? 416 | # puts "Response is authoritative" 417 | # else 418 | # puts "Answer is NOT authoritative" 419 | # end 420 | # 421 | def auth? 422 | @aa == 1 423 | end 424 | 425 | # Set the +aa+ flag (authoritative answer) to either +true+ 426 | # or +false+. You can also use 0 or 1. 427 | # 428 | # This flag indicates whether a DNS answer packet contains 429 | # authoritative data, meaning that is was generated by a 430 | # nameserver authoritative for the domain of the question. 431 | # 432 | # Must only be set to +true+ in DNS answer packets. 433 | # 434 | def aa=(val) 435 | case val 436 | when true 437 | @aa = 1 438 | when false 439 | @aa = 0 440 | when 0,1 441 | @aa = val 442 | else 443 | raise ArgumentError, ":aa must be true(or 1) or false(or 0)" 444 | end 445 | end 446 | 447 | # Checks whether the packet was truncated 448 | # 449 | # # Sending packet using UDP 450 | # if header.truncated? 451 | # puts "Warning, packet has been truncated!" 452 | # # Sending packet using TCP 453 | # end 454 | # # Do something with the answer 455 | # 456 | def truncated? 457 | @tc == 1 458 | end 459 | 460 | # Set the +tc+ flag (truncated packet) to either +true+ 461 | # ot +false+. You can also use 0 or 1. 462 | # 463 | # The truncated flag is used in response packets to indicate 464 | # that the amount of data to be trasmitted exceedes the 465 | # maximum allowed by the protocol in use, tipically UDP, and 466 | # that the data present in the packet has been truncated. 467 | # A different protocol (such has TCP) need to be used to 468 | # retrieve full data. 469 | # 470 | # Must only be set in DNS answer packets. 471 | # 472 | def tc=(val) 473 | case val 474 | when true 475 | @tc = 1 476 | when false 477 | @tc = 0 478 | when 0,1 479 | @tc = val 480 | else 481 | raise ArgumentError, ":tc must be true(or 1) or false(or 0)" 482 | end 483 | end 484 | 485 | # Checks whether the packet has a recursion bit 486 | # set, meaning that recursion is desired 487 | # 488 | def recursive? 489 | @rd == 1 490 | end 491 | 492 | # Sets the recursion desidered bit. 493 | # Remember that recursion query support is 494 | # optional. 495 | # 496 | # header.recursive = true 497 | # hdata = header.data # suitable for sending 498 | # 499 | # Consult RFC1034 and RFC1035 for a detailed explanation 500 | # of how recursion works. 501 | # 502 | def recursive=(val) 503 | case val 504 | when true 505 | @rd = 1 506 | when false 507 | @rd = 0 508 | when 1 509 | @rd = 1 510 | when 0 511 | @rd = 0 512 | else 513 | raise WrongRecursiveError, "Wrong value (#{val}), please specify true (1) or false (0)" 514 | end 515 | end 516 | 517 | # Alias for Header#recursive= to keep compatibility 518 | # with the Perl version. 519 | # 520 | def rd=(val) 521 | self.recursive = val 522 | end 523 | 524 | # Checks whether recursion is available. 525 | # This flag is usually set by nameservers to indicate 526 | # that they support recursive-type queries. 527 | # 528 | def r_available? 529 | @ra == 1 530 | end 531 | 532 | # Set the +ra+ flag (recursion available) to either +true+ or 533 | # +false+. You can also use 0 and 1. 534 | # 535 | # This flag must only be set in DNS answer packets. 536 | # 537 | def ra=(val) 538 | case val 539 | when true 540 | @ra = 1 541 | when false 542 | @ra = 0 543 | when 0,1 544 | @ra = val 545 | else 546 | raise ArgumentError, ":ra must be true(or 1) or false(or 0)" 547 | end 548 | end 549 | 550 | # Checks whether checking is enabled or disabled. 551 | # 552 | # Checking is enabled by default. 553 | # 554 | def checking? 555 | @cd == 0 556 | end 557 | 558 | # Set the +cd+ flag (checking disabled) to either +true+ 559 | # ot +false+. You can also use 0 or 1. 560 | # 561 | def cd=(val) 562 | case val 563 | when true 564 | @cd = 1 565 | when false 566 | @cd = 0 567 | when 0,1 568 | @cd = val 569 | else 570 | raise ArgumentError, ":cd must be true(or 1) or false(or 0)" 571 | end 572 | end 573 | 574 | # Checks whether +ad+ flag has been set. 575 | # 576 | # This flag is only relevant in DNSSEC context. 577 | # 578 | def verified? 579 | @ad == 1 580 | end 581 | 582 | # Set the +ad+ flag to either +true+ 583 | # ot +false+. You can also use 0 or 1. 584 | # 585 | # The AD bit is only set on answers where signatures have 586 | # been cryptographically verified or the server is 587 | # authoritative for the data and is allowed to set the bit by policy. 588 | # 589 | def ad=(val) 590 | case val 591 | when true 592 | @ad = 1 593 | when false 594 | @ad = 0 595 | when 0,1 596 | @ad = val 597 | else 598 | raise ArgumentError, ":ad must be true(or 1) or false(or 0)" 599 | end 600 | end 601 | 602 | # Returns an error array for the header response code, or 603 | # +nil+ if no error is generated. 604 | # 605 | # error, cause = header.rCode_str 606 | # puts "Error #{error} cause by: #{cause}" if error 607 | # #=> Error ForErr caused by: The name server 608 | # #=> was unable to interpret the query 609 | # 610 | def rCode_str 611 | return rCode.type, rCode.explanation 612 | end 613 | 614 | # Checks for errors in the DNS packet 615 | # 616 | # unless header.error? 617 | # puts "No errors in DNS answer packet" 618 | # end 619 | # 620 | def error? 621 | @rCode.code > 0 622 | end 623 | 624 | # Set the rCode value. This should only be done in DNS 625 | # answer packets. 626 | # 627 | def rCode=(val) 628 | @rCode = RCode.new(val) 629 | end 630 | 631 | # Sets the number of entries in a question section 632 | # 633 | def qdCount=(val) 634 | if (0..65535).include? val 635 | @qdCount = val 636 | else 637 | raise WrongCountError, "Wrong number of count (#{val}), must be 0-65535" 638 | end 639 | end 640 | 641 | # Sets the number of RRs in an answer section 642 | # 643 | def anCount=(val) 644 | if (0..65535).include? val 645 | @anCount = val 646 | else 647 | raise WrongCountError, "Wrong number of count (#{val}), must be 0-65535" 648 | end 649 | end 650 | 651 | # Sets the number of RRs in an authority section 652 | # 653 | def nsCount=(val) 654 | if (0..65535).include? val 655 | @nsCount = val 656 | else 657 | raise WrongCountError, "Wrong number of count (#{val}), must be 0-65535" 658 | end 659 | end 660 | 661 | # Sets the number of RRs in an addictional section 662 | # 663 | def arCount=(val) 664 | if (0..65535).include? val 665 | @arCount = val 666 | else 667 | raise WrongCountError, "Wrong number of count: `#{val}' must be 0-65535" 668 | end 669 | end 670 | 671 | private 672 | 673 | def new_from_scratch 674 | @id = genID # generate ad unique id 675 | @qr = @aa = @tc = @ra = @ad = @cd = 0 676 | @rCode = RCode.new(0) # no error 677 | @anCount = @nsCount = @arCount = 0 678 | @rd = @qdCount = 1 679 | @opCode = QUERY # standard query, default message 680 | end 681 | 682 | def new_from_binary(str) 683 | unless str.size == Net::DNS::HFIXEDSZ 684 | raise ArgumentError, "Header binary data has wrong size: `#{str.size}' bytes" 685 | end 686 | arr = str.unpack("n C2 n4") 687 | @id = arr[0] 688 | @qr = (arr[1] >> 7) & 0x01 689 | @opCode = (arr[1] >> 3) & 0x0F 690 | @aa = (arr[1] >> 2) & 0x01 691 | @tc = (arr[1] >> 1) & 0x01 692 | @rd = arr[1] & 0x1 693 | @ra = (arr[2] >> 7) & 0x01 694 | @ad = (arr[2] >> 5) & 0x01 695 | @cd = (arr[2] >> 4) & 0x01 696 | @rCode = RCode.new(arr[2] & 0xf) 697 | @qdCount = arr[3] 698 | @anCount = arr[4] 699 | @nsCount = arr[5] 700 | @arCount = arr[6] 701 | end 702 | 703 | def new_from_hash(hash) 704 | new_from_scratch 705 | hash.each do |key,val| 706 | eval "self.#{key.to_s} = val" 707 | end 708 | end 709 | 710 | def genID 711 | rand(65535) 712 | end 713 | 714 | end 715 | 716 | end 717 | end 718 | -------------------------------------------------------------------------------- /lib/net/dns/resolver.rb: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | require 'socket' 3 | require 'timeout' 4 | require 'net/dns/packet' 5 | require 'net/dns/resolver/timeouts' 6 | require 'packetfu' 7 | require 'ipaddr' 8 | 9 | # Resolver helper method. 10 | # 11 | # Calling the resolver directly: 12 | # 13 | # puts Resolver("www.google.com").answer.size 14 | # # => 5 15 | # 16 | # An optional block can be passed yielding the Net::DNS::Packet object. 17 | # 18 | # Resolver("www.google.com") { |packet| puts packet.size + " bytes" } 19 | # # => 484 bytes 20 | # 21 | def Resolver(name, type = Net::DNS::A, cls = Net::DNS::IN, &block) 22 | resolver = Net::DNS::Resolver.start(name, type, cls) 23 | if block_given? 24 | yield resolver 25 | else 26 | resolver 27 | end 28 | end 29 | 30 | module Net 31 | module DNS 32 | 33 | include Logger::Severity 34 | 35 | # = Net::DNS::Resolver - DNS resolver class 36 | # 37 | # The Net::DNS::Resolver class implements a complete DNS resolver written 38 | # in pure Ruby, without a single C line of code. It has all of the 39 | # tipical properties of an evoluted resolver, and a bit of OO which 40 | # comes from having used Ruby. 41 | # 42 | # This project started as a porting of the Net::DNS Perl module, 43 | # written by Martin Fuhr, but turned out (in the last months) to be 44 | # an almost complete rewriting. Well, maybe some of the features of 45 | # the Perl version are still missing, but guys, at least this is 46 | # readable code! 47 | # 48 | # == Environment 49 | # 50 | # The Following Environment variables can also be used to configure 51 | # the resolver: 52 | # 53 | # * +RES_NAMESERVERS+: A space-separated list of nameservers to query. 54 | # 55 | # # Bourne Shell 56 | # $ RES_NAMESERVERS="192.168.1.1 192.168.2.2 192.168.3.3" 57 | # $ export RES_NAMESERVERS 58 | # 59 | # # C Shell 60 | # % setenv RES_NAMESERVERS "192.168.1.1 192.168.2.2 192.168.3.3" 61 | # 62 | # * +RES_SEARCHLIST+: A space-separated list of domains to put in the 63 | # search list. 64 | # 65 | # # Bourne Shell 66 | # $ RES_SEARCHLIST="example.com sub1.example.com sub2.example.com" 67 | # $ export RES_SEARCHLIST 68 | # 69 | # # C Shell 70 | # % setenv RES_SEARCHLIST "example.com sub1.example.com sub2.example.com" 71 | # 72 | # * +LOCALDOMAIN+: The default domain. 73 | # 74 | # # Bourne Shell 75 | # $ LOCALDOMAIN=example.com 76 | # $ export LOCALDOMAIN 77 | # 78 | # # C Shell 79 | # % setenv LOCALDOMAIN example.com 80 | # 81 | # * +RES_OPTIONS+: A space-separated list of resolver options to set. 82 | # Options that take values are specified as option:value. 83 | # 84 | # # Bourne Shell 85 | # $ RES_OPTIONS="retrans:3 retry:2 debug" 86 | # $ export RES_OPTIONS 87 | # 88 | # # C Shell 89 | # % setenv RES_OPTIONS "retrans:3 retry:2 debug" 90 | # 91 | class Resolver 92 | 93 | class Error < StandardError 94 | end 95 | 96 | class NoResponseError < Error 97 | end 98 | 99 | # An hash with the defaults values of almost all the 100 | # configuration parameters of a resolver object. See 101 | # the description for each parameter to have an 102 | # explanation of its usage. 103 | Defaults = { 104 | :config_file => "/etc/resolv.conf", 105 | :log_file => $stdout, 106 | :port => 53, 107 | :searchlist => [], 108 | :nameservers => [IPAddr.new("127.0.0.1")], 109 | :domain => "", 110 | :source_port => 0, 111 | :source_address => IPAddr.new("0.0.0.0"), 112 | :source_address_inet6 => IPAddr.new('::'), 113 | :interface => "eth0", 114 | :retry_interval => 5, 115 | :retry_number => 4, 116 | :recursive => true, 117 | :defname => true, 118 | :dns_search => true, 119 | :use_tcp => false, 120 | :ignore_truncated => false, 121 | :packet_size => 512, 122 | :tcp_timeout => TcpTimeout.new(5), 123 | :udp_timeout => UdpTimeout.new(5), 124 | } 125 | 126 | 127 | class << self 128 | 129 | C = Object.const_get(defined?(RbConfig) ? :RbConfig : :Config)::CONFIG 130 | 131 | # Quick resolver method. Bypass the configuration using 132 | # the defaults. 133 | # 134 | # Net::DNS::Resolver.start "www.google.com" 135 | # 136 | def start(*params) 137 | new.search(*params) 138 | end 139 | 140 | # Returns true if running on a Windows platform. 141 | # 142 | # Note. This method doesn't rely on the RUBY_PLATFORM constant 143 | # because the comparison will fail when running on JRuby. 144 | # On JRuby RUBY_PLATFORM == 'java'. 145 | def platform_windows? 146 | !!(C["host_os"] =~ /msdos|mswin|djgpp|mingw/i) 147 | end 148 | 149 | end 150 | 151 | 152 | # Creates a new resolver object. 153 | # 154 | # Argument +config+ can either be empty or be an hash with 155 | # some configuration parameters. To know what each parameter 156 | # do, look at the description of each. 157 | # Some example: 158 | # 159 | # # Use the sistem defaults 160 | # res = Net::DNS::Resolver.new 161 | # 162 | # # Specify a configuration file 163 | # res = Net::DNS::Resolver.new(:config_file => '/my/dns.conf') 164 | # 165 | # # Set some option 166 | # res = Net::DNS::Resolver.new(:nameservers => "172.16.1.1", 167 | # :recursive => false, 168 | # :retry => 10) 169 | # 170 | # == Config file 171 | # 172 | # Net::DNS::Resolver uses a config file to read the usual 173 | # values a resolver needs, such as nameserver list and 174 | # domain names. On UNIX systems the defaults are read from the 175 | # following files, in the order indicated: 176 | # 177 | # * /etc/resolv.conf 178 | # * $HOME/.resolv.conf 179 | # * ./.resolv.conf 180 | # 181 | # The following keywords are recognized in resolver configuration files: 182 | # 183 | # * domain: the default domain. 184 | # * search: a space-separated list of domains to put in the search list. 185 | # * nameserver: a space-separated list of nameservers to query. 186 | # 187 | # Files except for /etc/resolv.conf must be owned by the effective userid 188 | # running the program or they won't be read. In addition, several environment 189 | # variables can also contain configuration information; see Environment 190 | # in the main description for Resolver class. 191 | # 192 | # On Windows Systems, an attempt is made to determine the system defaults 193 | # using the registry. This is still a work in progress; systems with many 194 | # dynamically configured network interfaces may confuse Net::DNS. 195 | # 196 | # You can include a configuration file of your own when creating a resolver 197 | # object: 198 | # 199 | # # Use my own configuration file 200 | # my $res = Net::DNS::Resolver->new(config_file => '/my/dns.conf'); 201 | # 202 | # This is supported on both UNIX and Windows. Values pulled from a custom 203 | # configuration file override the the system's defaults, but can still be 204 | # overridden by the other arguments to Resolver::new. 205 | # 206 | # Explicit arguments to Resolver::new override both the system's defaults 207 | # and the values of the custom configuration file, if any. 208 | # 209 | # == Parameters 210 | # 211 | # The following arguments to Resolver::new are supported: 212 | # 213 | # * nameservers: an array reference of nameservers to query. 214 | # * searchlist: an array reference of domains. 215 | # * recurse 216 | # * debug 217 | # * domain 218 | # * port 219 | # * srcaddr 220 | # * srcport 221 | # * tcp_timeout 222 | # * udp_timeout 223 | # * retrans 224 | # * retry 225 | # * usevc 226 | # * stayopen 227 | # * igntc 228 | # * defnames 229 | # * dnsrch 230 | # * persistent_tcp 231 | # * persistent_udp 232 | # * dnssec 233 | # 234 | # For more information on any of these options, please consult the 235 | # method of the same name. 236 | # 237 | # == Disclaimer 238 | # 239 | # Part of the above documentation is taken from the one in the 240 | # Net::DNS::Resolver Perl module. 241 | # 242 | def initialize(config = {}) 243 | raise ArgumentError, "Expected `config' to be a Hash" unless config.is_a?(Hash) 244 | 245 | # config.downcase_keys! 246 | @config = Defaults.merge config 247 | @raw = false 248 | 249 | # New logger facility 250 | @logger = Logger.new(@config[:log_file]) 251 | @logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN 252 | 253 | #------------------------------------------------------------ 254 | # Resolver configuration will be set in order from: 255 | # 1) initialize arguments 256 | # 2) ENV variables 257 | # 3) config file 258 | # 4) defaults (and /etc/resolv.conf for config) 259 | #------------------------------------------------------------ 260 | 261 | 262 | 263 | #------------------------------------------------------------ 264 | # Parsing config file 265 | #------------------------------------------------------------ 266 | parse_config_file 267 | 268 | #------------------------------------------------------------ 269 | # Parsing ENV variables 270 | #------------------------------------------------------------ 271 | parse_environment_variables 272 | 273 | #------------------------------------------------------------ 274 | # Parsing arguments 275 | #------------------------------------------------------------ 276 | config.each do |key,val| 277 | next if key == :log_file or key == :config_file 278 | begin 279 | eval "self.#{key.to_s} = val" 280 | rescue NoMethodError 281 | raise ArgumentError, "Option #{key} not valid" 282 | end 283 | end 284 | end 285 | 286 | # Get the resolver search list, returned as an array of entries. 287 | # 288 | # res.searchlist 289 | # #=> ["example.com","a.example.com","b.example.com"] 290 | # 291 | def searchlist 292 | @config[:searchlist].inspect 293 | end 294 | 295 | # Set the resolver searchlist. 296 | # +arg+ can be a single string or an array of strings. 297 | # 298 | # res.searchstring = "example.com" 299 | # res.searchstring = ["example.com","a.example.com","b.example.com"] 300 | # 301 | # Note that you can also append a new name to the searchlist. 302 | # 303 | # res.searchlist << "c.example.com" 304 | # res.searchlist 305 | # #=> ["example.com","a.example.com","b.example.com","c.example.com"] 306 | # 307 | # The default is an empty array. 308 | # 309 | def searchlist=(arg) 310 | case arg 311 | when String 312 | @config[:searchlist] = [arg] if valid? arg 313 | @logger.info "Searchlist changed to value #{@config[:searchlist].inspect}" 314 | when Array 315 | @config[:searchlist] = arg if arg.all? {|x| valid? x} 316 | @logger.info "Searchlist changed to value #{@config[:searchlist].inspect}" 317 | else 318 | raise ArgumentError, "Wrong argument format, neither String nor Array" 319 | end 320 | end 321 | 322 | # Get the list of resolver nameservers, in a dotted decimal format- 323 | # 324 | # res.nameservers 325 | # #=> ["192.168.0.1","192.168.0.2"] 326 | # 327 | def nameservers 328 | @config[:nameservers].map(&:to_s) 329 | end 330 | 331 | alias_method :nameserver, :nameservers 332 | 333 | # Set the list of resolver nameservers. 334 | # +arg+ can be a single ip address or an array of addresses. 335 | # 336 | # res.nameservers = "192.168.0.1" 337 | # res.nameservers = ["192.168.0.1","192.168.0.2"] 338 | # 339 | # If you want you can specify the addresses as IPAddr instances. 340 | # 341 | # ip = IPAddr.new("192.168.0.3") 342 | # res.nameservers << ip 343 | # #=> ["192.168.0.1","192.168.0.2","192.168.0.3"] 344 | # 345 | # The default is 127.0.0.1 (localhost) 346 | # 347 | def nameservers=(arg) 348 | @config[:nameservers] = convert_nameservers_arg_to_ips(arg) 349 | @logger.info "Nameservers list changed to value #{@config[:nameservers].inspect}" 350 | end 351 | alias_method("nameserver=","nameservers=") 352 | 353 | # Return a string with the default domain. 354 | def domain 355 | @config[:domain].inspect 356 | end 357 | 358 | # Set the domain for the query. 359 | def domain=(name) 360 | @config[:domain] = name if valid? name 361 | end 362 | 363 | # Return the defined size of the packet. 364 | def packet_size 365 | @config[:packet_size] 366 | end 367 | 368 | def packet_size=(arg) 369 | if arg.respond_to? :to_i 370 | @config[:packet_size] = arg.to_i 371 | @logger.info "Packet size changed to value #{@config[:packet_size].inspect}" 372 | else 373 | @logger.error "Packet size not set, #{arg.class} does not respond to to_i" 374 | end 375 | end 376 | 377 | # Get the port number to which the resolver sends queries. 378 | # 379 | # puts "Sending queries to port #{res.port}" 380 | # 381 | def port 382 | @config[:port] 383 | end 384 | 385 | # Set the port number to which the resolver sends queries. This can be useful 386 | # for testing a nameserver running on a non-standard port. 387 | # 388 | # res.port = 10053 389 | # 390 | # The default is port 53. 391 | # 392 | def port=(num) 393 | if (0..65535).include? num 394 | @config[:port] = num 395 | @logger.info "Port number changed to #{num}" 396 | else 397 | raise ArgumentError, "Wrong port number #{num}" 398 | end 399 | end 400 | 401 | # Get the value of the source port number. 402 | # 403 | # puts "Sending queries using port #{res.source_port}" 404 | # 405 | def source_port 406 | @config[:source_port] 407 | end 408 | alias srcport source_port 409 | 410 | # Set the local source port from which the resolver sends its queries. 411 | # 412 | # res.source_port = 40000 413 | # 414 | # Note that if you want to set a port you need root priviledges, as 415 | # raw sockets will be used to generate packets. The class will then 416 | # generate the exception ResolverPermissionError if you're not root. 417 | # 418 | # The default is 0, which means that the port will be chosen by the 419 | # underlaying layers. 420 | # 421 | def source_port=(num) 422 | unless root? 423 | raise ResolverPermissionError, "Are you root?" 424 | end 425 | if (0..65535).include?(num) 426 | @config[:source_port] = num 427 | else 428 | raise ArgumentError, "Wrong port number #{num}" 429 | end 430 | end 431 | alias srcport= source_port= 432 | 433 | # Get the local address from which the resolver sends queries 434 | # 435 | # puts "Sending queries using source address #{res.source_address}" 436 | # 437 | def source_address 438 | @config[:source_address].to_s 439 | end 440 | alias srcaddr source_address 441 | 442 | # Get the local ipv6 address from which the resolver sends queries 443 | # 444 | def source_address_inet6 445 | @config[:source_address_inet6].to_s 446 | end 447 | 448 | def interface 449 | @config[:interface] 450 | end 451 | 452 | # Set the local source address from which the resolver sends its queries. 453 | # 454 | # res.source_address = "172.16.100.1" 455 | # res.source_address = IPAddr.new("172.16.100.1") 456 | # 457 | # You can specify +arg+ as either a string containing the ip address 458 | # or an instance of IPAddr class. 459 | # 460 | # Normally this can be used to force queries out a specific interface 461 | # on a multi-homed host. In this case, you should of course need to 462 | # know the addresses of the interfaces. 463 | # 464 | # Another way to use this option is for some kind of spoofing attacks 465 | # towards weak nameservers, to probe the security of your network. 466 | # This includes specifing ranged attacks such as DoS and others. For 467 | # a paper on DNS security, checks htpt://www.marcoceresa.com/security/ 468 | # 469 | # Note that if you want to set a non-binded source address you need 470 | # root priviledges, as raw sockets will be used to generate packets. 471 | # The class will then generate an exception if you're not root. 472 | # 473 | # The default is 0.0.0.0, meaning any local address (chosen on routing needs). 474 | # 475 | def source_address=(addr) 476 | unless addr.respond_to? :to_s 477 | raise ArgumentError, "Wrong address argument #{addr}" 478 | end 479 | 480 | begin 481 | port = rand(64000)+1024 482 | @logger.warn "Try to determine state of source address #{addr} with port #{port}" 483 | a = TCPServer.new(addr.to_s,port) 484 | rescue SystemCallError => e 485 | case e.errno 486 | when 98 # Port already in use! 487 | @logger.warn "Port already in use" 488 | retry 489 | when 99 # Address is not valid: raw socket 490 | if Process.uid == 0 491 | @raw = true 492 | @logger.warn "Using raw sockets" 493 | else 494 | raise RuntimeError, "Raw sockets requested but not running as root." 495 | end 496 | else 497 | raise SystemCallError, e 498 | end 499 | else 500 | a.close 501 | end 502 | 503 | case addr 504 | when String 505 | @config[:source_address] = IPAddr.new(addr) 506 | @logger.info "Using new source address: #{@config[:source_address]}" 507 | when IPAddr 508 | @config[:source_address] = addr 509 | @logger.info "Using new source address: #{@config[:source_address]}" 510 | else 511 | raise ArgumentError, "Unknown dest_address format" 512 | end 513 | end 514 | alias srcaddr= source_address= 515 | 516 | def interface=(iface) 517 | @config[:interface] = iface 518 | end 519 | 520 | # Return the retrasmission interval (in seconds) the resolvers has 521 | # been set on. 522 | def retry_interval 523 | @config[:retry_interval] 524 | end 525 | alias retrans retry_interval 526 | 527 | # Set the retrasmission interval in seconds. Default 5 seconds. 528 | def retry_interval=(num) 529 | if num > 0 530 | @config[:retry_interval] = num 531 | @logger.info "Retransmission interval changed to #{num} seconds" 532 | else 533 | raise ArgumentError, "Interval must be positive" 534 | end 535 | end 536 | alias retrans= retry_interval= 537 | 538 | # The number of times the resolver will try a query. 539 | # 540 | # puts "Will try a max of #{res.retry_number} queries" 541 | # 542 | def retry_number 543 | @config[:retry_number] 544 | end 545 | 546 | # Set the number of times the resolver will try a query. 547 | # Default 4 times. 548 | def retry_number=(num) 549 | if num.kind_of? Integer and num > 0 550 | @config[:retry_number] = num 551 | @logger.info "Retrasmissions number changed to #{num}" 552 | else 553 | raise ArgumentError, "Retry value must be a positive integer" 554 | end 555 | end 556 | alias_method('retry=', 'retry_number=') 557 | 558 | # This method will return true if the resolver is configured to 559 | # perform recursive queries. 560 | # 561 | # print "The resolver will perform a " 562 | # print res.recursive? ? "" : "not " 563 | # puts "recursive query" 564 | # 565 | def recursive? 566 | @config[:recursive] 567 | end 568 | alias_method :recurse, :recursive? 569 | alias_method :recursive, :recursive? 570 | 571 | # Sets whether or not the resolver should perform recursive 572 | # queries. Default is true. 573 | # 574 | # res.recursive = false # perform non-recursive query 575 | # 576 | def recursive=(bool) 577 | case bool 578 | when TrueClass,FalseClass 579 | @config[:recursive] = bool 580 | @logger.info("Recursive state changed to #{bool}") 581 | else 582 | raise ArgumentError, "Argument must be boolean" 583 | end 584 | end 585 | alias_method :recurse=, :recursive= 586 | 587 | # Return a string representing the resolver state, suitable 588 | # for printing on the screen. 589 | # 590 | # puts "Resolver state:" 591 | # puts res.state 592 | # 593 | def state 594 | str = ";; RESOLVER state:\n;; " 595 | i = 1 596 | @config.each do |key,val| 597 | if key == :log_file or key == :config_file 598 | str << "#{key}: #{val} \t" 599 | else 600 | str << "#{key}: #{eval(key.to_s)} \t" 601 | end 602 | str << "\n;; " if i % 2 == 0 603 | i += 1 604 | end 605 | str 606 | end 607 | alias print state 608 | alias inspect state 609 | 610 | # Checks whether the +defname+ flag has been activate. 611 | def defname? 612 | @config[:defname] 613 | end 614 | alias defname defname? 615 | 616 | # Set the flag +defname+ in a boolean state. if +defname+ is true, 617 | # calls to Resolver#query will append the default domain to names 618 | # that contain no dots. 619 | # Example: 620 | # 621 | # # Domain example.com 622 | # res.defname = true 623 | # res.query("machine1") 624 | # #=> This will perform a query for machine1.example.com 625 | # 626 | # Default is true. 627 | # 628 | def defname=(bool) 629 | case bool 630 | when TrueClass,FalseClass 631 | @config[:defname] = bool 632 | @logger.info("Defname state changed to #{bool}") 633 | else 634 | raise ArgumentError, "Argument must be boolean" 635 | end 636 | end 637 | 638 | # Get the state of the dns_search flag. 639 | def dns_search 640 | @config[:dns_search] 641 | end 642 | alias_method :dnsrch, :dns_search 643 | 644 | # Set the flag +dns_search+ in a boolean state. If +dns_search+ 645 | # is true, when using the Resolver#search method will be applied 646 | # the search list. Default is true. 647 | def dns_search=(bool) 648 | case bool 649 | when TrueClass,FalseClass 650 | @config[:dns_search] = bool 651 | @logger.info("DNS search state changed to #{bool}") 652 | else 653 | raise ArgumentError, "Argument must be boolean" 654 | end 655 | end 656 | alias_method("dnsrch=","dns_search=") 657 | 658 | # Get the state of the use_tcp flag. 659 | # 660 | def use_tcp? 661 | @config[:use_tcp] 662 | end 663 | alias_method :usevc, :use_tcp? 664 | alias_method :use_tcp, :use_tcp? 665 | 666 | # If +use_tcp+ is true, the resolver will perform all queries 667 | # using TCP virtual circuits instead of UDP datagrams, which 668 | # is the default for the DNS protocol. 669 | # 670 | # res.use_tcp = true 671 | # res.query "host.example.com" 672 | # #=> Sending TCP segments... 673 | # 674 | # Default is false. 675 | # 676 | def use_tcp=(bool) 677 | case bool 678 | when TrueClass,FalseClass 679 | @config[:use_tcp] = bool 680 | @logger.info("Use tcp flag changed to #{bool}") 681 | else 682 | raise ArgumentError, "Argument must be boolean" 683 | end 684 | end 685 | alias usevc= use_tcp= 686 | 687 | def ignore_truncated? 688 | @config[:ignore_truncated] 689 | end 690 | alias_method :ignore_truncated, :ignore_truncated? 691 | 692 | def ignore_truncated=(bool) 693 | case bool 694 | when TrueClass,FalseClass 695 | @config[:ignore_truncated] = bool 696 | @logger.info("Ignore truncated flag changed to #{bool}") 697 | else 698 | raise ArgumentError, "Argument must be boolean" 699 | end 700 | end 701 | 702 | # Return an object representing the value of the stored TCP 703 | # timeout the resolver will use in is queries. This object 704 | # is an instance of the class +TcpTimeout+, and two methods 705 | # are available for printing informations: TcpTimeout#to_s 706 | # and TcpTimeout#pretty_to_s. 707 | # 708 | # Here's some example: 709 | # 710 | # puts "Timeout of #{res.tcp_timeout} seconds" # implicit to_s 711 | # #=> Timeout of 150 seconds 712 | # 713 | # puts "You set a timeout of " + res.tcp_timeout.pretty_to_s 714 | # #=> You set a timeout of 2 minutes and 30 seconds 715 | # 716 | # If the timeout is infinite, a string "infinite" will be returned. 717 | # 718 | def tcp_timeout 719 | @config[:tcp_timeout].to_s 720 | end 721 | 722 | # Set the value of TCP timeout for resolver queries that 723 | # will be performed using TCP. A value of 0 means that 724 | # the timeout will be infinite. 725 | # The value is stored internally as a +TcpTimeout+ object, see 726 | # the description for Resolver#tcp_timeout 727 | # 728 | # Default is 5 seconds. 729 | # 730 | def tcp_timeout=(secs) 731 | @config[:tcp_timeout] = TcpTimeout.new(secs) 732 | @logger.info("New TCP timeout value: #{@config[:tcp_timeout]} seconds") 733 | end 734 | 735 | # Return an object representing the value of the stored UDP 736 | # timeout the resolver will use in is queries. This object 737 | # is an instance of the class +UdpTimeout+, and two methods 738 | # are available for printing information: UdpTimeout#to_s 739 | # and UdpTimeout#pretty_to_s. 740 | # 741 | # Here's some example: 742 | # 743 | # puts "Timeout of #{res.udp_timeout} seconds" # implicit to_s 744 | # #=> Timeout of 150 seconds 745 | # 746 | # puts "You set a timeout of " + res.udp_timeout.pretty_to_s 747 | # #=> You set a timeout of 2 minutes and 30 seconds 748 | # 749 | # If the timeout is zero, a string "not defined" will 750 | # be returned. 751 | # 752 | def udp_timeout 753 | @config[:udp_timeout].to_s 754 | end 755 | 756 | # Set the value of UDP timeout for resolver queries that 757 | # will be performed using UDP. A value of 0 means that 758 | # the timeout will not be used, and the resolver will use 759 | # only +retry_number+ and +retry_interval+ parameters. 760 | # 761 | # Default is 5 seconds. 762 | # 763 | # The value is stored internally as a +UdpTimeout+ object, see 764 | # the description for Resolver#udp_timeout. 765 | # 766 | def udp_timeout=(secs) 767 | @config[:udp_timeout] = UdpTimeout.new(secs) 768 | @logger.info("New UDP timeout value: #{@config[:udp_timeout]} seconds") 769 | end 770 | 771 | # Set a new log file for the logger facility of the resolver 772 | # class. Could be a file descriptor too: 773 | # 774 | # res.log_file = $stderr 775 | # 776 | # Note that a new logging facility will be create, destroing 777 | # the old one, which will then be impossibile to recover. 778 | # 779 | def log_file=(log) 780 | @logger.close 781 | @config[:log_file] = log 782 | @logger = Logger.new(@config[:log_file]) 783 | @logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN 784 | end 785 | 786 | # This one permits to have a personal logger facility to handle 787 | # resolver messages, instead of new built-in one, which is set up 788 | # for a +$stdout+ (or +$stderr+) use. 789 | # 790 | # If you want your own logging facility you can create a new instance 791 | # of the +Logger+ class: 792 | # 793 | # log = Logger.new("/tmp/resolver.log","weekly",2*1024*1024) 794 | # log.level = Logger::DEBUG 795 | # log.progname = "ruby_resolver" 796 | # 797 | # and then pass it to the resolver: 798 | # 799 | # res.logger = log 800 | # 801 | # Note that this will destroy the precedent logger. 802 | # 803 | def logger=(logger) 804 | if logger.kind_of? Logger 805 | @logger.close 806 | @logger = logger 807 | else 808 | raise ArgumentError, "Argument must be an instance of Logger class" 809 | end 810 | end 811 | 812 | # Set the log level for the built-in logging facility. 813 | # 814 | # The log level can be one of the following: 815 | # 816 | # - +Net::DNS::DEBUG+ 817 | # - +Net::DNS::INFO+ 818 | # - +Net::DNS::WARN+ 819 | # - +Net::DNS::ERROR+ 820 | # - +Net::DNS::FATAL+ 821 | # 822 | # Note that if the global variable $DEBUG is set (like when the 823 | # -d switch is used at the command line) the logger level is 824 | # automatically set at DEGUB. 825 | # 826 | # For further informations, see Logger documentation in the 827 | # Ruby standard library. 828 | # 829 | def log_level=(level) 830 | @logger.level = level 831 | end 832 | 833 | # Performs a DNS query for the given name, applying the searchlist if 834 | # appropriate. The search algorithm is as follows: 835 | # 836 | # 1. If the name contains at least one dot, try it as is. 837 | # 2. If the name doesn't end in a dot then append each item in the search 838 | # list to the name. This is only done if +dns_search+ is true. 839 | # 3. If the name doesn't contain any dots, try it as is. 840 | # 841 | # The record type and class can be omitted; they default to +A+ and +IN+. 842 | # 843 | # packet = res.search('mailhost') 844 | # packet = res.search('mailhost.example.com') 845 | # packet = res.search('example.com', Net::DNS::MX) 846 | # packet = res.search('user.passwd.example.com', Net::DNS::TXT, Net::DNS::HS) 847 | # 848 | # If the name is an IP address (Ipv4 or IPv6), in the form of a string 849 | # or a +IPAddr+ object, then an appropriate PTR query will be performed: 850 | # 851 | # ip = IPAddr.new("172.16.100.2") 852 | # packet = res.search(ip) 853 | # packet = res.search("192.168.10.254") 854 | # 855 | # Returns a Net::DNS::Packet object. If you need to examine the response packet 856 | # whether it contains any answers or not, use the Resolver#query method instead. 857 | # 858 | def search(name,type=Net::DNS::A,cls=Net::DNS::IN) 859 | 860 | return query(name,type,cls) if name.class == IPAddr 861 | 862 | # If the name contains at least one dot then try it as is first. 863 | if name.include? "." 864 | @logger.debug "Search(#{name},#{Net::DNS::RR::Types.new(type)},#{Net::DNS::RR::Classes.new(cls)})" 865 | ans = query(name,type,cls) 866 | return ans if ans.header.anCount > 0 867 | end 868 | 869 | # If the name doesn't end in a dot then apply the search list. 870 | if name !~ /\.$/ and @config[:dns_search] 871 | @config[:searchlist].each do |domain| 872 | newname = name + "." + domain 873 | @logger.debug "Search(#{newname},#{Net::DNS::RR::Types.new(type)},#{Net::DNS::RR::Classes.new(cls)})" 874 | ans = query(newname,type,cls) 875 | return ans if ans.header.anCount > 0 876 | end 877 | end 878 | 879 | # Finally, if the name has no dots then try it as is. 880 | @logger.debug "Search(#{name},#{Net::DNS::RR::Types.new(type)},#{Net::DNS::RR::Classes.new(cls)})" 881 | query(name+".",type,cls) 882 | 883 | end 884 | 885 | # Performs a DNS query for the given name; the search list 886 | # is not applied. If the name doesn't contain any dots and 887 | # +defname+ is true then the default domain will be appended. 888 | # 889 | # The record type and class can be omitted; they default to +A+ 890 | # and +IN+. If the name looks like an IP address (IPv4 or IPv6), 891 | # then an appropriate PTR query will be performed. 892 | # 893 | # packet = res.query('mailhost') 894 | # packet = res.query('mailhost.example.com') 895 | # packet = res.query('example.com', Net::DNS::MX) 896 | # packet = res.query('user.passwd.example.com', Net::DNS::TXT, Net::DNS::HS) 897 | # 898 | # If the name is an IP address (Ipv4 or IPv6), in the form of a string 899 | # or a +IPAddr+ object, then an appropriate PTR query will be performed: 900 | # 901 | # ip = IPAddr.new("172.16.100.2") 902 | # packet = res.query(ip) 903 | # packet = res.query("192.168.10.254") 904 | # 905 | # Returns a Net::DNS::Packet object. If you need to examine the response 906 | # packet whether it contains any answers or not, use the Resolver#query 907 | # method instead. 908 | # 909 | def query(name,type=Net::DNS::A,cls=Net::DNS::IN) 910 | 911 | return send(name,type,cls) if name.class == IPAddr 912 | 913 | # If the name doesn't contain any dots then append the default domain. 914 | if name !~ /\./ and name !~ /:/ and @config[:defname] 915 | name += "." + @config[:domain] 916 | end 917 | 918 | @logger.debug "Query(#{name},#{Net::DNS::RR::Types.new(type)},#{Net::DNS::RR::Classes.new(cls)})" 919 | 920 | send(name,type,cls) 921 | 922 | end 923 | 924 | # Performs a DNS query for the given name. Neither the 925 | # searchlist nor the default domain will be appended. 926 | # 927 | # The argument list can be either a Net::DNS::Packet object 928 | # or a name string plus optional type and class, which if 929 | # omitted default to +A+ and +IN+. 930 | # 931 | # Returns a Net::DNS::Packet object. 932 | # 933 | # # Executes the query with a +Packet+ object 934 | # send_packet = Net::DNS::Packet.new("host.example.com", Net::DNS::NS, Net::DNS::HS) 935 | # packet = res.query(send_packet) 936 | # 937 | # # Executes the query with a host, type and cls 938 | # packet = res.query("host.example.com") 939 | # packet = res.query("host.example.com", Net::DNS::NS) 940 | # packet = res.query("host.example.com", Net::DNS::NS, Net::DNS::HS) 941 | # 942 | # If the name is an IP address (Ipv4 or IPv6), in the form of a string 943 | # or a IPAddr object, then an appropriate PTR query will be performed: 944 | # 945 | # ip = IPAddr.new("172.16.100.2") 946 | # packet = res.query(ip) 947 | # 948 | # packet = res.query("172.16.100.2") 949 | # 950 | # Use +packet.header.ancount+ or +packet.answer+ to find out if there 951 | # were any records in the answer section. 952 | # 953 | def query(argument, type = Net::DNS::A, cls = Net::DNS::IN) 954 | if @config[:nameservers].size == 0 955 | raise Resolver::Error, "No nameservers specified!" 956 | end 957 | 958 | method = :query_udp 959 | packet = if argument.kind_of? Net::DNS::Packet 960 | argument 961 | else 962 | make_query_packet(argument, type, cls) 963 | end 964 | 965 | # Store packet_data for performance improvements, 966 | # so methods don't keep on calling Packet#data 967 | packet_data = packet.data 968 | packet_size = packet_data.size 969 | 970 | # Choose whether use TCP, UDP or RAW 971 | if packet_size > @config[:packet_size] # Must use TCP, either plain or raw 972 | if @raw # Use raw sockets? 973 | @logger.info "Sending #{packet_size} bytes using TCP over RAW socket" 974 | method = :send_raw_tcp 975 | else 976 | @logger.info "Sending #{packet_size} bytes using TCP" 977 | method = :query_tcp 978 | end 979 | else # Packet size is inside the boundaries 980 | if @raw # Use raw sockets? 981 | @logger.info "Sending #{packet_size} bytes using UDP over RAW socket" 982 | method = :send_raw_udp 983 | elsif use_tcp? # User requested TCP 984 | @logger.info "Sending #{packet_size} bytes using TCP" 985 | method = :query_tcp 986 | else # Finally use UDP 987 | @logger.info "Sending #{packet_size} bytes using UDP" 988 | method = :query_udp 989 | end 990 | end 991 | 992 | if type == Net::DNS::AXFR 993 | if @raw 994 | @logger.warn "AXFR query, switching to TCP over RAW socket" 995 | method = :send_raw_tcp 996 | else 997 | @logger.warn "AXFR query, switching to TCP" 998 | method = :query_tcp 999 | end 1000 | end 1001 | 1002 | ans = self.send(method, packet, packet_data) 1003 | 1004 | # Don't have any responses with the raw, 1005 | # since currently raw is only used when source_address is changed 1006 | if @raw 1007 | return nil 1008 | end 1009 | 1010 | if not ans 1011 | message = "No response from nameservers list" 1012 | @logger.fatal(message) 1013 | raise NoResponseError, message 1014 | end 1015 | 1016 | @logger.info "Received #{ans[0].size} bytes from #{ans[1][2]+":"+ans[1][1].to_s}" 1017 | response = Net::DNS::Packet.parse(ans[0],ans[1]) 1018 | 1019 | if response.header.truncated? and not ignore_truncated? 1020 | @logger.warn "Packet truncated, retrying using TCP" 1021 | self.use_tcp = true 1022 | begin 1023 | return query(argument,type,cls) 1024 | ensure 1025 | self.use_tcp = false 1026 | end 1027 | end 1028 | 1029 | return response 1030 | end 1031 | 1032 | # Performs a zone transfer for the zone passed as a parameter. 1033 | # 1034 | # It is actually only a wrapper to a send with type set as Net::DNS::AXFR, 1035 | # since it is using the same infrastucture. 1036 | # 1037 | def axfr(name, cls = Net::DNS::IN) 1038 | @logger.info "Requested AXFR transfer, zone #{name} class #{cls}" 1039 | query(name, Net::DNS::AXFR, cls) 1040 | end 1041 | 1042 | # Performs an MX query for the domain name passed as parameter. 1043 | # 1044 | # It actually uses the same methods a normal Resolver query would 1045 | # use, but automatically sort the results based on preferences 1046 | # and returns an ordered array. 1047 | # 1048 | # res = Net::DNS::Resolver.new 1049 | # res.mx("google.com") 1050 | # 1051 | def mx(name, cls = Net::DNS::IN) 1052 | arr = [] 1053 | query(name, Net::DNS::MX, cls).answer.each do |entry| 1054 | arr << entry if entry.type == 'MX' 1055 | end 1056 | arr.sort_by { |a| a.preference } 1057 | end 1058 | 1059 | private 1060 | 1061 | # Parses a configuration file specified as the argument. 1062 | def parse_config_file 1063 | if self.class.platform_windows? 1064 | require 'win32/resolv' 1065 | arr = Win32::Resolv.get_resolv_info 1066 | self.domain = arr[0].first.to_s 1067 | self.nameservers = arr[1] 1068 | else 1069 | nameservers = [] 1070 | IO.foreach(@config[:config_file]) do |line| 1071 | line.gsub!(/\s*[;#].*/,"") 1072 | next unless line =~ /\S/ 1073 | case line 1074 | when /^\s*domain\s+(\S+)/ 1075 | self.domain = $1 1076 | when /^\s*search\s+(.*)/ 1077 | self.searchlist = $1.split(" ") 1078 | when /^\s*nameserver\s+(.*)/ 1079 | nameservers << $1.split(" ") 1080 | end 1081 | end 1082 | self.nameservers = nameservers.flatten 1083 | end 1084 | end 1085 | 1086 | # Parses environment variables. 1087 | def parse_environment_variables 1088 | if ENV['RES_NAMESERVERS'] 1089 | self.nameservers = ENV['RES_NAMESERVERS'].split(" ") 1090 | end 1091 | if ENV['RES_SEARCHLIST'] 1092 | self.searchlist = ENV['RES_SEARCHLIST'].split(" ") 1093 | end 1094 | if ENV['LOCALDOMAIN'] 1095 | self.domain = ENV['LOCALDOMAIN'] 1096 | end 1097 | if ENV['RES_OPTIONS'] 1098 | ENV['RES_OPTIONS'].split(" ").each do |opt| 1099 | name,val = opt.split(":") 1100 | begin 1101 | eval("self.#{name} = #{val}") 1102 | rescue NoMethodError 1103 | raise ArgumentError, "Invalid ENV option #{name}" 1104 | end 1105 | end 1106 | end 1107 | end 1108 | 1109 | def convert_nameservers_arg_to_ips(arg) 1110 | if arg.kind_of? IPAddr 1111 | [arg] 1112 | elsif arg.respond_to? :map 1113 | arg.map{|x| convert_nameservers_arg_to_ips(x) }.flatten 1114 | elsif arg.respond_to? :to_a 1115 | arg.to_a.map{|x| convert_nameservers_arg_to_ips(x) }.flatten 1116 | elsif arg.respond_to? :to_s 1117 | begin 1118 | [IPAddr.new(arg.to_s)] 1119 | rescue ArgumentError # arg is in the name form, not IP 1120 | nameservers_from_name(arg) 1121 | end 1122 | else 1123 | raise ArgumentError, "Wrong nameservers argument format, cannot convert to array of IPAddrs" 1124 | end 1125 | end 1126 | 1127 | def nameservers_from_name(arg) 1128 | arr = [] 1129 | arg.split(" ").each do |name| 1130 | Resolver.new.search(name).each_address do |ip| 1131 | arr << ip 1132 | end 1133 | end 1134 | arr 1135 | end 1136 | 1137 | def make_query_packet(string, type, cls) 1138 | begin 1139 | name = IPAddr.new(string.chomp(".")).reverse 1140 | type = Net::DNS::PTR 1141 | rescue ArgumentError 1142 | name = string if valid? string 1143 | end 1144 | 1145 | if name.nil? 1146 | raise ArgumentError, "Bad query string" 1147 | end 1148 | 1149 | # Create the packet 1150 | packet = Net::DNS::Packet.new(name, type, cls) 1151 | 1152 | if packet.query? 1153 | packet.header.recursive = @config[:recursive] ? 1 : 0 1154 | end 1155 | 1156 | # DNSSEC and TSIG stuff to be inserted here 1157 | 1158 | packet 1159 | end 1160 | 1161 | def query_tcp(packet, packet_data) 1162 | 1163 | ans = nil 1164 | length = [packet_data.size].pack("n") 1165 | 1166 | @config[:nameservers].each do |ns| 1167 | begin 1168 | buffer = "" 1169 | socket = Socket.new(Socket::AF_INET,Socket::SOCK_STREAM,0) 1170 | socket.bind(Socket.pack_sockaddr_in(@config[:source_port],@config[:source_address].to_s)) 1171 | 1172 | sockaddr = Socket.pack_sockaddr_in(@config[:port],ns.to_s) 1173 | 1174 | @config[:tcp_timeout].timeout do 1175 | socket.connect(sockaddr) 1176 | @logger.info "Contacting nameserver #{ns} port #{@config[:port]}" 1177 | socket.write(length+packet_data) 1178 | ans = socket.recv(Net::DNS::INT16SZ) 1179 | len = ans.unpack("n")[0] 1180 | 1181 | @logger.info "Receiving #{len} bytes..." 1182 | 1183 | if len == 0 1184 | @logger.warn "Receiving 0 lenght packet from nameserver #{ns}, trying next." 1185 | next 1186 | end 1187 | 1188 | while (buffer.size < len) 1189 | left = len - buffer.size 1190 | temp,from = socket.recvfrom(left) 1191 | buffer += temp 1192 | end 1193 | 1194 | unless buffer.size == len 1195 | @logger.warn "Malformed packet from nameserver #{ns}, trying next." 1196 | next 1197 | end 1198 | end 1199 | return [buffer,["",@config[:port],ns.to_s,ns.to_s]] 1200 | rescue TimeoutError 1201 | @logger.warn "Nameserver #{ns} not responding within TCP timeout, trying next one" 1202 | next 1203 | ensure 1204 | socket.close 1205 | end 1206 | end 1207 | ans 1208 | end 1209 | 1210 | def query_udp(packet, packet_data) 1211 | socket4 = UDPSocket.new 1212 | socket4.bind(@config[:source_address].to_s,@config[:source_port]) 1213 | if @config[:nameservers].any? { |ns| ns.ipv6? } 1214 | socket6 = UDPSocket.new(Socket::AF_INET6) 1215 | socket6.bind(@config[:source_address_inet6].to_s,@config[:source_port]) 1216 | end 1217 | 1218 | ans = nil 1219 | response = "" 1220 | @config[:nameservers].each do |ns| 1221 | begin 1222 | @config[:udp_timeout].timeout do 1223 | @logger.info "Contacting nameserver #{ns} port #{@config[:port]}" 1224 | ans = if ns.ipv6? 1225 | socket6.send(packet_data, 0, ns.to_s, @config[:port]) 1226 | socket6.recvfrom(@config[:packet_size]) 1227 | else 1228 | socket4.send(packet_data, 0, ns.to_s, @config[:port]) 1229 | socket4.recvfrom(@config[:packet_size]) 1230 | end 1231 | end 1232 | break if ans 1233 | rescue TimeoutError 1234 | @logger.warn "Nameserver #{ns} not responding within UDP timeout, trying next one" 1235 | next 1236 | end 1237 | end 1238 | ans 1239 | end 1240 | 1241 | def send_raw_tcp(packet, packet_data) 1242 | socket = nil 1243 | packet = PacketFu::TCPPacket.new({body: packet_data}) 1244 | 1245 | 1246 | if @config[:source_address] 1247 | octet = PacketFu::Octets.new 1248 | octet.read_quad @config[:source_address].to_s 1249 | packet.ip_src = octet 1250 | packet.udp_src =rand(0xffff-1024) + 1024 1251 | packet.eth_saddr = PacketFu::Utils.arp(@config[:source_address].to_s, {iface: @config[:interface]}) 1252 | elsif @config[:source_address_inet6] 1253 | octet = PacketFu::Octets.new 1254 | octet.read_quad @config[:source_address_inet6].to_s 1255 | packet.ip_src = octet 1256 | packet.udp_src = @config[:source_address_inet6].to_i 1257 | packet.eth_saddr = PacketFu::Utils.arp(@config[:source_address_inet6].to_s, {iface: @config[:interface]}) 1258 | else 1259 | raise ArgumentError, "No source address specified, cannot send" 1260 | end 1261 | 1262 | @config[:nameservers].each do |ns| 1263 | octet = PacketFu::Octets.new 1264 | packet.eth_daddr = PacketFu::Utils.arp(ns.to_s, {iface: @config[:interface]}) 1265 | octet.read_quad ns.to_s 1266 | packet.ip_dst = octet 1267 | packet.udp_dst = 53 1268 | packet.recalc arg=:all 1269 | packet.to_w @config[:interface] 1270 | end 1271 | nil 1272 | end 1273 | 1274 | def send_raw_udp(packet, packet_data) 1275 | socket = nil 1276 | packet = PacketFu::UDPPacket.new({body: packet_data}) 1277 | 1278 | 1279 | if @config[:source_address] 1280 | octet = PacketFu::Octets.new 1281 | octet.read_quad @config[:source_address].to_s 1282 | packet.ip_src = octet 1283 | packet.udp_src =rand(0xffff-1024) + 1024 1284 | packet.eth_saddr = PacketFu::Utils.arp(@config[:source_address].to_s, {iface: @config[:interface]}) 1285 | elsif @config[:source_address_inet6] 1286 | octet = PacketFu::Octets.new 1287 | octet.read_quad @config[:source_address_inet6].to_s 1288 | packet.ip_src = octet 1289 | packet.udp_src = @config[:source_address_inet6].to_i 1290 | packet.eth_saddr = PacketFu::Utils.arp(@config[:source_address_inet6].to_s, {iface: @config[:interface]}) 1291 | else 1292 | raise ArgumentError, "No source address specified, cannot send" 1293 | end 1294 | 1295 | @config[:nameservers].each do |ns| 1296 | octet = PacketFu::Octets.new 1297 | packet.eth_daddr = PacketFu::Utils.arp(ns.to_s, {iface: @config[:interface]}) 1298 | octet.read_quad ns.to_s 1299 | packet.ip_dst = octet 1300 | packet.udp_dst = 53 1301 | packet.recalc arg=:all 1302 | packet.to_w @config[:interface] 1303 | end 1304 | nil 1305 | end 1306 | 1307 | def valid?(name) 1308 | if name =~ /[^-\w\.]/ 1309 | false 1310 | else 1311 | true 1312 | end 1313 | end 1314 | end 1315 | end 1316 | end 1317 | --------------------------------------------------------------------------------