Skip to content

Commit 464c8b8

Browse files
mmimicapavelrappo
authored andcommitted
8228580: DnsClient TCP socket timeout
Reviewed-by: vtewari, chegar, prappo
1 parent c85075b commit 464c8b8

File tree

4 files changed

+289
-17
lines changed

4 files changed

+289
-17
lines changed

src/jdk.naming.dns/share/classes/com/sun/jndi/dns/DnsClient.java

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -29,7 +29,9 @@
2929
import java.net.DatagramSocket;
3030
import java.net.DatagramPacket;
3131
import java.net.InetAddress;
32+
import java.net.InetSocketAddress;
3233
import java.net.Socket;
34+
import java.net.SocketTimeoutException;
3335
import java.security.SecureRandom;
3436
import javax.naming.*;
3537

@@ -82,7 +84,7 @@ public class DnsClient {
8284
private static final SecureRandom random = JCAUtil.getSecureRandom();
8385
private InetAddress[] servers;
8486
private int[] serverPorts;
85-
private int timeout; // initial timeout on UDP queries in ms
87+
private int timeout; // initial timeout on UDP and TCP queries in ms
8688
private int retries; // number of UDP retries
8789

8890
private final Object udpSocketLock = new Object();
@@ -100,7 +102,7 @@ public class DnsClient {
100102
/*
101103
* Each server is of the form "server[:port]". IPv6 literal host names
102104
* include delimiting brackets.
103-
* "timeout" is the initial timeout interval (in ms) for UDP queries,
105+
* "timeout" is the initial timeout interval (in ms) for queries,
104106
* and "retries" gives the number of retries per server.
105107
*/
106108
public DnsClient(String[] servers, int timeout, int retries)
@@ -237,14 +239,15 @@ ResourceRecords query(DnsName fqdn, int qclass, int qtype,
237239

238240
// Try each server, starting with the one that just
239241
// provided the truncated message.
242+
int retryTimeout = (timeout * (1 << retry));
240243
for (int j = 0; j < servers.length; j++) {
241244
int ij = (i + j) % servers.length;
242245
if (doNotRetry[ij]) {
243246
continue;
244247
}
245248
try {
246249
Tcp tcp =
247-
new Tcp(servers[ij], serverPorts[ij]);
250+
new Tcp(servers[ij], serverPorts[ij], retryTimeout);
248251
byte[] msg2;
249252
try {
250253
msg2 = doTcpQuery(tcp, pkt);
@@ -327,7 +330,7 @@ ResourceRecords queryZone(DnsName zone, int qclass, boolean recursion)
327330
// Try each name server.
328331
for (int i = 0; i < servers.length; i++) {
329332
try {
330-
Tcp tcp = new Tcp(servers[i], serverPorts[i]);
333+
Tcp tcp = new Tcp(servers[i], serverPorts[i], timeout);
331334
byte[] msg;
332335
try {
333336
msg = doTcpQuery(tcp, pkt);
@@ -462,19 +465,19 @@ private byte[] doTcpQuery(Tcp tcp, Packet pkt) throws IOException {
462465
*/
463466
private byte[] continueTcpQuery(Tcp tcp) throws IOException {
464467

465-
int lenHi = tcp.in.read(); // high-order byte of response length
468+
int lenHi = tcp.read(); // high-order byte of response length
466469
if (lenHi == -1) {
467470
return null; // EOF
468471
}
469-
int lenLo = tcp.in.read(); // low-order byte of response length
472+
int lenLo = tcp.read(); // low-order byte of response length
470473
if (lenLo == -1) {
471474
throw new IOException("Corrupted DNS response: bad length");
472475
}
473476
int len = (lenHi << 8) | lenLo;
474477
byte[] msg = new byte[len];
475478
int pos = 0; // next unfilled position in msg
476479
while (len > 0) {
477-
int n = tcp.in.read(msg, pos, len);
480+
int n = tcp.read(msg, pos, len);
478481
if (n == -1) {
479482
throw new IOException(
480483
"Corrupted DNS response: too little data");
@@ -682,20 +685,62 @@ private static void dprint(String mess) {
682685

683686
class Tcp {
684687

685-
private Socket sock;
686-
java.io.InputStream in;
687-
java.io.OutputStream out;
688+
private final Socket sock;
689+
private final java.io.InputStream in;
690+
final java.io.OutputStream out;
691+
private int timeoutLeft;
688692

689-
Tcp(InetAddress server, int port) throws IOException {
690-
sock = new Socket(server, port);
691-
sock.setTcpNoDelay(true);
692-
out = new java.io.BufferedOutputStream(sock.getOutputStream());
693-
in = new java.io.BufferedInputStream(sock.getInputStream());
693+
Tcp(InetAddress server, int port, int timeout) throws IOException {
694+
sock = new Socket();
695+
try {
696+
long start = System.currentTimeMillis();
697+
sock.connect(new InetSocketAddress(server, port), timeout);
698+
timeoutLeft = (int) (timeout - (System.currentTimeMillis() - start));
699+
if (timeoutLeft <= 0)
700+
throw new SocketTimeoutException();
701+
702+
sock.setTcpNoDelay(true);
703+
out = new java.io.BufferedOutputStream(sock.getOutputStream());
704+
in = new java.io.BufferedInputStream(sock.getInputStream());
705+
} catch (Exception e) {
706+
try {
707+
sock.close();
708+
} catch (IOException ex) {
709+
e.addSuppressed(ex);
710+
}
711+
throw e;
712+
}
694713
}
695714

696715
void close() throws IOException {
697716
sock.close();
698717
}
718+
719+
private interface SocketReadOp {
720+
int read() throws IOException;
721+
}
722+
723+
private int readWithTimeout(SocketReadOp reader) throws IOException {
724+
if (timeoutLeft <= 0)
725+
throw new SocketTimeoutException();
726+
727+
sock.setSoTimeout(timeoutLeft);
728+
long start = System.currentTimeMillis();
729+
try {
730+
return reader.read();
731+
}
732+
finally {
733+
timeoutLeft -= System.currentTimeMillis() - start;
734+
}
735+
}
736+
737+
int read() throws IOException {
738+
return readWithTimeout(() -> in.read());
739+
}
740+
741+
int read(byte b[], int off, int len) throws IOException {
742+
return readWithTimeout(() -> in.read(b, off, len));
743+
}
699744
}
700745

701746
/*

src/jdk.naming.dns/share/classes/module-info.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -26,7 +26,38 @@
2626
/**
2727
* Provides the implementation of the DNS Java Naming provider.
2828
*
29+
* <h2>Environment Properties</h2>
30+
*
31+
* <p> The following JNDI environment properties may be used when creating
32+
* the initial context.
33+
*
34+
* <ul>
35+
* <li>com.sun.jndi.dns.timeout.initial</li>
36+
* <li>com.sun.jndi.dns.timeout.retries</li>
37+
* </ul>
38+
*
39+
* <p> These properties are used to alter the timeout-related defaults that the
40+
* DNS provider uses when submitting queries. The DNS provider submits queries
41+
* using the following exponential backoff algorithm. The provider submits a
42+
* query to a DNS server and waits for a response to arrive within a timeout
43+
* period (1 second by default). If it receives no response within the timeout
44+
* period, it queries the next server, and so on. If the provider receives no
45+
* response from any server, it doubles the timeout period and repeats the
46+
* process of submitting the query to each server, up to a maximum number of
47+
* retries (4 by default).
48+
*
49+
* <p> The {@code com.sun.jndi.dns.timeout.initial} property, if set, specifies
50+
* the number of milliseconds to use as the initial timeout period (i.e., before
51+
* any doubling). If this property has not been set, the default initial timeout
52+
* is 1000 milliseconds.
53+
*
54+
* <p> The {@code com.sun.jndi.dns.timeout.retries} property, if set, specifies
55+
* the number of times to retry each server using the exponential backoff
56+
* algorithm described previously. If this property has not been set, the
57+
* default number of retries is 4.
58+
*
2959
* @provides javax.naming.spi.InitialContextFactory
60+
*
3061
* @moduleGraph
3162
* @since 9
3263
*/
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#
2+
# Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
3+
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
#
5+
# This code is free software; you can redistribute it and/or modify it
6+
# under the terms of the GNU General Public License version 2 only, as
7+
# published by the Free Software Foundation.
8+
#
9+
# This code is distributed in the hope that it will be useful, but WITHOUT
10+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
# version 2 for more details (a copy is included in the LICENSE file that
13+
# accompanied this code).
14+
#
15+
# You should have received a copy of the GNU General Public License version
16+
# 2 along with this work; if not, write to the Free Software Foundation,
17+
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
#
19+
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
# or visit www.oracle.com if you need additional information or have any
21+
# questions.
22+
#
23+
24+
################################################################################
25+
# Capture file for TcpTimeout.java
26+
#
27+
# NOTE: This hexadecimal dump of DNS protocol messages was generated by
28+
# running the GetEnv application program against a real DNS
29+
# server along with DNSTracer
30+
#
31+
################################################################################
32+
33+
# DNS Request
34+
35+
0000: 32 72 01 00 00 01 00 00 00 00 00 00 05 68 6F 73 2r...........hos
36+
0010: 74 31 07 64 6F 6D 61 69 6E 31 03 63 6F 6D 00 00 t1.domain1.com..
37+
0020: FF 00 FF ...
38+
39+
40+
# DNS Response
41+
42+
0000: 32 72 82 00 00 01 00 06 00 01 00 01 05 68 6F 73 2r...........hos
43+
0010: 74 31 07 64 6F 6D 61 69 6E 31 03 63 6F 6D 00 00 t1.domain1.com..
44+
0020: FF 00 01 C0 0C 00 10 00 01 00 00 8C A0 00 15 14 ................
45+
0030: 41 20 76 65 72 79 20 70 6F 70 75 6C 61 72 20 68 A very popular h
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
import jtreg.SkippedException;
25+
26+
import javax.naming.directory.InitialDirContext;
27+
import java.io.IOException;
28+
import java.net.BindException;
29+
import java.net.InetAddress;
30+
import java.net.ServerSocket;
31+
32+
import static java.util.concurrent.TimeUnit.NANOSECONDS;
33+
import static jdk.test.lib.Utils.adjustTimeout;
34+
35+
/*
36+
* @test
37+
* @bug 8228580
38+
* @summary Tests that we get a DNS response when the UDP DNS server returns a
39+
* truncated response and the TCP DNS server does not respond at all
40+
* after connect.
41+
* @library ../lib/
42+
* @library /test/lib
43+
* @modules java.base/sun.security.util
44+
* @run main TcpTimeout
45+
* @run main TcpTimeout -Dcom.sun.jndi.dns.timeout.initial=5000
46+
*/
47+
48+
public class TcpTimeout extends DNSTestBase {
49+
private TcpDnsServer tcpDnsServer;
50+
51+
/* The acceptable variation in timeout measurement. */
52+
private static final long TOLERANCE = adjustTimeout(5_000);
53+
54+
/* The acceptable variation of early returns from timed socket operations. */
55+
private static final long PREMATURE_RETURN = adjustTimeout(100);
56+
57+
public static void main(String[] args) throws Exception {
58+
new TcpTimeout().run(args);
59+
}
60+
61+
@Override
62+
public void runTest() throws Exception {
63+
/* The default timeout value is 1 second, as stated in the
64+
jdk.naming.dns module docs. */
65+
long timeout = 1_000;
66+
var envTimeout = env().get("com.sun.jndi.dns.timeout.initial");
67+
if (envTimeout != null)
68+
timeout = Long.parseLong(String.valueOf(envTimeout));
69+
70+
setContext(new InitialDirContext(env()));
71+
72+
long startNanos = System.nanoTime();
73+
74+
/* perform query */
75+
var attrs = context().getAttributes("host1");
76+
77+
long elapsed = NANOSECONDS.toMillis(System.nanoTime() - startNanos);
78+
if (elapsed < timeout - PREMATURE_RETURN || elapsed > timeout + TOLERANCE) {
79+
throw new RuntimeException(String.format(
80+
"elapsed=%s, timeout=%s, TOLERANCE=%s, PREMATURE_RETURN=%s",
81+
elapsed, timeout, TOLERANCE, PREMATURE_RETURN));
82+
}
83+
84+
DNSTestUtils.debug(attrs);
85+
86+
/* Note that the returned attributes are truncated and the response
87+
is not valid. */
88+
var txtAttr = attrs.get("TXT");
89+
if (txtAttr == null)
90+
throw new RuntimeException("TXT attribute missing.");
91+
}
92+
93+
@Override
94+
public void initTest(String[] args) {
95+
/* We need to bind the TCP server on the same port the UDP server is
96+
listening to. This may not be possible if that port is in use. Retry
97+
MAX_RETRIES times relying on UDP port randomness. */
98+
final int MAX_RETRIES = 5;
99+
for (int i = 0; i < MAX_RETRIES; i++) {
100+
super.initTest(args);
101+
var udpServer = (Server) env().get(DNSTestUtils.TEST_DNS_SERVER_THREAD);
102+
int port = udpServer.getPort();
103+
try {
104+
tcpDnsServer = new TcpDnsServer(port);
105+
break; // success
106+
} catch (BindException be) {
107+
DNSTestUtils.debug("Failed to bind server socket on port " + port
108+
+ ", retry no. " + (i + 1) + ", " + be.getMessage());
109+
} catch (Exception ex) {
110+
throw new RuntimeException("Unexpected exception during initTest", ex);
111+
} finally {
112+
if (tcpDnsServer == null) { // cleanup behind exceptions
113+
super.cleanupTest();
114+
}
115+
}
116+
}
117+
118+
if (tcpDnsServer == null) {
119+
throw new SkippedException("Cannot start TCP server after "
120+
+ MAX_RETRIES
121+
+ " tries, skip the test");
122+
}
123+
}
124+
125+
@Override
126+
public void cleanupTest() {
127+
super.cleanupTest();
128+
if (tcpDnsServer != null)
129+
tcpDnsServer.stopServer();
130+
}
131+
132+
/**
133+
* A TCP server that accepts a connection and does nothing else: causes read
134+
* timeout on client side.
135+
*/
136+
private static class TcpDnsServer {
137+
final ServerSocket serverSocket;
138+
139+
TcpDnsServer(int port) throws IOException {
140+
serverSocket = new ServerSocket(port, 0, InetAddress.getLoopbackAddress());
141+
System.out.println("TcpDnsServer: listening on port " + port);
142+
}
143+
144+
void stopServer() {
145+
try {
146+
if (serverSocket != null)
147+
serverSocket.close();
148+
} catch (Exception ignored) { }
149+
}
150+
}
151+
}

0 commit comments

Comments
 (0)