diff --git a/src/libraries/Common/src/System/AppContextSwitchHelper.cs b/src/libraries/Common/src/System/AppContextSwitchHelper.cs new file mode 100644 index 00000000000000..e2607ca66eb3e8 --- /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 == "0" || 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 f09a5d14dd08b0..546996f4b97c33 100644 --- a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj +++ b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj @@ -25,6 +25,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 0358742914485d..e609a69f20f582 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; @@ -75,7 +76,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. @@ -94,7 +95,7 @@ static MsQuicApi() // Windows ships msquic in the assembly directory next to System.Net.Quic, so load that. // For single-file deployments, the assembly location is an empty string so we fall back // to AppContext.BaseDirectory which is the directory containing the single-file executable. - string path = typeof(MsQuicApi).Assembly.Location is string assemblyLocation && !string.IsNullOrEmpty(assemblyLocation) + string path = !ShouldUseAppLocalMsQuic() && typeof(MsQuicApi).Assembly.Location is string assemblyLocation && !string.IsNullOrEmpty(assemblyLocation) ? System.IO.Path.GetDirectoryName(assemblyLocation)! : AppContext.BaseDirectory; @@ -278,4 +279,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 6beddff6638336..4e06f6c6b0bd1e 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 @@ -16,27 +16,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 5e3b440377814a..7f9af662755d60 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs @@ -720,7 +720,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() { @@ -764,7 +764,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 0cc5c418719b61..2a4eb4db68a80f 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs @@ -44,6 +44,18 @@ 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()) + { + 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 699a41fcd1042b..1ca9a9e9dd7e5a 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestCollection.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestCollection.cs @@ -26,8 +26,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) { @@ -97,6 +98,13 @@ void DumpCounter(QUIC_PERFORMANCE_COUNTERS counter) Console.WriteLine($"Unobserved exceptions of {s_unobservedExceptions.Count} different types: {Environment.NewLine}{string.Join(Environment.NewLine + new string('=', 120) + Environment.NewLine, s_unobservedExceptions.Select(pair => $"Count {pair.Value}: {pair.Key}"))}"); } + 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"); @@ -111,6 +119,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 3ac6d417fa09d5..29cacbc56ddffe 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 @@ -42,4 +42,22 @@ + + + + + + + + + + + + +