diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs index 0846543a6aee66..ff131fb4e22989 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs @@ -65,18 +65,64 @@ public static ValueTask ConnectAsync(QuicClientConnectionOptions static async ValueTask StartConnectAsync(QuicClientConnectionOptions options, CancellationToken cancellationToken) { + MsQuicSafeHandle? config = MsQuicConfiguration.Create(options); QuicConnection connection = new QuicConnection(); + connection._configuration = config; using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - if (options.HandshakeTimeout != Timeout.InfiniteTimeSpan && options.HandshakeTimeout != TimeSpan.Zero) - { - linkedCts.CancelAfter(options.HandshakeTimeout); - } - try { - await connection.FinishConnectAsync(options, linkedCts.Token).ConfigureAwait(false); + if (!options.RemoteEndPoint.TryParse(out string? host, out IPAddress? address, out int port)) + { + throw new ArgumentException(SR.Format(SR.net_quic_unsupported_endpoint_type, options.RemoteEndPoint.GetType()), nameof(options)); + } + + if (address is not null && host is null) + { + if (options.HandshakeTimeout != Timeout.InfiniteTimeSpan && options.HandshakeTimeout != TimeSpan.Zero) + { + linkedCts.CancelAfter(options.HandshakeTimeout); + } + + await connection.FinishConnectAsync(options, null, address, port, linkedCts.Token).ConfigureAwait(false); + return connection; + } + + Debug.Assert(host is not null); + IPAddress[] addresses = await Dns.GetHostAddressesAsync(host, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + if (addresses.Length == 0) + { + throw new SocketException((int)SocketError.HostNotFound); + } + + ExceptionDispatchInfo? lastException = null; + + foreach (IPAddress addr in addresses) + { + if (connection._disposed != 0) + { + connection = new QuicConnection(); + connection._configuration = config; + } + try + { + await connection.FinishConnectAsync(options, host, addr, port, linkedCts.Token).ConfigureAwait(false); + lastException = null; + config = null; + break; + } + catch (Exception ex) + { + lastException = ExceptionDispatchInfo.Capture(ex); + connection._configuration = null; // Ignore disposal of config on failure, to reuse it. + await connection.DisposeAsync().ConfigureAwait(false); + } + } + + config?.Dispose(); + lastException?.Throw(); } catch (OperationCanceledException) { @@ -289,7 +335,7 @@ internal unsafe QuicConnection(QUIC_HANDLE* handle, QUIC_NEW_CONNECTION_INFO* in #endif } - private async ValueTask FinishConnectAsync(QuicClientConnectionOptions options, CancellationToken cancellationToken = default) + private async ValueTask FinishConnectAsync(QuicClientConnectionOptions options, string? host, IPAddress address, int port, CancellationToken cancellationToken = default) { if (_connectedTcs.TryInitialize(out ValueTask valueTask, this, cancellationToken)) { @@ -297,28 +343,6 @@ private async ValueTask FinishConnectAsync(QuicClientConnectionOptions options, _defaultStreamErrorCode = options.DefaultStreamErrorCode; _defaultCloseErrorCode = options.DefaultCloseErrorCode; - if (!options.RemoteEndPoint.TryParse(out string? host, out IPAddress? address, out int port)) - { - throw new ArgumentException(SR.Format(SR.net_quic_unsupported_endpoint_type, options.RemoteEndPoint.GetType()), nameof(options)); - } - - if (address is null) - { - Debug.Assert(host is not null); - - // Given just a ServerName to connect to, msquic would also use the first address after the resolution - // (https://github.com/microsoft/msquic/issues/1181) and it would not return a well-known error code - // for resolution failures we could rely on. By doing the resolution in managed code, we can guarantee - // that a SocketException will surface to the user if the name resolution fails. - IPAddress[] addresses = await Dns.GetHostAddressesAsync(host, cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - if (addresses.Length == 0) - { - throw new SocketException((int)SocketError.HostNotFound); - } - address = addresses[0]; - } - QuicAddr remoteQuicAddress = new IPEndPoint(address, port).ToQuicAddr(); MsQuicHelpers.SetMsQuicParameter(_handle, QUIC_PARAM_CONN_REMOTE_ADDRESS, remoteQuicAddress); @@ -336,7 +360,6 @@ private async ValueTask FinishConnectAsync(QuicClientConnectionOptions options, options.ClientAuthenticationOptions.CertificateRevocationCheckMode, options.ClientAuthenticationOptions.RemoteCertificateValidationCallback, options.ClientAuthenticationOptions.CertificateChainPolicy?.Clone()); - _configuration = MsQuicConfiguration.Create(options); // RFC 6066 forbids IP literals. // IDN mapping is handled by MsQuic. @@ -349,7 +372,7 @@ private async ValueTask FinishConnectAsync(QuicClientConnectionOptions options, { ThrowHelper.ThrowIfMsQuicError(MsQuicApi.Api.ConnectionStart( _handle, - _configuration, + _configuration!, (ushort)remoteQuicAddress.Family, (sbyte*)targetHostPtr, (ushort)port),