diff --git a/p2p/BUILD.gn b/p2p/BUILD.gn index c155289e18..9e3655d89f 100644 --- a/p2p/BUILD.gn +++ b/p2p/BUILD.gn @@ -66,6 +66,7 @@ rtc_static_library("rtc_p2p") { "base/transportdescription.h", "base/transportdescriptionfactory.cc", "base/transportdescriptionfactory.h", + "base/transportfactoryinterface.h", "base/transportinfo.h", "base/turnport.cc", "base/turnport.h", diff --git a/p2p/base/dtlstransport.cc b/p2p/base/dtlstransport.cc index 0e7bd60754..a576a1bebc 100644 --- a/p2p/base/dtlstransport.cc +++ b/p2p/base/dtlstransport.cc @@ -121,20 +121,26 @@ DtlsTransport::DtlsTransport(IceTransportInternal* ice_transport, ice_transport_(ice_transport), downward_(NULL), srtp_ciphers_(GetSupportedDtlsSrtpCryptoSuites(crypto_options)), - ssl_role_(rtc::SSL_CLIENT), ssl_max_version_(rtc::SSL_PROTOCOL_DTLS_12), crypto_options_(crypto_options) { RTC_DCHECK(ice_transport_); - ice_transport_->SignalWritableState.connect(this, - &DtlsTransport::OnWritableState); - ice_transport_->SignalReadPacket.connect(this, &DtlsTransport::OnReadPacket); - ice_transport_->SignalSentPacket.connect(this, &DtlsTransport::OnSentPacket); - ice_transport_->SignalReadyToSend.connect(this, - &DtlsTransport::OnReadyToSend); - ice_transport_->SignalReceivingState.connect( - this, &DtlsTransport::OnReceivingState); - ice_transport_->SignalNetworkRouteChanged.connect( - this, &DtlsTransport::OnNetworkRouteChanged); + ConnectToIceTransport(); +} + +DtlsTransport::DtlsTransport( + std::unique_ptr ice_transport, + const rtc::CryptoOptions& crypto_options) + : transport_name_(ice_transport->transport_name()), + component_(ice_transport->component()), + network_thread_(rtc::Thread::Current()), + ice_transport_(ice_transport.get()), + owned_ice_transport_(std::move(ice_transport)), + downward_(NULL), + srtp_ciphers_(GetSupportedDtlsSrtpCryptoSuites(crypto_options)), + ssl_max_version_(rtc::SSL_PROTOCOL_DTLS_12), + crypto_options_(crypto_options) { + RTC_DCHECK(owned_ice_transport_); + ConnectToIceTransport(); } DtlsTransport::~DtlsTransport() = default; @@ -198,9 +204,10 @@ bool DtlsTransport::SetSslMaxProtocolVersion(rtc::SSLProtocolVersion version) { return true; } -bool DtlsTransport::SetSslRole(rtc::SSLRole role) { +bool DtlsTransport::SetDtlsRole(rtc::SSLRole role) { if (dtls_) { - if (ssl_role_ != role) { + RTC_DCHECK(dtls_role_); + if (*dtls_role_ != role) { RTC_LOG(LS_ERROR) << "SSL Role can't be reversed after the session is setup."; return false; @@ -208,12 +215,15 @@ bool DtlsTransport::SetSslRole(rtc::SSLRole role) { return true; } - ssl_role_ = role; + dtls_role_ = std::move(role); return true; } -bool DtlsTransport::GetSslRole(rtc::SSLRole* role) const { - *role = ssl_role_; +bool DtlsTransport::GetDtlsRole(rtc::SSLRole* role) const { + if (!dtls_role_) { + return false; + } + *role = *dtls_role_; return true; } @@ -330,6 +340,7 @@ bool DtlsTransport::ExportKeyingMaterial(const std::string& label, } bool DtlsTransport::SetupDtls() { + RTC_DCHECK(dtls_role_); StreamInterfaceChannel* downward = new StreamInterfaceChannel(ice_transport_); dtls_.reset(rtc::SSLStreamAdapter::Create(downward)); @@ -344,7 +355,7 @@ bool DtlsTransport::SetupDtls() { dtls_->SetIdentity(local_certificate_->identity()->GetReference()); dtls_->SetMode(rtc::SSL_MODE_DTLS); dtls_->SetMaxProtocolVersion(ssl_max_version_); - dtls_->SetServerRole(ssl_role_); + dtls_->SetServerRole(*dtls_role_); dtls_->SignalEvent.connect(this, &DtlsTransport::OnDtlsEvent); dtls_->SignalSSLHandshakeError.connect(this, &DtlsTransport::OnDtlsHandshakeError); @@ -456,6 +467,20 @@ int DtlsTransport::SetOption(rtc::Socket::Option opt, int value) { return ice_transport_->SetOption(opt, value); } +void DtlsTransport::ConnectToIceTransport() { + RTC_DCHECK(ice_transport_); + ice_transport_->SignalWritableState.connect(this, + &DtlsTransport::OnWritableState); + ice_transport_->SignalReadPacket.connect(this, &DtlsTransport::OnReadPacket); + ice_transport_->SignalSentPacket.connect(this, &DtlsTransport::OnSentPacket); + ice_transport_->SignalReadyToSend.connect(this, + &DtlsTransport::OnReadyToSend); + ice_transport_->SignalReceivingState.connect( + this, &DtlsTransport::OnReceivingState); + ice_transport_->SignalNetworkRouteChanged.connect( + this, &DtlsTransport::OnNetworkRouteChanged); +} + // The state transition logic here is as follows: // (1) If we're not doing DTLS-SRTP, then the state is just the // state of the underlying impl() @@ -543,7 +568,7 @@ void DtlsTransport::OnReadPacket(rtc::PacketTransportInternal* transport, // the peer has chosen the client role, and proceed with the handshake. // The fingerprint will be verified when it's set. if (!dtls_ && local_certificate_) { - SetSslRole(rtc::SSL_SERVER); + SetDtlsRole(rtc::SSL_SERVER); SetupDtls(); } } else { @@ -677,7 +702,7 @@ void DtlsTransport::MaybeStartDtls() { // Now that the handshake has started, we can process a cached ClientHello // (if one exists). if (cached_client_hello_.size()) { - if (ssl_role_ == rtc::SSL_SERVER) { + if (*dtls_role_ == rtc::SSL_SERVER) { LOG_J(LS_INFO, this) << "Handling cached DTLS ClientHello packet."; if (!HandleDtlsPacket(cached_client_hello_.data(), cached_client_hello_.size())) { diff --git a/p2p/base/dtlstransport.h b/p2p/base/dtlstransport.h index a4f4bcd57b..da0e7d48e2 100644 --- a/p2p/base/dtlstransport.h +++ b/p2p/base/dtlstransport.h @@ -89,8 +89,12 @@ class DtlsTransport : public DtlsTransportInternal { // // |crypto_options| are the options used for the DTLS handshake. This affects // whether GCM crypto suites are negotiated. + // TODO(zhihuang): Remove this once we switch to JsepTransportController. explicit DtlsTransport(IceTransportInternal* ice_transport, const rtc::CryptoOptions& crypto_options); + explicit DtlsTransport(std::unique_ptr ice_transport, + const rtc::CryptoOptions& crypto_options); + ~DtlsTransport() override; const rtc::CryptoOptions& crypto_options() const override; @@ -114,7 +118,7 @@ class DtlsTransport : public DtlsTransportInternal { rtc::scoped_refptr GetLocalCertificate() const override; // SetRemoteFingerprint must be called after SetLocalCertificate, and any - // other methods like SetSslRole. It's what triggers the actual DTLS setup. + // other methods like SetDtlsRole. It's what triggers the actual DTLS setup. // TODO(deadbeef): Rename to "Start" like in ORTC? bool SetRemoteFingerprint(const std::string& digest_alg, const uint8_t* digest, @@ -128,13 +132,13 @@ class DtlsTransport : public DtlsTransportInternal { bool GetOption(rtc::Socket::Option opt, int* value) override; - virtual bool SetSslMaxProtocolVersion(rtc::SSLProtocolVersion version); + bool SetSslMaxProtocolVersion(rtc::SSLProtocolVersion version) override; // Find out which DTLS-SRTP cipher was negotiated bool GetSrtpCryptoSuite(int* cipher) override; - bool GetSslRole(rtc::SSLRole* role) const override; - bool SetSslRole(rtc::SSLRole role) override; + bool GetDtlsRole(rtc::SSLRole* role) const override; + bool SetDtlsRole(rtc::SSLRole role) override; // Find out which DTLS cipher was negotiated bool GetSslCipherSuite(int* cipher) override; @@ -186,6 +190,8 @@ class DtlsTransport : public DtlsTransportInternal { } private: + void ConnectToIceTransport(); + void OnWritableState(rtc::PacketTransportInternal* transport); void OnReadPacket(rtc::PacketTransportInternal* transport, const char* data, @@ -215,13 +221,14 @@ class DtlsTransport : public DtlsTransportInternal { rtc::Thread* network_thread_; // Everything should occur on this thread. // Underlying ice_transport, not owned by this class. IceTransportInternal* const ice_transport_; + std::unique_ptr owned_ice_transport_; std::unique_ptr dtls_; // The DTLS stream StreamInterfaceChannel* downward_; // Wrapper for ice_transport_, owned by dtls_. std::vector srtp_ciphers_; // SRTP ciphers to use with DTLS. bool dtls_active_ = false; rtc::scoped_refptr local_certificate_; - rtc::SSLRole ssl_role_; + rtc::Optional dtls_role_; rtc::SSLProtocolVersion ssl_max_version_; rtc::CryptoOptions crypto_options_; rtc::Buffer remote_fingerprint_value_; diff --git a/p2p/base/dtlstransport_unittest.cc b/p2p/base/dtlstransport_unittest.cc index 430b41dbc2..d77f77c954 100644 --- a/p2p/base/dtlstransport_unittest.cc +++ b/p2p/base/dtlstransport_unittest.cc @@ -353,10 +353,10 @@ class DtlsTransportTestBase { void Negotiate(bool client1_server = true) { client1_.SetupTransports(ICEROLE_CONTROLLING); client2_.SetupTransports(ICEROLE_CONTROLLED); - client1_.dtls_transport()->SetSslRole(client1_server ? rtc::SSL_SERVER - : rtc::SSL_CLIENT); - client2_.dtls_transport()->SetSslRole(client1_server ? rtc::SSL_CLIENT - : rtc::SSL_SERVER); + client1_.dtls_transport()->SetDtlsRole(client1_server ? rtc::SSL_SERVER + : rtc::SSL_CLIENT); + client2_.dtls_transport()->SetDtlsRole(client1_server ? rtc::SSL_CLIENT + : rtc::SSL_SERVER); if (client2_.certificate()) { SetRemoteFingerprintFromCert(client1_.dtls_transport(), client2_.certificate()); @@ -627,8 +627,8 @@ class DtlsEventOrderingTest client1_.SetupTransports(ICEROLE_CONTROLLING, simulated_delay_ms); client2_.SetupTransports(ICEROLE_CONTROLLED, simulated_delay_ms); // Similar to how NegotiateOrdering works. - client1_.dtls_transport()->SetSslRole(rtc::SSL_SERVER); - client2_.dtls_transport()->SetSslRole(rtc::SSL_CLIENT); + client1_.dtls_transport()->SetDtlsRole(rtc::SSL_SERVER); + client2_.dtls_transport()->SetDtlsRole(rtc::SSL_CLIENT); SetRemoteFingerprintFromCert(client2_.dtls_transport(), client1_.certificate()); diff --git a/p2p/base/dtlstransportinternal.h b/p2p/base/dtlstransportinternal.h index 1af32ac35c..e100305808 100644 --- a/p2p/base/dtlstransportinternal.h +++ b/p2p/base/dtlstransportinternal.h @@ -59,9 +59,9 @@ class DtlsTransportInternal : public rtc::PacketTransportInternal { virtual bool IsDtlsActive() const = 0; - virtual bool GetSslRole(rtc::SSLRole* role) const = 0; + virtual bool GetDtlsRole(rtc::SSLRole* role) const = 0; - virtual bool SetSslRole(rtc::SSLRole role) = 0; + virtual bool SetDtlsRole(rtc::SSLRole role) = 0; // Finds out which DTLS-SRTP cipher was negotiated. // TODO(zhihuang): Remove this once all dependencies implement this. @@ -98,6 +98,8 @@ class DtlsTransportInternal : public rtc::PacketTransportInternal { const uint8_t* digest, size_t digest_len) = 0; + virtual bool SetSslMaxProtocolVersion(rtc::SSLProtocolVersion version) = 0; + // Expose the underneath IceTransport. virtual IceTransportInternal* ice_transport() = 0; diff --git a/p2p/base/fakedtlstransport.h b/p2p/base/fakedtlstransport.h index 391486ceec..770f7c90ef 100644 --- a/p2p/base/fakedtlstransport.h +++ b/p2p/base/fakedtlstransport.h @@ -13,11 +13,13 @@ #include #include +#include #include #include "p2p/base/dtlstransportinternal.h" #include "p2p/base/fakeicetransport.h" #include "rtc_base/fakesslidentity.h" +#include "rtc_base/ptr_util.h" namespace cricket { @@ -38,10 +40,8 @@ class FakeDtlsTransport : public DtlsTransportInternal { this, &FakeDtlsTransport::OnNetworkRouteChanged); } - // If this constructor is called, a new fake ICE transport will be created, - // and this FakeDtlsTransport will take the ownership. - explicit FakeDtlsTransport(const std::string& name, int component) - : owned_ice_transport_(new FakeIceTransport(name, component)), + explicit FakeDtlsTransport(std::unique_ptr ice) + : owned_ice_transport_(std::move(ice)), transport_name_(owned_ice_transport_->transport_name()), component_(owned_ice_transport_->component()), dtls_fingerprint_("", nullptr, 0) { @@ -52,6 +52,11 @@ class FakeDtlsTransport : public DtlsTransportInternal { this, &FakeDtlsTransport::OnNetworkRouteChanged); } + // If this constructor is called, a new fake ICE transport will be created, + // and this FakeDtlsTransport will take the ownership. + explicit FakeDtlsTransport(const std::string& name, int component) + : FakeDtlsTransport(rtc::MakeUnique(name, component)) {} + ~FakeDtlsTransport() override { if (dest_ && dest_->dest_ == this) { dest_->dest_ = nullptr; @@ -101,6 +106,10 @@ class FakeDtlsTransport : public DtlsTransportInternal { dest->SetDestination(this, true); } dtls_state_ = DTLS_TRANSPORT_CONNECTED; + // If the |dtls_role_| is unset, set it to SSL_CLIENT by default. + if (!dtls_role_) { + dtls_role_ = std::move(rtc::SSL_CLIENT); + } SignalDtlsState(this, dtls_state_); ice_transport_->SetDestination( static_cast(dest->ice_transport()), asymmetric); @@ -125,12 +134,18 @@ class FakeDtlsTransport : public DtlsTransportInternal { dtls_fingerprint_ = rtc::SSLFingerprint(alg, digest, digest_len); return true; } - bool SetSslRole(rtc::SSLRole role) override { - ssl_role_ = role; + bool SetSslMaxProtocolVersion(rtc::SSLProtocolVersion version) override { return true; } - bool GetSslRole(rtc::SSLRole* role) const override { - *role = ssl_role_; + bool SetDtlsRole(rtc::SSLRole role) override { + dtls_role_ = std::move(role); + return true; + } + bool GetDtlsRole(rtc::SSLRole* role) const override { + if (!dtls_role_) { + return false; + } + *role = *dtls_role_; return true; } const rtc::CryptoOptions& crypto_options() const override { @@ -261,7 +276,7 @@ class FakeDtlsTransport : public DtlsTransportInternal { bool do_dtls_ = false; rtc::SSLProtocolVersion ssl_max_version_ = rtc::SSL_PROTOCOL_DTLS_12; rtc::SSLFingerprint dtls_fingerprint_; - rtc::SSLRole ssl_role_ = rtc::SSL_CLIENT; + rtc::Optional dtls_role_; int crypto_suite_ = rtc::SRTP_AES128_CM_SHA1_80; rtc::CryptoOptions crypto_options_; diff --git a/p2p/base/fakeicetransport.h b/p2p/base/fakeicetransport.h index 427481e0e2..1a569c6f62 100644 --- a/p2p/base/fakeicetransport.h +++ b/p2p/base/fakeicetransport.h @@ -144,7 +144,16 @@ class FakeIceTransport : public IceTransportInternal { void AddRemoteCandidate(const Candidate& candidate) override { remote_candidates_.push_back(candidate); } - void RemoveRemoteCandidate(const Candidate& candidate) override {} + void RemoveRemoteCandidate(const Candidate& candidate) override { + auto it = std::find(remote_candidates_.begin(), remote_candidates_.end(), + candidate); + if (it == remote_candidates_.end()) { + RTC_LOG(LS_INFO) << "Trying to remove a candidate which doesn't exist."; + return; + } + + remote_candidates_.erase(it); + } bool GetStats(ConnectionInfos* candidate_pair_stats_list, CandidateStatsList* candidate_stats_list) override { diff --git a/p2p/base/transportfactoryinterface.h b/p2p/base/transportfactoryinterface.h new file mode 100644 index 0000000000..ce32ee8483 --- /dev/null +++ b/p2p/base/transportfactoryinterface.h @@ -0,0 +1,42 @@ +/* + * Copyright 2018 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. + */ + +#ifndef P2P_BASE_TRANSPORTFACTORYINTERFACE_H_ +#define P2P_BASE_TRANSPORTFACTORYINTERFACE_H_ + +#include +#include + +#include "p2p/base/dtlstransportinternal.h" +#include "p2p/base/icetransportinternal.h" + +namespace cricket { + +// This interface is used to create DTLS/ICE transports. The external transports +// can be injected into the JsepTransportController through it. For example, the +// FakeIceTransport/FakeDtlsTransport can be injected by setting a +// FakeTransportFactory which implements this interface to the +// JsepTransportController. +class TransportFactoryInterface { + public: + virtual ~TransportFactoryInterface() {} + + virtual std::unique_ptr CreateIceTransport( + const std::string& transport_name, + int component) = 0; + + virtual std::unique_ptr CreateDtlsTransport( + std::unique_ptr ice, + const rtc::CryptoOptions& crypto_options) = 0; +}; + +} // namespace cricket + +#endif // P2P_BASE_TRANSPORTFACTORYINTERFACE_H_ diff --git a/pc/BUILD.gn b/pc/BUILD.gn index d536109aa8..c79a2adee1 100644 --- a/pc/BUILD.gn +++ b/pc/BUILD.gn @@ -44,6 +44,10 @@ rtc_static_library("rtc_pc_base") { "externalhmac.h", "jseptransport.cc", "jseptransport.h", + "jseptransport2.cc", + "jseptransport2.h", + "jseptransportcontroller.cc", + "jseptransportcontroller.h", "mediasession.cc", "mediasession.h", "rtcpmuxfilter.cc", @@ -64,6 +68,8 @@ rtc_static_library("rtc_pc_base") { "srtptransport.h", "transportcontroller.cc", "transportcontroller.h", + "transportstats.cc", + "transportstats.h", ] deps = [ @@ -270,7 +276,9 @@ if (rtc_include_tests) { "channelmanager_unittest.cc", "currentspeakermonitor_unittest.cc", "dtlssrtptransport_unittest.cc", + "jseptransport2_unittest.cc", "jseptransport_unittest.cc", + "jseptransportcontroller_unittest.cc", "mediasession_unittest.cc", "rtcpmuxfilter_unittest.cc", "rtptransport_unittest.cc", diff --git a/pc/dtlssrtptransport.cc b/pc/dtlssrtptransport.cc index f6cbf4d288..ff1bc0bfb1 100644 --- a/pc/dtlssrtptransport.cc +++ b/pc/dtlssrtptransport.cc @@ -263,8 +263,8 @@ bool DtlsSrtpTransport::ExtractParams( memcpy(&server_write_key[key_len], &dtls_buffer[offset], salt_len); rtc::SSLRole role; - if (!dtls_transport->GetSslRole(&role)) { - RTC_LOG(LS_WARNING) << "GetSslRole failed"; + if (!dtls_transport->GetDtlsRole(&role)) { + RTC_LOG(LS_WARNING) << "Failed to get the DTLS role."; return false; } diff --git a/pc/jseptransport.cc b/pc/jseptransport.cc index a6a6135b4d..0127d84398 100644 --- a/pc/jseptransport.cc +++ b/pc/jseptransport.cc @@ -25,18 +25,8 @@ using webrtc::SdpType; namespace cricket { -TransportChannelStats::TransportChannelStats() = default; - -TransportChannelStats::TransportChannelStats(const TransportChannelStats&) = - default; - -TransportChannelStats::~TransportChannelStats() = default; - -TransportStats::TransportStats() = default; - -TransportStats::~TransportStats() = default; - -bool BadTransportDescription(const std::string& desc, std::string* err_desc) { +static bool BadTransportDescription(const std::string& desc, + std::string* err_desc) { if (err_desc) { *err_desc = desc; } @@ -289,7 +279,7 @@ bool JsepTransport::ApplyNegotiatedTransportDescription( std::string* error_desc) { // Set SSL role. Role must be set before fingerprint is applied, which // initiates DTLS setup. - if (ssl_role_ && !dtls_transport->SetSslRole(*ssl_role_)) { + if (ssl_role_ && !dtls_transport->SetDtlsRole(*ssl_role_)) { return BadTransportDescription("Failed to set SSL role for the channel.", error_desc); } diff --git a/pc/jseptransport.h b/pc/jseptransport.h index 719ff7a031..4a8aec8590 100644 --- a/pc/jseptransport.h +++ b/pc/jseptransport.h @@ -23,6 +23,7 @@ #include "p2p/base/p2pconstants.h" #include "p2p/base/transportinfo.h" #include "pc/sessiondescription.h" +#include "pc/transportstats.h" #include "rtc_base/constructormagic.h" #include "rtc_base/messagequeue.h" #include "rtc_base/rtccertificate.h" @@ -33,34 +34,6 @@ namespace cricket { class DtlsTransportInternal; -struct TransportChannelStats { - TransportChannelStats(); - TransportChannelStats(const TransportChannelStats&); - ~TransportChannelStats(); - - int component = 0; - CandidateStatsList candidate_stats_list; - ConnectionInfos connection_infos; - int srtp_crypto_suite = rtc::SRTP_INVALID_CRYPTO_SUITE; - int ssl_cipher_suite = rtc::TLS_NULL_WITH_NULL_NULL; - DtlsTransportState dtls_state = DTLS_TRANSPORT_NEW; -}; - -// Information about all the channels of a transport. -// TODO(hta): Consider if a simple vector is as good as a map. -typedef std::vector TransportChannelStatsList; - -// Information about the stats of a transport. -struct TransportStats { - TransportStats(); - ~TransportStats(); - - std::string transport_name; - TransportChannelStatsList channel_stats; -}; - -bool BadTransportDescription(const std::string& desc, std::string* err_desc); - // Helper class used by TransportController that processes // TransportDescriptions. A TransportDescription represents the // transport-specific properties of an SDP m= section, processed according to diff --git a/pc/jseptransport2.cc b/pc/jseptransport2.cc new file mode 100644 index 0000000000..11f943848b --- /dev/null +++ b/pc/jseptransport2.cc @@ -0,0 +1,617 @@ +/* + * Copyright 2018 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 "pc/jseptransport2.h" + +#include +#include // for std::pair + +#include "api/candidate.h" +#include "p2p/base/p2pconstants.h" +#include "p2p/base/p2ptransportchannel.h" +#include "p2p/base/port.h" +#include "rtc_base/bind.h" +#include "rtc_base/checks.h" +#include "rtc_base/logging.h" +#include "rtc_base/ptr_util.h" + +using webrtc::SdpType; + +namespace cricket { + +static bool VerifyIceParams(const JsepTransportDescription& jsep_description) { + // For legacy protocols. + // TODO(zhihuang): Remove this once the legacy protocol is no longer + // supported. + if (jsep_description.transport_desc.ice_ufrag.empty() && + jsep_description.transport_desc.ice_pwd.empty()) { + return true; + } + + if (jsep_description.transport_desc.ice_ufrag.length() < + ICE_UFRAG_MIN_LENGTH || + jsep_description.transport_desc.ice_ufrag.length() > + ICE_UFRAG_MAX_LENGTH) { + return false; + } + if (jsep_description.transport_desc.ice_pwd.length() < ICE_PWD_MIN_LENGTH || + jsep_description.transport_desc.ice_pwd.length() > ICE_PWD_MAX_LENGTH) { + return false; + } + return true; +} + +JsepTransportDescription::JsepTransportDescription() {} + +JsepTransportDescription::JsepTransportDescription( + bool rtcp_mux_enabled, + const std::vector& cryptos, + const std::vector& encrypted_header_extension_ids, + const TransportDescription& transport_desc) + : rtcp_mux_enabled(rtcp_mux_enabled), + cryptos(cryptos), + encrypted_header_extension_ids(encrypted_header_extension_ids), + transport_desc(transport_desc) {} + +JsepTransportDescription::JsepTransportDescription( + const JsepTransportDescription& from) + : rtcp_mux_enabled(from.rtcp_mux_enabled), + cryptos(from.cryptos), + encrypted_header_extension_ids(from.encrypted_header_extension_ids), + transport_desc(from.transport_desc) {} + +JsepTransportDescription::~JsepTransportDescription() = default; + +JsepTransportDescription& JsepTransportDescription::operator=( + const JsepTransportDescription& from) { + if (this == &from) { + return *this; + } + rtcp_mux_enabled = from.rtcp_mux_enabled; + cryptos = from.cryptos; + encrypted_header_extension_ids = from.encrypted_header_extension_ids; + transport_desc = from.transport_desc; + + return *this; +} + +JsepTransport2::JsepTransport2( + const std::string& mid, + const rtc::scoped_refptr& local_certificate, + std::unique_ptr unencrypted_rtp_transport, + std::unique_ptr sdes_transport, + std::unique_ptr dtls_srtp_transport, + std::unique_ptr rtp_dtls_transport, + std::unique_ptr rtcp_dtls_transport) + : mid_(mid), + local_certificate_(local_certificate), + rtp_dtls_transport_(std::move(rtp_dtls_transport)), + rtcp_dtls_transport_(std::move(rtcp_dtls_transport)) { + RTC_DCHECK(rtp_dtls_transport_); + if (unencrypted_rtp_transport) { + RTC_DCHECK(!sdes_transport); + RTC_DCHECK(!dtls_srtp_transport); + unencrypted_rtp_transport_ = std::move(unencrypted_rtp_transport); + } else if (sdes_transport) { + RTC_DCHECK(!unencrypted_rtp_transport); + RTC_DCHECK(!dtls_srtp_transport); + sdes_transport_ = std::move(sdes_transport); + } else { + RTC_DCHECK(dtls_srtp_transport); + RTC_DCHECK(!unencrypted_rtp_transport); + RTC_DCHECK(!sdes_transport); + dtls_srtp_transport_ = std::move(dtls_srtp_transport); + } +} + +JsepTransport2::~JsepTransport2() {} + +webrtc::RTCError JsepTransport2::SetLocalJsepTransportDescription( + const JsepTransportDescription& jsep_description, + SdpType type) { + webrtc::RTCError error; + + if (!VerifyIceParams(jsep_description)) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, + "Invalid ice-ufrag or ice-pwd length."); + } + + if (!SetRtcpMux(jsep_description.rtcp_mux_enabled, type, + ContentSource::CS_LOCAL)) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, + "Failed to setup RTCP mux."); + } + + // If doing SDES, setup the SDES crypto parameters. + if (sdes_transport_) { + RTC_DCHECK(!unencrypted_rtp_transport_); + RTC_DCHECK(!dtls_srtp_transport_); + if (!SetSdes(jsep_description.cryptos, + jsep_description.encrypted_header_extension_ids, type, + ContentSource::CS_LOCAL)) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, + "Failed to setup SDES crypto parameters."); + } + } else if (dtls_srtp_transport_) { + RTC_DCHECK(!unencrypted_rtp_transport_); + RTC_DCHECK(!sdes_transport_); + dtls_srtp_transport_->UpdateRecvEncryptedHeaderExtensionIds( + jsep_description.encrypted_header_extension_ids); + } + + bool ice_restarting = + local_description_ != nullptr && + IceCredentialsChanged(local_description_->transport_desc.ice_ufrag, + local_description_->transport_desc.ice_pwd, + jsep_description.transport_desc.ice_ufrag, + jsep_description.transport_desc.ice_pwd); + local_description_.reset(new JsepTransportDescription(jsep_description)); + + rtc::SSLFingerprint* local_fp = + local_description_->transport_desc.identity_fingerprint.get(); + + if (!local_fp) { + local_certificate_ = nullptr; + } else { + error = VerifyCertificateFingerprint(local_certificate_.get(), local_fp); + if (!error.ok()) { + local_description_.reset(); + return error; + } + } + + SetLocalIceParameters(rtp_dtls_transport_->ice_transport()); + + if (rtcp_dtls_transport_) { + SetLocalIceParameters(rtcp_dtls_transport_->ice_transport()); + } + + // If PRANSWER/ANSWER is set, we should decide transport protocol type. + if (type == SdpType::kPrAnswer || type == SdpType::kAnswer) { + error = NegotiateAndSetDtlsParameters(type); + } + if (!error.ok()) { + local_description_.reset(); + return error; + } + + if (needs_ice_restart_ && ice_restarting) { + needs_ice_restart_ = false; + RTC_LOG(LS_VERBOSE) << "needs-ice-restart flag cleared for transport " + << mid(); + } + + return webrtc::RTCError::OK(); +} + +webrtc::RTCError JsepTransport2::SetRemoteJsepTransportDescription( + const JsepTransportDescription& jsep_description, + webrtc::SdpType type) { + webrtc::RTCError error; + + if (!VerifyIceParams(jsep_description)) { + remote_description_.reset(); + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, + "Invalid ice-ufrag or ice-pwd length."); + } + + if (!SetRtcpMux(jsep_description.rtcp_mux_enabled, type, + ContentSource::CS_REMOTE)) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, + "Failed to setup RTCP mux."); + } + + // If doing SDES, setup the SDES crypto parameters. + if (sdes_transport_) { + RTC_DCHECK(!unencrypted_rtp_transport_); + RTC_DCHECK(!dtls_srtp_transport_); + if (!SetSdes(jsep_description.cryptos, + jsep_description.encrypted_header_extension_ids, type, + ContentSource::CS_REMOTE)) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, + "Failed to setup SDES crypto parameters."); + } + } else if (dtls_srtp_transport_) { + RTC_DCHECK(!unencrypted_rtp_transport_); + RTC_DCHECK(!sdes_transport_); + dtls_srtp_transport_->UpdateSendEncryptedHeaderExtensionIds( + jsep_description.encrypted_header_extension_ids); + } + + remote_description_.reset(new JsepTransportDescription(jsep_description)); + SetRemoteIceParameters(rtp_dtls_transport_->ice_transport()); + + if (rtcp_dtls_transport_) { + SetRemoteIceParameters(rtcp_dtls_transport_->ice_transport()); + } + + // If PRANSWER/ANSWER is set, we should decide transport protocol type. + if (type == SdpType::kPrAnswer || type == SdpType::kAnswer) { + error = NegotiateAndSetDtlsParameters(SdpType::kOffer); + } + if (!error.ok()) { + remote_description_.reset(); + return error; + } + return webrtc::RTCError::OK(); +} + +webrtc::RTCError JsepTransport2::AddRemoteCandidates( + const Candidates& candidates) { + if (!local_description_ || !remote_description_) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_STATE, + mid() + + " is not ready to use the remote candidate " + "because the local or remote description is " + "not set."); + } + + for (const cricket::Candidate& candidate : candidates) { + auto transport = + candidate.component() == cricket::ICE_CANDIDATE_COMPONENT_RTP + ? rtp_dtls_transport_.get() + : rtcp_dtls_transport_.get(); + if (!transport) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, + "Candidate has an unknown component: " + + candidate.ToString() + " for mid " + mid()); + } + transport->ice_transport()->AddRemoteCandidate(candidate); + } + return webrtc::RTCError::OK(); +} + +void JsepTransport2::SetNeedsIceRestartFlag() { + if (!needs_ice_restart_) { + needs_ice_restart_ = true; + RTC_LOG(LS_VERBOSE) << "needs-ice-restart flag set for transport " << mid(); + } +} + +rtc::Optional JsepTransport2::GetDtlsRole() const { + RTC_DCHECK(rtp_dtls_transport_); + rtc::SSLRole dtls_role; + if (!rtp_dtls_transport_->GetDtlsRole(&dtls_role)) { + return rtc::Optional(); + } + + return rtc::Optional(dtls_role); +} + +bool JsepTransport2::GetStats(TransportStats* stats) { + stats->transport_name = mid(); + stats->channel_stats.clear(); + bool ret = GetTransportStats(rtp_dtls_transport_.get(), stats); + if (rtcp_dtls_transport_) { + ret &= GetTransportStats(rtcp_dtls_transport_.get(), stats); + } + return ret; +} + +webrtc::RTCError JsepTransport2::VerifyCertificateFingerprint( + const rtc::RTCCertificate* certificate, + const rtc::SSLFingerprint* fingerprint) const { + if (!fingerprint) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, + "No fingerprint"); + } + if (!certificate) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, + "Fingerprint provided but no identity available."); + } + std::unique_ptr fp_tmp(rtc::SSLFingerprint::Create( + fingerprint->algorithm, certificate->identity())); + RTC_DCHECK(fp_tmp.get() != NULL); + if (*fp_tmp == *fingerprint) { + return webrtc::RTCError::OK(); + } + std::ostringstream desc; + desc << "Local fingerprint does not match identity. Expected: "; + desc << fp_tmp->ToString(); + desc << " Got: " << fingerprint->ToString(); + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, desc.str()); +} + +void JsepTransport2::SetLocalIceParameters( + IceTransportInternal* ice_transport) { + RTC_DCHECK(ice_transport); + RTC_DCHECK(local_description_); + ice_transport->SetIceParameters( + local_description_->transport_desc.GetIceParameters()); +} + +void JsepTransport2::SetRemoteIceParameters( + IceTransportInternal* ice_transport) { + RTC_DCHECK(ice_transport); + RTC_DCHECK(remote_description_); + ice_transport->SetRemoteIceParameters( + remote_description_->transport_desc.GetIceParameters()); + ice_transport->SetRemoteIceMode(remote_description_->transport_desc.ice_mode); +} + +webrtc::RTCError JsepTransport2::SetNegotiatedDtlsParameters( + DtlsTransportInternal* dtls_transport, + rtc::Optional dtls_role, + rtc::SSLFingerprint* remote_fingerprint) { + RTC_DCHECK(dtls_transport); + // Set SSL role. Role must be set before fingerprint is applied, which + // initiates DTLS setup. + if (dtls_role && !dtls_transport->SetDtlsRole(*dtls_role)) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, + "Failed to set SSL role for the transport."); + } + // Apply remote fingerprint. + if (!remote_fingerprint || + !dtls_transport->SetRemoteFingerprint( + remote_fingerprint->algorithm, + reinterpret_cast(remote_fingerprint->digest.data()), + remote_fingerprint->digest.size())) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, + "Failed to apply remote fingerprint."); + } + return webrtc::RTCError::OK(); +} + +bool JsepTransport2::SetRtcpMux(bool enable, + webrtc::SdpType type, + ContentSource source) { + bool ret = false; + switch (type) { + case SdpType::kOffer: + ret = rtcp_mux_negotiator_.SetOffer(enable, source); + break; + case SdpType::kPrAnswer: + // This may activate RTCP muxing, but we don't yet destroy the transport + // because the final answer may deactivate it. + ret = rtcp_mux_negotiator_.SetProvisionalAnswer(enable, source); + break; + case SdpType::kAnswer: + ret = rtcp_mux_negotiator_.SetAnswer(enable, source); + if (ret && rtcp_mux_negotiator_.IsActive()) { + ActivateRtcpMux(); + } + break; + default: + RTC_NOTREACHED(); + } + + if (!ret) { + return false; + } + + auto transport = rtp_transport(); + transport->SetRtcpMuxEnabled(rtcp_mux_negotiator_.IsActive()); + return ret; +} + +void JsepTransport2::ActivateRtcpMux() { + if (unencrypted_rtp_transport_) { + RTC_DCHECK(!sdes_transport_); + RTC_DCHECK(!dtls_srtp_transport_); + unencrypted_rtp_transport_->SetRtcpPacketTransport(nullptr); + } else if (sdes_transport_) { + RTC_DCHECK(!unencrypted_rtp_transport_); + RTC_DCHECK(!dtls_srtp_transport_); + sdes_transport_->SetRtcpPacketTransport(nullptr); + } else { + RTC_DCHECK(dtls_srtp_transport_); + RTC_DCHECK(!unencrypted_rtp_transport_); + RTC_DCHECK(!sdes_transport_); + dtls_srtp_transport_->SetDtlsTransports(rtp_dtls_transport(), + /*rtcp_dtls_transport=*/nullptr); + } + rtcp_dtls_transport_.reset(); + // Notify the JsepTransportController to update the aggregate states. + SignalRtcpMuxActive(); +} + +bool JsepTransport2::SetSdes(const std::vector& cryptos, + const std::vector& encrypted_extension_ids, + webrtc::SdpType type, + ContentSource source) { + bool ret = false; + ret = sdes_negotiator_.Process(cryptos, type, source); + if (!ret) { + return ret; + } + + if (source == ContentSource::CS_LOCAL) { + recv_extension_ids_ = std::move(encrypted_extension_ids); + } else { + send_extension_ids_ = std::move(encrypted_extension_ids); + } + + // If setting an SDES answer succeeded, apply the negotiated parameters + // to the SRTP transport. + if ((type == SdpType::kPrAnswer || type == SdpType::kAnswer) && ret) { + if (sdes_negotiator_.send_cipher_suite() && + sdes_negotiator_.recv_cipher_suite()) { + RTC_DCHECK(send_extension_ids_); + RTC_DCHECK(recv_extension_ids_); + ret = sdes_transport_->SetRtpParams( + *(sdes_negotiator_.send_cipher_suite()), + sdes_negotiator_.send_key().data(), + static_cast(sdes_negotiator_.send_key().size()), + *(send_extension_ids_), *(sdes_negotiator_.recv_cipher_suite()), + sdes_negotiator_.recv_key().data(), + static_cast(sdes_negotiator_.recv_key().size()), + *(recv_extension_ids_)); + } else { + RTC_LOG(LS_INFO) << "No crypto keys are provided for SDES."; + if (type == SdpType::kAnswer) { + // Explicitly reset the |sdes_transport_| if no crypto param is + // provided in the answer. No need to call |ResetParams()| for + // |sdes_negotiator_| because it resets the params inside |SetAnswer|. + sdes_transport_->ResetParams(); + } + } + } + return ret; +} + +webrtc::RTCError JsepTransport2::NegotiateAndSetDtlsParameters( + SdpType local_description_type) { + if (!local_description_ || !remote_description_) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_STATE, + "Applying an answer transport description " + "without applying any offer."); + } + std::unique_ptr remote_fingerprint; + rtc::Optional negotiated_dtls_role; + + rtc::SSLFingerprint* local_fp = + local_description_->transport_desc.identity_fingerprint.get(); + rtc::SSLFingerprint* remote_fp = + remote_description_->transport_desc.identity_fingerprint.get(); + if (remote_fp && local_fp) { + remote_fingerprint = rtc::MakeUnique(*remote_fp); + webrtc::RTCError error = + NegotiateDtlsRole(local_description_type, + local_description_->transport_desc.connection_role, + remote_description_->transport_desc.connection_role, + &negotiated_dtls_role); + if (!error.ok()) { + return error; + } + } else if (local_fp && (local_description_type == SdpType::kAnswer)) { + return webrtc::RTCError( + webrtc::RTCErrorType::INVALID_PARAMETER, + "Local fingerprint supplied when caller didn't offer DTLS."); + } else { + // We are not doing DTLS + remote_fingerprint = rtc::MakeUnique("", nullptr, 0); + } + // Now that we have negotiated everything, push it downward. + // Note that we cache the result so that if we have race conditions + // between future SetRemote/SetLocal invocations and new transport + // creation, we have the negotiation state saved until a new + // negotiation happens. + webrtc::RTCError error = SetNegotiatedDtlsParameters( + rtp_dtls_transport_.get(), negotiated_dtls_role, + remote_fingerprint.get()); + if (!error.ok()) { + return error; + } + + if (rtcp_dtls_transport_) { + error = SetNegotiatedDtlsParameters(rtcp_dtls_transport_.get(), + negotiated_dtls_role, + remote_fingerprint.get()); + } + return error; +} + +webrtc::RTCError JsepTransport2::NegotiateDtlsRole( + SdpType local_description_type, + ConnectionRole local_connection_role, + ConnectionRole remote_connection_role, + rtc::Optional* negotiated_dtls_role) { + // From RFC 4145, section-4.1, The following are the values that the + // 'setup' attribute can take in an offer/answer exchange: + // Offer Answer + // ________________ + // active passive / holdconn + // passive active / holdconn + // actpass active / passive / holdconn + // holdconn holdconn + // + // Set the role that is most conformant with RFC 5763, Section 5, bullet 1 + // The endpoint MUST use the setup attribute defined in [RFC4145]. + // The endpoint that is the offerer MUST use the setup attribute + // value of setup:actpass and be prepared to receive a client_hello + // before it receives the answer. The answerer MUST use either a + // setup attribute value of setup:active or setup:passive. Note that + // if the answerer uses setup:passive, then the DTLS handshake will + // not begin until the answerer is received, which adds additional + // latency. setup:active allows the answer and the DTLS handshake to + // occur in parallel. Thus, setup:active is RECOMMENDED. Whichever + // party is active MUST initiate a DTLS handshake by sending a + // ClientHello over each flow (host/port quartet). + // IOW - actpass and passive modes should be treated as server and + // active as client. + bool is_remote_server = false; + if (local_description_type == SdpType::kOffer) { + if (local_connection_role != CONNECTIONROLE_ACTPASS) { + return webrtc::RTCError( + webrtc::RTCErrorType::INVALID_PARAMETER, + "Offerer must use actpass value for setup attribute."); + } + + if (remote_connection_role == CONNECTIONROLE_ACTIVE || + remote_connection_role == CONNECTIONROLE_PASSIVE || + remote_connection_role == CONNECTIONROLE_NONE) { + is_remote_server = (remote_connection_role == CONNECTIONROLE_PASSIVE); + } else { + return webrtc::RTCError( + webrtc::RTCErrorType::INVALID_PARAMETER, + "Answerer must use either active or passive value " + "for setup attribute."); + } + // If remote is NONE or ACTIVE it will act as client. + } else { + if (remote_connection_role != CONNECTIONROLE_ACTPASS && + remote_connection_role != CONNECTIONROLE_NONE) { + // Accept a remote role attribute that's not "actpass", but matches the + // current negotiated role. This is allowed by dtls-sdp, though our + // implementation will never generate such an offer as it's not + // recommended. + // + // See https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-dtls-sdp, + // section 5.5. + auto current_dtls_role = GetDtlsRole(); + if (!current_dtls_role || + (*current_dtls_role == rtc::SSL_CLIENT && + remote_connection_role == CONNECTIONROLE_ACTIVE) || + (*current_dtls_role == rtc::SSL_SERVER && + remote_connection_role == CONNECTIONROLE_PASSIVE)) { + return webrtc::RTCError( + webrtc::RTCErrorType::INVALID_PARAMETER, + "Offerer must use actpass value or current negotiated role for " + "setup attribute."); + } + } + + if (local_connection_role == CONNECTIONROLE_ACTIVE || + local_connection_role == CONNECTIONROLE_PASSIVE) { + is_remote_server = (local_connection_role == CONNECTIONROLE_ACTIVE); + } else { + return webrtc::RTCError( + webrtc::RTCErrorType::INVALID_PARAMETER, + "Answerer must use either active or passive value " + "for setup attribute."); + } + + // If local is passive, local will act as server. + } + + *negotiated_dtls_role = (is_remote_server ? std::move(rtc::SSL_CLIENT) + : std::move(rtc::SSL_SERVER)); + return webrtc::RTCError::OK(); +} + +bool JsepTransport2::GetTransportStats(DtlsTransportInternal* dtls_transport, + TransportStats* stats) { + RTC_DCHECK(dtls_transport); + TransportChannelStats substats; + substats.component = dtls_transport == rtcp_dtls_transport_.get() + ? ICE_CANDIDATE_COMPONENT_RTCP + : ICE_CANDIDATE_COMPONENT_RTP; + dtls_transport->GetSrtpCryptoSuite(&substats.srtp_crypto_suite); + dtls_transport->GetSslCipherSuite(&substats.ssl_cipher_suite); + substats.dtls_state = dtls_transport->dtls_state(); + if (!dtls_transport->ice_transport()->GetStats( + &substats.connection_infos, &substats.candidate_stats_list)) { + return false; + } + stats->channel_stats.push_back(substats); + return true; +} + +} // namespace cricket diff --git a/pc/jseptransport2.h b/pc/jseptransport2.h new file mode 100644 index 0000000000..16a20b9ab0 --- /dev/null +++ b/pc/jseptransport2.h @@ -0,0 +1,245 @@ +/* + * Copyright 2018 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. + */ + +#ifndef PC_JSEPTRANSPORT2_H_ +#define PC_JSEPTRANSPORT2_H_ + +#include +#include +#include +#include + +#include "api/candidate.h" +#include "api/jsep.h" +#include "api/optional.h" +#include "p2p/base/dtlstransport.h" +#include "p2p/base/p2pconstants.h" +#include "p2p/base/transportinfo.h" +#include "pc/dtlssrtptransport.h" +#include "pc/rtcpmuxfilter.h" +#include "pc/rtptransport.h" +#include "pc/sessiondescription.h" +#include "pc/srtpfilter.h" +#include "pc/srtptransport.h" +#include "pc/transportstats.h" +#include "rtc_base/constructormagic.h" +#include "rtc_base/messagequeue.h" +#include "rtc_base/rtccertificate.h" +#include "rtc_base/sigslot.h" +#include "rtc_base/sslstreamadapter.h" + +namespace cricket { + +class DtlsTransportInternal; + +struct JsepTransportDescription { + public: + JsepTransportDescription(); + JsepTransportDescription( + bool rtcp_mux_enabled, + const std::vector& cryptos, + const std::vector& encrypted_header_extension_ids, + const TransportDescription& transport_description); + JsepTransportDescription(const JsepTransportDescription& from); + ~JsepTransportDescription(); + + JsepTransportDescription& operator=(const JsepTransportDescription& from); + + bool rtcp_mux_enabled = true; + std::vector cryptos; + std::vector encrypted_header_extension_ids; + // TODO(zhihuang): Add the ICE and DTLS related variables and methods from + // TransportDescription and remove this extra layer of abstraction. + TransportDescription transport_desc; +}; + +// Helper class used by JsepTransportController that processes +// TransportDescriptions. A TransportDescription represents the +// transport-specific properties of an SDP m= section, processed according to +// JSEP. Each transport consists of DTLS and ICE transport channels for RTP +// (and possibly RTCP, if rtcp-mux isn't used). +// +// On Threading: JsepTransport performs work solely on the network thread, and +// so its methods should only be called on the network thread. +class JsepTransport2 : public sigslot::has_slots<> { + public: + // |mid| is just used for log statements in order to identify the Transport. + // Note that |local_certificate| is allowed to be null since a remote + // description may be set before a local certificate is generated. + JsepTransport2( + const std::string& mid, + const rtc::scoped_refptr& local_certificate, + std::unique_ptr unencrypted_rtp_transport, + std::unique_ptr sdes_transport, + std::unique_ptr dtls_srtp_transport, + std::unique_ptr rtp_dtls_transport, + std::unique_ptr rtcp_dtls_transport); + + ~JsepTransport2() override; + + // Returns the MID of this transport. This is only used for logging. + const std::string& mid() const { return mid_; } + + // Must be called before applying local session description. + // Needed in order to verify the local fingerprint. + void SetLocalCertificate( + const rtc::scoped_refptr& local_certificate) { + local_certificate_ = local_certificate; + } + + // Return the local certificate provided by SetLocalCertificate. + rtc::scoped_refptr GetLocalCertificate() const { + return local_certificate_; + } + + webrtc::RTCError SetLocalJsepTransportDescription( + const JsepTransportDescription& jsep_description, + webrtc::SdpType type); + + // Set the remote TransportDescription to be used by DTLS and ICE channels + // that are part of this Transport. + webrtc::RTCError SetRemoteJsepTransportDescription( + const JsepTransportDescription& jsep_description, + webrtc::SdpType type); + + webrtc::RTCError AddRemoteCandidates(const Candidates& candidates); + + // Set the "needs-ice-restart" flag as described in JSEP. After the flag is + // set, offers should generate new ufrags/passwords until an ICE restart + // occurs. + // + // This and the below method can be called safely from any thread as long as + // SetXTransportDescription is not in progress. + void SetNeedsIceRestartFlag(); + // Returns true if the ICE restart flag above was set, and no ICE restart has + // occurred yet for this transport (by applying a local description with + // changed ufrag/password). + bool needs_ice_restart() const { return needs_ice_restart_; } + + // Returns role if negotiated, or empty Optional if it hasn't been negotiated + // yet. + rtc::Optional GetDtlsRole() const; + + // TODO(deadbeef): Make this const. See comment in transportcontroller.h. + bool GetStats(TransportStats* stats); + + const JsepTransportDescription* local_description() const { + return local_description_.get(); + } + + const JsepTransportDescription* remote_description() const { + return remote_description_.get(); + } + + webrtc::RtpTransportInternal* rtp_transport() const { + if (dtls_srtp_transport_) { + return dtls_srtp_transport_.get(); + } else if (sdes_transport_) { + return sdes_transport_.get(); + } else { + return unencrypted_rtp_transport_.get(); + } + } + + DtlsTransportInternal* rtp_dtls_transport() const { + return rtp_dtls_transport_.get(); + } + + DtlsTransportInternal* rtcp_dtls_transport() const { + return rtcp_dtls_transport_.get(); + } + + // This is signaled when RTCP-mux becomes active and + // |rtcp_dtls_transport_| is destroyed. The JsepTransportController will + // handle the signal and update the aggregate transport states. + sigslot::signal<> SignalRtcpMuxActive; + + // TODO(deadbeef): The methods below are only public for testing. Should make + // them utility functions or objects so they can be tested independently from + // this class. + + // Returns an error if the certificate's identity does not match the + // fingerprint, or either is NULL. + webrtc::RTCError VerifyCertificateFingerprint( + const rtc::RTCCertificate* certificate, + const rtc::SSLFingerprint* fingerprint) const; + + private: + bool SetRtcpMux(bool enable, webrtc::SdpType type, ContentSource source); + + void ActivateRtcpMux(); + + bool SetSdes(const std::vector& cryptos, + const std::vector& encrypted_extension_ids, + webrtc::SdpType type, + ContentSource source); + + // Negotiates and sets the DTLS parameters based on the current local and + // remote transport description, such as the DTLS role to use, and whether + // DTLS should be activated. + // + // Called when an answer TransportDescription is applied. + webrtc::RTCError NegotiateAndSetDtlsParameters( + webrtc::SdpType local_description_type); + + // Negotiates the DTLS role based off the offer and answer as specified by + // RFC 4145, section-4.1. Returns an RTCError if role cannot be determined + // from the local description and remote description. + webrtc::RTCError NegotiateDtlsRole( + webrtc::SdpType local_description_type, + ConnectionRole local_connection_role, + ConnectionRole remote_connection_role, + rtc::Optional* negotiated_dtls_role); + + // Pushes down the ICE parameters from the local description, such + // as the ICE ufrag and pwd. + void SetLocalIceParameters(IceTransportInternal* ice); + + // Pushes down the ICE parameters from the remote description. + void SetRemoteIceParameters(IceTransportInternal* ice); + + // Pushes down the DTLS parameters obtained via negotiation. + webrtc::RTCError SetNegotiatedDtlsParameters( + DtlsTransportInternal* dtls_transport, + rtc::Optional dtls_role, + rtc::SSLFingerprint* remote_fingerprint); + + bool GetTransportStats(DtlsTransportInternal* dtls_transport, + TransportStats* stats); + + const std::string mid_; + // needs-ice-restart bit as described in JSEP. + bool needs_ice_restart_ = false; + rtc::scoped_refptr local_certificate_; + std::unique_ptr local_description_; + std::unique_ptr remote_description_; + + // To avoid downcasting and make it type safe, keep three unique pointers for + // different SRTP mode and only one of these is non-nullptr. + std::unique_ptr unencrypted_rtp_transport_; + std::unique_ptr sdes_transport_; + std::unique_ptr dtls_srtp_transport_; + + std::unique_ptr rtp_dtls_transport_; + std::unique_ptr rtcp_dtls_transport_; + + SrtpFilter sdes_negotiator_; + RtcpMuxFilter rtcp_mux_negotiator_; + + // Cache the encrypted header extension IDs for SDES negoitation. + rtc::Optional> send_extension_ids_; + rtc::Optional> recv_extension_ids_; + + RTC_DISALLOW_COPY_AND_ASSIGN(JsepTransport2); +}; + +} // namespace cricket + +#endif // PC_JSEPTRANSPORT2_H_ diff --git a/pc/jseptransport2_unittest.cc b/pc/jseptransport2_unittest.cc new file mode 100644 index 0000000000..fc098ae9b6 --- /dev/null +++ b/pc/jseptransport2_unittest.cc @@ -0,0 +1,1000 @@ +/* + * Copyright 2018 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 +#include + +#include "p2p/base/fakedtlstransport.h" +#include "p2p/base/fakeicetransport.h" +#include "pc/jseptransport2.h" +#include "rtc_base/gunit.h" + +namespace cricket { +using webrtc::SdpType; + +static const char kIceUfrag1[] = "U001"; +static const char kIcePwd1[] = "TESTICEPWD00000000000001"; +static const char kIceUfrag2[] = "U002"; +static const char kIcePwd2[] = "TESTIEPWD00000000000002"; +static const char kTransportName[] = "Test Transport"; + +enum class SrtpMode { + kSdes, + kDtlsSrtp, +}; + +struct NegotiateRoleParams { + ConnectionRole local_role; + ConnectionRole remote_role; + SdpType local_type; + SdpType remote_type; +}; + +class JsepTransport2Test : public testing::Test, public sigslot::has_slots<> { + protected: + std::unique_ptr CreateSdesTransport( + const std::string& transport_name, + rtc::PacketTransportInternal* rtp_packet_transport, + rtc::PacketTransportInternal* rtcp_packet_transport) { + bool rtcp_mux_enabled = (rtcp_packet_transport == nullptr); + auto srtp_transport = + rtc::MakeUnique(rtcp_mux_enabled); + + srtp_transport->SetRtpPacketTransport(rtp_packet_transport); + if (rtcp_packet_transport) { + srtp_transport->SetRtcpPacketTransport(rtp_packet_transport); + } + return srtp_transport; + } + + std::unique_ptr CreateDtlsSrtpTransport( + const std::string& transport_name, + cricket::DtlsTransportInternal* rtp_dtls_transport, + cricket::DtlsTransportInternal* rtcp_dtls_transport) { + bool rtcp_mux_enabled = (rtcp_dtls_transport == nullptr); + auto srtp_transport = + rtc::MakeUnique(rtcp_mux_enabled); + auto dtls_srtp_transport = + rtc::MakeUnique(std::move(srtp_transport)); + + dtls_srtp_transport->SetDtlsTransports(rtp_dtls_transport, + rtcp_dtls_transport); + return dtls_srtp_transport; + } + + // Create a new JsepTransport2 with a FakeDtlsTransport and a + // FakeIceTransport. + void CreateJsepTransport2(bool rtcp_mux_enabled, SrtpMode srtp_mode) { + auto ice = rtc::MakeUnique(kTransportName, + ICE_CANDIDATE_COMPONENT_RTP); + auto rtp_dtls_transport = + rtc::MakeUnique(std::move(ice)); + + std::unique_ptr rtcp_dtls_transport; + if (!rtcp_mux_enabled) { + ice = rtc::MakeUnique(kTransportName, + ICE_CANDIDATE_COMPONENT_RTCP); + rtcp_dtls_transport = rtc::MakeUnique(std::move(ice)); + } + + std::unique_ptr unencrypted_rtp_transport; + std::unique_ptr sdes_transport; + std::unique_ptr dtls_srtp_transport; + switch (srtp_mode) { + case SrtpMode::kSdes: + sdes_transport = + CreateSdesTransport(kTransportName, rtp_dtls_transport.get(), + rtcp_dtls_transport.get()); + sdes_transport_ = sdes_transport.get(); + break; + case SrtpMode::kDtlsSrtp: + dtls_srtp_transport = + CreateDtlsSrtpTransport(kTransportName, rtp_dtls_transport.get(), + rtcp_dtls_transport.get()); + break; + default: + RTC_NOTREACHED(); + } + + jsep_transport_ = rtc::MakeUnique( + kTransportName, /*local_certificate=*/nullptr, + std::move(unencrypted_rtp_transport), std::move(sdes_transport), + std::move(dtls_srtp_transport), std::move(rtp_dtls_transport), + std::move(rtcp_dtls_transport)); + + signal_rtcp_mux_active_received_ = false; + jsep_transport_->SignalRtcpMuxActive.connect( + this, &JsepTransport2Test::OnRtcpMuxActive); + } + + JsepTransportDescription MakeJsepTransportDescription( + bool rtcp_mux_enabled, + const char* ufrag, + const char* pwd, + const rtc::scoped_refptr& cert, + ConnectionRole role = CONNECTIONROLE_NONE) { + JsepTransportDescription jsep_description; + jsep_description.rtcp_mux_enabled = rtcp_mux_enabled; + + std::unique_ptr fingerprint; + if (cert) { + fingerprint.reset(rtc::SSLFingerprint::CreateFromCertificate(cert)); + } + jsep_description.transport_desc = + TransportDescription(std::vector(), ufrag, pwd, + ICEMODE_FULL, role, fingerprint.get()); + return jsep_description; + } + + Candidate CreateCandidate(int component) { + Candidate c; + c.set_address(rtc::SocketAddress("192.168.1.1", 8000)); + c.set_component(component); + c.set_protocol(UDP_PROTOCOL_NAME); + c.set_priority(1); + return c; + } + + void OnRtcpMuxActive() { signal_rtcp_mux_active_received_ = true; } + + std::unique_ptr jsep_transport_; + bool signal_rtcp_mux_active_received_ = false; + // The SrtpTransport is owned by |jsep_transport_|. Keep a raw pointer here + // for testing. + webrtc::SrtpTransport* sdes_transport_ = nullptr; +}; + +// The parameterized tests cover both cases when RTCP mux is enable and +// disabled. +class JsepTransport2WithRtcpMux : public JsepTransport2Test, + public testing::WithParamInterface {}; + +// This test verifies the ICE parameters are properly applied to the transports. +TEST_P(JsepTransport2WithRtcpMux, SetIceParameters) { + bool rtcp_mux_enabled = GetParam(); + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + + JsepTransportDescription jsep_description; + jsep_description.transport_desc = TransportDescription(kIceUfrag1, kIcePwd1); + jsep_description.rtcp_mux_enabled = rtcp_mux_enabled; + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(jsep_description, SdpType::kOffer) + .ok()); + auto fake_ice_transport = static_cast( + jsep_transport_->rtp_dtls_transport()->ice_transport()); + EXPECT_EQ(ICEMODE_FULL, fake_ice_transport->remote_ice_mode()); + EXPECT_EQ(kIceUfrag1, fake_ice_transport->ice_ufrag()); + EXPECT_EQ(kIcePwd1, fake_ice_transport->ice_pwd()); + if (!rtcp_mux_enabled) { + fake_ice_transport = static_cast( + jsep_transport_->rtcp_dtls_transport()->ice_transport()); + ASSERT_TRUE(fake_ice_transport); + EXPECT_EQ(ICEMODE_FULL, fake_ice_transport->remote_ice_mode()); + EXPECT_EQ(kIceUfrag1, fake_ice_transport->ice_ufrag()); + EXPECT_EQ(kIcePwd1, fake_ice_transport->ice_pwd()); + } + + jsep_description.transport_desc = TransportDescription(kIceUfrag2, kIcePwd2); + ASSERT_TRUE(jsep_transport_ + ->SetRemoteJsepTransportDescription(jsep_description, + SdpType::kAnswer) + .ok()); + fake_ice_transport = static_cast( + jsep_transport_->rtp_dtls_transport()->ice_transport()); + EXPECT_EQ(ICEMODE_FULL, fake_ice_transport->remote_ice_mode()); + EXPECT_EQ(kIceUfrag2, fake_ice_transport->remote_ice_ufrag()); + EXPECT_EQ(kIcePwd2, fake_ice_transport->remote_ice_pwd()); + if (!rtcp_mux_enabled) { + fake_ice_transport = static_cast( + jsep_transport_->rtcp_dtls_transport()->ice_transport()); + ASSERT_TRUE(fake_ice_transport); + EXPECT_EQ(ICEMODE_FULL, fake_ice_transport->remote_ice_mode()); + EXPECT_EQ(kIceUfrag2, fake_ice_transport->remote_ice_ufrag()); + EXPECT_EQ(kIcePwd2, fake_ice_transport->remote_ice_pwd()); + } +} + +// Similarly, test DTLS parameters are properly applied to the transports. +TEST_P(JsepTransport2WithRtcpMux, SetDtlsParameters) { + bool rtcp_mux_enabled = GetParam(); + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + + // Create certificates. + rtc::scoped_refptr local_cert = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("local", rtc::KT_DEFAULT))); + rtc::scoped_refptr remote_cert = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("remote", rtc::KT_DEFAULT))); + jsep_transport_->SetLocalCertificate(local_cert); + + // Apply offer. + JsepTransportDescription local_description = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1, + local_cert, CONNECTIONROLE_ACTPASS); + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(local_description, SdpType::kOffer) + .ok()); + // Apply Answer. + JsepTransportDescription remote_description = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2, + remote_cert, CONNECTIONROLE_ACTIVE); + ASSERT_TRUE(jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_description, + SdpType::kAnswer) + .ok()); + + // Verify that SSL role and remote fingerprint were set correctly based on + // transport descriptions. + auto role = jsep_transport_->GetDtlsRole(); + ASSERT_TRUE(role); + EXPECT_EQ(rtc::SSL_SERVER, role); // Because remote description was "active". + auto fake_dtls = + static_cast(jsep_transport_->rtp_dtls_transport()); + EXPECT_EQ(remote_description.transport_desc.identity_fingerprint->ToString(), + fake_dtls->dtls_fingerprint().ToString()); + + if (!rtcp_mux_enabled) { + auto fake_rtcp_dtls = + static_cast(jsep_transport_->rtcp_dtls_transport()); + EXPECT_EQ( + remote_description.transport_desc.identity_fingerprint->ToString(), + fake_rtcp_dtls->dtls_fingerprint().ToString()); + } +} + +// Same as above test, but with remote transport description using +// CONNECTIONROLE_PASSIVE, expecting SSL_CLIENT role. +TEST_P(JsepTransport2WithRtcpMux, SetDtlsParametersWithPassiveAnswer) { + bool rtcp_mux_enabled = GetParam(); + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + + // Create certificates. + rtc::scoped_refptr local_cert = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("local", rtc::KT_DEFAULT))); + rtc::scoped_refptr remote_cert = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("remote", rtc::KT_DEFAULT))); + jsep_transport_->SetLocalCertificate(local_cert); + + // Apply offer. + JsepTransportDescription local_description = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1, + local_cert, CONNECTIONROLE_ACTPASS); + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(local_description, SdpType::kOffer) + .ok()); + // Apply Answer. + JsepTransportDescription remote_description = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2, + remote_cert, CONNECTIONROLE_PASSIVE); + ASSERT_TRUE(jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_description, + SdpType::kAnswer) + .ok()); + + // Verify that SSL role and remote fingerprint were set correctly based on + // transport descriptions. + auto role = jsep_transport_->GetDtlsRole(); + ASSERT_TRUE(role); + EXPECT_EQ(rtc::SSL_CLIENT, + role); // Because remote description was "passive". + auto fake_dtls = + static_cast(jsep_transport_->rtp_dtls_transport()); + EXPECT_EQ(remote_description.transport_desc.identity_fingerprint->ToString(), + fake_dtls->dtls_fingerprint().ToString()); + + if (!rtcp_mux_enabled) { + auto fake_rtcp_dtls = + static_cast(jsep_transport_->rtcp_dtls_transport()); + EXPECT_EQ( + remote_description.transport_desc.identity_fingerprint->ToString(), + fake_rtcp_dtls->dtls_fingerprint().ToString()); + } +} + +// Tests SetNeedsIceRestartFlag and need_ice_restart, ensuring needs_ice_restart +// only starts returning "false" once an ICE restart has been initiated. +TEST_P(JsepTransport2WithRtcpMux, NeedsIceRestart) { + bool rtcp_mux_enabled = GetParam(); + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + + // Use the same JsepTransportDescription for both offer and answer. + JsepTransportDescription description; + description.transport_desc = TransportDescription(kIceUfrag1, kIcePwd1); + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(description, SdpType::kOffer) + .ok()); + ASSERT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(description, SdpType::kAnswer) + .ok()); + // Flag initially should be false. + EXPECT_FALSE(jsep_transport_->needs_ice_restart()); + + // After setting flag, it should be true. + jsep_transport_->SetNeedsIceRestartFlag(); + EXPECT_TRUE(jsep_transport_->needs_ice_restart()); + + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(description, SdpType::kOffer) + .ok()); + ASSERT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(description, SdpType::kAnswer) + .ok()); + EXPECT_TRUE(jsep_transport_->needs_ice_restart()); + + // Doing an offer/answer that restarts ICE should clear the flag. + description.transport_desc = TransportDescription(kIceUfrag2, kIcePwd2); + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(description, SdpType::kOffer) + .ok()); + ASSERT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(description, SdpType::kAnswer) + .ok()); + EXPECT_FALSE(jsep_transport_->needs_ice_restart()); +} + +TEST_P(JsepTransport2WithRtcpMux, GetStats) { + bool rtcp_mux_enabled = GetParam(); + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + + size_t expected_stats_size = rtcp_mux_enabled ? 1u : 2u; + TransportStats stats; + EXPECT_TRUE(jsep_transport_->GetStats(&stats)); + EXPECT_EQ(expected_stats_size, stats.channel_stats.size()); + EXPECT_EQ(ICE_CANDIDATE_COMPONENT_RTP, stats.channel_stats[0].component); + if (!rtcp_mux_enabled) { + EXPECT_EQ(ICE_CANDIDATE_COMPONENT_RTCP, stats.channel_stats[1].component); + } +} + +// Tests that VerifyCertificateFingerprint only returns true when the +// certificate matches the fingerprint. +TEST_P(JsepTransport2WithRtcpMux, VerifyCertificateFingerprint) { + bool rtcp_mux_enabled = GetParam(); + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + + EXPECT_FALSE( + jsep_transport_->VerifyCertificateFingerprint(nullptr, nullptr).ok()); + rtc::KeyType key_types[] = {rtc::KT_RSA, rtc::KT_ECDSA}; + + for (auto& key_type : key_types) { + rtc::scoped_refptr certificate = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("testing", key_type))); + ASSERT_NE(nullptr, certificate); + + std::string digest_algorithm; + ASSERT_TRUE(certificate->ssl_certificate().GetSignatureDigestAlgorithm( + &digest_algorithm)); + ASSERT_FALSE(digest_algorithm.empty()); + std::unique_ptr good_fingerprint( + rtc::SSLFingerprint::Create(digest_algorithm, certificate->identity())); + ASSERT_NE(nullptr, good_fingerprint); + + EXPECT_TRUE(jsep_transport_ + ->VerifyCertificateFingerprint(certificate.get(), + good_fingerprint.get()) + .ok()); + EXPECT_FALSE(jsep_transport_ + ->VerifyCertificateFingerprint(certificate.get(), nullptr) + .ok()); + EXPECT_FALSE( + jsep_transport_ + ->VerifyCertificateFingerprint(nullptr, good_fingerprint.get()) + .ok()); + + rtc::SSLFingerprint bad_fingerprint = *good_fingerprint; + bad_fingerprint.digest.AppendData("0", 1); + EXPECT_FALSE( + jsep_transport_ + ->VerifyCertificateFingerprint(certificate.get(), &bad_fingerprint) + .ok()); + } +} + +// Tests the logic of DTLS role negotiation for an initial offer/answer. +TEST_P(JsepTransport2WithRtcpMux, ValidDtlsRoleNegotiation) { + bool rtcp_mux_enabled = GetParam(); + // Just use the same certificate for both sides; doesn't really matter in a + // non end-to-end test. + rtc::scoped_refptr certificate = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("testing", rtc::KT_ECDSA))); + + JsepTransportDescription local_description = MakeJsepTransportDescription( + rtcp_mux_enabled, kIceUfrag1, kIcePwd1, certificate); + JsepTransportDescription remote_description = MakeJsepTransportDescription( + rtcp_mux_enabled, kIceUfrag2, kIcePwd2, certificate); + + // Parameters which set the SSL role to SSL_CLIENT. + NegotiateRoleParams valid_client_params[] = { + {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_ACTPASS, SdpType::kAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_ACTPASS, SdpType::kPrAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_PASSIVE, SdpType::kOffer, + SdpType::kAnswer}, + {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_PASSIVE, SdpType::kOffer, + SdpType::kPrAnswer}}; + + for (auto& param : valid_client_params) { + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + jsep_transport_->SetLocalCertificate(certificate); + + local_description.transport_desc.connection_role = param.local_role; + remote_description.transport_desc.connection_role = param.remote_role; + + // Set the offer first. + if (param.local_type == SdpType::kOffer) { + EXPECT_TRUE(jsep_transport_ + ->SetLocalJsepTransportDescription(local_description, + param.local_type) + .ok()); + EXPECT_TRUE(jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_description, + param.remote_type) + .ok()); + } else { + EXPECT_TRUE(jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_description, + param.remote_type) + .ok()); + EXPECT_TRUE(jsep_transport_ + ->SetLocalJsepTransportDescription(local_description, + param.local_type) + .ok()); + } + EXPECT_EQ(rtc::SSL_CLIENT, *jsep_transport_->GetDtlsRole()); + } + + // Parameters which set the SSL role to SSL_SERVER. + NegotiateRoleParams valid_server_params[] = { + {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTPASS, SdpType::kAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTPASS, SdpType::kPrAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_ACTIVE, SdpType::kOffer, + SdpType::kAnswer}, + {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_ACTIVE, SdpType::kOffer, + SdpType::kPrAnswer}}; + + for (auto& param : valid_server_params) { + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + jsep_transport_->SetLocalCertificate(certificate); + + local_description.transport_desc.connection_role = param.local_role; + remote_description.transport_desc.connection_role = param.remote_role; + + // Set the offer first. + if (param.local_type == SdpType::kOffer) { + EXPECT_TRUE(jsep_transport_ + ->SetLocalJsepTransportDescription(local_description, + param.local_type) + .ok()); + EXPECT_TRUE(jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_description, + param.remote_type) + .ok()); + } else { + EXPECT_TRUE(jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_description, + param.remote_type) + .ok()); + EXPECT_TRUE(jsep_transport_ + ->SetLocalJsepTransportDescription(local_description, + param.local_type) + .ok()); + } + EXPECT_EQ(rtc::SSL_SERVER, *jsep_transport_->GetDtlsRole()); + } +} + +// Tests the logic of DTLS role negotiation for an initial offer/answer. +TEST_P(JsepTransport2WithRtcpMux, InvalidDtlsRoleNegotiation) { + bool rtcp_mux_enabled = GetParam(); + // Just use the same certificate for both sides; doesn't really matter in a + // non end-to-end test. + rtc::scoped_refptr certificate = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("testing", rtc::KT_ECDSA))); + + JsepTransportDescription local_description = MakeJsepTransportDescription( + rtcp_mux_enabled, kIceUfrag1, kIcePwd1, certificate); + JsepTransportDescription remote_description = MakeJsepTransportDescription( + rtcp_mux_enabled, kIceUfrag2, kIcePwd2, certificate); + + NegotiateRoleParams duplicate_params[] = { + {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_ACTIVE, SdpType::kAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_ACTPASS, SdpType::kAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_PASSIVE, SdpType::kAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_ACTIVE, SdpType::kPrAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_ACTPASS, SdpType::kPrAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_PASSIVE, SdpType::kPrAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_ACTIVE, SdpType::kOffer, + SdpType::kAnswer}, + {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_ACTPASS, SdpType::kOffer, + SdpType::kAnswer}, + {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_PASSIVE, SdpType::kOffer, + SdpType::kAnswer}, + {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_ACTIVE, SdpType::kOffer, + SdpType::kPrAnswer}, + {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_ACTPASS, SdpType::kOffer, + SdpType::kPrAnswer}, + {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_PASSIVE, SdpType::kOffer, + SdpType::kPrAnswer}}; + + for (auto& param : duplicate_params) { + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + jsep_transport_->SetLocalCertificate(certificate); + + local_description.transport_desc.connection_role = param.local_role; + remote_description.transport_desc.connection_role = param.remote_role; + + if (param.local_type == SdpType::kOffer) { + EXPECT_TRUE(jsep_transport_ + ->SetLocalJsepTransportDescription(local_description, + param.local_type) + .ok()); + EXPECT_FALSE(jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_description, + param.remote_type) + .ok()); + } else { + EXPECT_TRUE(jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_description, + param.remote_type) + .ok()); + EXPECT_FALSE(jsep_transport_ + ->SetLocalJsepTransportDescription(local_description, + param.local_type) + .ok()); + } + } + + // Invalid parameters due to the offerer not using ACTPASS. + NegotiateRoleParams offerer_without_actpass_params[] = { + {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_PASSIVE, SdpType::kAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTIVE, SdpType::kAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_PASSIVE, SdpType::kAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_PASSIVE, SdpType::kPrAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTIVE, SdpType::kPrAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_PASSIVE, SdpType::kPrAnswer, + SdpType::kOffer}, + {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_PASSIVE, SdpType::kOffer, + SdpType::kAnswer}, + {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTIVE, SdpType::kOffer, + SdpType::kAnswer}, + {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTPASS, SdpType::kOffer, + SdpType::kAnswer}, + {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_PASSIVE, SdpType::kOffer, + SdpType::kPrAnswer}, + {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTIVE, SdpType::kOffer, + SdpType::kPrAnswer}, + {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTPASS, SdpType::kOffer, + SdpType::kPrAnswer}}; + + for (auto& param : offerer_without_actpass_params) { + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + jsep_transport_->SetLocalCertificate(certificate); + + local_description.transport_desc.connection_role = param.local_role; + remote_description.transport_desc.connection_role = param.remote_role; + + if (param.local_type == SdpType::kOffer) { + EXPECT_TRUE(jsep_transport_ + ->SetLocalJsepTransportDescription(local_description, + param.local_type) + .ok()); + EXPECT_FALSE(jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_description, + param.remote_type) + .ok()); + } else { + EXPECT_TRUE(jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_description, + param.remote_type) + .ok()); + EXPECT_FALSE(jsep_transport_ + ->SetLocalJsepTransportDescription(local_description, + param.local_type) + .ok()); + } + } +} + +INSTANTIATE_TEST_CASE_P(JsepTransport2Test, + JsepTransport2WithRtcpMux, + testing::Bool()); + +// Test that a reoffer in the opposite direction is successful as long as the +// role isn't changing. Doesn't test every possible combination like the test +// above. +TEST_F(JsepTransport2Test, ValidDtlsReofferFromAnswerer) { + // Just use the same certificate for both sides; doesn't really matter in a + // non end-to-end test. + rtc::scoped_refptr certificate = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("testing", rtc::KT_ECDSA))); + bool rtcp_mux_enabled = true; + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + jsep_transport_->SetLocalCertificate(certificate); + + JsepTransportDescription local_offer = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1, + certificate, CONNECTIONROLE_ACTPASS); + JsepTransportDescription remote_answer = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2, + certificate, CONNECTIONROLE_ACTIVE); + + EXPECT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(local_offer, SdpType::kOffer) + .ok()); + EXPECT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_answer, SdpType::kAnswer) + .ok()); + + // We were actpass->active previously, now in the other direction it's + // actpass->passive. + JsepTransportDescription remote_offer = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2, + certificate, CONNECTIONROLE_ACTPASS); + JsepTransportDescription local_answer = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1, + certificate, CONNECTIONROLE_PASSIVE); + + EXPECT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_offer, SdpType::kOffer) + .ok()); + EXPECT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(local_answer, SdpType::kAnswer) + .ok()); +} + +// Test that a reoffer in the opposite direction fails if the role changes. +// Inverse of test above. +TEST_F(JsepTransport2Test, InvalidDtlsReofferFromAnswerer) { + // Just use the same certificate for both sides; doesn't really matter in a + // non end-to-end test. + rtc::scoped_refptr certificate = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("testing", rtc::KT_ECDSA))); + bool rtcp_mux_enabled = true; + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + jsep_transport_->SetLocalCertificate(certificate); + + JsepTransportDescription local_offer = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1, + certificate, CONNECTIONROLE_ACTPASS); + JsepTransportDescription remote_answer = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2, + certificate, CONNECTIONROLE_ACTIVE); + + EXPECT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(local_offer, SdpType::kOffer) + .ok()); + EXPECT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_answer, SdpType::kAnswer) + .ok()); + + // Changing role to passive here isn't allowed. Though for some reason this + // only fails in SetLocalTransportDescription. + JsepTransportDescription remote_offer = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2, + certificate, CONNECTIONROLE_PASSIVE); + JsepTransportDescription local_answer = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1, + certificate, CONNECTIONROLE_ACTIVE); + + EXPECT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_offer, SdpType::kOffer) + .ok()); + EXPECT_FALSE( + jsep_transport_ + ->SetLocalJsepTransportDescription(local_answer, SdpType::kAnswer) + .ok()); +} + +// Test that a remote offer with the current negotiated role can be accepted. +// This is allowed by dtls-sdp, though we'll never generate such an offer, +// since JSEP requires generating "actpass". +TEST_F(JsepTransport2Test, RemoteOfferWithCurrentNegotiatedDtlsRole) { + rtc::scoped_refptr certificate = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("testing", rtc::KT_ECDSA))); + bool rtcp_mux_enabled = true; + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + jsep_transport_->SetLocalCertificate(certificate); + + JsepTransportDescription remote_desc = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1, + certificate, CONNECTIONROLE_ACTPASS); + JsepTransportDescription local_desc = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2, + certificate, CONNECTIONROLE_ACTIVE); + + // Normal initial offer/answer with "actpass" in the offer and "active" in + // the answer. + ASSERT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_desc, SdpType::kOffer) + .ok()); + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(local_desc, SdpType::kAnswer) + .ok()); + + // Sanity check that role was actually negotiated. + rtc::Optional role = jsep_transport_->GetDtlsRole(); + ASSERT_TRUE(role); + EXPECT_EQ(rtc::SSL_CLIENT, *role); + + // Subsequent offer with current negotiated role of "passive". + remote_desc.transport_desc.connection_role = CONNECTIONROLE_PASSIVE; + EXPECT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_desc, SdpType::kOffer) + .ok()); + EXPECT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(local_desc, SdpType::kAnswer) + .ok()); +} + +// Test that a remote offer with the inverse of the current negotiated DTLS +// role is rejected. +TEST_F(JsepTransport2Test, RemoteOfferThatChangesNegotiatedDtlsRole) { + rtc::scoped_refptr certificate = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("testing", rtc::KT_ECDSA))); + bool rtcp_mux_enabled = true; + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + jsep_transport_->SetLocalCertificate(certificate); + + JsepTransportDescription remote_desc = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1, + certificate, CONNECTIONROLE_ACTPASS); + JsepTransportDescription local_desc = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2, + certificate, CONNECTIONROLE_ACTIVE); + + // Normal initial offer/answer with "actpass" in the offer and "active" in + // the answer. + ASSERT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_desc, SdpType::kOffer) + .ok()); + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(local_desc, SdpType::kAnswer) + .ok()); + + // Sanity check that role was actually negotiated. + rtc::Optional role = jsep_transport_->GetDtlsRole(); + ASSERT_TRUE(role); + EXPECT_EQ(rtc::SSL_CLIENT, *role); + + // Subsequent offer with current negotiated role of "passive". + remote_desc.transport_desc.connection_role = CONNECTIONROLE_ACTIVE; + EXPECT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_desc, SdpType::kOffer) + .ok()); + EXPECT_FALSE( + jsep_transport_ + ->SetLocalJsepTransportDescription(local_desc, SdpType::kAnswer) + .ok()); +} + +// Testing that a legacy client that doesn't use the setup attribute will be +// interpreted as having an active role. +TEST_F(JsepTransport2Test, DtlsSetupWithLegacyAsAnswerer) { + rtc::scoped_refptr certificate = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("testing", rtc::KT_ECDSA))); + bool rtcp_mux_enabled = true; + CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp); + jsep_transport_->SetLocalCertificate(certificate); + + JsepTransportDescription remote_desc = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1, + certificate, CONNECTIONROLE_ACTPASS); + JsepTransportDescription local_desc = + MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2, + certificate, CONNECTIONROLE_ACTIVE); + + local_desc.transport_desc.connection_role = CONNECTIONROLE_ACTPASS; + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(local_desc, SdpType::kOffer) + .ok()); + // Use CONNECTIONROLE_NONE to simulate legacy endpoint. + remote_desc.transport_desc.connection_role = CONNECTIONROLE_NONE; + ASSERT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_desc, SdpType::kAnswer) + .ok()); + + rtc::Optional role = jsep_transport_->GetDtlsRole(); + ASSERT_TRUE(role); + // Since legacy answer ommitted setup atribute, and we offered actpass, we + // should act as passive (server). + EXPECT_EQ(rtc::SSL_SERVER, *role); +} + +// Tests that when the RTCP mux is successfully negotiated, the RTCP transport +// will be destroyed and the SignalRtpMuxActive will be fired. +TEST_F(JsepTransport2Test, RtcpMuxNegotiation) { + CreateJsepTransport2(/*rtcp_mux_enabled=*/false, SrtpMode::kDtlsSrtp); + JsepTransportDescription local_desc; + local_desc.rtcp_mux_enabled = true; + EXPECT_NE(nullptr, jsep_transport_->rtcp_dtls_transport()); + EXPECT_FALSE(signal_rtcp_mux_active_received_); + + // The remote side supports RTCP-mux. + JsepTransportDescription remote_desc; + remote_desc.rtcp_mux_enabled = true; + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(local_desc, SdpType::kOffer) + .ok()); + ASSERT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_desc, SdpType::kAnswer) + .ok()); + + EXPECT_EQ(nullptr, jsep_transport_->rtcp_dtls_transport()); + EXPECT_TRUE(signal_rtcp_mux_active_received_); + + // The remote side doesn't support RTCP-mux. + CreateJsepTransport2(/*rtcp_mux_enabled=*/false, SrtpMode::kDtlsSrtp); + signal_rtcp_mux_active_received_ = false; + remote_desc.rtcp_mux_enabled = false; + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(local_desc, SdpType::kOffer) + .ok()); + ASSERT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(remote_desc, SdpType::kAnswer) + .ok()); + + EXPECT_NE(nullptr, jsep_transport_->rtcp_dtls_transport()); + EXPECT_FALSE(signal_rtcp_mux_active_received_); +} + +TEST_F(JsepTransport2Test, SdesNegotiation) { + CreateJsepTransport2(/*rtcp_mux_enabled=*/true, SrtpMode::kSdes); + ASSERT_TRUE(sdes_transport_); + EXPECT_FALSE(sdes_transport_->IsActive()); + + JsepTransportDescription offer_desc; + offer_desc.cryptos.push_back(cricket::CryptoParams( + 1, rtc::CS_AES_CM_128_HMAC_SHA1_32, + "inline:" + rtc::CreateRandomString(40), std::string())); + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(offer_desc, SdpType::kOffer) + .ok()); + + JsepTransportDescription answer_desc; + answer_desc.cryptos.push_back(cricket::CryptoParams( + 1, rtc::CS_AES_CM_128_HMAC_SHA1_32, + "inline:" + rtc::CreateRandomString(40), std::string())); + ASSERT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(answer_desc, SdpType::kAnswer) + .ok()); + EXPECT_TRUE(sdes_transport_->IsActive()); +} + +TEST_F(JsepTransport2Test, SdesNegotiationWithEmptyCryptosInAnswer) { + CreateJsepTransport2(/*rtcp_mux_enabled=*/true, SrtpMode::kSdes); + ASSERT_TRUE(sdes_transport_); + EXPECT_FALSE(sdes_transport_->IsActive()); + + JsepTransportDescription offer_desc; + offer_desc.cryptos.push_back(cricket::CryptoParams( + 1, rtc::CS_AES_CM_128_HMAC_SHA1_32, + "inline:" + rtc::CreateRandomString(40), std::string())); + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(offer_desc, SdpType::kOffer) + .ok()); + + JsepTransportDescription answer_desc; + ASSERT_TRUE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(answer_desc, SdpType::kAnswer) + .ok()); + // SRTP is not active because the crypto parameter is answer is empty. + EXPECT_FALSE(sdes_transport_->IsActive()); +} + +TEST_F(JsepTransport2Test, SdesNegotiationWithMismatchedCryptos) { + CreateJsepTransport2(/*rtcp_mux_enabled=*/true, SrtpMode::kSdes); + ASSERT_TRUE(sdes_transport_); + EXPECT_FALSE(sdes_transport_->IsActive()); + + JsepTransportDescription offer_desc; + offer_desc.cryptos.push_back(cricket::CryptoParams( + 1, rtc::CS_AES_CM_128_HMAC_SHA1_32, + "inline:" + rtc::CreateRandomString(40), std::string())); + ASSERT_TRUE( + jsep_transport_ + ->SetLocalJsepTransportDescription(offer_desc, SdpType::kOffer) + .ok()); + + JsepTransportDescription answer_desc; + answer_desc.cryptos.push_back(cricket::CryptoParams( + 1, rtc::CS_AES_CM_128_HMAC_SHA1_80, + "inline:" + rtc::CreateRandomString(40), std::string())); + // Expected to fail because the crypto parameters don't match. + ASSERT_FALSE( + jsep_transport_ + ->SetRemoteJsepTransportDescription(answer_desc, SdpType::kAnswer) + .ok()); +} + +// Tests that the remote candidates can be added to the transports after both +// local and remote descriptions are set. +TEST_F(JsepTransport2Test, AddRemoteCandidates) { + CreateJsepTransport2(/*rtcp_mux_enabled=*/true, SrtpMode::kDtlsSrtp); + auto fake_ice_transport = static_cast( + jsep_transport_->rtp_dtls_transport()->ice_transport()); + + Candidates candidates; + candidates.push_back(CreateCandidate(/*COMPONENT_RTP*/ 1)); + candidates.push_back(CreateCandidate(/*COMPONENT_RTP*/ 1)); + + JsepTransportDescription desc; + ASSERT_TRUE( + jsep_transport_->SetLocalJsepTransportDescription(desc, SdpType::kOffer) + .ok()); + // Expected to fail because the remote description is unset. + EXPECT_FALSE(jsep_transport_->AddRemoteCandidates(candidates).ok()); + + ASSERT_TRUE( + jsep_transport_->SetRemoteJsepTransportDescription(desc, SdpType::kAnswer) + .ok()); + EXPECT_EQ(0u, fake_ice_transport->remote_candidates().size()); + EXPECT_TRUE(jsep_transport_->AddRemoteCandidates(candidates).ok()); + EXPECT_EQ(candidates.size(), fake_ice_transport->remote_candidates().size()); +} + +} // namespace cricket diff --git a/pc/jseptransport_unittest.cc b/pc/jseptransport_unittest.cc index 1111f36b0b..a40312ffca 100644 --- a/pc/jseptransport_unittest.cc +++ b/pc/jseptransport_unittest.cc @@ -119,7 +119,7 @@ TEST_F(JsepTransportTest, TestDtlsTransportParameters) { // Verify that SSL role and remote fingerprint were set correctly based on // transport descriptions. rtc::SSLRole role; - EXPECT_TRUE(fake_dtls_transports_[0]->GetSslRole(&role)); + EXPECT_TRUE(fake_dtls_transports_[0]->GetDtlsRole(&role)); EXPECT_EQ(rtc::SSL_SERVER, role); // Because remote description was "active". EXPECT_EQ(remote_desc.identity_fingerprint->ToString(), fake_dtls_transports_[0]->dtls_fingerprint().ToString()); @@ -152,7 +152,7 @@ TEST_F(JsepTransportTest, TestDtlsTransportParametersWithPassiveAnswer) { // Verify that SSL role and remote fingerprint were set correctly based on // transport descriptions. rtc::SSLRole role; - EXPECT_TRUE(fake_dtls_transports_[0]->GetSslRole(&role)); + EXPECT_TRUE(fake_dtls_transports_[0]->GetDtlsRole(&role)); EXPECT_EQ(rtc::SSL_CLIENT, role); // Because remote description was "passive". EXPECT_EQ(remote_desc.identity_fingerprint->ToString(), @@ -194,7 +194,7 @@ TEST_F(JsepTransportTest, TestTransportParametersAppliedToTwoComponents) { EXPECT_EQ(kIcePwd2, fake_ice_transports_[i]->remote_ice_pwd()); // Verify parameters of DTLS transports. rtc::SSLRole role; - EXPECT_TRUE(fake_dtls_transports_[i]->GetSslRole(&role)); + EXPECT_TRUE(fake_dtls_transports_[i]->GetDtlsRole(&role)); EXPECT_EQ(rtc::SSL_SERVER, role); // Because remote description was "active". EXPECT_EQ(remote_desc.identity_fingerprint->ToString(), diff --git a/pc/jseptransportcontroller.cc b/pc/jseptransportcontroller.cc new file mode 100644 index 0000000000..aac55c4c25 --- /dev/null +++ b/pc/jseptransportcontroller.cc @@ -0,0 +1,1024 @@ +/* + * Copyright 2017 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 "pc/jseptransportcontroller.h" + +#include +#include +#include + +#include "p2p/base/port.h" +#include "rtc_base/bind.h" +#include "rtc_base/checks.h" +#include "rtc_base/ptr_util.h" +#include "rtc_base/thread.h" + +using webrtc::SdpType; + +namespace { + +enum { + MSG_ICECONNECTIONSTATE, + MSG_ICEGATHERINGSTATE, + MSG_ICECANDIDATESGATHERED, +}; + +struct CandidatesData : public rtc::MessageData { + CandidatesData(const std::string& transport_name, + const cricket::Candidates& candidates) + : transport_name(transport_name), candidates(candidates) {} + + std::string transport_name; + cricket::Candidates candidates; +}; + +webrtc::RTCError VerifyCandidate(const cricket::Candidate& cand) { + // No address zero. + if (cand.address().IsNil() || cand.address().IsAnyIP()) { + return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, + "candidate has address of zero"); + } + + // Disallow all ports below 1024, except for 80 and 443 on public addresses. + int port = cand.address().port(); + if (cand.protocol() == cricket::TCP_PROTOCOL_NAME && + (cand.tcptype() == cricket::TCPTYPE_ACTIVE_STR || port == 0)) { + // Expected for active-only candidates per + // http://tools.ietf.org/html/rfc6544#section-4.5 so no error. + // Libjingle clients emit port 0, in "active" mode. + return webrtc::RTCError::OK(); + } + if (port < 1024) { + if ((port != 80) && (port != 443)) { + return webrtc::RTCError( + webrtc::RTCErrorType::INVALID_PARAMETER, + "candidate has port below 1024, but not 80 or 443"); + } + + if (cand.address().IsPrivateIP()) { + return webrtc::RTCError( + webrtc::RTCErrorType::INVALID_PARAMETER, + "candidate has port of 80 or 443 with private IP address"); + } + } + + return webrtc::RTCError::OK(); +} + +webrtc::RTCError VerifyCandidates(const cricket::Candidates& candidates) { + for (const cricket::Candidate& candidate : candidates) { + webrtc::RTCError error = VerifyCandidate(candidate); + if (!error.ok()) { + return error; + } + } + return webrtc::RTCError::OK(); +} + +} // namespace + +namespace webrtc { + +JsepTransportController::JsepTransportController( + rtc::Thread* signaling_thread, + rtc::Thread* network_thread, + cricket::PortAllocator* port_allocator, + Config config) + : signaling_thread_(signaling_thread), + network_thread_(network_thread), + port_allocator_(port_allocator), + config_(config) {} + +JsepTransportController::~JsepTransportController() { + // Channel destructors may try to send packets, so this needs to happen on + // the network thread. + network_thread_->Invoke( + RTC_FROM_HERE, + rtc::Bind(&JsepTransportController::DestroyAllJsepTransports_n, this)); +} + +RTCError JsepTransportController::SetLocalDescription( + SdpType type, + const cricket::SessionDescription* description) { + if (!network_thread_->IsCurrent()) { + return network_thread_->Invoke( + RTC_FROM_HERE, [=] { return SetLocalDescription(type, description); }); + } + + if (!initial_offerer_.has_value()) { + initial_offerer_.emplace(type == SdpType::kOffer); + if (*initial_offerer_) { + SetIceRole_n(cricket::ICEROLE_CONTROLLING); + } else { + SetIceRole_n(cricket::ICEROLE_CONTROLLED); + } + } + return ApplyDescription_n(/*local=*/true, type, description); +} + +RTCError JsepTransportController::SetRemoteDescription( + SdpType type, + const cricket::SessionDescription* description) { + if (!network_thread_->IsCurrent()) { + return network_thread_->Invoke( + RTC_FROM_HERE, [=] { return SetRemoteDescription(type, description); }); + } + + return ApplyDescription_n(/*local=*/false, type, description); +} + +RtpTransportInternal* JsepTransportController::GetRtpTransport( + const std::string& mid) const { + auto jsep_transport = GetJsepTransport(mid); + if (!jsep_transport) { + return nullptr; + } + return jsep_transport->rtp_transport(); +} + +cricket::DtlsTransportInternal* JsepTransportController::GetDtlsTransport( + const std::string& mid) const { + auto jsep_transport = GetJsepTransport(mid); + if (!jsep_transport) { + return nullptr; + } + return jsep_transport->rtp_dtls_transport(); +} + +cricket::DtlsTransportInternal* JsepTransportController::GetRtcpDtlsTransport( + const std::string& mid) const { + auto jsep_transport = GetJsepTransport(mid); + if (!jsep_transport) { + return nullptr; + } + return jsep_transport->rtcp_dtls_transport(); +} + +void JsepTransportController::SetIceConfig(const cricket::IceConfig& config) { + if (!network_thread_->IsCurrent()) { + network_thread_->Invoke(RTC_FROM_HERE, [&] { SetIceConfig(config); }); + return; + } + + ice_config_ = config; + for (auto& dtls : GetDtlsTransports()) { + dtls->ice_transport()->SetIceConfig(ice_config_); + } +} + +void JsepTransportController::SetNeedsIceRestartFlag() { + for (auto& kv : jsep_transports_by_mid_) { + kv.second->SetNeedsIceRestartFlag(); + } +} + +bool JsepTransportController::NeedsIceRestart( + const std::string& transport_name) const { + const cricket::JsepTransport2* transport = GetJsepTransport(transport_name); + if (!transport) { + return false; + } + return transport->needs_ice_restart(); +} + +rtc::Optional JsepTransportController::GetDtlsRole( + const std::string& transport_name) const { + if (!network_thread_->IsCurrent()) { + return network_thread_->Invoke>( + RTC_FROM_HERE, [&] { return GetDtlsRole(transport_name); }); + } + + const cricket::JsepTransport2* t = GetJsepTransport(transport_name); + if (!t) { + return rtc::Optional(); + } + return t->GetDtlsRole(); +} + +bool JsepTransportController::SetLocalCertificate( + const rtc::scoped_refptr& certificate) { + if (!network_thread_->IsCurrent()) { + return network_thread_->Invoke( + RTC_FROM_HERE, [&] { return SetLocalCertificate(certificate); }); + } + + // Can't change a certificate, or set a null certificate. + if (certificate_ || !certificate) { + return false; + } + certificate_ = certificate; + + // Set certificate for JsepTransport, which verifies it matches the + // fingerprint in SDP, and DTLS transport. + // Fallback from DTLS to SDES is not supported. + for (auto& kv : jsep_transports_by_mid_) { + kv.second->SetLocalCertificate(certificate_); + } + for (auto& dtls : GetDtlsTransports()) { + bool set_cert_success = dtls->SetLocalCertificate(certificate_); + RTC_DCHECK(set_cert_success); + } + return true; +} + +rtc::scoped_refptr +JsepTransportController::GetLocalCertificate( + const std::string& transport_name) const { + if (!network_thread_->IsCurrent()) { + return network_thread_->Invoke>( + RTC_FROM_HERE, [&] { return GetLocalCertificate(transport_name); }); + } + + const cricket::JsepTransport2* t = GetJsepTransport(transport_name); + if (!t) { + return nullptr; + } + return t->GetLocalCertificate(); +} + +std::unique_ptr +JsepTransportController::GetRemoteSSLCertificate( + const std::string& transport_name) const { + if (!network_thread_->IsCurrent()) { + return network_thread_->Invoke>( + RTC_FROM_HERE, [&] { return GetRemoteSSLCertificate(transport_name); }); + } + + // Get the certificate from the RTP channel's DTLS handshake. Should be + // identical to the RTCP channel's, since they were given the same remote + // fingerprint. + auto dtls = GetDtlsTransport(transport_name); + if (!dtls) { + return nullptr; + } + + return dtls->GetRemoteSSLCertificate(); +} + +void JsepTransportController::MaybeStartGathering() { + if (!network_thread_->IsCurrent()) { + network_thread_->Invoke(RTC_FROM_HERE, + [&] { MaybeStartGathering(); }); + return; + } + + for (auto& dtls : GetDtlsTransports()) { + dtls->ice_transport()->MaybeStartGathering(); + } +} + +RTCError JsepTransportController::AddRemoteCandidates( + const std::string& transport_name, + const cricket::Candidates& candidates) { + if (!network_thread_->IsCurrent()) { + return network_thread_->Invoke(RTC_FROM_HERE, [&] { + return AddRemoteCandidates(transport_name, candidates); + }); + } + + // Verify each candidate before passing down to the transport layer. + RTCError error = VerifyCandidates(candidates); + if (!error.ok()) { + return error; + } + + cricket::JsepTransport2* jsep_transport = GetJsepTransport(transport_name); + if (!jsep_transport) { + return RTCError(RTCErrorType::INVALID_PARAMETER, + "The transport doesn't exist."); + } + + return jsep_transport->AddRemoteCandidates(candidates); +} + +RTCError JsepTransportController::RemoveRemoteCandidates( + const cricket::Candidates& candidates) { + if (!network_thread_->IsCurrent()) { + return network_thread_->Invoke( + RTC_FROM_HERE, [&] { return RemoveRemoteCandidates(candidates); }); + } + + // Verify each candidate before passing down to the transport layer. + RTCError error = VerifyCandidates(candidates); + if (!error.ok()) { + return error; + } + + std::map candidates_by_transport_name; + for (const cricket::Candidate& cand : candidates) { + if (!cand.transport_name().empty()) { + candidates_by_transport_name[cand.transport_name()].push_back(cand); + } else { + RTC_LOG(LS_ERROR) << "Not removing candidate because it does not have a " + "transport name set: " + << cand.ToString(); + } + } + + for (const auto& kv : candidates_by_transport_name) { + const std::string& transport_name = kv.first; + const cricket::Candidates& candidates = kv.second; + cricket::JsepTransport2* jsep_transport = GetJsepTransport(transport_name); + if (!jsep_transport) { + return RTCError(RTCErrorType::INVALID_PARAMETER, + "The transport doesn't exist."); + } + for (const cricket::Candidate& candidate : candidates) { + auto dtls = candidate.component() == cricket::ICE_CANDIDATE_COMPONENT_RTP + ? jsep_transport->rtp_dtls_transport() + : jsep_transport->rtcp_dtls_transport(); + if (dtls) { + dtls->ice_transport()->RemoveRemoteCandidate(candidate); + } + } + } + return RTCError::OK(); +} + +bool JsepTransportController::GetStats(const std::string& transport_name, + cricket::TransportStats* stats) { + if (!network_thread_->IsCurrent()) { + return network_thread_->Invoke( + RTC_FROM_HERE, [=] { return GetStats(transport_name, stats); }); + } + + cricket::JsepTransport2* transport = GetJsepTransport(transport_name); + if (!transport) { + return false; + } + return transport->GetStats(stats); +} + +void JsepTransportController::SetMetricsObserver( + webrtc::MetricsObserverInterface* metrics_observer) { + if (!network_thread_->IsCurrent()) { + network_thread_->Invoke( + RTC_FROM_HERE, [=] { SetMetricsObserver(metrics_observer); }); + return; + } + + metrics_observer_ = metrics_observer; + for (auto& dtls : GetDtlsTransports()) { + dtls->ice_transport()->SetMetricsObserver(metrics_observer); + } +} + +std::unique_ptr +JsepTransportController::CreateDtlsTransport(const std::string& transport_name, + bool rtcp) { + RTC_DCHECK(network_thread_->IsCurrent()); + int component = rtcp ? cricket::ICE_CANDIDATE_COMPONENT_RTCP + : cricket::ICE_CANDIDATE_COMPONENT_RTP; + + std::unique_ptr dtls; + if (config_.external_transport_factory) { + auto ice = config_.external_transport_factory->CreateIceTransport( + transport_name, component); + dtls = config_.external_transport_factory->CreateDtlsTransport( + std::move(ice), config_.crypto_options); + } else { + auto ice = rtc::MakeUnique( + transport_name, component, port_allocator_); + dtls = rtc::MakeUnique(std::move(ice), + config_.crypto_options); + } + + RTC_DCHECK(dtls); + dtls->SetSslMaxProtocolVersion(config_.ssl_max_version); + dtls->ice_transport()->SetMetricsObserver(metrics_observer_); + dtls->ice_transport()->SetIceRole(ice_role_); + dtls->ice_transport()->SetIceTiebreaker(ice_tiebreaker_); + dtls->ice_transport()->SetIceConfig(ice_config_); + if (certificate_) { + bool set_cert_success = dtls->SetLocalCertificate(certificate_); + RTC_DCHECK(set_cert_success); + } + + // Connect to signals offered by the DTLS and ICE transport. + dtls->SignalWritableState.connect( + this, &JsepTransportController::OnTransportWritableState_n); + dtls->SignalReceivingState.connect( + this, &JsepTransportController::OnTransportReceivingState_n); + dtls->SignalDtlsHandshakeError.connect( + this, &JsepTransportController::OnDtlsHandshakeError); + dtls->ice_transport()->SignalGatheringState.connect( + this, &JsepTransportController::OnTransportGatheringState_n); + dtls->ice_transport()->SignalCandidateGathered.connect( + this, &JsepTransportController::OnTransportCandidateGathered_n); + dtls->ice_transport()->SignalCandidatesRemoved.connect( + this, &JsepTransportController::OnTransportCandidatesRemoved_n); + dtls->ice_transport()->SignalRoleConflict.connect( + this, &JsepTransportController::OnTransportRoleConflict_n); + dtls->ice_transport()->SignalStateChanged.connect( + this, &JsepTransportController::OnTransportStateChanged_n); + return dtls; +} + +std::unique_ptr +JsepTransportController::CreateUnencryptedRtpTransport( + const std::string& transport_name, + rtc::PacketTransportInternal* rtp_packet_transport, + rtc::PacketTransportInternal* rtcp_packet_transport) { + RTC_DCHECK(network_thread_->IsCurrent()); + // TODO(zhihuang): Add support of unencrypted RTP for testing. + return nullptr; +} + +std::unique_ptr +JsepTransportController::CreateSdesTransport( + const std::string& transport_name, + rtc::PacketTransportInternal* rtp_packet_transport, + rtc::PacketTransportInternal* rtcp_packet_transport) { + RTC_DCHECK(network_thread_->IsCurrent()); + bool rtcp_mux_enabled = rtcp_packet_transport == nullptr; + auto srtp_transport = + rtc::MakeUnique(rtcp_mux_enabled); + RTC_DCHECK(rtp_packet_transport); + srtp_transport->SetRtpPacketTransport(rtp_packet_transport); + if (rtcp_packet_transport) { + srtp_transport->SetRtcpPacketTransport(rtp_packet_transport); + } + if (config_.enable_external_auth) { + srtp_transport->EnableExternalAuth(); + } + return srtp_transport; +} + +std::unique_ptr +JsepTransportController::CreateDtlsSrtpTransport( + const std::string& transport_name, + cricket::DtlsTransportInternal* rtp_dtls_transport, + cricket::DtlsTransportInternal* rtcp_dtls_transport) { + RTC_DCHECK(network_thread_->IsCurrent()); + bool rtcp_mux_enabled = rtcp_dtls_transport == nullptr; + auto srtp_transport = + rtc::MakeUnique(rtcp_mux_enabled); + if (config_.enable_external_auth) { + srtp_transport->EnableExternalAuth(); + } + + auto dtls_srtp_transport = + rtc::MakeUnique(std::move(srtp_transport)); + + dtls_srtp_transport->SetDtlsTransports(rtp_dtls_transport, + rtcp_dtls_transport); + return dtls_srtp_transport; +} + +std::vector +JsepTransportController::GetDtlsTransports() { + std::vector dtls_transports; + for (auto it = jsep_transports_by_mid_.begin(); + it != jsep_transports_by_mid_.end(); ++it) { + auto jsep_transport = it->second.get(); + RTC_DCHECK(jsep_transport); + if (jsep_transport->rtp_dtls_transport()) { + dtls_transports.push_back(jsep_transport->rtp_dtls_transport()); + } + + if (jsep_transport->rtcp_dtls_transport()) { + dtls_transports.push_back(jsep_transport->rtcp_dtls_transport()); + } + } + return dtls_transports; +} + +void JsepTransportController::OnMessage(rtc::Message* pmsg) { + RTC_DCHECK(signaling_thread_->IsCurrent()); + + switch (pmsg->message_id) { + case MSG_ICECONNECTIONSTATE: { + rtc::TypedMessageData* data = + static_cast*>( + pmsg->pdata); + SignalIceConnectionState(data->data()); + delete data; + break; + } + case MSG_ICEGATHERINGSTATE: { + rtc::TypedMessageData* data = + static_cast*>( + pmsg->pdata); + SignalIceGatheringState(data->data()); + delete data; + break; + } + case MSG_ICECANDIDATESGATHERED: { + CandidatesData* data = static_cast(pmsg->pdata); + SignalIceCandidatesGathered(data->transport_name, data->candidates); + delete data; + break; + } + default: + RTC_NOTREACHED(); + } +} + +RTCError JsepTransportController::ApplyDescription_n( + bool local, + SdpType type, + const cricket::SessionDescription* description) { + RTC_DCHECK(network_thread_->IsCurrent()); + RTC_DCHECK(description); + + if (local) { + local_desc_ = description; + } else { + remote_desc_ = description; + } + + if (ShouldUpdateBundleGroup(type, description)) { + bundle_group_ = *description->GetGroupByName(cricket::GROUP_TYPE_BUNDLE); + } + + std::vector merged_encrypted_extension_ids; + if (bundle_group_) { + merged_encrypted_extension_ids = + MergeEncryptedHeaderExtensionIdsForBundle(description); + } + + for (const cricket::ContentInfo& content_info : description->contents()) { + // Don't create transports for rejected m-lines and bundled m-lines." + if (content_info.rejected || + (IsBundled(content_info.name) && content_info.name != *bundled_mid())) { + continue; + } + MaybeCreateJsepTransport(content_info.name, content_info); + } + + RTC_DCHECK(description->contents().size() == + description->transport_infos().size()); + for (size_t i = 0; i < description->contents().size(); ++i) { + const cricket::ContentInfo& content_info = description->contents()[i]; + const cricket::TransportInfo& transport_info = + description->transport_infos()[i]; + if (content_info.rejected) { + HandleRejectedContent(content_info); + continue; + } + + if (IsBundled(content_info.name) && content_info.name != *bundled_mid()) { + HandleBundledContent(content_info); + continue; + } + + std::vector extension_ids; + if (bundle_group_ && content_info.name == *bundled_mid()) { + extension_ids = merged_encrypted_extension_ids; + } else { + extension_ids = GetEncryptedHeaderExtensionIds(content_info); + } + + cricket::JsepTransport2* transport = GetJsepTransport(content_info.name); + RTC_DCHECK(transport); + + SetIceRole_n(DetermineIceRole(transport, transport_info, type, local)); + + RTCError error; + cricket::JsepTransportDescription jsep_description = + CreateJsepTransportDescription(content_info, transport_info, + extension_ids); + if (local) { + error = + transport->SetLocalJsepTransportDescription(jsep_description, type); + } else { + error = + transport->SetRemoteJsepTransportDescription(jsep_description, type); + } + + if (!error.ok()) { + LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER, + "Failed to apply the description for " + + content_info.name + ": " + error.message()); + } + } + return RTCError::OK(); +} + +void JsepTransportController::HandleRejectedContent( + const cricket::ContentInfo& content_info) { + // If the content is rejected, let the + // BaseChannel/SctpTransport change the RtpTransport/DtlsTransport first, + // then destroy the cricket::JsepTransport2. + if (content_info.type == cricket::MediaProtocolType::kRtp) { + SignalRtpTransportChanged(content_info.name, nullptr); + } else { + SignalDtlsTransportChanged(content_info.name, nullptr); + } + // Remove the rejected content from the |bundle_group_|. + if (IsBundled(content_info.name)) { + bundle_group_->RemoveContentName(content_info.name); + } + MaybeDestroyJsepTransport(content_info.name); +} + +void JsepTransportController::HandleBundledContent( + const cricket::ContentInfo& content_info) { + // If the content is bundled, let the + // BaseChannel/SctpTransport change the RtpTransport/DtlsTransport first, + // then destroy the cricket::JsepTransport2. + if (content_info.type == cricket::MediaProtocolType::kRtp) { + auto rtp_transport = + jsep_transports_by_mid_[*bundled_mid()]->rtp_transport(); + SignalRtpTransportChanged(content_info.name, rtp_transport); + } else { + auto dtls_transport = + jsep_transports_by_mid_[*bundled_mid()]->rtp_dtls_transport(); + SignalDtlsTransportChanged(content_info.name, dtls_transport); + } + MaybeDestroyJsepTransport(content_info.name); +} + +cricket::JsepTransportDescription +JsepTransportController::CreateJsepTransportDescription( + cricket::ContentInfo content_info, + cricket::TransportInfo transport_info, + const std::vector& encrypted_extension_ids) { + const cricket::MediaContentDescription* content_desc = + static_cast( + content_info.description); + RTC_DCHECK(content_desc); + bool rtcp_mux_enabled = content_info.type == cricket::MediaProtocolType::kSctp + ? true + : content_desc->rtcp_mux(); + + return cricket::JsepTransportDescription( + rtcp_mux_enabled, content_desc->cryptos(), encrypted_extension_ids, + transport_info.description); +} + +bool JsepTransportController::ShouldUpdateBundleGroup( + SdpType type, + const cricket::SessionDescription* description) { + if (config_.bundle_policy == + PeerConnectionInterface::kBundlePolicyMaxBundle) { + return true; + } + + if (type != SdpType::kAnswer) { + return false; + } + + RTC_DCHECK(local_desc_ && remote_desc_); + const cricket::ContentGroup* local_bundle = + local_desc_->GetGroupByName(cricket::GROUP_TYPE_BUNDLE); + const cricket::ContentGroup* remote_bundle = + remote_desc_->GetGroupByName(cricket::GROUP_TYPE_BUNDLE); + return local_bundle && remote_bundle; +} + +std::vector JsepTransportController::GetEncryptedHeaderExtensionIds( + const cricket::ContentInfo& content_info) { + const cricket::MediaContentDescription* content_desc = + static_cast( + content_info.description); + + if (!config_.crypto_options.enable_encrypted_rtp_header_extensions) { + return std::vector(); + } + + std::vector encrypted_header_extension_ids; + for (auto extension : content_desc->rtp_header_extensions()) { + if (!extension.encrypt) { + continue; + } + auto it = std::find(encrypted_header_extension_ids.begin(), + encrypted_header_extension_ids.end(), extension.id); + if (it == encrypted_header_extension_ids.end()) { + encrypted_header_extension_ids.push_back(extension.id); + } + } + return encrypted_header_extension_ids; +} + +std::vector +JsepTransportController::MergeEncryptedHeaderExtensionIdsForBundle( + const cricket::SessionDescription* description) { + RTC_DCHECK(description); + RTC_DCHECK(bundle_group_); + + std::vector merged_ids; + // Union the encrypted header IDs in the group when bundle is enabled. + for (const cricket::ContentInfo& content_info : description->contents()) { + if (bundle_group_->HasContentName(content_info.name)) { + std::vector extension_ids = + GetEncryptedHeaderExtensionIds(content_info); + for (int id : extension_ids) { + auto it = std::find(merged_ids.begin(), merged_ids.end(), id); + if (it == merged_ids.end()) { + merged_ids.push_back(id); + } + } + } + } + return merged_ids; +} + +const cricket::JsepTransport2* JsepTransportController::GetJsepTransport( + const std::string& mid) const { + auto target_mid = mid; + if (IsBundled(mid)) { + target_mid = *bundled_mid(); + } + auto it = jsep_transports_by_mid_.find(target_mid); + return (it == jsep_transports_by_mid_.end()) ? nullptr : it->second.get(); +} + +cricket::JsepTransport2* JsepTransportController::GetJsepTransport( + const std::string& mid) { + auto target_mid = mid; + if (IsBundled(mid)) { + target_mid = *bundled_mid(); + } + auto it = jsep_transports_by_mid_.find(target_mid); + return (it == jsep_transports_by_mid_.end()) ? nullptr : it->second.get(); +} + +void JsepTransportController::MaybeCreateJsepTransport( + const std::string& mid, + const cricket::ContentInfo& content_info) { + RTC_DCHECK(network_thread_->IsCurrent()); + + cricket::JsepTransport2* transport = GetJsepTransport(mid); + if (transport) { + return; + } + + const cricket::MediaContentDescription* content_desc = + static_cast( + content_info.description); + bool rtcp_mux_enabled = + content_desc->rtcp_mux() || + config_.rtcp_mux_policy == PeerConnectionInterface::kRtcpMuxPolicyRequire; + std::unique_ptr rtp_dtls_transport = + CreateDtlsTransport(mid, /*rtcp = */ false); + std::unique_ptr rtcp_dtls_transport; + if (!rtcp_mux_enabled) { + rtcp_dtls_transport = CreateDtlsTransport(mid, /*rtcp = */ true); + } + + std::unique_ptr unencrypted_rtp_transport; + std::unique_ptr sdes_transport; + std::unique_ptr dtls_srtp_transport; + if (config_.disable_encryption) { + unencrypted_rtp_transport = CreateUnencryptedRtpTransport( + mid, rtp_dtls_transport.get(), rtcp_dtls_transport.get()); + } else if (!content_desc->cryptos().empty()) { + sdes_transport = CreateSdesTransport(mid, rtp_dtls_transport.get(), + rtcp_dtls_transport.get()); + } else { + dtls_srtp_transport = CreateDtlsSrtpTransport(mid, rtp_dtls_transport.get(), + rtcp_dtls_transport.get()); + } + + std::unique_ptr jsep_transport = + rtc::MakeUnique( + mid, certificate_, std::move(unencrypted_rtp_transport), + std::move(sdes_transport), std::move(dtls_srtp_transport), + std::move(rtp_dtls_transport), std::move(rtcp_dtls_transport)); + jsep_transport->SignalRtcpMuxActive.connect( + this, &JsepTransportController::UpdateAggregateStates_n); + jsep_transports_by_mid_[mid] = std::move(jsep_transport); + UpdateAggregateStates_n(); +} + +void JsepTransportController::MaybeDestroyJsepTransport( + const std::string& mid) { + jsep_transports_by_mid_.erase(mid); + UpdateAggregateStates_n(); +} + +void JsepTransportController::DestroyAllJsepTransports_n() { + RTC_DCHECK(network_thread_->IsCurrent()); + jsep_transports_by_mid_.clear(); +} + +void JsepTransportController::SetIceRole_n(cricket::IceRole ice_role) { + RTC_DCHECK(network_thread_->IsCurrent()); + + ice_role_ = ice_role; + for (auto& dtls : GetDtlsTransports()) { + dtls->ice_transport()->SetIceRole(ice_role_); + } +} + +cricket::IceRole JsepTransportController::DetermineIceRole( + cricket::JsepTransport2* jsep_transport, + const cricket::TransportInfo& transport_info, + SdpType type, + bool local) { + cricket::IceRole ice_role = ice_role_; + auto tdesc = transport_info.description; + if (local) { + // The initial offer side may use ICE Lite, in which case, per RFC5245 + // Section 5.1.1, the answer side should take the controlling role if it is + // in the full ICE mode. + // + // When both sides use ICE Lite, the initial offer side must take the + // controlling role, and this is the default logic implemented in + // SetLocalDescription in JsepTransportController. + if (jsep_transport->remote_description() && + jsep_transport->remote_description()->transport_desc.ice_mode == + cricket::ICEMODE_LITE && + ice_role_ == cricket::ICEROLE_CONTROLLED && + tdesc.ice_mode == cricket::ICEMODE_FULL) { + ice_role = cricket::ICEROLE_CONTROLLING; + } + + // Older versions of Chrome expect the ICE role to be re-determined when an + // ICE restart occurs, and also don't perform conflict resolution correctly, + // so for now we can't safely stop doing this, unless the application opts + // in by setting |config_.redetermine_role_on_ice_restart_| to false. See: + // https://bugs.chromium.org/p/chromium/issues/detail?id=628676 + // TODO(deadbeef): Remove this when these old versions of Chrome reach a low + // enough population. + if (config_.redetermine_role_on_ice_restart && + jsep_transport->local_description() && + cricket::IceCredentialsChanged( + jsep_transport->local_description()->transport_desc.ice_ufrag, + jsep_transport->local_description()->transport_desc.ice_pwd, + tdesc.ice_ufrag, tdesc.ice_pwd) && + // Don't change the ICE role if the remote endpoint is ICE lite; we + // should always be controlling in that case. + (!jsep_transport->remote_description() || + jsep_transport->remote_description()->transport_desc.ice_mode != + cricket::ICEMODE_LITE)) { + ice_role = (type == SdpType::kOffer) ? cricket::ICEROLE_CONTROLLING + : cricket::ICEROLE_CONTROLLED; + } + } else { + // If our role is cricket::ICEROLE_CONTROLLED and the remote endpoint + // supports only ice_lite, this local endpoint should take the CONTROLLING + // role. + // TODO(deadbeef): This is a session-level attribute, so it really shouldn't + // be in a TransportDescription in the first place... + if (ice_role_ == cricket::ICEROLE_CONTROLLED && + tdesc.ice_mode == cricket::ICEMODE_LITE) { + ice_role = cricket::ICEROLE_CONTROLLING; + } + + // If we use ICE Lite and the remote endpoint uses the full implementation + // of ICE, the local endpoint must take the controlled role, and the other + // side must be the controlling role. + if (jsep_transport->local_description() && + jsep_transport->local_description()->transport_desc.ice_mode == + cricket::ICEMODE_LITE && + ice_role_ == cricket::ICEROLE_CONTROLLING && + tdesc.ice_mode == cricket::ICEMODE_LITE) { + ice_role = cricket::ICEROLE_CONTROLLED; + } + } + + return ice_role; +} + +void JsepTransportController::OnTransportWritableState_n( + rtc::PacketTransportInternal* transport) { + RTC_DCHECK(network_thread_->IsCurrent()); + RTC_LOG(LS_INFO) << " Transport " << transport->transport_name() + << " writability changed to " << transport->writable() + << "."; + UpdateAggregateStates_n(); +} + +void JsepTransportController::OnTransportReceivingState_n( + rtc::PacketTransportInternal* transport) { + RTC_DCHECK(network_thread_->IsCurrent()); + UpdateAggregateStates_n(); +} + +void JsepTransportController::OnTransportGatheringState_n( + cricket::IceTransportInternal* transport) { + RTC_DCHECK(network_thread_->IsCurrent()); + UpdateAggregateStates_n(); +} + +void JsepTransportController::OnTransportCandidateGathered_n( + cricket::IceTransportInternal* transport, + const cricket::Candidate& candidate) { + RTC_DCHECK(network_thread_->IsCurrent()); + + // We should never signal peer-reflexive candidates. + if (candidate.type() == cricket::PRFLX_PORT_TYPE) { + RTC_NOTREACHED(); + return; + } + std::vector candidates; + candidates.push_back(candidate); + CandidatesData* data = + new CandidatesData(transport->transport_name(), candidates); + signaling_thread_->Post(RTC_FROM_HERE, this, MSG_ICECANDIDATESGATHERED, data); +} + +void JsepTransportController::OnTransportCandidatesRemoved_n( + cricket::IceTransportInternal* transport, + const cricket::Candidates& candidates) { + invoker_.AsyncInvoke( + RTC_FROM_HERE, signaling_thread_, + rtc::Bind(&JsepTransportController::OnTransportCandidatesRemoved, this, + candidates)); +} + +void JsepTransportController::OnTransportCandidatesRemoved( + const cricket::Candidates& candidates) { + RTC_DCHECK(signaling_thread_->IsCurrent()); + SignalIceCandidatesRemoved(candidates); +} + +void JsepTransportController::OnTransportRoleConflict_n( + cricket::IceTransportInternal* transport) { + RTC_DCHECK(network_thread_->IsCurrent()); + // Note: since the role conflict is handled entirely on the network thread, + // we don't need to worry about role conflicts occurring on two ports at + // once. The first one encountered should immediately reverse the role. + cricket::IceRole reversed_role = (ice_role_ == cricket::ICEROLE_CONTROLLING) + ? cricket::ICEROLE_CONTROLLED + : cricket::ICEROLE_CONTROLLING; + RTC_LOG(LS_INFO) << "Got role conflict; switching to " + << (reversed_role == cricket::ICEROLE_CONTROLLING + ? "controlling" + : "controlled") + << " role."; + SetIceRole_n(reversed_role); +} + +void JsepTransportController::OnTransportStateChanged_n( + cricket::IceTransportInternal* transport) { + RTC_DCHECK(network_thread_->IsCurrent()); + RTC_LOG(LS_INFO) << transport->transport_name() << " Transport " + << transport->component() + << " state changed. Check if state is complete."; + UpdateAggregateStates_n(); +} + +void JsepTransportController::UpdateAggregateStates_n() { + RTC_DCHECK(network_thread_->IsCurrent()); + + auto dtls_transports = GetDtlsTransports(); + cricket::IceConnectionState new_connection_state = + cricket::kIceConnectionConnecting; + cricket::IceGatheringState new_gathering_state = cricket::kIceGatheringNew; + bool any_failed = false; + bool all_connected = !dtls_transports.empty(); + bool all_completed = !dtls_transports.empty(); + bool any_gathering = false; + bool all_done_gathering = !dtls_transports.empty(); + for (const auto& dtls : dtls_transports) { + any_failed = any_failed || dtls->ice_transport()->GetState() == + cricket::IceTransportState::STATE_FAILED; + all_connected = all_connected && dtls->writable(); + all_completed = + all_completed && dtls->writable() && + dtls->ice_transport()->GetState() == + cricket::IceTransportState::STATE_COMPLETED && + dtls->ice_transport()->GetIceRole() == cricket::ICEROLE_CONTROLLING && + dtls->ice_transport()->gathering_state() == + cricket::kIceGatheringComplete; + any_gathering = any_gathering || dtls->ice_transport()->gathering_state() != + cricket::kIceGatheringNew; + all_done_gathering = + all_done_gathering && dtls->ice_transport()->gathering_state() == + cricket::kIceGatheringComplete; + } + if (any_failed) { + new_connection_state = cricket::kIceConnectionFailed; + } else if (all_completed) { + new_connection_state = cricket::kIceConnectionCompleted; + } else if (all_connected) { + new_connection_state = cricket::kIceConnectionConnected; + } + if (ice_connection_state_ != new_connection_state) { + ice_connection_state_ = new_connection_state; + signaling_thread_->Post( + RTC_FROM_HERE, this, MSG_ICECONNECTIONSTATE, + new rtc::TypedMessageData( + new_connection_state)); + } + + if (all_done_gathering) { + new_gathering_state = cricket::kIceGatheringComplete; + } else if (any_gathering) { + new_gathering_state = cricket::kIceGatheringGathering; + } + if (ice_gathering_state_ != new_gathering_state) { + ice_gathering_state_ = new_gathering_state; + signaling_thread_->Post( + RTC_FROM_HERE, this, MSG_ICEGATHERINGSTATE, + new rtc::TypedMessageData( + new_gathering_state)); + } +} + +void JsepTransportController::OnDtlsHandshakeError( + rtc::SSLHandshakeError error) { + SignalDtlsHandshakeError(error); +} + +} // namespace webrtc diff --git a/pc/jseptransportcontroller.h b/pc/jseptransportcontroller.h new file mode 100644 index 0000000000..9755f4c9d7 --- /dev/null +++ b/pc/jseptransportcontroller.h @@ -0,0 +1,295 @@ +/* + * Copyright 2017 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. + */ + +#ifndef PC_JSEPTRANSPORTCONTROLLER_H_ +#define PC_JSEPTRANSPORTCONTROLLER_H_ + +#include +#include +#include +#include +#include + +#include "api/candidate.h" +#include "api/peerconnectioninterface.h" +#include "media/sctp/sctptransportinternal.h" +#include "p2p/base/dtlstransport.h" +#include "p2p/base/p2ptransportchannel.h" +#include "p2p/base/transportfactoryinterface.h" +#include "pc/channel.h" +#include "pc/dtlssrtptransport.h" +#include "pc/jseptransport2.h" +#include "pc/rtptransport.h" +#include "pc/srtptransport.h" +#include "rtc_base/asyncinvoker.h" +#include "rtc_base/constructormagic.h" +#include "rtc_base/refcountedobject.h" +#include "rtc_base/sigslot.h" +#include "rtc_base/sslstreamadapter.h" + +namespace rtc { +class Thread; +class PacketTransportInternal; +} // namespace rtc + +namespace webrtc { + +class JsepTransportController : public sigslot::has_slots<>, + public rtc::MessageHandler { + public: + struct Config { + // If |redetermine_role_on_ice_restart| is true, ICE role is redetermined + // upon setting a local transport description that indicates an ICE + // restart. + bool redetermine_role_on_ice_restart = true; + rtc::SSLProtocolVersion ssl_max_version = rtc::SSL_PROTOCOL_DTLS_12; + // |crypto_options| is used to determine if created DTLS transports + // negotiate GCM crypto suites or not. + rtc::CryptoOptions crypto_options; + PeerConnectionInterface::BundlePolicy bundle_policy = + PeerConnectionInterface::kBundlePolicyBalanced; + PeerConnectionInterface::RtcpMuxPolicy rtcp_mux_policy = + PeerConnectionInterface::kRtcpMuxPolicyRequire; + bool disable_encryption = false; + bool enable_external_auth = false; + // Used to inject the ICE/DTLS transports created externally. + cricket::TransportFactoryInterface* external_transport_factory = nullptr; + }; + + // The ICE related events are signaled on the |signaling_thread|. + // All the transport related methods are called on the |network_thread|. + JsepTransportController(rtc::Thread* signaling_thread, + rtc::Thread* network_thread, + cricket::PortAllocator* port_allocator, + Config config); + virtual ~JsepTransportController(); + + // The main method to be called; applies a description at the transport + // level, creating/destroying transport objects as needed and updating their + // properties. This includes RTP, DTLS, and ICE (but not SCTP). At least not + // yet? May make sense to in the future. + RTCError SetLocalDescription(SdpType type, + const cricket::SessionDescription* description); + + RTCError SetRemoteDescription(SdpType type, + const cricket::SessionDescription* description); + + // Get transports to be used for the provided |mid|. If bundling is enabled, + // calling GetRtpTransport for multiple MIDs may yield the same object. + RtpTransportInternal* GetRtpTransport(const std::string& mid) const; + cricket::DtlsTransportInternal* GetDtlsTransport( + const std::string& mid) const; + cricket::DtlsTransportInternal* GetRtcpDtlsTransport( + const std::string& mid) const; + + /********************* + * ICE-related methods + ********************/ + // This method is public to allow PeerConnection to update it from + // SetConfiguration. + void SetIceConfig(const cricket::IceConfig& config); + // Set the "needs-ice-restart" flag as described in JSEP. After the flag is + // set, offers should generate new ufrags/passwords until an ICE restart + // occurs. + void SetNeedsIceRestartFlag(); + // Returns true if the ICE restart flag above was set, and no ICE restart has + // occurred yet for this transport (by applying a local description with + // changed ufrag/password). If the transport has been deleted as a result of + // bundling, returns false. + bool NeedsIceRestart(const std::string& mid) const; + // Start gathering candidates for any new transports, or transports doing an + // ICE restart. + void MaybeStartGathering(); + RTCError AddRemoteCandidates( + const std::string& mid, + const std::vector& candidates); + RTCError RemoveRemoteCandidates( + const std::vector& candidates); + + /********************** + * DTLS-related methods + *********************/ + // Specifies the identity to use in this session. + // Can only be called once. + bool SetLocalCertificate( + const rtc::scoped_refptr& certificate); + rtc::scoped_refptr GetLocalCertificate( + const std::string& mid) const; + // Caller owns returned certificate. This method mainly exists for stats + // reporting. + std::unique_ptr GetRemoteSSLCertificate( + const std::string& mid) const; + // Get negotiated role, if one has been negotiated. + rtc::Optional GetDtlsRole(const std::string& mid) const; + + // TODO(deadbeef): GetStats isn't const because all the way down to + // OpenSSLStreamAdapter, GetSslCipherSuite and GetDtlsSrtpCryptoSuite are not + // const. Fix this. + bool GetStats(const std::string& mid, cricket::TransportStats* stats); + void SetMetricsObserver(webrtc::MetricsObserverInterface* metrics_observer); + + // All of these signals are fired on the signaling thread. + + // If any transport failed => failed, + // Else if all completed => completed, + // Else if all connected => connected, + // Else => connecting + sigslot::signal1 SignalIceConnectionState; + + // If all transports done gathering => complete, + // Else if any are gathering => gathering, + // Else => new + sigslot::signal1 SignalIceGatheringState; + + // (mid, candidates) + sigslot::signal2&> + SignalIceCandidatesGathered; + + sigslot::signal1&> + SignalIceCandidatesRemoved; + + sigslot::signal1 SignalDtlsHandshakeError; + + // This will be fired when BUNDLE is enabled, the PeerConnection will handle + // the signal and set the RtpTransport for the BaseChannel. + // The first argument is the MID and the second is the new RtpTransport. + // Before firing this signal, the previous RtpTransport must no longer be + // referenced. + sigslot::signal2 + SignalRtpTransportChanged; + + // SCTP version of the signal above. PeerConnection will set a new + // DtlsTransport for the SctpTransport. + sigslot::signal2 + SignalDtlsTransportChanged; + + private: + void OnMessage(rtc::Message* pmsg) override; + + RTCError ApplyDescription_n(bool local, + SdpType type, + const cricket::SessionDescription* description); + + void HandleRejectedContent(const cricket::ContentInfo& content_info); + void HandleBundledContent(const cricket::ContentInfo& content_info); + + cricket::JsepTransportDescription CreateJsepTransportDescription( + cricket::ContentInfo content_info, + cricket::TransportInfo transport_info, + const std::vector& encrypted_extension_ids); + + rtc::Optional bundled_mid() const { + rtc::Optional bundled_mid; + if (bundle_group_) { + bundled_mid = std::move(*(bundle_group_->FirstContentName())); + } + return bundled_mid; + } + + bool IsBundled(const std::string& mid) const { + return bundle_group_ && bundle_group_->HasContentName(mid); + } + + bool ShouldUpdateBundleGroup(SdpType type, + const cricket::SessionDescription* description); + + std::vector MergeEncryptedHeaderExtensionIdsForBundle( + const cricket::SessionDescription* description); + + std::vector GetEncryptedHeaderExtensionIds( + const cricket::ContentInfo& content_info); + + const cricket::JsepTransport2* GetJsepTransport(const std::string& mid) const; + cricket::JsepTransport2* GetJsepTransport(const std::string& mid); + + void MaybeCreateJsepTransport(const std::string& mid, + const cricket::ContentInfo& content_info); + void MaybeDestroyJsepTransport(const std::string& mid); + void DestroyAllJsepTransports_n(); + + void SetIceRole_n(cricket::IceRole ice_role); + + cricket::IceRole DetermineIceRole( + cricket::JsepTransport2* jsep_transport, + const cricket::TransportInfo& transport_info, + SdpType type, + bool local); + + std::unique_ptr CreateDtlsTransport( + const std::string& transport_name, + bool rtcp); + + std::unique_ptr CreateUnencryptedRtpTransport( + const std::string& transport_name, + rtc::PacketTransportInternal* rtp_packet_transport, + rtc::PacketTransportInternal* rtcp_packet_transport); + std::unique_ptr CreateSdesTransport( + const std::string& transport_name, + rtc::PacketTransportInternal* rtp_packet_transport, + rtc::PacketTransportInternal* rtcp_packet_transport); + std::unique_ptr CreateDtlsSrtpTransport( + const std::string& transport_name, + cricket::DtlsTransportInternal* rtp_dtls_transport, + cricket::DtlsTransportInternal* rtcp_dtls_transport); + + // Collect all the DtlsTransports, including RTP and RTCP, from the + // JsepTransports. JsepTransportController can iterate all the DtlsTransports + // and update the aggregate states. + std::vector GetDtlsTransports(); + + // Handlers for signals from Transport. + void OnTransportWritableState_n(rtc::PacketTransportInternal* transport); + void OnTransportReceivingState_n(rtc::PacketTransportInternal* transport); + void OnTransportGatheringState_n(cricket::IceTransportInternal* transport); + void OnTransportCandidateGathered_n(cricket::IceTransportInternal* transport, + const cricket::Candidate& candidate); + void OnTransportCandidatesRemoved(const cricket::Candidates& candidates); + void OnTransportCandidatesRemoved_n(cricket::IceTransportInternal* transport, + const cricket::Candidates& candidates); + void OnTransportRoleConflict_n(cricket::IceTransportInternal* transport); + void OnTransportStateChanged_n(cricket::IceTransportInternal* transport); + + void UpdateAggregateStates_n(); + + void OnDtlsHandshakeError(rtc::SSLHandshakeError error); + + rtc::Thread* const signaling_thread_ = nullptr; + rtc::Thread* const network_thread_ = nullptr; + cricket::PortAllocator* const port_allocator_ = nullptr; + + std::map> + jsep_transports_by_mid_; + + // Aggregate state for TransportChannelImpls. + cricket::IceConnectionState ice_connection_state_ = + cricket::kIceConnectionConnecting; + cricket::IceGatheringState ice_gathering_state_ = cricket::kIceGatheringNew; + + Config config_; + const cricket::SessionDescription* local_desc_ = nullptr; + const cricket::SessionDescription* remote_desc_ = nullptr; + rtc::Optional initial_offerer_; + + rtc::Optional bundle_group_; + + cricket::IceConfig ice_config_; + cricket::IceRole ice_role_ = cricket::ICEROLE_CONTROLLING; + uint64_t ice_tiebreaker_ = rtc::CreateRandomId64(); + rtc::scoped_refptr certificate_; + rtc::AsyncInvoker invoker_; + + webrtc::MetricsObserverInterface* metrics_observer_ = nullptr; + + RTC_DISALLOW_COPY_AND_ASSIGN(JsepTransportController); +}; + +} // namespace webrtc + +#endif // PC_JSEPTRANSPORTCONTROLLER_H_ diff --git a/pc/jseptransportcontroller_unittest.cc b/pc/jseptransportcontroller_unittest.cc new file mode 100644 index 0000000000..887c4beacf --- /dev/null +++ b/pc/jseptransportcontroller_unittest.cc @@ -0,0 +1,1210 @@ +/* + * Copyright 2018 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 +#include + +#include "p2p/base/fakedtlstransport.h" +#include "p2p/base/fakeicetransport.h" +#include "p2p/base/transportfactoryinterface.h" +#include "p2p/base/transportinfo.h" +#include "pc/jseptransportcontroller.h" +#include "rtc_base/gunit.h" +#include "rtc_base/ptr_util.h" +#include "rtc_base/thread.h" +#include "test/gtest.h" + +using cricket::FakeDtlsTransport; +using cricket::Candidate; +using cricket::Candidates; +using webrtc::SdpType; + +static const int kTimeout = 100; +static const char kIceUfrag1[] = "u0001"; +static const char kIcePwd1[] = "TESTICEPWD00000000000001"; +static const char kIceUfrag2[] = "u0002"; +static const char kIcePwd2[] = "TESTICEPWD00000000000002"; +static const char kIceUfrag3[] = "u0003"; +static const char kIcePwd3[] = "TESTICEPWD00000000000003"; +static const char kAudioMid1[] = "audio1"; +static const char kAudioMid2[] = "audio2"; +static const char kVideoMid1[] = "video1"; +static const char kVideoMid2[] = "video2"; +static const char kDataMid1[] = "data1"; + +namespace webrtc { + +class FakeTransportFactory : public cricket::TransportFactoryInterface { + public: + std::unique_ptr CreateIceTransport( + const std::string& transport_name, + int component) override { + return rtc::MakeUnique(transport_name, + component); + } + + std::unique_ptr CreateDtlsTransport( + std::unique_ptr ice, + const rtc::CryptoOptions& crypto_options) override { + std::unique_ptr fake_ice( + static_cast(ice.release())); + return rtc::MakeUnique(std::move(fake_ice)); + } +}; + +class JsepTransportControllerTest : public testing::Test, + public sigslot::has_slots<> { + public: + JsepTransportControllerTest() : signaling_thread_(rtc::Thread::Current()) { + fake_transport_factory_ = rtc::MakeUnique(); + } + + void CreateJsepTransportController( + JsepTransportController::Config config, + rtc::Thread* signaling_thread = rtc::Thread::Current(), + rtc::Thread* network_thread = rtc::Thread::Current(), + cricket::PortAllocator* port_allocator = nullptr) { + // The tests only works with |fake_transport_factory|; + config.external_transport_factory = fake_transport_factory_.get(); + transport_controller_ = rtc::MakeUnique( + signaling_thread, network_thread, port_allocator, config); + ConnectTransportControllerSignals(); + } + + void ConnectTransportControllerSignals() { + transport_controller_->SignalIceConnectionState.connect( + this, &JsepTransportControllerTest::OnConnectionState); + transport_controller_->SignalIceGatheringState.connect( + this, &JsepTransportControllerTest::OnGatheringState); + transport_controller_->SignalIceCandidatesGathered.connect( + this, &JsepTransportControllerTest::OnCandidatesGathered); + transport_controller_->SignalRtpTransportChanged.connect( + this, &JsepTransportControllerTest::OnRtpTransportChanged); + transport_controller_->SignalDtlsTransportChanged.connect( + this, &JsepTransportControllerTest::OnDtlsTransportChanged); + } + + std::unique_ptr + CreateSessionDescriptionWithoutBundle() { + auto description = rtc::MakeUnique(); + AddAudioSection(description.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(description.get(), kVideoMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + return description; + } + + std::unique_ptr + CreateSessionDescriptionWithBundleGroup() { + auto description = CreateSessionDescriptionWithoutBundle(); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kAudioMid1); + bundle_group.AddContentName(kVideoMid1); + description->AddGroup(bundle_group); + + return description; + } + + void AddAudioSection(cricket::SessionDescription* description, + const std::string& mid, + const std::string& ufrag, + const std::string& pwd, + cricket::IceMode ice_mode, + cricket::ConnectionRole conn_role, + rtc::scoped_refptr cert) { + std::unique_ptr audio( + new cricket::AudioContentDescription()); + description->AddContent(mid, cricket::MediaProtocolType::kRtp, + /*rejected=*/false, audio.release()); + AddTransportInfo(description, mid, ufrag, pwd, ice_mode, conn_role, cert); + } + + void AddVideoSection(cricket::SessionDescription* description, + const std::string& mid, + const std::string& ufrag, + const std::string& pwd, + cricket::IceMode ice_mode, + cricket::ConnectionRole conn_role, + rtc::scoped_refptr cert) { + std::unique_ptr video( + new cricket::VideoContentDescription()); + description->AddContent(mid, cricket::MediaProtocolType::kRtp, + /*rejected=*/false, video.release()); + AddTransportInfo(description, mid, ufrag, pwd, ice_mode, conn_role, cert); + } + + void AddDataSection(cricket::SessionDescription* description, + const std::string& mid, + cricket::MediaProtocolType protocol_type, + const std::string& ufrag, + const std::string& pwd, + cricket::IceMode ice_mode, + cricket::ConnectionRole conn_role, + rtc::scoped_refptr cert) { + std::unique_ptr data( + new cricket::DataContentDescription()); + description->AddContent(mid, protocol_type, + /*rejected=*/false, data.release()); + AddTransportInfo(description, mid, ufrag, pwd, ice_mode, conn_role, cert); + } + + void AddTransportInfo(cricket::SessionDescription* description, + const std::string& mid, + const std::string& ufrag, + const std::string& pwd, + cricket::IceMode ice_mode, + cricket::ConnectionRole conn_role, + rtc::scoped_refptr cert) { + std::unique_ptr fingerprint; + if (cert) { + fingerprint.reset(rtc::SSLFingerprint::CreateFromCertificate(cert)); + } + + cricket::TransportDescription transport_desc(std::vector(), + ufrag, pwd, ice_mode, + conn_role, fingerprint.get()); + description->AddTransportInfo(cricket::TransportInfo(mid, transport_desc)); + } + + cricket::IceConfig CreateIceConfig( + int receiving_timeout, + cricket::ContinualGatheringPolicy continual_gathering_policy) { + cricket::IceConfig config; + config.receiving_timeout = receiving_timeout; + config.continual_gathering_policy = continual_gathering_policy; + return config; + } + + Candidate CreateCandidate(const std::string& transport_name, int component) { + Candidate c; + c.set_transport_name(transport_name); + c.set_address(rtc::SocketAddress("192.168.1.1", 8000)); + c.set_component(component); + c.set_protocol(cricket::UDP_PROTOCOL_NAME); + c.set_priority(1); + return c; + } + + void CreateLocalDescriptionAndCompleteConnectionOnNetworkThread() { + if (!network_thread_->IsCurrent()) { + network_thread_->Invoke(RTC_FROM_HERE, [&] { + CreateLocalDescriptionAndCompleteConnectionOnNetworkThread(); + }); + return; + } + + auto description = CreateSessionDescriptionWithBundleGroup(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + transport_controller_->MaybeStartGathering(); + auto fake_audio_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + auto fake_video_dtls = static_cast( + transport_controller_->GetDtlsTransport(kVideoMid1)); + fake_audio_dtls->fake_ice_transport()->SignalCandidateGathered( + fake_audio_dtls->fake_ice_transport(), + CreateCandidate(kAudioMid1, /*component=*/1)); + fake_video_dtls->fake_ice_transport()->SignalCandidateGathered( + fake_video_dtls->fake_ice_transport(), + CreateCandidate(kVideoMid1, /*component=*/1)); + fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + fake_audio_dtls->fake_ice_transport()->SetConnectionCount(2); + fake_video_dtls->fake_ice_transport()->SetConnectionCount(2); + fake_audio_dtls->SetReceiving(true); + fake_video_dtls->SetReceiving(true); + fake_audio_dtls->SetWritable(true); + fake_video_dtls->SetWritable(true); + fake_audio_dtls->fake_ice_transport()->SetConnectionCount(1); + fake_video_dtls->fake_ice_transport()->SetConnectionCount(1); + } + + protected: + void OnConnectionState(cricket::IceConnectionState state) { + if (!signaling_thread_->IsCurrent()) { + signaled_on_non_signaling_thread_ = true; + } + connection_state_ = state; + ++connection_state_signal_count_; + } + + void OnGatheringState(cricket::IceGatheringState state) { + if (!signaling_thread_->IsCurrent()) { + signaled_on_non_signaling_thread_ = true; + } + gathering_state_ = state; + ++gathering_state_signal_count_; + } + + void OnCandidatesGathered(const std::string& transport_name, + const Candidates& candidates) { + if (!signaling_thread_->IsCurrent()) { + signaled_on_non_signaling_thread_ = true; + } + candidates_[transport_name].insert(candidates_[transport_name].end(), + candidates.begin(), candidates.end()); + ++candidates_signal_count_; + } + + void OnRtpTransportChanged(const std::string& mid, + RtpTransportInternal* rtp_transport) { + changed_rtp_transport_by_mid_[mid] = rtp_transport; + } + + void OnDtlsTransportChanged(const std::string& mid, + cricket::DtlsTransportInternal* dtls_transport) { + changed_dtls_transport_by_mid_[mid] = dtls_transport; + } + + // Information received from signals from transport controller. + cricket::IceConnectionState connection_state_ = + cricket::kIceConnectionConnecting; + bool receiving_ = false; + cricket::IceGatheringState gathering_state_ = cricket::kIceGatheringNew; + // transport_name => candidates + std::map candidates_; + // Counts of each signal emitted. + int connection_state_signal_count_ = 0; + int receiving_signal_count_ = 0; + int gathering_state_signal_count_ = 0; + int candidates_signal_count_ = 0; + + // |network_thread_| should be destroyed after |transport_controller_| + std::unique_ptr network_thread_; + std::unique_ptr transport_controller_; + std::unique_ptr fake_transport_factory_; + rtc::Thread* const signaling_thread_ = nullptr; + bool signaled_on_non_signaling_thread_ = false; + // Used to verify the SignalRtpTransportChanged/SignalDtlsTransportChanged are + // signaled correctly. + std::map changed_rtp_transport_by_mid_; + std::map + changed_dtls_transport_by_mid_; +}; + +TEST_F(JsepTransportControllerTest, GetRtpTransport) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + auto audio_rtp_transport = transport_controller_->GetRtpTransport(kAudioMid1); + auto video_rtp_transport = transport_controller_->GetRtpTransport(kVideoMid1); + EXPECT_NE(nullptr, audio_rtp_transport); + EXPECT_NE(nullptr, video_rtp_transport); + EXPECT_NE(audio_rtp_transport, video_rtp_transport); + // Return nullptr for non-existing ones. + EXPECT_EQ(nullptr, transport_controller_->GetRtpTransport(kAudioMid2)); +} + +TEST_F(JsepTransportControllerTest, GetDtlsTransport) { + JsepTransportController::Config config; + config.rtcp_mux_policy = PeerConnectionInterface::kRtcpMuxPolicyNegotiate; + CreateJsepTransportController(config); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + EXPECT_NE(nullptr, transport_controller_->GetDtlsTransport(kAudioMid1)); + EXPECT_NE(nullptr, transport_controller_->GetRtcpDtlsTransport(kAudioMid1)); + EXPECT_NE(nullptr, transport_controller_->GetDtlsTransport(kVideoMid1)); + EXPECT_NE(nullptr, transport_controller_->GetRtcpDtlsTransport(kVideoMid1)); + // Return nullptr for non-existing ones. + EXPECT_EQ(nullptr, transport_controller_->GetDtlsTransport(kVideoMid2)); + EXPECT_EQ(nullptr, transport_controller_->GetRtcpDtlsTransport(kVideoMid2)); +} + +TEST_F(JsepTransportControllerTest, SetIceConfig) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + transport_controller_->SetIceConfig( + CreateIceConfig(kTimeout, cricket::GATHER_CONTINUALLY)); + FakeDtlsTransport* fake_audio_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + ASSERT_NE(nullptr, fake_audio_dtls); + EXPECT_EQ(kTimeout, + fake_audio_dtls->fake_ice_transport()->receiving_timeout()); + EXPECT_TRUE(fake_audio_dtls->fake_ice_transport()->gather_continually()); + + // Test that value stored in controller is applied to new transports. + AddAudioSection(description.get(), kAudioMid2, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + fake_audio_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid2)); + ASSERT_NE(nullptr, fake_audio_dtls); + EXPECT_EQ(kTimeout, + fake_audio_dtls->fake_ice_transport()->receiving_timeout()); + EXPECT_TRUE(fake_audio_dtls->fake_ice_transport()->gather_continually()); +} + +// Tests the getter and setter of the ICE restart flag. +TEST_F(JsepTransportControllerTest, NeedIceRestart) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, description.get()) + .ok()); + + // Initially NeedsIceRestart should return false. + EXPECT_FALSE(transport_controller_->NeedsIceRestart(kAudioMid1)); + EXPECT_FALSE(transport_controller_->NeedsIceRestart(kVideoMid1)); + // Set the needs-ice-restart flag and verify NeedsIceRestart starts returning + // true. + transport_controller_->SetNeedsIceRestartFlag(); + EXPECT_TRUE(transport_controller_->NeedsIceRestart(kAudioMid1)); + EXPECT_TRUE(transport_controller_->NeedsIceRestart(kVideoMid1)); + // For a nonexistent transport, false should be returned. + EXPECT_FALSE(transport_controller_->NeedsIceRestart(kVideoMid2)); + + // Reset the ice_ufrag/ice_pwd for audio. + auto audio_transport_info = description->GetTransportInfoByName(kAudioMid1); + audio_transport_info->description.ice_ufrag = kIceUfrag2; + audio_transport_info->description.ice_pwd = kIcePwd2; + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + // Because the ICE is only restarted for audio, NeedsIceRestart is expected to + // return false for audio and true for video. + EXPECT_FALSE(transport_controller_->NeedsIceRestart(kAudioMid1)); + EXPECT_TRUE(transport_controller_->NeedsIceRestart(kVideoMid1)); +} + +TEST_F(JsepTransportControllerTest, MaybeStartGathering) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithBundleGroup(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + // After setting the local description, we should be able to start gathering + // candidates. + transport_controller_->MaybeStartGathering(); + EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout); + EXPECT_EQ(1, gathering_state_signal_count_); +} + +TEST_F(JsepTransportControllerTest, AddRemoveRemoteCandidates) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + transport_controller_->SetLocalDescription(SdpType::kOffer, + description.get()); + transport_controller_->SetRemoteDescription(SdpType::kAnswer, + description.get()); + auto fake_audio_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + ASSERT_NE(nullptr, fake_audio_dtls); + Candidates candidates; + candidates.push_back( + CreateCandidate(kAudioMid1, cricket::ICE_CANDIDATE_COMPONENT_RTP)); + EXPECT_TRUE( + transport_controller_->AddRemoteCandidates(kAudioMid1, candidates).ok()); + EXPECT_EQ(1U, + fake_audio_dtls->fake_ice_transport()->remote_candidates().size()); + + EXPECT_TRUE(transport_controller_->RemoveRemoteCandidates(candidates).ok()); + EXPECT_EQ(0U, + fake_audio_dtls->fake_ice_transport()->remote_candidates().size()); +} + +TEST_F(JsepTransportControllerTest, SetAndGetLocalCertificate) { + CreateJsepTransportController(JsepTransportController::Config()); + + rtc::scoped_refptr certificate1 = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("session1", rtc::KT_DEFAULT))); + rtc::scoped_refptr returned_certificate; + + auto description = rtc::MakeUnique(); + AddAudioSection(description.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + certificate1); + + // Apply the local certificate. + EXPECT_TRUE(transport_controller_->SetLocalCertificate(certificate1)); + // Apply the local description. + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + returned_certificate = transport_controller_->GetLocalCertificate(kAudioMid1); + EXPECT_TRUE(returned_certificate); + EXPECT_EQ(certificate1->identity()->certificate().ToPEMString(), + returned_certificate->identity()->certificate().ToPEMString()); + + // Should fail if called for a nonexistant transport. + EXPECT_EQ(nullptr, transport_controller_->GetLocalCertificate(kVideoMid1)); + + // Shouldn't be able to change the identity once set. + rtc::scoped_refptr certificate2 = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("session2", rtc::KT_DEFAULT))); + EXPECT_FALSE(transport_controller_->SetLocalCertificate(certificate2)); +} + +TEST_F(JsepTransportControllerTest, GetRemoteSSLCertificate) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithBundleGroup(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + rtc::FakeSSLCertificate fake_certificate("fake_data"); + + auto fake_audio_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + fake_audio_dtls->SetRemoteSSLCertificate(&fake_certificate); + std::unique_ptr returned_certificate = + transport_controller_->GetRemoteSSLCertificate(kAudioMid1); + EXPECT_TRUE(returned_certificate); + EXPECT_EQ(fake_certificate.ToPEMString(), + returned_certificate->ToPEMString()); + + // Should fail if called for a nonexistant transport. + EXPECT_FALSE(transport_controller_->GetRemoteSSLCertificate(kAudioMid2)); +} + +TEST_F(JsepTransportControllerTest, GetDtlsRole) { + CreateJsepTransportController(JsepTransportController::Config()); + auto offer_certificate = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("offer", rtc::KT_DEFAULT))); + auto answer_certificate = + rtc::RTCCertificate::Create(std::unique_ptr( + rtc::SSLIdentity::Generate("answer", rtc::KT_DEFAULT))); + transport_controller_->SetLocalCertificate(offer_certificate); + + auto offer_desc = rtc::MakeUnique(); + AddAudioSection(offer_desc.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + offer_certificate); + auto answer_desc = rtc::MakeUnique(); + AddAudioSection(answer_desc.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + answer_certificate); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, offer_desc.get()) + .ok()); + + rtc::Optional role = + transport_controller_->GetDtlsRole(kAudioMid1); + // The DTLS role is not decided yet. + EXPECT_FALSE(role); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, answer_desc.get()) + .ok()); + role = transport_controller_->GetDtlsRole(kAudioMid1); + + ASSERT_TRUE(role); + EXPECT_EQ(rtc::SSL_CLIENT, *role); +} + +TEST_F(JsepTransportControllerTest, GetStats) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithBundleGroup(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + cricket::TransportStats stats; + EXPECT_TRUE(transport_controller_->GetStats(kAudioMid1, &stats)); + EXPECT_EQ(kAudioMid1, stats.transport_name); + EXPECT_EQ(1u, stats.channel_stats.size()); + // Return false for non-existing transport. + EXPECT_FALSE(transport_controller_->GetStats(kAudioMid2, &stats)); +} + +TEST_F(JsepTransportControllerTest, SignalConnectionStateFailed) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + auto fake_ice = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)->ice_transport()); + fake_ice->SetCandidatesGatheringComplete(); + fake_ice->SetConnectionCount(1); + // The connection stats will be failed if there is no active connection. + fake_ice->SetConnectionCount(0); + EXPECT_EQ_WAIT(cricket::kIceConnectionFailed, connection_state_, kTimeout); + EXPECT_EQ(1, connection_state_signal_count_); +} + +TEST_F(JsepTransportControllerTest, SignalConnectionStateConnected) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + auto fake_audio_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + auto fake_video_dtls = static_cast( + transport_controller_->GetDtlsTransport(kVideoMid1)); + + // First, have one transport connect, and another fail, to ensure that + // the first transport connecting didn't trigger a "connected" state signal. + // We should only get a signal when all are connected. + fake_audio_dtls->fake_ice_transport()->SetConnectionCount(1); + fake_audio_dtls->SetWritable(true); + fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + // Decrease the number of the connection to trigger the signal. + fake_video_dtls->fake_ice_transport()->SetConnectionCount(1); + fake_video_dtls->fake_ice_transport()->SetConnectionCount(0); + fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + + EXPECT_EQ_WAIT(cricket::kIceConnectionFailed, connection_state_, kTimeout); + EXPECT_EQ(1, connection_state_signal_count_); + + // Set the connection count to be 2 and the cricket::FakeIceTransport will set + // the transport state to be STATE_CONNECTING. + fake_video_dtls->fake_ice_transport()->SetConnectionCount(2); + fake_video_dtls->SetWritable(true); + EXPECT_EQ_WAIT(cricket::kIceConnectionConnected, connection_state_, kTimeout); + EXPECT_EQ(2, connection_state_signal_count_); +} + +TEST_F(JsepTransportControllerTest, SignalConnectionStateComplete) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + auto fake_audio_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + auto fake_video_dtls = static_cast( + transport_controller_->GetDtlsTransport(kVideoMid1)); + + // First, have one transport connect, and another fail, to ensure that + // the first transport connecting didn't trigger a "connected" state signal. + // We should only get a signal when all are connected. + fake_audio_dtls->fake_ice_transport()->SetConnectionCount(1); + fake_audio_dtls->SetWritable(true); + fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + // Decrease the number of the connection to trigger the signal. + fake_video_dtls->fake_ice_transport()->SetConnectionCount(1); + fake_video_dtls->fake_ice_transport()->SetConnectionCount(0); + fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + + EXPECT_EQ_WAIT(cricket::kIceConnectionFailed, connection_state_, kTimeout); + EXPECT_EQ(1, connection_state_signal_count_); + + // Set the connection count to be 1 and the cricket::FakeIceTransport will set + // the transport state to be STATE_COMPLETED. + fake_video_dtls->fake_ice_transport()->SetConnectionCount(1); + fake_video_dtls->SetWritable(true); + EXPECT_EQ_WAIT(cricket::kIceConnectionCompleted, connection_state_, kTimeout); + EXPECT_EQ(2, connection_state_signal_count_); +} + +TEST_F(JsepTransportControllerTest, SignalIceGatheringStateGathering) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + auto fake_audio_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + fake_audio_dtls->fake_ice_transport()->MaybeStartGathering(); + // Should be in the gathering state as soon as any transport starts gathering. + EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout); + EXPECT_EQ(1, gathering_state_signal_count_); +} + +TEST_F(JsepTransportControllerTest, SignalIceGatheringStateComplete) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + auto fake_audio_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + auto fake_video_dtls = static_cast( + transport_controller_->GetDtlsTransport(kVideoMid1)); + + fake_audio_dtls->fake_ice_transport()->MaybeStartGathering(); + EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout); + EXPECT_EQ(1, gathering_state_signal_count_); + + // Have one transport finish gathering, to make sure gathering + // completion wasn't signalled if only one transport finished gathering. + fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + EXPECT_EQ(1, gathering_state_signal_count_); + + fake_video_dtls->fake_ice_transport()->MaybeStartGathering(); + EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout); + EXPECT_EQ(1, gathering_state_signal_count_); + + fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + EXPECT_EQ_WAIT(cricket::kIceGatheringComplete, gathering_state_, kTimeout); + EXPECT_EQ(2, gathering_state_signal_count_); +} + +// Test that when the last transport that hasn't finished connecting and/or +// gathering is destroyed, the aggregate state jumps to "completed". This can +// happen if, for example, we have an audio and video transport, the audio +// transport completes, then we start bundling video on the audio transport. +TEST_F(JsepTransportControllerTest, + SignalingWhenLastIncompleteTransportDestroyed) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithBundleGroup(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + auto fake_audio_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + auto fake_video_dtls = static_cast( + transport_controller_->GetDtlsTransport(kVideoMid1)); + EXPECT_NE(fake_audio_dtls, fake_video_dtls); + + fake_audio_dtls->fake_ice_transport()->MaybeStartGathering(); + EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout); + EXPECT_EQ(1, gathering_state_signal_count_); + + // Let the audio transport complete. + fake_audio_dtls->SetWritable(true); + fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + fake_audio_dtls->fake_ice_transport()->SetConnectionCount(1); + EXPECT_EQ(1, gathering_state_signal_count_); + + // Set the remote description and enable the bundle. + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, description.get()) + .ok()); + // The BUNDLE should be enabled, the incomplete video transport should be + // deleted and the states shoud be updated. + fake_video_dtls = static_cast( + transport_controller_->GetDtlsTransport(kVideoMid1)); + EXPECT_EQ(fake_audio_dtls, fake_video_dtls); + EXPECT_EQ_WAIT(cricket::kIceConnectionCompleted, connection_state_, kTimeout); + EXPECT_EQ_WAIT(cricket::kIceGatheringComplete, gathering_state_, kTimeout); + EXPECT_EQ(2, gathering_state_signal_count_); +} + +TEST_F(JsepTransportControllerTest, SignalCandidatesGathered) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithBundleGroup(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + transport_controller_->MaybeStartGathering(); + + auto fake_audio_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + fake_audio_dtls->fake_ice_transport()->SignalCandidateGathered( + fake_audio_dtls->fake_ice_transport(), CreateCandidate(kAudioMid1, 1)); + EXPECT_EQ_WAIT(1, candidates_signal_count_, kTimeout); + EXPECT_EQ(1u, candidates_[kAudioMid1].size()); +} + +TEST_F(JsepTransportControllerTest, IceSignalingOccursOnSignalingThread) { + network_thread_ = rtc::Thread::CreateWithSocketServer(); + network_thread_->Start(); + CreateJsepTransportController(JsepTransportController::Config(), + signaling_thread_, network_thread_.get(), + /*PortAllocator=*/nullptr); + CreateLocalDescriptionAndCompleteConnectionOnNetworkThread(); + + // connecting --> connected --> completed + EXPECT_EQ_WAIT(cricket::kIceConnectionCompleted, connection_state_, kTimeout); + EXPECT_EQ(2, connection_state_signal_count_); + + // new --> gathering --> complete + EXPECT_EQ_WAIT(cricket::kIceGatheringComplete, gathering_state_, kTimeout); + EXPECT_EQ(2, gathering_state_signal_count_); + + EXPECT_EQ_WAIT(1u, candidates_[kAudioMid1].size(), kTimeout); + EXPECT_EQ_WAIT(1u, candidates_[kVideoMid1].size(), kTimeout); + EXPECT_EQ(2, candidates_signal_count_); + + EXPECT_TRUE(!signaled_on_non_signaling_thread_); +} + +// Older versions of Chrome expect the ICE role to be re-determined when an +// ICE restart occurs, and also don't perform conflict resolution correctly, +// so for now we can't safely stop doing this. +// See: https://bugs.chromium.org/p/chromium/issues/detail?id=628676 +// TODO(deadbeef): Remove this when these old versions of Chrome reach a low +// enough population. +TEST_F(JsepTransportControllerTest, IceRoleRedeterminedOnIceRestartByDefault) { + CreateJsepTransportController(JsepTransportController::Config()); + // Let the |transport_controller_| be the controlled side initially. + auto remote_offer = rtc::MakeUnique(); + AddAudioSection(remote_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + auto local_answer = rtc::MakeUnique(); + AddAudioSection(local_answer.get(), kAudioMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kOffer, remote_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kAnswer, local_answer.get()) + .ok()); + + auto fake_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + EXPECT_EQ(cricket::ICEROLE_CONTROLLED, + fake_dtls->fake_ice_transport()->GetIceRole()); + + // New offer will trigger the ICE restart. + auto restart_local_offer = rtc::MakeUnique(); + AddAudioSection(restart_local_offer.get(), kAudioMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + EXPECT_TRUE( + transport_controller_ + ->SetLocalDescription(SdpType::kOffer, restart_local_offer.get()) + .ok()); + EXPECT_EQ(cricket::ICEROLE_CONTROLLING, + fake_dtls->fake_ice_transport()->GetIceRole()); +} + +// Test that if the TransportController was created with the +// |redetermine_role_on_ice_restart| parameter set to false, the role is *not* +// redetermined on an ICE restart. +TEST_F(JsepTransportControllerTest, IceRoleNotRedetermined) { + JsepTransportController::Config config; + config.redetermine_role_on_ice_restart = false; + + CreateJsepTransportController(config); + // Let the |transport_controller_| be the controlled side initially. + auto remote_offer = rtc::MakeUnique(); + AddAudioSection(remote_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + auto local_answer = rtc::MakeUnique(); + AddAudioSection(local_answer.get(), kAudioMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kOffer, remote_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kAnswer, local_answer.get()) + .ok()); + + auto fake_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + EXPECT_EQ(cricket::ICEROLE_CONTROLLED, + fake_dtls->fake_ice_transport()->GetIceRole()); + + // New offer will trigger the ICE restart. + auto restart_local_offer = rtc::MakeUnique(); + AddAudioSection(restart_local_offer.get(), kAudioMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + EXPECT_TRUE( + transport_controller_ + ->SetLocalDescription(SdpType::kOffer, restart_local_offer.get()) + .ok()); + EXPECT_EQ(cricket::ICEROLE_CONTROLLED, + fake_dtls->fake_ice_transport()->GetIceRole()); +} + +// Tests ICE-Lite mode in remote answer. +TEST_F(JsepTransportControllerTest, SetIceRoleWhenIceLiteInRemoteAnswer) { + CreateJsepTransportController(JsepTransportController::Config()); + auto local_offer = rtc::MakeUnique(); + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + auto fake_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + EXPECT_EQ(cricket::ICEROLE_CONTROLLING, + fake_dtls->fake_ice_transport()->GetIceRole()); + EXPECT_EQ(cricket::ICEMODE_FULL, + fake_dtls->fake_ice_transport()->remote_ice_mode()); + + auto remote_answer = rtc::MakeUnique(); + AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_LITE, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + EXPECT_EQ(cricket::ICEROLE_CONTROLLING, + fake_dtls->fake_ice_transport()->GetIceRole()); + EXPECT_EQ(cricket::ICEMODE_LITE, + fake_dtls->fake_ice_transport()->remote_ice_mode()); +} + +// Tests that the ICE role remains "controlling" if a subsequent offer that +// does an ICE restart is received from an ICE lite endpoint. Regression test +// for: https://crbug.com/710760 +TEST_F(JsepTransportControllerTest, + IceRoleIsControllingAfterIceRestartFromIceLiteEndpoint) { + CreateJsepTransportController(JsepTransportController::Config()); + auto remote_offer = rtc::MakeUnique(); + AddAudioSection(remote_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_LITE, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + auto local_answer = rtc::MakeUnique(); + AddAudioSection(local_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + // Initial Offer/Answer exchange. If the remote offerer is ICE-Lite, then the + // local side is the controlling. + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kOffer, remote_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kAnswer, local_answer.get()) + .ok()); + auto fake_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + EXPECT_EQ(cricket::ICEROLE_CONTROLLING, + fake_dtls->fake_ice_transport()->GetIceRole()); + + // In the subsequence remote offer triggers an ICE restart. + auto remote_offer2 = rtc::MakeUnique(); + AddAudioSection(remote_offer2.get(), kAudioMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_LITE, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + auto local_answer2 = rtc::MakeUnique(); + AddAudioSection(local_answer2.get(), kAudioMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kOffer, remote_offer2.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kAnswer, local_answer2.get()) + .ok()); + fake_dtls = static_cast( + transport_controller_->GetDtlsTransport(kAudioMid1)); + // The local side is still the controlling role since the remote side is using + // ICE-Lite. + EXPECT_EQ(cricket::ICEROLE_CONTROLLING, + fake_dtls->fake_ice_transport()->GetIceRole()); +} + +// Tests that the SDP has more than one audio/video m= sections. +TEST_F(JsepTransportControllerTest, MultipleMediaSectionsOfSameTypeWithBundle) { + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kAudioMid1); + bundle_group.AddContentName(kAudioMid2); + bundle_group.AddContentName(kVideoMid1); + bundle_group.AddContentName(kDataMid1); + + auto local_offer = rtc::MakeUnique(); + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kAudioMid2, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddDataSection(local_offer.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + + auto remote_answer = rtc::MakeUnique(); + AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddAudioSection(remote_answer.get(), kAudioMid2, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddDataSection(remote_answer.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + + local_offer->AddGroup(bundle_group); + remote_answer->AddGroup(bundle_group); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + // Verify that all the sections are bundled on kAudio1. + auto transport1 = transport_controller_->GetRtpTransport(kAudioMid1); + auto transport2 = transport_controller_->GetRtpTransport(kAudioMid2); + auto transport3 = transport_controller_->GetRtpTransport(kVideoMid1); + auto transport4 = transport_controller_->GetRtpTransport(kDataMid1); + EXPECT_EQ(transport1, transport2); + EXPECT_EQ(transport1, transport3); + EXPECT_EQ(transport1, transport4); + + // Verify the OnRtpTransport/DtlsTransportChanged signals are fired correctly. + auto it = changed_rtp_transport_by_mid_.find(kAudioMid2); + ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end()); + EXPECT_EQ(transport1, it->second); + it = changed_rtp_transport_by_mid_.find(kAudioMid2); + ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end()); + EXPECT_EQ(transport1, it->second); + it = changed_rtp_transport_by_mid_.find(kVideoMid1); + ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end()); + EXPECT_EQ(transport1, it->second); + // Verify the DtlsTransport for the SCTP data channel is reset correctly. + auto it2 = changed_dtls_transport_by_mid_.find(kDataMid1); + ASSERT_TRUE(it2 != changed_dtls_transport_by_mid_.end()); + EXPECT_EQ(transport1->rtp_packet_transport(), it2->second); +} + +// Tests that only a subset of all the m= sections are bundled. +TEST_F(JsepTransportControllerTest, BundleSubsetOfMediaSections) { + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kAudioMid1); + bundle_group.AddContentName(kVideoMid1); + + auto local_offer = rtc::MakeUnique(); + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kAudioMid2, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + + auto remote_answer = rtc::MakeUnique(); + AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddAudioSection(remote_answer.get(), kAudioMid2, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + + local_offer->AddGroup(bundle_group); + remote_answer->AddGroup(bundle_group); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + // Verifiy that only |kAudio1| and |kVideo1| are bundled. + auto transport1 = transport_controller_->GetRtpTransport(kAudioMid1); + auto transport2 = transport_controller_->GetRtpTransport(kAudioMid2); + auto transport3 = transport_controller_->GetRtpTransport(kVideoMid1); + EXPECT_NE(transport1, transport2); + EXPECT_EQ(transport1, transport3); + + auto it = changed_rtp_transport_by_mid_.find(kVideoMid1); + ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end()); + EXPECT_EQ(transport1, it->second); + it = changed_rtp_transport_by_mid_.find(kAudioMid2); + EXPECT_TRUE(it == changed_rtp_transport_by_mid_.end()); +} + +// Tests that the initial offer/answer only have data section and audio/video +// sections are added in the subsequent offer. +TEST_F(JsepTransportControllerTest, BundleOnDataSectionInSubsequentOffer) { + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kDataMid1); + + auto local_offer = rtc::MakeUnique(); + AddDataSection(local_offer.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + auto remote_answer = rtc::MakeUnique(); + AddDataSection(remote_answer.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + local_offer->AddGroup(bundle_group); + remote_answer->AddGroup(bundle_group); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + auto data_transport = transport_controller_->GetRtpTransport(kDataMid1); + + // Add audio/video sections in subsequent offer. + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + + // Reset the bundle group and do another offer/answer exchange. + bundle_group.AddContentName(kAudioMid1); + bundle_group.AddContentName(kVideoMid1); + local_offer->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE); + remote_answer->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE); + local_offer->AddGroup(bundle_group); + remote_answer->AddGroup(bundle_group); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + auto audio_transport = transport_controller_->GetRtpTransport(kAudioMid1); + auto video_transport = transport_controller_->GetRtpTransport(kVideoMid1); + EXPECT_EQ(data_transport, audio_transport); + EXPECT_EQ(data_transport, video_transport); +} + +TEST_F(JsepTransportControllerTest, VideoDataRejectedInAnswer) { + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kAudioMid1); + bundle_group.AddContentName(kVideoMid1); + bundle_group.AddContentName(kDataMid1); + + auto local_offer = rtc::MakeUnique(); + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddDataSection(local_offer.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + + auto remote_answer = rtc::MakeUnique(); + AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddDataSection(remote_answer.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + // Reject video and data section. + remote_answer->contents()[1].rejected = true; + remote_answer->contents()[2].rejected = true; + + local_offer->AddGroup(bundle_group); + remote_answer->AddGroup(bundle_group); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + // Verify the RtpTransport/DtlsTransport is destroyed correctly. + EXPECT_EQ(nullptr, transport_controller_->GetRtpTransport(kVideoMid1)); + EXPECT_EQ(nullptr, transport_controller_->GetDtlsTransport(kDataMid1)); + // Verify the signals are fired correctly. + auto it = changed_rtp_transport_by_mid_.find(kVideoMid1); + ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end()); + EXPECT_EQ(nullptr, it->second); + auto it2 = changed_dtls_transport_by_mid_.find(kDataMid1); + ASSERT_TRUE(it2 != changed_dtls_transport_by_mid_.end()); + EXPECT_EQ(nullptr, it2->second); +} + +// Tests that changing the bundled MID in subsequent offer/answer exchange is +// not supported. +// TODO(bugs.webrtc.org/6704): Change this test to expect success once issue is +// fixed +TEST_F(JsepTransportControllerTest, ChangeBundledMidNotSupported) { + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kAudioMid1); + bundle_group.AddContentName(kVideoMid1); + + auto local_offer = rtc::MakeUnique(); + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + + auto remote_answer = rtc::MakeUnique(); + AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + + local_offer->AddGroup(bundle_group); + remote_answer->AddGroup(bundle_group); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + EXPECT_EQ(transport_controller_->GetRtpTransport(kAudioMid1), + transport_controller_->GetRtpTransport(kVideoMid1)); + + // Reorder the bundle group. + EXPECT_TRUE(bundle_group.RemoveContentName(kAudioMid1)); + bundle_group.AddContentName(kAudioMid1); + // The answerer uses the new bundle group and now the bundle mid is changed to + // |kVideo1|. + remote_answer->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE); + remote_answer->AddGroup(bundle_group); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_FALSE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); +} + +} // namespace webrtc diff --git a/pc/srtpfilter.cc b/pc/srtpfilter.cc index 4fe9dae00b..5561ada06d 100644 --- a/pc/srtpfilter.cc +++ b/pc/srtpfilter.cc @@ -11,8 +11,8 @@ #include "pc/srtpfilter.h" #include - #include +#include #include "media/base/rtputils.h" #include "pc/srtpsession.h" @@ -35,6 +35,31 @@ bool SrtpFilter::IsActive() const { return state_ >= ST_ACTIVE; } +bool SrtpFilter::Process(const std::vector& cryptos, + webrtc::SdpType type, + ContentSource source) { + bool ret = false; + switch (type) { + case webrtc::SdpType::kOffer: + ret = SetOffer(cryptos, source); + break; + case webrtc::SdpType::kPrAnswer: + ret = SetProvisionalAnswer(cryptos, source); + break; + case webrtc::SdpType::kAnswer: + ret = SetAnswer(cryptos, source); + break; + default: + break; + } + + if (!ret) { + return false; + } + + return true; +} + bool SrtpFilter::SetOffer(const std::vector& offer_params, ContentSource source) { if (!ExpectOffer(source)) { diff --git a/pc/srtpfilter.h b/pc/srtpfilter.h index e89a68af99..dc0b37ea38 100644 --- a/pc/srtpfilter.h +++ b/pc/srtpfilter.h @@ -18,6 +18,7 @@ #include #include "api/cryptoparams.h" +#include "api/jsep.h" #include "api/optional.h" #include "pc/sessiondescription.h" #include "rtc_base/basictypes.h" @@ -54,6 +55,13 @@ class SrtpFilter { // Whether the filter is active (i.e. crypto has been properly negotiated). bool IsActive() const; + // Handle the offer/answer negotiation of the crypto parameters internally. + // TODO(zhihuang): Make SetOffer/ProvisionalAnswer/Answer private as helper + // methods once start using Process. + bool Process(const std::vector& cryptos, + webrtc::SdpType type, + ContentSource source); + // Indicates which crypto algorithms and keys were contained in the offer. // offer_params should contain a list of available parameters to use, or none, // if crypto is not desired. This must be called before SetAnswer. diff --git a/pc/transportstats.cc b/pc/transportstats.cc new file mode 100644 index 0000000000..df2df5892f --- /dev/null +++ b/pc/transportstats.cc @@ -0,0 +1,25 @@ +/* + * Copyright 2018 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 "pc/transportstats.h" + +namespace cricket { + +TransportChannelStats::TransportChannelStats() = default; + +TransportChannelStats::TransportChannelStats(const TransportChannelStats&) = + default; + +TransportChannelStats::~TransportChannelStats() = default; + +TransportStats::TransportStats() = default; + +TransportStats::~TransportStats() = default; + +} // namespace cricket diff --git a/pc/transportstats.h b/pc/transportstats.h new file mode 100644 index 0000000000..cff608ade1 --- /dev/null +++ b/pc/transportstats.h @@ -0,0 +1,50 @@ +/* + * Copyright 2018 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. + */ + +#ifndef PC_TRANSPORTSTATS_H_ +#define PC_TRANSPORTSTATS_H_ + +#include +#include + +#include "p2p/base/dtlstransport.h" +#include "p2p/base/port.h" + +namespace cricket { + +struct TransportChannelStats { + TransportChannelStats(); + TransportChannelStats(const TransportChannelStats&); + ~TransportChannelStats(); + + int component = 0; + CandidateStatsList candidate_stats_list; + ConnectionInfos connection_infos; + int srtp_crypto_suite = rtc::SRTP_INVALID_CRYPTO_SUITE; + int ssl_cipher_suite = rtc::TLS_NULL_WITH_NULL_NULL; + DtlsTransportState dtls_state = DTLS_TRANSPORT_NEW; +}; + +// Information about all the channels of a transport. +// TODO(hta): Consider if a simple vector is as good as a map. +typedef std::vector TransportChannelStatsList; + +// Information about the stats of a transport. +struct TransportStats { + TransportStats(); + ~TransportStats(); + + std::string transport_name; + TransportChannelStatsList channel_stats; +}; + +} // namespace cricket + +#endif // PC_TRANSPORTSTATS_H_