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.handler.codec.quic;
17
18 import io.netty.buffer.ByteBuf;
19 import io.netty.channel.Channel;
20 import io.netty.channel.ChannelHandlerContext;
21 import io.netty.channel.ChannelInboundHandlerAdapter;
22 import io.netty.channel.socket.DatagramPacket;
23 import io.netty.util.internal.ObjectUtil;
24 import org.jetbrains.annotations.Nullable;
25
26 import java.nio.ByteBuffer;
27 import java.util.List;
28 import java.util.concurrent.CopyOnWriteArrayList;
29 import java.util.concurrent.atomic.AtomicBoolean;
30
31
32 /**
33 * Special {@link io.netty.channel.ChannelHandler} that should be used to init {@link Channel}s that will be used
34 * for QUIC while <a href="https://man7.org/linux/man-pages/man7/socket.7.html">SO_REUSEPORT</a> is used to
35 * bind to same {@link java.net.InetSocketAddress} multiple times. This is necessary to ensure QUIC packets are always
36 * dispatched to the correct codec that keeps the mapping for the connection id.
37 * This implementation use a very simple mapping strategy by encoding the index of the internal datastructure that
38 * keeps track of the different {@link ChannelHandlerContext}s into the destination connection id. This way once a
39 * {@code QUIC} packet is received its possible to forward it to the right codec.
40 * Subclasses might change how encoding / decoding of the index is done by overriding {@link #decodeIndex(ByteBuf)}
41 * and {@link #newIdGenerator(int)}.
42 * <p>
43 * It is important that the same {@link QuicCodecDispatcher} instance is shared between all the {@link Channel}s that
44 * are bound to the same {@link java.net.InetSocketAddress} and use {@code SO_REUSEPORT}.
45 * <p>
46 * An alternative way to handle this would be to do the "routing" to the correct socket in an {@code epbf} program
47 * by implementing your own {@link QuicConnectionIdGenerator} that issue ids that can be understood and handled by the
48 * {@code epbf} program to route the packet to the correct socket.
49 *
50 */
51 public abstract class QuicCodecDispatcher extends ChannelInboundHandlerAdapter {
52 // 20 is the max as per RFC.
53 // See https://datatracker.ietf.org/doc/html/rfc9000#section-17.2
54 private static final int MAX_LOCAL_CONNECTION_ID_LENGTH = 20;
55
56 // Use a CopyOnWriteArrayList as modifications to the List should only happen during bootstrapping and teardown
57 // of the channels.
58 private final List<ChannelHandlerContextDispatcher> contextList = new CopyOnWriteArrayList<>();
59 private final int localConnectionIdLength;
60
61 /**
62 * Create a new instance using the default connection id length.
63 */
64 protected QuicCodecDispatcher() {
65 this(MAX_LOCAL_CONNECTION_ID_LENGTH);
66 }
67
68 /**
69 * Create a new instance
70 *
71 * @param localConnectionIdLength the local connection id length. This must be between 10 and 20.
72 */
73 protected QuicCodecDispatcher(int localConnectionIdLength) {
74 // Let's use 10 as a minimum to ensure we still have some bytes left for randomness as we already use
75 // 2 of the bytes to encode the index.
76 this.localConnectionIdLength = ObjectUtil.checkInRange(localConnectionIdLength,
77 10, MAX_LOCAL_CONNECTION_ID_LENGTH, "localConnectionIdLength");
78 }
79
80 @Override
81 public final boolean isSharable() {
82 return true;
83 }
84
85 @Override
86 public final void handlerAdded(ChannelHandlerContext ctx) throws Exception {
87 super.handlerAdded(ctx);
88
89 ChannelHandlerContextDispatcher ctxDispatcher = new ChannelHandlerContextDispatcher(ctx);
90 contextList.add(ctxDispatcher);
91 int idx = contextList.indexOf(ctxDispatcher);
92 try {
93 QuicConnectionIdGenerator idGenerator = newIdGenerator((short) idx);
94 initChannel(ctx.channel(), localConnectionIdLength, idGenerator);
95 } catch (Exception e) {
96 // Null out on exception and rethrow. We not remove the element as the indices need to be
97 // stable.
98 contextList.set(idx, null);
99 throw e;
100 }
101 }
102
103 @Override
104 public final void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
105 super.handlerRemoved(ctx);
106
107 for (int idx = 0; idx < contextList.size(); idx++) {
108 ChannelHandlerContextDispatcher ctxDispatcher = contextList.get(idx);
109 if (ctxDispatcher != null && ctxDispatcher.ctx.equals(ctx)) {
110 // null out, so we can collect the ChannelHandlerContext that was stored in the List.
111 contextList.set(idx, null);
112 break;
113 }
114 }
115 }
116
117 @Override
118 public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
119 DatagramPacket packet = (DatagramPacket) msg;
120 ByteBuf connectionId = getDestinationConnectionId(packet.content(), localConnectionIdLength);
121 if (connectionId != null) {
122 int idx = decodeIndex(connectionId);
123 // decodeIndex(...) returns -1 if decoding was not successful.
124 if (idx >= 0 && contextList.size() > idx) {
125 ChannelHandlerContextDispatcher selectedCtx = contextList.get(idx);
126 if (selectedCtx != null) {
127 selectedCtx.fireChannelRead(msg);
128 return;
129 }
130 }
131 }
132 // We were not be-able to dispatch to a specific ChannelHandlerContext, just forward and let the
133 // Quic*Codec handle it directly.
134 ctx.fireChannelRead(msg);
135 }
136
137 @Override
138 public final void channelReadComplete(ChannelHandlerContext ctx) {
139 // Loop over all ChannelHandlerContextDispatchers and ensure fireChannelReadComplete() is called if required.
140 // We use and old style for loop as CopyOnWriteArrayList implements RandomAccess and so we can
141 // reduce the object creations.
142 boolean dispatchForOwnContextAlready = false;
143 for (int i = 0; i < contextList.size(); i++) {
144 ChannelHandlerContextDispatcher ctxDispatcher = contextList.get(i);
145 if (ctxDispatcher != null) {
146 boolean fired = ctxDispatcher.fireChannelReadCompleteIfNeeded();
147 if (fired && !dispatchForOwnContextAlready) {
148 // Check if we dispatched to ctx so if we didnt at the end we can do it manually.
149 dispatchForOwnContextAlready = ctx.equals(ctxDispatcher.ctx);
150 }
151 }
152 }
153 if (!dispatchForOwnContextAlready) {
154 ctx.fireChannelReadComplete();
155 }
156 }
157
158 /**
159 * Init the {@link Channel} and add all the needed {@link io.netty.channel.ChannelHandler} to the pipeline.
160 * This also included building the {@code QUIC} codec via {@link QuicCodecBuilder} sub-type using the given local
161 * connection id length and {@link QuicConnectionIdGenerator}.
162 *
163 * @param channel the {@link Channel} to init.
164 * @param localConnectionIdLength the local connection id length that must be used with the
165 * {@link QuicCodecBuilder}.
166 * @param idGenerator the {@link QuicConnectionIdGenerator} that must be used with the
167 * {@link QuicCodecBuilder}.
168 * @throws Exception thrown on error.
169 */
170 protected abstract void initChannel(Channel channel, int localConnectionIdLength,
171 QuicConnectionIdGenerator idGenerator) throws Exception;
172
173 /**
174 * Return the idx that was encoded into the connectionId via the {@link QuicConnectionIdGenerator} before,
175 * or {@code -1} if decoding was not successful.
176 * <p>
177 * Subclasses may override this. In this case {@link #newIdGenerator(int)} should be overridden as well
178 * to implement the encoding scheme for the encoding side.
179 *
180 *
181 * @param connectionId the destination connection id of the {@code QUIC} connection.
182 * @return the index or -1.
183 */
184 protected int decodeIndex(ByteBuf connectionId) {
185 return decodeIdx(connectionId);
186 }
187
188 /**
189 * Return the destination connection id or {@code null} if decoding was not possible.
190 *
191 * @param buffer the buffer
192 * @return the id or {@code null}.
193 */
194 // Package-private for testing
195 @Nullable
196 static ByteBuf getDestinationConnectionId(ByteBuf buffer, int localConnectionIdLength) throws QuicException {
197 if (buffer.readableBytes() > Byte.BYTES) {
198 int offset = buffer.readerIndex();
199 boolean shortHeader = hasShortHeader(buffer);
200 offset += Byte.BYTES;
201 // We are only interested in packets with short header as these the packets that
202 // are exchanged after the server did provide the connection id that the client should use.
203 if (shortHeader) {
204 // See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.3
205 // 1-RTT Packet {
206 // Header Form (1) = 0,
207 // Fixed Bit (1) = 1,
208 // Spin Bit (1),
209 // Reserved Bits (2),
210 // Key Phase (1),
211 // Packet Number Length (2),
212 // Destination Connection ID (0..160),
213 // Packet Number (8..32),
214 // Packet Payload (8..),
215 //}
216 return QuicHeaderParser.sliceCid(buffer, offset, localConnectionIdLength);
217 }
218 }
219 return null;
220 }
221
222 // Package-private for testing
223 static boolean hasShortHeader(ByteBuf buffer) {
224 return QuicHeaderParser.hasShortHeader(buffer.getByte(buffer.readerIndex()));
225 }
226
227 // Package-private for testing
228 static int decodeIdx(ByteBuf connectionId) {
229 if (connectionId.readableBytes() >= 2) {
230 return connectionId.getUnsignedShort(connectionId.readerIndex());
231 }
232 return -1;
233 }
234
235 // Package-private for testing
236 static ByteBuffer encodeIdx(ByteBuffer buffer, int idx) {
237 // Allocate a new buffer and prepend it with the index.
238 ByteBuffer b = ByteBuffer.allocate(buffer.capacity() + Short.BYTES);
239 // We encode it as unsigned short.
240 b.putShort((short) idx).put(buffer).flip();
241 return b;
242 }
243
244 /**
245 * Returns a {@link QuicConnectionIdGenerator} that will encode the given index into all the
246 * ids that it produces.
247 * <p>
248 * Subclasses may override this. In this case {@link #decodeIndex(ByteBuf)} should be overridden as well
249 * to implement the encoding scheme for the decoding side.
250 *
251 * @param idx the index to encode into each id.
252 * @return the {@link QuicConnectionIdGenerator}.
253 */
254 protected QuicConnectionIdGenerator newIdGenerator(int idx) {
255 return new IndexAwareQuicConnectionIdGenerator(idx, SecureRandomQuicConnectionIdGenerator.INSTANCE);
256 }
257
258 private static final class IndexAwareQuicConnectionIdGenerator implements QuicConnectionIdGenerator {
259 private final int idx;
260 private final QuicConnectionIdGenerator idGenerator;
261
262 IndexAwareQuicConnectionIdGenerator(int idx, QuicConnectionIdGenerator idGenerator) {
263 this.idx = idx;
264 this.idGenerator = idGenerator;
265 }
266
267 @Override
268 public ByteBuffer newId(int length) {
269 if (length > Short.BYTES) {
270 return encodeIdx(idGenerator.newId(length - Short.BYTES), idx);
271 }
272 return idGenerator.newId(length);
273 }
274
275 @Override
276 public ByteBuffer newId(ByteBuffer input, int length) {
277 if (length > Short.BYTES) {
278 return encodeIdx(idGenerator.newId(input, length - Short.BYTES), idx);
279 }
280 return idGenerator.newId(input, length);
281 }
282
283 @Override
284 public ByteBuffer newId(ByteBuffer scid, ByteBuffer dcid, int length) {
285 if (length > Short.BYTES) {
286 return encodeIdx(idGenerator.newId(scid, dcid, length - Short.BYTES), idx);
287 }
288 return idGenerator.newId(scid, dcid, length);
289 }
290
291 @Override
292 public int maxConnectionIdLength() {
293 return idGenerator.maxConnectionIdLength();
294 }
295
296 @Override
297 public boolean isIdempotent() {
298 // Return false as the id might be different because of the idx that is encoded into it.
299 return false;
300 }
301 }
302
303 private static final class ChannelHandlerContextDispatcher extends AtomicBoolean {
304
305 private final ChannelHandlerContext ctx;
306
307 ChannelHandlerContextDispatcher(ChannelHandlerContext ctx) {
308 this.ctx = ctx;
309 }
310
311 void fireChannelRead(Object msg) {
312 ctx.fireChannelRead(msg);
313 set(true);
314 }
315
316 boolean fireChannelReadCompleteIfNeeded() {
317 if (getAndSet(false)) {
318 // There was a fireChannelRead() before, let's call fireChannelReadComplete()
319 // so the user is aware that we might be done with the reading loop.
320 ctx.fireChannelReadComplete();
321 return true;
322 }
323 return false;
324 }
325 }
326 }