diff --git a/async-dns.gemspec b/async-dns.gemspec index ba85231..df3656b 100644 --- a/async-dns.gemspec +++ b/async-dns.gemspec @@ -6,7 +6,7 @@ Gem::Specification.new do |spec| spec.name = "async-dns" spec.version = Async::DNS::VERSION - spec.summary = "An easy to use DNS client resolver and server for Ruby." + spec.summary = "An easy to use DNS client and server for Ruby." spec.authors = ["Samuel Williams", "Tony Arcieri", "Olle Jonsson", "Greg Thornton", "Hal Brodigan", "Hendrik Beskow", "Mike Perham", "Sean Dilda", "Stefan Wrobel"] spec.license = "MIT" diff --git a/fixtures/async/dns/server_context.rb b/fixtures/async/dns/server_context.rb index 3ceff6c..a788577 100644 --- a/fixtures/async/dns/server_context.rb +++ b/fixtures/async/dns/server_context.rb @@ -6,7 +6,7 @@ require 'sus/fixtures/async/reactor_context' require 'async/dns/server' -require 'async/dns/resolver' +require 'async/dns/client' require 'io/endpoint' @@ -26,19 +26,19 @@ def make_server(endpoint) Async::DNS::Server.new(endpoint) end - def make_resolver(endpoint) - Async::DNS::Resolver.new(endpoint) + def make_client(endpoint) + Async::DNS::Client.new(endpoint: endpoint) end - def resolver - @resolver ||= make_resolver(@resolver_endpoint) + def client + @client ||= make_client(@client_endpoint) end def before super @bound_endpoint = endpoint.bound - @resolver_endpoint = @bound_endpoint.local_address_endpoint + @client_endpoint = @bound_endpoint.local_address_endpoint @server = make_server(@bound_endpoint) @server_task = @server.run diff --git a/fixtures/async/dns/test_server.rb b/fixtures/async/dns/test_server.rb index 45049ae..73b3f69 100644 --- a/fixtures/async/dns/test_server.rb +++ b/fixtures/async/dns/test_server.rb @@ -6,15 +6,15 @@ module Async module DNS class TestServer < Server - def initialize(endpoint = DEFAULT_ENDPOINT, resolver: Resolver.new, **options) + def initialize(endpoint = DEFAULT_ENDPOINT, client: Client.new, **options) super(endpoint, **options) - @resolver = resolver + @client = client end def process(name, resource_class, transaction) # This is a simple example of how to pass the query to an upstream server: - transaction.passthrough!(@resolver) + transaction.passthrough!(@client) end end end diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md new file mode 100644 index 0000000..d1c6171 --- /dev/null +++ b/guides/getting-started/readme.md @@ -0,0 +1,16 @@ +# Getting Started + +This guide explains how to get started with the `async-dns` gem. + +## Installation + +Add the gem to your project: + +~~~ bash +$ bundle add async-dns +~~~ + +## Usage + +### Client + diff --git a/lib/async/dns.rb b/lib/async/dns.rb index 4c7c99d..7a8abf5 100644 --- a/lib/async/dns.rb +++ b/lib/async/dns.rb @@ -8,7 +8,7 @@ require_relative 'dns/version' require_relative 'dns/server' -require_relative 'dns/resolver' +require_relative 'dns/client' require_relative 'dns/handler' # @namespace diff --git a/lib/async/dns/client.rb b/lib/async/dns/client.rb new file mode 100644 index 0000000..43ede0c --- /dev/null +++ b/lib/async/dns/client.rb @@ -0,0 +1,266 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2015-2024, by Samuel Williams. +# Copyright, 2017, by Olle Jonsson. +# Copyright, 2024, by Sean Dilda. + +require_relative 'resolver' +require_relative 'handler' +require_relative 'system' +require_relative 'cache' + +require 'securerandom' +require 'async' + +require 'io/endpoint/composite_endpoint' +require 'io/endpoint/host_endpoint' + +module Async::DNS + # Represents a DNS connection which we don't know how to use. + class InvalidProtocolError < StandardError + end + + # Resolve names to addresses using the DNS protocol. + class Client < Resolver + # 10ms wait between making requests. Override with `options[:delay]` + DEFAULT_DELAY = 0.01 + + # Try a given request 10 times before failing. Override with `options[:retries]`. + DEFAULT_RETRIES = 10 + + # Servers are specified in the same manor as options[:listen], e.g. + # [:tcp/:udp, address, port] + # In the case of multiple servers, they will be checked in sequence. + def initialize(*arguments, endpoint: nil, origin: nil, cache: Cache.new) + super(*arguments) + + @endpoint = endpoint || System.default_nameservers + + # Legacy support for multiple endpoints: + if @endpoint.is_a?(Array) + endpoints = @endpoint.map do |specification| + ::IO::Endpoint.public_send(specification[0], *specification[1..-1]) + end + + @endpoint = ::IO::Endpoint.composite(*endpoints) + end + + @origin = origin + @cache = cache + @count = 0 + end + + attr_accessor :origin + + # Generates a fully qualified name from a given name. + # + # @parameter name [String | Resolv::DNS::Name] The name to fully qualify. + def fully_qualified_name(name) + # If we are passed an existing deconstructed name: + if Resolv::DNS::Name === name + if name.absolute? + return name + else + return name.with_origin(@origin) + end + end + + # ..else if we have a string, we need to do some basic processing: + if name.end_with? '.' + return Resolv::DNS::Name.create(name) + else + return Resolv::DNS::Name.create(name).with_origin(@origin) + end + end + + # Provides the next sequence identification number which is used to keep track of DNS messages. + def next_id! + # Using sequential numbers for the query ID is generally a bad thing because over UDP they can be spoofed. 16-bits isn't hard to guess either, but over UDP we also use a random port, so this makes effectively 32-bits of entropy to guess per request. + SecureRandom.random_number(2**16) + end + + # Query a named resource and return the response. + # + # Bypasses the cache and always makes a new request. + # + # @returns [Resolv::DNS::Message] The response from the server. + def query(name, resource_class = Resolv::DNS::Resource::IN::A) + self.dispatch_query(self.fully_qualified_name(name), resource_class) + end + + # Look up a named resource of the given resource_class. + def records_for(name, resource_classes) + Console.debug(self) {"Looking up records for #{name.inspect} with #{resource_classes.inspect}."} + name = self.fully_qualified_name(name) + resource_classes = Array(resource_classes) + + @cache.fetch(name, resource_classes) do |name, resource_class| + if response = self.dispatch_query(name, resource_class) + response.answer.each do |name, ttl, record| + Console.debug(self) {"Caching record for #{name.inspect} with #{record.class} and TTL #{ttl}."} + @cache.store(name, resource_class, record) + end + end + end + end + + if System.ipv6? + ADDRESS_RESOURCE_CLASSES = [Resolv::DNS::Resource::IN::A, Resolv::DNS::Resource::IN::AAAA] + else + ADDRESS_RESOURCE_CLASSES = [Resolv::DNS::Resource::IN::A] + end + + # Yields a list of `Resolv::IPv4` and `Resolv::IPv6` addresses for the given `name` and `resource_class`. Raises a ResolutionFailure if no severs respond. + def addresses_for(name, resource_classes = ADDRESS_RESOURCE_CLASSES) + records = self.records_for(name, resource_classes) + + if records.empty? + raise ResolutionFailure.new("Could not find any records for #{name.inspect}!") + end + + if records + addresses = [] + + records.each do |record| + if record.respond_to? :address + addresses << record.address + else + # The most common case here is that record.class is IN::CNAME and we need to figure out the address. Usually the upstream DNS server would have replied with this too, and this will be loaded from the response if possible without requesting additional information: + addresses += addresses_for(record.name, resource_classes) + end + end + + if addresses.empty? + addresses = nil + end + end + + return addresses + end + + def call(name) + if addresses = self.addresses_for(name) + return addresses + else + return super + end + end + + private + + # In general, DNS servers are only able to handle a single question at a time. This method is used to dispatch a single query to the server and wait for a response. + def dispatch_query(name, resource_class) + message = Resolv::DNS::Message.new(self.next_id!) + message.rd = 1 + + message.add_question(name, resource_class) + + return dispatch_request(message) + end + + # Send the message to available servers. If no servers respond correctly, nil is returned. This result indicates a failure of the Client to correctly contact any server and get a valid response. + def dispatch_request(message, parent: Async::Task.current) + request = Request.new(message, @endpoint) + error = nil + + request.each do |endpoint| + Console.debug "[#{message.id}] Sending request #{message.question.inspect} to address #{endpoint.inspect}" + + begin + response = try_server(request, endpoint) + + if valid_response(message, response) + return response + end + rescue => error + # Try the next server. + end + end + + if error + raise error + end + + return nil + end + + def try_server(request, endpoint) + endpoint.connect do |socket| + case socket.local_address.socktype + when Socket::SOCK_DGRAM + try_datagram_server(request, socket) + when Socket::SOCK_STREAM + try_stream_server(request, socket) + else + raise InvalidProtocolError.new(endpoint) + end + end + end + + def valid_response(message, response) + if response.tc != 0 + Console.warn "Received truncated response!", message_id: message.id + elsif response.id != message.id + Console.warn "Received response with incorrect message id: #{response.id}!", message_id: message.id + else + Console.debug "Received valid response with #{response.answer.size} answer(s).", message_id: message.id + + return true + end + + return false + end + + def try_datagram_server(request, socket) + socket.sendmsg(request.packet, 0) + + data, peer = socket.recvfrom(UDP_MAXIMUM_SIZE) + + return ::Resolv::DNS::Message.decode(data) + end + + def try_stream_server(request, socket) + transport = Transport.new(socket) + + transport.write_chunk(request.packet) + + data = transport.read_chunk + + return ::Resolv::DNS::Message.decode(data) + end + + # Manages a single DNS question message across one or more servers. + class Request + # Create a new request for the given message and endpoint. + # + # Encodes the message and stores it for later use. + # + # @parameter message [Resolv::DNS::Message] The message to send. + # @parameter endpoint [IO::Endpoint::Generic] The endpoint to send the message to. + def initialize(message, endpoint) + @message = message + @packet = message.encode + + @endpoint = endpoint + end + + # @attribute [Resolv::DNS::Message] The message to send. + attr :message + + # @attribute [String] The encoded message to send. + attr :packet + + def each(&block) + @endpoint.each(&block) + end + + def update_id!(id) + @message.id = id + @packet = @message.encode + end + end + + private_constant :Request + end +end diff --git a/lib/async/dns/resolver.rb b/lib/async/dns/resolver.rb index adc0601..dcfbe4e 100644 --- a/lib/async/dns/resolver.rb +++ b/lib/async/dns/resolver.rb @@ -1,259 +1,23 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2015-2024, by Samuel Williams. -# Copyright, 2017, by Olle Jonsson. -# Copyright, 2024, by Sean Dilda. - -require_relative 'handler' -require_relative 'system' -require_relative 'cache' - -require 'securerandom' -require 'async' - -require 'io/endpoint/composite_endpoint' -require 'io/endpoint/host_endpoint' - -module Async::DNS - # Represents a DNS connection which we don't know how to use. - class InvalidProtocolError < StandardError - end - - # Represents a failure to resolve a given name to an address. - class ResolutionFailure < StandardError - end - - # Resolve names to addresses using the DNS protocol. - class Resolver - # 10ms wait between making requests. Override with `options[:delay]` - DEFAULT_DELAY = 0.01 - - # Try a given request 10 times before failing. Override with `options[:retries]`. - DEFAULT_RETRIES = 10 - - # Servers are specified in the same manor as options[:listen], e.g. - # [:tcp/:udp, address, port] - # In the case of multiple servers, they will be checked in sequence. - def initialize(endpoint = nil, origin: nil, cache: Cache.new) - @endpoint = endpoint || System.default_nameservers - - # Legacy support for multiple endpoints: - if @endpoint.is_a?(Array) - endpoints = @endpoint.map do |specification| - ::IO::Endpoint.public_send(specification[0], *specification[1..-1]) - end - - @endpoint = ::IO::Endpoint.composite(*endpoints) - end - - @origin = origin - @cache = cache - @count = 0 - end - - attr_accessor :origin - - # Generates a fully qualified name from a given name. - # - # @parameter name [String | Resolv::DNS::Name] The name to fully qualify. - def fully_qualified_name(name) - # If we are passed an existing deconstructed name: - if Resolv::DNS::Name === name - if name.absolute? - return name - else - return name.with_origin(@origin) - end - end - - # ..else if we have a string, we need to do some basic processing: - if name.end_with? '.' - return Resolv::DNS::Name.create(name) - else - return Resolv::DNS::Name.create(name).with_origin(@origin) - end - end - - # Provides the next sequence identification number which is used to keep track of DNS messages. - def next_id! - # Using sequential numbers for the query ID is generally a bad thing because over UDP they can be spoofed. 16-bits isn't hard to guess either, but over UDP we also use a random port, so this makes effectively 32-bits of entropy to guess per request. - SecureRandom.random_number(2**16) - end - - # Query a named resource and return the response. - # - # Bypasses the cache and always makes a new request. - # - # @returns [Resolv::DNS::Message] The response from the server. - def query(name, resource_class = Resolv::DNS::Resource::IN::A) - self.dispatch_query(self.fully_qualified_name(name), resource_class) - end - - # Look up a named resource of the given resource_class. - def records_for(name, resource_classes) - Console.debug(self) {"Looking up records for #{name.inspect} with #{resource_classes.inspect}."} - name = self.fully_qualified_name(name) - resource_classes = Array(resource_classes) - - @cache.fetch(name, resource_classes) do |name, resource_class| - if response = self.dispatch_query(name, resource_class) - response.answer.each do |name, ttl, record| - Console.debug(self) {"Caching record for #{name.inspect} with #{record.class} and TTL #{ttl}."} - @cache.store(name, resource_class, record) - end - end - end - end - - if System.ipv6? - ADDRESS_RESOURCE_CLASSES = [Resolv::DNS::Resource::IN::A, Resolv::DNS::Resource::IN::AAAA] - else - ADDRESS_RESOURCE_CLASSES = [Resolv::DNS::Resource::IN::A] +module Async + module DNS + # Represents a failure to resolve a given name to an address. + class ResolutionFailure < StandardError end - # Yields a list of `Resolv::IPv4` and `Resolv::IPv6` addresses for the given `name` and `resource_class`. Raises a ResolutionFailure if no severs respond. - def addresses_for(name, resource_classes = ADDRESS_RESOURCE_CLASSES) - records = self.records_for(name, resource_classes) - - if records.empty? - raise ResolutionFailure.new("Could not find any records for #{name.inspect}!") - end - - addresses = [] - - if records - records.each do |record| - if record.respond_to? :address - addresses << record.address - else - # The most common case here is that record.class is IN::CNAME and we need to figure out the address. Usually the upstream DNS server would have replied with this too, and this will be loaded from the response if possible without requesting additional information: - addresses += addresses_for(record.name, resource_classes) - end - end - end - - if addresses.empty? + class Resolver + RESOLUTION_FAILURE = proc do |name| raise ResolutionFailure.new("Could not find any addresses for #{name.inspect}!") end - return addresses - end - - private - - # In general, DNS servers are only able to handle a single question at a time. This method is used to dispatch a single query to the server and wait for a response. - def dispatch_query(name, resource_class) - message = Resolv::DNS::Message.new(self.next_id!) - message.rd = 1 - - message.add_question(name, resource_class) - - return dispatch_request(message) - end - - # Send the message to available servers. If no servers respond correctly, nil is returned. This result indicates a failure of the resolver to correctly contact any server and get a valid response. - def dispatch_request(message, parent: Async::Task.current) - request = Request.new(message, @endpoint) - error = nil - - request.each do |endpoint| - Console.debug "[#{message.id}] Sending request #{message.question.inspect} to address #{endpoint.inspect}" - - begin - response = try_server(request, endpoint) - - if valid_response(message, response) - return response - end - rescue => error - # Try the next server. - end - end - - if error - raise error + def initialize(delegate = RESOLUTION_FAILURE) + @delegate = delegate end - return nil - end - - def try_server(request, endpoint) - endpoint.connect do |socket| - case socket.local_address.socktype - when Socket::SOCK_DGRAM - try_datagram_server(request, socket) - when Socket::SOCK_STREAM - try_stream_server(request, socket) - else - raise InvalidProtocolError.new(endpoint) - end - end - end - - def valid_response(message, response) - if response.tc != 0 - Console.warn "Received truncated response!", message_id: message.id - elsif response.id != message.id - Console.warn "Received response with incorrect message id: #{response.id}!", message_id: message.id - else - Console.debug "Received valid response with #{response.answer.size} answer(s).", message_id: message.id - - return true + # Return addresses for the given name. + # @returns [Array(String)] The addresses for the given name. + def call(name) + @delegate.call(name) end - - return false end - - def try_datagram_server(request, socket) - socket.sendmsg(request.packet, 0) - - data, peer = socket.recvfrom(UDP_MAXIMUM_SIZE) - - return ::Resolv::DNS::Message.decode(data) - end - - def try_stream_server(request, socket) - transport = Transport.new(socket) - - transport.write_chunk(request.packet) - - data = transport.read_chunk - - return ::Resolv::DNS::Message.decode(data) - end - - # Manages a single DNS question message across one or more servers. - class Request - # Create a new request for the given message and endpoint. - # - # Encodes the message and stores it for later use. - # - # @parameter message [Resolv::DNS::Message] The message to send. - # @parameter endpoint [IO::Endpoint::Generic] The endpoint to send the message to. - def initialize(message, endpoint) - @message = message - @packet = message.encode - - @endpoint = endpoint - end - - # @attribute [Resolv::DNS::Message] The message to send. - attr :message - - # @attribute [String] The encoded message to send. - attr :packet - - def each(&block) - @endpoint.each(&block) - end - - def update_id!(id) - @message.id = id - @packet = @message.encode - end - end - - private_constant :Request end -end +end \ No newline at end of file diff --git a/lib/async/dns/system.rb b/lib/async/dns/system.rb index 7da8a4d..7cd8041 100644 --- a/lib/async/dns/system.rb +++ b/lib/async/dns/system.rb @@ -36,7 +36,7 @@ def self.ipv6? end # An interface for querying the system's hosts file. - class Hosts + class Hosts < Resolver # Hosts for the local system. def self.local hosts = self.new @@ -60,7 +60,11 @@ def initialize # This is used to match names against the list of known hosts: def call(name) - @names.include?(name) + if addresses = @names[name] + return addresses + else + return super + end end # Lookup a name in the hosts file. diff --git a/lib/async/dns/transaction.rb b/lib/async/dns/transaction.rb index 643a6a3..e65d98a 100644 --- a/lib/async/dns/transaction.rb +++ b/lib/async/dns/transaction.rb @@ -67,16 +67,16 @@ def append!(name, resource_class = nil, options = {}) Transaction.new(@server, @query, name, resource_class || @resource_class, @response, **options).process end - # Use the given resolver to respond to the question. Uses `passthrough` to do the lookup and merges the result. + # Use the given Client to respond to the question. Uses `passthrough` to do the lookup and merges the result. # # If a block is supplied, this function yields with the `response` message if successful. This could be used, for example, to update a cache or modify the reply. # # If recursion is not requested, the result is `fail!(:Refused)`. This check is ignored if an explicit `options[:name]` or `options[:force]` is given. # - # If the resolver can't reach upstream servers, `fail!(:ServFail)` is invoked. - def passthrough!(resolver, force: false, **options, &block) + # If the Client can't reach upstream servers, `fail!(:ServFail)` is invoked. + def passthrough!(client, force: false, **options, &block) if @query.rd || force || options[:name] - response = passthrough(resolver, **options) + response = passthrough(client, **options) if response yield response if block_given? @@ -93,13 +93,13 @@ def passthrough!(resolver, force: false, **options, &block) end end - # Use the given resolver to respond to the question. + # Use the given Client to respond to the question. # # A block must be supplied, and provided a valid response is received from the upstream server, this function yields with the reply and reply_name. # # If `options[:name]` is provided, this overrides the default query name sent to the upstream server. The same logic applies to `options[:resource_class]`. - def passthrough(resolver, name: self.name, resource_class: self.resource_class) - resolver.query(name, resource_class) + def passthrough(client, name: self.name, resource_class: self.resource_class) + client.query(name, resource_class) end # Respond to the given query with a resource record. The arguments to this function depend on the `resource_class` requested. This function instantiates the resource class with the supplied arguments, and then passes it to {#append!}. diff --git a/readme.md b/readme.md index 0575311..af7f1f2 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # Async::DNS -Async::DNS is a high-performance DNS client resolver and server which can be easily integrated into other projects or used as a stand-alone daemon. It was forked from [RubyDNS](https://github.com/ioquatix/rubydns) which is now implemented in terms of this library. +Async::DNS is a high-performance DNS client and server which can be easily integrated into other projects or used as a stand-alone daemon. It was forked from [RubyDNS](https://github.com/ioquatix/rubydns) which is now implemented in terms of this library. [![Development Status](https://github.com/socketry/async-dns/workflows/Test/badge.svg)](https://github.com/socketry/async-dns/actions?workflow=Test) @@ -20,15 +20,15 @@ Or install it yourself as: ## Usage -### Resolver +### Client -Here is a simple example showing how to use the resolver: +Here is a simple example showing how to use the client: ``` ruby Async::Reactor.run do - resolver = Async::DNS::Resolver.new() + client = Async::DNS::Client.new - addresses = resolver.addresses_for("www.google.com.") + addresses = client.addresses_for("www.google.com.") puts addresses.inspect end @@ -38,11 +38,11 @@ end You can also specify custom DNS servers: ``` ruby -resolver = Async::DNS::Resolver.new(Async::DNS::System.standard_connections(['8.8.8.8'])) +client = Async::DNS::Client.new(endpoint: Async::DNS::System.standard_connections(['8.8.8.8'])) # or -resolver = Async::DNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]]) +client = Async::DNS::Client.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]]) ``` ### Server @@ -54,9 +54,9 @@ require 'async/dns' class TestServer < Async::DNS::Server def process(name, resource_class, transaction) - @resolver ||= Async::DNS::Resolver.new([[:udp, '8.8.8.8', 53], [:tcp, '8.8.8.8', 53]]) + @client ||= Async::DNS::Client.new([[:udp, '8.8.8.8', 53], [:tcp, '8.8.8.8', 53]]) - transaction.passthrough!(@resolver) + transaction.passthrough!(@client) end end @@ -78,7 +78,7 @@ On some platforms (e.g. Mac OS X) the number of file descriptors is relatively l ### Server -The performance is on the same magnitude as `bind9`. Some basic benchmarks resolving 1000 names concurrently, repeated 5 times, using `Async::DNS::Resolver` gives the following: +The performance is on the same magnitude as `bind9`. Some basic benchmarks resolving 1000 names concurrently, repeated 5 times, using `Async::DNS::Client` gives the following: ``` user system total real @@ -92,13 +92,13 @@ These benchmarks are included in the unit tests. To test bind9 performance, it m We welcome additional benchmarks and feedback regarding Async::DNS performance. To check the current performance results, consult the [travis build job output](https://travis-ci.org/socketry/async-dns). -### Resolver +### Client -The `Async::DNS::Resolver` is highly concurrent and can resolve individual names as fast as the built in `Resolv::DNS` resolver. Because the resolver is asynchronous, when dealing with multiple names, it can work more efficiently: +The `Async::DNS::Client` is highly concurrent and can resolve individual names as fast as the built in `Resolv::DNS` Client. Because the Client is asynchronous, when dealing with multiple names, it can work more efficiently: ``` user system total real -Async::DNS::Resolver 0.020000 0.010000 0.030000 ( 0.030507) +Async::DNS::Client 0.020000 0.010000 0.030000 ( 0.030507) Resolv::DNS 0.070000 0.010000 0.080000 ( 1.465975) ``` @@ -106,7 +106,7 @@ These benchmarks are included in the unit tests. ### Server -The performance is on the same magnitude as `bind9`. Some basic benchmarks resolving 1000 names concurrently, repeated 5 times, using `Async::DNS::Resolver` gives the following: +The performance is on the same magnitude as `bind9`. Some basic benchmarks resolving 1000 names concurrently, repeated 5 times, using `Async::DNS::Client` gives the following: ``` user system total real diff --git a/test/async/dns/client.rb b/test/async/dns/client.rb new file mode 100644 index 0000000..f037360 --- /dev/null +++ b/test/async/dns/client.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2015-2024, by Samuel Williams. +# Copyright, 2024, by Sean Dilda. + +require 'async/dns/client' +require 'sus/fixtures/async' + +describe Async::DNS::Client do + include Sus::Fixtures::Async::ReactorContext + + let(:client) {Async::DNS::Client.new} + + it "should result in non-existent domain" do + response = client.query('foobar.example.com', Resolv::DNS::Resource::IN::A) + + expect(response.rcode).to be == Resolv::DNS::RCode::NXDomain + end + + it "should result in some answers" do + response = client.query('google.com', Resolv::DNS::Resource::IN::A) + + expect(response.class).to be == Resolv::DNS::Message + expect(response.answer.size).to be > 0 + end + + with '#addresses_for' do + it "should return IP addresses" do + addresses = client.addresses_for('google.com') + + expect(addresses).to have_value(be_a Resolv::IPv4) + + if Async::DNS::System.ipv6? + expect(addresses).to have_value(be_a Resolv::IPv6) + end + end + + it "should recursively resolve CNAME records" do + # > dig A www.baidu.com + # + # ; <<>> DiG 9.18.27 <<>> A www.baidu.com + # ;; global options: +cmd + # ;; Got answer: + # ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14301 + # ;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1 + # + # ;; OPT PSEUDOSECTION: + # ; EDNS: version: 0, flags:; udp: 65494 + # ;; QUESTION SECTION: + # ;www.baidu.com. IN A + # + # ;; ANSWER SECTION: + # www.baidu.com. 1128 IN CNAME www.a.shifen.com. + # www.a.shifen.com. 15 IN CNAME www.wshifen.com. + # www.wshifen.com. 247 IN A 119.63.197.139 + # www.wshifen.com. 247 IN A 119.63.197.151 + + addresses = client.addresses_for('www.baidu.com') + + expect(addresses.size).to be > 0 + expect(addresses).to have_value(be_a Resolv::IPv4) + end + end + + with '#fully_qualified_name' do + let(:client) {Async::DNS::Client.new(origin: "foo.bar.")} + + it "should generate fully qualified domain name with specified origin" do + fully_qualified_name = client.fully_qualified_name("baz") + + expect(fully_qualified_name).to be(:absolute?) + expect(fully_qualified_name.to_s).to be == "baz.foo.bar." + end + end +end diff --git a/test/async/dns/resolver/consistency.rb b/test/async/dns/client/consistency.rb similarity index 87% rename from test/async/dns/resolver/consistency.rb rename to test/async/dns/client/consistency.rb index 6dbadcd..060bc07 100644 --- a/test/async/dns/resolver/consistency.rb +++ b/test/async/dns/client/consistency.rb @@ -62,7 +62,7 @@ "jimdo.com", ] -describe Async::DNS::Resolver do +describe Async::DNS::Client do include Sus::Fixtures::Async::ReactorContext def before @@ -74,22 +74,22 @@ def before end end - it "is consistent with built in resolver" do + it "is consistent with built in client" do resolved_a = resolved_b = nil async_dns_performance = Benchmark.measure do - resolver = Async::DNS::Resolver.new + client = Async::DNS::Client.new resolved_a = DOMAINS.to_h do |domain| - [domain, resolver.addresses_for(domain)] + [domain, client.addresses_for(domain)] end end resolv_dns_performance = Benchmark.measure do - resolver = Resolv::DNS.new + client = Resolv::DNS.new resolved_b = DOMAINS.to_h do |domain| - [domain, resolver.getaddresses(domain)] + [domain, client.getaddresses(domain)] end end diff --git a/test/async/dns/resolver.rb b/test/async/dns/resolver.rb index cc4b75b..a1c7377 100644 --- a/test/async/dns/resolver.rb +++ b/test/async/dns/resolver.rb @@ -4,73 +4,11 @@ # Copyright, 2015-2024, by Samuel Williams. # Copyright, 2024, by Sean Dilda. -require 'async/dns/resolver' +require 'async/dns/client' require 'sus/fixtures/async' describe Async::DNS::Resolver do include Sus::Fixtures::Async::ReactorContext - let(:resolver) {Async::DNS::Resolver.new} - - it "should result in non-existent domain" do - response = resolver.query('foobar.example.com', Resolv::DNS::Resource::IN::A) - - expect(response.rcode).to be == Resolv::DNS::RCode::NXDomain - end - - it "should result in some answers" do - response = resolver.query('google.com', Resolv::DNS::Resource::IN::A) - - expect(response.class).to be == Resolv::DNS::Message - expect(response.answer.size).to be > 0 - end - - with '#addresses_for' do - it "should return IP addresses" do - addresses = resolver.addresses_for('google.com') - - expect(addresses).to have_value(be_a Resolv::IPv4) - - if Async::DNS::System.ipv6? - expect(addresses).to have_value(be_a Resolv::IPv6) - end - end - - it "should recursively resolve CNAME records" do - # > dig A www.baidu.com - # - # ; <<>> DiG 9.18.27 <<>> A www.baidu.com - # ;; global options: +cmd - # ;; Got answer: - # ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14301 - # ;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1 - # - # ;; OPT PSEUDOSECTION: - # ; EDNS: version: 0, flags:; udp: 65494 - # ;; QUESTION SECTION: - # ;www.baidu.com. IN A - # - # ;; ANSWER SECTION: - # www.baidu.com. 1128 IN CNAME www.a.shifen.com. - # www.a.shifen.com. 15 IN CNAME www.wshifen.com. - # www.wshifen.com. 247 IN A 119.63.197.139 - # www.wshifen.com. 247 IN A 119.63.197.151 - - addresses = resolver.addresses_for('www.baidu.com') - - expect(addresses.size).to be > 0 - expect(addresses).to have_value(be_a Resolv::IPv4) - end - end - - with '#fully_qualified_name' do - let(:resolver) {Async::DNS::Resolver.new(origin: "foo.bar.")} - - it "should generate fully qualified domain name with specified origin" do - fully_qualified_name = resolver.fully_qualified_name("baz") - - expect(fully_qualified_name).to be(:absolute?) - expect(fully_qualified_name.to_s).to be == "baz.foo.bar." - end - end -end + let(:resolver) {Async::DNS::Resolver.default} +end \ No newline at end of file diff --git a/test/async/dns/server.rb b/test/async/dns/server.rb index 23f224d..be3b05b 100644 --- a/test/async/dns/server.rb +++ b/test/async/dns/server.rb @@ -11,7 +11,7 @@ include Async::DNS::ServerContext it "can resolve a domain name" do - response = resolver.query("example.com") + response = client.query("example.com") expect(response).to have_attributes( qr: be == 1, @@ -21,13 +21,13 @@ ) end - with 'default resolver' do + with 'default client' do def make_server(endpoint) Async::DNS::TestServer.new(endpoint) end it "can resolve a domain name" do - response = resolver.query("www.google.com") + response = client.query("www.google.com") expect(response).to have_attributes( qr: be == 1, @@ -38,7 +38,7 @@ def make_server(endpoint) end it "can resolve non-existent domain name" do - response = resolver.query("foobar.example.com") + response = client.query("foobar.example.com") expect(response).to have_attributes( qr: be == 1, diff --git a/test/async/dns/server/slow.rb b/test/async/dns/server/slow.rb index e5a7038..fd7dbc3 100644 --- a/test/async/dns/server/slow.rb +++ b/test/async/dns/server/slow.rb @@ -23,12 +23,12 @@ def make_server(endpoint) SlowServer.new(endpoint) end - def make_resolver(endpoint) - Async::DNS::Resolver.new(endpoint.with(timeout: 0.1)) + def make_client(endpoint) + Async::DNS::Client.new(endpoint: endpoint.with(timeout: 0.1)) end it "should fail with non-existent domain" do - response = resolver.query("example.net", IN::A) + response = client.query("example.net", IN::A) expect(response.rcode).to be == Resolv::DNS::RCode::NXDomain end @@ -36,7 +36,7 @@ def make_resolver(endpoint) skip_unless_method_defined(:timeout, IO) expect do - resolver.query("example.com", IN::A) + client.query("example.com", IN::A) end.to raise_exception(IO::TimeoutError) end end diff --git a/test/async/dns/server/truncated.rb b/test/async/dns/server/truncated.rb index 0ef14e3..fa95a8d 100644 --- a/test/async/dns/server/truncated.rb +++ b/test/async/dns/server/truncated.rb @@ -28,7 +28,7 @@ def make_server(endpoint) end it "should use tcp because of large response" do - response = resolver.query("truncation-100", IN::TXT) + response = client.query("truncation-100", IN::TXT) text = response.answer.first expect(text[2].strings.join).to be == ("Hello World! " * 1000) end diff --git a/test/async/dns/system.rb b/test/async/dns/system.rb index 268ba91..0491cdb 100755 --- a/test/async/dns/system.rb +++ b/test/async/dns/system.rb @@ -16,9 +16,9 @@ end it "should respond to query for google.com" do - resolver = Async::DNS::Resolver.new(Async::DNS::System.nameservers) + client = Async::DNS::Client.new(endpoint: Async::DNS::System.nameservers) - response = resolver.query('google.com') + response = client.query('google.com') expect(response.class).to be == Resolv::DNS::Message expect(response.rcode).to be == Resolv::DNS::RCode::NoError @@ -36,7 +36,7 @@ hosts.parse_hosts(file) end - expect(hosts.call('testing')).to be == true + expect(hosts.call('testing')).to be == ['1.2.3.4'] expect(hosts['testing']).to be == '1.2.3.4' end end diff --git a/test/async/dns/transaction.rb b/test/async/dns/transaction.rb index 3ff9885..3527316 100755 --- a/test/async/dns/transaction.rb +++ b/test/async/dns/transaction.rb @@ -16,7 +16,7 @@ let(:query) {Resolv::DNS::Message.new(0)} let(:question) {Resolv::DNS::Name.create("www.google.com.")} let(:response) {Resolv::DNS::Message.new(0)} - let(:resolver) {Async::DNS::Resolver.new} + let(:client) {Async::DNS::Client.new} it "should append an address" do transaction = Async::DNS::Transaction.new(server, query, question, IN::A, response) @@ -32,7 +32,7 @@ expect(transaction.response.answer.size).to be == 0 - transaction.passthrough!(resolver) + transaction.passthrough!(client) expect(transaction.response.answer.size).to be > 0 end @@ -42,7 +42,7 @@ expect(transaction.response.answer.size).to be == 0 - response = transaction.passthrough(resolver) + response = transaction.passthrough(client) expect(response.answer.length).to be > 0 end @@ -54,7 +54,7 @@ passthrough_response = nil - transaction.passthrough!(resolver) do |response| + transaction.passthrough!(client) do |response| passthrough_response = response end @@ -74,7 +74,7 @@ expect(transaction.response.answer.size).to be == 0 - transaction.passthrough!(resolver) + transaction.passthrough!(client) expect(transaction.response.answer.first[2]).to be_a IN::AAAA end @@ -84,7 +84,7 @@ expect(transaction.response.answer.size).to be == 0 - transaction.passthrough!(resolver) + transaction.passthrough!(client) expect(transaction.response.answer.first[2]).to be_a IN::MX end @@ -94,7 +94,7 @@ expect(transaction.response.answer.size).to be == 0 - transaction.passthrough!(resolver) + transaction.passthrough!(client) expect(transaction.response.answer.first[2]).to be_a IN::NS end @@ -104,7 +104,7 @@ expect(transaction.response.answer.size).to be == 0 - transaction.passthrough!(resolver) + transaction.passthrough!(client) expect(transaction.response.answer.first[2]).to be_a IN::PTR end @@ -114,7 +114,7 @@ expect(transaction.response.answer.size).to be == 0 - transaction.passthrough!(resolver) + transaction.passthrough!(client) expect(transaction.response.answer.first[2]).to be_a IN::SOA end