A DESCRIPTION OF THE REQUEST :
If Java tries to connect to a host for which the DNS record has multiple IP addresses in the A record, only the first returned IP is used.
JUSTIFICATION :
To be highly available (and also to efficiently distribute the flows), some web servers (eg: www.twitter.com) define several IP for the same DNS record.
In that way, if a given IP is not accessible (route down for instance, server down, firewalls...), the service is still guarantee.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Java should transparently try to connect to the first address and, if a connection timeout occurs, then test the second address, ..
ACTUAL -
If Java tries to connect to a host for which the DNS record has multiple IP addresses in the A record, only the first returned IP is used.
In fact, the constructor java.net.InetSocketAddress.InetSocketAddress(String, int) uses java.net.InetAddress.getByName(String) to resolve the IP, which does a "InetAddress.getAllByName(host)[0];"
---------- BEGIN SOURCE ----------
Let's run the given source code :
try {
// Verify the different IP
InetAddress[] addrs = InetAddress.getAllByName("www.twitter.com");
for (InetAddress add : addrs) {
System.out.println(add);
}
// Now, try to connect
URL u = new URL("http://www.twitter.com/");
HttpURLConnection con = (HttpURLConnection)u.openConnection();
con.setConnectTimeout(2000);
con.connect();
} catch (Exception e) {
e.printStackTrace();
}
Once you got the 3 defined IP, modify you firewall rules to block the first one. You'll get the following stack trace :
java.net.SocketTimeoutException: connect timed out
at java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:75)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:339)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182)
at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:157)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:391)
at java.net.Socket.connect(Socket.java:579)
at sun.security.ssl.SSLSocketImpl.connect(SSLSocketImpl.java:612)
at sun.net.NetworkClient.doConnect(NetworkClient.java:175)
at sun.net.www.http.HttpClient.openServer(HttpClient.java:388)
at sun.net.www.http.HttpClient.openServer(HttpClient.java:483)
at sun.net.www.protocol.https.HttpsClient.<init>(HttpsClient.java:278)
at sun.net.www.protocol.https.HttpsClient.New(HttpsClient.java:335)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.getNewHttpClient(AbstractDelegateHttpsURLConnection.java:191)
at sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:928)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:177)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:153)
We expect to check the second or the third IP in that case...
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
The only way I found to bypass this bug is to patch the method sun.net.NetworkClient.doConnect(String, int)
/**
* Return a socket connected to the server, with any
* appropriate options pre-established
*/
protected Socket doConnect (String server, int port)
throws IOException, UnknownHostException {
Socket s = null;
// Some hosts have multiple DNS records (fail over). So let's try the different
// addresses to connect the server as the InetAddress.getByName(server) always
// return the first result
InetAddress[] multipleAddr = InetAddress.getAllByName(server);
boolean connected = false;
for (int i = 0; i < multipleAddr.length && !connected; i++) {
if (proxy != null) {
if (proxy.type() == Proxy.Type.SOCKS) {
s = AccessController.doPrivileged(
new PrivilegedAction<Socket>() {
public Socket run() {
return new Socket(proxy);
}});
} else if (proxy.type() == Proxy.Type.DIRECT) {
s = createSocket();
} else {
// Still connecting through a proxy
// server & port will be the proxy address and port
s = new Socket(Proxy.NO_PROXY);
}
} else
s = createSocket();
// Instance specific timeouts do have priority, that means
// connectTimeout & readTimeout (-1 means not set)
// Then global default timeouts
// Then no timeout.
InetAddress addr = multipleAddr[i];
try {
if (connectTimeout >= 0) {
s.connect(new InetSocketAddress(addr, port), connectTimeout);
} else {
if (defaultConnectTimeout > 0) {
s.connect(new InetSocketAddress(addr, port), defaultConnectTimeout);
} else {
s.connect(new InetSocketAddress(addr, port));
}
}
connected = true;
} catch (SocketTimeoutException e) {
s.close(); // We'll recreate it
// Host cannot be reached ; let's try the other host if found
if (i == multipleAddr.length - 1) {
// This is the last host, throw the exception in that case
throw e;
}
}
}
// s may not be null here
if(s != null) {
if (readTimeout >= 0)
s.setSoTimeout(readTimeout);
else if (defaultSoTimeout > 0) {
s.setSoTimeout(defaultSoTimeout);
}
}
return s;
}
Then, I did a jar file and modifyied the bootclasspath :
java -Xbootclasspath/p:d:\tmp\dns-rt.jar ...
If Java tries to connect to a host for which the DNS record has multiple IP addresses in the A record, only the first returned IP is used.
JUSTIFICATION :
To be highly available (and also to efficiently distribute the flows), some web servers (eg: www.twitter.com) define several IP for the same DNS record.
In that way, if a given IP is not accessible (route down for instance, server down, firewalls...), the service is still guarantee.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Java should transparently try to connect to the first address and, if a connection timeout occurs, then test the second address, ..
ACTUAL -
If Java tries to connect to a host for which the DNS record has multiple IP addresses in the A record, only the first returned IP is used.
In fact, the constructor java.net.InetSocketAddress.InetSocketAddress(String, int) uses java.net.InetAddress.getByName(String) to resolve the IP, which does a "InetAddress.getAllByName(host)[0];"
---------- BEGIN SOURCE ----------
Let's run the given source code :
try {
// Verify the different IP
InetAddress[] addrs = InetAddress.getAllByName("www.twitter.com");
for (InetAddress add : addrs) {
System.out.println(add);
}
// Now, try to connect
URL u = new URL("http://www.twitter.com/");
HttpURLConnection con = (HttpURLConnection)u.openConnection();
con.setConnectTimeout(2000);
con.connect();
} catch (Exception e) {
e.printStackTrace();
}
Once you got the 3 defined IP, modify you firewall rules to block the first one. You'll get the following stack trace :
java.net.SocketTimeoutException: connect timed out
at java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:75)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:339)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182)
at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:157)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:391)
at java.net.Socket.connect(Socket.java:579)
at sun.security.ssl.SSLSocketImpl.connect(SSLSocketImpl.java:612)
at sun.net.NetworkClient.doConnect(NetworkClient.java:175)
at sun.net.www.http.HttpClient.openServer(HttpClient.java:388)
at sun.net.www.http.HttpClient.openServer(HttpClient.java:483)
at sun.net.www.protocol.https.HttpsClient.<init>(HttpsClient.java:278)
at sun.net.www.protocol.https.HttpsClient.New(HttpsClient.java:335)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.getNewHttpClient(AbstractDelegateHttpsURLConnection.java:191)
at sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:928)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:177)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:153)
We expect to check the second or the third IP in that case...
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
The only way I found to bypass this bug is to patch the method sun.net.NetworkClient.doConnect(String, int)
/**
* Return a socket connected to the server, with any
* appropriate options pre-established
*/
protected Socket doConnect (String server, int port)
throws IOException, UnknownHostException {
Socket s = null;
// Some hosts have multiple DNS records (fail over). So let's try the different
// addresses to connect the server as the InetAddress.getByName(server) always
// return the first result
InetAddress[] multipleAddr = InetAddress.getAllByName(server);
boolean connected = false;
for (int i = 0; i < multipleAddr.length && !connected; i++) {
if (proxy != null) {
if (proxy.type() == Proxy.Type.SOCKS) {
s = AccessController.doPrivileged(
new PrivilegedAction<Socket>() {
public Socket run() {
return new Socket(proxy);
}});
} else if (proxy.type() == Proxy.Type.DIRECT) {
s = createSocket();
} else {
// Still connecting through a proxy
// server & port will be the proxy address and port
s = new Socket(Proxy.NO_PROXY);
}
} else
s = createSocket();
// Instance specific timeouts do have priority, that means
// connectTimeout & readTimeout (-1 means not set)
// Then global default timeouts
// Then no timeout.
InetAddress addr = multipleAddr[i];
try {
if (connectTimeout >= 0) {
s.connect(new InetSocketAddress(addr, port), connectTimeout);
} else {
if (defaultConnectTimeout > 0) {
s.connect(new InetSocketAddress(addr, port), defaultConnectTimeout);
} else {
s.connect(new InetSocketAddress(addr, port));
}
}
connected = true;
} catch (SocketTimeoutException e) {
s.close(); // We'll recreate it
// Host cannot be reached ; let's try the other host if found
if (i == multipleAddr.length - 1) {
// This is the last host, throw the exception in that case
throw e;
}
}
}
// s may not be null here
if(s != null) {
if (readTimeout >= 0)
s.setSoTimeout(readTimeout);
else if (defaultSoTimeout > 0) {
s.setSoTimeout(defaultSoTimeout);
}
}
return s;
}
Then, I did a jar file and modifyied the bootclasspath :
java -Xbootclasspath/p:d:\tmp\dns-rt.jar ...