├── .rspec ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── celluloid-dns.gemspec ├── lib └── celluloid │ ├── dns.rb │ └── dns │ ├── chunked.rb │ ├── extensions │ ├── resolv.rb │ └── string.rb │ ├── handler.rb │ ├── logger.rb │ ├── message.rb │ ├── replace.rb │ ├── resolver.rb │ ├── server.rb │ ├── system.rb │ ├── transaction.rb │ ├── transport.rb │ └── version.rb ├── logo.png └── spec ├── celluloid └── dns │ ├── celluloid_bug_spec.rb │ ├── hosts.txt │ ├── ipv6_spec.rb │ ├── message_spec.rb │ ├── origin_spec.rb │ ├── replace_spec.rb │ ├── resolver_performance_spec.rb │ ├── resolver_spec.rb │ ├── server │ ├── bind9 │ │ ├── generate-local.rb │ │ ├── local.zone │ │ ├── named.conf │ │ └── named.run │ └── million.rb │ ├── server_performance_spec.rb │ ├── slow_server_spec.rb │ ├── socket_spec.rb │ ├── system_spec.rb │ ├── transaction_spec.rb │ └── truncation_spec.rb └── spec_helper.rb /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | --warnings 4 | --require spec_helper -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | addons: 4 | apt: 5 | packages: 6 | - bind9 7 | rvm: 8 | - 2.2.6 9 | - 2.3.3 10 | - 2.4.0 11 | - rbx-2 12 | - ruby-head 13 | - jruby-head 14 | matrix: 15 | allow_failures: 16 | - rvm: rbx-2 17 | - rvm: ruby-head 18 | - rvm: jruby-head 19 | env: COVERAGE=true 20 | notifications: 21 | irc: "irc.freenode.org#celluloid" -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem "celluloid", github: 'celluloid/celluloid' 6 | # gem "celluloid-io", github: 'celluloid/celluloid-io' 7 | 8 | group :test do 9 | gem 'simplecov' 10 | gem 'coveralls', require: false 11 | end 12 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/celluloid/celluloid.git 3 | revision: a635787672fb0789af1b402c603af135557c31aa 4 | specs: 5 | celluloid (0.18.0.pre) 6 | celluloid-pool (~> 0.20) 7 | celluloid-supervision (~> 0.20) 8 | timers (~> 4) 9 | 10 | PATH 11 | remote: . 12 | specs: 13 | celluloid-dns (0.17.3) 14 | celluloid (~> 0.17) 15 | celluloid-io (~> 0.17) 16 | timers (~> 4.1.0) 17 | 18 | GEM 19 | remote: https://rubygems.org/ 20 | specs: 21 | celluloid-io (0.17.3) 22 | celluloid (>= 0.17.2) 23 | nio4r (>= 1.1) 24 | timers (>= 4.1.1) 25 | celluloid-pool (0.20.5) 26 | timers (>= 4.1.1) 27 | celluloid-supervision (0.20.6) 28 | timers (>= 4.1.1) 29 | coveralls (0.8.19) 30 | json (>= 1.8, < 3) 31 | simplecov (~> 0.12.0) 32 | term-ansicolor (~> 1.3) 33 | thor (~> 0.19.1) 34 | tins (~> 1.6) 35 | diff-lcs (1.3) 36 | docile (1.1.5) 37 | hitimes (1.2.4) 38 | json (2.0.3) 39 | nio4r (2.0.0) 40 | process-daemon (1.0.1) 41 | rainbow (~> 2.0) 42 | rainbow (2.2.1) 43 | rake (12.0.0) 44 | rspec (3.4.0) 45 | rspec-core (~> 3.4.0) 46 | rspec-expectations (~> 3.4.0) 47 | rspec-mocks (~> 3.4.0) 48 | rspec-core (3.4.4) 49 | rspec-support (~> 3.4.0) 50 | rspec-expectations (3.4.0) 51 | diff-lcs (>= 1.2.0, < 2.0) 52 | rspec-support (~> 3.4.0) 53 | rspec-mocks (3.4.1) 54 | diff-lcs (>= 1.2.0, < 2.0) 55 | rspec-support (~> 3.4.0) 56 | rspec-support (3.4.1) 57 | simplecov (0.12.0) 58 | docile (~> 1.1.0) 59 | json (>= 1.8, < 3) 60 | simplecov-html (~> 0.10.0) 61 | simplecov-html (0.10.0) 62 | term-ansicolor (1.4.1) 63 | tins (~> 1.0) 64 | thor (0.19.4) 65 | timers (4.1.2) 66 | hitimes 67 | tins (1.13.2) 68 | 69 | PLATFORMS 70 | ruby 71 | 72 | DEPENDENCIES 73 | bundler (~> 1.3) 74 | celluloid! 75 | celluloid-dns! 76 | coveralls 77 | process-daemon (~> 1.0.0) 78 | rake 79 | rspec (~> 3.4.0) 80 | simplecov 81 | 82 | BUNDLED WITH 83 | 1.14.6 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Celluloid::DNS](https://github.com/celluloid/celluloid-dns/raw/master/logo.png) 2 | 3 | **`Celluloid::DNS` is dead. Long live [`Socketry::DNS`](https://github.com/socketry/socketry-dns)** 4 | 5 | Celluloid::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][1] which is now implemented in terms of this library. 6 | 7 | [1]: https://github.com/ioquatix/rubydns 8 | 9 | [![Gem Version](https://badge.fury.io/rb/celluloid-dns.svg)](http://rubygems.org/gems/celluloid-dns) 10 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/celluloid/celluloid/blob/master/LICENSE.txt) 11 | [![Build Status](https://secure.travis-ci.org/celluloid/celluloid-dns.svg?branch=master)](http://travis-ci.org/celluloid/celluloid-dns) 12 | [![Maintained: no](https://img.shields.io/maintenance/no/2017.svg)](https://github.com/celluloid/celluloid/issues/779) 13 | 14 | ## Installation 15 | 16 | Add this line to your application's Gemfile: 17 | 18 | gem 'celluloid-dns' 19 | 20 | And then execute: 21 | 22 | $ bundle 23 | 24 | Or install it yourself as: 25 | 26 | $ gem install celluloid-dns 27 | 28 | ## Usage 29 | 30 | ### Resolver 31 | 32 | Here is a simple example showing how to use the resolver: 33 | 34 | resolver = Celluloid::DNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]]) 35 | 36 | addresses = resolver.addresses_for("www.google.com.") 37 | 38 | expect(addresses.size).to be > 0 39 | 40 | addresses.each do |address| 41 | expect(address).to be_kind_of(Resolv::IPv4) | be_kind_of(Resolv::IPv6) 42 | end 43 | 44 | ### Server 45 | 46 | Here is a simple example showing how to use the server: 47 | 48 | class TestServer < Celluloid::DNS::Server 49 | def process(name, resource_class, transaction) 50 | @resolver ||= Celluloid::DNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]]) 51 | 52 | transaction.passthrough!(@resolver) 53 | end 54 | end 55 | 56 | server = TestServer.new(listen: [[:udp, 'localhost', 2346]]) 57 | server.run 58 | 59 | sleep 60 | 61 | Then to test you could use `dig` like so: 62 | 63 | dig @localhost -p 2346 google.com 64 | 65 | ## FAQ 66 | 67 | ### File Handle Limitations 68 | 69 | I get the error `Errno::EMFILE: Too many open files - socket(2) - udp` when trying to run a server. What should I do? 70 | 71 | On some platforms (e.g. Mac OS X) the number of file descriptors is relatively low by default and should be increased by calling `ulimit -n 10000` before running tests or even before starting a server which expects a large number of concurrent incoming connections. 72 | 73 | ### Server 74 | 75 | The performance is on the same magnitude as `bind9`. Some basic benchmarks resolving 1000 names concurrently, repeated 5 times, using `Celluloid::DNS::Resolver` gives the following: 76 | 77 | user system total real 78 | Celluloid::DNS::Server 4.280000 0.450000 4.730000 ( 4.854862) 79 | Bind9 4.970000 0.520000 5.490000 ( 5.541213) 80 | 81 | These benchmarks are included in the unit tests. To test bind9 performance, it must be installed and `which named` must return the executable. 82 | 83 | 84 | ## Performance 85 | 86 | We welcome additional benchmarks and feedback regarding Celluloid::DNS performance. To check the current performance results, consult the [travis build job output](https://travis-ci.org/celluloid/celluloid-dns). 87 | 88 | ### Resolver 89 | 90 | The `Celluloid::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: 91 | 92 | user system total real 93 | Celluloid::DNS::Resolver 0.020000 0.010000 0.030000 ( 0.030507) 94 | Resolv::DNS 0.070000 0.010000 0.080000 ( 1.465975) 95 | 96 | These benchmarks are included in the unit tests. 97 | 98 | ### Server 99 | 100 | The performance is on the same magnitude as `bind9`. Some basic benchmarks resolving 1000 names concurrently, repeated 5 times, using `Celluloid::DNS::Resolver` gives the following: 101 | 102 | user system total real 103 | Celluloid::DNS::Server 4.280000 0.450000 4.730000 ( 4.854862) 104 | Bind9 4.970000 0.520000 5.490000 ( 5.541213) 105 | 106 | These benchmarks are included in the unit tests. To test bind9 performance, it must be installed and `which named` must return the executable. 107 | 108 | ### DNSSEC support 109 | 110 | DNSSEC is currently not supported and is [unlikely to be supported in the future](http://sockpuppet.org/blog/2015/01/15/against-dnssec/). Feel free to submit a PR. 111 | 112 | ## Contributing 113 | 114 | 1. Fork it 115 | 2. Create your feature branch (`git checkout -b my-new-feature`) 116 | 3. Commit your changes (`git commit -am 'Add some feature'`) 117 | 4. Push to the branch (`git push origin my-new-feature`) 118 | 5. Create new Pull Request 119 | 120 | ### Desired Features 121 | 122 | * Support for more features of DNS such as zone transfer. 123 | * Some kind of system level integration, e.g. registering a DNS server with the currently running system resolver. 124 | 125 | ## License 126 | 127 | Released under the MIT license. 128 | 129 | Copyright, 2015, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams). 130 | 131 | Permission is hereby granted, free of charge, to any person obtaining a copy 132 | of this software and associated documentation files (the "Software"), to deal 133 | in the Software without restriction, including without limitation the rights 134 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 135 | copies of the Software, and to permit persons to whom the Software is 136 | furnished to do so, subject to the following conditions: 137 | 138 | The above copyright notice and this permission notice shall be included in 139 | all copies or substantial portions of the Software. 140 | 141 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 142 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 143 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 144 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 145 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 146 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 147 | THE SOFTWARE. 148 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) do |task| 5 | task.rspec_opts = ["--require", "simplecov"] if ENV['COVERAGE'] 6 | end 7 | 8 | task :default => :spec 9 | 10 | require 'celluloid/current' 11 | -------------------------------------------------------------------------------- /celluloid-dns.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/celluloid/dns/version', __FILE__) 3 | 4 | Gem::Specification.new do |spec| 5 | spec.name = "celluloid-dns" 6 | spec.version = Celluloid::DNS::VERSION 7 | spec.authors = ["Samuel Williams"] 8 | spec.email = ["samuel.williams@oriontransfer.co.nz"] 9 | spec.description = <<-EOF 10 | Celluloid::DNS provides a high-performance DNS client resolver and server 11 | which can be easily integrated into other projects or used as a stand-alone 12 | daemon. 13 | EOF 14 | spec.summary = "An easy to use DNS client resolver and server for Ruby." 15 | spec.homepage = "https://github.com/celluloid/celluloid-dns" 16 | spec.license = "MIT" 17 | 18 | spec.files = `git ls-files`.split($/) 19 | spec.executables = spec.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 20 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 21 | spec.require_paths = ["lib"] 22 | spec.has_rdoc = "yard" 23 | 24 | spec.required_ruby_version = '>= 2.0.0' 25 | 26 | spec.add_dependency("celluloid", "~> 0.17") 27 | spec.add_dependency("celluloid-io", "~> 0.17") 28 | spec.add_dependency("timers", "~> 4.1.0") 29 | 30 | spec.add_development_dependency "bundler", "~> 1.3" 31 | spec.add_development_dependency "process-daemon", "~> 1.0.0" 32 | spec.add_development_dependency "rspec", "~> 3.4.0" 33 | spec.add_development_dependency "rake" 34 | end 35 | -------------------------------------------------------------------------------- /lib/celluloid/dns.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2009, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'celluloid/current' 22 | require 'celluloid/io' 23 | 24 | require_relative 'dns/version' 25 | 26 | require_relative 'dns/message' 27 | require_relative 'dns/server' 28 | require_relative 'dns/resolver' 29 | require_relative 'dns/handler' 30 | require_relative 'dns/logger' 31 | 32 | module Celluloid 33 | module DNS 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/celluloid/dns/chunked.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2009, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | module Celluloid::DNS 22 | # Produces an array of arrays of binary data with each sub-array a maximum of chunk_size bytes. 23 | def self.chunked(string, chunk_size = 255) 24 | chunks = [] 25 | 26 | offset = 0 27 | while offset < string.bytesize 28 | chunks << string.byteslice(offset, chunk_size) 29 | offset += chunk_size 30 | end 31 | 32 | return chunks 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/celluloid/dns/extensions/resolv.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2009, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'resolv' 22 | 23 | class Resolv 24 | class DNS 25 | class Message 26 | # Merge the given message with this message. A number of heuristics are applied in order to ensure that the result makes sense. For example, If the current message is not recursive but is being merged with a message that was recursive, this bit is maintained. If either message is authoritive, then the result is also authoritive. 27 | # 28 | # Modifies the current message in place. 29 | def merge! (other) 30 | # Authoritive Answer 31 | @aa = @aa && other.aa 32 | 33 | @question += other.question 34 | @answer += other.answer 35 | @authority += other.authority 36 | @additional += other.additional 37 | 38 | # Recursion Available 39 | @ra = @ra || other.ra 40 | 41 | # Result Code (Error Code) 42 | @rcode = other.rcode unless other.rcode == 0 43 | 44 | # Recursion Desired 45 | @rd = @rd || other.rd 46 | end 47 | end 48 | 49 | class OriginError < ArgumentError 50 | end 51 | 52 | class Name 53 | def to_s 54 | "#{@labels.join('.')}#{@absolute ? '.' : ''}" 55 | end 56 | 57 | def inspect 58 | "#<#{self.class}: #{self.to_s}>" 59 | end 60 | 61 | # Return the name, typically absolute, with the specified origin as a suffix. If the origin is nil, don't change the name, but change it to absolute (as specified). 62 | def with_origin(origin, absolute = true) 63 | return self.class.new(@labels, absolute) if origin == nil 64 | 65 | origin = Label.split(origin) if String === origin 66 | 67 | return self.class.new(@labels + origin, absolute) 68 | end 69 | 70 | 71 | # Return the name, typically relative, without the specified origin suffix. If the origin is nil, don't change the name, but change it to absolute (as specified). 72 | def without_origin(origin, absolute = false) 73 | return self.class.new(@labels, absolute) if origin == nil 74 | 75 | origin = Label.split(origin) if String === origin 76 | 77 | if @labels.last(origin.length) == origin 78 | return self.class.new(@labels.first(@labels.length - origin.length), absolute) 79 | else 80 | raise OriginError.new("#{self} does not end with #{origin.join('.')}") 81 | end 82 | end 83 | end 84 | end 85 | 86 | if RUBY_VERSION == "2.3.0" 87 | # Clearly, the Ruby 2.3.0 release was throughly tested. 88 | class IPv6 89 | def self.create(arg) 90 | case arg 91 | when IPv6 92 | return arg 93 | when String 94 | address = ''.b 95 | if Regex_8Hex =~ arg 96 | arg.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')} 97 | elsif Regex_CompressedHex =~ arg 98 | prefix = $1 99 | suffix = $2 100 | a1 = ''.b 101 | a2 = ''.b 102 | prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')} 103 | suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')} 104 | omitlen = 16 - a1.length - a2.length 105 | address << a1 << "\0" * omitlen << a2 106 | elsif Regex_6Hex4Dec =~ arg 107 | prefix, a, b, c, d = $1, $2.to_i, $3.to_i, $4.to_i, $5.to_i 108 | if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d 109 | prefix.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')} 110 | address << [a, b, c, d].pack('CCCC') 111 | else 112 | raise ArgumentError.new("not numeric IPv6 address: " + arg) 113 | end 114 | elsif Regex_CompressedHex4Dec =~ arg 115 | prefix, suffix, a, b, c, d = $1, $2, $3.to_i, $4.to_i, $5.to_i, $6.to_i 116 | if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d 117 | a1 = ''.b 118 | a2 = ''.b 119 | prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')} 120 | suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')} 121 | omitlen = 12 - a1.length - a2.length 122 | address << a1 << "\0" * omitlen << a2 << [a, b, c, d].pack('CCCC') 123 | else 124 | raise ArgumentError.new("not numeric IPv6 address: " + arg) 125 | end 126 | else 127 | raise ArgumentError.new("not numeric IPv6 address: " + arg) 128 | end 129 | return IPv6.new(address) 130 | else 131 | raise ArgumentError.new("cannot interpret as IPv6 address: #{arg.inspect}") 132 | end 133 | end 134 | end 135 | end 136 | end -------------------------------------------------------------------------------- /lib/celluloid/dns/extensions/string.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2009, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require_relative '../chunked' 22 | 23 | class String 24 | # Chunk a string which is required for the TEXT `resource_class`. 25 | def chunked(chunk_size = 255) 26 | Celluloid::DNS::chunked(self) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/celluloid/dns/handler.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2009, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require_relative 'transport' 22 | 23 | module Celluloid::DNS 24 | class GenericHandler 25 | include Celluloid::IO 26 | 27 | def initialize(server) 28 | @server = server 29 | @logger = @server.logger || Celluloid.logger 30 | 31 | @connections = Celluloid::Condition.new 32 | end 33 | 34 | def stop 35 | shutdown 36 | 37 | @connections.wait 38 | end 39 | 40 | finalizer def stop 41 | # Celluloid.logger.debug(self.class.name) {"-> Shutdown..."} 42 | 43 | @socket.close if @socket 44 | @socket = nil 45 | 46 | # Celluloid.logger.debug(self.class.name) {"<- Shutdown..."} 47 | end 48 | 49 | def error_response(query = nil, code = Resolv::DNS::RCode::ServFail) 50 | # Encoding may fail, so we need to handle this particular case: 51 | server_failure = Resolv::DNS::Message::new(query ? query.id : 0) 52 | 53 | server_failure.qr = 1 54 | server_failure.opcode = query ? query.opcode : 0 55 | server_failure.aa = 1 56 | server_failure.rd = 0 57 | server_failure.ra = 0 58 | 59 | server_failure.rcode = code 60 | 61 | # We can't do anything at this point... 62 | return server_failure 63 | end 64 | 65 | def process_query(data, options) 66 | @logger.debug "<> Receiving incoming query (#{data.bytesize} bytes) to #{self.class.name}..." 67 | query = nil 68 | 69 | begin 70 | query = Celluloid::DNS::decode_message(data) 71 | 72 | return @server.process_query(query, options) 73 | rescue StandardError => error 74 | @logger.error "<> Error processing request: #{error.inspect}!" 75 | Celluloid::DNS::log_exception(@logger, error) 76 | 77 | return error_response(query) 78 | end 79 | end 80 | end 81 | 82 | # Handling incoming UDP requests, which are single data packets, and pass them on to the given server. 83 | class UDPSocketHandler < GenericHandler 84 | def initialize(server, socket) 85 | super(server) 86 | 87 | @socket = socket 88 | 89 | async.run 90 | end 91 | 92 | def run 93 | Celluloid.logger.debug(self.class.name) {"-> Run..."} 94 | 95 | handle_connection while @socket 96 | 97 | Celluloid.logger.debug(self.class.name) {"<- Run..."} 98 | end 99 | 100 | def respond(input_data, remote_host, remote_port) 101 | options = {peer: remote_host, port: remote_port, proto: :udp} 102 | 103 | response = process_query(input_data, options) 104 | 105 | output_data = response.encode 106 | 107 | @logger.debug "<#{response.id}> Writing #{output_data.bytesize} bytes response to client via UDP..." 108 | 109 | if output_data.bytesize > UDP_TRUNCATION_SIZE 110 | @logger.warn "<#{response.id}>Response via UDP was larger than #{UDP_TRUNCATION_SIZE}!" 111 | 112 | # Reencode data with truncation flag marked as true: 113 | truncation_error = Resolv::DNS::Message.new(response.id) 114 | truncation_error.tc = 1 115 | 116 | output_data = truncation_error.encode 117 | end 118 | 119 | @socket.send(output_data, 0, remote_host, remote_port) 120 | rescue IOError => error 121 | @logger.warn "<> UDP response failed: #{error.inspect}!" 122 | rescue EOFError => error 123 | @logger.warn "<> UDP session ended prematurely: #{error.inspect}!" 124 | rescue DecodeError 125 | @logger.warn "<> Could not decode incoming UDP data!" 126 | end 127 | 128 | def handle_connection 129 | # @logger.debug "Waiting for incoming UDP packet #{@socket.inspect}..." 130 | 131 | input_data, (_, remote_port, remote_host) = @socket.recvfrom(UDP_TRUNCATION_SIZE) 132 | 133 | async.respond(input_data, remote_host, remote_port) 134 | rescue IOError => error 135 | @logger.warn "<> UDP connection failed: #{error.inspect}!" 136 | rescue EOFError => error 137 | @logger.warn "<> UDP session ended prematurely!" 138 | end 139 | end 140 | 141 | class UDPHandler < UDPSocketHandler 142 | def initialize(server, host, port) 143 | family = Celluloid::DNS::address_family(host) 144 | socket = UDPSocket.new(family) 145 | 146 | socket.bind(host, port) 147 | 148 | super(server, socket) 149 | end 150 | end 151 | 152 | class TCPSocketHandler < GenericHandler 153 | def initialize(server, socket) 154 | super(server) 155 | 156 | @socket = socket 157 | 158 | async.run 159 | end 160 | 161 | def run 162 | Celluloid.logger.debug(self.class.name) {"-> Run..."} 163 | 164 | async.handle_connection(@socket.accept) while @socket 165 | 166 | Celluloid.logger.debug(self.class.name) {"<- Run..."} 167 | end 168 | 169 | def handle_connection(socket) 170 | _, remote_port, remote_host = socket.peeraddr 171 | options = {peer: remote_host, port: remote_port, proto: :tcp} 172 | 173 | input_data = StreamTransport.read_chunk(socket) 174 | 175 | response = process_query(input_data, options) 176 | 177 | length = StreamTransport.write_message(socket, response) 178 | 179 | @logger.debug "<#{response.id}> Wrote #{length} bytes via TCP..." 180 | rescue EOFError => error 181 | @logger.warn "<> TCP session ended prematurely!" 182 | rescue Errno::ECONNRESET => error 183 | @logger.warn "<> TCP connection reset by peer!" 184 | rescue DecodeError 185 | @logger.warn "<> Could not decode incoming TCP data!" 186 | ensure 187 | socket.close 188 | end 189 | end 190 | 191 | class TCPHandler < TCPSocketHandler 192 | def initialize(server, host, port) 193 | socket = TCPServer.new(host, port) 194 | 195 | super(server, socket) 196 | end 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /lib/celluloid/dns/logger.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2009, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'logger' 22 | 23 | module Celluloid::DNS 24 | # Logs an exception nicely to a standard `Logger`. 25 | def self.log_exception(logger, exception) 26 | logger.error "#{exception.class}: #{exception.message}" 27 | if exception.backtrace 28 | Array(exception.backtrace).each { |at| logger.error at } 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/celluloid/dns/message.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2009, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'stringio' 22 | require 'resolv' 23 | 24 | require 'base64' 25 | 26 | require_relative 'logger' 27 | require_relative 'extensions/resolv' 28 | 29 | module Celluloid::DNS 30 | UDP_TRUNCATION_SIZE = 512 31 | 32 | # The DNS message container. 33 | Message = Resolv::DNS::Message 34 | 35 | DecodeError = Resolv::DNS::DecodeError 36 | 37 | @@dump_bad_message = nil 38 | 39 | # Call this function with a path where bad messages will be saved. Any message that causes an exception to be thrown while decoding the binary will be saved in base64 for later inspection. The log file could grow quickly so be careful - not designed for long term use. 40 | def self.log_bad_messages!(log_path) 41 | bad_messages_log = Logger.new(log_path, 10, 1024*100) 42 | bad_messages_log.level = Logger::DEBUG 43 | 44 | @dump_bad_message = lambda do |error, data| 45 | bad_messages_log.debug("Bad message: #{Base64.encode64(data)}") 46 | Celluloid::DNS.log_exception(bad_messages_log, error) 47 | end 48 | end 49 | 50 | # Decodes binary data into a {Message}. 51 | def self.decode_message(data) 52 | # Otherwise the decode process might fail with non-binary data. 53 | if data.respond_to? :force_encoding 54 | data.force_encoding("BINARY") 55 | end 56 | 57 | begin 58 | return Message.decode(data) 59 | rescue DecodeError 60 | raise 61 | rescue StandardError => error 62 | new_error = DecodeError.new(error.message) 63 | new_error.set_backtrace(error.backtrace) 64 | 65 | raise new_error 66 | end 67 | 68 | rescue => error 69 | # Log the bad messsage if required: 70 | if @dump_bad_message 71 | @dump_bad_message.call(error, data) 72 | end 73 | 74 | raise 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/celluloid/dns/replace.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2015, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'resolv' 22 | require 'resolv-replace' 23 | 24 | module Celluloid::DNS 25 | module Replace 26 | class << self 27 | attr :resolver, true 28 | 29 | def resolver? 30 | resolver != nil 31 | end 32 | 33 | def get_address(host) 34 | begin 35 | resolver.addresses_for(host).sample.to_s 36 | rescue ResolutionFailure 37 | raise SocketError, "Hostname not known: #{host}" 38 | end 39 | end 40 | end 41 | end 42 | 43 | class << IPSocket 44 | @@resolver = nil 45 | 46 | def getaddress(host) 47 | if Replace.resolver? 48 | Replace.get_address(host) 49 | else 50 | original_resolv_getaddress(host) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/celluloid/dns/resolver.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require_relative 'handler' 22 | 23 | require 'securerandom' 24 | require 'celluloid/io' 25 | 26 | module Celluloid::DNS 27 | class InvalidProtocolError < StandardError 28 | end 29 | 30 | class InvalidResponseError < StandardError 31 | end 32 | 33 | class ResolutionFailure < StandardError 34 | end 35 | 36 | class Resolver 37 | # Wait for up to 5 seconds for a response. Override with `options[:timeout]` 38 | DEFAULT_TIMEOUT = 5.0 39 | 40 | # 10ms wait between making requests. Override with `options[:delay]` 41 | DEFAULT_DELAY = 0.01 42 | 43 | # Try a given request 10 times before failing. Override with `options[:retries]`. 44 | DEFAULT_RETRIES = 10 45 | 46 | include Celluloid::IO 47 | 48 | # Servers are specified in the same manor as options[:listen], e.g. 49 | # [:tcp/:udp, address, port] 50 | # In the case of multiple servers, they will be checked in sequence. 51 | def initialize(servers, options = {}) 52 | @servers = servers 53 | 54 | @options = options 55 | 56 | @origin = options[:origin] || nil 57 | 58 | @logger = options[:logger] || Celluloid.logger 59 | end 60 | 61 | attr_accessor :origin 62 | 63 | def fully_qualified_name(name) 64 | # If we are passed an existing deconstructed name: 65 | if Resolv::DNS::Name === name 66 | if name.absolute? 67 | return name 68 | else 69 | return name.with_origin(@origin) 70 | end 71 | end 72 | 73 | # ..else if we have a string, we need to do some basic processing: 74 | if name.end_with? '.' 75 | return Resolv::DNS::Name.create(name) 76 | else 77 | return Resolv::DNS::Name.create(name).with_origin(@origin) 78 | end 79 | end 80 | 81 | # Provides the next sequence identification number which is used to keep track of DNS messages. 82 | def next_id! 83 | # 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. 84 | SecureRandom.random_number(2**16) 85 | end 86 | 87 | # Look up a named resource of the given resource_class. 88 | def query(name, resource_class = Resolv::DNS::Resource::IN::A) 89 | message = Resolv::DNS::Message.new(next_id!) 90 | message.rd = 1 91 | message.add_question fully_qualified_name(name), resource_class 92 | 93 | dispatch_request(message) 94 | end 95 | 96 | # Yields a list of `Resolv::IPv4` and `Resolv::IPv6` addresses for the given `name` and `resource_class`. Raises a ResolutionFailure if no severs respond. 97 | def addresses_for(name, resource_class = Resolv::DNS::Resource::IN::A, options = {}) 98 | name = fully_qualified_name(name) 99 | 100 | cache = options.fetch(:cache, {}) 101 | retries = options.fetch(:retries, DEFAULT_RETRIES) 102 | delay = options.fetch(:delay, DEFAULT_DELAY) 103 | 104 | records = lookup(name, resource_class, cache) do |name, resource_class| 105 | response = nil 106 | 107 | retries.times do |i| 108 | # Wait 10ms before trying again: 109 | sleep delay if delay and i > 0 110 | 111 | response = query(name, resource_class) 112 | 113 | break if response 114 | end 115 | 116 | response or abort ResolutionFailure.new("Could not resolve #{name} after #{retries} attempt(s).") 117 | end 118 | 119 | addresses = [] 120 | 121 | if records 122 | records.each do |record| 123 | if record.respond_to? :address 124 | addresses << record.address 125 | else 126 | # 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. 127 | addresses += addresses_for(record.name, record.class, options.merge(cache: cache)) 128 | end 129 | end 130 | end 131 | 132 | if addresses.size > 0 133 | return addresses 134 | else 135 | abort ResolutionFailure.new("Could not find any addresses for #{name}.") 136 | end 137 | end 138 | 139 | def request_timeout 140 | @options[:timeout] || DEFAULT_TIMEOUT 141 | end 142 | 143 | # 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. 144 | def dispatch_request(message) 145 | request = Request.new(message, @servers) 146 | 147 | request.each do |server| 148 | @logger.debug "[#{message.id}] Sending request #{message.question.inspect} to server #{server.inspect}" if @logger 149 | 150 | begin 151 | response = nil 152 | 153 | # This may be causing a problem, perhaps try: 154 | # after(timeout) { socket.close } 155 | # https://github.com/celluloid/celluloid-io/issues/121 156 | timeout(request_timeout) do 157 | response = try_server(request, server) 158 | end 159 | 160 | if valid_response(message, response) 161 | return response 162 | end 163 | rescue TaskTimeout 164 | @logger.debug "[#{message.id}] Request timed out!" if @logger 165 | rescue InvalidResponseError 166 | @logger.warn "[#{message.id}] Invalid response from network: #{$!}!" if @logger 167 | rescue DecodeError 168 | @logger.warn "[#{message.id}] Error while decoding data from network: #{$!}!" if @logger 169 | rescue IOError 170 | @logger.warn "[#{message.id}] Error while reading from network: #{$!}!" if @logger 171 | end 172 | end 173 | 174 | return nil 175 | end 176 | 177 | private 178 | 179 | # Lookup a name/resource_class record but use the records cache if possible reather than making a new request if possible. 180 | def lookup(name, resource_class = Resolv::DNS::Resource::IN::A, records = {}) 181 | records.fetch(name) do 182 | response = yield(name, resource_class) 183 | 184 | if response 185 | response.answer.each do |name, ttl, record| 186 | (records[name] ||= []) << record 187 | end 188 | end 189 | 190 | records[name] 191 | end 192 | end 193 | 194 | def try_server(request, server) 195 | case server[0] 196 | when :udp 197 | try_udp_server(request, server[1], server[2]) 198 | when :tcp 199 | try_tcp_server(request, server[1], server[2]) 200 | else 201 | raise InvalidProtocolError.new(server) 202 | end 203 | end 204 | 205 | def valid_response(message, response) 206 | if response.tc != 0 207 | @logger.warn "[#{message.id}] Received truncated response!" if @logger 208 | elsif response.id != message.id 209 | @logger.warn "[#{message.id}] Received response with incorrect message id: #{response.id}!" if @logger 210 | else 211 | @logger.debug "[#{message.id}] Received valid response with #{response.answer.count} answer(s)." if @logger 212 | 213 | return true 214 | end 215 | 216 | return false 217 | end 218 | 219 | def try_udp_server(request, host, port) 220 | family = Celluloid::DNS::address_family(host) 221 | socket = UDPSocket.new(family) 222 | 223 | socket.send(request.packet, 0, host, port) 224 | 225 | data, (_, remote_port) = socket.recvfrom(UDP_TRUNCATION_SIZE) 226 | # Need to check host, otherwise security issue. 227 | 228 | # May indicate some kind of spoofing attack: 229 | if port != remote_port 230 | raise InvalidResponseError.new("Data was not received from correct remote port (#{port} != #{remote_port})") 231 | end 232 | 233 | message = Celluloid::DNS::decode_message(data) 234 | ensure 235 | socket.close if socket 236 | end 237 | 238 | def try_tcp_server(request, host, port) 239 | socket = TCPSocket.new(host, port) 240 | 241 | StreamTransport.write_chunk(socket, request.packet) 242 | 243 | input_data = StreamTransport.read_chunk(socket) 244 | 245 | message = Celluloid::DNS::decode_message(input_data) 246 | rescue Errno::ECONNREFUSED => error 247 | raise IOError.new(error.message) 248 | rescue Errno::EPIPE => error 249 | raise IOError.new(error.message) 250 | rescue Errno::ECONNRESET => error 251 | raise IOError.new(error.message) 252 | ensure 253 | socket.close if socket 254 | end 255 | 256 | # Manages a single DNS question message across one or more servers. 257 | class Request 258 | def initialize(message, servers) 259 | @message = message 260 | @packet = message.encode 261 | 262 | @servers = servers.dup 263 | 264 | # We select the protocol based on the size of the data: 265 | if @packet.bytesize > UDP_TRUNCATION_SIZE 266 | @servers.delete_if{|server| server[0] == :udp} 267 | end 268 | end 269 | 270 | attr :message 271 | attr :packet 272 | attr :logger 273 | 274 | def each(&block) 275 | @servers.each do |server| 276 | next if @packet.bytesize > UDP_TRUNCATION_SIZE 277 | 278 | yield server 279 | end 280 | end 281 | 282 | def update_id!(id) 283 | @message.id = id 284 | @packet = @message.encode 285 | end 286 | end 287 | end 288 | end 289 | -------------------------------------------------------------------------------- /lib/celluloid/dns/server.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2009, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'celluloid/io' 22 | 23 | require_relative 'transaction' 24 | require_relative 'logger' 25 | 26 | module Celluloid::DNS 27 | class Server 28 | include Celluloid::IO 29 | 30 | # The default server interfaces 31 | DEFAULT_INTERFACES = [[:udp, "0.0.0.0", 53], [:tcp, "0.0.0.0", 53]] 32 | 33 | # Instantiate a server with a block 34 | # 35 | # server = Server.new do 36 | # match(/server.mydomain.com/, IN::A) do |transaction| 37 | # transaction.respond!("1.2.3.4") 38 | # end 39 | # end 40 | # 41 | def initialize(options = {}) 42 | @logger = options[:logger] || Celluloid.logger 43 | @interfaces = options[:listen] || DEFAULT_INTERFACES 44 | 45 | @origin = options[:origin] || '.' 46 | end 47 | 48 | # Records are relative to this origin: 49 | attr_accessor :origin 50 | 51 | attr_accessor :logger 52 | 53 | # Fire the named event as part of running the server. 54 | def fire(event_name) 55 | end 56 | 57 | finalizer def stop 58 | # Celluloid.logger.debug(self.class.name) {"-> Shutdown..."} 59 | 60 | fire(:stop) 61 | 62 | # Celluloid.logger.debug(self.class.name) {"<- Shutdown..."} 63 | end 64 | 65 | # Give a name and a record type, try to match a rule and use it for processing the given arguments. 66 | def process(name, resource_class, transaction) 67 | raise NotImplementedError.new 68 | end 69 | 70 | # Process an incoming DNS message. Returns a serialized message to be sent back to the client. 71 | def process_query(query, options = {}, &block) 72 | start_time = Time.now 73 | 74 | # Setup response 75 | response = Resolv::DNS::Message::new(query.id) 76 | response.qr = 1 # 0 = Query, 1 = Response 77 | response.opcode = query.opcode # Type of Query; copy from query 78 | response.aa = 1 # Is this an authoritative response: 0 = No, 1 = Yes 79 | response.rd = query.rd # Is Recursion Desired, copied from query 80 | response.ra = 0 # Does name server support recursion: 0 = No, 1 = Yes 81 | response.rcode = 0 # Response code: 0 = No errors 82 | 83 | transaction = nil 84 | 85 | begin 86 | query.question.each do |question, resource_class| 87 | begin 88 | question = question.without_origin(@origin) 89 | 90 | @logger.debug {"<#{query.id}> Processing question #{question} #{resource_class}..."} 91 | 92 | transaction = Transaction.new(self, query, question, resource_class, response, options) 93 | 94 | transaction.process 95 | rescue Resolv::DNS::OriginError 96 | # This is triggered if the question is not part of the specified @origin: 97 | @logger.debug {"<#{query.id}> Skipping question #{question} #{resource_class} because #{$!}"} 98 | end 99 | end 100 | rescue StandardError => error 101 | @logger.error "<#{query.id}> Exception thrown while processing #{transaction}!" 102 | Celluloid::DNS.log_exception(@logger, error) 103 | 104 | response.rcode = Resolv::DNS::RCode::ServFail 105 | end 106 | 107 | end_time = Time.now 108 | @logger.debug {"<#{query.id}> Time to process request: #{end_time - start_time}s"} 109 | 110 | return response 111 | end 112 | 113 | # Setup all specified interfaces and begin accepting incoming connections. 114 | def run 115 | @logger.info "Starting Celluloid::DNS server (v#{Celluloid::DNS::VERSION})..." 116 | 117 | fire(:setup) 118 | 119 | # Setup server sockets 120 | @interfaces.each do |spec| 121 | if spec.is_a?(BasicSocket) 122 | spec.do_not_reverse_lookup 123 | protocol = spec.getsockopt(Socket::SOL_SOCKET, Socket::SO_TYPE).unpack("i")[0] 124 | ip = spec.local_address.ip_address 125 | port = spec.local_address.ip_port 126 | 127 | case protocol 128 | when Socket::SOCK_DGRAM 129 | @logger.info "<> Attaching to pre-existing UDP socket #{ip}:#{port}" 130 | link UDPSocketHandler.new(self, Celluloid::IO::Socket.try_convert(spec)) 131 | when Socket::SOCK_STREAM 132 | @logger.info "<> Attaching to pre-existing TCP socket #{ip}:#{port}" 133 | link TCPSocketHandler.new(self, Celluloid::IO::Socket.try_convert(spec)) 134 | else 135 | raise ArgumentError.new("Unknown socket protocol: #{protocol}") 136 | end 137 | elsif spec[0] == :udp 138 | @logger.info "<> Listening on #{spec.join(':')}" 139 | link UDPHandler.new(self, spec[1], spec[2]) 140 | elsif spec[0] == :tcp 141 | @logger.info "<> Listening on #{spec.join(':')}" 142 | link TCPHandler.new(self, spec[1], spec[2]) 143 | else 144 | raise ArgumentError.new("Invalid connection specification: #{spec.inspect}") 145 | end 146 | end 147 | 148 | fire(:start) 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /lib/celluloid/dns/system.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2009, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | begin 22 | require 'win32/resolv' 23 | rescue LoadError 24 | # Ignore this - we aren't running on windows. 25 | end 26 | 27 | module Celluloid::DNS 28 | # This module encapsulates system dependent name lookup functionality. 29 | module System 30 | RESOLV_CONF = "/etc/resolv.conf" 31 | HOSTS = "/etc/hosts" 32 | 33 | def self.hosts_path 34 | if RUBY_PLATFORM =~ /mswin32|mingw|bccwin/ 35 | Win32::Resolv.get_hosts_path 36 | else 37 | HOSTS 38 | end 39 | end 40 | 41 | # This code is very experimental 42 | class Hosts 43 | def initialize 44 | @addresses = {} 45 | @names = {} 46 | end 47 | 48 | attr :addresses 49 | attr :names 50 | 51 | # This is used to match names against the list of known hosts: 52 | def call(name) 53 | @names.include?(name) 54 | end 55 | 56 | def lookup(name) 57 | addresses = @names[name] 58 | 59 | if addresses 60 | addresses.last 61 | else 62 | nil 63 | end 64 | end 65 | 66 | alias [] lookup 67 | 68 | def add(address, names) 69 | @addresses[address] ||= [] 70 | @addresses[address] += names 71 | 72 | names.each do |name| 73 | @names[name] ||= [] 74 | @names[name] << address 75 | end 76 | end 77 | 78 | def parse_hosts(io) 79 | io.each do |line| 80 | line.sub!(/#.*/, '') 81 | address, hostname, *aliases = line.split(/\s+/) 82 | 83 | add(address, [hostname] + aliases) 84 | end 85 | end 86 | 87 | def self.local 88 | hosts = self.new 89 | 90 | path = System::hosts_path 91 | 92 | if path and File.exist?(path) 93 | File.open(path) do |file| 94 | hosts.parse_hosts(file) 95 | end 96 | end 97 | 98 | return hosts 99 | end 100 | end 101 | 102 | def self.parse_resolv_configuration(path) 103 | nameservers = [] 104 | File.open(path) do |file| 105 | file.each do |line| 106 | # Remove any comments: 107 | line.sub!(/[#;].*/, '') 108 | 109 | # Extract resolv.conf command: 110 | keyword, *args = line.split(/\s+/) 111 | 112 | case keyword 113 | when 'nameserver' 114 | nameservers += args 115 | end 116 | end 117 | end 118 | 119 | return nameservers 120 | end 121 | 122 | def self.standard_connections(nameservers) 123 | connections = [] 124 | 125 | nameservers.each do |host| 126 | connections << [:udp, host, 53] 127 | connections << [:tcp, host, 53] 128 | end 129 | 130 | return connections 131 | end 132 | 133 | # Get a list of standard nameserver connections which can be used for querying any standard servers that the system has been configured with. There is no equivalent facility to use the `hosts` file at present. 134 | def self.nameservers 135 | nameservers = [] 136 | 137 | if File.exist? RESOLV_CONF 138 | nameservers = parse_resolv_configuration(RESOLV_CONF) 139 | elsif defined?(Win32::Resolv) and RUBY_PLATFORM =~ /mswin32|cygwin|mingw|bccwin/ 140 | search, nameservers = Win32::Resolv.get_resolv_info 141 | end 142 | 143 | return standard_connections(nameservers) 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /lib/celluloid/dns/transaction.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2009, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | module Celluloid::DNS 22 | 23 | # This class provides all details of a single DNS question and response. This is used by the DSL to provide DNS related functionality. 24 | # 25 | # The main functions to complete the transaction are: {#append!} (evaluate a new query and append the results), {#passthrough!} (pass the query to an upstream server), {#respond!} (compute a specific response) and {#fail!} (fail with an error code). 26 | class Transaction 27 | # The default time used for responses (24 hours). 28 | DEFAULT_TTL = 86400 29 | 30 | def initialize(server, query, question, resource_class, response, options = {}) 31 | @server = server 32 | @query = query 33 | @question = question 34 | @resource_class = resource_class 35 | @response = response 36 | 37 | @options = options 38 | end 39 | 40 | # The resource_class that was requested. This is typically used to generate a response. 41 | attr :resource_class 42 | 43 | # The incoming query which is a set of questions. 44 | attr :query 45 | 46 | # The question that this transaction represents. 47 | attr :question 48 | 49 | # The current full response to the incoming query. 50 | attr :response 51 | 52 | # Any options or configuration associated with the given transaction. 53 | attr :options 54 | 55 | def [] key 56 | @options[key] 57 | end 58 | 59 | # The name of the question, which is typically the requested hostname. 60 | def name 61 | @question.to_s 62 | end 63 | 64 | # Shows the question name and resource class. Suitable for debugging purposes. 65 | def to_s 66 | "#{name} #{@resource_class.name}" 67 | end 68 | 69 | # Run a new query through the rules with the given name and resource type. The results of this query are appended to the current transaction's `response`. 70 | def append!(name, resource_class = nil, options = {}) 71 | Transaction.new(@server, @query, name, resource_class || @resource_class, @response, options).process 72 | end 73 | 74 | # Use the given resolver to respond to the question. Uses `passthrough` to do the lookup and merges the result. 75 | # 76 | # 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. 77 | # 78 | # If recursion is not requested, the result is `fail!(:Refused)`. This check is ignored if an explicit `options[:name]` or `options[:force]` is given. 79 | # 80 | # If the resolver can't reach upstream servers, `fail!(:ServFail)` is invoked. 81 | def passthrough!(resolver, options = {}, &block) 82 | if @query.rd || options[:force] || options[:name] 83 | response = passthrough(resolver, options) 84 | 85 | if response 86 | yield response if block_given? 87 | 88 | # Recursion is available and is being used: 89 | # See issue #26 for more details. 90 | @response.ra = 1 91 | @response.merge!(response) 92 | else 93 | fail!(:ServFail) 94 | end 95 | else 96 | fail!(:Refused) 97 | end 98 | end 99 | 100 | # Use the given resolver to respond to the question. 101 | # 102 | # 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. 103 | # 104 | # If `options[:name]` is provided, this overrides the default query name sent to the upstream server. The same logic applies to `options[:resource_class]`. 105 | def passthrough(resolver, options = {}) 106 | query_name = options[:name] || name 107 | query_resource_class = options[:resource_class] || resource_class 108 | 109 | resolver.query(query_name, query_resource_class) 110 | end 111 | 112 | # 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!}. 113 | # 114 | # e.g. For A records: `respond!("1.2.3.4")`, For MX records: `respond!(10, Name.create("mail.blah.com"))` 115 | 116 | # The last argument can optionally be a hash of `options`. If `options[:resource_class]` is provided, it overrides the default resource class of transaction. Additional `options` are passed to {#append!}. 117 | # 118 | # See `Resolv::DNS::Resource` for more information about the various `resource_classes` available (http://www.ruby-doc.org/stdlib/libdoc/resolv/rdoc/index.html). 119 | def respond!(*args) 120 | append_question! 121 | 122 | options = args.last.kind_of?(Hash) ? args.pop : {} 123 | resource_class = options[:resource_class] || @resource_class 124 | 125 | if resource_class == nil 126 | raise ArgumentError.new("Could not instantiate resource #{resource_class}!") 127 | end 128 | 129 | resource = resource_class.new(*args) 130 | 131 | add([resource], options) 132 | end 133 | 134 | # Append a list of resources. 135 | # 136 | # By default resources are appended to the `answers` section, but this can be changed by setting `options[:section]` to either `:authority` or `:additional`. 137 | # 138 | # The time-to-live (TTL) of the resources can be specified using `options[:ttl]` and defaults to `DEFAULT_TTL`. 139 | def add(resources, options = {}) 140 | # Use the default options if provided: 141 | options = options.merge(@options) 142 | 143 | ttl = options[:ttl] || DEFAULT_TTL 144 | name = options[:name] || @question.to_s + "." 145 | 146 | section = (options[:section] || 'answer').to_sym 147 | method = "add_#{section}".to_sym 148 | 149 | resources.each do |resource| 150 | @server.logger.debug "#{method}: #{resource.inspect} #{resource.class::TypeValue} #{resource.class::ClassValue}" 151 | 152 | @response.send(method, name, ttl, resource) 153 | end 154 | end 155 | 156 | # This function indicates that there was a failure to resolve the given question. The single argument must be an integer error code, typically given by the constants in {Resolv::DNS::RCode}. 157 | # 158 | # The easiest way to use this function it to simply supply a symbol. Here is a list of the most commonly used ones: 159 | # 160 | # - `:NoError`: No error occurred. 161 | # - `:FormErr`: The incoming data was not formatted correctly. 162 | # - `:ServFail`: The operation caused a server failure (internal error, etc). 163 | # - `:NXDomain`: Non-eXistant Domain (domain record does not exist). 164 | # - `:NotImp`: The operation requested is not implemented. 165 | # - `:Refused`: The operation was refused by the server. 166 | # - `:NotAuth`: The server is not authoritive for the zone. 167 | # 168 | # See [RFC2929](http://www.rfc-editor.org/rfc/rfc2929.txt) for more information about DNS error codes (specifically, page 3). 169 | # 170 | # **This function will complete deferred transactions.** 171 | def fail!(rcode) 172 | append_question! 173 | 174 | if rcode.kind_of? Symbol 175 | @response.rcode = Resolv::DNS::RCode.const_get(rcode) 176 | else 177 | @response.rcode = rcode.to_i 178 | end 179 | end 180 | 181 | # @deprecated 182 | def failure!(*args) 183 | @server.logger.warn "failure! is deprecated, use fail! instead" 184 | 185 | fail!(*args) 186 | end 187 | 188 | # A helper method to process the transaction on the given server. Unless the transaction is deferred, it will {#succeed} on completion. 189 | def process 190 | @server.process(name, @resource_class, self) 191 | end 192 | 193 | protected 194 | 195 | # A typical response to a DNS request includes both the question and response. This helper appends the question unless it looks like the user is already managing that aspect of the response. 196 | def append_question! 197 | if @response.question.size == 0 198 | @response.add_question(@question, @resource_class) 199 | end 200 | end 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /lib/celluloid/dns/transport.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2009, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'stringio' 22 | require 'ipaddr' 23 | 24 | require_relative 'message' 25 | 26 | module Celluloid::DNS 27 | def self.address_family(host) 28 | return IPAddr.new(host).family 29 | end 30 | 31 | # A helper class for processing incoming network data. 32 | class BinaryStringIO < StringIO 33 | def initialize 34 | super 35 | 36 | set_encoding("BINARY") 37 | end 38 | end 39 | 40 | module StreamTransport 41 | def self.read_chunk(socket) 42 | # The data buffer: 43 | buffer = BinaryStringIO.new 44 | 45 | # First we need to read in the length of the packet 46 | while buffer.size < 2 47 | buffer.write socket.readpartial(1) 48 | end 49 | 50 | # Read in the length, the first two bytes: 51 | length = buffer.string.byteslice(0, 2).unpack('n')[0] 52 | 53 | # Read data until we have the amount specified: 54 | while (buffer.size - 2) < length 55 | required = (2 + length) - buffer.size 56 | 57 | # Read precisely the required amount: 58 | buffer.write socket.readpartial(required) 59 | end 60 | 61 | return buffer.string.byteslice(2, length) 62 | end 63 | 64 | def self.write_message(socket, message) 65 | write_chunk(socket, message.encode) 66 | end 67 | 68 | def self.write_chunk(socket, output_data) 69 | # TODO: We need to check that the data was written completely and didn't fail! 70 | socket.write([output_data.bytesize].pack('n')) 71 | socket.write(output_data) 72 | 73 | return output_data.bytesize 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/celluloid/dns/version.rb: -------------------------------------------------------------------------------- 1 | # Copyright, 2009, 2012, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | module Celluloid 22 | module DNS 23 | VERSION = '0.17.3' 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celluloid/celluloid-dns/9ca57bb1c8dcbb041587784b5b1d947171aa6136/logo.png -------------------------------------------------------------------------------- /spec/celluloid/dns/celluloid_bug_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'celluloid/dns' 24 | 25 | module Celluloid::DNS::CelluloidBugSpec 26 | describe Celluloid::DNS::Resolver do 27 | context 'benchmark' do 28 | domains = %W( 29 | Facebook.com 30 | Twitter.com 31 | Google.com 32 | Youtube.com 33 | Wordpress.org 34 | Adobe.com 35 | Blogspot.com 36 | Wikipedia.org 37 | Linkedin.com 38 | Wordpress.com 39 | Yahoo.com 40 | Amazon.com 41 | Flickr.com 42 | Pinterest.com 43 | Tumblr.com 44 | W3.org 45 | Apple.com 46 | Myspace.com 47 | Vimeo.com 48 | Microsoft.com 49 | Youtu.be 50 | Qq.com 51 | Digg.com 52 | Baidu.com 53 | Stumbleupon.com 54 | Addthis.com 55 | Statcounter.com 56 | Feedburner.com 57 | TradeMe.co.nz 58 | Nytimes.com 59 | Reddit.com 60 | Weebly.com 61 | Bbc.co.uk 62 | Blogger.com 63 | Msn.com 64 | Macromedia.com 65 | Goo.gl 66 | Instagram.com 67 | Gov.uk 68 | Icio.us 69 | Yandex.ru 70 | Cnn.com 71 | Webs.com 72 | Google.de 73 | T.co 74 | Livejournal.com 75 | Imdb.com 76 | Mail.ru 77 | Jimdo.com 78 | ) 79 | 80 | it 'should resolve domains using futures' do 81 | # Celluloid.logger.level = Logger::ERROR 82 | 83 | resolver = Celluloid::DNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]], timeout: 1.0) 84 | 85 | futures = domains.map { |domain| resolver.future.addresses_for(domain) } 86 | 87 | futures.map { |future| future.value } 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/celluloid/dns/hosts.txt: -------------------------------------------------------------------------------- 1 | # A testing host: 2 | 1.2.3.4 testing apples -------------------------------------------------------------------------------- /spec/celluloid/dns/ipv6_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright, 2014, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'celluloid/dns' 24 | require 'celluloid/dns/system' 25 | 26 | module Celluloid::DNS::IPv6Spec 27 | IN = Resolv::DNS::Resource::IN 28 | 29 | class TestServer < Celluloid::DNS::Server 30 | def process(name, resource_class, transaction) 31 | @resolver ||= Celluloid::DNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]]) 32 | 33 | transaction.passthrough!(@resolver) 34 | end 35 | end 36 | 37 | describe Celluloid::DNS::TCPSocketHandler do 38 | before(:all) do 39 | @server = TestServer.new(listen: [[:tcp, '::', 2004]]) 40 | @server.run 41 | end 42 | 43 | after(:all) do 44 | @server.terminate 45 | end 46 | 47 | it "should connect to the server using TCP via IPv6" do 48 | resolver = Celluloid::DNS::Resolver.new([[:tcp, '::1', 2004]]) 49 | response = resolver.query('google.com') 50 | expect(response.class).to be == Celluloid::DNS::Message 51 | end 52 | end 53 | 54 | describe Celluloid::DNS::UDPSocketHandler do 55 | before(:all) do 56 | @server = TestServer.new(listen: [[:udp, '::', 2006]]) 57 | @server.run 58 | end 59 | 60 | after(:all) do 61 | @server.terminate 62 | end 63 | 64 | it "should connect to the server using UDP via IPv6" do 65 | resolver = Celluloid::DNS::Resolver.new([[:udp, '::1', 2006]]) 66 | response = resolver.query('google.com') 67 | expect(response.class).to be == Celluloid::DNS::Message 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/celluloid/dns/message_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'celluloid/dns' 24 | require 'base64' 25 | 26 | module Celluloid::DNS::MessageSpec 27 | describe Celluloid::DNS::Message do 28 | it "should be decoded correctly" do 29 | data = Base64.decode64(<<-EOF) 30 | HQCBgAABAAgAAAABA3d3dwV5YWhvbwNjb20AAAEAAcAMAAUAAQAAASwADwZm 31 | ZC1mcDMDd2cxAWLAEMArAAUAAQAAASwACQZkcy1mcDPAMsBGAAUAAQAAADwA 32 | FQ5kcy1hbnktZnAzLWxmYgN3YTHANsBbAAUAAQAAASwAEg9kcy1hbnktZnAz 33 | LXJlYWzAasB8AAEAAQAAADwABGKK/B7AfAABAAEAAAA8AARii7SVwHwAAQAB 34 | AAAAPAAEYou3GMB8AAEAAQAAADwABGKK/W0AACkQAAAAAAAAAA== 35 | EOF 36 | 37 | message = Celluloid::DNS::decode_message(data) 38 | expect(message.class).to be == Celluloid::DNS::Message 39 | expect(message.id).to be == 0x1d00 40 | 41 | expect(message.question.count).to be == 1 42 | expect(message.answer.count).to be == 8 43 | expect(message.authority.count).to be == 0 44 | expect(message.additional.count).to be == 1 45 | end 46 | 47 | it "should fail to decode due to bad AAAA length" do 48 | data = Base64.decode64(<<-EOF) 49 | 6p6BgAABAAEAAAABCGJhaWNhaWNuA2NvbQAAHAABwAwAHAABAAABHgAEMhd7 50 | dwAAKRAAAAAAAAAA 51 | EOF 52 | 53 | expect{Celluloid::DNS::decode_message(data)}.to raise_error(Celluloid::DNS::DecodeError) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/celluloid/dns/origin_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rspec 2 | 3 | # Copyright, 2014, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'celluloid/dns' 24 | 25 | module Celluloid::DNS::OriginSpec 26 | describe Celluloid::DNS::Resolver do 27 | it "should generate fully qualified domain name with specified origin" do 28 | resolver = Celluloid::DNS::Resolver.new([], origin: "foo.bar.") 29 | 30 | fully_qualified_name = resolver.fully_qualified_name("baz") 31 | 32 | expect(fully_qualified_name).to be_absolute 33 | expect(fully_qualified_name.to_s).to be == "baz.foo.bar." 34 | end 35 | end 36 | 37 | describe Resolv::DNS::Name do 38 | let(:name) {Resolv::DNS::Name.create("foo.bar")} 39 | 40 | it "should be relative" do 41 | expect(name).to_not be_absolute 42 | expect(name.to_s).to be == "foo.bar" 43 | end 44 | 45 | it "should add the specified origin" do 46 | fully_qualified_name = name.with_origin("org") 47 | 48 | expect(fully_qualified_name.to_a.size).to be 3 49 | expect(fully_qualified_name).to be_absolute 50 | expect(fully_qualified_name.to_s).to be == "foo.bar.org." 51 | end 52 | 53 | it "should handle nil origin as absolute" do 54 | fully_qualified_name = name.with_origin(nil) 55 | 56 | expect(fully_qualified_name.to_a.size).to be 2 57 | expect(fully_qualified_name).to be_absolute 58 | expect(fully_qualified_name.to_s).to be == "foo.bar." 59 | end 60 | 61 | it "should handle empty origin as absolute" do 62 | fully_qualified_name = name.with_origin('') 63 | 64 | expect(fully_qualified_name.to_a.size).to be 2 65 | expect(fully_qualified_name).to be_absolute 66 | expect(fully_qualified_name.to_s).to be == "foo.bar." 67 | end 68 | end 69 | 70 | describe Resolv::DNS::Name do 71 | let(:name) {Resolv::DNS::Name.create("foo.bar.")} 72 | 73 | it "should be absolute" do 74 | expect(name).to be_absolute 75 | expect(name.to_s).to be == "foo.bar." 76 | end 77 | 78 | it "should remove the specified origin" do 79 | relative_name = name.without_origin("bar") 80 | 81 | expect(relative_name.to_a.size).to be 1 82 | expect(relative_name).to_not be_absolute 83 | expect(relative_name.to_s).to be == "foo" 84 | end 85 | 86 | it "should not remove nil origin but become relative" do 87 | relative_name = name.without_origin(nil) 88 | 89 | expect(relative_name.to_a.size).to be 2 90 | expect(relative_name).to_not be_absolute 91 | expect(relative_name.to_s).to be == "foo.bar" 92 | end 93 | 94 | it "should not remove empty string origin but become relative" do 95 | relative_name = name.without_origin('') 96 | 97 | expect(relative_name.to_a.size).to be 2 98 | expect(relative_name).to_not be_absolute 99 | expect(relative_name.to_s).to be == "foo.bar" 100 | end 101 | 102 | it "should not raise an exception when origin isn't valid" do 103 | expect{name.without_origin('bob')}.to raise_exception(Resolv::DNS::OriginError) 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/celluloid/dns/replace_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rspec 2 | 3 | # Copyright, 2015, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'celluloid/dns' 24 | require 'celluloid/dns/replace' 25 | 26 | module Celluloid::DNS::ReplaceSpec 27 | describe Celluloid::DNS::Replace do 28 | let(:default_resolver) {Celluloid::DNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]])} 29 | 30 | after(:all) do 31 | Celluloid::DNS::Replace.resolver = nil 32 | end 33 | 34 | it "should replace TCPSocket hostname lookup" do 35 | Celluloid::DNS::Replace.resolver = default_resolver 36 | 37 | expect(default_resolver).to receive(:addresses_for).with('www.google.com').and_call_original 38 | 39 | TCPSocket.new('www.google.com', 80) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/celluloid/dns/resolver_performance_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'celluloid/dns' 24 | 25 | module Celluloid::DNS::ResolverPerformanceSpec 26 | describe Celluloid::DNS::Resolver do 27 | context 'benchmark' do 28 | domains = %W{ 29 | Facebook.com 30 | Twitter.com 31 | Google.com 32 | Youtube.com 33 | Wordpress.org 34 | Adobe.com 35 | Blogspot.com 36 | Wikipedia.org 37 | Linkedin.com 38 | Wordpress.com 39 | Yahoo.com 40 | Amazon.com 41 | Flickr.com 42 | Pinterest.com 43 | Tumblr.com 44 | W3.org 45 | Apple.com 46 | Myspace.com 47 | Vimeo.com 48 | Microsoft.com 49 | Youtu.be 50 | Qq.com 51 | Digg.com 52 | Baidu.com 53 | Stumbleupon.com 54 | Addthis.com 55 | Statcounter.com 56 | Feedburner.com 57 | TradeMe.co.nz 58 | Nytimes.com 59 | Reddit.com 60 | Weebly.com 61 | Bbc.co.uk 62 | Blogger.com 63 | Msn.com 64 | Macromedia.com 65 | Goo.gl 66 | Instagram.com 67 | Gov.uk 68 | Icio.us 69 | Yandex.ru 70 | Cnn.com 71 | Webs.com 72 | Google.de 73 | T.co 74 | Livejournal.com 75 | Imdb.com 76 | Mail.ru 77 | Jimdo.com 78 | } 79 | 80 | before do 81 | require 'benchmark' 82 | end 83 | 84 | it 'should be faster than native resolver' do 85 | # Celluloid.logger.level = Logger::ERROR 86 | 87 | Benchmark.bm(30) do |x| 88 | a = x.report("Celluloid::DNS::Resolver") do 89 | resolver = Celluloid::DNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]]) 90 | 91 | futures = domains.collect{|domain| resolver.future.addresses_for(domain)} 92 | 93 | futures.collect{|future| future.value} 94 | end 95 | 96 | b = x.report("Resolv::DNS") do 97 | resolver = Resolv::DNS.new(:nameserver => "8.8.8.8") 98 | 99 | resolved = domains.collect do |domain| 100 | [domain, resolver.getaddresses(domain)] 101 | end 102 | end 103 | 104 | expect(a.real).to be < b.real 105 | end 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/celluloid/dns/resolver_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rspec 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'celluloid/dns' 24 | 25 | module Celluloid::DNS::ResolverSpec 26 | describe Celluloid::DNS::Resolver do 27 | class JunkUDPServer 28 | include Celluloid::IO 29 | 30 | def initialize 31 | @socket = UDPSocket.new 32 | @socket.bind("0.0.0.0", 6060) 33 | 34 | async.run 35 | end 36 | 37 | finalizer :shutdown 38 | 39 | def finalize 40 | @socket.close if @socket 41 | end 42 | 43 | def run 44 | data, (_, port, host) = @socket.recvfrom(1024) 45 | 46 | @socket.send("Foobar", 0, host, port) 47 | end 48 | end 49 | 50 | class JunkTCPServer 51 | include Celluloid::IO 52 | 53 | def initialize 54 | @socket = TCPServer.new("0.0.0.0", 6060) 55 | 56 | async.run 57 | end 58 | 59 | finalizer :shutdown 60 | 61 | def finalize 62 | @socket.close if @socket 63 | end 64 | 65 | def run 66 | # @logger.debug "Waiting for incoming TCP connections #{@socket.inspect}..." 67 | loop { async.handle_connection @socket.accept } 68 | end 69 | 70 | def handle_connection(socket) 71 | socket.write("\0\0obar") 72 | ensure 73 | socket.close 74 | end 75 | end 76 | 77 | before(:all) do 78 | JunkUDPServer.supervise 79 | JunkTCPServer.supervise 80 | end 81 | 82 | it "should result in non-existent domain" do 83 | resolver = Celluloid::DNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]]) 84 | 85 | response = resolver.query('foobar.oriontransfer.org') 86 | 87 | expect(response.rcode).to be == Resolv::DNS::RCode::NXDomain 88 | end 89 | 90 | it "should result in some answers" do 91 | resolver = Celluloid::DNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]]) 92 | 93 | response = resolver.query('google.com') 94 | 95 | expect(response.class).to be == Celluloid::DNS::Message 96 | expect(response.answer.size).to be > 0 97 | end 98 | 99 | it "should return no results" do 100 | resolver = Celluloid::DNS::Resolver.new([]) 101 | 102 | response = resolver.query('google.com') 103 | 104 | expect(response).to be == nil 105 | end 106 | 107 | it "should fail to get addresses" do 108 | resolver = Celluloid::DNS::Resolver.new([]) 109 | 110 | expect{resolver.addresses_for('google.com')}.to raise_error(Celluloid::DNS::ResolutionFailure) 111 | end 112 | 113 | it "should fail with decode error from bad udp server" do 114 | resolver = Celluloid::DNS::Resolver.new([[:udp, "0.0.0.0", 6060]]) 115 | 116 | response = resolver.query('google.com') 117 | 118 | expect(response).to be == nil 119 | end 120 | 121 | it "should fail with decode error from bad tcp server" do 122 | resolver = Celluloid::DNS::Resolver.new([[:tcp, "0.0.0.0", 6060]]) 123 | 124 | response = resolver.query('google.com') 125 | 126 | expect(response).to be == nil 127 | end 128 | 129 | it "should return some IPv4 and IPv6 addresses" do 130 | resolver = Celluloid::DNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]]) 131 | 132 | addresses = resolver.addresses_for("www.google.com.") 133 | 134 | expect(addresses.size).to be > 0 135 | 136 | addresses.each do |address| 137 | expect(address).to be_kind_of(Resolv::IPv4) | be_kind_of(Resolv::IPv6) 138 | end 139 | end 140 | 141 | it "should recursively resolve CNAME records" do 142 | resolver = Celluloid::DNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]]) 143 | 144 | addresses = resolver.addresses_for('www.baidu.com') 145 | 146 | expect(addresses.size).to be > 0 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /spec/celluloid/dns/server/bind9/generate-local.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | File.open("local.zone", "w") do |f| 4 | f.write(DATA.read) 5 | 6 | (1..5_000).each do |i| 7 | f.puts "domain#{i} IN A #{69}.#{(i >> 16)%256}.#{(i >> 8)%256}.#{i%256}" 8 | end 9 | end 10 | 11 | __END__ 12 | ; 13 | ; BIND data file for local loopback interface 14 | ; 15 | $TTL 604800 16 | @ IN SOA ns.local. postmaster.localhost ( 17 | 2 ; Serial 18 | 604800 ; Refresh 19 | 86400 ; Retry 20 | 2419200 ; Expire 21 | 604800 ) ; Negative Cache TTL 22 | ; 23 | local. IN NS ns.local. 24 | ns.local. IN A 127.0.0.1 25 | 26 | -------------------------------------------------------------------------------- /spec/celluloid/dns/server/bind9/named.conf: -------------------------------------------------------------------------------- 1 | 2 | options { 3 | directory "./"; 4 | 5 | listen-on-v6 { none; }; 6 | listen-on { 127.0.0.1; }; 7 | 8 | pid-file "./named.pid"; 9 | }; 10 | 11 | zone "local" { 12 | type master; 13 | file "./local.zone"; 14 | }; 15 | -------------------------------------------------------------------------------- /spec/celluloid/dns/server/bind9/named.run: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/celluloid/celluloid-dns/9ca57bb1c8dcbb041587784b5b1d947171aa6136/spec/celluloid/dns/server/bind9/named.run -------------------------------------------------------------------------------- /spec/celluloid/dns/server/million.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright, 2009, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'celluloid/dns' 24 | require 'benchmark' 25 | 26 | require 'stackprof' 27 | 28 | Name = Resolv::DNS::Name 29 | IN = Resolv::DNS::Resource::IN 30 | 31 | # Generate a million A record "domains": 32 | 33 | million = {} 34 | 35 | Benchmark.bm do |x| 36 | x.report("Generate names") do 37 | (1..1_000_000).each do |i| 38 | domain = "domain#{i}.local" 39 | 40 | million[domain] = "#{69}.#{(i >> 16)%256}.#{(i >> 8)%256}.#{i%256}" 41 | end 42 | end 43 | end 44 | 45 | # Run the server: 46 | 47 | StackProf.run(mode: :cpu, out: 'celluloid/dns.stackprof') do 48 | Celluloid::DNS::run_server(:listen => [[:udp, '0.0.0.0', 5300]]) do 49 | @logger.level = Logger::WARN 50 | 51 | match(//, IN::A) do |transaction| 52 | transaction.respond!(million[transaction.name]) 53 | end 54 | 55 | # Default DNS handler 56 | otherwise do |transaction| 57 | logger.info "Passing DNS request upstream..." 58 | transaction.fail!(:NXDomain) 59 | end 60 | end 61 | end 62 | 63 | # Expected output: 64 | # 65 | # > dig @localhost -p 5300 domain1000000 66 | # 67 | # ; <<>> DiG 9.8.3-P1 <<>> @localhost -p 5300 domain1000000 68 | # ; (3 servers found) 69 | # ;; global options: +cmd 70 | # ;; Got answer: 71 | # ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 50336 72 | # ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 73 | # ;; WARNING: recursion requested but not available 74 | # 75 | # ;; QUESTION SECTION: 76 | # ;domain1000000. IN A 77 | # 78 | # ;; ANSWER SECTION: 79 | # domain1000000. 86400 IN A 69.15.66.64 80 | # 81 | # ;; Query time: 1 msec 82 | # ;; SERVER: 127.0.0.1#5300(127.0.0.1) 83 | # ;; WHEN: Fri May 16 19:17:48 2014 84 | # ;; MSG SIZE rcvd: 47 85 | # 86 | -------------------------------------------------------------------------------- /spec/celluloid/dns/server_performance_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'celluloid/dns' 24 | require 'benchmark' 25 | require 'process/daemon' 26 | 27 | module Celluloid::DNS::ServerPerformanceSpec 28 | describe Celluloid::DNS::Server do 29 | context 'benchmark' do 30 | class MillionServer < Celluloid::DNS::Server 31 | def initialize(*) 32 | super 33 | 34 | @million = {} 35 | 36 | (1..5_000).each do |i| 37 | domain = "domain#{i}.local" 38 | 39 | @million[domain] = "#{69}.#{(i >> 16)%256}.#{(i >> 8)%256}.#{i%256}" 40 | end 41 | end 42 | 43 | def process(name, resource_class, transaction) 44 | transaction.respond!(@million[name]) 45 | end 46 | end 47 | 48 | class CelluloidServerDaemon < Process::Daemon 49 | IN = Resolv::DNS::Resource::IN 50 | 51 | def working_directory 52 | File.expand_path("../tmp", __FILE__) 53 | end 54 | 55 | def startup 56 | puts "Booting celluloid..." 57 | Celluloid.boot 58 | 59 | puts "Starting DNS server..." 60 | @server = MillionServer.new(listen: [[:udp, '0.0.0.0', 5300]]) 61 | 62 | @server.async.run 63 | end 64 | 65 | def shutdown 66 | @server.terminate 67 | end 68 | end 69 | 70 | class Bind9ServerDaemon < Process::Daemon 71 | def working_directory 72 | File.expand_path("../server/bind9", __FILE__) 73 | end 74 | 75 | def run 76 | exec(self.class.named_executable, "-c", "named.conf", "-f", "-p", "5400", "-g") 77 | end 78 | 79 | def self.named_executable 80 | # Check if named executable is available: 81 | @named ||= `which named`.chomp 82 | end 83 | end 84 | 85 | before(:all) do 86 | @servers = [] 87 | @servers << ["Celluloid::DNS::Server", 5300] 88 | 89 | @domains = (1..1000).collect do |i| 90 | "domain#{i}.local" 91 | end 92 | 93 | CelluloidServerDaemon.start 94 | 95 | unless Bind9ServerDaemon.named_executable.empty? 96 | Bind9ServerDaemon.start 97 | @servers << ["Bind9", 5400] 98 | end 99 | end 100 | 101 | after(:all) do 102 | CelluloidServerDaemon.stop 103 | 104 | unless Bind9ServerDaemon.named_executable.empty? 105 | Bind9ServerDaemon.stop 106 | end 107 | end 108 | 109 | it 'takes time' do 110 | Celluloid.logger.level = Logger::ERROR 111 | 112 | Benchmark.bm(30) do |x| 113 | @servers.each do |name, port| 114 | resolver = Celluloid::DNS::Resolver.new([[:udp, '127.0.0.1', port]]) 115 | 116 | x.report(name) do 117 | # Number of requests remaining since this is an asynchronous event loop: 118 | 5.times do 119 | pending = @domains.size 120 | 121 | futures = @domains.collect{|domain| resolver.future.addresses_for(domain)} 122 | 123 | futures.collect do |future| 124 | expect(future.value).to_not be nil 125 | end 126 | end 127 | end 128 | end 129 | end 130 | end 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /spec/celluloid/dns/slow_server_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'celluloid/dns' 24 | 25 | module Celluloid::DNS::SlowServerSpec 26 | SERVER_PORTS = [[:udp, '127.0.0.1', 5330], [:tcp, '127.0.0.1', 5330]] 27 | IN = Resolv::DNS::Resource::IN 28 | 29 | class TestServer < Celluloid::DNS::Server 30 | def process(name, resource_class, transaction) 31 | @resolver ||= Celluloid::DNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]]) 32 | 33 | sleep(2) if name.end_with?('.com') 34 | 35 | transaction.fail!(:NXDomain) 36 | end 37 | end 38 | 39 | describe "Celluloid::DNS Slow Server" do 40 | before(:all) do 41 | @server = TestServer.new(listen: SERVER_PORTS) 42 | @server.run 43 | end 44 | 45 | after(:all) do 46 | @server.terminate 47 | end 48 | 49 | it "get no answer after 2 seconds" do 50 | start_time = Time.now 51 | 52 | resolver = Celluloid::DNS::Resolver.new(SERVER_PORTS, :timeout => 10) 53 | 54 | response = resolver.query("apple.com", IN::A) 55 | 56 | expect(response.answer.length).to be == 0 57 | 58 | end_time = Time.now 59 | 60 | expect(end_time - start_time).to be_within(0.1).of(2.0) 61 | end 62 | 63 | it "times out after 1 second" do 64 | start_time = Time.now 65 | 66 | resolver = Celluloid::DNS::Resolver.new(SERVER_PORTS, :timeout => 0.5) 67 | 68 | response = resolver.query("apple.com", IN::A) 69 | 70 | expect(response).to be nil 71 | 72 | end_time = Time.now 73 | 74 | expect(end_time - start_time).to be_within(0.1).of(1.0) 75 | end 76 | 77 | it "gets no answer immediately" do 78 | start_time = Time.now 79 | 80 | resolver = Celluloid::DNS::Resolver.new(SERVER_PORTS, :timeout => 0.5) 81 | 82 | response = resolver.query("oriontransfer.org", IN::A) 83 | 84 | expect(response.answer.length).to be 0 85 | 86 | end_time = Time.now 87 | 88 | expect(end_time - start_time).to be_within(0.1).of(0.0) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/celluloid/dns/socket_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright, 2014, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'celluloid/dns' 24 | require 'celluloid/dns/system' 25 | 26 | module Celluloid::DNS::SocketSpec 27 | IN = Resolv::DNS::Resource::IN 28 | 29 | class TestServer < Celluloid::DNS::Server 30 | def process(name, resource_class, transaction) 31 | @resolver ||= Celluloid::DNS::Resolver.new([[:udp, "8.8.8.8", 53], [:tcp, "8.8.8.8", 53]]) 32 | 33 | transaction.passthrough!(@resolver) 34 | end 35 | end 36 | 37 | describe Celluloid::DNS::TCPSocketHandler do 38 | after(:each) do 39 | @server.terminate 40 | end 41 | 42 | it "should create server with existing TCP socket" do 43 | socket = TCPServer.new('127.0.0.1', 2002) 44 | 45 | @server = TestServer.new(listen: [socket]) 46 | @server.run 47 | 48 | resolver = Celluloid::DNS::Resolver.new([[:tcp, '127.0.0.1', 2002]]) 49 | response = resolver.query('google.com') 50 | expect(response.class).to be == Celluloid::DNS::Message 51 | end 52 | end 53 | 54 | describe Celluloid::DNS::UDPSocketHandler do 55 | after(:each) do 56 | @server.terminate 57 | end 58 | 59 | it "should create server with existing UDP socket" do 60 | socket = UDPSocket.new 61 | socket.bind('127.0.0.1', 2002) 62 | 63 | @server = TestServer.new(listen: [socket]) 64 | @server.run 65 | 66 | resolver = Celluloid::DNS::Resolver.new([[:udp, '127.0.0.1', 2002]]) 67 | response = resolver.query('google.com') 68 | expect(response.class).to be == Celluloid::DNS::Message 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/celluloid/dns/system_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'celluloid/dns' 24 | require 'celluloid/dns/system' 25 | 26 | module Celluloid::DNS::SystemSpec 27 | describe Celluloid::DNS::System do 28 | it "should have at least one namesever" do 29 | expect(Celluloid::DNS::System::nameservers.length).to be > 0 30 | end 31 | 32 | it "should respond to query for google.com" do 33 | resolver = Celluloid::DNS::Resolver.new(Celluloid::DNS::System::nameservers) 34 | 35 | response = resolver.query('google.com') 36 | 37 | expect(response.class).to be == Celluloid::DNS::Message 38 | expect(response.rcode).to be == Resolv::DNS::RCode::NoError 39 | end 40 | end 41 | 42 | describe Celluloid::DNS::System::Hosts do 43 | it "should parse the hosts file" do 44 | hosts = Celluloid::DNS::System::Hosts.new 45 | 46 | # Load the test hosts data: 47 | File.open(File.expand_path("../hosts.txt", __FILE__)) do |file| 48 | hosts.parse_hosts(file) 49 | end 50 | 51 | expect(hosts.call('testing')).to be == true 52 | expect(hosts['testing']).to be == '1.2.3.4' 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/celluloid/dns/transaction_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rspec 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'celluloid/dns' 24 | require 'yaml' 25 | 26 | module Celluloid::DNS::TransactionSpec 27 | SERVER_PORTS = [[:udp, '8.8.8.8', 53], [:tcp, '8.8.8.8', 53]] 28 | IN = Resolv::DNS::Resource::IN 29 | 30 | describe Celluloid::DNS::Transaction do 31 | let(:server) { Celluloid::DNS::Server.new } 32 | let(:query) { Celluloid::DNS::Message.new(0) } 33 | let(:question) { Resolv::DNS::Name.create("www.google.com.") } 34 | let(:response) { Celluloid::DNS::Message.new(0) } 35 | let(:resolver) { Celluloid::DNS::Resolver.new([[:udp, '8.8.8.8', 53], [:tcp, '8.8.8.8', 53]])} 36 | 37 | it "should append an address" do 38 | transaction = Celluloid::DNS::Transaction.new(server, query, question, IN::A, response) 39 | 40 | transaction.respond!("1.2.3.4") 41 | 42 | expect(transaction.response.answer[0][0]).to be == question 43 | expect(transaction.response.answer[0][2].address.to_s).to be == "1.2.3.4" 44 | end 45 | 46 | it "should passthrough the request" do 47 | transaction = Celluloid::DNS::Transaction.new(server, query, question, IN::A, response) 48 | 49 | expect(transaction.response.answer.size).to be 0 50 | 51 | transaction.passthrough!(resolver) 52 | 53 | expect(transaction.response.answer.size).to be > 0 54 | end 55 | 56 | it "should return a response on passthrough" do 57 | transaction = Celluloid::DNS::Transaction.new(server, query, question, IN::A, response) 58 | 59 | expect(transaction.response.answer.size).to be 0 60 | 61 | response = transaction.passthrough(resolver) 62 | 63 | expect(response.answer.length).to be > 0 64 | end 65 | 66 | it "should call the block with the response when invoking passthrough!" do 67 | transaction = Celluloid::DNS::Transaction.new(server, query, question, IN::A, response) 68 | 69 | expect(transaction.response.answer.size).to be 0 70 | 71 | passthrough_response = nil 72 | 73 | transaction.passthrough!(resolver) do |response| 74 | passthrough_response = response 75 | end 76 | 77 | expect(passthrough_response.answer.length).to be > 0 78 | end 79 | 80 | it "should fail the request" do 81 | transaction = Celluloid::DNS::Transaction.new(server, query, question, IN::A, response) 82 | 83 | transaction.fail! :NXDomain 84 | 85 | expect(transaction.response.rcode).to be Resolv::DNS::RCode::NXDomain 86 | end 87 | 88 | it "should return AAAA record" do 89 | transaction = Celluloid::DNS::Transaction.new(server, query, question, IN::AAAA, response) 90 | 91 | expect(transaction.response.answer.size).to be 0 92 | 93 | transaction.passthrough!(resolver) 94 | 95 | expect(transaction.response.answer.first[2]).to be_kind_of IN::AAAA 96 | end 97 | 98 | it "should return MX record" do 99 | transaction = Celluloid::DNS::Transaction.new(server,query,"google.com",IN::MX, response) 100 | 101 | expect(transaction.response.answer.size).to be 0 102 | 103 | transaction.passthrough!(resolver) 104 | 105 | expect(transaction.response.answer.first[2]).to be_kind_of IN::MX 106 | end 107 | 108 | it "should return NS record" do 109 | transaction = Celluloid::DNS::Transaction.new(server, query, "google.com", IN::NS, response) 110 | 111 | expect(transaction.response.answer.size).to be 0 112 | 113 | transaction.passthrough!(resolver) 114 | 115 | expect(transaction.response.answer.first[2]).to be_kind_of IN::NS 116 | end 117 | 118 | it "should return PTR record" do 119 | transaction = Celluloid::DNS::Transaction.new(server, query, "8.8.8.8.in-addr.arpa", IN::PTR, response) 120 | 121 | expect(transaction.response.answer.size).to be 0 122 | 123 | transaction.passthrough!(resolver) 124 | 125 | expect(transaction.response.answer.first[2]).to be_kind_of IN::PTR 126 | end 127 | 128 | it "should return SOA record" do 129 | transaction = Celluloid::DNS::Transaction.new(server, query, "google.com", IN::SOA, response) 130 | 131 | expect(transaction.response.answer.size).to be 0 132 | 133 | transaction.passthrough!(resolver) 134 | 135 | expect(transaction.response.answer.first[2]).to be_kind_of IN::SOA 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /spec/celluloid/dns/truncation_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'celluloid/dns' 24 | require 'celluloid/dns/extensions/string' 25 | 26 | module Celluloid::DNS::TruncationSpec 27 | SERVER_PORTS = [[:udp, '127.0.0.1', 5520], [:tcp, '127.0.0.1', 5520]] 28 | IN = Resolv::DNS::Resource::IN 29 | 30 | class TestServer < Celluloid::DNS::Server 31 | def process(name, resource_class, transaction) 32 | case [name, resource_class] 33 | when ["truncation", IN::TXT] 34 | text = "Hello World! " * 100 35 | transaction.respond!(*text.chunked) 36 | else 37 | transaction.fail!(:NXDomain) 38 | end 39 | end 40 | end 41 | 42 | describe "Celluloid::DNS Truncation Server" do 43 | before(:all) do 44 | @test_server = TestServer.new(listen: SERVER_PORTS) 45 | @test_server.run 46 | end 47 | 48 | after(:all) do 49 | @test_server.terminate 50 | end 51 | 52 | it "should use tcp because of large response" do 53 | resolver = Celluloid::DNS::Resolver.new(SERVER_PORTS) 54 | 55 | response = resolver.query("truncation", IN::TXT) 56 | 57 | text = response.answer.first 58 | 59 | expect(text[2].strings.join).to be == ("Hello World! " * 100) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | 2 | if ENV['COVERAGE'] || ENV['TRAVIS'] 3 | begin 4 | require 'simplecov' 5 | 6 | SimpleCov.start do 7 | add_filter "/spec/" 8 | end 9 | 10 | # Work correctly across forks: 11 | pid = Process.pid 12 | SimpleCov.at_exit do 13 | SimpleCov.result.format! if Process.pid == pid 14 | end 15 | 16 | if ENV['TRAVIS'] 17 | require 'coveralls' 18 | Coveralls.wear! 19 | end 20 | rescue LoadError 21 | warn "Could not load simplecov: #{$!}" 22 | end 23 | end 24 | 25 | require "bundler/setup" 26 | require "celluloid/dns" 27 | 28 | abort "Warning, ulimit is too low!" if `ulimit -n`.to_i < 10000 29 | 30 | require 'celluloid/autostart' 31 | 32 | RSpec.configure do |config| 33 | # Enable flags like --only-failures and --next-failure 34 | config.example_status_persistence_file_path = ".rspec_status" 35 | 36 | config.expect_with :rspec do |c| 37 | c.syntax = :expect 38 | end 39 | end 40 | --------------------------------------------------------------------------------