src/jdk.dns.client/share/classes/jdk/dns/client/internal/DnsDatagramChannelFactory.java
branchaefimov-dns-client-branch
changeset 58870 35c438a6d45c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.dns.client/share/classes/jdk/dns/client/internal/DnsDatagramChannelFactory.java	Thu Oct 31 16:16:21 2019 +0000
@@ -0,0 +1,287 @@
+/*
+ * 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.  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 jdk.dns.client.internal;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ProtocolFamily;
+import java.net.SocketException;
+import java.nio.channels.DatagramChannel;
+import java.util.Objects;
+import java.util.Random;
+import java.util.concurrent.locks.ReentrantLock;
+
+class DnsDatagramChannelFactory {
+    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 = PortConfig.getLower();
+        static final int UPPER = 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;
+    final ReentrantLock factoryLock = new ReentrantLock();
+
+    DnsDatagramChannelFactory() {
+        this(new Random());
+    }
+
+    DnsDatagramChannelFactory(Random random) {
+        this(Objects.requireNonNull(random), null, DEVIATION, THRESHOLD);
+    }
+
+    DnsDatagramChannelFactory(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 DatagramChannel bound to a random port.
+     * @throws SocketException if the socket cannot be created.
+     */
+    public DatagramChannel open() throws SocketException {
+        factoryLock.lock();
+        try {
+            int lastseen = lastport;
+            DatagramChannel dc;
+
+            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
+                dc = openRandom();
+                if (dc != null) return dc;
+
+                // couldn't allocate a random port: reset all counters and fall
+                // through.
+                unsuitablePortCount = 0;
+                suitablePortCount = 0;
+                lastseen = 0;
+            }
+
+            // Allocate an ephemeral port (port 0)
+            dc = openDefault();
+            lastport = dc.socket().getLocalPort();
+            if (lastseen == 0) {
+                history.offer(lastport);
+                return dc;
+            }
+
+            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 dc;
+            }
+
+            // Undecided... the new port was too close. Let's allocate a random
+            // port using our own algorithm
+            assert !thresholdCrossed;
+            try {
+                dc.close();
+            } catch (IOException ioe) {
+                throw new SocketException(ioe.getMessage());
+            }
+            dc = openRandom();
+            unsuitablePortCount++;
+            return dc;
+        } finally {
+            factoryLock.unlock();
+        }
+    }
+
+    private DatagramChannel openDefault() throws SocketException {
+        if (family != null) {
+            try {
+                DatagramChannel dc = DatagramChannel.open(family);
+                try {
+                    dc.bind(null);
+                    return dc;
+                } catch (Throwable x) {
+                    dc.close();
+                    throw x;
+                }
+            } catch (SocketException x) {
+                throw x;
+            } catch (IOException x) {
+                SocketException e = new SocketException(x.getMessage());
+                e.initCause(x);
+                throw e;
+            }
+        }
+        try {
+            return DatagramChannel.open();
+        } catch (IOException ioe) {
+            throw new SocketException(ioe.getMessage());
+        }
+    }
+
+    boolean isUsingNativePortRandomization() {
+        factoryLock.lock();
+        try {
+            return unsuitablePortCount <= thresholdCount
+                    && suitablePortCount > thresholdCount;
+        } finally {
+            factoryLock.unlock();
+        }
+    }
+
+    boolean isUsingJavaPortRandomization() {
+        factoryLock.lock();
+        try {
+            return unsuitablePortCount > thresholdCount;
+        } finally {
+            factoryLock.unlock();
+        }
+    }
+
+    boolean isUndecided() {
+        factoryLock.lock();
+        try {
+            return !isUsingJavaPortRandomization()
+                    && !isUsingNativePortRandomization();
+        } finally {
+            factoryLock.unlock();
+        }
+    }
+
+    private DatagramChannel openRandom() {
+        int maxtries = MAX_RANDOM_TRIES;
+        while (maxtries-- > 0) {
+            int port = EphemeralPortRange.LOWER
+                    + random.nextInt(EphemeralPortRange.RANGE);
+            try {
+                if (family != null) {
+                    DatagramChannel dc = DatagramChannel.open(family);
+                    try {
+                        dc.bind(new InetSocketAddress(port));
+                        return dc;
+                    } catch (Throwable x) {
+                        dc.close();
+                        throw x;
+                    }
+                } else {
+
+                }
+                DatagramChannel dc = DatagramChannel.open(family);
+                return dc.bind(new InetSocketAddress(port));
+            } catch (IOException x) {
+                // try again until maxtries == 0;
+            }
+        }
+        return null;
+    }
+
+}