Java – Looking up DNS entries with JNDI

If your Java code needs to look up DNS records the JDK’s JNDI classes give you everything you need

For any common programming job there’ll almost certainly be an open source library that’ll do it for you. If you only need one function from it though, you can find yourself wondering if it’s really worth that extra Maven dependency. Not to mention the extra deployment bloat.

If you want to perform a basic naming lookup against a DNS sever, to get a domain’s mail server (MX) records for example, there are plenty of open source options available but with a few simple lines of code you can do the same thing with just the JDK’s JNDI classes.

Here’s how.

Getting DNS Attributes

DNS lookups are available via JNDI – the Java Naming and Directory Interface – which has been around since the earliest days of Enterprise Java and is bundled with recent JDK versions. The gateway to looking up basic DNS information is the DirContext interface and its getAttributes method.

Let’s kick off by writing an outline getRecords function which accepts a host name and a record type and returns a sorted list of matching DNS record entries:-

public class DNSINfo {
    public String[] getRecords(String hostName, String type) {
        Set<String> results = new TreeSet<String>();
        try {
            DirContext dnsContext = null; // TODO
            Attributes dnsEntries = dnsContext.getAttributes(
                    hostName, new String[]{type});
            if(dnsEntries != null) {
                // TODO
            }
        } catch(NamingException e) {
            // Handle exception
        }
        return results.toArray(new String[results.size()]);
    }
}

Our function calls getAttributes on a DirContext instance, passing it the name we want to look up (e.g. devguerrilla.com) and the type of records we want (e.g. MX). Whatever that call returns will be added to the results it returns. Here’s a simple Spock test to exercise it:-

def "Get MX records for devguerrilla.com"() {
    when :
        def records = new DNSInfo().getRecords("devguerrilla.com", "MX")
    then :
        records == ["0 mail.devguerrilla.com."]
}

Our test fails at the moment because we’ve a couple of TODOs to take care of in our getRecords function.

Getting a DirContext

The first thing we need to do is get an instance of something that implements DirContext.

Like many Java APIs, JNDI is designed to be platform independent, generic and re-usable but unfortunately that can entail a little extra work to configure it for each specific need. In this case the InitialDirContext class implements DirContext for us but in order to use it we need to tell the JVM the factory we want to use for the underlying implementation class. In order for us to do that, we need to know what underlying implementation class we need to use.

Fortunately, a DNS context factory does come with the JDK, helpfully called DnsContextFactory. We can supply this information to InitialDirContext by including it in a Hashtable of environment properties:-

public String[] getRecords(String hostName, String type) {
    Set<String> results = new TreeSet<String>();
    try {
        Hashtable<String, String> envProps = 
                new Hashtable<String, String>();
        envProps.put(Context.INITIAL_CONTEXT_FACTORY,
                "com.sun.jndi.dns.DnsContextFactory");
        DirContext dnsContext = new InitialDirContext(envProps);
        Attributes dnsEntries = dnsContext.getAttributes(
                hostName, new String[]{type});
        if(dnsEntries != null) {
            // TODO
        }
    } catch(NamingException e) {
        // Handle exception
    }
    return results.toArray(new String[results.size()]);
}

Alternatively it can be provided as a -D option to the JVM.

-Djava.naming.factory.initial=com.sun.jndi.dns.DnsContextFactory

If you’re deploying your code to an Enterprise Java container this configuration might be taken care of by the vendor, but you’ll probably still need to specify it directly for your unit tests. Also note that server and JDK vendors might provide their own alternative implementation and you may prefer to use that one in your unit test for consistency.

Processing the results

Our test still fails as we’ve one more TODO to look at. The Attributes object returned isn’t totally straightforward and needs a little navigating. One way to process the results is to call get(type) on it to retrieve the specific Attribute (e.g. the MX attribute) and then call getAll() on that to obtain a NamingEnumeration we can use to iterate all the values:-

public String[] getRecords(String hostName, String type) {
    Set<String> results = new TreeSet<String>();
    try {
        Hashtable<String, String> envProps = 
                new Hashtable<String, String>();
        envProps.put(Context.INITIAL_CONTEXT_FACTORY,
                "com.sun.jndi.dns.DnsContextFactory");
        DirContext dnsContext = new InitialDirContext(envProps);
        Attributes dnsEntries = dnsContext.getAttributes(
                hostName, new String[]{type});
        if(dnsEntries != null) {
            NamingEnumeration<?> dnsEntryIterator =
                    dnsEntries.get(type).getAll();
            while(dnsEntryIterator.hasMoreElements()) {
                results.add(dnsEntryIterator.next().toString());
            }
        }
    } catch(NamingException e) {
        // Handle exception
    }
    return results.toArray(new String[results.size()]);
}

With this final bit of code our test now passes. We can do another to get the A records too:-

def "Get A records for devguerrilla.com"() {
    when :
        def records = new DNSInfo().getRecords("devguerrilla.com", "A")
    then :
        records == ["173.254.28.76"]
}

Gotchas

Good unit tests should cover failure scenarios too so let’s add another test which uses a misspelt domain name that (at least currently) doesn’t exist:-

def "Get A records for misspelt devguerrilla.com"() {
    when :
        def records = new DNSInfo().getRecords("devgerrilla.com", "A")
    then :
        records == []
}

This turns out to have been worth doing, because – for me – this test fails:-

Condition not satisfied:

records == []
|       |
|       false
[92.242.132.16]

I’ve got a strange IP address back when I wasn’t expecting a result at all This is happening because my friendly neighbourhood broadband provider gives me an error page when I try to browse to a domain that doesn’t exist and this is its address.

$ nslookup error.talktalk.co.uk
Server:   192.168.0.1
Address:  192.168.0.1#53

Non-authoritative answer:
Name:  error.talktalk.co.uk
Address: 92.242.134.16

Generally an out-of-the-box DNS lookup via JNDI will use the same name resolution mechanism the host operating system is configured to use. There are a few configuration settings we can add to the properties we provide to InitialDirContext to control this behaviour which, for the DNS provider in the JDK, are listed here.

We could try forcing an authoritative response from the DNS server by setting java.naming.authoritative to true:-

envProps.put(Context.AUTHORITATIVE, "true");

Unfortunately, as noted in the documentation “some information might be made unavailable when you request that only authoritative responses be returned because the DNS protocol does not provide a way to request authoritative information” and that’s true for me here:-

javax.naming.NameNotFoundException:
DNS response not authoritative; remaining name 'devguerrilla.com'
    at com.sun.jndi.dns.DnsClient.query(DnsClient.java:203)
    at com.sun.jndi.dns.Resolver.query(Resolver.java:64)
    …

Another option here is to tell JNDI to use a specific DNS server by setting java.naming.provider.url.

envProps.put(Context.PROVIDER_URL, "dns://8.8.8.8/");

This makes our code use Google’s Public DNS instead and, finally, all our tests are green.

Leave a Reply

Your email address will not be published. Required fields are marked *