diff --git a/src/libraries/System.Net.Quic/src/Resources/Strings.resx b/src/libraries/System.Net.Quic/src/Resources/Strings.resx index 09e0e5dc47fd95..0aff3df0a71f73 100644 --- a/src/libraries/System.Net.Quic/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Quic/src/Resources/Strings.resx @@ -171,6 +171,9 @@ Connection timed out waiting for a response from the peer. + + Connection handshake was canceled due to the configured timeout of {0} seconds elapsing. + '{0}' is not supported by System.Net.Quic. @@ -220,7 +223,7 @@ Binding to socket failed, likely caused by a family mismatch between local and remote address. - Authentication failed. {0} + Authentication failed: {0}. 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 d46ce9ecff409c..fdc06d31632249 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 @@ -399,7 +399,7 @@ public async ValueTask AcceptInboundStreamAsync(CancellationToken ca } catch (ChannelClosedException ex) when (ex.InnerException is not null) { - ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + ExceptionDispatchInfo.Throw(ex.InnerException); throw; } finally @@ -608,7 +608,7 @@ private static unsafe int NativeCallback(QUIC_HANDLE* connection, void* context, } /// - /// If not closed explicitly by , closes the connection silently (leading to idle timeout on the peer side). + /// If not closed explicitly by , closes the connection with the . /// And releases all resources associated with the connection. /// /// A task that represents the asynchronous dispose operation. @@ -619,7 +619,7 @@ public async ValueTask DisposeAsync() return; } - // Check if the connection has been shut down and if not, shut it down silently. + // Check if the connection has been shut down and if not, shut it down. if (_shutdownTcs.TryInitialize(out ValueTask valueTask, this)) { unsafe diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicDefaults.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicDefaults.cs index 08eacf261caef9..4715effc5dbaf2 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicDefaults.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicDefaults.cs @@ -32,4 +32,9 @@ internal static partial class QuicDefaults /// Max value for application error codes that can be sent by QUIC, see . /// public const long MaxErrorCodeValue = (1L << 62) - 1; + + /// + /// Our own imposed timeout in the handshake process, since in certain cases MsQuic will not impose theirs, see . + /// + public static readonly TimeSpan HandshakeTimeout = TimeSpan.FromSeconds(10); } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicException.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicException.cs index f254c9b2f7a86b..83b2cb588a1e99 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicException.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicException.cs @@ -17,7 +17,18 @@ public sealed class QuicException : IOException /// The application protocol error code associated with the error. /// The message for the exception. public QuicException(QuicError error, long? applicationErrorCode, string message) - : base(message) + : this(error, applicationErrorCode, message, null) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The error associated with the exception. + /// The application protocol error code associated with the error. + /// The message for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + internal QuicException(QuicError error, long? applicationErrorCode, string message, Exception? innerException) + : base(message, innerException) { QuicError = error; ApplicationErrorCode = applicationErrorCode; diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.PendingConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.PendingConnection.cs deleted file mode 100644 index e69527eea783de..00000000000000 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.PendingConnection.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Security; -using System.Threading; -using System.Threading.Tasks; - -namespace System.Net.Quic; - -public sealed partial class QuicListener -{ - /// - /// Represents a connection that's been received via NEW_CONNECTION but not accepted yet. - /// - /// - /// When a new connection is being received, the handshake process needs to get started. - /// More specifically, the server-side connection options, including server certificate, need to selected and provided back to MsQuic. - /// Finally, after the handshake completes and the connection is established, the result needs to be stored and subsequently retrieved from within . - /// - private sealed class PendingConnection : IAsyncDisposable - { - /// - /// Our own imposed timeout in the handshake process, since in certain cases MsQuic will not impose theirs, see . - /// - /// - private static readonly TimeSpan s_handshakeTimeout = TimeSpan.FromSeconds(10); - - /// - /// It will contain the established in case of a successful handshake; otherwise, null. - /// - private readonly TaskCompletionSource _finishHandshakeTask; - /// - /// Use to impose the handshake timeout. - /// - private readonly CancellationTokenSource _cancellationTokenSource; - - public PendingConnection() - { - _finishHandshakeTask = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _cancellationTokenSource = new CancellationTokenSource(); - } - - /// - /// Kicks off the handshake process. It doesn't propagate the result outside directly but rather stores it in a task available via . - /// - /// - /// The method is async void on purpose so it starts an operation but doesn't wait for the result from the caller's perspective. - /// It does await but that never gets propagated to the caller for which the method ends with the first asynchronously processed await. - /// Once the asynchronous processing finishes, the result is stored in the task field that gets exposed via . - /// - /// The new connection. - /// The TLS ClientHello data. - /// The connection options selection callback. - public async void StartHandshake(QuicConnection connection, SslClientHelloInfo clientHello, Func> connectionOptionsCallback) - { - try - { - _cancellationTokenSource.CancelAfter(s_handshakeTimeout); - QuicServerConnectionOptions options = await connectionOptionsCallback(connection, clientHello, _cancellationTokenSource.Token).ConfigureAwait(false); - options.Validate(nameof(options)); // Validate and fill in defaults for the options. - await connection.FinishHandshakeAsync(options, clientHello.ServerName, _cancellationTokenSource.Token).ConfigureAwait(false); - _finishHandshakeTask.SetResult(connection); - } - catch (Exception ex) - { - // Handshake failed: - // 1. Connection cannot be handed out since it's useless --> return null, listener will wait for another one. - // 2. Shutdown the connection to send a transport error to the peer --> application error code doesn't matter here, use default. - - if (NetEventSource.Log.IsEnabled()) - { - NetEventSource.Error(connection, $"{connection} Connection handshake failed: {ex}"); - } - - await connection.CloseAsync(default).ConfigureAwait(false); - await connection.DisposeAsync().ConfigureAwait(false); - _finishHandshakeTask.SetException(ex); - } - } - - /// - /// Provides access to the result of the handshake started with . - /// - /// A cancellation token that can be used to cancel the asynchronous operation. - /// An asynchronous task that completes with the established connection if it succeeded or null if it failed. - public ValueTask FinishHandshakeAsync(CancellationToken cancellationToken = default) - => new(_finishHandshakeTask.Task.WaitAsync(cancellationToken)); - - - /// - /// Cancels the handshake in progress and awaits for it so that the connection can be safely cleaned from the listener queue. - /// - /// A task that represents the asynchronous dispose operation. - public async ValueTask DisposeAsync() - { - _cancellationTokenSource.Cancel(); - try - { - await _finishHandshakeTask.Task.ConfigureAwait(false); - } - catch - { - // Just swallow the exception, we don't want it to propagate outside of dispose and it has already been logged in StartHandshake catch block. - } - } - } -} diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs index de41265c05c962..a8d115e28e0f5f 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Net.Security; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; @@ -78,15 +79,26 @@ public static ValueTask ListenAsync(QuicListenerOptions options, C /// private readonly ValueTaskSource _shutdownTcs = new ValueTaskSource(); + /// + /// Used to stop pending connections when is requested. + /// + private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource(); + /// /// Selects connection options for incoming connections. /// private readonly Func> _connectionOptionsCallback; /// - /// Incoming connections waiting to be accepted via AcceptAsync. + /// Incoming connections waiting to be accepted via AcceptAsync. The item will either be fully connected or if the handshake failed. /// - private readonly Channel _acceptQueue; + private readonly Channel _acceptQueue; + /// + /// Allowed number of pending incoming connections. + /// Actual value correspond to - # in progress - .Count and is always >= 0. + /// Starts as , decrements with each NEW_CONNECTION, increments with . + /// + private int _pendingConnectionsCapacity; /// /// The actual listening endpoint. @@ -122,7 +134,8 @@ private unsafe QuicListener(QuicListenerOptions options) // Save the connection options before starting the listener _connectionOptionsCallback = options.ConnectionOptionsCallback; - _acceptQueue = Channel.CreateBounded(new BoundedChannelOptions(options.ListenBacklog) { SingleWriter = true }); + _acceptQueue = Channel.CreateUnbounded(); + _pendingConnectionsCapacity = options.ListenBacklog; // Start the listener, from now on MsQuic events will come. using MsQuicBuffers alpnBuffers = new MsQuicBuffers(); @@ -151,9 +164,6 @@ private unsafe QuicListener(QuicListenerOptions options) /// Accepts an inbound . /// /// - /// Note that doesn't have a mechanism to report inbound connections that fail the handshake process. - /// Such connections are only logged by the listener and never surfaced on the outside. - /// /// Propagates exceptions from , including validation errors from misconfigured , e.g. . /// Also propagates exceptions from failed connection handshake, e.g. , . /// @@ -166,15 +176,19 @@ public async ValueTask AcceptConnectionAsync(CancellationToken c GCHandle keepObject = GCHandle.Alloc(this); try { - PendingConnection pendingConnection = await _acceptQueue.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); - await using (pendingConnection.ConfigureAwait(false)) + object item = await _acceptQueue.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + Interlocked.Increment(ref _pendingConnectionsCapacity); + + if (item is QuicConnection connection) { - return await pendingConnection.FinishHandshakeAsync(cancellationToken).ConfigureAwait(false); + return connection; } + ExceptionDispatchInfo.Throw((Exception)item); + throw null; // Never reached. } catch (ChannelClosedException ex) when (ex.InnerException is not null) { - ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + ExceptionDispatchInfo.Throw(ex.InnerException); throw; } finally @@ -183,20 +197,102 @@ public async ValueTask AcceptConnectionAsync(CancellationToken c } } + /// + /// Kicks off the handshake process. It doesn't propagate the result outside directly but rather stores it in _acceptQueue for . + /// + /// + /// The method is async void on purpose so it starts an operation but doesn't wait for the result from the caller's perspective. + /// It does await but that never gets propagated to the caller for which the method ends with the first asynchronously processed await. + /// Once the asynchronous processing finishes, the result is stored in _acceptQueue. + /// + /// The new connection. + /// The TLS ClientHello data. + private async void StartConnectionHandshake(QuicConnection connection, SslClientHelloInfo clientHello) + { + CancellationToken cancellationToken = default; + try + { + CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token); + linkedCts.CancelAfter(QuicDefaults.HandshakeTimeout); + cancellationToken = linkedCts.Token; + QuicServerConnectionOptions options = await _connectionOptionsCallback(connection, clientHello, cancellationToken).ConfigureAwait(false); + options.Validate(nameof(options)); // Validate and fill in defaults for the options. + await connection.FinishHandshakeAsync(options, clientHello.ServerName, cancellationToken).ConfigureAwait(false); + if (!_acceptQueue.Writer.TryWrite(connection)) + { + // Channel has been closed, dispose the connection as it'll never be handed out. + await connection.DisposeAsync().ConfigureAwait(false); + } + } + catch (OperationCanceledException) when (_disposeCts.IsCancellationRequested) + { + // Handshake stopped by QuicListener.DisposeAsync: + // 1. Dispose the connection and by that shut it down --> application error code doesn't matter here as this is a transport error. + // 2. Connection won't be handed out since listener has stopped --> do not propagate anything. + + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Info(connection, $"{connection} Connection handshake stopped by listener"); + } + + await connection.DisposeAsync().ConfigureAwait(false); + } + catch (OperationCanceledException oce) when (cancellationToken.IsCancellationRequested) + { + // Handshake cancelled by QuicDefaults.HandshakeTimeout, probably stalled: + // 1. Connection must be killed so dispose it and by that shut it down --> application error code doesn't matter here as this is a transport error. + // 2. Connection won't be handed out since it's useless --> propagate appropriate exception, listener will pass it to the caller. + + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Error(connection, $"{connection} Connection handshake timed out: {oce}"); + } + + Exception ex = ExceptionDispatchInfo.SetCurrentStackTrace(new QuicException(QuicError.ConnectionTimeout, null, SR.Format(SR.net_quic_handshake_timeout, QuicDefaults.HandshakeTimeout), oce)); + await connection.DisposeAsync().ConfigureAwait(false); + if (!_acceptQueue.Writer.TryWrite(ex)) + { + // Channel has been closed, connection is already disposed, do nothing. + } + } + catch (Exception ex) + { + // Handshake failed: + // 1. Dispose the connection and by that shut it down --> application error code doesn't matter here as this is a transport error. + // 2. Connection cannot be handed out since it's useless --> propagate the exception as-is, listener will pass it to the caller. + + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Error(connection, $"{connection} Connection handshake failed: {ex}"); + } + + await connection.DisposeAsync().ConfigureAwait(false); + if (!_acceptQueue.Writer.TryWrite(ex)) + { + // Channel has been closed, connection is already disposed, do nothing. + } + } + } + private unsafe int HandleEventNewConnection(ref NEW_CONNECTION_DATA data) { // Check if there's capacity to have another connection waiting to be accepted. - PendingConnection pendingConnection = new PendingConnection(); - if (!_acceptQueue.Writer.TryWrite(pendingConnection)) + if (Interlocked.Decrement(ref _pendingConnectionsCapacity) < 0) { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Info(this, $"{this} Refusing connection from {data.Info->RemoteAddress->ToIPEndPoint()} due to backlog limit"); + } + + Interlocked.Increment(ref _pendingConnectionsCapacity); return QUIC_STATUS_CONNECTION_REFUSED; } QuicConnection connection = new QuicConnection(data.Connection, data.Info); SslClientHelloInfo clientHello = new SslClientHelloInfo(data.Info->ServerNameLength > 0 ? Marshal.PtrToStringUTF8((IntPtr)data.Info->ServerName, data.Info->ServerNameLength) : "", SslProtocols.Tls13); - // Kicks off the rest of the handshake in the background. - pendingConnection.StartHandshake(connection, clientHello, _connectionOptionsCallback); + // Kicks off the rest of the handshake in the background, the process itself will enqueue the result in the accept queue. + StartConnectionHandshake(connection, clientHello); return QUIC_STATUS_SUCCESS; @@ -276,10 +372,14 @@ public async ValueTask DisposeAsync() _handle.Dispose(); // Flush the queue and dispose all remaining connections. + _disposeCts.Cancel(); _acceptQueue.Writer.TryComplete(ExceptionDispatchInfo.SetCurrentStackTrace(ThrowHelper.GetOperationAbortedException())); - while (_acceptQueue.Reader.TryRead(out PendingConnection? pendingConnection)) + while (_acceptQueue.Reader.TryRead(out object? item)) { - await pendingConnection.DisposeAsync().ConfigureAwait(false); + if (item is QuicConnection connection) + { + await connection.DisposeAsync().ConfigureAwait(false); + } } } } diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs index d577a33f80feee..e00f758965a7a5 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs @@ -1,9 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using System.Net.Sockets; using System.Net.Security; +using System.Runtime.ExceptionServices; +using System.Security.Authentication; using Xunit; using Xunit.Abstractions; @@ -87,6 +91,216 @@ public async Task AcceptConnectionAsync_ThrowingOptionsCallback_Throws(bool useF Assert.Equal(expectedMessage, exception.Message); } + [Theory] + [InlineData(true)] + [InlineData(false)] + [OuterLoop("Exercises several seconds long timeout.")] + public async Task AcceptConnectionAsync_SlowOptionsCallback_TimesOut(bool useCancellationToken) + { + QuicListenerOptions listenerOptions = CreateQuicListenerOptions(); + // Stall the options callback to force the timeout. + listenerOptions.ConnectionOptionsCallback = async (connection, hello, cancellationToken) => + { + if (useCancellationToken) + { + var oce = await Assert.ThrowsAnyAsync(() => Task.Delay(QuicDefaults.HandshakeTimeout + TimeSpan.FromSeconds(1), cancellationToken)); + Assert.True(cancellationToken.IsCancellationRequested); + Assert.Equal(cancellationToken, oce.CancellationToken); + ExceptionDispatchInfo.Throw(oce); + } + await Task.Delay(QuicDefaults.HandshakeTimeout + TimeSpan.FromSeconds(1)); + return CreateQuicServerOptions(); + }; + await using QuicListener listener = await CreateQuicListener(listenerOptions); + + ValueTask connectTask = CreateQuicConnection(listener.LocalEndPoint); + Exception exception = await AssertThrowsQuicExceptionAsync(QuicError.ConnectionTimeout, async () => await listener.AcceptConnectionAsync()); + Assert.Equal(SR.Format(SR.net_quic_handshake_timeout, QuicDefaults.HandshakeTimeout), exception.Message); + + // Connect attempt should be stopped with "UserCanceled". + var connectException = await Assert.ThrowsAsync(async () => await connectTask); + Assert.Contains(TlsAlertMessage.UserCanceled.ToString(), connectException.Message); + } + + [Fact] + public async Task AcceptConnectionAsync_ListenerDisposed_Throws() + { + var serverDisposed = new TaskCompletionSource(); + var connectAttempted = new TaskCompletionSource(); + + QuicListenerOptions listenerOptions = CreateQuicListenerOptions(); + // Stall the options callback to force the timeout. + listenerOptions.ConnectionOptionsCallback = async (connection, hello, cancellationToken) => + { + connectAttempted.SetResult(); + await serverDisposed.Task; + Assert.True(cancellationToken.IsCancellationRequested); + return null; + }; + QuicListener listener = await CreateQuicListener(listenerOptions); + + // One accept that will have an incoming connection from the client. + ValueTask acceptTask1 = listener.AcceptConnectionAsync(); + // Another accept without any incoming connection. + ValueTask acceptTask2 = listener.AcceptConnectionAsync(); + + // Attempt to connect the first accept. + ValueTask connectTask = CreateQuicConnection(listener.LocalEndPoint); + + // First, wait for the connect attempt to reach the server; otherwise, the client exception might end up being HostUnreachable. + // Then, dispose the listener and un-block the waiting server options callback. + await connectAttempted.Task; + await listener.DisposeAsync(); + serverDisposed.SetResult(); + + var accept1Exception = await AssertThrowsQuicExceptionAsync(QuicError.OperationAborted, async () => await acceptTask1); + var accept2Exception = await AssertThrowsQuicExceptionAsync(QuicError.OperationAborted, async () => await acceptTask2); + + Assert.Equal(accept1Exception, accept2Exception); + + // Connect attempt should be stopped with "UserCanceled". + var connectException = await Assert.ThrowsAsync(async () => await connectTask); + Assert.Contains(TlsAlertMessage.UserCanceled.ToString(), connectException.Message); + } + + [Fact] + public async Task Listener_BacklogLimitRefusesConnection_ClientThrows() + { + QuicListenerOptions listenerOptions = CreateQuicListenerOptions(); + listenerOptions.ListenBacklog = 2; + await using QuicListener listener = await CreateQuicListener(listenerOptions); + + // The third connection attempt fails with ConnectionRefused. + await using var clientConnection1 = await CreateQuicConnection(listener.LocalEndPoint); + await using var clientConnection2 = await CreateQuicConnection(listener.LocalEndPoint); + await AssertThrowsQuicExceptionAsync(QuicError.ConnectionRefused, async () => await CreateQuicConnection(listener.LocalEndPoint)); + + // Accept one connection and attempt another one. + await using var serverConnection = await listener.AcceptConnectionAsync(); + await using var clientConnection3 = await CreateQuicConnection(listener.LocalEndPoint); + // Third one again, should fail. + await AssertThrowsQuicExceptionAsync(QuicError.ConnectionRefused, async () => await CreateQuicConnection(listener.LocalEndPoint)); + + // Accept the remaining connection to see that failure do not affect them. + await using var serverConnection2 = await listener.AcceptConnectionAsync(); + await using var serverConnection3 = await listener.AcceptConnectionAsync(); + } + + [Theory] + [InlineData(1, 2)] + [InlineData(2, 1)] + [InlineData(15, 10)] + [InlineData(10, 10)] + [InlineData(10, 15)] + public Task Listener_BacklogLimitRefusesConnection_ParallelClients_ClientThrows(int backlogLimit, int connectCount) + => Listener_BacklogLimitRefusesConnection_ParallelClients_ClientThrows_Core(backlogLimit, connectCount); + + [Theory] + [InlineData(100, 250)] + [InlineData(250, 100)] + [InlineData(100, 99)] + [InlineData(100, 100)] + [InlineData(100, 101)] + [InlineData(15, 100)] + [InlineData(10, 1_000)] + [OuterLoop("Higher number of connections slow the test down.")] + private Task Listener_BacklogLimitRefusesConnection_ParallelClients_ClientThrows_Slow(int backlogLimit, int connectCount) + => Listener_BacklogLimitRefusesConnection_ParallelClients_ClientThrows_Core(backlogLimit, connectCount); + + private async Task Listener_BacklogLimitRefusesConnection_ParallelClients_ClientThrows_Core(int backlogLimit, int connectCount) + { + QuicListenerOptions listenerOptions = CreateQuicListenerOptions(); + listenerOptions.ListenBacklog = backlogLimit; + await using QuicListener listener = await CreateQuicListener(listenerOptions); + + // Kick off requested number of parallel connects. + List connectTasks = new List(); + for (int i = 0; i < connectCount; ++i) + { + connectTasks.Add(CreateQuicConnection(listener.LocalEndPoint).AsTask()); + } + + // Count the number of successful connections and refused connections. + int success = 0; + int failure = 0; + await Parallel.ForEachAsync(connectTasks, async (connectTask, cancellationToken) => + { + try + { + await connectTask; + Interlocked.Increment(ref success); + } + catch (QuicException qex) when (qex.QuicError == QuicError.ConnectionRefused) + { + Interlocked.Increment(ref failure); + } + }); + + // Check that the numbers correspond to backlog limit. + int pendingConnections = 0; + if (backlogLimit < connectCount) + { + pendingConnections = backlogLimit; + Assert.Equal(backlogLimit, success); + Assert.Equal(connectCount - backlogLimit, failure); + } + else + { + pendingConnections = connectCount; + Assert.Equal(connectCount, success); + Assert.Equal(0, failure); + } + + // Accept all connections and check that the next accept pends. + for (int i = 0; i < pendingConnections; ++i) + { + await using var connection = await listener.AcceptConnectionAsync(); + } + + // All pending connection should be accepted and the following call needs to be cancelled. + var cts = new CancellationTokenSource(); + var token = cts.Token; + ValueTask acceptTask = listener.AcceptConnectionAsync(cts.Token); + Assert.False(acceptTask.IsCompleted); + cts.Cancel(); + var oce = await Assert.ThrowsAnyAsync(async () => await acceptTask); + Assert.Equal(cts.Token, oce.CancellationToken); + } + + [Fact] + public async Task AcceptConnectionAsync_CancelThrows() + { + await using QuicListener listener = await CreateQuicListener(); + + var cts = new CancellationTokenSource(); + var token = cts.Token; + + var acceptTask = listener.AcceptConnectionAsync(token); + cts.Cancel(); + + var exception = await Assert.ThrowsAnyAsync(async () => await acceptTask); + Assert.Equal(token, exception.CancellationToken); + } + + [Fact] + public async Task AcceptConnectionAsync_ClientConnects_CancelIgnored() + { + await using QuicListener listener = await CreateQuicListener(); + + var cts = new CancellationTokenSource(); + var token = cts.Token; + + var acceptTask = listener.AcceptConnectionAsync(token); + await using var clientConnection = await CreateQuicConnection(listener.LocalEndPoint); + + await Task.Delay(TimeSpan.FromSeconds(0.5)); + + // Cancellation should get ignored as the connection was connected. + cts.Cancel(); + + await using var serverConnection = await acceptTask; + } + [Fact] public async Task ListenOnAlreadyUsedPort_Throws_AddressInUse() { diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/System.Net.Quic.Functional.Tests.csproj b/src/libraries/System.Net.Quic/tests/FunctionalTests/System.Net.Quic.Functional.Tests.csproj index 70e4d8c5680d51..c08804402f30c9 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/System.Net.Quic.Functional.Tests.csproj +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/System.Net.Quic.Functional.Tests.csproj @@ -39,6 +39,7 @@ +