{"id":43656,"date":"2022-12-08T10:05:00","date_gmt":"2022-12-08T18:05:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=43656"},"modified":"2022-12-09T04:18:39","modified_gmt":"2022-12-09T12:18:39","slug":"dotnet-7-networking-improvements","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-7-networking-improvements\/","title":{"rendered":".NET 7 Networking Improvements"},"content":{"rendered":"<p>With the recent <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/announcing-dotnet-7\/\">release of .NET 7<\/a>, we&#8217;d like to introduce some interesting changes and additions done in the networking space. This blog post talks about .NET 7 changes in <a href=\"#http\">HTTP space<\/a>, <a href=\"#quic\">new QUIC APIs<\/a>, <a href=\"#security\">networking security<\/a> and <a href=\"#websockets\">WebSockets<\/a>.<\/p>\n<h2>HTTP<\/h2>\n<h3>Improved handling of connection attempt failures<\/h3>\n<p>In versions prior to .NET 6, in case there is no connection immediately available in the connection pool, a new HTTP request will always spin up a new connection attempt and wait for it (if the settings on the handler permit it, e.g. <code>MaxConnectionsPerServer<\/code> for HTTP\/1.1, or <code>EnableMultipleHttp2Connections<\/code> for HTTP\/2). The downside to this is, if it takes a while to establish that connection and another connection becomes available in the meantime, that request would continue waiting for the connection it spawned, hurting latency. In .NET 6.0 <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-6-networking-improvements\/#other-http-changes\">we changed this<\/a> to process requests on whichever connection that becomes available first, whether that\u2019s a newly established one or one that became ready to handle the request in the meantime. A new connection is still created (subject to limits), and if it wasn&#8217;t used by the initiating request, it is then pooled for subsequent requests to possibly use.<\/p>\n<p>Unfortunately, the .NET 6.0 implementation turned out to be problematic for some users: a failing connection attempt also fails the request on the top of the request queue, which may lead to unexpected request failures in <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/60654#issue-1030717005\">certain scenarios<\/a>. Moreover, if there is a pending connection in the pool that will never become usable, e.g. because of a misbehaving server or a network issue, new incoming requests associated with it will also stall and may time out.<\/p>\n<p>In .NET 7.0 we implemented the following changes to address these issues:<\/p>\n<ul>\n<li>A failing connection attempt can only fail it&#8217;s initiating request, and never an unrelated one. If the original request has been handled by the time a connection fails, the connection failure is ignored (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/62935\">dotnet\/runtime#62935<\/a>).<\/li>\n<li>If a request initiates a new connection, but then becomes handled by another connection from the pool, the new pending connection attempt will time out silently after a short period, regardless of <code>ConnectTimeout<\/code>. With this change, stalled connections will not stall unrelated requests (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/71785\">dotnet\/runtime#71785<\/a>). Note that the failures of such &#8220;disowned&#8221; pending connection attempts will happen in the background and never surface to the user, the only way to observe them is to enable telemetry.<\/li>\n<\/ul>\n<h3>HttpHeaders read thread safety<\/h3>\n<p>The <code>HttpHeaders<\/code> collections were never thread-safe. Accessing a header may force lazy parsing of its value, resulting in modifications to the underlying data structures.<\/p>\n<p>Before .NET 6, reading from the collection concurrently happened to be thread-safe in <em>most<\/em> cases.<\/p>\n<p>Starting with .NET 6, less locking was performed around header parsing as it was no longer needed internally.\nDue to this change, multiple examples of users erroneously accessing the headers concurrently surfaced, for example, in gRPC (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/55898\">dotnet\/runtime#55898<\/a>), NewRelic (<a href=\"https:\/\/github.com\/newrelic\/newrelic-dotnet-agent\/issues\/803\">newrelic\/newrelic-dotnet-agent#803<\/a>), or even HttpClient itself (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/65379\">dotnet\/runtime#65379<\/a>).\nViolating thread safety in .NET 6 may result in the header values being duplicated\/malformed or various exceptions being thrown during enumeration\/header accesses.<\/p>\n<p>.NET 7 makes the header behavior more intuitive. The <code>HttpHeaders<\/code> collection now matches the thread-safety guarantees of a <code>Dictionary<\/code>:<\/p>\n<blockquote><p>The collection can support multiple readers concurrently, as long as it is not modified. In the rare case where an enumeration contends with write accesses, the collection must be locked during the entire enumeration. To allow the collection to be accessed by multiple threads for reading and writing, you must implement your own synchronization.<\/p><\/blockquote>\n<p>This was achieved by the following changes:<\/p>\n<ul>\n<li>A &#8220;validating read&#8221; of an invalid value does not result in the removal of the invalid value &#8211; <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/67833\">dotnet\/runtime#67833<\/a> (thanks <a href=\"https:\/\/github.com\/heathbm\">@heathbm<\/a>).<\/li>\n<li>Concurrent reads are thread-safe &#8211; <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/68115\">dotnet\/runtime#68115<\/a>.<\/li>\n<\/ul>\n<h3>Detect HTTP\/2 and HTTP\/3 protocol errors<\/h3>\n<p>The HTTP\/2 and HTTP\/3 protocols define protocol-level error codes in <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc7540#section-7\">RFC 7540 Section 7<\/a> and <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9114.html#section-8.1\">RFC 9114 Section 8.1<\/a>, for example, <code>REFUSED_STREAM (0x7)<\/code> in HTTP\/2 or <code>H3_EXCESSIVE_LOAD (0x0107)<\/code> in HTTP\/3. Unlike HTTP-status codes, this is low-level error information that is unimportant for most <code>HttpClient<\/code> users, but it helps in advanced HTTP\/2 or HTTP\/3 scenarios, notably grpc-dotnet, where distinguishing protocol errors is vital to implement <a href=\"https:\/\/learn.microsoft.com\/aspnet\/core\/grpc\/retries?view=aspnetcore-7.0\">client retries<\/a>.<\/p>\n<p>We defined a new exception <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.http.httpprotocolexception?view=net-7.0\"><code>HttpProtocolException<\/code><\/a> to hold the protocol-level error code in it&#8217;s <code>ErrorCode<\/code> property.<\/p>\n<p>When calling <code>HttpClient<\/code> directly, <code>HttpProtocolException<\/code> can be an inner exception of <code>HttpRequestException<\/code>:<\/p>\n<pre><code class=\"language-csharp\">try\r\n{\r\n    using var response = await httpClient.GetStringAsync(url);\r\n}\r\ncatch (HttpRequestException ex) when (ex.InnerException is HttpProtocolException pex)\r\n{\r\n    Console.WriteLine(\"HTTP error code: \" + pex.ErrorCode)\r\n}<\/code><\/pre>\n<p>When working with <code>HttpContent<\/code>&#8216;s response stream, it is being thrown directly:<\/p>\n<pre><code class=\"language-csharp\">using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);\r\nusing var responseStream = await response.Content.ReadAsStreamAsync();\r\ntry\r\n{\r\n    await responseStream.ReadAsync(buffer);\r\n}\r\ncatch (HttpProtocolException pex)\r\n{\r\n    Console.WriteLine(\"HTTP error code: \" + pex.ErrorCode)\r\n}<\/code><\/pre>\n<h3>HTTP\/3<\/h3>\n<p>HTTP\/3 support in <code>HttpClient<\/code> was already feature complete in the previous .NET release, so we mostly concentrated our efforts in this space on the underlying <code>System.Net.Quic<\/code>. Despite that, we did introduce few fixes and changes in .NET 7.<\/p>\n<p>The most important change is that HTTP\/3 is now enabled by default (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/73153\">dotnet\/runtime#73153<\/a>). It doesn&#8217;t mean that all HTTP requests will prefer HTTP\/3 from now on, but in certain cases they might upgrade to it. For it to happen, the request must opt into version upgrade via <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.http.httprequestmessage.versionpolicy?view=net-7.0\"><code>HttpRequestMessage.VersionPolicy<\/code><\/a> set to <code>RequestVersionOrHigher<\/code>. Then, if the server announces HTTP\/3 authority in <code>Alt-Svc<\/code> header, <code>HttpClient<\/code> will use it for further requests, see <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9114#section-3.1.1\">RFC 9114 Section 3.1.1<\/a>.<\/p>\n<p>To name a few other interesting changes:<\/p>\n<ul>\n<li>HTTP telemetry was extended to cover HTTP\/3 &#8211; <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/40896\">dotnet\/runtime#40896<\/a>.<\/li>\n<li>Exception details were improved in case QUIC connection cannot be established &#8211; <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/70949\">dotnet\/runtime#70949<\/a>.<\/li>\n<li>Proper usage of Host header for Server Name Identification (SNI) was fixed &#8211; <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/57169\">dotnet\/runtime#57169<\/a>.<\/li>\n<\/ul>\n<h2>QUIC<\/h2>\n<p>QUIC is a new, transport layer protocol. It has been recently standardized in <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9000\">RFC 9000<\/a>. It uses UDP as an underlying protocol and it&#8217;s inherently secure as it mandates TLS 1.3 usage, see <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9001\">RFC 9001<\/a>. Another interesting difference from well-known transport protocols such as TCP and UDP is that it has stream multiplexing built-in on the transport layer. This allows having multiple, concurrent, independent data streams that do not affect each other.<\/p>\n<p>QUIC itself doesn&#8217;t define any semantics for the exchanged data as it&#8217;s a transport protocol. It&#8217;s rather used in application layer protocols, for example in <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9114\">HTTP\/3<\/a> or in <a href=\"https:\/\/learn.microsoft.com\/windows-server\/storage\/file-server\/smb-over-quic\">SMB over QUIC<\/a>. It can also be used for any custom defined protocol.<\/p>\n<p>The protocol offers many advantages over TCP with TLS. For instance, faster connection establishment as it doesn&#8217;t require as many round trips as TCP with TLS on top. Or avoidance of head-of-line blocking problem where one lost packet doesn&#8217;t block data of all the other streams. On the other hand, there are disadvantages that come with using QUIC. As it is a new protocol, its adoption is still growing and is limited. Apart from that, QUIC traffic might be even blocked by some networking components.<\/p>\n<h3>QUIC in .NET<\/h3>\n<p>We introduced QUIC implementation in .NET 5 in <code>System.Net.Quic<\/code> library. However, up until now the library was strictly internal and served only for own implementation of HTTP\/3. With the release of .NET 7, we&#8217;re making the library public and we&#8217;re exposing its APIs. Since we had only <code>HttpClient<\/code> and Kestrel as consumers of the APIs for this release, we decided to keep them as <a href=\"https:\/\/github.com\/dotnet\/designs\/blob\/main\/accepted\/2021\/preview-features\/preview-features.md\">preview feature<\/a>. It gives us ability to tweak the API in the next release before we settle on the final shape.<\/p>\n<p>From the implementation perspective, <code>System.Net.Quic<\/code> depends on <a href=\"https:\/\/github.com\/microsoft\/msquic\">MsQuic<\/a>, native implementation of QUIC protocol. As a result, <code>System.Net.Quic<\/code> platform support and dependencies are inherited from MsQuic and documented in <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/extensions\/httpclient-http3#platform-dependencies\">HTTP\/3 Platform dependencies<\/a>. In short, MsQuic library is shipped as part of .NET for Windows. For Linux, <code>libmsquic<\/code> must be manually installed via an appropriate package manager. For the other platforms, it is still possible to build MsQuic manually, whether against SChannel or OpenSSL, and use it with <code>System.Net.Quic<\/code>.<\/p>\n<h3>API overview<\/h3>\n<p><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic?view=net-7.0\"><code>System.Net.Quic<\/code><\/a> brings three major classes that enable usage of QUIC protocol:<\/p>\n<ul>\n<li><code>QuicListener<\/code> &#8211; server side class for accepting incoming connections.<\/li>\n<li><code>QuicConnection<\/code> &#8211; QUIC connection, corresponding to <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9000#section-5\">RFC 9000 Section 5<\/a>.<\/li>\n<li><code>QuicStream<\/code> &#8211; QUIC stream, corresponding to <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9000#section-2\">RFC 9000 Section 2<\/a>.<\/li>\n<\/ul>\n<p>But before any usage of these classes, user code should check whether QUIC is currently supported, as <code>libmsquic<\/code> might be missing, or TLS 1.3 might not be supported. For that, both <code>QuicListener<\/code> and <code>QuicConnection<\/code> expose a static property <code>IsSupported<\/code>:<\/p>\n<pre><code class=\"language-C#\">if (QuicListener.IsSupported)\r\n{\r\n    \/\/ Use QuicListener\r\n}\r\nelse\r\n{\r\n    \/\/ Fallback\/Error\r\n}\r\n\r\nif (QuicConnection.IsSupported)\r\n{\r\n    \/\/ Use QuicConnection\r\n}\r\nelse\r\n{\r\n    \/\/ Fallback\/Error\r\n}<\/code><\/pre>\n<p>Note that, at the moment both these properties are in sync and will report the same value, but that might change in the future. So we recommend to check <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quiclistener.issupported?view=net-7.0\"><code>QuicListener.IsSupported<\/code><\/a> for server-scenarios and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnection.issupported?view=net-7.0\"><code>QuicConnection.IsSupported<\/code><\/a> for the client ones.<\/p>\n<h4>QuicListener<\/h4>\n<p><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quiclistener?view=net-7.0\"><code>QuicListener<\/code><\/a> represents a server side class that accepts incoming connections from the clients. The listener is constructed and started with a static method <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quiclistener.listenasync?view=net-7.0\"><code>QuicListener.ListenAsync<\/code><\/a>. The method accepts an instance of <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quiclisteneroptions?view=net-7.0\"><code>QuicListenerOptions<\/code><\/a> class with all the settings necessary to start the listener and accept incoming connections. After that, listener is ready to hand out connections via <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quiclistener.acceptconnectionasync?view=net-7.0\"><code>AcceptConnectionAsync<\/code><\/a>. Connections returned by this method are always fully connected, meaning that the TLS handshake is finished and the connection is ready to be used. Finally, to stop listening and release all resources, <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quiclistener.disposeasync?view=net-7.0\"><code>DisposeAsync<\/code><\/a> must be called.<\/p>\n<p>The sample usage of <code>QuicListener<\/code>:<\/p>\n<pre><code class=\"language-C#\">using System.Net.Quic;\r\n\r\n\/\/ First, check if QUIC is supported.\r\nif (!QuicListener.IsSupported)\r\n{\r\n    Console.WriteLine(\"QUIC is not supported, check for presence of libmsquic and support of TLS 1.3.\");\r\n    return;\r\n}\r\n\r\n\/\/ We want the same configuration for each incoming connection, so we prepare the connection options upfront and reuse them.\r\n\/\/ This represents the minimal configuration necessary.\r\nvar serverConnectionOptions = new QuicServerConnectionOptions()\r\n{\r\n    \/\/ Used to abort stream if it's not properly closed by the user.\r\n    \/\/ See https:\/\/www.rfc-editor.org\/rfc\/rfc9000#section-20.2\r\n    DefaultStreamErrorCode = 0x0A, \/\/ Protocol-dependent error code.\r\n\r\n    \/\/ Used to close the connection if it's not done by the user.\r\n    \/\/ See https:\/\/www.rfc-editor.org\/rfc\/rfc9000#section-20.2\r\n    DefaultCloseErrorCode = 0x0B, \/\/ Protocol-dependent error code.\r\n\r\n    \/\/ Same options as for server side SslStream.\r\n    ServerAuthenticationOptions = new SslServerAuthenticationOptions\r\n    {\r\n        \/\/ List of supported application protocols, must be the same or subset of QuicListenerOptions.ApplicationProtocols.\r\n        ApplicationProtocols = new List&lt;SslApplicationProtocol&gt;() { \"protocol-name\" },\r\n        \/\/ Server certificate, it can also be provided via ServerCertificateContext or ServerCertificateSelectionCallback.\r\n        ServerCertificate = serverCertificate\r\n    }\r\n};\r\n\r\n\/\/ Initialize, configure the listener and start listening.\r\nvar listener = await QuicListener.ListenAsync(new QuicListenerOptions()\r\n{\r\n    \/\/ Listening endpoint, port 0 means any port.\r\n    ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0),\r\n    \/\/ List of all supported application protocols by this listener.\r\n    ApplicationProtocols = new List&lt;SslApplicationProtocol&gt;() { \"protocol-name\" },\r\n    \/\/ Callback to provide options for the incoming connections, it gets called once per each connection.\r\n    ConnectionOptionsCallback = (_, _, _) =&gt; ValueTask.FromResult(serverConnectionOptions)\r\n});\r\n\r\n\/\/ Accept and process the connections.\r\nwhile (isRunning)\r\n{\r\n    \/\/ Accept will propagate any exceptions that occurred during the connection establishment,\r\n    \/\/ including exceptions thrown from ConnectionOptionsCallback, caused by invalid QuicServerConnectionOptions or TLS handshake failures.\r\n    var connection = await listener.AcceptConnectionAsync();\r\n\r\n    \/\/ Process the connection...\r\n}\r\n\r\n\/\/ When finished, dispose the listener.\r\nawait listener.DisposeAsync();<\/code><\/pre>\n<p>More details about how this class was designed can be found in the <code>QuicListener<\/code> API Proposal (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/67560\">dotnet\/runtime#67560<\/a>).<\/p>\n<h4>QuicConnection<\/h4>\n<p><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnection?view=net-7.0\"><code>QuicConnection<\/code><\/a> is a class used for both server and client side QUIC connections. Server side connections are created internally by the listener and handed out via <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quiclistener.acceptconnectionasync?view=net-7.0\"><code>QuicListener.AcceptConnectionAsync<\/code><\/a>. Client side connections must be opened and connected to the server. As with the listener, there&#8217;s a static method <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnection.connectasync?view=net-7.0\"><code>QuicConnection.ConnectAsync<\/code><\/a> that instantiates and connects the connection. It accepts an instance of <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicclientconnectionoptions?view=net-7.0\"><code>QuicClientConnectionOptions<\/code><\/a>, an analogous class to <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicserverconnectionoptions?view=net-7.0\"><code>QuicServerConnectionOptions<\/code><\/a>. After that, the work with the connection doesn&#8217;t differ between client and server. It can open outgoing streams and accept incoming ones. It also provides properties with information about the connection, like <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnection.localendpoint?view=net-7.0\"><code>LocalEndPoint<\/code><\/a>, <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnection.remoteendpoint?view=net-7.0\"><code>RemoteEndPoint<\/code><\/a>, or <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnection.remotecertificate?view=net-7.0\"><code>RemoteCertificate<\/code><\/a>.<\/p>\n<p>When the work with the connection is done, it needs to be closed and disposed. QUIC protocol mandates using an application layer code for immediate closure, see <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9000#section-10.2\">RFC 9000 Section 10.2<\/a>. For that, <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnection.closeasync?view=net-7.0\"><code>CloseAsync<\/code><\/a> with application layer code can be called or if not, <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnection.disposeasync?view=net-7.0\"><code>DisposeAsync<\/code><\/a> will use the code provided in <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnectionoptions.defaultcloseerrorcode?view=net-7.0#system-net-quic-quicconnectionoptions-defaultcloseerrorcode\"><code>QuicConnectionOptions.DefaultCloseErrorCode<\/code><\/a>. Either way, <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnection.disposeasync?view=net-7.0\"><code>DisposeAsync<\/code><\/a> must be called at the end of the work with the connection to fully release all the associated resources.<\/p>\n<p>The sample usage of <code>QuicConnection<\/code>:<\/p>\n<pre><code class=\"language-C#\">using System.Net.Quic;\r\n\r\n\/\/ First, check if QUIC is supported.\r\nif (!QuicConnection.IsSupported)\r\n{\r\n    Console.WriteLine(\"QUIC is not supported, check for presence of libmsquic and support of TLS 1.3.\");\r\n    return;\r\n}\r\n\r\n\/\/ This represents the minimal configuration necessary to open a connection.\r\nvar clientConnectionOptions = new QuicClientConnectionOptions()\r\n{\r\n    \/\/ End point of the server to connect to.\r\n    RemoteEndPoint = listener.LocalEndPoint,\r\n\r\n    \/\/ Used to abort stream if it's not properly closed by the user.\r\n    \/\/ See https:\/\/www.rfc-editor.org\/rfc\/rfc9000#section-20.2\r\n    DefaultStreamErrorCode = 0x0A, \/\/ Protocol-dependent error code.\r\n\r\n    \/\/ Used to close the connection if it's not done by the user.\r\n    \/\/ See https:\/\/www.rfc-editor.org\/rfc\/rfc9000#section-20.2\r\n    DefaultCloseErrorCode = 0x0B, \/\/ Protocol-dependent error code.\r\n\r\n    \/\/ Optionally set limits for inbound streams.\r\n    MaxInboundUnidirectionalStreams = 10,\r\n    MaxInboundBidirectionalStreams = 100,\r\n\r\n    \/\/ Same options as for client side SslStream.\r\n    ClientAuthenticationOptions = new SslClientAuthenticationOptions()\r\n    {\r\n        \/\/ List of supported application protocols.\r\n        ApplicationProtocols = new List&lt;SslApplicationProtocol&gt;() { \"protocol-name\" }\r\n    }\r\n};\r\n\r\n\/\/ Initialize, configure and connect to the server.\r\nvar connection = await QuicConnection.ConnectAsync(clientConnectionOptions);\r\n\r\nConsole.WriteLine($\"Connected {connection.LocalEndPoint} --&gt; {connection.RemoteEndPoint}\");\r\n\r\n\/\/ Open a bidirectional (can both read and write) outbound stream.\r\nvar outgoingStream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);\r\n\r\n\/\/ Work with the outgoing stream ...\r\n\r\n\/\/ To accept any stream on a client connection, at least one of MaxInboundBidirectionalStreams or MaxInboundUnidirectionalStreams of QuicConnectionOptions must be set.\r\nwhile (isRunning)\r\n{\r\n    \/\/ Accept an inbound stream.\r\n    var incomingStream = await connection.AcceptInboundStreamAsync();\r\n\r\n    \/\/ Work with the incoming stream ...\r\n}\r\n\r\n\/\/ Close the connection with the custom code.\r\nawait connection.CloseAsync(0x0C);\r\n\r\n\/\/ Dispose the connection.\r\nawait connection.DisposeAsync();<\/code><\/pre>\n<p>More details about how this class was designed can be found in the <code>QuicConnection<\/code> API Proposal (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/68902\">dotnet\/runtime#68902<\/a>).<\/p>\n<h4>QuicStream<\/h4>\n<p><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicstream?view=net-7.0\"><code>QuicStream<\/code><\/a> is the actual type that is used to send and receive data in QUIC protocol. It derives from ordinary <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.io.stream?view=net-7.0\"><code>Stream<\/code><\/a> and can be used as such, but it also offers several features that are specific to QUIC protocol. Firstly, a QUIC stream can either be unidirectional or bidirectional, see <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9000#section-2.1\">RFC 9000 Section 2.1<\/a>. A bidirectional stream is able to send and receive data on both sides, whereas unidirectional stream can only write from the initiating side and read on the accepting one. Each peer can limit how many concurrent stream of each type is willing to accept, see <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnectionoptions.maxinboundbidirectionalstreams?view=net-7.0\"><code>QuicConnectionOptions.MaxInboundBidirectionalStreams<\/code><\/a> and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnectionoptions.maxinboundunidirectionalstreams?view=net-7.0\"><code>QuicConnectionOptions.MaxInboundUnidirectionalStreams<\/code><\/a>.<\/p>\n<p>Another particularity of QUIC stream is ability to explicitly close the writing side in the middle of work with the stream, see <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicstream.completewrites?view=net-7.0\"><code>CompleteWrites<\/code><\/a> or <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicstream.writeasync?view=net-7.0#system-net-quic-quicstream-writeasync(system-readonlymemory((system-byte)\"><code>WriteAsync<\/code><\/a>-system-boolean-system-threading-cancellationtoken)) overload with <code>completeWrites<\/code> argument. Closing of the writing side lets the peer know that no more data will arrive, yet the peer still can continue sending (in case of a bidirectional stream). This is useful in scenarios like HTTP request\/response exchange when the client sends the request and closes the writing side to let the server know that this is the end of the request content. Server is still able to send the response after that, but knows that no more data will arrive from the client. And for erroneous cases, either writing or reading side of the stream can be aborted, see <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicstream.abort?view=net-7.0\"><code>Abort<\/code><\/a>. The behavior of the individual methods for each stream type is summarized in the following table (note that both client and server can open and accept streams):<\/p>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th>Peer opening stream<\/th>\n<th>Peer accepting stream<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>CanRead<\/code><\/td>\n<td><em>bidirectional<\/em>: <code>true<\/code>\n<em>unidirectional<\/em>: <code>false<\/code><\/td>\n<td><code>true<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>CanWrite<\/code><\/td>\n<td><code>true<\/code><\/td>\n<td><em>bidirectional<\/em>: <code>true<\/code>\n<em>unidirectional<\/em>: <code>false<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>ReadAsync<\/code><\/td>\n<td><em>bidirectional<\/em>: reads data\n<em>unidirectional<\/em>: <code>InvalidOperationException<\/code><\/td>\n<td>reads data<\/td>\n<\/tr>\n<tr>\n<td><code>WriteAsync<\/code><\/td>\n<td>sends data =&gt; peer read returns the data<\/td>\n<td><em>bidirectional<\/em>: sends data =&gt; peer read returns the data\n<em>unidirectional<\/em>: <code>InvalidOperationException<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>CompleteWrites<\/code><\/td>\n<td>closes writing side =&gt; peer read returns 0<\/td>\n<td><em>bidirectional<\/em>: closes writing side =&gt; peer read returns 0\n<em>unidirectional<\/em>: no-op<\/td>\n<\/tr>\n<tr>\n<td><code>Abort(QuicAbortDirection.Read)<\/code><\/td>\n<td><em>bidirectional<\/em>: <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9000#section-19.5\">STOP_SENDING<\/a> =&gt; peer write throws <code>QuicException(QuicError.OperationAborted)<\/code>\n<em>unidirectional<\/em>: no-op<\/td>\n<td><a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9000#section-19.5\">STOP_SENDING<\/a> =&gt; peer write throws <code>QuicException(QuicError.OperationAborted)<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>Abort(QuicAbortDirection.Write)<\/code><\/td>\n<td><a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9000#section-19.4\">RESET_STREAM<\/a> =&gt; peer read throws <code>QuicException(QuicError.OperationAborted)<\/code><\/td>\n<td><em>bidirectional<\/em>: <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9000#section-19.4\">RESET_STREAM<\/a> =&gt; peer read throws <code>QuicException(QuicError.OperationAborted)<\/code>\n<em>unidirectional<\/em>: no-op<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>On top of these methods, <code>QuicStream<\/code> offers two specialized properties to get notified whenever either reading or writing side of the stream has been closed: <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicstream.readsclosed?view=net-7.0\"><code>ReadsClosed<\/code><\/a> and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicstream.writesclosed?view=net-7.0\"><code>WritesClosed<\/code><\/a>. Both return a <code>Task<\/code> that completes with its corresponding side getting closed, whether it be success or abort, in which case the <code>Task<\/code> will contain appropriate exception. These properties are useful when the user code needs to know about stream side getting closed without issuing call to <code>ReadAsync<\/code> or <code>WriteAsync<\/code>.<\/p>\n<p>Finally, when the work with the stream is done, it needs to be disposed with <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicstream.disposeasync?view=net-7.0\"><code>DisposeAsync<\/code><\/a>. The dispose will make sure that both reading and\/or writing side &#8211; depending on the stream type &#8211; is closed. If stream hasn&#8217;t been properly read till the end, dispose will issue an equivalent of <code>Abort(QuicAbortDirection.Read)<\/code>. However, if stream writing side hasn&#8217;t been closed, it will be gracefully closed as it would be with <code>CompleteWrites<\/code>. The reason for this difference is to make sure that scenarios working with an ordinary <code>Stream<\/code> behave as expected and lead to a successful path. Consider the following example:<\/p>\n<pre><code class=\"language-C#\">\/\/ Work done with all different types of streams.\r\nasync Task WorkWithStream(Stream stream)\r\n{\r\n    \/\/ This will dispose the stream at the end of the scope.\r\n    await using (stream)\r\n    {\r\n        \/\/ Simple echo, read data and send them back.\r\n        byte[] buffer = new byte[1024];\r\n        int count = 0;\r\n        \/\/ The loop stops when read returns 0 bytes as is common for all streams.\r\n        while ((count = await stream.ReadAsync(buffer)) &gt; 0)\r\n        {\r\n            await stream.WriteAsync(buffer.AsMemory(0, count));\r\n        }\r\n    }\r\n}\r\n\r\n\/\/ Open a QuicStream and pass to the common method.\r\nvar quicStream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);\r\nawait WorkWithStream(quicStream);\r\n<\/code><\/pre>\n<p>The sample usage of <code>QuicStream<\/code> in client scenario:<\/p>\n<pre><code class=\"language-C#\">\/\/ Consider connection from the connection example, open a bidirectional stream.\r\nawait using var stream = await connection.OpenStreamAsync(QuicStreamType.Bidirectional, cancellationToken);\r\n\r\n\/\/ Send some data.\r\nawait stream.WriteAsync(data, cancellationToken);\r\nawait stream.WriteAsync(data, cancellationToken);\r\n\r\n\/\/ End the writing-side together with the last data.\r\nawait stream.WriteAsync(data, endStream: true, cancellationToken);\r\n\/\/ Or separately.\r\nstream.CompleteWrites();\r\n\r\n\/\/ Read data until the end of stream.\r\nwhile (await stream.ReadAsync(buffer, cancellationToken) &gt; 0)\r\n{\r\n    \/\/ Handle buffer data...\r\n}\r\n\r\n\/\/ DisposeAsync called by await using at the top.<\/code><\/pre>\n<p>And the sample usage of <code>QuicStream<\/code> in server scenario:<\/p>\n<pre><code class=\"language-C#\">\/\/ Consider connection from the connection example, accept a stream.\r\nawait using var stream = await connection.AcceptStreamAsync(cancellationToken);\r\n\r\nif (stream.Type != QuicStreamType.Bidirectional)\r\n{\r\n    Console.WriteLine($\"Expected bidirectional stream, got {stream.Type}\");\r\n    return;\r\n}\r\n\r\n\/\/ Read the data.\r\nwhile (stream.ReadAsync(buffer, cancellationToken) &gt; 0)\r\n{\r\n    \/\/ Handle buffer data...\r\n\r\n    \/\/ Client completed the writes, the loop might be exited now without another ReadAsync.\r\n    if (stream.ReadsCompleted.IsCompleted)\r\n    {\r\n        break;\r\n    }\r\n}\r\n\r\n\/\/ Listen for Abort(QuicAbortDirection.Read) from the client.\r\nvar writesClosedTask = WritesClosedAsync(stream);\r\nasync ValueTask WritesClosedAsync(QuicStream stream)\r\n{\r\n    try\r\n    {\r\n        await stream.WritesClosed;\r\n    }\r\n    catch (Exception ex)\r\n    {\r\n        \/\/ Handle peer aborting our writing side ...\r\n    }\r\n}\r\n\r\n\/\/ DisposeAsync called by await using at the top.<\/code><\/pre>\n<p>More details about how this class was designed can be found in the <code>QuicStream<\/code> API Proposal (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/69675\">dotnet\/runtime#69675<\/a>).<\/p>\n<h3>Future<\/h3>\n<p>As <code>System.Net.Quic<\/code> is newly made public and we have only limited usages, we&#8217;ll appreciate any bug reports or insights on the API shape. Thanks to APIs being in preview mode, we still have a chance to tweak them for .NET 8 based on the feedback we get. Issues can be filed in <a href=\"https:\/\/github.com\/dotnet\/runtime\">dotnet\/runtime<\/a> repository.<\/p>\n<h2>Security<\/h2>\n<h3>Negotiate API<\/h3>\n<p><a href=\"https:\/\/learn.microsoft.com\/windows-server\/security\/windows-authentication\/windows-authentication-overview\">Windows Authentication<\/a> is an encompassing term for multiple technologies used in enterprises to authenticate users and applications against a central authority, usually the domain controller. It enables scenarios like single sign-on to email services or intranet applications. The underlying technologies used for the authentication are Kerberos, NTLM, and the encompassing Negotiate protocol, where the most suitable technology is chosen for a specific authentication scenario.<\/p>\n<p>Prior to .NET 7, Windows Authentication was exposed in the high-level APIs, like <code>HttpClient<\/code> (<code>Negotiate<\/code> and <code>NTLM<\/code> authentication schemes), <code>SmtpClient<\/code> (<code>GSSAPI<\/code> and <code>NTLM<\/code> authentication schemes), <code>NegotiateStream<\/code>, <a href=\"https:\/\/learn.microsoft.com\/aspnet\/core\/security\/authentication\/windowsauth?view=aspnetcore-7.0\">ASP.NET Core<\/a>, and the SQL Server client libraries. While that covers most scenarios for end users, it is limiting for library authors. Other libraries like the <a href=\"https:\/\/www.npgsql.org\/\">Npgsql PostgreSQL client<\/a>, <a href=\"https:\/\/github.com\/jstedfast\/MailKit\">MailKit<\/a>, <a href=\"https:\/\/github.com\/xqrzd\/kudu-client-net\">Apache Kudu client<\/a> and others needed to resort to various tricks to implement the same authentication schemes for low-level protocols that were not built on HTTP or other available high-level building blocks.<\/p>\n<p>.NET 7 introduces new API providing low-level building blocks to perform the authentication exchange for the above mentioned protocols, see <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/69920\">dotnet\/runtime#69920<\/a>. As with all the other APIs in .NET, it is built with cross-platform interoperability in mind. On Linux, macOS, iOS, and other similar platforms, it uses the GSSAPI system library. On Windows, it relies on the <a href=\"https:\/\/learn.microsoft.com\/windows\/win32\/rpc\/sspi-architectural-overview\">SSPI<\/a> library. For platforms where system implementation is not available, such as Android and tvOS, a limited client-only implementation is present.<\/p>\n<h4>How to use the API<\/h4>\n<p>In order to understand how the authentication API works, let&#8217;s start with an example of how the authentication session looks in a high-level protocol like SMTP. The example is taken from <a href=\"https:\/\/learn.microsoft.com\/openspecs\/windows_protocols\/ms-ssean\/0fa15a99-af88-428a-a51d-742b8450d877\">Microsoft protocol documentation<\/a> that explains it in greater detail.<\/p>\n<pre><code class=\"language-plaintext\">S: 220 server.contoso.com Authenticated Receive Connector\r\nC: EHLO client.contoso.com\r\nS: 250-server-contoso.com Hello [203.0.113.1]\r\nS: 250-AUTH GSSAPI NTLM\r\nS: 250 OK\r\nC: AUTH GSSAPI &lt;token1&gt;\r\nS: 334 &lt;token2&gt;\r\nC: &lt;token3&gt;\r\nS: 235 2.7.0 Authentication successful<\/code><\/pre>\n<p>The authentication starts with the client producing a challenge token. Then the server produces a response. The client processes the response, and a new challenge is sent to the server. This challenge\/response exchange can happen multiple times. It finishes when either party rejects the authentication or when both parties accept the authentication. The format of the tokens is defined by the Windows Authentication protocols, while the encapsulation is part of the high-level protocol specification. In this example, the SMTP protocol prepends the <code>334<\/code> code to tell the client that the server produced an authentication response, and the <code>235<\/code> code indicates successful authentication.<\/p>\n<p>The bulk of the new API centers around a new <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.negotiateauthentication?view=net-7.0\"><code>NegotiateAuthentication<\/code><\/a> class. It is used to instantiate the context for client-side or server-side authentication. There are various options to specify requirements for establishing the authenticated session, like requiring encryption or determining that a specific protocol (Negotiate, Kerberos, or NTLM) is to be used. Once the parameters are specified, the authentication proceeds by exchanging the authentication challenges\/responses between the client and the server. The <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.negotiateauthentication.getoutgoingblob?view=net-7.0\"><code>GetOutgoingBlob<\/code><\/a> method is used for that. It can work either on byte spans or base64-encoded strings.<\/p>\n<p>The following code will perform both, client and server, part of the authentication for the current user on the same machine:<\/p>\n<pre><code class=\"language-C#\">using System.Net;\r\nusing System.Net.Security;\r\n\r\nvar serverAuthentication = new NegotiateAuthentication(new NegotiateAuthenticationServerOptions { });\r\nvar clientAuthentication = new NegotiateAuthentication(\r\n    new NegotiateAuthenticationClientOptions\r\n    {\r\n        Package = \"Negotiate\",\r\n        Credential = CredentialCache.DefaultNetworkCredentials,\r\n        TargetName = \"HTTP\/localhost\",\r\n        RequiredProtectionLevel = ProtectionLevel.Sign\r\n    });\r\n\r\nstring? serverBlob = null;\r\nwhile (!clientAuthentication.IsAuthenticated)\r\n{\r\n    \/\/ Client produces the authentication challenge, or response to server's challenge\r\n    string? clientBlob = clientAuthentication.GetOutgoingBlob(serverBlob, out var clientStatusCode);\r\n    if (clientStatusCode == NegotiateAuthenticationStatusCode.ContinueNeeded)\r\n    {\r\n        \/\/ Send the client blob to the server; this would normally happen over a network\r\n        Console.WriteLine($\"C: {clientBlob}\");\r\n        serverBlob = serverAuthentication.GetOutgoingBlob(clientBlob, out var serverStatusCode);\r\n        if (serverStatusCode != NegotiateAuthenticationStatusCode.Completed &amp;&amp;\r\n            serverStatusCode != NegotiateAuthenticationStatusCode.ContinueNeeded)\r\n        {\r\n            Console.WriteLine($\"Server authentication failed with status code {serverStatusCode}\");\r\n            break;\r\n        }\r\n        Console.WriteLine($\"S: {serverBlob}\");\r\n    }\r\n    else\r\n    {\r\n        Console.WriteLine(\r\n            clientStatusCode == NegotiateAuthenticationStatusCode.Completed ?\r\n            \"Successfully authenticated\" :\r\n            $\"Authentication failed with status code {clientStatusCode}\");\r\n        break;\r\n    }\r\n}<\/code><\/pre>\n<p>Once the authenticated session is established, the <code>NegotiateAuthentication<\/code> instance can be used to sign\/encrypt the outgoing messages and verify\/decrypt the incoming messages. This is done through the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.negotiateauthentication.wrap?view=net-7.0\"><code>Wrap<\/code><\/a> and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.negotiateauthentication.unwrap?view=net-7.0\"><code>Unwrap<\/code><\/a> methods.<\/p>\n<p>This change was done as well as this part of the article was written by a community contributor <a href=\"https:\/\/github.com\/filipnavara\">@filipnavara<\/a>. Thanks!<\/p>\n<h3>Options for certificate validation<\/h3>\n<p>When a client receives server&#8217;s certificate, or vice-versa if client certificate is requested, the certificate is validated via <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.security.cryptography.x509certificates.x509chain?view=net-7.0\"><code>X509Chain<\/code><\/a>. The validation happens always, even if <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.remotecertificatevalidationcallback?view=net-7.0\"><code>RemoteCertificateValidationCallback<\/code><\/a> is provided, and additional certificates might get downloaded during the validation. Several issues were raised as there was no way to control this behavior. Among them were asks to completely prevent certificate download, put a timeout on it, or to provide custom store to get the certificates from. To mitigate this whole group of issues, we decided to introduce a new property <code>CertificateChainPolicy<\/code> on both <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.sslclientauthenticationoptions?view=net-7.0\"><code>SslClientAuthenticationOptions<\/code><\/a> and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.sslserverauthenticationoptions?view=net-7.0\"><code>SslServerAuthenticationOptions<\/code><\/a>. The goal of this property is to override the default behavior of <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.sslstream?view=net-7.0\"><code>SslStream<\/code><\/a> when building the chain during <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.sslstream.authenticateasclientasync?view=net-7.0\"><code>AuthenticateAsClientAsync<\/code><\/a> \/ <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.sslstream.authenticateasserverasync?view=net-7.0\"><code>AuthenticateAsServerAsync<\/code><\/a> operation. In normal circumstances, <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.security.cryptography.x509certificates.x509chainpolicy?view=net-7.0\"><code>X509ChainPolicy<\/code><\/a> is constructed automatically in the background. But if this new property is specified, it will take precedence and be used instead, giving the user full control over the certificate validation process.<\/p>\n<p>The usage of the chain policy might look like:<\/p>\n<pre><code class=\"language-C#\">\/\/ Client side:\r\nvar policy = new X509ChainPolicy();\r\npolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;\r\npolicy.ExtraStore.Add(s_ourPrivateRoot);\r\npolicy.UrlRetrievalTimeout = TimeSpan.FromSeconds(3);\r\n\r\nvar options  = new SslClientAuthenticationOptions();\r\noptions.TargetHost = \"myServer\";\r\noptions.CertificateChainPolicy = policy;\r\n\r\nvar sslStream = new SslStream(transportStream);\r\nsslStream.AuthenticateAsClientAsync(options, cancellationToken);\r\n\r\n\/\/ Server side:\r\nvar policy = new X509ChainPolicy();\r\npolicy.DisableCertificateDownloads = true;\r\n\r\nvar options  = new SslServerAuthenticationOptions();\r\noptions.CertificateChainPolicy = policy;\r\n\r\nvar sslStream = new SslStream(transportStream);\r\nsslStream.AuthenticateAsServerAsync(options, cancellationToken);<\/code><\/pre>\n<p>More info can be found in the API proposal (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/71191\">dotnet\/runtime#71191<\/a>).<\/p>\n<h3>Performance<\/h3>\n<p>Most of the networking perfomance improvements in .NET 7 are covered by Stephen&#8217;s article <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance_improvements_in_net_7\/#networking\">Performance Improvements in .NET 7 &#8211; Networking<\/a>, but some of the security ones are worth mentioning again.<\/p>\n<h4>TLS Resume<\/h4>\n<p>Establishing new TLS connection is fairly expensive operation as it requires multiple steps and several round trips. In scenarios where connection to the same server is re-created very often, time consumed by the handshakes will add up. TLS offers feature to mitigate this called Session Resumption, see <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc5246.html#section-7.3\">RFC 5246 Section 7.3<\/a> and <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc8446#section-2.2\">RFC 8446 Section 2.2<\/a>. In short, during the handshake, client can send an identification of previously established TLS session and if server agrees, the security context gets re-established based on the cached data from the previous connection. Even though the mechanics differ for different TLS versions, the end goal is the same, save a round-trip and some CPU time when re-establishing connection to a previously connected server.\nThis feature is automatically provided by SChannel on Windows, but with OpenSSL on Linux it required several changes to enable this:<\/p>\n<ul>\n<li>Server side (stateless) &#8211; <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/57079\">dotnet\/runtime#57079<\/a> and <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/63030\">dotnet\/runtime#63030<\/a>.<\/li>\n<li>Client side &#8211; <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/64369\">dotnet\/runtime#64369<\/a>.<\/li>\n<li>Cache size control &#8211; <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/69065\">dotnet\/runtime#69065<\/a>.<\/li>\n<\/ul>\n<p>If caching the TLS context is not desired, it can be turned off process-wide with either environment variable &#8220;DOTNET_SYSTEM_NET_SECURITY_DISABLETLSRESUME&#8221; or via <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.appcontext.setswitch?view=net-7.0\"><code>AppContext.SetSwitch<\/code><\/a> &#8220;System.Net.Security.TlsCacheSize&#8221;.<\/p>\n<h4>OCSP Stapling<\/h4>\n<p>Online Certificate Status Protocol (OCSP) Stapling is a mechanism for server to provide signed and timestamped proof (OCSP response) that the sent certificate has not been revoked, see <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc6961\">RFC 6961<\/a>. As a result, client doesn&#8217;t need to contact the OCSP server itself, reducing the number of requests necessary to establish the connection as well as the load exerted on the OCSP server. And as the OCSP response needs to be signed by the certificate authority (CA), it cannot be forged by the server providing the certificate. We didn&#8217;t take advantage of this TLS feature until this release, for more details see <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/33377\">dotnet\/runtime#33377<\/a>.<\/p>\n<h3>Consistency across platforms<\/h3>\n<p>We are aware, that some of the functionality provided by .NET is available only on certain platforms. But each release we try to narrow the gap a bit more. In .NET 7, we made several changes in the networking security space to improve the disparity:<\/p>\n<ul>\n<li>Support for post-handshake authentication on Linux for TLS 1.3 &#8211; <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/64268\">dotnet\/runtime#64268<\/a><\/li>\n<li>Remote certificate is now set on Windows in <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.sslclientauthenticationoptions.localcertificateselectioncallback?view=net-7.0\"><code>SslClientAuthenticationOptions.LocalCertificateSelectionCallback<\/code><\/a> &#8211; <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/65134\">dotnet\/runtime#65134<\/a><\/li>\n<li>Support for sending names of trusted CA in TLS handshake on OSX and Linux &#8211; <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/65195\">dotnet\/runtime#65195<\/a><\/li>\n<\/ul>\n<h2>WebSockets<\/h2>\n<h3>WebSocket handshake response details<\/h3>\n<p>Prior to .NET 7, server&#8217;s response part of WebSocket&#8217;s opening handshake (HTTP response to Upgrade request) was hidden inside <code>ClientWebSocket<\/code> implementation, and all handshake errors would surface as <code>WebSocketException<\/code> without much details beside the exception message. However, the information about HTTP response headers and status code might be important in both failure and success scenarios.<\/p>\n<p>In case of failure, HTTP status code can help to distinguish between retriable and non-retriable errors (e.g. server doesn&#8217;t support WebSockets at all, or it was just a transient network error). Headers might also contain additional information on how to handle the situation. The headers are useful even in case of a successful WebSocket handshake, e.g. they can contain token tied to a session, information related to subprotocol version, or that the server can go down soon.<\/p>\n<p>.NET 7 adds <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocketoptions.collecthttpresponsedetails?view=net-7.0\"><code>CollectHttpResponseDetails<\/code><\/a> setting to <code>ClientWebSocketOptions<\/code> that enables collecting upgrade response details in a <code>ClientWebSocket<\/code> instance during <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocket.connectasync?view=net-7.0\"><code>ConnectAsync<\/code><\/a> call. You can later access the data using <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocket.httpstatuscode?view=net-7.0\"><code>HttpStatusCode<\/code><\/a> and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocket.httpresponseheaders?view=net-7.0\"><code>HttpResponseHeaders<\/code><\/a> properties of the <code>ClientWebSocket<\/code> instance, even in case of <code>ConnectAsync<\/code> throwing an exception. Note that in the exceptional case, the information might be unavailable, i.e. if the server never responded to the request.<\/p>\n<p>Also note that in case of a successful connect and after consuming <code>HttpResponseHeaders<\/code> data, you can reduce <code>ClientWebSocket<\/code>&#8216;s memory footprint by setting <code>ClientWebSocket.HttpResponseHeaders<\/code> property to <code>null<\/code>.<\/p>\n<pre><code class=\"language-c#\">var ws = new ClientWebSocket();\r\nws.Options.CollectHttpResponseDetails = true;\r\ntry\r\n{\r\n    await ws.ConnectAsync(uri, cancellationToken);\r\n    \/\/ success scenario\r\n    ProcessSuccess(ws.HttpResponseHeaders);\r\n    ws.HttpResponseHeaders = null; \/\/ clean up (if needed)\r\n}\r\ncatch (WebSocketException)\r\n{\r\n    \/\/ failure scenario\r\n    if (ws.HttpStatusCode != 0)\r\n    {\r\n        ProcessFailure(ws.HttpStatusCode, ws.HttpResponseHeaders);\r\n    }\r\n}<\/code><\/pre>\n<h3>Providing external HTTP client<\/h3>\n<p>In the default case, <code>ClientWebSocket<\/code> uses a cached static <code>HttpMessageInvoker<\/code> instance to execute HTTP Upgrade request. However, there are certain <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocket.options?view=net-7.0#system-net-websockets-clientwebsocket-options\"><code>ClientWebSocketOptions<\/code><\/a> that prevent caching the invoker, such as <code>Proxy<\/code>, <code>ClientCertificates<\/code> or <code>Cookies<\/code>. <code>HttpMessageInvoker<\/code> instance with these parameters is not safe to be reused and needs be created each time <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocket.connectasync?view=net-7.0#system-net-websockets-clientwebsocket-connectasync(system-uri-system-threading-cancellationtoken)\"><code>ConnectAsync<\/code><\/a> is called. This results in many unnecessary allocations and makes reuse of <code>HttpMessageInvoker<\/code> connection pool impossible.<\/p>\n<p>.NET 7 allows you to pass an existing <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.http.httpmessageinvoker?view=net-7.0\"><code>HttpMessageInvoker<\/code><\/a> (e.g. <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.http.httpclient?view=net-7.0\"><code>HttpClient<\/code><\/a>) instance to <code>ConnectAsync<\/code> call, using the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocket.connectasync?view=net-7.0#system-net-websockets-clientwebsocket-connectasync(system-uri-system-net-http-httpmessageinvoker-system-threading-cancellationtoken)\"><code>ConnectAsync(Uri, HttpMessageInvoker, CancellationToken)<\/code><\/a> overload. In that case, HTTP Upgrade request would be executed using the provided instance.<\/p>\n<pre><code class=\"language-c#\">var httpClient = new HttpClient();\r\n\r\nvar ws = new ClientWebSocket();\r\nawait ws.ConnectAsync(uri, httpClient, cancellationToken);<\/code><\/pre>\n<p>Note that in case a custom HTTP invoker is passed, any of the following <code>ClientWebSocketOptions<\/code> <em>must not<\/em> be set, and should be set up on the HTTP invoker instead:<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocketoptions.clientcertificates?view=net-7.0\"><code>ClientCertificates<\/code><\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocketoptions.cookies?view=net-7.0\"><code>Cookies<\/code><\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocketoptions.credentials?view=net-7.0\"><code>Credentials<\/code><\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocketoptions.proxy?view=net-7.0\"><code>Proxy<\/code><\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocketoptions.remotecertificatevalidationcallback?view=net-7.0\"><code>RemoteCertificateValidationCallback<\/code><\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocketoptions.usedefaultcredentials?view=net-7.0\"><code>UseDefaultCredentials<\/code><\/a><\/li>\n<\/ul>\n<p>This is how you can set up all of these options on the <code>HttpMessageInvoker<\/code> instance:<\/p>\n<pre><code class=\"language-c#\">var handler = new HttpClientHandler();\r\nhandler.CookieContainer = cookies;\r\nhandler.UseCookies = cookies != null;\r\nhandler.ServerCertificateCustomValidationCallback = remoteCertificateValidationCallback;\r\nhandler.Credentials = useDefaultCredentials ?\r\n    CredentialCache.DefaultCredentials :\r\n    credentials;\r\nif (proxy == null)\r\n{\r\n    handler.UseProxy = false;\r\n}\r\nelse\r\n{\r\n    handler.Proxy = proxy;\r\n}\r\nif (clientCertificates?.Count &gt; 0)\r\n{\r\n    handler.ClientCertificates.AddRange(clientCertificates);\r\n}\r\nvar invoker = new HttpMessageInvoker(handler);\r\n\r\nvar ws = new ClientWebSocket();\r\nawait ws.ConnectAsync(uri, invoker, cancellationToken);<\/code><\/pre>\n<h3>WebSockets over HTTP\/2<\/h3>\n<p>.NET 7 also adds an ability to use WebSocket protocol over HTTP\/2, as described in <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc8441\">RFC 8441<\/a>. With that, WebSocket connection is established over a single stream on HTTP\/2 connection. This allows for a single TCP connection to be shared between several WebSocket connections and HTTP requests at the same time, resulting in more efficient use of the network.<\/p>\n<p>To enable WebSockets over HTTP\/2, you can set <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocketoptions.httpversion?view=net-7.0\"><code>ClientWebSocketOptions.HttpVersion<\/code><\/a> option to <code>HttpVersion.Version20<\/code>. You can also enable upgrade\/downgrade of HTTP version used by setting <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocketoptions.httpversionpolicy?view=net-7.0\"><code>ClientWebSocketOptions.HttpVersionPolicy<\/code><\/a> property. These options will behave in the same way <code>HttpRequestMessage.Version<\/code> and <code>HttpRequestMessage.VersionPolicy<\/code> behave.<\/p>\n<p>For example, the following code would probe for HTTP\/2 WebSockets, and if a WebSocket connection cannot be established, it will fallback to HTTP\/1.1:<\/p>\n<pre><code class=\"language-c#\">var ws = new ClientWebSocket();\r\nws.Options.HttpVersion = HttpVersion.Version20;\r\nws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrLower;\r\nawait ws.ConnectAsync(uri, httpClient, cancellationToken);<\/code><\/pre>\n<p>The combination of <code>HttpVersion.Version11<\/code> and <code>HttpVersionPolicy.RequestVersionOrHigher<\/code> will result in the same behavior as above, while <code>HttpVersionPolicy.RequestVersionExact<\/code> will disallow upgrade\/downgrade of HTTP version.<\/p>\n<p>By default, <code>HttpVersion = HttpVersion.Version11<\/code> and <code>HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrLower<\/code> are set, meaning that only HTTP\/1.1 will be used.<\/p>\n<p>The ability to multiplex WebSocket connections and HTTP requests over a single HTTP\/2 connection is a crucial part of this feature. For it to work as expected, you need to pass and reuse the same <code>HttpMessageInvoker<\/code> instance (e.g. <code>HttpClient<\/code>) from your code when calling <code>ConnectAsync<\/code>, i.e. use <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.websockets.clientwebsocket.connectasync?view=net-7.0#system-net-websockets-clientwebsocket-connectasync(system-uri-system-net-http-httpmessageinvoker-system-threading-cancellationtoken)\"><code>ConnectAsync(Uri, HttpMessageInvoker, CancellationToken)<\/code><\/a> overload. This will reuse the connection pool within the <code>HttpMessageInvoker<\/code> instance for the multiplexing.<\/p>\n<h2>Final notes<\/h2>\n<p>We try to pick the most interesting and impactful changes in the networking space. The article doesn&#8217;t contain all the changes we did, but they can be found in <a href=\"https:\/\/github.com\/dotnet\/runtime\">dotnet\/runtime<\/a> repository. As usual, performance improvements are mostly omitted as they get their spot in Stephens&#8217;s article <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance_improvements_in_net_7\">Performance Improvements in .NET 7<\/a>. We&#8217;d also like to hear from you, so if you encounter an issue or have any feedback, you can file it in <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\">our GitHub<\/a>. Finally, you can find similar blog posts for previous releases here: <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-6-networking-improvements\/\">.NET 6 Networking Improvements<\/a>, <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/net-5-new-networking-improvements\/\">.NET 5 Networking Improvements<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introducing new networking features in .NET 7 including HTTP space, new QUIC APIs, security, WebSockets, and more!<\/p>\n","protected":false},"author":53871,"featured_media":43657,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7591],"tags":[7611,7676,7696,123,7455],"class_list":["post-43656","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-networking","tag-dotnet-7","tag-http","tag-http-space","tag-security","tag-websockets"],"acf":[],"blog_post_summary":"<p>Introducing new networking features in .NET 7 including HTTP space, new QUIC APIs, security, WebSockets, and more!<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/43656","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/53871"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=43656"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/43656\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/43657"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=43656"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=43656"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=43656"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}