diff options
Diffstat (limited to 'lib/resolv.rb')
| -rw-r--r-- | lib/resolv.rb | 2885 |
1 files changed, 2344 insertions, 541 deletions
diff --git a/lib/resolv.rb b/lib/resolv.rb index edc17c2f19..6b58f92813 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -1,231 +1,124 @@ -=begin -= resolv library -resolv.rb is a resolver library written in Ruby. -Since it is written in Ruby, it is thread-aware. -I.e. it can resolv many hostnames concurrently. - -It is possible to lookup various resources of DNS using DNS module directly. - -== example - Resolv.getaddress("www.ruby-lang.org") - Resolv.getname("210.251.121.214") - - dns = Resolv::DNS.new - dns.getresources("www.ruby-lang.org", Resolv::DNS::Resource::IN::A).collect {|r| r.address} - dns.getresources("ruby-lang.org", Resolv::DNS::Resource::IN::MX).collect {|r| [r.exchange.to_s, r.preference]} - -== Resolv class - -=== class methods ---- Resolv.getaddress(name) ---- Resolv.getaddresses(name) ---- Resolv.each_address(name) {|address| ...} - They lookups IP addresses of ((|name|)) which represents a hostname - as a string by default resolver. - - getaddress returns first entry of lookupped addresses. - getaddresses returns lookupped addresses as an array. - each_address iterates over lookupped addresses. - ---- Resolv.getname(address) ---- Resolv.getnames(address) ---- Resolv.each_name(address) {|name| ...} - lookups hostnames of ((|address|)) which represents IP address as a string. - - getname returns first entry of lookupped names. - getnames returns lookupped names as an array. - each_names iterates over lookupped names. - -== Resolv::Hosts class -hostname resolver using /etc/hosts format. - -=== class methods ---- Resolv::Hosts.new(hosts='/etc/hosts') - -=== methods ---- Resolv::Hosts#getaddress(name) ---- Resolv::Hosts#getaddresses(name) ---- Resolv::Hosts#each_address(name) {|address| ...} - address lookup methods. - ---- Resolv::Hosts#getname(address) ---- Resolv::Hosts#getnames(address) ---- Resolv::Hosts#each_name(address) {|name| ...} - hostnames lookup methods. - -== Resolv::DNS class -DNS stub resolver. - -=== class methods ---- Resolv::DNS.new(resolv_conf='/etc/resolv.conf') - ---- Resolv::DNS.open(resolv_conf='/etc/resolv.conf') ---- Resolv::DNS.open(resolv_conf='/etc/resolv.conf') {|dns| ...} - -=== methods ---- Resolv::DNS#close - ---- Resolv::DNS#getaddress(name) ---- Resolv::DNS#getaddresses(name) ---- Resolv::DNS#each_address(name) {|address| ...} - address lookup methods. - - ((|name|)) must be a instance of Resolv::DNS::Name or String. Lookupped - address is represented as an instance of Resolv::IPv4 or Resolv::IPv6. - ---- Resolv::DNS#getname(address) ---- Resolv::DNS#getnames(address) ---- Resolv::DNS#each_name(address) {|name| ...} - hostnames lookup methods. - - ((|address|)) must be a instance of Resolv::IPv4, Resolv::IPv6 or String. - Lookupped name is represented as an instance of Resolv::DNS::Name. - ---- Resolv::DNS#getresource(name, typeclass) ---- Resolv::DNS#getresources(name, typeclass) ---- Resolv::DNS#each_resource(name, typeclass) {|resource| ...} - They lookup DNS resources of ((|name|)). - ((|name|)) must be a instance of Resolv::Name or String. - - ((|typeclass|)) should be one of follows: - * Resolv::DNS::Resource::IN::ANY - * Resolv::DNS::Resource::IN::NS - * Resolv::DNS::Resource::IN::CNAME - * Resolv::DNS::Resource::IN::SOA - * Resolv::DNS::Resource::IN::HINFO - * Resolv::DNS::Resource::IN::MINFO - * Resolv::DNS::Resource::IN::MX - * Resolv::DNS::Resource::IN::TXT - * Resolv::DNS::Resource::IN::ANY - * Resolv::DNS::Resource::IN::A - * Resolv::DNS::Resource::IN::WKS - * Resolv::DNS::Resource::IN::PTR - * Resolv::DNS::Resource::IN::AAAA - - Lookupped resource is represented as an instance of (a subclass of) - Resolv::DNS::Resource. - (Resolv::DNS::Resource::IN::A, etc.) - -== Resolv::DNS::Resource::IN::NS class ---- name -== Resolv::DNS::Resource::IN::CNAME class ---- name -== Resolv::DNS::Resource::IN::SOA class ---- mname ---- rname ---- serial ---- refresh ---- retry ---- expire ---- minimum -== Resolv::DNS::Resource::IN::HINFO class ---- cpu ---- os -== Resolv::DNS::Resource::IN::MINFO class ---- rmailbx ---- emailbx -== Resolv::DNS::Resource::IN::MX class ---- preference ---- exchange -== Resolv::DNS::Resource::IN::TXT class ---- data -== Resolv::DNS::Resource::IN::A class ---- address -== Resolv::DNS::Resource::IN::WKS class ---- address ---- protocol ---- bitmap -== Resolv::DNS::Resource::IN::PTR class ---- name -== Resolv::DNS::Resource::IN::AAAA class ---- address - -== Resolv::DNS::Name class - -=== class methods ---- Resolv::DNS::Name.create(name) - -=== methods ---- Resolv::DNS::Name#to_s - -== Resolv::DNS::Resource class - -== Resolv::IPv4 class -=== class methods ---- Resolv::IPv4.create(address) - -=== methods ---- Resolv::IPv4#to_s ---- Resolv::IPv4#to_name - -=== constants ---- Resolv::IPv4::Regex - regular expression for IPv4 address. - -== Resolv::IPv6 class -=== class methods ---- Resolv::IPv6.create(address) - -=== methods ---- Resolv::IPv6#to_s ---- Resolv::IPv6#to_name - -=== constants ---- Resolv::IPv6::Regex - regular expression for IPv6 address. - -== Bugs -* NIS is not supported. -* /etc/nsswitch.conf is not supported. -* IPv6 is not supported. - -=end +# frozen_string_literal: true require 'socket' -require 'fcntl' require 'timeout' -require 'thread' +require 'io/wait' +require 'securerandom' +require 'rbconfig' + +# Resolv is a thread-aware DNS resolver library written in Ruby. Resolv can +# handle multiple DNS requests concurrently without blocking the entire Ruby +# interpreter. +# +# See also resolv-replace.rb to replace the libc resolver with Resolv. +# +# Resolv can look up various DNS resources using the DNS module directly. +# +# Examples: +# +# p Resolv.getaddress "www.ruby-lang.org" +# p Resolv.getname "210.251.121.214" +# +# Resolv::DNS.open do |dns| +# ress = dns.getresources "www.ruby-lang.org", Resolv::DNS::Resource::IN::A +# p ress.map(&:address) +# ress = dns.getresources "ruby-lang.org", Resolv::DNS::Resource::IN::MX +# p ress.map { |r| [r.exchange.to_s, r.preference] } +# end +# +# +# == Bugs +# +# * NIS is not supported. +# * /etc/nsswitch.conf is not supported. class Resolv + + # The version string + VERSION = "0.7.1" + + ## + # Looks up the first IP address for +name+. + def self.getaddress(name) DefaultResolver.getaddress(name) end + ## + # Looks up all IP address for +name+. + def self.getaddresses(name) DefaultResolver.getaddresses(name) end + ## + # Iterates over all IP addresses for +name+. + def self.each_address(name, &block) DefaultResolver.each_address(name, &block) end + ## + # Looks up the hostname of +address+. + def self.getname(address) DefaultResolver.getname(address) end + ## + # Looks up all hostnames for +address+. + def self.getnames(address) DefaultResolver.getnames(address) end + ## + # Iterates over all hostnames for +address+. + def self.each_name(address, &proc) DefaultResolver.each_name(address, &proc) end - def initialize(resolvers=[Hosts.new, DNS.new]) - @resolvers = resolvers + ## + # Creates a new Resolv using +resolvers+. + # + # If +resolvers+ is not given, a hash, or +nil+, uses a Hosts resolver and + # and a DNS resolver. If +resolvers+ is a hash, uses the hash as + # configuration for the DNS resolver. + + def initialize(resolvers=(arg_not_set = true; nil), use_ipv6: (keyword_not_set = true; nil)) + if !keyword_not_set && !arg_not_set + warn "Support for separate use_ipv6 keyword is deprecated, as it is ignored if an argument is provided. Do not provide a positional argument if using the use_ipv6 keyword argument.", uplevel: 1 + end + + @resolvers = case resolvers + when Hash, nil + [Hosts.new, DNS.new(DNS::Config.default_config_hash.merge(resolvers || {}))] + else + resolvers + end end + ## + # Looks up the first IP address for +name+. + def getaddress(name) each_address(name) {|address| return address} raise ResolvError.new("no address for #{name}") end + ## + # Looks up all IP address for +name+. + def getaddresses(name) ret = [] each_address(name) {|address| ret << address} return ret end + ## + # Iterates over all IP addresses for +name+. + def each_address(name) if AddressRegex =~ name yield name @@ -241,17 +134,26 @@ class Resolv } end + ## + # Looks up the hostname of +address+. + def getname(address) each_name(address) {|name| return name} raise ResolvError.new("no name for #{address}") end + ## + # Looks up all hostnames for +address+. + def getnames(address) ret = [] each_name(address) {|name| ret << name} return ret end + ## + # Iterates over all hostnames for +address+. + def each_name(address) yielded = false @resolvers.each {|r| @@ -263,101 +165,138 @@ class Resolv } end - class ResolvError < StandardError - end + ## + # Indicates a failure to resolve a name or address. - class ResolvTimeout < TimeoutError - end + class ResolvError < StandardError; end + + ## + # Indicates a timeout resolving a name or address. + + class ResolvTimeout < Timeout::Error; end + + ## + # Resolv::Hosts is a hostname resolver that uses the system hosts file. class Hosts - if /mswin32|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM - require 'win32/resolv' - DefaultFileName = Win32::Resolv.get_hosts_path - else - DefaultFileName = '/etc/hosts' + if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM || ::RbConfig::CONFIG['host_os'] =~ /mswin/ + begin + require 'win32/resolv' unless defined?(Win32::Resolv) + hosts = Win32::Resolv.get_hosts_path || IO::NULL + rescue LoadError + end end + # The default file name for host names + DefaultFileName = hosts || '/etc/hosts' + + ## + # Creates a new Resolv::Hosts, using +filename+ for its data source. def initialize(filename = DefaultFileName) @filename = filename - @mutex = Mutex.new + @mutex = Thread::Mutex.new @initialized = nil end - def lazy_initialize + def lazy_initialize # :nodoc: @mutex.synchronize { unless @initialized @name2addr = {} @addr2name = {} - open(@filename) {|f| + File.open(@filename, 'rb') {|f| f.each {|line| line.sub!(/#.*/, '') - addr, hostname, *aliases = line.split(/\s+/) + addr, *hostnames = line.split(/\s+/) next unless addr - addr.untaint - hostname.untaint - @addr2name[addr] = [] unless @addr2name.include? addr - @addr2name[addr] << hostname - @addr2name[addr] += aliases - @name2addr[hostname] = [] unless @name2addr.include? hostname - @name2addr[hostname] << addr - aliases.each {|n| - n.untaint - @name2addr[n] = [] unless @name2addr.include? n - @name2addr[n] << addr - } + (@addr2name[addr] ||= []).concat(hostnames) + hostnames.each {|hostname| (@name2addr[hostname] ||= []) << addr} } } @name2addr.each {|name, arr| arr.reverse!} @initialized = true end } + self end + ## + # Gets the IP address of +name+ from the hosts file. + def getaddress(name) each_address(name) {|address| return address} raise ResolvError.new("#{@filename} has no name: #{name}") end + ## + # Gets all IP addresses for +name+ from the hosts file. + def getaddresses(name) ret = [] each_address(name) {|address| ret << address} return ret end + ## + # Iterates over all IP addresses for +name+ retrieved from the hosts file. + def each_address(name, &proc) lazy_initialize - if @name2addr.include?(name) - @name2addr[name].each(&proc) - end + @name2addr[name]&.each(&proc) end + ## + # Gets the hostname of +address+ from the hosts file. + def getname(address) each_name(address) {|name| return name} raise ResolvError.new("#{@filename} has no address: #{address}") end + ## + # Gets all hostnames for +address+ from the hosts file. + def getnames(address) ret = [] each_name(address) {|name| ret << name} return ret end + ## + # Iterates over all hostnames for +address+ retrieved from the hosts file. + def each_name(address, &proc) lazy_initialize - if @addr2name.include?(address) - @addr2name[address].each(&proc) - end + @addr2name[address]&.each(&proc) end end + ## + # Resolv::DNS is a DNS stub resolver. + # + # Information taken from the following places: + # + # * STD0013 + # * RFC 1035 + # * ftp://ftp.isi.edu/in-notes/iana/assignments/dns-parameters + # * etc. + class DNS - # STD0013 (RFC 1035, etc.) - # ftp://ftp.isi.edu/in-notes/iana/assignments/dns-parameters + + ## + # Default DNS Port Port = 53 + + ## + # Default DNS UDP packet size + UDPSize = 512 - DNSThreadGroup = ThreadGroup.new + ## + # Creates a new DNS resolver. See Resolv::DNS.new for argument details. + # + # Yields the created DNS resolver to the block, if given, otherwise + # returns it. def self.open(*args) dns = new(*args) @@ -369,68 +308,165 @@ class Resolv end end - def initialize(config="/etc/resolv.conf") - @mutex = Mutex.new - @config = Config.new(config) + ## + # Creates a new DNS resolver. + # + # +config_info+ can be: + # + # nil:: Uses /etc/resolv.conf. + # String:: Path to a file using /etc/resolv.conf's format. + # Hash:: Must contain :nameserver, :search and :ndots keys. + # :nameserver_port can be used to specify port number of nameserver address. + # :raise_timeout_errors can be used to raise timeout errors + # as exceptions instead of treating the same as an NXDOMAIN response. + # + # The value of :nameserver should be an address string or + # an array of address strings. + # - :nameserver => '8.8.8.8' + # - :nameserver => ['8.8.8.8', '8.8.4.4'] + # + # The value of :nameserver_port should be an array of + # pair of nameserver address and port number. + # - :nameserver_port => [['8.8.8.8', 53], ['8.8.4.4', 53]] + # + # Example: + # + # Resolv::DNS.new(:nameserver => ['210.251.121.21'], + # :search => ['ruby-lang.org'], + # :ndots => 1) + + def initialize(config_info=nil) + @mutex = Thread::Mutex.new + @config = Config.new(config_info) @initialized = nil end - def lazy_initialize + # Sets the resolver timeouts. This may be a single positive number + # or an array of positive numbers representing timeouts in seconds. + # If an array is specified, a DNS request will retry and wait for + # each successive interval in the array until a successful response + # is received. Specifying +nil+ reverts to the default timeouts: + # [ 5, second = 5 * 2 / nameserver_count, 2 * second, 4 * second ] + # + # Example: + # + # dns.timeouts = 3 + # + def timeouts=(values) + @config.timeouts = values + end + + def lazy_initialize # :nodoc: @mutex.synchronize { unless @initialized @config.lazy_initialize - - if nameserver = @config.single? - @requester = Requester::ConnectedUDP.new(nameserver) - else - @requester = Requester::UnconnectedUDP.new - end - @initialized = true end } + self end + ## + # Closes the DNS resolver. + def close @mutex.synchronize { if @initialized - @requester.close if @requester - @requester = nil @initialized = false end } end + ## + # Gets the IP address of +name+ from the DNS resolver. + # + # +name+ can be a Resolv::DNS::Name or a String. Retrieved address will + # be a Resolv::IPv4 or Resolv::IPv6 + def getaddress(name) each_address(name) {|address| return address} raise ResolvError.new("DNS result has no information for #{name}") end + ## + # Gets all IP addresses for +name+ from the DNS resolver. + # + # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will + # be a Resolv::IPv4 or Resolv::IPv6 + def getaddresses(name) ret = [] each_address(name) {|address| ret << address} return ret end + ## + # Iterates over all IP addresses for +name+ retrieved from the DNS + # resolver. + # + # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will + # be a Resolv::IPv4 or Resolv::IPv6 + def each_address(name) + if use_ipv6? + each_resource(name, Resource::IN::AAAA) {|resource| yield resource.address} + end each_resource(name, Resource::IN::A) {|resource| yield resource.address} end + def use_ipv6? # :nodoc: + @config.lazy_initialize unless @config.instance_variable_get(:@initialized) + + use_ipv6 = @config.use_ipv6? + unless use_ipv6.nil? + return use_ipv6 + end + + begin + list = Socket.ip_address_list + rescue NotImplementedError + return true + end + list.any? {|a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? } + end + private :use_ipv6? + + ## + # Gets the hostname for +address+ from the DNS resolver. + # + # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved + # name will be a Resolv::DNS::Name. + def getname(address) each_name(address) {|name| return name} raise ResolvError.new("DNS result has no information for #{address}") end + ## + # Gets all hostnames for +address+ from the DNS resolver. + # + # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved + # names will be Resolv::DNS::Name instances. + def getnames(address) ret = [] each_name(address) {|name| ret << name} return ret end + ## + # Iterates over all hostnames for +address+ retrieved from the DNS + # resolver. + # + # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved + # names will be Resolv::DNS::Name instances. + def each_name(address) case address when Name ptr = address + when IPv4, IPv6 + ptr = address.to_name when IPv4::Regex ptr = IPv4.create(address).to_name when IPv6::Regex @@ -441,58 +477,142 @@ class Resolv each_resource(ptr, Resource::IN::PTR) {|resource| yield resource.name} end + ## + # Look up the +typeclass+ DNS resource of +name+. + # + # +name+ must be a Resolv::DNS::Name or a String. + # + # +typeclass+ should be one of the following: + # + # * Resolv::DNS::Resource::IN::A + # * Resolv::DNS::Resource::IN::AAAA + # * Resolv::DNS::Resource::IN::ANY + # * Resolv::DNS::Resource::IN::CAA + # * Resolv::DNS::Resource::IN::CNAME + # * Resolv::DNS::Resource::IN::HINFO + # * Resolv::DNS::Resource::IN::HTTPS + # * Resolv::DNS::Resource::IN::LOC + # * Resolv::DNS::Resource::IN::MINFO + # * Resolv::DNS::Resource::IN::MX + # * Resolv::DNS::Resource::IN::NS + # * Resolv::DNS::Resource::IN::PTR + # * Resolv::DNS::Resource::IN::SOA + # * Resolv::DNS::Resource::IN::SRV + # * Resolv::DNS::Resource::IN::SVCB + # * Resolv::DNS::Resource::IN::TXT + # * Resolv::DNS::Resource::IN::WKS + # + # Returned resource is represented as a Resolv::DNS::Resource instance, + # i.e. Resolv::DNS::Resource::IN::A. + def getresource(name, typeclass) each_resource(name, typeclass) {|resource| return resource} raise ResolvError.new("DNS result has no information for #{name}") end + ## + # Looks up all +typeclass+ DNS resources for +name+. See #getresource for + # argument details. + def getresources(name, typeclass) ret = [] each_resource(name, typeclass) {|resource| ret << resource} return ret end + ## + # Iterates over all +typeclass+ DNS resources for +name+. See + # #getresource for argument details. + def each_resource(name, typeclass, &proc) + fetch_resource(name, typeclass) {|reply, reply_name| + extract_resources(reply, reply_name, typeclass, &proc) + } + end + + # :stopdoc: + + def fetch_resource(name, typeclass) lazy_initialize - q = Queue.new + truncated = {} + requesters = {} + udp_requester = begin + make_udp_requester + rescue Errno::EACCES + # fall back to TCP + end senders = {} + begin - @config.resolv(name) {|candidate, tout, nameserver| + @config.resolv(name) do |candidate, tout, nameserver, port| msg = Message.new msg.rd = 1 msg.add_question(candidate, typeclass) - unless sender = senders[[candidate, nameserver]] - sender = senders[[candidate, nameserver]] = - @requester.sender(msg, candidate, q, nameserver) + + requester = requesters.fetch([nameserver, port]) do + if !truncated[candidate] && udp_requester + udp_requester + else + requesters[[nameserver, port]] = make_tcp_requester(nameserver, port) + end end - sender.send - reply = reply_name = nil - timeout(tout, ResolvTimeout) { reply, reply_name = q.pop } + + unless sender = senders[[candidate, requester, nameserver, port]] + sender = requester.sender(msg, candidate, nameserver, port) + next if !sender + senders[[candidate, requester, nameserver, port]] = sender + end + reply, reply_name = requester.request(sender, tout) case reply.rcode when RCode::NoError - extract_resources(reply, reply_name, typeclass, &proc) + if reply.tc == 1 and not Requester::TCP === requester + # Retry via TCP: + truncated[candidate] = true + redo + else + yield(reply, reply_name) + end return when RCode::NXDomain raise Config::NXDomain.new(reply_name.to_s) else raise Config::OtherResolvError.new(reply_name.to_s) end - } + end ensure - @requester.delete(q) + udp_requester&.close + requesters.each_value { |requester| requester&.close } end end - def extract_resources(msg, name, typeclass) + def make_udp_requester # :nodoc: + nameserver_port = @config.nameserver_port + if nameserver_port.length == 1 + Requester::ConnectedUDP.new(*nameserver_port[0]) + else + Requester::UnconnectedUDP.new(*nameserver_port) + end + end + + def make_tcp_requester(host, port) # :nodoc: + return Requester::TCP.new(host, port) + rescue Errno::ECONNREFUSED + # Treat a refused TCP connection attempt to a nameserver like a timeout, + # as Resolv::DNS::Config#resolv considers ResolvTimeout exceptions as a + # hint to try the next nameserver: + raise ResolvTimeout + end + + def extract_resources(msg, name, typeclass) # :nodoc: if typeclass < Resource::ANY n0 = Name.create(name) - msg.each_answer {|n, ttl, data| + msg.each_resource {|n, ttl, data| yield data if n0 == n } end yielded = false n0 = Name.create(name) - msg.each_answer {|n, ttl, data| + msg.each_resource {|n, ttl, data| if n0 == n case data when typeclass @@ -504,7 +624,7 @@ class Resolv end } return if yielded - msg.each_answer {|n, ttl, data| + msg.each_resource {|n, ttl, data| if n0 == n case data when typeclass @@ -514,237 +634,457 @@ class Resolv } end - class Requester + def self.random(arg) # :nodoc: + begin + SecureRandom.random_number(arg) + rescue NotImplementedError + rand(arg) + end + end + + RequestID = {} # :nodoc: + RequestIDMutex = Thread::Mutex.new # :nodoc: + + def self.allocate_request_id(host, port) # :nodoc: + id = nil + RequestIDMutex.synchronize { + h = (RequestID[[host, port]] ||= {}) + begin + id = random(0x0000..0xffff) + end while h[id] + h[id] = true + } + id + end + + def self.free_request_id(host, port, id) # :nodoc: + RequestIDMutex.synchronize { + key = [host, port] + if h = RequestID[key] + h.delete id + if h.empty? + RequestID.delete key + end + end + } + end + + case RUBY_PLATFORM + when *[ + # https://www.rfc-editor.org/rfc/rfc6056.txt + # Appendix A. Survey of the Algorithms in Use by Some Popular Implementations + /freebsd/, /linux/, /netbsd/, /openbsd/, /solaris/, + /darwin/, # the same as FreeBSD + ] then + def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: + udpsock.bind(bind_host, 0) + end + else + # Sequential port assignment + def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: + # Ephemeral port number range recommended by RFC 6056 + port = random(1024..65535) + udpsock.bind(bind_host, port) + rescue Errno::EADDRINUSE, # POSIX + Errno::EACCES, # SunOS: See PRIV_SYS_NFS in privileges(5) + Errno::EPERM # FreeBSD: security.mac.portacl.port_high is configurable. See mac_portacl(4). + retry + end + end + + class Requester # :nodoc: def initialize @senders = {} + @socks = nil end - def close - thread, sock, @thread, @sock = @thread, @sock + def request(sender, tout) + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timelimit = start + tout begin - if thread - thread.kill - thread.join + sender.send + rescue Errno::EHOSTUNREACH, # multi-homed IPv6 may generate this + Errno::ENETUNREACH + raise ResolvTimeout + end + while true + before_select = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout = timelimit - before_select + if timeout <= 0 + raise ResolvTimeout + end + if @socks.size == 1 + select_result = @socks[0].wait_readable(timeout) ? [ @socks ] : nil + else + select_result = IO.select(@socks, nil, nil, timeout) + end + if !select_result + after_select = Process.clock_gettime(Process::CLOCK_MONOTONIC) + next if after_select < timelimit + raise ResolvTimeout + end + begin + reply, from = recv_reply(select_result[0]) + rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD + Errno::ECONNRESET, # Windows + EOFError + # No name server running on the server? + # Don't wait anymore. + raise ResolvTimeout + end + begin + msg = Message.decode(reply) + rescue DecodeError + next # broken DNS message ignored + end + if sender == sender_for(from, msg) + break + else + # unexpected DNS message ignored end - ensure - sock.close if sock end + return msg, sender.data end - def delete(arg) - case arg - when Sender - @senders.delete_if {|k, s| s == arg } - when Queue - @senders.delete_if {|k, s| s.queue == arg } - else - raise ArgumentError.new("neither Sender or Queue: #{arg}") - end + def sender_for(addr, msg) + @senders[[addr,msg.id]] + end + + def close + socks = @socks + @socks = nil + socks&.each(&:close) end - class Sender - def initialize(msg, data, sock, queue) + class Sender # :nodoc: + def initialize(msg, data, sock) @msg = msg @data = data @sock = sock - @queue = queue - end - attr_reader :queue - - def recv(msg) - @queue.push([msg, @data]) end end - class UnconnectedUDP < Requester - def initialize + class UnconnectedUDP < Requester # :nodoc: + def initialize(*nameserver_port) super() - @sock = UDPSocket.new - @sock.fcntl(Fcntl::F_SETFD, 1) if defined? Fcntl::F_SETFD - @id = {} - @id.default = -1 - @thread = Thread.new { - DNSThreadGroup.add Thread.current - loop { - reply, from = @sock.recvfrom(UDPSize) - msg = begin - Message.decode(reply) - rescue DecodeError - STDERR.print("DNS message decoding error: #{reply.inspect}\n") - next - end - if s = @senders[[[from[3],from[1]],msg.id]] - s.recv msg + @nameserver_port = nameserver_port + @initialized = false + @mutex = Thread::Mutex.new + end + + def lazy_initialize + @mutex.synchronize { + next if @initialized + @initialized = true + @socks_hash = {} + @socks = [] + @nameserver_port.each {|host, port| + if host.index(':') + bind_host = "::" + af = Socket::AF_INET6 else - #STDERR.print("non-handled DNS message: #{msg.inspect} from #{from.inspect}\n") + bind_host = "0.0.0.0" + af = Socket::AF_INET + end + next if @socks_hash[bind_host] + begin + sock = UDPSocket.new(af) + rescue Errno::EAFNOSUPPORT, Errno::EPROTONOSUPPORT + next # The kernel doesn't support the address family. end + @socks << sock + @socks_hash[bind_host] = sock + sock.do_not_reverse_lookup = true + DNS.bind_random_port(sock, bind_host) } } + self + end + + def recv_reply(readable_socks) + lazy_initialize + reply, from = readable_socks[0].recvfrom(UDPSize) + return reply, [from[3],from[1]] end - def sender(msg, data, queue, host, port=Port) + def sender(msg, data, host, port=Port) + host = Addrinfo.ip(host).ip_address + lazy_initialize + sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"] + return nil if !sock service = [host, port] - id = Thread.exclusive { - @id[service] = (@id[service] + 1) & 0xffff - } + id = DNS.allocate_request_id(host, port) request = msg.encode request[0,2] = [id].pack('n') return @senders[[service, id]] = - Sender.new(request, data, @sock, host, port, queue) + Sender.new(request, data, sock, host, port) end - class Sender < Requester::Sender - def initialize(msg, data, sock, host, port, queue) - super(msg, data, sock, queue) + def close + @mutex.synchronize { + if @initialized + super + @senders.each_key {|service, id| + DNS.free_request_id(service[0], service[1], id) + } + @initialized = false + end + } + end + + class Sender < Requester::Sender # :nodoc: + def initialize(msg, data, sock, host, port) + super(msg, data, sock) @host = host @port = port end + attr_reader :data def send + raise "@sock is nil." if @sock.nil? @sock.send(@msg, 0, @host, @port) end end end - class ConnectedUDP < Requester + class ConnectedUDP < Requester # :nodoc: def initialize(host, port=Port) super() @host = host @port = port - @sock = UDPSocket.new - @sock.connect(host, port) - @sock.fcntl(Fcntl::F_SETFD, 1) if defined? Fcntl::F_SETFD - @id = -1 - @thread = Thread.new { - DNSThreadGroup.add Thread.current - loop { - reply = @sock.recv(UDPSize) - msg = begin - Message.decode(reply) - rescue DecodeError - STDERR.print("DNS message decoding error: #{reply.inspect}") - next - end - if s = @senders[msg.id] - s.recv msg - else - #STDERR.print("non-handled DNS message: #{msg.inspect}") - end - } + @mutex = Thread::Mutex.new + @initialized = false + end + + def lazy_initialize + @mutex.synchronize { + next if @initialized + @initialized = true + is_ipv6 = @host.index(':') + sock = UDPSocket.new(is_ipv6 ? Socket::AF_INET6 : Socket::AF_INET) + @socks = [sock] + sock.do_not_reverse_lookup = true + DNS.bind_random_port(sock, is_ipv6 ? "::" : "0.0.0.0") + sock.connect(@host, @port) } + self + end + + def recv_reply(readable_socks) + lazy_initialize + reply = readable_socks[0].recv(UDPSize) + return reply, nil end - def sender(msg, data, queue, host=@host, port=@port) + def sender(msg, data, host=@host, port=@port) + lazy_initialize unless host == @host && port == @port raise RequestError.new("host/port don't match: #{host}:#{port}") end - id = Thread.exclusive { @id = (@id + 1) & 0xffff } + id = DNS.allocate_request_id(@host, @port) request = msg.encode request[0,2] = [id].pack('n') - return @senders[id] = Sender.new(request, data, @sock, queue) + return @senders[[nil,id]] = Sender.new(request, data, @socks[0]) end - class Sender < Requester::Sender + def close + @mutex.synchronize do + if @initialized + super + @senders.each_key {|from, id| + DNS.free_request_id(@host, @port, id) + } + @initialized = false + end + end + end + + class Sender < Requester::Sender # :nodoc: def send + raise "@sock is nil." if @sock.nil? @sock.send(@msg, 0) end + attr_reader :data end end - class TCP < Requester + class MDNSOneShot < UnconnectedUDP # :nodoc: + def sender(msg, data, host, port=Port) + lazy_initialize + id = DNS.allocate_request_id(host, port) + request = msg.encode + request[0,2] = [id].pack('n') + sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"] + return @senders[id] = + UnconnectedUDP::Sender.new(request, data, sock, host, port) + end + + def sender_for(addr, msg) + lazy_initialize + @senders[msg.id] + end + end + + class TCP < Requester # :nodoc: def initialize(host, port=Port) super() @host = host @port = port - @sock = TCPSocket.new - @sock.connect(host, port) - @sock.fcntl(Fcntl::F_SETFD, 1) if defined? Fcntl::F_SETFD - @id = -1 + sock = TCPSocket.new(@host, @port) + @socks = [sock] @senders = {} - @thread = Thread.new { - DNSThreadGroup.add Thread.current - loop { - len = @sock.read(2).unpack('n') - reply = @sock.read(len) - msg = begin - Message.decode(reply) - rescue DecodeError - STDERR.print("DNS message decoding error: #{reply.inspect}") - next - end - if s = @senders[msg.id] - s.push msg - else - #STDERR.print("non-handled DNS message: #{msg.inspect}") - end - } - } end - def sender(msg, data, queue, host=@host, port=@port) + def recv_reply(readable_socks) + len_data = readable_socks[0].read(2) + raise EOFError if len_data.nil? || len_data.bytesize != 2 + len = len_data.unpack('n')[0] + reply = @socks[0].read(len) + raise EOFError if reply.nil? || reply.bytesize != len + return reply, nil + end + + def sender(msg, data, host=@host, port=@port) unless host == @host && port == @port raise RequestError.new("host/port don't match: #{host}:#{port}") end - id = Thread.exclusive { @id = (@id + 1) & 0xffff } + id = DNS.allocate_request_id(@host, @port) request = msg.encode request[0,2] = [request.length, id].pack('nn') - return @senders[id] = Sender.new(request, data, @sock, queue) + return @senders[[nil,id]] = Sender.new(request, data, @socks[0]) end - class Sender < Requester::Sender + class Sender < Requester::Sender # :nodoc: def send @sock.print(@msg) @sock.flush end + attr_reader :data + end + + def close + super + @senders.each_key {|from,id| + DNS.free_request_id(@host, @port, id) + } end end + ## + # Indicates a problem with the DNS request. + class RequestError < StandardError end end - class Config - def initialize(filename="/etc/resolv.conf") - @mutex = Mutex.new - @filename = filename + class Config # :nodoc: + def initialize(config_info=nil) + @mutex = Thread::Mutex.new + @config_info = config_info @initialized = nil + @timeouts = nil + end + + def timeouts=(values) + if values + values = Array(values) + values.each do |t| + Numeric === t or raise ArgumentError, "#{t.inspect} is not numeric" + t > 0.0 or raise ArgumentError, "timeout=#{t} must be positive" + end + @timeouts = values + else + @timeouts = nil + end + end + + def Config.parse_resolv_conf(filename) + nameserver = [] + search = nil + ndots = 1 + File.open(filename, 'rb') {|f| + f.each {|line| + line.sub!(/[#;].*/, '') + keyword, *args = line.split(/\s+/) + next unless keyword + case keyword + when 'nameserver' + nameserver.concat(args.each(&:freeze)) + when 'domain' + next if args.empty? + search = [args[0].freeze] + when 'search' + next if args.empty? + search = args.each(&:freeze) + when 'options' + args.each {|arg| + case arg + when /\Andots:(\d+)\z/ + ndots = $1.to_i + end + } + end + } + } + return { :nameserver => nameserver.freeze, :search => search.freeze, :ndots => ndots.freeze }.freeze + end + + def Config.default_config_hash(filename="/etc/resolv.conf") + if File.exist? filename + Config.parse_resolv_conf(filename) + elsif defined?(Win32::Resolv) + search, nameserver = Win32::Resolv.get_resolv_info + config_hash = {} + config_hash[:nameserver] = nameserver if nameserver + config_hash[:search] = [search].flatten if search + config_hash + else + {} + end end def lazy_initialize @mutex.synchronize { unless @initialized - @nameserver = [] + @nameserver_port = [] + @use_ipv6 = nil @search = nil @ndots = 1 - begin - open(@filename) {|f| - f.each {|line| - line.sub!(/[#;].*/, '') - keyword, *args = line.split(/\s+/) - args.each { |arg| - arg.untaint - } - next unless keyword - case keyword - when 'nameserver' - @nameserver += args - when 'domain' - @search = [Label.split(args[0])] - when 'search' - @search = args.map {|arg| Label.split(arg)} - end - } - } - rescue Errno::ENOENT - if /mswin32|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM - search, nameserver = Win32::Resolv.get_resolv_info - @search = [search] if search - @nameserver = nameserver if nameserver + case @config_info + when nil + config_hash = Config.default_config_hash + when String + config_hash = Config.parse_resolv_conf(@config_info) + when Hash + config_hash = @config_info.dup + if String === config_hash[:nameserver] + config_hash[:nameserver] = [config_hash[:nameserver]] end + if String === config_hash[:search] + config_hash[:search] = [config_hash[:search]] + end + else + raise ArgumentError.new("invalid resolv configuration: #{@config_info.inspect}") + end + if config_hash.include? :nameserver + @nameserver_port = config_hash[:nameserver].map {|ns| [ns, Port] } end + if config_hash.include? :nameserver_port + @nameserver_port = config_hash[:nameserver_port].map {|ns, port| [ns, (port || Port)] } + end + if config_hash.include? :use_ipv6 + @use_ipv6 = config_hash[:use_ipv6] + end + @search = config_hash[:search] if config_hash.include? :search + @ndots = config_hash[:ndots] if config_hash.include? :ndots + @raise_timeout_errors = config_hash[:raise_timeout_errors] - @nameserver = ['0.0.0.0'] if @nameserver.empty? - unless @search + if @nameserver_port.empty? + @nameserver_port << ['0.0.0.0', Port] + end + if @search + @search = @search.map {|arg| Label.split(arg) } + else hostname = Socket.gethostname if /\./ =~ hostname @search = [Label.split($')] @@ -752,20 +1092,49 @@ class Resolv @search = [[]] end end + + if !@nameserver_port.kind_of?(Array) || + @nameserver_port.any? {|ns_port| + !(Array === ns_port) || + ns_port.length != 2 + !(String === ns_port[0]) || + !(Integer === ns_port[1]) + } + raise ArgumentError.new("invalid nameserver config: #{@nameserver_port.inspect}") + end + + if !@search.kind_of?(Array) || + !@search.all? {|ls| ls.all? {|l| Label::Str === l } } + raise ArgumentError.new("invalid search config: #{@search.inspect}") + end + + if !@ndots.kind_of?(Integer) + raise ArgumentError.new("invalid ndots config: #{@ndots.inspect}") + end + @initialized = true end } + self end def single? lazy_initialize - if @nameserver.length == 1 - return @nameserver[0] + if @nameserver_port.length == 1 + return @nameserver_port[0] else return nil end end + def nameserver_port + @nameserver_port + end + + def use_ipv6? + @use_ipv6 + end + def generate_candidates(name) candidates = nil name = Name.create(name) @@ -778,6 +1147,10 @@ class Resolv candidates = [] end candidates.concat(@search.map {|domain| Name.new(name.to_a + domain)}) + fname = Name.create("#{name}.") + if !candidates.include?(fname) + candidates << fname + end end return candidates end @@ -786,7 +1159,7 @@ class Resolv def generate_timeouts ts = [InitialTimeout] - ts << ts[-1] * 2 / @nameserver.length + ts << ts[-1] * 2 / @nameserver_port.length ts << ts[-1] * 2 ts << ts[-1] * 2 return ts @@ -794,36 +1167,43 @@ class Resolv def resolv(name) candidates = generate_candidates(name) - timeouts = generate_timeouts + timeouts = @timeouts || generate_timeouts + timeout_error = false begin candidates.each {|candidate| begin timeouts.each {|tout| - @nameserver.each {|nameserver| + @nameserver_port.each {|nameserver, port| begin - yield candidate, tout, nameserver + yield candidate, tout, nameserver, port rescue ResolvTimeout end } } + timeout_error = true raise ResolvError.new("DNS resolv timeout: #{name}") rescue NXDomain end } - rescue OtherResolvError - raise ResolvError.new("DNS error: #{$!.message}") + rescue ResolvError + raise if @raise_timeout_errors && timeout_error end - raise ResolvError.new("DNS resolv error: #{name}") end + ## + # Indicates no such domain was found. + class NXDomain < ResolvError end + ## + # Indicates some other unhandled resolver error was encountered. + class OtherResolvError < ResolvError end end - module OpCode + module OpCode # :nodoc: Query = 0 IQuery = 1 Status = 2 @@ -831,7 +1211,7 @@ class Resolv Update = 5 end - module RCode + module RCode # :nodoc: NoError = 0 FormErr = 1 ServFail = 2 @@ -852,23 +1232,31 @@ class Resolv BADALG = 21 end + ## + # Indicates that the DNS response was unable to be decoded. + class DecodeError < StandardError end + ## + # Indicates that the DNS request was unable to be encoded. + class EncodeError < StandardError end - module Label + module Label # :nodoc: def self.split(arg) labels = [] arg.scan(/[^\.]+/) {labels << Str.new($&)} return labels end - class Str + class Str # :nodoc: def initialize(string) @string = string - @downcase = string.downcase + # case insensivity of DNS labels doesn't apply non-ASCII characters. [RFC 4343] + # This assumes @string is given in ASCII compatible encoding. + @downcase = string.b.downcase end attr_reader :string, :downcase @@ -877,11 +1265,11 @@ class Resolv end def inspect - return "#<#{self.class} #{self.to_s}>" + return "#<#{self.class} #{self}>" end def ==(other) - return @downcase == other.downcase + return self.class == other.class && @downcase == other.downcase end def eql?(other) @@ -894,7 +1282,17 @@ class Resolv end end + ## + # A representation of a DNS name. + class Name + + ## + # Creates a new DNS name from +arg+. +arg+ can be: + # + # Name:: returns +arg+. + # String:: Creates a new Name. + def self.create(arg) case arg when Name @@ -906,42 +1304,93 @@ class Resolv end end - def initialize(labels, absolute=true) + def initialize(labels, absolute=true) # :nodoc: + labels = labels.map {|label| + case label + when String then Label::Str.new(label) + when Label::Str then label + else + raise ArgumentError, "unexpected label: #{label.inspect}" + end + } @labels = labels @absolute = absolute end + def inspect # :nodoc: + "#<#{self.class}: #{self}#{@absolute ? '.' : ''}>" + end + + ## + # True if this name is absolute. + def absolute? return @absolute end - def ==(other) - return @labels == other.to_a && @absolute == other.absolute? + def ==(other) # :nodoc: + return false unless Name === other + return false unless @absolute == other.absolute? + return @labels == other.to_a end - alias eql? == - def hash + alias eql? == # :nodoc: + + ## + # Returns true if +other+ is a subdomain. + # + # Example: + # + # domain = Resolv::DNS::Name.create("y.z") + # p Resolv::DNS::Name.create("w.x.y.z").subdomain_of?(domain) #=> true + # p Resolv::DNS::Name.create("x.y.z").subdomain_of?(domain) #=> true + # p Resolv::DNS::Name.create("y.z").subdomain_of?(domain) #=> false + # p Resolv::DNS::Name.create("z").subdomain_of?(domain) #=> false + # p Resolv::DNS::Name.create("x.y.z.").subdomain_of?(domain) #=> false + # p Resolv::DNS::Name.create("w.z").subdomain_of?(domain) #=> false + # + + def subdomain_of?(other) + raise ArgumentError, "not a domain name: #{other.inspect}" unless Name === other + return false if @absolute != other.absolute? + other_len = other.length + return false if @labels.length <= other_len + return @labels[-other_len, other_len] == other.to_a + end + + def hash # :nodoc: return @labels.hash ^ @absolute.hash end - def to_a + def to_a # :nodoc: return @labels end - def length + def length # :nodoc: return @labels.length end - def [](i) + def [](i) # :nodoc: return @labels[i] end + ## + # returns the domain name as a string. + # + # The domain name doesn't have a trailing dot even if the name object is + # absolute. + # + # Example: + # + # p Resolv::DNS::Name.create("x.y.z.").to_s #=> "x.y.z" + # p Resolv::DNS::Name.create("x.y.z").to_s #=> "x.y.z" + def to_s return @labels.join('.') end end - class Message + class Message # :nodoc: @@identifier = -1 def initialize(id = (@@identifier += 1) & 0xffff) @@ -1054,9 +1503,9 @@ class Resolv }.to_s end - class MessageEncoder + class MessageEncoder # :nodoc: def initialize - @data = '' + @data = ''.dup @names = {} yield self end @@ -1087,18 +1536,26 @@ class Resolv @data << d end - def put_name(d) - put_labels(d.to_a) + def put_string_list(ds) + ds.each {|d| + self.put_string(d) + } + end + + def put_name(d, compress: true) + put_labels(d.to_a, compress: compress) end - def put_labels(d) + def put_labels(d, compress: true) d.each_index {|i| domain = d[i..-1] - if idx = @names[domain] + if compress && idx = @names[domain] self.put_pack("n", 0xc000 | idx) return else - @names[domain] = @data.length + if @data.length < 0x4000 + @names[domain] = @data.length + end self.put_label(d[i]) end } @@ -1106,7 +1563,7 @@ class Resolv end def put_label(d) - self.put_string(d.string) + self.put_string(d.to_s) end end @@ -1116,13 +1573,15 @@ class Resolv id, flag, qdcount, ancount, nscount, arcount = msg.get_unpack('nnnnnn') o.id = id + o.tc = (flag >> 9) & 1 + o.rcode = flag & 15 + return o unless o.tc.zero? + o.qr = (flag >> 15) & 1 o.opcode = (flag >> 11) & 15 o.aa = (flag >> 10) & 1 - o.tc = (flag >> 9) & 1 o.rd = (flag >> 8) & 1 o.ra = (flag >> 7) & 1 - o.rcode = flag & 15 (1..qdcount).each { name, typeclass = msg.get_question o.add_question(name, typeclass) @@ -1143,30 +1602,35 @@ class Resolv return o end - class MessageDecoder + class MessageDecoder # :nodoc: def initialize(data) @data = data @index = 0 - @limit = data.length + @limit = data.bytesize yield self end + def inspect + "\#<#{self.class}: #{@data.byteslice(0, @index).inspect} #{@data.byteslice(@index..-1).inspect}>" + end + def get_length16 len, = self.get_unpack('n') save_limit = @limit @limit = @index + len d = yield(len) if @index < @limit - raise DecodeError.new("junk exist") + raise DecodeError.new("junk exists") elsif @limit < @index - raise DecodeError.new("limit exceed") + raise DecodeError.new("limit exceeded") end @limit = save_limit return d end def get_bytes(len = @limit - @index) - d = @data[@index, len] + raise DecodeError.new("limit exceeded") if @limit < @index + len + d = @data.byteslice(@index, len) @index += len return d end @@ -1174,6 +1638,7 @@ class Resolv def get_unpack(template) len = 0 template.each_byte {|byte| + byte = "%c" % byte case byte when ?c, ?C len += 1 @@ -1185,47 +1650,72 @@ class Resolv raise StandardError.new("unsupported template: '#{byte.chr}' in '#{template}'") end } - raise DecodeError.new("limit exceed") if @limit < @index + len + raise DecodeError.new("limit exceeded") if @limit < @index + len arr = @data.unpack("@#{@index}#{template}") @index += len return arr end def get_string - len = @data[@index] - raise DecodeError.new("limit exceed") if @limit < @index + 1 + len - d = @data[@index + 1, len] + raise DecodeError.new("limit exceeded") if @limit <= @index + len = @data.getbyte(@index) + raise DecodeError.new("limit exceeded") if @limit < @index + 1 + len + d = @data.byteslice(@index + 1, len) @index += 1 + len return d end + def get_string_list + strings = [] + while @index < @limit + strings << self.get_string + end + strings + end + + def get_list + [].tap do |values| + while @index < @limit + values << yield + end + end + end + def get_name return Name.new(self.get_labels) end - def get_labels(limit=nil) - limit = @index if !limit || @index < limit + def get_labels + prev_index = @index + save_index = nil d = [] + size = -1 while true - case @data[@index] + raise DecodeError.new("limit exceeded") if @limit <= @index + case @data.getbyte(@index) when 0 @index += 1 + if save_index + @index = save_index + end return d when 192..255 idx = self.get_unpack('n')[0] & 0x3fff - if limit <= idx + if prev_index <= idx raise DecodeError.new("non-backward name pointer") end - save_index = @index + prev_index = idx + if !save_index + save_index = @index + end @index = idx - d += self.get_labels(limit) - @index = save_index - return d else - d << self.get_label + l = self.get_label + d << l + size += 1 + l.string.bytesize + raise DecodeError.new("name label data exceed 255 octets") if size > 255 end end - return d end def get_label @@ -1242,71 +1732,490 @@ class Resolv name = self.get_name type, klass, ttl = self.get_unpack('nnN') typeclass = Resource.get_class(type, klass) - return name, ttl, self.get_length16 {typeclass.decode_rdata(self)} + res = self.get_length16 do + begin + typeclass.decode_rdata self + rescue => e + raise DecodeError, e.message, e.backtrace + end + end + res.instance_variable_set :@ttl, ttl + return name, ttl, res + end + end + end + + ## + # SvcParams for service binding RRs. [RFC9460] + + class SvcParams + include Enumerable + + ## + # Create a list of SvcParams with the given initial content. + # + # +params+ has to be an enumerable of +SvcParam+s. + # If its content has +SvcParam+s with the duplicate key, + # the one appears last takes precedence. + + def initialize(params = []) + @params = {} + + params.each do |param| + add param + end + end + + ## + # Get SvcParam for the given +key+ in this list. + + def [](key) + @params[canonical_key(key)] + end + + ## + # Get the number of SvcParams in this list. + + def count + @params.count + end + + ## + # Get whether this list is empty. + + def empty? + @params.empty? + end + + ## + # Add the SvcParam +param+ to this list, overwriting the existing one with the same key. + + def add(param) + @params[param.class.key_number] = param + end + + ## + # Remove the +SvcParam+ with the given +key+ and return it. + + def delete(key) + @params.delete(canonical_key(key)) + end + + ## + # Enumerate the +SvcParam+s in this list. + + def each(&block) + return enum_for(:each) unless block + @params.each_value(&block) + end + + def encode(msg) # :nodoc: + @params.keys.sort.each do |key| + msg.put_pack('n', key) + msg.put_length16 do + @params.fetch(key).encode(msg) + end + end + end + + def self.decode(msg) # :nodoc: + params = msg.get_list do + key, = msg.get_unpack('n') + msg.get_length16 do + SvcParam::ClassHash[key].decode(msg) + end + end + + return self.new(params) + end + + private + + def canonical_key(key) # :nodoc: + case key + when Integer + key + when /\Akey(\d+)\z/ + Integer($1) + when Symbol + SvcParam::ClassHash[key].key_number + else + raise TypeError, 'key must be either String or Symbol' + end + end + end + + ## + # Base class for SvcParam. [RFC9460] + + class SvcParam + + ## + # Get the presentation name of the SvcParamKey. + + def self.key_name + const_get(:KeyName) + end + + ## + # Get the registered number of the SvcParamKey. + + def self.key_number + const_get(:KeyNumber) + end + + ClassHash = Hash.new do |h, key| # :nodoc: + case key + when Integer + Generic.create(key) + when /\Akey(?<key>\d+)\z/ + Generic.create(key.to_int) + when Symbol + raise KeyError, "unknown key #{key}" + else + raise TypeError, 'key must be either String or Symbol' + end + end + + ## + # Generic SvcParam abstract class. + + class Generic < SvcParam + + ## + # SvcParamValue in wire-format byte string. + + attr_reader :value + + ## + # Create generic SvcParam + + def initialize(value) + @value = value + end + + def encode(msg) # :nodoc: + msg.put_bytes(@value) + end + + def self.decode(msg) # :nodoc: + return self.new(msg.get_bytes) + end + + def self.create(key_number) + c = Class.new(Generic) + key_name = :"key#{key_number}" + c.const_set(:KeyName, key_name) + c.const_set(:KeyNumber, key_number) + self.const_set(:"Key#{key_number}", c) + ClassHash[key_name] = ClassHash[key_number] = c + return c + end + end + + ## + # "mandatory" SvcParam -- Mandatory keys in service binding RR + + class Mandatory < SvcParam + KeyName = :mandatory + KeyNumber = 0 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Mandatory keys. + + attr_reader :keys + + ## + # Initialize "mandatory" ScvParam. + + def initialize(keys) + @keys = keys.map(&:to_int) + end + + def encode(msg) # :nodoc: + @keys.sort.each do |key| + msg.put_pack('n', key) + end + end + + def self.decode(msg) # :nodoc: + keys = msg.get_list { msg.get_unpack('n')[0] } + return self.new(keys) + end + end + + ## + # "alpn" SvcParam -- Additional supported protocols + + class ALPN < SvcParam + KeyName = :alpn + KeyNumber = 1 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Supported protocol IDs. + + attr_reader :protocol_ids + + ## + # Initialize "alpn" ScvParam. + + def initialize(protocol_ids) + @protocol_ids = protocol_ids.map(&:to_str) + end + + def encode(msg) # :nodoc: + msg.put_string_list(@protocol_ids) + end + + def self.decode(msg) # :nodoc: + return self.new(msg.get_string_list) + end + end + + ## + # "no-default-alpn" SvcParam -- No support for default protocol + + class NoDefaultALPN < SvcParam + KeyName = :'no-default-alpn' + KeyNumber = 2 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + def encode(msg) # :nodoc: + # no payload + end + + def self.decode(msg) # :nodoc: + return self.new + end + end + + ## + # "port" SvcParam -- Port for alternative endpoint + + class Port < SvcParam + KeyName = :port + KeyNumber = 3 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Port number. + + attr_reader :port + + ## + # Initialize "port" ScvParam. + + def initialize(port) + @port = port.to_int + end + + def encode(msg) # :nodoc: + msg.put_pack('n', @port) + end + + def self.decode(msg) # :nodoc: + port, = msg.get_unpack('n') + return self.new(port) + end + end + + ## + # "ipv4hint" SvcParam -- IPv4 address hints + + class IPv4Hint < SvcParam + KeyName = :ipv4hint + KeyNumber = 4 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Set of IPv4 addresses. + + attr_reader :addresses + + ## + # Initialize "ipv4hint" ScvParam. + + def initialize(addresses) + @addresses = addresses.map {|address| IPv4.create(address) } + end + + def encode(msg) # :nodoc: + @addresses.each do |address| + msg.put_bytes(address.address) + end + end + + def self.decode(msg) # :nodoc: + addresses = msg.get_list { IPv4.new(msg.get_bytes(4)) } + return self.new(addresses) + end + end + + ## + # "ipv6hint" SvcParam -- IPv6 address hints + + class IPv6Hint < SvcParam + KeyName = :ipv6hint + KeyNumber = 6 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Set of IPv6 addresses. + + attr_reader :addresses + + ## + # Initialize "ipv6hint" ScvParam. + + def initialize(addresses) + @addresses = addresses.map {|address| IPv6.create(address) } + end + + def encode(msg) # :nodoc: + @addresses.each do |address| + msg.put_bytes(address.address) + end + end + + def self.decode(msg) # :nodoc: + addresses = msg.get_list { IPv6.new(msg.get_bytes(16)) } + return self.new(addresses) + end + end + + ## + # "dohpath" SvcParam -- DNS over HTTPS path template [RFC9461] + + class DoHPath < SvcParam + KeyName = :dohpath + KeyNumber = 7 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # URI template for DoH queries. + + attr_reader :template + + ## + # Initialize "dohpath" ScvParam. + + def initialize(template) + @template = template.encode('utf-8') + end + + def encode(msg) # :nodoc: + msg.put_bytes(@template) + end + + def self.decode(msg) # :nodoc: + template = msg.get_bytes.force_encoding('utf-8') + return self.new(template) end end end + ## + # A DNS query abstract class. + class Query - def encode_rdata(msg) - raise EncodeError.new("#{self.class} is query.") + def encode_rdata(msg) # :nodoc: + raise EncodeError.new("#{self.class} is query.") end - def self.decode_rdata(msg) - raise DecodeError.new("#{self.class} is query.") + def self.decode_rdata(msg) # :nodoc: + raise DecodeError.new("#{self.class} is query.") end end + ## + # A DNS resource abstract class. + class Resource < Query - ClassHash = {} - def encode_rdata(msg) + ## + # Remaining Time To Live for this Resource. + + attr_reader :ttl + + ClassHash = Module.new do + module_function + + def []=(type_class_value, klass) + type_value, class_value = type_class_value + Resource.const_set(:"Type#{type_value}_Class#{class_value}", klass) + end + end + + def encode_rdata(msg) # :nodoc: raise NotImplementedError.new end - def self.decode_rdata(msg) + def self.decode_rdata(msg) # :nodoc: raise NotImplementedError.new end - def ==(other) - return self.class == other.class && - self.instance_variables == other.instance_variables && - self.instance_variables.collect {|name| self.instance_eval name} == - other.instance_variables.collect {|name| other.instance_eval name} + def ==(other) # :nodoc: + return false unless self.class == other.class + s_ivars = self.instance_variables + s_ivars.sort! + s_ivars.delete :@ttl + o_ivars = other.instance_variables + o_ivars.sort! + o_ivars.delete :@ttl + return s_ivars == o_ivars && + s_ivars.collect {|name| self.instance_variable_get name} == + o_ivars.collect {|name| other.instance_variable_get name} end - def eql?(other) + def eql?(other) # :nodoc: return self == other end - def hash + def hash # :nodoc: h = 0 - self.instance_variables.each {|name| - h ^= self.instance_eval("#{name}.hash") + vars = self.instance_variables + vars.delete :@ttl + vars.each {|name| + h ^= self.instance_variable_get(name).hash } return h end - def self.get_class(type_value, class_value) - return ClassHash[[type_value, class_value]] || + def self.get_class(type_value, class_value) # :nodoc: + cache = :"Type#{type_value}_Class#{class_value}" + + return (const_defined?(cache) && const_get(cache)) || Generic.create(type_value, class_value) end + ## + # A generic resource abstract class. + class Generic < Resource + + ## + # Creates a new generic resource. + def initialize(data) @data = data end + + ## + # Data for this generic resource. + attr_reader :data - def encode_rdata(msg) + def encode_rdata(msg) # :nodoc: msg.put_bytes(data) end - def self.decode_rdata(msg) + def self.decode_rdata(msg) # :nodoc: return self.new(msg.get_bytes) end - def self.create(type_value, class_value) + def self.create(type_value, class_value) # :nodoc: c = Class.new(Generic) c.const_set(:TypeValue, type_value) c.const_set(:ClassValue, class_value) @@ -1316,34 +2225,60 @@ class Resolv end end + ## + # Domain Name resource abstract class. + class DomainName < Resource + + ## + # Creates a new DomainName from +name+. + def initialize(name) @name = name end + + ## + # The name of this DomainName. + attr_reader :name - def encode_rdata(msg) + def encode_rdata(msg) # :nodoc: msg.put_name(@name) end - def self.decode_rdata(msg) + def self.decode_rdata(msg) # :nodoc: return self.new(msg.get_name) end end # Standard (class generic) RRs - ClassValue = nil + + ClassValue = nil # :nodoc: + + ## + # An authoritative name server. class NS < DomainName - TypeValue = 2 + TypeValue = 2 # :nodoc: end + ## + # The canonical name for an alias. + class CNAME < DomainName - TypeValue = 5 + TypeValue = 5 # :nodoc: end + ## + # Start Of Authority resource. + class SOA < Resource - TypeValue = 6 + + TypeValue = 6 # :nodoc: + + ## + # Creates a new SOA record. See the attr documentation for the + # details of each argument. def initialize(mname, rname, serial, refresh, retry_, expire, minimum) @mname = mname @@ -1354,15 +2289,52 @@ class Resolv @expire = expire @minimum = minimum end - attr_reader :mname, :rname, :serial, :refresh, :retry, :expire, :minimum - def encode_rdata(msg) + ## + # Name of the host where the master zone file for this zone resides. + + attr_reader :mname + + ## + # The person responsible for this domain name. + + attr_reader :rname + + ## + # The version number of the zone file. + + attr_reader :serial + + ## + # How often, in seconds, a secondary name server is to check for + # updates from the primary name server. + + attr_reader :refresh + + ## + # How often, in seconds, a secondary name server is to retry after a + # failure to check for a refresh. + + attr_reader :retry + + ## + # Time in seconds that a secondary name server is to use the data + # before refreshing from the primary name server. + + attr_reader :expire + + ## + # The minimum number of seconds to be used for TTL values in RRs. + + attr_reader :minimum + + def encode_rdata(msg) # :nodoc: msg.put_name(@mname) msg.put_name(@rname) msg.put_pack('NNNNN', @serial, @refresh, @retry, @expire, @minimum) end - def self.decode_rdata(msg) + def self.decode_rdata(msg) # :nodoc: mname = msg.get_name rname = msg.get_name serial, refresh, retry_, expire, minimum = msg.get_unpack('NNNNN') @@ -1371,102 +2343,324 @@ class Resolv end end + ## + # A Pointer to another DNS name. + class PTR < DomainName - TypeValue = 12 + TypeValue = 12 # :nodoc: end + ## + # Host Information resource. + class HINFO < Resource - TypeValue = 13 + + TypeValue = 13 # :nodoc: + + ## + # Creates a new HINFO running +os+ on +cpu+. def initialize(cpu, os) @cpu = cpu @os = os end - attr_reader :cpu, :os - def encode_rdata(msg) + ## + # CPU architecture for this resource. + + attr_reader :cpu + + ## + # Operating system for this resource. + + attr_reader :os + + def encode_rdata(msg) # :nodoc: msg.put_string(@cpu) msg.put_string(@os) end - def self.decode_rdata(msg) + def self.decode_rdata(msg) # :nodoc: cpu = msg.get_string os = msg.get_string return self.new(cpu, os) end end + ## + # Mailing list or mailbox information. + class MINFO < Resource - TypeValue = 14 + + TypeValue = 14 # :nodoc: def initialize(rmailbx, emailbx) @rmailbx = rmailbx @emailbx = emailbx end - attr_reader :rmailbx, :emailbx - def encode_rdata(msg) + ## + # Domain name responsible for this mail list or mailbox. + + attr_reader :rmailbx + + ## + # Mailbox to use for error messages related to the mail list or mailbox. + + attr_reader :emailbx + + def encode_rdata(msg) # :nodoc: msg.put_name(@rmailbx) msg.put_name(@emailbx) end - def self.decode_rdata(msg) + def self.decode_rdata(msg) # :nodoc: rmailbx = msg.get_string emailbx = msg.get_string return self.new(rmailbx, emailbx) end end + ## + # Mail Exchanger resource. + class MX < Resource - TypeValue= 15 + + TypeValue= 15 # :nodoc: + + ## + # Creates a new MX record with +preference+, accepting mail at + # +exchange+. def initialize(preference, exchange) @preference = preference @exchange = exchange end - attr_reader :preference, :exchange - def encode_rdata(msg) + ## + # The preference for this MX. + + attr_reader :preference + + ## + # The host of this MX. + + attr_reader :exchange + + def encode_rdata(msg) # :nodoc: msg.put_pack('n', @preference) msg.put_name(@exchange) end - def self.decode_rdata(msg) + def self.decode_rdata(msg) # :nodoc: preference, = msg.get_unpack('n') exchange = msg.get_name return self.new(preference, exchange) end end + ## + # Unstructured text resource. + class TXT < Resource - TypeValue = 16 - def initialize(data) - @data = data + TypeValue = 16 # :nodoc: + + def initialize(first_string, *rest_strings) + @strings = [first_string, *rest_strings] end - attr_reader :data - def encode_rdata(msg) - msg.put_string(@data) + ## + # Returns an Array of Strings for this TXT record. + + attr_reader :strings + + ## + # Returns the concatenated string from +strings+. + + def data + @strings.join("") + end + + def encode_rdata(msg) # :nodoc: + msg.put_string_list(@strings) + end + + def self.decode_rdata(msg) # :nodoc: + strings = msg.get_string_list + return self.new(*strings) + end + end + + ## + # Location resource + + class LOC < Resource + + TypeValue = 29 # :nodoc: + + def initialize(version, ssize, hprecision, vprecision, latitude, longitude, altitude) + @version = version + @ssize = Resolv::LOC::Size.create(ssize) + @hprecision = Resolv::LOC::Size.create(hprecision) + @vprecision = Resolv::LOC::Size.create(vprecision) + @latitude = Resolv::LOC::Coord.create(latitude) + @longitude = Resolv::LOC::Coord.create(longitude) + @altitude = Resolv::LOC::Alt.create(altitude) + end + + ## + # Returns the version value for this LOC record which should always be 00 + + attr_reader :version + + ## + # The spherical size of this LOC + # in meters using scientific notation as 2 integers of XeY + + attr_reader :ssize + + ## + # The horizontal precision using ssize type values + # in meters using scientific notation as 2 integers of XeY + # for precision use value/2 e.g. 2m = +/-1m + + attr_reader :hprecision + + ## + # The vertical precision using ssize type values + # in meters using scientific notation as 2 integers of XeY + # for precision use value/2 e.g. 2m = +/-1m + + attr_reader :vprecision + + ## + # The latitude for this LOC where 2**31 is the equator + # in thousandths of an arc second as an unsigned 32bit integer + + attr_reader :latitude + + ## + # The longitude for this LOC where 2**31 is the prime meridian + # in thousandths of an arc second as an unsigned 32bit integer + + attr_reader :longitude + + ## + # The altitude of the LOC above a reference sphere whose surface sits 100km below the WGS84 spheroid + # in centimeters as an unsigned 32bit integer + + attr_reader :altitude + + def encode_rdata(msg) # :nodoc: + msg.put_bytes(@version) + msg.put_bytes(@ssize.scalar) + msg.put_bytes(@hprecision.scalar) + msg.put_bytes(@vprecision.scalar) + msg.put_bytes(@latitude.coordinates) + msg.put_bytes(@longitude.coordinates) + msg.put_bytes(@altitude.altitude) end - def self.decode_rdata(msg) - data = msg.get_string - return self.new(data) + def self.decode_rdata(msg) # :nodoc: + version = msg.get_bytes(1) + ssize = msg.get_bytes(1) + hprecision = msg.get_bytes(1) + vprecision = msg.get_bytes(1) + latitude = msg.get_bytes(4) + longitude = msg.get_bytes(4) + altitude = msg.get_bytes(4) + return self.new( + version, + Resolv::LOC::Size.new(ssize), + Resolv::LOC::Size.new(hprecision), + Resolv::LOC::Size.new(vprecision), + Resolv::LOC::Coord.new(latitude,"lat"), + Resolv::LOC::Coord.new(longitude,"lon"), + Resolv::LOC::Alt.new(altitude) + ) end end + ## + # A Query type requesting any RR. + class ANY < Query - TypeValue = 255 + TypeValue = 255 # :nodoc: + end + + ## + # CAA resource record defined in RFC 8659 + # + # These records identify certificate authority allowed to issue + # certificates for the given domain. + + class CAA < Resource + TypeValue = 257 + + ## + # Creates a new CAA for +flags+, +tag+ and +value+. + + def initialize(flags, tag, value) + unless (0..255) === flags + raise ArgumentError.new('flags must be an Integer between 0 and 255') + end + unless (1..15) === tag.bytesize + raise ArgumentError.new('length of tag must be between 1 and 15') + end + + @flags = flags + @tag = tag + @value = value + end + + ## + # Flags for this property: + # - Bit 0 : 0 = not critical, 1 = critical + + attr_reader :flags + + ## + # Property tag ("issue", "issuewild", "iodef"...). + + attr_reader :tag + + ## + # Property value. + + attr_reader :value + + ## + # Whether the critical flag is set on this property. + + def critical? + flags & 0x80 != 0 + end + + def encode_rdata(msg) # :nodoc: + msg.put_pack('C', @flags) + msg.put_string(@tag) + msg.put_bytes(@value) + end + + def self.decode_rdata(msg) # :nodoc: + flags, = msg.get_unpack('C') + tag = msg.get_string + value = msg.get_bytes + self.new flags, tag, value + end end - ClassInsensitiveTypes = [ - NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, ANY + ClassInsensitiveTypes = [ # :nodoc: + NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY, CAA ] - # ARPA Internet specific RRs + ## + # module IN contains ARPA Internet specific RRs. + module IN - ClassValue = 1 + + ClassValue = 1 # :nodoc: ClassInsensitiveTypes.each {|s| c = Class.new(s) @@ -1476,40 +2670,76 @@ class Resolv self.const_set(s.name.sub(/.*::/, ''), c) } + ## + # IPv4 Address resource + class A < Resource - ClassHash[[TypeValue = 1, ClassValue = ClassValue]] = self + TypeValue = 1 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + + ## + # Creates a new A for +address+. def initialize(address) @address = IPv4.create(address) end + + ## + # The Resolv::IPv4 address for this A. + attr_reader :address - def encode_rdata(msg) + def encode_rdata(msg) # :nodoc: msg.put_bytes(@address.address) end - def self.decode_rdata(msg) + def self.decode_rdata(msg) # :nodoc: return self.new(IPv4.new(msg.get_bytes(4))) end end + ## + # Well Known Service resource. + class WKS < Resource - ClassHash[[TypeValue = 11, ClassValue = ClassValue]] = self + TypeValue = 11 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: def initialize(address, protocol, bitmap) @address = IPv4.create(address) @protocol = protocol @bitmap = bitmap end - attr_reader :address, :protocol, :bitmap - def encode_rdata(msg) + ## + # The host these services run on. + + attr_reader :address + + ## + # IP protocol number for these services. + + attr_reader :protocol + + ## + # A bit map of enabled services on this host. + # + # If protocol is 6 (TCP) then the 26th bit corresponds to the SMTP + # service (port 25). If this bit is set, then an SMTP server should + # be listening on TCP port 25; if zero, SMTP service is not + # supported. + + attr_reader :bitmap + + def encode_rdata(msg) # :nodoc: msg.put_bytes(@address.address) msg.put_pack("n", @protocol) msg.put_bytes(@bitmap) end - def self.decode_rdata(msg) + def self.decode_rdata(msg) # :nodoc: address = IPv4.new(msg.get_bytes(4)) protocol, = msg.get_unpack("n") bitmap = msg.get_bytes @@ -1517,28 +2747,209 @@ class Resolv end end + ## + # An IPv6 address record. + class AAAA < Resource - ClassHash[[TypeValue = 28, ClassValue = ClassValue]] = self + TypeValue = 28 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + + ## + # Creates a new AAAA for +address+. def initialize(address) @address = IPv6.create(address) end + + ## + # The Resolv::IPv6 address for this AAAA. + attr_reader :address - def encode_rdata(msg) + def encode_rdata(msg) # :nodoc: msg.put_bytes(@address.address) end - def self.decode_rdata(msg) + def self.decode_rdata(msg) # :nodoc: return self.new(IPv6.new(msg.get_bytes(16))) end end + + ## + # SRV resource record defined in RFC 2782 + # + # These records identify the hostname and port that a service is + # available at. + + class SRV < Resource + TypeValue = 33 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + + # Create a SRV resource record. + # + # See the documentation for #priority, #weight, #port and #target + # for +priority+, +weight+, +port and +target+ respectively. + + def initialize(priority, weight, port, target) + @priority = priority.to_int + @weight = weight.to_int + @port = port.to_int + @target = Name.create(target) + end + + # The priority of this target host. + # + # A client MUST attempt to contact the target host with the + # lowest-numbered priority it can reach; target hosts with the same + # priority SHOULD be tried in an order defined by the weight field. + # The range is 0-65535. Note that it is not widely implemented and + # should be set to zero. + + attr_reader :priority + + # A server selection mechanism. + # + # The weight field specifies a relative weight for entries with the + # same priority. Larger weights SHOULD be given a proportionately + # higher probability of being selected. The range of this number is + # 0-65535. Domain administrators SHOULD use Weight 0 when there + # isn't any server selection to do, to make the RR easier to read + # for humans (less noisy). Note that it is not widely implemented + # and should be set to zero. + + attr_reader :weight + + # The port on this target host of this service. + # + # The range is 0-65535. + + attr_reader :port + + # The domain name of the target host. + # + # A target of "." means that the service is decidedly not available + # at this domain. + + attr_reader :target + + def encode_rdata(msg) # :nodoc: + msg.put_pack("n", @priority) + msg.put_pack("n", @weight) + msg.put_pack("n", @port) + msg.put_name(@target, compress: false) + end + + def self.decode_rdata(msg) # :nodoc: + priority, = msg.get_unpack("n") + weight, = msg.get_unpack("n") + port, = msg.get_unpack("n") + target = msg.get_name + return self.new(priority, weight, port, target) + end + end + + ## + # Common implementation for SVCB-compatible resource records. + + class ServiceBinding + + ## + # Create a service binding resource record. + + def initialize(priority, target, params = []) + @priority = priority.to_int + @target = Name.create(target) + @params = SvcParams.new(params) + end + + ## + # The priority of this target host. + # + # The range is 0-65535. + # If set to 0, this RR is in AliasMode. Otherwise, it is in ServiceMode. + + attr_reader :priority + + ## + # The domain name of the target host. + + attr_reader :target + + ## + # The service parameters for the target host. + + attr_reader :params + + ## + # Whether this RR is in AliasMode. + + def alias_mode? + self.priority == 0 + end + + ## + # Whether this RR is in ServiceMode. + + def service_mode? + !alias_mode? + end + + def encode_rdata(msg) # :nodoc: + msg.put_pack("n", @priority) + msg.put_name(@target, compress: false) + @params.encode(msg) + end + + def self.decode_rdata(msg) # :nodoc: + priority, = msg.get_unpack("n") + target = msg.get_name + params = SvcParams.decode(msg) + return self.new(priority, target, params) + end + end + + ## + # SVCB resource record [RFC9460] + + class SVCB < ServiceBinding + TypeValue = 64 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + end + + ## + # HTTPS resource record [RFC9460] + + class HTTPS < ServiceBinding + TypeValue = 65 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + end end end end + ## + # A Resolv::DNS IPv4 address. + class IPv4 - Regex = /\A(\d+)\.(\d+)\.(\d+)\.(\d+)\z/ + + Regex256 = /0 + |1(?:[0-9][0-9]?)? + |2(?:[0-4][0-9]?|5[0-5]?|[6-9])? + |[3-9][0-9]?/x # :nodoc: + + ## + # Regular expression IPv4 addresses must match. + Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/ + + ## + # Creates a new IPv4 address from +arg+ which may be: + # + # IPv4:: returns +arg+. + # String:: +arg+ must match the IPv4::Regex constant def self.create(arg) case arg @@ -1554,85 +2965,147 @@ class Resolv raise ArgumentError.new("IPv4 address with invalid value: " + arg) end else - raise ArgumentError.new("cannot interprete as IPv4 address: #{arg.inspect}") + raise ArgumentError.new("cannot interpret as IPv4 address: #{arg.inspect}") end end - def initialize(address) - unless address.kind_of?(String) && address.length == 4 - raise ArgumentError.new('IPv4 address must be 4 bytes') + def initialize(address) # :nodoc: + unless address.kind_of?(String) + raise ArgumentError, 'IPv4 address must be a string' + end + unless address.length == 4 + raise ArgumentError, "IPv4 address expects 4 bytes but #{address.length} bytes" end @address = address end + + ## + # A String representation of this IPv4 address. + + ## + # The raw IPv4 address as a String. + attr_reader :address - def to_s + def to_s # :nodoc: return sprintf("%d.%d.%d.%d", *@address.unpack("CCCC")) end - def inspect - return "#<#{self.class} #{self.to_s}>" + def inspect # :nodoc: + return "#<#{self.class} #{self}>" end + ## + # Turns this IPv4 address into a Resolv::DNS::Name. + def to_name return DNS::Name.create( '%d.%d.%d.%d.in-addr.arpa.' % @address.unpack('CCCC').reverse) end - def ==(other) + def ==(other) # :nodoc: return @address == other.address end - def eql?(other) + def eql?(other) # :nodoc: return self == other end - def hash + def hash # :nodoc: return @address.hash end end + ## + # A Resolv::DNS IPv6 address. + class IPv6 + + ## + # IPv6 address format a:b:c:d:e:f:g:h Regex_8Hex = /\A (?:[0-9A-Fa-f]{1,4}:){7} [0-9A-Fa-f]{1,4} \z/x + ## + # Compressed IPv6 address format a::b + Regex_CompressedHex = /\A ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) \z/x + ## + # IPv4 mapped IPv6 address format a:b:c:d:e:f:w.x.y.z + Regex_6Hex4Dec = /\A ((?:[0-9A-Fa-f]{1,4}:){6,6}) (\d+)\.(\d+)\.(\d+)\.(\d+) \z/x + ## + # Compressed IPv4 mapped IPv6 address format a::b:w.x.y.z + Regex_CompressedHex4Dec = /\A ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: ((?:[0-9A-Fa-f]{1,4}:)*) (\d+)\.(\d+)\.(\d+)\.(\d+) \z/x + ## + # IPv6 link local address format fe80:b:c:d:e:f:g:h%em1 + Regex_8HexLinkLocal = /\A + [Ff][Ee]80 + (?::[0-9A-Fa-f]{1,4}){7} + %[-0-9A-Za-z._~]+ + \z/x + + ## + # Compressed IPv6 link local address format fe80::b%em1 + + Regex_CompressedHexLinkLocal = /\A + [Ff][Ee]80: + (?: + ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: + ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) + | + :((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) + )? + :[0-9A-Fa-f]{1,4}%[-0-9A-Za-z._~]+ + \z/x + + ## + # A composite IPv6 address Regexp. + Regex = / - (?:#{Regex_8Hex.source}) | - (?:#{Regex_CompressedHex.source}) | - (?:#{Regex_6Hex4Dec.source}) | - (?:#{Regex_CompressedHex4Dec.source})/x + (?:#{Regex_8Hex}) | + (?:#{Regex_CompressedHex}) | + (?:#{Regex_6Hex4Dec}) | + (?:#{Regex_CompressedHex4Dec}) | + (?:#{Regex_8HexLinkLocal}) | + (?:#{Regex_CompressedHexLinkLocal}) + /x + + ## + # Creates a new IPv6 address from +arg+ which may be: + # + # IPv6:: returns +arg+. + # String:: +arg+ must match one of the IPv6::Regex* constants def self.create(arg) case arg when IPv6 return arg when String - address = '' + address = ''.b if Regex_8Hex =~ arg arg.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')} elsif Regex_CompressedHex =~ arg prefix = $1 suffix = $2 - a1 = '' - a2 = '' + a1 = ''.b + a2 = ''.b prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')} suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')} omitlen = 16 - a1.length - a2.length @@ -1648,8 +3121,8 @@ class Resolv elsif Regex_CompressedHex4Dec =~ arg prefix, suffix, a, b, c, d = $1, $2, $3.to_i, $4.to_i, $5.to_i, $6.to_i if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d - a1 = '' - a2 = '' + a1 = ''.b + a2 = ''.b prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')} suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')} omitlen = 12 - a1.length - a2.length @@ -1662,49 +3135,379 @@ class Resolv end return IPv6.new(address) else - raise ArgumentError.new("cannot interprete as IPv6 address: #{arg.inspect}") + raise ArgumentError.new("cannot interpret as IPv6 address: #{arg.inspect}") end end - def initialize(address) + def initialize(address) # :nodoc: unless address.kind_of?(String) && address.length == 16 raise ArgumentError.new('IPv6 address must be 16 bytes') end @address = address end + + ## + # The raw IPv6 address as a String. + attr_reader :address - def to_s - address = sprintf("%X:%X:%X:%X:%X:%X:%X:%X", *@address.unpack("nnnnnnnn")) - unless address.sub!(/(^|:)0(:0)+(:|$)/, '::') - address.sub!(/(^|:)0(:|$)/, '::') - end - return address + def to_s # :nodoc: + sprintf("%x:%x:%x:%x:%x:%x:%x:%x", *@address.unpack("nnnnnnnn")).sub(/(^|:)0(:0)+(:|$)/, '::') end - def inspect - return "#<#{self.class} #{self.to_s}>" + def inspect # :nodoc: + return "#<#{self.class} #{self}>" end + ## + # Turns this IPv6 address into a Resolv::DNS::Name. + #-- + # ip6.arpa should be searched too. [RFC3152] + def to_name - # ip6.arpa should be searched too. [RFC3152] return DNS::Name.new( - @address.unpack("H32")[0].split(//).reverse + ['ip6', 'int']) + @address.unpack("H32")[0].split(//).reverse + ['ip6', 'arpa']) end - def ==(other) + def ==(other) # :nodoc: return @address == other.address end - def eql?(other) + def eql?(other) # :nodoc: return self == other end - def hash + def hash # :nodoc: return @address.hash end end + ## + # Resolv::MDNS is a one-shot Multicast DNS (mDNS) resolver. It blindly + # makes queries to the mDNS addresses without understanding anything about + # multicast ports. + # + # Information taken form the following places: + # + # * RFC 6762 + + class MDNS < DNS + + ## + # Default mDNS Port + + Port = 5353 + + ## + # Default IPv4 mDNS address + + AddressV4 = '224.0.0.251' + + ## + # Default IPv6 mDNS address + + AddressV6 = 'ff02::fb' + + ## + # Default mDNS addresses + + Addresses = [ + [AddressV4, Port], + [AddressV6, Port], + ] + + ## + # Creates a new one-shot Multicast DNS (mDNS) resolver. + # + # +config_info+ can be: + # + # nil:: + # Uses the default mDNS addresses + # + # Hash:: + # Must contain :nameserver or :nameserver_port like + # Resolv::DNS#initialize. + + def initialize(config_info=nil) + if config_info then + super({ nameserver_port: Addresses }.merge(config_info)) + else + super(nameserver_port: Addresses) + end + end + + ## + # Iterates over all IP addresses for +name+ retrieved from the mDNS + # resolver, provided name ends with "local". If the name does not end in + # "local" no records will be returned. + # + # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will + # be a Resolv::IPv4 or Resolv::IPv6 + + def each_address(name) + name = Resolv::DNS::Name.create(name) + + return unless name[-1].to_s == 'local' + + super(name) + end + + def make_udp_requester # :nodoc: + nameserver_port = @config.nameserver_port + Requester::MDNSOneShot.new(*nameserver_port) + end + + end + + module LOC # :nodoc: + + ## + # A Resolv::LOC::Size + + class Size + + # Regular expression LOC size must match. + + Regex = /\A0*(\d{1,8}(?:\.\d+)?)m\z/ + + ## + # Creates a new LOC::Size from +arg+ which may be: + # + # LOC::Size:: returns +arg+. + # String:: +arg+ must match the LOC::Size::Regex constant + + def self.create(arg) + case arg + when Size + return arg + when String + unless Regex =~ arg + raise ArgumentError.new("not a properly formed Size string: " + arg) + end + unless (0.0...1e8) === (scalar = $1.to_f) + raise ArgumentError.new("out of range as Size: #{arg}") + end + str = (scalar * 100).to_i.to_s + return new([(str[0].to_i << 4) + (str.bytesize-1)].pack("C")) + else + raise ArgumentError.new("cannot interpret as Size: #{arg.inspect}") + end + end + + # Internal use; use self.create. + def initialize(scalar) + @scalar = scalar + end + + ## + # The raw size + + attr_reader :scalar + + def to_s # :nodoc: + s, = @scalar.unpack("C") + return "#{(s >> 4) * (10.0 ** ((s & 0xf) - 2))}m" + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + end + + def ==(other) # :nodoc: + return @scalar == other.scalar + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + return @scalar.hash + end + + end + + ## + # A Resolv::LOC::Coord + + class Coord + + # Regular expression LOC Coord must match. + + Regex = /\A0*(\d{1,3})\s([0-5]?\d)\s([0-5]?\d(?:\.\d+)?)\s([NESW])\z/ + + # Bias for the equator/prime meridian, in thousandths of a second of arc. + Bias = 1 << 31 + + ## + # Creates a new LOC::Coord from +arg+ which may be: + # + # LOC::Coord:: returns +arg+. + # String:: +arg+ must match the LOC::Coord::Regex constant + + def self.create(arg) + case arg + when Coord + return arg + when String + unless m = Regex.match(arg) + raise ArgumentError.new("not a properly formed Coord string: " + arg) + end + + arc = (m[1].to_i * 3_600_000) + (m[2].to_i * 60_000) + (m[3].to_f * 1_000).to_i + dir = m[4] + lat = dir[/[NS]/] + unless arc <= (lat ? 324_000_000 : 648_000_000) # (lat ? 90 : 180) * 3_600_000 + raise ArgumentError.new("out of range as Coord: #{arg}") + end + + hemi = dir[/[NE]/] ? 1 : -1 + return new([arc * hemi + Bias].pack("N"), lat ? "lat" : "lon") + else + raise ArgumentError.new("cannot interpret as Coord: #{arg.inspect}") + end + end + + # Internal use; use self.create. + def initialize(coordinates,orientation) + unless coordinates.kind_of?(String) and coordinates.bytesize == 4 + raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}") + end + unless orientation == "lon" || orientation == "lat" + raise ArgumentError.new('Coord expects orientation to be a String argument of "lat" or "lon"') + end + @coordinates = coordinates + @orientation = orientation + end + + ## + # The raw coordinates + + attr_reader :coordinates + + ## The orientation of the hemisphere as 'lat' or 'lon' + + attr_reader :orientation + + def to_s # :nodoc: + c, = @coordinates.unpack("N") + val = (c -= Bias).abs + val, fracsecs = val.divmod(1000) + val, secs = val.divmod(60) + degs, mins = val.divmod(60) + hemi = if c.negative? + @orientation == "lon" ? "W" : "S" + else + @orientation == "lat" ? "N" : "E" + end + format("%d %02d %02d.%03d %s", degs, mins, secs, fracsecs, hemi) + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + end + + def ==(other) # :nodoc: + return @coordinates == other.coordinates + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + return @coordinates.hash + end + + end + + ## + # A Resolv::LOC::Alt + + class Alt + + # Regular expression LOC Alt must match. + + Regex = /\A([+-]?0*\d{1,8}(?:\.\d+)?)m\z/ + + # Bias to a base of 100,000m below the WGS 84 reference spheroid. + Bias = 100_000_00 + + ## + # Creates a new LOC::Alt from +arg+ which may be: + # + # LOC::Alt:: returns +arg+. + # String:: +arg+ must match the LOC::Alt::Regex constant + + def self.create(arg) + case arg + when Alt + return arg + when String + unless Regex =~ arg + raise ArgumentError.new("not a properly formed Alt string: " + arg) + end + altitude = ($1.to_f * 100).to_i + Bias + unless (0...0x1_0000_0000) === altitude + raise ArgumentError.new("out of raise as Alt: #{arg}") + end + return new([altitude].pack("N")) + else + raise ArgumentError.new("cannot interpret as Alt: #{arg.inspect}") + end + end + + # Internal use; use self.create. + def initialize(altitude) + @altitude = altitude + end + + ## + # The raw altitude + + attr_reader :altitude + + def to_s # :nodoc: + a, = @altitude.unpack("N") + return "#{(a - Bias).fdiv(100)}m" + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + end + + def ==(other) # :nodoc: + return @altitude == other.altitude + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + return @altitude.hash + end + + end + + end + + ## + # Default resolver to use for Resolv class methods. + DefaultResolver = self.new - AddressRegex = /(?:#{IPv4::Regex.source})|(?:#{IPv6::Regex.source})/ + + ## + # Replaces the resolvers in the default resolver with +new_resolvers+. This + # allows resolvers to be changed for resolv-replace. + + def DefaultResolver.replace_resolvers new_resolvers + @resolvers = new_resolvers + end + + ## + # Address Regexp to use for matching IP addresses. + + AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/ + end |
