[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) {