/* * Copyright © 2019, 2020, 2021, 2022, 2023 Peter Doornbosch * * This file is part of Kwik, an implementation of the QUIC protocol in Java. * * Kwik is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your option) * any later version. * * Kwik is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package net.luminis.quic; import net.luminis.quic.cc.FixedWindowCongestionController; import net.luminis.quic.cid.ConnectionIdInfo; import net.luminis.quic.cid.ConnectionIdStatus; import net.luminis.quic.frame.*; import net.luminis.quic.log.Logger; import net.luminis.quic.log.SysOutLogger; import net.luminis.quic.packet.*; import net.luminis.quic.send.SenderImpl; import net.luminis.tls.handshake.TlsClientEngine; import net.luminis.tls.util.ByteUtils; import net.luminis.quic.test.FieldSetter; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import net.luminis.quic.test.FieldReader; import java.net.URI; import java.nio.ByteBuffer; import java.time.Instant; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.Consumer; import static net.luminis.quic.QuicConstants.TransportErrorCode.TRANSPORT_PARAMETER_ERROR; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; class QuicClientConnectionImplTest { private static Logger logger; private final byte[] destinationConnectionId = { 0x00, 0x01, 0x02, 0x03 }; private QuicClientConnectionImpl connection; private byte[] originalDestinationId; private SenderImpl sender; @BeforeAll static void initLogger() { logger = new SysOutLogger(); // logger.logDebug(true); } @BeforeEach void initConnectionUnderTest() throws Exception { connection = QuicClientConnectionImpl.newBuilder() .connectionIdLength(4) .uri(new URI("//localhost:443")) .logger(logger).build(); sender = Mockito.mock(SenderImpl.class); var connectionIdManager = new FieldReader(connection, connection.getClass().getDeclaredField("connectionIdManager")).read(); FieldSetter.setField(connectionIdManager, "sender", sender); } @Test void connectRequiresAlpn() { assertThatThrownBy(() -> connection.connect(1000, null) ).isInstanceOf(NullPointerException.class); } @Test void connectRequiresNonEmptyAlpn() { assertThatThrownBy(() -> connection.connect(1000, " ") ).isInstanceOf(IllegalArgumentException.class); } @Test void testRetryPacketInitiatesInitialPacketWithToken() throws Exception { simulateSuccessfulConnect(); byte[] originalConnectionId = { 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 }; // By using a fixed value for the original destination connection, the integrity tag will also have a fixed value, which simplifies the test setFixedOriginalDestinationConnectionId(originalConnectionId); // First InitialPacket should not contain a token. verify(sender, never()).setInitialToken(any(byte[].class)); // Simulate a RetryPacket is received RetryPacket retryPacket = createRetryPacket(originalConnectionId, "9442e0ac29f6d650adc5e4b4a3cd12cc"); connection.process(retryPacket, null); // A second InitialPacket should be send with token verify(sender).setInitialToken( argThat(token -> token != null && Arrays.equals(token, new byte[] { 0x01, 0x02, 0x03 }))); } private void setFixedOriginalDestinationConnectionId(byte[] originalConnectionId) throws Exception { var connectionIdManager = new FieldReader(connection, connection.getClass().getDeclaredField("connectionIdManager")).read(); FieldSetter.setField(connectionIdManager, connectionIdManager.getClass().getDeclaredField("originalDestinationConnectionId"), originalConnectionId); } @Test void testSecondRetryPacketShouldBeIgnored() throws Exception { simulateSuccessfulConnect(); byte[] originalConnectionId = { 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 }; // By using a fixed value for the original destination connection, the integrity tag will also have a fixed value, which simplifies the test setFixedOriginalDestinationConnectionId(originalConnectionId); // Simulate a first RetryPacket is received RetryPacket retryPacket = createRetryPacket(connection.getDestinationConnectionId(), "5e5f918434a24d4b601745b4f0db7908"); connection.process(retryPacket, null); clearInvocations(sender); // Simulate a second RetryPacket is received RetryPacket secondRetryPacket = createRetryPacket(connection.getDestinationConnectionId(), "00f4bbc72790b7c7947f86ec9fb0a68d"); connection.process(secondRetryPacket, null); verify(sender, never()).send(any(QuicFrame.class), any(EncryptionLevel.class), any(Consumer.class)); } private RetryPacket createRetryPacket(byte[] originalDestinationConnectionId, String integrityTagValue) throws Exception { byte[] sourceConnectionId = { 0x0b, 0x0b, 0x0b, 0x0b }; byte[] destinationConnectionId = { 0x0f, 0x0f, 0x0f, 0x0f }; byte[] retryToken = { 0x01, 0x02, 0x03 }; RetryPacket retryPacket = new RetryPacket(Version.getDefault(), sourceConnectionId, destinationConnectionId, originalDestinationConnectionId, retryToken); FieldSetter.setField(retryPacket, RetryPacket.class.getDeclaredField("retryIntegrityTag"), ByteUtils.hexToBytes(integrityTagValue)); return retryPacket; } @Test void testRetryPacketWithIncorrectOriginalDestinationIdShouldBeDiscarded() throws Exception { simulateSuccessfulConnect(); // Simulate a RetryPacket with arbitrary original destination id is received RetryPacket retryPacket = createRetryPacket(new byte[] { 0x03, 0x0a, 0x0d, 0x09 }, "00112233445566778899aabbccddeeff"); connection.process(retryPacket, null); verify(sender, never()).send(any(QuicFrame.class), any(EncryptionLevel.class), any(Consumer.class)); } @Test void testAfterRetryPacketTransportParametersWithoutOriginalDestinationIdLeadsToConnectionError() throws Exception { simulateConnectionReceivingRetryPacket(); connection = Mockito.spy(connection); // Simulate a TransportParametersExtension is received that does not contain the right original destination id connection.setPeerTransportParameters(new TransportParameters()); ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Long.class); verify(connection).immediateCloseWithError(argThat(l -> l == EncryptionLevel.Handshake), errorCaptor.capture(), any(), any()); assertThat(errorCaptor.getValue()).isEqualTo(TRANSPORT_PARAMETER_ERROR.value); } @Test void testAfterRetryPacketTransportParametersWithIncorrectOriginalDestinationIdLeadsToConnectionError() throws Exception { RetryPacket retryPacket = simulateConnectionReceivingRetryPacket(); connection = Mockito.spy(connection); // Simulate a TransportParametersExtension is received that... TransportParameters transportParameters = new TransportParameters(); // - has the server's source cid (because the test stops after "sending" the retry-packet, this is not the "final" server source cid, but the one used in the retry packet) transportParameters.setInitialSourceConnectionId(retryPacket.getSourceConnectionId()); // - does contain the original destination id transportParameters.setOriginalDestinationConnectionId(originalDestinationId); // - does contain an original destination id (but incorrect) transportParameters.setRetrySourceConnectionId(new byte[] { 0x0d, 0x0d, 0x0d, 0x0d }); connection.setPeerTransportParameters(transportParameters); ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Long.class); verify(connection).immediateCloseWithError(argThat(l -> l == EncryptionLevel.Handshake), errorCaptor.capture(), any(), any()); assertThat(errorCaptor.getValue()).isEqualTo(TRANSPORT_PARAMETER_ERROR.value); } @Test void testAfterRetryPacketTransportParametersWithCorrectRetrySourceConnectionId() throws Exception { RetryPacket retryPacket = simulateConnectionReceivingRetryPacket(); connection = Mockito.spy(connection); // Simulate a TransportParametersExtension is received that... TransportParameters transportParameters = new TransportParameters(); // - has the server's source cid (because the test stops after "sending" the retry-packet, this is not the "final" server source cid, but the one used in the retry packet) transportParameters.setInitialSourceConnectionId(retryPacket.getSourceConnectionId()); // - does contain the original destination id transportParameters.setOriginalDestinationConnectionId(originalDestinationId); // - sets the retry cid to the source cid of the retry packet transportParameters.setRetrySourceConnectionId(retryPacket.getSourceConnectionId()); connection.setPeerTransportParameters(transportParameters); verify(connection, never()).immediateCloseWithError(any(EncryptionLevel.class), anyInt(), anyString()); } @Test void testWithNormalConnectionTransportParametersShouldNotContainRetrySourceId() throws Exception { byte[] originalSourceConnectionId = new byte[] { 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 }; setFixedOriginalDestinationConnectionId(originalSourceConnectionId); simulateSuccessfulConnect(); connection = spy(connection); // Simulate a TransportParametersExtension is received that does not contain a retry source id TransportParameters transportParameters = new TransportParameters(); // But it must contain transportParameters.setInitialSourceConnectionId(connection.getDestinationConnectionId()); transportParameters.setOriginalDestinationConnectionId(originalSourceConnectionId); connection.setPeerTransportParameters(transportParameters); verify(connection, never()).immediateCloseWithError(any(EncryptionLevel.class), anyInt(), anyString()); } @Test void testOnNormalConnectionTransportParametersWithOriginalDestinationIdLeadsToConnectionError() throws Exception { simulateSuccessfulConnect(); connection = spy(connection); // Simulate a TransportParametersExtension is received that does contain an original destination id TransportParameters transportParameters = new TransportParameters(); transportParameters.setRetrySourceConnectionId(new byte[] { 0x0d, 0x0d, 0x0d, 0x0d }); connection.setPeerTransportParameters(transportParameters); ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Long.class); verify(connection).immediateCloseWithError(argThat(l -> l == EncryptionLevel.Handshake), errorCaptor.capture(), any(), any()); assertThat(errorCaptor.getValue()).isEqualTo(TRANSPORT_PARAMETER_ERROR.value); } private RetryPacket simulateConnectionReceivingRetryPacket() throws Exception { simulateSuccessfulConnect(); originalDestinationId = new byte[]{ 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 }; // By using a fixed value for the original destination connection, the integrity tag will also have a fixed value, which simplifies the test setFixedOriginalDestinationConnectionId(originalDestinationId); // Simulate a RetryPacket is received RetryPacket retryPacket = createRetryPacket(connection.getDestinationConnectionId(), "9442e0ac29f6d650adc5e4b4a3cd12cc"); connection.process(retryPacket, null); return retryPacket; } private void simulateSuccessfulConnect() throws NoSuchFieldException { FieldSetter.setField(connection, connection.getClass().getDeclaredField("sender"), sender); when(sender.getCongestionController()).thenReturn(new FixedWindowCongestionController(logger)); FieldSetter.setField(connection, "tlsEngine", mock(TlsClientEngine.class)); } @Test void testCreateStream() throws Exception { TransportParameters parameters = new TransportParameters(10, 10, 10, 10); parameters.setInitialSourceConnectionId(connection.getDestinationConnectionId()); parameters.setOriginalDestinationConnectionId(connection.getDestinationConnectionId()); connection.setPeerTransportParameters(parameters); QuicStream stream = connection.createStream(true); int firstStreamId = stream.getStreamId(); int streamIdLowBits = firstStreamId & 0x03; assertThat(streamIdLowBits).isEqualTo(0x00); QuicStream stream2 = connection.createStream(true); assertThat(stream2.getStreamId()).isEqualTo(firstStreamId + 4); } @Test void testConnectionFlowControl() throws Exception { FieldSetter.setField(connection, connection.getClass().getDeclaredField("sender"), sender); long flowControlIncrement = (long) new FieldReader(connection, connection.getClass().getSuperclass().getDeclaredField("flowControlIncrement")).read(); connection.updateConnectionFlowControl(10); verify(sender, never()).send(any(QuicFrame.class), any(EncryptionLevel.class), any(Consumer.class)); // No initial update, value is advertised in transport parameters. connection.updateConnectionFlowControl((int) flowControlIncrement); verify(sender, times(1)).send(any(QuicFrame.class), any(EncryptionLevel.class), any(Consumer.class)); connection.updateConnectionFlowControl((int) (flowControlIncrement * 0.8)); verify(sender, times(1)).send(any(QuicFrame.class), any(EncryptionLevel.class), any(Consumer.class)); connection.updateConnectionFlowControl((int) (flowControlIncrement * 0.21)); verify(sender, times(2)).send(any(QuicFrame.class), any(EncryptionLevel.class) , any(Consumer.class)); } @Test void testMinimumQuicVersionIs23() { assertThatThrownBy( () -> QuicClientConnectionImpl.newBuilder() .version(Version.IETF_draft_19) .uri(new URI("//localhost:443")) .logger(logger).build()) .isInstanceOf(IllegalArgumentException.class); } @Test void testQuicVersion29IsSupported() throws Exception { assertThat(QuicClientConnectionImpl.newBuilder() .version(Version.IETF_draft_29) .connectionIdLength(4) .uri(new URI("//localhost:443")) .logger(logger).build()) .isNotNull(); } @Test void parsingValidVersionNegotiationPacketShouldSucceed() throws Exception { QuicPacket packet = connection.parsePacket(ByteBuffer.wrap(ByteUtils.hexToBytes("ff00000000040a0b0c0d040f0e0d0cff000018"))); assertThat(packet).isInstanceOf(VersionNegotiationPacket.class); } @Test void receivingTransportParametersInitializesFlowController() { TransportParameters parameters = new TransportParameters(30, 9000, 1, 1); parameters.setInitialSourceConnectionId(connection.getDestinationConnectionId()); parameters.setOriginalDestinationConnectionId(connection.getDestinationConnectionId()); connection.setPeerTransportParameters(parameters); QuicStream stream = connection.createStream(true); assertThat(connection.getFlowController().increaseFlowControlLimit(stream, 9999)).isEqualTo(9000); } @Test void receivingMaxStreamDataFrameIncreasesFlowControlLimit() { TransportParameters parameters = new TransportParameters(10, 0, 3, 3); parameters.setInitialSourceConnectionId(connection.getDestinationConnectionId()); parameters.setOriginalDestinationConnectionId(connection.getDestinationConnectionId()); parameters.setInitialMaxData(100_000); parameters.setInitialMaxStreamDataBidiRemote(9000); connection.setPeerTransportParameters(parameters); QuicStream stream = connection.createStream(true); assertThat(connection.getFlowController().increaseFlowControlLimit(stream, 9999)).isEqualTo(9000); connection.processFrames( new ShortHeaderPacket(Version.getDefault(), destinationConnectionId, new MaxStreamDataFrame(stream.getStreamId(), 10_000)), Instant.now()); assertThat(connection.getFlowController().increaseFlowControlLimit(stream, 99999)).isEqualTo(10_000); } @Test void receivingMaxDataFrameIncreasesFlowControlLimit() { TransportParameters parameters = new TransportParameters(10, 0, 3, 3); parameters.setInitialSourceConnectionId(connection.getDestinationConnectionId()); parameters.setOriginalDestinationConnectionId(connection.getDestinationConnectionId()); parameters.setInitialMaxData(1_000); parameters.setInitialMaxStreamDataBidiRemote(9000); connection.setPeerTransportParameters(parameters); QuicStream stream = connection.createStream(true); assertThat(connection.getFlowController().increaseFlowControlLimit(stream, 9999)).isEqualTo(1000); connection.processFrames( new ShortHeaderPacket(Version.getDefault(), destinationConnectionId, new MaxDataFrame(4_000)), Instant.now()); assertThat(connection.getFlowController().increaseFlowControlLimit(stream, 99999)).isEqualTo(4_000); } @Test void receivingConnectionCloseWhileConnectedResultsInReplyWithConnectionClose() throws Exception { FieldSetter.setField(connection, connection.getClass().getDeclaredField("sender"), sender); FieldSetter.setField(connection, connection.getClass().getSuperclass().getDeclaredField("connectionState"), QuicClientConnectionImpl.Status.Connected); connection.processFrames( new ShortHeaderPacket(Version.getDefault(), destinationConnectionId, new ConnectionCloseFrame(Version.getDefault())), Instant.now()); verify(sender).send(argThat(frame -> frame instanceof ConnectionCloseFrame), any(EncryptionLevel.class), any(Consumer.class)); } @Test void receivingConnectionCloseWhileConnectedResultsInReplyWithConnectionCloseOnce() throws Exception { FieldSetter.setField(connection, connection.getClass().getDeclaredField("sender"), sender); FieldSetter.setField(connection, connection.getClass().getSuperclass().getDeclaredField("connectionState"), QuicClientConnectionImpl.Status.Connected); connection.processFrames( new ShortHeaderPacket(Version.getDefault(), destinationConnectionId, new ConnectionCloseFrame(Version.getDefault())), Instant.now()); connection.processFrames( new ShortHeaderPacket(Version.getDefault(), destinationConnectionId, new ConnectionCloseFrame(Version.getDefault())), Instant.now()); connection.processFrames( new ShortHeaderPacket(Version.getDefault(), destinationConnectionId, new ConnectionCloseFrame(Version.getDefault())), Instant.now()); verify(sender, times(1)).send(argThat(frame -> frame instanceof ConnectionCloseFrame), any(EncryptionLevel.class), any(Consumer.class)); } @Test void closingConnectedConnectionTriggersConnectionClose() throws Exception { FieldSetter.setField(connection, connection.getClass().getDeclaredField("sender"), sender); FieldSetter.setField(connection, connection.getClass().getSuperclass().getDeclaredField("connectionState"), QuicClientConnectionImpl.Status.Connected); connection.close(); verify(sender).send(argThat(frame -> frame instanceof ConnectionCloseFrame), any(EncryptionLevel.class)); } @Test void receivingRetireConnectionIdLeadsToNewSourceConnectionId() throws Exception { // Given setTransportParametersWithActiveConnectionIdLimit(3); connection.newConnectionIds(1, 0); assertThat(connection.getSourceConnectionIds()).hasSize(2); RetireConnectionIdFrame retireFrame = new RetireConnectionIdFrame(Version.getDefault(), 0); connection.processFrames(new ShortHeaderPacket(Version.getDefault(), connection.getSourceConnectionId(), retireFrame), Instant.now()); assertThat(connection.getSourceConnectionIds()).hasSize(2); verify(sender).send(argThat(frame -> frame instanceof NewConnectionIdFrame), any(EncryptionLevel.class), any(Consumer.class)); } @Test void receivingPacketWitYetUnusedConnectionIdLeadsToNewSourceConnectionId() throws Exception { // Given setTransportParametersWithActiveConnectionIdLimit(7); // When byte[] newUnusedConnectionId = connection.newConnectionIds(1, 0)[0]; assertThat(newUnusedConnectionId).isNotEqualTo(connection.getSourceConnectionId()); clearInvocations(sender); connection.process(new ShortHeaderPacket(Version.getDefault(), newUnusedConnectionId, new Padding(20)), Instant.now()); // Then assertThat(connection.getSourceConnectionIds().get(0).getConnectionIdStatus()).isEqualTo(ConnectionIdStatus.USED); verify(sender, times(1)).send(argThat(frame -> frame instanceof NewConnectionIdFrame), any(EncryptionLevel.class), any(Consumer.class)); } @Test void receivingPacketWitYetUnusedConnectionIdDoesNotLeadToNewSourceConnectionIdWhenActiveCidLimitReached() throws Exception { FieldSetter.setField(connection, connection.getClass().getDeclaredField("sender"), sender); TransportParameters params = new TransportParameters(); params.setActiveConnectionIdLimit(1); connection.setPeerTransportParameters(params); byte[][] newConnectionIds = connection.newConnectionIds(1, 0); byte[] nextConnectionId = newConnectionIds[0]; assertThat(nextConnectionId).isNotEqualTo(connection.getSourceConnectionId()); clearInvocations(sender); connection.process(new ShortHeaderPacket(Version.getDefault(), nextConnectionId, new Padding(20)), Instant.now()); verify(sender, never()).send(any(QuicFrame.class), any(EncryptionLevel.class), any(Consumer.class)); } @Test void receivingPacketWitPrevouslyUsedConnectionIdDoesNotLeadToNewSourceConnectionId() throws Exception { FieldSetter.setField(connection, connection.getClass().getDeclaredField("sender"), sender); TransportParameters params = new TransportParameters(); params.setActiveConnectionIdLimit(8); connection.setPeerTransportParameters(params); byte[] firstConnectionId = connection.getSourceConnectionId(); Map sourceConnectionIds = connection.getSourceConnectionIds(); byte[][] newConnectionIds = connection.newConnectionIds(1, 0); byte[] nextConnectionId = newConnectionIds[0]; assertThat(nextConnectionId).isNotEqualTo(connection.getSourceConnectionId()); connection.process(new ShortHeaderPacket(Version.getDefault(), nextConnectionId, new Padding(20)), Instant.now()); clearInvocations(sender); connection.process(new ShortHeaderPacket(Version.getDefault(), firstConnectionId, new Padding(20)), Instant.now()); verify(sender, never()).send(any(QuicFrame.class), any(EncryptionLevel.class), any(Consumer.class)); } /* // TODO: this test must move to sender (?), as connection does not create packets anymore @Test void afterProcessingNewConnectionIdFrameWithRetireTheNewConnectionIdIsUsed() throws Exception { FieldSetter.setField(connection, connection.getClass().getDeclaredField("sender"), sender); FieldSetter.setField(connection, connection.getClass().getDeclaredField("connectionState"), QuicConnectionImpl.Status.Connected); NewConnectionIdFrame newConnectionIdFrame = new NewConnectionIdFrame(Version.getDefault(), 1, 1, new byte[]{ 0x0c, 0x0f, 0x0d, 0x0e }); connection.process(new ShortHeaderPacket(Version.getDefault(), connection.getSourceConnectionId(), newConnectionIdFrame), Instant.now()); ArgumentCaptor captor = ArgumentCaptor.forClass(QuicPacket.class); verify(sender, times(1)).send(captor.capture(), anyString(), any(Consumer.class)); QuicPacket packetSent = captor.getValue(); assertThat(((ShortHeaderPacket) packetSent).getDestinationConnectionId()).isEqualTo(new byte[]{ 0x0c, 0x0f, 0x0d, 0x0e }); assertThat(packetSent.getFrames()).contains(new RetireConnectionIdFrame(Version.getDefault(), 0)); } */ @Test void retireConnectionIdFrameShouldBeRetransmittedWhenLost() throws Exception { // Given FieldSetter.setField(connection, connection.getClass().getSuperclass().getDeclaredField("connectionState"), QuicClientConnectionImpl.Status.Connected); connection.process(new NewConnectionIdFrame(Version.getDefault(), 1, 0, new byte[]{ 0x0c, 0x0f, 0x0d, 0x0e }), null, null); // When connection.retireDestinationConnectionId(0); ArgumentCaptor frameCaptor = ArgumentCaptor.forClass(QuicFrame.class); ArgumentCaptor captor = ArgumentCaptor.forClass(Consumer.class); verify(sender, times(1)).send(frameCaptor.capture(), any(EncryptionLevel.class), captor.capture()); clearInvocations(sender); Consumer lostPacketCallback = captor.getValue(); lostPacketCallback.accept(frameCaptor.getValue()); // Then ArgumentCaptor secondFrameCaptor = ArgumentCaptor.forClass(QuicFrame.class); verify(sender, times(1)).send(secondFrameCaptor.capture(), any(EncryptionLevel.class), any(Consumer.class)); QuicFrame retransmitPacket = secondFrameCaptor.getValue(); assertThat(retransmitPacket.equals(new RetireConnectionIdFrame(Version.getDefault(), 0))); } @Test void receivingReorderedNewConnectionIdWithSequenceNumberThatIsAlreadyRetiredShouldImmediatelySendRetire() throws Exception { // Given FieldSetter.setField(connection, connection.getClass().getSuperclass().getDeclaredField("connectionState"), QuicClientConnectionImpl.Status.Connected); connection.process(new NewConnectionIdFrame(Version.getDefault(), 4, 3, new byte[]{ 0x04, 0x04, 0x04, 0x04 }), null, null); clearInvocations(sender); // When connection.process(new NewConnectionIdFrame(Version.getDefault(), 2, 0, new byte[]{ 0x02, 0x02, 0x02, 0x02 }), null, null); // Then verify(sender).send(argThat(frame -> frame.equals(new RetireConnectionIdFrame(Version.getDefault(), 2))), any(EncryptionLevel.class), any(Consumer.class)); } @Test void processingVersionNegotationWithClientVersionShouldBeIgnored() { VersionNegotiationPacket vnWithClientVersion = mock(VersionNegotiationPacket.class); when(vnWithClientVersion.getServerSupportedVersions()).thenReturn(List.of(Version.getDefault())); try { connection.process(vnWithClientVersion, Instant.now()); } catch (Throwable exception) { exception.printStackTrace(); fail(); } } @Test void versionNegotationAfterClientHasReceivedOthePacketShouldBeIgnored() { VersionNegotiationPacket vn = new VersionNegotiationPacket(); connection.process(new InitialPacket(Version.getDefault(), new byte[0], new byte[0], new byte[0], new PingFrame()), Instant.now()); try { connection.process(vn, Instant.now()); } catch (Throwable exception) { fail(); } } @Test void parseEmptyPacket() throws Exception { assertThatThrownBy( () -> connection.parsePacket(ByteBuffer.wrap(new byte[0])) ).isInstanceOf(InvalidPacketException.class); } @Test void parseLongHeaderPacketWithInvalidHeader1() throws Exception { assertThatThrownBy( () -> connection.parsePacket(ByteBuffer.wrap(new byte[] { (byte) 0xc0, 0x00})) ).isInstanceOf(InvalidPacketException.class); } @Test void parseLongHeaderPacketWithInvalidHeader2() throws Exception { assertThatThrownBy( () -> connection.parsePacket(ByteBuffer.wrap(new byte[] { (byte) 0xc0, 0x00, 0x00, 0x00 })) ).isInstanceOf(InvalidPacketException.class); } @Test void parseShortHeaderPacketWithInvalidHeader() throws Exception { assertThatThrownBy( () -> connection.parsePacket(ByteBuffer.wrap(new byte[] { (byte) 0x40 })) ).isInstanceOf(InvalidPacketException.class); } @Test void clientParsingZeroRttPacketShouldThrow() throws Exception { assertThatThrownBy(() -> connection.parsePacket(ByteBuffer.wrap(new byte[] { (byte) 0b11010001, 0x00, 0x00, 0x00, 0x01, 0, 0, 17, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 })) ).isInstanceOf(InvalidPacketException.class); } private void setTransportParametersWithActiveConnectionIdLimit(int connectionIdLimit) { TransportParameters params = new TransportParameters(); params.setInitialSourceConnectionId(connection.getDestinationConnectionId()); params.setOriginalDestinationConnectionId(connection.getDestinationConnectionId()); params.setActiveConnectionIdLimit(connectionIdLimit); connection.setPeerTransportParameters(params); } }