Skip to content

Commit 509c12b

Browse files
authored
Narrow AddressFamily passed to getaddrinfo when IPv6 is unsupported (dotnet#112642)
When IPv6 is unsupported or disabled, we should avoid passing `AF_UNSPEC` to the platform calls since those will do `AAAA` resolve attempts which might result in failures surfacing to the user, see dotnet#107979. Instead, we can narrow down the query to `AF_INET` so failures are avoided. The change has been validated by packet captures on Windows and Linux. There are no `AAAA` questions when `System.Net.DisableIPv6` is set to `true`.
1 parent 0b80e90 commit 509c12b

File tree

4 files changed

+128
-23
lines changed

4 files changed

+128
-23
lines changed

src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Threading;
99
using System.Threading.Tasks;
1010
using System.Runtime.Versioning;
11+
using System.Diagnostics.CodeAnalysis;
1112

1213
namespace System.Net
1314
{
@@ -386,10 +387,45 @@ private static IPHostEntry GetHostEntryCore(string hostName, AddressFamily addre
386387
private static IPAddress[] GetHostAddressesCore(string hostName, AddressFamily addressFamily, NameResolutionActivity? activityOrDefault = default) =>
387388
(IPAddress[])GetHostEntryOrAddressesCore(hostName, justAddresses: true, addressFamily, activityOrDefault);
388389

390+
private static bool ValidateAddressFamily(ref AddressFamily addressFamily, string hostName, bool justAddresses, [NotNullWhen(false)] out object? resultOnFailure)
391+
{
392+
if (!SocketProtocolSupportPal.OSSupportsIPv6)
393+
{
394+
if (addressFamily == AddressFamily.InterNetworkV6)
395+
{
396+
// The caller requested IPv6, but the OS doesn't support it; return an empty result.
397+
IPAddress[] addresses = Array.Empty<IPAddress>();
398+
resultOnFailure = justAddresses ? (object)
399+
addresses :
400+
new IPHostEntry
401+
{
402+
AddressList = addresses,
403+
HostName = hostName,
404+
Aliases = Array.Empty<string>()
405+
};
406+
return false;
407+
}
408+
else if (addressFamily == AddressFamily.Unspecified)
409+
{
410+
// Narrow the query to IPv4.
411+
addressFamily = AddressFamily.InterNetwork;
412+
}
413+
}
414+
415+
resultOnFailure = null;
416+
return true;
417+
}
418+
389419
private static object GetHostEntryOrAddressesCore(string hostName, bool justAddresses, AddressFamily addressFamily, NameResolutionActivity? activityOrDefault = default)
390420
{
391421
ValidateHostName(hostName);
392422

423+
if (!ValidateAddressFamily(ref addressFamily, hostName, justAddresses, out object? resultOnFailure))
424+
{
425+
Debug.Assert(!activityOrDefault.HasValue);
426+
return resultOnFailure;
427+
}
428+
393429
// NameResolutionActivity may have already been set if we're being called from RunAsync.
394430
NameResolutionActivity activity = activityOrDefault ?? NameResolutionTelemetry.Log.BeforeResolution(hostName);
395431

@@ -463,6 +499,11 @@ private static object GetHostEntryOrAddressesCore(IPAddress address, bool justAd
463499

464500
NameResolutionTelemetry.Log.AfterResolution(address, activity, answer: name);
465501

502+
if (!ValidateAddressFamily(ref addressFamily, name, justAddresses, out object? resultOnFailure))
503+
{
504+
return resultOnFailure;
505+
}
506+
466507
// Do the forward lookup to get the IPs for that host name
467508
activity = NameResolutionTelemetry.Log.BeforeResolution(name);
468509

@@ -518,6 +559,13 @@ private static Task GetHostEntryOrAddressesCoreAsync(string hostName, bool justR
518559
Task.FromCanceled<IPHostEntry>(cancellationToken);
519560
}
520561

562+
if (!ValidateAddressFamily(ref family, hostName, justAddresses, out object? resultOnFailure))
563+
{
564+
return justAddresses ? (Task)
565+
Task.FromResult((IPAddress[])resultOnFailure) :
566+
Task.FromResult((IPHostEntry)resultOnFailure);
567+
}
568+
521569
object asyncState;
522570

523571
// See if it's an IP Address.

src/libraries/System.Net.NameResolution/tests/FunctionalTests/ActivityTest.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public static async Task ForwardLookup_InvalidHostName_ActivityRecorded(bool cre
114114
{
115115
const string InvalidHostName = $"invalid...example.com...{nameof(ForwardLookup_InvalidHostName_ActivityRecorded)}";
116116

117-
await RemoteExecutor.Invoke(async (createParentActivity) =>
117+
await RemoteExecutor.Invoke(static async (createParentActivity) =>
118118
{
119119
using var recorder = new ActivityRecorder(ActivitySourceName, ActivityName)
120120
{
@@ -151,6 +151,26 @@ void Verify(int timesLookupRecorded)
151151
}, createParentActivity.ToString()).DisposeAsync();
152152
}
153153

154+
[ConditionalFact(typeof(GetHostEntryTest), nameof(GetHostEntryTest.GetHostEntry_DisableIPv6_Condition))]
155+
public static void ForwardLookup_DisableIPv6_AddressFamilyInterNetworkV6_ActivitiesAreFinished()
156+
{
157+
RemoteExecutor.Invoke(static async () =>
158+
{
159+
const string ValidHostName = "localhost";
160+
AppContext.SetSwitch("System.Net.DisableIPv6", true);
161+
using var recorder = new ActivityRecorder(ActivitySourceName, ActivityName);
162+
163+
await Dns.GetHostEntryAsync(ValidHostName);
164+
await Dns.GetHostAddressesAsync(ValidHostName);
165+
Dns.GetHostEntry(ValidHostName);
166+
Dns.GetHostAddresses(ValidHostName);
167+
Dns.EndGetHostEntry(Dns.BeginGetHostEntry(ValidHostName, null, null));
168+
Dns.EndGetHostAddresses(Dns.BeginGetHostAddresses(ValidHostName, null, null));
169+
170+
Assert.Equal(recorder.Started, recorder.Stopped);
171+
}).Dispose();
172+
}
173+
154174
static void VerifyForwardActivityInfo(Activity activity, string question)
155175
{
156176
Assert.Equal(ActivityKind.Internal, activity.Kind);

src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using System.Net.Sockets;
77
using System.Threading;
88
using System.Threading.Tasks;
9-
9+
using Microsoft.DotNet.RemoteExecutor;
1010
using Xunit;
1111

1212
namespace System.Net.NameResolution.Tests
@@ -171,6 +171,39 @@ public async Task DnsGetHostAddresses_PreCancelledToken_Throws()
171171
OperationCanceledException oce = await Assert.ThrowsAnyAsync<OperationCanceledException>(() => Dns.GetHostAddressesAsync(TestSettings.LocalHost, cts.Token));
172172
Assert.Equal(cts.Token, oce.CancellationToken);
173173
}
174+
175+
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
176+
[InlineData(false)]
177+
[InlineData(true)]
178+
public void GetHostAddresses_DisableIPv6_ExcludesIPv6Addresses(bool useAsyncOuter)
179+
{
180+
RemoteExecutor.Invoke(RunTest, useAsyncOuter.ToString()).Dispose();
181+
182+
static async Task RunTest(string useAsync)
183+
{
184+
AppContext.SetSwitch("System.Net.DisableIPv6", true);
185+
IPAddress[] addresses =
186+
bool.Parse(useAsync) ? await Dns.GetHostAddressesAsync(TestSettings.LocalHost) :
187+
Dns.GetHostAddresses(TestSettings.LocalHost);
188+
Assert.All(addresses, address => Assert.Equal(AddressFamily.InterNetwork, address.AddressFamily));
189+
}
190+
}
191+
192+
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
193+
[InlineData(false)]
194+
[InlineData(true)]
195+
public void GetHostAddresses_DisableIPv6_AddressFamilyInterNetworkV6_ReturnsEmpty(bool useAsyncOuter)
196+
{
197+
RemoteExecutor.Invoke(RunTest, useAsyncOuter.ToString()).Dispose();
198+
static async Task RunTest(string useAsync)
199+
{
200+
AppContext.SetSwitch("System.Net.DisableIPv6", true);
201+
IPAddress[] addresses =
202+
bool.Parse(useAsync) ? await Dns.GetHostAddressesAsync(TestSettings.LocalHost, AddressFamily.InterNetworkV6) :
203+
Dns.GetHostAddresses(TestSettings.LocalHost, AddressFamily.InterNetworkV6);
204+
Assert.Empty(addresses);
205+
}
206+
}
174207
}
175208

176209
// Cancellation tests are sequential to reduce the chance of timing issues.

src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections.Generic;
55
using System.IO;
6+
using System.Linq;
67
using System.Net.Sockets;
78
using System.Threading;
89
using System.Threading.Tasks;
@@ -108,38 +109,41 @@ private static async Task TestGetHostEntryAsync(Func<Task<IPHostEntry>> getHostE
108109
public static bool GetHostEntry_DisableIPv6_Condition = GetHostEntryWorks && RemoteExecutor.IsSupported;
109110

110111
[ConditionalTheory(nameof(GetHostEntry_DisableIPv6_Condition))]
111-
[InlineData("")]
112-
[InlineData(TestSettings.LocalHost)]
113-
public void Dns_GetHostEntry_DisableIPv6_ExcludesIPv6Addresses(string hostnameOuter)
112+
[InlineData("", false)]
113+
[InlineData("", true)]
114+
[InlineData(TestSettings.LocalHost, false)]
115+
[InlineData(TestSettings.LocalHost, true)]
116+
public void GetHostEntry_DisableIPv6_ExcludesIPv6Addresses(string hostnameOuter, bool useAsyncOuter)
114117
{
115-
RemoteExecutor.Invoke(RunTest, hostnameOuter).Dispose();
118+
string expectedHostName = Dns.GetHostEntry(hostnameOuter).HostName;
119+
RemoteExecutor.Invoke(RunTest, hostnameOuter, expectedHostName, useAsyncOuter.ToString()).Dispose();
116120

117-
static void RunTest(string hostnameInner)
121+
static async Task RunTest(string hostnameInner, string expectedHostName, string useAsync)
118122
{
119123
AppContext.SetSwitch("System.Net.DisableIPv6", true);
120-
IPHostEntry entry = Dns.GetHostEntry(hostnameInner);
121-
foreach (IPAddress address in entry.AddressList)
122-
{
123-
Assert.NotEqual(AddressFamily.InterNetworkV6, address.AddressFamily);
124-
}
124+
125+
IPHostEntry entry = bool.Parse(useAsync) ?
126+
await Dns.GetHostEntryAsync(hostnameInner) :
127+
Dns.GetHostEntry(hostnameInner);
128+
129+
Assert.Equal(entry.HostName, expectedHostName);
130+
Assert.All(entry.AddressList, address => Assert.Equal(AddressFamily.InterNetwork, address.AddressFamily));
125131
}
126132
}
127133

128134
[ConditionalTheory(nameof(GetHostEntry_DisableIPv6_Condition))]
129-
[InlineData("")]
130-
[InlineData(TestSettings.LocalHost)]
131-
public void Dns_GetHostEntryAsync_DisableIPv6_ExcludesIPv6Addresses(string hostnameOuter)
135+
[InlineData(false)]
136+
[InlineData(true)]
137+
public void GetHostEntry_DisableIPv6_AddressFamilyInterNetworkV6_ReturnsEmpty(bool useAsyncOuter)
132138
{
133-
RemoteExecutor.Invoke(RunTest, hostnameOuter).Dispose();
134-
135-
static async Task RunTest(string hostnameInner)
139+
RemoteExecutor.Invoke(RunTest, useAsyncOuter.ToString()).Dispose();
140+
static async Task RunTest(string useAsync)
136141
{
137142
AppContext.SetSwitch("System.Net.DisableIPv6", true);
138-
IPHostEntry entry = await Dns.GetHostEntryAsync(hostnameInner);
139-
foreach (IPAddress address in entry.AddressList)
140-
{
141-
Assert.NotEqual(AddressFamily.InterNetworkV6, address.AddressFamily);
142-
}
143+
IPHostEntry entry = bool.Parse(useAsync) ?
144+
await Dns.GetHostEntryAsync(TestSettings.LocalHost, AddressFamily.InterNetworkV6) :
145+
Dns.GetHostEntry(TestSettings.LocalHost, AddressFamily.InterNetworkV6);
146+
Assert.Empty(entry.AddressList);
143147
}
144148
}
145149

0 commit comments

Comments
 (0)