{"id":42564,"date":"2022-09-29T10:05:00","date_gmt":"2022-09-29T17:05:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=42564"},"modified":"2024-12-13T15:10:49","modified_gmt":"2024-12-13T23:10:49","slug":"experimental-webtransport-over-http-3-support-in-kestrel","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/experimental-webtransport-over-http-3-support-in-kestrel\/","title":{"rendered":"Experimental WebTransport over HTTP\/3 support in Kestrel"},"content":{"rendered":"<p>We\u2019re excited to announce experimental support for the WebTransport over HTTP\/3 protocol as part of .NET 7 RC1.<\/p>\n<p>This feature and blog post were written by our excellent intern <a href=\"https:\/\/github.com\/Daniel-Genkin-MS-2\">Daniel Genkin<\/a>!<\/p>\n<h2>What is WebTransport<\/h2>\n<p>WebTransport is a new <a href=\"https:\/\/ietf-wg-webtrans.github.io\/draft-ietf-webtrans-http3\/draft-ietf-webtrans-http3.html\">draft specification<\/a> for a transport protocol similar to WebSockets that allows the usage of multiple streams per connection.<\/p>\n<p><a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc6455.html\">WebSockets<\/a> allowed upgrading a whole HTTP TCP\/TLS connection to a bidirectional data stream. If you needed to open more streams you&#8217;d spend additional time and resources establishing new TCP and TLS sessions. <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc8441\">WebSockets over HTTP\/2<\/a> streamlined this by allowing multiple WebSocket streams to be established over one HTTP\/2 TCP\/TLS session. The downside here is that because this was still based on TCP, any packets lost from one stream would cause delays for every stream on the connection.<\/p>\n<p>With the introduction of HTTP\/3 and QUIC, which uses UDP rather than TCP, WebTransport can be used to establish multiple streams on one connection without them blocking each other. For example, consider an online game where the game state is transmitted on one bidirectional stream, the players&#8217; voices for the game&#8217;s voice chat feature on another bidirectional stream, and the player&#8217;s controls are transmitted on a unidirectional stream. With WebSockets, these would all need to be put on separate connections or squashed into a single stream. With WebTransport, you can keep all the traffic on one connection but separate them into their own streams and, if one stream were to drop network packets, the others could continue uninterrupted.<\/p>\n<h2>Enabling WebTransport<\/h2>\n<p>You can enable WebTransport support in ASP.NET Core by setting <code>EnablePreviewFeatures<\/code> to <code>True<\/code> and adding the following <code>RuntimeHostConfigurationOption<\/code> item in your project&#8217;s <code>.csproj<\/code> file:<\/p>\n<pre><code class=\"language-xml\">&lt;Project Sdk=\"Microsoft.NET.Sdk.Web\"&gt;\r\n  &lt;PropertyGroup&gt;\r\n    &lt;EnablePreviewFeatures&gt;True&lt;\/EnablePreviewFeatures&gt;\r\n  &lt;\/PropertyGroup&gt;\r\n\r\n  &lt;ItemGroup&gt;\r\n    &lt;RuntimeHostConfigurationOption Include=\"Microsoft.AspNetCore.Server.Kestrel.Experimental.WebTransportAndH3Datagrams\" Value=\"true\" \/&gt;\r\n  &lt;\/ItemGroup&gt;\r\n&lt;\/Project&gt;<\/code><\/pre>\n<h2>Setting up a Server<\/h2>\n<p>To setup a WebTransport connection, you first need to configure a web host to listen on a port over HTTP\/3:<\/p>\n<pre><code class=\"language-C#\">var builder = WebApplication.CreateBuilder(args);\r\nbuilder.WebHost.ConfigureKestrel((context, options) =&gt;\r\n{\r\n    \/\/ Port configured for WebTransport\r\n    options.ListenAnyIP([SOME PORT], listenOptions =&gt;\r\n    {\r\n        listenOptions.UseHttps(GenerateManualCertificate());\r\n        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;\r\n    });\r\n});\r\nvar app = builder.Build();<\/code><\/pre>\n<p>WebTransport uses HTTP\/3, so you must select the <code>listenOptions.UseHttps<\/code> setting as well as set the <code>listenOptions.Protocols<\/code> to include HTTP\/3.<\/p>\n<p>The default Kestrel development certificate cannot be used for WebTransport connections. For local testing you can use the workaround described in the <a href=\"#obtaining-a-test-certificate\">Obtaining a test certificate section<\/a>.<\/p>\n<p>Next, we define the code that will run when Kestrel receives a connection.<\/p>\n<pre><code class=\"language-C#\">app.Run(async (context) =&gt;\r\n{\r\n    var feature = context.Features.GetRequiredFeature&lt;IHttpWebTransportFeature&gt;();\r\n    if (!feature.IsWebTransportRequest)\r\n    {\r\n        return;\r\n    }\r\n    var session = await feature.AcceptAsync(CancellationToken.None);\r\n\r\n    \/\/ Use WebTransport via the newly established session.\r\n});\r\n\r\nawait app.RunAsync();<\/code><\/pre>\n<p>The <code>Run<\/code> method is triggered every time there is a connection request. The <code>IsWebTransportRequest<\/code> property on the <code>IHttpWebTransportFeature<\/code> indicates if the current request is a WebTransport request. Once a WebTransport request is received, calling <code>IHttpWebTransportFeature.AcceptAsync()<\/code> accepts the WebTransport session so you can interact with the client. This original request must be kept alive for the durration of the session or else all streams associated with that session will be closed.<\/p>\n<p>Calling <code>await app.RunAsync()<\/code> starts the server, which can then start accepting connections.<\/p>\n<h2>Interacting with a WebTransport Session<\/h2>\n<p>Once the session has been established both the client and server can create streams for that session.<\/p>\n<p>This example accepts a session, waits for a bidirectional stream, reads some data, reverse it, and then writes it back to the stream.<\/p>\n<pre><code class=\"language-C#\">app.Run(async (context) =&gt;\r\n{\r\n    var feature = context.Features.GetRequiredFeature&lt;IHttpWebTransportFeature&gt;();\r\n    if (!feature.IsWebTransportRequest)\r\n    {\r\n        return;\r\n    }\r\n    var session = await feature.AcceptAsync(CancellationToken.None);\r\n\r\n    ConnectionContext? stream = null;\r\n    IStreamDirectionFeature? direction = null;\r\n    while (true)\r\n    {\r\n        \/\/ wait until we get a stream\r\n        stream = await session.AcceptStreamAsync(CancellationToken.None);\r\n        if (stream is null)\r\n        {\r\n            \/\/ if a stream is null, this means that the session failed to get the next one.\r\n            \/\/ Thus, the session has ended, or some other issue has occurred. We end the\r\n            \/\/ connection in this case.\r\n            return;\r\n        }\r\n\r\n        \/\/ check that the stream is bidirectional. If yes, keep going, otherwise\r\n        \/\/ dispose its resources and keep waiting.\r\n        direction = stream.Features.GetRequiredFeature&lt;IStreamDirectionFeature&gt;();\r\n        if (direction.CanRead &amp;&amp; direction.CanWrite)\r\n        {\r\n            break;\r\n        }\r\n        else\r\n        {\r\n            await stream.DisposeAsync();\r\n        }\r\n    }\r\n\r\n    var inputPipe = stream!.Transport.Input;\r\n    var outputPipe = stream!.Transport.Output;\r\n\r\n    \/\/ read some data from the stream into the memory\r\n    var length = await inputPipe.AsStream().ReadAsync(memory);\r\n\r\n    \/\/ slice to only keep the relevant parts of the memory\r\n    var outputMemory = memory[..length];\r\n\r\n    \/\/ do some operations on the contents of the data\r\n    outputMemory.Span.Reverse();\r\n\r\n    \/\/ write back the data to the stream\r\n    await outputPipe.WriteAsync(outputMemory);\r\n});\r\n\r\nawait app.RunAsync();<\/code><\/pre>\n<p>This example opens a new stream from the server side and then sends data.<\/p>\n<pre><code class=\"language-C#\">app.Run(async (context) =&gt;\r\n{\r\n    var feature = context.Features.GetRequiredFeature&lt;IHttpWebTransportFeature&gt;();\r\n    if (!feature.IsWebTransportRequest)\r\n    {\r\n        return;\r\n    }\r\n    var session = await feature.AcceptAsync(CancellationToken.None);\r\n\r\n    \/\/ open a new stream from the server to the client\r\n    var stream = await session.OpenUnidirectionalStreamAsync(CancellationToken.None);\r\n\r\n    if (stream is not null)\r\n    {\r\n        \/\/ write data to the stream\r\n        var outputPipe = stream.Transport.Output;\r\n        await outputPipe.WriteAsync(new Memory&lt;byte&gt;(new byte[] { 65, 66, 67, 68, 69 }), CancellationToken.None);\r\n        await outputPipe.FlushAsync(CancellationToken.None);\r\n    }\r\n});\r\n\r\nawait app.RunAsync();<\/code><\/pre>\n<h2>Sample Apps<\/h2>\n<p>To showcase the functionality of the WebTransport support in Kestrel, we also created two sample apps: an <a href=\"https:\/\/github.com\/dotnet\/aspnetcore\/tree\/main\/src\/Servers\/Kestrel\/samples\/WebTransportInteractiveSampleApp\">interactive<\/a> one and a <a href=\"https:\/\/github.com\/dotnet\/aspnetcore\/tree\/main\/src\/Servers\/Kestrel\/samples\/WebTransportSampleApp\">console-based<\/a> one. You can run them in Visual Studio via the F5 run options.<\/p>\n<h2>Obtaining a test certificate<\/h2>\n<p>The current Kestrel development certificate cannot be used for WebTransport connections as it does not meet the requirements needed for WebTransport over HTTP\/3. You can generate a new certificate for testing via the following C# (this function will also automatically handle certificate rotation every time one expires):<\/p>\n<pre><code class=\"language-C#\">static X509Certificate2 GenerateManualCertificate()\r\n{\r\n    X509Certificate2 cert = null;\r\n    var store = new X509Store(\"KestrelWebTransportCertificates\", StoreLocation.CurrentUser);\r\n    store.Open(OpenFlags.ReadWrite);\r\n    if (store.Certificates.Count &gt; 0)\r\n    {\r\n        cert = store.Certificates[^1];\r\n\r\n        \/\/ rotate key after it expires\r\n        if (DateTime.Parse(cert.GetExpirationDateString(), null) &lt; DateTimeOffset.UtcNow)\r\n        {\r\n            cert = null;\r\n        }\r\n    }\r\n    if (cert == null)\r\n    {\r\n        \/\/ generate a new cert\r\n        var now = DateTimeOffset.UtcNow;\r\n        SubjectAlternativeNameBuilder sanBuilder = new();\r\n        sanBuilder.AddDnsName(\"localhost\");\r\n        using var ec = ECDsa.Create(ECCurve.NamedCurves.nistP256);\r\n        CertificateRequest req = new(\"CN=localhost\", ec, HashAlgorithmName.SHA256);\r\n        \/\/ Adds purpose\r\n        req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection\r\n        {\r\n            new(\"1.3.6.1.5.5.7.3.1\") \/\/ serverAuth\r\n        }, false));\r\n        \/\/ Adds usage\r\n        req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));\r\n        \/\/ Adds subject alternate names\r\n        req.CertificateExtensions.Add(sanBuilder.Build());\r\n        \/\/ Sign\r\n        using var crt = req.CreateSelfSigned(now, now.AddDays(14)); \/\/ 14 days is the max duration of a certificate for this\r\n        cert = new(crt.Export(X509ContentType.Pfx));\r\n\r\n        \/\/ Save\r\n        store.Add(cert);\r\n    }\r\n    store.Close();\r\n\r\n    var hash = SHA256.HashData(cert.RawData);\r\n    var certStr = Convert.ToBase64String(hash);\r\n    Console.WriteLine($\"\\n\\n\\n\\n\\nCertificate: {certStr}\\n\\n\\n\\n\"); \/\/ &lt;-- you will need to put this output into the JS API call to allow the connection\r\n    return cert;\r\n}\r\n\/\/ Adapted from: https:\/\/github.com\/wegylexy\/webtransport<\/code><\/pre>\n<h2>Going Forward<\/h2>\n<p>WebTransport offers exciting new possibilities for real-time app developers, making it easier and more efficient to stream multiple kinds of data back and forth with the client.<\/p>\n<p>Please send us <a href=\"https:\/\/github.com\/dotnet\/aspnetcore\/issues\">feedback<\/a> about how this works for your scenarios. We&#8217;ll use that feedback to continue the development of this feature in .NET 8, explore integrations with frameworks like SignalR and determine when to take it out of preview.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>ASP.NET Core now has experimental support for WebTransport over HTTP\/3, a new secure multiplexed transport protocol for the web. Learn how to try out this new transport protocol in your app.<\/p>\n","protected":false},"author":451,"featured_media":42565,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7509],"tags":[7676,7678,102,7677],"class_list":["post-42564","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-aspnetcore","tag-http","tag-kestrel","tag-networking","tag-webtransport"],"acf":[],"blog_post_summary":"<p>ASP.NET Core now has experimental support for WebTransport over HTTP\/3, a new secure multiplexed transport protocol for the web. Learn how to try out this new transport protocol in your app.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/42564","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\/451"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=42564"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/42564\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/42565"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=42564"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=42564"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=42564"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}