dcsctp: Persist all state in state cookie

In the example below, the association is being established between peer
A and Z, and A is the initiating party.

Before this CL, when an association was about to be established, Z would
after having received the INIT chunk, persist state in the socket about
which verification tag and initial TSN that was picked. These would be
re-generated on every incoming INIT (that's fine), but when A had
extracted the cookie from INIT_ACK and sent a reply (COOKIE_ECHO) with
the state cookie, that could fail validation when it's received by Z, if
the sent cookie was not the most recent one or if the COOKIE_ECHO had a
verification tag coming not from the most recent INIT_ACK, because Z had
replaced the state in the socket with the one generated when the second
INIT_ACK chunk was generated - state it used for validation of future
received data.

In other words:
A -> INIT 1
<timeout>
A -> INIT 2 (retransmission of INIT 1)
INIT 1 -> Z - sends INIT_ACK 1 with verification_tag=1, initial_tsn=1,
              cookie 1 (and records these to socket state)
INIT 2 -> Z - sends INIT_ACK 2 with verification_tag=2, initial_tsn=2,
              cookie 2 (replaces socket state with the new data)
INIT_ACK 1 -> A -> sends COOKIE_ECHO with verification_tag=1, cookie 1
COOKIE_ECHO (cookie 1) -> Z <FAILS, as the state isn't as expected>.

The solution is really to do what RFC4960 says, to not maintain any
state as the receiving peer until COOKIE_ECHO has been received. This
was initially not done because the underlying reason why this is
important in SCTP is to avoid denial of service, and this is why SCTP
has the four-way handshake. But for Data Channels - SCTP over DTLS -
this attack vector isn't available. So the implementation was
"simplified" by keeping socket state instead of encoding it in the
state cookie, but that obviously had downsides.

So with this CL, the non-initiating peer in connection establishment
doesn't keep any socket state, and puts all that state in the state
cookie instead. This allows any COOKIE_ECHO to be received by Z.

Bug: webrtc:15712
Change-Id: I596c7330ce27292612d3c9f86b21c712f6f4e408
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/330440
Commit-Queue: Victor Boivie <boivie@webrtc.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#41340}
This commit is contained in:
Victor Boivie 2023-12-07 11:52:11 +01:00 committed by WebRTC LUCI CQ
parent a88ea8a36f
commit 63e273ad4b
6 changed files with 235 additions and 71 deletions

View file

@ -296,20 +296,15 @@ void DcSctpSocket::SendInit() {
packet_sender_.Send(b, /*write_checksum=*/true); packet_sender_.Send(b, /*write_checksum=*/true);
} }
void DcSctpSocket::MakeConnectionParameters() {
VerificationTag new_verification_tag(
callbacks_.GetRandomInt(kMinVerificationTag, kMaxVerificationTag));
TSN initial_tsn(callbacks_.GetRandomInt(kMinInitialTsn, kMaxInitialTsn));
connect_params_.initial_tsn = initial_tsn;
connect_params_.verification_tag = new_verification_tag;
}
void DcSctpSocket::Connect() { void DcSctpSocket::Connect() {
RTC_DCHECK_RUN_ON(&thread_checker_); RTC_DCHECK_RUN_ON(&thread_checker_);
CallbackDeferrer::ScopedDeferrer deferrer(callbacks_); CallbackDeferrer::ScopedDeferrer deferrer(callbacks_);
if (state_ == State::kClosed) { if (state_ == State::kClosed) {
MakeConnectionParameters(); connect_params_.initial_tsn =
TSN(callbacks_.GetRandomInt(kMinInitialTsn, kMaxInitialTsn));
connect_params_.verification_tag = VerificationTag(
callbacks_.GetRandomInt(kMinVerificationTag, kMaxVerificationTag));
RTC_DLOG(LS_INFO) RTC_DLOG(LS_INFO)
<< log_prefix() << log_prefix()
<< rtc::StringFormat( << rtc::StringFormat(
@ -1153,11 +1148,16 @@ void DcSctpSocket::HandleInit(const CommonHeader& header,
} }
TieTag tie_tag(0); TieTag tie_tag(0);
VerificationTag my_verification_tag;
TSN my_initial_tsn;
if (state_ == State::kClosed) { if (state_ == State::kClosed) {
RTC_DLOG(LS_VERBOSE) << log_prefix() RTC_DLOG(LS_VERBOSE) << log_prefix()
<< "Received Init in closed state (normal)"; << "Received Init in closed state (normal)";
MakeConnectionParameters(); my_verification_tag = VerificationTag(
callbacks_.GetRandomInt(kMinVerificationTag, kMaxVerificationTag));
my_initial_tsn =
TSN(callbacks_.GetRandomInt(kMinInitialTsn, kMaxInitialTsn));
} else if (state_ == State::kCookieWait || state_ == State::kCookieEchoed) { } else if (state_ == State::kCookieWait || state_ == State::kCookieEchoed) {
// https://tools.ietf.org/html/rfc4960#section-5.2.1 // https://tools.ietf.org/html/rfc4960#section-5.2.1
// "This usually indicates an initialization collision, i.e., each // "This usually indicates an initialization collision, i.e., each
@ -1170,6 +1170,8 @@ void DcSctpSocket::HandleInit(const CommonHeader& header,
// endpoint) was sent." // endpoint) was sent."
RTC_DLOG(LS_VERBOSE) << log_prefix() RTC_DLOG(LS_VERBOSE) << log_prefix()
<< "Received Init indicating simultaneous connections"; << "Received Init indicating simultaneous connections";
my_verification_tag = connect_params_.verification_tag;
my_initial_tsn = connect_params_.initial_tsn;
} else { } else {
RTC_DCHECK(tcb_ != nullptr); RTC_DCHECK(tcb_ != nullptr);
// https://tools.ietf.org/html/rfc4960#section-5.2.2 // https://tools.ietf.org/html/rfc4960#section-5.2.2
@ -1184,17 +1186,16 @@ void DcSctpSocket::HandleInit(const CommonHeader& header,
<< "Received Init indicating restarted connection"; << "Received Init indicating restarted connection";
// Create a new verification tag - different from the previous one. // Create a new verification tag - different from the previous one.
for (int tries = 0; tries < 10; ++tries) { for (int tries = 0; tries < 10; ++tries) {
connect_params_.verification_tag = VerificationTag( my_verification_tag = VerificationTag(
callbacks_.GetRandomInt(kMinVerificationTag, kMaxVerificationTag)); callbacks_.GetRandomInt(kMinVerificationTag, kMaxVerificationTag));
if (connect_params_.verification_tag != tcb_->my_verification_tag()) { if (my_verification_tag != tcb_->my_verification_tag()) {
break; break;
} }
} }
// Make the initial TSN make a large jump, so that there is no overlap // Make the initial TSN make a large jump, so that there is no overlap
// with the old and new association. // with the old and new association.
connect_params_.initial_tsn = my_initial_tsn = TSN(*tcb_->retransmission_queue().next_tsn() + 1000000);
TSN(*tcb_->retransmission_queue().next_tsn() + 1000000);
tie_tag = tcb_->tie_tag(); tie_tag = tcb_->tie_tag();
} }
@ -1204,8 +1205,8 @@ void DcSctpSocket::HandleInit(const CommonHeader& header,
"Proceeding with connection. my_verification_tag=%08x, " "Proceeding with connection. my_verification_tag=%08x, "
"my_initial_tsn=%u, peer_verification_tag=%08x, " "my_initial_tsn=%u, peer_verification_tag=%08x, "
"peer_initial_tsn=%u", "peer_initial_tsn=%u",
*connect_params_.verification_tag, *connect_params_.initial_tsn, *my_verification_tag, *my_initial_tsn, *chunk->initiate_tag(),
*chunk->initiate_tag(), *chunk->initial_tsn()); *chunk->initial_tsn());
Capabilities capabilities = Capabilities capabilities =
ComputeCapabilities(options_, chunk->nbr_outbound_streams(), ComputeCapabilities(options_, chunk->nbr_outbound_streams(),
@ -1214,16 +1215,17 @@ void DcSctpSocket::HandleInit(const CommonHeader& header,
SctpPacket::Builder b(chunk->initiate_tag(), options_); SctpPacket::Builder b(chunk->initiate_tag(), options_);
Parameters::Builder params_builder = Parameters::Builder params_builder =
Parameters::Builder().Add(StateCookieParameter( Parameters::Builder().Add(StateCookieParameter(
StateCookie(chunk->initiate_tag(), chunk->initial_tsn(), StateCookie(chunk->initiate_tag(), my_verification_tag,
chunk->a_rwnd(), tie_tag, capabilities) chunk->initial_tsn(), my_initial_tsn, chunk->a_rwnd(),
tie_tag, capabilities)
.Serialize())); .Serialize()));
AddCapabilityParameters(options_, params_builder); AddCapabilityParameters(options_, params_builder);
InitAckChunk init_ack(/*initiate_tag=*/connect_params_.verification_tag, InitAckChunk init_ack(/*initiate_tag=*/my_verification_tag,
options_.max_receiver_window_buffer_size, options_.max_receiver_window_buffer_size,
options_.announced_maximum_outgoing_streams, options_.announced_maximum_outgoing_streams,
options_.announced_maximum_incoming_streams, options_.announced_maximum_incoming_streams,
connect_params_.initial_tsn, params_builder.Build()); my_initial_tsn, params_builder.Build());
b.Add(init_ack); b.Add(init_ack);
// If the peer has signaled that it supports zero checksum, INIT-ACK can then // If the peer has signaled that it supports zero checksum, INIT-ACK can then
// have its checksum as zero. // have its checksum as zero.
@ -1309,13 +1311,13 @@ void DcSctpSocket::HandleCookieEcho(
return; return;
} }
} else { } else {
if (header.verification_tag != connect_params_.verification_tag) { if (header.verification_tag != cookie->my_tag()) {
callbacks_.OnError( callbacks_.OnError(
ErrorKind::kParseFailed, ErrorKind::kParseFailed,
rtc::StringFormat( rtc::StringFormat(
"Received CookieEcho with invalid verification tag: %08x, " "Received CookieEcho with invalid verification tag: %08x, "
"expected %08x", "expected %08x",
*header.verification_tag, *connect_params_.verification_tag)); *header.verification_tag, *cookie->my_tag()));
return; return;
} }
} }
@ -1340,10 +1342,10 @@ void DcSctpSocket::HandleCookieEcho(
// send queue is already re-configured, and shouldn't be reset. // send queue is already re-configured, and shouldn't be reset.
send_queue_.Reset(); send_queue_.Reset();
CreateTransmissionControlBlock( CreateTransmissionControlBlock(cookie->capabilities(), cookie->my_tag(),
cookie->capabilities(), connect_params_.verification_tag, cookie->my_initial_tsn(), cookie->peer_tag(),
connect_params_.initial_tsn, cookie->initiate_tag(), cookie->peer_initial_tsn(), cookie->a_rwnd(),
cookie->initial_tsn(), cookie->a_rwnd(), MakeTieTag(callbacks_)); MakeTieTag(callbacks_));
} }
SctpPacket::Builder b = tcb_->PacketBuilder(); SctpPacket::Builder b = tcb_->PacketBuilder();
@ -1363,13 +1365,13 @@ bool DcSctpSocket::HandleCookieEchoWithTCB(const CommonHeader& header,
<< *tcb_->my_verification_tag() << *tcb_->my_verification_tag()
<< ", peer_tag=" << *header.verification_tag << ", peer_tag=" << *header.verification_tag
<< ", tcb_tag=" << *tcb_->peer_verification_tag() << ", tcb_tag=" << *tcb_->peer_verification_tag()
<< ", cookie_tag=" << *cookie.initiate_tag() << ", peer_tag=" << *cookie.peer_tag()
<< ", local_tie_tag=" << *tcb_->tie_tag() << ", local_tie_tag=" << *tcb_->tie_tag()
<< ", peer_tie_tag=" << *cookie.tie_tag(); << ", peer_tie_tag=" << *cookie.tie_tag();
// https://tools.ietf.org/html/rfc4960#section-5.2.4 // https://tools.ietf.org/html/rfc4960#section-5.2.4
// "Handle a COOKIE ECHO when a TCB Exists" // "Handle a COOKIE ECHO when a TCB Exists"
if (header.verification_tag != tcb_->my_verification_tag() && if (header.verification_tag != tcb_->my_verification_tag() &&
tcb_->peer_verification_tag() != cookie.initiate_tag() && tcb_->peer_verification_tag() != cookie.peer_tag() &&
cookie.tie_tag() == tcb_->tie_tag()) { cookie.tie_tag() == tcb_->tie_tag()) {
// "A) In this case, the peer may have restarted." // "A) In this case, the peer may have restarted."
if (state_ == State::kShutdownAckSent) { if (state_ == State::kShutdownAckSent) {
@ -1377,7 +1379,7 @@ bool DcSctpSocket::HandleCookieEchoWithTCB(const CommonHeader& header,
// that the peer has restarted ... it MUST NOT set up a new association // that the peer has restarted ... it MUST NOT set up a new association
// but instead resend the SHUTDOWN ACK and send an ERROR chunk with a // but instead resend the SHUTDOWN ACK and send an ERROR chunk with a
// "Cookie Received While Shutting Down" error cause to its peer." // "Cookie Received While Shutting Down" error cause to its peer."
SctpPacket::Builder b(cookie.initiate_tag(), options_); SctpPacket::Builder b(cookie.peer_tag(), options_);
b.Add(ShutdownAckChunk()); b.Add(ShutdownAckChunk());
b.Add(ErrorChunk(Parameters::Builder() b.Add(ErrorChunk(Parameters::Builder()
.Add(CookieReceivedWhileShuttingDownCause()) .Add(CookieReceivedWhileShuttingDownCause())
@ -1394,7 +1396,7 @@ bool DcSctpSocket::HandleCookieEchoWithTCB(const CommonHeader& header,
tcb_ = nullptr; tcb_ = nullptr;
callbacks_.OnConnectionRestarted(); callbacks_.OnConnectionRestarted();
} else if (header.verification_tag == tcb_->my_verification_tag() && } else if (header.verification_tag == tcb_->my_verification_tag() &&
tcb_->peer_verification_tag() != cookie.initiate_tag()) { tcb_->peer_verification_tag() != cookie.peer_tag()) {
// TODO(boivie): Handle the peer_tag == 0? // TODO(boivie): Handle the peer_tag == 0?
// "B) In this case, both sides may be attempting to start an // "B) In this case, both sides may be attempting to start an
// association at about the same time, but the peer endpoint started its // association at about the same time, but the peer endpoint started its
@ -1404,7 +1406,7 @@ bool DcSctpSocket::HandleCookieEchoWithTCB(const CommonHeader& header,
<< "Received COOKIE-ECHO indicating simultaneous connections"; << "Received COOKIE-ECHO indicating simultaneous connections";
tcb_ = nullptr; tcb_ = nullptr;
} else if (header.verification_tag != tcb_->my_verification_tag() && } else if (header.verification_tag != tcb_->my_verification_tag() &&
tcb_->peer_verification_tag() == cookie.initiate_tag() && tcb_->peer_verification_tag() == cookie.peer_tag() &&
cookie.tie_tag() == TieTag(0)) { cookie.tie_tag() == TieTag(0)) {
// "C) In this case, the local endpoint's cookie has arrived late. // "C) In this case, the local endpoint's cookie has arrived late.
// Before it arrived, the local endpoint sent an INIT and received an // Before it arrived, the local endpoint sent an INIT and received an
@ -1417,7 +1419,7 @@ bool DcSctpSocket::HandleCookieEchoWithTCB(const CommonHeader& header,
<< "Received COOKIE-ECHO indicating a late COOKIE-ECHO. Discarding"; << "Received COOKIE-ECHO indicating a late COOKIE-ECHO. Discarding";
return false; return false;
} else if (header.verification_tag == tcb_->my_verification_tag() && } else if (header.verification_tag == tcb_->my_verification_tag() &&
tcb_->peer_verification_tag() == cookie.initiate_tag()) { tcb_->peer_verification_tag() == cookie.peer_tag()) {
// "D) When both local and remote tags match, the endpoint should enter // "D) When both local and remote tags match, the endpoint should enter
// the ESTABLISHED state, if it is in the COOKIE-ECHOED state. It // the ESTABLISHED state, if it is in the COOKIE-ECHOED state. It
// should stop any cookie timer that may be running and send a COOKIE // should stop any cookie timer that may be running and send a COOKIE

View file

@ -148,8 +148,6 @@ class DcSctpSocket : public DcSctpSocketInterface {
// Changes the socket state, given a `reason` (for debugging/logging). // Changes the socket state, given a `reason` (for debugging/logging).
void SetState(State state, absl::string_view reason); void SetState(State state, absl::string_view reason);
// Fills in `connect_params` with random verification tag and initial TSN.
void MakeConnectionParameters();
// Closes the association. Note that the TCB will not be valid past this call. // Closes the association. Note that the TCB will not be valid past this call.
void InternalClose(ErrorKind error, absl::string_view message); void InternalClose(ErrorKind error, absl::string_view message);
// Closes the association, because of too many retransmission errors. // Closes the association, because of too many retransmission errors.

View file

@ -3061,5 +3061,149 @@ TEST(DcSctpSocketTest, HandlesForwardTsnOutOfOrderWithStreamResetting) {
testing::Optional(Property(&DcSctpMessage::ppid, PPID(53)))); testing::Optional(Property(&DcSctpMessage::ppid, PPID(53))));
} }
TEST(DcSctpSocketTest, ResentInitHasSameParameters) {
// If an INIT chunk has to be resent (due to INIT_ACK not received in time),
// the resent INIT must have the same properties as the original one.
SocketUnderTest a("A");
SocketUnderTest z("Z");
a.socket.Connect();
auto packet_1 = a.cb.ConsumeSentPacket();
// Times out, INIT is re-sent.
AdvanceTime(a, z, a.options.t1_init_timeout.ToTimeDelta());
auto packet_2 = a.cb.ConsumeSentPacket();
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket init_packet_1,
SctpPacket::Parse(packet_1, z.options));
ASSERT_HAS_VALUE_AND_ASSIGN(
InitChunk init_chunk_1,
InitChunk::Parse(init_packet_1.descriptors()[0].data));
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket init_packet_2,
SctpPacket::Parse(packet_2, z.options));
ASSERT_HAS_VALUE_AND_ASSIGN(
InitChunk init_chunk_2,
InitChunk::Parse(init_packet_2.descriptors()[0].data));
EXPECT_EQ(init_chunk_1.initial_tsn(), init_chunk_2.initial_tsn());
EXPECT_EQ(init_chunk_1.initiate_tag(), init_chunk_2.initiate_tag());
}
TEST(DcSctpSocketTest, ResentInitAckHasDifferentParameters) {
// For every INIT, an INIT_ACK is produced. Verify that the socket doesn't
// maintain any state by ensuring that two created INIT_ACKs for the same
// received INIT are different.
SocketUnderTest a("A");
SocketUnderTest z("Z");
a.socket.Connect();
auto packet_1 = a.cb.ConsumeSentPacket();
EXPECT_THAT(packet_1, HasChunks(ElementsAre(IsChunkType(InitChunk::kType))));
z.socket.ReceivePacket(packet_1);
auto packet_2 = z.cb.ConsumeSentPacket();
z.socket.ReceivePacket(packet_1);
auto packet_3 = z.cb.ConsumeSentPacket();
EXPECT_THAT(packet_2,
HasChunks(ElementsAre(IsChunkType(InitAckChunk::kType))));
EXPECT_THAT(packet_3,
HasChunks(ElementsAre(IsChunkType(InitAckChunk::kType))));
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket init_ack_packet_1,
SctpPacket::Parse(packet_2, z.options));
ASSERT_HAS_VALUE_AND_ASSIGN(
InitAckChunk init_ack_chunk_1,
InitAckChunk::Parse(init_ack_packet_1.descriptors()[0].data));
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket init_ack_packet_2,
SctpPacket::Parse(packet_3, z.options));
ASSERT_HAS_VALUE_AND_ASSIGN(
InitAckChunk init_ack_chunk_2,
InitAckChunk::Parse(init_ack_packet_2.descriptors()[0].data));
EXPECT_NE(init_ack_chunk_1.initiate_tag(), init_ack_chunk_2.initiate_tag());
EXPECT_NE(init_ack_chunk_1.initial_tsn(), init_ack_chunk_2.initial_tsn());
}
TEST(DcSctpSocketResendInitTest, ConnectionCanContinueFromFirstInitAck) {
// If an INIT chunk has to be resent (due to INIT_ACK not received in time),
// another INIT will be sent, and if both INITs were actually received, both
// will be responded to by an INIT_ACK. While these two INIT_ACKs may have
// different parameters, the connection must be able to finish with the cookie
// (as replied to using COOKIE_ECHO) from either INIT_ACK.
SocketUnderTest a("A");
SocketUnderTest z("Z");
a.socket.Send(DcSctpMessage(StreamID(1), PPID(53),
std::vector<uint8_t>(kLargeMessageSize)),
kSendOptions);
a.socket.Connect();
auto init_1 = a.cb.ConsumeSentPacket();
// Times out, INIT is re-sent.
AdvanceTime(a, z, a.options.t1_init_timeout.ToTimeDelta());
auto init_2 = a.cb.ConsumeSentPacket();
EXPECT_THAT(init_1, HasChunks(ElementsAre(IsChunkType(InitChunk::kType))));
EXPECT_THAT(init_2, HasChunks(ElementsAre(IsChunkType(InitChunk::kType))));
z.socket.ReceivePacket(init_1);
z.socket.ReceivePacket(init_2);
auto init_ack_1 = z.cb.ConsumeSentPacket();
auto init_ack_2 = z.cb.ConsumeSentPacket();
EXPECT_THAT(init_ack_1,
HasChunks(ElementsAre(IsChunkType(InitAckChunk::kType))));
EXPECT_THAT(init_ack_2,
HasChunks(ElementsAre(IsChunkType(InitAckChunk::kType))));
a.socket.ReceivePacket(init_ack_1);
// Then let the rest continue.
ExchangeMessages(a, z);
absl::optional<DcSctpMessage> msg = z.cb.ConsumeReceivedMessage();
ASSERT_TRUE(msg.has_value());
EXPECT_EQ(msg->stream_id(), StreamID(1));
EXPECT_THAT(msg->payload(), SizeIs(kLargeMessageSize));
}
TEST(DcSctpSocketResendInitTest, ConnectionCanContinueFromSecondInitAck) {
// Just as above, but discarding the first INIT_ACK.
SocketUnderTest a("A");
SocketUnderTest z("Z");
a.socket.Send(DcSctpMessage(StreamID(1), PPID(53),
std::vector<uint8_t>(kLargeMessageSize)),
kSendOptions);
a.socket.Connect();
auto init_1 = a.cb.ConsumeSentPacket();
// Times out, INIT is re-sent.
AdvanceTime(a, z, a.options.t1_init_timeout.ToTimeDelta());
auto init_2 = a.cb.ConsumeSentPacket();
EXPECT_THAT(init_1, HasChunks(ElementsAre(IsChunkType(InitChunk::kType))));
EXPECT_THAT(init_2, HasChunks(ElementsAre(IsChunkType(InitChunk::kType))));
z.socket.ReceivePacket(init_1);
z.socket.ReceivePacket(init_2);
auto init_ack_1 = z.cb.ConsumeSentPacket();
auto init_ack_2 = z.cb.ConsumeSentPacket();
EXPECT_THAT(init_ack_1,
HasChunks(ElementsAre(IsChunkType(InitAckChunk::kType))));
EXPECT_THAT(init_ack_2,
HasChunks(ElementsAre(IsChunkType(InitAckChunk::kType))));
a.socket.ReceivePacket(init_ack_2);
// Then let the rest continue.
ExchangeMessages(a, z);
absl::optional<DcSctpMessage> msg = z.cb.ConsumeReceivedMessage();
ASSERT_TRUE(msg.has_value());
EXPECT_EQ(msg->stream_id(), StreamID(1));
EXPECT_THAT(msg->payload(), SizeIs(kLargeMessageSize));
}
} // namespace } // namespace
} // namespace dcsctp } // namespace dcsctp

View file

@ -32,17 +32,19 @@ std::vector<uint8_t> StateCookie::Serialize() {
BoundedByteWriter<kCookieSize> buffer(cookie); BoundedByteWriter<kCookieSize> buffer(cookie);
buffer.Store32<0>(kMagic1); buffer.Store32<0>(kMagic1);
buffer.Store32<4>(kMagic2); buffer.Store32<4>(kMagic2);
buffer.Store32<8>(*initiate_tag_); buffer.Store32<8>(*peer_tag_);
buffer.Store32<12>(*initial_tsn_); buffer.Store32<12>(*my_tag_);
buffer.Store32<16>(a_rwnd_); buffer.Store32<16>(*peer_initial_tsn_);
buffer.Store32<20>(static_cast<uint32_t>(*tie_tag_ >> 32)); buffer.Store32<20>(*my_initial_tsn_);
buffer.Store32<24>(static_cast<uint32_t>(*tie_tag_)); buffer.Store32<24>(a_rwnd_);
buffer.Store8<28>(capabilities_.partial_reliability); buffer.Store32<28>(static_cast<uint32_t>(*tie_tag_ >> 32));
buffer.Store8<29>(capabilities_.message_interleaving); buffer.Store32<32>(static_cast<uint32_t>(*tie_tag_));
buffer.Store8<30>(capabilities_.reconfig); buffer.Store8<36>(capabilities_.partial_reliability);
buffer.Store16<32>(capabilities_.negotiated_maximum_incoming_streams); buffer.Store8<37>(capabilities_.message_interleaving);
buffer.Store16<34>(capabilities_.negotiated_maximum_outgoing_streams); buffer.Store8<38>(capabilities_.reconfig);
buffer.Store8<36>(capabilities_.zero_checksum); buffer.Store16<40>(capabilities_.negotiated_maximum_incoming_streams);
buffer.Store16<42>(capabilities_.negotiated_maximum_outgoing_streams);
buffer.Store8<44>(capabilities_.zero_checksum);
return cookie; return cookie;
} }
@ -62,23 +64,25 @@ absl::optional<StateCookie> StateCookie::Deserialize(
return absl::nullopt; return absl::nullopt;
} }
VerificationTag verification_tag(buffer.Load32<8>()); VerificationTag peer_tag(buffer.Load32<8>());
TSN initial_tsn(buffer.Load32<12>()); VerificationTag my_tag(buffer.Load32<12>());
uint32_t a_rwnd = buffer.Load32<16>(); TSN peer_initial_tsn(buffer.Load32<16>());
uint32_t tie_tag_upper = buffer.Load32<20>(); TSN my_initial_tsn(buffer.Load32<20>());
uint32_t tie_tag_lower = buffer.Load32<24>(); uint32_t a_rwnd = buffer.Load32<24>();
uint32_t tie_tag_upper = buffer.Load32<28>();
uint32_t tie_tag_lower = buffer.Load32<32>();
TieTag tie_tag(static_cast<uint64_t>(tie_tag_upper) << 32 | TieTag tie_tag(static_cast<uint64_t>(tie_tag_upper) << 32 |
static_cast<uint64_t>(tie_tag_lower)); static_cast<uint64_t>(tie_tag_lower));
Capabilities capabilities; Capabilities capabilities;
capabilities.partial_reliability = buffer.Load8<28>() != 0; capabilities.partial_reliability = buffer.Load8<36>() != 0;
capabilities.message_interleaving = buffer.Load8<29>() != 0; capabilities.message_interleaving = buffer.Load8<37>() != 0;
capabilities.reconfig = buffer.Load8<30>() != 0; capabilities.reconfig = buffer.Load8<38>() != 0;
capabilities.negotiated_maximum_incoming_streams = buffer.Load16<32>(); capabilities.negotiated_maximum_incoming_streams = buffer.Load16<40>();
capabilities.negotiated_maximum_outgoing_streams = buffer.Load16<34>(); capabilities.negotiated_maximum_outgoing_streams = buffer.Load16<42>();
capabilities.zero_checksum = buffer.Load8<36>() != 0; capabilities.zero_checksum = buffer.Load8<44>() != 0;
return StateCookie(verification_tag, initial_tsn, a_rwnd, tie_tag, return StateCookie(peer_tag, my_tag, peer_initial_tsn, my_initial_tsn, a_rwnd,
capabilities); tie_tag, capabilities);
} }
} // namespace dcsctp } // namespace dcsctp

View file

@ -27,15 +27,19 @@ namespace dcsctp {
// Do not trust anything in it; no pointers or anything like that. // Do not trust anything in it; no pointers or anything like that.
class StateCookie { class StateCookie {
public: public:
static constexpr size_t kCookieSize = 37; static constexpr size_t kCookieSize = 45;
StateCookie(VerificationTag initiate_tag, StateCookie(VerificationTag peer_tag,
TSN initial_tsn, VerificationTag my_tag,
TSN peer_initial_tsn,
TSN my_initial_tsn,
uint32_t a_rwnd, uint32_t a_rwnd,
TieTag tie_tag, TieTag tie_tag,
Capabilities capabilities) Capabilities capabilities)
: initiate_tag_(initiate_tag), : peer_tag_(peer_tag),
initial_tsn_(initial_tsn), my_tag_(my_tag),
peer_initial_tsn_(peer_initial_tsn),
my_initial_tsn_(my_initial_tsn),
a_rwnd_(a_rwnd), a_rwnd_(a_rwnd),
tie_tag_(tie_tag), tie_tag_(tie_tag),
capabilities_(capabilities) {} capabilities_(capabilities) {}
@ -47,15 +51,21 @@ class StateCookie {
static absl::optional<StateCookie> Deserialize( static absl::optional<StateCookie> Deserialize(
rtc::ArrayView<const uint8_t> cookie); rtc::ArrayView<const uint8_t> cookie);
VerificationTag initiate_tag() const { return initiate_tag_; } VerificationTag peer_tag() const { return peer_tag_; }
TSN initial_tsn() const { return initial_tsn_; } VerificationTag my_tag() const { return my_tag_; }
TSN peer_initial_tsn() const { return peer_initial_tsn_; }
TSN my_initial_tsn() const { return my_initial_tsn_; }
uint32_t a_rwnd() const { return a_rwnd_; } uint32_t a_rwnd() const { return a_rwnd_; }
TieTag tie_tag() const { return tie_tag_; } TieTag tie_tag() const { return tie_tag_; }
const Capabilities& capabilities() const { return capabilities_; } const Capabilities& capabilities() const { return capabilities_; }
private: private:
const VerificationTag initiate_tag_; // Also called "Tag_A" in RFC4960.
const TSN initial_tsn_; const VerificationTag peer_tag_;
// Also called "Tag_Z" in RFC4960.
const VerificationTag my_tag_;
const TSN peer_initial_tsn_;
const TSN my_initial_tsn_;
const uint32_t a_rwnd_; const uint32_t a_rwnd_;
const TieTag tie_tag_; const TieTag tie_tag_;
const Capabilities capabilities_; const Capabilities capabilities_;

View file

@ -24,14 +24,18 @@ TEST(StateCookieTest, SerializeAndDeserialize) {
.zero_checksum = true, .zero_checksum = true,
.negotiated_maximum_incoming_streams = 123, .negotiated_maximum_incoming_streams = 123,
.negotiated_maximum_outgoing_streams = 234}; .negotiated_maximum_outgoing_streams = 234};
StateCookie cookie(VerificationTag(123), TSN(456), StateCookie cookie(/*peer_tag=*/VerificationTag(123),
/*my_tag=*/VerificationTag(321),
/*peer_initial_tsn=*/TSN(456), /*my_initial_tsn=*/TSN(654),
/*a_rwnd=*/789, TieTag(101112), capabilities); /*a_rwnd=*/789, TieTag(101112), capabilities);
std::vector<uint8_t> serialized = cookie.Serialize(); std::vector<uint8_t> serialized = cookie.Serialize();
EXPECT_THAT(serialized, SizeIs(StateCookie::kCookieSize)); EXPECT_THAT(serialized, SizeIs(StateCookie::kCookieSize));
ASSERT_HAS_VALUE_AND_ASSIGN(StateCookie deserialized, ASSERT_HAS_VALUE_AND_ASSIGN(StateCookie deserialized,
StateCookie::Deserialize(serialized)); StateCookie::Deserialize(serialized));
EXPECT_EQ(deserialized.initiate_tag(), VerificationTag(123)); EXPECT_EQ(deserialized.peer_tag(), VerificationTag(123));
EXPECT_EQ(deserialized.initial_tsn(), TSN(456)); EXPECT_EQ(deserialized.my_tag(), VerificationTag(321));
EXPECT_EQ(deserialized.peer_initial_tsn(), TSN(456));
EXPECT_EQ(deserialized.my_initial_tsn(), TSN(654));
EXPECT_EQ(deserialized.a_rwnd(), 789u); EXPECT_EQ(deserialized.a_rwnd(), 789u);
EXPECT_EQ(deserialized.tie_tag(), TieTag(101112)); EXPECT_EQ(deserialized.tie_tag(), TieTag(101112));
EXPECT_TRUE(deserialized.capabilities().partial_reliability); EXPECT_TRUE(deserialized.capabilities().partial_reliability);
@ -48,7 +52,9 @@ TEST(StateCookieTest, ValidateMagicValue) {
Capabilities capabilities = {.partial_reliability = true, Capabilities capabilities = {.partial_reliability = true,
.message_interleaving = false, .message_interleaving = false,
.reconfig = true}; .reconfig = true};
StateCookie cookie(VerificationTag(123), TSN(456), StateCookie cookie(/*peer_tag=*/VerificationTag(123),
/*my_tag=*/VerificationTag(321),
/*peer_initial_tsn=*/TSN(456), /*my_initial_tsn=*/TSN(654),
/*a_rwnd=*/789, TieTag(101112), capabilities); /*a_rwnd=*/789, TieTag(101112), capabilities);
std::vector<uint8_t> serialized = cookie.Serialize(); std::vector<uint8_t> serialized = cookie.Serialize();
ASSERT_THAT(serialized, SizeIs(StateCookie::kCookieSize)); ASSERT_THAT(serialized, SizeIs(StateCookie::kCookieSize));