diff --git a/src/connectivity/Makefile.am b/src/connectivity/Makefile.am index d16c5cc4dc359167b0b98e3b85f55309842796c5..3c8f482f2f6191e54e4f8085caef526679b7bcec 100644 --- a/src/connectivity/Makefile.am +++ b/src/connectivity/Makefile.am @@ -18,6 +18,10 @@ libconnectivity_la_SOURCES = \ ./connectivity/peer_connection.h \ ./connectivity/sip_utils.cpp \ ./connectivity/sip_utils.h \ + ./connectivity/turn_transport.cpp \ + ./connectivity/turn_transport.h \ + ./connectivity/turn_cache.cpp \ + ./connectivity/turn_cache.h \ ./connectivity/utf8_utils.cpp \ ./connectivity/utf8_utils.h \ ./connectivity/transport/peer_channel.h diff --git a/src/connectivity/peer_connection.h b/src/connectivity/peer_connection.h index be7f102961c3e39ec6dbf51ea1b56aa54d6d4e23..b6d010b03658f7074f4fd9169785500f0a55f05e 100644 --- a/src/connectivity/peer_connection.h +++ b/src/connectivity/peer_connection.h @@ -52,9 +52,6 @@ using OnStateChangeCb = std::function<bool(tls::TlsSessionState state)>; using OnReadyCb = std::function<void(bool ok)>; using onShutdownCb = std::function<void(void)>; -class TurnTransport; -class ConnectedTurnTransport; - //============================================================================== class Stream diff --git a/src/connectivity/turn_cache.cpp b/src/connectivity/turn_cache.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2a6d856f92c6421fecf115af063f0d5312190d0d --- /dev/null +++ b/src/connectivity/turn_cache.cpp @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * + * Author: Sébastien Blin <sebastien.blin@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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "connectivity/turn_cache.h" + +#include "logger.h" +#include "fileutils.h" +#include "manager.h" +#include "opendht/thread_pool.h" // TODO remove asio + +namespace jami { + +TurnCache::TurnCache(const std::string& accountId, + const std::string& cachePath, + const TurnTransportParams& params, + bool enabled) + : accountId_(accountId) + , cachePath_(cachePath) + , io_context(Manager::instance().ioContext()) +{ + refreshTimer_ = std::make_unique<asio::steady_timer>(*io_context, + std::chrono::steady_clock::now()); + reconfigure(params, enabled); +} + +TurnCache::~TurnCache() {} + +std::optional<IpAddr> +TurnCache::getResolvedTurn(uint16_t family) const +{ + if (family == AF_INET && cacheTurnV4_) { + return *cacheTurnV4_; + } else if (family == AF_INET6 && cacheTurnV6_) { + return *cacheTurnV6_; + } + return std::nullopt; +} + +void +TurnCache::reconfigure(const TurnTransportParams& params, bool enabled) +{ + params_ = params; + enabled_ = enabled; + { + std::lock_guard<std::mutex> lk(cachedTurnMutex_); + // Force re-resolution + isRefreshing_ = false; + cacheTurnV4_.reset(); + cacheTurnV6_.reset(); + testTurnV4_.reset(); + testTurnV6_.reset(); + } + refreshTimer_->expires_at(std::chrono::steady_clock::now()); + refreshTimer_->async_wait(std::bind(&TurnCache::refresh, this, std::placeholders::_1)); +} + +void +TurnCache::refresh(const asio::error_code& ec) +{ + if (ec == asio::error::operation_aborted) + return; + // The resolution of the TURN server can take quite some time (if timeout). + // So, run this in its own io thread to avoid to block the main thread. + // Avoid multiple refresh + if (isRefreshing_.exchange(true)) + return; + if (!enabled_) { + // In this case, we do not use any TURN server + std::lock_guard<std::mutex> lk(cachedTurnMutex_); + cacheTurnV4_.reset(); + cacheTurnV6_.reset(); + isRefreshing_ = false; + return; + } + JAMI_INFO("[Account %s] Refresh cache for TURN server resolution", accountId_.c_str()); + // Retrieve old cached value if available. + // This means that we directly get the correct value when launching the application on the + // same network + // No need to resolve, it's already a valid address + auto server = params_.domain; + if (IpAddr::isValid(server, AF_INET)) { + testTurn(IpAddr(server, AF_INET)); + return; + } else if (IpAddr::isValid(server, AF_INET6)) { + testTurn(IpAddr(server, AF_INET6)); + return; + } + // Else cache resolution result + fileutils::recursive_mkdir(cachePath_ + DIR_SEPARATOR_STR + "domains", 0700); + auto pathV4 = cachePath_ + DIR_SEPARATOR_STR + "domains" + DIR_SEPARATOR_STR + "v4." + server; + IpAddr testV4, testV6; + if (auto turnV4File = std::ifstream(pathV4)) { + std::string content((std::istreambuf_iterator<char>(turnV4File)), + std::istreambuf_iterator<char>()); + testV4 = IpAddr(content, AF_INET); + } + auto pathV6 = cachePath_ + DIR_SEPARATOR_STR + "domains" + DIR_SEPARATOR_STR + "v6." + server; + if (auto turnV6File = std::ifstream(pathV6)) { + std::string content((std::istreambuf_iterator<char>(turnV6File)), + std::istreambuf_iterator<char>()); + testV6 = IpAddr(content, AF_INET6); + } + // Resolve just in case. The user can have a different connectivity + auto turnV4 = IpAddr {server, AF_INET}; + { + if (turnV4) { + // Cache value to avoid a delay when starting up Jami + std::ofstream turnV4File(pathV4); + turnV4File << turnV4.toString(); + } else + fileutils::remove(pathV4, true); + // Update TURN + testV4 = IpAddr(std::move(turnV4)); + } + auto turnV6 = IpAddr {server, AF_INET6}; + { + if (turnV6) { + // Cache value to avoid a delay when starting up Jami + std::ofstream turnV6File(pathV6); + turnV6File << turnV6.toString(); + } else + fileutils::remove(pathV6, true); + // Update TURN + testV6 = IpAddr(std::move(turnV6)); + } + if (testV4) + testTurn(testV4); + if (testV6) + testTurn(testV6); + + refreshTurnDelay(!testV4 && !testV6); +} + +void +TurnCache::testTurn(IpAddr server) +{ + TurnTransportParams params = params_; + params.server = server; + std::lock_guard<std::mutex> lk(cachedTurnMutex_); + auto& turn = server.isIpv4() ? testTurnV4_ : testTurnV6_; + turn.reset(); // Stop previous TURN + try { + turn = std::make_unique<TurnTransport>( + params, std::move([this, server](bool ok) { + std::lock_guard<std::mutex> lk(cachedTurnMutex_); + auto& cacheTurn = server.isIpv4() ? cacheTurnV4_ : cacheTurnV6_; + if (!ok) { + JAMI_ERROR("Connection to {:s} failed - reset", server.toString()); + cacheTurn.reset(); + } else { + JAMI_DEBUG("Connection to {:s} ready", server.toString()); + cacheTurn = std::make_unique<IpAddr>(server); + } + refreshTurnDelay(!cacheTurnV6_ && !cacheTurnV4_); + auto& turn = server.isIpv4() ? testTurnV4_ : testTurnV6_; + turn->shutdown(); + })); + } catch (const std::exception& e) { + JAMI_ERROR("TurnTransport creation error: {}", e.what()); + } +} + +void +TurnCache::refreshTurnDelay(bool scheduleNext) +{ + isRefreshing_ = false; + if (scheduleNext) { + JAMI_WARNING("[Account {:s}] Cache for TURN resolution failed.", accountId_); + refreshTimer_->expires_at(std::chrono::steady_clock::now() + turnRefreshDelay_); + refreshTimer_->async_wait(std::bind(&TurnCache::refresh, this, std::placeholders::_1)); + if (turnRefreshDelay_ < std::chrono::minutes(30)) + turnRefreshDelay_ *= 2; + } else { + JAMI_DEBUG("[Account {:s}] Cache refreshed for TURN resolution", accountId_); + turnRefreshDelay_ = std::chrono::seconds(10); + } +} + +} // namespace jami diff --git a/src/connectivity/turn_cache.h b/src/connectivity/turn_cache.h new file mode 100644 index 0000000000000000000000000000000000000000..967ede7abb8dabd42318af168889d80912ab10d0 --- /dev/null +++ b/src/connectivity/turn_cache.h @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * + * Author: Sébastien Blin <sebastien.blin@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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include "connectivity/ip_utils.h" +#include "connectivity/turn_transport.h" + +#include <atomic> +#include <asio.hpp> +#include <chrono> +#include <functional> +#include <memory> +#include <mutex> +#include <optional> +#include <string> + +namespace jami { + +class TurnCache +{ +public: + TurnCache(const std::string& accountId, + const std::string& cachePath, + const TurnTransportParams& params, + bool enabled); + ~TurnCache(); + + std::optional<IpAddr> getResolvedTurn(uint16_t family = AF_INET) const; + /** + * Pass a new configuration for the cache + * @param param The new configuration + */ + void reconfigure(const TurnTransportParams& params, bool enabled); + /** + * Refresh cache from current configuration + */ + void refresh(const asio::error_code& ec = {}); + +private: + std::string accountId_; + std::string cachePath_; + TurnTransportParams params_; + std::atomic_bool enabled_ {false}; + /** + * Avoid to refresh the cache multiple times + */ + std::atomic_bool isRefreshing_ {false}; + /** + * This will cache the turn server resolution each time we launch + * Jami, or for each connectivityChange() + */ + void testTurn(IpAddr server); + std::unique_ptr<TurnTransport> testTurnV4_; + std::unique_ptr<TurnTransport> testTurnV6_; + + // Used to detect if a turn server is down. + void refreshTurnDelay(bool scheduleNext); + std::chrono::seconds turnRefreshDelay_ {std::chrono::seconds(10)}; + + // Store resoved turn addresses + mutable std::mutex cachedTurnMutex_ {}; + std::unique_ptr<IpAddr> cacheTurnV4_ {}; + std::unique_ptr<IpAddr> cacheTurnV6_ {}; + + // io + std::shared_ptr<asio::io_context> io_context; + std::unique_ptr<asio::steady_timer> refreshTimer_; +}; + +} // namespace jami diff --git a/src/connectivity/turn_transport.cpp b/src/connectivity/turn_transport.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5675a6c5d6e292f32ef6f48e6cbdf19e3831e256 --- /dev/null +++ b/src/connectivity/turn_transport.cpp @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2004-2022 Savoir-faire Linux Inc. + * + * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> + * Author: Sébastien Blin <sebastien.blin@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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "connectivity/turn_transport.h" +#include "connectivity/sip_utils.h" +#include "manager.h" + +#include <atomic> +#include <thread> + +#include <pjnath.h> +#include <pjlib-util.h> +#include <pjlib.h> + +#define TRY(ret) \ + do { \ + if ((ret) != PJ_SUCCESS) \ + throw std::runtime_error(#ret " failed"); \ + } while (0) + +namespace jami { + +class TurnTransport::Impl +{ +public: + Impl(std::function<void(bool)>&& cb) { cb_ = std::move(cb); } + ~Impl(); + + /** + * Detect new TURN state + */ + void onTurnState(pj_turn_state_t old_state, pj_turn_state_t new_state); + + /** + * Pool events from pjsip + */ + void ioJob(); + + void start() + { + ioWorker = std::thread([this] { ioJob(); }); + } + + void stop() { stopped_ = true; } + + TurnTransportParams settings; + + pj_caching_pool poolCache {}; + pj_pool_t* pool {nullptr}; + pj_stun_config stunConfig {}; + pj_turn_sock* relay {nullptr}; + pj_str_t relayAddr {}; + IpAddr peerRelayAddr; // address where peers should connect to + IpAddr mappedAddr; + std::function<void(bool)> cb_; + + std::thread ioWorker; + std::atomic_bool stopped_ {false}; +}; + +TurnTransport::Impl::~Impl() +{ + stop(); + if (ioWorker.joinable()) + ioWorker.join(); + pj_caching_pool_destroy(&poolCache); +} +void +TurnTransport::Impl::onTurnState(pj_turn_state_t old_state, pj_turn_state_t new_state) +{ + if (new_state == PJ_TURN_STATE_READY) { + pj_turn_session_info info; + pj_turn_sock_get_info(relay, &info); + peerRelayAddr = IpAddr {info.relay_addr}; + mappedAddr = IpAddr {info.mapped_addr}; + JAMI_DEBUG("TURN server ready, peer relay address: {:s}", + peerRelayAddr.toString(true, true).c_str()); + cb_(true); + } else if (old_state <= PJ_TURN_STATE_READY and new_state > PJ_TURN_STATE_READY) { + JAMI_WARNING("TURN server disconnected ({:s})", pj_turn_state_name(new_state)); + cb_(false); + } +} +void +TurnTransport::Impl::ioJob() +{ + const pj_time_val delay = {0, 10}; + while (!stopped_) { + pj_ioqueue_poll(stunConfig.ioqueue, &delay); + pj_timer_heap_poll(stunConfig.timer_heap, nullptr); + } +} + +TurnTransport::TurnTransport(const TurnTransportParams& params, std::function<void(bool)>&& cb) + : pimpl_ {new Impl(std::move(cb))} +{ + auto server = params.server; + if (!server.getPort()) + server.setPort(PJ_STUN_PORT); + if (server.isUnspecified()) + throw std::invalid_argument("invalid turn server address"); + pimpl_->settings = params; + // PJSIP memory pool + pj_caching_pool_init(&pimpl_->poolCache, &pj_pool_factory_default_policy, 0); + pimpl_->pool = pj_pool_create(&pimpl_->poolCache.factory, "TurnTransport", 512, 512, nullptr); + if (not pimpl_->pool) + throw std::runtime_error("pj_pool_create() failed"); + // STUN config + pj_stun_config_init(&pimpl_->stunConfig, &pimpl_->poolCache.factory, 0, nullptr, nullptr); + // create global timer heap + TRY(pj_timer_heap_create(pimpl_->pool, 1000, &pimpl_->stunConfig.timer_heap)); + // create global ioqueue + TRY(pj_ioqueue_create(pimpl_->pool, 16, &pimpl_->stunConfig.ioqueue)); + // TURN callbacks + pj_turn_sock_cb relay_cb; + pj_bzero(&relay_cb, sizeof(relay_cb)); + relay_cb.on_state = + [](pj_turn_sock* relay, pj_turn_state_t old_state, pj_turn_state_t new_state) { + auto pimpl = static_cast<Impl*>(pj_turn_sock_get_user_data(relay)); + pimpl->onTurnState(old_state, new_state); + }; + // TURN socket config + pj_turn_sock_cfg turn_sock_cfg; + pj_turn_sock_cfg_default(&turn_sock_cfg); + turn_sock_cfg.max_pkt_size = 4096; + // TURN socket creation + TRY(pj_turn_sock_create(&pimpl_->stunConfig, + server.getFamily(), + PJ_TURN_TP_TCP, + &relay_cb, + &turn_sock_cfg, + &*this->pimpl_, + &pimpl_->relay)); + // TURN allocation setup + pj_turn_alloc_param turn_alloc_param; + pj_turn_alloc_param_default(&turn_alloc_param); + turn_alloc_param.peer_conn_type = PJ_TURN_TP_TCP; + pj_stun_auth_cred cred; + pj_bzero(&cred, sizeof(cred)); + cred.type = PJ_STUN_AUTH_CRED_STATIC; + pj_cstr(&cred.data.static_cred.realm, pimpl_->settings.realm.c_str()); + pj_cstr(&cred.data.static_cred.username, pimpl_->settings.username.c_str()); + cred.data.static_cred.data_type = PJ_STUN_PASSWD_PLAIN; + pj_cstr(&cred.data.static_cred.data, pimpl_->settings.password.c_str()); + pimpl_->relayAddr = pj_strdup3(pimpl_->pool, server.toString().c_str()); + // TURN connection/allocation + JAMI_DEBUG("Connecting to TURN {:s}", server.toString(true, true)); + TRY(pj_turn_sock_alloc(pimpl_->relay, + &pimpl_->relayAddr, + server.getPort(), + nullptr, + &cred, + &turn_alloc_param)); + pimpl_->start(); +} + +TurnTransport::~TurnTransport() {} + +void +TurnTransport::shutdown() +{ + pimpl_->stop(); +} + +} // namespace jami diff --git a/src/connectivity/turn_transport.h b/src/connectivity/turn_transport.h new file mode 100644 index 0000000000000000000000000000000000000000..5602221291fca9d0847fd04fb16bca2e62ec9f0d --- /dev/null +++ b/src/connectivity/turn_transport.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * + * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> + * Author: Sébastien Blin <sebastien.blin@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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include "connectivity/ip_utils.h" + +#include <functional> +#include <memory> +#include <string> + +namespace jami { + +struct TurnTransportParams +{ + IpAddr server; + std::string domain; // Used by cache_turn + // Plain Credentials + std::string realm; + std::string username; + std::string password; +}; + +/** + * This class is used to test connection to TURN servers + * No other logic is implemented. + */ +class TurnTransport +{ +public: + TurnTransport(const TurnTransportParams& param, std::function<void(bool)>&& cb); + ~TurnTransport(); + void shutdown(); + +private: + TurnTransport() = delete; + class Impl; + std::unique_ptr<Impl> pimpl_; +}; + +} // namespace jami diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index ee003ccfeb8b10a6a590e7732ce1c72c341a046a..7c3d0e06c0a10b53dcbf8d44af6b4ecb01989d4c 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -290,8 +290,7 @@ JamiAccount::JamiAccount(const std::string& accountId) , dataPath_(cachePath_ + DIR_SEPARATOR_STR "values") , connectionManager_ {} , nonSwarmTransferManager_(std::make_shared<TransferManager>(accountId, "")) -{ -} +{} JamiAccount::~JamiAccount() noexcept { @@ -902,7 +901,7 @@ JamiAccount::loadConfig() registeredName_ = config().registeredName; try { auto str = fileutils::loadCacheTextFile(cachePath_ + DIR_SEPARATOR_STR "dhtproxy", - std::chrono::hours(24 * 7)); + std::chrono::hours(24 * 7)); std::string err; Json::Value root; Json::CharReaderBuilder rbuilder; @@ -1965,11 +1964,14 @@ JamiAccount::doRegister_() getAccountID().c_str(), name.c_str()); - if (this->config().turnEnabled && !cacheTurnV4_) { - // If TURN is enabled, but no TURN cached, there can be a temporary resolution - // error to solve. Sometimes, a connectivity change is not enough, so even if - // this case is really rare, it should be easy to avoid. - cacheTurnServers(); + if (this->config().turnEnabled && turnCache_) { + auto addr = turnCache_->getResolvedTurn(); + if (addr == std::nullopt) { + // If TURN is enabled, but no TURN cached, there can be a temporary + // resolution error to solve. Sometimes, a connectivity change is not + // enough, so even if this case is really rare, it should be easy to avoid. + turnCache_->refresh(); + } } auto uri = Uri(name); @@ -2278,7 +2280,7 @@ JamiAccount::setRegistrationState(RegistrationState state, if (registrationState_ != state) { if (state == RegistrationState::REGISTERED) { JAMI_WARN("[Account %s] connected", getAccountID().c_str()); - cacheTurnServers(); + turnCache_->refresh(); storeActiveIpAddress(); } else if (state == RegistrationState::TRYING) { JAMI_WARN("[Account %s] connecting…", getAccountID().c_str()); @@ -2342,11 +2344,10 @@ JamiAccount::setCertificateStatus(const std::string& cert_id, { bool done = accountManager_ ? accountManager_->setCertificateStatus(cert_id, status) : false; if (done) { - findCertificate(cert_id); - emitSignal<libjami::ConfigurationSignal::CertificateStateChanged>(getAccountID(), - cert_id, - tls::TrustStore::statusToStr( - status)); + dht_->findCertificate(dht::InfoHash(cert_id), + [](const std::shared_ptr<dht::crypto::Certificate>& crt) {}); + emitSignal<libjami::ConfigurationSignal::CertificateStateChanged>( + getAccountID(), cert_id, tls::TrustStore::statusToStr(status)); } return done; } @@ -3532,106 +3533,6 @@ JamiAccount::handleMessage(const std::string& from, const std::pair<std::string, return false; } -void -JamiAccount::cacheTurnServers() -{ - // The resolution of the TURN server can take quite some time (if timeout). - // So, run this in its own io thread to avoid to block the main thread. - dht::ThreadPool::io().run([w = weak()] { - auto this_ = w.lock(); - if (not this_) - return; - // Avoid multiple refresh - if (this_->isRefreshing_.exchange(true)) - return; - const auto& conf = this_->config(); - if (!conf.turnEnabled) { - // In this case, we do not use any TURN server - std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_); - this_->cacheTurnV4_.reset(); - this_->cacheTurnV6_.reset(); - this_->isRefreshing_ = false; - return; - } - JAMI_INFO("[Account %s] Refresh cache for TURN server resolution", - this_->getAccountID().c_str()); - // Retrieve old cached value if available. - // This means that we directly get the correct value when launching the application on the - // same network - std::string server = conf.turnServer.empty() ? DEFAULT_TURN_SERVER : conf.turnServer; - // No need to resolve, it's already a valid address - if (IpAddr::isValid(server, AF_INET)) { - this_->cacheTurnV4_ = std::make_unique<IpAddr>(server, AF_INET); - this_->isRefreshing_ = false; - return; - } else if (IpAddr::isValid(server, AF_INET6)) { - this_->cacheTurnV6_ = std::make_unique<IpAddr>(server, AF_INET6); - this_->isRefreshing_ = false; - return; - } - // Else cache resolution result - fileutils::recursive_mkdir(this_->cachePath_ + DIR_SEPARATOR_STR + "domains", 0700); - auto pathV4 = this_->cachePath_ + DIR_SEPARATOR_STR + "domains" + DIR_SEPARATOR_STR + "v4." - + server; - if (auto turnV4File = std::ifstream(pathV4)) { - std::string content((std::istreambuf_iterator<char>(turnV4File)), - std::istreambuf_iterator<char>()); - std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_); - this_->cacheTurnV4_ = std::make_unique<IpAddr>(content, AF_INET); - } - auto pathV6 = this_->cachePath_ + DIR_SEPARATOR_STR + "domains" + DIR_SEPARATOR_STR + "v6." - + server; - if (auto turnV6File = std::ifstream(pathV6)) { - std::string content((std::istreambuf_iterator<char>(turnV6File)), - std::istreambuf_iterator<char>()); - std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_); - this_->cacheTurnV6_ = std::make_unique<IpAddr>(content, AF_INET6); - } - // Resolve just in case. The user can have a different connectivity - auto turnV4 = IpAddr {server, AF_INET}; - { - if (turnV4) { - // Cache value to avoid a delay when starting up Jami - std::ofstream turnV4File(pathV4); - turnV4File << turnV4.toString(); - } else - fileutils::remove(pathV4, true); - std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_); - // Update TURN - this_->cacheTurnV4_ = std::make_unique<IpAddr>(std::move(turnV4)); - } - auto turnV6 = IpAddr {server, AF_INET6}; - { - if (turnV6) { - // Cache value to avoid a delay when starting up Jami - std::ofstream turnV6File(pathV6); - turnV6File << turnV6.toString(); - } else - fileutils::remove(pathV6, true); - std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_); - // Update TURN - this_->cacheTurnV6_ = std::make_unique<IpAddr>(std::move(turnV6)); - } - this_->isRefreshing_ = false; - if (!this_->cacheTurnV6_ && !this_->cacheTurnV4_) { - JAMI_WARN("[Account %s] Cache for TURN resolution failed.", - this_->getAccountID().c_str()); - Manager::instance().scheduleTaskIn( - [w]() { - if (auto shared = w.lock()) - shared->cacheTurnServers(); - }, - this_->turnRefreshDelay_); - if (this_->turnRefreshDelay_ < std::chrono::minutes(30)) - this_->turnRefreshDelay_ *= 2; - } else { - JAMI_INFO("[Account %s] Cache refreshed for TURN resolution", - this_->getAccountID().c_str()); - this_->turnRefreshDelay_ = std::chrono::seconds(10); - } - }); -} - void JamiAccount::callConnectionClosed(const DeviceId& deviceId, bool eraseDummy) { diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h index 7c68f9f82ee815eae2a2bdd089c906b145c74638..72761daac1a71a3bef7f232c85f75a099023d16e 100644 --- a/src/jamidht/jamiaccount.h +++ b/src/jamidht/jamiaccount.h @@ -36,6 +36,7 @@ #include "data_transfer.h" #include "uri.h" #include "jamiaccount_config.h" +#include "connectivity/peer_connection.h" #include "noncopyable.h" #include "connectivity/ip_utils.h" @@ -611,7 +612,8 @@ private: struct BuddyInfo; struct DiscoveredPeer; - inline std::string getProxyConfigKey() const { + inline std::string getProxyConfigKey() const + { const auto& conf = config(); return dht::InfoHash::get(conf.proxyServer + conf.proxyListUrl).toString(); } @@ -785,7 +787,12 @@ private: * This will cache the turn server resolution each time we launch * Jami, or for each connectivityChange() */ + // TODO move in separate class + void testTurn(IpAddr server); void cacheTurnServers(); + std::unique_ptr<TurnTransport> testTurnV4_; + std::unique_ptr<TurnTransport> testTurnV6_; + void refreshTurnDelay(bool scheduleNext); std::chrono::seconds turnRefreshDelay_ {std::chrono::seconds(10)}; diff --git a/src/meson.build b/src/meson.build index 06da38ed7c12ddf3805893e7127e308b2e4e7999..b0b5b08d658b3c67c36f02e033104a38f74464fb 100644 --- a/src/meson.build +++ b/src/meson.build @@ -29,6 +29,8 @@ libjami_sources = files( 'connectivity/multiplexed_socket.cpp', 'connectivity/peer_connection.cpp', 'connectivity/sip_utils.cpp', + 'connectivity/turn_cache.cpp', + 'connectivity/turn_transport.cpp', 'connectivity/utf8_utils.cpp', 'im/instant_messaging.cpp', 'im/message_engine.cpp', diff --git a/src/sip/sipaccountbase.cpp b/src/sip/sipaccountbase.cpp index 32c9aa56769d997d31f5569d78ee2861ad9b664a..1ecf03a0af7d46c20e420dbd54ebb257d11bd0a2 100644 --- a/src/sip/sipaccountbase.cpp +++ b/src/sip/sipaccountbase.cpp @@ -143,6 +143,20 @@ SIPAccountBase::loadConfig() IpAddr publishedIp {conf.publishedIp}; if (not conf.publishedSameasLocal and publishedIp) setPublishedAddress(publishedIp); + TurnTransportParams turnParams; + turnParams.domain = conf.turnServer; + turnParams.username = conf.turnServerUserName; + turnParams.password = conf.turnServerPwd; + turnParams.realm = conf.turnServerRealm; + if (!turnCache_) { + auto cachePath = fileutils::get_cache_dir() + DIR_SEPARATOR_STR + getAccountID(); + turnCache_ = std::make_unique<TurnCache>(getAccountID(), + cachePath, + turnParams, + conf.turnEnabled); + } else { + turnCache_->reconfigure(turnParams, conf.turnEnabled); + } } std::map<std::string, std::string> @@ -241,13 +255,11 @@ SIPAccountBase::getIceOptions() const noexcept // if (config().stunEnabled) // opts.stunServers.emplace_back(StunServerInfo().setUri(stunServer_)); - if (config().turnEnabled) { - auto cached = false; - std::lock_guard<std::mutex> lk(cachedTurnMutex_); - cached = cacheTurnV4_ || cacheTurnV6_; - if (cacheTurnV4_ && *cacheTurnV4_) { + if (config().turnEnabled && turnCache_) { + auto turnAddr = turnCache_->getResolvedTurn(); + if (turnAddr != std::nullopt) { opts.turnServers.emplace_back(TurnServerInfo() - .setUri(cacheTurnV4_->toString(true)) + .setUri(turnAddr->toString(true)) .setUsername(config().turnServerUserName) .setPassword(config().turnServerPwd) .setRealm(config().turnServerRealm)); @@ -261,14 +273,6 @@ SIPAccountBase::getIceOptions() const noexcept // .setPassword(turnServerPwd_) // .setRealm(turnServerRealm_)); //} - // Nothing cached, so do the resolution - if (!cached) { - opts.turnServers.emplace_back(TurnServerInfo() - .setUri(config().turnServer) - .setUsername(config().turnServerUserName) - .setPassword(config().turnServerPwd) - .setRealm(config().turnServerRealm)); - } } return opts; } diff --git a/src/sip/sipaccountbase.h b/src/sip/sipaccountbase.h index a660d8266400e11f30886f2a90f284bb81fc39a4..70abe43ac5189dee7b6d467ac1cfce68e8ff7248 100644 --- a/src/sip/sipaccountbase.h +++ b/src/sip/sipaccountbase.h @@ -28,6 +28,7 @@ #include "connectivity/sip_utils.h" #include "connectivity/ip_utils.h" +#include "connectivity/turn_cache.h" #include "noncopyable.h" #include "im/message_engine.h" #include "sipaccountbase_config.h" @@ -85,7 +86,8 @@ public: virtual ~SIPAccountBase() noexcept; - const SipAccountBaseConfig& config() const { + const SipAccountBaseConfig& config() const + { return *static_cast<const SipAccountBaseConfig*>(&Account::config()); } @@ -216,21 +218,6 @@ public: public: // overloaded methods virtual void flush() override; - /** - * Return current turn resolved addresses - * @return {unique_ptr(v4 resolved), unique_ptr(v6 resolved)} - */ - std::array<std::unique_ptr<IpAddr>, 2> turnCache() - { - std::lock_guard<std::mutex> lk {cachedTurnMutex_}; - std::array<std::unique_ptr<IpAddr>, 2> result = {}; - if (cacheTurnV4_ && *cacheTurnV4_) - result[0] = std::make_unique<IpAddr>(*cacheTurnV4_); - if (cacheTurnV6_ && *cacheTurnV6_) - result[1] = std::make_unique<IpAddr>(*cacheTurnV6_); - return result; - } - protected: /** * Retrieve volatile details such as recent registration errors @@ -279,9 +266,7 @@ protected: std::chrono::steady_clock::time_point::min()}; std::shared_ptr<Task> composingTimeout_; - mutable std::mutex cachedTurnMutex_ {}; - std::unique_ptr<IpAddr> cacheTurnV4_ {}; - std::unique_ptr<IpAddr> cacheTurnV6_ {}; + std::unique_ptr<TurnCache> turnCache_; private: NON_COPYABLE(SIPAccountBase); diff --git a/test/unitTest/call/call.cpp b/test/unitTest/call/call.cpp index 4e6f24965d4c93a434dbe30e3e0295b98901795e..a52088dc6b24cfc5a26ee191bd0049b61fa8ba7e 100644 --- a/test/unitTest/call/call.cpp +++ b/test/unitTest/call/call.cpp @@ -38,7 +38,7 @@ using namespace libjami::Account; using namespace libjami::Call::Details; - +using namespace std::literals::chrono_literals; namespace jami { namespace test { @@ -48,7 +48,8 @@ public: CallTest() { // Init daemon - libjami::init(libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG)); + libjami::init( + libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG)); if (not Manager::instance().initialized) CPPUNIT_ASSERT(libjami::start("jami-sample.yml")); } @@ -68,6 +69,7 @@ private: void testDeclineMultiDevice(); void testTlsInfosPeerCertificate(); void testSocketInfos(); + void testInvalidTurn(); CPPUNIT_TEST_SUITE(CallTest); CPPUNIT_TEST(testCall); @@ -76,6 +78,7 @@ private: CPPUNIT_TEST(testDeclineMultiDevice); CPPUNIT_TEST(testTlsInfosPeerCertificate); CPPUNIT_TEST(testSocketInfos); + CPPUNIT_TEST(testInvalidTurn); CPPUNIT_TEST_SUITE_END(); }; @@ -138,12 +141,12 @@ CallTest::testCall() JAMI_INFO("Start call between alice and Bob"); auto call = libjami::placeCallWithMedia(aliceId, bobUri, {}); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return callReceived.load(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callReceived.load(); })); JAMI_INFO("Stop call between alice and Bob"); callStopped = 0; Manager::instance().hangupCall(aliceId, call); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return callStopped == 2; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callStopped == 2; })); } void @@ -190,17 +193,16 @@ CallTest::testCachedCall() successfullyConnected = true; cv.notify_one(); }); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(30), [&] { return successfullyConnected.load(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return successfullyConnected.load(); })); JAMI_INFO("Start call between alice and Bob"); auto call = libjami::placeCallWithMedia(aliceId, bobUri, {}); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return callReceived.load(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callReceived.load(); })); callStopped = 0; JAMI_INFO("Stop call between alice and Bob"); Manager::instance().hangupCall(aliceId, call); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return callStopped == 2; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callStopped == 2; })); } void @@ -292,14 +294,12 @@ CallTest::testDeclineMultiDevice() JAMI_INFO("Start call between alice and Bob"); auto call = libjami::placeCallWithMedia(aliceId, bobUri, {}); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { - return callReceived == 2 && !callIdBob.empty(); - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callReceived == 2 && !callIdBob.empty(); })); JAMI_INFO("Stop call between alice and Bob"); callStopped = 0; Manager::instance().refuseCall(bobId, callIdBob); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callStopped.load() >= 3; /* >= because there is subcalls */ })); } @@ -344,11 +344,10 @@ CallTest::testTlsInfosPeerCertificate() JAMI_INFO("Start call between alice and Bob"); auto callId = libjami::placeCallWithMedia(aliceId, bobUri, {}); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return !bobCallId.empty(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return !bobCallId.empty(); })); Manager::instance().answerCall(bobId, bobCallId); - CPPUNIT_ASSERT( - cv.wait_for(lk, std::chrono::seconds(30), [&] { return aliceCallState == "CURRENT"; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return aliceCallState == "CURRENT"; })); auto call = std::dynamic_pointer_cast<SIPCall>(aliceAccount->getCall(callId)); auto* transport = call->getTransport(); @@ -360,7 +359,7 @@ CallTest::testTlsInfosPeerCertificate() JAMI_INFO("Stop call between alice and Bob"); callStopped = 0; Manager::instance().hangupCall(aliceId, callId); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return callStopped == 2; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callStopped == 2; })); } void @@ -413,12 +412,10 @@ CallTest::testSocketInfos() JAMI_INFO("Start call between alice and Bob"); auto callId = libjami::placeCallWithMedia(aliceId, bobUri, {}); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return !bobCallId.empty(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return !bobCallId.empty(); })); Manager::instance().answerCall(bobId, bobCallId); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { - return aliceCallState == "CURRENT" && mediaReady; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return aliceCallState == "CURRENT" && mediaReady; })); JAMI_INFO("Detail debug"); auto details = libjami::getCallDetails(aliceId, callId); @@ -434,7 +431,55 @@ CallTest::testSocketInfos() JAMI_INFO("Stop call between alice and Bob"); callStopped = 0; Manager::instance().hangupCall(aliceId, callId); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return callStopped == 2; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callStopped == 2; })); +} + +void +CallTest::testInvalidTurn() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + auto aliceUri = aliceAccount->getUsername(); + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers; + std::atomic_bool callReceived {false}; + std::atomic<int> callStopped {0}; + // Watch signals + confHandlers.insert(libjami::exportable_callback<libjami::CallSignal::IncomingCallWithMedia>( + [&](const std::string&, + const std::string&, + const std::string&, + const std::vector<std::map<std::string, std::string>>&) { + callReceived = true; + cv.notify_one(); + })); + confHandlers.insert(libjami::exportable_callback<libjami::CallSignal::StateChange>( + [&](const std::string&, const std::string&, const std::string& state, signed) { + if (state == "OVER") { + callStopped += 1; + if (callStopped == 2) + cv.notify_one(); + } + })); + libjami::registerSignalHandlers(confHandlers); + + std::map<std::string, std::string> details; + details[ConfProperties::TURN::SERVER] = "1.1.1.1"; + libjami::setAccountDetails(aliceId, details); + + JAMI_INFO("Start call between alice and Bob"); + auto call = libjami::placeCallWithMedia(aliceId, bobUri, {}); + + CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return callReceived.load(); })); + + JAMI_INFO("Stop call between alice and Bob"); + callStopped = 0; + Manager::instance().hangupCall(aliceId, call); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callStopped == 2; })); } } // namespace test