blob: b7ec7c99fa633c77f564f3787fed1716b030cc94 [file] [log] [blame]
/*
* Copyright (C) 2004-2023 Savoir-faire Linux Inc.
*
* 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 "upnp/upnp_context.h"
#include "protocol/upnp_protocol.h"
#if HAVE_LIBNATPMP
#include "protocol/natpmp/nat_pmp.h"
#endif
#if HAVE_LIBUPNP
#include "protocol/pupnp/pupnp.h"
#endif
#include <asio.hpp>
#include <asio/steady_timer.hpp>
#if __has_include(<fmt/std.h>)
#include <fmt/std.h>
#else
#include <fmt/ostream.h>
#endif
#include <fmt/chrono.h>
namespace dhtnet {
namespace upnp {
constexpr static auto MAPPING_RENEWAL_THROTTLING_DELAY = std::chrono::seconds(10);
constexpr static int MAX_REQUEST_RETRIES = 20;
constexpr static int MAX_REQUEST_REMOVE_COUNT = 10; // TODO: increase?
constexpr static uint16_t UPNP_TCP_PORT_MIN {10000};
constexpr static uint16_t UPNP_TCP_PORT_MAX {UPNP_TCP_PORT_MIN + 5000};
constexpr static uint16_t UPNP_UDP_PORT_MIN {20000};
constexpr static uint16_t UPNP_UDP_PORT_MAX {UPNP_UDP_PORT_MIN + 5000};
UPnPContext::UPnPContext(const std::shared_ptr<asio::io_context>& ioContext, const std::shared_ptr<dht::log::Logger>& logger)
: ctx(createIoContext(ioContext, logger))
, logger_(logger)
, mappingRenewalTimer_(*ctx)
, renewalSchedulingTimer_(*ctx)
, syncTimer_(*ctx)
, connectivityChangedTimer_(*ctx)
, igdDiscoveryTimer_(*ctx)
{
if (logger_) logger_->debug("Creating UPnPContext instance [{}]", fmt::ptr(this));
// Set port ranges
portRange_.emplace(PortType::TCP, std::make_pair(UPNP_TCP_PORT_MIN, UPNP_TCP_PORT_MAX));
portRange_.emplace(PortType::UDP, std::make_pair(UPNP_UDP_PORT_MIN, UPNP_UDP_PORT_MAX));
ctx->post([this] { init(); });
}
std::shared_ptr<asio::io_context>
UPnPContext::createIoContext(const std::shared_ptr<asio::io_context>& ctx, const std::shared_ptr<dht::log::Logger>& logger) {
if (ctx) {
return ctx;
} else {
if (logger) logger->debug("UPnPContext: starting dedicated io_context thread");
auto ioCtx = std::make_shared<asio::io_context>();
ioContextRunner_ = std::make_unique<std::thread>([ioCtx, l=logger]() {
try {
auto work = asio::make_work_guard(*ioCtx);
ioCtx->run();
} catch (const std::exception& ex) {
if (l) l->error("Unexpected io_context thread exception: {}", ex.what());
}
});
return ioCtx;
}
}
void
UPnPContext::shutdown(std::condition_variable& cv)
{
if (logger_) logger_->debug("Shutdown UPnPContext instance [{}]", fmt::ptr(this));
stopUpnp(true);
for (auto const& [_, proto] : protocolList_) {
proto->terminate();
}
std::lock_guard lock(mappingMutex_);
mappingList_->clear();
controllerList_.clear();
protocolList_.clear();
shutdownComplete_ = true;
if (shutdownTimedOut_) {
// If we timed out in shutdown(), then calling notify_one is not necessary,
// and doing so anyway can cause bugs, see:
// https://git.jami.net/savoirfairelinux/dhtnet/-/issues/28
return;
}
cv.notify_one();
}
void
UPnPContext::shutdown()
{
std::unique_lock lk(mappingMutex_);
std::condition_variable cv;
ctx->post([&, this] { shutdown(cv); });
if (logger_) logger_->debug("Waiting for shutdown ...");
if (cv.wait_for(lk, std::chrono::seconds(30), [this] { return shutdownComplete_; })) {
if (logger_) logger_->debug("Shutdown completed");
} else {
if (logger_) logger_->error("Shutdown timed out");
shutdownTimedOut_ = true;
}
// NOTE: It's important to unlock mappingMutex_ here, otherwise we get a
// deadlock when the call to cv.wait_for() above times out before we return
// from proto->terminate() in shutdown(cv).
lk.unlock();
if (ioContextRunner_) {
if (logger_) logger_->debug("UPnPContext: stopping io_context thread {}", fmt::ptr(this));
ctx->stop();
ioContextRunner_->join();
ioContextRunner_.reset();
if (logger_) logger_->debug("UPnPContext: stopping io_context thread - finished {}", fmt::ptr(this));
}
}
UPnPContext::~UPnPContext()
{
if (logger_) logger_->debug("UPnPContext instance [{}] destroyed", fmt::ptr(this));
}
void
UPnPContext::init()
{
#if HAVE_LIBNATPMP
auto natPmp = std::make_shared<NatPmp>(ctx, logger_);
natPmp->setObserver(this);
protocolList_.emplace(NatProtocolType::NAT_PMP, std::move(natPmp));
#endif
#if HAVE_LIBUPNP
auto pupnp = std::make_shared<PUPnP>(ctx, logger_);
pupnp->setObserver(this);
protocolList_.emplace(NatProtocolType::PUPNP, std::move(pupnp));
#endif
}
void
UPnPContext::startUpnp()
{
assert(not controllerList_.empty());
if (logger_) logger_->debug("Starting UPNP context");
// Request a new IGD search.
for (auto const& [_, protocol] : protocolList_) {
ctx->dispatch([p=protocol] { p->searchForIgd(); });
}
started_ = true;
}
void
UPnPContext::stopUpnp(bool forceRelease)
{
if (logger_) logger_->debug("Stopping UPnP context");
connectivityChangedTimer_.cancel();
mappingRenewalTimer_.cancel();
renewalSchedulingTimer_.cancel();
syncTimer_.cancel();
syncRequested_ = false;
// Clear all current mappings
// Use a temporary list to avoid processing the mappings while holding the lock.
std::list<Mapping::sharedPtr_t> toRemoveList;
{
std::lock_guard lock(mappingMutex_);
PortType types[2] {PortType::TCP, PortType::UDP};
for (auto& type : types) {
const auto& mappingList = getMappingList(type);
for (const auto& [_, map] : mappingList) {
toRemoveList.emplace_back(map);
}
}
// Invalidate the current IGD.
currentIgd_.reset();
}
for (auto const& map : toRemoveList) {
requestRemoveMapping(map);
if (map->getAutoUpdate() && !forceRelease) {
// Set the mapping's state to PENDING so that it
// gets recreated if we restart UPnP later.
map->setState(MappingState::PENDING);
} else {
unregisterMapping(map);
}
}
// Clear all current IGDs.
for (auto const& [_, protocol] : protocolList_) {
ctx->dispatch([p=protocol]{ p->clearIgds(); });
}
started_ = false;
}
uint16_t
UPnPContext::generateRandomPort(PortType type)
{
auto minPort = type == PortType::TCP ? UPNP_TCP_PORT_MIN : UPNP_UDP_PORT_MIN;
auto maxPort = type == PortType::TCP ? UPNP_TCP_PORT_MAX : UPNP_UDP_PORT_MAX;
// Seed the generator.
static std::mt19937 gen(dht::crypto::getSeededRandomEngine());
// Define the range.
std::uniform_int_distribution<uint16_t> dist(minPort, maxPort);
return dist(gen);
}
void
UPnPContext::connectivityChanged()
{
// Debounce the connectivity change notification.
connectivityChangedTimer_.expires_after(std::chrono::milliseconds(50));
connectivityChangedTimer_.async_wait(std::bind(&UPnPContext::_connectivityChanged, this, std::placeholders::_1));
}
void
UPnPContext::_connectivityChanged(const asio::error_code& ec)
{
if (ec == asio::error::operation_aborted)
return;
auto hostAddr = ip_utils::getLocalAddr(AF_INET);
if (logger_) logger_->debug("Connectivity change check: host address {}", hostAddr.toString());
auto restartUpnp = false;
// On reception of "connectivity change" notification, the UPNP search
// will be restarted if either there is no valid IGD, or the IGD address
// changed.
if (not isReady()) {
restartUpnp = true;
} else {
// Check if the host address changed.
for (auto const& [_, protocol] : protocolList_) {
if (protocol->isReady() and hostAddr != protocol->getHostAddress()) {
if (logger_) logger_->warn("Host address changed from {} to {}",
protocol->getHostAddress().toString(),
hostAddr.toString());
protocol->clearIgds();
restartUpnp = true;
break;
}
}
}
// We have at least one valid IGD and the host address did
// not change, so no need to restart.
if (not restartUpnp) {
return;
}
// No registered controller. A new search will be performed when
// a controller is registered.
if (controllerList_.empty())
return;
if (logger_) logger_->debug("Connectivity changed. Clear the IGDs and restart");
stopUpnp();
startUpnp();
}
void
UPnPContext::setPublicAddress(const IpAddr& addr)
{
if (not addr)
return;
std::lock_guard lock(publicAddressMutex_);
if (knownPublicAddress_ != addr) {
knownPublicAddress_ = std::move(addr);
if (logger_) logger_->debug("Setting the known public address to {}", addr.toString());
}
}
bool
UPnPContext::isReady() const
{
std::lock_guard lock(mappingMutex_);
return currentIgd_ ? true : false;
}
IpAddr
UPnPContext::getExternalIP() const
{
std::lock_guard lock(mappingMutex_);
if (currentIgd_)
return currentIgd_->getPublicIp();
return {};
}
Mapping::sharedPtr_t
UPnPContext::reserveMapping(Mapping& requestedMap)
{
auto desiredPort = requestedMap.getExternalPort();
if (desiredPort == 0) {
if (logger_) logger_->debug("Desired port is not set, will provide the first available port for [{}]",
requestedMap.getTypeStr());
} else {
if (logger_) logger_->debug("Try to find mapping for port {:d} [{}]", desiredPort, requestedMap.getTypeStr());
}
Mapping::sharedPtr_t mapRes;
{
std::lock_guard lock(mappingMutex_);
const auto& mappingList = getMappingList(requestedMap.getType());
// We try to provide a mapping in "OPEN" state. If not found,
// we provide any available mapping. In this case, it's up to
// the caller to use it or not.
for (auto const& [_, map] : mappingList) {
// If the desired port is null, we pick the first available port.
if (map->isValid() and (desiredPort == 0 or map->getExternalPort() == desiredPort)
and map->isAvailable()) {
// Considere the first available mapping regardless of its
// state. A mapping with OPEN state will be used if found.
if (not mapRes)
mapRes = map;
if (map->getState() == MappingState::OPEN) {
// Found an "OPEN" mapping. We are done.
mapRes = map;
break;
}
}
}
}
// Create a mapping if none was available.
if (not mapRes) {
mapRes = registerMapping(requestedMap);
}
if (mapRes) {
// Make the mapping unavailable
mapRes->setAvailable(false);
// Copy attributes.
mapRes->setNotifyCallback(requestedMap.getNotifyCallback());
mapRes->enableAutoUpdate(requestedMap.getAutoUpdate());
// Notify the listener.
if (auto cb = mapRes->getNotifyCallback())
cb(mapRes);
}
enforceAvailableMappingsLimits();
return mapRes;
}
void
UPnPContext::releaseMapping(const Mapping& map)
{
ctx->dispatch([this, map] {
if (shutdownComplete_)
return;
auto mapPtr = getMappingWithKey(map.getMapKey());
if (not mapPtr) {
// Might happen if the mapping failed or was never granted.
if (logger_) logger_->debug("Mapping {} does not exist or was already removed", map.toString());
return;
}
if (mapPtr->isAvailable()) {
if (logger_) logger_->warn("Trying to release an unused mapping {}", mapPtr->toString());
return;
}
// Reset the mapping options: disable auto-update and remove the notify callback.
// This is important because the mapping will be available again and can be reused
// by another (or the same) controller which may have different preferences.
// The notify callback is also removed to avoid calling it when the mapping is not used anymore.
mapPtr->setNotifyCallback(nullptr);
mapPtr->enableAutoUpdate(false);
mapPtr->setAvailable(true);
if (logger_) logger_->debug("Mapping {} released", mapPtr->toString());
enforceAvailableMappingsLimits();
});
}
void
UPnPContext::registerController(void* controller)
{
{
std::lock_guard lock(mappingMutex_);
if (shutdownComplete_) {
if (logger_) logger_->warn("UPnPContext already shut down");
return;
}
auto ret = controllerList_.emplace(controller);
if (not ret.second) {
if (logger_) logger_->warn("Controller {} is already registered", fmt::ptr(controller));
return;
}
}
if (logger_) logger_->debug("Successfully registered controller {}", fmt::ptr(controller));
if (not started_)
startUpnp();
}
void
UPnPContext::unregisterController(void* controller)
{
if (shutdownComplete_)
return;
std::unique_lock lock(mappingMutex_);
if (controllerList_.erase(controller) == 1) {
if (logger_) logger_->debug("Successfully unregistered controller {}", fmt::ptr(controller));
} else {
if (logger_) logger_->debug("Controller {} was already removed", fmt::ptr(controller));
}
if (controllerList_.empty()) {
lock.unlock();
stopUpnp();
}
}
std::vector<IGDInfo>
UPnPContext::getIgdsInfo() const
{
std::vector<IGDInfo> igdInfoList;
for (const auto& [_, protocol] : protocolList_) {
for (auto& igd : protocol->getIgdList()) {
IGDInfo info;
info.uid = igd->getUID();
info.localIp = igd->getLocalIp();
info.publicIp = igd->getPublicIp();
info.mappingInfoList = protocol->getMappingsInfo(igd);
igdInfoList.push_back(std::move(info));
}
}
return igdInfoList;
}
// TODO: refactor this function so that it can never fail unless there are literally no ports available
uint16_t
UPnPContext::getAvailablePortNumber(PortType type)
{
// Only return an available random port. No actual
// reservation is made here.
std::lock_guard lock(mappingMutex_);
const auto& mappingList = getMappingList(type);
int tryCount = 0;
while (tryCount++ < MAX_REQUEST_RETRIES) {
uint16_t port = generateRandomPort(type);
Mapping map(type, port, port);
if (mappingList.find(map.getMapKey()) == mappingList.end())
return port;
}
// Very unlikely to get here.
if (logger_) logger_->error("Could not find an available port after {} trials", MAX_REQUEST_RETRIES);
return 0;
}
void
UPnPContext::requestMapping(const Mapping::sharedPtr_t& map)
{
assert(map);
auto const& igd = getCurrentIgd();
// We must have at least a valid IGD pointer if we get here.
// Note that this method is called only if there was a valid IGD, but
// because the processing is asynchronous, there may no longer
// be one by the time this code executes.
if (not igd) {
if (logger_) logger_->debug("Unable to request mapping {}: no valid IGDs available",
map->toString());
return;
}
map->setIgd(igd);
if (logger_) logger_->debug("Request mapping {} using protocol [{}] IGD [{}]",
map->toString(),
igd->getProtocolName(),
igd->toString());
updateMappingState(map, MappingState::IN_PROGRESS);
auto const& protocol = protocolList_.at(igd->getProtocol());
protocol->requestMappingAdd(*map);
}
void
UPnPContext::provisionNewMappings(PortType type, int portCount)
{
if (logger_) logger_->debug("Provision {:d} new mappings of type [{}]", portCount, Mapping::getTypeStr(type));
while (portCount > 0) {
auto port = getAvailablePortNumber(type);
if (port > 0) {
// Found an available port number
portCount--;
Mapping map(type, port, port, true);
registerMapping(map);
} else {
// Very unlikely to get here!
if (logger_) logger_->error("Cannot provision port: no available port number");
return;
}
}
}
void
UPnPContext::deleteUnneededMappings(PortType type, int portCount)
{
if (logger_) logger_->debug("Remove {:d} unneeded mapping of type [{}]", portCount, Mapping::getTypeStr(type));
std::lock_guard lock(mappingMutex_);
auto& mappingList = getMappingList(type);
for (auto it = mappingList.begin(); it != mappingList.end();) {
auto map = it->second;
assert(map);
if (not map->isAvailable()) {
it++;
continue;
}
if (map->getState() == MappingState::OPEN and portCount > 0) {
// Close portCount mappings in "OPEN" state.
requestRemoveMapping(map);
it = mappingList.erase(it);
portCount--;
} else if (map->getState() != MappingState::OPEN) {
// If this methods is called, it means there are more open
// mappings than required. So, all mappings in a state other
// than "OPEN" state (typically in in-progress state) will
// be deleted as well.
it = mappingList.erase(it);
} else {
it++;
}
}
}
void
UPnPContext::updateCurrentIgd()
{
std::lock_guard lock(mappingMutex_);
if (currentIgd_ and currentIgd_->isValid()) {
if (logger_) logger_->debug("Current IGD is still valid, no need to update");
return;
}
// Reset and search for the best IGD.
currentIgd_.reset();
for (auto const& [_, protocol] : protocolList_) {
if (protocol->isReady()) {
auto igdList = protocol->getIgdList();
assert(not igdList.empty());
auto const& igd = igdList.front();
if (not igd->isValid())
continue;
// Prefer NAT-PMP over PUPNP.
if (currentIgd_ and igd->getProtocol() != NatProtocolType::NAT_PMP)
continue;
// Update.
currentIgd_ = igd;
}
}
if (currentIgd_ and currentIgd_->isValid()) {
if (logger_) logger_->debug("Current IGD updated to [{}] IGD [{} {}] ",
currentIgd_->getProtocolName(),
currentIgd_->getUID(),
currentIgd_->toString());
} else {
if (logger_) logger_->warn("Couldn't update current IGD: no valid IGD was found");
}
}
std::shared_ptr<IGD>
UPnPContext::getCurrentIgd() const
{
return currentIgd_;
}
void
UPnPContext::enforceAvailableMappingsLimits()
{
// If there is no valid IGD, do nothing.
if (!isReady())
return;
for (auto type : {PortType::TCP, PortType::UDP}) {
int pendingCount = 0;
int inProgressCount = 0;
int openCount = 0;
{
std::lock_guard lock(mappingMutex_);
const auto& mappingList = getMappingList(type);
for (const auto& [_, mapping] : mappingList) {
if (!mapping->isAvailable())
continue;
switch (mapping->getState()) {
case MappingState::PENDING:
pendingCount++;
break;
case MappingState::IN_PROGRESS:
inProgressCount++;
break;
case MappingState::OPEN:
openCount++;
break;
default:
break;
}
}
}
int availableCount = openCount + pendingCount + inProgressCount;
if (logger_) logger_->debug("Number of 'available' {} mappings in the local list: {} ({} open + {} pending + {} in progress)",
Mapping::getTypeStr(type),
availableCount,
openCount,
pendingCount,
inProgressCount);
int minAvailableMappings = getMinAvailableMappings(type);
if (minAvailableMappings > availableCount) {
provisionNewMappings(type, minAvailableMappings - availableCount);
continue;
}
int maxAvailableMappings = getMaxAvailableMappings(type);
if (openCount > maxAvailableMappings) {
deleteUnneededMappings(type, openCount - maxAvailableMappings);
}
}
}
void
UPnPContext::renewMappings()
{
if (!started_)
return;
const auto& igd = getCurrentIgd();
if (!igd) {
if (logger_) logger_->debug("Cannot renew mappings: no valid IGD available");
return;
}
auto now = sys_clock::now();
auto nextRenewalTime = sys_clock::time_point::max();
std::vector<Mapping::sharedPtr_t> toRenew;
int toRenewLaterCount = 0;
for (auto type : {PortType::TCP, PortType::UDP}) {
std::lock_guard lock(mappingMutex_);
const auto& mappingList = getMappingList(type);
for (const auto& [_, map] : mappingList) {
if (not map->isValid())
continue;
if (map->getState() != MappingState::OPEN)
continue;
auto mapRenewalTime = map->getRenewalTime();
if (now >= mapRenewalTime) {
toRenew.emplace_back(map);
} else if (mapRenewalTime < sys_clock::time_point::max()) {
toRenewLaterCount++;
if (mapRenewalTime < nextRenewalTime)
nextRenewalTime = map->getRenewalTime();
}
}
}
if (!toRenew.empty()) {
if (logger_) logger_->debug("Sending renewal requests for {} mappings", toRenew.size());
}
for (const auto& map : toRenew) {
const auto& protocol = protocolList_.at(map->getIgd()->getProtocol());
protocol->requestMappingRenew(*map);
}
if (toRenewLaterCount > 0) {
nextRenewalTime += MAPPING_RENEWAL_THROTTLING_DELAY;
if (logger_) logger_->debug("{} mappings didn't need to be renewed (next renewal scheduled for {:%Y-%m-%d %H:%M:%S})",
toRenewLaterCount,
fmt::localtime(sys_clock::to_time_t(nextRenewalTime)));
mappingRenewalTimer_.expires_at(nextRenewalTime);
mappingRenewalTimer_.async_wait([this](asio::error_code const& ec) {
if (ec != asio::error::operation_aborted)
renewMappings();
});
}
}
void
UPnPContext::scheduleMappingsRenewal()
{
// Debounce the scheduling function so that it doesn't get called multiple
// times when several mappings are added or renewed in rapid succession.
renewalSchedulingTimer_.expires_after(std::chrono::milliseconds(500));
renewalSchedulingTimer_.async_wait([this](asio::error_code const& ec) {
if (ec != asio::error::operation_aborted)
_scheduleMappingsRenewal();
});
}
void
UPnPContext::_scheduleMappingsRenewal()
{
if (!started_)
return;
sys_clock::time_point nextRenewalTime = sys_clock::time_point::max();
for (auto type : {PortType::TCP, PortType::UDP}) {
std::lock_guard lock(mappingMutex_);
const auto& mappingList = getMappingList(type);
for (const auto& [_, map] : mappingList) {
if (map->getState() == MappingState::OPEN &&
map->getRenewalTime() < nextRenewalTime)
nextRenewalTime = map->getRenewalTime();
}
}
if (nextRenewalTime == sys_clock::time_point::max())
return;
// Add a small delay so that we don't have to call renewMappings multiple
// times in a row (and iterate over the whole list of mappings each time)
// when multiple mappings have almost the same renewal time.
nextRenewalTime += MAPPING_RENEWAL_THROTTLING_DELAY;
if (nextRenewalTime == mappingRenewalTimer_.expiry())
return;
if (logger_) logger_->debug("Scheduling next port mapping renewal for {:%Y-%m-%d %H:%M:%S}",
fmt::localtime(sys_clock::to_time_t(nextRenewalTime)));
mappingRenewalTimer_.expires_at(nextRenewalTime);
mappingRenewalTimer_.async_wait([this](asio::error_code const& ec) {
if (ec != asio::error::operation_aborted)
renewMappings();
});
}
void
UPnPContext::syncLocalMappingListWithIgd()
{
std::lock_guard lock(syncMutex_);
if (syncRequested_)
return;
syncRequested_ = true;
syncTimer_.expires_after(std::chrono::minutes(5));
syncTimer_.async_wait([this](asio::error_code const& ec) {
if (ec != asio::error::operation_aborted)
_syncLocalMappingListWithIgd();
});
}
void
UPnPContext::_syncLocalMappingListWithIgd()
{
{
std::lock_guard lock(syncMutex_);
syncRequested_ = false;
}
const auto& igd = getCurrentIgd();
if (!started_ || !igd || igd->getProtocol() != NatProtocolType::PUPNP) {
return;
}
auto pupnp = protocolList_.at(NatProtocolType::PUPNP);
if (!pupnp->isReady())
return;
if (logger_) logger_->debug("Synchronizing local mapping list with IGD [{}]",
igd->toString());
auto remoteMapList = pupnp->getMappingsListByDescr(igd,
Mapping::UPNP_MAPPING_DESCRIPTION_PREFIX);
bool requestsInProgress = false;
// Use a temporary list to avoid processing mappings while holding the lock.
std::list<Mapping::sharedPtr_t> failedMappings;
for (auto type: {PortType::TCP, PortType::UDP}) {
std::lock_guard lock(mappingMutex_);
for (auto& [_, map] : getMappingList(type)) {
if (map->getProtocol() != NatProtocolType::PUPNP) {
continue;
}
switch (map->getState()) {
case MappingState::PENDING:
case MappingState::IN_PROGRESS:
requestsInProgress = true;
break;
case MappingState::OPEN: {
auto it = remoteMapList.find(map->getMapKey());
if (it == remoteMapList.end()) {
if (logger_) logger_->warn("Mapping {} (IGD {}) marked as \"OPEN\" but not found in the "
"remote list. Setting state to \"FAILED\".",
map->toString(),
igd->toString());
failedMappings.emplace_back(map);
} else {
auto oldExpiryTime = map->getExpiryTime();
auto newExpiryTime = it->second.getExpiryTime();
// The value of newExpiryTime is based on the mapping's "lease duration" that we got from
// the IGD, which is supposed to be (according to the UPnP specification) the number of
// seconds remaining before the mapping expires. In practice, the duration values returned
// by some routers are only precise to the hour (i.e. they're always multiples of 3600). This
// means that newExpiryTime can exceed the real expiry time by up to an hour in the worst case.
// In order to avoid accidentally scheduling a mapping's renewal too late, we only allow ourselves to
// push back its renewal time if newExpiryTime is bigger than oldExpiryTime by a sufficient margin.
if (newExpiryTime < oldExpiryTime ||
newExpiryTime > oldExpiryTime + std::chrono::seconds(2 * 3600)) {
auto newRenewalTime = map->getRenewalTime() + (newExpiryTime - oldExpiryTime) / 2;
map->setRenewalTime(newRenewalTime);
map->setExpiryTime(newExpiryTime);
}
}
break;
}
default:
break;
}
}
}
scheduleMappingsRenewal();
for (auto const& map : failedMappings) {
updateMappingState(map, MappingState::FAILED);
}
if (!failedMappings.empty())
enforceAvailableMappingsLimits();
if (requestsInProgress) {
// It's unlikely that there will be requests in progress when this function is
// called, but if there are, that suggests that we are dealing with a slow
// router, so we return early instead of sending additional deletion requests
// (which aren't essential and could end up "competing" with higher-priority
// creation/renewal requests).
return;
}
// Use a temporary list to avoid processing mappings while holding the lock.
std::list<Mapping> toRemoveFromIgd;
{
std::lock_guard lock(mappingMutex_);
for (auto const& [_, map] : remoteMapList) {
const auto& mappingList = getMappingList(map.getType());
auto it = mappingList.find(map.getMapKey());
if (it == mappingList.end()) {
// Not present, request mapping remove.
toRemoveFromIgd.emplace_back(std::move(map));
// Make only few remove requests at once.
if (toRemoveFromIgd.size() >= MAX_REQUEST_REMOVE_COUNT)
break;
}
}
}
for (const auto& map : toRemoveFromIgd) {
pupnp->requestMappingRemove(map);
}
}
void
UPnPContext::pruneMappingsWithInvalidIgds(const std::shared_ptr<IGD>& igd)
{
// Use temporary list to avoid holding the lock while
// processing the mapping list.
std::list<Mapping::sharedPtr_t> toRemoveList;
{
std::lock_guard lock(mappingMutex_);
PortType types[2] {PortType::TCP, PortType::UDP};
for (auto& type : types) {
const auto& mappingList = getMappingList(type);
for (auto const& [_, map] : mappingList) {
if (map->getIgd() == igd)
toRemoveList.emplace_back(map);
}
}
}
for (auto const& map : toRemoveList) {
if (logger_) logger_->debug("Remove mapping {} (has an invalid IGD {} [{}])",
map->toString(),
igd->toString(),
igd->getProtocolName());
updateMappingState(map, MappingState::FAILED);
}
}
void
UPnPContext::processPendingRequests()
{
// This list holds the mappings to be requested. This is
// needed to avoid performing the requests while holding
// the lock.
std::list<Mapping::sharedPtr_t> requestsList;
// Populate the list of requests to perform.
{
std::lock_guard lock(mappingMutex_);
PortType typeArray[2] {PortType::TCP, PortType::UDP};
for (auto type : typeArray) {
const auto& mappingList = getMappingList(type);
for (const auto& [_, map] : mappingList) {
if (map->getState() == MappingState::PENDING) {
if (logger_) logger_->debug("Will attempt to send a request for pending mapping {}",
map->toString());
requestsList.emplace_back(map);
}
}
}
}
// Process the pending requests.
for (auto const& map : requestsList) {
requestMapping(map);
}
}
void
UPnPContext::onIgdUpdated(const std::shared_ptr<IGD>& igd, UpnpIgdEvent event)
{
assert(igd);
char const* IgdState = event == UpnpIgdEvent::ADDED ? "ADDED"
: event == UpnpIgdEvent::REMOVED ? "REMOVED"
: "INVALID";
auto const& igdLocalAddr = igd->getLocalIp();
auto protocolName = igd->getProtocolName();
if (logger_) logger_->debug("New event for IGD [{} {}] [{}]: [{}]",
igd->getUID(),
igd->toString(),
protocolName,
IgdState);
if (not igdLocalAddr) {
if (logger_) logger_->warn("[{}] IGD [{} {}] has an invalid local address, ignoring",
protocolName,
igd->getUID(),
igd->toString());
return;
}
if (not igd->getPublicIp()) {
if (logger_) logger_->warn("[{}] IGD [{} {}] has an invalid public address, ignoring",
protocolName,
igd->getUID(),
igd->toString());
return;
}
{
std::lock_guard lock(publicAddressMutex_);
if (knownPublicAddress_ and igd->getPublicIp() != knownPublicAddress_) {
if (logger_) logger_->warn("[{}] IGD external address [{}] does not match known public address [{}]."
" The mapped addresses might not be reachable",
protocolName,
igd->getPublicIp().toString(),
knownPublicAddress_.toString());
}
}
if (event == UpnpIgdEvent::REMOVED or event == UpnpIgdEvent::INVALID_STATE) {
if (logger_) logger_->warn("State of IGD [{} {}] [{}] changed to [{}]. Pruning the mapping list",
igd->getUID(),
igd->toString(),
protocolName,
IgdState);
pruneMappingsWithInvalidIgds(igd);
}
updateCurrentIgd();
if (isReady()) {
processPendingRequests();
enforceAvailableMappingsLimits();
}
}
void
UPnPContext::onMappingAdded(const std::shared_ptr<IGD>& igd, const Mapping& mapRes)
{
// Check if we have a pending request for this response.
auto map = getMappingWithKey(mapRes.getMapKey());
if (not map) {
// We may receive a response for a canceled request. Just ignore it.
if (logger_) logger_->debug("Response for mapping {} [IGD {}] [{}] does not have a local match",
mapRes.toString(),
igd->toString(),
mapRes.getProtocolName());
return;
}
// The mapping request is new and successful. Update.
map->setIgd(igd);
map->setInternalAddress(mapRes.getInternalAddress());
map->setExternalPort(mapRes.getExternalPort());
map->setRenewalTime(mapRes.getRenewalTime());
map->setExpiryTime(mapRes.getExpiryTime());
// Update the state and report to the owner.
updateMappingState(map, MappingState::OPEN);
scheduleMappingsRenewal();
if (logger_) logger_->debug("Mapping {} (on IGD {} [{}]) successfully performed",
map->toString(),
igd->toString(),
map->getProtocolName());
// Call setValid() to reset the errors counter. We need
// to reset the counter on each successful response.
igd->setValid(true);
if (igd->getProtocol() == NatProtocolType::PUPNP)
syncLocalMappingListWithIgd();
}
void
UPnPContext::onMappingRenewed(const std::shared_ptr<IGD>& igd, const Mapping& map)
{
auto mapPtr = getMappingWithKey(map.getMapKey());
if (not mapPtr) {
if (logger_) logger_->warn("Renewed mapping {} from IGD {} [{}] does not have a match in local list",
map.toString(),
igd->toString(),
map.getProtocolName());
return;
}
if (!mapPtr->isValid() || mapPtr->getState() != MappingState::OPEN) {
if (logger_) logger_->warn("Renewed mapping {} from IGD {} [{}] is in unexpected state",
mapPtr->toString(),
igd->toString(),
mapPtr->getProtocolName());
return;
}
mapPtr->setRenewalTime(map.getRenewalTime());
mapPtr->setExpiryTime(map.getExpiryTime());
scheduleMappingsRenewal();
if (igd->getProtocol() == NatProtocolType::PUPNP)
syncLocalMappingListWithIgd();
}
void
UPnPContext::requestRemoveMapping(const Mapping::sharedPtr_t& map)
{
if (not map or not map->isValid()) {
// Silently ignore if the mapping is invalid
return;
}
auto protocol = protocolList_.at(map->getIgd()->getProtocol());
protocol->requestMappingRemove(*map);
}
void
UPnPContext::onMappingRemoved(const std::shared_ptr<IGD>& igd, const Mapping& mapRes)
{
if (not mapRes.isValid())
return;
auto map = getMappingWithKey(mapRes.getMapKey());
// Notify the listener.
if (map and map->getNotifyCallback())
map->getNotifyCallback()(map);
}
void
UPnPContext::onIgdDiscoveryStarted()
{
std::lock_guard lock(igdDiscoveryMutex_);
igdDiscoveryInProgress_ = true;
if (logger_) logger_->debug("IGD Discovery started");
igdDiscoveryTimer_.expires_after(igdDiscoveryTimeout_);
igdDiscoveryTimer_.async_wait([this] (const asio::error_code& ec) {
if (ec != asio::error::operation_aborted && igdDiscoveryInProgress_) {
_endIgdDiscovery();
}
});
}
void
UPnPContext::_endIgdDiscovery()
{
std::lock_guard lockDiscovery_(igdDiscoveryMutex_);
igdDiscoveryInProgress_ = false;
if (logger_) logger_->debug("IGD Discovery ended");
if (isReady()) {
return;
}
// Use a temporary list to avoid holding the lock while processing the mapping list.
// This is necessary because updateMappingState calls a user-defined callback, which
// in turn could end up calling a function (such as reserveMapping) that would also
// attempt to lock mappingMutex_.
std::list<Mapping::sharedPtr_t> toRemoveList;
{
std::lock_guard lock(mappingMutex_);
PortType types[2] {PortType::TCP, PortType::UDP};
for (auto type : types) {
const auto& mappingList = getMappingList(type);
for (const auto& [_, map] : mappingList) {
toRemoveList.emplace_back(map);
}
}
}
// We reached the end of the IGD discovery period without finding a valid IGD,
// so all mapping requests are considered to have failed.
for (auto const& map : toRemoveList) {
updateMappingState(map, MappingState::FAILED);
}
}
void
UPnPContext::setIgdDiscoveryTimeout(std::chrono::milliseconds timeout)
{
std::lock_guard lock(igdDiscoveryMutex_);
igdDiscoveryTimeout_ = timeout;
}
Mapping::sharedPtr_t
UPnPContext::registerMapping(Mapping& map)
{
Mapping::sharedPtr_t mapPtr;
if (map.getExternalPort() == 0) {
auto port = getAvailablePortNumber(map.getType());
if (port == 0) {
if (logger_) logger_->error("Unable to register mapping: no available port number");
return mapPtr;
}
map.setExternalPort(port);
map.setInternalPort(port);
}
// Newly added mapping must be in pending state by default.
map.setState(MappingState::PENDING);
{
std::lock_guard lock(mappingMutex_);
auto& mappingList = getMappingList(map.getType());
auto ret = mappingList.emplace(map.getMapKey(), std::make_shared<Mapping>(map));
if (not ret.second) {
if (logger_) logger_->warn("Mapping request for {} already added!", map.toString());
return {};
}
mapPtr = ret.first->second;
assert(mapPtr);
}
if (not isReady()) {
// There is no valid IGD available
std::lock_guard lock(igdDiscoveryMutex_);
// IGD discovery is in progress, the mapping request will be made once an IGD becomes available
if (igdDiscoveryInProgress_) {
if (logger_) logger_->debug("Mapping {} will be requested when an IGD becomes available",
map.toString());
} else {
// it's not in the IGD discovery phase, the mapping request will fail
if (logger_) logger_->warn("Request for mapping {} failed, no IGD available",
map.toString());
updateMappingState(mapPtr, MappingState::FAILED);
}
} else {
// There is a valid IGD available, request the mapping.
requestMapping(mapPtr);
}
return mapPtr;
}
void
UPnPContext::unregisterMapping(const Mapping::sharedPtr_t& map)
{
if (not map) {
return;
}
std::lock_guard lock(mappingMutex_);
auto& mappingList = getMappingList(map->getType());
if (mappingList.erase(map->getMapKey()) == 1) {
if (logger_) logger_->debug("Unregistered mapping {}", map->toString());
} else {
// The mapping may already be un-registered. Just ignore it.
if (logger_) logger_->debug("Can't unregister mapping {} [{}] since it doesn't have a local match",
map->toString(),
map->getProtocolName());
}
}
std::map<Mapping::key_t, Mapping::sharedPtr_t>&
UPnPContext::getMappingList(PortType type)
{
unsigned typeIdx = type == PortType::TCP ? 0 : 1;
return mappingList_[typeIdx];
}
Mapping::sharedPtr_t
UPnPContext::getMappingWithKey(Mapping::key_t key)
{
std::lock_guard lock(mappingMutex_);
auto const& mappingList = getMappingList(Mapping::getTypeFromMapKey(key));
auto it = mappingList.find(key);
if (it == mappingList.end())
return nullptr;
return it->second;
}
void
UPnPContext::onMappingRequestFailed(const Mapping& mapRes)
{
auto igd = mapRes.getIgd();
auto const& map = getMappingWithKey(mapRes.getMapKey());
if (not map) {
// We may receive a response for a removed request. Just ignore it.
if (logger_) logger_->debug("Ignoring failed request for mapping {} [IGD {}] since it doesn't have a local match",
mapRes.toString(),
igd->toString());
return;
}
updateMappingState(map, MappingState::FAILED);
if (logger_) logger_->warn("Request for mapping {} on IGD {} failed",
map->toString(),
igd->toString());
enforceAvailableMappingsLimits();
}
void
UPnPContext::updateMappingState(const Mapping::sharedPtr_t& map, MappingState newState, bool notify)
{
assert(map);
// Ignore if the state did not change.
if (newState == map->getState()) {
return;
}
// Update the state.
map->setState(newState);
// Notify the listener if set.
if (notify and map->getNotifyCallback())
map->getNotifyCallback()(map);
if (newState == MappingState::FAILED)
handleFailedMapping(map);
}
void
UPnPContext::handleFailedMapping(const Mapping::sharedPtr_t& map)
{
if (!map->getAutoUpdate()) {
// If the mapping is not set to auto-update, it will be unregistered.
unregisterMapping(map);
return;
}
if (isReady()) {
Mapping newMapping(map->getType());
newMapping.enableAutoUpdate(true);
newMapping.setNotifyCallback(map->getNotifyCallback());
reserveMapping(newMapping);
if (logger_) logger_->debug("Mapping {} has auto-update enabled, a new mapping will be requested",
map->toString());
// TODO: figure out if this line is actually necessary
// (See https://review.jami.net/c/jami-daemon/+/16940)
map->setNotifyCallback(nullptr);
} else {
// If there is no valid IGD, the mapping is marked as pending
// and will be requested when an IGD becomes available.
updateMappingState(map, MappingState::PENDING, false);
if (logger_) logger_->debug("Mapping {} will be requested when an IGD becomes available",
map->toString());
}
}
} // namespace upnp
} // namespace dhtnet