diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs index bc290c6b907..78d1fcfb6a5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs @@ -63,6 +63,12 @@ public ValueTask ResolveServiceAsync(string name, CancellationT ObjectDisposedException.ThrowIf(_disposed, this); cancellationToken.ThrowIfCancellationRequested(); + if (CheckIsReservedDnsName(name) is ReservedNameType type && type != ReservedNameType.None) + { + // RFC 6761 requires that libraries return negative results for all queries except localhost A/AAAA + return ValueTask.FromResult(Array.Empty()); + } + // dnsSafeName is Disposed by SendQueryWithTelemetry EncodedDomainName dnsSafeName = GetNormalizedHostName(name); return SendQueryWithTelemetry(name, dnsSafeName, QueryType.SRV, ProcessResponse, cancellationToken); @@ -111,24 +117,9 @@ public ValueTask ResolveServiceAsync(string name, CancellationT public async ValueTask ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default) { - if (string.Equals(name, "localhost", StringComparison.OrdinalIgnoreCase)) + if (CheckIsReservedDnsName(name) is ReservedNameType type && type != ReservedNameType.None) { - // name localhost exists outside of DNS and can't be resolved by a DNS server - int len = (Socket.OSSupportsIPv4 ? 1 : 0) + (Socket.OSSupportsIPv6 ? 1 : 0); - AddressResult[] res = new AddressResult[len]; - - int index = 0; - if (Socket.OSSupportsIPv6) // prefer IPv6 - { - res[index] = new AddressResult(DateTime.MaxValue, IPAddress.IPv6Loopback); - index++; - } - if (Socket.OSSupportsIPv4) - { - res[index] = new AddressResult(DateTime.MaxValue, IPAddress.Loopback); - } - - return res; + return ResolveDnsReservedNameAddress(type, null); } var ipv4AddressesTask = ResolveIPAddressesAsync(name, AddressFamily.InterNetwork, cancellationToken); @@ -153,19 +144,9 @@ internal ValueTask ResolveIPAddressesAsync(string name, Address throw new ArgumentOutOfRangeException(nameof(addressFamily), addressFamily, "Invalid address family"); } - if (string.Equals(name, "localhost", StringComparison.OrdinalIgnoreCase)) + if (CheckIsReservedDnsName(name) is ReservedNameType type && type != ReservedNameType.None) { - // name localhost exists outside of DNS and can't be resolved by a DNS server - if (addressFamily == AddressFamily.InterNetwork && Socket.OSSupportsIPv4) - { - return ValueTask.FromResult([new AddressResult(DateTime.MaxValue, IPAddress.Loopback)]); - } - else if (addressFamily == AddressFamily.InterNetworkV6 && Socket.OSSupportsIPv6) - { - return ValueTask.FromResult([new AddressResult(DateTime.MaxValue, IPAddress.IPv6Loopback)]); - } - - return ValueTask.FromResult([]); + return ValueTask.FromResult(ResolveDnsReservedNameAddress(type, addressFamily)); } // dnsSafeName is Disposed by SendQueryWithTelemetry @@ -342,6 +323,41 @@ static bool TryReadAddress(in DnsResourceRecord record, QueryType type, [NotNull } } + private static AddressResult[] ResolveDnsReservedNameAddress(ReservedNameType type, AddressFamily? addressFamily) + { + switch (type) + { + case ReservedNameType.Localhost: + // resolve to appropriate loopback address + bool doIpv6 = Socket.OSSupportsIPv6 && (addressFamily == null || addressFamily == AddressFamily.InterNetworkV6); + bool doIpv4 = Socket.OSSupportsIPv4 && (addressFamily == null || addressFamily == AddressFamily.InterNetwork); + + int len = (doIpv4 ? 1 : 0) + (doIpv6 ? 1 : 0); + AddressResult[] res = new AddressResult[len]; + + int count = 0; + if (doIpv6) // put IPv6 first if both are requested + { + res[count] = new AddressResult(DateTime.MaxValue, IPAddress.IPv6Loopback); + count++; + } + if (doIpv4) + { + res[count] = new AddressResult(DateTime.MaxValue, IPAddress.Loopback); + } + + return res; + + case ReservedNameType.Invalid: + // RFC 6761 requires that libraries return negative results for 'invalid' + return []; + + default: + Debug.Fail("Should be unreachable"); + throw new ArgumentOutOfRangeException(nameof(type), type, "Invalid reserved name type"); + } + } + private async ValueTask SendQueryWithTelemetry(string name, EncodedDomainName dnsSafeName, QueryType queryType, Func processResponseFunc, CancellationToken cancellationToken) { NameResolutionActivity activity = Telemetry.StartNameResolution(name, queryType, _timeProvider.GetTimestamp()); @@ -928,4 +944,44 @@ private static EncodedDomainName GetNormalizedHostName(string name) } } } + + // + // See RFC 6761 for reserved DNS names. + // + private static ReservedNameType CheckIsReservedDnsName(string name) + { + ReadOnlySpan nameAsSpan = name; + nameAsSpan = nameAsSpan.TrimEnd('.'); // trim potential explicit root label + + if (MatchesReservedName(nameAsSpan, "localhost")) + { + return ReservedNameType.Localhost; + } + + if (MatchesReservedName(nameAsSpan, "invalid")) + { + return ReservedNameType.Invalid; + } + + return ReservedNameType.None; + + static bool MatchesReservedName(ReadOnlySpan name, string reservedName) + { + // check if equal to reserved name or is a subdomain of it + if (name.EndsWith(reservedName, StringComparison.OrdinalIgnoreCase) && + (name.Length == reservedName.Length || name[name.Length - reservedName.Length - 1] == '.')) + { + return true; + } + + return false; + } + } + + private enum ReservedNameType + { + None, + Localhost, + Invalid, + } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs index c2d033ecdae..0a6c97a26c1 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs @@ -57,6 +57,9 @@ public async Task ResolveIPv4_NoSuchName_Success(bool includeSoa) [Theory] [InlineData("www.resolveipv4.com")] [InlineData("www.resolveipv4.com.")] + [InlineData("notlocalhost")] + [InlineData("notlocalhost.")] + [InlineData("notinvalid.")] [InlineData("www.ř.com")] public async Task ResolveIPv4_Simple_Success(string name) { @@ -220,15 +223,39 @@ public async Task ResolveIP_InvalidAddressFamily_Throws() } [Theory] - [InlineData(AddressFamily.InterNetwork, "127.0.0.1")] - [InlineData(AddressFamily.InterNetworkV6, "::1")] - public async Task ResolveIP_Localhost_ReturnsLoopback(AddressFamily family, string addressAsString) + [InlineData("localhost", AddressFamily.InterNetwork, "127.0.0.1")] + [InlineData("localhost", AddressFamily.InterNetworkV6, "::1")] + [InlineData("localhost.", AddressFamily.InterNetwork, "127.0.0.1")] + [InlineData("inner.localhost.", AddressFamily.InterNetwork, "127.0.0.1")] + [InlineData("inner.localhost", AddressFamily.InterNetwork, "127.0.0.1")] + [InlineData("invalid", AddressFamily.InterNetwork, null)] + [InlineData("invalid", AddressFamily.InterNetworkV6, null)] + [InlineData("invalid.", AddressFamily.InterNetwork, null)] + [InlineData("inner.invalid.", AddressFamily.InterNetwork, null)] + [InlineData("inner.invalid", AddressFamily.InterNetwork, null)] + public async Task ResolveIP_SpecialName(string localhost, AddressFamily family, string? addressAsString) { - IPAddress address = IPAddress.Parse(addressAsString); - AddressResult[] results = await Resolver.ResolveIPAddressesAsync("localhost", family); - AddressResult result = Assert.Single(results); + IPAddress? address = addressAsString != null ? IPAddress.Parse(addressAsString) : null; - Assert.Equal(address, result.Address); + bool serverCalled = false; + _ = DnsServer.ProcessUdpRequest(builder => + { + serverCalled = true; + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(localhost, family); + Assert.False(serverCalled, "Special name resolution should not call the DNS server."); + + if (address == null) + { + Assert.Empty(results); + } + else + { + AddressResult result = Assert.Single(results); + Assert.Equal(address, result.Address); + } } [Fact]