upnp_context.cpp 9.88 KB
Newer Older
Stepan Salenikovich's avatar
Stepan Salenikovich committed
1
/*
Sébastien Blin's avatar
Sébastien Blin committed
2
 *  Copyright (C) 2004-2019 Savoir-faire Linux Inc.
Guillaume Roguez's avatar
Guillaume Roguez committed
3
 *
Stepan Salenikovich's avatar
Stepan Salenikovich committed
4
 *  Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
5
 *  Author: Eden Abitbol <eden.abitbol@savoirfairelinux.com>
Stepan Salenikovich's avatar
Stepan Salenikovich committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
 *
 *  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.
 */

22
#include "upnp_context.h"
Stepan Salenikovich's avatar
Stepan Salenikovich committed
23

24
namespace jami { namespace upnp {
25

26 27 28 29 30
static uint16_t
generateRandomPort()
{
    // Seed the generator.
    static std::mt19937 gen(dht::crypto::getSeededRandomEngine());
Adrien Béraud's avatar
Adrien Béraud committed
31

32 33
    // Define the range.
    std::uniform_int_distribution<uint16_t> dist(Mapping::UPNP_PORT_MIN, Mapping::UPNP_PORT_MAX);
34

35 36
    return dist(gen);
}
Stepan Salenikovich's avatar
Stepan Salenikovich committed
37 38 39 40 41 42 43 44 45

std::shared_ptr<UPnPContext>
getUPnPContext()
{
    static auto context = std::make_shared<UPnPContext>();
    return context;
}

UPnPContext::UPnPContext()
46 47
{
    using namespace std::placeholders;
Adrien Béraud's avatar
Adrien Béraud committed
48
#if HAVE_LIBNATPMP
49 50 51 52
    auto natPmp = std::make_unique<NatPmp>();
    natPmp->setOnIgdChanged(std::bind(&UPnPContext::igdListChanged, this, _1, _2, _3, _4));
    natPmp->searchForIGD();
    protocolList_.push_back(std::move(natPmp));
Adrien Béraud's avatar
Adrien Béraud committed
53 54
#endif
#if HAVE_LIBUPNP
55 56 57 58
    auto pupnp = std::make_unique<PUPnP>();
    pupnp->setOnIgdChanged(std::bind(&UPnPContext::igdListChanged, this, _1, _2, _3, _4));
    pupnp->searchForIGD();
    protocolList_.push_back(std::move(pupnp));
Adrien Béraud's avatar
Adrien Béraud committed
59
#endif
Stepan Salenikovich's avatar
Stepan Salenikovich committed
60 61 62 63
}

UPnPContext::~UPnPContext()
{
64
    igdList_.clear();
65 66
}

67 68 69
void
UPnPContext::connectivityChanged()
{
70
    {
71
        std::lock_guard<std::mutex> lock(igdListMutex_);
72 73 74 75 76 77 78 79
        for (auto const& protocol : protocolList_)
            protocol->clearIGDs();
        if (not igdList_.empty()) {
            // Clear main IGD list.
            igdList_.clear();
            for (const auto& listener : igdListeners_) {
                listener.second();
            }
Adrien Béraud's avatar
Adrien Béraud committed
80
        }
81 82
    }

83 84
    for (auto const& protocol : protocolList_)
        protocol->searchForIGD();
Stepan Salenikovich's avatar
Stepan Salenikovich committed
85 86 87
}

bool
88
UPnPContext::hasValidIGD()
Stepan Salenikovich's avatar
Stepan Salenikovich committed
89
{
90
    std::lock_guard<std::mutex> lock(igdListMutex_);
91
    return not igdList_.empty();
Stepan Salenikovich's avatar
Stepan Salenikovich committed
92 93
}

94
size_t
95
UPnPContext::addIGDListener(IgdFoundCallback&& cb)
96
{
97 98 99
   JAMI_DBG("UPnP Context: Adding IGD listener");

    std::lock_guard<std::mutex> lock(igdListMutex_);
100 101
    auto token = ++listenerToken_;
    igdListeners_.emplace(token, std::move(cb));
102

103 104 105 106 107 108
    return token;
}

void
UPnPContext::removeIGDListener(size_t token)
{
109 110 111
    std::lock_guard<std::mutex> lock(igdListMutex_);
    if (igdListeners_.erase(token) > 0) {
        JAMI_DBG("UPnP Context: Removing igd listener");
Stepan Salenikovich's avatar
Stepan Salenikovich committed
112 113 114 115
    }
}

uint16_t
Adrien Béraud's avatar
Adrien Béraud committed
116
UPnPContext::chooseRandomPort(const IGD& igd, PortType type)
Stepan Salenikovich's avatar
Stepan Salenikovich committed
117
{
118
    auto globalMappings = type == PortType::UDP ? &igd.udpMappings : &igd.tcpMappings;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
119 120 121

    uint16_t port = generateRandomPort();

122
    // Keep generating random ports until we find one which is not used.
Stepan Salenikovich's avatar
Stepan Salenikovich committed
123 124 125 126 127 128 129 130
    while(globalMappings->find(port) != globalMappings->end()) {
        port = generateRandomPort();
    }

    return port;
}

Mapping
131
UPnPContext::addMapping(uint16_t port_desired, uint16_t port_local, PortType type, bool unique)
Stepan Salenikovich's avatar
Stepan Salenikovich committed
132
{
133 134
    // Lock mutex on the igd list.
    std::lock_guard<std::mutex> igdListLock(igdListMutex_);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
135

136 137 138 139 140 141 142 143
    // Add the mapping to the first valid IGD we find in the list.
    IGD* igd = nullptr;
    if (not igdList_.empty()) {
        for (auto const& item : igdList_) {
            if (item.second) {
                igd = item.second;
                break;
            }
Stepan Salenikovich's avatar
Stepan Salenikovich committed
144 145 146
        }
    }

147 148 149
    if (not igd) {
        JAMI_WARN("UPnPContext: no valid IGD available");
        return {};
Stepan Salenikovich's avatar
Stepan Salenikovich committed
150 151
    }

152 153
    // Get mapping type (UDP/TCP).
    auto globalMappings = type == PortType::UDP ? &igd->udpMappings : &igd->tcpMappings;
154

155 156 157
    // If we want a unique port, we must make sure the client isn't already using the port.
    if (unique) {
        bool unique_found = false;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
158

159 160 161 162 163 164
        // Keep generating random ports until we find a unique one.
        while (not unique_found) {
            auto iter = globalMappings->find(port_desired);     // Check if that port is not already used by the client.
            if (iter != globalMappings->end()) {
                port_desired = chooseRandomPort(*igd, type);    // Port already used, try another one.
                JAMI_DBG("Port %d is already in use. Finding another unique port...", port_desired);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
165
            } else {
166
                unique_found = true;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
167 168 169
            }
        }
    }
Éloi Bail's avatar
Éloi Bail committed
170

171 172
    UPnPProtocol::UpnpError upnp_err = UPnPProtocol::UpnpError::ERROR_OK;
    unsigned numberRetries = 0;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
173

174
    Mapping mapping = addMapping(igd, port_desired, port_local, type, upnp_err);
Éloi Bail's avatar
Éloi Bail committed
175

176 177 178
    while (not mapping and
           upnp_err == UPnPProtocol::UpnpError::CONFLICT_IN_MAPPING and
           numberRetries < MAX_RETRIES) {
Stepan Salenikovich's avatar
Stepan Salenikovich committed
179

180
        port_desired = chooseRandomPort(*igd, type);
Adrien Béraud's avatar
Adrien Béraud committed
181

182 183 184
        upnp_err = UPnPProtocol::UpnpError::ERROR_OK;
        mapping = addMapping(igd, port_desired, port_local, type, upnp_err);
        ++numberRetries;
Adrien Béraud's avatar
Adrien Béraud committed
185 186
    }

187 188
    if (not mapping and numberRetries >= MAX_RETRIES) {
        JAMI_ERR("UPnPContext: Could not add mapping after %u retries, giving up", MAX_RETRIES);
Adrien Béraud's avatar
Adrien Béraud committed
189 190
    }

191
    return mapping;
Adrien Béraud's avatar
Adrien Béraud committed
192 193
}

194 195
Mapping
UPnPContext::addMapping(IGD* igd, uint16_t port_external, uint16_t port_internal, PortType type, UPnPProtocol::UpnpError& upnp_error)
Adrien Béraud's avatar
Adrien Béraud committed
196
{
197 198 199 200 201 202
    // Iterate over the IGD list and call add the mapping with the corresponding protocol.
    if (not igdList_.empty()) {
        for (auto const& item : igdList_) {
            if (item.second == igd) {
                return item.first->addMapping(item.second, port_external, port_internal, type, upnp_error);
            }
Adrien Béraud's avatar
Adrien Béraud committed
203 204 205
        }
    }

206
    return {};
Adrien Béraud's avatar
Adrien Béraud committed
207 208
}

Stepan Salenikovich's avatar
Stepan Salenikovich committed
209
void
210
UPnPContext::removeMapping(const Mapping& mapping)
Stepan Salenikovich's avatar
Stepan Salenikovich committed
211
{
212 213 214 215 216
    // Remove wanted mappings from all IGDs in list.
    if (not igdList_.empty()) {
        for (auto const& item : igdList_) {
            item.first->removeMapping(mapping);
        }
Stepan Salenikovich's avatar
Stepan Salenikovich committed
217 218 219
    }
}

220 221
IpAddr
UPnPContext::getLocalIP() const
Stepan Salenikovich's avatar
Stepan Salenikovich committed
222
{
223 224
    // Lock mutex on the igd list.
    std::lock_guard<std::mutex> igdListLock(igdListMutex_);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
225

226 227 228 229 230 231
    // Return first valid local Ip.
    if (not igdList_.empty()) {
        for (auto const& item : igdList_) {
            if (item.second) {
                return item.second->localIp_;
            }
Stepan Salenikovich's avatar
Stepan Salenikovich committed
232 233 234
        }
    }

235 236
    JAMI_WARN("UPnP: No valid IGD available");
    return {};
Stepan Salenikovich's avatar
Stepan Salenikovich committed
237 238
}

239 240
IpAddr
UPnPContext::getExternalIP() const
Stepan Salenikovich's avatar
Stepan Salenikovich committed
241
{
242 243 244 245 246 247 248 249 250
    // Lock mutex on the igd list.
    std::lock_guard<std::mutex> igdListLock(igdListMutex_);

    // Return first valid external Ip.
    if (not igdList_.empty()) {
        for (auto const& item : igdList_) {
            if (item.second) {
                return item.second->publicIp_;
            }
Stepan Salenikovich's avatar
Stepan Salenikovich committed
251 252 253
        }
    }

254 255
    JAMI_WARN("UPnP: No valid IGD available");
    return {};
Stepan Salenikovich's avatar
Stepan Salenikovich committed
256 257
}

258
bool
259
UPnPContext::isIgdInList(const IpAddr& publicIpAddr)
Stepan Salenikovich's avatar
Stepan Salenikovich committed
260
{
261 262 263 264
    for (auto const& item : igdList_) {
        if (item.second->publicIp_) {
            if (item.second->publicIp_ == publicIpAddr) {
                return true;
265 266
            }
        }
Stepan Salenikovich's avatar
Stepan Salenikovich committed
267
    }
268 269
    return false;
}
Stepan Salenikovich's avatar
Stepan Salenikovich committed
270

271 272 273 274
UPnPProtocol::Type 
UPnPContext::getIgdProtocol(IGD* igd)
{
    std::lock_guard<std::mutex> igdListLock(igdListMutex_);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
275

276 277 278 279
    for (auto const& item : igdList_) {
        if (item.second->publicIp_ == igd->publicIp_) {
            return item.first->getType();
        }
Stepan Salenikovich's avatar
Stepan Salenikovich committed
280 281
    }

282
    return UPnPProtocol::Type::UNKNOWN;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
283 284
}

285 286
bool
UPnPContext::igdListChanged(UPnPProtocol* protocol, IGD* igd, IpAddr publicIpAddr, bool added)
Stepan Salenikovich's avatar
Stepan Salenikovich committed
287
{
288
    std::lock_guard<std::mutex> lock(igdListMutex_);
289 290 291 292 293 294 295 296
    if (added) {
        return addIgdToList(protocol, igd);
    } else {
        if (publicIpAddr) {
            return removeIgdFromList(publicIpAddr);
        } else {
            return removeIgdFromList(igd);
        }
Stepan Salenikovich's avatar
Stepan Salenikovich committed
297 298 299 300
    }
}

bool
301
UPnPContext::addIgdToList(UPnPProtocol* protocol, IGD* igd)
Stepan Salenikovich's avatar
Stepan Salenikovich committed
302
{
303
    // Check if IGD has a valid public IP.
304
    if (not igd->publicIp_) {
305
        JAMI_WARN("UPnPContext: IGD trying to be added has invalid public IpAddress");
Stepan Salenikovich's avatar
Stepan Salenikovich committed
306 307 308
        return false;
    }

309
    if (isIgdInList(igd->publicIp_)) {
310
        // If the protocol of the IGD that is already in the list isn't NatPmp, then swap.
311 312 313 314 315 316
        if (getIgdProtocol(igd) != UPnPProtocol::Type::NAT_PMP and protocol->getType() == UPnPProtocol::Type::NAT_PMP) {
            JAMI_WARN("UPnPContext: Attempting to swap IGD UPnP protocol");
            if (!removeIgdFromList(igd)) {
                JAMI_WARN("UPnPContext: Failed to swap IGD UPnP protocol");
                return false;
            }
317 318 319 320
        } else {
            return false;
        }
    }
Stepan Salenikovich's avatar
Stepan Salenikovich committed
321

322
    igdList_.emplace_back(protocol, igd);
323 324 325
    
    for (const auto& item : igdListeners_) {
        item.second();
Stepan Salenikovich's avatar
Stepan Salenikovich committed
326 327
    }

328
    return true;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
329 330
}

331 332
bool
UPnPContext::removeIgdFromList(IGD* igd)
Stepan Salenikovich's avatar
Stepan Salenikovich committed
333
{
334
    auto it = igdList_.begin();
335 336 337 338 339
    while (it != igdList_.end()) {
        if (it->second->publicIp_ == igd->publicIp_) {
            JAMI_WARN("UPnPContext: IGD with public IP %s was removed from the list", it->second->publicIp_.toString().c_str());
            igdList_.erase(it);
            return true;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
340
        } else {
341
            it++;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
342 343
        }
    }
344 345

    return false;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
346 347 348
}

bool
349
UPnPContext::removeIgdFromList(IpAddr publicIpAddr)
Stepan Salenikovich's avatar
Stepan Salenikovich committed
350
{
351
    auto it = igdList_.begin();
352 353 354 355 356 357 358 359
    while (it != igdList_.end()) {
        if (it->second->publicIp_ == publicIpAddr) {
            JAMI_WARN("UPnPContext: IGD with public IP %s was removed from the list", it->second->publicIp_.toString().c_str());
            igdList_.erase(it);
            return true;
        } else {
            it++;
        }
Stepan Salenikovich's avatar
Stepan Salenikovich committed
360 361
    }

362 363
    return false;
}
Stepan Salenikovich's avatar
Stepan Salenikovich committed
364

Adrien Béraud's avatar
Adrien Béraud committed
365
}} // namespace jami::upnp