Skip to content
Snippets Groups Projects
Select Git revision
  • master default protected
1 result

upnp_context.cpp

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    upnp_context.cpp 44.45 KiB
    /*
     *  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)
     , connectivityChangedTimer_(*ctx)
     , mappingRenewalTimer_(*ctx)
     , renewalSchedulingTimer_(*ctx)
     , syncTimer_(*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("Attempt 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;
                        // Make the mapping unavailable while we're holding the lock on
                        // mappingMutex_ to ensure no other thread will try to use it.
                        mapRes->setAvailable(false);
                        break;
                    }
                }
            }
        }
    
        // Create a mapping if none was available.
        if (not mapRes) {
            mapRes = registerMapping(requestedMap, /* available */ false);
        }
    
        if (mapRes) {
            // Copy attributes.
            mapRes->setNotifyCallback(requestedMap.getNotifyCallback());
            mapRes->enableAutoUpdate(requestedMap.getAutoUpdate());
            // Notify the listener.
            Mapping::notify(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("Attempting 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 shutdown");
                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("Unable to find an available port after {} attempt(s)", 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("Unable to provision port: No available port number");
                return;
            }
        }
    }
    
    void
    UPnPContext::deleteUnneededMappings(PortType type, int portCount)
    {
        if (logger_) logger_->debug("Remove {:d} unneeded mapping(s) 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 method 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("Unable to 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;
            int unavailableCount = 0;
            {
                std::lock_guard lock(mappingMutex_);
                const auto& mappingList = getMappingList(type);
                for (const auto& [_, mapping] : mappingList) {
                    if (!mapping->isAvailable()) {
                        unavailableCount++;
                        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 {} mappings in the local list: {} available ({} open + {} pending + {} in progress), {} unavailable",
                                        Mapping::getTypeStr(type),
                                        availableCount,
                                        openCount,
                                        pendingCount,
                                        inProgressCount,
                                        unavailableCount);
    
            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("Unable to 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 {} mapping(s)", 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("{} mapping(s) 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("Attempting 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 an 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.
        Mapping::notify(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, bool available)
    {
        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;
            // It's important to set the mapping's availability while mappingMutex_ is locked,
            // otherwise a call to reserveMapping from a different thread could try to use the
            // mapping we just added to the mapping list even if `available` is false.
            mapPtr->setAvailable(available);
            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);
                // The call to `updateMappingState` above will cause the mapping to be
                // removed from the mapping list, so we return a null pointer.
                return {};
            }
        } 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("Unable to 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)
            Mapping::notify(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()) {
            // We have a valid IGD, but the mapping request
            // failed, so try again with a new port number.
            Mapping newMapping(map->getType());
            newMapping.enableAutoUpdate(true);
            newMapping.setNotifyCallback(map->getNotifyCallback());
    
            // Remove the old (failed) mapping from the local list
            map->setNotifyCallback(nullptr);
            unregisterMapping(map);
    
            if (logger_) logger_->debug("Mapping {} had auto-update enabled, a new mapping will be requested",
                                        map->toString());
            reserveMapping(newMapping);
        } 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