Thursday, August 14, 2008

Java SSL connection calls InetAddress.getLocalHost


That sounds innocuous, right?
When you call InetAddress.getLocalhost(), a reverse DNS lookup for your hostname is done. In the worst case, you’ve specified a DNS server that isn’t reachable, and so you have to wait for the DNS timeout, which can be quite long, like 30 seconds or 2 minutes. The reason the crypto code in JCE is doing this is for a random seed generator. Seems you could find something else more random than your hostname…
Below I’ve replicated the sample code that I created for this fix, in case it’s of any use to anyone:
I’ve found what I believe is a workaround to this problem, that seems to work against Java6. It works by setting the system property impl.prefix, and using implementations derived from the following classes:
java.net.PlainDatagramSocketImpl
java.net.Inet4AddressImpl
java.net.Inet6AddressImpl
The override implementations of Inet4AddressImpl and Inet6AddressImpl are designed to make sure that InetAddress.getLocalHost() returns an answer without causing any network access. That means that SSL connections, when constructing their random seed that includes the local hostname, will not hang when DNS cannot be reached.
The reason PlainDatagramSocketImpl is overridden is because the system propertyimpl.prefix is also used to construct it; if impl.prefix is not specified, then a prefix of “Plain” is assumed, and thus PlainDatagramSocketImpl is loaded. Therefore we must provide an implementation that with our own matching prefix.
The main class, DefeatGetLocalHost sets the system property impl.prefix to “DefeatGetLocalHost”. This will cause the following classes to be loaded when they are needed:
java.net.DefeatGetLocalHostDatagramSocketImpl
java.net.DefeatGetLocalHostInet4AddressImpl
java.net.DefeatGetLocalHostInet6AddressImpl
The reason that these derived classes are set in the same package, java.net, is because constructors and methods are package protected; therefore placing them in the same package provides the highest level of compatibility.
Also, in order to get our derived classes in package java.net to load in the Java runtime, we have to append the boot classpath. This is done with: -Xbootclasspath/a: after which we specify the directory with our class files.
In the next comment are the source files that I wrote to demonstrate. Compile it and execute DefeatGetLocalHost using -Xbootclasspath/a: to include the overridden classes.
java/net/DefeatGetLocalHostDatagramSocketImpl.java:
package java.net;

class DefeatGetLocalHostDatagramSocketImpl extends PlainDatagramSocketImpl {
}
java/net/DefeatGetLocalHostInet4AddressImpl.java:
package java.net;
import java.io.IOException;

class DefeatGetLocalHostInet4AddressImpl extends Inet4AddressImpl {

    public String getLocalHostName() {
        System.out.println("Using implementation " +
                           this.getClass().getName() + ".getLocalHostName");
        return "localhost";
    }

    public InetAddress[] lookupAllHostAddr(String hostname)
        throws UnknownHostException {

        System.out.println("Using implementation " +
                           this.getClass().getName() + ".lookupAllHostAddr");

        if (hostname.equals("localhost")) {
            return new InetAddress[] {
                InetAddress.getByAddress(new byte[] {
                        (byte)127, (byte)0, (byte)0, (byte)1
                    })
            };
        }

        return super.lookupAllHostAddr(hostname);
    }
}
java/net/DefeatGetLocalHostInet6AddressImpl.java:
package java.net;
import java.io.IOException;

class DefeatGetLocalHostInet6AddressImpl extends Inet6AddressImpl {

    public String getLocalHostName() {
        System.out.println("Using implementation " +
                           this.getClass().getName() + ".getLocalHostName");
        return "localhost";
    }

    public InetAddress[] lookupAllHostAddr(String hostname)
        throws UnknownHostException {

        System.out.println("Using implementation " +
                           this.getClass().getName() + ".lookupAllHostAddr");

        if (hostname.equals("localhost")) {
            return new InetAddress[] {
                InetAddress.getByAddress(new byte[] {
                        (byte)127, (byte)0, (byte)0, (byte)1
                    })
            };
        }

        return super.lookupAllHostAddr(hostname);
    }
}
DefeatGetLocalHost.java:
public class DefeatGetLocalHost {

    public static void main(String[] args) {
        try {
            safeMain(args);
        } catch(Throwable e) {
            e.printStackTrace();
        }
    }

    private static void safeMain(String[] args)
        throws java.net.UnknownHostException, java.net.SocketException {

        System.setProperty("impl.prefix", "DefeatGetLocalHost");

        System.out.println("Getting localhost:");
        System.out.println(java.net.InetAddress.getLocalHost().getHostAddress());

        System.out.println("Creating DatagramSocket:");
        java.net.DatagramSocket dg = new java.net.DatagramSocket();
        dg.close();

        System.out.println("Success");
    }
}