diff --git a/codec-quic/pom.xml b/codec-quic/pom.xml new file mode 100644 index 000000000000..d29a1c725113 --- /dev/null +++ b/codec-quic/pom.xml @@ -0,0 +1,58 @@ + + + + 4.0.0 + + io.netty + netty-parent + 5.0.0.Final-SNAPSHOT + + + netty-codec-quic + jar + + Netty/Codec/Quic + + + io.netty.codec.quic + + + + + ${project.groupId} + netty-common + ${project.version} + + + ${project.groupId} + netty-buffer + ${project.version} + + + ${project.groupId} + netty-transport + ${project.version} + + + ${project.groupId} + netty-codec + ${project.version} + + + ${project.groupId} + netty-handler + ${project.version} + + + com.jcraft + jzlib + true + + + org.mockito + mockito-core + + + \ No newline at end of file diff --git a/codec-quic/src/main/java/io/netty/handler/codec/quic/QuicMessage.java b/codec-quic/src/main/java/io/netty/handler/codec/quic/QuicMessage.java new file mode 100644 index 000000000000..a6f3a0b1991d --- /dev/null +++ b/codec-quic/src/main/java/io/netty/handler/codec/quic/QuicMessage.java @@ -0,0 +1,17 @@ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; + +public class QuicMessage { + ByteBuf byteBuf; + + QuicMessage() {} + + public QuicMessage(ByteBuf byteBuf) { + this.byteBuf = byteBuf; + } + + public ByteBuf getByteBuf() { + return byteBuf; + } +} diff --git a/codec-quic/src/main/java/io/netty/handler/codec/quic/QuicRequest.java b/codec-quic/src/main/java/io/netty/handler/codec/quic/QuicRequest.java new file mode 100644 index 000000000000..8d08c9b3de0f --- /dev/null +++ b/codec-quic/src/main/java/io/netty/handler/codec/quic/QuicRequest.java @@ -0,0 +1,87 @@ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.InetSocketAddress; + +public class QuicRequest extends QuicMessage { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(QuicRequest.class); + + private final InetSocketAddress inetSocketAddress; + private final int headerForm; + private final int fixedBit; + private final int longPacketType; + private final int typeSpecificBits; + private final int version; + private final byte[] dcid; + private final byte[] scid; + private final int tokenLength; + private final byte[] packetNumber; + + public QuicRequest(InetSocketAddress inetSocketAddress, int headerForm, int fixedBit, + int longPacketType, int typeSpecificBits, int version, byte[] dcid, byte[] scid, int tokenLength, + byte[] packetNumber) { + this.inetSocketAddress = inetSocketAddress; + this.headerForm = headerForm; + this.fixedBit = fixedBit; + this.longPacketType = longPacketType; + this.typeSpecificBits = typeSpecificBits; + this.version = version; + this.dcid = dcid.clone(); + this.scid = scid.clone(); + this.tokenLength = tokenLength; + this.packetNumber = packetNumber.clone(); + } + + public InetSocketAddress getInetSocketAddress() { + return inetSocketAddress; + } + + @Override + public ByteBuf getByteBuf() { + byte header = (byte) (((headerForm & 0x01) << 7) + ((fixedBit & 0x01) << 6) + ((longPacketType & 0x03) << 5) + (typeSpecificBits & 0x0f)); + System.out.println(header); + return Unpooled.buffer() + .writeByte(header) + .writeInt(version) + .writeByte(dcid.length - 1) + .writeBytes(dcid) + .writeByte(scid.length - 1) + .writeBytes(scid) + .writeByte(tokenLength) + .writeBytes(variableLengthIntegerEncoding(packetNumber.length)) + .writeBytes(packetNumber); + } + + static byte[] variableLengthIntegerEncoding(long length) { + if (length < 64) { + return new byte[] { (byte) (length & 0xff) }; + } else if (length < 16384) { + return new byte[] { (byte) (((length & 0xff00) >> 8) + 0x40), (byte) (length & 0xff) }; + } else if (length < 1073741823) { + return new byte[] { + (byte) (((length & 0xff000000) >> 24) + 0x80), + (byte) ((length & 0xff0000) >> 16), + (byte) ((length & 0xff00) >> 8), + (byte) (length & 0xff), + }; + } else if (length < 4611686018427387904L) { + return new byte[] { + (byte) (((length & 0xff00000000000000L) >> 56) + 0xc0), + (byte) ((length & 0xff000000000000L) >> 48), + (byte) ((length & 0xff0000000000L) >> 40), + (byte) ((length & 0xff00000000L) >> 32), + (byte) ((length & 0xff000000) >> 24), + (byte) ((length & 0xff0000) >> 16), + (byte) ((length & 0xff00) >> 8), + (byte) (length & 0xff), + }; + } else { + throw new IllegalArgumentException("invalid length: " + length); + } + } +} diff --git a/codec-quic/src/main/java/io/netty/handler/codec/quic/QuicRequestEncoder.java b/codec-quic/src/main/java/io/netty/handler/codec/quic/QuicRequestEncoder.java new file mode 100644 index 000000000000..c811ae66b3e9 --- /dev/null +++ b/codec-quic/src/main/java/io/netty/handler/codec/quic/QuicRequestEncoder.java @@ -0,0 +1,14 @@ +package io.netty.handler.codec.quic; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramPacket; +import io.netty.handler.codec.MessageToMessageEncoder; + +import java.util.List; + +public class QuicRequestEncoder extends MessageToMessageEncoder { + @Override + protected void encode(ChannelHandlerContext ctx, QuicRequest msg, List out) throws Exception { + out.add(new DatagramPacket(msg.getByteBuf(), msg.getInetSocketAddress())); + } +} diff --git a/codec-quic/src/main/java/io/netty/handler/codec/quic/QuicResponseDecoder.java b/codec-quic/src/main/java/io/netty/handler/codec/quic/QuicResponseDecoder.java new file mode 100644 index 000000000000..e7d5d522d5e7 --- /dev/null +++ b/codec-quic/src/main/java/io/netty/handler/codec/quic/QuicResponseDecoder.java @@ -0,0 +1,119 @@ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramPacket; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.util.List; + +public class QuicResponseDecoder extends MessageToMessageDecoder { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(QuicResponseDecoder.class); + + private enum State { + INITIAL, + HEADER, + DCID_LEN, + DCID, + SCID_LEN, + SCID, + } + + private State state = State.INITIAL; + private int dcidLen; + private int scidLen; + private int version; + + @Override + protected void decode(ChannelHandlerContext ctx, DatagramPacket msg, List out) throws Exception { + final ByteBuf byteBuf = msg.content(); + + switch (state) { + case INITIAL: + if (!byteBuf.isReadable()) { + return; + } + final byte headerByte = byteBuf.readByte(); + final boolean headerForm = headerForm(headerByte); + + // only process long header packets for now... + if (!headerForm) { + logger.info("headerForm: {}", headerForm); + return; + } + + final boolean fixedBit = fixedBit(headerByte); + final short longPacketType = longPacketType(headerByte); + final int typeSpecificBits = typeSpecificBits(headerByte); + logger.info("headerForm: {}, fixedBit: {}, longPacketType: {}, typeSpecificBits: {}", + headerForm, fixedBit, longPacketType, typeSpecificBits); + state = State.HEADER; + case HEADER: + if (!byteBuf.isReadable(4)) { + return; + } + final ByteBuf versionByteBuf = byteBuf.readBytes(4); + version = versionByteBuf.readInt(); + logger.info("version: {}", version); + state = State.DCID_LEN; + case DCID_LEN: + if (!byteBuf.isReadable()) { + return; + } + dcidLen = byteBuf.readByte(); + logger.info("dcidLen: {}", dcidLen); + state = State.DCID; + case DCID: + if (!byteBuf.isReadable()) { + return; + } + final ByteBuf dcid = byteBuf.readBytes(dcidLen); + logger.info("dcid: {}", dcid); + state = State.SCID_LEN; + case SCID_LEN: + if (!byteBuf.isReadable()) { + return; + } + scidLen = byteBuf.readByte(); + logger.info("scidLen: {}", scidLen); + state = State.SCID; + case SCID: + if (!byteBuf.isReadable()) { + return; + } + final ByteBuf scid = byteBuf.readBytes(scidLen); + logger.info("scid: {}", scid); + } + + // it will be identified as a Version Negotiation packet based on the Version field having a value of 0 + if (version == 0) { + while (byteBuf.readableBytes() > 0) { + ByteBuf versionBytBuf = byteBuf.readBytes(4); + logger.info("supported version hexdump: {}", ByteBufUtil.hexDump(versionBytBuf)); + } + } + + out.add(new QuicMessage(ReferenceCountUtil.retain(byteBuf))); + } + + static int typeSpecificBits(byte b) { + return b & 0x0f; + } + + static short longPacketType(byte b) { + return (short) ((b & 0x30) >> 4); + } + + static boolean fixedBit(byte b) { + return (b & 0x40) >> 6 == 1; + } + + static boolean headerForm(byte b) { + return (b & 0x80) >> 7 == 1; + } +} diff --git a/codec-quic/src/test/java/io/netty/handler/codec/quic/BasicQuicTest.java b/codec-quic/src/test/java/io/netty/handler/codec/quic/BasicQuicTest.java new file mode 100644 index 000000000000..9c3436a8baa2 --- /dev/null +++ b/codec-quic/src/test/java/io/netty/handler/codec/quic/BasicQuicTest.java @@ -0,0 +1,140 @@ +package io.netty.handler.codec.quic; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.MultithreadEventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioHandler; +import io.netty.channel.socket.DatagramPacket; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.logging.LoggingHandler; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static org.junit.Assert.*; + + +public class BasicQuicTest { + + private static final String ECHO_MESSAGE = "hai:)"; + private static final String CHROMIUM_ENDPOINT = "https://quic.rocks:4433/"; + + + @Test + public void testSimpleEcho() throws Throwable { + + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference result = new AtomicReference<>(); + + final Channel serverChannel = setupServer(retval -> { + result.set(retval); + latch.countDown(); + }); + + final InetSocketAddress recipient = new InetSocketAddress(20080); + final Channel clientChannel = getClient(recipient, ignored -> {}); + clientChannel.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer( + ECHO_MESSAGE.getBytes()), recipient)).sync(); + + latch.await(); + + assertEquals(ECHO_MESSAGE, result.get()); + + serverChannel.closeFuture().await(); + clientChannel.closeFuture().await(); + } + + @Test + public void trySendInitialPacket() throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference result = new AtomicReference<>(); + + final InetSocketAddress remote = new InetSocketAddress("quic.tech", 4433); + final Channel client = getClient(remote, res -> { + result.set(Unpooled.copiedBuffer(res)); + latch.countDown(); + }); + client.writeAndFlush(new QuicRequest(remote, 1, 1, 0, 0, + 1, new byte[] {0}, new byte[] {0}, 0, new byte[] {0})).sync(); + + latch.await(); + assertNotNull(result.get()); + + client.close().await(); + } + + @Test + public void initialPacket() { + final byte[] bytes = ByteBufUtil.decodeHexDump("07ff706cb107568ef7116f5f58a9ed9010"); + System.out.println(ByteBufUtil.prettyHexDump(Unpooled.copiedBuffer(bytes))); + } + + private static Channel setupServer(Consumer validator) throws Exception { + final MultithreadEventLoopGroup group = new MultithreadEventLoopGroup(NioHandler.newFactory()); + final Bootstrap b = new Bootstrap(); + b.group(group).channel(NioDatagramChannel.class) + .option(ChannelOption.SO_BROADCAST, true) + .handler(new ChannelInitializer() { + @Override + public void initChannel(final NioDatagramChannel ch) throws Exception { + + final ChannelPipeline p = ch.pipeline(); + p.addLast(new LoggingHandler()); + p.addLast(new SimpleChannelInboundHandler() { + @Override + protected void messageReceived(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception { + final ByteBuf buf = msg.content(); + final int rcvPktLength = buf.readableBytes(); + final byte[] rcvPktBuf = new byte[rcvPktLength]; + buf.readBytes(rcvPktBuf); + validator.accept(new String(rcvPktBuf)); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + System.out.println(cause); + } + }); + } + }); + return b.bind(new InetSocketAddress(20080)).sync().await().channel(); + } + + public static Channel getClient(InetSocketAddress address, Consumer validator) throws Exception { + final Bootstrap bootstrap = new Bootstrap(); + final MultithreadEventLoopGroup workerGroup = new MultithreadEventLoopGroup(NioHandler.newFactory()); + bootstrap.group(workerGroup) + .channel(NioDatagramChannel.class) + .option(ChannelOption.SO_BROADCAST, true) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(NioDatagramChannel ch)throws Exception { + final ChannelPipeline p = ch.pipeline(); + p.addLast(new LoggingHandler()); + p.addLast(new QuicResponseDecoder()); + p.addLast(new QuicRequestEncoder()); + p.addLast(new SimpleChannelInboundHandler() { + @Override + protected void messageReceived(ChannelHandlerContext ctx, QuicMessage msg) throws Exception { + final ByteBuf buf = msg.getByteBuf(); + validator.accept(buf); + } + }); + } + }); + final ChannelFuture channelFuture = bootstrap.connect(address).sync(); + return channelFuture.channel(); + } +} diff --git a/codec-quic/src/test/java/io/netty/handler/codec/quic/QuicRequestTest.java b/codec-quic/src/test/java/io/netty/handler/codec/quic/QuicRequestTest.java new file mode 100644 index 000000000000..18c6c6ff15c8 --- /dev/null +++ b/codec-quic/src/test/java/io/netty/handler/codec/quic/QuicRequestTest.java @@ -0,0 +1,37 @@ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBufUtil; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class QuicRequestTest { + + @Test + public void test1byteEncoding() { + final long length = 37; + final byte[] retval = QuicRequest.variableLengthIntegerEncoding(length); + assertEquals("25", ByteBufUtil.hexDump(retval)); + } + + @Test + public void test2byteEncoding() { + final long length = 15293; + final byte[] retval = QuicRequest.variableLengthIntegerEncoding(length); + assertEquals("7bbd", ByteBufUtil.hexDump(retval)); + } + + @Test + public void test4byteEncoding() { + final long length = 494878333; + final byte[] retval = QuicRequest.variableLengthIntegerEncoding(length); + assertEquals("9d7f3e7d", ByteBufUtil.hexDump(retval)); + } + + @Test + public void test8byteEncoding() { + final long length = 151288809941952652L; + final byte[] retval = QuicRequest.variableLengthIntegerEncoding(length); + assertEquals("c2197c5eff14e88c", ByteBufUtil.hexDump(retval)); + } +} diff --git a/codec-quic/src/test/java/io/netty/handler/codec/quic/QuicResponseDecoderTest.java b/codec-quic/src/test/java/io/netty/handler/codec/quic/QuicResponseDecoderTest.java new file mode 100644 index 000000000000..6f167bbb85ee --- /dev/null +++ b/codec-quic/src/test/java/io/netty/handler/codec/quic/QuicResponseDecoderTest.java @@ -0,0 +1,28 @@ +package io.netty.handler.codec.quic; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class QuicResponseDecoderTest { + + @Test + public void testHeaderForm() { + assertTrue(QuicResponseDecoder.headerForm((byte) 0b10000000)); + assertFalse(QuicResponseDecoder.headerForm((byte) 0b00000000)); + } + + @Test + public void testFixedBit() { + assertTrue(QuicResponseDecoder.fixedBit((byte) 0b01000000)); + assertFalse(QuicResponseDecoder.fixedBit((byte) 0b00000000)); + } + + @Test + public void testLongPacketType() { + assertEquals(0, QuicResponseDecoder.longPacketType((byte) 0b00000000)); + assertEquals(1, QuicResponseDecoder.longPacketType((byte) 0b00010000)); + assertEquals(2, QuicResponseDecoder.longPacketType((byte) 0b00100000)); + assertEquals(3, QuicResponseDecoder.longPacketType((byte) 0b00110000)); + } +} diff --git a/pom.xml b/pom.xml index 1f763d5e6ee2..c4e5a42ab65e 100644 --- a/pom.xml +++ b/pom.xml @@ -372,6 +372,7 @@ transport-blockhound-tests microbench bom + codec-quic