Accessing the Dusty Corners of DNS with Java

by David Tiller

Most Java developers have never needed to interact with the DNS directly; the most well-known job of DNS, that of resolving hostnames to IP addresses, is performed automatically every time a network operation involving a hostname is performed. Similarly, finding a server that is willing to accept email for a particular email address is performed transparently using DNS. How would a Java developer get access to such arcane, low-level information to do something as simple as validate that a particular email address has a server somewhere in the world willing to accept email for it? It all comes down to hierarchy.

Overview Of DNS

DNS, or Domain Name System, was created to provide a globally-distributed database of information about systems and capabilities. To help keep system and organizational information organized, it was decided to break up the chore of maintaining and updating the information across lines known as 'domains'. There are two common types of domain: the top-level domain and subdomains. A top-level domain represents a major division of the DNS space. For example, the TLD '.edu' is reserved for educational institutions, '.com' for businesses, etc. Subdomains (sometimes called second-, third-, etc level domains) represent an organizational entity or subunit; examples would be 'captechventures.com', which uniquely identifies the company 'captechventures' in the '.com' top-level domain. An organization can choose to further divide their domain into subsequent subdomains; 'richmond.captechventures.com' and 'dc.captechventures.com' would be examples. Read from right to left, Fully Qualified Domain Names (ones that includes all domains from the lowest level all the way up to the TLD) form a hierarchical tree of domains.

Within these domains administrators are free to define hostnames and services. Definitions take the form of Resource Records, each of which defines a different type of mapping. To define a hostname to IP address mapping, an entry called an 'A record' is made in the DNS configuration for the desired domain. To map the hostname 'www.captechventures.com' to the IP address 209.96.236.197, the following entry would be added to the 'captechventures.com' DNS configuration:

www.captechventures.com.    6321    IN    A    209.96.236.197

The name on the left is the FQDN of the host. The number 6321 is the cache time-to-live of the entry, 'IN' indicates this mapping is for the 'IN'ternet class of address, the 'A' is the resource record type, and the rest is the IP address in dotted quad notation. Similarly, servers that are willing to accept SMTP email for users in a particular domain are indicated by 'MX' resource records. MX records include timeout values, classes, and types just like 'A' records, but also include a priority that indicates the order in which MX server should be contacted. When queried using any of a whole host of command-line tools such as dig or nslookup, the popular email service GMail returns the following servers that accept email for gmail.com users:

gmail.com.   613    IN    MX    5     gmail-smtp-in.l.google.com.
gmail.com.   613    IN    MX    10    alt1.gmail-smtp-in.l.google.com.
gmail.com.   613    IN    MX    20    alt2.gmail-smtp-in.l.google.com.
gmail.com.   613    IN    MX    30    alt3.gmail-smtp-in.l.google.com.
gmail.com.   613    IN    MX    40    alt4.gmail-smtp-in.l.google.com.

Getting Access with Java

Given that DNS is hierarchical and that there's no java.dns package, how can one get to this sort of data? By leveraging another hierarcical Java construct, the classes in javax.naming. You might say, "Hey! Those classes are for looking up JNDI names and LDAP information!" You're correct; one use of javax.naming.InitialContext is to look up JNDI names, and one use of javax.naming.directory.InitialDirContext is for looking up LDAP information, but they're much more versatile that that. Specifically, Sun (now Oracle) provides several different Initial Context Factories that provide a wide range of lookup services including DNS resource records.

If you've used JNDI, you're likely familiar with the concept of an InitialContextFactory. To look up DNS information, the context factory to use is "com.sun.jndi.dns.DnsContextFactory". The following code snippet shows how to create an InitialDirContext object ready to perform DNS lookups. Note that all of the code examples below omit error checking and exception handling for clarity.

Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
InitialDirContext idc = new InitialDirContext(env);

If you wish to use DNS servers other than the DNS servers defined on the host where the code will be executed, you can specify the DNS server(s) with an additional environment entry:

env.put(Context.PROVIDER_URL, "dns://ns1.my.domain");

Similarly, if you'd like to adjust the timeouts, recursion status, and many other DNS lookup parameters, see this excellent article.

Once you have a valid InitialDirContext, looking up DNS information is as simple as specifying what Resource Record type(s) you'd like information on.

private static final String MX_ATTRIB = "MX";
private static final String ADDR_ATTRIB = "A";
private static String[] MX_ATTRIBS = {MX_ATTRIB};
private static String[] ADDR_ATTRIBS = {ADDR_ATTRIB};
 
private InitialDirContext idc;
 
public DNSLookup() {
  Properties env = new Properties();
  env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
  idc = new InitialDirContext(env);
}
 
...
 
public ListString> getMXServers(String domain) {
 
  ListString> servers = new ArrayListString>();
  Attributes attrs = idc.getAttributes(domain, MX_ATTRIBS);
  Attribute attr = attrs.get(MX_ATTRIB);
 
  if (attr != null) {
    for (int i = 0; i  attr.size(); i++) {
      String mxAttr = (String) attr.get(i);
      String[] parts = mxAttr.split(" ");
 
      // Split off the priority, and take the last field
      servers.add(parts[parts.length - 1]);
    }
  }
 
  return servers;
}

Once you have a list of MX servers for a domain, you could perform a similar lookup to make sure at least one MX server has a valid IP address. Do perform that lookup, simply substitute A_ATTRIB(s) for MX_ATTRIB(s), and delete the string manipulation that removes the leading priority from the MX records.

public ListString> getIPAddresses(String hostname) {
 
  ListString> ipAddresses = new ArrayListString>();
  Attributes attrs = idc.getAttributes(hostname, ADDR_ATTRIBS);
  Attribute attr = attrs.get(ADDR_ATTRIB);
 
  if (attr != null) {
    for (int i = 0; i  attr.size(); i++) {
      ipAddresses.add((String) attr.get(i));
    }
  }
 
  return ipAddresses;
}

With "MX" and "A" lookups under your belt, you can validate than the domain for any email address has at least one valid MX server and that its IP address is valid. You could even come up with a service that scans a database of email addresses, strips off the domain, performs the lookup, and sets a flag in the database if the lookup fails. If you want to get fancy, you could even use the java.util.concurrent.Executors.newFixedThreadPool to handle several lookups in parallel. Note that InitialDirContexts are _not_ threadsafe, and must be allocated per thread (the easiest way being via the ThreadLocal mechanism).

If your curiousity had been piqued, follow me into the world of ...

Reverse IP lookups and Canonical Names

In order to look up a hostname given an IP address, you search for a resource record of type "PTR". This RR is a bit of an odd duck - it is a pointer to other parts of the DNS configuration. When looking up a PTR record, you actually search for the dotted quad IP address with the quads reversed, and with .IN-ADDR.ARPA. appended. For example, to see what hostname was defined for IP address 209.96.236.195, you'd search for "195.236.96.209.IN-ADDR.ARPA." Rearranging the quads puts the search term into least-to-most specific order when read backwards, which is how 'normal' domain names are arranged.

public String getRevName(String ipAddr) {
 
  String revName = null;
  String[] quads = ipAddr.split("\\.");
 
  //StringBuilder would be better, I know.
  ipAddr = "";
 
  for (int i = quads.length - 1; i >= 0; i--) {
    ipAddr += quads[i] + ".";
  }
 
  ipAddr += "in-addr.arpa.";
  Attributes attrs = idc.getAttributes(ipAddr, new String[] {"PTR"});
  Attribute attr = attrs.get("PTR");
 
  if (attr != null) {
    revName = (String) attr.get(0);
  }
 
  return revName;
}

Here's an example with a twist:
If you look up MX records for captechventures.com, one of them is 'asesino.captechventures.com'. The result of an A record lookup for that hostname yields 209.96.236.195. If you perform the above reverse lookup however, you get the hostname "ns1.captechventures.com"! What happened? It turns out that you can map many hostnames to a particular IP address, but an IP address can only map to a single hostname. The A record for "ns1.captechventures.com" returns the same IP address as 'asesino.captechventures.com', but the PTR record for '195.236.96.209.IN-ADDR.ARPA.' points to 'ns1.captechventures.com' only.

A similar concept are CNAME records. The 'C' in CNAME stands for Canonical, meaning the offical name of the host. Instead of having two A records point to the same IP address, one could configure one A mapping as normal (ns1.captechventures.com. A 209.96.236.195), and any subsequent hostnames that refer to the same IP address would have CNAME records like:

asesino.captechventures.com.    CNAME    ns1.captechventures.com.
testbox.captechventures.com.    CNAME    ns1.captechventures.com.

The advantage here is that if the IP address of the physical server changes, you only have to adjust it in one place, that being the ns1->209.96.236.195 mapping. The other mappings will still work, since in essence they're saying "my IP address is the same as 'ns1.captechventures.com.' The same restriction on reverse lookups that applies to multiple A records also applies to CNAME'ed records. autodiscover.captechventures.com and email.captechventures.com form such a CNAME pair (autodiscover.captechventures.com. 7185 IN CNAME email.captechventures.com).

What if you'd like to look for any information for a domain or host? You can perform a search that uses wildcards in either the record class or record type fields, or both. Instead of "MX" or "A" or any of the other types, you can use "*" to mean 'ANY'. Querying captechventures.com for type 'ANY' (using dig) or Java code using attribute name {"*"} returns our MX servers and our SOA record:

captechventures.com.    4664    IN    MX    10 barracuda.captechventures.com.
captechventures.com.    4664    IN    MX    20 asesino.captechventures.com.
captechventures.com.    4560    IN    SOA   ns.captechventures.com. bbajcsi.captechventures.com. 2010061605 10800 3600 604800 3600

"*" and "* *" mean any class of information and any type. "IN *" means any internet type, "* A" means any class of address type.

Conclusion

Hopefully this introduction to DNS access via Java has given you another arrow in your quiver to use when you face the DNS dragon and given you a better idea of the different ways in which the javax.naming classes can be used.

References

Much more information regarding the mapping of DNS Resource Records and different ways to access DNS information can be found here, and examples of different lookups can be found here.