8228580: DnsClient TCP socket timeout
Reviewed-by: vtewari, chegar, prappo
Contributed-by: Milan Mimica <milan.mimica@gmail.com>
--- a/src/jdk.naming.dns/share/classes/com/sun/jndi/dns/DnsClient.java Tue Sep 24 10:36:35 2019 -0700
+++ b/src/jdk.naming.dns/share/classes/com/sun/jndi/dns/DnsClient.java Tue Sep 24 22:57:28 2019 +0100
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -29,7 +29,9 @@
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.net.InetAddress;
+import java.net.InetSocketAddress;
import java.net.Socket;
+import java.net.SocketTimeoutException;
import java.security.SecureRandom;
import javax.naming.*;
@@ -82,7 +84,7 @@
private static final SecureRandom random = JCAUtil.getSecureRandom();
private InetAddress[] servers;
private int[] serverPorts;
- private int timeout; // initial timeout on UDP queries in ms
+ private int timeout; // initial timeout on UDP and TCP queries in ms
private int retries; // number of UDP retries
private final Object udpSocketLock = new Object();
@@ -100,7 +102,7 @@
/*
* Each server is of the form "server[:port]". IPv6 literal host names
* include delimiting brackets.
- * "timeout" is the initial timeout interval (in ms) for UDP queries,
+ * "timeout" is the initial timeout interval (in ms) for queries,
* and "retries" gives the number of retries per server.
*/
public DnsClient(String[] servers, int timeout, int retries)
@@ -237,6 +239,7 @@
// Try each server, starting with the one that just
// provided the truncated message.
+ int retryTimeout = (timeout * (1 << retry));
for (int j = 0; j < servers.length; j++) {
int ij = (i + j) % servers.length;
if (doNotRetry[ij]) {
@@ -244,7 +247,7 @@
}
try {
Tcp tcp =
- new Tcp(servers[ij], serverPorts[ij]);
+ new Tcp(servers[ij], serverPorts[ij], retryTimeout);
byte[] msg2;
try {
msg2 = doTcpQuery(tcp, pkt);
@@ -327,7 +330,7 @@
// Try each name server.
for (int i = 0; i < servers.length; i++) {
try {
- Tcp tcp = new Tcp(servers[i], serverPorts[i]);
+ Tcp tcp = new Tcp(servers[i], serverPorts[i], timeout);
byte[] msg;
try {
msg = doTcpQuery(tcp, pkt);
@@ -462,11 +465,11 @@
*/
private byte[] continueTcpQuery(Tcp tcp) throws IOException {
- int lenHi = tcp.in.read(); // high-order byte of response length
+ int lenHi = tcp.read(); // high-order byte of response length
if (lenHi == -1) {
return null; // EOF
}
- int lenLo = tcp.in.read(); // low-order byte of response length
+ int lenLo = tcp.read(); // low-order byte of response length
if (lenLo == -1) {
throw new IOException("Corrupted DNS response: bad length");
}
@@ -474,7 +477,7 @@
byte[] msg = new byte[len];
int pos = 0; // next unfilled position in msg
while (len > 0) {
- int n = tcp.in.read(msg, pos, len);
+ int n = tcp.read(msg, pos, len);
if (n == -1) {
throw new IOException(
"Corrupted DNS response: too little data");
@@ -682,20 +685,62 @@
class Tcp {
- private Socket sock;
- java.io.InputStream in;
- java.io.OutputStream out;
+ private final Socket sock;
+ private final java.io.InputStream in;
+ final java.io.OutputStream out;
+ private int timeoutLeft;
- Tcp(InetAddress server, int port) throws IOException {
- sock = new Socket(server, port);
- sock.setTcpNoDelay(true);
- out = new java.io.BufferedOutputStream(sock.getOutputStream());
- in = new java.io.BufferedInputStream(sock.getInputStream());
+ Tcp(InetAddress server, int port, int timeout) throws IOException {
+ sock = new Socket();
+ try {
+ long start = System.currentTimeMillis();
+ sock.connect(new InetSocketAddress(server, port), timeout);
+ timeoutLeft = (int) (timeout - (System.currentTimeMillis() - start));
+ if (timeoutLeft <= 0)
+ throw new SocketTimeoutException();
+
+ sock.setTcpNoDelay(true);
+ out = new java.io.BufferedOutputStream(sock.getOutputStream());
+ in = new java.io.BufferedInputStream(sock.getInputStream());
+ } catch (Exception e) {
+ try {
+ sock.close();
+ } catch (IOException ex) {
+ e.addSuppressed(ex);
+ }
+ throw e;
+ }
}
void close() throws IOException {
sock.close();
}
+
+ private interface SocketReadOp {
+ int read() throws IOException;
+ }
+
+ private int readWithTimeout(SocketReadOp reader) throws IOException {
+ if (timeoutLeft <= 0)
+ throw new SocketTimeoutException();
+
+ sock.setSoTimeout(timeoutLeft);
+ long start = System.currentTimeMillis();
+ try {
+ return reader.read();
+ }
+ finally {
+ timeoutLeft -= System.currentTimeMillis() - start;
+ }
+ }
+
+ int read() throws IOException {
+ return readWithTimeout(() -> in.read());
+ }
+
+ int read(byte b[], int off, int len) throws IOException {
+ return readWithTimeout(() -> in.read(b, off, len));
+ }
}
/*
--- a/src/jdk.naming.dns/share/classes/module-info.java Tue Sep 24 10:36:35 2019 -0700
+++ b/src/jdk.naming.dns/share/classes/module-info.java Tue Sep 24 22:57:28 2019 +0100
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -26,7 +26,38 @@
/**
* Provides the implementation of the DNS Java Naming provider.
*
+ * <h2>Environment Properties</h2>
+ *
+ * <p> The following JNDI environment properties may be used when creating
+ * the initial context.
+ *
+ * <ul>
+ * <li>com.sun.jndi.dns.timeout.initial</li>
+ * <li>com.sun.jndi.dns.timeout.retries</li>
+ * </ul>
+ *
+ * <p> These properties are used to alter the timeout-related defaults that the
+ * DNS provider uses when submitting queries. The DNS provider submits queries
+ * using the following exponential backoff algorithm. The provider submits a
+ * query to a DNS server and waits for a response to arrive within a timeout
+ * period (1 second by default). If it receives no response within the timeout
+ * period, it queries the next server, and so on. If the provider receives no
+ * response from any server, it doubles the timeout period and repeats the
+ * process of submitting the query to each server, up to a maximum number of
+ * retries (4 by default).
+ *
+ * <p> The {@code com.sun.jndi.dns.timeout.initial} property, if set, specifies
+ * the number of milliseconds to use as the initial timeout period (i.e., before
+ * any doubling). If this property has not been set, the default initial timeout
+ * is 1000 milliseconds.
+ *
+ * <p> The {@code com.sun.jndi.dns.timeout.retries} property, if set, specifies
+ * the number of times to retry each server using the exponential backoff
+ * algorithm described previously. If this property has not been set, the
+ * default number of retries is 4.
+ *
* @provides javax.naming.spi.InitialContextFactory
+ *
* @moduleGraph
* @since 9
*/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/com/sun/jndi/dns/ConfigTests/TcpTimeout.dns Tue Sep 24 22:57:28 2019 +0100
@@ -0,0 +1,45 @@
+#
+# Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This code is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 only, as
+# published by the Free Software Foundation.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# version 2 for more details (a copy is included in the LICENSE file that
+# accompanied this code).
+#
+# You should have received a copy of the GNU General Public License version
+# 2 along with this work; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+# or visit www.oracle.com if you need additional information or have any
+# questions.
+#
+
+################################################################################
+# Capture file for TcpTimeout.java
+#
+# NOTE: This hexadecimal dump of DNS protocol messages was generated by
+# running the GetEnv application program against a real DNS
+# server along with DNSTracer
+#
+################################################################################
+
+# DNS Request
+
+0000: 32 72 01 00 00 01 00 00 00 00 00 00 05 68 6F 73 2r...........hos
+0010: 74 31 07 64 6F 6D 61 69 6E 31 03 63 6F 6D 00 00 t1.domain1.com..
+0020: FF 00 FF ...
+
+
+# DNS Response
+
+0000: 32 72 82 00 00 01 00 06 00 01 00 01 05 68 6F 73 2r...........hos
+0010: 74 31 07 64 6F 6D 61 69 6E 31 03 63 6F 6D 00 00 t1.domain1.com..
+0020: FF 00 01 C0 0C 00 10 00 01 00 00 8C A0 00 15 14 ................
+0030: 41 20 76 65 72 79 20 70 6F 70 75 6C 61 72 20 68 A very popular h
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/com/sun/jndi/dns/ConfigTests/TcpTimeout.java Tue Sep 24 22:57:28 2019 +0100
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import jtreg.SkippedException;
+
+import javax.naming.directory.InitialDirContext;
+import java.io.IOException;
+import java.net.BindException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static jdk.test.lib.Utils.adjustTimeout;
+
+/*
+ * @test
+ * @bug 8228580
+ * @summary Tests that we get a DNS response when the UDP DNS server returns a
+ * truncated response and the TCP DNS server does not respond at all
+ * after connect.
+ * @library ../lib/
+ * @library /test/lib
+ * @modules java.base/sun.security.util
+ * @run main TcpTimeout
+ * @run main TcpTimeout -Dcom.sun.jndi.dns.timeout.initial=5000
+ */
+
+public class TcpTimeout extends DNSTestBase {
+ private TcpDnsServer tcpDnsServer;
+
+ /* The acceptable variation in timeout measurement. */
+ private static final long TOLERANCE = adjustTimeout(5_000);
+
+ /* The acceptable variation of early returns from timed socket operations. */
+ private static final long PREMATURE_RETURN = adjustTimeout(100);
+
+ public static void main(String[] args) throws Exception {
+ new TcpTimeout().run(args);
+ }
+
+ @Override
+ public void runTest() throws Exception {
+ /* The default timeout value is 1 second, as stated in the
+ jdk.naming.dns module docs. */
+ long timeout = 1_000;
+ var envTimeout = env().get("com.sun.jndi.dns.timeout.initial");
+ if (envTimeout != null)
+ timeout = Long.parseLong(String.valueOf(envTimeout));
+
+ setContext(new InitialDirContext(env()));
+
+ long startNanos = System.nanoTime();
+
+ /* perform query */
+ var attrs = context().getAttributes("host1");
+
+ long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos);
+ if (elapsed < timeout - PREMATURE_RETURN || elapsed > timeout + TOLERANCE) {
+ throw new RuntimeException(String.format(
+ "elapsed=%s, timeout=%s, TOLERANCE=%s, PREMATURE_RETURN=%s",
+ elapsed, timeout, TOLERANCE, PREMATURE_RETURN));
+ }
+
+ DNSTestUtils.debug(attrs);
+
+ /* Note that the returned attributes are truncated and the response
+ is not valid. */
+ var txtAttr = attrs.get("TXT");
+ if (txtAttr == null)
+ throw new RuntimeException("TXT attribute missing.");
+ }
+
+ @Override
+ public void initTest(String[] args) {
+ /* We need to bind the TCP server on the same port the UDP server is
+ listening to. This may not be possible if that port is in use. Retry
+ MAX_RETRIES times relying on UDP port randomness. */
+ final int MAX_RETRIES = 5;
+ for (int i = 0; i < MAX_RETRIES; i++) {
+ super.initTest(args);
+ var udpServer = (Server) env().get(DNSTestUtils.TEST_DNS_SERVER_THREAD);
+ int port = udpServer.getPort();
+ try {
+ tcpDnsServer = new TcpDnsServer(port);
+ break; // success
+ } catch (BindException be) {
+ DNSTestUtils.debug("Failed to bind server socket on port " + port
+ + ", retry no. " + (i + 1) + ", " + be.getMessage());
+ } catch (Exception ex) {
+ throw new RuntimeException("Unexpected exception during initTest", ex);
+ } finally {
+ if (tcpDnsServer == null) { // cleanup behind exceptions
+ super.cleanupTest();
+ }
+ }
+ }
+
+ if (tcpDnsServer == null) {
+ throw new SkippedException("Cannot start TCP server after "
+ + MAX_RETRIES
+ + " tries, skip the test");
+ }
+ }
+
+ @Override
+ public void cleanupTest() {
+ super.cleanupTest();
+ if (tcpDnsServer != null)
+ tcpDnsServer.stopServer();
+ }
+
+ /**
+ * A TCP server that accepts a connection and does nothing else: causes read
+ * timeout on client side.
+ */
+ private static class TcpDnsServer {
+ final ServerSocket serverSocket;
+
+ TcpDnsServer(int port) throws IOException {
+ serverSocket = new ServerSocket(port, 0, InetAddress.getLoopbackAddress());
+ System.out.println("TcpDnsServer: listening on port " + port);
+ }
+
+ void stopServer() {
+ try {
+ if (serverSocket != null)
+ serverSocket.close();
+ } catch (Exception ignored) { }
+ }
+ }
+}