Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

`Dns.GetHostEntryAsync` returning odd results #405

Closed
Daniel15 opened this issue Nov 30, 2019 · 4 comments
Milestone

Comments

@Daniel15
Copy link

@Daniel15 Daniel15 commented Nov 30, 2019

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

This comment has been minimized.

Copy link
Author

@Daniel15 Daniel15 commented Dec 1, 2019

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:

// Do a reverse lookup to get the host name.
string name = NameResolutionPal.TryGetNameInfo(address, out SocketError errorCode, out int nativeErrorCode);
if (errorCode != SocketError.Success)
{
if (NetEventSource.IsEnabled) NetEventSource.Error(address, $"{address} DNS lookup failed with {errorCode}");
throw SocketExceptionFactory.CreateSocketException(errorCode, nativeErrorCode);
}
// Do the forward lookup to get the IPs for that host name
errorCode = NameResolutionPal.TryGetAddrInfo(name, justAddresses, out string hostName, out string[] aliases, out IPAddress[] addresses, out nativeErrorCode);

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

This comment has been minimized.

Copy link
Author

@Daniel15 Daniel15 commented Dec 1, 2019

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 scalablecory added bug os-linux and removed untriaged labels Dec 2, 2019
@scalablecory scalablecory added this to the 5.0 milestone Dec 2, 2019
@scalablecory

This comment has been minimized.

Copy link
Member

@scalablecory scalablecory commented Dec 2, 2019

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

This comment has been minimized.

Copy link
Author

@Daniel15 Daniel15 commented Dec 3, 2019

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.

@Daniel15 Daniel15 closed this Dec 3, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants
You can’t perform that action at this time.