net: don't prefer IPv4 addresses during resolution
authorcjihrig <cjihrig@gmail.com>
Thu, 22 May 2014 02:13:09 +0000 (22:13 -0400)
committerTrevor Norris <trev.norris@gmail.com>
Mon, 4 Aug 2014 23:57:18 +0000 (16:57 -0700)
Currently the address resolution family defaults to IPv4. Instead remove
the preference and instead resolve to a family suitable for the host.

Expose the getaddrinfo flags and allow them to be passed.

Add documentation about new flags.

Reviewed-by: Trevor Norris <trev.norris@gmail.com>
doc/api/dns.markdown
lib/dns.js
lib/net.js
src/cares_wrap.cc
test/internet/test-dns.js
test/simple/test-dns.js

index 94e19a3..5b8477f 100644 (file)
@@ -31,12 +31,31 @@ resolves the IP addresses which are returned.
       });
     });
 
-## dns.lookup(hostname, [family], callback)
+## dns.lookup(hostname, [options], callback)
 
 Resolves a hostname (e.g. `'google.com'`) into the first found A (IPv4) or
-AAAA (IPv6) record.
-The `family` can be the integer `4` or `6`. Defaults to `null` that indicates
-both Ip v4 and v6 address family.
+AAAA (IPv6) record. `options` can be an object or integer. If `options` is
+not provided, then IP v4 and v6 addresses are both valid. If `options` is
+an integer, then it must be `4` or `6`.
+
+Alternatively, `options` can be an object containing two properties,
+`family` and `hints`. Both properties are optional. If `family` is provided,
+it must be the integer `4` or `6`. If `family` is not provided then IP v4
+and v6 addresses are accepted. The `hints` field, if present, should be one
+or more of the supported `getaddrinfo` flags. If `hints` is not provided,
+then no flags are passed to `getaddrinfo`. Multiple flags can be passed
+through `hints` by logically `OR`ing their values. An example usage of
+`options` is shown below.
+
+```
+{
+  family: 4,
+  hints: dns.ADDRCONFIG | dns.V4MAPPED
+}
+```
+
+See [supported `getaddrinfo` flags](#dns_supported_getaddrinfo_flags) below for
+more information on supported flags.
 
 The callback has arguments `(err, address, family)`.  The `address` argument
 is a string representation of a IP v4 or v6 address. The `family` argument
@@ -120,7 +139,7 @@ of SRV records are priority, weight, port, and name (e.g.,
 
 ## dns.resolveSoa(hostname, callback)
 
-The same as `dns.resolve()`, but only for start of authority record queries 
+The same as `dns.resolve()`, but only for start of authority record queries
 (`SOA` record).
 
 `addresses` is an object with the following structure:
@@ -201,3 +220,14 @@ Each DNS query can return one of the following error codes:
 - `dns.LOADIPHLPAPI`: Error loading iphlpapi.dll.
 - `dns.ADDRGETNETWORKPARAMS`: Could not find GetNetworkParams function.
 - `dns.CANCELLED`: DNS query cancelled.
+
+## Supported getaddrinfo flags
+
+The following flags can be passed as hints to `dns.lookup`.
+
+- `dns.ADDRCONFIG`: Returned address types are determined by the types
+of addresses supported by the current system. For example, IPv4 addresses
+are only returned if the current system has at least one IPv4 address
+configured. Loopback addresses are not considered.
+- `dns.V4MAPPED`: If the IPv6 family was specified, but no IPv6 addresses
+were found, then return IPv4 mapped IPv6 addresses.
index f1b63a1..dfe37df 100644 (file)
@@ -99,20 +99,37 @@ function onlookup(err, addresses) {
 
 
 // Easy DNS A/AAAA look up
-// lookup(hostname, [family,] callback)
-exports.lookup = function(hostname, family, callback) {
-  // parse arguments
-  if (arguments.length === 2) {
-    callback = family;
+// lookup(hostname, [options,] callback)
+exports.lookup = function lookup(hostname, options, callback) {
+  var hints = 0;
+  var family;
+
+  // Parse arguments
+  if (typeof options === 'function') {
+    callback = options;
     family = 0;
-  } else if (!family) {
+  // Allow user to pass falsy values to options, and still pass callback.
+  } else if (typeof callback !== 'function') {
+    throw TypeError('invalid arguments: callback must be passed');
+  } else if (!options) {
     family = 0;
-  } else {
-    family = +family;
-    if (family !== 4 && family !== 6) {
-      throw new Error('invalid argument: `family` must be 4 or 6');
+  } else if (util.isObject(options)) {
+    hints = options.hints >>> 0;
+    family = options.family >>> 0;
+
+    if (hints !== 0 &&
+        hints !== exports.ADDRCONFIG &&
+        hints !== exports.V4MAPPED &&
+        hints !== (exports.ADDRCONFIG | exports.V4MAPPED)) {
+      throw new TypeError('invalid argument: hints must use valid flags');
     }
+  } else {
+    family = options >>> 0;
   }
+
+  if (family !== 0 && family !== 4 && family !== 6)
+    throw new TypeError('invalid argument: family must be 4 or 6');
+
   callback = makeAsync(callback);
 
   if (!hostname) {
@@ -133,7 +150,7 @@ exports.lookup = function(hostname, family, callback) {
     oncomplete: onlookup
   };
 
-  var err = cares.getaddrinfo(req, hostname, family);
+  var err = cares.getaddrinfo(req, hostname, family, hints);
   if (err) {
     callback(errnoException(err, 'getaddrinfo', hostname));
     return {};
@@ -290,6 +307,9 @@ exports.setServers = function(servers) {
   }
 };
 
+// uv_getaddrinfo flags
+exports.ADDRCONFIG = cares.AI_ADDRCONFIG;
+exports.V4MAPPED = cares.AI_V4MAPPED;
 
 // ERROR CODES
 exports.NODATA = 'ENODATA';
index 1877e42..aa815fb 100644 (file)
@@ -881,11 +881,20 @@ Socket.prototype.connect = function(options, cb) {
     connect(self, self._host, options.port, 4);
 
   } else {
+    var dns = require('dns');
     var host = options.host;
-    var family = options.family || 4;
+    var dnsopts = {
+      family: options.family,
+      hints: 0
+    };
+
+    if (dnsopts.family !== 4 && dnsopts.family !== 6)
+      dnsopts.hints = dns.ADDRCONFIG | dns.V4MAPPED;
+
     debug('connect: find host ' + host);
+    debug('connect: dns options ' + dnsopts);
     self._host = host;
-    require('dns').lookup(host, family, function(err, ip, addressType) {
+    dns.lookup(host, dnsopts, function(err, ip, addressType) {
       self.emit('lookup', err, ip, addressType);
 
       // It's possible we were destroyed while looking this up.
index 7523e9d..af72c49 100644 (file)
@@ -1014,10 +1014,13 @@ static void GetAddrInfo(const FunctionCallbackInfo<Value>& args) {
   assert(args[0]->IsObject());
   assert(args[1]->IsString());
   assert(args[2]->IsInt32());
+  assert(args[3]->IsInt32());
   Local<Object> req_wrap_obj = args[0].As<Object>();
   node::Utf8Value hostname(args[1]);
 
   int family;
+  int32_t flags = args[3]->Int32Value();
+
   switch (args[2]->Int32Value()) {
   case 0:
     family = AF_UNSPEC;
@@ -1042,6 +1045,7 @@ static void GetAddrInfo(const FunctionCallbackInfo<Value>& args) {
   memset(&hints, 0, sizeof(struct addrinfo));
   hints.ai_family = family;
   hints.ai_socktype = SOCK_STREAM;
+  hints.ai_flags = flags;
 
   int err = uv_getaddrinfo(env->event_loop(),
                            &req_wrap->req_,
@@ -1246,6 +1250,10 @@ static void Initialize(Handle<Object> target,
               Integer::New(env->isolate(), AF_INET6));
   target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "AF_UNSPEC"),
               Integer::New(env->isolate(), AF_UNSPEC));
+  target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "AI_ADDRCONFIG"),
+              Integer::New(env->isolate(), AI_ADDRCONFIG));
+  target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "AI_V4MAPPED"),
+              Integer::New(env->isolate(), AI_V4MAPPED));
 }
 
 }  // namespace cares_wrap
index 6165b30..902545c 100644 (file)
@@ -337,6 +337,36 @@ TEST(function test_lookup_ipv4_implicit(done) {
 });
 
 
+TEST(function test_lookup_ipv4_explicit_object(done) {
+  var req = dns.lookup('www.google.com', {
+    family: 4
+  }, function(err, ip, family) {
+    if (err) throw err;
+    assert.ok(net.isIPv4(ip));
+    assert.strictEqual(family, 4);
+
+    done();
+  });
+
+  checkWrap(req);
+});
+
+
+TEST(function test_lookup_ipv4_hint_addrconfig(done) {
+  var req = dns.lookup('www.google.com', {
+    hint: dns.ADDRCONFIG
+  }, function(err, ip, family) {
+    if (err) throw err;
+    assert.ok(net.isIPv4(ip));
+    assert.strictEqual(family, 4);
+
+    done();
+  });
+
+  checkWrap(req);
+});
+
+
 TEST(function test_lookup_ipv6_explicit(done) {
   var req = dns.lookup('ipv6.google.com', 6, function(err, ip, family) {
     if (err) throw err;
@@ -365,6 +395,36 @@ TEST(function test_lookup_ipv6_implicit(done) {
 */
 
 
+TEST(function test_lookup_ipv6_explicit_object(done) {
+  var req = dns.lookup('ipv6.google.com', {
+    family: 6
+  }, function(err, ip, family) {
+    if (err) throw err;
+    assert.ok(net.isIPv6(ip));
+    assert.strictEqual(family, 6);
+
+    done();
+  });
+
+  checkWrap(req);
+});
+
+
+TEST(function test_lookup_ipv6_hint(done) {
+  var req = dns.lookup('ipv6.google.com', {
+    hint: dns.V4MAPPED
+  }, function(err, ip, family) {
+    if (err) throw err;
+    assert.ok(net.isIPv6(ip));
+    assert.strictEqual(family, 6);
+
+    done();
+  });
+
+  checkWrap(req);
+});
+
+
 TEST(function test_lookup_failure(done) {
   var req = dns.lookup('does.not.exist', 4, function(err, ip, family) {
     assert.ok(err instanceof Error);
index e7dce4b..48f7d5f 100644 (file)
@@ -27,6 +27,8 @@ var dns = require('dns');
 var existing = dns.getServers();
 assert(existing.length);
 
+function noop() {}
+
 var goog = [
   '8.8.8.8',
   '8.8.4.4',
@@ -61,12 +63,54 @@ assert.deepEqual(dns.getServers(), portsExpected);
 assert.doesNotThrow(function () { dns.setServers([]); });
 assert.deepEqual(dns.getServers(), []);
 
-assert.throws(
-  function() {
-    dns.resolve('test.com', [], new Function);
-  },
-  function(err) {
-    return !(err instanceof TypeError);
-  },
-  "Unexpected error"
-);
+assert.throws(function() {
+  dns.resolve('test.com', [], noop);
+}, function(err) {
+  return !(err instanceof TypeError);
+}, 'Unexpected error');
+
+assert.throws(function() {
+  dns.lookup('www.google.com', { hints: 1 }, noop);
+});
+
+assert.throws(function() {
+  dns.lookup('www.google.com');
+}, 'invalid arguments: callback must be passed');
+
+assert.throws(function() {
+  dns.lookup('www.google.com', 4);
+}, 'invalid arguments: callback must be passed');
+
+assert.doesNotThrow(function() {
+  dns.lookup('www.google.com', 6, noop);
+});
+
+assert.doesNotThrow(function() {
+  dns.lookup('www.google.com', {}, noop);
+});
+
+assert.doesNotThrow(function() {
+  dns.lookup('www.google.com', {
+    family: 4,
+    hints: 0
+  }, noop);
+});
+
+assert.doesNotThrow(function() {
+  dns.lookup('www.google.com', {
+    family: 6,
+    hints: dns.ADDRCONFIG
+  }, noop);
+});
+
+assert.doesNotThrow(function() {
+  dns.lookup('www.google.com', {
+    hints: dns.V4MAPPED
+  }, noop);
+});
+
+assert.doesNotThrow(function() {
+  dns.lookup('www.google.com', {
+    hints: dns.ADDRCONFIG | dns.V4MAPPED
+  }, noop);
+});