[io] Do delayed IPv6 lookups on all platforms.

Bug: https://github.com/dart-lang/sdk/issues/50868
Change-Id: I5ad57f4634287b4299fbf74fde075d518154bf08
Tested: unit
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/288621
Commit-Queue: Brian Quinlan <bquinlan@google.com>
Reviewed-by: Alexander Aprelev <aam@google.com>
diff --git a/sdk/lib/_internal/vm/bin/socket_patch.dart b/sdk/lib/_internal/vm/bin/socket_patch.dart
index a9cd936..5ae91a1 100644
--- a/sdk/lib/_internal/vm/bin/socket_patch.dart
+++ b/sdk/lib/_internal/vm/bin/socket_patch.dart
@@ -517,17 +517,6 @@
     });
   }
 
-  static Stream<List<InternetAddress>> lookupAsStream(String host,
-      {InternetAddressType type = InternetAddressType.any}) {
-    final controller = StreamController<List<InternetAddress>>();
-    controller.onListen = () {
-      lookup(host, type: type).then((list) {
-        controller.add(list);
-      }, onError: controller.addError).whenComplete(controller.close);
-    };
-    return controller.stream;
-  }
-
   static Future<InternetAddress> reverseLookup(InternetAddress addr) {
     return _IOService._dispatch(_IOService.socketReverseLookup,
         [(addr as _InternetAddress)._in_addr]).then((response) {
@@ -597,12 +586,20 @@
         (char == '8' || char == '9' || char == 'a' || char == 'b');
   }
 
-  /// Explicitly makes two separate OS lookup requests: first for IPv4, then
-  /// after short delay for IPv6.
-  /// This avoids making single OS lookup request that internally does both IPv4
-  /// and IPv6 together, which on iOS sometimes seems to be taking unreasonably
-  /// long because of slow IPv6 lookup even though IPv4 lookup is fast.
-  static Stream<List<InternetAddress>> staggeredLookup(String host) {
+  /// Explicitly makes two separate OS lookup requests: first for IPv4 then,
+  /// after a short delay, for IPv6.
+  ///
+  /// This avoids making a single OS lookup request that internally does both
+  /// IPv4 and IPv6 lookups together, which will be as slow as the slowest
+  /// lookup. Some broken DNS servers do not support IPv6 AAAA records and
+  /// will cause the IPv6 lookup to timeout.
+  ///
+  /// The IPv4 lookup is done first because, in practice, IPv4 traffic is
+  /// routed more reliably.
+  ///
+  /// See https://dartbug.com/50868.
+  static Stream<List<InternetAddress>> staggeredLookup(
+      String host, _InternetAddress? source) {
     final controller = StreamController<List<InternetAddress>>(sync: true);
 
     controller.onListen = () {
@@ -613,40 +610,50 @@
       // closed.
       final ipv4Completer = Completer<void>();
       final ipv6Completer = Completer<void>();
+      // Only report an error if no address lookups were sucessful.
+      var anySuccess = false;
 
       void lookupAddresses(InternetAddressType type, Completer<void> done) {
         lookup(host, type: type).then((addresses) {
+          anySuccess = true;
           if (done.isCompleted) {
             // By the time lookup is done, [connectNext] might have
             // been able to connect to one of the resolved addresses.
             return;
           }
           controller.add(addresses);
+          done.complete();
         }, onError: (e, st) {
           if (done.isCompleted) {
             // By the time lookup is done, [connectNext] might have
             // been able to connect to one of the resolved addresses.
             return;
           }
-          controller.addError(e, st);
-        }).whenComplete(() {
-          if (!done.isCompleted) {
-            done.complete();
-          }
+          done.completeError(e, st);
         });
       }
 
-      lookupAddresses(InternetAddressType.IPv4, ipv4Completer);
-      // Give a chance for a connect to an IPv4 address to complete before
-      // starting an IPv6 lookup. If IPv4 connect succeeds before timer goes
-      // off, the timer gets cancelled.
       const concurrentLookupDelay = Duration(milliseconds: 10);
-      final ipv6LookupDelay = Timer(concurrentLookupDelay, () {
-        lookupAddresses(InternetAddressType.IPv6, ipv6Completer);
-      });
+      Timer? ipv6LookupDelay;
+
+      lookupAddresses(InternetAddressType.IPv4, ipv4Completer);
+      if (source != null && source.type == InternetAddressType.IPv4) {
+        // Binding to an IPv4 address and connecting to an IPv6 address will
+        // never work.
+        ipv6Completer.complete();
+      } else {
+        // Introduce a delay before IPv6 lookup in order to favor IPv4.
+        ipv6LookupDelay = Timer(concurrentLookupDelay,
+            () => lookupAddresses(InternetAddressType.IPv6, ipv6Completer));
+      }
 
       Future.wait([ipv4Completer.future, ipv6Completer.future])
-          .then((_) => controller.close());
+          .then((_) => controller.close(), onError: (e, st) {
+        if (!anySuccess) {
+          controller.addError(e, st);
+        }
+        controller.close();
+      });
 
       controller.onCancel = () {
         // This is invoked when [connectNext] managed to connect to one of the
@@ -654,7 +661,7 @@
         // the addresses.
         if (!ipv4Completer.isCompleted) ipv4Completer.complete();
         if (!ipv6Completer.isCompleted) ipv6Completer.complete();
-        ipv6LookupDelay.cancel();
+        ipv6LookupDelay?.cancel();
       };
     };
     return controller.stream;
@@ -677,6 +684,15 @@
     _InternetAddress? source;
     if (sourceAddress != null) {
       if (sourceAddress is _InternetAddress) {
+        // A host of type [String] is interpreted to be a an internet host
+        // (or numeric IP e.g. '127.0.0.1'), which is never reachable using
+        // a Unix Domain Socket.
+        if (host is String && sourceAddress.type == InternetAddressType.unix) {
+          // ArgumentError would be better but changing it would not be
+          // backwards-compatible.
+          throw SocketException(
+              "Cannot connect to an internet host using a unix domain socket");
+        }
         source = sourceAddress;
       } else if (sourceAddress is String) {
         source = new _InternetAddress.fromString(sourceAddress);
@@ -689,26 +705,24 @@
     final stackTrace = StackTrace.current;
 
     return new Future.value(host).then<ConnectionTask<_NativeSocket>>((host) {
+      if (host is String) {
+        // Attempt to interpret the host as a numeric address
+        // (e.g. "127.0.0.1"). This will prevent [InternetAddress.lookup] from
+        // generating an unnecessary address in a different address family e.g.
+        // `InternetAddress.lookup('127.0.0.1', InternetAddressType.IPv6)`
+        // may return `InternetAddress('::ffff:127.0.0.1').
+        host = _InternetAddress.tryParse(host) ?? host;
+      }
       if (host is _InternetAddress) {
         return tryConnectToResolvedAddresses(host, port, source, sourcePort,
             Stream.value(<_InternetAddress>[host]), stackTrace);
       }
       final hostname = host as String;
-      final staggeredLookupOverride = bool.fromEnvironment(
-          "dart.library.io.force_staggered_ipv6_lookup",
-          defaultValue: false);
 
-      // On ios name resolution can get delayed by slow IPv6 name resolution,
-      // so we run IPv4 and IPv6 name resolution in parallel(IPv6 slightly
-      // delayed so if IPv4 is successfully looked up, we don't do IPv6 look up
-      // at all) and grab first successfully resolved name we are able to connect to.
-      final Stream<List<InternetAddress>> stream =
-          Platform.isIOS || staggeredLookupOverride
-              ? staggeredLookup(hostname)
-              : lookupAsStream(hostname);
-
+      final Stream<List<InternetAddress>> addresses =
+          staggeredLookup(hostname, source);
       return tryConnectToResolvedAddresses(
-          host, port, source, sourcePort, stream, stackTrace);
+          host, port, source, sourcePort, addresses, stackTrace);
     });
   }
 
diff --git a/tests/standalone/io/http_client_connect_test.dart b/tests/standalone/io/http_client_connect_test.dart
index f2554a3..ff328b8 100644
--- a/tests/standalone/io/http_client_connect_test.dart
+++ b/tests/standalone/io/http_client_connect_test.dart
@@ -260,7 +260,10 @@
   HttpServer.bind("127.0.0.1", 0).then((server) {
     int handled = 0;
     server.listen((request) {
-      Expect.isTrue(server.connectionsInfo().total <= connectionCap);
+      Expect.isTrue(
+          server.connectionsInfo().total <= connectionCap,
+          '${server.connectionsInfo().total} <= $connectionCap ' +
+              '(connections: $connections)');
       request.response.close();
       handled++;
       if (handled == connections) {
diff --git a/tests/standalone_2/io/http_client_connect_test.dart b/tests/standalone_2/io/http_client_connect_test.dart
index ed2c614..c1a6b82 100644
--- a/tests/standalone_2/io/http_client_connect_test.dart
+++ b/tests/standalone_2/io/http_client_connect_test.dart
@@ -260,7 +260,10 @@
   HttpServer.bind("127.0.0.1", 0).then((server) {
     int handled = 0;
     server.listen((request) {
-      Expect.isTrue(server.connectionsInfo().total <= connectionCap);
+      Expect.isTrue(
+          server.connectionsInfo().total <= connectionCap,
+          '${server.connectionsInfo().total} <= $connectionCap ' +
+              '(connections: $connections)');
       request.response.close();
       handled++;
       if (handled == connections) {