Skip to content

Support overriding MsQuic.dll in the application directory on Windows. #103351

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 1, 2025
35 changes: 35 additions & 0 deletions src/libraries/Common/src/System/AppContextSwitchHelper.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
1 change: 1 addition & 0 deletions src/libraries/System.Net.Quic/src/System.Net.Quic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<Compile Include="System\Net\Quic\**\*.cs" Exclude="System\Net\Quic\*.Unsupported.cs"/>
<!-- System.Net common -->
<Compile Include="$(CommonPath)DisableRuntimeMarshalling.cs" Link="Common\DisableRuntimeMarshalling.cs" />
<Compile Include="$(CommonPath)System\AppContextSwitchHelper.cs" Link="Common\System\AppContextSwitchHelper.cs" />
<Compile Include="$(CommonPath)System\Net\SafeHandleCache.cs" Link="Common\System\Net\SafeHandleCache.cs" />
<Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="Common\System\Net\ArrayBuffer.cs" />
<Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="Common\System\Net\MultiArrayBuffer.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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;

Expand Down Expand Up @@ -278,4 +279,7 @@ private static bool IsTls13Disabled(bool isServer)
#endif
return false;
}

private static bool ShouldUseAppLocalMsQuic() => AppContextSwitchHelper.GetBooleanConfig(
"System.Net.Quic.AppLocalMsQuic");
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ public unsafe class QuicTestCollection : ICollectionFixture<QuicTestCollection>,
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)
{
Expand Down Expand Up @@ -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");
Expand All @@ -111,6 +119,13 @@ private static Version GetMsQuicVersion()
return (string)msQuicApiType.GetProperty("MsQuicLibraryVersion", BindingFlags.NonPublic | BindingFlags.Static).GetGetMethod(true).Invoke(null, Array.Empty<object?>());
}

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<object?>());
}

private static QUIC_API_TABLE* GetApiTable()
{
Type msQuicApiType = Type.GetType("System.Net.Quic.MsQuicApi, System.Net.Quic");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,22 @@
<ItemGroup>
<PackageReference Include="System.Net.TestData" Version="$(SystemNetTestDataVersion)" />
</ItemGroup>

<!-- Support running tests on older windows by explicitly including OpenSSL version of MsQuic,
The binary will be used only if "System.Net.Quic.AppLocalMsQuic" appctx switch is 'true',
Which we manually flip in the test setup on appropriate platform. -->
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'windows'">
<PackageReference Include="Microsoft.Native.Quic.MsQuic.OpenSSL"
Version="$(MicrosoftNativeQuicMsQuicSchannelVersion)"
PrivateAssets="all"
GeneratePathProperty="true" />

</ItemGroup>
<Target Name="CopyCustomContent" AfterTargets="Build" BeforeTargets="PrepareForRun" Condition="'$(TargetPlatformIdentifier)' == 'windows'">
<Copy SourceFiles="$(PkgMicrosoft_Native_Quic_MsQuic_OpenSSL)\build\native\bin\x64\msquic.dll" DestinationFolder="$(OutDir)" />
</Target>
<Target Name="CopyCustomContentOnPublish" AfterTargets="Publish" BeforeTargets="PrepareForRun" Condition="'$(TargetPlatformIdentifier)' == 'windows'">
<Copy SourceFiles="$(PkgMicrosoft_Native_Quic_MsQuic_OpenSSL)\build\native\bin\x64\msquic.dll" DestinationFolder="$(PublishDir)" />
</Target>

</Project>
Loading