View Javadoc
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 }