/* * 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.recovery; import net.luminis.quic.*; import net.luminis.quic.cc.CongestionController; import net.luminis.quic.frame.*; import net.luminis.quic.log.Logger; import net.luminis.quic.packet.InitialPacket; import net.luminis.quic.packet.QuicPacket; import net.luminis.quic.qlog.QLog; import net.luminis.quic.send.Sender; import net.luminis.quic.test.TestClock; import net.luminis.quic.test.TestScheduledExecutor; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import net.luminis.quic.test.FieldSetter; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.time.Instant; import java.util.List; import java.util.concurrent.ScheduledExecutorService; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; class RecoveryManagerTest extends RecoveryTests { private RecoveryManager recoveryManager; private LostPacketHandler lostPacketHandler; private int defaultRtt = 80; private int defaultRttVar = defaultRtt / 4; private Sender probeSender; private RttEstimator rttEstimator; private TestClock clock; @BeforeEach void initObjectUnderTest() throws Exception { rttEstimator = mock(RttEstimator.class); when(rttEstimator.getSmoothedRtt()).thenReturn(defaultRtt); when(rttEstimator.getLatestRtt()).thenReturn(defaultRtt); when(rttEstimator.getRttVar()).thenReturn(defaultRttVar); probeSender = mock(Sender.class); Logger logger = mock(Logger.class); when(logger.getQLog()).thenReturn(mock(QLog.class)); // logger = new SysOutLogger(); // logger.logRecovery(true); clock = new TestClock(); recoveryManager = new RecoveryManager(clock, Role.Client, rttEstimator, mock(CongestionController.class), probeSender, logger); ScheduledExecutorService scheduler = new TestScheduledExecutor(clock); FieldSetter.setField(recoveryManager, recoveryManager.getClass().getDeclaredField("scheduler"), scheduler); } @BeforeEach void initLostPacketCallback() { lostPacketHandler = mock(LostPacketHandler.class); } @AfterEach void shutdownRecoveryManager() { recoveryManager.stopRecovery(); } // https://tools.ietf.org/html/draft-ietf-quic-recovery-20#section-6.1.2 // "If packets sent prior to the largest // acknowledged packet cannot yet be declared lost, then a timer SHOULD // be set for the remaining time." @Test void nonAckedPacketThatCannotYetBeDeclaredLostIsLostAfterLossTime() { // Given two packets sent, with half RTT interval recoveryManager.packetSent(createPacket(1), clock.instant(), lostPacketHandler::process); clock.fastForward(defaultRtt / 2); recoveryManager.packetSent(createPacket(2), clock.instant(), this::noOp); // When an ack is received immediately (impossible in reality, but fine for a test) recoveryManager.onAckReceived(new AckFrame(2), PnSpace.App, clock.instant()); // Then only after some time (1 RTT), the packet is lost verify(lostPacketHandler, never()).process(any(QuicPacket.class)); clock.fastForward(defaultRtt); verify(lostPacketHandler, times(1)).process(argThat(new PacketMatcherByPacketNumber(1))); } @Test void whenAckElicitingPacketIsNotAckedProbeIsSent() { // Given recovery manager is not in handshake state anymore recoveryManager.handshakeStateChangedEvent(HandshakeState.Confirmed); // When packet is set recoveryManager.packetSent(createPacket(2), clock.instant(), this::noOp); // Then only after probe timeout, a probe has been sent. int probeTimeout = defaultRtt + 4 * defaultRttVar; clock.fastForward(probeTimeout * 9 / 10); verify(probeSender, never()).sendProbe(anyList(), any(EncryptionLevel.class)); clock.fastForward(probeTimeout * 1 / 10); verify(probeSender, times(1)).sendProbe(anyList(), any(EncryptionLevel.class)); } @Test void whenProbeIsNotAckedAnotherOneIsSent() { // Given recovery manager is not in handshake state anymore and when probes are sent, packetSent() is called recoveryManager.handshakeStateChangedEvent(HandshakeState.Confirmed); ensureSendProbeCallsPacketSent(3, 4, 5); // When a packet is sent but not acked Instant firstPacketTime = clock.instant(); recoveryManager.packetSent(createPacket(2), firstPacketTime, p -> {}); // After probe timeout time, the first probe is sent int firstProbeTimeout = defaultRtt + 4 * defaultRttVar; clock.fastForward(firstProbeTimeout); verify(probeSender, times(1)).sendProbe(anyList(), any(EncryptionLevel.class)); // Then only after 2nd probe timeout, a second and third probe are sent. int secondProbeTimeout = firstProbeTimeout * 2; clock.fastForward(secondProbeTimeout * 9 / 10); verify(probeSender, times(1)).sendProbe(anyList(), any(EncryptionLevel.class)); // Not yet clock.fastForward(secondProbeTimeout * 1 / 10); verify(probeSender, times(3)).sendProbe(anyList(), any(EncryptionLevel.class)); // Yet it should, and 2 probes are sent simultaneously } @Test void noProbeIsSentForAck() { // Given peer has completed address validation recoveryManager.onAckReceived(new AckFrame(0), PnSpace.App, Instant.now()); // When sending a packet that is not ack-eliciting QuicPacket ackPacket = createPacket(8, new AckFrame(20)); recoveryManager.packetSent(ackPacket, Instant.now(), p -> {}); // Then after probe timeout, no probe is sent, not even after 10 times probe timeout a probe is sent. int probeTimeout = defaultRtt + 4 * defaultRttVar; clock.fastForward(probeTimeout * 10); verify(probeSender, never()).sendProbe(EncryptionLevel.App); } @Test void whenAckElicitingPacketsAreNotAckedProbeIsSentForLastOnly() { // Given recovery manager is not in handshake state anymore recoveryManager.handshakeStateChangedEvent(HandshakeState.Confirmed); // When multiple packets are sent, with interval smaller than probe timeout int probeTimeout = defaultRtt + 4 * defaultRttVar; int interval = probeTimeout / 2; recoveryManager.packetSent(createPacket(10), clock.instant(), p -> {}); clock.fastForward(interval); recoveryManager.packetSent(createPacket(11), clock.instant(), p -> {}); clock.fastForward(interval); recoveryManager.packetSent(createPacket(12), clock.instant(), p -> {}); clock.fastForward(interval); recoveryManager.packetSent(createPacket(13), clock.instant(), p -> {}); clock.fastForward(interval); recoveryManager.packetSent(createPacket(14), clock.instant(), p -> {}); clock.fastForward(interval); recoveryManager.packetSent(createPacket(15), clock.instant(), p -> {}); // Then, finally, only one probe is sent, time after last packet verify(probeSender, never()).sendProbe(anyList(), any(EncryptionLevel.class)); clock.fastForward(probeTimeout); verify(probeSender, times(1)).sendProbe(anyList(), any(EncryptionLevel.class)); } @Test void probeTimeoutShouldMoveToLastAckEliciting() { // Given recovery manager is not in handshake state anymore recoveryManager.handshakeStateChangedEvent(HandshakeState.Confirmed); int probeTimeout = defaultRtt + 4 * defaultRttVar; // When two (ack-eliciting) packets are sent, with some time in between, // and the first is acked some time later recoveryManager.packetSent(createPacket(10), clock.instant(), p -> {}); clock.fastForward(probeTimeout / 2); recoveryManager.packetSent(createPacket(11), clock.instant(), p -> {}); clock.fastForward(probeTimeout / 2); // Ack on first packet, second packet must be the baseline for the probe-timeout recoveryManager.onAckReceived(new AckFrame(10), PnSpace.App, Instant.now()); // Then, a probe should be sent time after the second packet was sent // (which is now 1/2 probeTimeout in the past, so after 1/2 probeTimeout, the probe should be sent) clock.fastForward(probeTimeout * 3 / 8); verify(probeSender, never()).sendProbe(EncryptionLevel.App); clock.fastForward(probeTimeout * 1 / 8); // Now, second packet was sent more than probe-timeout ago, so now we should have a probe timeout verify(probeSender, times(1)).sendProbe(anyList(), any(EncryptionLevel.class)); } @Test void whenProbesAreAckedProbeTimeoutIsResetToNormal() throws InterruptedException { // Given recovery manager is not in handshake state anymore and when probes are sent, packetSent() is called recoveryManager.handshakeStateChangedEvent(HandshakeState.Confirmed); ensureSendProbeCallsPacketSent(3, 4, 5); // And a packet was sent and not acked, so recoveryManager.packetSent(createPacket(2), clock.instant(), p -> {}); // A first probe int firstProbeTimeout = defaultRtt + 4 * defaultRttVar; clock.fastForward(firstProbeTimeout); verify(probeSender, times(1)).sendProbe(anyList(), any(EncryptionLevel.class)); clearInvocations(probeSender); // And a second probe are sent int secondProbeTimeout = firstProbeTimeout * 2; clock.fastForward(secondProbeTimeout); verify(probeSender, times(2)).sendProbe(anyList(), any(EncryptionLevel.class)); // Yet it should, and 2 probes simultaneously clearInvocations(probeSender); // When an ack is received (on the first probe) recoveryManager.onAckReceived(new AckFrame(3), PnSpace.App, clock.instant()); // Then the probe timeout is reset to the value of the first probe timeout (the exponential multiplier is reset) clock.fastForward(firstProbeTimeout * 7 / 8); verify(probeSender, never()).sendProbe(anyList(), any(EncryptionLevel.class)); clock.fastForward(firstProbeTimeout * 1 / 8); verify(probeSender, times(1)).sendProbe(anyList(), any(EncryptionLevel.class)); } @Test void earliestLossTimeIsFound() throws Exception { // Given mock loss detectors are instantiated and registered in the recovery manager LossDetector[] detectors = new LossDetector[3]; for (int i = 0; i < 3; i++) { detectors[i] = mock(LossDetector.class); } FieldSetter.setField(recoveryManager, recoveryManager.getClass().getDeclaredField("lossDetectors"), detectors); Instant someInstant = Instant.now(); when(detectors[0].getLossTime()).thenReturn(someInstant); when(detectors[1].getLossTime()).thenReturn(null); when(detectors[2].getLossTime()).thenReturn(someInstant.minusMillis(100)); // When earliest loss time is determined // Then the value of the earliest is returned assertThat(recoveryManager.getEarliestLossTime(LossDetector::getLossTime).pnSpace.ordinal()).isEqualTo(2); } @Test void initialPacketRetransmit() { // Given an initial packet is sent (with crypto frames only) recoveryManager.packetSent(createCryptoPacket(0), clock.instant(), lostPacket -> lostPacketHandler.process(lostPacket)); // When the first probe is sent int firstProbeTimeout = defaultRtt + 4 * defaultRttVar; clock.fastForward(firstProbeTimeout * 7 / 8); verify(probeSender, times(0)).sendProbe(anyList(), any(EncryptionLevel.class)); clock.fastForward(firstProbeTimeout * 1 / 8); // Then the probe contains (retransmitted) crypto frames verify(probeSender, times(1)).sendProbe(argThat(frames -> frames.stream().allMatch(f -> f instanceof CryptoFrame)), any(EncryptionLevel.class)); // And the lost packet handler is not called (because that would lead to an additional retransmit: the probe is the retransmit) verify(lostPacketHandler, never()).process(any(InitialPacket.class)); } @Test void probeIsSentToPeerAwaitingAddressValidation() throws InterruptedException { // Given a client sends an initial packet that is acknowledged recoveryManager.packetSent(createCryptoPacket(0), clock.instant(), lostPacket -> {}); clock.fastForward(defaultRtt); recoveryManager.onAckReceived(new AckFrame(0), PnSpace.Initial, clock.instant()); // When nothing is received during first probe timeout int probeTimeout = defaultRtt + 4 * defaultRttVar; clock.fastForward(probeTimeout); // Then even though all client packets are acked, it sends to a probe to prevent a deadlock when server cannot send due to the amplification limit verify(probeSender, times(1)).sendProbe(anyList(), any(EncryptionLevel.class)); } @Test void framesToRetransmitShouldNotBePing() throws Exception { QuicPacket pingPacket = createHandshakePacket(0, new PingFrame()); recoveryManager.packetSent(pingPacket, clock.instant(), p -> {}); QuicPacket handshakePacket = createHandshakePacket(1, new CryptoFrame(Version.getDefault(), new byte[100])); recoveryManager.packetSent(handshakePacket, clock.instant(), p -> {}); List framesToRetransmit = recoveryManager.getFramesToRetransmit(PnSpace.Handshake); assertThat(framesToRetransmit).isNotEmpty(); assertThat(framesToRetransmit).doesNotHaveAnyElementsOfTypes(PingFrame.class); assertThat(framesToRetransmit).hasAtLeastOneElementOfType(CryptoFrame.class); } @Test void framesToRetransmitShouldNotBePingAndPaddingAndAck() throws Exception { QuicPacket pingPacket = createHandshakePacket(0, new PingFrame(), new Padding(2), new AckFrame(0)); recoveryManager.packetSent(pingPacket, clock.instant(), p -> {}); QuicPacket handshakePacket = createHandshakePacket(1, new CryptoFrame(Version.getDefault(), new byte[100])); recoveryManager.packetSent(handshakePacket, clock.instant(), p -> {}); List framesToRetransmit = recoveryManager.getFramesToRetransmit(PnSpace.Handshake); assertThat(framesToRetransmit).isNotEmpty(); assertThat(framesToRetransmit).doesNotHaveAnyElementsOfTypes(PingFrame.class); assertThat(framesToRetransmit).hasAtLeastOneElementOfType(CryptoFrame.class); } /** * Ensure that packetSent is called when probes packets are sent with the given packetNumbers. * In production code, packetSent is called by the sender, when it actually sends a packet. * This method hooks in onto sender's sendProbe method, calling packetSent when sendProbe is called. * @param packetNumbers */ private void ensureSendProbeCallsPacketSent(int... packetNumbers) { doAnswer(new Answer() { private int count; @Override public Void answer(InvocationOnMock invocationOnMock) throws Throwable { // Necessary to trigger setting the lastAckElicitingSent, which normally happens when a real packet is sent. int packetNumber = count < packetNumbers.length? packetNumbers[count++]: 666; recoveryManager.packetSent(createPacket(packetNumber), clock.instant(), p -> {}); return null; } }).when(probeSender).sendProbe(anyList(), any(EncryptionLevel.class)); } void noOp(QuicPacket lostPacket) {} }