blob: 21f11eee07a333c3f770e21647d33f3bd944586f [file] [log] [blame]
Adrien BĂ©raud612b55b2023-05-29 10:42:04 -04001/*
2 * Copyright (C) 2004-2023 Savoir-faire Linux Inc.
3 *
4 * Author: Eden Abitbol <eden.abitbol@savoirfairelinux.com>
5 * Author: Mohamed Chibani <mohamed.chibani@savoirfairelinux.com>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 */
21
22#include "nat_pmp.h"
23
24#if HAVE_LIBNATPMP
25
26namespace jami {
27namespace upnp {
28
29NatPmp::NatPmp()
30{
31 JAMI_DBG("NAT-PMP: Instance [%p] created", this);
32 runOnNatPmpQueue([this] {
33 threadId_ = getCurrentThread();
34 igd_ = std::make_shared<PMPIGD>();
35 });
36}
37
38NatPmp::~NatPmp()
39{
40 JAMI_DBG("NAT-PMP: Instance [%p] destroyed", this);
41}
42
43void
44NatPmp::initNatPmp()
45{
46 if (not isValidThread()) {
47 runOnNatPmpQueue([w = weak()] {
48 if (auto pmpThis = w.lock()) {
49 pmpThis->initNatPmp();
50 }
51 });
52 return;
53 }
54
55 initialized_ = false;
56
57 {
58 std::lock_guard<std::mutex> lock(natpmpMutex_);
59 hostAddress_ = ip_utils::getLocalAddr(AF_INET);
60 }
61
62 // Local address must be valid.
63 if (not getHostAddress() or getHostAddress().isLoopback()) {
64 JAMI_WARN("NAT-PMP: Does not have a valid local address!");
65 return;
66 }
67
68 assert(igd_);
69 if (igd_->isValid()) {
70 igd_->setValid(false);
71 processIgdUpdate(UpnpIgdEvent::REMOVED);
72 }
73
74 igd_->setLocalIp(IpAddr());
75 igd_->setPublicIp(IpAddr());
76 igd_->setUID("");
77
78 JAMI_DBG("NAT-PMP: Trying to initialize IGD");
79
80 int err = initnatpmp(&natpmpHdl_, 0, 0);
81
82 if (err < 0) {
83 JAMI_WARN("NAT-PMP: Initializing IGD using default gateway failed!");
84 const auto& localGw = ip_utils::getLocalGateway();
85 if (not localGw) {
86 JAMI_WARN("NAT-PMP: Couldn't find valid gateway on local host");
87 err = NATPMP_ERR_CANNOTGETGATEWAY;
88 } else {
89 JAMI_WARN("NAT-PMP: Trying to initialize using detected gateway %s",
90 localGw.toString().c_str());
91
92 struct in_addr inaddr;
93 inet_pton(AF_INET, localGw.toString().c_str(), &inaddr);
94 err = initnatpmp(&natpmpHdl_, 1, inaddr.s_addr);
95 }
96 }
97
98 if (err < 0) {
99 JAMI_ERR("NAT-PMP: Can't initialize libnatpmp -> %s", getNatPmpErrorStr(err));
100 return;
101 }
102
103 char addrbuf[INET_ADDRSTRLEN];
104 inet_ntop(AF_INET, &natpmpHdl_.gateway, addrbuf, sizeof(addrbuf));
105 IpAddr igdAddr(addrbuf);
106 JAMI_DBG("NAT-PMP: Initialized on gateway %s", igdAddr.toString().c_str());
107
108 // Set the local (gateway) address.
109 igd_->setLocalIp(igdAddr);
110 // NAT-PMP protocol does not have UID, but we will set generic
111 // one debugging purposes.
112 igd_->setUID("NAT-PMP Gateway");
113
114 // Search and set the public address.
115 getIgdPublicAddress();
116
117 // Update and notify.
118 if (igd_->isValid()) {
119 initialized_ = true;
120 processIgdUpdate(UpnpIgdEvent::ADDED);
121 };
122}
123
124void
125NatPmp::setObserver(UpnpMappingObserver* obs)
126{
127 if (not isValidThread()) {
128 runOnNatPmpQueue([w = weak(), obs] {
129 if (auto pmpThis = w.lock()) {
130 pmpThis->setObserver(obs);
131 }
132 });
133 return;
134 }
135
136 JAMI_DBG("NAT-PMP: Setting observer to %p", obs);
137
138 observer_ = obs;
139}
140
141void
142NatPmp::terminate(std::condition_variable& cv)
143{
144 initialized_ = false;
145 observer_ = nullptr;
146
147 {
148 std::lock_guard<std::mutex> lock(natpmpMutex_);
149 shutdownComplete_ = true;
150 cv.notify_one();
151 }
152}
153
154void
155NatPmp::terminate()
156{
157 std::unique_lock<std::mutex> lk(natpmpMutex_);
158 std::condition_variable cv {};
159
160 runOnNatPmpQueue([w = weak(), &cv = cv] {
161 if (auto pmpThis = w.lock()) {
162 pmpThis->terminate(cv);
163 }
164 });
165
166 if (cv.wait_for(lk, std::chrono::seconds(10), [this] { return shutdownComplete_; })) {
167 JAMI_DBG("NAT-PMP: Shutdown completed");
168 } else {
169 JAMI_ERR("NAT-PMP: Shutdown timed-out");
170 }
171}
172
173const IpAddr
174NatPmp::getHostAddress() const
175{
176 std::lock_guard<std::mutex> lock(natpmpMutex_);
177 return hostAddress_;
178}
179
180void
181NatPmp::clearIgds()
182{
183 if (not isValidThread()) {
184 runOnNatPmpQueue([w = weak()] {
185 if (auto pmpThis = w.lock()) {
186 pmpThis->clearIgds();
187 }
188 });
189 return;
190 }
191
192 bool do_close = false;
193
194 if (igd_) {
195 if (igd_->isValid()) {
196 do_close = true;
197 }
198 igd_->setValid(false);
199 }
200
201 initialized_ = false;
202 if (searchForIgdTimer_)
203 searchForIgdTimer_->cancel();
204
205 igdSearchCounter_ = 0;
206
207 if (do_close) {
208 closenatpmp(&natpmpHdl_);
209 memset(&natpmpHdl_, 0, sizeof(natpmpHdl_));
210 }
211}
212
213void
214NatPmp::searchForIgd()
215{
216 if (not isValidThread()) {
217 runOnNatPmpQueue([w = weak()] {
218 if (auto pmpThis = w.lock()) {
219 pmpThis->searchForIgd();
220 }
221 });
222 return;
223 }
224
225 if (not initialized_) {
226 initNatPmp();
227 }
228
229 // Schedule a retry in case init failed.
230 if (not initialized_) {
231 if (igdSearchCounter_++ < MAX_RESTART_SEARCH_RETRIES) {
232 JAMI_DBG("NAT-PMP: Start search for IGDs. Attempt %i", igdSearchCounter_);
233
234 // Cancel the current timer (if any) and re-schedule.
235 if (searchForIgdTimer_)
236 searchForIgdTimer_->cancel();
237
238 searchForIgdTimer_ = getNatpmpScheduler()->scheduleIn([this] { searchForIgd(); },
239 NATPMP_SEARCH_RETRY_UNIT
240 * igdSearchCounter_);
241 } else {
242 JAMI_WARN("NAT-PMP: Setup failed after %u trials. NAT-PMP will be disabled!",
243 MAX_RESTART_SEARCH_RETRIES);
244 }
245 }
246}
247
248std::list<std::shared_ptr<IGD>>
249NatPmp::getIgdList() const
250{
251 std::lock_guard<std::mutex> lock(natpmpMutex_);
252 std::list<std::shared_ptr<IGD>> igdList;
253 if (igd_->isValid())
254 igdList.emplace_back(igd_);
255 return igdList;
256}
257
258bool
259NatPmp::isReady() const
260{
261 if (observer_ == nullptr) {
262 JAMI_ERR("NAT-PMP: the observer is not set!");
263 return false;
264 }
265
266 // Must at least have a valid local address.
267 if (not getHostAddress() or getHostAddress().isLoopback())
268 return false;
269
270 return igd_ and igd_->isValid();
271}
272
273void
274NatPmp::incrementErrorsCounter(const std::shared_ptr<IGD>& igdIn)
275{
276 if (not validIgdInstance(igdIn)) {
277 return;
278 }
279
280 if (not igd_->isValid()) {
281 // Already invalid. Nothing to do.
282 return;
283 }
284
285 if (not igd_->incrementErrorsCounter()) {
286 // Disable this IGD.
287 igd_->setValid(false);
288 // Notify the listener.
289 JAMI_WARN("NAT-PMP: No more valid IGD!");
290
291 processIgdUpdate(UpnpIgdEvent::INVALID_STATE);
292 }
293}
294
295void
296NatPmp::requestMappingAdd(const Mapping& mapping)
297{
298 // Process on nat-pmp thread.
299 if (not isValidThread()) {
300 runOnNatPmpQueue([w = weak(), mapping] {
301 if (auto pmpThis = w.lock()) {
302 pmpThis->requestMappingAdd(mapping);
303 }
304 });
305 return;
306 }
307
308 Mapping map(mapping);
309 assert(map.getIgd());
310 auto err = addPortMapping(map);
311 if (err < 0) {
312 JAMI_WARN("NAT-PMP: Request for mapping %s on %s failed with error %i: %s",
313 map.toString().c_str(),
314 igd_->toString().c_str(),
315 err,
316 getNatPmpErrorStr(err));
317
318 if (isErrorFatal(err)) {
319 // Fatal error, increment the counter.
320 incrementErrorsCounter(igd_);
321 }
322 // Notify the listener.
323 processMappingRequestFailed(std::move(map));
324 } else {
325 JAMI_DBG("NAT-PMP: Request for mapping %s on %s succeeded",
326 map.toString().c_str(),
327 igd_->toString().c_str());
328 // Notify the listener.
329 processMappingAdded(std::move(map));
330 }
331}
332
333void
334NatPmp::requestMappingRenew(const Mapping& mapping)
335{
336 // Process on nat-pmp thread.
337 if (not isValidThread()) {
338 runOnNatPmpQueue([w = weak(), mapping] {
339 if (auto pmpThis = w.lock()) {
340 pmpThis->requestMappingRenew(mapping);
341 }
342 });
343 return;
344 }
345
346 Mapping map(mapping);
347 auto err = addPortMapping(map);
348 if (err < 0) {
349 JAMI_WARN("NAT-PMP: Renewal request for mapping %s on %s failed with error %i: %s",
350 map.toString().c_str(),
351 igd_->toString().c_str(),
352 err,
353 getNatPmpErrorStr(err));
354 // Notify the listener.
355 processMappingRequestFailed(std::move(map));
356
357 if (isErrorFatal(err)) {
358 // Fatal error, increment the counter.
359 incrementErrorsCounter(igd_);
360 }
361 } else {
362 JAMI_DBG("NAT-PMP: Renewal request for mapping %s on %s succeeded",
363 map.toString().c_str(),
364 igd_->toString().c_str());
365 // Notify the listener.
366 processMappingRenewed(map);
367 }
368}
369
370int
371NatPmp::readResponse(natpmp_t& handle, natpmpresp_t& response)
372{
373 int err = 0;
374 unsigned readRetriesCounter = 0;
375
376 while (true) {
377 if (readRetriesCounter++ > MAX_READ_RETRIES) {
378 err = NATPMP_ERR_SOCKETERROR;
379 break;
380 }
381
382 fd_set fds;
383 struct timeval timeout;
384 FD_ZERO(&fds);
385 FD_SET(handle.s, &fds);
386 getnatpmprequesttimeout(&handle, &timeout);
387 // Wait for data.
388 if (select(FD_SETSIZE, &fds, NULL, NULL, &timeout) == -1) {
389 err = NATPMP_ERR_SOCKETERROR;
390 break;
391 }
392
393 // Read the data.
394 err = readnatpmpresponseorretry(&handle, &response);
395
396 if (err == NATPMP_TRYAGAIN) {
397 std::this_thread::sleep_for(std::chrono::milliseconds(TIMEOUT_BEFORE_READ_RETRY));
398 } else {
399 break;
400 }
401 }
402
403 return err;
404}
405
406int
407NatPmp::sendMappingRequest(const Mapping& mapping, uint32_t& lifetime)
408{
409 CHECK_VALID_THREAD();
410
411 int err = sendnewportmappingrequest(&natpmpHdl_,
412 mapping.getType() == PortType::UDP ? NATPMP_PROTOCOL_UDP
413 : NATPMP_PROTOCOL_TCP,
414 mapping.getInternalPort(),
415 mapping.getExternalPort(),
416 lifetime);
417
418 if (err < 0) {
419 JAMI_ERR("NAT-PMP: Send mapping request failed with error %s %i",
420 getNatPmpErrorStr(err),
421 errno);
422 return err;
423 }
424
425 unsigned readRetriesCounter = 0;
426
427 while (readRetriesCounter++ < MAX_READ_RETRIES) {
428 // Read the response
429 natpmpresp_t response;
430 err = readResponse(natpmpHdl_, response);
431
432 if (err < 0) {
433 JAMI_WARN("NAT-PMP: Read response on IGD %s failed with error %s",
434 igd_->toString().c_str(),
435 getNatPmpErrorStr(err));
436 } else if (response.type != NATPMP_RESPTYPE_TCPPORTMAPPING
437 and response.type != NATPMP_RESPTYPE_UDPPORTMAPPING) {
438 JAMI_ERR("NAT-PMP: Unexpected response type (%i) for mapping %s from IGD %s.",
439 response.type,
440 mapping.toString().c_str(),
441 igd_->toString().c_str());
442 // Try to read again.
443 continue;
444 }
445
446 lifetime = response.pnu.newportmapping.lifetime;
447 // Done.
448 break;
449 }
450
451 return err;
452}
453
454int
455NatPmp::addPortMapping(Mapping& mapping)
456{
457 auto const& igdIn = mapping.getIgd();
458 assert(igdIn);
459 assert(igdIn->getProtocol() == NatProtocolType::NAT_PMP);
460
461 if (not igdIn->isValid() or not validIgdInstance(igdIn)) {
462 mapping.setState(MappingState::FAILED);
463 return NATPMP_ERR_INVALIDARGS;
464 }
465
466 mapping.setInternalAddress(getHostAddress().toString());
467
468 uint32_t lifetime = MAPPING_ALLOCATION_LIFETIME;
469 int err = sendMappingRequest(mapping, lifetime);
470
471 if (err < 0) {
472 mapping.setState(MappingState::FAILED);
473 return err;
474 }
475
476 // Set the renewal time and update.
477 mapping.setRenewalTime(sys_clock::now() + std::chrono::seconds(lifetime * 4 / 5));
478 mapping.setState(MappingState::OPEN);
479
480 return 0;
481}
482
483void
484NatPmp::requestMappingRemove(const Mapping& mapping)
485{
486 // Process on nat-pmp thread.
487 if (not isValidThread()) {
488 runOnNatPmpQueue([w = weak(), mapping] {
489 if (auto pmpThis = w.lock()) {
490 Mapping map {mapping};
491 pmpThis->removePortMapping(map);
492 }
493 });
494 return;
495 }
496}
497
498void
499NatPmp::removePortMapping(Mapping& mapping)
500{
501 auto igdIn = mapping.getIgd();
502 assert(igdIn);
503 if (not igdIn->isValid()) {
504 return;
505 }
506
507 if (not validIgdInstance(igdIn)) {
508 return;
509 }
510
511 Mapping mapToRemove(mapping);
512
513 uint32_t lifetime = 0;
514 int err = sendMappingRequest(mapping, lifetime);
515
516 if (err < 0) {
517 // Nothing to do if the request fails, just log the error.
518 JAMI_WARN("NAT-PMP: Send remove request failed with error %s. Ignoring",
519 getNatPmpErrorStr(err));
520 }
521
522 // Update and notify the listener.
523 mapToRemove.setState(MappingState::FAILED);
524 processMappingRemoved(std::move(mapToRemove));
525}
526
527void
528NatPmp::getIgdPublicAddress()
529{
530 CHECK_VALID_THREAD();
531
532 // Set the public address for this IGD if it does not
533 // have one already.
534 if (igd_->getPublicIp()) {
535 JAMI_WARN("NAT-PMP: IGD %s already have a public address (%s)",
536 igd_->toString().c_str(),
537 igd_->getPublicIp().toString().c_str());
538 return;
539 }
540 assert(igd_->getProtocol() == NatProtocolType::NAT_PMP);
541
542 int err = sendpublicaddressrequest(&natpmpHdl_);
543
544 if (err < 0) {
545 JAMI_ERR("NAT-PMP: send public address request on IGD %s failed with error: %s",
546 igd_->toString().c_str(),
547 getNatPmpErrorStr(err));
548
549 if (isErrorFatal(err)) {
550 // Fatal error, increment the counter.
551 incrementErrorsCounter(igd_);
552 }
553 return;
554 }
555
556 natpmpresp_t response;
557 err = readResponse(natpmpHdl_, response);
558
559 if (err < 0) {
560 JAMI_WARN("NAT-PMP: Read response on IGD %s failed - %s",
561 igd_->toString().c_str(),
562 getNatPmpErrorStr(err));
563 return;
564 }
565
566 if (response.type != NATPMP_RESPTYPE_PUBLICADDRESS) {
567 JAMI_ERR("NAT-PMP: Unexpected response type (%i) for public address request from IGD %s.",
568 response.type,
569 igd_->toString().c_str());
570 return;
571 }
572
573 IpAddr publicAddr(response.pnu.publicaddress.addr);
574
575 if (not publicAddr) {
576 JAMI_ERR("NAT-PMP: IGD %s returned an invalid public address %s",
577 igd_->toString().c_str(),
578 publicAddr.toString().c_str());
579 }
580
581 // Update.
582 igd_->setPublicIp(publicAddr);
583 igd_->setValid(true);
584
585 JAMI_DBG("NAT-PMP: Setting IGD %s public address to %s",
586 igd_->toString().c_str(),
587 igd_->getPublicIp().toString().c_str());
588}
589
590void
591NatPmp::removeAllMappings()
592{
593 CHECK_VALID_THREAD();
594
595 JAMI_WARN("NAT-PMP: Send request to close all existing mappings to IGD %s",
596 igd_->toString().c_str());
597
598 int err = sendnewportmappingrequest(&natpmpHdl_, NATPMP_PROTOCOL_TCP, 0, 0, 0);
599 if (err < 0) {
600 JAMI_WARN("NAT-PMP: Send close all TCP mappings request failed with error %s",
601 getNatPmpErrorStr(err));
602 }
603 err = sendnewportmappingrequest(&natpmpHdl_, NATPMP_PROTOCOL_UDP, 0, 0, 0);
604 if (err < 0) {
605 JAMI_WARN("NAT-PMP: Send close all UDP mappings request failed with error %s",
606 getNatPmpErrorStr(err));
607 }
608}
609
610const char*
611NatPmp::getNatPmpErrorStr(int errorCode) const
612{
613#ifdef ENABLE_STRNATPMPERR
614 return strnatpmperr(errorCode);
615#else
616 switch (errorCode) {
617 case NATPMP_ERR_INVALIDARGS:
618 return "INVALIDARGS";
619 break;
620 case NATPMP_ERR_SOCKETERROR:
621 return "SOCKETERROR";
622 break;
623 case NATPMP_ERR_CANNOTGETGATEWAY:
624 return "CANNOTGETGATEWAY";
625 break;
626 case NATPMP_ERR_CLOSEERR:
627 return "CLOSEERR";
628 break;
629 case NATPMP_ERR_RECVFROM:
630 return "RECVFROM";
631 break;
632 case NATPMP_ERR_NOPENDINGREQ:
633 return "NOPENDINGREQ";
634 break;
635 case NATPMP_ERR_NOGATEWAYSUPPORT:
636 return "NOGATEWAYSUPPORT";
637 break;
638 case NATPMP_ERR_CONNECTERR:
639 return "CONNECTERR";
640 break;
641 case NATPMP_ERR_WRONGPACKETSOURCE:
642 return "WRONGPACKETSOURCE";
643 break;
644 case NATPMP_ERR_SENDERR:
645 return "SENDERR";
646 break;
647 case NATPMP_ERR_FCNTLERROR:
648 return "FCNTLERROR";
649 break;
650 case NATPMP_ERR_GETTIMEOFDAYERR:
651 return "GETTIMEOFDAYERR";
652 break;
653 case NATPMP_ERR_UNSUPPORTEDVERSION:
654 return "UNSUPPORTEDVERSION";
655 break;
656 case NATPMP_ERR_UNSUPPORTEDOPCODE:
657 return "UNSUPPORTEDOPCODE";
658 break;
659 case NATPMP_ERR_UNDEFINEDERROR:
660 return "UNDEFINEDERROR";
661 break;
662 case NATPMP_ERR_NOTAUTHORIZED:
663 return "NOTAUTHORIZED";
664 break;
665 case NATPMP_ERR_NETWORKFAILURE:
666 return "NETWORKFAILURE";
667 break;
668 case NATPMP_ERR_OUTOFRESOURCES:
669 return "OUTOFRESOURCES";
670 break;
671 case NATPMP_TRYAGAIN:
672 return "TRYAGAIN";
673 break;
674 default:
675 return "UNKNOWNERR";
676 break;
677 }
678#endif
679}
680
681bool
682NatPmp::isErrorFatal(int error)
683{
684 switch (error) {
685 case NATPMP_ERR_INVALIDARGS:
686 case NATPMP_ERR_SOCKETERROR:
687 case NATPMP_ERR_CANNOTGETGATEWAY:
688 case NATPMP_ERR_CLOSEERR:
689 case NATPMP_ERR_RECVFROM:
690 case NATPMP_ERR_NOGATEWAYSUPPORT:
691 case NATPMP_ERR_CONNECTERR:
692 case NATPMP_ERR_SENDERR:
693 case NATPMP_ERR_UNDEFINEDERROR:
694 case NATPMP_ERR_UNSUPPORTEDVERSION:
695 case NATPMP_ERR_UNSUPPORTEDOPCODE:
696 case NATPMP_ERR_NOTAUTHORIZED:
697 case NATPMP_ERR_NETWORKFAILURE:
698 case NATPMP_ERR_OUTOFRESOURCES:
699 return true;
700 default:
701 return false;
702 }
703}
704
705bool
706NatPmp::validIgdInstance(const std::shared_ptr<IGD>& igdIn)
707{
708 if (igd_.get() != igdIn.get()) {
709 JAMI_ERR("NAT-PMP: IGD (%s) does not match local instance (%s)",
710 igdIn->toString().c_str(),
711 igd_->toString().c_str());
712 return false;
713 }
714
715 return true;
716}
717
718void
719NatPmp::processIgdUpdate(UpnpIgdEvent event)
720{
721 if (igd_->isValid()) {
722 // Remove all current mappings if any.
723 removeAllMappings();
724 }
725
726 if (observer_ == nullptr)
727 return;
728 // Process the response on the context thread.
729 runOnUpnpContextQueue([obs = observer_, igd = igd_, event] { obs->onIgdUpdated(igd, event); });
730}
731
732void
733NatPmp::processMappingAdded(const Mapping& map)
734{
735 if (observer_ == nullptr)
736 return;
737
738 // Process the response on the context thread.
739 runOnUpnpContextQueue([obs = observer_, igd = igd_, map] { obs->onMappingAdded(igd, map); });
740}
741
742void
743NatPmp::processMappingRequestFailed(const Mapping& map)
744{
745 if (observer_ == nullptr)
746 return;
747
748 // Process the response on the context thread.
749 runOnUpnpContextQueue([obs = observer_, igd = igd_, map] { obs->onMappingRequestFailed(map); });
750}
751
752void
753NatPmp::processMappingRenewed(const Mapping& map)
754{
755 if (observer_ == nullptr)
756 return;
757
758 // Process the response on the context thread.
759 runOnUpnpContextQueue([obs = observer_, igd = igd_, map] { obs->onMappingRenewed(igd, map); });
760}
761
762void
763NatPmp::processMappingRemoved(const Mapping& map)
764{
765 if (observer_ == nullptr)
766 return;
767
768 // Process the response on the context thread.
769 runOnUpnpContextQueue([obs = observer_, igd = igd_, map] { obs->onMappingRemoved(igd, map); });
770}
771
772} // namespace upnp
773} // namespace jami
774
775#endif //-- #if HAVE_LIBNATPMP