mediasoup 3.20.3 → 3.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/worker/include/RTC/SCTP/association/Association.hpp +16 -2
- package/worker/include/RTC/SCTP/association/TransmissionControlBlock.hpp +16 -1
- package/worker/include/RTC/SCTP/association/TransmissionControlBlockContextInterface.hpp +19 -2
- package/worker/include/RTC/SCTP/public/SctpTypes.hpp +2 -0
- package/worker/include/RTC/SCTP/rx/ReassemblyQueue.hpp +7 -0
- package/worker/include/RTC/SCTP/tx/RetransmissionErrorCounter.hpp +3 -4
- package/worker/meson.build +3 -0
- package/worker/mocks/include/RTC/SCTP/association/MockAssociationListener.hpp +68 -7
- package/worker/mocks/include/handles/MockBackoffTimerHandle.hpp +10 -4
- package/worker/mocks/src/handles/MockBackoffTimerHandle.cpp +7 -0
- package/worker/src/RTC/SCTP/association/Association.cpp +52 -12
- package/worker/src/RTC/SCTP/association/HeartbeatHandler.cpp +33 -13
- package/worker/src/RTC/SCTP/association/StreamResetHandler.cpp +15 -11
- package/worker/src/RTC/SCTP/association/TransmissionControlBlock.cpp +34 -11
- package/worker/src/RTC/SCTP/rx/ReassemblyQueue.cpp +17 -8
- package/worker/src/RTC/Transport.cpp +1 -1
- package/worker/test/src/RTC/SCTP/association/TestAssociation.cpp +1755 -0
- package/worker/test/src/RTC/SCTP/association/TestPacketSender.cpp +121 -0
- package/worker/test/src/RTC/SCTP/association/TestStreamResetHandler.cpp +369 -0
- package/worker/test/src/RTC/SCTP/association/TestTransmissionControlBlock.cpp +11 -0
- package/worker/test/src/RTC/SCTP/rx/TestReassemblyQueue.cpp +24 -0
|
@@ -0,0 +1,1755 @@
|
|
|
1
|
+
#include "common.hpp"
|
|
2
|
+
#include "RTC/SCTP/association/Association.hpp"
|
|
3
|
+
#include "RTC/SCTP/packet/ErrorCause.hpp"
|
|
4
|
+
#include "RTC/SCTP/packet/Packet.hpp"
|
|
5
|
+
#include "RTC/SCTP/packet/chunks/AbortAssociationChunk.hpp"
|
|
6
|
+
#include "RTC/SCTP/packet/chunks/CookieAckChunk.hpp"
|
|
7
|
+
#include "RTC/SCTP/packet/chunks/CookieEchoChunk.hpp"
|
|
8
|
+
#include "RTC/SCTP/packet/chunks/DataChunk.hpp"
|
|
9
|
+
#include "RTC/SCTP/packet/chunks/HeartbeatAckChunk.hpp"
|
|
10
|
+
#include "RTC/SCTP/packet/chunks/HeartbeatRequestChunk.hpp"
|
|
11
|
+
#include "RTC/SCTP/packet/chunks/IDataChunk.hpp"
|
|
12
|
+
#include "RTC/SCTP/packet/chunks/InitAckChunk.hpp"
|
|
13
|
+
#include "RTC/SCTP/packet/chunks/InitChunk.hpp"
|
|
14
|
+
#include "RTC/SCTP/packet/chunks/OperationErrorChunk.hpp"
|
|
15
|
+
#include "RTC/SCTP/packet/chunks/SackChunk.hpp"
|
|
16
|
+
#include "RTC/SCTP/packet/chunks/ShutdownAckChunk.hpp"
|
|
17
|
+
#include "RTC/SCTP/packet/chunks/ShutdownChunk.hpp"
|
|
18
|
+
#include "RTC/SCTP/packet/chunks/ShutdownCompleteChunk.hpp"
|
|
19
|
+
#include "RTC/SCTP/packet/errorCauses/UnrecognizedChunkTypeErrorCause.hpp"
|
|
20
|
+
#include "RTC/SCTP/packet/parameters/HeartbeatInfoParameter.hpp"
|
|
21
|
+
#include "RTC/SCTP/public/Message.hpp"
|
|
22
|
+
#include "RTC/SCTP/public/SctpOptions.hpp"
|
|
23
|
+
#include "RTC/SCTP/public/SctpTypes.hpp"
|
|
24
|
+
#include "mocks/include/MockShared.hpp"
|
|
25
|
+
#include "mocks/include/RTC/SCTP/association/MockAssociationListener.hpp"
|
|
26
|
+
#include <array>
|
|
27
|
+
#include <catch2/catch_test_macros.hpp>
|
|
28
|
+
#include <span>
|
|
29
|
+
#include <string_view>
|
|
30
|
+
#include <vector>
|
|
31
|
+
|
|
32
|
+
namespace
|
|
33
|
+
{
|
|
34
|
+
// Initial value of the simulated clock (ms).
|
|
35
|
+
constexpr uint64_t InitialNowMs{ 1000000 };
|
|
36
|
+
|
|
37
|
+
// All backoff timer labels an SCTP association may create. Used by `runTimers()`
|
|
38
|
+
// to fire whichever timers have expired after advancing time.
|
|
39
|
+
constexpr std::array<std::string_view, 8> SctpTimerLabels{
|
|
40
|
+
"sctp-t1-init", "sctp-t1-cookie", "sctp-t2-shutdown", "sctp-t3-rtx",
|
|
41
|
+
"sctp-delayed-ack", "sctp-heartbeat-interval", "sctp-heartbeat-timeout", "sctp-re-config",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Builds the default SctpOptions used in tests. Tweaked to make timers
|
|
46
|
+
* predictable.
|
|
47
|
+
*/
|
|
48
|
+
RTC::SCTP::SctpOptions makeSctpOptions()
|
|
49
|
+
{
|
|
50
|
+
RTC::SCTP::SctpOptions sctpOptions;
|
|
51
|
+
|
|
52
|
+
// To make the heartbeat interval more predictable in tests.
|
|
53
|
+
sctpOptions.heartbeatIntervalIncludeRtt = false;
|
|
54
|
+
sctpOptions.maxBurst = 4;
|
|
55
|
+
|
|
56
|
+
return sctpOptions;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* An SCTP Association under test, together with its simulated clock and
|
|
61
|
+
* listener.
|
|
62
|
+
*/
|
|
63
|
+
class AssociationUnderTest
|
|
64
|
+
{
|
|
65
|
+
public:
|
|
66
|
+
/**
|
|
67
|
+
* `mayConnectOnReceivedSctpData` defaults to false so that, just like
|
|
68
|
+
* dcsctp, the connection is only initiated by explicitly calling `Connect()`.
|
|
69
|
+
* In production mediasoup associations auto-initiate the connection upon
|
|
70
|
+
* receiving data (both peers are active), but for tests we want to mimic
|
|
71
|
+
* dcsctp's asymmetric handshake (one active peer, one passive peer) so that
|
|
72
|
+
* packet sequences and counts match.
|
|
73
|
+
*/
|
|
74
|
+
explicit AssociationUnderTest(
|
|
75
|
+
RTC::SCTP::SctpOptions sctpOptions = makeSctpOptions(),
|
|
76
|
+
bool mayConnectOnReceivedSctpData = false)
|
|
77
|
+
// NOTE: The order in which these members are initialized is **critical**.
|
|
78
|
+
: sctpOptions(sctpOptions),
|
|
79
|
+
shared(/*getTimeMs*/
|
|
80
|
+
[this]()
|
|
81
|
+
{
|
|
82
|
+
return this->nowMs;
|
|
83
|
+
}),
|
|
84
|
+
association(
|
|
85
|
+
this->sctpOptions,
|
|
86
|
+
std::addressof(this->listener),
|
|
87
|
+
std::addressof(this->shared),
|
|
88
|
+
/*isDataChannel*/ true,
|
|
89
|
+
mayConnectOnReceivedSctpData)
|
|
90
|
+
{
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Advances the simulated clock of this association by `incrementMs`.
|
|
95
|
+
*/
|
|
96
|
+
void AdvanceTimeMs(uint64_t incrementMs)
|
|
97
|
+
{
|
|
98
|
+
this->nowMs += incrementMs;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public:
|
|
102
|
+
uint64_t nowMs{ InitialNowMs };
|
|
103
|
+
RTC::SCTP::SctpOptions sctpOptions;
|
|
104
|
+
mocks::RTC::SCTP::MockAssociationListener listener;
|
|
105
|
+
mocks::MockShared shared;
|
|
106
|
+
RTC::SCTP::Association association;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parses a previously consumed sent packet buffer into a `SCTP::Packet`.
|
|
111
|
+
*/
|
|
112
|
+
std::unique_ptr<RTC::SCTP::Packet> parsePacket(const std::vector<uint8_t>& buffer)
|
|
113
|
+
{
|
|
114
|
+
return std::unique_ptr<RTC::SCTP::Packet>(RTC::SCTP::Packet::Parse(buffer.data(), buffer.size()));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns true if the packet contains a single chunk of the given type.
|
|
119
|
+
*/
|
|
120
|
+
template<typename ChunkType>
|
|
121
|
+
bool packetHasSingleChunkOfType(const std::vector<uint8_t>& buffer)
|
|
122
|
+
{
|
|
123
|
+
const auto packet = parsePacket(buffer);
|
|
124
|
+
|
|
125
|
+
return packet && packet->GetChunksCount() == 1 &&
|
|
126
|
+
packet->GetFirstChunkOfType<ChunkType>() != nullptr;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Returns true if the packet contains a DATA or I-DATA chunk.
|
|
131
|
+
*/
|
|
132
|
+
bool packetHasDataChunk(const std::vector<uint8_t>& buffer)
|
|
133
|
+
{
|
|
134
|
+
const auto packet = parsePacket(buffer);
|
|
135
|
+
|
|
136
|
+
return packet && (packet->GetFirstChunkOfType<RTC::SCTP::DataChunk>() != nullptr ||
|
|
137
|
+
packet->GetFirstChunkOfType<RTC::SCTP::IDataChunk>() != nullptr);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Delivers a single sent packet from `from` to `to`. The packet must exist.
|
|
142
|
+
*/
|
|
143
|
+
void deliverFirstSentPacket(AssociationUnderTest& from, AssociationUnderTest& to)
|
|
144
|
+
{
|
|
145
|
+
const auto buffer = from.listener.ConsumeFirstSentPacket();
|
|
146
|
+
|
|
147
|
+
REQUIRE(!buffer.empty());
|
|
148
|
+
|
|
149
|
+
to.association.ReceiveSctpData(buffer.data(), buffer.size());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Delivers all currently queued packets between `a` and `z` back and forth
|
|
154
|
+
* until neither has anything more to send.
|
|
155
|
+
*/
|
|
156
|
+
void exchangeMessages(AssociationUnderTest& a, AssociationUnderTest& z)
|
|
157
|
+
{
|
|
158
|
+
bool deliveredPacket{ false };
|
|
159
|
+
|
|
160
|
+
do
|
|
161
|
+
{
|
|
162
|
+
deliveredPacket = false;
|
|
163
|
+
|
|
164
|
+
const auto bufferFromA = a.listener.ConsumeFirstSentPacket();
|
|
165
|
+
|
|
166
|
+
if (!bufferFromA.empty())
|
|
167
|
+
{
|
|
168
|
+
deliveredPacket = true;
|
|
169
|
+
z.association.ReceiveSctpData(bufferFromA.data(), bufferFromA.size());
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const auto bufferFromZ = z.listener.ConsumeFirstSentPacket();
|
|
173
|
+
|
|
174
|
+
if (!bufferFromZ.empty())
|
|
175
|
+
{
|
|
176
|
+
deliveredPacket = true;
|
|
177
|
+
a.association.ReceiveSctpData(bufferFromZ.data(), bufferFromZ.size());
|
|
178
|
+
}
|
|
179
|
+
} while (deliveredPacket);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Fires every Association backoff timer that has expired, looping until none
|
|
184
|
+
* remain expired.
|
|
185
|
+
*/
|
|
186
|
+
void runTimers(AssociationUnderTest& s)
|
|
187
|
+
{
|
|
188
|
+
bool fired{ false };
|
|
189
|
+
|
|
190
|
+
do
|
|
191
|
+
{
|
|
192
|
+
fired = false;
|
|
193
|
+
|
|
194
|
+
for (const auto label : SctpTimerLabels)
|
|
195
|
+
{
|
|
196
|
+
auto* timer = s.shared.GetBackoffTimer(label);
|
|
197
|
+
|
|
198
|
+
// NOTE: We must check `IsRunning()` because MockBackoffTimerHandle's
|
|
199
|
+
// `EvaluateHasExpired()` only compares times. A stopped timer whose
|
|
200
|
+
// expiry time is in the past would otherwise be fired again and again.
|
|
201
|
+
if (timer && timer->IsRunning() && timer->EvaluateHasExpired())
|
|
202
|
+
{
|
|
203
|
+
fired = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} while (fired);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Advances the simulated clock of both associations by `durationMs` and fires
|
|
211
|
+
* any timer that has expired.
|
|
212
|
+
*/
|
|
213
|
+
void advanceTimeMs(AssociationUnderTest& a, AssociationUnderTest& z, uint64_t durationMs)
|
|
214
|
+
{
|
|
215
|
+
a.AdvanceTimeMs(durationMs);
|
|
216
|
+
z.AdvanceTimeMs(durationMs);
|
|
217
|
+
|
|
218
|
+
runTimers(a);
|
|
219
|
+
runTimers(z);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Calls `Connect()` on `a` (the active peer) and drives the handshake to
|
|
224
|
+
* completion against `z` (the passive peer).
|
|
225
|
+
*/
|
|
226
|
+
void connectAssociations(AssociationUnderTest& a, AssociationUnderTest& z)
|
|
227
|
+
{
|
|
228
|
+
a.association.Connect();
|
|
229
|
+
exchangeMessages(a, z);
|
|
230
|
+
|
|
231
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
232
|
+
REQUIRE(z.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Connects `a` (active) against `z` (passive) and returns `a`'s local
|
|
237
|
+
* verification tag, i.e. the tag that the peer must put in packets sent to
|
|
238
|
+
* `a`. It's captured from the COOKIE-ACK that `z` sends to `a`, and is needed
|
|
239
|
+
* to craft raw packets to be injected into `a`.
|
|
240
|
+
*/
|
|
241
|
+
uint32_t connectAndGetVerificationTag(AssociationUnderTest& a, AssociationUnderTest& z)
|
|
242
|
+
{
|
|
243
|
+
a.association.Connect();
|
|
244
|
+
// Z reads INIT, produces INIT-ACK.
|
|
245
|
+
deliverFirstSentPacket(a, z);
|
|
246
|
+
// A reads INIT-ACK, produces COOKIE-ECHO.
|
|
247
|
+
deliverFirstSentPacket(z, a);
|
|
248
|
+
// Z reads COOKIE-ECHO, produces COOKIE-ACK.
|
|
249
|
+
deliverFirstSentPacket(a, z);
|
|
250
|
+
|
|
251
|
+
// Z now has a pending COOKIE-ACK destined to A; its verification tag is A's
|
|
252
|
+
// local verification tag.
|
|
253
|
+
const auto buffer = z.listener.ConsumeFirstSentPacket();
|
|
254
|
+
|
|
255
|
+
REQUIRE(!buffer.empty());
|
|
256
|
+
|
|
257
|
+
const uint32_t verificationTag = parsePacket(buffer)->GetVerificationTag();
|
|
258
|
+
|
|
259
|
+
// Deliver the COOKIE-ACK so the connection is fully established on A.
|
|
260
|
+
a.association.ReceiveSctpData(buffer.data(), buffer.size());
|
|
261
|
+
|
|
262
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
263
|
+
REQUIRE(z.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
264
|
+
|
|
265
|
+
return verificationTag;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Creates an empty SCTP packet addressed to a peer, carrying the given
|
|
270
|
+
* verification tag. `buffer` must outlive the returned packet, as the packet
|
|
271
|
+
* is built in place on it.
|
|
272
|
+
*/
|
|
273
|
+
std::unique_ptr<RTC::SCTP::Packet> buildPacket(std::vector<uint8_t>& buffer, uint32_t verificationTag)
|
|
274
|
+
{
|
|
275
|
+
std::unique_ptr<RTC::SCTP::Packet> packet(
|
|
276
|
+
RTC::SCTP::Packet::Factory(buffer.data(), buffer.size()));
|
|
277
|
+
|
|
278
|
+
packet->SetSourcePort(5000);
|
|
279
|
+
packet->SetDestinationPort(5000);
|
|
280
|
+
packet->SetVerificationTag(verificationTag);
|
|
281
|
+
|
|
282
|
+
return packet;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Writes the packet checksum and injects it into the given association.
|
|
287
|
+
*/
|
|
288
|
+
void injectPacket(AssociationUnderTest& target, RTC::SCTP::Packet* packet)
|
|
289
|
+
{
|
|
290
|
+
packet->WriteCRC32cChecksum();
|
|
291
|
+
|
|
292
|
+
target.association.ReceiveSctpData(packet->GetBuffer(), packet->GetLength());
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Enqueues a message to be sent on the given association.
|
|
297
|
+
*/
|
|
298
|
+
RTC::SCTP::Types::SendMessageStatus sendMessage(
|
|
299
|
+
AssociationUnderTest& s,
|
|
300
|
+
uint16_t streamId,
|
|
301
|
+
uint32_t ppid,
|
|
302
|
+
std::vector<uint8_t> payload,
|
|
303
|
+
const RTC::SCTP::SendMessageOptions& sendMessageOptions = {})
|
|
304
|
+
{
|
|
305
|
+
return s.association.SendMessage(
|
|
306
|
+
RTC::SCTP::Message(streamId, ppid, std::move(payload)), sendMessageOptions);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Consumes every received message on the given association and returns their
|
|
311
|
+
* payload protocol identifiers, in the order they were received.
|
|
312
|
+
*/
|
|
313
|
+
std::vector<uint32_t> getReceivedMessagePpids(AssociationUnderTest& s)
|
|
314
|
+
{
|
|
315
|
+
std::vector<uint32_t> ppids;
|
|
316
|
+
|
|
317
|
+
for (;;)
|
|
318
|
+
{
|
|
319
|
+
const auto message = s.listener.ConsumeFirstReceivedMessage();
|
|
320
|
+
|
|
321
|
+
if (!message.has_value())
|
|
322
|
+
{
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
ppids.push_back(message->GetPayloadProtocolId());
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return ppids;
|
|
330
|
+
}
|
|
331
|
+
} // namespace
|
|
332
|
+
|
|
333
|
+
SCENARIO("SCTP Association", "[sctp][association]")
|
|
334
|
+
{
|
|
335
|
+
SECTION("establishes connection")
|
|
336
|
+
{
|
|
337
|
+
AssociationUnderTest a;
|
|
338
|
+
AssociationUnderTest z;
|
|
339
|
+
|
|
340
|
+
a.association.Connect();
|
|
341
|
+
// Z reads INIT, produces INIT-ACK.
|
|
342
|
+
deliverFirstSentPacket(a, z);
|
|
343
|
+
// A reads INIT-ACK, produces COOKIE-ECHO.
|
|
344
|
+
deliverFirstSentPacket(z, a);
|
|
345
|
+
// Z reads COOKIE-ECHO, produces COOKIE-ACK.
|
|
346
|
+
deliverFirstSentPacket(a, z);
|
|
347
|
+
// A reads COOKIE-ACK.
|
|
348
|
+
deliverFirstSentPacket(z, a);
|
|
349
|
+
|
|
350
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
351
|
+
REQUIRE(z.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
352
|
+
REQUIRE(a.listener.IsConnected() == true);
|
|
353
|
+
REQUIRE(z.listener.IsConnected() == true);
|
|
354
|
+
REQUIRE(a.listener.HasRestarted() == false);
|
|
355
|
+
REQUIRE(z.listener.HasRestarted() == false);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
SECTION("establishes connection with setup collision")
|
|
359
|
+
{
|
|
360
|
+
AssociationUnderTest a;
|
|
361
|
+
AssociationUnderTest z;
|
|
362
|
+
|
|
363
|
+
a.association.Connect();
|
|
364
|
+
z.association.Connect();
|
|
365
|
+
|
|
366
|
+
exchangeMessages(a, z);
|
|
367
|
+
|
|
368
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
369
|
+
REQUIRE(z.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
370
|
+
REQUIRE(a.listener.HasRestarted() == false);
|
|
371
|
+
REQUIRE(z.listener.HasRestarted() == false);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
SECTION("establishes simultaneous connection")
|
|
375
|
+
{
|
|
376
|
+
AssociationUnderTest a;
|
|
377
|
+
AssociationUnderTest z;
|
|
378
|
+
|
|
379
|
+
a.association.Connect();
|
|
380
|
+
|
|
381
|
+
// INIT isn't received by Z, as it wasn't ready yet.
|
|
382
|
+
a.listener.ConsumeFirstSentPacket();
|
|
383
|
+
|
|
384
|
+
z.association.Connect();
|
|
385
|
+
|
|
386
|
+
// A reads INIT, produces INIT-ACK.
|
|
387
|
+
deliverFirstSentPacket(z, a);
|
|
388
|
+
// Z reads INIT-ACK, sends COOKIE-ECHO.
|
|
389
|
+
deliverFirstSentPacket(a, z);
|
|
390
|
+
// A reads COOKIE-ECHO, establishes connection.
|
|
391
|
+
deliverFirstSentPacket(z, a);
|
|
392
|
+
|
|
393
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
394
|
+
|
|
395
|
+
// Proceed with the remaining packets.
|
|
396
|
+
exchangeMessages(a, z);
|
|
397
|
+
|
|
398
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
399
|
+
REQUIRE(z.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
400
|
+
REQUIRE(a.listener.HasRestarted() == false);
|
|
401
|
+
REQUIRE(z.listener.HasRestarted() == false);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
SECTION("establishes connection with lost COOKIE-ACK")
|
|
405
|
+
{
|
|
406
|
+
AssociationUnderTest a;
|
|
407
|
+
AssociationUnderTest z;
|
|
408
|
+
|
|
409
|
+
a.association.Connect();
|
|
410
|
+
// Z reads INIT, produces INIT-ACK.
|
|
411
|
+
deliverFirstSentPacket(a, z);
|
|
412
|
+
// A reads INIT-ACK, produces COOKIE-ECHO.
|
|
413
|
+
deliverFirstSentPacket(z, a);
|
|
414
|
+
// Z reads COOKIE-ECHO, produces COOKIE-ACK.
|
|
415
|
+
deliverFirstSentPacket(a, z);
|
|
416
|
+
// COOKIE-ACK is lost.
|
|
417
|
+
z.listener.ConsumeFirstSentPacket();
|
|
418
|
+
|
|
419
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTING);
|
|
420
|
+
REQUIRE(z.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
421
|
+
|
|
422
|
+
// This will make A re-send the COOKIE-ECHO.
|
|
423
|
+
advanceTimeMs(a, z, a.sctpOptions.t1CookieTimeoutMs);
|
|
424
|
+
|
|
425
|
+
// Z reads COOKIE-ECHO, produces COOKIE-ACK.
|
|
426
|
+
deliverFirstSentPacket(a, z);
|
|
427
|
+
// A reads COOKIE-ACK.
|
|
428
|
+
deliverFirstSentPacket(z, a);
|
|
429
|
+
|
|
430
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
431
|
+
REQUIRE(z.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
SECTION("resends INIT and establishes connection")
|
|
435
|
+
{
|
|
436
|
+
AssociationUnderTest a;
|
|
437
|
+
AssociationUnderTest z;
|
|
438
|
+
|
|
439
|
+
a.association.Connect();
|
|
440
|
+
|
|
441
|
+
// INIT is never received by Z.
|
|
442
|
+
REQUIRE(
|
|
443
|
+
packetHasSingleChunkOfType<RTC::SCTP::InitChunk>(a.listener.ConsumeFirstSentPacket()) == true);
|
|
444
|
+
|
|
445
|
+
advanceTimeMs(a, z, a.sctpOptions.t1InitTimeoutMs);
|
|
446
|
+
|
|
447
|
+
// Z reads INIT, produces INIT-ACK.
|
|
448
|
+
deliverFirstSentPacket(a, z);
|
|
449
|
+
// A reads INIT-ACK, produces COOKIE-ECHO.
|
|
450
|
+
deliverFirstSentPacket(z, a);
|
|
451
|
+
// Z reads COOKIE-ECHO, produces COOKIE-ACK.
|
|
452
|
+
deliverFirstSentPacket(a, z);
|
|
453
|
+
// A reads COOKIE-ACK.
|
|
454
|
+
deliverFirstSentPacket(z, a);
|
|
455
|
+
|
|
456
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
457
|
+
REQUIRE(z.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
SECTION("resending INIT too many times aborts")
|
|
461
|
+
{
|
|
462
|
+
AssociationUnderTest a;
|
|
463
|
+
AssociationUnderTest z;
|
|
464
|
+
|
|
465
|
+
a.association.Connect();
|
|
466
|
+
|
|
467
|
+
// INIT is never received by Z.
|
|
468
|
+
REQUIRE(
|
|
469
|
+
packetHasSingleChunkOfType<RTC::SCTP::InitChunk>(a.listener.ConsumeFirstSentPacket()) == true);
|
|
470
|
+
|
|
471
|
+
const auto maxInitRetransmissions = a.sctpOptions.maxInitRetransmissions.value();
|
|
472
|
+
|
|
473
|
+
for (size_t i = 0; i < maxInitRetransmissions; ++i)
|
|
474
|
+
{
|
|
475
|
+
advanceTimeMs(a, z, a.sctpOptions.t1InitTimeoutMs * (1u << i));
|
|
476
|
+
|
|
477
|
+
// INIT is resent.
|
|
478
|
+
REQUIRE(
|
|
479
|
+
packetHasSingleChunkOfType<RTC::SCTP::InitChunk>(a.listener.ConsumeFirstSentPacket()) == true);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Another timeout, after the max init retransmits.
|
|
483
|
+
advanceTimeMs(a, z, a.sctpOptions.t1InitTimeoutMs * (1u << maxInitRetransmissions));
|
|
484
|
+
|
|
485
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CLOSED);
|
|
486
|
+
REQUIRE(a.listener.HasFailed() == true);
|
|
487
|
+
REQUIRE(a.listener.GetFailedErrorKind() == RTC::SCTP::Types::ErrorKind::TOO_MANY_RETRIES);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
SECTION("resends COOKIE-ECHO and establishes connection")
|
|
491
|
+
{
|
|
492
|
+
AssociationUnderTest a;
|
|
493
|
+
AssociationUnderTest z;
|
|
494
|
+
|
|
495
|
+
a.association.Connect();
|
|
496
|
+
|
|
497
|
+
// Z reads INIT, produces INIT-ACK.
|
|
498
|
+
deliverFirstSentPacket(a, z);
|
|
499
|
+
// A reads INIT-ACK, produces COOKIE-ECHO.
|
|
500
|
+
deliverFirstSentPacket(z, a);
|
|
501
|
+
|
|
502
|
+
// COOKIE-ECHO is never received by Z.
|
|
503
|
+
REQUIRE(
|
|
504
|
+
packetHasSingleChunkOfType<RTC::SCTP::CookieEchoChunk>(a.listener.ConsumeFirstSentPacket()) ==
|
|
505
|
+
true);
|
|
506
|
+
|
|
507
|
+
advanceTimeMs(a, z, a.sctpOptions.t1CookieTimeoutMs);
|
|
508
|
+
|
|
509
|
+
// Z reads COOKIE-ECHO, produces COOKIE-ACK.
|
|
510
|
+
deliverFirstSentPacket(a, z);
|
|
511
|
+
// A reads COOKIE-ACK.
|
|
512
|
+
deliverFirstSentPacket(z, a);
|
|
513
|
+
|
|
514
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
515
|
+
REQUIRE(z.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
SECTION("resending COOKIE-ECHO too many times aborts")
|
|
519
|
+
{
|
|
520
|
+
AssociationUnderTest a;
|
|
521
|
+
AssociationUnderTest z;
|
|
522
|
+
|
|
523
|
+
a.association.Connect();
|
|
524
|
+
|
|
525
|
+
// Z reads INIT, produces INIT-ACK.
|
|
526
|
+
deliverFirstSentPacket(a, z);
|
|
527
|
+
// A reads INIT-ACK, produces COOKIE-ECHO.
|
|
528
|
+
deliverFirstSentPacket(z, a);
|
|
529
|
+
|
|
530
|
+
// COOKIE-ECHO is never received by Z.
|
|
531
|
+
REQUIRE(
|
|
532
|
+
packetHasSingleChunkOfType<RTC::SCTP::CookieEchoChunk>(a.listener.ConsumeFirstSentPacket()) ==
|
|
533
|
+
true);
|
|
534
|
+
|
|
535
|
+
const auto maxInitRetransmissions = a.sctpOptions.maxInitRetransmissions.value();
|
|
536
|
+
|
|
537
|
+
for (size_t i = 0; i < maxInitRetransmissions; ++i)
|
|
538
|
+
{
|
|
539
|
+
advanceTimeMs(a, z, a.sctpOptions.t1CookieTimeoutMs * (1u << i));
|
|
540
|
+
|
|
541
|
+
// COOKIE-ECHO is resent.
|
|
542
|
+
REQUIRE(
|
|
543
|
+
packetHasSingleChunkOfType<RTC::SCTP::CookieEchoChunk>(a.listener.ConsumeFirstSentPacket()) ==
|
|
544
|
+
true);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Another timeout, after the max init retransmits.
|
|
548
|
+
advanceTimeMs(a, z, a.sctpOptions.t1CookieTimeoutMs * (1u << maxInitRetransmissions));
|
|
549
|
+
|
|
550
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CLOSED);
|
|
551
|
+
REQUIRE(a.listener.HasFailed() == true);
|
|
552
|
+
REQUIRE(a.listener.GetFailedErrorKind() == RTC::SCTP::Types::ErrorKind::TOO_MANY_RETRIES);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
SECTION("shutting down while establishing connection")
|
|
556
|
+
{
|
|
557
|
+
AssociationUnderTest a;
|
|
558
|
+
AssociationUnderTest z;
|
|
559
|
+
|
|
560
|
+
a.association.Connect();
|
|
561
|
+
|
|
562
|
+
// Z reads INIT, produces INIT-ACK.
|
|
563
|
+
deliverFirstSentPacket(a, z);
|
|
564
|
+
// A reads INIT-ACK, produces COOKIE-ECHO.
|
|
565
|
+
deliverFirstSentPacket(z, a);
|
|
566
|
+
// Z reads COOKIE-ECHO, produces COOKIE-ACK.
|
|
567
|
+
deliverFirstSentPacket(a, z);
|
|
568
|
+
// Drop COOKIE-ACK, just to more easily verify shutdown protocol.
|
|
569
|
+
z.listener.ConsumeFirstSentPacket();
|
|
570
|
+
|
|
571
|
+
// As Association A has received INIT-ACK, it has a TCB and is connected,
|
|
572
|
+
// while Association Z needs to receive COOKIE-ECHO to get there. A still
|
|
573
|
+
// has timers running at this point.
|
|
574
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTING);
|
|
575
|
+
REQUIRE(z.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
576
|
+
|
|
577
|
+
// Association A is now shut down, which should make it stop those timers.
|
|
578
|
+
a.association.Shutdown();
|
|
579
|
+
|
|
580
|
+
// Z reads SHUTDOWN, produces SHUTDOWN-ACK.
|
|
581
|
+
deliverFirstSentPacket(a, z);
|
|
582
|
+
// A reads SHUTDOWN-ACK, produces SHUTDOWN-COMPLETE.
|
|
583
|
+
deliverFirstSentPacket(z, a);
|
|
584
|
+
// Z reads SHUTDOWN-COMPLETE.
|
|
585
|
+
deliverFirstSentPacket(a, z);
|
|
586
|
+
|
|
587
|
+
REQUIRE(a.listener.ConsumeFirstSentPacket().empty() == true);
|
|
588
|
+
REQUIRE(z.listener.ConsumeFirstSentPacket().empty() == true);
|
|
589
|
+
|
|
590
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CLOSED);
|
|
591
|
+
REQUIRE(z.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CLOSED);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
SECTION("shuts down connection")
|
|
595
|
+
{
|
|
596
|
+
AssociationUnderTest a;
|
|
597
|
+
AssociationUnderTest z;
|
|
598
|
+
|
|
599
|
+
connectAssociations(a, z);
|
|
600
|
+
|
|
601
|
+
a.association.Shutdown();
|
|
602
|
+
// Z reads SHUTDOWN, produces SHUTDOWN-ACK.
|
|
603
|
+
deliverFirstSentPacket(a, z);
|
|
604
|
+
// A reads SHUTDOWN-ACK, produces SHUTDOWN-COMPLETE.
|
|
605
|
+
deliverFirstSentPacket(z, a);
|
|
606
|
+
// Z reads SHUTDOWN-COMPLETE.
|
|
607
|
+
deliverFirstSentPacket(a, z);
|
|
608
|
+
|
|
609
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CLOSED);
|
|
610
|
+
REQUIRE(z.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CLOSED);
|
|
611
|
+
REQUIRE(a.listener.IsClosed() == true);
|
|
612
|
+
REQUIRE(z.listener.IsClosed() == true);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
SECTION("shutdown timer expiring too many times closes connection")
|
|
616
|
+
{
|
|
617
|
+
AssociationUnderTest a;
|
|
618
|
+
AssociationUnderTest z;
|
|
619
|
+
|
|
620
|
+
connectAssociations(a, z);
|
|
621
|
+
|
|
622
|
+
a.association.Shutdown();
|
|
623
|
+
// Drop the first SHUTDOWN packet.
|
|
624
|
+
a.listener.ConsumeFirstSentPacket();
|
|
625
|
+
|
|
626
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::SHUTTING_DOWN);
|
|
627
|
+
|
|
628
|
+
const auto maxRetransmissions = a.sctpOptions.maxRetransmissions.value();
|
|
629
|
+
|
|
630
|
+
for (size_t i = 0; i < maxRetransmissions; ++i)
|
|
631
|
+
{
|
|
632
|
+
advanceTimeMs(a, z, a.sctpOptions.initialRtoMs * (1u << i));
|
|
633
|
+
|
|
634
|
+
// SHUTDOWN is resent (and dropped).
|
|
635
|
+
REQUIRE(
|
|
636
|
+
packetHasSingleChunkOfType<RTC::SCTP::ShutdownChunk>(a.listener.ConsumeFirstSentPacket()) ==
|
|
637
|
+
true);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// The last expiry makes it abort the connection.
|
|
641
|
+
advanceTimeMs(a, z, a.sctpOptions.initialRtoMs * (1u << maxRetransmissions));
|
|
642
|
+
|
|
643
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CLOSED);
|
|
644
|
+
REQUIRE(a.listener.IsClosed() == true);
|
|
645
|
+
REQUIRE(a.listener.GetClosedErrorKind() == RTC::SCTP::Types::ErrorKind::TOO_MANY_RETRIES);
|
|
646
|
+
// An ABORT chunk is sent.
|
|
647
|
+
REQUIRE(
|
|
648
|
+
packetHasSingleChunkOfType<RTC::SCTP::AbortAssociationChunk>(
|
|
649
|
+
a.listener.ConsumeFirstSentPacket()) == true);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
SECTION("T2-shutdown timer expiry resends SHUTDOWN-ACK")
|
|
653
|
+
{
|
|
654
|
+
AssociationUnderTest a;
|
|
655
|
+
AssociationUnderTest z;
|
|
656
|
+
|
|
657
|
+
connectAssociations(a, z);
|
|
658
|
+
|
|
659
|
+
z.association.Shutdown();
|
|
660
|
+
|
|
661
|
+
// A receives SHUTDOWN, sends SHUTDOWN-ACK. A is now in SHUTDOWN-ACK-SENT
|
|
662
|
+
// state.
|
|
663
|
+
deliverFirstSentPacket(z, a);
|
|
664
|
+
|
|
665
|
+
// Consume the SHUTDOWN-ACK sent by A (drop it).
|
|
666
|
+
REQUIRE(
|
|
667
|
+
packetHasSingleChunkOfType<RTC::SCTP::ShutdownAckChunk>(a.listener.ConsumeFirstSentPacket()) ==
|
|
668
|
+
true);
|
|
669
|
+
|
|
670
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::SHUTTING_DOWN);
|
|
671
|
+
REQUIRE(z.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::SHUTTING_DOWN);
|
|
672
|
+
|
|
673
|
+
// Advance time to trigger timer expiry, which makes A resend SHUTDOWN-ACK.
|
|
674
|
+
advanceTimeMs(a, z, a.sctpOptions.initialRtoMs);
|
|
675
|
+
|
|
676
|
+
REQUIRE(
|
|
677
|
+
packetHasSingleChunkOfType<RTC::SCTP::ShutdownAckChunk>(a.listener.ConsumeFirstSentPacket()) ==
|
|
678
|
+
true);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
SECTION("shutdown is ignored in SHUTDOWN-RECEIVED state")
|
|
682
|
+
{
|
|
683
|
+
AssociationUnderTest a;
|
|
684
|
+
AssociationUnderTest z;
|
|
685
|
+
|
|
686
|
+
connectAssociations(a, z);
|
|
687
|
+
|
|
688
|
+
z.association.Shutdown();
|
|
689
|
+
|
|
690
|
+
// A reads SHUTDOWN, produces SHUTDOWN-ACK.
|
|
691
|
+
deliverFirstSentPacket(z, a);
|
|
692
|
+
|
|
693
|
+
REQUIRE(
|
|
694
|
+
packetHasSingleChunkOfType<RTC::SCTP::ShutdownAckChunk>(a.listener.ConsumeFirstSentPacket()) ==
|
|
695
|
+
true);
|
|
696
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::SHUTTING_DOWN);
|
|
697
|
+
|
|
698
|
+
// Second shutdown while already shutting down, must be ignored.
|
|
699
|
+
a.association.Shutdown();
|
|
700
|
+
REQUIRE(a.listener.ConsumeFirstSentPacket().empty() == true);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
SECTION("shutdown is ignored in SHUTDOWN-PENDING state")
|
|
704
|
+
{
|
|
705
|
+
AssociationUnderTest a;
|
|
706
|
+
AssociationUnderTest z;
|
|
707
|
+
|
|
708
|
+
connectAssociations(a, z);
|
|
709
|
+
|
|
710
|
+
// Send a message that will remain outstanding (unacknowledged) which
|
|
711
|
+
// transitions the association to SHUTDOWN-PENDING instead of SHUTDOWN-SENT.
|
|
712
|
+
sendMessage(a, 1, 53, { 1, 2 });
|
|
713
|
+
a.association.Shutdown();
|
|
714
|
+
|
|
715
|
+
REQUIRE(packetHasDataChunk(a.listener.ConsumeFirstSentPacket()) == true);
|
|
716
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::SHUTTING_DOWN);
|
|
717
|
+
|
|
718
|
+
// Second shutdown while already shutting down, must be ignored.
|
|
719
|
+
a.association.Shutdown();
|
|
720
|
+
REQUIRE(a.listener.ConsumeFirstSentPacket().empty() == true);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
SECTION("establishes connection while sending data")
|
|
724
|
+
{
|
|
725
|
+
AssociationUnderTest a;
|
|
726
|
+
AssociationUnderTest z;
|
|
727
|
+
|
|
728
|
+
a.association.Connect();
|
|
729
|
+
|
|
730
|
+
sendMessage(a, 1, 53, { 1, 2 });
|
|
731
|
+
|
|
732
|
+
// Z reads INIT, produces INIT-ACK.
|
|
733
|
+
deliverFirstSentPacket(a, z);
|
|
734
|
+
// A reads INIT-ACK, produces COOKIE-ECHO.
|
|
735
|
+
deliverFirstSentPacket(z, a);
|
|
736
|
+
// Z reads COOKIE-ECHO, produces COOKIE-ACK.
|
|
737
|
+
deliverFirstSentPacket(a, z);
|
|
738
|
+
// A reads COOKIE-ACK.
|
|
739
|
+
deliverFirstSentPacket(z, a);
|
|
740
|
+
|
|
741
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
742
|
+
REQUIRE(z.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
743
|
+
|
|
744
|
+
// Deliver the remaining packets so that the message reaches Z.
|
|
745
|
+
exchangeMessages(a, z);
|
|
746
|
+
|
|
747
|
+
const auto message = z.listener.ConsumeFirstReceivedMessage();
|
|
748
|
+
|
|
749
|
+
REQUIRE(message.has_value() == true);
|
|
750
|
+
REQUIRE(message->GetStreamId() == 1);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
SECTION("sends a message after established")
|
|
754
|
+
{
|
|
755
|
+
AssociationUnderTest a;
|
|
756
|
+
AssociationUnderTest z;
|
|
757
|
+
|
|
758
|
+
connectAssociations(a, z);
|
|
759
|
+
|
|
760
|
+
sendMessage(a, 1, 53, { 1, 2 });
|
|
761
|
+
// Z reads the DATA chunk.
|
|
762
|
+
deliverFirstSentPacket(a, z);
|
|
763
|
+
|
|
764
|
+
const auto message = z.listener.ConsumeFirstReceivedMessage();
|
|
765
|
+
|
|
766
|
+
REQUIRE(message.has_value() == true);
|
|
767
|
+
REQUIRE(message->GetStreamId() == 1);
|
|
768
|
+
REQUIRE(message->GetPayloadProtocolId() == 53);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
SECTION("resends a packet when the T3-RTX timer expires")
|
|
772
|
+
{
|
|
773
|
+
AssociationUnderTest a;
|
|
774
|
+
AssociationUnderTest z;
|
|
775
|
+
|
|
776
|
+
connectAssociations(a, z);
|
|
777
|
+
|
|
778
|
+
sendMessage(a, 1, 53, { 1, 2 });
|
|
779
|
+
// Drop the first DATA packet.
|
|
780
|
+
a.listener.ConsumeFirstSentPacket();
|
|
781
|
+
|
|
782
|
+
// Advance time so that the T3-RTX timer expires and the DATA is resent.
|
|
783
|
+
advanceTimeMs(a, z, a.sctpOptions.initialRtoMs);
|
|
784
|
+
|
|
785
|
+
// Z reads the retransmitted DATA chunk.
|
|
786
|
+
deliverFirstSentPacket(a, z);
|
|
787
|
+
|
|
788
|
+
const auto message = z.listener.ConsumeFirstReceivedMessage();
|
|
789
|
+
|
|
790
|
+
REQUIRE(message.has_value() == true);
|
|
791
|
+
REQUIRE(message->GetStreamId() == 1);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
SECTION("sends a lot of bytes with a missed packet")
|
|
795
|
+
{
|
|
796
|
+
AssociationUnderTest a;
|
|
797
|
+
AssociationUnderTest z;
|
|
798
|
+
|
|
799
|
+
connectAssociations(a, z);
|
|
800
|
+
|
|
801
|
+
const std::vector<uint8_t> payload(a.sctpOptions.mtu * 20);
|
|
802
|
+
|
|
803
|
+
sendMessage(a, 1, 53, payload);
|
|
804
|
+
|
|
805
|
+
// Z reads the first DATA.
|
|
806
|
+
deliverFirstSentPacket(a, z);
|
|
807
|
+
// Second DATA is lost.
|
|
808
|
+
a.listener.ConsumeFirstSentPacket();
|
|
809
|
+
|
|
810
|
+
// Retransmit and handle the rest.
|
|
811
|
+
exchangeMessages(a, z);
|
|
812
|
+
|
|
813
|
+
const auto message = z.listener.ConsumeFirstReceivedMessage();
|
|
814
|
+
|
|
815
|
+
REQUIRE(message.has_value() == true);
|
|
816
|
+
REQUIRE(message->GetStreamId() == 1);
|
|
817
|
+
REQUIRE(message->GetPayloadLength() == payload.size());
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
SECTION("sends a heartbeat on idle connection")
|
|
821
|
+
{
|
|
822
|
+
AssociationUnderTest a;
|
|
823
|
+
AssociationUnderTest z;
|
|
824
|
+
|
|
825
|
+
connectAssociations(a, z);
|
|
826
|
+
|
|
827
|
+
REQUIRE(a.listener.ConsumeFirstSentPacket().empty() == true);
|
|
828
|
+
|
|
829
|
+
// Let the heartbeat interval timer expire.
|
|
830
|
+
advanceTimeMs(a, z, a.sctpOptions.heartbeatIntervalMs);
|
|
831
|
+
|
|
832
|
+
const auto buffer = a.listener.ConsumeFirstSentPacket();
|
|
833
|
+
|
|
834
|
+
REQUIRE(packetHasSingleChunkOfType<RTC::SCTP::HeartbeatRequestChunk>(buffer) == true);
|
|
835
|
+
|
|
836
|
+
// Feed it to Z and expect a HEARTBEAT-ACK that is propagated back to A.
|
|
837
|
+
z.association.ReceiveSctpData(buffer.data(), buffer.size());
|
|
838
|
+
deliverFirstSentPacket(z, a);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
SECTION("sends many messages")
|
|
842
|
+
{
|
|
843
|
+
AssociationUnderTest a;
|
|
844
|
+
AssociationUnderTest z;
|
|
845
|
+
|
|
846
|
+
connectAssociations(a, z);
|
|
847
|
+
|
|
848
|
+
constexpr int Iterations{ 100 };
|
|
849
|
+
|
|
850
|
+
std::vector<RTC::SCTP::Message> messages;
|
|
851
|
+
|
|
852
|
+
messages.reserve(Iterations);
|
|
853
|
+
|
|
854
|
+
for (int i = 0; i < Iterations; ++i)
|
|
855
|
+
{
|
|
856
|
+
messages.emplace_back(/*streamId*/ 1, /*ppid*/ 53, std::vector<uint8_t>{ 1, 2 });
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const auto statuses = a.association.SendManyMessages(messages, /*sendMessageOptions*/ {});
|
|
860
|
+
|
|
861
|
+
REQUIRE(statuses.size() == Iterations);
|
|
862
|
+
|
|
863
|
+
for (const auto status : statuses)
|
|
864
|
+
{
|
|
865
|
+
REQUIRE(status == RTC::SCTP::Types::SendMessageStatus::SUCCESS);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
exchangeMessages(a, z);
|
|
869
|
+
|
|
870
|
+
for (int i = 0; i < Iterations; ++i)
|
|
871
|
+
{
|
|
872
|
+
REQUIRE(z.listener.ConsumeFirstReceivedMessage().has_value() == true);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
REQUIRE(z.listener.ConsumeFirstReceivedMessage().has_value() == false);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
SECTION("initial metrics are unset")
|
|
879
|
+
{
|
|
880
|
+
const AssociationUnderTest a;
|
|
881
|
+
|
|
882
|
+
REQUIRE(a.association.MakeMetrics().has_value() == false);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
SECTION("message interleaving metrics are set")
|
|
886
|
+
{
|
|
887
|
+
for (const bool aEnable : { false, true })
|
|
888
|
+
{
|
|
889
|
+
for (const bool zEnable : { false, true })
|
|
890
|
+
{
|
|
891
|
+
auto aSctpOptions = makeSctpOptions();
|
|
892
|
+
|
|
893
|
+
aSctpOptions.enableMessageInterleaving = aEnable;
|
|
894
|
+
|
|
895
|
+
auto zSctpOptions = makeSctpOptions();
|
|
896
|
+
|
|
897
|
+
zSctpOptions.enableMessageInterleaving = zEnable;
|
|
898
|
+
|
|
899
|
+
AssociationUnderTest a(aSctpOptions);
|
|
900
|
+
AssociationUnderTest z(zSctpOptions);
|
|
901
|
+
|
|
902
|
+
connectAssociations(a, z);
|
|
903
|
+
|
|
904
|
+
REQUIRE(a.association.MakeMetrics()->usesMessageInterleaving == (aEnable && zEnable));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
SECTION("rx and tx packet metrics increase")
|
|
910
|
+
{
|
|
911
|
+
AssociationUnderTest a;
|
|
912
|
+
AssociationUnderTest z;
|
|
913
|
+
|
|
914
|
+
connectAssociations(a, z);
|
|
915
|
+
|
|
916
|
+
// A sent INIT and COOKIE-ECHO, received INIT-ACK and COOKIE-ACK.
|
|
917
|
+
REQUIRE(a.association.MakeMetrics()->txPacketsCount == 2);
|
|
918
|
+
REQUIRE(a.association.MakeMetrics()->rxPacketsCount == 2);
|
|
919
|
+
REQUIRE(a.association.MakeMetrics()->txMessagesCount == 0);
|
|
920
|
+
REQUIRE(
|
|
921
|
+
a.association.MakeMetrics()->cwndBytes == a.sctpOptions.initialCwndMtus * a.sctpOptions.mtu);
|
|
922
|
+
REQUIRE(a.association.MakeMetrics()->unackDataCount == 0);
|
|
923
|
+
|
|
924
|
+
REQUIRE(z.association.MakeMetrics()->rxPacketsCount == 2);
|
|
925
|
+
REQUIRE(z.association.MakeMetrics()->rxMessagesCount == 0);
|
|
926
|
+
|
|
927
|
+
sendMessage(a, 1, 53, { 1, 2 });
|
|
928
|
+
|
|
929
|
+
REQUIRE(a.association.MakeMetrics()->unackDataCount == 1);
|
|
930
|
+
|
|
931
|
+
// Z reads DATA.
|
|
932
|
+
deliverFirstSentPacket(a, z);
|
|
933
|
+
// A reads SACK.
|
|
934
|
+
deliverFirstSentPacket(z, a);
|
|
935
|
+
|
|
936
|
+
REQUIRE(a.association.MakeMetrics()->unackDataCount == 0);
|
|
937
|
+
REQUIRE(z.listener.ConsumeFirstReceivedMessage().has_value() == true);
|
|
938
|
+
|
|
939
|
+
REQUIRE(a.association.MakeMetrics()->txPacketsCount == 3);
|
|
940
|
+
REQUIRE(a.association.MakeMetrics()->rxPacketsCount == 3);
|
|
941
|
+
REQUIRE(a.association.MakeMetrics()->txMessagesCount == 1);
|
|
942
|
+
|
|
943
|
+
REQUIRE(z.association.MakeMetrics()->rxPacketsCount == 3);
|
|
944
|
+
REQUIRE(z.association.MakeMetrics()->rxMessagesCount == 1);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
SECTION("streams have initial priority")
|
|
948
|
+
{
|
|
949
|
+
auto sctpOptions = makeSctpOptions();
|
|
950
|
+
|
|
951
|
+
sctpOptions.defaultStreamPriority = 42;
|
|
952
|
+
|
|
953
|
+
AssociationUnderTest a(sctpOptions);
|
|
954
|
+
|
|
955
|
+
REQUIRE(a.association.GetStreamPriority(1) == 42);
|
|
956
|
+
|
|
957
|
+
sendMessage(a, 2, 53, { 1, 2 });
|
|
958
|
+
|
|
959
|
+
REQUIRE(a.association.GetStreamPriority(2) == 42);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
SECTION("can change stream priority")
|
|
963
|
+
{
|
|
964
|
+
auto sctpOptions = makeSctpOptions();
|
|
965
|
+
|
|
966
|
+
sctpOptions.defaultStreamPriority = 42;
|
|
967
|
+
|
|
968
|
+
AssociationUnderTest a(sctpOptions);
|
|
969
|
+
|
|
970
|
+
a.association.SetStreamPriority(1, 43);
|
|
971
|
+
|
|
972
|
+
REQUIRE(a.association.GetStreamPriority(1) == 43);
|
|
973
|
+
|
|
974
|
+
sendMessage(a, 2, 53, { 1, 2 });
|
|
975
|
+
|
|
976
|
+
a.association.SetStreamPriority(2, 43);
|
|
977
|
+
|
|
978
|
+
REQUIRE(a.association.GetStreamPriority(2) == 43);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
SECTION("respects the per-stream queue limit")
|
|
982
|
+
{
|
|
983
|
+
auto sctpOptions = makeSctpOptions();
|
|
984
|
+
|
|
985
|
+
sctpOptions.maxSendBufferSize = 4000;
|
|
986
|
+
sctpOptions.perStreamSendQueueLimit = 1000;
|
|
987
|
+
|
|
988
|
+
AssociationUnderTest a(sctpOptions);
|
|
989
|
+
|
|
990
|
+
REQUIRE(
|
|
991
|
+
sendMessage(a, 1, 53, std::vector<uint8_t>(600)) ==
|
|
992
|
+
RTC::SCTP::Types::SendMessageStatus::SUCCESS);
|
|
993
|
+
REQUIRE(
|
|
994
|
+
sendMessage(a, 1, 53, std::vector<uint8_t>(600)) ==
|
|
995
|
+
RTC::SCTP::Types::SendMessageStatus::SUCCESS);
|
|
996
|
+
REQUIRE(
|
|
997
|
+
sendMessage(a, 1, 53, std::vector<uint8_t>(600)) ==
|
|
998
|
+
RTC::SCTP::Types::SendMessageStatus::ERROR_RESOURCE_EXHAUSTION);
|
|
999
|
+
// The per-stream limit for stream 1 is reached, but not for stream 2.
|
|
1000
|
+
REQUIRE(
|
|
1001
|
+
sendMessage(a, 2, 53, std::vector<uint8_t>(600)) ==
|
|
1002
|
+
RTC::SCTP::Types::SendMessageStatus::SUCCESS);
|
|
1003
|
+
REQUIRE(
|
|
1004
|
+
sendMessage(a, 2, 53, std::vector<uint8_t>(600)) ==
|
|
1005
|
+
RTC::SCTP::Types::SendMessageStatus::SUCCESS);
|
|
1006
|
+
REQUIRE(
|
|
1007
|
+
sendMessage(a, 2, 53, std::vector<uint8_t>(600)) ==
|
|
1008
|
+
RTC::SCTP::Types::SendMessageStatus::ERROR_RESOURCE_EXHAUSTION);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
SECTION("has reasonable buffered amount values")
|
|
1012
|
+
{
|
|
1013
|
+
AssociationUnderTest a;
|
|
1014
|
+
AssociationUnderTest z;
|
|
1015
|
+
|
|
1016
|
+
connectAssociations(a, z);
|
|
1017
|
+
|
|
1018
|
+
REQUIRE(a.association.GetStreamBufferedAmount(1) == 0);
|
|
1019
|
+
|
|
1020
|
+
// Sending a small message will directly send it as a single packet, so
|
|
1021
|
+
// nothing is left in the queue.
|
|
1022
|
+
sendMessage(a, 1, 53, std::vector<uint8_t>(10));
|
|
1023
|
+
|
|
1024
|
+
REQUIRE(a.association.GetStreamBufferedAmount(1) == 0);
|
|
1025
|
+
|
|
1026
|
+
// Sending a large message will directly start sending a few packets, so the
|
|
1027
|
+
// buffered amount is not the full message size.
|
|
1028
|
+
const size_t largeSize = a.sctpOptions.mtu * 20;
|
|
1029
|
+
|
|
1030
|
+
sendMessage(a, 1, 53, std::vector<uint8_t>(largeSize));
|
|
1031
|
+
|
|
1032
|
+
REQUIRE(a.association.GetStreamBufferedAmount(1) > 0);
|
|
1033
|
+
REQUIRE(a.association.GetStreamBufferedAmount(1) < largeSize);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
SECTION("has a default buffered amount low threshold of zero")
|
|
1037
|
+
{
|
|
1038
|
+
const AssociationUnderTest a;
|
|
1039
|
+
|
|
1040
|
+
REQUIRE(a.association.GetStreamBufferedAmountLowThreshold(1) == 0);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
SECTION("triggers OnAssociationStreamBufferedAmountLow with default threshold zero")
|
|
1044
|
+
{
|
|
1045
|
+
AssociationUnderTest a;
|
|
1046
|
+
AssociationUnderTest z;
|
|
1047
|
+
|
|
1048
|
+
REQUIRE(a.listener.HasOnStreamBufferedAmountLowBeenCalledWithStreamId(1) == false);
|
|
1049
|
+
|
|
1050
|
+
connectAssociations(a, z);
|
|
1051
|
+
|
|
1052
|
+
sendMessage(a, 1, 53, std::vector<uint8_t>(10));
|
|
1053
|
+
exchangeMessages(a, z);
|
|
1054
|
+
|
|
1055
|
+
REQUIRE(a.listener.HasOnStreamBufferedAmountLowBeenCalledWithStreamId(1) == true);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
SECTION("detects the peer implementation")
|
|
1059
|
+
{
|
|
1060
|
+
AssociationUnderTest a;
|
|
1061
|
+
AssociationUnderTest z;
|
|
1062
|
+
|
|
1063
|
+
connectAssociations(a, z);
|
|
1064
|
+
|
|
1065
|
+
// Both peers are mediasoup.
|
|
1066
|
+
REQUIRE(
|
|
1067
|
+
a.association.MakeMetrics()->peerImplementation ==
|
|
1068
|
+
RTC::SCTP::Types::SctpImplementation::MEDIASOUP);
|
|
1069
|
+
// As A initiated the connection establishment, the passive peer Z will not
|
|
1070
|
+
// receive enough information to know about A's implementation.
|
|
1071
|
+
REQUIRE(
|
|
1072
|
+
z.association.MakeMetrics()->peerImplementation ==
|
|
1073
|
+
RTC::SCTP::Types::SctpImplementation::UNKNOWN);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
SECTION("both peers detect the peer implementation")
|
|
1077
|
+
{
|
|
1078
|
+
AssociationUnderTest a;
|
|
1079
|
+
AssociationUnderTest z;
|
|
1080
|
+
|
|
1081
|
+
a.association.Connect();
|
|
1082
|
+
z.association.Connect();
|
|
1083
|
+
|
|
1084
|
+
exchangeMessages(a, z);
|
|
1085
|
+
|
|
1086
|
+
REQUIRE(
|
|
1087
|
+
a.association.MakeMetrics()->peerImplementation ==
|
|
1088
|
+
RTC::SCTP::Types::SctpImplementation::MEDIASOUP);
|
|
1089
|
+
REQUIRE(
|
|
1090
|
+
z.association.MakeMetrics()->peerImplementation ==
|
|
1091
|
+
RTC::SCTP::Types::SctpImplementation::MEDIASOUP);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
SECTION("exposes the number of negotiated streams")
|
|
1095
|
+
{
|
|
1096
|
+
auto aSctpOptions = makeSctpOptions();
|
|
1097
|
+
|
|
1098
|
+
aSctpOptions.announcedMaxInboundStreams = 12;
|
|
1099
|
+
aSctpOptions.announcedMaxOutboundStreams = 45;
|
|
1100
|
+
|
|
1101
|
+
auto zSctpOptions = makeSctpOptions();
|
|
1102
|
+
|
|
1103
|
+
zSctpOptions.announcedMaxInboundStreams = 23;
|
|
1104
|
+
zSctpOptions.announcedMaxOutboundStreams = 34;
|
|
1105
|
+
|
|
1106
|
+
AssociationUnderTest a(aSctpOptions);
|
|
1107
|
+
AssociationUnderTest z(zSctpOptions);
|
|
1108
|
+
|
|
1109
|
+
connectAssociations(a, z);
|
|
1110
|
+
|
|
1111
|
+
const auto metricsA = a.association.MakeMetrics();
|
|
1112
|
+
|
|
1113
|
+
REQUIRE(metricsA->negotiatedMaxInboundStreams == 12);
|
|
1114
|
+
REQUIRE(metricsA->negotiatedMaxOutboundStreams == 23);
|
|
1115
|
+
|
|
1116
|
+
const auto metricsZ = z.association.MakeMetrics();
|
|
1117
|
+
|
|
1118
|
+
REQUIRE(metricsZ->negotiatedMaxInboundStreams == 23);
|
|
1119
|
+
REQUIRE(metricsZ->negotiatedMaxOutboundStreams == 12);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
SECTION("always sends INIT with non-zero checksum")
|
|
1123
|
+
{
|
|
1124
|
+
auto sctpOptions = makeSctpOptions();
|
|
1125
|
+
|
|
1126
|
+
sctpOptions.zeroChecksumAlternateErrorDetectionMethod =
|
|
1127
|
+
RTC::SCTP::ZeroChecksumAcceptableParameter::AlternateErrorDetectionMethod::SCTP_OVER_DTLS;
|
|
1128
|
+
|
|
1129
|
+
AssociationUnderTest a(sctpOptions);
|
|
1130
|
+
|
|
1131
|
+
a.association.Connect();
|
|
1132
|
+
|
|
1133
|
+
const auto buffer = a.listener.ConsumeFirstSentPacket();
|
|
1134
|
+
|
|
1135
|
+
REQUIRE(packetHasSingleChunkOfType<RTC::SCTP::InitChunk>(buffer) == true);
|
|
1136
|
+
REQUIRE(parsePacket(buffer)->GetChecksum() != 0);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
SECTION("may send INIT-ACK with zero checksum")
|
|
1140
|
+
{
|
|
1141
|
+
auto sctpOptions = makeSctpOptions();
|
|
1142
|
+
|
|
1143
|
+
sctpOptions.zeroChecksumAlternateErrorDetectionMethod =
|
|
1144
|
+
RTC::SCTP::ZeroChecksumAcceptableParameter::AlternateErrorDetectionMethod::SCTP_OVER_DTLS;
|
|
1145
|
+
|
|
1146
|
+
AssociationUnderTest a(sctpOptions);
|
|
1147
|
+
AssociationUnderTest z(sctpOptions);
|
|
1148
|
+
|
|
1149
|
+
a.association.Connect();
|
|
1150
|
+
// Z reads INIT, produces INIT-ACK.
|
|
1151
|
+
deliverFirstSentPacket(a, z);
|
|
1152
|
+
|
|
1153
|
+
const auto buffer = z.listener.ConsumeFirstSentPacket();
|
|
1154
|
+
|
|
1155
|
+
REQUIRE(packetHasSingleChunkOfType<RTC::SCTP::InitAckChunk>(buffer) == true);
|
|
1156
|
+
REQUIRE(parsePacket(buffer)->GetChecksum() == 0);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
SECTION("always sends COOKIE-ECHO with non-zero checksum")
|
|
1160
|
+
{
|
|
1161
|
+
auto sctpOptions = makeSctpOptions();
|
|
1162
|
+
|
|
1163
|
+
sctpOptions.zeroChecksumAlternateErrorDetectionMethod =
|
|
1164
|
+
RTC::SCTP::ZeroChecksumAcceptableParameter::AlternateErrorDetectionMethod::SCTP_OVER_DTLS;
|
|
1165
|
+
|
|
1166
|
+
AssociationUnderTest a(sctpOptions);
|
|
1167
|
+
AssociationUnderTest z(sctpOptions);
|
|
1168
|
+
|
|
1169
|
+
a.association.Connect();
|
|
1170
|
+
// Z reads INIT, produces INIT-ACK.
|
|
1171
|
+
deliverFirstSentPacket(a, z);
|
|
1172
|
+
// A reads INIT-ACK, produces COOKIE-ECHO.
|
|
1173
|
+
deliverFirstSentPacket(z, a);
|
|
1174
|
+
|
|
1175
|
+
const auto buffer = a.listener.ConsumeFirstSentPacket();
|
|
1176
|
+
|
|
1177
|
+
REQUIRE(packetHasSingleChunkOfType<RTC::SCTP::CookieEchoChunk>(buffer) == true);
|
|
1178
|
+
REQUIRE(parsePacket(buffer)->GetChecksum() != 0);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
SECTION("sends COOKIE-ACK with zero checksum")
|
|
1182
|
+
{
|
|
1183
|
+
auto sctpOptions = makeSctpOptions();
|
|
1184
|
+
|
|
1185
|
+
sctpOptions.zeroChecksumAlternateErrorDetectionMethod =
|
|
1186
|
+
RTC::SCTP::ZeroChecksumAcceptableParameter::AlternateErrorDetectionMethod::SCTP_OVER_DTLS;
|
|
1187
|
+
|
|
1188
|
+
AssociationUnderTest a(sctpOptions);
|
|
1189
|
+
AssociationUnderTest z(sctpOptions);
|
|
1190
|
+
|
|
1191
|
+
a.association.Connect();
|
|
1192
|
+
// Z reads INIT, produces INIT-ACK.
|
|
1193
|
+
deliverFirstSentPacket(a, z);
|
|
1194
|
+
// A reads INIT-ACK, produces COOKIE-ECHO.
|
|
1195
|
+
deliverFirstSentPacket(z, a);
|
|
1196
|
+
// Z reads COOKIE-ECHO, produces COOKIE-ACK.
|
|
1197
|
+
deliverFirstSentPacket(a, z);
|
|
1198
|
+
|
|
1199
|
+
const auto buffer = z.listener.ConsumeFirstSentPacket();
|
|
1200
|
+
|
|
1201
|
+
REQUIRE(packetHasSingleChunkOfType<RTC::SCTP::CookieAckChunk>(buffer) == true);
|
|
1202
|
+
REQUIRE(parsePacket(buffer)->GetChecksum() == 0);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
SECTION("sends DATA with zero checksum")
|
|
1206
|
+
{
|
|
1207
|
+
auto sctpOptions = makeSctpOptions();
|
|
1208
|
+
|
|
1209
|
+
sctpOptions.zeroChecksumAlternateErrorDetectionMethod =
|
|
1210
|
+
RTC::SCTP::ZeroChecksumAcceptableParameter::AlternateErrorDetectionMethod::SCTP_OVER_DTLS;
|
|
1211
|
+
|
|
1212
|
+
AssociationUnderTest a(sctpOptions);
|
|
1213
|
+
AssociationUnderTest z(sctpOptions);
|
|
1214
|
+
|
|
1215
|
+
connectAssociations(a, z);
|
|
1216
|
+
|
|
1217
|
+
sendMessage(a, 1, 53, std::vector<uint8_t>(a.sctpOptions.mtu - 100));
|
|
1218
|
+
|
|
1219
|
+
const auto buffer = a.listener.ConsumeFirstSentPacket();
|
|
1220
|
+
|
|
1221
|
+
REQUIRE(packetHasDataChunk(buffer) == true);
|
|
1222
|
+
REQUIRE(parsePacket(buffer)->GetChecksum() == 0);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
SECTION("both sides send heartbeats")
|
|
1226
|
+
{
|
|
1227
|
+
// Make them have slightly different heartbeat intervals, to validate that
|
|
1228
|
+
// sending an ack by Z doesn't restart its heartbeat timer.
|
|
1229
|
+
auto aSctpOptions = makeSctpOptions();
|
|
1230
|
+
|
|
1231
|
+
aSctpOptions.heartbeatIntervalMs = 1000;
|
|
1232
|
+
|
|
1233
|
+
auto zSctpOptions = makeSctpOptions();
|
|
1234
|
+
|
|
1235
|
+
zSctpOptions.heartbeatIntervalMs = 1100;
|
|
1236
|
+
|
|
1237
|
+
AssociationUnderTest a(aSctpOptions);
|
|
1238
|
+
AssociationUnderTest z(zSctpOptions);
|
|
1239
|
+
|
|
1240
|
+
connectAssociations(a, z);
|
|
1241
|
+
|
|
1242
|
+
advanceTimeMs(a, z, 1000);
|
|
1243
|
+
|
|
1244
|
+
const auto bufferA = a.listener.ConsumeFirstSentPacket();
|
|
1245
|
+
|
|
1246
|
+
REQUIRE(packetHasSingleChunkOfType<RTC::SCTP::HeartbeatRequestChunk>(bufferA) == true);
|
|
1247
|
+
|
|
1248
|
+
// Z receives the heartbeat and sends an ACK that is propagated back to A.
|
|
1249
|
+
z.association.ReceiveSctpData(bufferA.data(), bufferA.size());
|
|
1250
|
+
deliverFirstSentPacket(z, a);
|
|
1251
|
+
|
|
1252
|
+
// A little while later, Z should send heartbeats to A.
|
|
1253
|
+
advanceTimeMs(a, z, 100);
|
|
1254
|
+
|
|
1255
|
+
const auto bufferZ = z.listener.ConsumeFirstSentPacket();
|
|
1256
|
+
|
|
1257
|
+
REQUIRE(packetHasSingleChunkOfType<RTC::SCTP::HeartbeatRequestChunk>(bufferZ) == true);
|
|
1258
|
+
|
|
1259
|
+
// A receives the heartbeat and sends an ACK that is propagated back to Z.
|
|
1260
|
+
a.association.ReceiveSctpData(bufferZ.data(), bufferZ.size());
|
|
1261
|
+
deliverFirstSentPacket(a, z);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
SECTION("closes connection after too many lost heartbeats")
|
|
1265
|
+
{
|
|
1266
|
+
AssociationUnderTest a;
|
|
1267
|
+
// Disable Z's heartbeats so that it doesn't interfere.
|
|
1268
|
+
auto zSctpOptions = makeSctpOptions();
|
|
1269
|
+
|
|
1270
|
+
zSctpOptions.heartbeatIntervalMs = 0;
|
|
1271
|
+
|
|
1272
|
+
AssociationUnderTest z(zSctpOptions);
|
|
1273
|
+
|
|
1274
|
+
connectAssociations(a, z);
|
|
1275
|
+
|
|
1276
|
+
REQUIRE(a.listener.ConsumeFirstSentPacket().empty() == true);
|
|
1277
|
+
|
|
1278
|
+
const auto maxRetransmissions = a.sctpOptions.maxRetransmissions.value();
|
|
1279
|
+
|
|
1280
|
+
uint64_t timeToNextHeartbeatMs = a.sctpOptions.heartbeatIntervalMs;
|
|
1281
|
+
|
|
1282
|
+
for (size_t i = 0; i < maxRetransmissions; ++i)
|
|
1283
|
+
{
|
|
1284
|
+
// Let the heartbeat interval timer expire, sending a heartbeat.
|
|
1285
|
+
advanceTimeMs(a, z, timeToNextHeartbeatMs);
|
|
1286
|
+
|
|
1287
|
+
// Drop every heartbeat.
|
|
1288
|
+
REQUIRE(
|
|
1289
|
+
packetHasSingleChunkOfType<RTC::SCTP::HeartbeatRequestChunk>(
|
|
1290
|
+
a.listener.ConsumeFirstSentPacket()) == true);
|
|
1291
|
+
|
|
1292
|
+
// Let the heartbeat timeout expire.
|
|
1293
|
+
advanceTimeMs(a, z, 1000);
|
|
1294
|
+
|
|
1295
|
+
timeToNextHeartbeatMs = a.sctpOptions.heartbeatIntervalMs - 1000;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// The last heartbeat.
|
|
1299
|
+
advanceTimeMs(a, z, timeToNextHeartbeatMs);
|
|
1300
|
+
|
|
1301
|
+
REQUIRE(a.listener.HasSentPackets() == true);
|
|
1302
|
+
|
|
1303
|
+
a.listener.ConsumeFirstSentPacket();
|
|
1304
|
+
|
|
1305
|
+
// Should suffice as exceeding RTO, which aborts the connection.
|
|
1306
|
+
advanceTimeMs(a, z, 1000);
|
|
1307
|
+
|
|
1308
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CLOSED);
|
|
1309
|
+
REQUIRE(a.listener.IsClosed() == true);
|
|
1310
|
+
REQUIRE(a.listener.GetClosedErrorKind() == RTC::SCTP::Types::ErrorKind::TOO_MANY_RETRIES);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
SECTION("recovers after a successful heartbeat ack")
|
|
1314
|
+
{
|
|
1315
|
+
AssociationUnderTest a;
|
|
1316
|
+
// Disable Z's heartbeats so that it doesn't interfere.
|
|
1317
|
+
auto zSctpOptions = makeSctpOptions();
|
|
1318
|
+
|
|
1319
|
+
zSctpOptions.heartbeatIntervalMs = 0;
|
|
1320
|
+
|
|
1321
|
+
AssociationUnderTest z(zSctpOptions);
|
|
1322
|
+
|
|
1323
|
+
connectAssociations(a, z);
|
|
1324
|
+
|
|
1325
|
+
REQUIRE(a.listener.ConsumeFirstSentPacket().empty() == true);
|
|
1326
|
+
|
|
1327
|
+
const auto maxRetransmissions = a.sctpOptions.maxRetransmissions.value();
|
|
1328
|
+
|
|
1329
|
+
uint64_t timeToNextHeartbeatMs = a.sctpOptions.heartbeatIntervalMs;
|
|
1330
|
+
|
|
1331
|
+
for (size_t i = 0; i < maxRetransmissions; ++i)
|
|
1332
|
+
{
|
|
1333
|
+
advanceTimeMs(a, z, timeToNextHeartbeatMs);
|
|
1334
|
+
|
|
1335
|
+
// Drop every heartbeat.
|
|
1336
|
+
a.listener.ConsumeFirstSentPacket();
|
|
1337
|
+
|
|
1338
|
+
// Let the heartbeat timeout expire.
|
|
1339
|
+
advanceTimeMs(a, z, 1000);
|
|
1340
|
+
|
|
1341
|
+
timeToNextHeartbeatMs = a.sctpOptions.heartbeatIntervalMs - 1000;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// Get the last heartbeat and ack it (round trip through Z).
|
|
1345
|
+
advanceTimeMs(a, z, timeToNextHeartbeatMs);
|
|
1346
|
+
|
|
1347
|
+
const auto buffer = a.listener.ConsumeFirstSentPacket();
|
|
1348
|
+
|
|
1349
|
+
REQUIRE(packetHasSingleChunkOfType<RTC::SCTP::HeartbeatRequestChunk>(buffer) == true);
|
|
1350
|
+
|
|
1351
|
+
z.association.ReceiveSctpData(buffer.data(), buffer.size());
|
|
1352
|
+
// A reads the HEARTBEAT-ACK, which clears the error counter.
|
|
1353
|
+
deliverFirstSentPacket(z, a);
|
|
1354
|
+
|
|
1355
|
+
// Should suffice as exceeding RTO, but the timer will not fire as it was
|
|
1356
|
+
// stopped by the ack.
|
|
1357
|
+
advanceTimeMs(a, z, 1000);
|
|
1358
|
+
|
|
1359
|
+
REQUIRE(a.association.GetAssociationState() == RTC::SCTP::Types::AssociationState::CONNECTED);
|
|
1360
|
+
REQUIRE(a.listener.IsClosed() == false);
|
|
1361
|
+
|
|
1362
|
+
// Verify that we get new heartbeats again.
|
|
1363
|
+
advanceTimeMs(a, z, timeToNextHeartbeatMs);
|
|
1364
|
+
|
|
1365
|
+
REQUIRE(
|
|
1366
|
+
packetHasSingleChunkOfType<RTC::SCTP::HeartbeatRequestChunk>(
|
|
1367
|
+
a.listener.ConsumeFirstSentPacket()) == true);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
SECTION("resets a stream")
|
|
1371
|
+
{
|
|
1372
|
+
AssociationUnderTest a;
|
|
1373
|
+
AssociationUnderTest z;
|
|
1374
|
+
|
|
1375
|
+
connectAssociations(a, z);
|
|
1376
|
+
|
|
1377
|
+
sendMessage(a, 1, 53, { 1, 2 });
|
|
1378
|
+
// Z reads the DATA chunk.
|
|
1379
|
+
deliverFirstSentPacket(a, z);
|
|
1380
|
+
|
|
1381
|
+
REQUIRE(z.listener.ConsumeFirstReceivedMessage().has_value() == true);
|
|
1382
|
+
|
|
1383
|
+
// A reads the SACK.
|
|
1384
|
+
deliverFirstSentPacket(z, a);
|
|
1385
|
+
|
|
1386
|
+
// Reset the outgoing stream. This will directly send a RE-CONFIG.
|
|
1387
|
+
const std::vector<uint16_t> streamIds{ 1 };
|
|
1388
|
+
|
|
1389
|
+
REQUIRE(a.association.ResetStreams(streamIds) == RTC::SCTP::Types::ResetStreamsStatus::PERFORMED);
|
|
1390
|
+
|
|
1391
|
+
// Z reads the RE-CONFIG, which triggers OnAssociationInboundStreamsReset and
|
|
1392
|
+
// sends a RE-CONFIG response.
|
|
1393
|
+
deliverFirstSentPacket(a, z);
|
|
1394
|
+
|
|
1395
|
+
REQUIRE(z.listener.HasInboundStreamsResetForStreamId(1) == true);
|
|
1396
|
+
|
|
1397
|
+
// A reads the response, which triggers OnAssociationStreamsResetPerformed.
|
|
1398
|
+
deliverFirstSentPacket(z, a);
|
|
1399
|
+
|
|
1400
|
+
REQUIRE(a.listener.HasStreamsResetPerformedForStreamId(1) == true);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
SECTION("small messages with priority arrive in a specific order")
|
|
1404
|
+
{
|
|
1405
|
+
AssociationUnderTest a;
|
|
1406
|
+
AssociationUnderTest z;
|
|
1407
|
+
|
|
1408
|
+
a.association.SetStreamPriority(1, 700);
|
|
1409
|
+
a.association.SetStreamPriority(2, 200);
|
|
1410
|
+
a.association.SetStreamPriority(3, 100);
|
|
1411
|
+
|
|
1412
|
+
// Enqueue messages before connecting, to ensure they aren't sent as soon as
|
|
1413
|
+
// sendMessage() is called.
|
|
1414
|
+
sendMessage(a, 3, 301, std::vector<uint8_t>(10));
|
|
1415
|
+
sendMessage(a, 1, 101, std::vector<uint8_t>(10));
|
|
1416
|
+
sendMessage(a, 2, 201, std::vector<uint8_t>(10));
|
|
1417
|
+
sendMessage(a, 1, 102, std::vector<uint8_t>(10));
|
|
1418
|
+
sendMessage(a, 1, 103, std::vector<uint8_t>(10));
|
|
1419
|
+
|
|
1420
|
+
connectAssociations(a, z);
|
|
1421
|
+
exchangeMessages(a, z);
|
|
1422
|
+
|
|
1423
|
+
REQUIRE(getReceivedMessagePpids(z) == std::vector<uint32_t>{ 101, 102, 103, 201, 301 });
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
SECTION("large messages with priority arrive in a specific order")
|
|
1427
|
+
{
|
|
1428
|
+
AssociationUnderTest a;
|
|
1429
|
+
AssociationUnderTest z;
|
|
1430
|
+
|
|
1431
|
+
const size_t largeSize = a.sctpOptions.mtu * 20;
|
|
1432
|
+
|
|
1433
|
+
a.association.SetStreamPriority(1, 700);
|
|
1434
|
+
a.association.SetStreamPriority(2, 200);
|
|
1435
|
+
a.association.SetStreamPriority(3, 100);
|
|
1436
|
+
|
|
1437
|
+
// Enqueue messages before connecting, to ensure they aren't sent as soon as
|
|
1438
|
+
// sendMessage() is called.
|
|
1439
|
+
sendMessage(a, 3, 301, std::vector<uint8_t>(largeSize));
|
|
1440
|
+
sendMessage(a, 1, 101, std::vector<uint8_t>(largeSize));
|
|
1441
|
+
sendMessage(a, 2, 201, std::vector<uint8_t>(largeSize));
|
|
1442
|
+
sendMessage(a, 1, 102, std::vector<uint8_t>(largeSize));
|
|
1443
|
+
|
|
1444
|
+
connectAssociations(a, z);
|
|
1445
|
+
exchangeMessages(a, z);
|
|
1446
|
+
|
|
1447
|
+
REQUIRE(getReceivedMessagePpids(z) == std::vector<uint32_t>{ 101, 102, 201, 301 });
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
SECTION("a higher priority message interrupts a lower priority message")
|
|
1451
|
+
{
|
|
1452
|
+
AssociationUnderTest a;
|
|
1453
|
+
AssociationUnderTest z;
|
|
1454
|
+
|
|
1455
|
+
connectAssociations(a, z);
|
|
1456
|
+
|
|
1457
|
+
const size_t largeSize = a.sctpOptions.mtu * 20;
|
|
1458
|
+
|
|
1459
|
+
a.association.SetStreamPriority(2, 128);
|
|
1460
|
+
sendMessage(a, 2, 201, std::vector<uint8_t>(largeSize));
|
|
1461
|
+
|
|
1462
|
+
// Due to a non-zero initial congestion window, the message will already
|
|
1463
|
+
// start to send, but will not be sent completely. Now enqueue two messages
|
|
1464
|
+
// on a higher priority stream: one small and one large.
|
|
1465
|
+
a.association.SetStreamPriority(1, 512);
|
|
1466
|
+
sendMessage(a, 1, 101, std::vector<uint8_t>(10));
|
|
1467
|
+
sendMessage(a, 1, 102, std::vector<uint8_t>(largeSize));
|
|
1468
|
+
|
|
1469
|
+
exchangeMessages(a, z);
|
|
1470
|
+
|
|
1471
|
+
REQUIRE(getReceivedMessagePpids(z) == std::vector<uint32_t>{ 101, 102, 201 });
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
SECTION("lifecycle events are generated for acked messages")
|
|
1475
|
+
{
|
|
1476
|
+
AssociationUnderTest a;
|
|
1477
|
+
AssociationUnderTest z;
|
|
1478
|
+
|
|
1479
|
+
connectAssociations(a, z);
|
|
1480
|
+
|
|
1481
|
+
const size_t largeSize = a.sctpOptions.mtu * 20;
|
|
1482
|
+
|
|
1483
|
+
sendMessage(a, 2, 101, std::vector<uint8_t>(largeSize), { .lifecycleId = 41 });
|
|
1484
|
+
sendMessage(a, 2, 102, std::vector<uint8_t>(largeSize));
|
|
1485
|
+
sendMessage(a, 2, 103, std::vector<uint8_t>(largeSize), { .lifecycleId = 42 });
|
|
1486
|
+
|
|
1487
|
+
exchangeMessages(a, z);
|
|
1488
|
+
// In case of delayed ack.
|
|
1489
|
+
advanceTimeMs(a, z, a.sctpOptions.delayedAckMaxTimeoutMs);
|
|
1490
|
+
exchangeMessages(a, z);
|
|
1491
|
+
|
|
1492
|
+
REQUIRE(a.listener.HasOnAssociationLifecycleMessageDeliveredBeenCalledWithLifecycleId(41) == true);
|
|
1493
|
+
REQUIRE(a.listener.HasOnAssociationLifecycleMessageEndBeenCalledWithLifecycleId(41) == true);
|
|
1494
|
+
REQUIRE(a.listener.HasOnAssociationLifecycleMessageDeliveredBeenCalledWithLifecycleId(42) == true);
|
|
1495
|
+
REQUIRE(a.listener.HasOnAssociationLifecycleMessageEndBeenCalledWithLifecycleId(42) == true);
|
|
1496
|
+
|
|
1497
|
+
REQUIRE(getReceivedMessagePpids(z) == std::vector<uint32_t>{ 101, 102, 103 });
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
SECTION("lifecycle events for an expired message with a lifetime limit")
|
|
1501
|
+
{
|
|
1502
|
+
AssociationUnderTest a;
|
|
1503
|
+
AssociationUnderTest z;
|
|
1504
|
+
|
|
1505
|
+
// Send it before the association is connected, to prevent it from being
|
|
1506
|
+
// sent too quickly. The idea is that it should be expired before even
|
|
1507
|
+
// attempting to send it in full.
|
|
1508
|
+
sendMessage(a, 1, 51, std::vector<uint8_t>(10), { .lifetimeMs = 100, .lifecycleId = 1 });
|
|
1509
|
+
|
|
1510
|
+
advanceTimeMs(a, z, 200);
|
|
1511
|
+
|
|
1512
|
+
connectAssociations(a, z);
|
|
1513
|
+
exchangeMessages(a, z);
|
|
1514
|
+
|
|
1515
|
+
REQUIRE(
|
|
1516
|
+
a.listener.HasOnAssociationLifecycleMessageExpiredSentBeenCalledWithLifecycleId(
|
|
1517
|
+
1, /*maybeDelivered*/ false) == true);
|
|
1518
|
+
REQUIRE(a.listener.HasOnAssociationLifecycleMessageEndBeenCalledWithLifecycleId(1) == true);
|
|
1519
|
+
|
|
1520
|
+
REQUIRE(getReceivedMessagePpids(z).empty() == true);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
SECTION("responds with an error to an unknown chunk")
|
|
1524
|
+
{
|
|
1525
|
+
AssociationUnderTest a;
|
|
1526
|
+
AssociationUnderTest z;
|
|
1527
|
+
|
|
1528
|
+
const uint32_t verificationTag = connectAndGetVerificationTag(a, z);
|
|
1529
|
+
|
|
1530
|
+
std::vector<uint8_t> buffer(a.sctpOptions.mtu);
|
|
1531
|
+
|
|
1532
|
+
const auto packet = buildPacket(buffer, verificationTag);
|
|
1533
|
+
|
|
1534
|
+
// Build a 4-byte chunk and patch its type to an unknown value (0x49), whose
|
|
1535
|
+
// top two bits (01) request the receiver to skip it and report an error.
|
|
1536
|
+
auto* cookieAckChunk = packet->BuildChunkInPlace<RTC::SCTP::CookieAckChunk>();
|
|
1537
|
+
|
|
1538
|
+
cookieAckChunk->Consolidate();
|
|
1539
|
+
|
|
1540
|
+
const_cast<uint8_t*>(packet->GetBuffer())[12] = 0x49;
|
|
1541
|
+
|
|
1542
|
+
injectPacket(a, packet.get());
|
|
1543
|
+
|
|
1544
|
+
// A replies with an OPERATION-ERROR chunk carrying an Unrecognized Chunk
|
|
1545
|
+
// Type error cause.
|
|
1546
|
+
const auto buffer2 = a.listener.ConsumeFirstSentPacket();
|
|
1547
|
+
const auto replyPacket = parsePacket(buffer2);
|
|
1548
|
+
|
|
1549
|
+
REQUIRE(replyPacket);
|
|
1550
|
+
|
|
1551
|
+
const auto* operationErrorChunk =
|
|
1552
|
+
replyPacket->GetFirstChunkOfType<RTC::SCTP::OperationErrorChunk>();
|
|
1553
|
+
|
|
1554
|
+
REQUIRE(operationErrorChunk);
|
|
1555
|
+
|
|
1556
|
+
const auto* errorCause = operationErrorChunk->GetErrorCauseAt(0);
|
|
1557
|
+
|
|
1558
|
+
REQUIRE(errorCause);
|
|
1559
|
+
REQUIRE(errorCause->GetCode() == RTC::SCTP::ErrorCause::ErrorCauseCode::UNRECOGNIZED_CHUNK_TYPE);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
SECTION("reports a received error chunk as a callback")
|
|
1563
|
+
{
|
|
1564
|
+
AssociationUnderTest a;
|
|
1565
|
+
AssociationUnderTest z;
|
|
1566
|
+
|
|
1567
|
+
const uint32_t verificationTag = connectAndGetVerificationTag(a, z);
|
|
1568
|
+
|
|
1569
|
+
std::vector<uint8_t> buffer(a.sctpOptions.mtu);
|
|
1570
|
+
|
|
1571
|
+
const auto packet = buildPacket(buffer, verificationTag);
|
|
1572
|
+
|
|
1573
|
+
auto* operationErrorChunk = packet->BuildChunkInPlace<RTC::SCTP::OperationErrorChunk>();
|
|
1574
|
+
auto* unrecognizedChunkTypeErrorCause =
|
|
1575
|
+
operationErrorChunk->BuildErrorCauseInPlace<RTC::SCTP::UnrecognizedChunkTypeErrorCause>();
|
|
1576
|
+
|
|
1577
|
+
const uint8_t unknownChunk[]{ 0x49, 0x00, 0x00, 0x04 };
|
|
1578
|
+
|
|
1579
|
+
unrecognizedChunkTypeErrorCause->SetUnrecognizedChunk(unknownChunk, sizeof(unknownChunk));
|
|
1580
|
+
unrecognizedChunkTypeErrorCause->Consolidate();
|
|
1581
|
+
operationErrorChunk->Consolidate();
|
|
1582
|
+
|
|
1583
|
+
injectPacket(a, packet.get());
|
|
1584
|
+
|
|
1585
|
+
REQUIRE(a.listener.HasErrored() == true);
|
|
1586
|
+
REQUIRE(a.listener.GetErroredErrorKind() == RTC::SCTP::Types::ErrorKind::PEER_REPORTED);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
SECTION("answers a heartbeat request with an ack")
|
|
1590
|
+
{
|
|
1591
|
+
AssociationUnderTest a;
|
|
1592
|
+
AssociationUnderTest z;
|
|
1593
|
+
|
|
1594
|
+
const uint32_t verificationTag = connectAndGetVerificationTag(a, z);
|
|
1595
|
+
|
|
1596
|
+
std::vector<uint8_t> buffer(a.sctpOptions.mtu);
|
|
1597
|
+
|
|
1598
|
+
const auto packet = buildPacket(buffer, verificationTag);
|
|
1599
|
+
|
|
1600
|
+
auto* heartbeatRequestChunk = packet->BuildChunkInPlace<RTC::SCTP::HeartbeatRequestChunk>();
|
|
1601
|
+
auto* heartbeatInfoParameter =
|
|
1602
|
+
heartbeatRequestChunk->BuildParameterInPlace<RTC::SCTP::HeartbeatInfoParameter>();
|
|
1603
|
+
|
|
1604
|
+
const uint8_t info[]{ 1, 2, 3, 4 };
|
|
1605
|
+
|
|
1606
|
+
heartbeatInfoParameter->SetInfo(info, sizeof(info));
|
|
1607
|
+
heartbeatInfoParameter->Consolidate();
|
|
1608
|
+
heartbeatRequestChunk->Consolidate();
|
|
1609
|
+
|
|
1610
|
+
injectPacket(a, packet.get());
|
|
1611
|
+
|
|
1612
|
+
// A replies with a HEARTBEAT-ACK chunk echoing the Heartbeat Info.
|
|
1613
|
+
const auto buffer2 = a.listener.ConsumeFirstSentPacket();
|
|
1614
|
+
const auto replyPacket = parsePacket(buffer2);
|
|
1615
|
+
|
|
1616
|
+
REQUIRE(replyPacket);
|
|
1617
|
+
|
|
1618
|
+
const auto* heartbeatAckChunk = replyPacket->GetFirstChunkOfType<RTC::SCTP::HeartbeatAckChunk>();
|
|
1619
|
+
|
|
1620
|
+
REQUIRE(heartbeatAckChunk);
|
|
1621
|
+
|
|
1622
|
+
const auto* replyInfoParameter =
|
|
1623
|
+
heartbeatAckChunk->GetFirstParameterOfType<RTC::SCTP::HeartbeatInfoParameter>();
|
|
1624
|
+
|
|
1625
|
+
REQUIRE(replyInfoParameter);
|
|
1626
|
+
REQUIRE(replyInfoParameter->HasInfo() == true);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
SECTION("sends a message with limited retransmissions")
|
|
1630
|
+
{
|
|
1631
|
+
// Disable message interleaving so that ordered DATA chunks (with SSN) are
|
|
1632
|
+
// used.
|
|
1633
|
+
auto sctpOptions = makeSctpOptions();
|
|
1634
|
+
|
|
1635
|
+
sctpOptions.enableMessageInterleaving = false;
|
|
1636
|
+
|
|
1637
|
+
AssociationUnderTest a(sctpOptions);
|
|
1638
|
+
AssociationUnderTest z(sctpOptions);
|
|
1639
|
+
|
|
1640
|
+
connectAssociations(a, z);
|
|
1641
|
+
|
|
1642
|
+
RTC::SCTP::SendMessageOptions sendMessageOptions;
|
|
1643
|
+
|
|
1644
|
+
sendMessageOptions.maxRetransmissions = 0;
|
|
1645
|
+
|
|
1646
|
+
const std::vector<uint8_t> payload(a.sctpOptions.mtu - 100);
|
|
1647
|
+
|
|
1648
|
+
sendMessage(a, 1, 51, payload, sendMessageOptions);
|
|
1649
|
+
sendMessage(a, 1, 52, payload, sendMessageOptions);
|
|
1650
|
+
sendMessage(a, 1, 53, payload, sendMessageOptions);
|
|
1651
|
+
|
|
1652
|
+
// Z reads the first DATA.
|
|
1653
|
+
deliverFirstSentPacket(a, z);
|
|
1654
|
+
// Second DATA is lost.
|
|
1655
|
+
a.listener.ConsumeFirstSentPacket();
|
|
1656
|
+
// Z reads the third DATA.
|
|
1657
|
+
deliverFirstSentPacket(a, z);
|
|
1658
|
+
|
|
1659
|
+
// Let everything settle: the missing chunk will be nacked, and only after
|
|
1660
|
+
// the t3-rtx timer expires will it be marked for retransmission and then,
|
|
1661
|
+
// since it has no retransmissions left, abandoned (triggering a FORWARD-TSN).
|
|
1662
|
+
// We exchange packets and advance timers (delayed ack and RTO) until there's
|
|
1663
|
+
// nothing more to do, instead of asserting the exact intermediate sequence.
|
|
1664
|
+
exchangeMessages(a, z);
|
|
1665
|
+
advanceTimeMs(a, z, a.sctpOptions.delayedAckMaxTimeoutMs);
|
|
1666
|
+
exchangeMessages(a, z);
|
|
1667
|
+
advanceTimeMs(a, z, a.sctpOptions.initialRtoMs);
|
|
1668
|
+
exchangeMessages(a, z);
|
|
1669
|
+
advanceTimeMs(a, z, a.sctpOptions.delayedAckMaxTimeoutMs);
|
|
1670
|
+
exchangeMessages(a, z);
|
|
1671
|
+
|
|
1672
|
+
// The first and third messages are delivered; the second one is abandoned.
|
|
1673
|
+
REQUIRE(getReceivedMessagePpids(z) == std::vector<uint32_t>{ 51, 53 });
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
SECTION("lifecycle events for fail on max retransmissions")
|
|
1677
|
+
{
|
|
1678
|
+
auto sctpOptions = makeSctpOptions();
|
|
1679
|
+
|
|
1680
|
+
sctpOptions.enableMessageInterleaving = false;
|
|
1681
|
+
|
|
1682
|
+
AssociationUnderTest a(sctpOptions);
|
|
1683
|
+
AssociationUnderTest z(sctpOptions);
|
|
1684
|
+
|
|
1685
|
+
connectAssociations(a, z);
|
|
1686
|
+
|
|
1687
|
+
const std::vector<uint8_t> payload(a.sctpOptions.mtu - 100);
|
|
1688
|
+
|
|
1689
|
+
sendMessage(a, 1, 51, payload, { .maxRetransmissions = 0, .lifecycleId = 1 });
|
|
1690
|
+
sendMessage(a, 1, 52, payload, { .maxRetransmissions = 0, .lifecycleId = 2 });
|
|
1691
|
+
sendMessage(a, 1, 53, payload, { .maxRetransmissions = 0, .lifecycleId = 3 });
|
|
1692
|
+
|
|
1693
|
+
// Z reads the first DATA.
|
|
1694
|
+
deliverFirstSentPacket(a, z);
|
|
1695
|
+
// Second DATA is lost.
|
|
1696
|
+
a.listener.ConsumeFirstSentPacket();
|
|
1697
|
+
|
|
1698
|
+
exchangeMessages(a, z);
|
|
1699
|
+
|
|
1700
|
+
// Handle the delayed SACK.
|
|
1701
|
+
advanceTimeMs(a, z, a.sctpOptions.delayedAckMaxTimeoutMs);
|
|
1702
|
+
exchangeMessages(a, z);
|
|
1703
|
+
|
|
1704
|
+
// The chunk is now nacked. Let the RTO expire to discard the message.
|
|
1705
|
+
advanceTimeMs(a, z, a.sctpOptions.initialRtoMs);
|
|
1706
|
+
exchangeMessages(a, z);
|
|
1707
|
+
|
|
1708
|
+
// Handle the delayed SACK.
|
|
1709
|
+
advanceTimeMs(a, z, a.sctpOptions.delayedAckMaxTimeoutMs);
|
|
1710
|
+
exchangeMessages(a, z);
|
|
1711
|
+
|
|
1712
|
+
REQUIRE(a.listener.HasOnAssociationLifecycleMessageDeliveredBeenCalledWithLifecycleId(1) == true);
|
|
1713
|
+
REQUIRE(a.listener.HasOnAssociationLifecycleMessageEndBeenCalledWithLifecycleId(1) == true);
|
|
1714
|
+
REQUIRE(
|
|
1715
|
+
a.listener.HasOnAssociationLifecycleMessageExpiredSentBeenCalledWithLifecycleId(
|
|
1716
|
+
2, /*maybeDelivered*/ true) == true);
|
|
1717
|
+
REQUIRE(a.listener.HasOnAssociationLifecycleMessageEndBeenCalledWithLifecycleId(2) == true);
|
|
1718
|
+
REQUIRE(a.listener.HasOnAssociationLifecycleMessageDeliveredBeenCalledWithLifecycleId(3) == true);
|
|
1719
|
+
REQUIRE(a.listener.HasOnAssociationLifecycleMessageEndBeenCalledWithLifecycleId(3) == true);
|
|
1720
|
+
|
|
1721
|
+
REQUIRE(getReceivedMessagePpids(z) == std::vector<uint32_t>{ 51, 53 });
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
SECTION("lifecycle events for an expired message with a retransmit limit")
|
|
1725
|
+
{
|
|
1726
|
+
auto sctpOptions = makeSctpOptions();
|
|
1727
|
+
|
|
1728
|
+
sctpOptions.enableMessageInterleaving = false;
|
|
1729
|
+
|
|
1730
|
+
AssociationUnderTest a(sctpOptions);
|
|
1731
|
+
AssociationUnderTest z(sctpOptions);
|
|
1732
|
+
|
|
1733
|
+
connectAssociations(a, z);
|
|
1734
|
+
|
|
1735
|
+
// Will not be able to send it in full within the congestion window, so it
|
|
1736
|
+
// will need SACKs to be received for more fragments to be sent.
|
|
1737
|
+
const std::vector<uint8_t> payload(a.sctpOptions.mtu * 20);
|
|
1738
|
+
|
|
1739
|
+
sendMessage(a, 1, 51, payload, { .maxRetransmissions = 0, .lifecycleId = 1 });
|
|
1740
|
+
|
|
1741
|
+
// Z reads the first DATA.
|
|
1742
|
+
deliverFirstSentPacket(a, z);
|
|
1743
|
+
// Second DATA is lost.
|
|
1744
|
+
a.listener.ConsumeFirstSentPacket();
|
|
1745
|
+
|
|
1746
|
+
exchangeMessages(a, z);
|
|
1747
|
+
|
|
1748
|
+
REQUIRE(
|
|
1749
|
+
a.listener.HasOnAssociationLifecycleMessageExpiredSentBeenCalledWithLifecycleId(
|
|
1750
|
+
1, /*maybeDelivered*/ false) == true);
|
|
1751
|
+
REQUIRE(a.listener.HasOnAssociationLifecycleMessageEndBeenCalledWithLifecycleId(1) == true);
|
|
1752
|
+
|
|
1753
|
+
REQUIRE(getReceivedMessagePpids(z).empty() == true);
|
|
1754
|
+
}
|
|
1755
|
+
}
|