1 /*
2 * Copyright 2024 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. You may obtain a copy of the License at:
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16 package io.netty.resolver.dns;
17
18 import io.netty.util.internal.MathUtil;
19
20 import java.security.SecureRandom;
21
22 /**
23 * Special data-structure that will allow to retrieve the next query id to use, while still guarantee some sort
24 * of randomness.
25 * The query id will be between 0 (inclusive) and 65535 (inclusive) as defined by the RFC.
26 */
27 final class DnsQueryIdSpace {
28 private static final int MAX_ID = 65535;
29 private static final int BUCKETS = 4;
30 // Each bucket is 16kb of size.
31 private static final int BUCKET_SIZE = (MAX_ID + 1) / BUCKETS;
32
33 // If there are other buckets left that have at least 500 usable ids we will drop an unused bucket.
34 private static final int BUCKET_DROP_THRESHOLD = 500;
35 private final DnsQueryIdRange[] idBuckets = new DnsQueryIdRange[BUCKETS];
36 private final SecureRandom random = new SecureRandom();
37
38 DnsQueryIdSpace() {
39 assert idBuckets.length == MathUtil.findNextPositivePowerOfTwo(idBuckets.length);
40 // We start with 1 bucket.
41 idBuckets[0] = newBucket(0, random);
42 }
43
44 private static DnsQueryIdRange newBucket(int idBucketsIdx, SecureRandom random) {
45 return new DnsQueryIdRange(BUCKET_SIZE, idBucketsIdx * BUCKET_SIZE, random);
46 }
47
48 /**
49 * Returns the next ID to use for a query or {@code -1} if there is none left to use.
50 *
51 * @return next id to use.
52 */
53 int nextId() {
54 int freeIdx = -1;
55 for (int bucketIdx = 0; bucketIdx < idBuckets.length; bucketIdx++) {
56 DnsQueryIdRange bucket = idBuckets[bucketIdx];
57 if (bucket != null) {
58 int id = bucket.nextId();
59 if (id != -1) {
60 return id;
61 }
62 } else if (freeIdx == -1 ||
63 // Let's make it somehow random which free slot is used.
64 random.nextBoolean()) {
65 // We have a slot that we can use to create a new bucket if we need to.
66 freeIdx = bucketIdx;
67 }
68 }
69 if (freeIdx == -1) {
70 // No ids left and no slot left to create a new bucket.
71 return -1;
72 }
73
74 // We still have some slots free to store a new bucket. Let's do this now and use it to generate the next id.
75 DnsQueryIdRange bucket = newBucket(freeIdx, random);
76 idBuckets[freeIdx] = bucket;
77 int id = bucket.nextId();
78 assert id >= 0;
79 return id;
80 }
81
82 /**
83 * Push back the id, so it can be used again for the next query.
84 *
85 * @param id the id.
86 */
87 void pushId(int id) {
88 int bucketIdx = id / BUCKET_SIZE;
89 if (bucketIdx >= idBuckets.length) {
90 throw new IllegalArgumentException("id too large: " + id);
91 }
92 DnsQueryIdRange bucket = idBuckets[bucketIdx];
93 assert bucket != null;
94 bucket.pushId(id);
95
96 if (bucket.usableIds() == bucket.maxUsableIds()) {
97 // All ids are usable in this bucket. Let's check if there are other buckets left that have still
98 // some space left and if so drop this bucket.
99 for (int idx = 0; idx < idBuckets.length; idx++) {
100 if (idx != bucketIdx) {
101 DnsQueryIdRange otherBucket = idBuckets[idx];
102 if (otherBucket != null && otherBucket.usableIds() > BUCKET_DROP_THRESHOLD) {
103 // Drop bucket on the floor to reduce memory usage, there is another bucket left we can
104 // use that still has enough ids to use.
105 idBuckets[bucketIdx] = null;
106 return;
107 }
108 }
109 }
110 }
111 }
112
113 /**
114 * Return how much more usable ids are left.
115 *
116 * @return the number of ids that are left for usage.
117 */
118 int usableIds() {
119 int usableIds = 0;
120 for (DnsQueryIdRange bucket: idBuckets) {
121 // If there is nothing stored in the index yet we can assume the whole bucket is usable
122 usableIds += bucket == null ? BUCKET_SIZE : bucket.usableIds();
123 }
124 return usableIds;
125 }
126
127 /**
128 * Return the maximum number of ids that are supported.
129 *
130 * @return the maximum number of ids.
131 */
132 int maxUsableIds() {
133 return BUCKET_SIZE * idBuckets.length;
134 }
135
136 /**
137 * Provides a query if from a range of possible ids.
138 */
139 private static final class DnsQueryIdRange {
140
141 // Holds all possible ids which are stored as unsigned shorts
142 private final short[] ids;
143 private final int startId;
144 private final SecureRandom random;
145 private int count;
146
147 DnsQueryIdRange(int bucketSize, int startId, SecureRandom random) {
148 this.ids = new short[bucketSize];
149 this.startId = startId;
150 this.random = random;
151 for (int v = startId; v < bucketSize + startId; v++) {
152 pushId(v);
153 }
154 }
155
156 /**
157 * Returns the next ID to use for a query or {@code -1} if there is none left to use.
158 *
159 * @return next id to use.
160 */
161 int nextId() {
162 assert count >= 0;
163 if (count == 0) {
164 return -1;
165 }
166 short id = ids[count - 1];
167 count--;
168
169 return id & 0xFFFF;
170 }
171
172 /**
173 * Push back the id, so it can be used again for the next query.
174 *
175 * @param id the id.
176 */
177 void pushId(int id) {
178 if (count == ids.length) {
179 throw new IllegalStateException("overflow");
180 }
181 assert id <= startId + ids.length && id >= startId;
182 // pick a slot for our index, and whatever was in that slot before will get moved to the tail.
183 int insertionPosition = random.nextInt(count + 1);
184 short moveId = ids[insertionPosition];
185 short insertId = (short) id;
186
187 // Assert that the ids are different or its the same index.
188 assert moveId != insertId || insertionPosition == count;
189
190 ids[count] = moveId;
191 ids[insertionPosition] = insertId;
192 count++;
193 }
194
195 /**
196 * Return how much more usable ids are left.
197 *
198 * @return the number of ids that are left for usage.
199 */
200 int usableIds() {
201 return count;
202 }
203
204 /**
205 * Return the maximum number of ids that are supported.
206 *
207 * @return the maximum number of ids.
208 */
209 int maxUsableIds() {
210 return ids.length;
211 }
212 }
213 }