From 067aa165d97372f3e281786801d014fccea8b12c Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Mon, 12 Jul 2021 14:24:54 +0200 Subject: [PATCH] Expose a global switch to disable IPV6 (#55012) Fixes #47583. Resolves #52287, #54807 and similar issues, without changing customer application code. Introduces an AppContext switch `System.Net.DisableIPv6` and environment variable `DOTNET_SYSTEM_NET_DISABLEIPV6` to emulate the lack of OS-level IPv6 support. This is useful to workaround dual-stack socket issues when the OS reports support of IPv6, but some other underlying infrastructure element (typically VPN) doesn't function well with IPv6 request. For consistency, this switch also impacts NameResolution and Ping, not only Sockets. --- .../Net/SocketProtocolSupportPal.Unix.cs | 6 +- .../Net/SocketProtocolSupportPal.Windows.cs | 6 +- .../System/Net/SocketProtocolSupportPal.cs | 36 +++++++++++ .../src/System.Net.NameResolution.csproj | 2 + .../src/System/Net/NameResolutionPal.Unix.cs | 6 +- .../tests/FunctionalTests/GetHostEntryTest.cs | 59 ++++++++++++++++--- ...System.Net.NameResolution.Pal.Tests.csproj | 2 + .../src/System.Net.Ping.csproj | 2 + .../src/System.Net.Sockets.csproj | 2 + .../tests/FunctionalTests/OSSupport.cs | 32 ++++++++++ 10 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 src/libraries/Common/src/System/Net/SocketProtocolSupportPal.cs diff --git a/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.Unix.cs b/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.Unix.cs index d06ce8117fc5a2..8de1e2a0f4ce31 100644 --- a/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.Unix.cs +++ b/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.Unix.cs @@ -7,12 +7,8 @@ namespace System.Net { - internal static class SocketProtocolSupportPal + internal static partial class SocketProtocolSupportPal { - public static bool OSSupportsIPv6 { get; } = IsSupported(AddressFamily.InterNetworkV6); - public static bool OSSupportsIPv4 { get; } = IsSupported(AddressFamily.InterNetwork); - public static bool OSSupportsUnixDomainSockets { get; } = IsSupported(AddressFamily.Unix); - private static unsafe bool IsSupported(AddressFamily af) { IntPtr invalid = (IntPtr)(-1); diff --git a/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.Windows.cs b/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.Windows.cs index de0465a51c6c7e..50e7db176e2f14 100644 --- a/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.Windows.cs +++ b/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.Windows.cs @@ -9,12 +9,8 @@ namespace System.Net { - internal static class SocketProtocolSupportPal + internal static partial class SocketProtocolSupportPal { - public static bool OSSupportsIPv6 { get; } = IsSupported(AddressFamily.InterNetworkV6); - public static bool OSSupportsIPv4 { get; } = IsSupported(AddressFamily.InterNetwork); - public static bool OSSupportsUnixDomainSockets { get; } = IsSupported(AddressFamily.Unix); - private static bool IsSupported(AddressFamily af) { Interop.Winsock.EnsureInitialized(); diff --git a/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.cs b/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.cs new file mode 100644 index 00000000000000..a61f47a0fa458d --- /dev/null +++ b/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Sockets; + +namespace System.Net +{ + internal static partial class SocketProtocolSupportPal + { + private const string DisableIPv6AppCtxSwitch = "System.Net.DisableIPv6"; + private const string DisableIPv6EnvironmentVariable = "DOTNET_SYSTEM_NET_DISABLEIPV6"; + + public static bool OSSupportsIPv6 { get; } = IsSupported(AddressFamily.InterNetworkV6) && !IsIPv6Disabled(); + public static bool OSSupportsIPv4 { get; } = IsSupported(AddressFamily.InterNetwork); + public static bool OSSupportsUnixDomainSockets { get; } = IsSupported(AddressFamily.Unix); + + private static bool IsIPv6Disabled() + { + // First check for the AppContext switch, giving it priority over the environment variable. + if (AppContext.TryGetSwitch(DisableIPv6AppCtxSwitch, out bool disabled)) + { + return disabled; + } + + // AppContext switch wasn't used. Check the environment variable. + string? envVar = Environment.GetEnvironmentVariable(DisableIPv6EnvironmentVariable); + + if (envVar is not null) + { + return envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase); + } + + return false; + } + } +} diff --git a/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj b/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj index d2d9b9b46910cc..1642f1fff33dda 100644 --- a/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj +++ b/src/libraries/System.Net.NameResolution/src/System.Net.NameResolution.csproj @@ -31,6 +31,8 @@ Link="Common\System\Net\IPAddressParserStatics.cs" /> + diff --git a/src/libraries/System.Net.NameResolution/src/System/Net/NameResolutionPal.Unix.cs b/src/libraries/System.Net.NameResolution/src/System/Net/NameResolutionPal.Unix.cs index 46858edea5dfab..74c62684a82c5b 100644 --- a/src/libraries/System.Net.NameResolution/src/System/Net/NameResolutionPal.Unix.cs +++ b/src/libraries/System.Net.NameResolution/src/System/Net/NameResolutionPal.Unix.cs @@ -76,9 +76,11 @@ private static unsafe void ParseHostEntry(Interop.Sys.HostEntry hostEntry, bool Interop.Sys.IPAddress* addressHandle = hostEntry.IPAddressList; for (int i = 0; i < hostEntry.IPAddressCount; i++) { - if (Array.IndexOf(nativeAddresses, addressHandle[i], 0, nativeAddressCount) == -1) + Interop.Sys.IPAddress nativeAddr = addressHandle[i]; + if (Array.IndexOf(nativeAddresses, nativeAddr, 0, nativeAddressCount) == -1 && + (!nativeAddr.IsIPv6 || SocketProtocolSupportPal.OSSupportsIPv6)) // Do not include IPv6 addresses if IPV6 support is force-disabled { - nativeAddresses[nativeAddressCount++] = addressHandle[i]; + nativeAddresses[nativeAddressCount++] = nativeAddr; } } diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs index f860a2274e8187..7a5d4c899e9218 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs @@ -6,7 +6,7 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; - +using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.XUnitExtensions; using Xunit; @@ -21,9 +21,16 @@ public async Task Dns_GetHostEntryAsync_IPAddress_Ok() await TestGetHostEntryAsync(() => Dns.GetHostEntryAsync(localIPAddress)); } - [ActiveIssue("https://github.com/dotnet/runtime/issues/1488", TestPlatforms.OSX)] - [ActiveIssue("https://github.com/dotnet/runtime/issues/51377", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst)] - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotArm64Process))] // [ActiveIssue("https://github.com/dotnet/runtime/issues/27622")] + + public static bool GetHostEntryWorks = + // [ActiveIssue("https://github.com/dotnet/runtime/issues/27622")] + PlatformDetection.IsNotArmNorArm64Process && + // [ActiveIssue("https://github.com/dotnet/runtime/issues/1488", TestPlatforms.OSX)] + PlatformDetection.IsNotOSX && + // [ActiveIssue("https://github.com/dotnet/runtime/issues/51377", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst)] + !PlatformDetection.IsiOS && !PlatformDetection.IstvOS && !PlatformDetection.IsMacCatalyst; + + [ConditionalTheory(nameof(GetHostEntryWorks))] [InlineData("")] [InlineData(TestSettings.LocalHost)] public async Task Dns_GetHostEntry_HostString_Ok(string hostName) @@ -77,12 +84,10 @@ public async Task Dns_GetHostEntry_HostString_Ok(string hostName) } } - [ActiveIssue("https://github.com/dotnet/runtime/issues/1488", TestPlatforms.OSX)] - [ActiveIssue("https://github.com/dotnet/runtime/issues/51377", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst)] - [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotArm64Process))] // [ActiveIssue("https://github.com/dotnet/runtime/issues/27622")] + [ConditionalTheory(nameof(GetHostEntryWorks))] [InlineData("")] [InlineData(TestSettings.LocalHost)] - public async Task Dns_GetHostEntryAsync_HostString_Ok(string hostName) + public async Task Dns_GetHostEntryAsync_HostString_Ok(string hostName) { if (PlatformDetection.IsSLES) { @@ -112,6 +117,44 @@ private static async Task TestGetHostEntryAsync(Func> getHostE Assert.Equal(list1, list2); } + public static bool GetHostEntry_DisableIPv6_Condition = GetHostEntryWorks && RemoteExecutor.IsSupported; + + [ConditionalTheory(nameof(GetHostEntry_DisableIPv6_Condition))] + [InlineData("")] + [InlineData(TestSettings.LocalHost)] + public void Dns_GetHostEntry_DisableIPv6_ExcludesIPv6Addresses(string hostnameOuter) + { + RemoteExecutor.Invoke(RunTest, hostnameOuter).Dispose(); + + static void RunTest(string hostnameInner) + { + AppContext.SetSwitch("System.Net.DisableIPv6", true); + IPHostEntry entry = Dns.GetHostEntry(hostnameInner); + foreach (IPAddress address in entry.AddressList) + { + Assert.NotEqual(AddressFamily.InterNetworkV6, address.AddressFamily); + } + } + } + + [ConditionalTheory(nameof(GetHostEntry_DisableIPv6_Condition))] + [InlineData("")] + [InlineData(TestSettings.LocalHost)] + public void Dns_GetHostEntryAsync_DisableIPv6_ExcludesIPv6Addresses(string hostnameOuter) + { + RemoteExecutor.Invoke(RunTest, hostnameOuter).Dispose(); + + static async Task RunTest(string hostnameInner) + { + AppContext.SetSwitch("System.Net.DisableIPv6", true); + IPHostEntry entry = await Dns.GetHostEntryAsync(hostnameInner); + foreach (IPAddress address in entry.AddressList) + { + Assert.NotEqual(AddressFamily.InterNetworkV6, address.AddressFamily); + } + } + } + [Fact] public async Task Dns_GetHostEntry_NullStringHost_Fail() { diff --git a/src/libraries/System.Net.NameResolution/tests/PalTests/System.Net.NameResolution.Pal.Tests.csproj b/src/libraries/System.Net.NameResolution/tests/PalTests/System.Net.NameResolution.Pal.Tests.csproj index 7b2b9ee2c9fd60..3b9a81c4ee5d35 100644 --- a/src/libraries/System.Net.NameResolution/tests/PalTests/System.Net.NameResolution.Pal.Tests.csproj +++ b/src/libraries/System.Net.NameResolution/tests/PalTests/System.Net.NameResolution.Pal.Tests.csproj @@ -28,6 +28,8 @@ Link="Common\System\Net\IPEndPointStatics.cs" /> + + diff --git a/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj b/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj index 35d10bb679f163..496c628956f8c1 100644 --- a/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj +++ b/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj @@ -65,6 +65,8 @@ Link="Common\System\Net\SocketAddress.cs" /> + diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/OSSupport.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/OSSupport.cs index 3e2886baf52b7e..9ed625aecd0ffc 100644 --- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/OSSupport.cs +++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/OSSupport.cs @@ -3,6 +3,7 @@ using System.Threading; +using Microsoft.DotNet.RemoteExecutor; using Xunit; namespace System.Net.Sockets.Tests @@ -25,6 +26,37 @@ public void SupportsIPv6_MatchesOSSupportsIPv6() #pragma warning restore } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void DisableIPv6_OSSupportsIPv6_False() + { + RemoteInvokeOptions options = new RemoteInvokeOptions(); + options.StartInfo.EnvironmentVariables["DOTNET_SYSTEM_NET_DISABLEIPV6"] = "1"; + RemoteExecutor.Invoke(RunTest, options).Dispose(); + + static void RunTest() + { + Assert.False(Socket.OSSupportsIPv6); + } + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void DisableIPv6_SocketConstructor_CreatesIPv4Socket() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + AppContext.SetSwitch("System.Net.DisableIPv6", true); + using Socket socket1 = new Socket(SocketType.Stream, ProtocolType.Tcp); + using Socket socket2 = new Socket(SocketType.Dgram, ProtocolType.Udp); + + Assert.Equal(AddressFamily.InterNetwork, socket1.AddressFamily); + Assert.Equal(AddressFamily.InterNetwork, socket2.AddressFamily); + Assert.False(socket1.DualMode); + Assert.False(socket2.DualMode); + } + } + [Fact] public void IOControl_FIONREAD_Success() {