Skip to content

Commit a93f581

Browse files
Victor BoivieWebRTC LUCI CQ
authored andcommitted
dcsctp: Don't generate FORWARD-TSN across stream resets
This was a fun bug which proved to be challenging to find a good solution for. The issue comes from the combination of partial reliability and stream resetting, which are covered in different RFCs, and where they don't refer to each other... Stream resetting (RFC 6525) is used in WebRTC for closing a Data Channel, and is done by signaling to the receiver that the stream sequence number (SSN) should be set to zero (0) at some time. Partial reliability (RFC 3758) - and expiring messages that will not be retransmitted - is done by signaling that the SSN should be set to a certain value at a certain TSN, as the messages up until the provided SSN are not to be expected to be sent again. As these two functionalities both work by signaling to the receiver what the next expected SSN should be, they need to do it correctly not to overwrite each others' intent. And here was the bug. An example scenario where this caused issues, where we are Z (the receiver), getting packets from the sender (A): 5 A->Z DATA (TSN=30, B, SID=2, SSN=0) 6 Z->A SACK (Ack=30) 7 A->Z DATA (TSN=31, E, SID=2, SSN=0) 8 A->Z RE_CONFIG (REQ=30, TSN=31, SID=2) 9 Z->A RE_CONFIG (RESP=30, Performed) 10 Z->A SACK (Ack=31) 11 A->Z DATA (TSN=32, SID=1) 12 A->Z FORWARD_TSN (TSN=32, SID=2, SSN=0) Let's assume that the path Z->A had packet loss and A never really received our responses (#6, #9, #10) in time. At #5, Z receives a DATA fragment, which it acks, and at #7 the end of that message. The stream is then reset (#8) which it signals that it was performed (#9) and acked (#10), and data on another stream (2) was received (#11). Since A hasn't received any ACKS yet, and those chunks on SID=2 all expired, A sends a FORWARD-TSN saying that "Skip to TSN=32, and don't expect SID=2, SSN=0". That makes the receiver expect the SSN on SID=2 to be SSN=1 next time at TSN=32. But that's not good at all - A reset the stream at #8 and will want to send the next message on SID=2 using SSN=0 - not 1. The FORWARD-TSN clearly can't have a TSN that is beyond the stream reset TSN for that stream. This is just one example - combining stream resetting and partial reliability, together with a lossy network, and different variants of this can occur, which results in the receiver possibly not delivering packets because it expects a different SSN than the one the sender is later using. So this CL adds "breakpoints" to how far a FORWARD-TSN can stretch. It will simply not cross any Stream Reset last assigned TSNs, and only when a receiver has acked that all TSNs up till the Stream Reset last assigned TSN has been received, it will proceed expiring chunks after that. Bug: webrtc:14600 Change-Id: Ibae8c9308f5dfe8d734377d42cce653e69e95731 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/321600 Commit-Queue: Victor Boivie <[email protected]> Reviewed-by: Harald Alvestrand <[email protected]> Cr-Commit-Position: refs/heads/main@{#40829}
1 parent d863386 commit a93f581

File tree

7 files changed

+127
-7
lines changed

7 files changed

+127
-7
lines changed

net/dcsctp/socket/stream_reset_handler.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,8 @@ absl::optional<ReConfigChunk> StreamResetHandler::MakeStreamResetRequest() {
278278
return absl::nullopt;
279279
}
280280

281-
current_request_.emplace(TSN(*retransmission_queue_->next_tsn() - 1),
282-
retransmission_queue_->GetStreamsReadyToBeReset());
281+
current_request_.emplace(retransmission_queue_->last_assigned_tsn(),
282+
retransmission_queue_->BeginResetStreams());
283283
reconfig_timer_->set_duration(ctx_->current_rto());
284284
reconfig_timer_->Start();
285285
return MakeReconfigChunk();

net/dcsctp/tx/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ rtc_library("outstanding_data") {
104104
"../../../api:array_view",
105105
"../../../rtc_base:checks",
106106
"../../../rtc_base:logging",
107+
"../../../rtc_base/containers:flat_set",
107108
"../common:math",
108109
"../common:sequence_numbers",
109110
"../common:str_join",

net/dcsctp/tx/outstanding_data.cc

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ void OutstandingData::RemoveAcked(UnwrappedTSN cumulative_tsn_ack,
159159

160160
outstanding_data_.erase(outstanding_data_.begin(), first_unacked);
161161
last_cumulative_tsn_ack_ = cumulative_tsn_ack;
162+
stream_reset_breakpoint_tsns_.erase(stream_reset_breakpoint_tsns_.begin(),
163+
stream_reset_breakpoint_tsns_.upper_bound(
164+
cumulative_tsn_ack.next_value()));
162165
}
163166

164167
void OutstandingData::AckGapBlocks(
@@ -487,7 +490,8 @@ ForwardTsnChunk OutstandingData::CreateForwardTsn() const {
487490
UnwrappedTSN new_cumulative_ack = last_cumulative_tsn_ack_;
488491

489492
for (const auto& [tsn, item] : outstanding_data_) {
490-
if ((tsn != new_cumulative_ack.next_value()) || !item.is_abandoned()) {
493+
if (stream_reset_breakpoint_tsns_.contains(tsn) ||
494+
(tsn != new_cumulative_ack.next_value()) || !item.is_abandoned()) {
491495
break;
492496
}
493497
new_cumulative_ack = tsn;
@@ -510,7 +514,8 @@ IForwardTsnChunk OutstandingData::CreateIForwardTsn() const {
510514
UnwrappedTSN new_cumulative_ack = last_cumulative_tsn_ack_;
511515

512516
for (const auto& [tsn, item] : outstanding_data_) {
513-
if ((tsn != new_cumulative_ack.next_value()) || !item.is_abandoned()) {
517+
if (stream_reset_breakpoint_tsns_.contains(tsn) ||
518+
(tsn != new_cumulative_ack.next_value()) || !item.is_abandoned()) {
514519
break;
515520
}
516521
new_cumulative_ack = tsn;
@@ -540,4 +545,8 @@ void OutstandingData::ResetSequenceNumbers(UnwrappedTSN next_tsn,
540545
next_tsn_ = next_tsn;
541546
last_cumulative_tsn_ack_ = last_cumulative_tsn;
542547
}
548+
549+
void OutstandingData::BeginResetStreams() {
550+
stream_reset_breakpoint_tsns_.insert(next_tsn_);
551+
}
543552
} // namespace dcsctp

net/dcsctp/tx/outstanding_data.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "net/dcsctp/packet/chunk/sack_chunk.h"
2323
#include "net/dcsctp/packet/data.h"
2424
#include "net/dcsctp/public/types.h"
25+
#include "rtc_base/containers/flat_set.h"
2526

2627
namespace dcsctp {
2728

@@ -159,6 +160,10 @@ class OutstandingData {
159160
void ResetSequenceNumbers(UnwrappedTSN next_tsn,
160161
UnwrappedTSN last_cumulative_tsn);
161162

163+
// Called when an outgoing stream reset is sent, marking the last assigned TSN
164+
// as a breakpoint that a FORWARD-TSN shouldn't cross.
165+
void BeginResetStreams();
166+
162167
private:
163168
// A fragmented message's DATA chunk while in the retransmission queue, and
164169
// its associated metadata.
@@ -345,6 +350,10 @@ class OutstandingData {
345350
std::set<UnwrappedTSN> to_be_fast_retransmitted_;
346351
// Data chunks that are to be retransmitted.
347352
std::set<UnwrappedTSN> to_be_retransmitted_;
353+
// Wben a stream reset has begun, the "next TSN to assign" is added to this
354+
// set, and removed when the cum-ack TSN reaches it. This is used to limit a
355+
// FORWARD-TSN to reset streams past a "stream reset last assigned TSN".
356+
webrtc::flat_set<UnwrappedTSN> stream_reset_breakpoint_tsns_;
348357
};
349358
} // namespace dcsctp
350359
#endif // NET_DCSCTP_TX_OUTSTANDING_DATA_H_

net/dcsctp/tx/outstanding_data_test.cc

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@ namespace {
2727
using ::testing::MockFunction;
2828
using State = ::dcsctp::OutstandingData::State;
2929
using ::testing::_;
30+
using ::testing::AllOf;
3031
using ::testing::ElementsAre;
3132
using ::testing::IsEmpty;
3233
using ::testing::Pair;
34+
using ::testing::Property;
3335
using ::testing::Return;
3436
using ::testing::StrictMock;
37+
using ::testing::UnorderedElementsAre;
3538

3639
constexpr TimeMs kNow(42);
3740

@@ -587,5 +590,97 @@ TEST_F(OutstandingDataTest, LifecycleReturnsAbandonedAfterT3rtxExpired) {
587590
EXPECT_FALSE(ack2.has_packet_loss);
588591
EXPECT_THAT(ack2.abandoned_lifecycle_ids, ElementsAre(LifecycleId(42)));
589592
}
593+
594+
TEST_F(OutstandingDataTest, GeneratesForwardTsnUntilNextStreamResetTsn) {
595+
// This test generates:
596+
// * Stream 1: TSN 10, 11, 12 <RESET>
597+
// * Stream 2: TSN 13, 14 <RESET>
598+
// * Stream 3: TSN 15, 16
599+
//
600+
// Then it expires chunk 12-15, and ensures that the generated FORWARD-TSN
601+
// only includes up till TSN 12 until the cum ack TSN has reached 12, and then
602+
// 13 and 14 are included, and then after the cum ack TSN has reached 14, then
603+
// 15 is included.
604+
//
605+
// What it shouldn't do, is to generate a FORWARD-TSN directly at the start
606+
// with new TSN=15, and setting [(sid=1, ssn=44), (sid=2, ssn=46),
607+
// (sid=3, ssn=47)], because that will confuse the receiver at TSN=17,
608+
// receiving SID=1, SSN=0 (it's reset!), expecting SSN to be 45.
609+
constexpr DataGeneratorOptions kStream1 = {.stream_id = StreamID(1)};
610+
constexpr DataGeneratorOptions kStream2 = {.stream_id = StreamID(2)};
611+
constexpr DataGeneratorOptions kStream3 = {.stream_id = StreamID(3)};
612+
EXPECT_CALL(on_discard_, Call).WillRepeatedly(Return(false));
613+
614+
// TSN 10-12
615+
buf_.Insert(gen_.Ordered({1}, "BE", kStream1), kNow);
616+
buf_.Insert(gen_.Ordered({1}, "BE", kStream1), kNow);
617+
buf_.Insert(gen_.Ordered({1}, "BE", kStream1), kNow, MaxRetransmits(0));
618+
619+
buf_.BeginResetStreams();
620+
621+
// TSN 13, 14
622+
buf_.Insert(gen_.Ordered({1}, "BE", kStream2), kNow, MaxRetransmits(0));
623+
buf_.Insert(gen_.Ordered({1}, "BE", kStream2), kNow, MaxRetransmits(0));
624+
625+
buf_.BeginResetStreams();
626+
627+
// TSN 15, 16
628+
buf_.Insert(gen_.Ordered({1}, "BE", kStream3), kNow, MaxRetransmits(0));
629+
buf_.Insert(gen_.Ordered({1}, "BE", kStream3), kNow);
630+
631+
EXPECT_FALSE(buf_.ShouldSendForwardTsn());
632+
633+
buf_.HandleSack(unwrapper_.Unwrap(TSN(11)), {}, false);
634+
buf_.NackAll();
635+
EXPECT_THAT(buf_.GetChunkStatesForTesting(),
636+
ElementsAre(Pair(TSN(11), State::kAcked), //
637+
Pair(TSN(12), State::kAbandoned), //
638+
Pair(TSN(13), State::kAbandoned), //
639+
Pair(TSN(14), State::kAbandoned), //
640+
Pair(TSN(15), State::kAbandoned), //
641+
Pair(TSN(16), State::kToBeRetransmitted)));
642+
643+
EXPECT_TRUE(buf_.ShouldSendForwardTsn());
644+
EXPECT_THAT(
645+
buf_.CreateForwardTsn(),
646+
AllOf(Property(&ForwardTsnChunk::new_cumulative_tsn, TSN(12)),
647+
Property(&ForwardTsnChunk::skipped_streams,
648+
UnorderedElementsAre(ForwardTsnChunk::SkippedStream(
649+
StreamID(1), SSN(44))))));
650+
651+
// Ack 12, allowing a FORWARD-TSN that spans to TSN=14 to be created.
652+
buf_.HandleSack(unwrapper_.Unwrap(TSN(12)), {}, false);
653+
EXPECT_TRUE(buf_.ShouldSendForwardTsn());
654+
EXPECT_THAT(
655+
buf_.CreateForwardTsn(),
656+
AllOf(Property(&ForwardTsnChunk::new_cumulative_tsn, TSN(14)),
657+
Property(&ForwardTsnChunk::skipped_streams,
658+
UnorderedElementsAre(ForwardTsnChunk::SkippedStream(
659+
StreamID(2), SSN(46))))));
660+
661+
// Ack 13, allowing a FORWARD-TSN that spans to TSN=14 to be created.
662+
buf_.HandleSack(unwrapper_.Unwrap(TSN(13)), {}, false);
663+
EXPECT_TRUE(buf_.ShouldSendForwardTsn());
664+
EXPECT_THAT(
665+
buf_.CreateForwardTsn(),
666+
AllOf(Property(&ForwardTsnChunk::new_cumulative_tsn, TSN(14)),
667+
Property(&ForwardTsnChunk::skipped_streams,
668+
UnorderedElementsAre(ForwardTsnChunk::SkippedStream(
669+
StreamID(2), SSN(46))))));
670+
671+
// Ack 14, allowing a FORWARD-TSN that spans to TSN=15 to be created.
672+
buf_.HandleSack(unwrapper_.Unwrap(TSN(14)), {}, false);
673+
EXPECT_TRUE(buf_.ShouldSendForwardTsn());
674+
EXPECT_THAT(
675+
buf_.CreateForwardTsn(),
676+
AllOf(Property(&ForwardTsnChunk::new_cumulative_tsn, TSN(15)),
677+
Property(&ForwardTsnChunk::skipped_streams,
678+
UnorderedElementsAre(ForwardTsnChunk::SkippedStream(
679+
StreamID(3), SSN(47))))));
680+
681+
buf_.HandleSack(unwrapper_.Unwrap(TSN(15)), {}, false);
682+
EXPECT_FALSE(buf_.ShouldSendForwardTsn());
683+
}
684+
590685
} // namespace
591686
} // namespace dcsctp

net/dcsctp/tx/retransmission_queue.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,10 @@ void RetransmissionQueue::PrepareResetStream(StreamID stream_id) {
573573
bool RetransmissionQueue::HasStreamsReadyToBeReset() const {
574574
return send_queue_.HasStreamsReadyToBeReset();
575575
}
576+
std::vector<StreamID> RetransmissionQueue::BeginResetStreams() {
577+
outstanding_data_.BeginResetStreams();
578+
return send_queue_.GetStreamsReadyToBeReset();
579+
}
576580
void RetransmissionQueue::CommitResetStreams() {
577581
send_queue_.CommitResetStreams();
578582
}

net/dcsctp/tx/retransmission_queue.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ class RetransmissionQueue {
103103
// Returns the next TSN that will be allocated for sent DATA chunks.
104104
TSN next_tsn() const { return outstanding_data_.next_tsn().Wrap(); }
105105

106+
TSN last_assigned_tsn() const {
107+
return UnwrappedTSN::AddTo(outstanding_data_.next_tsn(), -1).Wrap();
108+
}
109+
106110
// Returns the size of the congestion window, in bytes. This is the number of
107111
// bytes that may be in-flight.
108112
size_t cwnd() const { return cwnd_; }
@@ -148,9 +152,7 @@ class RetransmissionQueue {
148152
// to stream resetting.
149153
void PrepareResetStream(StreamID stream_id);
150154
bool HasStreamsReadyToBeReset() const;
151-
std::vector<StreamID> GetStreamsReadyToBeReset() const {
152-
return send_queue_.GetStreamsReadyToBeReset();
153-
}
155+
std::vector<StreamID> BeginResetStreams();
154156
void CommitResetStreams();
155157
void RollbackResetStreams();
156158

0 commit comments

Comments
 (0)