dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.96k stars 4.65k forks source link

`Dns.GetHostEntryAsync` returning odd results #405

Closed Daniel15 closed 4 years ago

Daniel15 commented 4 years ago

Dns.GetHostEntryAsync is returning weird results for me. Calling it on several IPs such as 118.127.7.26, 203.26.198.8 and 141.0.104.145 are returning my server's domain name as the HostName value, rather than the proper reverse DNS of the IP address. The host command on the server does return the correct values though:

% host 118.127.7.26
26.7.127.118.in-addr.arpa domain name pointer rtif-118.127.7.26-31.cs-1.as45671.net.au.

% host 203.26.198.8
8.198.26.203.in-addr.arpa domain name pointer ten2-2.br-1-1.as45671.net.au.

% host 141.0.104.145
145.104.0.141.in-addr.arpa domain name pointer 141.0.104.145.static.lyse.net.

How can I debug this? How does the DNS resolution in the framework work?

Daniel15 commented 4 years ago

So NameResolutionPal.TryGetNameInfo returns the right hostname, but Dns.GetHostEntryAsync doesn't. My guess is that it's something to do with this logic that does a forward lookup right after performing the reverse lookup:

https://github.com/dotnet/runtime/blob/4f9ae42d861fcb4be2fcd5d3d55d5f227d30e723/src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs#L396-L405

Is there some way to avoid this? I just want the PTR record, I don't care about if the forward lookup matches.

NameResolutionPal.TryGetNameInfo is perfect for my use case, but unfortunately it's internal-only 😕 I was testing by copying and pasting the code into my app, but obviously that's not really maintainable.

Daniel15 commented 4 years ago

Here's my example code where I copied and pasted the NameResolutionPal internals to directly test TryGetNameInfo:

using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace DnsTest
{
    class Program
    {
        static async Task Main(string[] args)
        {
            await Task.WhenAll(
                Lookup("118.127.7.26"),
                Lookup("203.26.198.8"),
                Lookup("141.0.104.145")
            );
        }

        static async Task Lookup(string ip)
        {
            var result = await Dns.GetHostEntryAsync(ip);
            Console.WriteLine($"{ip} Dns.GetHostEntryAsync = {result.HostName}");

            var result2 = TryGetNameInfo(IPAddress.Parse(ip), out var socketError, out var nativeErrorCode);
            Console.WriteLine($"{ip} TryGetNameInfo = {result2}");
        }

        public static unsafe string TryGetNameInfo(IPAddress addr, out SocketError socketError, out int nativeErrorCode)
        {
            byte* buffer = stackalloc byte[Interop.Sys.NI_MAXHOST + 1 /*for null*/];

            byte isIPv6;
            int rawAddressLength;
            if (addr.AddressFamily == AddressFamily.InterNetwork)
            {
                isIPv6 = 0;
                rawAddressLength = IPAddressParserStatics.IPv4AddressBytes;
            }
            else
            {
                isIPv6 = 1;
                rawAddressLength = IPAddressParserStatics.IPv6AddressBytes;
            }

            byte* rawAddress = stackalloc byte[rawAddressLength];
            addr.TryWriteBytes(new Span<byte>(rawAddress, rawAddressLength), out int bytesWritten);
            Debug.Assert(bytesWritten == rawAddressLength);

            int error = Interop.Sys.GetNameInfo(
                rawAddress,
                (uint)rawAddressLength,
                isIPv6,
                buffer,
                Interop.Sys.NI_MAXHOST,
                null,
                0,
                Interop.Sys.GetNameInfoFlags.NI_NAMEREQD);

            socketError = GetSocketErrorForNativeError(error);
            nativeErrorCode = error;
            return socketError == SocketError.Success ? Marshal.PtrToStringAnsi((IntPtr)buffer) : null;
        }

        private static SocketError GetSocketErrorForNativeError(int error)
        {
            switch (error)
            {
                case 0:
                    return SocketError.Success;
                case (int)Interop.Sys.GetAddrInfoErrorFlags.EAI_AGAIN:
                    return SocketError.TryAgain;
                case (int)Interop.Sys.GetAddrInfoErrorFlags.EAI_BADFLAGS:
                case (int)Interop.Sys.GetAddrInfoErrorFlags.EAI_BADARG:
                    return SocketError.InvalidArgument;
                case (int)Interop.Sys.GetAddrInfoErrorFlags.EAI_FAIL:
                    return SocketError.NoRecovery;
                case (int)Interop.Sys.GetAddrInfoErrorFlags.EAI_FAMILY:
                    return SocketError.AddressFamilyNotSupported;
                case (int)Interop.Sys.GetAddrInfoErrorFlags.EAI_NONAME:
                    return SocketError.HostNotFound;
                case (int)Interop.Sys.GetAddrInfoErrorFlags.EAI_MEMORY:
                    throw new OutOfMemoryException();
                default:
                    Debug.Fail("Unexpected error: " + error.ToString());
                    return SocketError.SocketError;
            }
        }
    }

    internal static partial class Interop
    {
        internal static partial class Sys
        {
            internal const int NI_MAXHOST = 1025;

            [Flags]
            internal enum GetNameInfoFlags : int
            {
                NI_NAMEREQD = 0x1,
                NI_NUMERICHOST = 0x2,
            }

            internal enum GetAddrInfoErrorFlags : int
            {
                EAI_AGAIN = 1,      // Temporary failure in name resolution.
                EAI_BADFLAGS = 2,   // Invalid value for `ai_flags' field.
                EAI_FAIL = 3,       // Non-recoverable failure in name resolution.
                EAI_FAMILY = 4,     // 'ai_family' not supported.
                EAI_NONAME = 5,     // NAME or SERVICE is unknown.
                EAI_BADARG = 6,     // One or more input arguments were invalid.
                EAI_NOMORE = 7,     // No more entries are present in the list.
                EAI_MEMORY = 8,     // Out of memory.
            }

            [DllImport("System.Native", EntryPoint = "SystemNative_GetNameInfo")] // Libraries.SystemNative
            internal static extern unsafe int GetNameInfo(
                byte* address,
                uint addressLength,
                byte isIpv6,
                byte* host,
                uint hostLength,
                byte* service,
                uint serviceLength,
                GetNameInfoFlags flags);
        }
    }

    internal static class IPAddressParserStatics
    {
        public const int IPv4AddressBytes = 4;
        public const int IPv6AddressBytes = 16;
        public const int IPv6AddressShorts = IPv6AddressBytes / 2;
    }
}

Output when I run it:

% ./DnsTest
118.127.7.26 Dns.GetHostEntryAsync = d.sb
118.127.7.26 TryGetNameInfo = rtif-118.127.7.26-31.cs-1.as45671.net.au
203.26.198.8 Dns.GetHostEntryAsync = d.sb
203.26.198.8 TryGetNameInfo = ten2-2.br-1-1.as45671.net.au
141.0.104.145 Dns.GetHostEntryAsync = d.sb
141.0.104.145 TryGetNameInfo = 141.0.104.145.static.lyse.net

So GetHostEntryAsync is definitely acting weird here. I might just use DnsClient.NET instead of the framework DNS code since it seems more reliable for this use case.

scalablecory commented 4 years ago

The Dns class is a bit of a misnomer. It is not just DNS, but rather a name resolver -- it can look at more things than just DNS (e.g. /etc/hosts, NetBIOS names, etc.). So the result might not be incorrect here -- is it returning just your local domain name and stopping there, or also returning DNS-based domains?

Daniel15 commented 4 years ago

Turns out the issue is that I have a wildcard subdomain (so [anything].d.sb returns a valid IP), which was causing this behaviour. ping actually exhibited the same behaviour:

% ping asdfasdfasdfasdf
PING d.sb (209.141.56.29) 56(84) bytes of data.
64 bytes from d.sb (209.141.56.29): icmp_seq=1 ttl=64 time=0.063 ms
^C

I found this interesting section in the resolv.conf manpage (http://man7.org/linux/man-pages/man5/resolv.conf.5.html):

   search Search list for host-name lookup.
          By default, the search list contains one entry, the local
          domain name.  It is determined from the local hostname
          returned by gethostname(2); the local domain name is taken to
          be everything after the first '.'.  Finally, if the hostname
          does not contain a '.', the root domain is assumed as the
          local domain name.

So what the Linux DNS resolver was doing was concatenating .d.sb to the end of the requested hostname.

Adding

search .

to /etc/resolv.conf fixed it, as it prevents the behaviour.