diff --git a/src/libraries/Common/src/System/AppContextSwitchHelper.cs b/src/libraries/Common/src/System/AppContextSwitchHelper.cs new file mode 100644 index 0000000000000..5d6f5f2b7c405 --- /dev/null +++ b/src/libraries/Common/src/System/AppContextSwitchHelper.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; + +namespace System +{ + internal static class AppContextSwitchHelper + { + internal static bool GetBooleanConfig(string switchName, bool defaultValue = false) => + AppContext.TryGetSwitch(switchName, out bool value) ? value : defaultValue; + + internal static bool GetBooleanConfig(string switchName, string envVariable, bool defaultValue = false) + { + if (AppContext.TryGetSwitch(switchName, out bool value)) + { + return value; + } + + if (Environment.GetEnvironmentVariable(envVariable) is string str) + { + if (str == "1" || string.Equals(str, bool.TrueString, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + if (str == "1" || string.Equals(str, bool.FalseString, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + + return defaultValue; + } + } +} diff --git a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj index 3dc678ce8442c..3f45ed87f8165 100644 --- a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj +++ b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj @@ -29,6 +29,7 @@ + diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicApi.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicApi.cs index 28c326b7b65fb..5509a842a9b0f 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicApi.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicApi.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net.Sockets; +using System.IO; using System.Runtime.InteropServices; using Microsoft.Quic; using static Microsoft.Quic.MsQuic; @@ -23,6 +24,9 @@ internal sealed unsafe partial class MsQuicApi private static readonly delegate* unmanaged[Cdecl] MsQuicOpenVersion; private static readonly delegate* unmanaged[Cdecl] MsQuicClose; + [LibraryImport("Kernel32.dll", EntryPoint = "GetModuleFileNameW")] + internal static partial int GetModuleNameW(IntPtr hModule, char* lpFilename, int nSize); + public MsQuicSafeHandle Registration { get; } public QUIC_API_TABLE* ApiTable { get; } @@ -73,7 +77,7 @@ private MsQuicApi(QUIC_API_TABLE* apiTable) static MsQuicApi() { bool loaded = false; - IntPtr msQuicHandle; + IntPtr msQuicHandle = IntPtr.Zero; Version = default; // MsQuic is using DualMode sockets and that will fail even for IPv4 if AF_INET6 is not available. @@ -89,8 +93,29 @@ static MsQuicApi() if (OperatingSystem.IsWindows()) { - // Windows ships msquic in the assembly directory. - loaded = NativeLibrary.TryLoad(Interop.Libraries.MsQuic, typeof(MsQuicApi).Assembly, DllImportSearchPath.AssemblyDirectory, out msQuicHandle); + // We ship Schannel version of MsQuic as part of the .NET runtime. + // This version works on newer OS versions (see s_minWindowsVersion). + // + // For cases where the Schannel version cannot be used, we want to + // support developers explicitly providing OpenSSL version of MsQuic. + // in the application directory. + if (ShouldUseAppLocalMsQuic()) + { + var path = Path.Combine(AppContext.BaseDirectory, Interop.Libraries.MsQuic); + + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Info(null, $"Attempting to load MsQuic library from '{path}'."); + } + + System.Console.WriteLine($"Attempting to load MsQuic library from '{path}'."); + loaded = NativeLibrary.TryLoad(path, out msQuicHandle); + } + else + { + System.Console.WriteLine("Loading from assembly directory"); + loaded = NativeLibrary.TryLoad(Interop.Libraries.MsQuic, typeof(MsQuicApi).Assembly, DllImportSearchPath.AssemblyDirectory, out msQuicHandle); + } } else { @@ -110,6 +135,22 @@ static MsQuicApi() return; } + if (OperatingSystem.IsWindows()) + { + Span pathBuffer = stackalloc char[260]; + int len; + fixed (char* pathBufferPtr = pathBuffer) + { + len = GetModuleNameW(msQuicHandle, pathBufferPtr, pathBuffer.Length); + } + + if (len > 0) + { + string path = new string(pathBuffer.Slice(0, len)); + System.Console.WriteLine($"Loaded MsQuic library from '{path}'."); + } + } + MsQuicOpenVersion = (delegate* unmanaged[Cdecl])NativeLibrary.GetExport(msQuicHandle, nameof(MsQuicOpenVersion)); MsQuicClose = (delegate* unmanaged[Cdecl])NativeLibrary.GetExport(msQuicHandle, nameof(MsQuicClose)); @@ -263,4 +304,7 @@ private static bool IsTls13Disabled(bool isServer) #endif return false; } + + private static bool ShouldUseAppLocalMsQuic() => AppContextSwitchHelper.GetBooleanConfig( + "System.Net.Quic.AppLocalMsQuic"); } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.Cache.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.Cache.cs index 4db0f88bf71a4..e487148351fb7 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.Cache.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.Cache.cs @@ -15,27 +15,9 @@ namespace System.Net.Quic; internal static partial class MsQuicConfiguration { - private const string DisableCacheEnvironmentVariable = "DOTNET_SYSTEM_NET_QUIC_DISABLE_CONFIGURATION_CACHE"; - private const string DisableCacheCtxSwitch = "System.Net.Quic.DisableConfigurationCache"; - - internal static bool ConfigurationCacheEnabled { get; } = GetConfigurationCacheEnabled(); - - private static bool GetConfigurationCacheEnabled() - { - // AppContext switch takes precedence - if (AppContext.TryGetSwitch(DisableCacheCtxSwitch, out bool value)) - { - return !value; - } - // check environment variable second - else if (Environment.GetEnvironmentVariable(DisableCacheEnvironmentVariable) is string envVar) - { - return !(envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase)); - } - - // enabled by default - return true; - } + internal static bool ConfigurationCacheEnabled { get; } = !AppContextSwitchHelper.GetBooleanConfig( + "System.Net.Quic.DisableConfigurationCache", + "DOTNET_SYSTEM_NET_QUIC_DISABLE_CONFIGURATION_CACHE"); private static readonly MsQuicConfigurationCache s_configurationCache = new MsQuicConfigurationCache(); diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs index 41ac5e41da24d..64b40f60f1d21 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs @@ -731,7 +731,7 @@ public async Task ConnectWithClientCertificate(bool sendCertificate, ClientCertS await serverConnection.DisposeAsync(); } - [Fact] + [ConditionalFact(typeof(QuicTestCollection), nameof(QuicTestCollection.IsUsingSchannelBackend))] [PlatformSpecific(TestPlatforms.Windows)] public async Task Server_CertificateWithEphemeralKey_Throws() { @@ -775,7 +775,7 @@ public async Task Server_CertificateWithEphemeralKey_Throws() } } - [Fact] + [ConditionalFact(typeof(QuicTestCollection), nameof(QuicTestCollection.IsUsingSchannelBackend))] [PlatformSpecific(TestPlatforms.Windows)] public async Task Client_CertificateWithEphemeralKey_Throws() { diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs index c3e0e4e7372ab..1a44e76e3ae17 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs @@ -44,6 +44,19 @@ public abstract class QuicTestBase : IDisposable public const int PassingTestTimeoutMilliseconds = 4 * 60 * 1000; public static TimeSpan PassingTestTimeout => TimeSpan.FromMilliseconds(PassingTestTimeoutMilliseconds); + static QuicTestBase() + { + // Opt in to run with OpenSSL version on MsQuic on older windows where Schannel + // version is not supported. + // + // This has to happen here in order to be called before QuicTestBase.IsSupported + if (PlatformDetection.IsWindows10OrLater && !QuicTestCollection.IsWindowsVersionWithSchannelSupport()) + { + System.Console.WriteLine("Setting AppContext switch to use OpenSSL version of MsQuic on Windows."); + AppContext.SetSwitch("System.Net.Quic.AppLocalMsQuic", true); + } + } + public QuicTestBase(ITestOutputHelper output) { _output = output; diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestCollection.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestCollection.cs index 5125c72e0ca8d..7d44714f47d34 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestCollection.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestCollection.cs @@ -25,8 +25,9 @@ public unsafe class QuicTestCollection : ICollectionFixture, public QuicTestCollection() { string msQuicLibraryVersion = GetMsQuicLibraryVersion(); + string tlsBackend = IsUsingSchannelBackend() ? "Schannel" : "OpenSSL"; // If any of the reflection bellow breaks due to changes in "System.Net.Quic.MsQuicApi", also check and fix HttpStress project as it uses the same hack. - Console.WriteLine($"MsQuic {(IsSupported ? "supported" : "not supported")} and using '{msQuicLibraryVersion}'."); + Console.WriteLine($"MsQuic {(IsSupported ? "supported" : "not supported")} and using '{msQuicLibraryVersion}' {(IsSupported ? $"({tlsBackend})" : "")}."); if (IsSupported) { @@ -84,6 +85,14 @@ void DumpCounter(QUIC_PERFORMANCE_COUNTERS counter) System.Console.WriteLine(sb.ToString()); } + internal static bool IsWindowsVersionWithSchannelSupport() + { + // copied from MsQuicApi implementation to avoid triggering the static constructor + Version minWindowsVersion = new Version(10, 0, 20145, 1000); + return OperatingSystem.IsWindowsVersionAtLeast(minWindowsVersion.Major, + minWindowsVersion.Minor, minWindowsVersion.Build, minWindowsVersion.Revision); + } + private static Version GetMsQuicVersion() { Type msQuicApiType = Type.GetType("System.Net.Quic.MsQuicApi, System.Net.Quic"); @@ -98,6 +107,13 @@ private static Version GetMsQuicVersion() return (string)msQuicApiType.GetProperty("MsQuicLibraryVersion", BindingFlags.NonPublic | BindingFlags.Static).GetGetMethod(true).Invoke(null, Array.Empty()); } + internal static bool IsUsingSchannelBackend() + { + Type msQuicApiType = Type.GetType("System.Net.Quic.MsQuicApi, System.Net.Quic"); + + return (bool)msQuicApiType.GetProperty("UsesSChannelBackend", BindingFlags.NonPublic | BindingFlags.Static).GetGetMethod(true).Invoke(null, Array.Empty()); + } + private static QUIC_API_TABLE* GetApiTable() { Type msQuicApiType = Type.GetType("System.Net.Quic.MsQuicApi, System.Net.Quic"); diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/System.Net.Quic.Functional.Tests.csproj b/src/libraries/System.Net.Quic/tests/FunctionalTests/System.Net.Quic.Functional.Tests.csproj index c237c7d942a95..7b1a83446123e 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/System.Net.Quic.Functional.Tests.csproj +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/System.Net.Quic.Functional.Tests.csproj @@ -43,4 +43,22 @@ + + + + + + + + + + + + + diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAllowTlsResumeTests.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAllowTlsResumeTests.cs index bc0df20697b38..83777bcee18ac 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAllowTlsResumeTests.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAllowTlsResumeTests.cs @@ -133,8 +133,9 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( server.Dispose(); } - [Theory] + [ConditionalTheory] [MemberData(nameof(SslProtocolsData))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/103354", typeof(PlatformDetection), nameof(PlatformDetection.IsArmOrArm64Process))] public Task NoClientCert_DefaultValue_ResumeSucceeds(SslProtocols sslProtocol) { SslServerAuthenticationOptions serverOptions = new SslServerAuthenticationOptions @@ -194,8 +195,9 @@ public static TheoryData ClientCertTestDat return data; } - [Theory] + [ConditionalTheory] [MemberData(nameof(ClientCertTestData))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/103354", typeof(PlatformDetection), nameof(PlatformDetection.IsArmOrArm64Process))] public Task ClientCert_DefaultValue_ResumeSucceeds(SslProtocols sslProtocol, bool certificateRequired, ClientCertSource certSource) { SslServerAuthenticationOptions serverOptions = new SslServerAuthenticationOptions