mirror of
https://github.com/mollyim/webrtc.git
synced 2025-05-20 00:57:49 +01:00

This completes the basic implementation of the dcSCTP library. There are a few remaining commits to e.g. add compatibility tests and benchmarks, as well as more support for e.g. RFC8260, but those are not strictly vital for evaluation of the library. The Socket contains the connection establishment and teardown sequences as well as the general chunk dispatcher. Bug: webrtc:12614 Change-Id: I313b6c8f4accc144e3bb88ddba22269ebb8eb3cd Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/214342 Commit-Queue: Victor Boivie <boivie@webrtc.org> Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org> Reviewed-by: Tommi <tommi@webrtc.org> Reviewed-by: Harald Alvestrand <hta@webrtc.org> Cr-Commit-Position: refs/heads/master@{#33890}
1089 lines
38 KiB
C++
1089 lines
38 KiB
C++
/*
|
|
* Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree. An additional intellectual property rights grant can be found
|
|
* in the file PATENTS. All contributing project authors may
|
|
* be found in the AUTHORS file in the root of the source tree.
|
|
*/
|
|
#include "net/dcsctp/socket/dcsctp_socket.h"
|
|
|
|
#include <cstdint>
|
|
#include <deque>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "absl/memory/memory.h"
|
|
#include "absl/strings/string_view.h"
|
|
#include "absl/types/optional.h"
|
|
#include "api/array_view.h"
|
|
#include "net/dcsctp/packet/chunk/chunk.h"
|
|
#include "net/dcsctp/packet/chunk/cookie_echo_chunk.h"
|
|
#include "net/dcsctp/packet/chunk/data_chunk.h"
|
|
#include "net/dcsctp/packet/chunk/data_common.h"
|
|
#include "net/dcsctp/packet/chunk/error_chunk.h"
|
|
#include "net/dcsctp/packet/chunk/heartbeat_ack_chunk.h"
|
|
#include "net/dcsctp/packet/chunk/heartbeat_request_chunk.h"
|
|
#include "net/dcsctp/packet/chunk/idata_chunk.h"
|
|
#include "net/dcsctp/packet/chunk/init_chunk.h"
|
|
#include "net/dcsctp/packet/chunk/sack_chunk.h"
|
|
#include "net/dcsctp/packet/error_cause/error_cause.h"
|
|
#include "net/dcsctp/packet/error_cause/unrecognized_chunk_type_cause.h"
|
|
#include "net/dcsctp/packet/parameter/heartbeat_info_parameter.h"
|
|
#include "net/dcsctp/packet/parameter/parameter.h"
|
|
#include "net/dcsctp/packet/sctp_packet.h"
|
|
#include "net/dcsctp/packet/tlv_trait.h"
|
|
#include "net/dcsctp/public/dcsctp_message.h"
|
|
#include "net/dcsctp/public/dcsctp_options.h"
|
|
#include "net/dcsctp/public/dcsctp_socket.h"
|
|
#include "net/dcsctp/rx/reassembly_queue.h"
|
|
#include "net/dcsctp/socket/mock_dcsctp_socket_callbacks.h"
|
|
#include "net/dcsctp/testing/testing_macros.h"
|
|
#include "rtc_base/gunit.h"
|
|
#include "test/gmock.h"
|
|
|
|
namespace dcsctp {
|
|
namespace {
|
|
using ::testing::_;
|
|
using ::testing::AllOf;
|
|
using ::testing::ElementsAre;
|
|
using ::testing::HasSubstr;
|
|
using ::testing::IsEmpty;
|
|
using ::testing::SizeIs;
|
|
|
|
constexpr SendOptions kSendOptions;
|
|
|
|
MATCHER_P(HasDataChunkWithSsn, ssn, "") {
|
|
absl::optional<SctpPacket> packet = SctpPacket::Parse(arg);
|
|
if (!packet.has_value()) {
|
|
*result_listener << "data didn't parse as an SctpPacket";
|
|
return false;
|
|
}
|
|
|
|
if (packet->descriptors()[0].type != DataChunk::kType) {
|
|
*result_listener << "the first chunk in the packet is not a data chunk";
|
|
return false;
|
|
}
|
|
|
|
absl::optional<DataChunk> dc =
|
|
DataChunk::Parse(packet->descriptors()[0].data);
|
|
if (!dc.has_value()) {
|
|
*result_listener << "The first chunk didn't parse as a data chunk";
|
|
return false;
|
|
}
|
|
|
|
if (dc->ssn() != ssn) {
|
|
*result_listener << "the ssn is " << *dc->ssn();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
MATCHER_P(HasDataChunkWithMid, mid, "") {
|
|
absl::optional<SctpPacket> packet = SctpPacket::Parse(arg);
|
|
if (!packet.has_value()) {
|
|
*result_listener << "data didn't parse as an SctpPacket";
|
|
return false;
|
|
}
|
|
|
|
if (packet->descriptors()[0].type != IDataChunk::kType) {
|
|
*result_listener << "the first chunk in the packet is not an i-data chunk";
|
|
return false;
|
|
}
|
|
|
|
absl::optional<IDataChunk> dc =
|
|
IDataChunk::Parse(packet->descriptors()[0].data);
|
|
if (!dc.has_value()) {
|
|
*result_listener << "The first chunk didn't parse as an i-data chunk";
|
|
return false;
|
|
}
|
|
|
|
if (dc->message_id() != mid) {
|
|
*result_listener << "the mid is " << *dc->message_id();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
MATCHER_P(HasSackWithCumAckTsn, tsn, "") {
|
|
absl::optional<SctpPacket> packet = SctpPacket::Parse(arg);
|
|
if (!packet.has_value()) {
|
|
*result_listener << "data didn't parse as an SctpPacket";
|
|
return false;
|
|
}
|
|
|
|
if (packet->descriptors()[0].type != SackChunk::kType) {
|
|
*result_listener << "the first chunk in the packet is not a data chunk";
|
|
return false;
|
|
}
|
|
|
|
absl::optional<SackChunk> sc =
|
|
SackChunk::Parse(packet->descriptors()[0].data);
|
|
if (!sc.has_value()) {
|
|
*result_listener << "The first chunk didn't parse as a data chunk";
|
|
return false;
|
|
}
|
|
|
|
if (sc->cumulative_tsn_ack() != tsn) {
|
|
*result_listener << "the cum_ack_tsn is " << *sc->cumulative_tsn_ack();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
MATCHER(HasSackWithNoGapAckBlocks, "") {
|
|
absl::optional<SctpPacket> packet = SctpPacket::Parse(arg);
|
|
if (!packet.has_value()) {
|
|
*result_listener << "data didn't parse as an SctpPacket";
|
|
return false;
|
|
}
|
|
|
|
if (packet->descriptors()[0].type != SackChunk::kType) {
|
|
*result_listener << "the first chunk in the packet is not a data chunk";
|
|
return false;
|
|
}
|
|
|
|
absl::optional<SackChunk> sc =
|
|
SackChunk::Parse(packet->descriptors()[0].data);
|
|
if (!sc.has_value()) {
|
|
*result_listener << "The first chunk didn't parse as a data chunk";
|
|
return false;
|
|
}
|
|
|
|
if (!sc->gap_ack_blocks().empty()) {
|
|
*result_listener << "there are gap ack blocks";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
TSN AddTo(TSN tsn, int delta) {
|
|
return TSN(*tsn + delta);
|
|
}
|
|
|
|
DcSctpOptions MakeOptionsForTest(bool enable_message_interleaving) {
|
|
DcSctpOptions options;
|
|
// To make the interval more predictable in tests.
|
|
options.heartbeat_interval_include_rtt = false;
|
|
options.enable_message_interleaving = enable_message_interleaving;
|
|
return options;
|
|
}
|
|
|
|
class DcSctpSocketTest : public testing::Test {
|
|
protected:
|
|
explicit DcSctpSocketTest(bool enable_message_interleaving = false)
|
|
: options_(MakeOptionsForTest(enable_message_interleaving)),
|
|
sock_a_("A", cb_a_, nullptr, options_),
|
|
sock_z_("Z", cb_z_, nullptr, options_) {}
|
|
|
|
void AdvanceTime(DurationMs duration) {
|
|
cb_a_.AdvanceTime(duration);
|
|
cb_z_.AdvanceTime(duration);
|
|
}
|
|
|
|
static void ExchangeMessages(DcSctpSocket& sock_a,
|
|
MockDcSctpSocketCallbacks& cb_a,
|
|
DcSctpSocket& sock_z,
|
|
MockDcSctpSocketCallbacks& cb_z) {
|
|
bool delivered_packet = false;
|
|
do {
|
|
delivered_packet = false;
|
|
std::vector<uint8_t> packet_from_a = cb_a.ConsumeSentPacket();
|
|
if (!packet_from_a.empty()) {
|
|
delivered_packet = true;
|
|
sock_z.ReceivePacket(std::move(packet_from_a));
|
|
}
|
|
std::vector<uint8_t> packet_from_z = cb_z.ConsumeSentPacket();
|
|
if (!packet_from_z.empty()) {
|
|
delivered_packet = true;
|
|
sock_a.ReceivePacket(std::move(packet_from_z));
|
|
}
|
|
} while (delivered_packet);
|
|
}
|
|
|
|
void RunTimers() {
|
|
for (const auto timeout_id : cb_a_.RunTimers()) {
|
|
sock_a_.HandleTimeout(timeout_id);
|
|
}
|
|
for (const auto timeout_id : cb_z_.RunTimers()) {
|
|
sock_z_.HandleTimeout(timeout_id);
|
|
}
|
|
}
|
|
|
|
const DcSctpOptions options_;
|
|
testing::NiceMock<MockDcSctpSocketCallbacks> cb_a_;
|
|
testing::NiceMock<MockDcSctpSocketCallbacks> cb_z_;
|
|
DcSctpSocket sock_a_;
|
|
DcSctpSocket sock_z_;
|
|
};
|
|
|
|
TEST_F(DcSctpSocketTest, EstablishConnection) {
|
|
EXPECT_CALL(cb_a_, OnConnected).Times(1);
|
|
EXPECT_CALL(cb_z_, OnConnected).Times(1);
|
|
EXPECT_CALL(cb_a_, OnConnectionRestarted).Times(0);
|
|
EXPECT_CALL(cb_z_, OnConnectionRestarted).Times(0);
|
|
|
|
sock_a_.Connect();
|
|
// Z reads INIT, produces INIT_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// A reads INIT_ACK, produces COOKIE_ECHO
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
// Z reads COOKIE_ECHO, produces COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// A reads COOKIE_ACK.
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, EstablishConnectionWithSetupCollision) {
|
|
EXPECT_CALL(cb_a_, OnConnected).Times(1);
|
|
EXPECT_CALL(cb_z_, OnConnected).Times(1);
|
|
EXPECT_CALL(cb_a_, OnConnectionRestarted).Times(0);
|
|
EXPECT_CALL(cb_z_, OnConnectionRestarted).Times(0);
|
|
sock_a_.Connect();
|
|
sock_z_.Connect();
|
|
|
|
ExchangeMessages(sock_a_, cb_a_, sock_z_, cb_z_);
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, EstablishSimultaneousConnection) {
|
|
EXPECT_CALL(cb_a_, OnConnected).Times(1);
|
|
EXPECT_CALL(cb_z_, OnConnected).Times(1);
|
|
EXPECT_CALL(cb_a_, OnConnectionRestarted).Times(0);
|
|
EXPECT_CALL(cb_z_, OnConnectionRestarted).Times(0);
|
|
sock_a_.Connect();
|
|
|
|
// INIT isn't received by Z, as it wasn't ready yet.
|
|
cb_a_.ConsumeSentPacket();
|
|
|
|
sock_z_.Connect();
|
|
|
|
// A reads INIT, produces INIT_ACK
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
// Z reads INIT_ACK, sends COOKIE_ECHO
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
|
|
// A reads COOKIE_ECHO - establishes connection.
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
|
|
// Proceed with the remaining packets.
|
|
ExchangeMessages(sock_a_, cb_a_, sock_z_, cb_z_);
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, EstablishConnectionLostCookieAck) {
|
|
EXPECT_CALL(cb_a_, OnConnected).Times(1);
|
|
EXPECT_CALL(cb_z_, OnConnected).Times(1);
|
|
EXPECT_CALL(cb_a_, OnConnectionRestarted).Times(0);
|
|
EXPECT_CALL(cb_z_, OnConnectionRestarted).Times(0);
|
|
|
|
sock_a_.Connect();
|
|
// Z reads INIT, produces INIT_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// A reads INIT_ACK, produces COOKIE_ECHO
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
// Z reads COOKIE_ECHO, produces COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// COOKIE_ACK is lost.
|
|
cb_z_.ConsumeSentPacket();
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnecting);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
// This will make A re-send the COOKIE_ECHO
|
|
AdvanceTime(DurationMs(options_.t1_cookie_timeout));
|
|
RunTimers();
|
|
|
|
// Z reads COOKIE_ECHO, produces COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// A reads COOKIE_ACK.
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, ResendInitAndEstablishConnection) {
|
|
sock_a_.Connect();
|
|
// INIT is never received by Z.
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket init_packet,
|
|
SctpPacket::Parse(cb_a_.ConsumeSentPacket()));
|
|
EXPECT_EQ(init_packet.descriptors()[0].type, InitChunk::kType);
|
|
|
|
AdvanceTime(options_.t1_init_timeout);
|
|
RunTimers();
|
|
|
|
// Z reads INIT, produces INIT_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// A reads INIT_ACK, produces COOKIE_ECHO
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
// Z reads COOKIE_ECHO, produces COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// A reads COOKIE_ACK.
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, ResendingInitTooManyTimesAborts) {
|
|
sock_a_.Connect();
|
|
|
|
// INIT is never received by Z.
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket init_packet,
|
|
SctpPacket::Parse(cb_a_.ConsumeSentPacket()));
|
|
EXPECT_EQ(init_packet.descriptors()[0].type, InitChunk::kType);
|
|
|
|
for (int i = 0; i < options_.max_init_retransmits; ++i) {
|
|
AdvanceTime(options_.t1_init_timeout * (1 << i));
|
|
RunTimers();
|
|
|
|
// INIT is resent
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket resent_init_packet,
|
|
SctpPacket::Parse(cb_a_.ConsumeSentPacket()));
|
|
EXPECT_EQ(resent_init_packet.descriptors()[0].type, InitChunk::kType);
|
|
}
|
|
|
|
// Another timeout, after the max init retransmits.
|
|
AdvanceTime(options_.t1_init_timeout * (1 << options_.max_init_retransmits));
|
|
EXPECT_CALL(cb_a_, OnAborted).Times(1);
|
|
RunTimers();
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kClosed);
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, ResendCookieEchoAndEstablishConnection) {
|
|
sock_a_.Connect();
|
|
|
|
// Z reads INIT, produces INIT_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// A reads INIT_ACK, produces COOKIE_ECHO
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
// COOKIE_ECHO is never received by Z.
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket init_packet,
|
|
SctpPacket::Parse(cb_a_.ConsumeSentPacket()));
|
|
EXPECT_EQ(init_packet.descriptors()[0].type, CookieEchoChunk::kType);
|
|
|
|
AdvanceTime(options_.t1_init_timeout);
|
|
RunTimers();
|
|
|
|
// Z reads COOKIE_ECHO, produces COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// A reads COOKIE_ACK.
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, ResendingCookieEchoTooManyTimesAborts) {
|
|
sock_a_.Connect();
|
|
|
|
// Z reads INIT, produces INIT_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// A reads INIT_ACK, produces COOKIE_ECHO
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
// COOKIE_ECHO is never received by Z.
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket init_packet,
|
|
SctpPacket::Parse(cb_a_.ConsumeSentPacket()));
|
|
EXPECT_EQ(init_packet.descriptors()[0].type, CookieEchoChunk::kType);
|
|
|
|
for (int i = 0; i < options_.max_init_retransmits; ++i) {
|
|
AdvanceTime(options_.t1_cookie_timeout * (1 << i));
|
|
RunTimers();
|
|
|
|
// COOKIE_ECHO is resent
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket resent_init_packet,
|
|
SctpPacket::Parse(cb_a_.ConsumeSentPacket()));
|
|
EXPECT_EQ(resent_init_packet.descriptors()[0].type, CookieEchoChunk::kType);
|
|
}
|
|
|
|
// Another timeout, after the max init retransmits.
|
|
AdvanceTime(options_.t1_cookie_timeout *
|
|
(1 << options_.max_init_retransmits));
|
|
EXPECT_CALL(cb_a_, OnAborted).Times(1);
|
|
RunTimers();
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kClosed);
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, ShutdownConnection) {
|
|
sock_a_.Connect();
|
|
|
|
ExchangeMessages(sock_a_, cb_a_, sock_z_, cb_z_);
|
|
|
|
RTC_LOG(LS_INFO) << "Shutting down";
|
|
|
|
sock_a_.Shutdown();
|
|
// Z reads SHUTDOWN, produces SHUTDOWN_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// A reads SHUTDOWN_ACK, produces SHUTDOWN_COMPLETE
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
// Z reads SHUTDOWN_COMPLETE.
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kClosed);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kClosed);
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, EstablishConnectionWhileSendingData) {
|
|
sock_a_.Connect();
|
|
|
|
sock_a_.Send(DcSctpMessage(StreamID(1), PPID(53), {1, 2}), kSendOptions);
|
|
|
|
// Z reads INIT, produces INIT_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// // A reads INIT_ACK, produces COOKIE_ECHO
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
// // Z reads COOKIE_ECHO, produces COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// // A reads COOKIE_ACK.
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
absl::optional<DcSctpMessage> msg = cb_z_.ConsumeReceivedMessage();
|
|
ASSERT_TRUE(msg.has_value());
|
|
EXPECT_EQ(msg->stream_id(), StreamID(1));
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, SendMessageAfterEstablished) {
|
|
sock_a_.Connect();
|
|
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
sock_a_.Send(DcSctpMessage(StreamID(1), PPID(53), {1, 2}), kSendOptions);
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
|
|
absl::optional<DcSctpMessage> msg = cb_z_.ConsumeReceivedMessage();
|
|
ASSERT_TRUE(msg.has_value());
|
|
EXPECT_EQ(msg->stream_id(), StreamID(1));
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, TimeoutResendsPacket) {
|
|
sock_a_.Connect();
|
|
|
|
// Z reads INIT, produces INIT_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// // A reads INIT_ACK, produces COOKIE_ECHO
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
// // Z reads COOKIE_ECHO, produces COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// // A reads COOKIE_ACK.
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
sock_a_.Send(DcSctpMessage(StreamID(1), PPID(53), {1, 2}), kSendOptions);
|
|
cb_a_.ConsumeSentPacket();
|
|
|
|
RTC_LOG(LS_INFO) << "Advancing time";
|
|
AdvanceTime(options_.rto_initial);
|
|
RunTimers();
|
|
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
|
|
absl::optional<DcSctpMessage> msg = cb_z_.ConsumeReceivedMessage();
|
|
ASSERT_TRUE(msg.has_value());
|
|
EXPECT_EQ(msg->stream_id(), StreamID(1));
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, SendALotOfBytesMissedSecondPacket) {
|
|
sock_a_.Connect();
|
|
|
|
// Z reads INIT, produces INIT_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// // A reads INIT_ACK, produces COOKIE_ECHO
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
// // Z reads COOKIE_ECHO, produces COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// // A reads COOKIE_ACK.
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
std::vector<uint8_t> payload(options_.mtu * 10);
|
|
sock_a_.Send(DcSctpMessage(StreamID(1), PPID(53), payload), kSendOptions);
|
|
|
|
// First DATA
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// Second DATA (lost)
|
|
cb_a_.ConsumeSentPacket();
|
|
|
|
// Retransmit and handle the rest
|
|
ExchangeMessages(sock_a_, cb_a_, sock_z_, cb_z_);
|
|
|
|
absl::optional<DcSctpMessage> msg = cb_z_.ConsumeReceivedMessage();
|
|
ASSERT_TRUE(msg.has_value());
|
|
EXPECT_EQ(msg->stream_id(), StreamID(1));
|
|
EXPECT_THAT(msg->payload(), testing::ElementsAreArray(payload));
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, SendingHeartbeatAnswersWithAck) {
|
|
sock_a_.Connect();
|
|
// Z reads INIT, INIT_ACK, COOKIE_ECHO, COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
// Inject a HEARTBEAT chunk
|
|
SctpPacket::Builder b(sock_a_.verification_tag(), DcSctpOptions());
|
|
uint8_t info[] = {1, 2, 3, 4};
|
|
Parameters::Builder params_builder;
|
|
params_builder.Add(HeartbeatInfoParameter(info));
|
|
b.Add(HeartbeatRequestChunk(params_builder.Build()));
|
|
sock_a_.ReceivePacket(b.Build());
|
|
|
|
// HEARTBEAT_ACK is sent as a reply. Capture it.
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket ack_packet,
|
|
SctpPacket::Parse(cb_a_.ConsumeSentPacket()));
|
|
ASSERT_THAT(ack_packet.descriptors(), SizeIs(1));
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(
|
|
HeartbeatAckChunk ack,
|
|
HeartbeatAckChunk::Parse(ack_packet.descriptors()[0].data));
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(HeartbeatInfoParameter info_param, ack.info());
|
|
EXPECT_THAT(info_param.info(), ElementsAre(1, 2, 3, 4));
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, ExpectHeartbeatToBeSent) {
|
|
sock_a_.Connect();
|
|
// Z reads INIT, INIT_ACK, COOKIE_ECHO, COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
EXPECT_THAT(cb_a_.ConsumeSentPacket(), IsEmpty());
|
|
|
|
AdvanceTime(options_.heartbeat_interval);
|
|
RunTimers();
|
|
|
|
std::vector<uint8_t> hb_packet_raw = cb_a_.ConsumeSentPacket();
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket hb_packet,
|
|
SctpPacket::Parse(hb_packet_raw));
|
|
ASSERT_THAT(hb_packet.descriptors(), SizeIs(1));
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(
|
|
HeartbeatRequestChunk hb,
|
|
HeartbeatRequestChunk::Parse(hb_packet.descriptors()[0].data));
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(HeartbeatInfoParameter info_param, hb.info());
|
|
|
|
// The info is a single 64-bit number.
|
|
EXPECT_THAT(hb.info()->info(), SizeIs(8));
|
|
|
|
// Feed it to Sock-z and expect a HEARTBEAT_ACK that will be propagated back.
|
|
sock_z_.ReceivePacket(hb_packet_raw);
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, CloseConnectionAfterTooManyLostHeartbeats) {
|
|
sock_a_.Connect();
|
|
// Z reads INIT, INIT_ACK, COOKIE_ECHO, COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
EXPECT_THAT(cb_a_.ConsumeSentPacket(), testing::IsEmpty());
|
|
// Force-close socket Z so that it doesn't interfere from now on.
|
|
sock_z_.Close();
|
|
|
|
DurationMs time_to_next_hearbeat = options_.heartbeat_interval;
|
|
|
|
for (int i = 0; i < options_.max_retransmissions; ++i) {
|
|
RTC_LOG(LS_INFO) << "Letting HEARTBEAT interval timer expire - sending...";
|
|
AdvanceTime(time_to_next_hearbeat);
|
|
RunTimers();
|
|
|
|
// Dropping every heartbeat.
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket hb_packet,
|
|
SctpPacket::Parse(cb_a_.ConsumeSentPacket()));
|
|
EXPECT_EQ(hb_packet.descriptors()[0].type, HeartbeatRequestChunk::kType);
|
|
|
|
RTC_LOG(LS_INFO) << "Letting the heartbeat expire.";
|
|
AdvanceTime(DurationMs(1000));
|
|
RunTimers();
|
|
|
|
time_to_next_hearbeat = options_.heartbeat_interval - DurationMs(1000);
|
|
}
|
|
|
|
RTC_LOG(LS_INFO) << "Letting HEARTBEAT interval timer expire - sending...";
|
|
AdvanceTime(time_to_next_hearbeat);
|
|
RunTimers();
|
|
|
|
// Last heartbeat
|
|
EXPECT_THAT(cb_a_.ConsumeSentPacket(), Not(IsEmpty()));
|
|
|
|
EXPECT_CALL(cb_a_, OnAborted).Times(1);
|
|
// Should suffice as exceeding RTO
|
|
AdvanceTime(DurationMs(1000));
|
|
RunTimers();
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, RecoversAfterASuccessfulAck) {
|
|
sock_a_.Connect();
|
|
// Z reads INIT, INIT_ACK, COOKIE_ECHO, COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
EXPECT_THAT(cb_a_.ConsumeSentPacket(), testing::IsEmpty());
|
|
// Force-close socket Z so that it doesn't interfere from now on.
|
|
sock_z_.Close();
|
|
|
|
DurationMs time_to_next_hearbeat = options_.heartbeat_interval;
|
|
|
|
for (int i = 0; i < options_.max_retransmissions; ++i) {
|
|
AdvanceTime(time_to_next_hearbeat);
|
|
RunTimers();
|
|
|
|
// Dropping every heartbeat.
|
|
cb_a_.ConsumeSentPacket();
|
|
|
|
RTC_LOG(LS_INFO) << "Letting the heartbeat expire.";
|
|
AdvanceTime(DurationMs(1000));
|
|
RunTimers();
|
|
|
|
time_to_next_hearbeat = options_.heartbeat_interval - DurationMs(1000);
|
|
}
|
|
|
|
RTC_LOG(LS_INFO) << "Getting the last heartbeat - and acking it";
|
|
AdvanceTime(time_to_next_hearbeat);
|
|
RunTimers();
|
|
|
|
std::vector<uint8_t> hb_packet_raw = cb_a_.ConsumeSentPacket();
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket hb_packet,
|
|
SctpPacket::Parse(hb_packet_raw));
|
|
ASSERT_THAT(hb_packet.descriptors(), SizeIs(1));
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(
|
|
HeartbeatRequestChunk hb,
|
|
HeartbeatRequestChunk::Parse(hb_packet.descriptors()[0].data));
|
|
|
|
SctpPacket::Builder b(sock_a_.verification_tag(), options_);
|
|
b.Add(HeartbeatAckChunk(std::move(hb).extract_parameters()));
|
|
sock_a_.ReceivePacket(b.Build());
|
|
|
|
// Should suffice as exceeding RTO - which will not fire.
|
|
EXPECT_CALL(cb_a_, OnAborted).Times(0);
|
|
AdvanceTime(DurationMs(1000));
|
|
RunTimers();
|
|
EXPECT_THAT(cb_a_.ConsumeSentPacket(), IsEmpty());
|
|
|
|
// Verify that we get new heartbeats again.
|
|
RTC_LOG(LS_INFO) << "Expecting a new heartbeat";
|
|
AdvanceTime(time_to_next_hearbeat);
|
|
RunTimers();
|
|
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket another_packet,
|
|
SctpPacket::Parse(cb_a_.ConsumeSentPacket()));
|
|
EXPECT_EQ(another_packet.descriptors()[0].type, HeartbeatRequestChunk::kType);
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, ResetStream) {
|
|
sock_a_.Connect();
|
|
// Z reads INIT, INIT_ACK, COOKIE_ECHO, COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
sock_a_.Send(DcSctpMessage(StreamID(1), PPID(53), {1, 2}), {});
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
|
|
absl::optional<DcSctpMessage> msg = cb_z_.ConsumeReceivedMessage();
|
|
ASSERT_TRUE(msg.has_value());
|
|
EXPECT_EQ(msg->stream_id(), StreamID(1));
|
|
|
|
// Handle SACK
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
// Reset the outgoing stream. This will directly send a RE-CONFIG.
|
|
sock_a_.ResetStreams(std::vector<StreamID>({StreamID(1)}));
|
|
|
|
// Receiving the packet will trigger a callback, indicating that A has
|
|
// reset its stream. It will also send a RE-CONFIG with a response.
|
|
EXPECT_CALL(cb_z_, OnIncomingStreamsReset).Times(1);
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
|
|
// Receiving a response will trigger a callback. Streams are now reset.
|
|
EXPECT_CALL(cb_a_, OnStreamsResetPerformed).Times(1);
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, ResetStreamWillMakeChunksStartAtZeroSsn) {
|
|
sock_a_.Connect();
|
|
// Z reads INIT, INIT_ACK, COOKIE_ECHO, COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
std::vector<uint8_t> payload(options_.mtu - 100);
|
|
|
|
sock_a_.Send(DcSctpMessage(StreamID(1), PPID(53), payload), {});
|
|
sock_a_.Send(DcSctpMessage(StreamID(1), PPID(53), payload), {});
|
|
|
|
auto packet1 = cb_a_.ConsumeSentPacket();
|
|
EXPECT_THAT(packet1, HasDataChunkWithSsn(SSN(0)));
|
|
sock_z_.ReceivePacket(packet1);
|
|
|
|
auto packet2 = cb_a_.ConsumeSentPacket();
|
|
EXPECT_THAT(packet2, HasDataChunkWithSsn(SSN(1)));
|
|
sock_z_.ReceivePacket(packet2);
|
|
|
|
// Handle SACK
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
absl::optional<DcSctpMessage> msg1 = cb_z_.ConsumeReceivedMessage();
|
|
ASSERT_TRUE(msg1.has_value());
|
|
EXPECT_EQ(msg1->stream_id(), StreamID(1));
|
|
|
|
absl::optional<DcSctpMessage> msg2 = cb_z_.ConsumeReceivedMessage();
|
|
ASSERT_TRUE(msg2.has_value());
|
|
EXPECT_EQ(msg2->stream_id(), StreamID(1));
|
|
|
|
// Reset the outgoing stream. This will directly send a RE-CONFIG.
|
|
sock_a_.ResetStreams(std::vector<StreamID>({StreamID(1)}));
|
|
// RE-CONFIG, req
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// RE-CONFIG, resp
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
sock_a_.Send(DcSctpMessage(StreamID(1), PPID(53), payload), {});
|
|
|
|
sock_a_.Send(DcSctpMessage(StreamID(1), PPID(53), payload), {});
|
|
|
|
auto packet3 = cb_a_.ConsumeSentPacket();
|
|
EXPECT_THAT(packet3, HasDataChunkWithSsn(SSN(0)));
|
|
sock_z_.ReceivePacket(packet3);
|
|
|
|
auto packet4 = cb_a_.ConsumeSentPacket();
|
|
EXPECT_THAT(packet4, HasDataChunkWithSsn(SSN(1)));
|
|
sock_z_.ReceivePacket(packet4);
|
|
|
|
// Handle SACK
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, OnePeerReconnects) {
|
|
EXPECT_CALL(cb_a_, OnConnectionRestarted).Times(1);
|
|
sock_a_.Connect();
|
|
// Z reads INIT, INIT_ACK, COOKIE_ECHO, COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
// Let's be evil here - reconnect while a fragmented packet was about to be
|
|
// sent. The receiving side should get it in full.
|
|
std::vector<uint8_t> payload(options_.mtu * 10);
|
|
sock_a_.Send(DcSctpMessage(StreamID(1), PPID(53), payload), kSendOptions);
|
|
|
|
// First DATA
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
|
|
// Create a new association, z2 - and don't use z anymore.
|
|
testing::NiceMock<MockDcSctpSocketCallbacks> cb_z2;
|
|
DcSctpSocket sock_z2("Z2", cb_z2, nullptr, options_);
|
|
|
|
sock_z2.Connect();
|
|
|
|
// Retransmit and handle the rest. As there will be some chunks in-flight that
|
|
// have the wrong verification tag, those will yield errors.
|
|
ExchangeMessages(sock_a_, cb_a_, sock_z2, cb_z2);
|
|
|
|
absl::optional<DcSctpMessage> msg = cb_z2.ConsumeReceivedMessage();
|
|
ASSERT_TRUE(msg.has_value());
|
|
EXPECT_EQ(msg->stream_id(), StreamID(1));
|
|
EXPECT_THAT(msg->payload(), testing::ElementsAreArray(payload));
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, SendMessageWithLimitedRtx) {
|
|
sock_a_.Connect();
|
|
// Z reads INIT, INIT_ACK, COOKIE_ECHO, COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
SendOptions send_options;
|
|
send_options.max_retransmissions = 0;
|
|
std::vector<uint8_t> payload(options_.mtu - 100);
|
|
sock_a_.Send(DcSctpMessage(StreamID(1), PPID(51), payload), send_options);
|
|
sock_a_.Send(DcSctpMessage(StreamID(1), PPID(52), payload), send_options);
|
|
sock_a_.Send(DcSctpMessage(StreamID(1), PPID(53), payload), send_options);
|
|
|
|
// First DATA
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
// Second DATA (lost)
|
|
cb_a_.ConsumeSentPacket();
|
|
// Third DATA
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
|
|
// Handle SACK
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
// Now the missing data chunk will be marked as nacked, but it might still be
|
|
// in-flight and the reported gap could be due to out-of-order delivery. So
|
|
// the RetransmissionQueue will not mark it as "to be retransmitted" until
|
|
// after the t3-rtx timer has expired.
|
|
AdvanceTime(options_.rto_initial);
|
|
RunTimers();
|
|
|
|
// The chunk will be marked as retransmitted, and then as abandoned, which
|
|
// will trigger a FORWARD-TSN to be sent.
|
|
|
|
// FORWARD-TSN (third)
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
|
|
// The receiver might have moved into delayed ack mode.
|
|
AdvanceTime(options_.rto_initial);
|
|
RunTimers();
|
|
|
|
// Handle SACK
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
absl::optional<DcSctpMessage> msg1 = cb_z_.ConsumeReceivedMessage();
|
|
ASSERT_TRUE(msg1.has_value());
|
|
EXPECT_EQ(msg1->ppid(), PPID(51));
|
|
|
|
absl::optional<DcSctpMessage> msg2 = cb_z_.ConsumeReceivedMessage();
|
|
ASSERT_TRUE(msg2.has_value());
|
|
EXPECT_EQ(msg2->ppid(), PPID(53));
|
|
|
|
absl::optional<DcSctpMessage> msg3 = cb_z_.ConsumeReceivedMessage();
|
|
EXPECT_FALSE(msg3.has_value());
|
|
}
|
|
|
|
struct FakeChunkConfig : ChunkConfig {
|
|
static constexpr int kType = 0x49;
|
|
static constexpr size_t kHeaderSize = 4;
|
|
static constexpr int kVariableLengthAlignment = 0;
|
|
};
|
|
|
|
class FakeChunk : public Chunk, public TLVTrait<FakeChunkConfig> {
|
|
public:
|
|
FakeChunk() {}
|
|
|
|
FakeChunk(FakeChunk&& other) = default;
|
|
FakeChunk& operator=(FakeChunk&& other) = default;
|
|
|
|
void SerializeTo(std::vector<uint8_t>& out) const override {
|
|
AllocateTLV(out);
|
|
}
|
|
std::string ToString() const override { return "FAKE"; }
|
|
};
|
|
|
|
TEST_F(DcSctpSocketTest, ReceivingUnknownChunkRespondsWithError) {
|
|
sock_a_.Connect();
|
|
// Z reads INIT, INIT_ACK, COOKIE_ECHO, COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
// Inject a FAKE chunk
|
|
SctpPacket::Builder b(sock_a_.verification_tag(), DcSctpOptions());
|
|
b.Add(FakeChunk());
|
|
sock_a_.ReceivePacket(b.Build());
|
|
|
|
// ERROR is sent as a reply. Capture it.
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket reply_packet,
|
|
SctpPacket::Parse(cb_a_.ConsumeSentPacket()));
|
|
ASSERT_THAT(reply_packet.descriptors(), SizeIs(1));
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(
|
|
ErrorChunk error, ErrorChunk::Parse(reply_packet.descriptors()[0].data));
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(
|
|
UnrecognizedChunkTypeCause cause,
|
|
error.error_causes().get<UnrecognizedChunkTypeCause>());
|
|
EXPECT_THAT(cause.unrecognized_chunk(), ElementsAre(0x49, 0x00, 0x00, 0x04));
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, ReceivingErrorChunkReportsAsCallback) {
|
|
sock_a_.Connect();
|
|
// Z reads INIT, INIT_ACK, COOKIE_ECHO, COOKIE_ACK
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
sock_z_.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z_.ConsumeSentPacket());
|
|
|
|
EXPECT_EQ(sock_a_.state(), SocketState::kConnected);
|
|
EXPECT_EQ(sock_z_.state(), SocketState::kConnected);
|
|
|
|
// Inject a ERROR chunk
|
|
SctpPacket::Builder b(sock_a_.verification_tag(), DcSctpOptions());
|
|
b.Add(
|
|
ErrorChunk(Parameters::Builder()
|
|
.Add(UnrecognizedChunkTypeCause({0x49, 0x00, 0x00, 0x04}))
|
|
.Build()));
|
|
|
|
EXPECT_CALL(cb_a_, OnError(ErrorKind::kPeerReported,
|
|
HasSubstr("Unrecognized Chunk Type")));
|
|
sock_a_.ReceivePacket(b.Build());
|
|
}
|
|
|
|
TEST_F(DcSctpSocketTest, PassingHighWatermarkWillOnlyAcceptCumAckTsn) {
|
|
// Create a new association, z2 - and don't use z anymore.
|
|
testing::NiceMock<MockDcSctpSocketCallbacks> cb_z2;
|
|
DcSctpOptions options = options_;
|
|
options.max_receiver_window_buffer_size = 100;
|
|
DcSctpSocket sock_z2("Z2", cb_z2, nullptr, options);
|
|
|
|
EXPECT_CALL(cb_z2, OnClosed).Times(0);
|
|
EXPECT_CALL(cb_z2, OnAborted).Times(0);
|
|
|
|
sock_a_.Connect();
|
|
std::vector<uint8_t> init_data = cb_a_.ConsumeSentPacket();
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(SctpPacket init_packet,
|
|
SctpPacket::Parse(init_data));
|
|
ASSERT_HAS_VALUE_AND_ASSIGN(
|
|
InitChunk init_chunk,
|
|
InitChunk::Parse(init_packet.descriptors()[0].data));
|
|
sock_z2.ReceivePacket(init_data);
|
|
sock_a_.ReceivePacket(cb_z2.ConsumeSentPacket());
|
|
sock_z2.ReceivePacket(cb_a_.ConsumeSentPacket());
|
|
sock_a_.ReceivePacket(cb_z2.ConsumeSentPacket());
|
|
|
|
// Fill up Z2 to the high watermark limit.
|
|
TSN tsn = init_chunk.initial_tsn();
|
|
AnyDataChunk::Options opts;
|
|
opts.is_beginning = Data::IsBeginning(true);
|
|
sock_z2.ReceivePacket(
|
|
SctpPacket::Builder(sock_z2.verification_tag(), options)
|
|
.Add(DataChunk(tsn, StreamID(1), SSN(0), PPID(53),
|
|
std::vector<uint8_t>(
|
|
100 * ReassemblyQueue::kHighWatermarkLimit + 1),
|
|
opts))
|
|
.Build());
|
|
|
|
// First DATA will always trigger a SACK. It's not interesting.
|
|
EXPECT_THAT(cb_z2.ConsumeSentPacket(),
|
|
AllOf(HasSackWithCumAckTsn(tsn), HasSackWithNoGapAckBlocks()));
|
|
|
|
// This DATA should be accepted - it's advancing cum ack tsn.
|
|
sock_z2.ReceivePacket(SctpPacket::Builder(sock_z2.verification_tag(), options)
|
|
.Add(DataChunk(AddTo(tsn, 1), StreamID(1), SSN(0),
|
|
PPID(53), std::vector<uint8_t>(1),
|
|
/*options=*/{}))
|
|
.Build());
|
|
|
|
// The receiver might have moved into delayed ack mode.
|
|
cb_z2.AdvanceTime(options.rto_initial);
|
|
for (const auto timeout_id : cb_z2.RunTimers()) {
|
|
sock_z2.HandleTimeout(timeout_id);
|
|
}
|
|
|
|
EXPECT_THAT(
|
|
cb_z2.ConsumeSentPacket(),
|
|
AllOf(HasSackWithCumAckTsn(AddTo(tsn, 1)), HasSackWithNoGapAckBlocks()));
|
|
|
|
// This DATA will not be accepted - it's not advancing cum ack tsn.
|
|
sock_z2.ReceivePacket(SctpPacket::Builder(sock_z2.verification_tag(), options)
|
|
.Add(DataChunk(AddTo(tsn, 3), StreamID(1), SSN(0),
|
|
PPID(53), std::vector<uint8_t>(1),
|
|
/*options=*/{}))
|
|
.Build());
|
|
|
|
// Sack will be sent in IMMEDIATE mode when this is happening.
|
|
EXPECT_THAT(
|
|
cb_z2.ConsumeSentPacket(),
|
|
AllOf(HasSackWithCumAckTsn(AddTo(tsn, 1)), HasSackWithNoGapAckBlocks()));
|
|
|
|
// This DATA will not be accepted either.
|
|
sock_z2.ReceivePacket(SctpPacket::Builder(sock_z2.verification_tag(), options)
|
|
.Add(DataChunk(AddTo(tsn, 4), StreamID(1), SSN(0),
|
|
PPID(53), std::vector<uint8_t>(1),
|
|
/*options=*/{}))
|
|
.Build());
|
|
|
|
// Sack will be sent in IMMEDIATE mode when this is happening.
|
|
EXPECT_THAT(
|
|
cb_z2.ConsumeSentPacket(),
|
|
AllOf(HasSackWithCumAckTsn(AddTo(tsn, 1)), HasSackWithNoGapAckBlocks()));
|
|
|
|
// This DATA should be accepted, and it fills the reassembly queue.
|
|
sock_z2.ReceivePacket(SctpPacket::Builder(sock_z2.verification_tag(), options)
|
|
.Add(DataChunk(AddTo(tsn, 2), StreamID(1), SSN(0),
|
|
PPID(53), std::vector<uint8_t>(10),
|
|
/*options=*/{}))
|
|
.Build());
|
|
|
|
// The receiver might have moved into delayed ack mode.
|
|
cb_z2.AdvanceTime(options.rto_initial);
|
|
for (const auto timeout_id : cb_z2.RunTimers()) {
|
|
sock_z2.HandleTimeout(timeout_id);
|
|
}
|
|
|
|
EXPECT_THAT(
|
|
cb_z2.ConsumeSentPacket(),
|
|
AllOf(HasSackWithCumAckTsn(AddTo(tsn, 2)), HasSackWithNoGapAckBlocks()));
|
|
|
|
EXPECT_CALL(cb_z2, OnAborted(ErrorKind::kResourceExhaustion, _));
|
|
EXPECT_CALL(cb_z2, OnClosed).Times(0);
|
|
|
|
// This DATA will make the connection close. It's too full now.
|
|
sock_z2.ReceivePacket(SctpPacket::Builder(sock_z2.verification_tag(), options)
|
|
.Add(DataChunk(AddTo(tsn, 3), StreamID(1), SSN(0),
|
|
PPID(53), std::vector<uint8_t>(10),
|
|
/*options=*/{}))
|
|
.Build());
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace dcsctp
|