diff --git a/src/libraries/System.Net.Quic/src/Resources/Strings.resx b/src/libraries/System.Net.Quic/src/Resources/Strings.resx index 0aff3df0a71f..dc8b015c3bf6 100644 --- a/src/libraries/System.Net.Quic/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Quic/src/Resources/Strings.resx @@ -195,21 +195,12 @@ QuicConnection is configured to not accept any streams. - - The local address is already in use. - - - The server is currently unreachable. - The server refused the connection. A QUIC protocol error was encountered - - Another QUIC listener is already listening on one of the requested application protocols on the same port. - A version negotiation error was encountered. @@ -219,9 +210,6 @@ The connection timed out from inactivity. - - Binding to socket failed, likely caused by a family mismatch between local and remote address. - Authentication failed: {0}. @@ -239,5 +227,8 @@ The supplied {0} is an invalid size for the {1} end point. + + User configured callback failed. + diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs index 333ef433bb99..dad23bfc342c 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.SslConnectionOptions.cs @@ -68,6 +68,7 @@ public unsafe int ValidateCertificate(QUIC_BUFFER* certificatePtr, QUIC_BUFFER* SslPolicyErrors sslPolicyErrors = SslPolicyErrors.None; IntPtr certificateBuffer = 0; int certificateLength = 0; + bool wrapException = false; X509Chain? chain = null; X509Certificate2? result = null; @@ -130,8 +131,10 @@ public unsafe int ValidateCertificate(QUIC_BUFFER* certificatePtr, QUIC_BUFFER* int status = QUIC_STATUS_SUCCESS; if (_validationCallback is not null) { + wrapException = true; if (!_validationCallback(_connection, result, chain, sslPolicyErrors)) { + wrapException = false; if (_isClient) { throw new AuthenticationException(SR.net_quic_cert_custom_validation); @@ -153,9 +156,14 @@ public unsafe int ValidateCertificate(QUIC_BUFFER* certificatePtr, QUIC_BUFFER* certificate = result; return status; } - catch + catch (Exception ex) { result?.Dispose(); + if (wrapException) + { + throw new QuicException(QuicError.CallbackError, null, SR.net_quic_callback_error, ex); + } + throw; } finally 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 7b4c6613afab..eac32181dba2 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 @@ -209,13 +209,16 @@ public async ValueTask AcceptConnectionAsync(CancellationToken c /// The TLS ClientHello data. private async void StartConnectionHandshake(QuicConnection connection, SslClientHelloInfo clientHello) { + bool wrapException = false; CancellationToken cancellationToken = default; try { using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token); linkedCts.CancelAfter(QuicDefaults.HandshakeTimeout); cancellationToken = linkedCts.Token; + wrapException = true; QuicServerConnectionOptions options = await _connectionOptionsCallback(connection, clientHello, cancellationToken).ConfigureAwait(false); + wrapException = 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)) @@ -267,7 +270,10 @@ private async void StartConnectionHandshake(QuicConnection connection, SslClient } await connection.DisposeAsync().ConfigureAwait(false); - if (!_acceptQueue.Writer.TryWrite(ex)) + if (!_acceptQueue.Writer.TryWrite( + wrapException ? + ExceptionDispatchInfo.SetCurrentStackTrace(new QuicException(QuicError.CallbackError, null, SR.net_quic_callback_error, ex)) : + ex)) { // Channel has been closed, connection is already disposed, do nothing. } diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs index 1a15e3f373f0..0471fdb2bbb4 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs @@ -384,7 +384,8 @@ public async Task CertificateCallbackThrowPropagates() clientOptions.ClientAuthenticationOptions.TargetHost = "foobar1"; - await Assert.ThrowsAsync(() => CreateQuicConnection(clientOptions).AsTask()); + Exception exception = await AssertThrowsQuicExceptionAsync(QuicError.CallbackError, async () => await CreateQuicConnection(clientOptions)); + Assert.True(exception.InnerException is ArithmeticException); await Assert.ThrowsAsync(async () => await listener.AcceptConnectionAsync()); // Make sure the listener is still usable and there is no lingering bad connection diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs index 68790da90195..68509bf6b557 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs @@ -96,8 +96,42 @@ public async Task AcceptConnectionAsync_ThrowingOptionsCallback_Throws(bool useF await using QuicListener listener = await CreateQuicListener(listenerOptions); ValueTask connectTask = CreateQuicConnection(listener.LocalEndPoint); - Exception exception = await Assert.ThrowsAsync(async () => await listener.AcceptConnectionAsync()); - Assert.Equal(expectedMessage, exception.Message); + + Exception exception = await AssertThrowsQuicExceptionAsync(QuicError.CallbackError, async () => await listener.AcceptConnectionAsync()); + Assert.NotNull(exception.InnerException); + Assert.Equal(expectedMessage, exception.InnerException.Message); + await Assert.ThrowsAsync(() => connectTask.AsTask()); + } + + [Fact] + public async Task AcceptConnectionAsync_ThrowingCallbackOde_KeepRunning() + { + bool firstRun = true; + + QuicListenerOptions listenerOptions = CreateQuicListenerOptions(); + // Throw an exception, which should throw the same from accept. + listenerOptions.ConnectionOptionsCallback = (_, _, _) => + { + if (firstRun) + { + firstRun = false; + throw new ObjectDisposedException("failed"); + } + + return ValueTask.FromResult(CreateQuicServerOptions()); + }; + await using QuicListener listener = await CreateQuicListener(listenerOptions); + + ValueTask connectTask = CreateQuicConnection(listener.LocalEndPoint); + + Exception exception = await AssertThrowsQuicExceptionAsync(QuicError.CallbackError, async () => await listener.AcceptConnectionAsync()); + Assert.True(exception.InnerException is ObjectDisposedException); + await Assert.ThrowsAsync(() => connectTask.AsTask()); + + // Throwing ODE in callback should keep Listener running + connectTask = CreateQuicConnection(listener.LocalEndPoint); + await using QuicConnection serverConnection = await listener.AcceptConnectionAsync(); + await using QuicConnection clientConnection = await connectTask; } [Theory]