1 /*
2  * Copyright (c) 2017, 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.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 package com.sun.jndi.dns;
26 
27 import java.io.IOException;
28 import java.net.DatagramSocket;
29 import java.net.ProtocolFamily;
30 import java.net.SocketException;
31 import java.net.InetSocketAddress;
32 import java.nio.channels.DatagramChannel;
33 import java.util.Objects;
34 import java.util.Random;
35 
36 class DNSDatagramSocketFactory {
37     static final int DEVIATION = 3;
38     static final int THRESHOLD = 6;
39     static final int BIT_DEVIATION = 2;
40     static final int HISTORY = 32;
41     static final int MAX_RANDOM_TRIES = 5;
42     /**
43      * The dynamic allocation port range (aka ephemeral ports), as configured
44      * on the system. Use nested class for lazy evaluation.
45      */
46     static final class EphemeralPortRange {
EphemeralPortRange()47         private EphemeralPortRange() {}
48         static final int LOWER = sun.net.PortConfig.getLower();
49         static final int UPPER = sun.net.PortConfig.getUpper();
50         static final int RANGE = UPPER - LOWER + 1;
51     }
52 
53     // Records a subset of max {@code capacity} previously used ports
54     static final class PortHistory {
55         final int capacity;
56         final int[] ports;
57         final Random random;
58         int index;
PortHistory(int capacity, Random random)59         PortHistory(int capacity, Random random) {
60             this.random = random;
61             this.capacity = capacity;
62             this.ports = new int[capacity];
63         }
64         // returns true if the history contains the specified port.
contains(int port)65         public boolean contains(int port) {
66             int p = 0;
67             for (int i=0; i<capacity; i++) {
68                 if ((p = ports[i]) == 0 || p == port) break;
69             }
70             return p == port;
71         }
72         // Adds the port to the history - doesn't check whether the port
73         // is already present. Always adds the port and always return true.
add(int port)74         public boolean add(int port) {
75             if (ports[index] != 0) { // at max capacity
76                 // remove one port at random and store the new port there
77                 ports[random.nextInt(capacity)] = port;
78             } else { // there's a free slot
79                 ports[index] = port;
80             }
81             if (++index == capacity) index = 0;
82             return true;
83         }
84         // Adds the port to the history if not already present.
85         // Return true if the port was added, false if the port was already
86         // present.
offer(int port)87         public boolean offer(int port) {
88             if (contains(port)) return false;
89             else return add(port);
90         }
91     }
92 
93     int lastport = 0;
94     int suitablePortCount;
95     int unsuitablePortCount;
96     final ProtocolFamily family; // null (default) means dual stack
97     final int thresholdCount; // decision point
98     final int deviation;
99     final Random random;
100     final PortHistory history;
101 
DNSDatagramSocketFactory()102     DNSDatagramSocketFactory() {
103         this(new Random());
104     }
105 
DNSDatagramSocketFactory(Random random)106     DNSDatagramSocketFactory(Random random) {
107         this(Objects.requireNonNull(random), null, DEVIATION, THRESHOLD);
108     }
DNSDatagramSocketFactory(Random random, ProtocolFamily family, int deviation, int threshold)109     DNSDatagramSocketFactory(Random random,
110                              ProtocolFamily family,
111                              int deviation,
112                              int threshold) {
113         this.random = Objects.requireNonNull(random);
114         this.history = new PortHistory(HISTORY, random);
115         this.family = family;
116         this.deviation = Math.max(1, deviation);
117         this.thresholdCount = Math.max(2, threshold);
118     }
119 
120     /**
121      * Opens a datagram socket listening to the wildcard address on a
122      * random port. If the underlying OS supports UDP port randomization
123      * out of the box (if binding a socket to port 0 binds it to a random
124      * port) then the underlying OS implementation is used. Otherwise, this
125      * method will allocate and bind a socket on a randomly selected ephemeral
126      * port in the dynamic range.
127      * @return A new DatagramSocket bound to a random port.
128      * @throws SocketException if the socket cannot be created.
129      */
open()130     public synchronized DatagramSocket open() throws SocketException {
131         int lastseen = lastport;
132         DatagramSocket s;
133 
134         boolean thresholdCrossed = unsuitablePortCount > thresholdCount;
135         if (thresholdCrossed) {
136             // Underlying stack does not support random UDP port out of the box.
137             // Use our own algorithm to allocate a random UDP port
138             s = openRandom();
139             if (s != null) return s;
140 
141             // couldn't allocate a random port: reset all counters and fall
142             // through.
143             unsuitablePortCount = 0; suitablePortCount = 0; lastseen = 0;
144         }
145 
146         // Allocate an ephemeral port (port 0)
147         s = openDefault();
148         lastport = s.getLocalPort();
149         if (lastseen == 0) {
150             history.offer(lastport);
151             return s;
152         }
153 
154         thresholdCrossed = suitablePortCount > thresholdCount;
155         boolean farEnough = Integer.bitCount(lastseen ^ lastport) > BIT_DEVIATION
156                             && Math.abs(lastport - lastseen) > deviation;
157         boolean recycled = history.contains(lastport);
158         boolean suitable = (thresholdCrossed || farEnough && !recycled);
159         if (suitable && !recycled) history.add(lastport);
160 
161         if (suitable) {
162             if (!thresholdCrossed) {
163                 suitablePortCount++;
164             } else if (!farEnough || recycled) {
165                 unsuitablePortCount = 1;
166                 suitablePortCount = thresholdCount/2;
167             }
168             // Either the underlying stack supports random UDP port allocation,
169             // or the new port is sufficiently distant from last port to make
170             // it look like it is. Let's use it.
171             return s;
172         }
173 
174         // Undecided... the new port was too close. Let's allocate a random
175         // port using our own algorithm
176         assert !thresholdCrossed;
177         DatagramSocket ss = openRandom();
178         if (ss == null) return s;
179         unsuitablePortCount++;
180         s.close();
181         return ss;
182     }
183 
openDefault()184     private DatagramSocket openDefault() throws SocketException {
185         if (family != null) {
186             try {
187                 DatagramChannel c = DatagramChannel.open(family);
188                 try {
189                     DatagramSocket s = c.socket();
190                     s.bind(null);
191                     return s;
192                 } catch (Throwable x) {
193                     c.close();
194                     throw x;
195                 }
196             } catch (SocketException x) {
197                 throw x;
198             } catch (IOException x) {
199                 SocketException e = new SocketException(x.getMessage());
200                 e.initCause(x);
201                 throw e;
202             }
203         }
204         return new DatagramSocket();
205     }
206 
isUsingNativePortRandomization()207     synchronized boolean isUsingNativePortRandomization() {
208         return  unsuitablePortCount <= thresholdCount
209                 && suitablePortCount > thresholdCount;
210     }
211 
isUsingJavaPortRandomization()212     synchronized boolean isUsingJavaPortRandomization() {
213         return unsuitablePortCount > thresholdCount ;
214     }
215 
isUndecided()216     synchronized boolean isUndecided() {
217         return !isUsingJavaPortRandomization()
218                 && !isUsingNativePortRandomization();
219     }
220 
openRandom()221     private DatagramSocket openRandom() {
222         int maxtries = MAX_RANDOM_TRIES;
223         while (maxtries-- > 0) {
224             int port = EphemeralPortRange.LOWER
225                     + random.nextInt(EphemeralPortRange.RANGE);
226             try {
227                 if (family != null) {
228                     DatagramChannel c = DatagramChannel.open(family);
229                     try {
230                         DatagramSocket s = c.socket();
231                         s.bind(new InetSocketAddress(port));
232                         return s;
233                     } catch (Throwable x) {
234                         c.close();
235                         throw x;
236                     }
237                 }
238                 return new DatagramSocket(port);
239             } catch (IOException x) {
240                 // try again until maxtries == 0;
241             }
242         }
243         return null;
244     }
245 
246 }
247