-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[QUIC] Try every ip address on host connection #99204
Changes from 3 commits
78ff704
5ea605f
e819731
fdbf47a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -65,18 +65,64 @@ public static ValueTask<QuicConnection> ConnectAsync(QuicClientConnectionOptions | |
|
||
static async ValueTask<QuicConnection> 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); | ||
} | ||
Comment on lines
+83
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if this is desired behavior, but currently if we are not successful to connect every ip address that we get from DNS resolution, we're going to throw this: instead of cc @ManickaP There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO unreachable host is better error message than connection timeout. ConnectionTimeout implies we reached the host but the host was slow in completing the connection. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think in sockets the exception depends on the condition. If the port is closed and actively refusing connection via RST or ICMP we would get unreachable error, if it fails on timeout e.g. no response we would throw the timeout. Note that for tests on loopback there is observable difference between Windows and Linux. |
||
|
||
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,36 +335,14 @@ 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)) | ||
{ | ||
_canAccept = options.MaxInboundBidirectionalStreams > 0 || options.MaxInboundUnidirectionalStreams > 0; | ||
_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), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels this is wrong. I think the
options.HandshakeTimeout
should cover timeout forConnectAsync
itself not for each address we try. In practice the observed timeout would depend on number of addresses given host resolves to.But we do not have any real explanation
https://learn.microsoft.com/en-us/dotnet/api/system.net.quic.quicconnectionoptions.handshaketimeout?view=net-9.0
On the other hand, It already has
CancellationToken
to it somewhat feels duplicate.