Uh oh!
There was an error while loading. Please reload this page.
Handle RFC 6761 special-use domain names in Dns resolution - #123076
Conversation
Tagging subscribers to this area: @dotnet/ncl |
There was a problem hiding this comment.
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 |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
84b6b50 to
8fbc05eComparelaveeshb
commented
Jan 20, 2026
Correction to formatting above: Thanks for the feedback @rzikm! I've updated the implementation based on your suggestion: Changes made:
Benefits of this approach:
The All 133 tests pass, including new tests that verify localhost subdomains return the same addresses as plain |
Uh oh!
There was an error while loading. Please reload this page.
wfurt
commented
Jan 30, 2026
Some of the test failures seems relevant @laveeshb @rzikm is out this week but we should figure out the test failures. |
laveeshb
commented
Jan 30, 2026
@wfurt I'll take a look at the unit test failures |
karelz
commented
Feb 10, 2026
@laveeshb any idea when you will get time to get to it? |
laveeshb
commented
Feb 10, 2026
laveeshb
commented
Feb 11, 2026
Investigating the test failure in The failing test shows: After investigation, I believe this is happening because:
This is actually consistent with the agreed behavior from @rzikm's comment:
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 Proposed fix: Update the test to verify that localhost subdomains return loopback addresses (RFC 6761 requirement), rather than requiring exact equality with plain // 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 @rzikm@wfurt - Does this analysis look correct? Should I proceed with the test fix, or is there a preference for different behavior? |
b86c5db to
b58b6c1CompareThere was a problem hiding this comment.
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 tolocalhost, 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
*.localhostresolution 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}"));
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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.
4b6ce99 to
a054182Compare- 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).
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- 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.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- 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 constsUh oh!
There was an error while loading. Please reload this page.
- Make IsLocalhostSubdomain lazy in sync path: only call it on error or empty-result paths, not eagerly on every resolution. - Remove redundant justAddresses ternary in async empty-result fallback branches where the result type already implies the value. - Fix flaky IPv6 test: only assert Length >= 1 for IPv4; for IPv6 just validate address families since Linux CI may lack IPv6 localhost. - Remove .localhost/.foo..localhost from MalformedReservedName tests since Linux OS resolver handles these successfully.
Uh oh!
There was an error while loading. Please reload this page.
Disables `DnsGetHostEntry_LocalhostSubdomain_RespectsAddressFamily` and `DnsGetHostAddresses_LocalhostSubdomain_RespectsAddressFamily` on Android using `[ActiveIssue]`. ## Problem These tests (introduced in #123076 for RFC 6761 localhost subdomain handling) consistently fail on Android CI (`windows.11.amd64.android.open`): ``` System.Net.Sockets.SocketException : hostname nor servname provided, or not known at System.Net.Dns.GetHostEntryOrAddressesCore(...) at System.Net.Dns.GetHostEntryOrAddressesCore(...) // fallback for "localhost" at System.Net.Dns.GetHostEntryCore(...) at System.Net.Dns.GetHostEntry(...) ``` The tests call e.g. `Dns.GetHostEntry("test.localhost", AddressFamily.InterNetwork)`. The RFC 6761 implementation tries the OS resolver for `"test.localhost"` first, then falls back to resolving `"localhost"` with the same `AddressFamily`. On Android, `getaddrinfo("localhost", ..., AF_INET/AF_INET6)` may fail with `EAI_NONAME` while `getaddrinfo("localhost", ..., AF_UNSPEC)` succeeds — causing both the initial and fallback resolution to fail. Other localhost subdomain tests that use `AddressFamily.Unspecified` (e.g. `ReturnsLoopback`) pass on Android. Example console log: https://helixr1107v0xdeko0k025g8.blob.core.windows.net/dotnet-runtime-refs-heads-main-76f79e897d5246b58a/System.Net.NameResolution.Functional.Tests/1/console.858b3560.log?helixlogtype=result Contributes to #124751 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…3076) ## 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 Fixesdotnet#118569
…4752) Disables `DnsGetHostEntry_LocalhostSubdomain_RespectsAddressFamily` and `DnsGetHostAddresses_LocalhostSubdomain_RespectsAddressFamily` on Android using `[ActiveIssue]`. ## Problem These tests (introduced in dotnet#123076 for RFC 6761 localhost subdomain handling) consistently fail on Android CI (`windows.11.amd64.android.open`): ``` System.Net.Sockets.SocketException : hostname nor servname provided, or not known at System.Net.Dns.GetHostEntryOrAddressesCore(...) at System.Net.Dns.GetHostEntryOrAddressesCore(...) // fallback for "localhost" at System.Net.Dns.GetHostEntryCore(...) at System.Net.Dns.GetHostEntry(...) ``` The tests call e.g. `Dns.GetHostEntry("test.localhost", AddressFamily.InterNetwork)`. The RFC 6761 implementation tries the OS resolver for `"test.localhost"` first, then falls back to resolving `"localhost"` with the same `AddressFamily`. On Android, `getaddrinfo("localhost", ..., AF_INET/AF_INET6)` may fail with `EAI_NONAME` while `getaddrinfo("localhost", ..., AF_UNSPEC)` succeeds — causing both the initial and fallback resolution to fail. Other localhost subdomain tests that use `AddressFamily.Unspecified` (e.g. `ReturnsLoopback`) pass on Android. Example console log: https://helixr1107v0xdeko0k025g8.blob.core.windows.net/dotnet-runtime-refs-heads-main-76f79e897d5246b58a/System.Net.NameResolution.Functional.Tests/1/console.858b3560.log?helixlogtype=result Contributes to dotnet#124751 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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:
invalidand*.invalid- ReturnSocketError.HostNotFound(NXDOMAIN) per Section 6.4*.localhostsubdomains - Try OS resolver first, fall back to plainlocalhostresolution if OS returns failure or empty (per Section 6.3)localhost- Continues to use OS resolver to preserve/etc/hostscustomizationsBehavior Change
Before this PR:
Dns.GetHostAddresses("test.invalid")- Would query DNS servers and eventually failDns.GetHostAddresses("foo.localhost")- Would fail with HostNotFound (most systems don't have subdomains configured)After this PR:
Dns.GetHostAddresses("test.invalid")- Immediately returnsSocketError.HostNotFoundwithout network I/ODns.GetHostAddresses("foo.localhost")- Tries OS resolver first, falls back to returning loopback addresses if OS fails/returns emptyThis is an intentional breaking change for RFC 6761 compliance. Applications that previously caught
SocketExceptionfor*.localhostsubdomains will now get successful results (loopback addresses). This enables local development scenarios where services use*.localhostsubdomains.Implementation Notes
Updated based on team feedback from @rzikm:
The previous implementation returned pre-allocated loopback arrays immediately for
*.localhostsubdomains. This was changed to:/etc/hostsor local DNS) to work correctlylocalhostresolution - If OS resolver fails or returns empty results, resolve plainlocalhostinstead to guarantee loopback addressesThis approach:
localhoston typical systemsAddressFamilyfilteringKey implementation details:
GetHostEntryOrAddressesCore) are moved outside thetry/catchto preventLogFailurefrom double-reporting telemetry when the fallback itself throwsCompleteAsync) guard thecatchfilter with!fallbackOccurredto prevent catching exceptions from an already-initiated fallbackTryHandleRfc6761InvalidDomainusesnew SocketException((int)SocketError.HostNotFound)directly instead ofCreateException(SocketError.HostNotFound, 0)which would overwrite the HResult with 0 (S_OK)Testing
Added comprehensive tests covering both
GetHostAddressesandGetHostEntry:invalid,test.invalid,foo.bar.invalid,invalid.,test.invalid.)foo.localhost,bar.foo.localhost,foo.localhost.)INVALID,Test.INVALID,FOO.LOCALHOST,Test.LocalHost)AddressFamilyfiltering for localhost subdomainsnotlocalhost,localhostfoo,invalidname,testinvalid).localhost,foo..localhost,.invalid,test..invalid) passed through to OS resolverlocalhost.(with trailing dot) not treated as a subdomainFixes#118569