Skip to content

Handle RFC 6761 special-use domain names in Dns resolution#123076

Open
laveeshb wants to merge 7 commits intodotnet:mainfrom
laveeshb:fix/rfc6761-special-domains
Open

Handle RFC 6761 special-use domain names in Dns resolution#123076
laveeshb wants to merge 7 commits intodotnet:mainfrom
laveeshb:fix/rfc6761-special-domains

Conversation

@laveeshb
Copy link
Contributor

@laveeshb laveeshb commented Jan 11, 2026

Handle RFC 6761 special-use domain names in Dns resolution

Description

This PR implements RFC 6761 compliant handling for special-use domain names in System.Net.Dns.

RFC 6761 compliance:

  • invalid and *.invalid - Return SocketError.HostNotFound (NXDOMAIN) per Section 6.4
  • *.localhost subdomains - Try OS resolver first, fall back to plain localhost resolution if OS returns failure or empty (per Section 6.3)
  • Plain localhost - Continues to use OS resolver to preserve /etc/hosts customizations

Behavior Change

Before this PR:

  • Dns.GetHostAddresses("test.invalid") - Would query DNS servers and eventually fail
  • Dns.GetHostAddresses("foo.localhost") - Would fail with HostNotFound (most systems don't have subdomains configured)

After this PR:

  • Dns.GetHostAddresses("test.invalid") - Immediately returns SocketError.HostNotFound without network I/O
  • Dns.GetHostAddresses("foo.localhost") - Tries OS resolver first, falls back to returning loopback addresses if OS fails/returns empty

This is an intentional breaking change for RFC 6761 compliance. Applications that previously caught SocketException for *.localhost subdomains will now get successful results (loopback addresses). This enables local development scenarios where services use *.localhost subdomains.

Implementation Notes

Updated based on team feedback from @rzikm:

The previous implementation returned pre-allocated loopback arrays immediately for *.localhost subdomains. This was changed to:

  1. Try OS resolver first - Allows systems with custom localhost subdomain configurations (e.g., in /etc/hosts or local DNS) to work correctly
  2. Fall back to plain localhost resolution - If OS resolver fails or returns empty results, resolve plain localhost instead to guarantee loopback addresses

This approach:

  • Respects system-level customizations
  • Returns the same addresses as localhost on typical systems
  • Preserves IPv4/IPv6 preference from OS resolver
  • Works correctly with AddressFamily filtering

Key implementation details:

  • Fallback paths in the sync path (GetHostEntryOrAddressesCore) are moved outside the try/catch to prevent LogFailure from double-reporting telemetry when the fallback itself throws
  • Fallback paths in the async path (CompleteAsync) guard the catch filter with !fallbackOccurred to prevent catching exceptions from an already-initiated fallback
  • TryHandleRfc6761InvalidDomain uses new SocketException((int)SocketError.HostNotFound) directly instead of CreateException(SocketError.HostNotFound, 0) which would overwrite the HResult with 0 (S_OK)

Testing

Added comprehensive tests covering both GetHostAddresses and GetHostEntry:

  • Invalid domain rejection (invalid, test.invalid, foo.bar.invalid, invalid., test.invalid.)
  • Localhost subdomain resolution (foo.localhost, bar.foo.localhost, foo.localhost.)
  • Case-insensitivity (INVALID, Test.INVALID, FOO.LOCALHOST, Test.LocalHost)
  • AddressFamily filtering for localhost subdomains
  • Trailing dot handling (DNS root notation)
  • Negative cases: similar-but-not-reserved names (notlocalhost, localhostfoo, invalidname, testinvalid)
  • Malformed reserved names (.localhost, foo..localhost, .invalid, test..invalid) passed through to OS resolver
  • localhost. (with trailing dot) not treated as a subdomain
  • Verification that localhost subdomains return loopback addresses

Fixes #118569

Copilot AI review requested due to automatic review settings January 11, 2026 18:09
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Jan 11, 2026
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements RFC 6761 compliant handling for special-use domain names in System.Net.Dns. The implementation adds logic to intercept DNS queries for invalid/*.invalid domains (returning NXDOMAIN) and *.localhost subdomains (returning loopback addresses) before they reach the OS resolver. Plain localhost continues to use the OS resolver to preserve /etc/hosts customizations.

Changes:

  • Added RFC 6761 special-use domain name handling to DNS resolution with proper telemetry integration
  • Implemented helper methods for efficient string matching and loopback address allocation
  • Added comprehensive test coverage for invalid domains, localhost subdomains, and address family filtering

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs Added helper methods (IsReservedName, GetLoopbackAddresses, TryHandleRfc6761SpecialName) and integrated RFC 6761 handling into both sync and async resolution paths
src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs Added tests for invalid domain rejection, localhost subdomain resolution, address family filtering, and edge cases
src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs Added parallel tests for GetHostAddresses API covering invalid domains and localhost subdomains

}

// RFC 6761 Section 6.3: "*.localhost" subdomains must resolve to loopback.
// Note: Plain "localhost" is handled by the OS resolver (preserves /etc/hosts customizations).
Copy link
Member

Choose a reason for hiding this comment

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

It feels weird to allow customizations for "localhost" name itself, but not for subdomains.

It also means that without any customization, names "localhost" and "a.localhost" may return different results (e.g. IPv4 and IPv6 addresses in different order)

Copy link
Contributor Author

@laveeshb laveeshb Jan 13, 2026

Choose a reason for hiding this comment

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

You're right that this creates an inconsistency where localhost and a.localhost may return different results.

The current approach defers plain \localhost\ to the OS resolver intentionally — handling it in code would be a breaking change for users with custom hosts file entries (e.g., mapping localhost to a specific IP).

Handling both identically would be more RFC 6761 compliant and provide consistent behavior, but at the cost of backward compatibility.

My preference would be to avoid the breaking change, but I'm not sure what the general practice is in dotnet for situations like this. Is there guidance on when RFC compliance should take precedence over backward compatibility?

Copy link
Member

Choose a reason for hiding this comment

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

what if we strip the subdomains and return the same result as simply querying "localhost"? that would preserve the existing behavior and in majority of the cases the subdomains would be resolved to loopback addresses anyway (and we no longer need to care about whether IPv4 or IPv6 is enabled as that would be handled by the native layer)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done - this is exactly what the updated implementation does.

@rzikm rzikm requested a review from a team January 12, 2026 11:09
@laveeshb laveeshb force-pushed the fix/rfc6761-special-domains branch from 84b6b50 to 8fbc05e Compare January 13, 2026 07:33
@rzikm rzikm self-assigned this Jan 13, 2026
@laveeshb
Copy link
Contributor Author

Correction to formatting above:

Thanks for the feedback @rzikm! I've updated the implementation based on your suggestion:

Changes made:

  • For *.localhost subdomains, the code now tries the OS resolver first
  • If the OS resolver fails (returns error) or returns empty results, it falls back to resolving plain localhost
  • This preserves any system-level customizations (e.g., entries in /etc/hosts or local DNS) while ensuring RFC 6761 compliance as a fallback

Benefits of this approach:

  1. Systems that have custom localhost subdomain configurations will get those results
  2. On typical systems (no custom config), subdomains get the same addresses as localhost
  3. The AddressFamily filter is properly respected through the fallback
  4. Telemetry is properly logged for both the original resolution and any fallback

The invalid domain handling remains unchanged - those immediately return NXDOMAIN without hitting the OS resolver, as per RFC 6761 Section 6.4.

All 133 tests pass, including new tests that verify localhost subdomains return the same addresses as plain localhost.

bool isLocalhostSubdomain = IsLocalhostSubdomain(hostName);
Task? t;
if (NameResolutionTelemetry.AnyDiagnosticsEnabled())
if (NameResolutionTelemetry.AnyDiagnosticsEnabled() || isLocalhostSubdomain)
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need the telemetry-enabled path for localhost subdomains? What is preventing us from using the non-telemetry path?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The telemetry-enabled path (GetAddrInfoWithTelemetryAsync) contains the localhost subdomain fallback logic - if the OS resolver returns an empty address list or throws a SocketException, it falls back to resolving plain localhost instead. This is the RFC 6761 Section 6.3 compliant behavior for *.localhost subdomains.

The non-telemetry path (NameResolutionPal.GetAddrInfoAsync) doesn't have this fallback handling - it just returns whatever the OS resolver returns. So we need to route localhost subdomains through the telemetry-enabled path to ensure the fallback mechanism is invoked when needed.

The condition could perhaps be renamed to be clearer, e.g., GetAddrInfoWithFallbackAsync or add a comment explaining that the method handles both telemetry and RFC 6761 fallback. Would that help clarify the intent?

Copy link
Member

@wfurt wfurt left a comment

Choose a reason for hiding this comment

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

LGTM

@wfurt
Copy link
Member

wfurt commented Jan 30, 2026

Some of the test failures seems relevant @laveeshb

❌ System.Net.NameResolution.Tests.GetHostAddressesTest.DnsGetHostAddresses_LocalhostSubdomain_ReturnsSameAsLocalhost [[Console]](https://helixr1107v0xdcypoyl9e7f.blob.core.windows.net/dotnet-runtime-refs-pull-123076-merge-32f26020ac8f449d85/System.Net.NameResolution.Functional.Tests/3/console.c960e416.log?helixlogtype=result) [[Details]](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1259351&view=ms.vss-test-web.build-test-results-tab&runId=35135746&resultId=147460&paneView=debug) [[Artifacts]](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1259351&view=ms.vss-test-web.build-test-results-tab&runId=35135746&resultId=147460&paneView=dotnet-dnceng.dnceng-build-release-tasks.helix-test-information-tab) [[66.67% failure rate]](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1259351&view=ms.vss-test-web.build-test-results-tab&runId=35135746&resultId=147460&paneView=history)
[[ 🚧 Report test infrastructure issue]](https://helix.dot.net/ki/new?labels=Known%20Build%20Error%2CFirst%20Responder%2CDetected%20By%20-%20Customer&pr=https%3A%2F%2Fgithub.com%2Fdotnet%2Fruntime%2Fpull%2F123076&build-leg=System.Net.NameResolution.Tests.GetHostAddressesTest.DnsGetHostAddresses_LocalhostSubdomain_ReturnsSameAsLocalhost&repository=dotnet%2Fdnceng&template=z-build-break-infrastructure-issue-template.yml&build=https%3A%2F%2Fdev.azure.com%2Fdnceng-public%2Fcbb18261-c48f-4abb-8651-8cdcb5474649%2F_build%2Fresults%3FbuildId%3D1259351) [[ 📄 Report test repository issue]](https://helix.dot.net/ki/new?labels=Known%20Build%20Error%2Cblocking-clean-ci&pr=https%3A%2F%2Fgithub.com%2Fdotnet%2Fruntime%2Fpull%2F123076&build-leg=System.Net.NameResolution.Tests.GetHostAddressesTest.DnsGetHostAddresses_LocalhostSubdomain_ReturnsSameAsLocalhost&repository=dotnet%2Fruntime&build=https%3A%2F%2Fdev.azure.com%2Fdnceng-public%2Fcbb18261-c48f-4abb-8651-8cdcb5474649%2F_build%2Fresults%3FbuildId%3D1259351)
Failing Configurations (4)
[net11.0-linux-Debug-x64-Mono_Minijit_Debug-Ubuntu.2204.Amd64.Open](https://dev.azure.com/dnceng-public/cbb18261-c48f-4abb-8651-8cdcb5474649/_build/results?buildId=1259351&view=ms.vss-test-web.build-test-results-tab&runId=35135746&resultId=147460)
[net11.0-linux-Debug-x64-Mono_Interpreter_Debug-Ubuntu.2204.Amd64.Open](https://dev.azure.com/dnceng-public/cbb18261-c48f-4abb-8651-8cdcb5474649/_build/results?buildId=1259351&view=ms.vss-test-web.build-test-results-tab&runId=35135744&resultId=130293)
[net11.0-linux-Debug-x64-coreclr_release-Ubuntu.2204.Amd64.Open](https://dev.azure.com/dnceng-public/cbb18261-c48f-4abb-8651-8cdcb5474649/_build/results?buildId=1259351&view=ms.vss-test-web.build-test-results-tab&runId=35138412&resultId=152701)
[Centos.10.Amd64.Open](https://dev.azure.com/dnceng-public/cbb18261-c48f-4abb-8651-8cdcb5474649/_build/results?buildId=1259351&view=ms.vss-test-web.build-test-results-tab&runId=35138416&resultId=194537)
Exception Message
Assert.Equal() Failure: Collections differ
           ↓ (pos 0)
Expected: [127.0.0.1]
Actual:   [::1, 127.0.0.1]
           ↑ (pos 0)

@rzikm is out this week but we should figure out the test failures.

@laveeshb
Copy link
Contributor Author

Some of the test failures seems relevant @laveeshb

❌ System.Net.NameResolution.Tests.GetHostAddressesTest.DnsGetHostAddresses_LocalhostSubdomain_ReturnsSameAsLocalhost [[Console]](https://helixr1107v0xdcypoyl9e7f.blob.core.windows.net/dotnet-runtime-refs-pull-123076-merge-32f26020ac8f449d85/System.Net.NameResolution.Functional.Tests/3/console.c960e416.log?helixlogtype=result) [[Details]](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1259351&view=ms.vss-test-web.build-test-results-tab&runId=35135746&resultId=147460&paneView=debug) [[Artifacts]](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1259351&view=ms.vss-test-web.build-test-results-tab&runId=35135746&resultId=147460&paneView=dotnet-dnceng.dnceng-build-release-tasks.helix-test-information-tab) [[66.67% failure rate]](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1259351&view=ms.vss-test-web.build-test-results-tab&runId=35135746&resultId=147460&paneView=history)
[[ 🚧 Report test infrastructure issue]](https://helix.dot.net/ki/new?labels=Known%20Build%20Error%2CFirst%20Responder%2CDetected%20By%20-%20Customer&pr=https%3A%2F%2Fgithub.com%2Fdotnet%2Fruntime%2Fpull%2F123076&build-leg=System.Net.NameResolution.Tests.GetHostAddressesTest.DnsGetHostAddresses_LocalhostSubdomain_ReturnsSameAsLocalhost&repository=dotnet%2Fdnceng&template=z-build-break-infrastructure-issue-template.yml&build=https%3A%2F%2Fdev.azure.com%2Fdnceng-public%2Fcbb18261-c48f-4abb-8651-8cdcb5474649%2F_build%2Fresults%3FbuildId%3D1259351) [[ 📄 Report test repository issue]](https://helix.dot.net/ki/new?labels=Known%20Build%20Error%2Cblocking-clean-ci&pr=https%3A%2F%2Fgithub.com%2Fdotnet%2Fruntime%2Fpull%2F123076&build-leg=System.Net.NameResolution.Tests.GetHostAddressesTest.DnsGetHostAddresses_LocalhostSubdomain_ReturnsSameAsLocalhost&repository=dotnet%2Fruntime&build=https%3A%2F%2Fdev.azure.com%2Fdnceng-public%2Fcbb18261-c48f-4abb-8651-8cdcb5474649%2F_build%2Fresults%3FbuildId%3D1259351)
Failing Configurations (4)
[net11.0-linux-Debug-x64-Mono_Minijit_Debug-Ubuntu.2204.Amd64.Open](https://dev.azure.com/dnceng-public/cbb18261-c48f-4abb-8651-8cdcb5474649/_build/results?buildId=1259351&view=ms.vss-test-web.build-test-results-tab&runId=35135746&resultId=147460)
[net11.0-linux-Debug-x64-Mono_Interpreter_Debug-Ubuntu.2204.Amd64.Open](https://dev.azure.com/dnceng-public/cbb18261-c48f-4abb-8651-8cdcb5474649/_build/results?buildId=1259351&view=ms.vss-test-web.build-test-results-tab&runId=35135744&resultId=130293)
[net11.0-linux-Debug-x64-coreclr_release-Ubuntu.2204.Amd64.Open](https://dev.azure.com/dnceng-public/cbb18261-c48f-4abb-8651-8cdcb5474649/_build/results?buildId=1259351&view=ms.vss-test-web.build-test-results-tab&runId=35138412&resultId=152701)
[Centos.10.Amd64.Open](https://dev.azure.com/dnceng-public/cbb18261-c48f-4abb-8651-8cdcb5474649/_build/results?buildId=1259351&view=ms.vss-test-web.build-test-results-tab&runId=35138416&resultId=194537)
Exception Message
Assert.Equal() Failure: Collections differ
           ↓ (pos 0)
Expected: [127.0.0.1]
Actual:   [::1, 127.0.0.1]
           ↑ (pos 0)

@rzikm is out this week but we should figure out the test failures.

@wfurt I'll take a look at the unit test failures

@karelz
Copy link
Member

karelz commented Feb 10, 2026

@laveeshb any idea when you will get time to get to it?

@laveeshb
Copy link
Contributor Author

@laveeshb any idea when you will get time to get to it?

@karelz I have it in my TODO list for this week

@laveeshb
Copy link
Contributor Author

Investigating the test failure in DnsGetHostAddresses_LocalhostSubdomain_ReturnsSameAsLocalhost

The failing test shows:

Expected: [127.0.0.1]
Actual:   [::1, 127.0.0.1]

After investigation, I believe this is happening because:

  1. On modern Linux systems (Ubuntu 22.04, CentOS 10) with systemd-resolved, the OS resolver successfully handles *.localhost subdomains and returns loopback addresses ([::1, 127.0.0.1])
  2. Plain localhost resolution uses /etc/hosts, which on these systems only has 127.0.0.1
  3. Since the OS resolver returns a non-empty result for foo.localhost, the fallback to localhost is never triggered

This is actually consistent with the agreed behavior from @rzikm's comment:

"attempting to resolve those subdomains in the native resolver first, and if resolves to empty set of addresses, then falling back to whatever 'localhost' resolves to"

The OS resolver succeeds with valid loopback addresses, so no fallback occurs - the behavior is working as designed.

The test was incorrectly assuming that the OS resolver would always fail for *.localhost subdomains, triggering the fallback. The test comment says "since the fallback delegates to localhost resolution" - but that's not always true.

Proposed fix: Update the test to verify that localhost subdomains return loopback addresses (RFC 6761 requirement), rather than requiring exact equality with plain localhost:

// Verify both return loopback addresses (the key RFC 6761 requirement)
Assert.All(localhostAddresses, addr => Assert.True(IPAddress.IsLoopback(addr)));
Assert.All(subdomainAddresses, addr => Assert.True(IPAddress.IsLoopback(addr)));

This makes the test consistent with DnsGetHostAddresses_LocalhostSubdomain_ReturnsLoopback, which already validates the core requirement. Alternatively, we could remove this test as redundant.

@rzikm @wfurt - Does this analysis look correct? Should I proceed with the test fix, or is there a preference for different behavior?

@laveeshb laveeshb force-pushed the fix/rfc6761-special-domains branch from b86c5db to b58b6c1 Compare February 11, 2026 15:27
Copilot AI review requested due to automatic review settings February 11, 2026 15:27
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

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

  • This equality assertion will fail on machines where the OS resolver returns a configured address set for foo.localhost/bar.localhost (the new code returns OS results when non-empty). To keep the test deterministic, use a unique subdomain per run to ensure fallback to localhost, or change the assertion to allow OS-customized results.
            IPAddress[] localhostAddresses = Dns.GetHostAddresses("localhost");
            IPAddress[] subdomainAddresses = Dns.GetHostAddresses("foo.localhost");

            // Both should return loopback addresses (the subdomain falls back to localhost resolution)
            Assert.True(localhostAddresses.Length >= 1);
            Assert.True(subdomainAddresses.Length >= 1);

            // The addresses should be equivalent (same loopback addresses)
            Assert.Equal(
                localhostAddresses.OrderBy(a => a.ToString()).ToArray(),
                subdomainAddresses.OrderBy(a => a.ToString()).ToArray());

            // Async version
            localhostAddresses = await Dns.GetHostAddressesAsync("localhost");
            subdomainAddresses = await Dns.GetHostAddressesAsync("bar.localhost");

            Assert.Equal(
                localhostAddresses.OrderBy(a => a.ToString()).ToArray(),
                subdomainAddresses.OrderBy(a => a.ToString()).ToArray());
        }

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

  • These assertions require that any *.localhost resolution returns only loopback addresses. But the implementation explicitly tries the OS resolver first to preserve /etc/hosts / local DNS customizations, which may legitimately return non-loopback (or different loopback) addresses. This makes the test configuration-dependent. Consider using a per-run unique subdomain to reliably exercise the fallback path (and then assert loopback), or relax the assertion to accept non-empty OS-provided results.
            // The subdomain goes to OS resolver first. If it fails (likely on most systems),
            // it falls back to resolving plain "localhost", which should return loopback addresses.
            IPHostEntry entry = Dns.GetHostEntry(hostName);
            Assert.True(entry.AddressList.Length >= 1, "Expected at least one loopback address");
            Assert.All(entry.AddressList, addr => Assert.True(IPAddress.IsLoopback(addr), $"Expected loopback address but got: {addr}"));

            entry = await Dns.GetHostEntryAsync(hostName);
            Assert.True(entry.AddressList.Length >= 1, "Expected at least one loopback address");
            Assert.All(entry.AddressList, addr => Assert.True(IPAddress.IsLoopback(addr), $"Expected loopback address but got: {addr}"));

@rzikm
Copy link
Member

rzikm commented Feb 12, 2026

Does this analysis look correct? Should I proceed with the test fix, or is there a preference for different behavior?

LGTM, proceed with your proposed fix

Copilot AI review requested due to automatic review settings February 12, 2026 14:39
@laveeshb laveeshb force-pushed the fix/rfc6761-special-domains branch from 45d43ea to 4b6ce99 Compare February 12, 2026 14:39
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Implement RFC 6761 compliant handling for special-use domain names:

- 'invalid' and '*.invalid' domains return SocketError.HostNotFound (NXDOMAIN)
- '*.localhost' subdomains resolve to loopback addresses (127.0.0.1 and/or ::1)
- Plain 'localhost' continues to use OS resolver to preserve /etc/hosts customizations

Implementation details:
- Pre-allocated loopback address arrays to avoid allocations on hot path
- Respects AddressFamily parameter for filtered results
- Includes telemetry for diagnostics consistency
- Case-insensitive matching per RFC specification

Added tests for invalid domains, localhost subdomains, AddressFamily filtering,
and edge cases like 'notlocalhost' to ensure similar names aren't incorrectly matched.
- Fix IPv6 address order: use [IPv6Loopback, Loopback] to match Windows resolver behavior
- Add validation to reject malformed hostnames (starting with '.' or containing '..') in IsReservedName, letting them fall through to OS resolver
- Add tests for malformed hostname edge cases (.localhost, foo..localhost, .invalid, test..invalid)
- RFC 6761 Section 6.4: 'invalid' and '*.invalid' domains immediately return NXDOMAIN
- RFC 6761 Section 6.3: '*.localhost' subdomains try OS resolver first, fall back to plain 'localhost' resolution if OS returns failure or empty results
- Handles trailing dots (DNS root notation) correctly
- Rejects malformed hostnames (starting with '.' or containing '..') by passing to OS resolver
- Comprehensive test coverage for all edge cases
The DnsGetHostAddresses_LocalhostSubdomain_ReturnsSameAsLocalhost test was
failing because on systems with systemd-resolved, the OS resolver handles
*.localhost natively and may return different addresses (e.g., both IPv6+IPv4)
than plain localhost (IPv4 only from /etc/hosts).

Update the test to verify the RFC 6761 requirement: localhost subdomains
must return loopback addresses. This is consistent with the agreed design
where the OS resolver is tried first and fallback only occurs if empty.
@laveeshb laveeshb force-pushed the fix/rfc6761-special-domains branch from 4b6ce99 to a054182 Compare February 12, 2026 15:07
- Fix TryHandleRfc6761InvalidDomain setting HResult to 0 (success) by using
  SocketException constructor directly instead of CreateException with
  nativeError=0, which overwrote the failure HResult.
- Rename ReturnsSameAsLocalhost tests to ReturnsLoopback to match what
  they actually verify.
- Fix GetHostEntryTest to verify loopback addresses instead of exact
  equality (same fix as GetHostAddressesTest).
Copilot AI review requested due to automatic review settings February 12, 2026 15:09
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

- Move localhost fallback recursive call outside try-catch in sync path
  to prevent LogFailure from calling AfterResolution a second time when
  the fallback itself throws.
- Add !fallbackOccurred guard to async catch filter to prevent
  re-catching exceptions from an already-initiated fallback.
- Rename [Fact] ReturnsLoopback() methods to BothReturnLoopback() to
  avoid name collision with [Theory] ReturnsLoopback(string) overloads.
- Add case-insensitive InlineData to GetHostAddresses localhost tests.
- Add SimilarButNotReserved, MalformedReservedName, and
  LocalhostWithTrailingDot negative tests to GetHostAddressesTest for
  parity with GetHostEntryTest.
throw CreateException(errorCode, nativeErrorCode);
}
}
else if (isLocalhostSubdomain && addresses.Length == 0)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
else if (isLocalhostSubdomain && addresses.Length == 0)
else if (addresses.Length == 0 && IsLocalhostSubdomain(hostName))

Presumably if the common case is that DNS is giving us some addresses, we can avoid the (admittedly small) overhead of IsLocalhostSubdomain

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done — swapped to addresses.Length == 0 && isLocalhostSubdomain.

Copy link
Member

Choose a reason for hiding this comment

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

I meant that we can run the IsLocalhostSubdomain(hostName) checks lazily instead of lifting it to a local. In the common case where we do get a result, we don't have to call it.

fallbackOccurred = true;

// Resolve plain "localhost" instead
return await ((Task<T>)(justAddresses
Copy link
Member

Choose a reason for hiding this comment

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

Isn't it guaranteed that justAddresses = true here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No — GetAddrInfoWithTelemetryAsync<T> is called with both T = IPAddress[] (justAddresses=true) and T = IPHostEntry (justAddresses=false) from the caller at line ~724. The CompleteAsync function handles both cases, which is why it has separate result is IPAddress[] and result is IPHostEntry checks for the empty-result fallback.

Copy link
Member

Choose a reason for hiding this comment

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

The method is, but the result is IPAddress[] check implies that justAddresses == true in this branch, no?

- Use EndsWith('.') API instead of manual length-1 char checks
- Restore ValidateAddressFamily before IP/hostname split in async path
- Swap condition order: addresses.Length == 0 first for micro-optimization
- Use 'object? result = null' with Debug.Assert instead of null!
- Extract 'localhost'/'invalid' string literals into shared consts
Copilot AI review requested due to automatic review settings February 12, 2026 21:16
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment on lines +265 to +269
Assert.True(addresses.Length >= 1, "Expected at least one address");
Assert.All(addresses, addr => Assert.Equal(addressFamily, addr.AddressFamily));

addresses = await Dns.GetHostAddressesAsync(hostName, addressFamily);
Assert.True(addresses.Length >= 1, "Expected at least one address");
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

In the AddressFamily-specific localhost-subdomain test, the InterNetworkV6 case asserts addresses.Length >= 1. This is likely to be flaky on Linux CI: TestSettings indicates Linux CI may not have IPv6 localhost configured, so Dns.GetHostAddresses("localhost", InterNetworkV6) (and the *.localhost fallback) can legitimately return an empty array even when Socket.OSSupportsIPv6 is true. Consider conditioning the non-empty assertion on actually having at least one IPv6 localhost address, or removing the Length >= 1 requirement and only validating the address families for any returned addresses.

Suggested change
Assert.True(addresses.Length >= 1, "Expected at least one address");
Assert.All(addresses, addr => Assert.Equal(addressFamily, addr.AddressFamily));
addresses = await Dns.GetHostAddressesAsync(hostName, addressFamily);
Assert.True(addresses.Length >= 1, "Expected at least one address");
Assert.All(addresses, addr => Assert.Equal(addressFamily, addr.AddressFamily));
addresses = await Dns.GetHostAddressesAsync(hostName, addressFamily);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Net community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Domain name resolution does not handle RFC6761 special names correctly

5 participants