13

I have a WinForms app, and I'm trying to get reverse DNS entries for a list of IPs displayed on the form.

The main issue I've run into is System.Net.Dns.GetHostEntry is ridiculously slow, particularly when no reverse DNS entry is found. With straight DNS, this should be fast, since the DNS server will return NXDOMAIN. Internally, it's calling ws2_32.dll getnameinfo(), which states "Name resolution can be by the Domain Name System (DNS), a local hosts file, or by other naming mechanisms" - so I'm assuming it's those "other naming mechanisms" that's causing it to be so slow, but does anyone know what those mechanisms are?

Generally this is taking 5 seconds per IP, unless it finds a reverse entry, and then it's almost immediate. I've partly worked around this using threads, but since I am doing a large list and I can only run so many threads at once, it still takes a while to get through them all.

Is there a better way to find reverse DNS entries that is going to be faster?

gregmac
  • 24,276
  • 10
  • 87
  • 118

5 Answers5

11

Maybe this can help? The WayBack Machine version of the dead link.

(Dead link: http://www.chapleau.info/blog/2008/09/09/reverse-dns-lookup-with-timeout-in-c.html)

The code, for posterity:

private delegate IPHostEntry GetHostEntryHandler(string ip);

public string GetReverseDNS(string ip, int timeout)
{
    try
    {
        GetHostEntryHandler callback = new GetHostEntryHandler(Dns.GetHostEntry);
        IAsyncResult result = callback.BeginInvoke(ip,null,null);
        if (result.AsyncWaitHandle.WaitOne(timeout, false))
        {
            return callback.EndInvoke(result).HostName;
        }
        else
        {
            return ip;
        }
    }
    catch (Exception)
    {
        return ip;
    }
}
Sixto Saez
  • 12,610
  • 5
  • 43
  • 51
ra00l
  • 570
  • 4
  • 20
8

Unfortunately, there is no way (of which I am aware) to change this timeout in the Windows API, on the client side. The best you can do is edit the registry to alter the length of the timeouts in DNS queries. See this technet article for details. To my knowledge, attempts 1, 2, & 3 are run when you do this, hence the 5 second delay.

The only other option is to use some form of background processing, such as this asynchronous version of reverse DNS lookups. This is going to use threading, though, so you'll eventually run into the timeouts (it'll be better, since it'll be across many waiting threads, but still not perfect). Personally, if you're going to process a huge number, I'd mix both approaches - do a reverse lookup ansyncrhonously AND modify the registry to make the timeout shorter.


Edit after comments:

If you look at the flags on getnameinfo, there is a flags parameter. I believe you can P/Invoke into this and set the flags NI_NAMEREQD | NI_NUMERICHOST to get the behavior you are after. (The first says to error out immediately if there is no DNS entry, which helps the timeout - the second says to do the reverse lookup.)

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • 1
    I actually did use that version to begin with. It effectively gets around the timeout problem. My issue is more that there has to be a timeout at all. Go run nslookup or dig on the command line with some random IP - it will generally return in <1 s and say "*** server.pf.local can't find 42.23.1.42: Non-existent domain" (or NXDOMAIN, in the case of dig) -- I'm wondering why GetHostEntry() doesn't work the same way. – gregmac Jun 15 '09 at 17:37
  • I believe you can accomplish what you want via P/Invoke, by using different flags than the defaults on getnameinfo. See my edit. – Reed Copsey Jun 15 '09 at 18:14
5

You can improve the speed of a failed lookup considerably by querying the in-addr.arpa domain. E.g to perform a reverse IP lookup for IP address A.B.C.D you should query DNS for the domain D.C.B.A.in-addr.arpa. If reverse lookup is possible a PTR record with the host name is returned.

Unfortunately .NET does not have a general API for querying DNS. But by using P/Invoke you can call the DNS API to get the desired result (the function will return null if the reverse lookup fails).

using System;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;

public static String ReverseIPLookup(IPAddress ipAddress) {
  if (ipAddress.AddressFamily != AddressFamily.InterNetwork)
    throw new ArgumentException("IP address is not IPv4.", "ipAddress");
  var domain = String.Join(
    ".", ipAddress.GetAddressBytes().Reverse().Select(b => b.ToString())
  ) + ".in-addr.arpa";
  return DnsGetPtrRecord(domain);
}

static String DnsGetPtrRecord(String domain) {
  const Int16 DNS_TYPE_PTR = 0x000C;
  const Int32 DNS_QUERY_STANDARD = 0x00000000;
  const Int32 DNS_ERROR_RCODE_NAME_ERROR = 9003;
  IntPtr queryResultSet = IntPtr.Zero;
  try {
    var dnsStatus = DnsQuery(
      domain,
      DNS_TYPE_PTR,
      DNS_QUERY_STANDARD,
      IntPtr.Zero,
      ref queryResultSet,
      IntPtr.Zero
    );
    if (dnsStatus == DNS_ERROR_RCODE_NAME_ERROR)
      return null;
    if (dnsStatus != 0)
      throw new Win32Exception(dnsStatus);
    DnsRecordPtr dnsRecordPtr;
    for (var pointer = queryResultSet; pointer != IntPtr.Zero; pointer = dnsRecordPtr.pNext) {
      dnsRecordPtr = (DnsRecordPtr) Marshal.PtrToStructure(pointer, typeof(DnsRecordPtr));
      if (dnsRecordPtr.wType == DNS_TYPE_PTR)
        return Marshal.PtrToStringUni(dnsRecordPtr.pNameHost);
    }
    return null;
  }
  finally {
    const Int32 DnsFreeRecordList = 1;
    if (queryResultSet != IntPtr.Zero)
      DnsRecordListFree(queryResultSet, DnsFreeRecordList);
  }
}

[DllImport("Dnsapi.dll", EntryPoint = "DnsQuery_W", ExactSpelling=true, CharSet = CharSet.Unicode, SetLastError = true)]
static extern Int32 DnsQuery(String lpstrName, Int16 wType, Int32 options, IntPtr pExtra, ref IntPtr ppQueryResultsSet, IntPtr pReserved);

[DllImport("Dnsapi.dll", SetLastError = true)]
static extern void DnsRecordListFree(IntPtr pRecordList, Int32 freeType);

[StructLayout(LayoutKind.Sequential)]
struct DnsRecordPtr {
  public IntPtr pNext;
  public String pName;
  public Int16 wType;
  public Int16 wDataLength;
  public Int32 flags;
  public Int32 dwTtl;
  public Int32 dwReserved;
  public IntPtr pNameHost;
}
Martin Liversage
  • 104,481
  • 22
  • 209
  • 256
  • 1
    This worked fine for a resolvable address but hung when I gave it an unresolvable address. – JimSTAT Apr 01 '15 at 18:55
  • On Windows, DnsQuery() would return Status 9003 (no DNS record) when using the "a.b.c.d" notation, but would succeed with the "d.c.b.a.in-addr.arpa" notation. – TeasingDart Jun 20 '20 at 21:35
2

In case anyone hits this...

I switched from using the TcpClient constructor to calling the obsolete Dns.GetHostByName instead.

For whatever reason it performs much better.

public TcpClientIP(string hostname, int port) : base()
{
    try
    {
        if (_legacyDnsEnabled)
        {
            var host = Dns.GetHostByName(hostname);
            var ips = host.AddressList.Select(o => new IPAddress(o.GetAddressBytes())).ToArray();
            Connect(ips, port);
            return;
        }
    }
    catch(SocketException e)
    { }

    Connect(hostname, port);
}
Steve
  • 976
  • 12
  • 12
  • This only works if you have a hostname already, not if you have an IP address and want a hostname as in the original question – mike nelson Oct 12 '17 at 22:39
  • 2
    @Steve probably means Dns.GetHostByAddress(IpAddress). This might do what you want and is much faster. On Windows 10 Dns.GetHostEntry() takes 12 seconds for me, whereas Dns.GetHostByAddress() takes less than 2 seconds. – JonP Jun 01 '18 at 14:06
  • On my system involving two NICs, where the second one doesn't have a DNS, the timeout is the same. – tm1 Sep 06 '18 at 08:50
  • @JonP and others who may land here ...a bit late but, I have seen a very similar performance delta between "GetHostEntry" and "GetHostbyAddress". Granted that at some point "GetHostbyAddress" may simply stop working...but it is visibly faster. – galileo Mar 25 '23 at 15:14
  • I'm finding it's about 6 seconds for both GetHostEntry and GetHostByAddress, so no benefit in my case for switching. – user2728841 Jun 27 '23 at 12:30
0

Mainly adding a comment in case someone finds this via google, as I did...

The behavior may be OS version specific; these notes apply for Server 2008 R2.

The NI_NUMERICHOST flag doesn't do what you want; this cases the API to return the numeric version of the host identifier (ie: IP address), rather than the host name.

Even with NI_NAMEREQD, there is still a timeout if the information is not found (5 seconds by default). I'm not sure if this is due to a cascading lookup timeout, or something else, but this flag does not prevent the timeout (nor does any other flag, as far as I can tell).

It appears this calls the WSALookupService API's internally, although it's unclear what flags are being passed. Also, note that the information returned can be incorrect; in one of my test cases, nslookup returned no result, but getnameinfo returned as inaccurate and unqualified name. So... yeah, no good answer yet, but hopefully this information is helpful.

Nick
  • 6,808
  • 1
  • 22
  • 34