1 # frozen_string_literal: true
12 # Resolv is a thread-aware DNS resolver library written in Ruby. Resolv can
13 # handle multiple DNS requests concurrently without blocking the entire Ruby
16 # See also resolv-replace.rb to replace the libc resolver with Resolv.
18 # Resolv can look up various DNS resources using the DNS module directly.
22 # p Resolv.getaddress "www.ruby-lang.org"
23 # p Resolv.getname "210.251.121.214"
25 # Resolv::DNS.open do |dns|
26 # ress = dns.getresources "www.ruby-lang.org", Resolv::DNS::Resource::IN::A
27 # p ress.map(&:address)
28 # ress = dns.getresources "ruby-lang.org", Resolv::DNS::Resource::IN::MX
29 # p ress.map { |r| [r.exchange.to_s, r.preference] }
35 # * NIS is not supported.
36 # * /etc/nsswitch.conf is not supported.
43 # Looks up the first IP address for +name+.
45 def self.getaddress(name)
46 DefaultResolver.getaddress(name)
50 # Looks up all IP address for +name+.
52 def self.getaddresses(name)
53 DefaultResolver.getaddresses(name)
57 # Iterates over all IP addresses for +name+.
59 def self.each_address(name, &block)
60 DefaultResolver.each_address(name, &block)
64 # Looks up the hostname of +address+.
66 def self.getname(address)
67 DefaultResolver.getname(address)
71 # Looks up all hostnames for +address+.
73 def self.getnames(address)
74 DefaultResolver.getnames(address)
78 # Iterates over all hostnames for +address+.
80 def self.each_name(address, &proc)
81 DefaultResolver.each_name(address, &proc)
85 # Creates a new Resolv using +resolvers+.
87 def initialize(resolvers=nil, use_ipv6: nil)
88 @resolvers = resolvers || [Hosts.new, DNS.new(DNS::Config.default_config_hash.merge(use_ipv6: use_ipv6))]
92 # Looks up the first IP address for +name+.
95 each_address(name) {|address| return address}
96 raise ResolvError.new("no address for #{name}")
100 # Looks up all IP address for +name+.
102 def getaddresses(name)
104 each_address(name) {|address| ret << address}
109 # Iterates over all IP addresses for +name+.
111 def each_address(name)
112 if AddressRegex =~ name
118 r.each_address(name) {|address|
127 # Looks up the hostname of +address+.
130 each_name(address) {|name| return name}
131 raise ResolvError.new("no name for #{address}")
135 # Looks up all hostnames for +address+.
137 def getnames(address)
139 each_name(address) {|name| ret << name}
144 # Iterates over all hostnames for +address+.
146 def each_name(address)
149 r.each_name(address) {|name|
158 # Indicates a failure to resolve a name or address.
160 class ResolvError < StandardError; end
163 # Indicates a timeout resolving a name or address.
165 class ResolvTimeout < Timeout::Error; end
168 # Resolv::Hosts is a hostname resolver that uses the system hosts file.
171 if /mswin|mingw|cygwin/ =~ RUBY_PLATFORM and
173 require 'win32/resolv'
174 DefaultFileName = Win32::Resolv.get_hosts_path || IO::NULL
178 DefaultFileName ||= '/etc/hosts'
181 # Creates a new Resolv::Hosts, using +filename+ for its data source.
183 def initialize(filename = DefaultFileName)
185 @mutex = Thread::Mutex.new
189 def lazy_initialize # :nodoc:
194 File.open(@filename, 'rb') {|f|
197 addr, *hostnames = line.split(/\s+/)
199 (@addr2name[addr] ||= []).concat(hostnames)
200 hostnames.each {|hostname| (@name2addr[hostname] ||= []) << addr}
203 @name2addr.each {|name, arr| arr.reverse!}
211 # Gets the IP address of +name+ from the hosts file.
214 each_address(name) {|address| return address}
215 raise ResolvError.new("#{@filename} has no name: #{name}")
219 # Gets all IP addresses for +name+ from the hosts file.
221 def getaddresses(name)
223 each_address(name) {|address| ret << address}
228 # Iterates over all IP addresses for +name+ retrieved from the hosts file.
230 def each_address(name, &proc)
232 @name2addr[name]&.each(&proc)
236 # Gets the hostname of +address+ from the hosts file.
239 each_name(address) {|name| return name}
240 raise ResolvError.new("#{@filename} has no address: #{address}")
244 # Gets all hostnames for +address+ from the hosts file.
246 def getnames(address)
248 each_name(address) {|name| ret << name}
253 # Iterates over all hostnames for +address+ retrieved from the hosts file.
255 def each_name(address, &proc)
257 @addr2name[address]&.each(&proc)
262 # Resolv::DNS is a DNS stub resolver.
264 # Information taken from the following places:
268 # * ftp://ftp.isi.edu/in-notes/iana/assignments/dns-parameters
279 # Default DNS UDP packet size
284 # Creates a new DNS resolver. See Resolv::DNS.new for argument details.
286 # Yields the created DNS resolver to the block, if given, otherwise
291 return dns unless block_given?
300 # Creates a new DNS resolver.
302 # +config_info+ can be:
304 # nil:: Uses /etc/resolv.conf.
305 # String:: Path to a file using /etc/resolv.conf's format.
306 # Hash:: Must contain :nameserver, :search and :ndots keys.
307 # :nameserver_port can be used to specify port number of nameserver address.
308 # :raise_timeout_errors can be used to raise timeout errors
309 # as exceptions instead of treating the same as an NXDOMAIN response.
311 # The value of :nameserver should be an address string or
312 # an array of address strings.
313 # - :nameserver => '8.8.8.8'
314 # - :nameserver => ['8.8.8.8', '8.8.4.4']
316 # The value of :nameserver_port should be an array of
317 # pair of nameserver address and port number.
318 # - :nameserver_port => [['8.8.8.8', 53], ['8.8.4.4', 53]]
322 # Resolv::DNS.new(:nameserver => ['210.251.121.21'],
323 # :search => ['ruby-lang.org'],
326 def initialize(config_info=nil)
327 @mutex = Thread::Mutex.new
328 @config = Config.new(config_info)
332 # Sets the resolver timeouts. This may be a single positive number
333 # or an array of positive numbers representing timeouts in seconds.
334 # If an array is specified, a DNS request will retry and wait for
335 # each successive interval in the array until a successful response
336 # is received. Specifying +nil+ reverts to the default timeouts:
337 # [ 5, second = 5 * 2 / nameserver_count, 2 * second, 4 * second ]
343 def timeouts=(values)
344 @config.timeouts = values
347 def lazy_initialize # :nodoc:
350 @config.lazy_initialize
358 # Closes the DNS resolver.
369 # Gets the IP address of +name+ from the DNS resolver.
371 # +name+ can be a Resolv::DNS::Name or a String. Retrieved address will
372 # be a Resolv::IPv4 or Resolv::IPv6
375 each_address(name) {|address| return address}
376 raise ResolvError.new("DNS result has no information for #{name}")
380 # Gets all IP addresses for +name+ from the DNS resolver.
382 # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will
383 # be a Resolv::IPv4 or Resolv::IPv6
385 def getaddresses(name)
387 each_address(name) {|address| ret << address}
392 # Iterates over all IP addresses for +name+ retrieved from the DNS
395 # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will
396 # be a Resolv::IPv4 or Resolv::IPv6
398 def each_address(name)
399 each_resource(name, Resource::IN::A) {|resource| yield resource.address}
401 each_resource(name, Resource::IN::AAAA) {|resource| yield resource.address}
405 def use_ipv6? # :nodoc:
406 use_ipv6 = @config.use_ipv6?
412 list = Socket.ip_address_list
413 rescue NotImplementedError
416 list.any? {|a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
421 # Gets the hostname for +address+ from the DNS resolver.
423 # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved
424 # name will be a Resolv::DNS::Name.
427 each_name(address) {|name| return name}
428 raise ResolvError.new("DNS result has no information for #{address}")
432 # Gets all hostnames for +address+ from the DNS resolver.
434 # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved
435 # names will be Resolv::DNS::Name instances.
437 def getnames(address)
439 each_name(address) {|name| ret << name}
444 # Iterates over all hostnames for +address+ retrieved from the DNS
447 # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved
448 # names will be Resolv::DNS::Name instances.
450 def each_name(address)
455 ptr = address.to_name
457 ptr = IPv4.create(address).to_name
459 ptr = IPv6.create(address).to_name
461 raise ResolvError.new("cannot interpret as address: #{address}")
463 each_resource(ptr, Resource::IN::PTR) {|resource| yield resource.name}
467 # Look up the +typeclass+ DNS resource of +name+.
469 # +name+ must be a Resolv::DNS::Name or a String.
471 # +typeclass+ should be one of the following:
473 # * Resolv::DNS::Resource::IN::A
474 # * Resolv::DNS::Resource::IN::AAAA
475 # * Resolv::DNS::Resource::IN::ANY
476 # * Resolv::DNS::Resource::IN::CNAME
477 # * Resolv::DNS::Resource::IN::HINFO
478 # * Resolv::DNS::Resource::IN::MINFO
479 # * Resolv::DNS::Resource::IN::MX
480 # * Resolv::DNS::Resource::IN::NS
481 # * Resolv::DNS::Resource::IN::PTR
482 # * Resolv::DNS::Resource::IN::SOA
483 # * Resolv::DNS::Resource::IN::TXT
484 # * Resolv::DNS::Resource::IN::WKS
486 # Returned resource is represented as a Resolv::DNS::Resource instance,
487 # i.e. Resolv::DNS::Resource::IN::A.
489 def getresource(name, typeclass)
490 each_resource(name, typeclass) {|resource| return resource}
491 raise ResolvError.new("DNS result has no information for #{name}")
495 # Looks up all +typeclass+ DNS resources for +name+. See #getresource for
498 def getresources(name, typeclass)
500 each_resource(name, typeclass) {|resource| ret << resource}
505 # Iterates over all +typeclass+ DNS resources for +name+. See
506 # #getresource for argument details.
508 def each_resource(name, typeclass, &proc)
509 fetch_resource(name, typeclass) {|reply, reply_name|
510 extract_resources(reply, reply_name, typeclass, &proc)
514 def fetch_resource(name, typeclass)
517 requester = make_udp_requester
523 @config.resolv(name) {|candidate, tout, nameserver, port|
524 requester ||= make_tcp_requester(nameserver, port)
527 msg.add_question(candidate, typeclass)
528 unless sender = senders[[candidate, nameserver, port]]
529 sender = requester.sender(msg, candidate, nameserver, port)
531 senders[[candidate, nameserver, port]] = sender
533 reply, reply_name = requester.request(sender, tout)
536 if reply.tc == 1 and not Requester::TCP === requester
539 requester = make_tcp_requester(nameserver, port)
541 # This will use TCP for all remaining candidates (assuming the
542 # current candidate does not already respond successfully via
543 # TCP). This makes sense because we already know the full
544 # response will not fit in an untruncated UDP packet.
547 yield(reply, reply_name)
551 raise Config::NXDomain.new(reply_name.to_s)
553 raise Config::OtherResolvError.new(reply_name.to_s)
561 def make_udp_requester # :nodoc:
562 nameserver_port = @config.nameserver_port
563 if nameserver_port.length == 1
564 Requester::ConnectedUDP.new(*nameserver_port[0])
566 Requester::UnconnectedUDP.new(*nameserver_port)
570 def make_tcp_requester(host, port) # :nodoc:
571 return Requester::TCP.new(host, port)
574 def extract_resources(msg, name, typeclass) # :nodoc:
575 if typeclass < Resource::ANY
576 n0 = Name.create(name)
577 msg.each_resource {|n, ttl, data|
578 yield data if n0 == n
582 n0 = Name.create(name)
583 msg.each_resource {|n, ttl, data|
595 msg.each_resource {|n, ttl, data|
605 if defined? SecureRandom
606 def self.random(arg) # :nodoc:
608 SecureRandom.random_number(arg)
609 rescue NotImplementedError
614 def self.random(arg) # :nodoc:
619 RequestID = {} # :nodoc:
620 RequestIDMutex = Thread::Mutex.new # :nodoc:
622 def self.allocate_request_id(host, port) # :nodoc:
624 RequestIDMutex.synchronize {
625 h = (RequestID[[host, port]] ||= {})
627 id = random(0x0000..0xffff)
634 def self.free_request_id(host, port, id) # :nodoc:
635 RequestIDMutex.synchronize {
637 if h = RequestID[key]
646 def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc:
648 port = random(1024..65535)
649 udpsock.bind(bind_host, port)
650 rescue Errno::EADDRINUSE, # POSIX
651 Errno::EACCES, # SunOS: See PRIV_SYS_NFS in privileges(5)
652 Errno::EPERM # FreeBSD: security.mac.portacl.port_high is configurable. See mac_portacl(4).
657 class Requester # :nodoc:
663 def request(sender, tout)
664 start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
665 timelimit = start + tout
668 rescue Errno::EHOSTUNREACH, # multi-homed IPv6 may generate this
673 before_select = Process.clock_gettime(Process::CLOCK_MONOTONIC)
674 timeout = timelimit - before_select
679 select_result = @socks[0].wait_readable(timeout) ? [ @socks ] : nil
681 select_result = IO.select(@socks, nil, nil, timeout)
684 after_select = Process.clock_gettime(Process::CLOCK_MONOTONIC)
685 next if after_select < timelimit
689 reply, from = recv_reply(select_result[0])
690 rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD
691 Errno::ECONNRESET # Windows
692 # No name server running on the server?
693 # Don't wait anymore.
697 msg = Message.decode(reply)
699 next # broken DNS message ignored
701 if sender == sender_for(from, msg)
704 # unexpected DNS message ignored
707 return msg, sender.data
710 def sender_for(addr, msg)
711 @senders[[addr,msg.id]]
720 class Sender # :nodoc:
721 def initialize(msg, data, sock)
728 class UnconnectedUDP < Requester # :nodoc:
729 def initialize(*nameserver_port)
731 @nameserver_port = nameserver_port
733 @mutex = Thread::Mutex.new
742 @nameserver_port.each {|host, port|
745 af = Socket::AF_INET6
747 bind_host = "0.0.0.0"
750 next if @socks_hash[bind_host]
752 sock = UDPSocket.new(af)
753 rescue Errno::EAFNOSUPPORT, Errno::EPROTONOSUPPORT
754 next # The kernel doesn't support the address family.
757 @socks_hash[bind_host] = sock
758 sock.do_not_reverse_lookup = true
759 DNS.bind_random_port(sock, bind_host)
765 def recv_reply(readable_socks)
767 reply, from = readable_socks[0].recvfrom(UDPSize)
768 return reply, [from[3],from[1]]
771 def sender(msg, data, host, port=Port)
772 host = Addrinfo.ip(host).ip_address
774 sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"]
776 service = [host, port]
777 id = DNS.allocate_request_id(host, port)
779 request[0,2] = [id].pack('n')
780 return @senders[[service, id]] =
781 Sender.new(request, data, sock, host, port)
788 @senders.each_key {|service, id|
789 DNS.free_request_id(service[0], service[1], id)
796 class Sender < Requester::Sender # :nodoc:
797 def initialize(msg, data, sock, host, port)
798 super(msg, data, sock)
805 raise "@sock is nil." if @sock.nil?
806 @sock.send(@msg, 0, @host, @port)
811 class ConnectedUDP < Requester # :nodoc:
812 def initialize(host, port=Port)
816 @mutex = Thread::Mutex.new
824 is_ipv6 = @host.index(':')
825 sock = UDPSocket.new(is_ipv6 ? Socket::AF_INET6 : Socket::AF_INET)
827 sock.do_not_reverse_lookup = true
828 DNS.bind_random_port(sock, is_ipv6 ? "::" : "0.0.0.0")
829 sock.connect(@host, @port)
834 def recv_reply(readable_socks)
836 reply = readable_socks[0].recv(UDPSize)
840 def sender(msg, data, host=@host, port=@port)
842 unless host == @host && port == @port
843 raise RequestError.new("host/port don't match: #{host}:#{port}")
845 id = DNS.allocate_request_id(@host, @port)
847 request[0,2] = [id].pack('n')
848 return @senders[[nil,id]] = Sender.new(request, data, @socks[0])
852 @mutex.synchronize do
855 @senders.each_key {|from, id|
856 DNS.free_request_id(@host, @port, id)
863 class Sender < Requester::Sender # :nodoc:
865 raise "@sock is nil." if @sock.nil?
872 class MDNSOneShot < UnconnectedUDP # :nodoc:
873 def sender(msg, data, host, port=Port)
875 id = DNS.allocate_request_id(host, port)
877 request[0,2] = [id].pack('n')
878 sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"]
879 return @senders[id] =
880 UnconnectedUDP::Sender.new(request, data, sock, host, port)
883 def sender_for(addr, msg)
889 class TCP < Requester # :nodoc:
890 def initialize(host, port=Port)
894 sock = TCPSocket.new(@host, @port)
899 def recv_reply(readable_socks)
900 len = readable_socks[0].read(2).unpack('n')[0]
901 reply = @socks[0].read(len)
905 def sender(msg, data, host=@host, port=@port)
906 unless host == @host && port == @port
907 raise RequestError.new("host/port don't match: #{host}:#{port}")
909 id = DNS.allocate_request_id(@host, @port)
911 request[0,2] = [request.length, id].pack('nn')
912 return @senders[[nil,id]] = Sender.new(request, data, @socks[0])
915 class Sender < Requester::Sender # :nodoc:
925 @senders.each_key {|from,id|
926 DNS.free_request_id(@host, @port, id)
932 # Indicates a problem with the DNS request.
934 class RequestError < StandardError
938 class Config # :nodoc:
939 def initialize(config_info=nil)
940 @mutex = Thread::Mutex.new
941 @config_info = config_info
946 def timeouts=(values)
948 values = Array(values)
950 Numeric === t or raise ArgumentError, "#{t.inspect} is not numeric"
951 t > 0.0 or raise ArgumentError, "timeout=#{t} must be positive"
959 def Config.parse_resolv_conf(filename)
963 File.open(filename, 'rb') {|f|
965 line.sub!(/[#;].*/, '')
966 keyword, *args = line.split(/\s+/)
970 nameserver.concat(args)
980 when /\Andots:(\d+)\z/
987 return { :nameserver => nameserver, :search => search, :ndots => ndots }
990 def Config.default_config_hash(filename="/etc/resolv.conf")
991 if File.exist? filename
992 config_hash = Config.parse_resolv_conf(filename)
994 if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM
995 require 'win32/resolv'
996 search, nameserver = Win32::Resolv.get_resolv_info
998 config_hash[:nameserver] = nameserver if nameserver
999 config_hash[:search] = [search].flatten if search
1006 @mutex.synchronize {
1008 @nameserver_port = []
1014 config_hash = Config.default_config_hash
1016 config_hash = Config.parse_resolv_conf(@config_info)
1018 config_hash = @config_info.dup
1019 if String === config_hash[:nameserver]
1020 config_hash[:nameserver] = [config_hash[:nameserver]]
1022 if String === config_hash[:search]
1023 config_hash[:search] = [config_hash[:search]]
1026 raise ArgumentError.new("invalid resolv configuration: #{@config_info.inspect}")
1028 if config_hash.include? :nameserver
1029 @nameserver_port = config_hash[:nameserver].map {|ns| [ns, Port] }
1031 if config_hash.include? :nameserver_port
1032 @nameserver_port = config_hash[:nameserver_port].map {|ns, port| [ns, (port || Port)] }
1034 if config_hash.include? :use_ipv6
1035 @use_ipv6 = config_hash[:use_ipv6]
1037 @search = config_hash[:search] if config_hash.include? :search
1038 @ndots = config_hash[:ndots] if config_hash.include? :ndots
1039 @raise_timeout_errors = config_hash[:raise_timeout_errors]
1041 if @nameserver_port.empty?
1042 @nameserver_port << ['0.0.0.0', Port]
1045 @search = @search.map {|arg| Label.split(arg) }
1047 hostname = Socket.gethostname
1049 @search = [Label.split($')]
1055 if !@nameserver_port.kind_of?(Array) ||
1056 @nameserver_port.any? {|ns_port|
1057 !(Array === ns_port) ||
1059 !(String === ns_port[0]) ||
1060 !(Integer === ns_port[1])
1062 raise ArgumentError.new("invalid nameserver config: #{@nameserver_port.inspect}")
1065 if !@search.kind_of?(Array) ||
1066 !@search.all? {|ls| ls.all? {|l| Label::Str === l } }
1067 raise ArgumentError.new("invalid search config: #{@search.inspect}")
1070 if !@ndots.kind_of?(Integer)
1071 raise ArgumentError.new("invalid ndots config: #{@ndots.inspect}")
1082 if @nameserver_port.length == 1
1083 return @nameserver_port[0]
1097 def generate_candidates(name)
1099 name = Name.create(name)
1103 if @ndots <= name.length - 1
1104 candidates = [Name.new(name.to_a)]
1108 candidates.concat(@search.map {|domain| Name.new(name.to_a + domain)})
1109 fname = Name.create("#{name}.")
1110 if !candidates.include?(fname)
1119 def generate_timeouts
1120 ts = [InitialTimeout]
1121 ts << ts[-1] * 2 / @nameserver_port.length
1128 candidates = generate_candidates(name)
1129 timeouts = @timeouts || generate_timeouts
1130 timeout_error = false
1132 candidates.each {|candidate|
1134 timeouts.each {|tout|
1135 @nameserver_port.each {|nameserver, port|
1137 yield candidate, tout, nameserver, port
1138 rescue ResolvTimeout
1142 timeout_error = true
1143 raise ResolvError.new("DNS resolv timeout: #{name}")
1148 raise if @raise_timeout_errors && timeout_error
1153 # Indicates no such domain was found.
1155 class NXDomain < ResolvError
1159 # Indicates some other unhandled resolver error was encountered.
1161 class OtherResolvError < ResolvError
1165 module OpCode # :nodoc:
1173 module RCode # :nodoc:
1195 # Indicates that the DNS response was unable to be decoded.
1197 class DecodeError < StandardError
1201 # Indicates that the DNS request was unable to be encoded.
1203 class EncodeError < StandardError
1206 module Label # :nodoc:
1209 arg.scan(/[^\.]+/) {labels << Str.new($&)}
1214 def initialize(string)
1216 # case insensivity of DNS labels doesn't apply non-ASCII characters. [RFC 4343]
1217 # This assumes @string is given in ASCII compatible encoding.
1218 @downcase = string.b.downcase
1220 attr_reader :string, :downcase
1227 return "#<#{self.class} #{self}>"
1231 return self.class == other.class && @downcase == other.downcase
1235 return self == other
1239 return @downcase.hash
1245 # A representation of a DNS name.
1250 # Creates a new DNS name from +arg+. +arg+ can be:
1252 # Name:: returns +arg+.
1253 # String:: Creates a new Name.
1255 def self.create(arg)
1260 return Name.new(Label.split(arg), /\.\z/ =~ arg ? true : false)
1262 raise ArgumentError.new("cannot interpret as DNS name: #{arg.inspect}")
1266 def initialize(labels, absolute=true) # :nodoc:
1267 labels = labels.map {|label|
1269 when String then Label::Str.new(label)
1270 when Label::Str then label
1272 raise ArgumentError, "unexpected label: #{label.inspect}"
1276 @absolute = absolute
1279 def inspect # :nodoc:
1280 "#<#{self.class}: #{self}#{@absolute ? '.' : ''}>"
1284 # True if this name is absolute.
1290 def ==(other) # :nodoc:
1291 return false unless Name === other
1292 return false unless @absolute == other.absolute?
1293 return @labels == other.to_a
1296 alias eql? == # :nodoc:
1299 # Returns true if +other+ is a subdomain.
1303 # domain = Resolv::DNS::Name.create("y.z")
1304 # p Resolv::DNS::Name.create("w.x.y.z").subdomain_of?(domain) #=> true
1305 # p Resolv::DNS::Name.create("x.y.z").subdomain_of?(domain) #=> true
1306 # p Resolv::DNS::Name.create("y.z").subdomain_of?(domain) #=> false
1307 # p Resolv::DNS::Name.create("z").subdomain_of?(domain) #=> false
1308 # p Resolv::DNS::Name.create("x.y.z.").subdomain_of?(domain) #=> false
1309 # p Resolv::DNS::Name.create("w.z").subdomain_of?(domain) #=> false
1312 def subdomain_of?(other)
1313 raise ArgumentError, "not a domain name: #{other.inspect}" unless Name === other
1314 return false if @absolute != other.absolute?
1315 other_len = other.length
1316 return false if @labels.length <= other_len
1317 return @labels[-other_len, other_len] == other.to_a
1321 return @labels.hash ^ @absolute.hash
1328 def length # :nodoc:
1329 return @labels.length
1337 # returns the domain name as a string.
1339 # The domain name doesn't have a trailing dot even if the name object is
1344 # p Resolv::DNS::Name.create("x.y.z.").to_s #=> "x.y.z"
1345 # p Resolv::DNS::Name.create("x.y.z").to_s #=> "x.y.z"
1348 return @labels.join('.')
1352 class Message # :nodoc:
1355 def initialize(id = (@@identifier += 1) & 0xffff)
1361 @rd = 0 # recursion desired
1362 @ra = 0 # recursion available
1370 attr_accessor :id, :qr, :opcode, :aa, :tc, :rd, :ra, :rcode
1371 attr_reader :question, :answer, :authority, :additional
1374 return @id == other.id &&
1376 @opcode == other.opcode &&
1381 @rcode == other.rcode &&
1382 @question == other.question &&
1383 @answer == other.answer &&
1384 @authority == other.authority &&
1385 @additional == other.additional
1388 def add_question(name, typeclass)
1389 @question << [Name.create(name), typeclass]
1393 @question.each {|name, typeclass|
1394 yield name, typeclass
1398 def add_answer(name, ttl, data)
1399 @answer << [Name.create(name), ttl, data]
1403 @answer.each {|name, ttl, data|
1404 yield name, ttl, data
1408 def add_authority(name, ttl, data)
1409 @authority << [Name.create(name), ttl, data]
1413 @authority.each {|name, ttl, data|
1414 yield name, ttl, data
1418 def add_additional(name, ttl, data)
1419 @additional << [Name.create(name), ttl, data]
1423 @additional.each {|name, ttl, data|
1424 yield name, ttl, data
1429 each_answer {|name, ttl, data| yield name, ttl, data}
1430 each_authority {|name, ttl, data| yield name, ttl, data}
1431 each_additional {|name, ttl, data| yield name, ttl, data}
1435 return MessageEncoder.new {|msg|
1436 msg.put_pack('nnnnnn',
1439 (@opcode & 15) << 11 |
1452 msg.put_pack('nn', typeclass::TypeValue, typeclass::ClassValue)
1454 [@answer, @authority, @additional].each {|rr|
1458 msg.put_pack('nnN', data.class::TypeValue, data.class::ClassValue, ttl)
1459 msg.put_length16 {data.encode_rdata(msg)}
1465 class MessageEncoder # :nodoc:
1480 def put_pack(template, *d)
1481 @data << d.pack(template)
1485 length_index = @data.length
1487 data_start = @data.length
1489 data_end = @data.length
1490 @data[length_index, 2] = [data_end - data_start].pack("n")
1494 self.put_pack("C", d.length)
1498 def put_string_list(ds)
1504 def put_name(d, compress: true)
1505 put_labels(d.to_a, compress: compress)
1508 def put_labels(d, compress: true)
1511 if compress && idx = @names[domain]
1512 self.put_pack("n", 0xc000 | idx)
1515 if @data.length < 0x4000
1516 @names[domain] = @data.length
1518 self.put_label(d[i])
1525 self.put_string(d.to_s)
1529 def Message.decode(m)
1531 MessageDecoder.new(m) {|msg|
1532 id, flag, qdcount, ancount, nscount, arcount =
1533 msg.get_unpack('nnnnnn')
1535 o.tc = (flag >> 9) & 1
1537 return o unless o.tc.zero?
1539 o.qr = (flag >> 15) & 1
1540 o.opcode = (flag >> 11) & 15
1541 o.aa = (flag >> 10) & 1
1542 o.rd = (flag >> 8) & 1
1543 o.ra = (flag >> 7) & 1
1545 name, typeclass = msg.get_question
1546 o.add_question(name, typeclass)
1549 name, ttl, data = msg.get_rr
1550 o.add_answer(name, ttl, data)
1553 name, ttl, data = msg.get_rr
1554 o.add_authority(name, ttl, data)
1557 name, ttl, data = msg.get_rr
1558 o.add_additional(name, ttl, data)
1564 class MessageDecoder # :nodoc:
1565 def initialize(data)
1568 @limit = data.bytesize
1573 "\#<#{self.class}: #{@data.byteslice(0, @index).inspect} #{@data.byteslice(@index..-1).inspect}>"
1577 len, = self.get_unpack('n')
1579 @limit = @index + len
1582 raise DecodeError.new("junk exists")
1583 elsif @limit < @index
1584 raise DecodeError.new("limit exceeded")
1590 def get_bytes(len = @limit - @index)
1591 raise DecodeError.new("limit exceeded") if @limit < @index + len
1592 d = @data.byteslice(@index, len)
1597 def get_unpack(template)
1599 template.each_byte {|byte|
1609 raise StandardError.new("unsupported template: '#{byte.chr}' in '#{template}'")
1612 raise DecodeError.new("limit exceeded") if @limit < @index + len
1613 arr = @data.unpack("@#{@index}#{template}")
1619 raise DecodeError.new("limit exceeded") if @limit <= @index
1620 len = @data.getbyte(@index)
1621 raise DecodeError.new("limit exceeded") if @limit < @index + 1 + len
1622 d = @data.byteslice(@index + 1, len)
1629 while @index < @limit
1630 strings << self.get_string
1637 while @index < @limit
1644 return Name.new(self.get_labels)
1652 raise DecodeError.new("limit exceeded") if @limit <= @index
1653 case @data.getbyte(@index)
1661 idx = self.get_unpack('n')[0] & 0x3fff
1662 if prev_index <= idx
1663 raise DecodeError.new("non-backward name pointer")
1677 return Label::Str.new(self.get_string)
1681 name = self.get_name
1682 type, klass = self.get_unpack("nn")
1683 return name, Resource.get_class(type, klass)
1687 name = self.get_name
1688 type, klass, ttl = self.get_unpack('nnN')
1689 typeclass = Resource.get_class(type, klass)
1690 res = self.get_length16 do
1692 typeclass.decode_rdata self
1694 raise DecodeError, e.message, e.backtrace
1697 res.instance_variable_set :@ttl, ttl
1698 return name, ttl, res
1704 # SvcParams for service binding RRs. [RFC9460]
1710 # Create a list of SvcParams with the given initial content.
1712 # +params+ has to be an enumerable of +SvcParam+s.
1713 # If its content has +SvcParam+s with the duplicate key,
1714 # the one appears last takes precedence.
1716 def initialize(params = [])
1719 params.each do |param|
1725 # Get SvcParam for the given +key+ in this list.
1728 @params[canonical_key(key)]
1732 # Get the number of SvcParams in this list.
1739 # Get whether this list is empty.
1746 # Add the SvcParam +param+ to this list, overwriting the existing one with the same key.
1749 @params[param.class.key_number] = param
1753 # Remove the +SvcParam+ with the given +key+ and return it.
1756 @params.delete(canonical_key(key))
1760 # Enumerate the +SvcParam+s in this list.
1763 return enum_for(:each) unless block
1764 @params.each_value(&block)
1767 def encode(msg) # :nodoc:
1768 @params.keys.sort.each do |key|
1769 msg.put_pack('n', key)
1771 @params.fetch(key).encode(msg)
1776 def self.decode(msg) # :nodoc:
1777 params = msg.get_list do
1778 key, = msg.get_unpack('n')
1780 SvcParam::ClassHash[key].decode(msg)
1784 return self.new(params)
1789 def canonical_key(key) # :nodoc:
1796 SvcParam::ClassHash[key].key_number
1798 raise TypeError, 'key must be either String or Symbol'
1805 # Base class for SvcParam. [RFC9460]
1810 # Get the presentation name of the SvcParamKey.
1817 # Get the registered number of the SvcParamKey.
1820 const_get(:KeyNumber)
1823 ClassHash = Hash.new do |h, key| # :nodoc:
1827 when /\Akey(?<key>\d+)\z/
1828 Generic.create(key.to_int)
1830 raise KeyError, "unknown key #{key}"
1832 raise TypeError, 'key must be either String or Symbol'
1837 # Generic SvcParam abstract class.
1839 class Generic < SvcParam
1842 # SvcParamValue in wire-format byte string.
1847 # Create generic SvcParam
1849 def initialize(value)
1853 def encode(msg) # :nodoc:
1854 msg.put_bytes(@value)
1857 def self.decode(msg) # :nodoc:
1858 return self.new(msg.get_bytes)
1861 def self.create(key_number)
1862 c = Class.new(Generic)
1863 key_name = :"key#{key_number}"
1864 c.const_set(:KeyName, key_name)
1865 c.const_set(:KeyNumber, key_number)
1866 self.const_set(:"Key#{key_number}", c)
1867 ClassHash[key_name] = ClassHash[key_number] = c
1873 # "mandatory" SvcParam -- Mandatory keys in service binding RR
1875 class Mandatory < SvcParam
1876 KeyName = :mandatory
1878 ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
1886 # Initialize "mandatory" ScvParam.
1888 def initialize(keys)
1889 @keys = keys.map(&:to_int)
1892 def encode(msg) # :nodoc:
1893 @keys.sort.each do |key|
1894 msg.put_pack('n', key)
1898 def self.decode(msg) # :nodoc:
1899 keys = msg.get_list { msg.get_unpack('n')[0] }
1900 return self.new(keys)
1905 # "alpn" SvcParam -- Additional supported protocols
1907 class ALPN < SvcParam
1910 ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
1913 # Supported protocol IDs.
1915 attr_reader :protocol_ids
1918 # Initialize "alpn" ScvParam.
1920 def initialize(protocol_ids)
1921 @protocol_ids = protocol_ids.map(&:to_str)
1924 def encode(msg) # :nodoc:
1925 msg.put_string_list(@protocol_ids)
1928 def self.decode(msg) # :nodoc:
1929 return self.new(msg.get_string_list)
1934 # "no-default-alpn" SvcParam -- No support for default protocol
1936 class NoDefaultALPN < SvcParam
1937 KeyName = :'no-default-alpn'
1939 ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
1941 def encode(msg) # :nodoc:
1945 def self.decode(msg) # :nodoc:
1951 # "port" SvcParam -- Port for alternative endpoint
1953 class Port < SvcParam
1956 ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
1964 # Initialize "port" ScvParam.
1966 def initialize(port)
1970 def encode(msg) # :nodoc:
1971 msg.put_pack('n', @port)
1974 def self.decode(msg) # :nodoc:
1975 port, = msg.get_unpack('n')
1976 return self.new(port)
1981 # "ipv4hint" SvcParam -- IPv4 address hints
1983 class IPv4Hint < SvcParam
1986 ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
1989 # Set of IPv4 addresses.
1991 attr_reader :addresses
1994 # Initialize "ipv4hint" ScvParam.
1996 def initialize(addresses)
1997 @addresses = addresses.map {|address| IPv4.create(address) }
2000 def encode(msg) # :nodoc:
2001 @addresses.each do |address|
2002 msg.put_bytes(address.address)
2006 def self.decode(msg) # :nodoc:
2007 addresses = msg.get_list { IPv4.new(msg.get_bytes(4)) }
2008 return self.new(addresses)
2013 # "ipv6hint" SvcParam -- IPv6 address hints
2015 class IPv6Hint < SvcParam
2018 ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
2021 # Set of IPv6 addresses.
2023 attr_reader :addresses
2026 # Initialize "ipv6hint" ScvParam.
2028 def initialize(addresses)
2029 @addresses = addresses.map {|address| IPv6.create(address) }
2032 def encode(msg) # :nodoc:
2033 @addresses.each do |address|
2034 msg.put_bytes(address.address)
2038 def self.decode(msg) # :nodoc:
2039 addresses = msg.get_list { IPv6.new(msg.get_bytes(16)) }
2040 return self.new(addresses)
2045 # "dohpath" SvcParam -- DNS over HTTPS path template [RFC9461]
2047 class DoHPath < SvcParam
2050 ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
2053 # URI template for DoH queries.
2055 attr_reader :template
2058 # Initialize "dohpath" ScvParam.
2060 def initialize(template)
2061 @template = template.encode('utf-8')
2064 def encode(msg) # :nodoc:
2065 msg.put_bytes(@template)
2068 def self.decode(msg) # :nodoc:
2069 template = msg.get_bytes.force_encoding('utf-8')
2070 return self.new(template)
2076 # A DNS query abstract class.
2079 def encode_rdata(msg) # :nodoc:
2080 raise EncodeError.new("#{self.class} is query.")
2083 def self.decode_rdata(msg) # :nodoc:
2084 raise DecodeError.new("#{self.class} is query.")
2089 # A DNS resource abstract class.
2091 class Resource < Query
2094 # Remaining Time To Live for this Resource.
2098 ClassHash = {} # :nodoc:
2100 def encode_rdata(msg) # :nodoc:
2101 raise NotImplementedError.new
2104 def self.decode_rdata(msg) # :nodoc:
2105 raise NotImplementedError.new
2108 def ==(other) # :nodoc:
2109 return false unless self.class == other.class
2110 s_ivars = self.instance_variables
2112 s_ivars.delete :@ttl
2113 o_ivars = other.instance_variables
2115 o_ivars.delete :@ttl
2116 return s_ivars == o_ivars &&
2117 s_ivars.collect {|name| self.instance_variable_get name} ==
2118 o_ivars.collect {|name| other.instance_variable_get name}
2121 def eql?(other) # :nodoc:
2122 return self == other
2127 vars = self.instance_variables
2130 h ^= self.instance_variable_get(name).hash
2135 def self.get_class(type_value, class_value) # :nodoc:
2136 return ClassHash[[type_value, class_value]] ||
2137 Generic.create(type_value, class_value)
2141 # A generic resource abstract class.
2143 class Generic < Resource
2146 # Creates a new generic resource.
2148 def initialize(data)
2153 # Data for this generic resource.
2157 def encode_rdata(msg) # :nodoc:
2161 def self.decode_rdata(msg) # :nodoc:
2162 return self.new(msg.get_bytes)
2165 def self.create(type_value, class_value) # :nodoc:
2166 c = Class.new(Generic)
2167 c.const_set(:TypeValue, type_value)
2168 c.const_set(:ClassValue, class_value)
2169 Generic.const_set("Type#{type_value}_Class#{class_value}", c)
2170 ClassHash[[type_value, class_value]] = c
2176 # Domain Name resource abstract class.
2178 class DomainName < Resource
2181 # Creates a new DomainName from +name+.
2183 def initialize(name)
2188 # The name of this DomainName.
2192 def encode_rdata(msg) # :nodoc:
2196 def self.decode_rdata(msg) # :nodoc:
2197 return self.new(msg.get_name)
2201 # Standard (class generic) RRs
2203 ClassValue = nil # :nodoc:
2206 # An authoritative name server.
2208 class NS < DomainName
2209 TypeValue = 2 # :nodoc:
2213 # The canonical name for an alias.
2215 class CNAME < DomainName
2216 TypeValue = 5 # :nodoc:
2220 # Start Of Authority resource.
2222 class SOA < Resource
2224 TypeValue = 6 # :nodoc:
2227 # Creates a new SOA record. See the attr documentation for the
2228 # details of each argument.
2230 def initialize(mname, rname, serial, refresh, retry_, expire, minimum)
2241 # Name of the host where the master zone file for this zone resides.
2246 # The person responsible for this domain name.
2251 # The version number of the zone file.
2256 # How often, in seconds, a secondary name server is to check for
2257 # updates from the primary name server.
2259 attr_reader :refresh
2262 # How often, in seconds, a secondary name server is to retry after a
2263 # failure to check for a refresh.
2268 # Time in seconds that a secondary name server is to use the data
2269 # before refreshing from the primary name server.
2274 # The minimum number of seconds to be used for TTL values in RRs.
2276 attr_reader :minimum
2278 def encode_rdata(msg) # :nodoc:
2279 msg.put_name(@mname)
2280 msg.put_name(@rname)
2281 msg.put_pack('NNNNN', @serial, @refresh, @retry, @expire, @minimum)
2284 def self.decode_rdata(msg) # :nodoc:
2285 mname = msg.get_name
2286 rname = msg.get_name
2287 serial, refresh, retry_, expire, minimum = msg.get_unpack('NNNNN')
2289 mname, rname, serial, refresh, retry_, expire, minimum)
2294 # A Pointer to another DNS name.
2296 class PTR < DomainName
2297 TypeValue = 12 # :nodoc:
2301 # Host Information resource.
2303 class HINFO < Resource
2305 TypeValue = 13 # :nodoc:
2308 # Creates a new HINFO running +os+ on +cpu+.
2310 def initialize(cpu, os)
2316 # CPU architecture for this resource.
2321 # Operating system for this resource.
2325 def encode_rdata(msg) # :nodoc:
2326 msg.put_string(@cpu)
2330 def self.decode_rdata(msg) # :nodoc:
2331 cpu = msg.get_string
2333 return self.new(cpu, os)
2338 # Mailing list or mailbox information.
2340 class MINFO < Resource
2342 TypeValue = 14 # :nodoc:
2344 def initialize(rmailbx, emailbx)
2350 # Domain name responsible for this mail list or mailbox.
2352 attr_reader :rmailbx
2355 # Mailbox to use for error messages related to the mail list or mailbox.
2357 attr_reader :emailbx
2359 def encode_rdata(msg) # :nodoc:
2360 msg.put_name(@rmailbx)
2361 msg.put_name(@emailbx)
2364 def self.decode_rdata(msg) # :nodoc:
2365 rmailbx = msg.get_string
2366 emailbx = msg.get_string
2367 return self.new(rmailbx, emailbx)
2372 # Mail Exchanger resource.
2376 TypeValue= 15 # :nodoc:
2379 # Creates a new MX record with +preference+, accepting mail at
2382 def initialize(preference, exchange)
2383 @preference = preference
2384 @exchange = exchange
2388 # The preference for this MX.
2390 attr_reader :preference
2393 # The host of this MX.
2395 attr_reader :exchange
2397 def encode_rdata(msg) # :nodoc:
2398 msg.put_pack('n', @preference)
2399 msg.put_name(@exchange)
2402 def self.decode_rdata(msg) # :nodoc:
2403 preference, = msg.get_unpack('n')
2404 exchange = msg.get_name
2405 return self.new(preference, exchange)
2410 # Unstructured text resource.
2412 class TXT < Resource
2414 TypeValue = 16 # :nodoc:
2416 def initialize(first_string, *rest_strings)
2417 @strings = [first_string, *rest_strings]
2421 # Returns an Array of Strings for this TXT record.
2423 attr_reader :strings
2426 # Returns the concatenated string from +strings+.
2432 def encode_rdata(msg) # :nodoc:
2433 msg.put_string_list(@strings)
2436 def self.decode_rdata(msg) # :nodoc:
2437 strings = msg.get_string_list
2438 return self.new(*strings)
2445 class LOC < Resource
2447 TypeValue = 29 # :nodoc:
2449 def initialize(version, ssize, hprecision, vprecision, latitude, longitude, altitude)
2451 @ssize = Resolv::LOC::Size.create(ssize)
2452 @hprecision = Resolv::LOC::Size.create(hprecision)
2453 @vprecision = Resolv::LOC::Size.create(vprecision)
2454 @latitude = Resolv::LOC::Coord.create(latitude)
2455 @longitude = Resolv::LOC::Coord.create(longitude)
2456 @altitude = Resolv::LOC::Alt.create(altitude)
2460 # Returns the version value for this LOC record which should always be 00
2462 attr_reader :version
2465 # The spherical size of this LOC
2466 # in meters using scientific notation as 2 integers of XeY
2471 # The horizontal precision using ssize type values
2472 # in meters using scientific notation as 2 integers of XeY
2473 # for precision use value/2 e.g. 2m = +/-1m
2475 attr_reader :hprecision
2478 # The vertical precision using ssize type values
2479 # in meters using scientific notation as 2 integers of XeY
2480 # for precision use value/2 e.g. 2m = +/-1m
2482 attr_reader :vprecision
2485 # The latitude for this LOC where 2**31 is the equator
2486 # in thousandths of an arc second as an unsigned 32bit integer
2488 attr_reader :latitude
2491 # The longitude for this LOC where 2**31 is the prime meridian
2492 # in thousandths of an arc second as an unsigned 32bit integer
2494 attr_reader :longitude
2497 # The altitude of the LOC above a reference sphere whose surface sits 100km below the WGS84 spheroid
2498 # in centimeters as an unsigned 32bit integer
2500 attr_reader :altitude
2503 def encode_rdata(msg) # :nodoc:
2504 msg.put_bytes(@version)
2505 msg.put_bytes(@ssize.scalar)
2506 msg.put_bytes(@hprecision.scalar)
2507 msg.put_bytes(@vprecision.scalar)
2508 msg.put_bytes(@latitude.coordinates)
2509 msg.put_bytes(@longitude.coordinates)
2510 msg.put_bytes(@altitude.altitude)
2513 def self.decode_rdata(msg) # :nodoc:
2514 version = msg.get_bytes(1)
2515 ssize = msg.get_bytes(1)
2516 hprecision = msg.get_bytes(1)
2517 vprecision = msg.get_bytes(1)
2518 latitude = msg.get_bytes(4)
2519 longitude = msg.get_bytes(4)
2520 altitude = msg.get_bytes(4)
2523 Resolv::LOC::Size.new(ssize),
2524 Resolv::LOC::Size.new(hprecision),
2525 Resolv::LOC::Size.new(vprecision),
2526 Resolv::LOC::Coord.new(latitude,"lat"),
2527 Resolv::LOC::Coord.new(longitude,"lon"),
2528 Resolv::LOC::Alt.new(altitude)
2534 # A Query type requesting any RR.
2537 TypeValue = 255 # :nodoc:
2541 # CAA resource record defined in RFC 8659
2543 # These records identify certificate authority allowed to issue
2544 # certificates for the given domain.
2546 class CAA < Resource
2550 # Creates a new CAA for +flags+, +tag+ and +value+.
2552 def initialize(flags, tag, value)
2553 unless (0..255) === flags
2554 raise ArgumentError.new('flags must be an Integer between 0 and 255')
2556 unless (1..15) === tag.bytesize
2557 raise ArgumentError.new('length of tag must be between 1 and 15')
2566 # Flags for this proprty:
2567 # - Bit 0 : 0 = not critical, 1 = critical
2572 # Property tag ("issue", "issuewild", "iodef"...).
2582 # Whether the critical flag is set on this property.
2588 def encode_rdata(msg) # :nodoc:
2589 msg.put_pack('C', @flags)
2590 msg.put_string(@tag)
2591 msg.put_bytes(@value)
2594 def self.decode_rdata(msg) # :nodoc:
2595 flags, = msg.get_unpack('C')
2596 tag = msg.get_string
2597 value = msg.get_bytes
2598 self.new flags, tag, value
2602 ClassInsensitiveTypes = [ # :nodoc:
2603 NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY, CAA
2607 # module IN contains ARPA Internet specific RRs.
2611 ClassValue = 1 # :nodoc:
2613 ClassInsensitiveTypes.each {|s|
2615 c.const_set(:TypeValue, s::TypeValue)
2616 c.const_set(:ClassValue, ClassValue)
2617 ClassHash[[s::TypeValue, ClassValue]] = c
2618 self.const_set(s.name.sub(/.*::/, ''), c)
2622 # IPv4 Address resource
2626 ClassValue = IN::ClassValue
2627 ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
2630 # Creates a new A for +address+.
2632 def initialize(address)
2633 @address = IPv4.create(address)
2637 # The Resolv::IPv4 address for this A.
2639 attr_reader :address
2641 def encode_rdata(msg) # :nodoc:
2642 msg.put_bytes(@address.address)
2645 def self.decode_rdata(msg) # :nodoc:
2646 return self.new(IPv4.new(msg.get_bytes(4)))
2651 # Well Known Service resource.
2653 class WKS < Resource
2655 ClassValue = IN::ClassValue
2656 ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
2658 def initialize(address, protocol, bitmap)
2659 @address = IPv4.create(address)
2660 @protocol = protocol
2665 # The host these services run on.
2667 attr_reader :address
2670 # IP protocol number for these services.
2672 attr_reader :protocol
2675 # A bit map of enabled services on this host.
2677 # If protocol is 6 (TCP) then the 26th bit corresponds to the SMTP
2678 # service (port 25). If this bit is set, then an SMTP server should
2679 # be listening on TCP port 25; if zero, SMTP service is not
2684 def encode_rdata(msg) # :nodoc:
2685 msg.put_bytes(@address.address)
2686 msg.put_pack("n", @protocol)
2687 msg.put_bytes(@bitmap)
2690 def self.decode_rdata(msg) # :nodoc:
2691 address = IPv4.new(msg.get_bytes(4))
2692 protocol, = msg.get_unpack("n")
2693 bitmap = msg.get_bytes
2694 return self.new(address, protocol, bitmap)
2699 # An IPv6 address record.
2701 class AAAA < Resource
2703 ClassValue = IN::ClassValue
2704 ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
2707 # Creates a new AAAA for +address+.
2709 def initialize(address)
2710 @address = IPv6.create(address)
2714 # The Resolv::IPv6 address for this AAAA.
2716 attr_reader :address
2718 def encode_rdata(msg) # :nodoc:
2719 msg.put_bytes(@address.address)
2722 def self.decode_rdata(msg) # :nodoc:
2723 return self.new(IPv6.new(msg.get_bytes(16)))
2728 # SRV resource record defined in RFC 2782
2730 # These records identify the hostname and port that a service is
2733 class SRV < Resource
2735 ClassValue = IN::ClassValue
2736 ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
2738 # Create a SRV resource record.
2740 # See the documentation for #priority, #weight, #port and #target
2741 # for +priority+, +weight+, +port and +target+ respectively.
2743 def initialize(priority, weight, port, target)
2744 @priority = priority.to_int
2745 @weight = weight.to_int
2747 @target = Name.create(target)
2750 # The priority of this target host.
2752 # A client MUST attempt to contact the target host with the
2753 # lowest-numbered priority it can reach; target hosts with the same
2754 # priority SHOULD be tried in an order defined by the weight field.
2755 # The range is 0-65535. Note that it is not widely implemented and
2756 # should be set to zero.
2758 attr_reader :priority
2760 # A server selection mechanism.
2762 # The weight field specifies a relative weight for entries with the
2763 # same priority. Larger weights SHOULD be given a proportionately
2764 # higher probability of being selected. The range of this number is
2765 # 0-65535. Domain administrators SHOULD use Weight 0 when there
2766 # isn't any server selection to do, to make the RR easier to read
2767 # for humans (less noisy). Note that it is not widely implemented
2768 # and should be set to zero.
2772 # The port on this target host of this service.
2774 # The range is 0-65535.
2778 # The domain name of the target host.
2780 # A target of "." means that the service is decidedly not available
2785 def encode_rdata(msg) # :nodoc:
2786 msg.put_pack("n", @priority)
2787 msg.put_pack("n", @weight)
2788 msg.put_pack("n", @port)
2789 msg.put_name(@target, compress: false)
2792 def self.decode_rdata(msg) # :nodoc:
2793 priority, = msg.get_unpack("n")
2794 weight, = msg.get_unpack("n")
2795 port, = msg.get_unpack("n")
2796 target = msg.get_name
2797 return self.new(priority, weight, port, target)
2802 # Common implementation for SVCB-compatible resource records.
2804 class ServiceBinding
2807 # Create a service binding resource record.
2809 def initialize(priority, target, params = [])
2810 @priority = priority.to_int
2811 @target = Name.create(target)
2812 @params = SvcParams.new(params)
2816 # The priority of this target host.
2818 # The range is 0-65535.
2819 # If set to 0, this RR is in AliasMode. Otherwise, it is in ServiceMode.
2821 attr_reader :priority
2824 # The domain name of the target host.
2829 # The service parameters for the target host.
2834 # Whether this RR is in AliasMode.
2841 # Whether this RR is in ServiceMode.
2847 def encode_rdata(msg) # :nodoc:
2848 msg.put_pack("n", @priority)
2849 msg.put_name(@target, compress: false)
2853 def self.decode_rdata(msg) # :nodoc:
2854 priority, = msg.get_unpack("n")
2855 target = msg.get_name
2856 params = SvcParams.decode(msg)
2857 return self.new(priority, target, params)
2862 # SVCB resource record [RFC9460]
2864 class SVCB < ServiceBinding
2866 ClassValue = IN::ClassValue
2867 ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
2871 # HTTPS resource record [RFC9460]
2873 class HTTPS < ServiceBinding
2875 ClassValue = IN::ClassValue
2876 ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
2883 # A Resolv::DNS IPv4 address.
2888 # Regular expression IPv4 addresses must match.
2892 |2(?:[0-4][0-9]?|5[0-5]?|[6-9])?
2894 Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/
2896 def self.create(arg)
2901 if (0..255) === (a = $1.to_i) &&
2902 (0..255) === (b = $2.to_i) &&
2903 (0..255) === (c = $3.to_i) &&
2904 (0..255) === (d = $4.to_i)
2905 return self.new([a, b, c, d].pack("CCCC"))
2907 raise ArgumentError.new("IPv4 address with invalid value: " + arg)
2910 raise ArgumentError.new("cannot interpret as IPv4 address: #{arg.inspect}")
2914 def initialize(address) # :nodoc:
2915 unless address.kind_of?(String)
2916 raise ArgumentError, 'IPv4 address must be a string'
2918 unless address.length == 4
2919 raise ArgumentError, "IPv4 address expects 4 bytes but #{address.length} bytes"
2925 # A String representation of this IPv4 address.
2928 # The raw IPv4 address as a String.
2930 attr_reader :address
2933 return sprintf("%d.%d.%d.%d", *@address.unpack("CCCC"))
2936 def inspect # :nodoc:
2937 return "#<#{self.class} #{self}>"
2941 # Turns this IPv4 address into a Resolv::DNS::Name.
2944 return DNS::Name.create(
2945 '%d.%d.%d.%d.in-addr.arpa.' % @address.unpack('CCCC').reverse)
2948 def ==(other) # :nodoc:
2949 return @address == other.address
2952 def eql?(other) # :nodoc:
2953 return self == other
2957 return @address.hash
2962 # A Resolv::DNS IPv6 address.
2967 # IPv6 address format a:b:c:d:e:f:g:h
2969 (?:[0-9A-Fa-f]{1,4}:){7}
2974 # Compressed IPv6 address format a::b
2976 Regex_CompressedHex = /\A
2977 ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
2978 ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)
2982 # IPv4 mapped IPv6 address format a:b:c:d:e:f:w.x.y.z
2984 Regex_6Hex4Dec = /\A
2985 ((?:[0-9A-Fa-f]{1,4}:){6,6})
2986 (\d+)\.(\d+)\.(\d+)\.(\d+)
2990 # Compressed IPv4 mapped IPv6 address format a::b:w.x.y.z
2992 Regex_CompressedHex4Dec = /\A
2993 ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
2994 ((?:[0-9A-Fa-f]{1,4}:)*)
2995 (\d+)\.(\d+)\.(\d+)\.(\d+)
2999 # IPv6 link local address format fe80:b:c:d:e:f:g:h%em1
3000 Regex_8HexLinkLocal = /\A
3002 (?::[0-9A-Fa-f]{1,4}){7}
3007 # Compressed IPv6 link local address format fe80::b%em1
3009 Regex_CompressedHexLinkLocal = /\A
3012 ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
3013 ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)
3015 :((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)
3017 :[0-9A-Fa-f]{1,4}%[-0-9A-Za-z._~]+
3021 # A composite IPv6 address Regexp.
3025 (?:#{Regex_CompressedHex}) |
3026 (?:#{Regex_6Hex4Dec}) |
3027 (?:#{Regex_CompressedHex4Dec}) |
3028 (?:#{Regex_8HexLinkLocal}) |
3029 (?:#{Regex_CompressedHexLinkLocal})
3033 # Creates a new IPv6 address from +arg+ which may be:
3035 # IPv6:: returns +arg+.
3036 # String:: +arg+ must match one of the IPv6::Regex* constants
3038 def self.create(arg)
3044 if Regex_8Hex =~ arg
3045 arg.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')}
3046 elsif Regex_CompressedHex =~ arg
3051 prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')}
3052 suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')}
3053 omitlen = 16 - a1.length - a2.length
3054 address << a1 << "\0" * omitlen << a2
3055 elsif Regex_6Hex4Dec =~ arg
3056 prefix, a, b, c, d = $1, $2.to_i, $3.to_i, $4.to_i, $5.to_i
3057 if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d
3058 prefix.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')}
3059 address << [a, b, c, d].pack('CCCC')
3061 raise ArgumentError.new("not numeric IPv6 address: " + arg)
3063 elsif Regex_CompressedHex4Dec =~ arg
3064 prefix, suffix, a, b, c, d = $1, $2, $3.to_i, $4.to_i, $5.to_i, $6.to_i
3065 if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d
3068 prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')}
3069 suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')}
3070 omitlen = 12 - a1.length - a2.length
3071 address << a1 << "\0" * omitlen << a2 << [a, b, c, d].pack('CCCC')
3073 raise ArgumentError.new("not numeric IPv6 address: " + arg)
3076 raise ArgumentError.new("not numeric IPv6 address: " + arg)
3078 return IPv6.new(address)
3080 raise ArgumentError.new("cannot interpret as IPv6 address: #{arg.inspect}")
3084 def initialize(address) # :nodoc:
3085 unless address.kind_of?(String) && address.length == 16
3086 raise ArgumentError.new('IPv6 address must be 16 bytes')
3092 # The raw IPv6 address as a String.
3094 attr_reader :address
3097 sprintf("%x:%x:%x:%x:%x:%x:%x:%x", *@address.unpack("nnnnnnnn")).sub(/(^|:)0(:0)+(:|$)/, '::')
3100 def inspect # :nodoc:
3101 return "#<#{self.class} #{self}>"
3105 # Turns this IPv6 address into a Resolv::DNS::Name.
3107 # ip6.arpa should be searched too. [RFC3152]
3110 return DNS::Name.new(
3111 @address.unpack("H32")[0].split(//).reverse + ['ip6', 'arpa'])
3114 def ==(other) # :nodoc:
3115 return @address == other.address
3118 def eql?(other) # :nodoc:
3119 return self == other
3123 return @address.hash
3128 # Resolv::MDNS is a one-shot Multicast DNS (mDNS) resolver. It blindly
3129 # makes queries to the mDNS addresses without understanding anything about
3132 # Information taken form the following places:
3144 # Default IPv4 mDNS address
3146 AddressV4 = '224.0.0.251'
3149 # Default IPv6 mDNS address
3151 AddressV6 = 'ff02::fb'
3154 # Default mDNS addresses
3162 # Creates a new one-shot Multicast DNS (mDNS) resolver.
3164 # +config_info+ can be:
3167 # Uses the default mDNS addresses
3170 # Must contain :nameserver or :nameserver_port like
3171 # Resolv::DNS#initialize.
3173 def initialize(config_info=nil)
3175 super({ nameserver_port: Addresses }.merge(config_info))
3177 super(nameserver_port: Addresses)
3182 # Iterates over all IP addresses for +name+ retrieved from the mDNS
3183 # resolver, provided name ends with "local". If the name does not end in
3184 # "local" no records will be returned.
3186 # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will
3187 # be a Resolv::IPv4 or Resolv::IPv6
3189 def each_address(name)
3190 name = Resolv::DNS::Name.create(name)
3192 return unless name[-1].to_s == 'local'
3197 def make_udp_requester # :nodoc:
3198 nameserver_port = @config.nameserver_port
3199 Requester::MDNSOneShot.new(*nameserver_port)
3207 # A Resolv::LOC::Size
3211 Regex = /^(\d+\.*\d*)[m]$/
3214 # Creates a new LOC::Size from +arg+ which may be:
3216 # LOC::Size:: returns +arg+.
3217 # String:: +arg+ must match the LOC::Size::Regex constant
3219 def self.create(arg)
3226 scalar = [(($1.to_f*(1e2)).to_i.to_s[0].to_i*(2**4)+(($1.to_f*(1e2)).to_i.to_s.length-1))].pack("C")
3228 raise ArgumentError.new("not a properly formed Size string: " + arg)
3230 return Size.new(scalar)
3232 raise ArgumentError.new("cannot interpret as Size: #{arg.inspect}")
3236 def initialize(scalar)
3246 s = @scalar.unpack("H2").join.to_s
3247 return ((s[0].to_i)*(10**(s[1].to_i-2))).to_s << "m"
3250 def inspect # :nodoc:
3251 return "#<#{self.class} #{self}>"
3254 def ==(other) # :nodoc:
3255 return @scalar == other.scalar
3258 def eql?(other) # :nodoc:
3259 return self == other
3269 # A Resolv::LOC::Coord
3273 Regex = /^(\d+)\s(\d+)\s(\d+\.\d+)\s([NESW])$/
3276 # Creates a new LOC::Coord from +arg+ which may be:
3278 # LOC::Coord:: returns +arg+.
3279 # String:: +arg+ must match the LOC::Coord::Regex constant
3281 def self.create(arg)
3287 if Regex =~ arg && $1.to_f < 180
3289 hemi = (m[4][/[NE]/]) || (m[4][/[SW]/]) ? 1 : -1
3290 coordinates = [ ((m[1].to_i*(36e5)) + (m[2].to_i*(6e4)) +
3291 (m[3].to_f*(1e3))) * hemi+(2**31) ].pack("N")
3292 orientation = m[4][/[NS]/] ? 'lat' : 'lon'
3294 raise ArgumentError.new("not a properly formed Coord string: " + arg)
3296 return Coord.new(coordinates,orientation)
3298 raise ArgumentError.new("cannot interpret as Coord: #{arg.inspect}")
3302 def initialize(coordinates,orientation)
3303 unless coordinates.kind_of?(String)
3304 raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}")
3306 unless orientation.kind_of?(String) && orientation[/^lon$|^lat$/]
3307 raise ArgumentError.new('Coord expects orientation to be a String argument of "lat" or "lon"')
3309 @coordinates = coordinates
3310 @orientation = orientation
3314 # The raw coordinates
3316 attr_reader :coordinates
3318 ## The orientation of the hemisphere as 'lat' or 'lon'
3320 attr_reader :orientation
3323 c = @coordinates.unpack("N").join.to_i
3324 val = (c - (2**31)).abs
3325 fracsecs = (val % 1e3).to_i.to_s
3327 secs = (val % 60).to_i.to_s
3329 mins = (val % 60).to_i.to_s
3330 degs = (val / 60).to_i.to_s
3334 hemi = @orientation[/^lat$/] ? "N" : "E"
3336 hemi = @orientation[/^lon$/] ? "W" : "S"
3338 return degs << " " << mins << " " << secs << "." << fracsecs << " " << hemi
3341 def inspect # :nodoc:
3342 return "#<#{self.class} #{self}>"
3345 def ==(other) # :nodoc:
3346 return @coordinates == other.coordinates
3349 def eql?(other) # :nodoc:
3350 return self == other
3354 return @coordinates.hash
3360 # A Resolv::LOC::Alt
3364 Regex = /^([+-]*\d+\.*\d*)[m]$/
3367 # Creates a new LOC::Alt from +arg+ which may be:
3369 # LOC::Alt:: returns +arg+.
3370 # String:: +arg+ must match the LOC::Alt::Regex constant
3372 def self.create(arg)
3379 altitude = [($1.to_f*(1e2))+(1e7)].pack("N")
3381 raise ArgumentError.new("not a properly formed Alt string: " + arg)
3383 return Alt.new(altitude)
3385 raise ArgumentError.new("cannot interpret as Alt: #{arg.inspect}")
3389 def initialize(altitude)
3390 @altitude = altitude
3396 attr_reader :altitude
3399 a = @altitude.unpack("N").join.to_i
3400 return ((a.to_f/1e2)-1e5).to_s + "m"
3403 def inspect # :nodoc:
3404 return "#<#{self.class} #{self}>"
3407 def ==(other) # :nodoc:
3408 return @altitude == other.altitude
3411 def eql?(other) # :nodoc:
3412 return self == other
3416 return @altitude.hash
3424 # Default resolver to use for Resolv class methods.
3426 DefaultResolver = self.new
3429 # Replaces the resolvers in the default resolver with +new_resolvers+. This
3430 # allows resolvers to be changed for resolv-replace.
3432 def DefaultResolver.replace_resolvers new_resolvers
3433 @resolvers = new_resolvers
3437 # Address Regexp to use for matching IP addresses.
3439 AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/