From 60af631f892f65ccae9d95aeb835586d25bd8a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= <11718369+ManickaP@users.noreply.github.com> Date: Sun, 10 Jul 2022 22:13:50 +0200 Subject: [PATCH] [QUIC] API QuicListener (#71579) * QuicListener new API shape including compilable implementation * Quic and Http tests compile * Tests work * Feedback * Fix Windows error * Feedback * Listener comment; PreviewFeature attribute * Updated helix image with msquic * Cleaned up PreviewFeature attribute. * Feedback * Split event handlers into methods. * Added comments for pending connection. * Switch expression for HandleEvent --- .../coreclr/templates/helix-queues-setup.yml | 2 +- .../libraries/helix-queues-setup.yml | 4 +- .../System/Net/Http/Http3LoopbackServer.cs | 37 +- .../src/System.Net.Http.csproj | 1 + .../Http/SocketsHttpHandler/ConnectHelper.cs | 5 +- .../SocketsHttpHandler/Http3Connection.cs | 3 +- .../SocketsHttpHandler/HttpConnectionPool.cs | 2 +- .../System.Net.Http.Functional.Tests.csproj | 1 + .../System.Net.Quic/ref/System.Net.Quic.cs | 41 ++- .../ref/System.Net.Quic.csproj | 4 + .../src/ExcludeApiList.PNSE.txt | 6 +- .../src/Resources/Strings.resx | 3 + .../src/System.Net.Quic.csproj | 25 +- .../MsQuic/Internal/MsQuicAddressHelpers.cs | 6 +- .../MsQuic/Internal/MsQuicApi.cs | 8 +- .../MsQuic/Interop/MsQuicSafeHandle.cs | 65 ---- .../Interop/SafeMsQuicConfigurationHandle.cs | 25 +- .../Interop/SafeMsQuicConnectionHandle.cs | 2 +- .../Interop/SafeMsQuicListenerHandle.cs | 14 - .../Interop/SafeMsQuicRegistrationHandle.cs | 14 - .../MsQuic/Interop/SafeMsQuicStreamHandle.cs | 2 +- .../MsQuic/MsQuicConnection.cs | 269 +++++++------- .../Implementations/MsQuic/MsQuicListener.cs | 330 ------------------ .../Net/Quic/Internal/ValueTaskSource.cs | 154 ++++++++ .../Net/Quic/Interop/MsQuicSafeHandle.cs | 120 +++++++ .../System/Net/Quic/NetEventSource.Quic.cs | 2 +- .../Net/Quic/QuicClientConnectionOptions.cs | 33 -- .../Net/Quic/QuicConnection.Unsupported.cs | 9 +- .../src/System/Net/Quic/QuicConnection.cs | 2 + .../System/Net/Quic/QuicConnectionOptions.cs | 86 +++++ .../Quic/QuicListener.PendingConnection.cs | 99 ++++++ .../Net/Quic/QuicListener.Unsupported.cs | 9 +- .../src/System/Net/Quic/QuicListener.cs | 281 ++++++++++++++- .../System/Net/Quic/QuicListenerOptions.cs | 45 +-- .../src/System/Net/Quic/QuicOptions.cs | 35 -- .../MsQuicCipherSuitesPolicyTests.cs | 35 +- .../tests/FunctionalTests/MsQuicTests.cs | 266 +++++++++----- .../FunctionalTests/QuicConnectionTests.cs | 11 +- .../FunctionalTests/QuicListenerTests.cs | 12 +- ...icStreamConnectedStreamConformanceTests.cs | 10 +- .../tests/FunctionalTests/QuicStreamTests.cs | 11 +- .../tests/FunctionalTests/QuicTestBase.cs | 41 ++- .../System.Net.Quic.Functional.Tests.csproj | 1 + .../src/MatchingRefApiCompatBaseline.txt | 1 + .../src/ReferenceAssemblyExclusions.txt | 1 + .../System/Net/Security/SslClientHelloInfo.cs | 2 +- 46 files changed, 1238 insertions(+), 897 deletions(-) delete mode 100644 src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicSafeHandle.cs delete mode 100644 src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicListenerHandle.cs delete mode 100644 src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicRegistrationHandle.cs delete mode 100644 src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicListener.cs create mode 100644 src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs create mode 100644 src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/MsQuicSafeHandle.cs delete mode 100644 src/libraries/System.Net.Quic/src/System/Net/Quic/QuicClientConnectionOptions.cs create mode 100644 src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnectionOptions.cs create mode 100644 src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.PendingConnection.cs delete mode 100644 src/libraries/System.Net.Quic/src/System/Net/Quic/QuicOptions.cs diff --git a/eng/pipelines/coreclr/templates/helix-queues-setup.yml b/eng/pipelines/coreclr/templates/helix-queues-setup.yml index 4f69c89f6ba13..fdb3cacfc5c46 100644 --- a/eng/pipelines/coreclr/templates/helix-queues-setup.yml +++ b/eng/pipelines/coreclr/templates/helix-queues-setup.yml @@ -98,7 +98,7 @@ jobs: - (Debian.11.Amd64)Ubuntu.1804.amd64@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-amd64-20210304164428-5a7c380 - Ubuntu.1804.Amd64 - (Centos.8.Amd64)Ubuntu.1604.amd64@mcr.microsoft.com/dotnet-buildtools/prereqs:centos-8-helix-20201229003624-c1bf759 - - (Fedora.34.Amd64)Ubuntu.1604.amd64@mcr.microsoft.com/dotnet-buildtools/prereqs:fedora-34-helix-20220523142223-4f64125 + - (Fedora.34.Amd64)Ubuntu.1604.amd64@mcr.microsoft.com/dotnet-buildtools/prereqs:fedora-34-helix-20220708122731-4f64125 - RedHat.7.Amd64 # OSX arm64 diff --git a/eng/pipelines/libraries/helix-queues-setup.yml b/eng/pipelines/libraries/helix-queues-setup.yml index ae8623cce234e..bf61291a60a17 100644 --- a/eng/pipelines/libraries/helix-queues-setup.yml +++ b/eng/pipelines/libraries/helix-queues-setup.yml @@ -62,14 +62,14 @@ jobs: - (Centos.8.Amd64.Open)Ubuntu.1804.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:centos-8-helix-20201229003624-c1bf759 - RedHat.7.Amd64.Open - SLES.15.Amd64.Open - - (Fedora.34.Amd64.Open)Ubuntu.1804.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:fedora-34-helix-20220523142223-4f64125 + - (Fedora.34.Amd64.Open)Ubuntu.1804.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:fedora-34-helix-20220708122731-4f64125 - (Ubuntu.2204.Amd64.Open)Ubuntu.1804.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-amd64-20220504035722-1b9461f - (Debian.10.Amd64.Open)Ubuntu.1804.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-10-helix-amd64-bfcd90a-20200121150006 - ${{ if or(ne(parameters.jobParameters.testScope, 'outerloop'), ne(parameters.jobParameters.runtimeFlavor, 'mono')) }}: - ${{ if or(eq(parameters.jobParameters.isExtraPlatforms, true), eq(parameters.jobParameters.includeAllPlatforms, true)) }}: - (Centos.8.Amd64.Open)Ubuntu.1604.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:centos-8-helix-20201229003624-c1bf759 - SLES.15.Amd64.Open - - (Fedora.34.Amd64.Open)ubuntu.1604.amd64.open@mcr.microsoft.com/dotnet-buildtools/prereqs:fedora-34-helix-20220523142223-4f64125 + - (Fedora.34.Amd64.Open)ubuntu.1604.amd64.open@mcr.microsoft.com/dotnet-buildtools/prereqs:fedora-34-helix-20220708122731-4f64125 - (Ubuntu.2204.Amd64.Open)Ubuntu.1804.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-helix-amd64-20220504035722-1b9461f - (Debian.11.Amd64.Open)Ubuntu.1804.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-11-helix-amd64-20210304164428-5a7c380 - (Mariner.1.0.Amd64.Open)ubuntu.1604.amd64.open@mcr.microsoft.com/dotnet-buildtools/prereqs:cbl-mariner-1.0-helix-20210528192219-92bf620 diff --git a/src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs index c87eb6c78351b..ba5109bc24bd7 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs @@ -17,7 +17,7 @@ public sealed class Http3LoopbackServer : GenericLoopbackServer private X509Certificate2 _cert; private QuicListener _listener; - public override Uri Address => new Uri($"https://{_listener.ListenEndPoint}/"); + public override Uri Address => new Uri($"https://{_listener.LocalEndPoint}/"); public Http3LoopbackServer(Http3Options options = null) { @@ -28,18 +28,29 @@ public Http3LoopbackServer(Http3Options options = null) var listenerOptions = new QuicListenerOptions() { ListenEndPoint = new IPEndPoint(options.Address, 0), - ServerAuthenticationOptions = new SslServerAuthenticationOptions + ApplicationProtocols = new List { - EnabledSslProtocols = options.SslProtocols, - ApplicationProtocols = new List - { - new SslApplicationProtocol(options.Alpn) - }, - ServerCertificate = _cert, - ClientCertificateRequired = false + new SslApplicationProtocol(options.Alpn) }, - MaxUnidirectionalStreams = options.MaxUnidirectionalStreams, - MaxBidirectionalStreams = options.MaxBidirectionalStreams, + ConnectionOptionsCallback = (_, _, _) => + { + var serverOptions = new QuicServerConnectionOptions() + { + MaxBidirectionalStreams = options.MaxBidirectionalStreams, + MaxUnidirectionalStreams = options.MaxUnidirectionalStreams, + ServerAuthenticationOptions = new SslServerAuthenticationOptions + { + EnabledSslProtocols = options.SslProtocols, + ApplicationProtocols = new List + { + new SslApplicationProtocol(options.Alpn) + }, + ServerCertificate = _cert, + ClientCertificateRequired = false + } + }; + return ValueTask.FromResult(serverOptions); + } }; ValueTask valueTask = QuicListener.ListenAsync(listenerOptions); @@ -49,7 +60,7 @@ public Http3LoopbackServer(Http3Options options = null) public override void Dispose() { - _listener.Dispose(); + _listener.DisposeAsync().GetAwaiter().GetResult(); _cert.Dispose(); } @@ -133,7 +144,7 @@ public class Http3Options : GenericLoopbackOptions public Http3Options() { - MaxUnidirectionalStreams = 100; + MaxUnidirectionalStreams = 10; MaxBidirectionalStreams = 100; Alpn = SslApplicationProtocol.Http3.ToString(); } diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index a40be7c1c50c5..858f2f57425a1 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -4,6 +4,7 @@ true $(DefineConstants);HTTP_DLL $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Linux;$(NetCoreAppCurrent)-OSX;$(NetCoreAppCurrent)-FreeBSD;$(NetCoreAppCurrent)-MacCatalyst;$(NetCoreAppCurrent)-iOS;$(NetCoreAppCurrent)-tvOS;$(NetCoreAppCurrent)-Browser;$(NetCoreAppCurrent)-illumos;$(NetCoreAppCurrent)-Solaris;$(NetCoreAppCurrent)-Android;$(NetCoreAppCurrent) + true diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs index ec2e912e4a35c..ebe0ba529a0b9 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs @@ -104,11 +104,14 @@ public static async ValueTask EstablishSslConnectionAsync(SslClientAu [SupportedOSPlatform("windows")] [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] - public static async ValueTask ConnectQuicAsync(HttpRequestMessage request, DnsEndPoint endPoint, SslClientAuthenticationOptions clientAuthenticationOptions, CancellationToken cancellationToken) + public static async ValueTask ConnectQuicAsync(HttpRequestMessage request, DnsEndPoint endPoint, TimeSpan idleTimeout, SslClientAuthenticationOptions clientAuthenticationOptions, CancellationToken cancellationToken) { clientAuthenticationOptions = SetUpRemoteCertificateValidationCallback(clientAuthenticationOptions, request); QuicConnection connection = await QuicConnection.ConnectAsync(new QuicClientConnectionOptions() { + MaxBidirectionalStreams = 0, // Client doesn't support inbound streams: https://www.rfc-editor.org/rfc/rfc9114.html#name-bidirectional-streams. An extension might change this. + MaxUnidirectionalStreams = 5, // Minimum is 3: https://www.rfc-editor.org/rfc/rfc9114.html#unidirectional-streams (1x control stream + 2x QPACK). Set to 100 if/when support for PUSH streams is added. + IdleTimeout = idleTimeout, RemoteEndPoint = endPoint, ClientAuthenticationOptions = clientAuthenticationOptions }, cancellationToken).ConfigureAwait(false); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs index b9f743adb5150..24438ffa02e3d 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs @@ -540,7 +540,8 @@ private async Task ProcessServerStreamAsync(QuicStream stream) NetEventSource.Info(this, $"Ignoring server-initiated stream of unknown type {unknownStreamType}."); } - stream.AbortWrite((long)Http3ErrorCode.StreamCreationError); + stream.AbortRead((long)Http3ErrorCode.StreamCreationError); + stream.Dispose(); return; } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 633cc33b0d9f3..dfa7f096c5cf9 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -882,7 +882,7 @@ private async ValueTask GetHttp3ConnectionAsync(HttpRequestMess QuicConnection quicConnection; try { - quicConnection = await ConnectHelper.ConnectQuicAsync(request, new DnsEndPoint(authority.IdnHost, authority.Port), _sslOptionsHttp3!, cancellationToken).ConfigureAwait(false); + quicConnection = await ConnectHelper.ConnectQuicAsync(request, new DnsEndPoint(authority.IdnHost, authority.Port), _poolManager.Settings._pooledConnectionIdleTimeout, _sslOptionsHttp3!, cancellationToken).ConfigureAwait(false); } catch (Exception e) { diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj index 3f697770ad136..8a56b5033f29e 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj @@ -6,6 +6,7 @@ true $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Linux;$(NetCoreAppCurrent)-Browser;$(NetCoreAppCurrent)-OSX true + true diff --git a/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs b/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs index adcecf5a5eff3..1d72607683bdb 100644 --- a/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs +++ b/src/libraries/System.Net.Quic/ref/System.Net.Quic.cs @@ -6,12 +6,12 @@ namespace System.Net.Quic { - public partial class QuicClientConnectionOptions : System.Net.Quic.QuicOptions + public sealed partial class QuicClientConnectionOptions : System.Net.Quic.QuicConnectionOptions { public QuicClientConnectionOptions() { } - public System.Net.Security.SslClientAuthenticationOptions? ClientAuthenticationOptions { get { throw null; } set { } } + public required System.Net.Security.SslClientAuthenticationOptions ClientAuthenticationOptions { get { throw null; } set { } } public System.Net.IPEndPoint? LocalEndPoint { get { throw null; } set { } } - public System.Net.EndPoint? RemoteEndPoint { get { throw null; } set { } } + public required System.Net.EndPoint RemoteEndPoint { get { throw null; } set { } } } public sealed partial class QuicConnection : System.IDisposable { @@ -34,41 +34,48 @@ public void Dispose() { } } public partial class QuicConnectionAbortedException : System.Net.Quic.QuicException { - public QuicConnectionAbortedException(string message, long errorCode) : base(default(string)) { } + public QuicConnectionAbortedException(string message, long errorCode) : base (default(string)) { } public long ErrorCode { get { throw null; } } } + public abstract partial class QuicConnectionOptions + { + internal QuicConnectionOptions() { } + public System.TimeSpan IdleTimeout { get { throw null; } set { } } + public int MaxBidirectionalStreams { get { throw null; } set { } } + public int MaxUnidirectionalStreams { get { throw null; } set { } } + } public partial class QuicException : System.Exception { public QuicException(string? message) { } public QuicException(string? message, System.Exception? innerException) { } public QuicException(string? message, System.Exception? innerException, int result) { } } - public sealed partial class QuicListener : System.IDisposable + public sealed partial class QuicListener : System.IAsyncDisposable { internal QuicListener() { } public static bool IsSupported { get { throw null; } } - public System.Net.IPEndPoint ListenEndPoint { get { throw null; } } + public System.Net.IPEndPoint LocalEndPoint { get { throw null; } } public System.Threading.Tasks.ValueTask AcceptConnectionAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public void Dispose() { } + public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } public static System.Threading.Tasks.ValueTask ListenAsync(System.Net.Quic.QuicListenerOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public override string ToString() { throw null; } } - public partial class QuicListenerOptions : System.Net.Quic.QuicOptions + public sealed partial class QuicListenerOptions { public QuicListenerOptions() { } + public required System.Collections.Generic.List ApplicationProtocols { get { throw null; } set { } } + public required System.Func> ConnectionOptionsCallback { get { throw null; } set { } } public int ListenBacklog { get { throw null; } set { } } - public System.Net.IPEndPoint? ListenEndPoint { get { throw null; } set { } } - public System.Net.Security.SslServerAuthenticationOptions? ServerAuthenticationOptions { get { throw null; } set { } } + public required System.Net.IPEndPoint ListenEndPoint { get { throw null; } set { } } } public partial class QuicOperationAbortedException : System.Net.Quic.QuicException { - public QuicOperationAbortedException(string message) : base(default(string)) { } + public QuicOperationAbortedException(string message) : base (default(string)) { } } - public partial class QuicOptions + public sealed partial class QuicServerConnectionOptions : System.Net.Quic.QuicConnectionOptions { - public QuicOptions() { } - public System.TimeSpan IdleTimeout { get { throw null; } set { } } - public int MaxBidirectionalStreams { get { throw null; } set { } } - public int MaxUnidirectionalStreams { get { throw null; } set { } } + public QuicServerConnectionOptions() { } + public required System.Net.Security.SslServerAuthenticationOptions ServerAuthenticationOptions { get { throw null; } set { } } } public sealed partial class QuicStream : System.IO.Stream { @@ -113,7 +120,7 @@ public override void WriteByte(byte value) { } } public partial class QuicStreamAbortedException : System.Net.Quic.QuicException { - public QuicStreamAbortedException(string message, long errorCode) : base(default(string)) { } + public QuicStreamAbortedException(string message, long errorCode) : base (default(string)) { } public long ErrorCode { get { throw null; } } } } diff --git a/src/libraries/System.Net.Quic/ref/System.Net.Quic.csproj b/src/libraries/System.Net.Quic/ref/System.Net.Quic.csproj index 7a406f5679219..4e5d642f64624 100644 --- a/src/libraries/System.Net.Quic/ref/System.Net.Quic.csproj +++ b/src/libraries/System.Net.Quic/ref/System.Net.Quic.csproj @@ -6,6 +6,10 @@ true + + + + diff --git a/src/libraries/System.Net.Quic/src/ExcludeApiList.PNSE.txt b/src/libraries/System.Net.Quic/src/ExcludeApiList.PNSE.txt index d6b3f48e2a732..e960c9feb456b 100644 --- a/src/libraries/System.Net.Quic/src/ExcludeApiList.PNSE.txt +++ b/src/libraries/System.Net.Quic/src/ExcludeApiList.PNSE.txt @@ -1,2 +1,6 @@ P:System.Net.Quic.QuicConnection.IsSupported -P:System.Net.Quic.QuicListener.IsSupported \ No newline at end of file +P:System.Net.Quic.QuicListener.IsSupported +C:System.Net.Quic.QuicListenerOptions +C:System.Net.Quic.QuicConnectionOptions +C:System.Net.Quic.QuicClientConnectionOptions +C:System.Net.Quic.QuicServerConnectionOptions \ No newline at end of file diff --git a/src/libraries/System.Net.Quic/src/Resources/Strings.resx b/src/libraries/System.Net.Quic/src/Resources/Strings.resx index 5af37faa5af73..7c032ca720035 100644 --- a/src/libraries/System.Net.Quic/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Quic/src/Resources/Strings.resx @@ -181,5 +181,8 @@ The supplied {0} is an invalid size for the {1} end point. + + QuicConnection is configured to not accept any streams. + diff --git a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj index bd0c2e14c66c3..5d35afdb51134 100644 --- a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj +++ b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj @@ -11,23 +11,12 @@ SR.SystemNetQuic_PlatformNotSupported ExcludeApiList.PNSE.txt + + + - - - - - - - - - - - - - - - + @@ -41,8 +30,10 @@ - - + + + + diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Internal/MsQuicAddressHelpers.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Internal/MsQuicAddressHelpers.cs index b7f0ecd263bba..436405fc30281 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Internal/MsQuicAddressHelpers.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Internal/MsQuicAddressHelpers.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Net.Sockets; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.Quic; @@ -10,10 +11,11 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal { internal static class MsQuicAddressHelpers { - internal static unsafe IPEndPoint INetToIPEndPoint(IntPtr pInetAddress) + internal static unsafe IPEndPoint ToIPEndPoint(this ref QuicAddr quicAddress) { // MsQuic always uses storage size as if IPv6 was used - Span addressBytes = new Span((byte*)pInetAddress, Internals.SocketAddress.IPv6AddressSize); + // QuicAddr is native memory, it cannot be moved by GC, thus no need for fixed expression here. + Span addressBytes = new Span((byte*)Unsafe.AsPointer(ref quicAddress), Internals.SocketAddress.IPv6AddressSize); return new Internals.SocketAddress(SocketAddressPal.GetAddressFamily(addressBytes), addressBytes).GetIPEndPoint(); } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Internal/MsQuicApi.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Internal/MsQuicApi.cs index 2078043ddd564..c63f545c9783a 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Internal/MsQuicApi.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Internal/MsQuicApi.cs @@ -20,18 +20,18 @@ internal sealed unsafe class MsQuicApi private static readonly Version MsQuicVersion = new Version(2, 0); - public SafeMsQuicRegistrationHandle Registration { get; } + public MsQuicSafeHandle Registration { get; } public QUIC_API_TABLE* ApiTable { get; } // This is workaround for a bug in ILTrimmer. // Without these DynamicDependency attributes, .ctor() will be removed from the safe handles. // Remove once fixed: https://github.com/mono/linker/issues/1660 - [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(SafeMsQuicRegistrationHandle))] [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(SafeMsQuicConfigurationHandle))] - [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(SafeMsQuicListenerHandle))] [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(SafeMsQuicConnectionHandle))] [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(SafeMsQuicStreamHandle))] + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(MsQuicSafeHandle))] + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(MsQuicContextSafeHandle))] private MsQuicApi(QUIC_API_TABLE* apiTable) { ApiTable = apiTable; @@ -47,7 +47,7 @@ private MsQuicApi(QUIC_API_TABLE* apiTable) QUIC_HANDLE* handle; ThrowIfFailure(ApiTable->RegistrationOpen(&cfg, &handle), "RegistrationOpen failed"); - Registration = new SafeMsQuicRegistrationHandle(handle); + Registration = new MsQuicSafeHandle(handle, apiTable->RegistrationClose, SafeHandleType.Registration); } } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicSafeHandle.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicSafeHandle.cs deleted file mode 100644 index 9b2095bc86d44..0000000000000 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicSafeHandle.cs +++ /dev/null @@ -1,65 +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.Runtime.InteropServices; -using Microsoft.Quic; - -namespace System.Net.Quic.Implementations.MsQuic.Internal -{ - internal abstract class MsQuicSafeHandle : SafeHandle - { - // The index must corespond to SafeHandleType enum value and the value must correspond to MsQuic logging abbreviation string. - // This is used for our logging that uses the same format of object identification as MsQuic to easily correlate log events. - private static readonly string[] TypeName = new string[] - { - " reg", - "cnfg", - "list", - "conn", - "strm" - }; - - private readonly Action _releaseAction; - private readonly string _traceId; - - public override bool IsInvalid => handle == IntPtr.Zero; - - public unsafe QUIC_HANDLE* QuicHandle => (QUIC_HANDLE*)DangerousGetHandle(); - - protected unsafe MsQuicSafeHandle(QUIC_HANDLE* handle, Action releaseAction, SafeHandleType safeHandleType) - : base((IntPtr)handle, ownsHandle: true) - { - _releaseAction = releaseAction; - _traceId = $"[{TypeName[(int)safeHandleType]}][0x{DangerousGetHandle():X11}]"; - - if (NetEventSource.Log.IsEnabled()) - { - NetEventSource.Info(this, "MsQuicSafeHandle created"); - } - } - - protected override bool ReleaseHandle() - { - _releaseAction(handle); - SetHandle(IntPtr.Zero); - - if (NetEventSource.Log.IsEnabled()) - { - NetEventSource.Info(this, "MsQuicSafeHandle released"); - } - - return true; - } - - public override string ToString() => _traceId; - } - - internal enum SafeHandleType - { - Registration, - Configuration, - Listener, - Connection, - Stream - } -} diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicConfigurationHandle.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicConfigurationHandle.cs index 06c52eef54162..00529322c0d8a 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicConfigurationHandle.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicConfigurationHandle.cs @@ -16,7 +16,7 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal internal sealed class SafeMsQuicConfigurationHandle : MsQuicSafeHandle { public unsafe SafeMsQuicConfigurationHandle(QUIC_HANDLE* handle) - : base(handle, ptr => MsQuicApi.Api.ApiTable->ConfigurationClose((QUIC_HANDLE*)ptr), SafeHandleType.Configuration) + : base(handle, MsQuicApi.Api.ApiTable->ConfigurationClose, SafeHandleType.Configuration) { } // TODO: consider moving the static code from here to keep all the handle classes small and simple. @@ -72,7 +72,7 @@ public static SafeMsQuicConfigurationHandle Create(QuicClientConnectionOptions o return Create(options, flags, certificate: certificate, certificateContext: null, options.ClientAuthenticationOptions?.ApplicationProtocols, options.ClientAuthenticationOptions?.CipherSuitesPolicy); } - public static SafeMsQuicConfigurationHandle Create(QuicOptions options, SslServerAuthenticationOptions? serverAuthenticationOptions, string? targetHost = null) + public static SafeMsQuicConfigurationHandle Create(QuicServerConnectionOptions options, SslServerAuthenticationOptions? serverAuthenticationOptions, string? targetHost = null) { QUIC_CREDENTIAL_FLAGS flags = QUIC_CREDENTIAL_FLAGS.NONE; X509Certificate? certificate = serverAuthenticationOptions?.ServerCertificate; @@ -102,7 +102,7 @@ public static SafeMsQuicConfigurationHandle Create(QuicOptions options, SslServe // TODO: this is called from MsQuicListener and when it fails it wreaks havoc in MsQuicListener finalizer. // Consider moving bigger logic like this outside of constructor call chains. - private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options, QUIC_CREDENTIAL_FLAGS flags, X509Certificate? certificate, SslStreamCertificateContext? certificateContext, List? alpnProtocols, CipherSuitesPolicy? cipherSuitesPolicy) + private static unsafe SafeMsQuicConfigurationHandle Create(QuicConnectionOptions options, QUIC_CREDENTIAL_FLAGS flags, X509Certificate? certificate, SslStreamCertificateContext? certificateContext, List? alpnProtocols, CipherSuitesPolicy? cipherSuitesPolicy) { // TODO: some of these checks should be done by the QuicOptions type. if (alpnProtocols == null || alpnProtocols.Count == 0) @@ -147,15 +147,18 @@ private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options, settings.IsSet.PeerBidiStreamCount = 1; settings.PeerBidiStreamCount = (ushort)options.MaxBidirectionalStreams; - settings.IsSet.IdleTimeoutMs = 1; - if (options.IdleTimeout != Timeout.InfiniteTimeSpan) + if (options.IdleTimeout != TimeSpan.Zero) { - if (options.IdleTimeout <= TimeSpan.Zero) throw new Exception("IdleTimeout must not be negative."); - settings.IdleTimeoutMs = (ulong)options.IdleTimeout.TotalMilliseconds; - } - else - { - settings.IdleTimeoutMs = 0; + settings.IsSet.IdleTimeoutMs = 1; + if (options.IdleTimeout != Timeout.InfiniteTimeSpan) + { + if (options.IdleTimeout <= TimeSpan.Zero) throw new Exception("IdleTimeout must not be negative."); + settings.IdleTimeoutMs = (ulong)options.IdleTimeout.TotalMilliseconds; + } + else + { + settings.IdleTimeoutMs = 0; + } } SafeMsQuicConfigurationHandle configurationHandle; diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicConnectionHandle.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicConnectionHandle.cs index 4d308bdadf49b..5a084d6d4af7d 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicConnectionHandle.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicConnectionHandle.cs @@ -8,7 +8,7 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal internal sealed class SafeMsQuicConnectionHandle : MsQuicSafeHandle { public unsafe SafeMsQuicConnectionHandle(QUIC_HANDLE* handle) - : base(handle, ptr => MsQuicApi.Api.ApiTable->ConnectionClose((QUIC_HANDLE*)ptr), SafeHandleType.Connection) + : base(handle, MsQuicApi.Api.ApiTable->ConnectionClose, SafeHandleType.Connection) { } } } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicListenerHandle.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicListenerHandle.cs deleted file mode 100644 index 1188efdcb9a97..0000000000000 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicListenerHandle.cs +++ /dev/null @@ -1,14 +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 Microsoft.Quic; - -namespace System.Net.Quic.Implementations.MsQuic.Internal -{ - internal sealed class SafeMsQuicListenerHandle : MsQuicSafeHandle - { - public unsafe SafeMsQuicListenerHandle(QUIC_HANDLE* handle) - : base(handle, ptr => MsQuicApi.Api.ApiTable->ListenerClose((QUIC_HANDLE*)ptr), SafeHandleType.Listener) - { } - } -} diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicRegistrationHandle.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicRegistrationHandle.cs deleted file mode 100644 index 629946e8291e4..0000000000000 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicRegistrationHandle.cs +++ /dev/null @@ -1,14 +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 Microsoft.Quic; - -namespace System.Net.Quic.Implementations.MsQuic.Internal -{ - internal sealed class SafeMsQuicRegistrationHandle : MsQuicSafeHandle - { - public unsafe SafeMsQuicRegistrationHandle(QUIC_HANDLE* handle) - : base(handle, ptr => MsQuicApi.Api.ApiTable->RegistrationClose((QUIC_HANDLE*)ptr), SafeHandleType.Registration) - { } - } -} diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicStreamHandle.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicStreamHandle.cs index 4cbf56d78c127..d3848b5a9ee1f 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicStreamHandle.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicStreamHandle.cs @@ -8,7 +8,7 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal internal sealed class SafeMsQuicStreamHandle : MsQuicSafeHandle { public unsafe SafeMsQuicStreamHandle(QUIC_HANDLE* handle) - : base(handle, ptr => MsQuicApi.Api.ApiTable->StreamClose((QUIC_HANDLE*)ptr), SafeHandleType.Stream) + : base(handle, MsQuicApi.Api.ApiTable->StreamClose, SafeHandleType.Stream) { } } } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs index 550bbf19cd76f..7020ba2589364 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs @@ -33,6 +33,8 @@ internal sealed class MsQuicConnection : IDisposable private readonly State _state = new State(); private int _disposed; + private bool _canAccept; + private IPEndPoint? _localEndPoint; private readonly EndPoint _remoteEndPoint; private SslApplicationProtocol _negotiatedAlpnProtocol; @@ -45,13 +47,11 @@ internal sealed class State // These exists to prevent GC of the MsQuicConnection in the middle of an async op (Connect or Shutdown). public MsQuicConnection? Connection; - public MsQuicListener.State? ListenerState; - public TaskCompletionSource? ConnectTcs; + public readonly ValueTaskSource ConnectTcs = new ValueTaskSource(); // TODO: only allocate these when there is an outstanding shutdown. public readonly TaskCompletionSource ShutdownTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - public bool Connected; public long AbortErrorCode = -1; public int StreamCount; private bool _closing; @@ -127,16 +127,11 @@ public void SetClosing() } // constructor for inbound connections - public unsafe MsQuicConnection(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, MsQuicListener.State listenerState, SafeMsQuicConnectionHandle handle, bool remoteCertificateRequired = false, X509RevocationMode revocationMode = X509RevocationMode.Offline, RemoteCertificateValidationCallback? remoteCertificateValidationCallback = null, ServerCertificateSelectionCallback? serverCertificateSelectionCallback = null) + internal unsafe MsQuicConnection(QUIC_HANDLE* handle, QUIC_NEW_CONNECTION_INFO* info) { - _state.Handle = handle; + _state.Handle = new SafeMsQuicConnectionHandle(handle); _state.StateGCHandle = GCHandle.Alloc(_state); - _state.RemoteCertificateRequired = remoteCertificateRequired; - _state.RevocationMode = revocationMode; - _state.RemoteCertificateValidationCallback = remoteCertificateValidationCallback; _state.IsServer = true; - _localEndPoint = localEndPoint; - _remoteEndPoint = remoteEndPoint; try { @@ -149,10 +144,13 @@ public unsafe MsQuicConnection(IPEndPoint localEndPoint, IPEndPoint remoteEndPoi throw; } - _state.ListenerState = listenerState; + _remoteEndPoint = info->RemoteAddress->ToIPEndPoint(); + _localEndPoint = info->LocalAddress->ToIPEndPoint(); + _negotiatedAlpnProtocol = new SslApplicationProtocol(new Span(info->NegotiatedAlpn, info->NegotiatedAlpnLength).ToArray()); + if (NetEventSource.Log.IsEnabled()) { - NetEventSource.Info(_state, $"{handle} Inbound connection created"); + NetEventSource.Info(_state, $"{_state.Handle} Inbound connection created"); } } @@ -161,15 +159,13 @@ public unsafe MsQuicConnection(QuicClientConnectionOptions options) { ArgumentNullException.ThrowIfNull(options.RemoteEndPoint, nameof(options.RemoteEndPoint)); + _canAccept = options.MaxBidirectionalStreams > 0 || options.MaxUnidirectionalStreams > 0; _remoteEndPoint = options.RemoteEndPoint; _configuration = SafeMsQuicConfigurationHandle.Create(options); _state.RemoteCertificateRequired = true; - if (options.ClientAuthenticationOptions != null) - { - _state.RevocationMode = options.ClientAuthenticationOptions.CertificateRevocationCheckMode; - _state.RemoteCertificateValidationCallback = options.ClientAuthenticationOptions.RemoteCertificateValidationCallback; - _state.TargetHost = options.ClientAuthenticationOptions.TargetHost; - } + _state.RevocationMode = options.ClientAuthenticationOptions.CertificateRevocationCheckMode; + _state.RemoteCertificateValidationCallback = options.ClientAuthenticationOptions.RemoteCertificateValidationCallback; + _state.TargetHost = options.ClientAuthenticationOptions.TargetHost; _state.StateGCHandle = GCHandle.Alloc(_state); try @@ -203,66 +199,37 @@ public unsafe MsQuicConnection(QuicClientConnectionOptions options) internal SslApplicationProtocol NegotiatedApplicationProtocol => _negotiatedAlpnProtocol; - internal bool Connected => _state.Connected; + internal bool Connected => _state.ConnectTcs.IsCompleted; private static unsafe int HandleEventConnected(State state, ref QUIC_CONNECTION_EVENT connectionEvent) { - if (state.Connected) + if (state.ConnectTcs.IsCompleted) { return QUIC_STATUS_SUCCESS; } - if (state.IsServer) - { - state.Connected = true; - MsQuicListener.State? listenerState = state.ListenerState; - state.ListenerState = null; + // Connected will already be true for connections accepted from a listener. + Debug.Assert(!Monitor.IsEntered(state)); - if (listenerState != null) - { - if (listenerState.PendingConnections.TryRemove(state.Handle.DangerousGetHandle(), out MsQuicConnection? connection)) - { - // Move connection from pending to Accept queue and hand it out. - if (listenerState.AcceptConnectionQueue.Writer.TryWrite(connection)) - { - return QUIC_STATUS_SUCCESS; - } - // Listener is closed - connection.Dispose(); - } - } - - return QUIC_STATUS_USER_CANCELED; - } - else - { - // Connected will already be true for connections accepted from a listener. - Debug.Assert(!Monitor.IsEntered(state)); - - - Debug.Assert(state.Connection != null); - state.Connection._localEndPoint = MsQuicParameterHelpers.GetIPEndPointParam(MsQuicApi.Api, state.Handle, QUIC_PARAM_CONN_LOCAL_ADDRESS); - state.Connection.SetNegotiatedAlpn((IntPtr)connectionEvent.CONNECTED.NegotiatedAlpn, connectionEvent.CONNECTED.NegotiatedAlpnLength); - state.Connection = null; + Debug.Assert(state.Connection != null); + //state.Connection._remoteEndPoint = MsQuicParameterHelpers.GetIPEndPointParam(MsQuicApi.Api, state.Handle, QUIC_PARAM_CONN_REMOTE_ADDRESS); + state.Connection._localEndPoint = MsQuicParameterHelpers.GetIPEndPointParam(MsQuicApi.Api, state.Handle, QUIC_PARAM_CONN_LOCAL_ADDRESS); + state.Connection._negotiatedAlpnProtocol = new SslApplicationProtocol(new Span(connectionEvent.CONNECTED.NegotiatedAlpn, connectionEvent.CONNECTED.NegotiatedAlpnLength).ToArray()); + state.Connection = null; - state.Connected = true; - state.ConnectTcs!.SetResult(QUIC_STATUS_SUCCESS); - state.ConnectTcs = null; - } + state.ConnectTcs.TrySetResult(); return QUIC_STATUS_SUCCESS; } private static int HandleEventShutdownInitiatedByTransport(State state, ref QUIC_CONNECTION_EVENT connectionEvent) { - if (!state.Connected && state.ConnectTcs != null) + if (!state.ConnectTcs.IsCompleted) { Debug.Assert(state.Connection != null); state.Connection = null; - Exception ex = new MsQuicException(connectionEvent.SHUTDOWN_INITIATED_BY_TRANSPORT.Status, "Connection has been shutdown by transport"); - state.ConnectTcs!.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(ex)); - state.ConnectTcs = null; + state.ConnectTcs.TrySetException(new MsQuicException(connectionEvent.SHUTDOWN_INITIATED_BY_TRANSPORT.Status, "Connection has been shutdown by transport")); } // To throw QuicConnectionAbortedException (instead of QuicOperationAbortedException) out of AcceptStreamAsync() since @@ -286,18 +253,6 @@ private static int HandleEventShutdownComplete(State state, ref QUIC_CONNECTION_ // This is the final event on the connection, so free the GCHandle used by the event callback. state.StateGCHandle.Free(); - if (state.ListenerState != null) - { - // This is inbound connection that never got connected - because of TLS validation or some other reason. - // Remove connection from pending queue and dispose it. - if (state.ListenerState.PendingConnections.TryRemove(state.Handle.DangerousGetHandle(), out MsQuicConnection? connection)) - { - connection.Dispose(); - } - - state.ListenerState = null; - } - state.Connection = null; state.ShutdownTcs.SetResult(QUIC_STATUS_SUCCESS); @@ -444,6 +399,11 @@ internal async ValueTask AcceptStreamAsync(CancellationToken cance { ObjectDisposedException.ThrowIf(_disposed == 1, this); + if (!_canAccept) + { + throw new InvalidOperationException(SR.net_quic_accept_not_allowed); + } + MsQuicStream stream; try @@ -517,89 +477,117 @@ internal unsafe ValueTask ConnectAsync(CancellationToken cancellationToken = def Debug.Assert(_state.StateGCHandle.IsAllocated); - _state.Connection = this; - string targetHost; - int port; - - if (_remoteEndPoint is IPEndPoint ipEndPoint) + if (_state.ConnectTcs.TryInitialize(out ValueTask valueTask, cancellationToken: cancellationToken)) { - Debug.Assert(!Monitor.IsEntered(_state), "!Monitor.IsEntered(_state)"); - MsQuicParameterHelpers.SetIPEndPointParam(MsQuicApi.Api, _state.Handle, QUIC_PARAM_CONN_REMOTE_ADDRESS, ipEndPoint); - targetHost = _state.TargetHost ?? ((IPEndPoint)_remoteEndPoint).Address.ToString(); - port = ((IPEndPoint)_remoteEndPoint).Port; + _state.Connection = this; + string targetHost; + int port; - } - else if (_remoteEndPoint is DnsEndPoint dnsEndPoint) - { - port = dnsEndPoint.Port; - string dnsHost = dnsEndPoint.Host!; + if (_remoteEndPoint is IPEndPoint ipEndPoint) + { + Debug.Assert(!Monitor.IsEntered(_state), "!Monitor.IsEntered(_state)"); + MsQuicParameterHelpers.SetIPEndPointParam(MsQuicApi.Api, _state.Handle, QUIC_PARAM_CONN_REMOTE_ADDRESS, ipEndPoint); + targetHost = _state.TargetHost ?? ((IPEndPoint)_remoteEndPoint).Address.ToString(); + port = ((IPEndPoint)_remoteEndPoint).Port; - // We don't have way how to set separate SNI and name for connection at this moment. - // If the name is actually IP address we can use it to make at least some cases work for people - // who want to bypass DNS but connect to specific virtual host. - if (!dnsHost.Equals(_state.TargetHost, StringComparison.InvariantCultureIgnoreCase) && !string.IsNullOrEmpty(_state.TargetHost)) + } + else if (_remoteEndPoint is DnsEndPoint dnsEndPoint) { - targetHost = _state.TargetHost!; - if (IPAddress.TryParse(dnsHost, out IPAddress? address)) + port = dnsEndPoint.Port; + string dnsHost = dnsEndPoint.Host!; + + // We don't have way how to set separate SNI and name for connection at this moment. + // If the name is actually IP address we can use it to make at least some cases work for people + // who want to bypass DNS but connect to specific virtual host. + if (!dnsHost.Equals(_state.TargetHost, StringComparison.InvariantCultureIgnoreCase) && !string.IsNullOrEmpty(_state.TargetHost)) { - // This is form of IPAddress and _state.TargetHost is set to different string - Debug.Assert(!Monitor.IsEntered(_state), "!Monitor.IsEntered(_state)"); - MsQuicParameterHelpers.SetIPEndPointParam(MsQuicApi.Api, _state.Handle, QUIC_PARAM_CONN_REMOTE_ADDRESS, new IPEndPoint(address, port)); + targetHost = _state.TargetHost!; + if (IPAddress.TryParse(dnsHost, out IPAddress? address)) + { + // This is form of IPAddress and _state.TargetHost is set to different string + Debug.Assert(!Monitor.IsEntered(_state), "!Monitor.IsEntered(_state)"); + MsQuicParameterHelpers.SetIPEndPointParam(MsQuicApi.Api, _state.Handle, QUIC_PARAM_CONN_REMOTE_ADDRESS, new IPEndPoint(address, port)); + } + else + { + IPAddress[] addresses = Dns.GetHostAddressesAsync(dnsHost, cancellationToken).GetAwaiter().GetResult(); + cancellationToken.ThrowIfCancellationRequested(); + if (addresses.Length == 0) + { + throw new SocketException((int)SocketError.HostNotFound); + } + Debug.Assert(!Monitor.IsEntered(_state), "!Monitor.IsEntered(_state)"); + // We can do something better than just using first IP but that is what + // MsQuic does today anyway. + MsQuicParameterHelpers.SetIPEndPointParam(MsQuicApi.Api, _state.Handle, QUIC_PARAM_CONN_REMOTE_ADDRESS, new IPEndPoint(addresses[0], port)); + } } else { - IPAddress[] addresses = Dns.GetHostAddressesAsync(dnsHost, cancellationToken).GetAwaiter().GetResult(); - cancellationToken.ThrowIfCancellationRequested(); - if (addresses.Length == 0) - { - throw new SocketException((int)SocketError.HostNotFound); - } - Debug.Assert(!Monitor.IsEntered(_state), "!Monitor.IsEntered(_state)"); - // We can do something better than just using first IP but that is what - // MsQuic does today anyway. - MsQuicParameterHelpers.SetIPEndPointParam(MsQuicApi.Api, _state.Handle, QUIC_PARAM_CONN_REMOTE_ADDRESS, new IPEndPoint(addresses[0], port)); + // We defer everything to MsQuic. + targetHost = dnsHost; } } else { - // We defer everything to MsQuic. - targetHost = dnsHost; + throw new ArgumentException($"Unsupported remote endpoint type '{_remoteEndPoint.GetType()}'."); + } + + IntPtr pTargetHost = Marshal.StringToCoTaskMemAnsi(targetHost); + try + { + Debug.Assert(!Monitor.IsEntered(_state), "!Monitor.IsEntered(_state)"); + ThrowIfFailure(MsQuicApi.Api.ApiTable->ConnectionStart( + _state.Handle.QuicHandle, + _configuration.QuicHandle, + af, + (sbyte*)pTargetHost, + (ushort)port), "Failed to connect to peer"); + + // this handle is ref counted by MsQuic, so safe to dispose here. + _configuration.Dispose(); + _configuration = null; + } + catch + { + _state.Connection = null; + throw; + } + finally + { + Marshal.FreeCoTaskMem(pTargetHost); } - } - else - { - throw new ArgumentException($"Unsupported remote endpoint type '{_remoteEndPoint.GetType()}'."); } - // We store TCS to local variable to avoid NRE if callbacks finish fast and set _state.ConnectTcs to null. - var tcs = _state.ConnectTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + return valueTask; + } - IntPtr pTargetHost = Marshal.StringToCoTaskMemAnsi(targetHost); - try - { - Debug.Assert(!Monitor.IsEntered(_state), "!Monitor.IsEntered(_state)"); - ThrowIfFailure(MsQuicApi.Api.ApiTable->ConnectionStart( - _state.Handle.QuicHandle, - _configuration.QuicHandle, - af, - (sbyte*)pTargetHost, - (ushort)port), "Failed to connect to peer"); + internal unsafe ValueTask FinishHandshakeAsync(QuicServerConnectionOptions options, string? targetHost, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed == 1, this); - // this handle is ref counted by MsQuic, so safe to dispose here. - _configuration.Dispose(); - _configuration = null; - } - catch - { - _state.Connection = null; - throw; - } - finally + if (_state.ConnectTcs.TryInitialize(out var valueTask, this, cancellationToken)) { - Marshal.FreeCoTaskMem(pTargetHost); + _canAccept = options.MaxBidirectionalStreams > 0 || options.MaxUnidirectionalStreams > 0; + _state.Connection = this; + try + { + _state.RemoteCertificateRequired = options.ServerAuthenticationOptions.ClientCertificateRequired; + _state.RevocationMode = options.ServerAuthenticationOptions.CertificateRevocationCheckMode; + _state.RemoteCertificateValidationCallback = options.ServerAuthenticationOptions.RemoteCertificateValidationCallback; + _configuration = SafeMsQuicConfigurationHandle.Create(options, options.ServerAuthenticationOptions, targetHost); + ThrowIfFailure(MsQuicApi.Api.ApiTable->ConnectionSetConfiguration( + _state.Handle.QuicHandle, + _configuration.QuicHandle)); + } + catch + { + _state.Connection = null; + throw; + } } - return new ValueTask(tcs.Task); + return valueTask; } private unsafe ValueTask ShutdownAsync( @@ -627,16 +615,6 @@ private unsafe ValueTask ShutdownAsync( return new ValueTask(_state.ShutdownTcs.Task); } - internal void SetNegotiatedAlpn(IntPtr alpn, int alpnLength) - { - if (alpn != IntPtr.Zero && alpnLength != 0) - { - var buffer = new byte[alpnLength]; - Marshal.Copy(alpn, buffer, 0, alpnLength); - _negotiatedAlpnProtocol = new SslApplicationProtocol(buffer); - } - } - #pragma warning disable CS3016 [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] #pragma warning restore CS3016 @@ -681,12 +659,11 @@ private static unsafe int NativeCallback(QUIC_HANDLE* connection, void* context, NetEventSource.Error(state, $"{state.Handle} Exception occurred during handling {connectionEvent->Type} connection callback: {ex}"); } - if (state.ConnectTcs != null) + if (!state.ConnectTcs.IsCompleted) { // This is opportunistic if we get exception and have ability to propagate it to caller. state.ConnectTcs.TrySetException(ex); state.Connection = null; - state.ConnectTcs = null; } else { diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicListener.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicListener.cs deleted file mode 100644 index 526ad099321f8..0000000000000 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicListener.cs +++ /dev/null @@ -1,330 +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.Buffers; -using System.Buffers.Binary; -using System.Collections.Generic; -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Net.Quic.Implementations.MsQuic.Internal; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using Microsoft.Quic; -using System.Runtime.CompilerServices; -using static Microsoft.Quic.MsQuic; - -namespace System.Net.Quic.Implementations.MsQuic -{ - internal sealed class MsQuicListener : IDisposable - { - private readonly State _state; - private GCHandle _stateHandle; - private volatile bool _disposed; - - private readonly IPEndPoint _listenEndPoint; - - internal sealed class State - { - // set immediately in ctor, but we need a GCHandle to State in order to create the handle. - public SafeMsQuicListenerHandle Handle = null!; - - public TaskCompletionSource StopCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - public readonly SafeMsQuicConfigurationHandle? ConnectionConfiguration; - public readonly Channel AcceptConnectionQueue; - // Pending connections are held back until they're ready to be used, which includes TLS negotiation. - // If the negotiation succeeds, the connection is put into the accept queue; otherwise, it's discarded. - public readonly ConcurrentDictionary PendingConnections; - - public QuicOptions ConnectionOptions = new QuicOptions(); - public SslServerAuthenticationOptions AuthenticationOptions = new SslServerAuthenticationOptions(); - - public State(QuicListenerOptions options) - { - ConnectionOptions.IdleTimeout = options.IdleTimeout; - ConnectionOptions.MaxBidirectionalStreams = options.MaxBidirectionalStreams; - ConnectionOptions.MaxUnidirectionalStreams = options.MaxUnidirectionalStreams; - - bool delayConfiguration = false; - - if (options.ServerAuthenticationOptions != null) - { - AuthenticationOptions.ClientCertificateRequired = options.ServerAuthenticationOptions.ClientCertificateRequired; - AuthenticationOptions.CertificateRevocationCheckMode = options.ServerAuthenticationOptions.CertificateRevocationCheckMode; - AuthenticationOptions.RemoteCertificateValidationCallback = options.ServerAuthenticationOptions.RemoteCertificateValidationCallback; - AuthenticationOptions.ServerCertificateSelectionCallback = options.ServerAuthenticationOptions.ServerCertificateSelectionCallback; - AuthenticationOptions.ApplicationProtocols = options.ServerAuthenticationOptions.ApplicationProtocols; - AuthenticationOptions.CipherSuitesPolicy = options.ServerAuthenticationOptions.CipherSuitesPolicy; - - if (options.ServerAuthenticationOptions.ServerCertificate == null && options.ServerAuthenticationOptions.ServerCertificateContext == null && - options.ServerAuthenticationOptions.ServerCertificateSelectionCallback != null) - { - // We don't have any certificate but we have selection callback so we need to wait for SNI. - delayConfiguration = true; - } - } - - if (!delayConfiguration) - { - ConnectionConfiguration = SafeMsQuicConfigurationHandle.Create(options, options.ServerAuthenticationOptions); - } - - PendingConnections = new ConcurrentDictionary(); - AcceptConnectionQueue = Channel.CreateBounded(new BoundedChannelOptions(options.ListenBacklog) - { - SingleReader = true, - SingleWriter = true - }); - } - } - - internal unsafe MsQuicListener(QuicListenerOptions options) - { - ArgumentNullException.ThrowIfNull(options.ListenEndPoint, nameof(options.ListenEndPoint)); - - _state = new State(options); - _stateHandle = GCHandle.Alloc(_state); - try - { - QUIC_HANDLE* handle; - Debug.Assert(!Monitor.IsEntered(_state), "!Monitor.IsEntered(_state)"); - ThrowIfFailure(MsQuicApi.Api.ApiTable->ListenerOpen( - MsQuicApi.Api.Registration.QuicHandle, - &NativeCallback, - (void*)GCHandle.ToIntPtr(_stateHandle), - &handle), "ListenerOpen failed"); - _state.Handle = new SafeMsQuicListenerHandle(handle); - } - catch - { - _stateHandle.Free(); - throw; - } - - if (NetEventSource.Log.IsEnabled()) - { - NetEventSource.Info(_state, $"{_state.Handle} Listener created"); - } - - _listenEndPoint = Start(options); - - if (NetEventSource.Log.IsEnabled()) - { - NetEventSource.Info(_state, $"{_state.Handle} Listener started"); - } - } - - internal IPEndPoint ListenEndPoint - { - get - { - return new IPEndPoint(_listenEndPoint.Address, _listenEndPoint.Port); - } - } - - internal async ValueTask AcceptConnectionAsync(CancellationToken cancellationToken = default) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - try - { - return await _state.AcceptConnectionQueue.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); - } - catch (ChannelClosedException) - { - throw new QuicOperationAbortedException(); - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - ~MsQuicListener() - { - Dispose(false); - } - - private void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - // TODO: solve listener stopping in better way now that it receives STOP_COMPLETED event. - StopAsync().GetAwaiter().GetResult(); - _state?.Handle?.Dispose(); - - // Note that it's safe to free the state GCHandle here, because: - // (1) We called ListenerStop above, which will block until all listener events are processed. So we will not receive any more listener events. - // (2) This class is finalizable, which means we will always get called even if the user doesn't explicitly Dispose us. - // If we ever change this class to not be finalizable, and instead rely on the SafeHandle finalization, then we will need to make - // the SafeHandle responsible for freeing this GCHandle, since it will have the only chance to do so when finalized. - - if (_stateHandle.IsAllocated) _stateHandle.Free(); - - _state?.ConnectionConfiguration?.Dispose(); - _disposed = true; - } - - private unsafe IPEndPoint Start(QuicListenerOptions options) - { - List applicationProtocols = options.ServerAuthenticationOptions!.ApplicationProtocols!; - IPEndPoint listenEndPoint = options.ListenEndPoint!; - - Debug.Assert(_stateHandle.IsAllocated); - try - { - Debug.Assert(!Monitor.IsEntered(_state), "!Monitor.IsEntered(_state)"); - using var msquicBuffers = new MsQuicBuffers(); - msquicBuffers.Initialize(applicationProtocols, applicationProtocol => applicationProtocol.Protocol); - - QuicAddr address = listenEndPoint.ToQuicAddr(); - - if (listenEndPoint.Address.Equals(IPAddress.IPv6Any)) - { - // For IPv6Any, MsQuic would listen only for IPv6 connections. This would make it impossible - // to connect the listener by using the IPv4 address (which could have been e.g. resolved by DNS). - // Using the Unspecified family makes MsQuic handle connections from all IP addresses. - address.Family = QUIC_ADDRESS_FAMILY_UNSPEC; - } - - ThrowIfFailure(MsQuicApi.Api.ApiTable->ListenerStart( - _state.Handle.QuicHandle, - msquicBuffers.Buffers, - (uint)applicationProtocols.Count, - &address), "ListenerStart failed"); - } - catch - { - _stateHandle.Free(); - throw; - } - - Debug.Assert(!Monitor.IsEntered(_state), "!Monitor.IsEntered(_state)"); - // override the address family to the original value in case we had to use UNSPEC - return MsQuicParameterHelpers.GetIPEndPointParam(MsQuicApi.Api, _state.Handle, QUIC_PARAM_LISTENER_LOCAL_ADDRESS, listenEndPoint.AddressFamily); - } - - private unsafe Task StopAsync() - { - // TODO finalizers are called even if the object construction fails. - if (_state == null) - { - return Task.CompletedTask; - } - - _state.AcceptConnectionQueue?.Writer.TryComplete(); - - if (_state.Handle != null) - { - Debug.Assert(!Monitor.IsEntered(_state), "!Monitor.IsEntered(_state)"); - MsQuicApi.Api.ApiTable->ListenerStop(_state.Handle.QuicHandle); - } - return _state.StopCompletion.Task; - } - -#pragma warning disable CS3016 - [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] -#pragma warning restore CS3016 - private static unsafe int NativeCallback(QUIC_HANDLE* listener, void* context, QUIC_LISTENER_EVENT* listenerEvent) - { - GCHandle gcHandle = GCHandle.FromIntPtr((IntPtr)context); - Debug.Assert(gcHandle.IsAllocated); - Debug.Assert(gcHandle.Target is not null); - var state = (State)gcHandle.Target; - - - if (listenerEvent->Type == QUIC_LISTENER_EVENT_TYPE.STOP_COMPLETE) - { - state.StopCompletion.TrySetResult(); - return QUIC_STATUS_SUCCESS; - } - - if (listenerEvent->Type != QUIC_LISTENER_EVENT_TYPE.NEW_CONNECTION) - { - return QUIC_STATUS_INTERNAL_ERROR; - } - - SafeMsQuicConnectionHandle? connectionHandle = null; - MsQuicConnection? msQuicConnection = null; - try - { - ref QUIC_NEW_CONNECTION_INFO connectionInfo = ref *listenerEvent->NEW_CONNECTION.Info; - - IPEndPoint localEndPoint = MsQuicAddressHelpers.INetToIPEndPoint((IntPtr)connectionInfo.LocalAddress); - IPEndPoint remoteEndPoint = MsQuicAddressHelpers.INetToIPEndPoint((IntPtr)connectionInfo.RemoteAddress); - - string targetHost = string.Empty; // compat with SslStream - if (connectionInfo.ServerNameLength > 0 && (IntPtr)connectionInfo.ServerName != IntPtr.Zero) - { - // TBD We should figure out what to do with international names. - targetHost = Marshal.PtrToStringAnsi((IntPtr)connectionInfo.ServerName, connectionInfo.ServerNameLength); - } - - SafeMsQuicConfigurationHandle? connectionConfiguration = state.ConnectionConfiguration; - - if (connectionConfiguration == null) - { - Debug.Assert(state.AuthenticationOptions.ServerCertificateSelectionCallback != null); - try - { - // ServerCertificateSelectionCallback is synchronous. We will call it as needed when building configuration - connectionConfiguration = SafeMsQuicConfigurationHandle.Create(state.ConnectionOptions, state.AuthenticationOptions, targetHost); - } - catch (Exception ex) - { - if (NetEventSource.Log.IsEnabled()) - { - NetEventSource.Error(state, $"[Listener#{state.GetHashCode()}] Exception occurred during creating configuration in connection callback: {ex}"); - } - } - - if (connectionConfiguration == null) - { - // We don't have safe handle yet so MsQuic will cleanup new connection. - return QUIC_STATUS_INTERNAL_ERROR; - } - } - - connectionHandle = new SafeMsQuicConnectionHandle(listenerEvent->NEW_CONNECTION.Connection); - - Debug.Assert(!Monitor.IsEntered(state), "!Monitor.IsEntered(state)"); - int status = MsQuicApi.Api.ApiTable->ConnectionSetConfiguration(connectionHandle.QuicHandle, connectionConfiguration.QuicHandle); - if (StatusSucceeded(status)) - { - msQuicConnection = new MsQuicConnection(localEndPoint, remoteEndPoint, state, connectionHandle, state.AuthenticationOptions.ClientCertificateRequired, state.AuthenticationOptions.CertificateRevocationCheckMode, state.AuthenticationOptions.RemoteCertificateValidationCallback); - msQuicConnection.SetNegotiatedAlpn((IntPtr)connectionInfo.NegotiatedAlpn, connectionInfo.NegotiatedAlpnLength); - - if (!state.PendingConnections.TryAdd(connectionHandle.DangerousGetHandle(), msQuicConnection)) - { - msQuicConnection.Dispose(); - } - - return QUIC_STATUS_SUCCESS; - } - - // If we fall-through here something wrong happened. - } - catch (Exception ex) - { - if (NetEventSource.Log.IsEnabled()) - { - NetEventSource.Error(state, $"[Listener#{state.GetHashCode()}] Exception occurred during handling {listenerEvent->Type} connection callback: {ex}"); - } - } - - // This handle will be cleaned up by MsQuic by returning InternalError. - connectionHandle?.SetHandleAsInvalid(); - msQuicConnection?.Dispose(); - return QUIC_STATUS_INTERNAL_ERROR; - } - } -} diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs new file mode 100644 index 0000000000000..d51864562c9e8 --- /dev/null +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs @@ -0,0 +1,154 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; + +namespace System.Net.Quic; + +internal sealed class ValueTaskSource : IValueTaskSource +{ + // None -> [TryInitialize] -> Awaiting -> [TrySetResult|TrySetException] -> Completed + // None -> [TrySetResult|TrySetException] -> Completed + private enum State : byte + { + None, + Awaiting, + Completed + } + + private State _state; + private ManualResetValueTaskSourceCore _valueTaskSource; + private CancellationTokenRegistration _cancellationRegistration; + private GCHandle _keepAlive; + + public ValueTaskSource(bool runContinuationsAsynchronously = true) + { + _state = State.None; + _valueTaskSource = new ManualResetValueTaskSourceCore() { RunContinuationsAsynchronously = runContinuationsAsynchronously }; + _cancellationRegistration = default; + _keepAlive = default; + } + + public bool IsCompleted => (State)Volatile.Read(ref Unsafe.As(ref _state)) == State.Completed; + + public bool TryInitialize(out ValueTask valueTask, object? keepAlive = null, CancellationToken cancellationToken = default) + { + lock (this) + { + // Set up value task either way, so the the caller can get the result even if they do not start the operation. + valueTask = new ValueTask(this, _valueTaskSource.Version); + + // Cancellation might kick off synchronously, re-entering the lock and changing the state to completed. + if (_state == State.None) + { + // Register cancellation if the token can be cancelled and the task is not completed yet. + if (cancellationToken.CanBeCanceled) + { + _cancellationRegistration = cancellationToken.UnsafeRegister(static (obj, cancellationToken) => + { + ValueTaskSource parent = (ValueTaskSource)obj!; + parent.TrySetException(new OperationCanceledException(cancellationToken)); + }, this); + } + } + + State state = _state; + + // If we're the first here and we will return true. + if (state == State.None) + { + // Keep alive the caller object until the result is read from the task. + // Used for keeping caller alive during async interop calls. + if (keepAlive is not null) + { + Debug.Assert(!_keepAlive.IsAllocated); + _keepAlive = GCHandle.Alloc(keepAlive); + } + + _state = State.Awaiting; + return true; + } + + return false; + } + } + + private bool TryComplete(Exception? exception) + { + CancellationTokenRegistration cancellationRegistration = default; + try + { + lock (this) + { + try + { + State state = _state; + + if (state != State.Completed) + { + _state = State.Completed; + + // Swap the cancellation registration so the one that's been registered gets eventually Disposed. + // Ideally, we would dispose it here, but if the callbacks kicks in, it tries to take the lock held by this thread. + cancellationRegistration = _cancellationRegistration; + _cancellationRegistration = default; + + if (exception is not null) + { + // Set up the exception stack strace for the caller. + exception = exception.StackTrace is null ? ExceptionDispatchInfo.SetCurrentStackTrace(exception) : exception; + _valueTaskSource.SetException(exception); + } + else + { + _valueTaskSource.SetResult(true); + } + + return true; + } + + return false; + } + finally + { + // Un-root the the kept alive object in all cases. + if (_keepAlive.IsAllocated) + { + _keepAlive.Free(); + } + } + } + } + finally + { + // Dispose the cancellation if registered. + // Must be done outside of lock since Dispose will wait on pending cancellation callbacks which requires taking the lock. + cancellationRegistration.Dispose(); + } + } + + public bool TrySetResult() + { + return TryComplete(null); + } + + public bool TrySetException(Exception exception) + { + return TryComplete(exception); + } + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) + => _valueTaskSource.GetStatus(token); + + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + => _valueTaskSource.OnCompleted(continuation, state, token, flags); + + void IValueTaskSource.GetResult(short token) + => _valueTaskSource.GetResult(token); +} diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/MsQuicSafeHandle.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/MsQuicSafeHandle.cs new file mode 100644 index 0000000000000..1e6d924bbac98 --- /dev/null +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/MsQuicSafeHandle.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.Quic; + +namespace System.Net.Quic; + +internal unsafe class MsQuicSafeHandle : SafeHandle +{ + // The index must correspond to SafeHandleType enum value and the value must correspond to MsQuic logging abbreviation string. + // This is used for our logging that uses the same format of object identification as MsQuic to easily correlate log events. + private static readonly string[] s_typeName = new string[] + { + " reg", + "cnfg", + "list", + "conn", + "strm" + }; + + private readonly delegate* unmanaged[Cdecl] _releaseAction; + private readonly string _traceId; + + public override bool IsInvalid => handle == IntPtr.Zero; + + public QUIC_HANDLE* QuicHandle => (QUIC_HANDLE*)DangerousGetHandle(); + + public MsQuicSafeHandle(QUIC_HANDLE* handle, delegate* unmanaged[Cdecl] releaseAction, SafeHandleType safeHandleType) + : base((IntPtr)handle, ownsHandle: true) + { + _releaseAction = releaseAction; + _traceId = $"[{s_typeName[(int)safeHandleType]}][0x{DangerousGetHandle():X11}]"; + + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Info(this, "MsQuicSafeHandle created"); + } + } + + protected override bool ReleaseHandle() + { + _releaseAction(QuicHandle); + SetHandle(IntPtr.Zero); + + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Info(this, "MsQuicSafeHandle released"); + } + + return true; + } + + public override string ToString() => _traceId; +} + +internal enum SafeHandleType +{ + Registration, + Configuration, + Listener, + Connection, + Stream +} + +internal sealed class MsQuicContextSafeHandle : MsQuicSafeHandle +{ + /// + /// Holds a weak reference to the managed instance. + /// It allows delegating MsQuic events to the managed object while it still can be collected and finalized. + /// + private readonly GCHandle _context; + + /// + /// Optional parent safe handle, used to increment/decrement reference count with the lifetime of this instance. + /// + private MsQuicSafeHandle? _parent; + + public unsafe MsQuicContextSafeHandle(QUIC_HANDLE* handle, GCHandle context, delegate* unmanaged[Cdecl] releaseAction, SafeHandleType safeHandleType, MsQuicSafeHandle? parent = null) + : base(handle, releaseAction, safeHandleType) + { + _context = context; + _parent = parent; + if (_parent is not null) + { + bool release = false; + _parent.DangerousAddRef(ref release); + if (!release) + { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Error(this, $"{this} {_parent} ref count not incremented"); + } + _parent = null; + } + else + { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Info(this, $"{_parent} ref count incremented"); + } + } + } + } + + protected override bool ReleaseHandle() + { + base.ReleaseHandle(); + _context.Free(); + if (_parent is not null) + { + _parent.DangerousRelease(); + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Info(this, $"{_parent} ref count decremented"); + } + } + return true; + } +} diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/NetEventSource.Quic.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/NetEventSource.Quic.cs index a848475ee8365..7a66c960efc09 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/NetEventSource.Quic.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/NetEventSource.Quic.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.Tracing; -using System.Net.Quic.Implementations.MsQuic.Internal; +using System.Net.Quic; namespace System.Net { diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicClientConnectionOptions.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicClientConnectionOptions.cs deleted file mode 100644 index 4b2ce8fcb84de..0000000000000 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicClientConnectionOptions.cs +++ /dev/null @@ -1,33 +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; - -namespace System.Net.Quic -{ - /// - /// Options to provide to the when connecting to a Listener. - /// - public class QuicClientConnectionOptions : QuicOptions - { - /// - /// Client authentication options to use when establishing a . - /// - public SslClientAuthenticationOptions? ClientAuthenticationOptions { get; set; } - - /// - /// The local endpoint that will be bound to. - /// - public IPEndPoint? LocalEndPoint { get; set; } - - /// - /// The endpoint to connect to. - /// - public EndPoint? RemoteEndPoint { get; set; } - - public QuicClientConnectionOptions() - { - IdleTimeout = TimeSpan.FromTicks(2 * TimeSpan.TicksPerMinute); - } - } -} diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.Unsupported.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.Unsupported.cs index 93ad515214433..e32239db6d568 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.Unsupported.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.Unsupported.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Net.Quic +namespace System.Net.Quic; + +public sealed partial class QuicConnection { - public sealed partial class QuicConnection - { - public static bool IsSupported => false; - } + public static bool IsSupported => false; } 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 b919e6e9d38b6..d76d6c7700c8b 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 @@ -45,6 +45,8 @@ internal QuicConnection(MsQuicConnection provider) public SslApplicationProtocol NegotiatedApplicationProtocol => _provider.NegotiatedApplicationProtocol; + internal ValueTask FinishHandshakeAsync(QuicServerConnectionOptions options, string? targetHost, CancellationToken cancellationToken = default) => _provider.FinishHandshakeAsync(options, targetHost, cancellationToken); + /// /// Connect to the remote endpoint. /// diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnectionOptions.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnectionOptions.cs new file mode 100644 index 0000000000000..34f7aa47d0d9a --- /dev/null +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnectionOptions.cs @@ -0,0 +1,86 @@ +// 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; + +namespace System.Net.Quic; + +/// +/// Shared options for both client (outbound) and server (inbound) . +/// +public abstract class QuicConnectionOptions +{ + /// + /// Prevent sub-classing by code outside of this assembly. + /// + internal QuicConnectionOptions() + { } + + /// + /// Limit on the number of bidirectional streams the remote peer connection can create on an open connection. + /// Default 0 for client and 100 for server connection. + /// + public int MaxBidirectionalStreams { get; set; } + + /// + /// Limit on the number of unidirectional streams the remote peer connection can create on an open connection. + /// Default 0 for client and 10 for server connection. + /// + public int MaxUnidirectionalStreams { get; set; } + + /// + /// Idle timeout for connections, after which the connection will be closed. + /// Default means underlying implementation default idle timeout. + /// + public TimeSpan IdleTimeout { get; set; } = TimeSpan.Zero; +} + +/// +/// Options for client (outbound) . +/// +public sealed class QuicClientConnectionOptions : QuicConnectionOptions +{ + /// + /// Client authentication options to use when establishing a new connection. + /// + public required SslClientAuthenticationOptions ClientAuthenticationOptions { get; set; } + + /// + /// The remote endpoint to connect to. May be both , which will get resolved to an IP before connecting, or directly . + /// + public required EndPoint RemoteEndPoint { get; set; } + + /// + /// The optional local endpoint that will be bound to. + /// + public IPEndPoint? LocalEndPoint { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public QuicClientConnectionOptions() + { + MaxBidirectionalStreams = 0; + MaxUnidirectionalStreams = 0; + } +} + +/// +/// Options for server (inbound) . Provided by . +/// +public sealed class QuicServerConnectionOptions : QuicConnectionOptions +{ + /// + /// Server authentication options to use when accepting a new connection. + /// + public required SslServerAuthenticationOptions ServerAuthenticationOptions { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public QuicServerConnectionOptions() + { + MaxBidirectionalStreams = 100; + MaxUnidirectionalStreams = 10; + } +} 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 new file mode 100644 index 0000000000000..fe51fd3774ec0 --- /dev/null +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.PendingConnection.cs @@ -0,0 +1,99 @@ +// 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); + 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 handshake failed: {ex}"); + } + + await connection.CloseAsync(default).ConfigureAwait(false); + connection.Dispose(); + _finishHandshakeTask.SetResult(null); + } + } + + /// + /// 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 ValueTask DisposeAsync() + { + _cancellationTokenSource.Cancel(); + return new ValueTask(_finishHandshakeTask.Task); + } + } +} diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.Unsupported.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.Unsupported.cs index 6d37aeff8a0b6..71f83a2abac79 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.Unsupported.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.Unsupported.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Net.Quic +namespace System.Net.Quic; + +public sealed partial class QuicListener { - public sealed partial class QuicListener - { - public static bool IsSupported => false; - } + public static bool IsSupported => false; } 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 9d29f34176d1f..75cbde31833b1 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,45 +1,288 @@ // 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.Quic.Implementations; using System.Net.Quic.Implementations.MsQuic; using System.Net.Quic.Implementations.MsQuic.Internal; using System.Net.Security; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Security.Authentication; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; +using Microsoft.Quic; +using static Microsoft.Quic.MsQuic; -namespace System.Net.Quic +using NEW_CONNECTION_DATA = Microsoft.Quic.QUIC_LISTENER_EVENT._Anonymous_e__Union._NEW_CONNECTION_e__Struct; +using STOP_COMPLETE_DATA = Microsoft.Quic.QUIC_LISTENER_EVENT._Anonymous_e__Union._STOP_COMPLETE_e__Struct; + +namespace System.Net.Quic; + +/// +/// Represents a listener that listens for incoming QUIC connections, see RFC 9000: Connections for more details. +/// allows accepting multiple . +/// +/// +/// Unlike the connection and stream, lifetime is not linked to any of the accepted connections. +/// It can be safely disposed while keeping the accepted connection alive. The will only stop listening for any other inbound connections. +/// +public sealed partial class QuicListener : IAsyncDisposable { - public sealed class QuicListener : IDisposable + /// + /// Returns true if QUIC is supported on the current machine and can be used; otherwise, false. + /// + /// + /// The current implementation depends on MsQuic native library, this property checks its presence (Linux machines). + /// It also checks whether TLS 1.3, requirement for QUIC protocol, is available and enabled (Windows machines). + /// + public static bool IsSupported => MsQuicApi.IsQuicSupported; + + /// + /// Creates a new and starts listening for new connections. + /// + /// Options for the listener. + /// A cancellation token that can be used to cancel the asynchronous operation. + /// An asynchronous task that completes with the started listener. + public static ValueTask ListenAsync(QuicListenerOptions options, CancellationToken cancellationToken = default) + { + if (!IsSupported) + { + throw new PlatformNotSupportedException(SR.SystemNetQuic_PlatformNotSupported); + } + + // Validate and fill in defaults for the options. + if (options.ApplicationProtocols.Count <= 0) + { + throw new ArgumentException($"Expected at least one item in '{nameof(QuicListenerOptions.ApplicationProtocols)}' to start the listener.", nameof(options)); + } + if (options.ListenBacklog == 0) + { + options.ListenBacklog = 512; + } + + QuicListener listener = new QuicListener(options); + + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Info(listener, $"Listener listens on {listener.LocalEndPoint}"); + } + + return ValueTask.FromResult(listener); + } + + /// + /// Handle to MsQuic listener object. + /// + private MsQuicContextSafeHandle _handle; + + /// + /// Set to non-zero once disposed. Prevents double and/or concurrent disposal. + /// + private int _disposed; + + /// + /// Completed when SHUTDOWN_COMPLETE arrives. + /// + private readonly ValueTaskSource _shutdownTcs = new ValueTaskSource(); + + /// + /// Selects connection options for incoming connections. + /// + private readonly Func> _connectionOptionsCallback; + + /// + /// Incoming connections waiting to be accepted via AcceptAsync. + /// + private readonly Channel _acceptQueue; + + /// + /// The actual listening endpoint. + /// + public IPEndPoint LocalEndPoint { get; } + + public override string ToString() => _handle.ToString(); + + private unsafe QuicListener(QuicListenerOptions options) { - public static bool IsSupported => MsQuicApi.IsQuicSupported; + GCHandle context = GCHandle.Alloc(this, GCHandleType.Weak); + try + { + QUIC_HANDLE* handle; + ThrowIfFailure(MsQuicApi.Api.ApiTable->ListenerOpen( + MsQuicApi.Api.Registration.QuicHandle, + &NativeCallback, + (void*)GCHandle.ToIntPtr(context), + &handle), + "ListenerOpen failed"); + _handle = new MsQuicContextSafeHandle(handle, context, MsQuicApi.Api.ApiTable->ListenerClose, SafeHandleType.Listener); + } + catch + { + context.Free(); + throw; + } - public static ValueTask ListenAsync(QuicListenerOptions options, CancellationToken cancellationToken = default) + // Save the connection options before starting the listener + _connectionOptionsCallback = options.ConnectionOptionsCallback; + _acceptQueue = Channel.CreateBounded(new BoundedChannelOptions(options.ListenBacklog) { SingleWriter = true }); + + // Start the listener, from now on MsQuic events will come. + using MsQuicBuffers alpnBuffers = new MsQuicBuffers(); + alpnBuffers.Initialize(options.ApplicationProtocols, applicationProtocol => applicationProtocol.Protocol); + QuicAddr address = options.ListenEndPoint.ToQuicAddr(); + if (options.ListenEndPoint.Address.Equals(IPAddress.IPv6Any)) { - if (!IsSupported) + // For IPv6Any, MsQuic would listen only for IPv6 connections. This would make it impossible + // to connect the listener by using the IPv4 address (which could have been e.g. resolved by DNS). + // Using the Unspecified family makes MsQuic handle connections from all IP addresses. + address.Family = QUIC_ADDRESS_FAMILY_UNSPEC; + } + ThrowIfFailure(MsQuicApi.Api.ApiTable->ListenerStart( + _handle.QuicHandle, + alpnBuffers.Buffers, + (uint)alpnBuffers.Count, + &address), + "ListenerStart failed"); + + // Get the actual listening endpoint. + LocalEndPoint = MsQuicParameterHelpers.GetIPEndPointParam(MsQuicApi.Api, _handle, QUIC_PARAM_LISTENER_LOCAL_ADDRESS, options.ListenEndPoint.AddressFamily); + } + + /// + /// 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. + /// + /// A cancellation token that can be used to cancel the asynchronous operation. + /// A task that will contain a fully connected which successfully finished the handshake and is ready to be used. + public async ValueTask AcceptConnectionAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed == 1, this); + + try + { + while (true) { - throw new PlatformNotSupportedException(SR.SystemNetQuic_PlatformNotSupported); + PendingConnection pendingConnection = await _acceptQueue.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + await using (pendingConnection.ConfigureAwait(false)) + { + QuicConnection? connection = await pendingConnection.FinishHandshakeAsync(cancellationToken).ConfigureAwait(false); + // Handshake failed, discard this connection and try to get another from the queue. + if (connection is null) + { + continue; + } + + return connection; + } } + } + catch (ChannelClosedException ex) when (ex.InnerException is not null) + { + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + throw; + } + } + + 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)) + { + return QUIC_STATUS_CONNECTION_REFUSED; + } + + QuicConnection connection = new QuicConnection(new MsQuicConnection(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); + + return QUIC_STATUS_SUCCESS; + + } + private unsafe int HandleEventStopComplete(ref STOP_COMPLETE_DATA data) + { + _shutdownTcs.TrySetResult(); + return QUIC_STATUS_SUCCESS; + } + + private unsafe int HandleListenerEvent(ref QUIC_LISTENER_EVENT listenerEvent) + => listenerEvent.Type switch + { + QUIC_LISTENER_EVENT_TYPE.NEW_CONNECTION => HandleEventNewConnection(ref listenerEvent.NEW_CONNECTION), + QUIC_LISTENER_EVENT_TYPE.STOP_COMPLETE => HandleEventStopComplete(ref listenerEvent.STOP_COMPLETE), + _ => QUIC_STATUS_SUCCESS + }; + +#pragma warning disable CS3016 + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] +#pragma warning restore CS3016 + private static unsafe int NativeCallback(QUIC_HANDLE* listener, void* context, QUIC_LISTENER_EVENT* listenerEvent) + { + GCHandle stateHandle = GCHandle.FromIntPtr((IntPtr)context); - return ValueTask.FromResult(new QuicListener(new MsQuicListener(options))); + // Check if the instance hasn't been collected. + if (!stateHandle.IsAllocated || stateHandle.Target is not QuicListener instance) + { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Error(null, $"Received event {listenerEvent->Type}"); + } + return QUIC_STATUS_INVALID_STATE; } - private readonly MsQuicListener _provider; + try + { + // Process the event. + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Info(instance, $"Received event {listenerEvent->Type}"); + } + return instance.HandleListenerEvent(ref *listenerEvent); + } + catch (Exception ex) + { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Error(instance, $"Exception while processing event {listenerEvent->Type}: {ex}"); + } + return QUIC_STATUS_INTERNAL_ERROR; + } + } - internal QuicListener(MsQuicListener provider) + /// + /// Stops listening for new connections and releases all resources associated with the listener. + /// + /// A task that represents the asynchronous dispose operation. + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) { - _provider = provider; + return; } - public IPEndPoint ListenEndPoint => _provider.ListenEndPoint; + // Check if the listener has been shut down and if not, shut it down. + if (_shutdownTcs.TryInitialize(out ValueTask valueTask, this)) + { + unsafe + { + MsQuicApi.Api.ApiTable->ListenerStop(_handle.QuicHandle); + } + } - /// - /// Accept a connection. - /// - /// - public async ValueTask AcceptConnectionAsync(CancellationToken cancellationToken = default) => - new QuicConnection(await _provider.AcceptConnectionAsync(cancellationToken).ConfigureAwait(false)); + await valueTask.ConfigureAwait(false); + _handle.Dispose(); - public void Dispose() => _provider.Dispose(); + // Flush the queue and dispose all remaining connections. + _acceptQueue.Writer.TryComplete(ExceptionDispatchInfo.SetCurrentStackTrace(new QuicOperationAbortedException())); + while (_acceptQueue.Reader.TryRead(out PendingConnection? pendingConnection)) + { + await pendingConnection.DisposeAsync().ConfigureAwait(false); + } } } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListenerOptions.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListenerOptions.cs index b20a8b71285e2..36241a36e2405 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListenerOptions.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListenerOptions.cs @@ -1,33 +1,36 @@ // 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.Net.Security; +using System.Threading; +using System.Threading.Tasks; -namespace System.Net.Quic +namespace System.Net.Quic; + +/// +/// Options to provide to the . +/// +public sealed class QuicListenerOptions { /// - /// Options to provide to the . + /// The endpoint to listen on. /// - public class QuicListenerOptions : QuicOptions - { - /// - /// Server Ssl options to use for ALPN, SNI, etc. - /// - public SslServerAuthenticationOptions? ServerAuthenticationOptions { get; set; } + public required IPEndPoint ListenEndPoint { get; set; } - /// - /// The endpoint to listen on. - /// - public IPEndPoint? ListenEndPoint { get; set; } + /// + /// List of application protocols which the listener will accept. At least one must be specified. + /// + public required List ApplicationProtocols { get; set; } - /// - /// Number of connections to be held without accepting the connection. - /// - public int ListenBacklog { get; set; } = 512; + /// + /// Number of connections to be held without accepting the connection. + /// + /// + public int ListenBacklog { get; set; } - public QuicListenerOptions() - { - IdleTimeout = TimeSpan.FromTicks(10 * TimeSpan.TicksPerMinute); - } - } + /// + /// Selection callback to choose inbound connection options dynamically. + /// + public required Func> ConnectionOptionsCallback { get; set; } } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicOptions.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicOptions.cs deleted file mode 100644 index 44a5fd0ae9d97..0000000000000 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicOptions.cs +++ /dev/null @@ -1,35 +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; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; - -namespace System.Net.Quic -{ - /// - /// Options for QUIC - /// - public class QuicOptions - { - /// - /// Limit on the number of bidirectional streams the remote peer connection can create on an open connection. - /// Default is 100. - /// - // TODO consider constraining these limits to 0 to whatever the max of the QUIC library we are using. - public int MaxBidirectionalStreams { get; set; } = 100; - - /// - /// Limit on the number of unidirectional streams the remote peer connection can create on an open connection. - /// Default is 100. - /// - // TODO consider constraining these limits to 0 to whatever the max of the QUIC library we are using. - public int MaxUnidirectionalStreams { get; set; } = 100; - - /// - /// Idle timeout for connections, after which the connection will be closed. - /// - public TimeSpan IdleTimeout { get; set; } - } -} diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicCipherSuitesPolicyTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicCipherSuitesPolicyTests.cs index 43337eba15176..1996b0d774da4 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicCipherSuitesPolicyTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicCipherSuitesPolicyTests.cs @@ -1,5 +1,6 @@ // 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.Net.Security; using System.Threading.Tasks; using Xunit; @@ -16,13 +17,21 @@ public MsQuicCipherSuitesPolicyTests(ITestOutputHelper output) : base(output) { private async Task TestConnection(CipherSuitesPolicy serverPolicy, CipherSuitesPolicy clientPolicy) { - var listenerOptions = CreateQuicListenerOptions(); - listenerOptions.ServerAuthenticationOptions.CipherSuitesPolicy = serverPolicy; - using QuicListener listener = await CreateQuicListener(listenerOptions); + var listenerOptions = new QuicListenerOptions() + { + ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => + { + var serverOptions = CreateQuicServerOptions(); + serverOptions.ServerAuthenticationOptions.CipherSuitesPolicy = serverPolicy; + return ValueTask.FromResult(serverOptions); + } + }; + await using QuicListener listener = await CreateQuicListener(listenerOptions); - var clientOptions = CreateQuicClientOptions(); + var clientOptions = CreateQuicClientOptions(listener.LocalEndPoint); clientOptions.ClientAuthenticationOptions.CipherSuitesPolicy = clientPolicy; - clientOptions.RemoteEndPoint = listener.ListenEndPoint; using QuicConnection clientConnection = await CreateQuicConnection(clientOptions); await clientConnection.ConnectAsync(); @@ -42,13 +51,21 @@ public Task SupportedCipher_Success() public void NoSupportedCiphers_ThrowsArgumentException(TlsCipherSuite[] ciphers) { CipherSuitesPolicy policy = new CipherSuitesPolicy(ciphers); - var listenerOptions = CreateQuicListenerOptions(); - listenerOptions.ServerAuthenticationOptions.CipherSuitesPolicy = policy; + var listenerOptions = new QuicListenerOptions() + { + ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => + { + var serverOptions = CreateQuicServerOptions(); + serverOptions.ServerAuthenticationOptions.CipherSuitesPolicy = policy; + return ValueTask.FromResult(serverOptions); + } + }; Assert.ThrowsAsync(async () => await CreateQuicListener(listenerOptions)); - var clientOptions = CreateQuicClientOptions(); + var clientOptions = CreateQuicClientOptions(new IPEndPoint(IPAddress.Loopback, 5000)); clientOptions.ClientAuthenticationOptions.CipherSuitesPolicy = policy; - clientOptions.RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 5000); Assert.ThrowsAsync(async () => await CreateQuicConnection(clientOptions)); } diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs index 31775e0131c1f..cd1f0061d3cc7 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs @@ -34,8 +34,8 @@ public async Task UnidirectionalAndBidirectionalStreamCountsWork() { (QuicConnection clientConnection, QuicConnection serverConnection) = await CreateConnectedQuicConnection(); - Assert.Equal(100, serverConnection.GetRemoteAvailableBidirectionalStreamCount()); - Assert.Equal(100, serverConnection.GetRemoteAvailableUnidirectionalStreamCount()); + Assert.Equal(0, serverConnection.GetRemoteAvailableBidirectionalStreamCount()); + Assert.Equal(0, serverConnection.GetRemoteAvailableUnidirectionalStreamCount()); serverConnection.Dispose(); clientConnection.Dispose(); } @@ -43,16 +43,17 @@ public async Task UnidirectionalAndBidirectionalStreamCountsWork() [Fact] public async Task UnidirectionalAndBidirectionalChangeValues() { - QuicClientConnectionOptions listenerOptions = new QuicClientConnectionOptions() + QuicClientConnectionOptions clientOptions = new QuicClientConnectionOptions() { MaxBidirectionalStreams = 10, MaxUnidirectionalStreams = 20, + RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 0), ClientAuthenticationOptions = GetSslClientAuthenticationOptions() }; - (QuicConnection clientConnection, QuicConnection serverConnection) = await CreateConnectedQuicConnection(listenerOptions); + (QuicConnection clientConnection, QuicConnection serverConnection) = await CreateConnectedQuicConnection(clientOptions); Assert.Equal(100, clientConnection.GetRemoteAvailableBidirectionalStreamCount()); - Assert.Equal(100, clientConnection.GetRemoteAvailableUnidirectionalStreamCount()); + Assert.Equal(10, clientConnection.GetRemoteAvailableUnidirectionalStreamCount()); Assert.Equal(10, serverConnection.GetRemoteAvailableBidirectionalStreamCount()); Assert.Equal(20, serverConnection.GetRemoteAvailableUnidirectionalStreamCount()); serverConnection.Dispose(); @@ -65,13 +66,21 @@ public async Task ConnectWithCertificateChain() (X509Certificate2 certificate, X509Certificate2Collection chain) = System.Net.Security.Tests.TestHelper.GenerateCertificates("localhost", longChain: true); X509Certificate2 rootCA = chain[chain.Count - 1]; - var listenerOptions = new QuicListenerOptions(); - listenerOptions.ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0); - listenerOptions.ServerAuthenticationOptions = GetSslServerAuthenticationOptions(); - listenerOptions.ServerAuthenticationOptions.ServerCertificateContext = SslStreamCertificateContext.Create(certificate, chain); - listenerOptions.ServerAuthenticationOptions.ServerCertificate = null; + var listenerOptions = new QuicListenerOptions() + { + ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => + { + var serverOptions = CreateQuicServerOptions(); + serverOptions.ServerAuthenticationOptions.ServerCertificateContext = SslStreamCertificateContext.Create(certificate, chain); + serverOptions.ServerAuthenticationOptions.ServerCertificate = null; + return ValueTask.FromResult(serverOptions); + } + }; - QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(); + // Use whatever endpoint, it'll get overwritten in CreateConnectedQuicConnection. + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listenerOptions.ListenEndPoint); clientOptions.ClientAuthenticationOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) => { Assert.Equal(certificate.Subject, cert.Subject); @@ -113,18 +122,24 @@ public async Task UntrustedClientCertificateFails() throw new SkipTestException("Client certificates are not supported on Windows Server 2022."); } - var listenerOptions = new QuicListenerOptions(); - listenerOptions.ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0); - listenerOptions.ServerAuthenticationOptions = GetSslServerAuthenticationOptions(); - listenerOptions.ServerAuthenticationOptions.ClientCertificateRequired = true; - listenerOptions.ServerAuthenticationOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) => + var listenerOptions = new QuicListenerOptions() { - return false; + ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => + { + var serverOptions = CreateQuicServerOptions(); + serverOptions.ServerAuthenticationOptions.ClientCertificateRequired = true; + serverOptions.ServerAuthenticationOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) => + { + return false; + }; + return ValueTask.FromResult(serverOptions); + } }; - using QuicListener listener = await CreateQuicListener(listenerOptions); - QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(); - clientOptions.RemoteEndPoint = listener.ListenEndPoint; + await using QuicListener listener = await CreateQuicListener(listenerOptions); + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listener.LocalEndPoint); clientOptions.ClientAuthenticationOptions.ClientCertificates = new X509CertificateCollection() { ClientCertificate }; QuicConnection clientConnection = await CreateQuicConnection(clientOptions); @@ -155,13 +170,15 @@ public async Task CertificateCallbackThrowPropagates() X509Certificate? receivedCertificate = null; bool validationResult = false; - var listenerOptions = new QuicListenerOptions(); - listenerOptions.ListenEndPoint = new IPEndPoint(Socket.OSSupportsIPv6 ? IPAddress.IPv6Loopback : IPAddress.Loopback, 0); - listenerOptions.ServerAuthenticationOptions = GetSslServerAuthenticationOptions(); - using QuicListener listener = await CreateQuicListener(listenerOptions); + var listenerOptions = new QuicListenerOptions() + { + ListenEndPoint = new IPEndPoint(Socket.OSSupportsIPv6 ? IPAddress.IPv6Loopback : IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => ValueTask.FromResult(CreateQuicServerOptions()) + }; + await using QuicListener listener = await CreateQuicListener(listenerOptions); - QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(); - clientOptions.RemoteEndPoint = listener.ListenEndPoint; + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listener.LocalEndPoint); clientOptions.ClientAuthenticationOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) => { receivedCertificate = cert; @@ -178,7 +195,7 @@ public async Task CertificateCallbackThrowPropagates() await Assert.ThrowsAsync(() => clientConnection.ConnectAsync(cts.Token).AsTask()); - Assert.Equal(listenerOptions.ServerAuthenticationOptions.ServerCertificate, receivedCertificate); + Assert.Equal(ServerCertificate, receivedCertificate); clientConnection.Dispose(); // Make sure the listener is still usable and there is no lingering bad connection @@ -201,27 +218,34 @@ public async Task ConnectWithServerCertificateCallback() string? receivedHostName = null; X509Certificate? receivedCertificate = null; - var listenerOptions = new QuicListenerOptions(); - listenerOptions.ListenEndPoint = new IPEndPoint(Socket.OSSupportsIPv6 ? IPAddress.IPv6Loopback : IPAddress.Loopback, 0); - listenerOptions.ServerAuthenticationOptions = GetSslServerAuthenticationOptions(); - listenerOptions.ServerAuthenticationOptions.ServerCertificate = null; - listenerOptions.ServerAuthenticationOptions.ServerCertificateSelectionCallback = (sender, hostName) => + var listenerOptions = new QuicListenerOptions() { - receivedHostName = hostName; - if (hostName == "foobar1") - { - return c1; - } - else if (hostName == "foobar2") + ListenEndPoint = new IPEndPoint(Socket.OSSupportsIPv6 ? IPAddress.IPv6Loopback : IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => { - return c2; - } + var serverOptions = CreateQuicServerOptions(); + serverOptions.ServerAuthenticationOptions.ServerCertificate = null; + serverOptions.ServerAuthenticationOptions.ServerCertificateSelectionCallback = (sender, hostName) => + { + receivedHostName = hostName; + if (hostName == "foobar1") + { + return c1; + } + else if (hostName == "foobar2") + { + return c2; + } - return null; + return null; + }; + return ValueTask.FromResult(serverOptions); + } }; - using QuicListener listener = await CreateQuicListener(listenerOptions); - QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(); + await using QuicListener listener = await CreateQuicListener(listenerOptions); + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listener.LocalEndPoint); clientOptions.ClientAuthenticationOptions.TargetHost = "foobar1"; clientOptions.ClientAuthenticationOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) => { @@ -264,21 +288,28 @@ public async Task ConnectWithIpSetsSni(string destination) string expectedName = "foobar"; string? receivedHostName = null; - var listenerOptions = CreateQuicListenerOptions(); - // loopback may resolve to IPv6 - listenerOptions.ListenEndPoint = new IPEndPoint(IPAddress.IPv6Any, 0); - listenerOptions.ServerAuthenticationOptions.ServerCertificate = null; - listenerOptions.ServerAuthenticationOptions.ServerCertificateSelectionCallback = (sender, hostName) => + var listenerOptions = new QuicListenerOptions() { - receivedHostName = hostName; - return certificate; + // loopback may resolve to IPv6 + ListenEndPoint = new IPEndPoint(IPAddress.IPv6Any, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => + { + var serverOptions = CreateQuicServerOptions(); + serverOptions.ServerAuthenticationOptions.ServerCertificate = null; + serverOptions.ServerAuthenticationOptions.ServerCertificateSelectionCallback = (sender, hostName) => + { + receivedHostName = hostName; + return certificate; + }; + return ValueTask.FromResult(serverOptions); + } }; - using QuicListener listener = await CreateQuicListener(listenerOptions); + await using QuicListener listener = await CreateQuicListener(listenerOptions); - QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(); + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(new DnsEndPoint(destination, listener.LocalEndPoint.Port)); clientOptions.ClientAuthenticationOptions.TargetHost = expectedName; - clientOptions.RemoteEndPoint = new DnsEndPoint(destination, listener.ListenEndPoint.Port); (QuicConnection clientConnection, QuicConnection serverConnection) = await CreateConnectedQuicConnection(clientOptions, listener); Assert.Equal(expectedName, receivedHostName); @@ -291,14 +322,20 @@ public async Task ConnectWithCertificateForDifferentName_Throws() { (X509Certificate2 certificate, _) = System.Net.Security.Tests.TestHelper.GenerateCertificates("localhost"); - var quicOptions = new QuicListenerOptions(); - quicOptions.ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0); - quicOptions.ServerAuthenticationOptions = GetSslServerAuthenticationOptions(); - quicOptions.ServerAuthenticationOptions.ServerCertificate = certificate; - using QuicListener listener = await CreateQuicListener(quicOptions); + var quicOptions = new QuicListenerOptions() + { + ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => + { + var serverOptions = CreateQuicServerOptions(); + serverOptions.ServerAuthenticationOptions.ServerCertificate = certificate; + return ValueTask.FromResult(serverOptions); + } + }; + await using QuicListener listener = await CreateQuicListener(quicOptions); - QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(); - clientOptions.RemoteEndPoint = listener.ListenEndPoint; + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listener.LocalEndPoint); // Use different target host on purpose to get RemoteCertificateNameMismatch ssl error. clientOptions.ClientAuthenticationOptions.TargetHost = "loopback"; clientOptions.ClientAuthenticationOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) => @@ -326,12 +363,20 @@ public async Task ConnectWithCertificateForLoopbackIP_IndicatesExpectedError(str (X509Certificate2 certificate, _) = System.Net.Security.Tests.TestHelper.GenerateCertificates(expectsError ? "badhost" : "localhost"); - var listenerOptions = new QuicListenerOptions(); - listenerOptions.ListenEndPoint = new IPEndPoint(ipAddress, 0); - listenerOptions.ServerAuthenticationOptions = GetSslServerAuthenticationOptions(); - listenerOptions.ServerAuthenticationOptions.ServerCertificate = certificate; + var listenerOptions = new QuicListenerOptions() + { + ListenEndPoint = new IPEndPoint(ipAddress, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => + { + var serverOptions = CreateQuicServerOptions(); + serverOptions.ServerAuthenticationOptions.ServerCertificate = certificate; + return ValueTask.FromResult(serverOptions); + } + }; - QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(); + // Use whatever endpoint, it'll get overwritten in CreateConnectedQuicConnection. + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listenerOptions.ListenEndPoint); clientOptions.ClientAuthenticationOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) => { Assert.Equal(certificate.Subject, cert.Subject); @@ -358,25 +403,32 @@ public async Task ConnectWithClientCertificate(bool sendCertificate, bool useCli bool clientCertificateOK = false; - var listenerOptions = new QuicListenerOptions(); - listenerOptions.ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0); - listenerOptions.ServerAuthenticationOptions = GetSslServerAuthenticationOptions(); - listenerOptions.ServerAuthenticationOptions.ClientCertificateRequired = true; - listenerOptions.ServerAuthenticationOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) => + var listenerOptions = new QuicListenerOptions() { - if (sendCertificate) + ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => { - _output.WriteLine("client certificate {0}", cert); - Assert.NotNull(cert); - Assert.Equal(ClientCertificate.Thumbprint, ((X509Certificate2)cert).Thumbprint); - } + var serverOptions = CreateQuicServerOptions(); + serverOptions.ServerAuthenticationOptions.ClientCertificateRequired = true; + serverOptions.ServerAuthenticationOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) => + { + if (sendCertificate) + { + _output.WriteLine("client certificate {0}", cert); + Assert.NotNull(cert); + Assert.Equal(ClientCertificate.Thumbprint, ((X509Certificate2)cert).Thumbprint); + } - clientCertificateOK = true; - return true; + clientCertificateOK = true; + return true; + }; + return ValueTask.FromResult(serverOptions); + } }; - using QuicListener listener = await CreateQuicListener(listenerOptions); - QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(); + await using QuicListener listener = await CreateQuicListener(listenerOptions); + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listener.LocalEndPoint); if (useClientSelectionCallback) { clientOptions.ClientAuthenticationOptions.LocalCertificateSelectionCallback = delegate @@ -410,9 +462,19 @@ ValueTask OpenStreamAsync(QuicConnection connection) => unidirection ? connection.OpenUnidirectionalStreamAsync() : connection.OpenBidirectionalStreamAsync(); - QuicListenerOptions listenerOptions = CreateQuicListenerOptions(); - listenerOptions.MaxUnidirectionalStreams = 1; - listenerOptions.MaxBidirectionalStreams = 1; + + QuicListenerOptions listenerOptions = new QuicListenerOptions() + { + ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => + { + var serverOptions = CreateQuicServerOptions(); + serverOptions.MaxBidirectionalStreams = 1; + serverOptions.MaxUnidirectionalStreams = 1; + return ValueTask.FromResult(serverOptions); + } + }; (QuicConnection clientConnection, QuicConnection serverConnection) = await CreateConnectedQuicConnection(null, listenerOptions); // Open one stream, second call should block @@ -441,9 +503,18 @@ ValueTask OpenStreamAsync(QuicConnection connection, CancellationTok ? connection.OpenUnidirectionalStreamAsync(token) : connection.OpenBidirectionalStreamAsync(token); - QuicListenerOptions listenerOptions = CreateQuicListenerOptions(); - listenerOptions.MaxUnidirectionalStreams = 1; - listenerOptions.MaxBidirectionalStreams = 1; + QuicListenerOptions listenerOptions = new QuicListenerOptions() + { + ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => + { + var serverOptions = CreateQuicServerOptions(); + serverOptions.MaxBidirectionalStreams = 1; + serverOptions.MaxUnidirectionalStreams = 1; + return ValueTask.FromResult(serverOptions); + } + }; (QuicConnection clientConnection, QuicConnection serverConnection) = await CreateConnectedQuicConnection(null, listenerOptions); CancellationTokenSource cts = new CancellationTokenSource(); @@ -502,9 +573,18 @@ ValueTask OpenStreamAsync(QuicConnection connection, CancellationTok ? connection.OpenUnidirectionalStreamAsync(token) : connection.OpenBidirectionalStreamAsync(token); - QuicListenerOptions listenerOptions = CreateQuicListenerOptions(); - listenerOptions.MaxUnidirectionalStreams = 1; - listenerOptions.MaxBidirectionalStreams = 1; + QuicListenerOptions listenerOptions = new QuicListenerOptions() + { + ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => + { + var serverOptions = CreateQuicServerOptions(); + serverOptions.MaxBidirectionalStreams = 1; + serverOptions.MaxUnidirectionalStreams = 1; + return ValueTask.FromResult(serverOptions); + } + }; (QuicConnection clientConnection, QuicConnection serverConnection) = await CreateConnectedQuicConnection(null, listenerOptions); // Open one stream, second call should block @@ -536,10 +616,12 @@ ValueTask OpenStreamAsync(QuicConnection connection, CancellationTok [OuterLoop("May take several seconds")] public async Task SetListenerTimeoutWorksWithSmallTimeout() { - var listenerOptions = new QuicListenerOptions(); - listenerOptions.IdleTimeout = TimeSpan.FromSeconds(1); - listenerOptions.ServerAuthenticationOptions = GetSslServerAuthenticationOptions(); - listenerOptions.ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0); + var listenerOptions = new QuicListenerOptions() + { + ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => ValueTask.FromResult(CreateQuicServerOptions()) + }; (QuicConnection clientConnection, QuicConnection serverConnection) = await CreateConnectedQuicConnection(null, listenerOptions); await Assert.ThrowsAsync(async () => await serverConnection.AcceptStreamAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(100))); diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs index 7b54875067131..ce5ff99716a61 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs @@ -19,12 +19,12 @@ public QuicConnectionTests(ITestOutputHelper output) : base(output) { } [Fact] public async Task TestConnect() { - using QuicListener listener = await CreateQuicListener(); + await using QuicListener listener = await CreateQuicListener(); - using QuicConnection clientConnection = await CreateQuicConnection(listener.ListenEndPoint); + using QuicConnection clientConnection = await CreateQuicConnection(listener.LocalEndPoint); Assert.False(clientConnection.Connected); - Assert.Equal(listener.ListenEndPoint, clientConnection.RemoteEndPoint); + Assert.Equal(listener.LocalEndPoint, clientConnection.RemoteEndPoint); ValueTask connectTask = clientConnection.ConnectAsync(); ValueTask acceptTask = listener.AcceptConnectionAsync(); @@ -34,8 +34,8 @@ public async Task TestConnect() Assert.True(clientConnection.Connected); Assert.True(serverConnection.Connected); - Assert.Equal(listener.ListenEndPoint, serverConnection.LocalEndPoint); - Assert.Equal(listener.ListenEndPoint, clientConnection.RemoteEndPoint); + Assert.Equal(listener.LocalEndPoint, serverConnection.LocalEndPoint); + Assert.Equal(listener.LocalEndPoint, clientConnection.RemoteEndPoint); Assert.Equal(clientConnection.LocalEndPoint, serverConnection.RemoteEndPoint); Assert.Equal(ApplicationProtocol.ToString(), clientConnection.NegotiatedApplicationProtocol.ToString()); Assert.Equal(ApplicationProtocol.ToString(), serverConnection.NegotiatedApplicationProtocol.ToString()); @@ -226,7 +226,6 @@ public async Task Dispose_WithOpenLocalStream_LocalStreamFailsWithQuicOperationA { // Set a short idle timeout so that after we dispose the connection, the peer will discover the connection is dead before too long. QuicListenerOptions listenerOptions = CreateQuicListenerOptions(); - listenerOptions.IdleTimeout = TimeSpan.FromSeconds(1); using var sync = new SemaphoreSlim(0); diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs index 324330ea9785c..44d30bcab1d06 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicListenerTests.cs @@ -18,9 +18,9 @@ public async Task Listener_Backlog_Success() { await Task.Run(async () => { - using QuicListener listener = await CreateQuicListener(); + await using QuicListener listener = await CreateQuicListener(); - using QuicConnection clientConnection = await CreateQuicConnection(listener.ListenEndPoint); + using QuicConnection clientConnection = await CreateQuicConnection(listener.LocalEndPoint); var clientStreamTask = clientConnection.ConnectAsync(); using QuicConnection serverConnection = await listener.AcceptConnectionAsync(); @@ -33,9 +33,9 @@ public async Task Listener_Backlog_Success_IPv6() { await Task.Run(async () => { - using QuicListener listener = await CreateQuicListener(new IPEndPoint(IPAddress.IPv6Loopback, 0)); + await using QuicListener listener = await CreateQuicListener(new IPEndPoint(IPAddress.IPv6Loopback, 0)); - using QuicConnection clientConnection = await CreateQuicConnection(listener.ListenEndPoint); + using QuicConnection clientConnection = await CreateQuicConnection(listener.LocalEndPoint); var clientStreamTask = clientConnection.ConnectAsync(); using QuicConnection serverConnection = await listener.AcceptConnectionAsync(); @@ -52,9 +52,9 @@ await Task.Run(async () => // Use a copy of IPAddress.IPv6Any to make sure address detection doesn't rely on reference equality comparison. IPAddress IPv6Any = new IPAddress((ReadOnlySpan)new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0); - using QuicListener listener = await CreateQuicListener(new IPEndPoint(IPv6Any, 0)); + await using QuicListener listener = await CreateQuicListener(new IPEndPoint(IPv6Any, 0)); - using QuicConnection clientConnection = await CreateQuicConnection(new IPEndPoint(IPAddress.Loopback, listener.ListenEndPoint.Port)); + using QuicConnection clientConnection = await CreateQuicConnection(new IPEndPoint(IPAddress.Loopback, listener.LocalEndPoint.Port)); var clientStreamTask = clientConnection.ConnectAsync(); using QuicConnection serverConnection = await listener.AcceptConnectionAsync(); diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamConnectedStreamConformanceTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamConnectedStreamConformanceTests.cs index f0ac82216343a..9034aaf94ee3d 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamConnectedStreamConformanceTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamConnectedStreamConformanceTests.cs @@ -54,7 +54,11 @@ protected override async Task CreateConnectedStreamsAsync() var listener = await QuicListener.ListenAsync(new QuicListenerOptions() { ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), - ServerAuthenticationOptions = GetSslServerAuthenticationOptions() + ApplicationProtocols = new List() { new SslApplicationProtocol("quictest") }, + ConnectionOptionsCallback = (_, _, _) => ValueTask.FromResult(new QuicServerConnectionOptions() + { + ServerAuthenticationOptions = GetSslServerAuthenticationOptions() + }) }); byte[] buffer = new byte[1] { 42 }; @@ -73,7 +77,7 @@ await WhenAllOrAnyFailed( { connection2 = await QuicConnection.ConnectAsync(new QuicClientConnectionOptions() { - RemoteEndPoint = listener.ListenEndPoint, + RemoteEndPoint = listener.LocalEndPoint, ClientAuthenticationOptions = GetSslClientAuthenticationOptions() }); await connection2.ConnectAsync(); @@ -90,7 +94,7 @@ await WhenAllOrAnyFailed( })); // No need to keep the listener once we have connected connection and streams - listener.Dispose(); + await listener.DisposeAsync(); var result = new StreamPairWithOtherDisposables(stream1, stream2); result.Disposables.Add(connection1); diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs index fc1232273ed2b..6269c5fab80b5 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs @@ -255,15 +255,18 @@ await RunClientServer( [Fact] public async Task TestStreams() { - using QuicListener listener = await CreateQuicListener(); - (QuicConnection clientConnection, QuicConnection serverConnection) = await CreateConnectedQuicConnection(listener); + await using QuicListener listener = await CreateQuicListener(); + var clientOptions = CreateQuicClientOptions(listener.LocalEndPoint); + clientOptions.MaxBidirectionalStreams = 1; + clientOptions.MaxUnidirectionalStreams = 1; + (QuicConnection clientConnection, QuicConnection serverConnection) = await CreateConnectedQuicConnection(clientOptions, listener); using (clientConnection) using (serverConnection) { Assert.True(clientConnection.Connected); Assert.True(serverConnection.Connected); - Assert.Equal(listener.ListenEndPoint, serverConnection.LocalEndPoint); - Assert.Equal(listener.ListenEndPoint, clientConnection.RemoteEndPoint); + Assert.Equal(listener.LocalEndPoint, serverConnection.LocalEndPoint); + Assert.Equal(listener.LocalEndPoint, clientConnection.RemoteEndPoint); Assert.Equal(clientConnection.LocalEndPoint, serverConnection.RemoteEndPoint); await CreateAndTestBidirectionalStream(clientConnection, serverConnection); diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs index 8f970cf6e3f72..ddf1e8e1a755e 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs @@ -42,6 +42,14 @@ public bool RemoteCertificateValidationCallback(object sender, X509Certificate? return true; } + public QuicServerConnectionOptions CreateQuicServerOptions() + { + return new QuicServerConnectionOptions() + { + ServerAuthenticationOptions = GetSslServerAuthenticationOptions() + }; + } + public SslServerAuthenticationOptions GetSslServerAuthenticationOptions() { return new SslServerAuthenticationOptions() @@ -61,18 +69,18 @@ public SslClientAuthenticationOptions GetSslClientAuthenticationOptions() }; } - public QuicClientConnectionOptions CreateQuicClientOptions() + public QuicClientConnectionOptions CreateQuicClientOptions(EndPoint endpoint) { return new QuicClientConnectionOptions() { + RemoteEndPoint = endpoint, ClientAuthenticationOptions = GetSslClientAuthenticationOptions() }; } internal ValueTask CreateQuicConnection(IPEndPoint endpoint) { - var options = CreateQuicClientOptions(); - options.RemoteEndPoint = endpoint; + var options = CreateQuicClientOptions(endpoint); return CreateQuicConnection(options); } @@ -86,16 +94,14 @@ internal QuicListenerOptions CreateQuicListenerOptions() return new QuicListenerOptions() { ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0), - ServerAuthenticationOptions = GetSslServerAuthenticationOptions() + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => ValueTask.FromResult(CreateQuicServerOptions()) }; } internal ValueTask CreateQuicListener(int maxUnidirectionalStreams = 100, int maxBidirectionalStreams = 100) { var options = CreateQuicListenerOptions(); - options.MaxUnidirectionalStreams = maxUnidirectionalStreams; - options.MaxBidirectionalStreams = maxBidirectionalStreams; - return CreateQuicListener(options); } @@ -104,7 +110,8 @@ internal ValueTask CreateQuicListener(IPEndPoint endpoint) var options = new QuicListenerOptions() { ListenEndPoint = endpoint, - ServerAuthenticationOptions = GetSslServerAuthenticationOptions() + ApplicationProtocols = new List() { ApplicationProtocol }, + ConnectionOptionsCallback = (_, _, _) => ValueTask.FromResult(CreateQuicServerOptions()) }; return CreateQuicListener(options); } @@ -114,13 +121,17 @@ internal ValueTask CreateQuicListener(IPEndPoint endpoint) internal Task<(QuicConnection, QuicConnection)> CreateConnectedQuicConnection(QuicListener listener) => CreateConnectedQuicConnection(null, listener); internal async Task<(QuicConnection, QuicConnection)> CreateConnectedQuicConnection(QuicClientConnectionOptions? clientOptions, QuicListenerOptions listenerOptions) { - using (QuicListener listener = await CreateQuicListener(listenerOptions)) + await using (QuicListener listener = await CreateQuicListener(listenerOptions)) { clientOptions ??= new QuicClientConnectionOptions() { + RemoteEndPoint = listener.LocalEndPoint, ClientAuthenticationOptions = GetSslClientAuthenticationOptions() }; - clientOptions.RemoteEndPoint = listener.ListenEndPoint; + if (clientOptions.RemoteEndPoint is IPEndPoint iPEndPoint && !iPEndPoint.Equals(listener.LocalEndPoint)) + { + clientOptions.RemoteEndPoint = listener.LocalEndPoint; + } return await CreateConnectedQuicConnection(clientOptions, listener); } } @@ -137,10 +148,10 @@ internal ValueTask CreateQuicListener(IPEndPoint endpoint) disposeListener = true; } - clientOptions ??= CreateQuicClientOptions(); - if (clientOptions.RemoteEndPoint == null) + clientOptions ??= CreateQuicClientOptions(listener.LocalEndPoint); + if (clientOptions.RemoteEndPoint is IPEndPoint iPEndPoint && !iPEndPoint.Equals(listener.LocalEndPoint)) { - clientOptions.RemoteEndPoint = listener.ListenEndPoint; + clientOptions.RemoteEndPoint = listener.LocalEndPoint; } QuicConnection clientConnection = null; @@ -171,7 +182,7 @@ internal ValueTask CreateQuicListener(IPEndPoint endpoint) QuicConnection serverConnection = await serverTask.ConfigureAwait(false); if (disposeListener) { - listener.Dispose(); + await listener.DisposeAsync(); } Assert.True(serverConnection.Connected); @@ -215,7 +226,7 @@ internal async Task RunClientServer(Func clientFunction, F const long ClientCloseErrorCode = 11111; const long ServerCloseErrorCode = 22222; - using QuicListener listener = await CreateQuicListener(listenerOptions ?? CreateQuicListenerOptions()); + await using QuicListener listener = await CreateQuicListener(listenerOptions ?? CreateQuicListenerOptions()); using var serverFinished = new SemaphoreSlim(0); using var clientFinished = new SemaphoreSlim(0); 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 2fcbb42d4008c..89f016cb962f6 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 @@ -3,6 +3,7 @@ true true $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix + true diff --git a/src/libraries/System.Net.Security/src/MatchingRefApiCompatBaseline.txt b/src/libraries/System.Net.Security/src/MatchingRefApiCompatBaseline.txt index 173770f6a9132..64fce2983b6da 100644 --- a/src/libraries/System.Net.Security/src/MatchingRefApiCompatBaseline.txt +++ b/src/libraries/System.Net.Security/src/MatchingRefApiCompatBaseline.txt @@ -1,3 +1,4 @@ # Exposed publicly only in implementation for Quic MembersMustExist : Member 'public System.Security.Cryptography.X509Certificates.X509Certificate2 System.Security.Cryptography.X509Certificates.X509Certificate2 System.Net.Security.SslStreamCertificateContext.Certificate' does not exist in the reference but it does exist in the implementation. MembersMustExist : Member 'public System.Security.Cryptography.X509Certificates.X509Certificate2[] System.Security.Cryptography.X509Certificates.X509Certificate2[] System.Net.Security.SslStreamCertificateContext.IntermediateCertificates' does not exist in the reference but it does exist in the implementation. +MembersMustExist : Member 'public void System.Net.Security.SslClientHelloInfo..ctor(System.String, System.Security.Authentication.SslProtocols)' does not exist in the reference but it does exist in the implementation. \ No newline at end of file diff --git a/src/libraries/System.Net.Security/src/ReferenceAssemblyExclusions.txt b/src/libraries/System.Net.Security/src/ReferenceAssemblyExclusions.txt index b54e28e188b90..3194936ec9bc4 100644 --- a/src/libraries/System.Net.Security/src/ReferenceAssemblyExclusions.txt +++ b/src/libraries/System.Net.Security/src/ReferenceAssemblyExclusions.txt @@ -1,3 +1,4 @@ # Used internally by System.Net.Quic F:System.Net.Security.SslStreamCertificateContext.Certificate F:System.Net.Security.SslStreamCertificateContext.IntermediateCertificates +C:System.Net.Security.SslClientHelloInfo.SslClientHelloInfo \ No newline at end of file diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslClientHelloInfo.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslClientHelloInfo.cs index c8ee0cb0b78e8..13d6ce7a462e2 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslClientHelloInfo.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslClientHelloInfo.cs @@ -13,7 +13,7 @@ public readonly struct SslClientHelloInfo public readonly string ServerName { get; } public readonly SslProtocols SslProtocols { get; } - internal SslClientHelloInfo(string serverName, SslProtocols sslProtocols) + public SslClientHelloInfo(string serverName, SslProtocols sslProtocols) { ServerName = serverName; SslProtocols = sslProtocols;