ice: avoid duplicating srfx candidates

Avoid adding generic srflx candidates if UPNP candidates are
available. This will reduc the size of the check list and also
increase the chance for UPNP candidates to be selected.

Change-Id: Ia1504beffe7e12063ff1c1ad0f54d65bb60496d6
Gitlab: #607
This commit is contained in:
Mohamed Chibani
2021-08-10 09:56:08 -04:00
parent 92c4c00ad0
commit 34d45147e6
5 changed files with 908 additions and 78 deletions

View File

@ -135,7 +135,7 @@ public:
IceTransportCompleteCb on_negodone_cb_ {};
IceRecvInfo on_recv_cb_ {};
mutable std::mutex iceMutex_ {};
pj_ice_strans* icest_{nullptr};
pj_ice_strans* icest_ {nullptr};
unsigned streamsCount_ {0};
unsigned compCountPerStream_ {0};
unsigned compCount_ {0};
@ -343,13 +343,35 @@ IceTransport::Impl::Impl(const char* name, const IceTransportOptions& options)
addDefaultCandidates();
addServerReflexiveCandidates(setupGenericReflexiveCandidates());
// Note: For server reflexive candidates, UPNP mappings will
// be used if available. Then, the public address learnt during
// the account registration process will be added only if it
// differs from the UPNP public address.
// Also note that UPNP candidates should be added first in order
// to have a higher priority when performing the connectivity
// checks.
std::vector<std::pair<IpAddr, IpAddr>> upnpSrflxCand;
if (upnp_) {
requestUpnpMappings();
auto const& upnpMaps = setupUpnpReflexiveCandidates();
if (not upnpMaps.empty())
addServerReflexiveCandidates(upnpMaps);
upnpSrflxCand = setupUpnpReflexiveCandidates();
if (not upnpSrflxCand.empty()) {
JAMI_DBG("[ice:%p] Add UPNP srflx candidates:", this);
addServerReflexiveCandidates(upnpSrflxCand);
}
}
auto genericSrflxCand = setupGenericReflexiveCandidates();
if (not genericSrflxCand.empty()) {
if (upnpSrflxCand.empty()
or (upnpSrflxCand[0].second.toString() != genericSrflxCand[0].second.toString())) {
JAMI_DBG("[ice:%p] Add generic srflx candidates:", this);
addServerReflexiveCandidates(genericSrflxCand);
}
}
if (upnpSrflxCand.empty() and genericSrflxCand.empty()) {
JAMI_WARN("[ice:%p] No server reflexive candidates added", this);
}
pool_.reset(
@ -450,8 +472,7 @@ IceTransport::Impl::~Impl()
assert(strans);
// must be done before ioqueue/timer destruction
JAMI_INFO("[ice:%p] Destroying ice_strans %p",
pj_ice_strans_get_user_data(strans), strans);
JAMI_INFO("[ice:%p] Destroying ice_strans %p", pj_ice_strans_get_user_data(strans), strans);
pj_ice_strans_stop_ice(strans);
pj_ice_strans_destroy(strans);
@ -460,7 +481,8 @@ IceTransport::Impl::~Impl()
// Because when destroying the TURN session pjproject creates a pj_timer
// to postpone the TURN destruction. This timer is only called if we poll
// the event queue.
while (handleEvents(500));
while (handleEvents(500))
;
if (config_.stun_cfg.ioqueue)
pj_ioqueue_destroy(config_.stun_cfg.ioqueue);
@ -630,7 +652,6 @@ IceTransport::Impl::setInitiatorSession()
JAMI_DBG("[ice:%p] as master", this);
initiatorSession_ = true;
if (_isInitialized()) {
std::lock_guard<std::mutex> lk(iceMutex_);
if (not icest_) {
@ -654,7 +675,6 @@ IceTransport::Impl::setSlaveSession()
JAMI_DBG("[ice:%p] as slave", this);
initiatorSession_ = false;
if (_isInitialized()) {
std::lock_guard<std::mutex> lk(iceMutex_);
if (not icest_) {
@ -737,12 +757,11 @@ void
IceTransport::Impl::getUFragPwd()
{
if (icest_) {
pj_str_t local_ufrag, local_pwd;
pj_str_t local_ufrag, local_pwd;
pj_ice_strans_get_ufrag_pwd(icest_, &local_ufrag, &local_pwd, nullptr, nullptr);
local_ufrag_.assign(local_ufrag.ptr, local_ufrag.slen);
local_pwd_.assign(local_pwd.ptr, local_pwd.slen);
pj_ice_strans_get_ufrag_pwd(icest_, &local_ufrag, &local_pwd, nullptr, nullptr);
local_ufrag_.assign(local_ufrag.ptr, local_ufrag.slen);
local_pwd_.assign(local_pwd.ptr, local_pwd.slen);
}
}
@ -798,7 +817,7 @@ IceTransport::Impl::addStunConfig(int af)
void
IceTransport::Impl::addDefaultCandidates()
{
JAMI_DBG("[ice:%p]: Setup default candidates", this);
JAMI_DBG("[ice:%p] Setup default candidates", this);
// STUN configs layout:
// - index 0 : host IPv4
@ -846,7 +865,7 @@ IceTransport::Impl::requestUpnpMappings()
mapPtr->toString().c_str());
}
} else {
JAMI_WARN("[ice:%p]: UPNP mapping request failed!", this);
JAMI_WARN("[ice:%p] UPNP mapping request failed!", this);
upnp_->releaseMapping(requestedMap);
}
}
@ -863,7 +882,7 @@ IceTransport::Impl::addServerReflexiveCandidates(
const std::vector<std::pair<IpAddr, IpAddr>>& addrList)
{
if (addrList.size() != compCount_) {
JAMI_WARN("[ice:%p]: Provided addr list size %lu does not match component count %u",
JAMI_WARN("[ice:%p] Provided addr list size %lu does not match component count %u",
this,
addrList.size(),
compCount_);
@ -882,6 +901,12 @@ IceTransport::Impl::addServerReflexiveCandidates(
auto& localAddr = addrList[idx].first;
auto& publicAddr = addrList[idx].second;
JAMI_DBG("[ice:%p] Add srflx reflexive candidates [%s : %s] for comp %u",
this,
localAddr.toString(true).c_str(),
publicAddr.toString(true).c_str(),
id);
pj_sockaddr_cp(&stun.cfg.user_mapping[idx].local_addr, localAddr.pjPtr());
pj_sockaddr_cp(&stun.cfg.user_mapping[idx].mapped_addr, publicAddr.pjPtr());
@ -904,12 +929,14 @@ std::vector<std::pair<IpAddr, IpAddr>>
IceTransport::Impl::setupGenericReflexiveCandidates()
{
if (not accountLocalAddr_) {
JAMI_WARN("[ice:%p]: Local address needed for reflexive candidates!", this);
JAMI_WARN("[ice:%p] Missing local address, generic srflx candidates wont be generated!",
this);
return {};
}
if (not accountPublicAddr_) {
JAMI_WARN("[ice:%p]: Public address needed for reflexive candidates!", this);
JAMI_WARN("[ice:%p] Missing public address, generic srflx candidates wont be generated!",
this);
return {};
}
@ -934,11 +961,6 @@ IceTransport::Impl::setupGenericReflexiveCandidates()
accountLocalAddr_.setPort(port);
accountPublicAddr_.setPort(port);
addrList.emplace_back(accountLocalAddr_, accountPublicAddr_);
JAMI_DBG("[ice:%p]: Add generic local reflexive candidates [%s : %s]",
this,
accountLocalAddr_.toString(true).c_str(),
accountPublicAddr_.toString(true).c_str());
}
return addrList;
@ -954,7 +976,7 @@ IceTransport::Impl::setupUpnpReflexiveCandidates()
std::lock_guard<std::mutex> lock(upnpMappingsMutex_);
if (static_cast<unsigned>(upnpMappings_.size()) < compCount_) {
JAMI_WARN("[ice:%p]: Not enough mappings %lu. Expected %u",
JAMI_WARN("[ice:%p] Not enough mappings %lu. Expected %u",
this,
upnpMappings_.size(),
compCount_);
@ -963,7 +985,6 @@ IceTransport::Impl::setupUpnpReflexiveCandidates()
std::vector<std::pair<IpAddr, IpAddr>> addrList;
unsigned compId = 1;
addrList.reserve(upnpMappings_.size());
for (auto const& [_, map] : upnpMappings_) {
assert(map.getMapKey());
@ -972,13 +993,6 @@ IceTransport::Impl::setupUpnpReflexiveCandidates()
IpAddr publicAddr {map.getExternalAddress()};
publicAddr.setPort(map.getExternalPort());
addrList.emplace_back(localAddr, publicAddr);
JAMI_DBG("[ice:%p]: Add UPNP local reflexive candidates [%s : %s] for comp %u",
this,
localAddr.toString(true).c_str(),
publicAddr.toString(true).c_str(),
compId);
compId++;
}
return addrList;
@ -1382,8 +1396,7 @@ IceTransport::getLocalCandidates(unsigned streamIdx, unsigned compId) const
// order to be compliant with the spec.
auto globalCompId = streamIdx * 2 + compId;
if (pj_ice_strans_enum_cands(pimpl_->icest_, globalCompId, &cand_cnt, cand)
!= PJ_SUCCESS) {
if (pj_ice_strans_enum_cands(pimpl_->icest_, globalCompId, &cand_cnt, cand) != PJ_SUCCESS) {
JAMI_ERR("[ice:%p] pj_ice_strans_enum_cands() failed", pimpl_.get());
return res;
}
@ -1560,8 +1573,6 @@ IceTransport::recvfrom(unsigned compId, char* buf, size_t len, std::error_code&
void
IceTransport::setOnRecv(unsigned compId, IceRecvCb cb)
{
JAMI_DBG("Setting 'on recv' callback for comp %i", compId);
ASSERT_COMP_ID(compId, getComponentCount());
auto& io = pimpl_->compIO_[compId - 1];
@ -1610,12 +1621,11 @@ IceTransport::send(unsigned compId, const unsigned char* buf, size_t len)
remote.getLength());
if (status == PJ_EPENDING && isTCPEnabled()) {
// NOTE; because we are in TCP, the sent size will count the header (2
// bytes length).
pimpl_->waitDataCv_.wait(lk, [&] {
return pimpl_->lastSentLen_ >= static_cast<pj_size_t>(len)
or pimpl_->destroying_.load();
or pimpl_->destroying_.load();
});
pimpl_->lastSentLen_ = 0;
} else if (status != PJ_SUCCESS && status != PJ_EPENDING) {

View File

@ -695,8 +695,6 @@ SIPCall::setInviteSession(pjsip_inv_session* inviteSession)
void
SIPCall::terminateSipSession(int status)
{
sip_utils::register_thread();
JAMI_DBG("[call:%s] Terminate SIP session", getCallId().c_str());
std::lock_guard<std::recursive_mutex> lk {callMutex_};
if (inviteSession_ and inviteSession_->state != PJSIP_INV_STATE_DISCONNECTED) {
@ -863,7 +861,7 @@ SIPCall::answer(const std::vector<DRing::MediaMap>& mediaList)
auto opts = account->getIceOptions();
auto publicAddr = getPublicAddress();
auto publicAddr = account->getPublishedIpAddress();
if (publicAddr) {
opts.accountPublicAddr = publicAddr;
@ -1624,6 +1622,7 @@ SIPCall::setPeerUaVersion(std::string_view ua)
void
SIPCall::onPeerRinging()
{
JAMI_DBG("[call:%s] Peer ringing", getCallId().c_str());
setState(ConnectionState::RINGING);
}
@ -2936,6 +2935,17 @@ SIPCall::initIceMediaTransport(bool master, std::optional<IceTransportOptions> o
return static_cast<bool>(tmpMediaTransport_);
}
std::vector<std::string>
SIPCall::getLocalIceCandidates(unsigned compId) const
{
auto iceTransp = getIceMediaTransport();
if (not iceTransp) {
JAMI_WARN("[call:%s] no media ICE transport", getCallId().c_str());
return {};
}
return iceTransp->getLocalCandidates(compId);
}
void
SIPCall::resetTransport(std::shared_ptr<IceTransport>&& transport)
{
@ -3021,12 +3031,6 @@ SIPCall::setupIceResponse()
}
auto opt = account->getIceOptions();
opt.accountPublicAddr = getPublicAddress();
if (not opt.accountPublicAddr) {
JAMI_ERR("[call:%s] No public address, ICE can't be initialized", getCallId().c_str());
onFailure(EIO);
return;
}
opt.accountLocalAddr = ip_utils::getInterfaceAddr(account->getLocalInterface(),
opt.accountPublicAddr.getFamily());
@ -3037,6 +3041,8 @@ SIPCall::setupIceResponse()
return;
}
opt.accountPublicAddr = account->getPublishedIpAddress();
if (not initIceMediaTransport(false, opt)) {
JAMI_ERR("[call:%s] ICE initialization failed", getCallId().c_str());
// Fatal condition
@ -3062,28 +3068,6 @@ SIPCall::newIceSocket(unsigned compId)
return std::unique_ptr<IceSocket> {new IceSocket(mediaTransport_, compId)};
}
IpAddr
SIPCall::getPublicAddress() const
{
auto account = getSIPAccount();
if (not account) {
JAMI_ERR("No account detected");
return {};
}
auto publicAddr = account->getPublishedIpAddress();
if (not publicAddr) {
// If the published address is unknown, just use the local address. Not
// optimal, but may work just fine if both endpoints are in the same
// local network.
publicAddr = ip_utils::getInterfaceAddr(account->getLocalInterface(), pj_AF_INET());
JAMI_WARN("[call:%s] Missing public address, using local address instead",
getCallId().c_str());
}
return publicAddr;
}
void
SIPCall::rtpSetupSuccess(MediaType type, bool isRemote)
{

View File

@ -267,11 +267,19 @@ public:
bool initIceMediaTransport(bool master,
std::optional<IceTransportOptions> options = std::nullopt);
std::vector<std::string> getLocalIceCandidates(unsigned compId) const;
void setInviteSession(pjsip_inv_session* inviteSession = nullptr);
std::unique_ptr<pjsip_inv_session, InvSessionDeleter> inviteSession_;
private:
IceTransport* getIceMediaTransport() const
{
std::lock_guard<std::mutex> lk(transportMtx_);
return tmpMediaTransport_ ? tmpMediaTransport_.get() : mediaTransport_.get();
}
void generateMediaPorts();
void openPortsUPnP();
@ -280,8 +288,6 @@ private:
std::unique_ptr<IceSocket> newIceSocket(unsigned compId);
IpAddr getPublicAddress() const;
void deinitRecorder();
void rtpSetupSuccess(MediaType type, bool isRemote);
@ -295,11 +301,6 @@ private:
void setVideoOrientation(int rotation);
mutable std::mutex transportMtx_ {};
IceTransport* getIceMediaTransport() const
{
std::lock_guard<std::mutex> lk(transportMtx_);
return tmpMediaTransport_ ? tmpMediaTransport_.get() : mediaTransport_.get();
}
#ifdef ENABLE_PLUGIN
/**

View File

@ -177,6 +177,9 @@ ut_ice_SOURCES = ice/ice.cpp common.cpp
check_PROGRAMS += ut_ice_sdp_parser
ut_ice_sdp_parser_SOURCES = ice/ice_sdp_parser.cpp common.cpp
check_PROGRAMS += ut_ice_media_cand_exchange
ut_ice_media_cand_exchange_SOURCES = ice/ice_media_cand_exchange.cpp common.cpp
#
# Calls using SIP accounts
#

View File

@ -0,0 +1,832 @@
/*
* Copyright (C) 2021 Savoir-faire Linux Inc.
*
* Author: Mohamed Chibani <mohamed.chibani@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <cppunit/TestAssert.h>
#include <cppunit/TestFixture.h>
#include <cppunit/extensions/HelperMacros.h>
#include <condition_variable>
#include <string>
#include "manager.h"
#include "jamidht/jamiaccount.h"
#include "sip/sipaccount.h"
#include "../../test_runner.h"
#include "jami.h"
#include "media_const.h"
#include "call_const.h"
#include "account_const.h"
#include "sip/sipcall.h"
#include "upnp/upnp_control.h"
#include "common.h"
using namespace DRing::Account;
using namespace DRing::Call;
namespace jami {
namespace test {
struct CallData
{
struct Signal
{
Signal(const std::string& callId, const std::string& name, const std::string& event = {})
: callId_(std::move(callId))
, name_(std::move(name))
, event_(std::move(event)) {};
std::string callId_ {};
std::string name_ {};
std::string event_ {};
};
std::string accountId_ {};
std::string displayName_ {};
std::string userName_ {};
std::string alias_ {};
std::string callId_ {};
std::vector<Signal> signals_;
std::condition_variable cv_ {};
std::mutex mtx_;
bool accountRegistered_ {false};
bool upnpEnabled_ {false};
bool turnEnabled_ {false};
IpAddr dest_ {};
// SIP accounts only
uint16_t listeningPort_ {0};
};
class IceMediaCandExchangeTest : public CppUnit::TestFixture
{
public:
IceMediaCandExchangeTest()
{
// Init daemon
DRing::init(DRing::InitFlag(DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_CONSOLE_LOG));
if (not Manager::instance().initialized)
CPPUNIT_ASSERT(DRing::start("jami-sample.yml"));
}
~IceMediaCandExchangeTest() { DRing::fini(); }
static std::string name() { return "IceMediaCandExchangeTest"; }
void setUp();
void tearDown();
private:
// Test cases.
// This is not actual test, it only checks if upnp is
// functional. This information is needed to validate
// generated ICE srflx candidates.
void check_upnp();
void jami_account_no_turn();
void jami_account_with_turn();
void sip_account_no_turn();
void sip_account_with_turn();
CPPUNIT_TEST_SUITE(IceMediaCandExchangeTest);
CPPUNIT_TEST(check_upnp);
CPPUNIT_TEST(jami_account_no_turn);
CPPUNIT_TEST(jami_account_with_turn);
CPPUNIT_TEST_SUITE_END();
// Event/Signal handlers
static void onCallStateChange(const std::string& callId,
const std::string& state,
CallData& callData);
static void onIncomingCallWithMedia(const std::string& accountId,
const std::string& callId,
const std::vector<DRing::MediaMap> mediaList,
CallData& callData);
static void onMediaNegotiationStatus(const std::string& callId,
const std::string& event,
CallData& callData);
// Helpers
void test_call(const char* accountType);
static void validate_ice_candidates(CallData& user,
const char* accountType,
bool hasUpnp,
bool upnpSameAsPublished);
static void setupJamiAccount(CallData& user);
static void setupSipAccount(CallData& user);
static void setupAccounts(CallData& bob, CallData& alice, const char* accountType);
static void configureAccount(CallData& user, const char* accountType);
static std::string getUserAlias(const std::string& callId);
// Wait for a signal from the callbacks. Some signals also report the event that
// triggered the signal a like the StateChange signal.
static bool waitForSignal(CallData& callData,
const std::string& signal,
const std::string& expectedEvent = {});
private:
CallData aliceData_;
CallData bobData_;
const size_t MEDIA_COUNT {2};
std::condition_variable upnpCv_ {};
std::mutex upnpMtx_;
bool hasUpnp_ {false};
bool upnpAddrSameAsPublished_ {false};
};
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(IceMediaCandExchangeTest, IceMediaCandExchangeTest::name());
void
IceMediaCandExchangeTest::setUp()
{
// Everything is done in ConfigureTest()
}
void
IceMediaCandExchangeTest::tearDown()
{
JAMI_INFO("Remove created accounts...");
wait_for_removal_of({aliceData_.accountId_, bobData_.accountId_});
}
std::string
IceMediaCandExchangeTest::getUserAlias(const std::string& callId)
{
auto call = Manager::instance().getCallFromCallID(callId);
if (call) {
auto const& account = call->getAccount().lock();
if (not account) {
return {};
}
return account->getAccountDetails()[ConfProperties::ALIAS];
}
JAMI_WARN("Call [%s] does not exist!", callId.c_str());
return {};
}
void
IceMediaCandExchangeTest::onIncomingCallWithMedia(const std::string& accountId,
const std::string& callId,
const std::vector<DRing::MediaMap> mediaList,
CallData& callData)
{
CPPUNIT_ASSERT_EQUAL(callData.accountId_, accountId);
JAMI_INFO("Signal [%s] - user [%s] - call [%s] - media count [%lu]",
DRing::CallSignal::IncomingCallWithMedia::name,
callData.alias_.c_str(),
callId.c_str(),
mediaList.size());
if (not Manager::instance().getCallFromCallID(callId)) {
JAMI_WARN("Call [%s] does not exist!", callId.c_str());
callData.callId_ = {};
return;
}
std::unique_lock<std::mutex> lock {callData.mtx_};
callData.callId_ = callId;
callData.signals_.emplace_back(
CallData::Signal(callId, DRing::CallSignal::IncomingCallWithMedia::name));
callData.cv_.notify_one();
}
void
IceMediaCandExchangeTest::onCallStateChange(const std::string& callId,
const std::string& state,
CallData& callData)
{
JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]",
DRing::CallSignal::StateChange::name,
callData.alias_.c_str(),
callId.c_str(),
state.c_str());
{
std::unique_lock<std::mutex> lock {callData.mtx_};
callData.signals_.emplace_back(
CallData::Signal(callId, DRing::CallSignal::StateChange::name, state));
}
if (state == "RINGING" or state == "CURRENT" or state == "HUNGUP" or state == "OVER") {
callData.cv_.notify_one();
}
}
void
IceMediaCandExchangeTest::onMediaNegotiationStatus(const std::string& callId,
const std::string& event,
CallData& callData)
{
auto call = Manager::instance().getCallFromCallID(callId);
if (not call) {
JAMI_WARN("Call [%s] does not exist!", callId.c_str());
return;
}
auto account = call->getAccount().lock();
if (not account) {
JAMI_WARN("Account owning the call [%s] does not exist!", callId.c_str());
return;
}
JAMI_INFO("Signal [%s] - user [%s] - call [%s] - state [%s]",
DRing::CallSignal::MediaNegotiationStatus::name,
account->getAccountDetails()[ConfProperties::ALIAS].c_str(),
call->getCallId().c_str(),
event.c_str());
if (account->getAccountID() != callData.accountId_)
return;
{
std::unique_lock<std::mutex> lock {callData.mtx_};
callData.signals_.emplace_back(
CallData::Signal(callId, DRing::CallSignal::MediaNegotiationStatus::name, event));
}
callData.cv_.notify_one();
}
bool
IceMediaCandExchangeTest::waitForSignal(CallData& callData,
const std::string& expectedSignal,
const std::string& expectedEvent)
{
const std::chrono::seconds TIME_OUT {45};
std::unique_lock<std::mutex> lock {callData.mtx_};
// Combined signal + event (if any).
std::string sigEvent(expectedSignal);
if (not expectedEvent.empty())
sigEvent += "::" + expectedEvent;
JAMI_INFO("[%s] is waiting for [%s] signal/event", callData.alias_.c_str(), sigEvent.c_str());
auto res = callData.cv_.wait_for(lock, TIME_OUT, [&] {
// Search for the expected signal in list of received signals.
for (auto it = callData.signals_.begin(); it != callData.signals_.end(); it++) {
// The predicate is true if the signal names match, and if the
// expectedEvent is not empty, the events must also match.
if (it->name_ == expectedSignal
and (expectedEvent.empty() or it->event_ == expectedEvent)) {
// Done with this signal.
callData.signals_.erase(it);
return true;
}
}
// Signal/event not found.
return false;
});
if (not res) {
JAMI_ERR("[%s] waiting for signal/event [%s] [call:%s] timed-out!",
callData.alias_.c_str(),
sigEvent.c_str(),
callData.callId_.c_str());
JAMI_INFO("[%s] currently has the following signals:", callData.alias_.c_str());
for (auto const& sig : callData.signals_) {
JAMI_INFO() << "Signal [" << sig.name_
<< (sig.event_.empty() ? "" : ("::" + sig.event_)) << "] "
<< "call [" << sig.callId_ << "]";
}
}
return res;
}
void
IceMediaCandExchangeTest::setupJamiAccount(CallData& user)
{
auto account = Manager::instance().getAccount<JamiAccount>(user.accountId_);
auto details = account->getAccountDetails();
user.userName_ = details[ConfProperties::USERNAME];
user.alias_ = details[ConfProperties::ALIAS];
// Apply the settings according to the test case
details[ConfProperties::UPNP_ENABLED] = user.upnpEnabled_ ? "true" : "false";
details[ConfProperties::TURN::ENABLED] = user.turnEnabled_ ? "true" : "false";
DRing::setAccountDetails(user.accountId_, details);
account->enableMultiStream(true);
}
void
IceMediaCandExchangeTest::setupSipAccount(CallData& user)
{
CPPUNIT_ASSERT_GREATER(0, static_cast<int>(user.listeningPort_));
auto details = DRing::getAccountTemplate(DRing::Account::ProtocolNames::SIP);
details[ConfProperties::TYPE] = DRing::Account::ProtocolNames::SIP;
details[ConfProperties::DISPLAYNAME] = user.displayName_.c_str();
details[ConfProperties::ALIAS] = user.displayName_.c_str();
details[ConfProperties::LOCAL_PORT] = std::to_string(user.listeningPort_);
details[ConfProperties::PUBLISHED_PORT] = std::to_string(user.listeningPort_);
details[ConfProperties::PUBLISHED_SAMEAS_LOCAL] = "true";
details[ConfProperties::SRTP::ENABLED] = "false";
details[ConfProperties::SRTP::KEY_EXCHANGE] = "NONE";
// Apply the settings according to the test case
details[ConfProperties::UPNP_ENABLED] = user.upnpEnabled_ ? "true" : "false";
details[ConfProperties::TURN::ENABLED] = user.turnEnabled_ ? "true" : "false";
user.accountId_ = Manager::instance().addAccount(details);
CPPUNIT_ASSERT(not user.accountId_.empty());
auto const& account = Manager::instance().getAccount<Account>(user.accountId_);
details = account->getAccountDetails();
user.userName_ = details[ConfProperties::USERNAME];
user.alias_ = details[ConfProperties::ALIAS];
account->enableIceForMedia(true);
account->enableMultiStream(true);
user.dest_ = ip_utils::getLocalAddr(AF_INET);
user.dest_.setPort(user.listeningPort_);
}
void
IceMediaCandExchangeTest::setupAccounts(CallData& aliceData,
CallData& bobData,
const char* accountType)
{
if (strcmp(accountType, DRing::Account::ProtocolNames::SIP) == 0) {
JAMI_INFO("Setup SIP accounts and configure test case ...");
aliceData.displayName_ = "ALICE";
aliceData.listeningPort_ = 5080;
setupSipAccount(aliceData);
bobData.displayName_ = "BOB";
bobData.listeningPort_ = 5082;
setupSipAccount(bobData);
} else {
JAMI_INFO("Setup JAMI accounts and configure test case ...");
auto actors = load_actors("actors/alice-bob-no-upnp.yml");
aliceData.accountId_ = actors["alice"];
auto const& aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceData.accountId_);
bobData.accountId_ = actors["bob"];
auto const& bobAccount = Manager::instance().getAccount<JamiAccount>(bobData.accountId_);
wait_for_announcement_of({aliceAccount->getAccountID(), bobAccount->getAccountID()});
setupJamiAccount(bobData);
setupJamiAccount(aliceData);
}
// Setup signal handlers.
std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> signalHandlers;
signalHandlers.insert(DRing::exportable_callback<DRing::CallSignal::IncomingCallWithMedia>(
[&](const std::string& accountId,
const std::string& callId,
const std::string&,
const std::vector<DRing::MediaMap> mediaList) {
auto user = getUserAlias(callId);
if (user.empty()) {
// The call was probably already removed, in this case, just
// check callId against current calls.
if (callId == aliceData.callId_)
user = aliceData.alias_;
if (callId == bobData.callId_)
user = bobData.alias_;
}
if (not user.empty()) {
onIncomingCallWithMedia(accountId,
callId,
mediaList,
user == aliceData.alias_ ? aliceData : bobData);
} else {
JAMI_WARN("Received [CallSignal::IncomingCallWithMedia] for a removed call [%s]",
callId.c_str());
}
}));
signalHandlers.insert(DRing::exportable_callback<DRing::CallSignal::StateChange>(
[&](const std::string& callId, const std::string& state, signed) {
auto user = getUserAlias(callId);
if (user.empty()) {
// The call was probably already removed, in this case, just
// check callId against current calls.
if (callId == aliceData.callId_)
user = aliceData.alias_;
if (callId == bobData.callId_)
user = bobData.alias_;
}
if (not user.empty()) {
onCallStateChange(callId, state, user == aliceData.alias_ ? aliceData : bobData);
} else {
JAMI_WARN("Received [StateChange::%s] for a removed call [%s]",
state.c_str(),
callId.c_str());
}
}));
signalHandlers.insert(DRing::exportable_callback<DRing::CallSignal::MediaNegotiationStatus>(
[&](const std::string& callId,
const std::string& event,
const std::vector<std::map<std::string, std::string>>& /* mediaList */) {
auto user = getUserAlias(callId);
if (user.empty()) {
// The call was probably already removed, in this case, just
// check callId against current calls.
if (callId == aliceData.callId_)
user = aliceData.alias_;
if (callId == bobData.callId_)
user = bobData.alias_;
}
if (not user.empty()) {
onMediaNegotiationStatus(callId,
event,
user == aliceData.alias_ ? aliceData : bobData);
} else {
JAMI_WARN("Received [CallSignal::MediaNegotiationStatus] for a removed call [%s]",
callId.c_str());
}
}));
signalHandlers.insert(
DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>(
[&](const std::string& accountId, const std::map<std::string, std::string>&) {
auto account = Manager::instance().getAccount(accountId);
if (not account) {
JAMI_ERR("No account with ID [%s]", accountId.c_str());
return;
}
auto details = account->getVolatileAccountDetails();
auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
if (accountId == aliceData.accountId_) {
aliceData.accountRegistered_ = daemonStatus == "REGISTERED";
aliceData.cv_.notify_one();
} else if (accountId == bobData.accountId_) {
bobData.accountRegistered_ = daemonStatus == "REGISTERED";
bobData.cv_.notify_one();
} else {
JAMI_ERR("Account with ID [%s] is unknown", accountId.c_str());
}
}));
DRing::registerSignalHandlers(signalHandlers);
}
void
IceMediaCandExchangeTest::configureAccount(CallData& user, const char* accountType)
{
auto details
= strcmp(accountType, DRing::Account::ProtocolNames::RING) == 0
? Manager::instance().getAccount<JamiAccount>(user.accountId_)->getAccountDetails()
: Manager::instance().getAccount<SIPAccount>(user.accountId_)->getAccountDetails();
// Apply the settings according to the test case
details[ConfProperties::UPNP_ENABLED] = user.upnpEnabled_ ? "true" : "false";
details[ConfProperties::TURN::ENABLED] = user.turnEnabled_ ? "true" : "false";
DRing::setAccountDetails(user.accountId_, details);
// Note: setAccountDetails will trigger a re-register of the account, so
// we need to wait for the registration to proceed.
// Only done for JAMI accounts.
if (strcmp(accountType, DRing::Account::ProtocolNames::RING) != 0)
return;
auto account = Manager::instance().getAccount<JamiAccount>(user.accountId_);
CPPUNIT_ASSERT(account);
JAMI_INFO("Waiting for [%s] account [%s] to register ...",
user.alias_.c_str(),
user.accountId_.c_str());
std::unique_lock<std::mutex> lock {user.mtx_};
CPPUNIT_ASSERT(
user.cv_.wait_for(lock, std::chrono::seconds(60), [&] { return user.accountRegistered_; }));
}
void
IceMediaCandExchangeTest::validate_ice_candidates(CallData& user,
const char* accountType,
bool hasUpnp,
bool upnpSameAsPublished)
{
auto sipCall = std::dynamic_pointer_cast<SIPCall>(
Manager::instance().getCallFromCallID(user.callId_));
CPPUNIT_ASSERT(sipCall);
// Only check the first component for now.
auto localCand = sipCall->getLocalIceCandidates(1);
// Should not be empty.
CPPUNIT_ASSERT(not localCand.empty());
int srflxCandCount = 0;
int relayCandCount = 0;
for (auto const& cand : localCand) {
JAMI_INFO("[%s] ICE cand: [%s]", user.alias_.c_str(), cand.c_str());
if (cand.find("srflx") != std::string::npos) {
srflxCandCount++;
} else if (cand.find("relay") != std::string::npos) {
relayCandCount++;
}
}
// Note:
// Enabling UPNP does not guarantee that we have valid UPNP srflx
// candidate. However, for RING accounts, we must have at least one
// srflx candidate generated using the public address discovered
// after connecting to the DHT.
// For SIP accounts, we use un-registered (P2P) mode), so srflx
// candidates are only added if UPNP is enabled and the mapping is
// successful.
int expectedSrflxCandCount = 0;
if (strcmp(accountType, DRing::Account::ProtocolNames::RING) == 0) {
if (hasUpnp) {
if (upnpSameAsPublished) {
// UPNP and published are the same, published wont be added.
expectedSrflxCandCount = 1;
} else {
// Both UPNP and published will be added.
expectedSrflxCandCount = 2;
}
} else {
// No UPNP, only published address.
expectedSrflxCandCount = 1;
}
} else {
// SIP accounts in un-registered mode will have only UPNP srflx
// candidates if available
if (hasUpnp) {
expectedSrflxCandCount = 1;
}
}
// Validate srflx
CPPUNIT_ASSERT_EQUAL(expectedSrflxCandCount, srflxCandCount);
// Validate relay
CPPUNIT_ASSERT_EQUAL(user.turnEnabled_ ? 1 : 0, relayCandCount);
}
void
IceMediaCandExchangeTest::test_call(const char* accountType)
{
JAMI_INFO("=== Start a call and validate ICE for account type [%s] ===", accountType);
MediaAttribute media_0(MediaType::MEDIA_AUDIO);
media_0.label_ = "audio_0";
media_0.enabled_ = true;
MediaAttribute media_1(MediaType::MEDIA_VIDEO);
media_1.label_ = "video_0";
media_1.enabled_ = true;
std::vector<MediaAttribute> offer;
offer.emplace_back(media_0);
offer.emplace_back(media_1);
std::vector<MediaAttribute> answer;
answer.emplace_back(media_0);
answer.emplace_back(media_1);
CPPUNIT_ASSERT_EQUAL(MEDIA_COUNT, offer.size());
CPPUNIT_ASSERT_EQUAL(MEDIA_COUNT, answer.size());
// Set the destination according to account type.
auto dest = strcmp(accountType, DRing::Account::ProtocolNames::SIP) == 0
? bobData_.dest_.toString(true)
: bobData_.userName_;
CPPUNIT_ASSERT(not dest.empty());
aliceData_.callId_ = DRing::placeCallWithMedia(aliceData_.accountId_,
dest,
MediaAttribute::mediaAttributesToMediaMaps(
offer));
CPPUNIT_ASSERT(not aliceData_.callId_.empty());
JAMI_INFO("ALICE [%s] started a call with BOB [%s:%s] and wait for answer",
aliceData_.accountId_.c_str(),
bobData_.accountId_.c_str(),
dest.c_str());
// Give it some time to ring
std::this_thread::sleep_for(std::chrono::seconds(2));
// Wait for call to be processed.
CPPUNIT_ASSERT(
waitForSignal(aliceData_, DRing::CallSignal::StateChange::name, StateEvent::RINGING));
// Wait for incoming call signal.
CPPUNIT_ASSERT(waitForSignal(bobData_, DRing::CallSignal::IncomingCallWithMedia::name));
// Answer the call.
DRing::acceptWithMedia(bobData_.callId_, MediaAttribute::mediaAttributesToMediaMaps(answer));
// Wait for media negotiation complete signal.
CPPUNIT_ASSERT(waitForSignal(bobData_,
DRing::CallSignal::MediaNegotiationStatus::name,
DRing::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS));
// Wait for the StateChange signal.
CPPUNIT_ASSERT(
waitForSignal(bobData_, DRing::CallSignal::StateChange::name, StateEvent::CURRENT));
JAMI_INFO("BOB answered the call [%s]", bobData_.callId_.c_str());
// Wait for media negotiation complete signal.
CPPUNIT_ASSERT(waitForSignal(aliceData_,
DRing::CallSignal::MediaNegotiationStatus::name,
DRing::Media::MediaNegotiationStatusEvents::NEGOTIATION_SUCCESS));
// Validate ICE candidates
validate_ice_candidates(aliceData_, accountType, hasUpnp_, upnpAddrSameAsPublished_);
validate_ice_candidates(bobData_, accountType, hasUpnp_, upnpAddrSameAsPublished_);
// Give some time to media to start.
std::this_thread::sleep_for(std::chrono::seconds(2));
// Bob hang-up.
JAMI_INFO("Hang up BOB's call and wait for ALICE to hang up");
Manager::instance().hangupCall(bobData_.callId_);
CPPUNIT_ASSERT(
waitForSignal(bobData_, DRing::CallSignal::StateChange::name, StateEvent::HUNGUP));
CPPUNIT_ASSERT(
waitForSignal(aliceData_, DRing::CallSignal::StateChange::name, StateEvent::HUNGUP));
CPPUNIT_ASSERT(waitForSignal(bobData_, DRing::CallSignal::StateChange::name, StateEvent::OVER));
CPPUNIT_ASSERT(
waitForSignal(aliceData_, DRing::CallSignal::StateChange::name, StateEvent::OVER));
JAMI_INFO("Call terminated on both sides");
// Reset signals
aliceData_.signals_.clear();
aliceData_.callId_.clear();
bobData_.signals_.clear();
bobData_.callId_.clear();
}
void
IceMediaCandExchangeTest::check_upnp()
{
auto accountType = DRing::Account::ProtocolNames::RING;
setupAccounts(aliceData_, bobData_, accountType);
auto const& account = Manager::instance().getAccount<JamiAccount>(aliceData_.accountId_);
auto publishedAddr = account->getPublishedIpAddress(AF_INET);
const std::chrono::seconds TIME_OUT {15};
auto upnpCtrl = std::make_shared<upnp::Controller>();
upnp::Mapping map {upnp::PortType::UDP};
std::string upnpAddr {};
map.setNotifyCallback([this_ = this, &publishedAddr, &upnpAddr](
upnp::Mapping::sharedPtr_t mapRes) {
if (mapRes->getState() == upnp::MappingState::OPEN) {
upnpAddr = mapRes->getExternalAddress();
this_->upnpAddrSameAsPublished_ = publishedAddr.toString(false).compare(upnpAddr) == 0;
this_->hasUpnp_ = true;
this_->upnpCv_.notify_one();
} else {
this_->upnpAddrSameAsPublished_ = false;
this_->hasUpnp_ = false;
}
});
upnpCtrl->reserveMapping(map);
JAMI_INFO("Waiting for upnp request response ...");
std::unique_lock<std::mutex> lock(upnpMtx_);
auto res = upnpCv_.wait_for(lock, TIME_OUT, [this_ = this] { return this_->hasUpnp_; });
if (res) {
JAMI_INFO("UPNP is available");
if (upnpAddrSameAsPublished_) {
JAMI_INFO("UPNP address [%s] same as published address: expect only one srflx cand per "
"component",
upnpAddr.c_str());
} else {
JAMI_INFO("UPNP [%s] and published [%s] addresses differ: expect two srflx cand per "
"component",
upnpAddr.c_str(),
publishedAddr.toString().c_str());
}
} else {
JAMI_WARN("UPNP is not available (timeout!)");
}
}
void
IceMediaCandExchangeTest::jami_account_no_turn()
{
JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
auto accountType = DRing::Account::ProtocolNames::RING;
setupAccounts(aliceData_, bobData_, accountType);
bobData_.turnEnabled_ = false;
aliceData_.turnEnabled_ = false;
{
aliceData_.upnpEnabled_ = false;
bobData_.upnpEnabled_ = false;
configureAccount(aliceData_, accountType);
configureAccount(bobData_, accountType);
test_call(accountType);
}
{
aliceData_.upnpEnabled_ = true;
bobData_.upnpEnabled_ = false;
configureAccount(aliceData_, accountType);
configureAccount(bobData_, accountType);
test_call(accountType);
}
{
aliceData_.upnpEnabled_ = false;
bobData_.upnpEnabled_ = true;
configureAccount(aliceData_, accountType);
configureAccount(bobData_, accountType);
test_call(accountType);
}
{
aliceData_.upnpEnabled_ = true;
bobData_.upnpEnabled_ = true;
configureAccount(aliceData_, accountType);
configureAccount(bobData_, accountType);
test_call(accountType);
}
}
void
IceMediaCandExchangeTest::jami_account_with_turn()
{
JAMI_INFO("=== Begin test %s ===", __FUNCTION__);
auto accountType = DRing::Account::ProtocolNames::RING;
setupAccounts(aliceData_, bobData_, accountType);
bobData_.turnEnabled_ = true;
aliceData_.turnEnabled_ = true;
{
aliceData_.upnpEnabled_ = false;
bobData_.upnpEnabled_ = false;
configureAccount(aliceData_, accountType);
configureAccount(bobData_, accountType);
test_call(accountType);
}
{
aliceData_.upnpEnabled_ = true;
bobData_.upnpEnabled_ = false;
configureAccount(aliceData_, accountType);
configureAccount(bobData_, accountType);
test_call(accountType);
}
{
aliceData_.upnpEnabled_ = false;
bobData_.upnpEnabled_ = true;
configureAccount(aliceData_, accountType);
configureAccount(bobData_, accountType);
test_call(accountType);
}
{
aliceData_.upnpEnabled_ = true;
bobData_.upnpEnabled_ = true;
configureAccount(aliceData_, accountType);
configureAccount(bobData_, accountType);
test_call(accountType);
}
}
} // namespace test
} // namespace jami
RING_TEST_RUNNER(jami::test::IceMediaCandExchangeTest::name())