Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 29 additions & 11 deletions src/Grpc.Net.Client/GrpcChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,19 +185,9 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr
Log.AddressPathUnused(Logger, Address.OriginalString);
}

// Grpc.Net.Client + .NET Framework + WinHttpHandler requires features in WinHTTP, shipped in Windows, to work correctly.
// This scenario is supported in these versions of Windows or later:
// -Windows Server 2022 has partial support.
// -Unary and server streaming methods are supported.
// -Client and bidi streaming methods aren't supported.
// -Windows 11 has full support.
//
// GrpcChannel validates the Windows version is WinServer2022 or later. Win11 version number is greater than WinServer2022.
// Note that this doesn't block using unsupported client and bidi streaming methods on WinServer2022.
const int WinServer2022BuildVersion = 20348;
if (HttpHandlerType == HttpHandlerType.WinHttpHandler &&
OperatingSystem.IsWindows &&
OperatingSystem.OSVersion.Build < WinServer2022BuildVersion)
!ValidateWinHttpHandlerOperatingSystemVersion())
{
throw new InvalidOperationException("The channel configuration isn't valid on this operating system. " +
"The channel is configured to use WinHttpHandler and the current version of Windows " +
Expand All @@ -206,6 +196,34 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr
}
}

private bool ValidateWinHttpHandlerOperatingSystemVersion()
{
// Grpc.Net.Client + .NET Framework + WinHttpHandler requires features in WinHTTP, shipped in Windows, to work correctly.
// This scenario is supported in these versions of Windows or later:
// -Windows Server 2019 and Windows Server 2022 have partial support.
// -Unary and server streaming methods are supported.
// -Client and bidi streaming methods aren't supported.
// -Windows 11 has full support.
const int WinServer2022BuildVersion = 20348;
const int WinServer2019BuildVersion = 17763;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like this is a recent change? Should we be checking the revision version since 17763 will be true for any version of 2019.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true.

I don't know when the change happened. Figuring out this is tough, so I'd like to just allow all of 2019 and then assume that the server is up to date with its patches.

If the feature isn't supported then the client will still error, it just won't get this friendly error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the feature isn't supported then the client will still error, it just won't get this friendly error.

Ah, ok 😃


// Validate the Windows version is WinServer2022 or later. Win11 version number is greater than WinServer2022.
// Note that this doesn't block using unsupported client and bidi streaming methods on WinServer2022.
if (OperatingSystem.OSVersion.Build >= WinServer2022BuildVersion)
{
return true;
}

// Validate the Windows version is WinServer2019. Its build numbers are mixed with Windows 10, so we must check
// the OS version is Windows Server and the build number together to avoid allowing Windows 10.
if (OperatingSystem.IsWindowsServer && OperatingSystem.OSVersion.Build >= WinServer2019BuildVersion)
{
return true;
}

return false;
}

private void ResolveCredentials(GrpcChannelOptions channelOptions, out bool isSecure, out List<CallCredentials>? callCredentials)
{
if (channelOptions.Credentials != null)
Expand Down
16 changes: 9 additions & 7 deletions src/Grpc.Net.Client/Internal/NtDll.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

#endregion

#if !NET5_0_OR_GREATER

using System.Runtime.InteropServices;

namespace Grpc.Net.Client.Internal;
Expand All @@ -27,19 +25,25 @@ namespace Grpc.Net.Client.Internal;
/// </summary>
internal static class NtDll
{
#pragma warning disable SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
[DllImport("ntdll.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern NTSTATUS RtlGetVersion(ref OSVERSIONINFOEX versionInfo);
#pragma warning restore SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time

internal static Version DetectWindowsVersion()
internal static void DetectWindowsVersion(out Version version, out bool isWindowsServer)
{
var osVersionInfo = new OSVERSIONINFOEX { OSVersionInfoSize = Marshal.SizeOf(typeof(OSVERSIONINFOEX)) };
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoexa
const byte VER_NT_SERVER = 3;

var osVersionInfo = new OSVERSIONINFOEX { OSVersionInfoSize = Marshal.SizeOf<OSVERSIONINFOEX>() };

if (RtlGetVersion(ref osVersionInfo) != NTSTATUS.STATUS_SUCCESS)
{
throw new InvalidOperationException($"Failed to call internal {nameof(RtlGetVersion)}.");
}

return new Version(osVersionInfo.MajorVersion, osVersionInfo.MinorVersion, osVersionInfo.BuildNumber, 0);
version = new Version(osVersionInfo.MajorVersion, osVersionInfo.MinorVersion, osVersionInfo.BuildNumber, 0);
isWindowsServer = osVersionInfo.ProductType == VER_NT_SERVER;
}

internal enum NTSTATUS : uint
Expand Down Expand Up @@ -68,5 +72,3 @@ internal struct OSVERSIONINFOEX
public byte Reserved;
}
}

#endif
29 changes: 28 additions & 1 deletion src/Grpc.Net.Client/Internal/OperatingSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,20 @@ internal interface IOperatingSystem
bool IsBrowser { get; }
bool IsAndroid { get; }
bool IsWindows { get; }
bool IsWindowsServer { get; }
Version OSVersion { get; }
}

internal sealed class OperatingSystem : IOperatingSystem
{
public static readonly OperatingSystem Instance = new OperatingSystem();

private readonly Lazy<bool> _isWindowsServer;

public bool IsBrowser { get; }
public bool IsAndroid { get; }
public bool IsWindows { get; }
public bool IsWindowsServer => _isWindowsServer.Value;
public Version OSVersion { get; }

private OperatingSystem()
Expand All @@ -44,6 +48,19 @@ private OperatingSystem()
IsWindows = System.OperatingSystem.IsWindows();
IsBrowser = System.OperatingSystem.IsBrowser();
OSVersion = Environment.OSVersion.Version;

// Windows Server detection requires a P/Invoke call to RtlGetVersion.
// Get the value lazily so that it is only called if needed.
_isWindowsServer = new Lazy<bool>(() =>
{
if (IsWindows)
{
NtDll.DetectWindowsVersion(out _, out var isWindowsServer);
return isWindowsServer;
}

return false;
}, LazyThreadSafetyMode.ExecutionAndPublication);
#else
IsAndroid = false;
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
Expand All @@ -55,7 +72,17 @@ private OperatingSystem()
//
// Get correct Windows version directly from Windows by calling RtlGetVersion.
// https://www.pinvoke.net/default.aspx/ntdll/RtlGetVersion.html
OSVersion = IsWindows ? NtDll.DetectWindowsVersion() : Environment.OSVersion.Version;
if (IsWindows)
{
NtDll.DetectWindowsVersion(out var windowsVersion, out var windowsServer);
OSVersion = windowsVersion;
_isWindowsServer = new Lazy<bool>(() => windowsServer, LazyThreadSafetyMode.ExecutionAndPublication);
}
else
{
OSVersion = Environment.OSVersion.Version;
_isWindowsServer = new Lazy<bool>(() => false, LazyThreadSafetyMode.ExecutionAndPublication);
}
#endif
}
}
1 change: 1 addition & 0 deletions test/Grpc.Net.Client.Tests/GetStatusTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ private class TestOperatingSystem : IOperatingSystem
public bool IsBrowser { get; set; }
public bool IsAndroid { get; set; }
public bool IsWindows { get; set; }
public bool IsWindowsServer { get; }
public Version OSVersion { get; set; } = new Version(1, 2, 3, 4);
}

Expand Down
1 change: 1 addition & 0 deletions test/Grpc.Net.Client.Tests/GrpcChannelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ private class TestOperatingSystem : IOperatingSystem
public bool IsBrowser { get; set; }
public bool IsAndroid { get; set; }
public bool IsWindows { get; set; }
public bool IsWindowsServer { get; }
public Version OSVersion { get; set; } = new Version(1, 2, 3, 4);
}

Expand Down
19 changes: 16 additions & 3 deletions test/Grpc.Net.Client.Tests/OperatingSystemTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,28 @@ public class OperatingSystemTests
[Platform("Win", Reason = "Only runs on Windows where ntdll.dll is present.")]
public void DetectWindowsVersion_Windows_MatchesEnvironment()
{
// It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibilty setting.
Assert.AreEqual(Environment.OSVersion.Version, NtDll.DetectWindowsVersion());
NtDll.DetectWindowsVersion(out var version, out _);

// It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibility setting.
Assert.AreEqual(Environment.OSVersion.Version, version);
}

[Test]
[Platform("Win", Reason = "Only runs on Windows where ntdll.dll is present.")]
public void InstanceAndIsWindowsServer_Windows_MatchesEnvironment()
{
NtDll.DetectWindowsVersion(out var version, out var isWindowsServer);

Assert.AreEqual(true, OperatingSystem.Instance.IsWindows);
Assert.AreEqual(version, OperatingSystem.Instance.OSVersion);
Assert.AreEqual(isWindowsServer, OperatingSystem.Instance.IsWindowsServer);
}
#endif

[Test]
public void OSVersion_ModernDotNet_MatchesEnvironment()
{
// It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibilty setting.
// It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibility setting.
Assert.AreEqual(Environment.OSVersion.Version, OperatingSystem.Instance.OSVersion);
}
}