Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ public ValueTask<ServiceResult[]> 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<ServiceResult>());
}

// dnsSafeName is Disposed by SendQueryWithTelemetry
EncodedDomainName dnsSafeName = GetNormalizedHostName(name);
return SendQueryWithTelemetry(name, dnsSafeName, QueryType.SRV, ProcessResponse, cancellationToken);
Expand Down Expand Up @@ -111,24 +117,9 @@ public ValueTask<ServiceResult[]> ResolveServiceAsync(string name, CancellationT

public async ValueTask<AddressResult[]> 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);
Expand All @@ -153,19 +144,9 @@ internal ValueTask<AddressResult[]> 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<AddressResult[]>([new AddressResult(DateTime.MaxValue, IPAddress.Loopback)]);
}
else if (addressFamily == AddressFamily.InterNetworkV6 && Socket.OSSupportsIPv6)
{
return ValueTask.FromResult<AddressResult[]>([new AddressResult(DateTime.MaxValue, IPAddress.IPv6Loopback)]);
}

return ValueTask.FromResult<AddressResult[]>([]);
return ValueTask.FromResult(ResolveDnsReservedNameAddress(type, addressFamily));
}

// dnsSafeName is Disposed by SendQueryWithTelemetry
Expand Down Expand Up @@ -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<TResult[]> SendQueryWithTelemetry<TResult>(string name, EncodedDomainName dnsSafeName, QueryType queryType, Func<EncodedDomainName, QueryType, DnsResponse, (SendQueryError error, TResult[] result)> processResponseFunc, CancellationToken cancellationToken)
{
NameResolutionActivity activity = Telemetry.StartNameResolution(name, queryType, _timeProvider.GetTimestamp());
Expand Down Expand Up @@ -928,4 +944,44 @@ private static EncodedDomainName GetNormalizedHostName(string name)
}
}
}

//
// See RFC 6761 for reserved DNS names.
//
private static ReservedNameType CheckIsReservedDnsName(string name)
{
ReadOnlySpan<char> 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<char> 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,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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]
Expand Down
Loading