From 7649d5bec7e60f79ecdee7612e4729bdbb178265 Mon Sep 17 00:00:00 2001
From: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
Date: Tue, 9 Dec 2014 10:18:36 -0500
Subject: [PATCH] daemon: add upnp support

New settings in daemon preferences to use UPnP, default false.
Currently opens ports for SIP and ip2ip accounts and their calls.
Opens ports negotiated by SDP sesssion.
Tries to open ports for ICE candidates and adds ICE candidates
with the public IP to help ICE negotiation.
Added miniupnpc to contrib and patched to be able to generate just
a static lib.
Supports multiple clients on the same UPnP router. Will select a
different port at random if the desired port is already taken by
another client.

Refs #63247
Refs #63303

Change-Id: I00ae79bc3a24d3e54513a6d299fd9b6a70bcc3c2
---
 daemon/configure.ac                           |  55 ++
 daemon/contrib/src/miniupnpc/SHA512SUMS       |   1 +
 daemon/contrib/src/miniupnpc/makefile.patch   |  16 +
 daemon/contrib/src/miniupnpc/rules.mak        |  26 +
 .../contrib/src/pjproject/pj_ice_sess.patch   |  22 +
 daemon/contrib/src/pjproject/rules.mak        |   1 +
 daemon/src/Makefile.am                        |   3 +-
 daemon/src/account.cpp                        |  27 +
 daemon/src/account.h                          |  39 ++
 daemon/src/call.cpp                           |   2 +-
 daemon/src/iax/iaxaccount.h                   |   7 +
 daemon/src/ice_transport.cpp                  |  87 ++-
 daemon/src/ice_transport.h                    |  30 +
 daemon/src/ip_utils.h                         |   4 +
 daemon/src/managerimpl.cpp                    |  29 +-
 daemon/src/preferences.cpp                    |   4 +
 daemon/src/preferences.h                      |   8 +
 daemon/src/ringdht/ringaccount.cpp            |  68 +-
 daemon/src/ringdht/ringaccount.h              |  13 +
 daemon/src/sip/sdp.h                          |  18 +
 daemon/src/sip/sipaccount.cpp                 |  65 +-
 daemon/src/sip/sipaccount.h                   |  14 +
 daemon/src/sip/sipaccountbase.cpp             |   4 +
 daemon/src/sip/sipaccountbase.h               |   5 +-
 daemon/src/sip/sipcall.cpp                    |  39 +-
 daemon/src/sip/sipcall.h                      |   8 +
 daemon/src/sip/sipvoiplink.cpp                |  22 +-
 daemon/src/upnp/Makefile.am                   |  11 +
 daemon/src/upnp/upnp.cpp                      | 619 ++++++++++++++++++
 daemon/src/upnp/upnp.h                        | 247 +++++++
 30 files changed, 1459 insertions(+), 35 deletions(-)
 create mode 100644 daemon/contrib/src/miniupnpc/SHA512SUMS
 create mode 100644 daemon/contrib/src/miniupnpc/makefile.patch
 create mode 100644 daemon/contrib/src/miniupnpc/rules.mak
 create mode 100644 daemon/contrib/src/pjproject/pj_ice_sess.patch
 create mode 100644 daemon/src/upnp/Makefile.am
 create mode 100644 daemon/src/upnp/upnp.cpp
 create mode 100644 daemon/src/upnp/upnp.h

diff --git a/daemon/configure.ac b/daemon/configure.ac
index 99b79a1e66..ea7a954613 100644
--- a/daemon/configure.ac
+++ b/daemon/configure.ac
@@ -537,6 +537,60 @@ AC_ARG_ENABLE([ipv6], AS_HELP_STRING([--enable-ipv6], [Enable IPv6 support]))
 AC_DEFINE_UNQUOTED([HAVE_IPV6], `if test "x$enable_ipv6" = "xyes"; then echo 1; else echo 0; fi`, [Define if you have IPv6])
 AM_CONDITIONAL(BUILD_IPV6, test "x$enable_ipv6" = "xyes" )
 
+# miniupnpc
+# required dependency(ies): libminiupc
+dnl check for libminiupnpc (doesn't use pkg-config)
+AC_ARG_WITH([upnp], [AS_HELP_STRING([--without-upnp],
+            [disable support for upnp])], [], [with_upnp=yes])
+
+LIBMINIUPNPC=
+AS_IF([test "x$with_upnp" != xno],
+      [AC_CHECK_HEADER([miniupnpc/miniupnpc.h], , AC_MSG_FAILURE([Unable to find libminiupnpc headers (you may need to install the dev package).  You may use --without-upnp to compile without upnp support.]))]
+      [AC_CHECK_LIB([miniupnpc], [simpleUPnPcommand], [
+          UPNP_LIBS=-lminiupnpc
+          AC_SUBST(UPNP_LIBS)
+          miniupnp_version="unknown"],
+        [AC_MSG_FAILURE([libminiupnpc link test failed. You may use --without-upnp to compile without upnp support.])
+          miniupnp_version="none"
+          AC_DEFINE([HAVE_UPNP], 0, [Define if you have miniupnpc])]
+      )],
+      [AC_DEFINE([HAVE_UPNP], 0, [Define if you have miniupnpc])])
+
+dnl need to determine API version
+dnl if libminiupnpc is v1.7 or higher, then the API version should be defined
+if test "x$miniupnp_version" = "xunknown" ; then
+  AC_MSG_CHECKING("if libminiupnpc is v1.7 or higher to determine API version")
+  AC_RUN_IFELSE(
+    [AC_LANG_PROGRAM(
+      [#include <stdlib.h>
+       #include <miniupnpc/miniupnpc.h>],
+      [#ifdef MINIUPNPC_API_VERSION
+       return EXIT_SUCCESS;
+       #else
+       return EXIT_FAILURE;
+       #endif]
+    )],
+    [miniupnp_version=">= 1.7"
+      AC_MSG_RESULT("found v1.7 or higher")
+      AC_DEFINE([HAVE_UPNP], 1, [Define if you have miniupnpc])],
+    [AC_MSG_RESULT("found v1.6 or lower")])
+fi
+
+dnl otherwise it must be v1.6 or lower; we don't support lower than v1.6, which is API v8
+dnl v1.6 introduced IPv6 commands, so we check for one of them
+if test "x$miniupnp_version" = "xunknown" ; then
+  AC_MSG_CHECKING("if libminiupnpc is at least v1.6")
+  AC_CHECK_LIB([miniupnpc], [UPNP_GetFirewallStatus],[
+      miniupnp_version="1.6"
+      AC_DEFINE(MINIUPNPC_API_VERSION, 8, [libminiupnpc 1.6 has API version 8])
+      AC_MSG_RESULT("detected libminiupnpc is v1.6")
+      AC_DEFINE([HAVE_UPNP], 1, [Define if you have miniupnpc])],
+    [AC_DEFINE([HAVE_UPNP], 0, [Define if you have miniupnpc])
+      AC_MSG_RESULT("detected libminiupnpc is lower than v1.6")
+      AC_MSG_FAILURE([libminiupnpc less than v1.6 is not supported])])
+fi
+
+AM_CONDITIONAL(BUILD_UPNP, test "x$with_upnp" = "xyes" )
 
 # DOXYGEN
 # required dependency(ies): doxygen
@@ -592,6 +646,7 @@ AC_CONFIG_FILES([Makefile \
                  src/media/video/Makefile \
                  src/media/video/v4l2/Makefile \
                  src/media/video/test/Makefile \
+                 src/upnp/Makefile \
                  test/Makefile \
                  ringtones/Makefile \
                  man/Makefile \
diff --git a/daemon/contrib/src/miniupnpc/SHA512SUMS b/daemon/contrib/src/miniupnpc/SHA512SUMS
new file mode 100644
index 0000000000..c368315349
--- /dev/null
+++ b/daemon/contrib/src/miniupnpc/SHA512SUMS
@@ -0,0 +1 @@
+7994749fa92e6478285dada191f4ad0d15fdce6cf281ee4d1d3d5615548338d68be3b9ce96a04ef4b81c697589689398060cc6f610101fa1d0e6f9e24b7850a9  miniupnpc-1.9.tar.gz
diff --git a/daemon/contrib/src/miniupnpc/makefile.patch b/daemon/contrib/src/miniupnpc/makefile.patch
new file mode 100644
index 0000000000..acafe2d11a
--- /dev/null
+++ b/daemon/contrib/src/miniupnpc/makefile.patch
@@ -0,0 +1,16 @@
+--- a/Makefile	2015-01-05 15:45:40.426809962 -0500
++++ b/Makefile	2015-01-05 15:48:24.470817044 -0500
+@@ -212,6 +212,13 @@
+ endif
+ endif
+
++install-static:	updateversion $(FILESTOINSTALL)
++	$(INSTALL) -d $(DESTDIR)$(INSTALLDIRINC)
++	$(INSTALL) -m 644 $(HEADERS) $(DESTDIR)$(INSTALLDIRINC)
++	$(INSTALL) -d $(DESTDIR)$(INSTALLDIRLIB)
++	$(INSTALL) -m 644 $(LIBRARY) $(DESTDIR)$(INSTALLDIRLIB)
++	$(INSTALL) -d $(DESTDIR)$(INSTALLDIRBIN)
++	$(INSTALL) -m 755 external-ip.sh $(DESTDIR)$(INSTALLDIRBIN)/external-ip
+
+ cleaninstall:
+	$(RM) -r $(DESTDIR)$(INSTALLDIRINC)
diff --git a/daemon/contrib/src/miniupnpc/rules.mak b/daemon/contrib/src/miniupnpc/rules.mak
new file mode 100644
index 0000000000..d77aff5f79
--- /dev/null
+++ b/daemon/contrib/src/miniupnpc/rules.mak
@@ -0,0 +1,26 @@
+# miniupnpc
+
+MINIUPNPC_VERSION := 1.9
+MINIUPNPC_URL := http://miniupnp.free.fr/files/download.php?file=miniupnpc-$(MINIUPNPC_VERSION).tar.gz
+
+PKGS += miniupnpc
+ifeq ($(call need_pkg,"miniupnpc >= 1.6"),)
+PKGS_FOUND += miniupnpc
+endif
+
+$(TARBALLS)/miniupnpc-$(MINIUPNPC_VERSION).tar.gz:
+	$(call download,$(MINIUPNPC_URL))
+
+.sum-miniupnpc: miniupnpc-$(MINIUPNPC_VERSION).tar.gz
+	$(warning $@ not implemented)
+	touch $@
+
+miniupnpc: miniupnpc-$(MINIUPNPC_VERSION).tar.gz .sum-miniupnpc
+	$(UNPACK)
+	$(APPLY) $(SRC)/miniupnpc/makefile.patch
+	$(UPDATE_AUTOCONFIG) && cd $(UNPACK_DIR)
+	$(MOVE)
+
+.miniupnpc: miniupnpc
+	cd $< && INSTALLPREFIX=$(PREFIX) $(MAKE) install-static
+	touch $@
diff --git a/daemon/contrib/src/pjproject/pj_ice_sess.patch b/daemon/contrib/src/pjproject/pj_ice_sess.patch
new file mode 100644
index 0000000000..bd040ffe3e
--- /dev/null
+++ b/daemon/contrib/src/pjproject/pj_ice_sess.patch
@@ -0,0 +1,22 @@
+--- a/pjnath/include/pjnath/ice_strans.h
++++ b/pjnath/include/pjnath/ice_strans.h
+@@ -845,6 +845,8 @@ PJ_DECL(pj_status_t) pj_ice_strans_sendt
+					  int dst_addr_len);
+
+
++PJ_DECL(pj_ice_sess *) pj_ice_strans_get_ice_sess(pj_ice_strans *ice_st);
++
+ /**
+  * @}
+  */
+--- a/pjnath/src/pjnath/ice_strans.c
++++ b/pjnath/src/pjnath/ice_strans.c
+@@ -1243,6 +1243,11 @@ PJ_DEF(pj_status_t) pj_ice_strans_sendto
+	return PJ_EINVALIDOP;
+ }
+
++PJ_DECL(pj_ice_sess *) pj_ice_strans_get_ice_sess( pj_ice_strans *ice_st )
++{
++	return ice_st->ice;
++}
++
diff --git a/daemon/contrib/src/pjproject/rules.mak b/daemon/contrib/src/pjproject/rules.mak
index a4e1d79618..829bf3fac3 100644
--- a/daemon/contrib/src/pjproject/rules.mak
+++ b/daemon/contrib/src/pjproject/rules.mak
@@ -58,6 +58,7 @@ endif
 	$(APPLY) $(SRC)/pjproject/ipv6.patch
 	$(APPLY) $(SRC)/pjproject/ice_config.patch
 	$(APPLY) $(SRC)/pjproject/multiple_listeners.patch
+	$(APPLY) $(SRC)/pjproject/pj_ice_sess.patch
 	$(UPDATE_AUTOCONFIG)
 	$(MOVE)
 
diff --git a/daemon/src/Makefile.am b/daemon/src/Makefile.am
index 77cce13c7a..19237c386c 100644
--- a/daemon/src/Makefile.am
+++ b/daemon/src/Makefile.am
@@ -34,7 +34,7 @@ TLS_LIB = @GNUTLS_LIBS@
 TLS_CFLAGS = @GNUTLS_CFLAGS@
 endif
 
-SUBDIRS = client media config hooks history sip $(IAX_SUBDIR) $(RINGACC_SUBDIR) $(INSTANT_MESSAGING_SUBDIR) $(RING_VIDEO_SUBDIR)
+SUBDIRS = client media config hooks history sip upnp $(IAX_SUBDIR) $(RINGACC_SUBDIR) $(INSTANT_MESSAGING_SUBDIR) $(RING_VIDEO_SUBDIR)
 
 # libring
 
@@ -47,6 +47,7 @@ libring_la_LIBADD = \
 	./config/libconfig.la \
 	./hooks/libhooks.la \
 	./history/libhistory.la \
+	./upnp/libupnp.la \
 	$(RINGACC_LIBA) \
 	$(IAX_LIBA) \
 	$(IM_LIBA) \
diff --git a/daemon/src/account.cpp b/daemon/src/account.cpp
index 8183b2c9e1..34ebabb595 100644
--- a/daemon/src/account.cpp
+++ b/daemon/src/account.cpp
@@ -35,8 +35,10 @@
 #include "config.h"
 #endif
 #include "account.h"
+
 #include <algorithm>
 #include <iterator>
+#include <mutex>
 
 #ifdef RING_VIDEO
 #include "libav_utils.h"
@@ -52,6 +54,9 @@
 
 #include <yaml-cpp/yaml.h>
 
+#include "upnp/upnp.h"
+#include "ip_utils.h"
+
 namespace ring {
 
 const char * const Account::AUDIO_CODECS_KEY            = "audioCodecs";  // 0/9/110/111/112/
@@ -454,4 +459,26 @@ Account::parseBool(const std::map<std::string, std::string> &details, const char
 
 #undef find_iter
 
+void
+Account::setUseUPnP(bool useUPnP)
+{
+    std::unique_lock<std::mutex> lk(upnp_mtx);
+
+    if (useUPnP == static_cast<bool>(upnp_))
+        return;
+
+    if (useUPnP){
+        upnp_.reset(new upnp::Controller());
+        upnpIp_ = upnp_->getExternalIP();
+    } else
+        upnp_.reset();
+}
+
+bool
+Account::getUseUPnP() const
+{
+    std::unique_lock<std::mutex> lk(upnp_mtx);
+    return static_cast<bool>(upnp_);
+}
+
 } // namespace ring
diff --git a/daemon/src/account.h b/daemon/src/account.h
index 73658097a0..15590e4762 100644
--- a/daemon/src/account.h
+++ b/daemon/src/account.h
@@ -33,9 +33,14 @@
 #ifndef ACCOUNT_H
 #define ACCOUNT_H
 
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
 #include "noncopyable.h"
 #include "config/serializable.h"
 #include "registration_states.h"
+#include "ip_utils.h"
 
 #include <functional>
 #include <string>
@@ -45,6 +50,12 @@
 #include <set>
 #include <random>
 #include <stdexcept>
+#include <atomic>
+#include <mutex>
+
+namespace ring { namespace upnp {
+class Controller;
+}}
 
 namespace YAML {
 class Emitter;
@@ -243,6 +254,26 @@ class Account : public Serializable, public std::enable_shared_from_this<Account
         static const char * const VIDEO_CODEC_PARAMETERS;
         static const char * const VIDEO_CODEC_BITRATE;
 
+        /**
+         * Set whether or not to use UPnP
+         */
+        void setUseUPnP(bool useUPnP);
+
+        /**
+         * Get whether UPnP is used.
+         * @return bool Flag which determines if UPnP is used or not.
+         */
+        bool getUseUPnP() const;
+
+        /**
+         * Get the UPnP IP (external router) address.
+         * If use UPnP is set to false, the address will be empty.
+         */
+        IpAddr getUPnPIpAddress() const {
+            std::unique_lock<std::mutex> lk(upnp_mtx);
+            return upnpIp_;
+        }
+
     private:
         NON_COPYABLE(Account);
 
@@ -375,6 +406,14 @@ class Account : public Serializable, public std::enable_shared_from_this<Account
          * Random generator engine
          */
         std::mt19937_64 rand_ {};
+
+        /**
+         * UPnP IP address (external router address),
+         * used only if use UPnP is set to true
+         */
+        IpAddr upnpIp_ {};
+        std::unique_ptr<ring::upnp::Controller> upnp_;
+        mutable std::mutex upnp_mtx {};
 };
 
 } // namespace ring
diff --git a/daemon/src/call.cpp b/daemon/src/call.cpp
index 8e3ee2ffb7..2c4d4c838f 100644
--- a/daemon/src/call.cpp
+++ b/daemon/src/call.cpp
@@ -308,7 +308,7 @@ Call::initIceTransport(bool master, unsigned channel_num)
 {
     auto& iceTransportFactory = Manager::instance().getIceTransportFactory();
     iceTransport_ = iceTransportFactory.createTransport(getCallId().c_str(), channel_num,
-                                                        master);
+                                                        master, account_.getUseUPnP());
 }
 
 int
diff --git a/daemon/src/iax/iaxaccount.h b/daemon/src/iax/iaxaccount.h
index ff3a70989e..1aa100ead2 100644
--- a/daemon/src/iax/iaxaccount.h
+++ b/daemon/src/iax/iaxaccount.h
@@ -132,6 +132,13 @@ class IAXAccount : public Account {
         std::shared_ptr<enable_if_base_of<T, IAXCall> >
         newIncomingCall(const std::string& id);
 
+        /**
+         * Set whether or not to use UPnP
+         */
+        void setUseUPnP(bool) {
+            /* do nothing for now as UPnP isn't implemented for IAX */
+        }
+
     private:
 
         void setAccountDetails(const std::map<std::string, std::string> &details);
diff --git a/daemon/src/ice_transport.cpp b/daemon/src/ice_transport.cpp
index d26a59f205..2b83fba7f2 100644
--- a/daemon/src/ice_transport.cpp
+++ b/daemon/src/ice_transport.cpp
@@ -32,6 +32,7 @@
 #include "logger.h"
 #include "sip/sip_utils.h"
 #include "manager.h"
+#include "upnp/upnp.h"
 
 #include <pjlib.h>
 #include <utility>
@@ -94,7 +95,7 @@ IceTransport::cb_on_ice_complete(pj_ice_strans* ice_st,
 }
 
 IceTransport::IceTransport(const char* name, int component_count,
-                           bool master,
+                           bool master, bool upnp_enabled,
                            IceTransportCompleteCb on_initdone_cb,
                            IceTransportCompleteCb on_negodone_cb)
     : pool_(nullptr, pj_pool_release)
@@ -104,6 +105,9 @@ IceTransport::IceTransport(const char* name, int component_count,
     , compIO_(component_count)
     , initiator_session_(master)
 {
+    if (upnp_enabled)
+        upnp_.reset(new upnp::Controller());
+
     auto& iceTransportFactory = Manager::instance().getIceTransportFactory();
 
     pool_.reset(pj_pool_create(iceTransportFactory.getPoolFactory(),
@@ -125,6 +129,8 @@ IceTransport::IceTransport(const char* name, int component_count,
         throw std::runtime_error("pj_ice_strans_create() failed");
 }
 
+IceTransport::~IceTransport() = default;
+
 void
 IceTransport::onComplete(pj_ice_strans* ice_st, pj_ice_strans_op op,
                          pj_status_t status)
@@ -156,6 +162,8 @@ IceTransport::onComplete(pj_ice_strans* ice_st, pj_ice_strans_op op,
             }
             if (on_initdone_cb_)
                 on_initdone_cb_(*this, done);
+            if (iceTransportInitDone_)
+                selectUPnPIceCandidates();
         } else if (op == PJ_ICE_STRANS_OP_NEGOTIATION) {
             iceTransportNegoDone_ = done;
             if (on_negodone_cb_)
@@ -406,6 +414,80 @@ IceTransport::getLocalCandidates(unsigned comp_id) const
     return res;
 }
 
+std::vector<IpAddr>
+IceTransport::getLocalCandidatesAddr(unsigned comp_id) const
+{
+    std::vector<IpAddr> cand_addrs;
+    pj_ice_sess_cand cand[PJ_ARRAY_SIZE(cand_)];
+    unsigned cand_cnt = PJ_ARRAY_SIZE(cand);
+
+    if (pj_ice_strans_enum_cands(icest_.get(), comp_id+1, &cand_cnt, cand) != PJ_SUCCESS) {
+        RING_ERR("pj_ice_strans_enum_cands() failed");
+        return cand_addrs;
+    }
+
+    for (unsigned i=0; i<cand_cnt; ++i) {
+        cand_addrs.push_back(cand[i].addr);
+    }
+}
+
+void
+IceTransport::addCandidate(int comp_id, const IpAddr& addr)
+{
+    pj_ice_sess_cand cand;
+
+    cand.type = PJ_ICE_CAND_TYPE_HOST;
+    cand.status = PJ_SUCCESS;
+    cand.comp_id = comp_id + 1; /* starts at 1, not 0 */
+    cand.transport_id = 1; /* 1 = STUN */
+    cand.local_pref = 65535; /* host */
+    /* cand.foundation = ? */
+    /* cand.prio = calculated by ice session */
+    /* make base and addr the same since we're not going through a server */
+    pj_sockaddr_cp(&cand.base_addr, addr.pjPtr());
+    pj_sockaddr_cp(&cand.addr, addr.pjPtr());
+    pj_bzero(&cand.rel_addr, sizeof(cand.rel_addr)); /* not usring rel_addr */
+    pj_ice_calc_foundation(pool_.get(), &cand.foundation, cand.type, &cand.base_addr);
+
+    pj_ice_sess_add_cand(pj_ice_strans_get_ice_sess(icest_.get()),
+        cand.comp_id,
+        cand.transport_id,
+        cand.type,
+        cand.local_pref,
+        &cand.foundation,
+        &cand.addr,
+        &cand.base_addr,
+        &cand.rel_addr,
+        pj_sockaddr_get_len(&cand.addr),
+        NULL);
+}
+
+void
+IceTransport::selectUPnPIceCandidates()
+{
+    /* use upnp to open ports and add the proper candidates */
+    if (upnp_) {
+        /* for every component, get the candidate(s)
+         * create a port mapping either with that port, or with an available port
+         * add candidate with that port and public IP
+         */
+        IpAddr publicIP = upnp_->getExternalIP();
+        for(unsigned comp_id = 0; comp_id < component_count_; comp_id++) {
+            RING_DBG("UPnP : Opening port(s) for Ice comp %d and adding candidate with public IP.", comp_id);
+            std::vector<IpAddr> candidates = getLocalCandidatesAddr(comp_id);
+            for(IpAddr addr : candidates) {
+                uint16_t port = addr.getPort();
+                uint16_t port_used;
+                if (upnp_->addAnyMapping(port, upnp::PortType::UDP, true, &port_used)) {
+                    publicIP.setPort(port_used);
+                    addCandidate(comp_id, publicIP);
+                } else
+                    RING_WARN("UPnP : Could not create a port mapping for the ICE candidae.");
+            }
+        }
+    }
+}
+
 std::vector<uint8_t>
 IceTransport::getLocalAttributesAndCandidates() const
 {
@@ -713,10 +795,11 @@ std::shared_ptr<IceTransport>
 IceTransportFactory::createTransport(const char* name,
                                      int component_count,
                                      bool master,
+                                     bool upnp_enabled,
                                      IceTransportCompleteCb&& on_initdone_cb,
                                      IceTransportCompleteCb&& on_negodone_cb)
 {
-    return std::make_shared<IceTransport>(name, component_count, master,
+    return std::make_shared<IceTransport>(name, component_count, master, upnp_enabled,
                                           std::forward<IceTransportCompleteCb>(on_initdone_cb),
                                           std::forward<IceTransportCompleteCb>(on_negodone_cb));
 }
diff --git a/daemon/src/ice_transport.h b/daemon/src/ice_transport.h
index b3c7abaee6..98a7eba494 100644
--- a/daemon/src/ice_transport.h
+++ b/daemon/src/ice_transport.h
@@ -48,6 +48,10 @@
 
 namespace ring {
 
+namespace upnp {
+class Controller;
+}
+
 class IceTransport;
 
 using IceTransportCompleteCb = std::function<void(IceTransport&, bool)>;
@@ -66,9 +70,15 @@ class IceTransport {
          */
         IceTransport(const char* name, int component_count,
                      bool master,
+                     bool upnp_enabled = false,
                      IceTransportCompleteCb on_initdone_cb={},
                      IceTransportCompleteCb on_negodone_cb={});
 
+        /**
+         * Destructor
+         */
+        ~IceTransport();
+
         /**
          * Set/change transport role as initiator.
          * Should be called before start method.
@@ -147,6 +157,8 @@ class IceTransport {
 
         ssize_t waitForData(int comp_id, unsigned int timeout);
 
+        unsigned getComponentCount() const {return component_count_;};
+
     private:
         static constexpr int MAX_CANDIDATES {32};
 
@@ -212,6 +224,23 @@ class IceTransport {
         std::vector<ComponentIO> compIO_;
 
         bool initiator_session_ {true};
+
+        /**
+         * Returns the IP of each candidate for a given component in the ICE session
+         */
+        std::vector<IpAddr> getLocalCandidatesAddr(unsigned comp_id) const;
+
+        /**
+         * Adds candidate to ICE session
+         */
+        void addCandidate(int comp_id, const IpAddr& addr);
+
+        /**
+         * Creates UPnP port mappings and adds ICE candidates based on those mappings
+         */
+        void selectUPnPIceCandidates();
+
+        std::unique_ptr<upnp::Controller> upnp_;
 };
 
 class IceTransportFactory {
@@ -222,6 +251,7 @@ class IceTransportFactory {
         std::shared_ptr<IceTransport> createTransport(const char* name,
                                                       int component_count,
                                                       bool master,
+                                                      bool upnp_enabled = false,
                                                       IceTransportCompleteCb&& on_initdone_cb={},
                                                       IceTransportCompleteCb&& on_negodone_cb={});
 
diff --git a/daemon/src/ip_utils.h b/daemon/src/ip_utils.h
index 2b094831b1..572a3c6796 100644
--- a/daemon/src/ip_utils.h
+++ b/daemon/src/ip_utils.h
@@ -133,6 +133,10 @@ public:
         return ss;
     }
 
+    inline const pj_sockaddr* pjPtr() const {
+        return &addr;
+    }
+
     inline pj_sockaddr* pjPtr() {
         return &addr;
     }
diff --git a/daemon/src/managerimpl.cpp b/daemon/src/managerimpl.cpp
index 4780bc2678..cb4586f80b 100644
--- a/daemon/src/managerimpl.cpp
+++ b/daemon/src/managerimpl.cpp
@@ -81,6 +81,8 @@
 #include "conference.h"
 #include "ice_transport.h"
 
+#include "upnp/upnp.h"
+
 #include <cerrno>
 #include <algorithm>
 #include <ctime>
@@ -240,6 +242,12 @@ ManagerImpl::init(const std::string &config_file)
     }
 
     history_.load(preferences.getHistoryLimit());
+
+    if (preferences.getUseUPnP()) {
+        RING_DBG("Remove any old UPnP mappings for RING mapped to this IP");
+        upnp::Controller().removeMappingsByLocalIPAndDescription();
+    }
+
     registerAccounts();
 }
 
@@ -286,6 +294,11 @@ ManagerImpl::finish()
     } catch (const VoipLinkException &err) {
         RING_ERR("%s", err.what());
     }
+
+    if (preferences.getUseUPnP()) {
+        RING_DBG("Remove any remaning ports mapped to this client for RING");
+        upnp::Controller().removeMappingsByLocalIPAndDescription();
+    }
 }
 
 bool
@@ -2393,9 +2406,10 @@ ManagerImpl::setAccountDetails(const std::string& accountID,
         // Serialize configuration to disk once it is done
         saveConfig();
 
-        if (account->isEnabled())
+        if (account->isEnabled()) {
+            account->setUseUPnP(preferences.getUseUPnP());
             account->doRegister();
-        else
+        } else
             account->doUnregister();
 
         // Update account details to the client side
@@ -2440,6 +2454,8 @@ ManagerImpl::addAccount(const std::map<std::string, std::string>& details)
 
     preferences.addAccount(newAccountID);
 
+    newAccount->setUseUPnP(preferences.getUseUPnP());
+
     newAccount->doRegister();
 
     saveConfig();
@@ -2698,8 +2714,10 @@ ManagerImpl::registerAccounts()
 
         a->loadConfig();
 
-        if (a->isEnabled())
+        if (a->isEnabled()) {
+            a->setUseUPnP(preferences.getUseUPnP());
             a->doRegister();
+        }
     }
 }
 
@@ -2724,9 +2742,10 @@ ManagerImpl::sendRegister(const std::string& accountID, bool enable)
 
     Manager::instance().saveConfig();
 
-    if (acc->isEnabled())
+    if (acc->isEnabled()) {
+        acc->setUseUPnP(preferences.getUseUPnP());
         acc->doRegister();
-    else
+    } else
         acc->doUnregister();
 }
 
diff --git a/daemon/src/preferences.cpp b/daemon/src/preferences.cpp
index 504c34fbc3..57c81f6ca0 100644
--- a/daemon/src/preferences.cpp
+++ b/daemon/src/preferences.cpp
@@ -77,6 +77,7 @@ static const char * const ZONE_TONE_CHOICE_KEY = "zoneToneChoice";
 static const char * const PORT_NUM_KEY = "portNum";
 static const char * const SEARCH_BAR_DISPLAY_KEY = "searchBarDisplay";
 static const char * const MD5_HASH_KEY = "md5Hash";
+static const char * const USE_UPNP = "useUPnP";
 
 // voip preferences
 constexpr const char * const VoipPreference::CONFIG_LABEL;
@@ -137,6 +138,7 @@ Preferences::Preferences() :
     , portNum_(5060)
     , searchBarDisplay_(true)
     , md5Hash_(false)
+    , useUPnP_(false)
 {}
 
 void Preferences::verifyAccountOrder(const std::vector<std::string> &accountIDs)
@@ -195,6 +197,7 @@ void Preferences::serialize(YAML::Emitter &out)
     out << YAML::Key << REGISTRATION_EXPIRE_KEY << YAML::Value << registrationExpire_;
     out << YAML::Key << SEARCH_BAR_DISPLAY_KEY << YAML::Value << searchBarDisplay_;
     out << YAML::Key << ZONE_TONE_CHOICE_KEY << YAML::Value << zoneToneChoice_;
+    out << YAML::Key << USE_UPNP << YAML::Value << useUPnP_;
     out << YAML::EndMap;
 }
 
@@ -210,6 +213,7 @@ void Preferences::unserialize(const YAML::Node &in)
     parseValue(node, PORT_NUM_KEY, portNum_);
     parseValue(node, SEARCH_BAR_DISPLAY_KEY, searchBarDisplay_);
     parseValue(node, MD5_HASH_KEY, md5Hash_);
+    parseValue(node, USE_UPNP, useUPnP_);
 }
 
 VoipPreference::VoipPreference() :
diff --git a/daemon/src/preferences.h b/daemon/src/preferences.h
index 54887d3265..f601e7c616 100644
--- a/daemon/src/preferences.h
+++ b/daemon/src/preferences.h
@@ -126,6 +126,13 @@ class Preferences : public Serializable {
             md5Hash_ = md5;
         }
 
+        bool getUseUPnP() const {
+            return useUPnP_;
+        }
+        void setUseUPnP(bool upnp) {
+            useUPnP_ = upnp;
+        }
+
     private:
         std::string accountOrder_;
         int historyLimit_;
@@ -135,6 +142,7 @@ class Preferences : public Serializable {
         int portNum_;
         bool searchBarDisplay_;
         bool md5Hash_;
+        bool useUPnP_;
         constexpr static const char * const CONFIG_LABEL = "preferences";
 };
 
diff --git a/daemon/src/ringdht/ringaccount.cpp b/daemon/src/ringdht/ringaccount.cpp
index f0e5bd7223..b94087a5a3 100644
--- a/daemon/src/ringdht/ringaccount.cpp
+++ b/daemon/src/ringdht/ringaccount.cpp
@@ -61,6 +61,8 @@
 #include "config/yamlparser.h"
 #include <yaml-cpp/yaml.h>
 
+#include "upnp/upnp.h"
+
 #include <algorithm>
 #include <array>
 #include <memory>
@@ -154,7 +156,8 @@ RingAccount::newOutgoingCall(const std::string& id, const std::string& toUrl)
     auto ice = iceTransportFactory.createTransport(
         ("sip:"+call->getCallId()).c_str(),
         ICE_COMPONENTS,
-        true
+        true,
+        getUseUPnP()
     );
     if (not ice or ice->waitForInitialization(ICE_INIT_TIMEOUT) <= 0) {
         call->setConnectionState(Call::DISCONNECTED);
@@ -224,12 +227,18 @@ RingAccount::createOutgoingCall(const std::shared_ptr<SIPCall>& call, const std:
     call->setPeerNumber(getToUri(to_id+"@"+target.toString(true).c_str()));
     call->initRecFilename(to_id);
 
-    //const auto localAddress = ip_utils::getInterfaceAddr(getLocalInterface(), peer.getFamily());
+    const auto localAddress = ip_utils::getInterfaceAddr(getLocalInterface());
     call->setCallMediaLocal(call->getIceTransport()->getDefaultLocalAddress());
 
-    // May use the published address as well
-    //const auto addrSdp = isStunEnabled() or (not getPublishedSameasLocal()) ? getPublishedIpAddress() : localAddress;
-
+    IpAddr addrSdp;
+    if (getUseUPnP()) {
+        /* use UPnP addr, or published addr if its set */
+        addrSdp = getPublishedSameasLocal() ?
+            getUPnPIpAddress() : getPublishedIpAddress();
+    } else {
+        addrSdp = isStunEnabled() or (not getPublishedSameasLocal()) ?
+            getPublishedIpAddress() : localAddress;
+    }
 
     // Initialize the session using ULAW as default codec in case of early media
     // The session should be ready to receive media once the first INVITE is sent, before
@@ -244,12 +253,7 @@ RingAccount::createOutgoingCall(const std::shared_ptr<SIPCall>& call, const std:
     // Building the local SDP offer
     auto& sdp = call->getSDP();
 
-    /*if (getPublishedSameasLocal())
-        sdp.setPublishedIP(addrSdp);
-    else
-        sdp.setPublishedIP(getPublishedAddress());
-*/
-    sdp.setPublishedIP(ip_utils::getInterfaceAddr(getLocalInterface()));
+    sdp.setPublishedIP(addrSdp);
     const bool created = sdp.createOffer(getActiveAudioCodecs(), getActiveVideoCodecs());
 
     if (not created or not SIPStartCall(call, target))
@@ -369,6 +373,7 @@ void RingAccount::unserialize(const YAML::Node &node)
     in_port_t port {DHT_DEFAULT_PORT};
     parseValue(node, Conf::DHT_PORT_KEY, port);
     dhtPort_ = port ? port : DHT_DEFAULT_PORT;
+    dhtPortUsed_ = dhtPort_;
     parseValue(node, Conf::DHT_PRIVKEY_PATH_KEY, privkeyPath_);
     parseValue(node, Conf::DHT_CERT_PATH_KEY, certPath_);
     parseValue(node, Conf::DHT_CA_CERT_PATH_KEY, cacertPath_);
@@ -490,6 +495,7 @@ void RingAccount::setAccountDetails(const std::map<std::string, std::string> &de
     parseInt(details, Conf::CONFIG_DHT_PORT, dhtPort_);
     if (dhtPort_ == 0)
         dhtPort_ = DHT_DEFAULT_PORT;
+    dhtPortUsed_ = dhtPort_;
     parseString(details, Conf::CONFIG_DHT_PRIVKEY_PATH, privkeyPath_);
     parseString(details, Conf::CONFIG_DHT_CERT_PATH, certPath_);
     checkIdentityPath();
@@ -541,6 +547,32 @@ RingAccount::handleEvents()
     }
 }
 
+bool RingAccount::mapPortUPnP()
+{
+    if (getUseUPnP()) {
+        /* create port mapping from published port to local port to the local IP
+         * note that since different RING accounts can use the same port,
+         * it may already be open, thats OK
+         *
+         * if the desired port is taken by another client, then it will try to map
+         * a different port, if succesfull, then we have to use that port for DHT
+         */
+        uint16_t port_used;
+        if (upnp_->addAnyMapping(dhtPort_, ring::upnp::PortType::UDP, false, &port_used)) {
+            if (port_used != dhtPort_)
+                RING_DBG("UPnP could not map port %u for DHT, using %u instead", dhtPort_, port_used);
+            dhtPortUsed_ = port_used;
+            return true;
+        } else {
+            /* failed to map any port */
+            return false;
+        }
+    } else {
+        /* not using UPnP, so return true */
+        return true;
+    }
+}
+
 void RingAccount::doRegister()
 {
     if (not isEnabled()) {
@@ -548,6 +580,9 @@ void RingAccount::doRegister()
         return;
     }
 
+    if (not mapPortUPnP())
+        RING_WARN("Could not successfully map DHT port with UPnP, continuing with account registration anyways.");
+
     try {
         loadTreatedCalls();
         if (dht_.isRunning()) {
@@ -555,7 +590,7 @@ void RingAccount::doRegister()
             dht_.join();
         }
         auto identity = loadIdentity();
-        dht_.run(dhtPort_, identity.second, false, [=](dht::Dht::Status s4, dht::Dht::Status s6) {
+        dht_.run(dhtPortUsed_, identity.second, false, [=](dht::Dht::Status s4, dht::Dht::Status s6) {
             RING_WARN("Dht status : %d %d", (int)s4, (int)s6);
             auto status = std::max(s4, s6);
             switch(status) {
@@ -663,7 +698,8 @@ void RingAccount::doRegister()
                         auto ice = iceTransportFactory.createTransport(
                             ("sip:"+call->getCallId()).c_str(),
                             ICE_COMPONENTS,
-                            false
+                            false,
+                            this_.getUseUPnP()
                         );
                         if (ice->waitForInitialization(ICE_INIT_TIMEOUT) <= 0)
                             throw std::runtime_error("Can't initialize ICE..");
@@ -705,7 +741,6 @@ void RingAccount::doRegister()
                 return true;
             }
         );
-
     }
     catch (const std::exception& e) {
         RING_ERR("Error registering DHT account: %s", e.what());
@@ -729,6 +764,11 @@ void RingAccount::doUnregister(std::function<void(bool)> released_cb)
     setRegistrationState(RegistrationState::UNREGISTERED);
     if (released_cb)
         released_cb(false);
+
+    if (getUseUPnP()) {
+        RING_DBG("UPnP : removing port mapping for DHT account.");
+        upnp_->removeMappings();
+    }
 }
 
 void
diff --git a/daemon/src/ringdht/ringaccount.h b/daemon/src/ringdht/ringaccount.h
index 1d43b2a1c2..8bf7117fa1 100644
--- a/daemon/src/ringdht/ringaccount.h
+++ b/daemon/src/ringdht/ringaccount.h
@@ -280,6 +280,11 @@ class RingAccount : public SIPAccountBase {
 
         void regenerateCAList();
 
+        /**
+         * Maps require port via UPnP
+         */
+        bool mapPortUPnP();
+
         /**
          * @return pjsip_tls_setting structure, filled from the configuration
          * file, that can be used directly by PJSIP to initialize
@@ -350,6 +355,14 @@ class RingAccount : public SIPAccountBase {
          */
         in_port_t dhtPort_ {DHT_DEFAULT_PORT};
 
+        /**
+         * DHT port actually used,
+         * this holds the actual port used for DHT, which may not be the port
+         * selected in the configuration in the case that UPnP is used and the
+         * configured port is already used by another client
+         */
+        in_port_t dhtPortUsed_ {DHT_DEFAULT_PORT};
+
         /**
          * The TLS settings, used only if tls is chosen as a sip transport.
          */
diff --git a/daemon/src/sip/sdp.h b/daemon/src/sip/sdp.h
index 71901c22d4..170c54bff5 100644
--- a/daemon/src/sip/sdp.h
+++ b/daemon/src/sip/sdp.h
@@ -186,11 +186,21 @@ class Sdp {
             localAudioControlPort_ = port + 1;
         }
 
+        void setLocalPublishedAudioPorts(int audio_port, int control_port) {
+            localAudioDataPort_ = audio_port;
+            localAudioControlPort_ = control_port;
+        }
+
         void setLocalPublishedVideoPort (int port) {
             localVideoDataPort_ = port;
             localVideoControlPort_ = port + 1;
         }
 
+        void setLocalPublishedVideoPorts(int video_port, int control_port) {
+            localVideoDataPort_ = video_port;
+            localVideoControlPort_ = control_port;
+        }
+
         void updatePorts(const std::vector<pj_sockaddr> &sockets);
 
         /**
@@ -229,10 +239,18 @@ class Sdp {
             return localVideoDataPort_;
         }
 
+        unsigned int getLocalVideoControlPort() const {
+            return localVideoControlPort_;
+        }
+
         unsigned int getLocalAudioPort() const {
             return localAudioDataPort_;
         }
 
+        unsigned int getLocalAudioControlPort() const {
+            return localAudioControlPort_;
+        }
+
         void addAttributeToLocalAudioMedia(const char *attr);
         void removeAttributeFromLocalAudioMedia(const char *attr);
         void addAttributeToLocalVideoMedia(const char *attr);
diff --git a/daemon/src/sip/sipaccount.cpp b/daemon/src/sip/sipaccount.cpp
index e6afd1daf0..e17405f8da 100644
--- a/daemon/src/sip/sipaccount.cpp
+++ b/daemon/src/sip/sipaccount.cpp
@@ -70,6 +70,9 @@
 #include <sstream>
 #include <cstdlib>
 
+#include "upnp/upnp.h"
+#include "ip_utils.h"
+
 namespace ring {
 
 using yaml_utils::parseValue;
@@ -226,9 +229,15 @@ SIPAccount::newOutgoingCall(const std::string& id, const std::string& toUrl)
     const auto localAddress = ip_utils::getInterfaceAddr(getLocalInterface(), family);
     call->setCallMediaLocal(localAddress);
 
-    // May use the published address as well
-    const auto addrSdp = isStunEnabled() or (not getPublishedSameasLocal()) ?
-        getPublishedIpAddress() : localAddress;
+    IpAddr addrSdp;
+    if (getUseUPnP()) {
+        /* use UPnP addr, or published addr if its set */
+        addrSdp = getPublishedSameasLocal() ?
+            getUPnPIpAddress() : getPublishedIpAddress();
+    } else {
+        addrSdp = isStunEnabled() or (not getPublishedSameasLocal()) ?
+            getPublishedIpAddress() : localAddress;
+    }
 
     // Initialize the session using ULAW as default codec in case of early media
     // The session should be ready to receive media once the first INVITE is sent, before
@@ -411,6 +420,13 @@ void SIPAccount::usePublishedAddressPortInVIA()
     via_addr_.port = publishedPort_;
 }
 
+void SIPAccount::useUPnPAddressPortInVIA()
+{
+    via_addr_.host.ptr = (char *) getUPnPIpAddress().toString().c_str();
+    via_addr_.host.slen = getUPnPIpAddress().toString().size();
+    via_addr_.port = publishedPortUsed_;
+}
+
 template <typename T>
 static void
 validate(std::string &member, const std::string &param, const T& valid)
@@ -690,6 +706,32 @@ std::map<std::string, std::string> SIPAccount::getVolatileAccountDetails() const
     return a;
 }
 
+bool SIPAccount::mapPortUPnP()
+{
+    if (getUseUPnP()) {
+        /* create port mapping from published port to local port to the local IP
+         * note that since different accounts can use the same port,
+         * it may already be open, thats OK
+         *
+         * if the desired port is taken by another client, then it will try to map
+         * a different port, if succesfull, then we have to use that port for SIP
+         */
+        uint16_t port_used;
+        if (upnp_->addAnyMapping(publishedPort_, localPort_, ring::upnp::PortType::UDP, false, false, &port_used)) {
+            if (port_used != publishedPort_)
+                RING_DBG("UPnP could not map published port %u for SIP, using %u instead", publishedPort_, port_used);
+            publishedPortUsed_ = port_used;
+            return true;
+        } else {
+            /* failed to map any port */
+            return false;
+        }
+    } else {
+        /* not using UPnP, so return true */
+        return true;
+    }
+}
+
 void SIPAccount::doRegister()
 {
     if (not isEnabled()) {
@@ -699,6 +741,9 @@ void SIPAccount::doRegister()
 
     RING_DBG("doRegister %s", hostname_.c_str());
 
+    if (not mapPortUPnP())
+        RING_WARN("Could not successfully map SIP port with UPnP, continuing with account registration anyways.");
+
     if (hostname_.empty() || isIP2IP()) {
         doRegister_();
         return;
@@ -836,6 +881,11 @@ void SIPAccount::doUnregister(std::function<void(bool)> released_cb)
         setTransport();
     if (released_cb)
         released_cb(true);
+
+    if (getUseUPnP()) {
+        RING_DBG("UPnP : removing port mapping for SIP account.");
+        upnp_->removeMappings();
+    }
 }
 
 void SIPAccount::startKeepAliveTimer()
@@ -913,7 +963,7 @@ SIPAccount::sendRegister()
     const pj_str_t pjContact(getContactHeader());
 
     if (transport_) {
-        if (not getPublishedSameasLocal() or (not received.empty() and received != getPublishedAddress())) {
+        if (getUseUPnP() or not getPublishedSameasLocal() or (not received.empty() and received != getPublishedAddress())) {
             pjsip_host_port *via = getViaAddr();
             RING_DBG("Setting VIA sent-by to %.*s:%d", via->host.slen, via->host.ptr, via->port);
 
@@ -1369,7 +1419,12 @@ SIPAccount::getContactHeader(pjsip_transport* t)
         hostname_,
         address, port);
 
-    if (not publishedSameasLocal_) {
+    if (getUseUPnP()) {
+        address = getUPnPIpAddress().toString();
+        port = publishedPortUsed_;
+        useUPnPAddressPortInVIA();
+        RING_DBG("Using UPnP address %s and port %d", address.c_str(), port);
+    } else if (not publishedSameasLocal_) {
         address = publishedIpAddress_;
         port = publishedPort_;
         RING_DBG("Using published address %s and port %d", address.c_str(), port);
diff --git a/daemon/src/sip/sipaccount.h b/daemon/src/sip/sipaccount.h
index 5c3dc64e87..170b430908 100644
--- a/daemon/src/sip/sipaccount.h
+++ b/daemon/src/sip/sipaccount.h
@@ -510,6 +510,7 @@ class SIPAccount : public SIPAccountBase {
         bool SIPStartCall(std::shared_ptr<SIPCall>& call);
 
         void usePublishedAddressPortInVIA();
+        void useUPnPAddressPortInVIA();
         bool fullMatch(const std::string &username, const std::string &hostname, pjsip_endpoint *endpt, pj_pool_t *pool) const;
         bool userMatch(const std::string &username) const;
         bool hostnameMatch(const std::string &hostname, pjsip_endpoint *endpt, pj_pool_t *pool) const;
@@ -569,6 +570,11 @@ class SIPAccount : public SIPAccountBase {
          */
         static std::string getLoginName();
 
+        /**
+         * Maps require port via UPnP
+         */
+        bool mapPortUPnP();
+
         /**
          * Resolved IP of hostname_ (for registration)
          */
@@ -730,6 +736,14 @@ class SIPAccount : public SIPAccountBase {
          * Presence data structure
          */
         SIPPresence * presence_;
+
+        /**
+         * SIP port actually used,
+         * this holds the actual port used for SIP, which may not be the port
+         * selected in the configuration in the case that UPnP is used and the
+         * configured port is already used by another client
+         */
+        pj_uint16_t publishedPortUsed_ {DEFAULT_SIP_PORT};
 };
 
 } // namespace ring
diff --git a/daemon/src/sip/sipaccountbase.cpp b/daemon/src/sip/sipaccountbase.cpp
index 0c5dab3afa..c98a0f41c0 100644
--- a/daemon/src/sip/sipaccountbase.cpp
+++ b/daemon/src/sip/sipaccountbase.cpp
@@ -51,6 +51,10 @@ SIPAccountBase::SIPAccountBase(const std::string& accountID)
     : Account(accountID), link_(getSIPVoIPLink())
 {}
 
+SIPAccountBase::~SIPAccountBase() {
+    setTransport();
+}
+
 template <typename T>
 static void
 validate(std::string &member, const std::string &param, const T& valid)
diff --git a/daemon/src/sip/sipaccountbase.h b/daemon/src/sip/sipaccountbase.h
index d477211870..302dfe8304 100644
--- a/daemon/src/sip/sipaccountbase.h
+++ b/daemon/src/sip/sipaccountbase.h
@@ -46,6 +46,7 @@
 #include <vector>
 #include <map>
 #include <sstream>
+#include <memory>
 
 namespace ring {
 
@@ -113,9 +114,7 @@ public:
      */
     SIPAccountBase(const std::string& accountID);
 
-    virtual ~SIPAccountBase() {
-        setTransport();
-    }
+    virtual ~SIPAccountBase();
 
     /**
      * Create incoming SIPCall.
diff --git a/daemon/src/sip/sipcall.cpp b/daemon/src/sip/sipcall.cpp
index 0fea72980a..6a7a41cd63 100644
--- a/daemon/src/sip/sipcall.cpp
+++ b/daemon/src/sip/sipcall.cpp
@@ -42,6 +42,7 @@
 #include "sdp.h"
 #include "manager.h"
 #include "array_size.h"
+#include "upnp/upnp.h"
 
 #include "audio/audiortp/avformat_rtp_session.h"
 #include "client/callmanager.h"
@@ -125,7 +126,10 @@ SIPCall::SIPCall(SIPAccountBase& account, const std::string& id, Call::CallType
     , videortp_(id, getSettings())
 #endif
     , sdp_(new Sdp(id))
-{}
+{
+    if (account.getUseUPnP())
+        upnp_.reset(new upnp::Controller());
+}
 
 SIPCall::~SIPCall()
 {
@@ -844,4 +848,37 @@ SIPCall::stopAllMedias()
 #endif
 }
 
+void
+SIPCall::openPortsUPnP()
+{
+    auto& account = getSIPAccount();
+    if (upnp_) {
+        /**
+         * Try to open the desired ports with UPnP,
+         * if they are used, use the alternative port and update the SDP session with the newly chosen port(s)
+         *
+         * TODO: the inital ports were chosen from the list of available ports and were marked as used
+         *       the newly selected port should possibly be checked against the list of used ports and marked
+         *       as used, the old port should be "released"
+         */
+        RING_DBG("UPnP : openening ports via upnp for SDP session.");
+        uint16_t audio_port_used;
+        if (upnp_->addAnyMapping(sdp_->getLocalAudioPort(), upnp::PortType::UDP, true, &audio_port_used)) {
+            uint16_t control_port_used;
+            if (upnp_->addAnyMapping(sdp_->getLocalAudioControlPort(), upnp::PortType::UDP, true, &control_port_used)) {
+                sdp_->setLocalPublishedAudioPorts(audio_port_used, control_port_used);
+            }
+        }
+#ifdef RING_VIDEO
+        uint16_t video_port_used;
+        if (upnp_->addAnyMapping(sdp_->getLocalVideoPort(), upnp::PortType::UDP, true, &video_port_used)) {
+            uint16_t control_port_used;
+            if (upnp_->addAnyMapping(sdp_->getLocalVideoControlPort(), upnp::PortType::UDP, true, &control_port_used)) {
+                sdp_->setLocalPublishedVideoPorts(video_port_used, control_port_used);
+            }
+        }
+#endif
+    }
+}
+
 } // namespace ring
diff --git a/daemon/src/sip/sipcall.h b/daemon/src/sip/sipcall.h
index 5880f1501f..9c663d768d 100644
--- a/daemon/src/sip/sipcall.h
+++ b/daemon/src/sip/sipcall.h
@@ -60,6 +60,10 @@ class SIPAccountBase;
 class SipTransport;
 class AVFormatRtpSession;
 
+namespace upnp {
+class Controller;
+}
+
 /**
  * @file sipcall.h
  * @brief SIPCall are SIP implementation of a normal Call
@@ -178,6 +182,8 @@ class SIPCall : public Call
 
         void startAllMedia();
 
+        void openPortsUPnP();
+
     private:
         NON_COPYABLE(SIPCall);
 
@@ -221,6 +227,8 @@ class SIPCall : public Call
 
         char contactBuffer_[PJSIP_MAX_URL_SIZE] {};
         pj_str_t contactHeader_ {contactBuffer_, 0};
+
+        std::unique_ptr<ring::upnp::Controller> upnp_;
 };
 
 } // namespace ring
diff --git a/daemon/src/sip/sipvoiplink.cpp b/daemon/src/sip/sipvoiplink.cpp
index e2bf329e31..ca0b471e6a 100644
--- a/daemon/src/sip/sipvoiplink.cpp
+++ b/daemon/src/sip/sipvoiplink.cpp
@@ -316,9 +316,15 @@ transaction_request_cb(pjsip_rx_data *rdata)
     auto family = pjsip_transport_type_get_af(pjsip_transport_get_type_from_flag(transport->get()->flag));
     IpAddr addrToUse = ip_utils::getInterfaceAddr(account->getLocalInterface(), family);
 
-    // May use the published address as well
-    IpAddr addrSdp = account->isStunEnabled() or (not account->getPublishedSameasLocal())
+    IpAddr addrSdp;
+    if (account->getUseUPnP()) {
+        /* use UPnP addr, or published addr if its set */
+        addrSdp = account->getPublishedSameasLocal() ?
+            account->getUPnPIpAddress() : account->getPublishedIpAddress();
+    } else {
+        addrSdp = account->isStunEnabled() or (not account->getPublishedSameasLocal())
                     ? account->getPublishedIpAddress() : addrToUse;
+    }
 
     call->setConnectionState(Call::PROGRESSING);
     call->setPeerNumber(peerNumber);
@@ -886,9 +892,17 @@ sdp_create_offer_cb(pjsip_inv_session *inv, pjmedia_sdp_session **p_offer)
 
     // FIXME : for now, use the same address family as the SIP transport
     auto family = pjsip_transport_type_get_af(account.getTransportType());
-    IpAddr address = account.getPublishedSameasLocal()
+
+    IpAddr address;
+    if (account.getUseUPnP()) {
+        /* use UPnP addr, or published addr if its set */
+        address = account.getPublishedSameasLocal() ?
+            account.getUPnPIpAddress() : account.getPublishedIpAddress();
+    } else {
+        address = account.getPublishedSameasLocal()
                     ? IpAddr(ip_utils::getInterfaceAddr(account.getLocalInterface(), family))
                     : account.getPublishedIpAddress();
+    }
 
     call->setCallMediaLocal(address);
 
@@ -994,6 +1008,8 @@ sdp_media_update_cb(pjsip_inv_session *inv, pj_status_t status)
     // Update connection information
     sdp.setMediaTransportInfoFromRemoteSdp();
 
+    call->openPortsUPnP();
+
     // Handle possible ICE transport
     if (!call->startIce())
         RING_WARN("ICE not started");
diff --git a/daemon/src/upnp/Makefile.am b/daemon/src/upnp/Makefile.am
new file mode 100644
index 0000000000..1560c07e7c
--- /dev/null
+++ b/daemon/src/upnp/Makefile.am
@@ -0,0 +1,11 @@
+include $(top_srcdir)/globals.mak
+
+noinst_LTLIBRARIES = libupnp.la
+
+libupnp_la_SOURCES = \
+        upnp.cpp \
+        upnp.h
+
+libupnp_la_CXXFLAGS = @CXXFLAGS@
+
+libupnp_la_LDFLAGS = @UPNP_LIBS@
diff --git a/daemon/src/upnp/upnp.cpp b/daemon/src/upnp/upnp.cpp
new file mode 100644
index 0000000000..0ba1b2d2c6
--- /dev/null
+++ b/daemon/src/upnp/upnp.cpp
@@ -0,0 +1,619 @@
+/*
+ *  Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *  Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ *
+ *  Additional permission under GNU GPL version 3 section 7:
+ *
+ *  If you modify this program, or any covered work, by linking or
+ *  combining it with the OpenSSL project's OpenSSL library (or a
+ *  modified version of that library), containing parts covered by the
+ *  terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ *  grants you additional permission to convey the resulting work.
+ *  Corresponding Source for a non-source form of such a combination
+ *  shall include the source code for the parts of OpenSSL used as well
+ *  as that of the covered work.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "upnp.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <map>
+#include <random>
+#include <memory>
+
+#if HAVE_UPNP
+#include <miniupnpc/upnperrors.h>
+#include <miniupnpc/miniwget.h>
+#include <miniupnpc/miniupnpc.h>
+#include <miniupnpc/upnpcommands.h>
+#endif
+
+#include "logger.h"
+#include "ip_utils.h"
+#include "ring_types.h"
+
+namespace ring { namespace upnp {
+
+#if HAVE_UPNP
+
+IGD::IGD(IGDdatas d, UPNPUrls u)
+    : datas_(std::make_shared<const IGDdatas>(d))
+{
+    UPNPUrls newURLs = UPNPUrls(u);
+    copyURLs(u, &newURLs);
+    urls_ = std::make_shared<const UPNPUrls>(newURLs);
+};
+
+IGD::~IGD()
+{
+    /* if last user, then free the URLs */
+    if (urls_.unique() and urls_) {
+        UPNPUrls urls = UPNPUrls(*urls_.get());
+        FreeUPNPUrls(&urls);
+    }
+}
+
+void
+IGD::copyURLs(const UPNPUrls &urlsSource, UPNPUrls *urlsDest)
+{
+    if (urlsSource.controlURL != nullptr)
+        urlsDest->controlURL = strdup(urlsSource.controlURL);
+    if (urlsSource.ipcondescURL != nullptr)
+        urlsDest->ipcondescURL = strdup(urlsSource.ipcondescURL);
+    if (urlsSource.controlURL_CIF != nullptr)
+        urlsDest->controlURL_CIF = strdup(urlsSource.controlURL_CIF);
+    if (urlsSource.controlURL_6FC != nullptr)
+        urlsDest->controlURL_6FC = strdup(urlsSource.controlURL_6FC);
+#ifdef MINIUPNPC_VERSION /* if not defined, its version 1.6 */
+    if (urlsSource.rootdescURL != nullptr)
+        urlsDest->rootdescURL = strdup(urlsSource.rootdescURL);
+#endif
+}
+
+#endif /* HAVE_UPNP */
+
+/**
+ * checks if the instance of IGD is empty
+ * ie: not actually an IGD
+ */
+bool
+IGD::isEmpty() const
+{
+#if HAVE_UPNP
+    if (urls_ and urls_->controlURL != nullptr) {
+        if (urls_->controlURL[0] == '\0') {
+            return true;
+        } else {
+            return false;
+        }
+    } else {
+        return true;
+    }
+#else
+    return true;
+#endif
+}
+
+bool operator== (Mapping &cMap1, Mapping &cMap2)
+{
+    /* we don't compare the description because it doesn't change the function of the
+     * mapping; we don't compare the IGD because for now we assume that we always
+     * use the same one and that all mappings are active
+     */
+    return (cMap1.local_ip == cMap2.local_ip &&
+            cMap1.port_external == cMap2.port_external &&
+            cMap1.port_internal == cMap2.port_internal &&
+            cMap1.type == cMap2.type);
+}
+
+bool operator!= (Mapping &cMap1, Mapping &cMap2)
+{
+    return !(cMap1 == cMap2);
+}
+
+Controller::Controller()
+    : udpGlobalMappings_(getGlobalInstance<UDPMapGlobal>())
+    , tcpGlobalMappings_(getGlobalInstance<TCPMapGlobal>())
+    , defaultIGD_(getIGD())
+{}
+
+Controller::~Controller()
+{
+    /* remove all mappings */
+    removeMappings();
+}
+
+/**
+ * tries to add mapping
+ */
+bool
+Controller::addMapping(uint16_t port_external, uint16_t port_internal, PortType type, int *upnp_error)
+{
+    /* initialiaze upnp_error */
+    if (upnp_error)
+        *upnp_error = -1; /* UPNPCOMMAND_UNKNOWN_ERROR */
+#if HAVE_UPNP
+    if (defaultIGD_->isEmpty()) {
+        RING_WARN("UPnP : cannot perform command as the IGD has either not been chosen or is not available.");
+        return false;
+    }
+
+    int upnp_status;
+
+    IpAddr local_ip = ip_utils::getLocalAddr(pj_AF_INET());
+    if (!local_ip) {
+        RING_DBG("UPnP : cannot determine local IP");
+        return false;
+    }
+
+    Mapping mapping{defaultIGD_, local_ip, port_external, port_internal};
+
+    /* check if a mapping with the same external port already exists in this instance
+     * if one exists, check that it is the same, if so, nothing needs to be done
+     * if it is not the same, then we can't add it
+     */
+    auto& instanceMappings = type == PortType::UDP ? (PortMapLocal&)udpInstanceMappings_ : (PortMapLocal&)tcpInstanceMappings_;
+    auto instanceIter = instanceMappings.find(port_external);
+    if (instanceIter != instanceMappings.end()) {
+        /* mapping exists with the same exteran port */
+        Mapping *mapping_ptr = &instanceIter->second;
+        if (*mapping_ptr == mapping) {
+            /* the same mapping has already been mapped by this instance, nothing to do */
+            RING_DBG("UPnP : mapping has already been added previously.");
+            if (upnp_error)
+                *upnp_error = UPNPCOMMAND_SUCCESS;
+            return true;
+        } else {
+            /* this port is already used by a different mapping */
+            RING_WARN("UPnP : cannot add a mapping with an external port which is already used by another:\n\tcurrent: %s\n\ttrying to add: %s",
+                      mapping_ptr->toString().c_str(), mapping.toString().c_str());
+            if (upnp_error)
+                *upnp_error = 718; /* ConflictInMappingEntry */
+            return false;
+        }
+    }
+
+    /* mapping doesn't exist in this instance, check if it was added by another
+     * if the mapping is the same, then we just need to increment the number of users globally
+     * if the mapping is not the same, then we have to return fail, as the external port is used
+     * for something else
+     * if the mapping doesn't exist, then try to add it
+     */
+    auto globalMappings = type == PortType::UDP ? (PortMapGlobal*)udpGlobalMappings_.get() : (PortMapGlobal*)tcpGlobalMappings_.get();
+    auto iter = globalMappings->find(port_external);
+    if (iter != globalMappings->end()) {
+        /* mapping exists with same external port */
+        GlobalMapping *mapping_ptr = &iter->second;
+        if (*mapping_ptr == mapping) {
+            /* the same mapping, so nothing needs to be done */
+            RING_DBG("UPnP : mapping was already added.");
+            if (upnp_error)
+                *upnp_error = UPNPCOMMAND_SUCCESS;
+            /* increment users */
+            mapping_ptr->users++;
+            RING_DBG("UPnp : number of users of mapping should now be incremented: %d", iter->second.users);
+            /* add it to map of instance mappings */
+            instanceMappings.emplace(std::make_pair(port_external,std::move(mapping)));
+            return true;
+        } else {
+            /* this port is already used by a different mapping */
+            RING_WARN("UPnP : cannot add a mapping with an external port which is already used by another:\n\tcurrent: %s\n\ttrying to add: %s",
+                      mapping_ptr->toString().c_str(), mapping.toString().c_str());
+            if (upnp_error)
+                *upnp_error = 718; /* ConflictInMappingEntry */
+            return false;
+        }
+    }
+
+    /* mapping doesn't exist, so try to add it */
+    RING_DBG("UPnP : adding port mapping : %s", mapping.toString().c_str());
+
+    upnp_status = UPNP_AddPortMapping(mapping.igd->getURLs()->controlURL,
+                                      mapping.igd->getDatas()->first.servicetype,
+                                      mapping.getExternalPort().c_str(),
+                                      mapping.getInternalPort().c_str(),
+                                      mapping.local_ip.toString().c_str(),
+                                      mapping.description.c_str(),
+                                      mapping.getType().c_str(),
+                                      NULL, NULL);
+
+    if (upnp_error)
+        *upnp_error = upnp_status;
+
+    if(upnp_status!=UPNPCOMMAND_SUCCESS) {
+        RING_DBG("UPnP : AddPortMapping(%s) failed with error: %d: %s",
+                 mapping.toString().c_str(), upnp_status, strupnperror(upnp_status));
+        return false;
+    } else {
+        /* success; add it to global list and local list */
+        globalMappings->emplace(std::make_pair(port_external, std::move(GlobalMapping{mapping})));
+        instanceMappings.emplace(std::make_pair(port_external,std::move(mapping)));
+        return true;
+    }
+#else
+    return false;
+#endif
+}
+
+/**
+ * tries to add mapping from and to the port_desired
+ * if unique == true, makes sure the client is not using this port already
+ * if the mapping fails, tries other available ports until success
+ *defaultIGD_
+ * tries to use a random port between 1024 < > 65535 if desired port fails
+ *
+ * maps port_desired to port_local; if use_same_port == true, makes sure that
+ * that the extranl and internal ports are the same
+ */
+bool
+Controller::addAnyMapping(uint16_t port_desired, uint16_t port_local, PortType type, bool use_same_port, bool unique, uint16_t *port_used)
+{
+#if HAVE_UPNP
+    auto globalMappings = type == PortType::UDP ? (PortMapGlobal*)udpGlobalMappings_.get() : (PortMapGlobal*)tcpGlobalMappings_.get();
+    if (unique) {
+        /* check that port is not already used by the client */
+        auto iter = globalMappings->find(port_desired);
+        if (iter != globalMappings->end()) {
+            /* port already used, we need a unique port */
+            port_desired = chooseRandomPort(type);
+        }
+    }
+
+    if (use_same_port)
+        port_local = port_desired;
+
+    int upnp_error;
+    bool result = addMapping(port_desired, port_local, type, &upnp_error);
+    /* keep trying to add the mapping as long as the upnp error is 718 == conflicting mapping
+     * if adding the mapping fails for any other reason, give up
+     */
+    while( result == false and (upnp_error == 718 or upnp_error == 402) ) {
+        /* acceptable errors to keep trying:
+         * 718 : conflictin mapping
+         * 402 : invalid args (due to router implementation)
+         */
+        /* TODO: make sure we don't try sellecting the same random port twice if it fails ? */
+        port_desired = chooseRandomPort(type);
+        if (use_same_port)
+            port_local = port_desired;
+        result = addMapping(port_desired, port_local, type, &upnp_error);
+    }
+
+    *port_used = port_desired;
+    return result;
+#else
+    return false;
+#endif
+}
+
+/**
+ * addAnyMapping with the local port being the same as the external port
+ */
+bool
+Controller::addAnyMapping(uint16_t port_desired, PortType type, bool unique, uint16_t *port_used) {
+    addAnyMapping(port_desired, port_desired, type, true, unique, port_used);
+}
+
+/**
+ * chooses a random port that is not yet used by the daemon for UPnP
+ */
+uint16_t
+Controller::chooseRandomPort(PortType type)
+{
+    auto globalMappings = type == PortType::UDP ? (PortMapGlobal*)udpGlobalMappings_.get() : (PortMapGlobal*)tcpGlobalMappings_.get();
+
+    std::random_device rd; // obtain a random number from hardware
+    std::mt19937 gen(rd()); // seed the generator
+    std::uniform_int_distribution<uint16_t> dist(Mapping::UPNP_PORT_MIN, Mapping::UPNP_PORT_MAX); // define the range
+
+    uint16_t port = dist(gen);
+
+    /* keep generating random ports until we find one which is not used */
+    while(globalMappings->find(port) != globalMappings->end()) {
+        port = dist(gen);
+    }
+
+    RING_DBG("UPnP : chose random port %u", port);
+
+    return port;
+}
+
+/**
+ * removes mappings added by this instance of the specified port type
+ * if an mapping has more than one user in the global list, it is not deleted
+ * from the router, but the number of users is decremented
+ */
+void
+Controller::removeMappings(PortType type) {
+#if HAVE_UPNP
+    auto& instanceMappings = type == PortType::UDP ? (PortMapLocal&)udpInstanceMappings_ : (PortMapLocal&)tcpInstanceMappings_;
+    for (auto instanceIter = instanceMappings.begin(); instanceIter != instanceMappings.end(); ){
+        /* first check if there is more than one global user of this mapping */
+        int users = 1;
+        auto globalMappings = type == PortType::UDP ? (PortMapGlobal*)udpGlobalMappings_.get() : (PortMapGlobal*)tcpGlobalMappings_.get();
+        auto iter = globalMappings->find(instanceIter->first);
+        if ( iter != globalMappings->end() ) {
+            GlobalMapping *mapping_ptr = &iter->second;
+            users = mapping_ptr->users;
+
+            if ( users > 1 ) {
+                /* just decrement the users */
+                mapping_ptr->users--;
+            } else {
+                /* remove the mapping from the global list */
+                globalMappings->erase(iter);
+            }
+        }
+
+        /* now remove the mapping from the instance list...
+         * even if the delete operation fails, we assume that the mapping was deleted
+         */
+        auto& mapping = instanceIter->second;
+        instanceIter = instanceMappings.erase(instanceIter);
+
+        /* delete the mapping from the router if this instance was the only user */
+        if (users < 2) {
+            RING_DBG("UPnP : deleting mapping: %s", mapping.toString().c_str());
+            int upnp_status = UPNP_DeletePortMapping(mapping.igd->getURLs()->controlURL,
+                                                     mapping.igd->getDatas()->first.servicetype,
+                                                     mapping.getExternalPort().c_str(),
+                                                     mapping.getType().c_str(),
+                                                     NULL);
+            if(upnp_status != UPNPCOMMAND_SUCCESS) {
+                RING_DBG("UPnP : DeletePortMapping(%s) failed with error: %d: %s",
+                         mapping.toString().c_str(), upnp_status, strupnperror(upnp_status));
+            } else {
+                /* RING_DBG("UPnP : deletion success"); */
+            }
+        } else {
+            RING_DBG("UPnP : removing mapping: %s, but not deleting because there are %d other users",
+                     mapping.toString().c_str(), users - 1);
+        }
+    }
+#endif
+}
+
+/**
+ * removes all mappings added by this instance
+ */
+void
+Controller::removeMappings()
+{
+#if HAVE_UPNP
+    removeMappings(PortType::UDP);
+    removeMappings(PortType::TCP);
+#endif
+}
+
+/**
+ * removes all mappings with the local IP and the given description
+ */
+void
+Controller::removeMappingsByLocalIPAndDescription(const std::string& description)
+{
+#if HAVE_UPNP
+    if (defaultIGD_->isEmpty()) {
+        RING_WARN("UPnP : cannot perform command as the IGD has either not been chosen or is not available.");
+        return;
+    }
+
+    /* need to get the local addr */
+    IpAddr local_ip = ip_utils::getLocalAddr(pj_AF_INET());
+    if (!local_ip) {
+        RING_DBG("UPnP : cannot determine local IP");
+        return;
+    }
+
+    int upnp_status;
+    int i = 0;
+    char index[6];
+    char intClient[40];
+    char intPort[6];
+    char extPort[6];
+    char protocol[4];
+    char desc[80];
+    char enabled[6];
+    char rHost[64];
+    char duration[16];
+
+    RING_DBG("UPnP : removing all port mappings with description: \"%s\" and local ip: %s",
+             description.c_str(), local_ip.toString().c_str());
+
+    do {
+        snprintf(index, 6, "%d", i);
+        rHost[0] = '\0';
+        enabled[0] = '\0';
+        duration[0] = '\0';
+        desc[0] = '\0';
+        extPort[0] = '\0';
+        intPort[0] = '\0';
+        intClient[0] = '\0';
+        upnp_status = UPNP_GetGenericPortMappingEntry(defaultIGD_->getURLs()->controlURL,
+                                                      defaultIGD_->getDatas()->first.servicetype,
+                                                      index,
+                                                      extPort, intClient, intPort,
+                                                      protocol, desc, enabled,
+                                                      rHost, duration);
+        if(upnp_status == UPNPCOMMAND_SUCCESS) {
+            /* remove if matches description and ip
+             * once the port mapping is deleted, there will be one less, and the rest will "move down"
+             * that is, we don't need to increment the mapping index in that case
+             */
+            if( strcmp(description.c_str(), desc) == 0 and strcmp(local_ip.toString().c_str(), intClient) == 0) {
+                RING_DBG("UPnP : found mapping with matching description and ip:\n\t%s %5s->%s:%-5s '%s'",
+                         protocol, extPort, intClient, intPort, desc);
+                int delete_err = 0;
+                delete_err = UPNP_DeletePortMapping(defaultIGD_->getURLs()->controlURL, defaultIGD_->getDatas()->first.servicetype, extPort, protocol, NULL);
+                if(delete_err != UPNPCOMMAND_SUCCESS) {
+                    RING_DBG("UPnP : UPNP_DeletePortMapping() failed with error code %d : %s", delete_err, strupnperror(delete_err));
+                } else {
+                    RING_DBG("UPnP : deletion success");
+                    /* decrement the mapping index since it will be incremented */
+                    i--;
+                    /* if we can determine the protocol, remove it for the list of mappings */
+                    uint16_t port = std::stoi(extPort);
+                    if (strcmp(protocol, "UDP") == 0 or strcmp(protocol, "udp") == 0) {
+                        udpGlobalMappings_->erase(port);
+                        udpInstanceMappings_.erase(port);
+                    } else if (strcmp(protocol, "TCP") == 0 or strcmp(protocol, "tcp") == 0) {
+                        tcpGlobalMappings_->erase(port);
+                        tcpInstanceMappings_.erase(port);
+                    } else {
+                        RING_WARN("UPnP : cannot recognize protocol of removed mapping: %s", protocol);
+                    }
+                }
+            }
+        } else if (upnp_status == 713) {
+            /* 713 : SpecifiedArrayIndexInvalid
+             * this means there are no more mappings to check, and we're done
+             */
+        } else {
+            RING_DBG("UPnP : GetGenericPortMappingEntry() failed with error code %d : %s", upnp_status, strupnperror(upnp_status));
+        }
+        i++;
+    } while(upnp_status == UPNPCOMMAND_SUCCESS);
+#endif
+}
+
+/**
+ * tries to get the external ip of the router
+ */
+IpAddr
+Controller::getExternalIP()
+{
+#if HAVE_UPNP
+    if (defaultIGD_->isEmpty()) {
+        RING_WARN("UPnP : cannot perform command as the IGD has either not been chosen or is not available.");
+        return IpAddr();
+    }
+
+    int upnp_status;
+    char externalIPAddress[40];
+
+    RING_DBG("UPnP : getting external IP");
+
+    upnp_status = UPNP_GetExternalIPAddress(defaultIGD_->getURLs()->controlURL,
+                                            defaultIGD_->getDatas()->first.servicetype,
+                                            externalIPAddress);
+    if(upnp_status != UPNPCOMMAND_SUCCESS) {
+        RING_DBG("UPnP : GetExternalIPAddress failed with error code %d : %s", upnp_status, strupnperror(upnp_status));
+        return IpAddr();
+    } else {
+        RING_DBG("UPnP : got external IP = %s", externalIPAddress);
+        return IpAddr(std::string(externalIPAddress));
+    }
+#else
+    /* return empty address */
+    return IpAddr();
+#endif
+}
+
+/**
+ * tries to find a valid IGD on the network
+ */
+static IGD
+chooseIGD(void)
+{
+#if HAVE_UPNP
+    struct UPNPDev * devlist = nullptr;
+    int upnp_status = 0;
+
+    RING_DBG("UPnP : finding default IGD");
+
+    /* look for UPnP devices on the network */
+    devlist = upnpDiscover(2000, NULL, NULL, 0, 0, &upnp_status);
+
+    if (devlist) {
+        struct UPNPDev * device = nullptr;
+        struct UPNPUrls newIGDURLs = UPNPUrls();
+        struct IGDdatas newIGDDatas = IGDdatas();
+        char lanaddr[64];
+
+        newIGDURLs = UPNPUrls();
+        newIGDDatas = IGDdatas();
+
+        RING_DBG("UPnP devices found on the network");
+        for(device = devlist; device; device = device->pNext)
+        {
+            RING_DBG(" desc: %s\n st: %s",
+                     device->descURL, device->st);
+        }
+
+        upnp_status = UPNP_GetValidIGD(devlist, &newIGDURLs, &newIGDDatas, lanaddr, sizeof(lanaddr));
+
+        switch(upnp_status) {
+            case -1:
+                RING_ERR("UPnP : internal error getting valid IGD");
+                newIGDURLs = UPNPUrls();
+                newIGDDatas = IGDdatas();
+                break;
+            case 0:
+                RING_WARN("UPnP : no valid IGD found");
+                FreeUPNPUrls(&newIGDURLs);
+                newIGDURLs = UPNPUrls();
+                newIGDDatas = IGDdatas();
+                break;
+            case 1:
+                RING_DBG("UPnP : found valid IGD : %s", newIGDURLs.controlURL);
+                break;
+            case 2:
+                RING_DBG("UPnP : found a (not connected?) IGD, will try to use it anyway: %s", newIGDURLs.controlURL);
+                break;
+            case 3:
+                RING_DBG("UPnP : UPnP device found, cannot determine if it is an IGD, will try to use it anyway : %s", newIGDURLs.controlURL);
+                break;
+            default:
+                RING_DBG("UPnP : device found, cannot determine if it is a UPnP device, will try to use it anyway : %s", newIGDURLs.controlURL);
+        }
+
+        if (upnp_status > 0)
+            RING_DBG("UPnP : local IP address reported as: %s", lanaddr);
+
+        IGD igd{newIGDDatas, newIGDURLs};
+
+        freeUPNPDevlist(devlist);
+        devlist = nullptr;
+
+        return igd;
+    } else {
+        RING_WARN("UPnP : looking for IGD UPnP devices on the network failed with error: %d : %s", upnp_status, strupnperror(upnp_status));
+        return IGD();
+    }
+
+#else
+    return IGD();
+#endif
+}
+
+std::shared_ptr<IGD>
+getIGD(void)
+{
+    static std::shared_ptr<IGD> igd;
+
+    if (not igd)
+        igd = std::make_shared<IGD>(chooseIGD());
+
+    return igd;
+}
+
+}} // namespace ring::upnp
diff --git a/daemon/src/upnp/upnp.h b/daemon/src/upnp/upnp.h
new file mode 100644
index 0000000000..22a16d8e59
--- /dev/null
+++ b/daemon/src/upnp/upnp.h
@@ -0,0 +1,247 @@
+/*
+ *  Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
+ *  Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ *
+ *  Additional permission under GNU GPL version 3 section 7:
+ *
+ *  If you modify this program, or any covered work, by linking or
+ *  combining it with the OpenSSL project's OpenSSL library (or a
+ *  modified version of that library), containing parts covered by the
+ *  terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ *  grants you additional permission to convey the resulting work.
+ *  Corresponding Source for a non-source form of such a combination
+ *  shall include the source code for the parts of OpenSSL used as well
+ *  as that of the covered work.
+ */
+
+#ifndef UPNP_H_
+#define UPNP_H_
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string>
+#include <map>
+#include <memory>
+
+#include "noncopyable.h"
+#include "logger.h"
+#include "ip_utils.h"
+
+#if HAVE_UPNP
+#include <miniupnpc/miniupnpc.h>
+#endif
+
+namespace ring { namespace upnp {
+
+/* defines a UPnP capable Internet Gateway Device (a router) */
+class IGD {
+public:
+
+#if HAVE_UPNP
+
+    IGD(IGDdatas d = IGDdatas(), UPNPUrls u = UPNPUrls());
+
+    /* move constructor and operator */
+    IGD(IGD&& other) = default;
+    IGD& operator=(IGD&&) = default;
+
+    ~IGD();
+
+    std::shared_ptr<const IGDdatas> getDatas() const {return datas_;};
+    std::shared_ptr<const UPNPUrls> getURLs() const {return urls_;};
+#else
+    /* use default constructor and destructor */
+#endif
+    bool isEmpty() const;
+
+private:
+    NON_COPYABLE(IGD);
+
+#if HAVE_UPNP
+    void copyURLs(const UPNPUrls &urlsSource, UPNPUrls *urlsDest);
+
+    std::shared_ptr<const IGDdatas> datas_;
+    std::shared_ptr<const UPNPUrls> urls_;
+#endif
+
+};
+
+enum class PortType {UDP,TCP};
+
+/* defines a UPnP port mapping */
+class Mapping {
+public:
+    constexpr static char const * UPNP_DEFAULT_MAPPING_DESCRIPTION = "RING";
+    /* TODO: what should the port range really be?
+     * Should it be the ephemeral ports as defined by the system?
+     */
+    constexpr static uint16_t UPNP_PORT_MIN = 1024;
+    constexpr static uint16_t UPNP_PORT_MAX = 65535;
+
+    std::shared_ptr<const IGD> igd; /* the IGD associated with this mapping */
+    IpAddr local_ip; /* the destination of the mapping */
+    uint16_t port_external;
+    uint16_t port_internal;
+    PortType type; /* UPD or TCP */
+    std::string description;
+
+    Mapping(
+        std::shared_ptr<const IGD> igd = std::make_shared<const IGD>(),
+        IpAddr local_ip = IpAddr(),
+        uint16_t port_external = 0,
+        uint16_t port_internal = 0,
+        PortType type = PortType::UDP,
+        std::string description = UPNP_DEFAULT_MAPPING_DESCRIPTION)
+    : igd(igd)
+    , local_ip(local_ip)
+    , port_external(port_external)
+    , port_internal(port_internal)
+    , type(type)
+    , description(description)
+    {};
+
+    /* move constructor and operator */
+    Mapping(Mapping&& other) = default;
+    Mapping& operator=(Mapping&&) = default;
+
+    friend bool operator== (Mapping &cRedir1, Mapping &cRedir2);
+    friend bool operator!= (Mapping &cRedir1, Mapping &cRedir2);
+
+    std::string getExternalPort() const {return std::to_string(port_external);};
+    std::string getInternalPort() const {return std::to_string(port_internal);};
+    std::string getType() const { return type == PortType::UDP ? "UDP" : "TCP";};
+    std::string toString() const {
+        return local_ip.toString() + ", " + getExternalPort() + ":" + getInternalPort() + ", " + getType();
+    };
+
+    bool isValid() const {
+        return igd->isEmpty() or port_external == 0 or port_internal == 0 ? false : true;
+    };
+private:
+    NON_COPYABLE(Mapping);
+};
+
+/**
+ * GlobalMapping is like a mapping, but it tracks the number of global users,
+ * ie: the number of upnp:Controller which are using this mapping
+ * this is usually only relevant for accounts (not calls) as multiple SIP accounts
+ * can use the same SIP port and we don't want to delete a mapping from the router
+ * if other accounts are using it
+ */
+class GlobalMapping : public Mapping {
+public:
+    /* number of users of this mapping;
+     * this is only relevant when multiple accounts are using the same SIP port */
+    unsigned users;
+    GlobalMapping(const Mapping& mapping, unsigned users = 1)
+        : Mapping(mapping.igd, mapping.local_ip, mapping.port_external, mapping.port_internal, mapping.type, mapping.description)
+        , users(users)
+    {};
+};
+
+class Controller {
+public:
+    /* constructor */
+    Controller();
+    /* destructor */
+    ~Controller();
+
+    /**
+     * tries to add mapping from and to the port_desired
+     * if unique == true, makes sure the client is not using this port already
+     * if the mapping fails, tries other available ports until success
+     *
+     * tries to use a random port between 1024 < > 65535 if desired port fails
+     *
+     * maps port_desired to port_local; if use_same_port == true, makes sure that
+     * that the extranl and internal ports are the same
+     */
+    bool addAnyMapping(uint16_t port_desired, uint16_t port_local, PortType type, bool use_same_port, bool unique, uint16_t *port_used);
+
+    /**
+     * addAnyMapping with the local port being the same as the external port
+     */
+    bool addAnyMapping(uint16_t port_desired, PortType type, bool unique, uint16_t *port_used);
+
+    /**
+     * removes all mappings added by this instance
+     */
+    void removeMappings();
+
+    /**
+     * removes all mappings with the local IP and the given description
+     */
+    void removeMappingsByLocalIPAndDescription(const std::string& description = Mapping::UPNP_DEFAULT_MAPPING_DESCRIPTION);
+
+    /**
+     * tries to get the external ip of the IGD (router)
+     */
+    IpAddr getExternalIP();
+
+private:
+    /* subclasses to make it easier to differentiate and cast maps of port mappings */
+    class PortMapLocal : public std::map<uint16_t, Mapping> {};
+    class UDPMapLocal : public PortMapLocal {};
+    class TCPMapLocal : public PortMapLocal {};
+    class PortMapGlobal : public std::map<uint16_t, GlobalMapping> {};
+    class UDPMapGlobal: public PortMapGlobal {};
+    class TCPMapGlobal : public PortMapGlobal {};
+
+    /**
+     * The IGD being used by this instance;
+     * TODO: Currently, we assume that the IGD will not change once it has been selected;
+     *       however, the user could switch routers while the client is running
+     */
+    std::shared_ptr<IGD> defaultIGD_;
+
+    /**
+     * list of mappings created by this instance
+     * the key is the external port number, as there can only be one mapping
+     * at a time for each external port
+     */
+    UDPMapLocal udpInstanceMappings_;
+    TCPMapLocal tcpInstanceMappings_;
+
+    /**
+     * list of all mappings
+     */
+    std::shared_ptr<UDPMapGlobal> udpGlobalMappings_;
+    std::shared_ptr<TCPMapGlobal> tcpGlobalMappings_;
+
+    /**
+     * tries to add mapping
+     */
+    bool addMapping(uint16_t port_external, uint16_t port_internal, PortType type = PortType::UDP, int *upnp_error = nullptr);
+
+    /**
+     * Try to remove all mappings of the given type
+     */
+    void removeMappings(PortType type);
+
+    /**
+     * chooses a random port that is not yet used by the daemon for UPnP
+     */
+    uint16_t chooseRandomPort(PortType type);
+};
+
+std::shared_ptr<IGD> getIGD();
+
+}} // namespace ring::upnp
+
+#endif /* UPNP_H_ */
\ No newline at end of file
-- 
GitLab