From 9416cd6527ea0f48d62ee32e303544b88e72b6fc Mon Sep 17 00:00:00 2001 From: Sebastian Pick <48058165+sebastianpick@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:58:37 +0100 Subject: [PATCH] Add 'cancel on loss' send mode to MsQuicStream. (#4037) --- docs/Streams.md | 6 + docs/api/StreamSend.md | 1 + src/core/stream.h | 4 +- src/core/stream_send.c | 34 +++- src/cs/lib/msquic_generated.cs | 20 +++ src/inc/msquic.h | 7 +- src/inc/msquic.hpp | 2 +- src/plugins/dbg/quictypes.h | 2 + src/test/MsQuicTests.h | 20 ++- src/test/bin/quic_gtest.cpp | 23 +++ src/test/bin/quic_gtest.h | 18 +++ src/test/bin/winkernel/control.cpp | 7 + src/test/lib/DataTest.cpp | 250 +++++++++++++++++++++++++++++ 13 files changed, 388 insertions(+), 6 deletions(-) diff --git a/docs/Streams.md b/docs/Streams.md index 68e01907fa..4f22c4f1e1 100644 --- a/docs/Streams.md +++ b/docs/Streams.md @@ -64,6 +64,12 @@ When the send has been completely shut down the app will get a `QUIC_STREAM_EVEN An app can opt in to sending stream data with 0-RTT keys (if available) by including the `QUIC_SEND_FLAG_ALLOW_0_RTT` flag on [StreamSend](api/StreamSend.md) call. MsQuic doesn't make any guarantees that the data will actually be sent with 0-RTT keys. There are several reasons it may not happen, such as keys not being available, packet loss, flow control, etc. +## Cancel On Loss + +In case it is desirable to cancel a stream when packet loss is deteced instead of retransmitting the affected packets, the `QUIC_SEND_FLAG_CANCEL_ON_LOSS` can be supplied on a [StreamSend](api/StreamSend.md) call. Doing so will irreversibly switch the associated stream to this behavior. This includes *every* subsequent send call on the same stream, even if the call itself does not include the above flag. + +If a stream gets canceled because it is in 'cancel on loss' mode, a `QUIC_STREAM_EVENT_CANCEL_ON_LOSS` event will get emitted. The event allows the app to provide an error code that is communicated to the peer via a `QUIC_STREAM_EVENT_PEER_SEND_ABORTED` event. + # Receiving Data is received and delivered to apps via the `QUIC_STREAM_EVENT_RECEIVE` event. The event indicates zero, one or more contiguous buffers up to the application. diff --git a/docs/api/StreamSend.md b/docs/api/StreamSend.md index 1059f7a6af..c8a5b03fc4 100644 --- a/docs/api/StreamSend.md +++ b/docs/api/StreamSend.md @@ -45,6 +45,7 @@ Value | Meaning **QUIC_SEND_FLAG_FIN**
4 | Indicates the the stream send is the last or final data to be sent on the stream and should be gracefully shutdown (equivalent to calling [StreamShutdown](StreamShutdown.md) with the `QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL` flag). **QUIC_SEND_FLAG_DGRAM_PRIORITY**
8 | **Unused and ignored** for `StreamSend` **QUIC_SEND_FLAG_DELAY_SEND**
16 | Provides a hint to MsQuic to indicate the data does not need to be sent immediately, likely because more is soon to follow. +**QUIC_SEND_FLAG_CANCEL_ON_LOSS**
32 | Informs MsQuic to irreversibly mark the associated stream to be canceled when packet loss has been detected on it. I.e., all sends on a given stream are subject to this behavior from the moment the flag has been supplied for the first time. `ClientSendContext` diff --git a/src/core/stream.h b/src/core/stream.h index 787c7e5649..4e05e2cfc8 100644 --- a/src/core/stream.h +++ b/src/core/stream.h @@ -124,7 +124,7 @@ typedef union QUIC_STREAM_FLAGS { BOOLEAN LocalCloseReset : 1; // Locally closed (locally aborted). BOOLEAN LocalCloseResetReliable : 1; // Indicates that we should shutdown the send path once we sent/ACK'd ReliableOffsetSend bytes. BOOLEAN LocalCloseResetReliableAcked : 1; // Indicates the peer has acknowledged we will stop sending once we sent/ACK'd ReliableOffsetSend bytes. - BOOLEAN RemoteCloseResetReliable : 1; // Indicates that the peer initiaited a reliable reset. Keep Recv path available for RecvMaxLength bytes. + BOOLEAN RemoteCloseResetReliable : 1; // Indicates that the peer initiated a reliable reset. Keep Recv path available for RecvMaxLength bytes. BOOLEAN ReceivedStopSending : 1; // Peer sent STOP_SENDING frame. BOOLEAN LocalCloseAcked : 1; // Any close acknowledged. BOOLEAN FinAcked : 1; // Our FIN was acknowledged. @@ -144,6 +144,8 @@ typedef union QUIC_STREAM_FLAGS { BOOLEAN ReceiveCallPending : 1; // There is an uncompleted receive to the app. BOOLEAN ReceiveCallActive : 1; // There is an active receive to the app. BOOLEAN SendDelayed : 1; // A delayed send is currently queued. + BOOLEAN CancelOnLoss : 1; // Indicates that the stream is to be canceled + // if loss is detected. BOOLEAN HandleSendShutdown : 1; // Send shutdown complete callback delivered. BOOLEAN HandleShutdown : 1; // Shutdown callback delivered. diff --git a/src/core/stream_send.c b/src/core/stream_send.c index 2c529c2bf6..2ce3864417 100644 --- a/src/core/stream_send.c +++ b/src/core/stream_send.c @@ -150,7 +150,7 @@ QuicStreamSendShutdown( while (ApiSendRequests != NULL) { // - // These sends were queued by the app after queueing a graceful + // These sends were queued by the app after queuing a graceful // shutdown. Bad app! // QUIC_SEND_REQUEST* SendRequest = ApiSendRequests; @@ -171,7 +171,7 @@ QuicStreamSendShutdown( } else if (Stream->ReliableOffsetSend == 0 || Stream->Flags.LocalCloseResetReliable) { // - // Enter abortive branch if we are not aborting reliablely or we have done it already. + // Enter abortive branch if we are not aborting reliably or we have done it already. // Essentially, Reset trumps Reliable Reset, so if we have to call shutdown again, we reset. // @@ -613,6 +613,15 @@ QuicStreamSendFlush( CXPLAT_DBG_ASSERT(!(SendRequest->Flags & QUIC_SEND_FLAG_BUFFERED)); + // + // If a send has the 'cancel on loss' flag set, we irreversibly switch + // the associated stream over to that behavior. + // + if (!Stream->Flags.CancelOnLoss && + (SendRequest->Flags & QUIC_SEND_FLAG_CANCEL_ON_LOSS) != 0) { + Stream->Flags.CancelOnLoss = TRUE; + } + if (!Stream->Flags.SendEnabled) { // // Only possible if they queue multiple sends, with a FIN flag set @@ -1364,6 +1373,27 @@ QuicStreamOnLoss( Done: if (AddSendFlags != 0) { + // + // Check stream's 'cancel on loss' flag to determine how to handle + // the resends queued up at this point. + // + if (Stream->Flags.CancelOnLoss) { + QUIC_STREAM_EVENT Event; + Event.Type = QUIC_STREAM_EVENT_CANCEL_ON_LOSS; + Event.CANCEL_ON_LOSS.ErrorCode = 0; + (void)QuicStreamIndicateEvent(Stream, &Event); + + // + // Immediately terminate stream (in both directions, if open) + // giving the error code from the app. + // + QuicStreamShutdown( + Stream, + QUIC_STREAM_SHUTDOWN_FLAG_ABORT, + Event.CANCEL_ON_LOSS.ErrorCode); + + return FALSE; // Don't resend any data. + } if (!Stream->Flags.InRecovery) { Stream->Flags.InRecovery = TRUE; // TODO - Do we really need to be in recovery if no real data bytes need to be recovered? diff --git a/src/cs/lib/msquic_generated.cs b/src/cs/lib/msquic_generated.cs index b8c0b092df..11ac59c461 100644 --- a/src/cs/lib/msquic_generated.cs +++ b/src/cs/lib/msquic_generated.cs @@ -193,6 +193,7 @@ internal enum QUIC_SEND_FLAGS FIN = 0x0004, DGRAM_PRIORITY = 0x0008, DELAY_SEND = 0x0010, + CANCEL_ON_LOSS = 0x0020, } internal enum QUIC_DATAGRAM_SEND_STATE @@ -2746,6 +2747,7 @@ internal enum QUIC_STREAM_EVENT_TYPE SHUTDOWN_COMPLETE = 7, IDEAL_SEND_BUFFER_SIZE = 8, PEER_ACCEPTED = 9, + CANCEL_ON_LOSS = 10, } internal partial struct QUIC_STREAM_EVENT @@ -2819,6 +2821,14 @@ internal ref _Anonymous_e__Union._IDEAL_SEND_BUFFER_SIZE_e__Struct IDEAL_SEND_BU } } + internal ref _Anonymous_e__Union._CANCEL_ON_LOSS_e__Struct CANCEL_ON_LOSS + { + get + { + return ref MemoryMarshal.GetReference(MemoryMarshal.CreateSpan(ref Anonymous.CANCEL_ON_LOSS, 1)); + } + } + [StructLayout(LayoutKind.Explicit)] internal partial struct _Anonymous_e__Union { @@ -2854,6 +2864,10 @@ internal partial struct _Anonymous_e__Union [NativeTypeName("struct (anonymous struct)")] internal _IDEAL_SEND_BUFFER_SIZE_e__Struct IDEAL_SEND_BUFFER_SIZE; + [FieldOffset(0)] + [NativeTypeName("struct (anonymous struct)")] + internal _CANCEL_ON_LOSS_e__Struct CANCEL_ON_LOSS; + internal partial struct _START_COMPLETE_e__Struct { [NativeTypeName("HRESULT")] @@ -3011,6 +3025,12 @@ internal partial struct _IDEAL_SEND_BUFFER_SIZE_e__Struct [NativeTypeName("uint64_t")] internal ulong ByteCount; } + + internal partial struct _CANCEL_ON_LOSS_e__Struct + { + [NativeTypeName("QUIC_UINT62")] + internal ulong ErrorCode; + } } } diff --git a/src/inc/msquic.h b/src/inc/msquic.h index dc7295a611..46f238452e 100644 --- a/src/inc/msquic.h +++ b/src/inc/msquic.h @@ -240,13 +240,14 @@ typedef enum QUIC_SEND_FLAGS { QUIC_SEND_FLAG_FIN = 0x0004, // Indicates the request is the one last sent on the stream. QUIC_SEND_FLAG_DGRAM_PRIORITY = 0x0008, // Indicates the datagram is higher priority than others. QUIC_SEND_FLAG_DELAY_SEND = 0x0010, // Indicates the send should be delayed because more will be queued soon. + QUIC_SEND_FLAG_CANCEL_ON_LOSS = 0x0020, // Indicates that a stream is to be cancelled when packet loss is detected. } QUIC_SEND_FLAGS; DEFINE_ENUM_FLAG_OPERATORS(QUIC_SEND_FLAGS) typedef enum QUIC_DATAGRAM_SEND_STATE { QUIC_DATAGRAM_SEND_UNKNOWN, // Not yet sent. - QUIC_DATAGRAM_SEND_SENT, // Sent and awaiting acknowledegment + QUIC_DATAGRAM_SEND_SENT, // Sent and awaiting acknowledgment QUIC_DATAGRAM_SEND_LOST_SUSPECT, // Suspected as lost, but still tracked QUIC_DATAGRAM_SEND_LOST_DISCARDED, // Lost and not longer being tracked QUIC_DATAGRAM_SEND_ACKNOWLEDGED, // Acknowledged @@ -1385,6 +1386,7 @@ typedef enum QUIC_STREAM_EVENT_TYPE { QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE = 7, QUIC_STREAM_EVENT_IDEAL_SEND_BUFFER_SIZE = 8, QUIC_STREAM_EVENT_PEER_ACCEPTED = 9, + QUIC_STREAM_EVENT_CANCEL_ON_LOSS = 10, } QUIC_STREAM_EVENT_TYPE; typedef struct QUIC_STREAM_EVENT { @@ -1430,6 +1432,9 @@ typedef struct QUIC_STREAM_EVENT { struct { uint64_t ByteCount; } IDEAL_SEND_BUFFER_SIZE; + struct { + /* out */ QUIC_UINT62 ErrorCode; + } CANCEL_ON_LOSS; }; } QUIC_STREAM_EVENT; diff --git a/src/inc/msquic.hpp b/src/inc/msquic.hpp index 63da51fb8b..31e2725e78 100644 --- a/src/inc/msquic.hpp +++ b/src/inc/msquic.hpp @@ -864,7 +864,7 @@ struct MsQuicListener { QUIC_STATUS Start( _In_ const MsQuicAlpn& Alpns, - _In_ const QUIC_ADDR* Address = nullptr + _In_opt_ const QUIC_ADDR* Address = nullptr ) noexcept { return MsQuic->ListenerStart(Handle, Alpns, Alpns.Length(), Address); } diff --git a/src/plugins/dbg/quictypes.h b/src/plugins/dbg/quictypes.h index eb13feeaa8..413e62b810 100644 --- a/src/plugins/dbg/quictypes.h +++ b/src/plugins/dbg/quictypes.h @@ -62,6 +62,8 @@ typedef union QUIC_STREAM_FLAGS { BOOLEAN ReceiveCallPending : 1; // There is an uncompleted receive to the app. BOOLEAN ReceiveCallActive : 1; // There is an active receive to the app. BOOLEAN SendDelayed : 1; // A delayed send is currently queued. + BOOLEAN CancelOnLoss : 1; // Indicates that the stream is to be canceled + // if loss is detected. BOOLEAN HandleSendShutdown : 1; // Send shutdown complete callback delivered. BOOLEAN HandleShutdown : 1; // Shutdown callback delivered. diff --git a/src/test/MsQuicTests.h b/src/test/MsQuicTests.h index 129e0171db..45ee390a95 100644 --- a/src/test/MsQuicTests.h +++ b/src/test/MsQuicTests.h @@ -467,6 +467,11 @@ QuicAbortiveTransfers( _In_ QUIC_ABORTIVE_TRANSFER_FLAGS Flags ); +void +QuicCancelOnLossSend( + _In_ bool DropPackets + ); + void QuicTestCidUpdate( _In_ int Family, @@ -1253,4 +1258,17 @@ typedef struct { #define IOCTL_QUIC_RUN_CONN_CLOSE_BEFORE_STREAM_CLOSE \ QUIC_CTL_CODE(117, METHOD_BUFFERED, FILE_WRITE_DATA) -#define QUIC_MAX_IOCTL_FUNC_CODE 117 +#pragma pack(push) +#pragma pack(1) + +typedef struct { + bool DropPackets; +} QUIC_RUN_CANCEL_ON_LOSS_PARAMS; + +#pragma pack(pop) + +#define IOCTL_QUIC_RUN_CANCEL_ON_LOSS \ + QUIC_CTL_CODE(118, METHOD_BUFFERED, FILE_WRITE_DATA) + // QUIC_RUN_CANCEL_ON_LOSS_PARAMS + +#define QUIC_MAX_IOCTL_FUNC_CODE 118 diff --git a/src/test/bin/quic_gtest.cpp b/src/test/bin/quic_gtest.cpp index cced03ae14..217dbf94e1 100644 --- a/src/test/bin/quic_gtest.cpp +++ b/src/test/bin/quic_gtest.cpp @@ -1973,6 +1973,20 @@ TEST_P(WithAbortiveArgs, AbortiveShutdown) { } } +#if QUIC_TEST_DATAPATH_HOOKS_ENABLED +TEST_P(WithCancelOnLossArgs, CancelOnLossSend) { + TestLoggerT Logger("QuicCancelOnLossSend", GetParam()); + if (TestingKernelMode) { + QUIC_RUN_CANCEL_ON_LOSS_PARAMS Params = { + GetParam().DropPackets + }; + ASSERT_TRUE(DriverClient.Run(IOCTL_QUIC_RUN_CANCEL_ON_LOSS, Params)); + } else { + QuicCancelOnLossSend(GetParam().DropPackets); + } +} +#endif + TEST_P(WithCidUpdateArgs, CidUpdate) { TestLoggerT Logger("QuicTestCidUpdate", GetParam()); if (TestingKernelMode) { @@ -2437,6 +2451,15 @@ INSTANTIATE_TEST_SUITE_P( WithAbortiveArgs, testing::ValuesIn(AbortiveArgs::Generate())); +#if QUIC_TEST_DATAPATH_HOOKS_ENABLED + +INSTANTIATE_TEST_SUITE_P( + Misc, + WithCancelOnLossArgs, + testing::ValuesIn(CancelOnLossArgs::Generate())); + +#endif + INSTANTIATE_TEST_SUITE_P( Misc, WithCidUpdateArgs, diff --git a/src/test/bin/quic_gtest.h b/src/test/bin/quic_gtest.h index ffb172cf40..4d84a375df 100644 --- a/src/test/bin/quic_gtest.h +++ b/src/test/bin/quic_gtest.h @@ -592,6 +592,24 @@ class WithAbortiveArgs : public testing::Test, public testing::WithParamInterface { }; +struct CancelOnLossArgs { + bool DropPackets; + static ::std::vector Generate() { + ::std::vector list; + for (bool DropPackets : {false, true}) + list.push_back({ DropPackets }); + return list; + } +}; + +std::ostream& operator << (std::ostream& o, const CancelOnLossArgs& args) { + return o << "DropPackets: " << (args.DropPackets ? "true" : "false"); +} + +class WithCancelOnLossArgs : public testing::Test, + public testing::WithParamInterface { +}; + struct CidUpdateArgs { int Family; uint16_t Iterations; diff --git a/src/test/bin/winkernel/control.cpp b/src/test/bin/winkernel/control.cpp index ec0a96ad4a..4e8c4c8893 100644 --- a/src/test/bin/winkernel/control.cpp +++ b/src/test/bin/winkernel/control.cpp @@ -510,6 +510,7 @@ size_t QUIC_IOCTL_BUFFER_SIZES[] = 0, sizeof(INT32), 0, + sizeof(QUIC_RUN_CANCEL_ON_LOSS_PARAMS), }; CXPLAT_STATIC_ASSERT( @@ -528,6 +529,7 @@ typedef union { QUIC_RUN_ABORTIVE_SHUTDOWN_PARAMS Params4; QUIC_RUN_CID_UPDATE_PARAMS Params5; QUIC_RUN_RECEIVE_RESUME_PARAMS Params6; + QUIC_RUN_CANCEL_ON_LOSS_PARAMS Params7; UINT8 EnableKeepAlive; UINT8 StopListenerFirst; QUIC_RUN_DRILL_INITIAL_PACKET_CID_PARAMS DrillParams; @@ -1419,6 +1421,11 @@ QuicTestCtlEvtIoDeviceControl( QuicTestCtlRun(QuicTestConnectionCloseBeforeStreamClose()); break; + case IOCTL_QUIC_RUN_CANCEL_ON_LOSS: + CXPLAT_FRE_ASSERT(Params != nullptr); + QuicTestCtlRun(QuicCancelOnLossSend(Params->Params7.DropPackets)); + break; + default: Status = STATUS_NOT_IMPLEMENTED; break; diff --git a/src/test/lib/DataTest.cpp b/src/test/lib/DataTest.cpp index c3cda1c0ce..e9f7a6ef5c 100644 --- a/src/test/lib/DataTest.cpp +++ b/src/test/lib/DataTest.cpp @@ -1363,6 +1363,256 @@ QuicAbortiveTransfers( } } +struct CancelOnLossContext +{ + CancelOnLossContext(bool IsDropScenario, bool IsServer, MsQuicConfiguration* Configuration) + : IsDropScenario{ IsDropScenario } + , IsServer{ IsServer } + , Configuration{ Configuration } + { } + + ~CancelOnLossContext() { + delete Stream; + Stream = nullptr; + + delete Connection; + Connection = nullptr; + } + + // Static parameters + static constexpr uint64_t SuccessExitCode = 42; + static constexpr uint64_t ErrorExitCode = 24; + + // State + const bool IsDropScenario = false; + const bool IsServer = false; + const MsQuicConfiguration* Configuration = nullptr; + MsQuicConnection* Connection = nullptr; + MsQuicStream* Stream = nullptr; + + // Connection tracking + CxPlatEvent ConnectedEvent = {}; + + // Test case tracking + uint64_t ExitCode = 0; + CxPlatEvent SendPhaseEndedEvent = {}; +}; + + +_Function_class_(MsQuicStreamCallback) +QUIC_STATUS +QuicCancelOnLossStreamHandler( + _In_ struct MsQuicStream* /* Stream */, + _In_opt_ void* Context, + _Inout_ QUIC_STREAM_EVENT* Event + ) +{ + if (Context == nullptr) { + return QUIC_STATUS_INVALID_PARAMETER; + } + + auto TestContext = reinterpret_cast(Context); + + QUIC_STATUS Status = QUIC_STATUS_SUCCESS; + + switch (Event->Type) { + case QUIC_STREAM_EVENT_RECEIVE: + if (TestContext->IsServer) { // only server receives + TestContext->SendPhaseEndedEvent.Set(); + TestContext->ExitCode = CancelOnLossContext::SuccessExitCode; + } + break; + case QUIC_STREAM_EVENT_PEER_SEND_ABORTED: + if (TestContext->IsServer) { // server-side 'cancel on loss' detection + TestContext->SendPhaseEndedEvent.Set(); + TestContext->ExitCode = Event->PEER_SEND_ABORTED.ErrorCode; + } else { + Status = QUIC_STATUS_INVALID_STATE; + } + break; + case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN: + if (TestContext->IsServer) { + TestContext->SendPhaseEndedEvent.Set(); + } + break; + case QUIC_STREAM_EVENT_SEND_COMPLETE: + if (!TestContext->IsServer) { // only client sends + if (!TestContext->IsDropScenario) { // if drop scenario, we use 'cancel on loss' event + TestContext->SendPhaseEndedEvent.Set(); + } + } else { + Status = QUIC_STATUS_INVALID_STATE; + } + break; + case QUIC_STREAM_EVENT_CANCEL_ON_LOSS: + if (!TestContext->IsServer && TestContext->IsDropScenario) { // only client sends & only happens if in drop scenario + Event->CANCEL_ON_LOSS.ErrorCode = CancelOnLossContext::ErrorExitCode; + TestContext->SendPhaseEndedEvent.Set(); + } else { + Status = QUIC_STATUS_INVALID_STATE; + } + break; + default: + break; + } + + return Status; +} + +_Function_class_(MsQuicConnectionCallback) +QUIC_STATUS +QuicCancelOnLossConnectionHandler( + _In_ struct MsQuicConnection* /* Connection */, + _In_opt_ void* Context, + _Inout_ QUIC_CONNECTION_EVENT* Event + ) +{ + if (Context == nullptr) { + return QUIC_STATUS_INVALID_PARAMETER; + } + + auto TestContext = reinterpret_cast(Context); + + QUIC_STATUS Status = QUIC_STATUS_SUCCESS; + + switch (Event->Type) { + case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED: + TestContext->Stream = new(std::nothrow) MsQuicStream( + Event->PEER_STREAM_STARTED.Stream, + CleanUpManual, + QuicCancelOnLossStreamHandler, + Context); + break; + case QUIC_CONNECTION_EVENT_CONNECTED: + TestContext->ConnectedEvent.Set(); + break; + default: + break; + } + + return Status; +} + +void +QuicCancelOnLossSend( + _In_ bool DropPackets + ) +{ + MsQuicRegistration Registration; + TEST_TRUE(Registration.IsValid()); + + MsQuicAlpn Alpn("MsQuicTest"); + + MsQuicSettings Settings; + Settings.SetIdleTimeoutMs(1'000); + Settings.SetServerResumptionLevel(QUIC_SERVER_NO_RESUME); + Settings.SetPeerBidiStreamCount(1); + Settings.SetMinimumMtu(1280).SetMaximumMtu(1280); // avoid running path MTU discovery (PMTUD) + + uint8_t RawBuffer[] = "cancel on loss message"; + QUIC_BUFFER MessageBuffer = { sizeof(RawBuffer), RawBuffer }; + + SelectiveLossHelper LossHelper; // used later to trigger packet drops + + // Start the server. + MsQuicConfiguration ServerConfiguration(Registration, Alpn, Settings, ServerSelfSignedCredConfig); + TEST_TRUE(ServerConfiguration.IsValid()); + + CancelOnLossContext ServerContext{ DropPackets, true /* IsServer */, &ServerConfiguration}; + QuicAddr ServerLocalAddr; + + MsQuicAutoAcceptListener Listener(Registration, ServerConfiguration, QuicCancelOnLossConnectionHandler, &ServerContext); + TEST_TRUE(Listener.IsValid()); + TEST_EQUAL(Listener.Start(Alpn), QUIC_STATUS_SUCCESS); + TEST_EQUAL(Listener.GetLocalAddr(ServerLocalAddr), QUIC_STATUS_SUCCESS); + + // Start the client. + MsQuicCredentialConfig ClientCredConfig; + MsQuicConfiguration ClientConfiguration(Registration, Alpn, Settings, ClientCredConfig); + TEST_TRUE(ClientConfiguration.IsValid()); + + CancelOnLossContext ClientContext{ DropPackets, false /* IsServer */, &ClientConfiguration}; + + // Initiate connection. + ClientContext.Connection = new(std::nothrow) MsQuicConnection( + Registration, + CleanUpManual, + QuicCancelOnLossConnectionHandler, + &ClientContext); + TEST_TRUE(ClientContext.Connection->IsValid()); + + QUIC_STATUS Status = ClientContext.Connection->Start( + ClientConfiguration, + QUIC_ADDRESS_FAMILY_INET, + QUIC_TEST_LOOPBACK_FOR_AF(QUIC_ADDRESS_FAMILY_INET), + ServerLocalAddr.GetPort()); + if (QUIC_FAILED(Status)) { + TEST_FAILURE("Failed to start a connection from the client."); + return; + } + + // Wait for connection to be established. + constexpr uint32_t EventWaitTimeoutMs{ 1'000 }; + + if (!ClientContext.ConnectedEvent.WaitTimeout(EventWaitTimeoutMs)) { + TEST_FAILURE("Client failed to get connected before timeout!"); + return; + } + if (!ServerContext.ConnectedEvent.WaitTimeout(EventWaitTimeoutMs)) { + TEST_FAILURE("Server failed to get connected before timeout!"); + return; + } + + // Sleep a bit to wait for all handshake packets to be exchanged. + CxPlatSleep(100); + + // Set up stream. + ClientContext.Stream = new(std::nothrow) MsQuicStream( + *ClientContext.Connection, + QUIC_STREAM_OPEN_FLAG_NONE, + CleanUpManual, + QuicCancelOnLossStreamHandler, + &ClientContext); + TEST_TRUE(ClientContext.Stream->IsValid()); + Status = ClientContext.Stream->Start(); + if (QUIC_FAILED(Status)) { + TEST_FAILURE("Client failed to start stream."); + return; + } + + // Send test message. + Status = ClientContext.Stream->Send(&MessageBuffer, 1, QUIC_SEND_FLAG_CANCEL_ON_LOSS); + if (QUIC_FAILED(Status)) { + TEST_FAILURE("Client failed to send message."); + return; + } + + // If requested, drop packets. + if (DropPackets) { + LossHelper.DropPackets(1); + } + + // Wait for the send phase to conclude. + if (!ClientContext.SendPhaseEndedEvent.WaitTimeout(EventWaitTimeoutMs)) { + TEST_FAILURE("Timed out waiting for send phase to conclude on client."); + return; + } + if (!ServerContext.SendPhaseEndedEvent.WaitTimeout(EventWaitTimeoutMs)) { + TEST_FAILURE("Timed out waiting for send phase to conclude on server."); + } + + // Check results. + if (DropPackets) { + TEST_EQUAL(ServerContext.ExitCode, CancelOnLossContext::ErrorExitCode); + } else { + TEST_EQUAL(ServerContext.ExitCode, CancelOnLossContext::SuccessExitCode); + } + + if (Listener.LastConnection) { + Listener.LastConnection->Close(); + } +} + struct RecvResumeTestContext { RecvResumeTestContext( _In_ HQUIC ServerConfiguration,