8182125: Improve reliability of DNS lookups
Reviewed-by: chegar, rriggs, dfuchs
This commit is contained in:
parent
248948c08b
commit
5069e85826
@ -212,7 +212,8 @@ module java.base {
|
||||
exports jdk.internal.util.jar to
|
||||
jdk.jartool;
|
||||
exports sun.net to
|
||||
jdk.incubator.httpclient;
|
||||
jdk.incubator.httpclient,
|
||||
jdk.naming.dns;
|
||||
exports sun.net.ext to
|
||||
jdk.net;
|
||||
exports sun.net.dns to
|
||||
|
@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright (c) 2017, 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. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package com.sun.jndi.dns;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.ProtocolFamily;
|
||||
import java.net.SocketException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.channels.DatagramChannel;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
|
||||
class DNSDatagramSocketFactory {
|
||||
static final int DEVIATION = 3;
|
||||
static final int THRESHOLD = 6;
|
||||
static final int BIT_DEVIATION = 2;
|
||||
static final int HISTORY = 32;
|
||||
static final int MAX_RANDOM_TRIES = 5;
|
||||
/**
|
||||
* The dynamic allocation port range (aka ephemeral ports), as configured
|
||||
* on the system. Use nested class for lazy evaluation.
|
||||
*/
|
||||
static final class EphemeralPortRange {
|
||||
private EphemeralPortRange() {}
|
||||
static final int LOWER = sun.net.PortConfig.getLower();
|
||||
static final int UPPER = sun.net.PortConfig.getUpper();
|
||||
static final int RANGE = UPPER - LOWER + 1;
|
||||
}
|
||||
|
||||
// Records a subset of max {@code capacity} previously used ports
|
||||
static final class PortHistory {
|
||||
final int capacity;
|
||||
final int[] ports;
|
||||
final Random random;
|
||||
int index;
|
||||
PortHistory(int capacity, Random random) {
|
||||
this.random = random;
|
||||
this.capacity = capacity;
|
||||
this.ports = new int[capacity];
|
||||
}
|
||||
// returns true if the history contains the specified port.
|
||||
public boolean contains(int port) {
|
||||
int p = 0;
|
||||
for (int i=0; i<capacity; i++) {
|
||||
if ((p = ports[i]) == 0 || p == port) break;
|
||||
}
|
||||
return p == port;
|
||||
}
|
||||
// Adds the port to the history - doesn't check whether the port
|
||||
// is already present. Always adds the port and always return true.
|
||||
public boolean add(int port) {
|
||||
if (ports[index] != 0) { // at max capacity
|
||||
// remove one port at random and store the new port there
|
||||
ports[random.nextInt(capacity)] = port;
|
||||
} else { // there's a free slot
|
||||
ports[index] = port;
|
||||
}
|
||||
if (++index == capacity) index = 0;
|
||||
return true;
|
||||
}
|
||||
// Adds the port to the history if not already present.
|
||||
// Return true if the port was added, false if the port was already
|
||||
// present.
|
||||
public boolean offer(int port) {
|
||||
if (contains(port)) return false;
|
||||
else return add(port);
|
||||
}
|
||||
}
|
||||
|
||||
int lastport = 0;
|
||||
int suitablePortCount;
|
||||
int unsuitablePortCount;
|
||||
final ProtocolFamily family; // null (default) means dual stack
|
||||
final int thresholdCount; // decision point
|
||||
final int deviation;
|
||||
final Random random;
|
||||
final PortHistory history;
|
||||
|
||||
DNSDatagramSocketFactory() {
|
||||
this(new Random());
|
||||
}
|
||||
|
||||
DNSDatagramSocketFactory(Random random) {
|
||||
this(Objects.requireNonNull(random), null, DEVIATION, THRESHOLD);
|
||||
}
|
||||
DNSDatagramSocketFactory(Random random,
|
||||
ProtocolFamily family,
|
||||
int deviation,
|
||||
int threshold) {
|
||||
this.random = Objects.requireNonNull(random);
|
||||
this.history = new PortHistory(HISTORY, random);
|
||||
this.family = family;
|
||||
this.deviation = Math.max(1, deviation);
|
||||
this.thresholdCount = Math.max(2, threshold);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a datagram socket listening to the wildcard address on a
|
||||
* random port. If the underlying OS supports UDP port randomization
|
||||
* out of the box (if binding a socket to port 0 binds it to a random
|
||||
* port) then the underlying OS implementation is used. Otherwise, this
|
||||
* method will allocate and bind a socket on a randomly selected ephemeral
|
||||
* port in the dynamic range.
|
||||
* @return A new DatagramSocket bound to a random port.
|
||||
* @throws SocketException if the socket cannot be created.
|
||||
*/
|
||||
public synchronized DatagramSocket open() throws SocketException {
|
||||
int lastseen = lastport;
|
||||
DatagramSocket s;
|
||||
|
||||
boolean thresholdCrossed = unsuitablePortCount > thresholdCount;
|
||||
if (thresholdCrossed) {
|
||||
// Underlying stack does not support random UDP port out of the box.
|
||||
// Use our own algorithm to allocate a random UDP port
|
||||
s = openRandom();
|
||||
if (s != null) return s;
|
||||
|
||||
// couldn't allocate a random port: reset all counters and fall
|
||||
// through.
|
||||
unsuitablePortCount = 0; suitablePortCount = 0; lastseen = 0;
|
||||
}
|
||||
|
||||
// Allocate an ephemeral port (port 0)
|
||||
s = openDefault();
|
||||
lastport = s.getLocalPort();
|
||||
if (lastseen == 0) {
|
||||
history.offer(lastport);
|
||||
return s;
|
||||
}
|
||||
|
||||
thresholdCrossed = suitablePortCount > thresholdCount;
|
||||
boolean farEnough = Integer.bitCount(lastseen ^ lastport) > BIT_DEVIATION
|
||||
&& Math.abs(lastport - lastseen) > deviation;
|
||||
boolean recycled = history.contains(lastport);
|
||||
boolean suitable = (thresholdCrossed || farEnough && !recycled);
|
||||
if (suitable && !recycled) history.add(lastport);
|
||||
|
||||
if (suitable) {
|
||||
if (!thresholdCrossed) {
|
||||
suitablePortCount++;
|
||||
} else if (!farEnough || recycled) {
|
||||
unsuitablePortCount = 1;
|
||||
suitablePortCount = thresholdCount/2;
|
||||
}
|
||||
// Either the underlying stack supports random UDP port allocation,
|
||||
// or the new port is sufficiently distant from last port to make
|
||||
// it look like it is. Let's use it.
|
||||
return s;
|
||||
}
|
||||
|
||||
// Undecided... the new port was too close. Let's allocate a random
|
||||
// port using our own algorithm
|
||||
assert !thresholdCrossed;
|
||||
DatagramSocket ss = openRandom();
|
||||
if (ss == null) return s;
|
||||
unsuitablePortCount++;
|
||||
s.close();
|
||||
return ss;
|
||||
}
|
||||
|
||||
private DatagramSocket openDefault() throws SocketException {
|
||||
if (family != null) {
|
||||
try {
|
||||
DatagramChannel c = DatagramChannel.open(family);
|
||||
try {
|
||||
DatagramSocket s = c.socket();
|
||||
s.bind(null);
|
||||
return s;
|
||||
} catch (Throwable x) {
|
||||
c.close();
|
||||
throw x;
|
||||
}
|
||||
} catch (SocketException x) {
|
||||
throw x;
|
||||
} catch (IOException x) {
|
||||
SocketException e = new SocketException(x.getMessage());
|
||||
e.initCause(x);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return new DatagramSocket();
|
||||
}
|
||||
|
||||
synchronized boolean isUsingNativePortRandomization() {
|
||||
return unsuitablePortCount <= thresholdCount
|
||||
&& suitablePortCount > thresholdCount;
|
||||
}
|
||||
|
||||
synchronized boolean isUsingJavaPortRandomization() {
|
||||
return unsuitablePortCount > thresholdCount ;
|
||||
}
|
||||
|
||||
synchronized boolean isUndecided() {
|
||||
return !isUsingJavaPortRandomization()
|
||||
&& !isUsingNativePortRandomization();
|
||||
}
|
||||
|
||||
private DatagramSocket openRandom() {
|
||||
int maxtries = MAX_RANDOM_TRIES;
|
||||
while (maxtries-- > 0) {
|
||||
int port = EphemeralPortRange.LOWER
|
||||
+ random.nextInt(EphemeralPortRange.RANGE);
|
||||
try {
|
||||
if (family != null) {
|
||||
DatagramChannel c = DatagramChannel.open(family);
|
||||
try {
|
||||
DatagramSocket s = c.socket();
|
||||
s.bind(new InetSocketAddress(port));
|
||||
return s;
|
||||
} catch (Throwable x) {
|
||||
c.close();
|
||||
throw x;
|
||||
}
|
||||
}
|
||||
return new DatagramSocket(port);
|
||||
} catch (IOException x) {
|
||||
// try again until maxtries == 0;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -85,7 +85,9 @@ public class DnsClient {
|
||||
private int timeout; // initial timeout on UDP queries in ms
|
||||
private int retries; // number of UDP retries
|
||||
|
||||
private DatagramSocket udpSocket;
|
||||
private final Object udpSocketLock = new Object();
|
||||
private static final DNSDatagramSocketFactory factory =
|
||||
new DNSDatagramSocketFactory(random);
|
||||
|
||||
// Requests sent
|
||||
private Map<Integer, ResourceRecord> reqs;
|
||||
@ -105,14 +107,6 @@ public class DnsClient {
|
||||
throws NamingException {
|
||||
this.timeout = timeout;
|
||||
this.retries = retries;
|
||||
try {
|
||||
udpSocket = new DatagramSocket();
|
||||
} catch (java.net.SocketException e) {
|
||||
NamingException ne = new ConfigurationException();
|
||||
ne.setRootCause(e);
|
||||
throw ne;
|
||||
}
|
||||
|
||||
this.servers = new InetAddress[servers.length];
|
||||
serverPorts = new int[servers.length];
|
||||
|
||||
@ -142,6 +136,16 @@ public class DnsClient {
|
||||
resps = Collections.synchronizedMap(new HashMap<Integer, byte[]>());
|
||||
}
|
||||
|
||||
DatagramSocket getDatagramSocket() throws NamingException {
|
||||
try {
|
||||
return factory.open();
|
||||
} catch (java.net.SocketException e) {
|
||||
NamingException ne = new ConfigurationException();
|
||||
ne.setRootCause(e);
|
||||
throw ne;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void finalize() {
|
||||
close();
|
||||
@ -151,7 +155,6 @@ public class DnsClient {
|
||||
private Object queuesLock = new Object();
|
||||
|
||||
public void close() {
|
||||
udpSocket.close();
|
||||
synchronized (queuesLock) {
|
||||
reqs.clear();
|
||||
resps.clear();
|
||||
@ -393,43 +396,45 @@ public class DnsClient {
|
||||
|
||||
int minTimeout = 50; // msec after which there are no retries.
|
||||
|
||||
synchronized (udpSocket) {
|
||||
DatagramPacket opkt = new DatagramPacket(
|
||||
pkt.getData(), pkt.length(), server, port);
|
||||
DatagramPacket ipkt = new DatagramPacket(new byte[8000], 8000);
|
||||
// Packets may only be sent to or received from this server address
|
||||
udpSocket.connect(server, port);
|
||||
int pktTimeout = (timeout * (1 << retry));
|
||||
try {
|
||||
udpSocket.send(opkt);
|
||||
synchronized (udpSocketLock) {
|
||||
try (DatagramSocket udpSocket = getDatagramSocket()) {
|
||||
DatagramPacket opkt = new DatagramPacket(
|
||||
pkt.getData(), pkt.length(), server, port);
|
||||
DatagramPacket ipkt = new DatagramPacket(new byte[8000], 8000);
|
||||
// Packets may only be sent to or received from this server address
|
||||
udpSocket.connect(server, port);
|
||||
int pktTimeout = (timeout * (1 << retry));
|
||||
try {
|
||||
udpSocket.send(opkt);
|
||||
|
||||
// timeout remaining after successive 'receive()'
|
||||
int timeoutLeft = pktTimeout;
|
||||
int cnt = 0;
|
||||
do {
|
||||
if (debug) {
|
||||
cnt++;
|
||||
dprint("Trying RECEIVE(" +
|
||||
cnt + ") retry(" + (retry + 1) +
|
||||
") for:" + xid + " sock-timeout:" +
|
||||
timeoutLeft + " ms.");
|
||||
}
|
||||
udpSocket.setSoTimeout(timeoutLeft);
|
||||
long start = System.currentTimeMillis();
|
||||
udpSocket.receive(ipkt);
|
||||
long end = System.currentTimeMillis();
|
||||
// timeout remaining after successive 'receive()'
|
||||
int timeoutLeft = pktTimeout;
|
||||
int cnt = 0;
|
||||
do {
|
||||
if (debug) {
|
||||
cnt++;
|
||||
dprint("Trying RECEIVE(" +
|
||||
cnt + ") retry(" + (retry + 1) +
|
||||
") for:" + xid + " sock-timeout:" +
|
||||
timeoutLeft + " ms.");
|
||||
}
|
||||
udpSocket.setSoTimeout(timeoutLeft);
|
||||
long start = System.currentTimeMillis();
|
||||
udpSocket.receive(ipkt);
|
||||
long end = System.currentTimeMillis();
|
||||
|
||||
byte[] data = ipkt.getData();
|
||||
if (isMatchResponse(data, xid)) {
|
||||
return data;
|
||||
}
|
||||
timeoutLeft = pktTimeout - ((int) (end - start));
|
||||
} while (timeoutLeft > minTimeout);
|
||||
byte[] data = ipkt.getData();
|
||||
if (isMatchResponse(data, xid)) {
|
||||
return data;
|
||||
}
|
||||
timeoutLeft = pktTimeout - ((int) (end - start));
|
||||
} while (timeoutLeft > minTimeout);
|
||||
|
||||
} finally {
|
||||
udpSocket.disconnect();
|
||||
} finally {
|
||||
udpSocket.disconnect();
|
||||
}
|
||||
return null; // no matching packet received within the timeout
|
||||
}
|
||||
return null; // no matching packet received within the timeout
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2000, 2017, 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
|
||||
@ -355,8 +355,19 @@ public class ResourceRecord {
|
||||
pos += typeAndLen;
|
||||
} else if ((typeAndLen & 0xC0) == 0xC0) { // name compression
|
||||
++level;
|
||||
endPos = pos + 2;
|
||||
// cater for the case where the name pointed to is itself
|
||||
// compressed: we don't want endPos to be reset by the second
|
||||
// compression level.
|
||||
int ppos = pos;
|
||||
if (endPos == -1) endPos = pos + 2;
|
||||
pos = getUShort(pos) & 0x3FFF;
|
||||
if (debug) {
|
||||
dprint("decode: name compression at " + ppos
|
||||
+ " -> " + pos + " endPos=" + endPos);
|
||||
assert endPos > 0;
|
||||
assert pos < ppos;
|
||||
assert pos >= Header.HEADER_SIZE;
|
||||
}
|
||||
} else
|
||||
throw new IOException("Invalid label type: " + typeAndLen);
|
||||
}
|
||||
@ -405,6 +416,11 @@ public class ResourceRecord {
|
||||
}
|
||||
}
|
||||
// Unknown RR type/class
|
||||
if (debug) {
|
||||
dprint("Unknown RR type for RR data: " + rrtype + " rdlen=" + rdlen
|
||||
+ ", pos=" + pos +", msglen=" + msg.length + ", remaining="
|
||||
+ (msg.length-pos));
|
||||
}
|
||||
byte[] rd = new byte[rdlen];
|
||||
System.arraycopy(msg, pos, rd, 0, rdlen);
|
||||
return rd;
|
||||
@ -613,4 +629,15 @@ public class ResourceRecord {
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
private static final boolean debug = false;
|
||||
|
||||
private static void dprint(String mess) {
|
||||
if (debug) {
|
||||
System.err.println("DNS: " + mess);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user