From 0020ffe27f5c891b7c9a9114df2506f58b45a610 Mon Sep 17 00:00:00 2001 From: igerasim Date: Mon, 7 Aug 2017 19:57:21 -0700 Subject: [PATCH] 8182125: Improve reliability of DNS lookups Reviewed-by: chegar, rriggs, dfuchs --- .../jndi/dns/DNSDatagramSocketFactory.java | 246 ++++++++++++++++++ .../classes/com/sun/jndi/dns/DnsClient.java | 93 +++---- .../com/sun/jndi/dns/ResourceRecord.java | 31 ++- 3 files changed, 324 insertions(+), 46 deletions(-) create mode 100644 src/share/classes/com/sun/jndi/dns/DNSDatagramSocketFactory.java diff --git a/src/share/classes/com/sun/jndi/dns/DNSDatagramSocketFactory.java b/src/share/classes/com/sun/jndi/dns/DNSDatagramSocketFactory.java new file mode 100644 index 000000000..04b983f69 --- /dev/null +++ b/src/share/classes/com/sun/jndi/dns/DNSDatagramSocketFactory.java @@ -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 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; + } + +} diff --git a/src/share/classes/com/sun/jndi/dns/DnsClient.java b/src/share/classes/com/sun/jndi/dns/DnsClient.java index 4ad7990e0..b1237499b 100644 --- a/src/share/classes/com/sun/jndi/dns/DnsClient.java +++ b/src/share/classes/com/sun/jndi/dns/DnsClient.java @@ -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 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()); } + DatagramSocket getDatagramSocket() throws NamingException { + try { + return factory.open(); + } catch (java.net.SocketException e) { + NamingException ne = new ConfigurationException(); + ne.setRootCause(e); + throw ne; + } + } + protected void finalize() { close(); } @@ -150,7 +154,6 @@ public class DnsClient { private Object queuesLock = new Object(); public void close() { - udpSocket.close(); synchronized (queuesLock) { reqs.clear(); resps.clear(); @@ -392,43 +395,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(); - - byte[] data = ipkt.getData(); - if (isMatchResponse(data, xid)) { - return data; - } - timeoutLeft = pktTimeout - ((int) (end - start)); - } while (timeoutLeft > minTimeout); + // 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); - } finally { - udpSocket.disconnect(); + } finally { + udpSocket.disconnect(); + } + return null; // no matching packet received within the timeout } - return null; // no matching packet received within the timeout } } diff --git a/src/share/classes/com/sun/jndi/dns/ResourceRecord.java b/src/share/classes/com/sun/jndi/dns/ResourceRecord.java index 846135c45..775215c67 100644 --- a/src/share/classes/com/sun/jndi/dns/ResourceRecord.java +++ b/src/share/classes/com/sun/jndi/dns/ResourceRecord.java @@ -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 buf.toString(); } + + //------------------------------------------------------------------------- + + private static final boolean debug = false; + + private static void dprint(String mess) { + if (debug) { + System.err.println("DNS: " + mess); + } + } + } -- GitLab