From 845e6d5512c3521a07d7fa93737b0da041a8a3c6 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Thu, 7 Nov 2019 20:53:33 -0800 Subject: [PATCH] Add rest of MsQuic transport. (#16812) --- .gitignore | 1 + ...ore.Connections.Abstractions.netcoreapp.cs | 15 + ...Connections.Abstractions.netstandard2.0.cs | 15 + ...Connections.Abstractions.netstandard2.1.cs | 15 + .../src/Features/IQuicCreateStreamFeature.cs | 13 + .../Features/IQuicStreamListenerFeature.cs | 12 + .../Features/IUnidirectionalStreamFeature.cs | 9 + src/Servers/Kestrel/Kestrel.sln | 58 ++- ...ver.Kestrel.Transport.MsQuic.netcoreapp.cs | 35 ++ .../src/Internal/IMsQuicTrace.cs | 16 + .../src/Internal/MsQuicApi.cs | 51 ++ .../src/Internal/MsQuicConnection.cs | 334 +++++++++++++ .../src/Internal/MsQuicConnectionListener.cs | 195 ++++++++ .../src/Internal/MsQuicConstants.cs | 9 +- .../src/Internal/MsQuicNativeMethods.cs | 161 ++++--- .../src/Internal/MsQuicStream.cs | 443 ++++++++++++++++++ .../src/Internal/MsQuicTrace.cs | 53 +++ .../src/Internal/MsQuicTransportContext.cs | 21 + .../src/Internal/QuicSecConfig.cs | 44 ++ .../src/Internal/QuicSession.cs | 141 ++++++ .../Internal/ResettableCompletionSource.cs | 58 +++ .../src/Internal/UIntExtensions.cs | 25 + ...ore.Server.Kestrel.Transport.MsQuic.csproj | 25 +- .../src/MsQuicConnectionFactory.cs | 64 +++ .../src/MsQuicTransportFactory.cs | 47 ++ .../src/MsQuicTransportOptions.cs | 55 +++ .../src/WebHostBuilderMsQuicExtensions.cs | 30 ++ .../Kestrel/samples/QuicSampleApp/Program.cs | 104 ++++ .../QuicSampleApp/QuicSampleApp.csproj | 16 + .../samples/QuicSampleClient/Program.cs | 106 +++++ .../QuicSampleClient/QuicSampleClient.csproj | 15 + src/Servers/Kestrel/shared/DuplexPipe.cs | 2 +- .../dotnet-watch/test/DotNetWatcherTests.cs | 3 +- .../dotnet-watch/test/GlobbingAppTests.cs | 3 +- src/Tools/dotnet-watch/test/NoDepsAppTests.cs | 3 +- 35 files changed, 2115 insertions(+), 82 deletions(-) create mode 100644 src/Servers/Connections.Abstractions/src/Features/IQuicCreateStreamFeature.cs create mode 100644 src/Servers/Connections.Abstractions/src/Features/IQuicStreamListenerFeature.cs create mode 100644 src/Servers/Connections.Abstractions/src/Features/IUnidirectionalStreamFeature.cs create mode 100644 src/Servers/Kestrel/Transport.MsQuic/src/Internal/IMsQuicTrace.cs create mode 100644 src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnection.cs create mode 100644 src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnectionListener.cs create mode 100644 src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicStream.cs create mode 100644 src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicTrace.cs create mode 100644 src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicTransportContext.cs create mode 100644 src/Servers/Kestrel/Transport.MsQuic/src/Internal/QuicSecConfig.cs create mode 100644 src/Servers/Kestrel/Transport.MsQuic/src/Internal/QuicSession.cs create mode 100644 src/Servers/Kestrel/Transport.MsQuic/src/Internal/ResettableCompletionSource.cs create mode 100644 src/Servers/Kestrel/Transport.MsQuic/src/Internal/UIntExtensions.cs create mode 100644 src/Servers/Kestrel/Transport.MsQuic/src/MsQuicConnectionFactory.cs create mode 100644 src/Servers/Kestrel/Transport.MsQuic/src/MsQuicTransportFactory.cs create mode 100644 src/Servers/Kestrel/Transport.MsQuic/src/MsQuicTransportOptions.cs create mode 100644 src/Servers/Kestrel/Transport.MsQuic/src/WebHostBuilderMsQuicExtensions.cs create mode 100644 src/Servers/Kestrel/samples/QuicSampleApp/Program.cs create mode 100644 src/Servers/Kestrel/samples/QuicSampleApp/QuicSampleApp.csproj create mode 100644 src/Servers/Kestrel/samples/QuicSampleClient/Program.cs create mode 100644 src/Servers/Kestrel/samples/QuicSampleClient/QuicSampleClient.csproj diff --git a/.gitignore b/.gitignore index fddfe14e16..427adc7189 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ modules/ *.psess *.res *.snk +*.so *.suo *.tlog *.user diff --git a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netcoreapp.cs b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netcoreapp.cs index 3922d970b9..0e39f6a158 100644 --- a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netcoreapp.cs +++ b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netcoreapp.cs @@ -136,6 +136,14 @@ namespace Microsoft.AspNetCore.Connections public override string ToString() { throw null; } } } +namespace Microsoft.AspNetCore.Connections.Abstractions.Features +{ + public partial interface IQuicCreateStreamFeature + { + System.Threading.Tasks.ValueTask StartBidirectionalStreamAsync(); + System.Threading.Tasks.ValueTask StartUnidirectionalStreamAsync(); + } +} namespace Microsoft.AspNetCore.Connections.Features { public partial interface IConnectionCompleteFeature @@ -185,6 +193,10 @@ namespace Microsoft.AspNetCore.Connections.Features { System.Buffers.MemoryPool MemoryPool { get; } } + public partial interface IQuicStreamListenerFeature + { + System.Threading.Tasks.ValueTask AcceptAsync(); + } public partial interface ITlsHandshakeFeature { System.Security.Authentication.CipherAlgorithmType CipherAlgorithm { get; } @@ -200,4 +212,7 @@ namespace Microsoft.AspNetCore.Connections.Features Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; } Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; } } + public partial interface IUnidirectionalStreamFeature + { + } } diff --git a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs index 3922d970b9..0e39f6a158 100644 --- a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs +++ b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs @@ -136,6 +136,14 @@ namespace Microsoft.AspNetCore.Connections public override string ToString() { throw null; } } } +namespace Microsoft.AspNetCore.Connections.Abstractions.Features +{ + public partial interface IQuicCreateStreamFeature + { + System.Threading.Tasks.ValueTask StartBidirectionalStreamAsync(); + System.Threading.Tasks.ValueTask StartUnidirectionalStreamAsync(); + } +} namespace Microsoft.AspNetCore.Connections.Features { public partial interface IConnectionCompleteFeature @@ -185,6 +193,10 @@ namespace Microsoft.AspNetCore.Connections.Features { System.Buffers.MemoryPool MemoryPool { get; } } + public partial interface IQuicStreamListenerFeature + { + System.Threading.Tasks.ValueTask AcceptAsync(); + } public partial interface ITlsHandshakeFeature { System.Security.Authentication.CipherAlgorithmType CipherAlgorithm { get; } @@ -200,4 +212,7 @@ namespace Microsoft.AspNetCore.Connections.Features Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; } Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; } } + public partial interface IUnidirectionalStreamFeature + { + } } diff --git a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.1.cs b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.1.cs index 3922d970b9..0e39f6a158 100644 --- a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.1.cs +++ b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.1.cs @@ -136,6 +136,14 @@ namespace Microsoft.AspNetCore.Connections public override string ToString() { throw null; } } } +namespace Microsoft.AspNetCore.Connections.Abstractions.Features +{ + public partial interface IQuicCreateStreamFeature + { + System.Threading.Tasks.ValueTask StartBidirectionalStreamAsync(); + System.Threading.Tasks.ValueTask StartUnidirectionalStreamAsync(); + } +} namespace Microsoft.AspNetCore.Connections.Features { public partial interface IConnectionCompleteFeature @@ -185,6 +193,10 @@ namespace Microsoft.AspNetCore.Connections.Features { System.Buffers.MemoryPool MemoryPool { get; } } + public partial interface IQuicStreamListenerFeature + { + System.Threading.Tasks.ValueTask AcceptAsync(); + } public partial interface ITlsHandshakeFeature { System.Security.Authentication.CipherAlgorithmType CipherAlgorithm { get; } @@ -200,4 +212,7 @@ namespace Microsoft.AspNetCore.Connections.Features Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; } Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; } } + public partial interface IUnidirectionalStreamFeature + { + } } diff --git a/src/Servers/Connections.Abstractions/src/Features/IQuicCreateStreamFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IQuicCreateStreamFeature.cs new file mode 100644 index 0000000000..1de25e4313 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/Features/IQuicCreateStreamFeature.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Connections.Abstractions.Features +{ + public interface IQuicCreateStreamFeature + { + ValueTask StartUnidirectionalStreamAsync(); + ValueTask StartBidirectionalStreamAsync(); + } +} diff --git a/src/Servers/Connections.Abstractions/src/Features/IQuicStreamListenerFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IQuicStreamListenerFeature.cs new file mode 100644 index 0000000000..e9f63aeb36 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/Features/IQuicStreamListenerFeature.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Connections.Features +{ + public interface IQuicStreamListenerFeature + { + ValueTask AcceptAsync(); + } +} diff --git a/src/Servers/Connections.Abstractions/src/Features/IUnidirectionalStreamFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IUnidirectionalStreamFeature.cs new file mode 100644 index 0000000000..1c6550d008 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/Features/IUnidirectionalStreamFeature.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Connections.Features +{ + public interface IUnidirectionalStreamFeature + { + } +} diff --git a/src/Servers/Kestrel/Kestrel.sln b/src/Servers/Kestrel/Kestrel.sln index 6ed4f64fd5..34e55eb7b2 100644 --- a/src/Servers/Kestrel/Kestrel.sln +++ b/src/Servers/Kestrel/Kestrel.sln @@ -84,7 +84,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebUti EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "http2cat", "samples\http2cat\http2cat.csproj", "{3D6821F5-F242-4828-8DDE-89488E85512D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic", "Transport.MsQuic\src\Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.csproj", "{1BC94F37-AF61-4641-A80A-EC32A15C5344}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuicSampleApp", "samples\QuicSampleApp\QuicSampleApp.csproj", "{53A8634C-DFC5-4A5B-8864-9EF1707E3F18}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic", "Transport.MsQuic\src\Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.csproj", "{62CFF861-807E-43F6-9403-22AA7F06C9A6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuicSampleClient", "samples\QuicSampleClient\QuicSampleClient.csproj", "{F39A942B-85A8-4C1B-A5BC-435555E79F20}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -468,18 +472,42 @@ Global {3D6821F5-F242-4828-8DDE-89488E85512D}.Release|x64.Build.0 = Release|Any CPU {3D6821F5-F242-4828-8DDE-89488E85512D}.Release|x86.ActiveCfg = Release|Any CPU {3D6821F5-F242-4828-8DDE-89488E85512D}.Release|x86.Build.0 = Release|Any CPU - {1BC94F37-AF61-4641-A80A-EC32A15C5344}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1BC94F37-AF61-4641-A80A-EC32A15C5344}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1BC94F37-AF61-4641-A80A-EC32A15C5344}.Debug|x64.ActiveCfg = Debug|Any CPU - {1BC94F37-AF61-4641-A80A-EC32A15C5344}.Debug|x64.Build.0 = Debug|Any CPU - {1BC94F37-AF61-4641-A80A-EC32A15C5344}.Debug|x86.ActiveCfg = Debug|Any CPU - {1BC94F37-AF61-4641-A80A-EC32A15C5344}.Debug|x86.Build.0 = Debug|Any CPU - {1BC94F37-AF61-4641-A80A-EC32A15C5344}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1BC94F37-AF61-4641-A80A-EC32A15C5344}.Release|Any CPU.Build.0 = Release|Any CPU - {1BC94F37-AF61-4641-A80A-EC32A15C5344}.Release|x64.ActiveCfg = Release|Any CPU - {1BC94F37-AF61-4641-A80A-EC32A15C5344}.Release|x64.Build.0 = Release|Any CPU - {1BC94F37-AF61-4641-A80A-EC32A15C5344}.Release|x86.ActiveCfg = Release|Any CPU - {1BC94F37-AF61-4641-A80A-EC32A15C5344}.Release|x86.Build.0 = Release|Any CPU + {53A8634C-DFC5-4A5B-8864-9EF1707E3F18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53A8634C-DFC5-4A5B-8864-9EF1707E3F18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53A8634C-DFC5-4A5B-8864-9EF1707E3F18}.Debug|x64.ActiveCfg = Debug|Any CPU + {53A8634C-DFC5-4A5B-8864-9EF1707E3F18}.Debug|x64.Build.0 = Debug|Any CPU + {53A8634C-DFC5-4A5B-8864-9EF1707E3F18}.Debug|x86.ActiveCfg = Debug|Any CPU + {53A8634C-DFC5-4A5B-8864-9EF1707E3F18}.Debug|x86.Build.0 = Debug|Any CPU + {53A8634C-DFC5-4A5B-8864-9EF1707E3F18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53A8634C-DFC5-4A5B-8864-9EF1707E3F18}.Release|Any CPU.Build.0 = Release|Any CPU + {53A8634C-DFC5-4A5B-8864-9EF1707E3F18}.Release|x64.ActiveCfg = Release|Any CPU + {53A8634C-DFC5-4A5B-8864-9EF1707E3F18}.Release|x64.Build.0 = Release|Any CPU + {53A8634C-DFC5-4A5B-8864-9EF1707E3F18}.Release|x86.ActiveCfg = Release|Any CPU + {53A8634C-DFC5-4A5B-8864-9EF1707E3F18}.Release|x86.Build.0 = Release|Any CPU + {62CFF861-807E-43F6-9403-22AA7F06C9A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62CFF861-807E-43F6-9403-22AA7F06C9A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62CFF861-807E-43F6-9403-22AA7F06C9A6}.Debug|x64.ActiveCfg = Debug|Any CPU + {62CFF861-807E-43F6-9403-22AA7F06C9A6}.Debug|x64.Build.0 = Debug|Any CPU + {62CFF861-807E-43F6-9403-22AA7F06C9A6}.Debug|x86.ActiveCfg = Debug|Any CPU + {62CFF861-807E-43F6-9403-22AA7F06C9A6}.Debug|x86.Build.0 = Debug|Any CPU + {62CFF861-807E-43F6-9403-22AA7F06C9A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62CFF861-807E-43F6-9403-22AA7F06C9A6}.Release|Any CPU.Build.0 = Release|Any CPU + {62CFF861-807E-43F6-9403-22AA7F06C9A6}.Release|x64.ActiveCfg = Release|Any CPU + {62CFF861-807E-43F6-9403-22AA7F06C9A6}.Release|x64.Build.0 = Release|Any CPU + {62CFF861-807E-43F6-9403-22AA7F06C9A6}.Release|x86.ActiveCfg = Release|Any CPU + {62CFF861-807E-43F6-9403-22AA7F06C9A6}.Release|x86.Build.0 = Release|Any CPU + {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Debug|x64.ActiveCfg = Debug|Any CPU + {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Debug|x64.Build.0 = Debug|Any CPU + {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Debug|x86.ActiveCfg = Debug|Any CPU + {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Debug|x86.Build.0 = Debug|Any CPU + {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|Any CPU.Build.0 = Release|Any CPU + {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x64.ActiveCfg = Release|Any CPU + {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x64.Build.0 = Release|Any CPU + {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x86.ActiveCfg = Release|Any CPU + {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -517,7 +545,9 @@ Global {E0AD50A3-2518-4060-8BB9-5649B04B3A6D} = {F0A1281A-B512-49D2-8362-21EE32B3674F} {EE45763C-753D-4228-8E5D-A71F8BDB3D89} = {F0A1281A-B512-49D2-8362-21EE32B3674F} {3D6821F5-F242-4828-8DDE-89488E85512D} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0} - {1BC94F37-AF61-4641-A80A-EC32A15C5344} = {2B456D08-F72B-4EB8-B663-B6D78FC04BF8} + {53A8634C-DFC5-4A5B-8864-9EF1707E3F18} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0} + {62CFF861-807E-43F6-9403-22AA7F06C9A6} = {2B456D08-F72B-4EB8-B663-B6D78FC04BF8} + {F39A942B-85A8-4C1B-A5BC-435555E79F20} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {48207B50-7D05-4B10-B585-890FE0F4FCE1} diff --git a/src/Servers/Kestrel/Transport.MsQuic/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.netcoreapp.cs b/src/Servers/Kestrel/Transport.MsQuic/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.netcoreapp.cs index 618082bc4a..56555304f1 100644 --- a/src/Servers/Kestrel/Transport.MsQuic/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.netcoreapp.cs +++ b/src/Servers/Kestrel/Transport.MsQuic/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.netcoreapp.cs @@ -1,3 +1,38 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +namespace Microsoft.AspNetCore.Hosting +{ + public static partial class WebHostBuilderMsQuicExtensions + { + public static Microsoft.AspNetCore.Hosting.IWebHostBuilder UseMsQuic(this Microsoft.AspNetCore.Hosting.IWebHostBuilder hostBuilder) { throw null; } + public static Microsoft.AspNetCore.Hosting.IWebHostBuilder UseMsQuic(this Microsoft.AspNetCore.Hosting.IWebHostBuilder hostBuilder, System.Action configureOptions) { throw null; } + } +} +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic +{ + public partial class MsQuicConnectionFactory : Microsoft.AspNetCore.Connections.IConnectionFactory + { + public MsQuicConnectionFactory(Microsoft.Extensions.Options.IOptions options, Microsoft.Extensions.Hosting.IHostApplicationLifetime lifetime, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } + [System.Diagnostics.DebuggerStepThroughAttribute] + public System.Threading.Tasks.ValueTask ConnectAsync(System.Net.EndPoint endPoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + } + public partial class MsQuicTransportFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory + { + public MsQuicTransportFactory(Microsoft.Extensions.Hosting.IHostApplicationLifetime applicationLifetime, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions options) { } + [System.Diagnostics.DebuggerStepThroughAttribute] + public System.Threading.Tasks.ValueTask BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + } + public partial class MsQuicTransportOptions + { + public MsQuicTransportOptions() { } + public string Alpn { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public System.Security.Cryptography.X509Certificates.X509Certificate2 Certificate { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public System.TimeSpan IdleTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public ushort MaxBidirectionalStreamCount { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public long? MaxReadBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public ushort MaxUnidirectionalStreamCount { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public long? MaxWriteBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public string RegistrationName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + } +} diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/IMsQuicTrace.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/IMsQuicTrace.cs new file mode 100644 index 0000000000..07c2cf0436 --- /dev/null +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/IMsQuicTrace.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal +{ + internal interface IMsQuicTrace : ILogger + { + void NewConnection(string connectionId); + void NewStream(string streamId); + void ConnectionError(string connectionId, Exception ex); + void StreamError(string streamId, Exception ex); + } +} diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicApi.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicApi.cs index 811b2a5dfe..0bcbecbc94 100644 --- a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicApi.cs +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicApi.cs @@ -3,6 +3,9 @@ using System; using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal { @@ -160,6 +163,54 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal Buffer.Buffer); } + public async ValueTask CreateSecurityConfig(X509Certificate2 certificate) + { + QuicSecConfig secConfig = null; + var tcs = new TaskCompletionSource(); + var secConfigCreateStatus = MsQuicConstants.InternalError; + + var status = SecConfigCreateDelegate( + _registrationContext, + (uint)QUIC_SEC_CONFIG_FLAG.CERT_CONTEXT, + certificate.Handle, + null, + IntPtr.Zero, + SecCfgCreateCallbackHandler); + + MsQuicStatusException.ThrowIfFailed(status); + + void SecCfgCreateCallbackHandler( + IntPtr context, + uint status, + IntPtr securityConfig) + { + secConfig = new QuicSecConfig(this, securityConfig); + secConfigCreateStatus = status; + tcs.SetResult(null); + } + + await tcs.Task; + + MsQuicStatusException.ThrowIfFailed(secConfigCreateStatus); + + return secConfig; + } + + public QuicSession SessionOpen( + string alpn) + { + var sessionPtr = IntPtr.Zero; + + var status = SessionOpenDelegate( + _registrationContext, + Encoding.UTF8.GetBytes(alpn), + IntPtr.Zero, + ref sessionPtr); + MsQuicStatusException.ThrowIfFailed(status); + + return new QuicSession(this, sessionPtr); + } + public void Dispose() { Dispose(disposing: true); diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnection.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnection.cs new file mode 100644 index 0000000000..617fa31caf --- /dev/null +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnection.cs @@ -0,0 +1,334 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.InteropServices; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Abstractions.Features; +using Microsoft.AspNetCore.Connections.Features; +using static Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal.MsQuicNativeMethods; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal +{ + internal class MsQuicConnection : TransportConnection, IQuicStreamListenerFeature, IQuicCreateStreamFeature, IDisposable + { + public MsQuicApi _api; + private bool _disposed; + private readonly MsQuicTransportContext _context; + private readonly IMsQuicTrace _log; + private IntPtr _nativeObjPtr; + private static GCHandle _handle; + private ConnectionCallbackDelegate _connectionDelegate; + private readonly Channel _acceptQueue = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = true + }); + + public MsQuicConnection(MsQuicApi api, MsQuicTransportContext context, IntPtr nativeObjPtr) + { + _api = api; + _context = context; + _log = context.Log; + _nativeObjPtr = nativeObjPtr; + + SetCallbackHandler(); + + SetIdleTimeout(_context.Options.IdleTimeout); + + Features.Set(this); + Features.Set(this); + + _log.NewConnection(ConnectionId); + } + + internal uint HandleEvent(ref ConnectionEvent connectionEvent) + { + var status = MsQuicConstants.Success; + switch (connectionEvent.Type) + { + case QUIC_CONNECTION_EVENT.CONNECTED: + { + status = HandleEventConnected( + connectionEvent); + } + break; + + case QUIC_CONNECTION_EVENT.SHUTDOWN_BEGIN: + { + status = HandleEventShutdownBegin( + connectionEvent); + } + break; + + case QUIC_CONNECTION_EVENT.SHUTDOWN_BEGIN_PEER: + { + status = HandleEventShutdownBeginPeer( + connectionEvent); + } + break; + + case QUIC_CONNECTION_EVENT.SHUTDOWN_COMPLETE: + { + status = HandleEventShutdownComplete( + connectionEvent); + } + break; + + case QUIC_CONNECTION_EVENT.NEW_STREAM: + { + status = HandleEventNewStream( + connectionEvent); + } + break; + + case QUIC_CONNECTION_EVENT.STREAMS_AVAILABLE: + { + status = HandleEventStreamsAvailable( + connectionEvent); + } + break; + + default: + break; + } + return status; + } + + protected virtual uint HandleEventConnected(ConnectionEvent connectionEvent) + { + return MsQuicConstants.Success; + } + + protected virtual uint HandleEventShutdownBegin(ConnectionEvent connectionEvent) + { + return MsQuicConstants.Success; + } + + protected virtual uint HandleEventShutdownBeginPeer(ConnectionEvent connectionEvent) + { + return MsQuicConstants.Success; + } + + protected virtual uint HandleEventShutdownComplete(ConnectionEvent connectionEvent) + { + return MsQuicConstants.Success; + } + + protected virtual uint HandleEventNewStream(ConnectionEvent connectionEvent) + { + var msQuicStream = new MsQuicStream(_api, this, _context, connectionEvent.StreamFlags, connectionEvent.Data.NewStream.Stream); + + _acceptQueue.Writer.TryWrite(msQuicStream); + + return MsQuicConstants.Success; + } + + protected virtual uint HandleEventStreamsAvailable(ConnectionEvent connectionEvent) + { + return MsQuicConstants.Success; + } + + public async ValueTask AcceptAsync() + { + if (await _acceptQueue.Reader.WaitToReadAsync()) + { + if (_acceptQueue.Reader.TryRead(out var stream)) + { + return stream; + } + } + + return null; + } + + public ValueTask StartUnidirectionalStreamAsync() + { + return StartStreamAsync(QUIC_STREAM_OPEN_FLAG.UNIDIRECTIONAL); + } + + public ValueTask StartBidirectionalStreamAsync() + { + return StartStreamAsync(QUIC_STREAM_OPEN_FLAG.NONE); + } + + private async ValueTask StartStreamAsync(QUIC_STREAM_OPEN_FLAG flags) + { + var stream = StreamOpen(flags); + await stream.StartAsync(); + return stream; + } + + public unsafe void SetIdleTimeout(TimeSpan timeout) + { + var msTime = (ulong)timeout.TotalMilliseconds; + var buffer = new QuicBuffer() + { + Length = sizeof(ulong), + Buffer = (byte*)&msTime + }; + SetParam(QUIC_PARAM_CONN.IDLE_TIMEOUT, buffer); + } + + public void SetPeerBiDirectionalStreamCount(ushort count) + { + SetUshortParamter(QUIC_PARAM_CONN.PEER_BIDI_STREAM_COUNT, count); + } + + public void SetPeerUnidirectionalStreamCount(ushort count) + { + SetUshortParamter(QUIC_PARAM_CONN.PEER_UNIDI_STREAM_COUNT, count); + } + + public void SetLocalBidirectionalStreamCount(ushort count) + { + SetUshortParamter(QUIC_PARAM_CONN.LOCAL_BIDI_STREAM_COUNT, count); + } + + public void SetLocalUnidirectionalStreamCount(ushort count) + { + SetUshortParamter(QUIC_PARAM_CONN.LOCAL_UNIDI_STREAM_COUNT, count); + } + + public unsafe void EnableBuffering() + { + var val = true; + var buffer = new QuicBuffer() + { + Length = sizeof(bool), + Buffer = (byte*)&val + }; + SetParam(QUIC_PARAM_CONN.USE_SEND_BUFFER, buffer); + } + + public unsafe void DisableBuffering() + { + var val = false; + var buffer = new QuicBuffer() + { + Length = sizeof(bool), + Buffer = (byte*)&val + }; + SetParam(QUIC_PARAM_CONN.USE_SEND_BUFFER, buffer); + } + + public ValueTask StartAsync( + ushort family, + string serverName, + ushort serverPort) + { + var status = _api.ConnectionStartDelegate( + _nativeObjPtr, + family, + serverName, + serverPort); + + MsQuicStatusException.ThrowIfFailed(status); + + return new ValueTask(); + } + + public MsQuicStream StreamOpen( + QUIC_STREAM_OPEN_FLAG flags) + { + var streamPtr = IntPtr.Zero; + var status = _api.StreamOpenDelegate( + _nativeObjPtr, + (uint)flags, + MsQuicStream.NativeCallbackHandler, + IntPtr.Zero, + out streamPtr); + MsQuicStatusException.ThrowIfFailed(status); + + return new MsQuicStream(_api, this, _context, flags, streamPtr); + } + + public void SetCallbackHandler() + { + _handle = GCHandle.Alloc(this); + _connectionDelegate = new ConnectionCallbackDelegate(NativeCallbackHandler); + _api.SetCallbackHandlerDelegate( + _nativeObjPtr, + _connectionDelegate, + GCHandle.ToIntPtr(_handle)); + } + + public void Shutdown( + QUIC_CONNECTION_SHUTDOWN_FLAG Flags, + ushort ErrorCode) + { + var status = _api.ConnectionShutdownDelegate( + _nativeObjPtr, + (uint)Flags, + ErrorCode); + MsQuicStatusException.ThrowIfFailed(status); + } + + internal static uint NativeCallbackHandler( + IntPtr connection, + IntPtr context, + ref ConnectionEvent connectionEventStruct) + { + var handle = GCHandle.FromIntPtr(context); + var quicConnection = (MsQuicConnection)handle.Target; + return quicConnection.HandleEvent(ref connectionEventStruct); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~MsQuicConnection() + { + Dispose(false); + } + + private void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (_nativeObjPtr != IntPtr.Zero) + { + _api.ConnectionCloseDelegate?.Invoke(_nativeObjPtr); + } + + _nativeObjPtr = IntPtr.Zero; + _api = null; + + _handle.Free(); + _disposed = true; + } + + private unsafe void SetUshortParamter(QUIC_PARAM_CONN param, ushort count) + { + var buffer = new QuicBuffer() + { + Length = sizeof(ushort), + Buffer = (byte*)&count + }; + SetParam(param, buffer); + } + + private void SetParam( + QUIC_PARAM_CONN param, + QuicBuffer buf) + { + MsQuicStatusException.ThrowIfFailed(_api.UnsafeSetParam( + _nativeObjPtr, + (uint)QUIC_PARAM_LEVEL.CONNECTION, + (uint)param, + buf)); + } + + public override void Abort(ConnectionAbortedException abortReason) + { + } + } +} diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnectionListener.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnectionListener.cs new file mode 100644 index 0000000000..44a0434727 --- /dev/null +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnectionListener.cs @@ -0,0 +1,195 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using static Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal.MsQuicNativeMethods; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal +{ + /// + /// Listens for new Quic Connections. + /// + internal class MsQuicConnectionListener : IConnectionListener, IAsyncDisposable, IDisposable + { + private IMsQuicTrace _log; + private MsQuicApi _api; + private QuicSecConfig _secConfig; + private QuicSession _session; + private bool _disposed; + private bool _stopped; + private IntPtr _nativeObjPtr; + private GCHandle _handle; + private ListenerCallbackDelegate _listenerDelegate; + private MsQuicTransportContext _transportContext; + + private readonly Channel _acceptConnectionQueue = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = true + }); + + public MsQuicConnectionListener(MsQuicTransportOptions options, IHostApplicationLifetime lifetime, IMsQuicTrace log, EndPoint endpoint) + { + _api = new MsQuicApi(); + _log = log; + _transportContext = new MsQuicTransportContext(lifetime, _log, options); + EndPoint = endpoint; + } + + public EndPoint EndPoint { get; set; } + + public async ValueTask AcceptAsync(CancellationToken cancellationToken = default) + { + if (await _acceptConnectionQueue.Reader.WaitToReadAsync()) + { + if (_acceptConnectionQueue.Reader.TryRead(out var connection)) + { + return connection; + } + } + + return null; + } + + internal async Task BindAsync() + { + await StartAsync(); + } + + public async ValueTask UnbindAsync(CancellationToken cancellationToken = default) + { + if (_stopped) + { + return; + } + + // TODO abort all streams and connections here? + _stopped = true; + + await DisposeAsync(); + } + + public async Task StartAsync(CancellationToken cancellationToken = default) + { + _api.RegistrationOpen(Encoding.ASCII.GetBytes(_transportContext.Options.RegistrationName)); + + _secConfig = await _api.CreateSecurityConfig(_transportContext.Options.Certificate); + + _session = _api.SessionOpen(_transportContext.Options.Alpn); + _log.LogDebug(0, "Started session"); + + _nativeObjPtr = _session.ListenerOpen(NativeCallbackHandler); + + SetCallbackHandler(); + + _session.SetIdleTimeout(_transportContext.Options.IdleTimeout); + _session.SetPeerBiDirectionalStreamCount(_transportContext.Options.MaxBidirectionalStreamCount); + _session.SetPeerUnidirectionalStreamCount(_transportContext.Options.MaxBidirectionalStreamCount); + + var address = MsQuicNativeMethods.Convert(EndPoint as IPEndPoint); + MsQuicStatusException.ThrowIfFailed(_api.ListenerStartDelegate( + _nativeObjPtr, + ref address)); + } + + internal uint ListenerCallbackHandler( + ref ListenerEvent evt) + { + switch (evt.Type) + { + case QUIC_LISTENER_EVENT.NEW_CONNECTION: + { + evt.Data.NewConnection.SecurityConfig = _secConfig.NativeObjPtr; + var msQuicConnection = new MsQuicConnection(_api, _transportContext, evt.Data.NewConnection.Connection); + _acceptConnectionQueue.Writer.TryWrite(msQuicConnection); + } + break; + default: + return MsQuicConstants.InternalError; + } + + return MsQuicConstants.Success; + } + + protected void StopAcceptingConnections() + { + _acceptConnectionQueue.Writer.TryComplete(); + } + + internal static uint NativeCallbackHandler( + IntPtr listener, + IntPtr context, + ref ListenerEvent connectionEventStruct) + { + var handle = GCHandle.FromIntPtr(context); + var quicListener = (MsQuicConnectionListener)handle.Target; + + return quicListener.ListenerCallbackHandler(ref connectionEventStruct); + } + + internal void SetCallbackHandler() + { + _handle = GCHandle.Alloc(this); + _listenerDelegate = new ListenerCallbackDelegate(NativeCallbackHandler); + _api.SetCallbackHandlerDelegate( + _nativeObjPtr, + _listenerDelegate, + GCHandle.ToIntPtr(_handle)); + } + + ~MsQuicConnectionListener() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public ValueTask DisposeAsync() + { + if (_disposed) + { + return new ValueTask(); + } + + Dispose(true); + + return new ValueTask(); + } + + private void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + StopAcceptingConnections(); + + if (_nativeObjPtr != IntPtr.Zero) + { + _api.ListenerStopDelegate(_nativeObjPtr); + _api.ListenerCloseDelegate(_nativeObjPtr); + } + + _nativeObjPtr = IntPtr.Zero; + _api = null; + _disposed = true; + } + } +} diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConstants.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConstants.cs index 13aad975ca..69071d60a9 100644 --- a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConstants.cs +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConstants.cs @@ -8,7 +8,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal { internal static class MsQuicConstants { - private const uint Success = 0; + internal static uint InternalError = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Windows.InternalError : Linux.InternalError; + internal static uint Success = 0; + internal static uint Pending = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Windows.Pending : Linux.Pending; + private const uint SuccessConst = 0; internal static Func ErrorTypeFromErrorCode = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Windows.GetError : (Func)Linux.GetError; @@ -37,7 +40,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal { switch (status) { - case Success: + case SuccessConst: return "SUCCESS"; case Pending: return "PENDING"; @@ -103,7 +106,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal { switch (status) { - case Success: + case SuccessConst: return "SUCCESS"; case Pending: return "PENDING"; diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicNativeMethods.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicNativeMethods.cs index cc10abfccf..8d583e13e8 100644 --- a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicNativeMethods.cs +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicNativeMethods.cs @@ -3,6 +3,7 @@ using System; using System.Net; +using System.Net.Sockets; using System.Runtime.InteropServices; using System.Text; @@ -13,7 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal /// internal unsafe static class MsQuicNativeMethods { - internal const string dllName = "msquic.dll"; + internal const string dllName = "msquic"; [DllImport(dllName)] internal static extern int MsQuicOpen(int version, out NativeApi* registration); @@ -67,7 +68,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal internal delegate void SetCallbackHandlerDelegate( IntPtr Handle, - IntPtr Handler, + Delegate del, IntPtr Context); internal delegate uint SetParamDelegate( @@ -413,61 +414,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal { internal QUIC_STREAM_EVENT Type; internal StreamEventDataUnion Data; - internal uint ReceiveAbortError => Data.PeerRecvAbort.ErrorCode; - internal uint SendAbortError => Data.PeerSendAbort.ErrorCode; - internal ulong AbsoluteOffset => Data.Recv.AbsoluteOffset; - internal ulong TotalBufferLength => Data.Recv.TotalBufferLength; - internal void CopyToBuffer(Span buffer) - { - var length = (int)Data.Recv.Buffers[0].Length; - new Span(Data.Recv.Buffers[0].Buffer, length).CopyTo(buffer); - } - internal bool Canceled => Data.SendComplete.IsCanceled(); - internal IntPtr ClientContext => Data.SendComplete.ClientContext; - internal bool GracefulShutdown => Data.SendShutdownComplete.Graceful; - } - - internal delegate uint StreamCallbackDelegate( - IntPtr Stream, - IntPtr Context, - ref StreamEvent Event); - - internal delegate uint StreamOpenDelegate( - IntPtr Connection, - uint Flags, - StreamCallbackDelegate Handler, - IntPtr Context, - out IntPtr Stream); - - internal delegate uint StreamStartDelegate( - IntPtr Stream, - uint Flags - ); - - internal delegate uint StreamCloseDelegate( - IntPtr Stream); - - internal delegate uint StreamShutdownDelegate( - IntPtr Stream, - uint Flags, - ushort ErrorCode); - - internal delegate uint StreamSendDelegate( - IntPtr Stream, - QuicBuffer* Buffers, - uint BufferCount, - uint Flags, - IntPtr ClientSendContext); - - internal delegate uint StreamReceiveCompleteDelegate( - IntPtr Stream, - ulong BufferLength); - - [StructLayout(LayoutKind.Sequential)] - internal unsafe struct QuicBuffer - { - internal uint Length; - internal byte* Buffer; } [StructLayout(LayoutKind.Sequential)] @@ -536,5 +482,106 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal [FieldOffset(0)] internal ushort si_family; } + + internal delegate uint StreamCallbackDelegate( + IntPtr Stream, + IntPtr Context, + StreamEvent Event); + + internal delegate uint StreamOpenDelegate( + IntPtr Connection, + uint Flags, + StreamCallbackDelegate Handler, + IntPtr Context, + out IntPtr Stream); + + internal delegate uint StreamStartDelegate( + IntPtr Stream, + uint Flags + ); + + internal delegate uint StreamCloseDelegate( + IntPtr Stream); + + internal delegate uint StreamShutdownDelegate( + IntPtr Stream, + uint Flags, + ushort ErrorCode); + + internal delegate uint StreamSendDelegate( + IntPtr Stream, + QuicBuffer* Buffers, + uint BufferCount, + uint Flags, + IntPtr ClientSendContext); + + internal delegate uint StreamReceiveCompleteDelegate( + IntPtr Stream, + ulong BufferLength); + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct QuicBuffer + { + internal uint Length; + internal byte* Buffer; + } + + private const ushort IPv4 = 2; + private const ushort IPv6 = 23; + + public static SOCKADDR_INET Convert(IPEndPoint endpoint) + { + var socketAddress = new SOCKADDR_INET(); + var buffer = endpoint.Address.GetAddressBytes(); + switch (endpoint.Address.AddressFamily) + { + case AddressFamily.InterNetwork: + socketAddress.Ipv4.sin_addr0 = buffer[0]; + socketAddress.Ipv4.sin_addr1 = buffer[1]; + socketAddress.Ipv4.sin_addr2 = buffer[2]; + socketAddress.Ipv4.sin_addr3 = buffer[3]; + socketAddress.Ipv4.sin_family = IPv4; + break; + case AddressFamily.InterNetworkV6: + socketAddress.Ipv6.sin6_addr0 = buffer[0]; + socketAddress.Ipv6.sin6_addr1 = buffer[1]; + socketAddress.Ipv6.sin6_addr2 = buffer[2]; + socketAddress.Ipv6.sin6_addr3 = buffer[3]; + socketAddress.Ipv6.sin6_addr4 = buffer[4]; + socketAddress.Ipv6.sin6_addr5 = buffer[5]; + socketAddress.Ipv6.sin6_addr6 = buffer[6]; + socketAddress.Ipv6.sin6_addr7 = buffer[7]; + socketAddress.Ipv6.sin6_addr8 = buffer[8]; + socketAddress.Ipv6.sin6_addr9 = buffer[9]; + socketAddress.Ipv6.sin6_addr10 = buffer[10]; + socketAddress.Ipv6.sin6_addr11 = buffer[11]; + socketAddress.Ipv6.sin6_addr12 = buffer[12]; + socketAddress.Ipv6.sin6_addr13 = buffer[13]; + socketAddress.Ipv6.sin6_addr14 = buffer[14]; + socketAddress.Ipv6.sin6_addr15 = buffer[15]; + socketAddress.Ipv6.sin6_family = IPv6; + break; + default: + throw new ArgumentException("Only IPv4 or IPv6 are supported"); + } + + SetPort(endpoint.Address.AddressFamily, ref socketAddress, endpoint.Port); + return socketAddress; + } + + private static void SetPort(AddressFamily addressFamily, ref SOCKADDR_INET socketAddrInet, int originalPort) + { + var convertedPort = (ushort)IPAddress.HostToNetworkOrder((short)originalPort); + switch (addressFamily) + { + case AddressFamily.InterNetwork: + socketAddrInet.Ipv4.sin_port = convertedPort; + break; + case AddressFamily.InterNetworkV6: + default: + socketAddrInet.Ipv6.sin6_port = convertedPort; + break; + } + } } } diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicStream.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicStream.cs new file mode 100644 index 0000000000..ddb2891ed4 --- /dev/null +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicStream.cs @@ -0,0 +1,443 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.IO.Pipelines; +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using static Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal.MsQuicNativeMethods; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal +{ + internal class MsQuicStream : TransportConnection, IUnidirectionalStreamFeature + { + private Task _processingTask; + private MsQuicConnection _connection; + private readonly CancellationTokenSource _streamClosedTokenSource = new CancellationTokenSource(); + private IMsQuicTrace _log; + private bool _disposed; + private IntPtr _nativeObjPtr; + private GCHandle _handle; + private StreamCallbackDelegate _delegate; + private string _connectionId; + + internal ResettableCompletionSource _resettableCompletion; + private MemoryHandle[] _bufferArrays; + private GCHandle _sendBuffer; + + public MsQuicStream(MsQuicApi api, MsQuicConnection connection, MsQuicTransportContext context, QUIC_STREAM_OPEN_FLAG flags, IntPtr nativeObjPtr) + { + Debug.Assert(connection != null); + + Api = api; + _nativeObjPtr = nativeObjPtr; + + _connection = connection; + MemoryPool = context.Options.MemoryPoolFactory(); + _log = context.Log; + + ConnectionClosed = _streamClosedTokenSource.Token; + + var maxReadBufferSize = context.Options.MaxReadBufferSize.Value; + var maxWriteBufferSize = context.Options.MaxWriteBufferSize.Value; + _resettableCompletion = new ResettableCompletionSource(this); + + // TODO should we allow these PipeScheduler to be configurable here? + var inputOptions = new PipeOptions(MemoryPool, PipeScheduler.ThreadPool, PipeScheduler.Inline, maxReadBufferSize, maxReadBufferSize / 2, useSynchronizationContext: false); + var outputOptions = new PipeOptions(MemoryPool, PipeScheduler.Inline, PipeScheduler.ThreadPool, maxWriteBufferSize, maxWriteBufferSize / 2, useSynchronizationContext: false); + + var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions); + + // TODO when stream is unidirectional, don't create an output pipe. + if (flags.HasFlag(QUIC_STREAM_OPEN_FLAG.UNIDIRECTIONAL)) + { + Features.Set(this); + } + + // TODO populate the ITlsConnectionFeature (requires client certs). + var feature = new FakeTlsConnectionFeature(); + Features.Set(feature); + + Transport = pair.Transport; + Application = pair.Application; + + SetCallbackHandler(); + + _processingTask = ProcessSends(); + + // Concatenate stream id with ConnectionId. + _log.NewStream(ConnectionId); + } + + public override MemoryPool MemoryPool { get; } + public PipeWriter Input => Application.Output; + public PipeReader Output => Application.Input; + + public override string ConnectionId { + get + { + if (_connectionId == null) + { + _connectionId = $"{_connection.ConnectionId}:{base.ConnectionId}"; + } + return _connectionId; + } + set + { + _connectionId = value; + } + } + + private async Task ProcessSends() + { + var output = Output; + try + { + while (true) + { + var result = await output.ReadAsync(); + _log.LogDebug(0, "Handling send event"); + + if (result.IsCanceled) + { + // TODO how to get abort codepath sync'd + ShutDown(QUIC_STREAM_SHUTDOWN_FLAG.ABORT, 0); + break; + } + + var buffer = result.Buffer; + + var end = buffer.End; + var isCompleted = result.IsCompleted; + if (!buffer.IsEmpty) + { + await SendAsync(buffer, QUIC_SEND_FLAG.NONE); + } + + output.AdvanceTo(end); + + if (isCompleted) + { + // Once the stream pipe is closed, shutdown the stream. + ShutDown(QUIC_STREAM_SHUTDOWN_FLAG.GRACEFUL, 0); + break; + } + } + } + catch (Exception) + { + ShutDown(QUIC_STREAM_SHUTDOWN_FLAG.ABORT, 0); + } + } + + internal uint HandleEvent(ref MsQuicNativeMethods.StreamEvent evt) + { + var status = MsQuicConstants.Success; + + switch (evt.Type) + { + case QUIC_STREAM_EVENT.START_COMPLETE: + status = HandleStartComplete(); + break; + case QUIC_STREAM_EVENT.RECV: + { + HandleEventRecv( + ref evt); + } + break; + case QUIC_STREAM_EVENT.SEND_COMPLETE: + { + status = HandleEventSendComplete(ref evt); + } + break; + case QUIC_STREAM_EVENT.PEER_SEND_CLOSE: + { + status = HandleEventPeerSendClose(); + } + break; + // TODO figure out difference between SEND_ABORT and RECEIVE_ABORT + case QUIC_STREAM_EVENT.PEER_SEND_ABORT: + { + _streamClosedTokenSource.Cancel(); + status = HandleEventPeerSendAbort(); + } + break; + case QUIC_STREAM_EVENT.PEER_RECV_ABORT: + { + _streamClosedTokenSource.Cancel(); + status = HandleEventPeerRecvAbort(); + } + break; + case QUIC_STREAM_EVENT.SEND_SHUTDOWN_COMPLETE: + { + status = HandleEventSendShutdownComplete(ref evt); + } + break; + case QUIC_STREAM_EVENT.SHUTDOWN_COMPLETE: + { + Close(); + return MsQuicConstants.Success; + } + + default: + break; + } + return status; + } + + private uint HandleEventPeerRecvAbort() + { + return MsQuicConstants.Success; + } + + private uint HandleEventPeerSendAbort() + { + return MsQuicConstants.Success; + } + + private uint HandleStartComplete() + { + _resettableCompletion.Complete(MsQuicConstants.Success); + return MsQuicConstants.Success; + } + + private uint HandleEventSendShutdownComplete(ref MsQuicNativeMethods.StreamEvent evt) + { + return MsQuicConstants.Success; + } + + private uint HandleEventPeerSendClose() + { + Input.Complete(); + return MsQuicConstants.Success; + } + + public uint HandleEventSendComplete(ref MsQuicNativeMethods.StreamEvent evt) + { + _sendBuffer.Free(); + foreach (var gchBufferArray in _bufferArrays) + { + gchBufferArray.Dispose(); + } + _resettableCompletion.Complete(evt.Data.PeerRecvAbort.ErrorCode); + return MsQuicConstants.Success; + } + + protected void HandleEventRecv(ref MsQuicNativeMethods.StreamEvent evt) + { + static unsafe void CopyToBuffer(Span buffer, StreamEvent evt) + { + var length = (int)evt.Data.Recv.Buffers[0].Length; + new Span(evt.Data.Recv.Buffers[0].Buffer, length).CopyTo(buffer); + } + + _log.LogDebug(0, "Handling receive event"); + var input = Input; + var length = (int)evt.Data.Recv.TotalBufferLength; + var result = input.GetSpan(length); + CopyToBuffer(result, evt); + + input.Advance(length); + + var flushTask = input.FlushAsync(); + + if (!flushTask.IsCompletedSuccessfully) + { + _ = AwaitFlush(flushTask); + + return; + } + + async Task AwaitFlush(ValueTask ft) + { + await ft; + // TODO figure out when to call these for receive. + EnableReceive(); + ReceiveComplete(length); + } + } + + public override void Abort(ConnectionAbortedException abortReason) + { + Shutdown(abortReason); + + // Cancel ProcessSends loop after calling shutdown to ensure the correct _shutdownReason gets set. + Output.CancelPendingRead(); + } + + private void Shutdown(Exception shutdownReason) + { + } + + public MsQuicApi Api { get; set; } + + internal static uint NativeCallbackHandler( + IntPtr stream, + IntPtr context, + StreamEvent connectionEventStruct) + { + var handle = GCHandle.FromIntPtr(context); + var quicStream = (MsQuicStream)handle.Target; + + return quicStream.HandleEvent(ref connectionEventStruct); + } + + public void SetCallbackHandler() + { + _handle = GCHandle.Alloc(this); + + _delegate = new StreamCallbackDelegate(NativeCallbackHandler); + Api.SetCallbackHandlerDelegate( + _nativeObjPtr, + _delegate, + GCHandle.ToIntPtr(_handle)); + } + + public unsafe ValueTask SendAsync( + ReadOnlySequence buffers, + QUIC_SEND_FLAG flags) + { + var bufferCount = 0; + foreach (var memory in buffers) + { + bufferCount++; + } + + var quicBufferArray = new QuicBuffer[bufferCount]; + _bufferArrays = new MemoryHandle[bufferCount]; + + var i = 0; + foreach (var memory in buffers) + { + var handle = memory.Pin(); + _bufferArrays[i] = handle; + quicBufferArray[i].Length = (uint)memory.Length; + quicBufferArray[i].Buffer = (byte*)handle.Pointer; + i++; + } + + _sendBuffer = GCHandle.Alloc(quicBufferArray, GCHandleType.Pinned); + + var quicBufferPointer = (QuicBuffer*)Marshal.UnsafeAddrOfPinnedArrayElement(quicBufferArray, 0); + + var status = Api.StreamSendDelegate( + _nativeObjPtr, + quicBufferPointer, + (uint)bufferCount, + (uint)flags, + _nativeObjPtr); + + MsQuicStatusException.ThrowIfFailed(status); + + return _resettableCompletion.GetValueTask(); + } + + public ValueTask StartAsync() + { + var status = Api.StreamStartDelegate( + _nativeObjPtr, + (uint)QUIC_STREAM_START_FLAG.ASYNC); + + MsQuicStatusException.ThrowIfFailed(status); + return _resettableCompletion.GetValueTask(); + } + + public void ReceiveComplete(int bufferLength) + { + var status = (uint)Api.StreamReceiveComplete(_nativeObjPtr, (ulong)bufferLength); + MsQuicStatusException.ThrowIfFailed(status); + } + + public void ShutDown( + QUIC_STREAM_SHUTDOWN_FLAG flags, + ushort errorCode) + { + var status = (uint)Api.StreamShutdownDelegate( + _nativeObjPtr, + (uint)flags, + errorCode); + MsQuicStatusException.ThrowIfFailed(status); + } + + public void Close() + { + var status = (uint)Api.StreamCloseDelegate?.Invoke(_nativeObjPtr); + MsQuicStatusException.ThrowIfFailed(status); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public unsafe void EnableReceive() + { + var val = true; + var buffer = new QuicBuffer() + { + Length = sizeof(bool), + Buffer = (byte*)&val + }; + SetParam(QUIC_PARAM_STREAM.RECEIVE_ENABLED, buffer); + } + + private void SetParam( + QUIC_PARAM_STREAM param, + QuicBuffer buf) + { + MsQuicStatusException.ThrowIfFailed(Api.UnsafeSetParam( + _nativeObjPtr, + (uint)QUIC_PARAM_LEVEL.SESSION, + (uint)param, + buf)); + } + + ~MsQuicStream() + { + _log.LogDebug("Destructor"); + Dispose(false); + } + + private void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (_nativeObjPtr != IntPtr.Zero) + { + Api.StreamCloseDelegate?.Invoke(_nativeObjPtr); + } + + _handle.Free(); + _nativeObjPtr = IntPtr.Zero; + Api = null; + + _disposed = true; + } + } + + internal class FakeTlsConnectionFeature : ITlsConnectionFeature + { + public FakeTlsConnectionFeature() + { + } + + public X509Certificate2 ClientCertificate { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public Task GetClientCertificateAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicTrace.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicTrace.cs new file mode 100644 index 0000000000..318e1fddb8 --- /dev/null +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicTrace.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal +{ + internal class MsQuicTrace : IMsQuicTrace + { + private static readonly Action _acceptedConnection = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, nameof(NewConnection)), @"Connection id ""{ConnectionId}"" accepted."); + private static readonly Action _acceptedStream = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, nameof(NewStream)), @"Stream id ""{ConnectionId}"" accepted."); + private static readonly Action _connectionError = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, nameof(NewStream)), @"Connection id ""{ConnectionId}"" hit an exception: ""{Reason}""."); + private static readonly Action _streamError = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, nameof(NewStream)), @"Connection id ""{ConnectionId}"" hit an exception: ""{Reason}""."); + + private ILogger _logger; + + public MsQuicTrace(ILogger logger) + { + _logger = logger; + } + + public IDisposable BeginScope(TState state) => _logger.BeginScope(state); + + public bool IsEnabled(LogLevel logLevel) => _logger.IsEnabled(logLevel); + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + => _logger.Log(logLevel, eventId, state, exception, formatter); + + public void NewConnection(string connectionId) + { + _acceptedConnection(_logger, connectionId, null); + } + + public void NewStream(string streamId) + { + _acceptedStream(_logger, streamId, null); + } + public void ConnectionError(string connectionId, Exception ex) + { + _connectionError(_logger, connectionId, ex.Message, ex); + } + + public void StreamError(string streamId, Exception ex) + { + _streamError(_logger, streamId, ex.Message, ex); + } + } +} diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicTransportContext.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicTransportContext.cs new file mode 100644 index 0000000000..42612c591b --- /dev/null +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicTransportContext.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.Hosting; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal +{ + internal class MsQuicTransportContext + { + public MsQuicTransportContext(IHostApplicationLifetime appLifetime, IMsQuicTrace log, MsQuicTransportOptions options) + { + AppLifetime = appLifetime; + Log = log; + Options = options; + } + + public IHostApplicationLifetime AppLifetime { get; } + public IMsQuicTrace Log { get; } + public MsQuicTransportOptions Options { get; } + } +} diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/QuicSecConfig.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/QuicSecConfig.cs new file mode 100644 index 0000000000..670efa71b2 --- /dev/null +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/QuicSecConfig.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal +{ + internal class QuicSecConfig : IDisposable + { + private bool _disposed; + private MsQuicApi _registration; + + public QuicSecConfig(MsQuicApi registration, IntPtr nativeObjPtr) + { + _registration = registration; + NativeObjPtr = nativeObjPtr; + } + + public IntPtr NativeObjPtr { get; private set; } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + _registration.SecConfigDeleteDelegate?.Invoke(NativeObjPtr); + NativeObjPtr = IntPtr.Zero; + _disposed = true; + } + + ~QuicSecConfig() + { + Dispose(disposing: false); + } + } +} diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/QuicSession.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/QuicSession.cs new file mode 100644 index 0000000000..ba2bf8c868 --- /dev/null +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/QuicSession.cs @@ -0,0 +1,141 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Threading.Tasks; +using static Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal.MsQuicNativeMethods; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal +{ + internal sealed class QuicSession : IDisposable + { + private bool _disposed = false; + private IntPtr _nativeObjPtr; + private MsQuicApi _registration; + + internal QuicSession(MsQuicApi registration, IntPtr nativeObjPtr) + { + _registration = registration; + _nativeObjPtr = nativeObjPtr; + } + + public async ValueTask ConnectionOpenAsync(IPEndPoint endpoint, MsQuicTransportContext context) + { + var status = _registration.ConnectionOpenDelegate( + _nativeObjPtr, + MsQuicConnection.NativeCallbackHandler, + IntPtr.Zero, + out var connectionPtr); + + MsQuicStatusException.ThrowIfFailed(status); + + var msQuicConnection = new MsQuicConnection(_registration, context, connectionPtr); + + await msQuicConnection.StartAsync((ushort)endpoint.AddressFamily, endpoint.Address.ToString(), (ushort)endpoint.Port); + + return msQuicConnection; + } + + internal IntPtr ListenerOpen(ListenerCallbackDelegate callback) + { + var status = _registration.ListenerOpenDelegate( + _nativeObjPtr, + callback, + IntPtr.Zero, + out var listenerPointer + ); + + MsQuicStatusException.ThrowIfFailed(status); + + return listenerPointer; + } + + public void ShutDown( + QUIC_CONNECTION_SHUTDOWN_FLAG Flags, + ushort ErrorCode) + { + _registration.SessionShutdownDelegate( + _nativeObjPtr, + (uint)Flags, + ErrorCode); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void SetPeerBiDirectionalStreamCount(ushort count) + { + SetUshortParamter(QUIC_PARAM_SESSION.PEER_BIDI_STREAM_COUNT, count); + } + + public void SetPeerUnidirectionalStreamCount(ushort count) + { + SetUshortParamter(QUIC_PARAM_SESSION.PEER_UNIDI_STREAM_COUNT, count); + } + + private unsafe void SetUshortParamter(QUIC_PARAM_SESSION param, ushort count) + { + var buffer = new MsQuicNativeMethods.QuicBuffer() + { + Length = sizeof(ushort), + Buffer = (byte*)&count + }; + + SetParam(param, buffer); + } + + public void SetDisconnectTimeout(TimeSpan timeout) + { + SetULongParamter(QUIC_PARAM_SESSION.DISCONNECT_TIMEOUT, (ulong)timeout.TotalMilliseconds); + } + + public void SetIdleTimeout(TimeSpan timeout) + { + SetULongParamter(QUIC_PARAM_SESSION.IDLE_TIMEOUT, (ulong)timeout.TotalMilliseconds); + + } + private unsafe void SetULongParamter(QUIC_PARAM_SESSION param, ulong count) + { + var buffer = new MsQuicNativeMethods.QuicBuffer() + { + Length = sizeof(ulong), + Buffer = (byte*)&count + }; + SetParam(param, buffer); + } + + private void SetParam( + QUIC_PARAM_SESSION param, + MsQuicNativeMethods.QuicBuffer buf) + { + MsQuicStatusException.ThrowIfFailed(_registration.UnsafeSetParam( + _nativeObjPtr, + (uint)QUIC_PARAM_LEVEL.SESSION, + (uint)param, + buf)); + } + + ~QuicSession() + { + Dispose(false); + } + + private void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + _registration.SessionCloseDelegate?.Invoke(_nativeObjPtr); + _nativeObjPtr = IntPtr.Zero; + _registration = null; + + _disposed = true; + } + } +} diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/ResettableCompletionSource.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/ResettableCompletionSource.cs new file mode 100644 index 0000000000..9c71211f96 --- /dev/null +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/ResettableCompletionSource.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal +{ + internal class ResettableCompletionSource : IValueTaskSource + { + private ManualResetValueTaskSourceCore _valueTaskSource; + private readonly MsQuicStream _stream; + + public ResettableCompletionSource(MsQuicStream stream) + { + _stream = stream; + _valueTaskSource.RunContinuationsAsynchronously = true; + } + + public ValueTask GetValueTask() + { + return new ValueTask(this, _valueTaskSource.Version); + } + + public uint GetResult(short token) + { + var isValid = token == _valueTaskSource.Version; + try + { + return _valueTaskSource.GetResult(token); + } + finally + { + if (isValid) + { + _valueTaskSource.Reset(); + _stream._resettableCompletion = this; + } + } + } + + public ValueTaskSourceStatus GetStatus(short token) + { + return _valueTaskSource.GetStatus(token); + } + + public void OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) + { + _valueTaskSource.OnCompleted(continuation, state, token, flags); + } + + public void Complete(uint result) + { + _valueTaskSource.SetResult(result); + } + } +} diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/UIntExtensions.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/UIntExtensions.cs new file mode 100644 index 0000000000..31d2b41fb6 --- /dev/null +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/UIntExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.InteropServices; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal +{ + internal static class UIntExtensions + { + internal static bool Succeeded(this uint status) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return status >= 0x80000000; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return (int)status <= 0; + } + + return false; + } + } +} diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.csproj b/src/Servers/Kestrel/Transport.MsQuic/src/Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.csproj index 9cdcb4e919..c052a41f72 100644 --- a/src/Servers/Kestrel/Transport.MsQuic/src/Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.csproj +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.csproj @@ -10,11 +10,34 @@ false + + + + + + + + + - + + + + PreserveNewest + PreserveNewest + + + PreserveNewest + PreserveNewest + + + PreserveNewest + PreserveNewest + + diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/MsQuicConnectionFactory.cs b/src/Servers/Kestrel/Transport.MsQuic/src/MsQuicConnectionFactory.cs new file mode 100644 index 0000000000..4d6f45c889 --- /dev/null +++ b/src/Servers/Kestrel/Transport.MsQuic/src/MsQuicConnectionFactory.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic +{ + public class MsQuicConnectionFactory : IConnectionFactory + { + private MsQuicApi _api; + private QuicSession _session; + private bool _started; + private MsQuicTransportContext _transportContext; + + public MsQuicConnectionFactory(IOptions options, IHostApplicationLifetime lifetime, ILoggerFactory loggerFactory) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + _api = new MsQuicApi(); + var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Client"); + var trace = new MsQuicTrace(logger); + + _transportContext = new MsQuicTransportContext(lifetime, trace, options.Value); + } + + public async ValueTask ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken = default) + { + if (!(endPoint is IPEndPoint ipEndPoint)) + { + throw new NotSupportedException($"{endPoint} is not supported"); + } + + if (!_started) + { + _started = true; + await StartAsync(); + } + + var connection = await _session.ConnectionOpenAsync(endPoint as IPEndPoint, _transportContext); + return connection; + } + + private ValueTask StartAsync() + { + _api.RegistrationOpen(Encoding.ASCII.GetBytes(_transportContext.Options.RegistrationName)); + _session = _api.SessionOpen(_transportContext.Options.Alpn); + _session.SetIdleTimeout(_transportContext.Options.IdleTimeout); + _session.SetPeerBiDirectionalStreamCount(_transportContext.Options.MaxBidirectionalStreamCount); + _session.SetPeerUnidirectionalStreamCount(_transportContext.Options.MaxBidirectionalStreamCount); + return new ValueTask(); + } + } +} diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/MsQuicTransportFactory.cs b/src/Servers/Kestrel/Transport.MsQuic/src/MsQuicTransportFactory.cs new file mode 100644 index 0000000000..2871fb5612 --- /dev/null +++ b/src/Servers/Kestrel/Transport.MsQuic/src/MsQuicTransportFactory.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic +{ + public class MsQuicTransportFactory : IConnectionListenerFactory + { + private MsQuicTrace _log; + private IHostApplicationLifetime _applicationLifetime; + private MsQuicTransportOptions _options; + + public MsQuicTransportFactory(IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic"); + _log = new MsQuicTrace(logger); + _applicationLifetime = applicationLifetime; + _options = options.Value; + } + + public async ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) + { + var transport = new MsQuicConnectionListener(_options, _applicationLifetime, _log, endpoint); + await transport.BindAsync(); + return transport; + } + } +} diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/MsQuicTransportOptions.cs b/src/Servers/Kestrel/Transport.MsQuic/src/MsQuicTransportOptions.cs new file mode 100644 index 0000000000..17e3c32f1a --- /dev/null +++ b/src/Servers/Kestrel/Transport.MsQuic/src/MsQuicTransportOptions.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic +{ + public class MsQuicTransportOptions + { + /// + /// The maximum number of concurrent bi-directional streams per connection. + /// + public ushort MaxBidirectionalStreamCount { get; set; } = 100; + + /// + /// The maximum number of concurrent inbound uni-directional streams per connection. + /// + public ushort MaxUnidirectionalStreamCount { get; set; } = 10; + + /// + /// The Application Layer Protocol Negotiation string. + /// + public string Alpn { get; set; } + + /// + /// The registration name to use in MsQuic. + /// + public string RegistrationName { get; set; } + + /// + /// The certificate that MsQuic will use. + /// + public X509Certificate2 Certificate { get; set; } + + /// + /// Sets the idle timeout for connections and streams. + /// + public TimeSpan IdleTimeout { get; set; } + + /// + /// The maximum read size. + /// + public long? MaxReadBufferSize { get; set; } = 1024 * 1024; + + /// + /// The maximum write size. + /// + public long? MaxWriteBufferSize { get; set; } = 64 * 1024; + + internal Func> MemoryPoolFactory { get; set; } = System.Buffers.SlabMemoryPoolFactory.Create; + + } +} diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/WebHostBuilderMsQuicExtensions.cs b/src/Servers/Kestrel/Transport.MsQuic/src/WebHostBuilderMsQuicExtensions.cs new file mode 100644 index 0000000000..15fffe52a6 --- /dev/null +++ b/src/Servers/Kestrel/Transport.MsQuic/src/WebHostBuilderMsQuicExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic; +using Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Hosting +{ + public static class WebHostBuilderMsQuicExtensions + { + public static IWebHostBuilder UseMsQuic(this IWebHostBuilder hostBuilder) + { + return hostBuilder.ConfigureServices(services => + { + services.AddSingleton(); + }); + } + + public static IWebHostBuilder UseMsQuic(this IWebHostBuilder hostBuilder, Action configureOptions) + { + return hostBuilder.UseMsQuic().ConfigureServices(services => + { + services.Configure(configureOptions); + }); + } + } +} diff --git a/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs b/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs new file mode 100644 index 0000000000..fa4156c09e --- /dev/null +++ b/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs @@ -0,0 +1,104 @@ +using System; +using System.Buffers; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace QuicSampleApp +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + app.Run((httpContext) => + { + return Task.CompletedTask; + }); + } + + public static void Main(string[] args) + { + var cert = CertificateLoader.LoadFromStoreCert("localhost", StoreName.My.ToString(), StoreLocation.CurrentUser, true); + var hostBuilder = new WebHostBuilder() + .ConfigureLogging((_, factory) => + { + factory.SetMinimumLevel(LogLevel.Debug); + factory.AddConsole(); + }) + .UseKestrel() + .UseMsQuic(options => + { + options.Certificate = cert; + options.RegistrationName = "AspNetCore-MsQuic"; + options.Alpn = "QuicTest"; + options.IdleTimeout = TimeSpan.FromHours(1); + }) + .ConfigureKestrel((context, options) => + { + var basePort = 5555; + + options.Listen(IPAddress.Any, basePort, listenOptions => + { + listenOptions.Use((next) => + { + return async connection => + { + var streamFeature = connection.Features.Get(); + if (streamFeature != null) + { + while (true) + { + var connectionContext = await streamFeature.AcceptAsync(); + if (connectionContext == null) + { + return; + } + _ = next(connectionContext); + } + } + else + { + await next(connection); + } + }; + }); + + async Task EchoServer(ConnectionContext connection) + { + // For graceful shutdown + try + { + while (true) + { + var result = await connection.Transport.Input.ReadAsync(); + + if (result.IsCompleted) + { + break; + } + + await connection.Transport.Output.WriteAsync(result.Buffer.ToArray()); + + connection.Transport.Input.AdvanceTo(result.Buffer.End); + } + } + catch (OperationCanceledException) + { + } + } + listenOptions.Run(EchoServer); + }); + }) + .UseStartup(); + + hostBuilder.Build().Run(); + } + } +} diff --git a/src/Servers/Kestrel/samples/QuicSampleApp/QuicSampleApp.csproj b/src/Servers/Kestrel/samples/QuicSampleApp/QuicSampleApp.csproj new file mode 100644 index 0000000000..4a5ee8de15 --- /dev/null +++ b/src/Servers/Kestrel/samples/QuicSampleApp/QuicSampleApp.csproj @@ -0,0 +1,16 @@ + + + + $(DefaultNetCoreTargetFramework) + false + true + false + + + + + + + + + diff --git a/src/Servers/Kestrel/samples/QuicSampleClient/Program.cs b/src/Servers/Kestrel/samples/QuicSampleClient/Program.cs new file mode 100644 index 0000000000..48f4be9e94 --- /dev/null +++ b/src/Servers/Kestrel/samples/QuicSampleClient/Program.cs @@ -0,0 +1,106 @@ +using System; +using System.Buffers; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic; +using Microsoft.AspNetCore.Connections.Abstractions.Features; +using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Connections; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; + +namespace QuicSampleClient +{ + class Program + { + static async Task Main(string[] args) + { + var host = new HostBuilder() + .ConfigureLogging(loggingBuilder => + { + loggingBuilder.AddConsole(); + loggingBuilder.SetMinimumLevel(LogLevel.Error); + }) + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + services.AddOptions(); + services.Configure((options) => + { + options.Alpn = "QuicTest"; + options.RegistrationName = "Quic-AspNetCore-client"; + options.Certificate = CertificateLoader.LoadFromStoreCert("localhost", StoreName.My.ToString(), StoreLocation.CurrentUser, true); + options.IdleTimeout = TimeSpan.FromHours(1); + }); + }) + .Build(); + await host.Services.GetService().RunAsync(); + } + + private class MsQuicClientService + { + private readonly IConnectionFactory _connectionFactory; + private readonly ILogger _logger; + public MsQuicClientService(IConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task RunAsync() + { + var connectionContext = await _connectionFactory.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 5555)); + var createStreamFeature = connectionContext.Features.Get(); + var streamContext = await createStreamFeature.StartBidirectionalStreamAsync(); + + Console.CancelKeyPress += new ConsoleCancelEventHandler((sender, args) => + { + streamContext.Transport.Input.CancelPendingRead(); + streamContext.Transport.Output.CancelPendingFlush(); + }); + + var input = "asdf"; + while (true) + { + try + { + //var input = Console.ReadLine(); + if (input.Length == 0) + { + continue; + } + var flushResult = await streamContext.Transport.Output.WriteAsync(Encoding.ASCII.GetBytes(input)); + if (flushResult.IsCanceled) + { + break; + } + + var readResult = await streamContext.Transport.Input.ReadAsync(); + if (readResult.IsCanceled) + { + break; + } + if (readResult.Buffer.Length > 0) + { + Console.WriteLine(Encoding.ASCII.GetString(readResult.Buffer.ToArray())); + } + + streamContext.Transport.Input.AdvanceTo(readResult.Buffer.End); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + break; + } + } + + await streamContext.Transport.Input.CompleteAsync(); + await streamContext.Transport.Output.CompleteAsync(); + } + } + } +} diff --git a/src/Servers/Kestrel/samples/QuicSampleClient/QuicSampleClient.csproj b/src/Servers/Kestrel/samples/QuicSampleClient/QuicSampleClient.csproj new file mode 100644 index 0000000000..6477f0dcb3 --- /dev/null +++ b/src/Servers/Kestrel/samples/QuicSampleClient/QuicSampleClient.csproj @@ -0,0 +1,15 @@ + + + + Exe + $(DefaultNetCoreTargetFramework) + false + + + + + + + + + diff --git a/src/Servers/Kestrel/shared/DuplexPipe.cs b/src/Servers/Kestrel/shared/DuplexPipe.cs index 1c5896fc0d..cee167b20b 100644 --- a/src/Servers/Kestrel/shared/DuplexPipe.cs +++ b/src/Servers/Kestrel/shared/DuplexPipe.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace System.IO.Pipelines diff --git a/src/Tools/dotnet-watch/test/DotNetWatcherTests.cs b/src/Tools/dotnet-watch/test/DotNetWatcherTests.cs index d6e57ee5cf..221bd5e899 100644 --- a/src/Tools/dotnet-watch/test/DotNetWatcherTests.cs +++ b/src/Tools/dotnet-watch/test/DotNetWatcherTests.cs @@ -22,8 +22,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests _app = new KitchenSinkApp(logger); } - [ConditionalFact] - [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/8267")] + [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/16912")] public async Task RunsWithDotnetWatchEnvVariable() { Assert.True(string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DOTNET_WATCH")), "DOTNET_WATCH cannot be set already when this test is running"); diff --git a/src/Tools/dotnet-watch/test/GlobbingAppTests.cs b/src/Tools/dotnet-watch/test/GlobbingAppTests.cs index 8667c06e7d..aa4036ebf2 100644 --- a/src/Tools/dotnet-watch/test/GlobbingAppTests.cs +++ b/src/Tools/dotnet-watch/test/GlobbingAppTests.cs @@ -85,8 +85,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests await _app.HasRestarted(); } - [ConditionalFact] - [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/8267")] + [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/16912")] public async Task ChangeExcludedFile() { await _app.StartWatcherAsync(); diff --git a/src/Tools/dotnet-watch/test/NoDepsAppTests.cs b/src/Tools/dotnet-watch/test/NoDepsAppTests.cs index 4f2b420492..9ad560d348 100644 --- a/src/Tools/dotnet-watch/test/NoDepsAppTests.cs +++ b/src/Tools/dotnet-watch/test/NoDepsAppTests.cs @@ -42,8 +42,7 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests Assert.NotEqual(processIdentifier, processIdentifier2); } - [ConditionalFact] - [SkipOnHelix("https://github.com/aspnet/AspNetCore/issues/8267")] + [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/16912")] public async Task RestartProcessThatTerminatesAfterFileChange() { await _app.StartWatcherAsync();