Bug -- resolv.rb dies when "domain" in resolv.conf has no arg

Example input:

-- domain.rb --
domain
nameserver 1.2.3.4

···

--

What happens with an unpatched resolv.rb is:

[ensemble] ~/p/ruby/zeroconf $ ruby18 -rresolv -rpp -e '(r=Resolv::DNS::Config.new("./domain.conf")).lazy_initialize; pp r'
/usr/local/lib/ruby/1.8/resolv.rb:921:in `split': private method `scan' called for nil:NilClass (NoMethodError)
        from /usr/local/lib/ruby/1.8/resolv.rb:788:in `lazy_initialize'
        from /usr/local/lib/ruby/1.8/resolv.rb:788:in `map'
        from /usr/local/lib/ruby/1.8/resolv.rb:788:in `lazy_initialize'
        from /usr/local/lib/ruby/1.8/resolv.rb:761:in `synchronize'
        from /usr/local/lib/ruby/1.8/resolv.rb:761:in `lazy_initialize'
        from -e:1

Please don't tell me the resolv.conf synatx is invalid - these files are
automatically created by OS X's network system. Also, resolv.rb accepts
an empty nameserver and an empty search command without problems.

-- search.rb --
search
nameserver 1.2.3.4
--

Patch to fix this:

Index: 1.8-resolv.rb

--- 1.8-resolv.rb (revision 5)
+++ 1.8-resolv.rb (working copy)
@@ -734,7 +734,7 @@
             when 'nameserver'
               nameserver += args
             when 'domain'
- search = [args[0]]
+ search = args[0, 1]
             when 'search'
               search = args
             end

Without this patch, @search becomes [nil] when domain had no arguments,
causing death later.

This can be tested with:

ruby18 -r1.8-resolv -rpp -e '(r=Resolv::DNS::Config.new("./search.conf")).lazy_initialize; pp r'
ruby18 -r1.8-resolv -rpp -e '(r=Resolv::DNS::Config.new("./domain.conf")).lazy_initialize; pp r'

You can see the behaviour before/after the patch.

Note that search (before) and domain (now, instead of dieing) with no
arguments both surpress the automagical choice of a search path based on
Socket.hostname. I believe this to be the correct behaviour, because it
seems reasonable to want to surpress the lookup (as a config option),
and because this is how the OS X C library resolver behaves.

Thanks,
Sam

In article <20050117002220.GA1001@ensemble.local>,
  Sam Roberts <sroberts@uniserve.com> writes:

[ensemble] ~/p/ruby/zeroconf $ ruby18 -rresolv -rpp -e '(r=Resolv::DNS::Config.new("./domain.conf")).lazy_initialize; pp r'
/usr/local/lib/ruby/1.8/resolv.rb:921:in `split': private method `scan' called for nil:NilClass (NoMethodError)
        from /usr/local/lib/ruby/1.8/resolv.rb:788:in `lazy_initialize'
        from /usr/local/lib/ruby/1.8/resolv.rb:788:in `map'
        from /usr/local/lib/ruby/1.8/resolv.rb:788:in `lazy_initialize'
        from /usr/local/lib/ruby/1.8/resolv.rb:761:in `synchronize'
        from /usr/local/lib/ruby/1.8/resolv.rb:761:in `lazy_initialize'
        from -e:1

Thank you for the report.

Note that search (before) and domain (now, instead of dieing) with no
arguments both surpress the automagical choice of a search path based on
Socket.hostname. I believe this to be the correct behaviour, because it
seems reasonable to want to surpress the lookup (as a config option),
and because this is how the OS X C library resolver behaves.

bind-9.3.0/lib/bind/resolv/res_init.c seems to ignore domain and
search directive with no arguments. So I modified resolv.rb to ignore
them. This may be incompatible with OS X C library resolver, though.

···

--
Tanaka Akira

resolv.rb:877 does

          raise ResolvError.new("DNS resolv error: #{name}")

This means that when Resolv::DNS fails to find at least one record for
a query, it raises an error, preventing the next resolver in @resolvers
from being checked (see Resolv#each_address()).

Symptoms:

- you can't do a DNS lookup, then fallback to a Hosts lookup:

  resolv = Resolv.new([Resolv::DNS.new, Resolv::Hosts.new])

  resolv.getaddress("localhost")

  # This will fail (unless your DNS server has an entry for "localhost",
  # some do). Doing the opposite (the default) does work, i.e. doing
  # the hosts lookup, then the DNS lookup.

- resolv.rb:228 (and probably 256) are no-ops

  Instead of getting ResolvError.new("no address for #{name}') raised as
  it appears was intended, you will always get the lower level "DNS
  resolv error".

- if you have two DNS resolvers registered the second will never be
  reached

  You might do this if the first uses resolv.conf for its configuration,
  and you add a second with a default config specified with a Hash:

   resolv = Resolv.new([Resolv::DNS.new, Resolv::DNS.new( ... my config ...) ])

- If you write you own resolver module, then the failure of the DNS
  module prevents the next resolver from being used to attempt the
  lookup.

Cheers,
Sam

Example patch:

···

===================================================================
--- 1.8-resolv.rb (revision 11)
+++ 1.8-resolv.rb (working copy)
@@ -226,6 +226,8 @@
   def getaddress(name)
     each_address(name) {|address| return address}
     raise ResolvError.new("no address for #{name}")
+# - this line is a no-op, it isn't reached because an internal DNS error is
+# raised instead.
   end

   def getaddresses(name)
@@ -872,7 +874,8 @@
         rescue OtherResolvError
           raise ResolvError.new("DNS error: #{$!.message}")
         end
- raise ResolvError.new("DNS resolv error: #{name}")
+# raise ResolvError.new("DNS resolv error: #{name}")
+# - this shouldn't be fatal, the next resolver might be able to resolve this name!
       end

       class NXDomain < ResolvError

Test code:

#!/usr/bin/ruby

require 'pp'
require 'socket'
require 'resolv'

puts "(C library resolver) localhost -->"
addr = IPSocket.getaddress("localhost")
pp addr

# This is the default, args = [Resolv::Hosts.new, Resolv::DNS.new]
resolv = Resolv.new

puts "(Hosts, DNS) localhost -->"
addr = resolv.getaddress("localhost")
pp addr
puts "OK - address resolves!"

begin
  puts "(Hosts, DNS) asergh.net -->"
  addr = resolv.getaddress("asergh.net")
  pp addr
rescue
  pp $!
  unless($!.to_s =~ /^no address for/)
    puts "BUG - this should have hit resolv.rb:228, instead it is returning a DNS-specific error!"
  end
end

# This is trying DNS lookup before host lookup
resolv = Resolv.new([Resolv::DNS.new, Resolv::Hosts.new])

begin
  puts "(resolv) localhost -->"
  addr = resolv.getaddress("localhost")
  pp addr
rescue
  pp $!
  puts "BUG - this failed to find localhost when the resolvers were reordered!"
end

Quoteing akr@m17n.org, on Wed, Jan 19, 2005 at 01:29:57AM +0900:

In article <20050117002220.GA1001@ensemble.local>,
  Sam Roberts <sroberts@uniserve.com> writes:

> [ensemble] ~/p/ruby/zeroconf $ ruby18 -rresolv -rpp -e '(r=Resolv::DNS::Config.new("./domain.conf")).lazy_initialize; pp r'
> /usr/local/lib/ruby/1.8/resolv.rb:921:in `split': private method `scan' called for nil:NilClass (NoMethodError)
> from /usr/local/lib/ruby/1.8/resolv.rb:788:in `lazy_initialize'
> from /usr/local/lib/ruby/1.8/resolv.rb:788:in `map'
> from /usr/local/lib/ruby/1.8/resolv.rb:788:in `lazy_initialize'
> from /usr/local/lib/ruby/1.8/resolv.rb:761:in `synchronize'
> from /usr/local/lib/ruby/1.8/resolv.rb:761:in `lazy_initialize'
> from -e:1

Thank you for the report.

Thank you for the fix!

> Note that search (before) and domain (now, instead of dieing) with no
> arguments both surpress the automagical choice of a search path based on
> Socket.hostname. I believe this to be the correct behaviour, because it
> seems reasonable to want to surpress the lookup (as a config option),
> and because this is how the OS X C library resolver behaves.

bind-9.3.0/lib/bind/resolv/res_init.c seems to ignore domain and
search directive with no arguments. So I modified resolv.rb to ignore
them. This may be incompatible with OS X C library resolver, though.

That's OK, it can't be compatible with everybody's resolver libraries,
so being compatible with bind9 seems very reasonable.

Sam

In article <20050119150325.GA1113@ensemble.local>,
  Sam Roberts <sroberts@uniserve.com> writes:

resolv.rb:877 does

          raise ResolvError.new("DNS resolv error: #{name}")

This means that when Resolv::DNS fails to find at least one record for
a query, it raises an error, preventing the next resolver in @resolvers
from being checked (see Resolv#each_address()).

Thank you for the report.

Resolv::DNS#each_address shouldn't raise ResolvError.

···

--
Tanaka Akira