diff --git a/daemon/configure.ac b/daemon/configure.ac
index de3bf82e8670c12ec79f27c1dc04fdfa76ade7b3..85c8f27d3e30d909587ca5bfbdfa375b9c327bca 100644
--- a/daemon/configure.ac
+++ b/daemon/configure.ac
@@ -531,60 +531,17 @@ 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)
+# LIBUPNP
+dnl check for libupnp
 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" )
+AS_IF([test "x$with_upnp" = "xyes"],
+      [PKG_CHECK_MODULES(LIBUPNP, [libupnp],
+        [AC_DEFINE([HAVE_LIBUPNP], 1, [Define if you have libupnp])],
+        [AC_MSG_WARN([Missing libupnp development files])
+         AC_DEFINE([HAVE_LIBUPNP], 0, [Define if you have libupnp])])
+      ])
 
 # DOXYGEN
 # required dependency(ies): doxygen
diff --git a/daemon/contrib/src/miniupnpc/SHA512SUMS b/daemon/contrib/src/miniupnpc/SHA512SUMS
deleted file mode 100644
index c3683153494bed968ba9e69decb579028c3c9966..0000000000000000000000000000000000000000
--- a/daemon/contrib/src/miniupnpc/SHA512SUMS
+++ /dev/null
@@ -1 +0,0 @@
-7994749fa92e6478285dada191f4ad0d15fdce6cf281ee4d1d3d5615548338d68be3b9ce96a04ef4b81c697589689398060cc6f610101fa1d0e6f9e24b7850a9  miniupnpc-1.9.tar.gz
diff --git a/daemon/contrib/src/miniupnpc/makefile.patch b/daemon/contrib/src/miniupnpc/makefile.patch
deleted file mode 100644
index acafe2d11a7917c7ac293625a9cb8d8c382453ce..0000000000000000000000000000000000000000
--- a/daemon/contrib/src/miniupnpc/makefile.patch
+++ /dev/null
@@ -1,16 +0,0 @@
---- 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
deleted file mode 100644
index d77aff5f7987e77995c1670e4ddfe6ec267ca9ea..0000000000000000000000000000000000000000
--- a/daemon/contrib/src/miniupnpc/rules.mak
+++ /dev/null
@@ -1,26 +0,0 @@
-# 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/upnp/SHA512SUMS b/daemon/contrib/src/upnp/SHA512SUMS
new file mode 100644
index 0000000000000000000000000000000000000000..f30c34e49bdcddbfb931a4a10580a611a8c7203c
--- /dev/null
+++ b/daemon/contrib/src/upnp/SHA512SUMS
@@ -0,0 +1 @@
+97af62a7483cc19cfe80157cbc3383c1b4b7c9c39b848f4ed063784b74df0b9b0527f7b467e01451e0a44dbf9e8a9eab510619146a6ee1e3dce46f3e4af6e661  libupnp-1.6.19.tar.bz2
diff --git a/daemon/contrib/src/upnp/libupnp-configure.patch b/daemon/contrib/src/upnp/libupnp-configure.patch
new file mode 100644
index 0000000000000000000000000000000000000000..dbc14cca5e50f88e22edf19da1cace37abfe3edf
--- /dev/null
+++ b/daemon/contrib/src/upnp/libupnp-configure.patch
@@ -0,0 +1,82 @@
+--- libupnp/configure.ac.orig	2011-02-09 00:55:44.000000000 +0100
++++ libupnp/configure.ac	2011-02-10 23:39:44.154929678 +0100
+@@ -397,7 +397,6 @@
+ AC_PROG_MAKE_SET
+ AC_PROG_EGREP
+ 
+-#
+ # Default compilation flags
+ #
+ echo "--------------------- Default compilation flags -------------------------------"
+@@ -531,39 +530,46 @@
+ # Checks for POSIX Threads
+ #
+ echo "--------------------------- pthread stuff -------------------------------------"
+-ACX_PTHREAD(
+-	[],
+-	[AC_MSG_ERROR([POSIX threads are required to build this program])])
++#ACX_PTHREAD(
++#	[],
++#	[AC_MSG_ERROR([POSIX threads are required to build this program])])
+ #
++PTHREAD_LIBS=" -lpthreadGC2 -lws2_32"
++PTHREAD_CFLAGS=" -DPTW32_STATIC_LIB -DUPNP_STATIC_LIB"
+ # Update environment variables for pthreads
+ #
+-CC="$PTHREAD_CC"
++#CC="$PTHREAD_CC"
+ CFLAGS="$PTHREAD_CFLAGS $CFLAGS"
+ LIBS="$PTHREAD_LIBS $LIBS"
++
++AC_SUBST(PTHREAD_LIBS)
++AC_SUBST(PTHREAD_CFLAGS)
++AC_SUBST(PTHREAD_CC)
++
+ #
+ # Determine if pthread_rwlock_t is available
+ #
+-echo "----------------------- pthread_rwlock_t stuff --------------------------------"
+-AC_MSG_CHECKING([if pthread_rwlock_t is available])
+-AC_LANG([C])
+-AC_COMPILE_IFELSE(
+-	[AC_LANG_PROGRAM(
+-		[#include <pthread.h>],
+-		[pthread_rwlock_t *x;])],
+-	[AC_DEFINE([UPNP_USE_RWLOCK], [1], [Use pthread_rwlock_t])
+-		AC_MSG_RESULT([yes, supported without any options])],
+-	[AC_COMPILE_IFELSE(
+-		[AC_LANG_PROGRAM(
+-			[#define _GNU_SOURCE
+-			#include <pthread.h>],
+-			[pthread_rwlock_t *x;])],
+-		[AC_DEFINE([UPNP_USE_RWLOCK], [1], [Use pthread_rwlock_t])
+-			CPPFLAGS="$CPPFLAGS -D_GNU_SOURCE"
+-			AC_MSG_RESULT([yes, definition of _GNU_SOURCE required])],
+-		[AC_DEFINE([UPNP_USE_RWLOCK], [0], [Do not use pthread_rwlock_t])
+-			AC_MSG_RESULT([no, needs to fallback to pthread_mutex])
+-			AC_MSG_ERROR([pthread_rwlock_t not available])])])
+-echo "-------------------------------------------------------------------------------"
++#echo "----------------------- pthread_rwlock_t stuff --------------------------------"
++#AC_MSG_CHECKING([if pthread_rwlock_t is available])
++#AC_LANG([C])
++#AC_COMPILE_IFELSE(
++#	[AC_LANG_PROGRAM(
++#		[#include <pthread.h>],
++#		[pthread_rwlock_t *x;])],
++#	[AC_DEFINE([UPNP_USE_RWLOCK], [1], [Use pthread_rwlock_t])
++#		AC_MSG_RESULT([yes, supported without any options])],
++#	[AC_COMPILE_IFELSE(
++#		[AC_LANG_PROGRAM(
++#			[#define _GNU_SOURCE
++#			#include <pthread.h>],
++#			[pthread_rwlock_t *x;])],
++#		[AC_DEFINE([UPNP_USE_RWLOCK], [1], [Use pthread_rwlock_t])
++#			CPPFLAGS="$CPPFLAGS -D_GNU_SOURCE"
++#			AC_MSG_RESULT([yes, definition of _GNU_SOURCE required])],
++#		[AC_DEFINE([UPNP_USE_RWLOCK], [0], [Do not use pthread_rwlock_t])
++#			AC_MSG_RESULT([no, needs to fallback to pthread_mutex])
++#			AC_MSG_ERROR([pthread_rwlock_t not available])])])
++#echo "-------------------------------------------------------------------------------"
+ 
+ 
+ AC_CONFIG_FILES([
diff --git a/daemon/contrib/src/upnp/libupnp-ipv6.patch b/daemon/contrib/src/upnp/libupnp-ipv6.patch
new file mode 100644
index 0000000000000000000000000000000000000000..7b89bc26886b12733866faad69fda62e7bfe3c5f
--- /dev/null
+++ b/daemon/contrib/src/upnp/libupnp-ipv6.patch
@@ -0,0 +1,49 @@
+From 438ace99538713fb1370411188e0f370069a1818 Mon Sep 17 00:00:00 2001
+From: Konstantin Pavlov <thresh@videolan.org>
+Date: Tue, 29 May 2012 10:18:40 +0400
+Subject: [PATCH] Fix compile under mingw with IPv6 enabled.
+
+---
+ upnp/src/genlib/miniserver/miniserver.c |    7 +++++++
+ upnp/src/ssdp/ssdp_server.c             |    7 +++++++
+ 2 files changed, 14 insertions(+)
+
+diff --git a/upnp/src/genlib/miniserver/miniserver.c b/upnp/src/genlib/miniserver/miniserver.c
+index af310ca..1ae422f 100644
+--- a/upnp/src/genlib/miniserver/miniserver.c
++++ b/upnp/src/genlib/miniserver/miniserver.c
+@@ -68,6 +68,13 @@
+ /*! . */
+ #define APPLICATION_LISTENING_PORT 49152
+ 
++/* IPV6_V6ONLY is missing from MinGW, hack taken from
++ * http://svn.apache.org/repos/asf/apr/apr/trunk/network_io/win32/sockopt.c
++ */
++#ifndef IPV6_V6ONLY
++#define IPV6_V6ONLY 27
++#endif
++
+ struct mserv_request_t {
+ 	/*! Connection handle. */
+ 	SOCKET connfd;
+diff --git a/upnp/src/ssdp/ssdp_server.c b/upnp/src/ssdp/ssdp_server.c
+index 231c2c5..6a9c27f 100644
+--- a/upnp/src/ssdp/ssdp_server.c
++++ b/upnp/src/ssdp/ssdp_server.c
+@@ -69,6 +69,13 @@
+ 	#endif /* UPNP_ENABLE_IPV6 */
+ #endif /* INCLUDE_CLIENT_APIS */
+ 
++/* IPV6_V6ONLY is missing from MinGW, hack taken from
++ * http://svn.apache.org/repos/asf/apr/apr/trunk/network_io/win32/sockopt.c
++ */
++#ifndef IPV6_V6ONLY
++#define IPV6_V6ONLY 27
++#endif
++
+ void RequestHandler();
+ 
+ enum Listener {
+-- 
+1.7.9.7
+
diff --git a/daemon/contrib/src/upnp/libupnp-win32.patch b/daemon/contrib/src/upnp/libupnp-win32.patch
new file mode 100644
index 0000000000000000000000000000000000000000..686e3bae19c1da6fd74fb0fef455a6f71b9ab8a9
--- /dev/null
+++ b/daemon/contrib/src/upnp/libupnp-win32.patch
@@ -0,0 +1,45 @@
+--- libupnp/configure.ac.orig	2011-02-10 23:53:25.000000000 +0100
++++ libupnp/configure.ac	2011-02-10 23:54:23.574454501 +0100
+@@ -546,6 +546,7 @@
+ AC_SUBST(PTHREAD_CFLAGS)
+ AC_SUBST(PTHREAD_CC)
+ 
++AC_DEFINE([_WIN32_WINNT], 0x0501, [Define to '0x0500' for Windows 2000 APIs.])
+ #
+ # Determine if pthread_rwlock_t is available
+ #
+--- libupnp/libupnp.pc.in	2010-12-23 21:24:05.000000000 +0100
++++ libupnp.new/libupnp.pc.in	2011-02-13 11:27:23.000000000 +0100
+@@ -6,6 +6,6 @@
+ Name: libupnp
+ Description: Linux SDK for UPnP Devices
+ Version: @VERSION@
+-Libs: @PTHREAD_CFLAGS@ @PTHREAD_LIBS@ -L${libdir} -lupnp -lthreadutil -lixml 
++Libs: @PTHREAD_CFLAGS@ -L${libdir} -lupnp -lthreadutil -lixml -liphlpapi @PTHREAD_LIBS@ 
+ Cflags: @PTHREAD_CFLAGS@ -I${includedir}/upnp
+ 
+--- libupnp/upnp/src/inc/upnputil.h	2010-12-23 21:24:06.000000000 +0100
++++ libupnp.new/upnp/src/inc/upnputil.h	2011-02-13 08:24:24.000000000 +0100
+@@ -125,7 +125,7 @@
+ 		#define strncasecmp		strnicmp
+ 		#define sleep(a)		Sleep((a)*1000)
+ 		#define usleep(a)		Sleep((a)/1000)
+-		#define strerror_r(a,b,c)	(strerror_s((b),(c),(a)))
++		#define strerror_r(a,b,c)       strncpy( b, strerror(a), c)
+ 	#else
+ 		#define max(a, b)   (((a)>(b))? (a):(b))
+ 		#define min(a, b)   (((a)<(b))? (a):(b))
+--- upnp/upnp/inc/UpnpInet.h	2011-04-03 04:50:36.000000000 +0200
++++ upnp.neww/upnp/inc/UpnpInet.h	2011-11-18 01:54:45.418529337 +0100
+@@ -15,11 +15,6 @@
+ 
+ #ifdef WIN32
+ 	#include <stdarg.h>
+-	#ifndef UPNP_USE_MSVCPP
+-		/* Removed: not required (and cause compilation issues) */
+-		#include <winbase.h>
+-		#include <windef.h>
+-	#endif
+ 	#include <winsock2.h>
+ 	#include <iphlpapi.h>
+ 	#include <ws2tcpip.h>
diff --git a/daemon/contrib/src/upnp/libupnp-win64.patch b/daemon/contrib/src/upnp/libupnp-win64.patch
new file mode 100644
index 0000000000000000000000000000000000000000..8064e6d214e3c382602272c44c26aff7611a9b1a
--- /dev/null
+++ b/daemon/contrib/src/upnp/libupnp-win64.patch
@@ -0,0 +1,41 @@
+--- libupnp/threadutil/inc/ThreadPool.h	2011-01-20 07:46:57.000000000 +0100
++++ libupnp.new/threadutil/inc/ThreadPool.h	2011-09-23 01:36:12.000000000 +0200
+@@ -45,6 +45,7 @@
+ #include <errno.h>
+
+ #ifdef WIN32
++	#ifndef _TIMEZONE_DEFINED
+ 	#include <time.h>
+ 	struct timezone
+ 	{
+@@ -52,6 +53,7 @@
+ 		int  tz_dsttime;     /* type of dst correction */
+ 	};
+ 	int gettimeofday(struct timeval *tv, struct timezone *tz);
++	#endif
+ #else /* WIN32 */
+ 	#include <sys/param.h>
+ 	#include <sys/time.h> /* for gettimeofday() */
+--- libupnp-1.6.16/upnp/inc/upnp.h.orig	2012-03-22 00:15:38.000000000 +0100
++++ libupnp-1.6.16/upnp/inc/upnp.h	2012-03-28 18:58:55.043642000 +0200
+@@ -61,6 +61,20 @@
+ 	/* Other systems ??? */
+ #endif
+ 
++#   if defined( __MINGW32__ )
++#       if !defined( _OFF_T_ )
++            typedef long long _off_t;
++            typedef _off_t off_t;
++#           define _OFF_T_
++#       else
++#           ifdef off_t
++#               undef off_t
++#           endif
++#           define off_t long long
++#       endif
++#   endif
++
++
+ #define LINE_SIZE  (size_t)180
+ #define NAME_SIZE  (size_t)256
+ #define MNFT_NAME_SIZE  64
diff --git a/daemon/contrib/src/upnp/miniserver.patch b/daemon/contrib/src/upnp/miniserver.patch
new file mode 100644
index 0000000000000000000000000000000000000000..45ff90c675953400c07c4834defdad80fcf3f4db
--- /dev/null
+++ b/daemon/contrib/src/upnp/miniserver.patch
@@ -0,0 +1,17 @@
+--- upnp/upnp/src/api/upnpapi.c.orig	2013-04-08 00:23:46.000000000 +0200
++++ upnp/upnp/src/api/upnpapi.c	2013-04-08 00:25:49.000000000 +0200
+@@ -358,13 +358,13 @@
+ 		return retVal;
+ 	}
+ 
++#ifdef INTERNAL_WEB_SERVER
+ #ifdef INCLUDE_DEVICE_APIS
+ #if EXCLUDE_SOAP == 0
+ 	SetSoapCallback(soap_device_callback);
+ #endif
+ #endif /* INCLUDE_DEVICE_APIS */
+ 
+-#ifdef INTERNAL_WEB_SERVER
+ #if EXCLUDE_GENA == 0
+ 	SetGenaCallback(genaCallback);
+ #endif
diff --git a/daemon/contrib/src/upnp/rules.mak b/daemon/contrib/src/upnp/rules.mak
new file mode 100644
index 0000000000000000000000000000000000000000..717fdd9b1d3f35bba1bf7c7f5498718ef4ec07db
--- /dev/null
+++ b/daemon/contrib/src/upnp/rules.mak
@@ -0,0 +1,38 @@
+# UPNP
+UPNP_VERSION := 1.6.19
+UPNP_URL := http://sourceforge.net/projects/pupnp/files/pupnp/libUPnP%20$(UPNP_VERSION)/libupnp-$(UPNP_VERSION).tar.bz2/download
+
+PKGS += upnp
+ifeq ($(call need_pkg,'libupnp'),)
+PKGS_FOUND += upnp
+endif
+
+$(TARBALLS)/libupnp-$(UPNP_VERSION).tar.bz2:
+	$(call download,$(UPNP_URL))
+
+.sum-upnp: libupnp-$(UPNP_VERSION).tar.bz2
+
+ifdef HAVE_WIN32
+DEPS_upnp += pthreads $(DEPS_pthreads)
+LIBUPNP_ECFLAGS = -DPTW32_STATIC_LIB
+endif
+
+upnp: libupnp-$(UPNP_VERSION).tar.bz2 .sum-upnp
+	$(UNPACK)
+ifdef HAVE_WIN32
+	$(APPLY) $(SRC)/upnp/libupnp-configure.patch
+	$(APPLY) $(SRC)/upnp/libupnp-win32.patch
+	$(APPLY) $(SRC)/upnp/libupnp-win64.patch
+endif
+	$(APPLY) $(SRC)/upnp/libupnp-ipv6.patch
+	$(APPLY) $(SRC)/upnp/miniserver.patch
+	$(UPDATE_AUTOCONFIG) && cd $(UNPACK_DIR) && mv config.guess config.sub build-aux/
+	$(MOVE)
+
+.upnp: upnp
+ifdef HAVE_WIN32
+	$(RECONF)
+endif
+	cd $< && $(HOSTVARS) CFLAGS="$(CFLAGS) -DUPNP_STATIC_LIB $(LIBUPNP_ECFLAGS)" ./configure --disable-samples --without-documentation --disable-blocking_tcp_connections $(HOSTCONF)
+	cd $< && $(MAKE) install
+	touch $@
diff --git a/daemon/src/Makefile.am b/daemon/src/Makefile.am
index 1a7b319c3e0842d8fbe7f16564fed7852cafb9ed..f32f2ad5c52be427175b243e2af4f5f8246d66b2 100644
--- a/daemon/src/Makefile.am
+++ b/daemon/src/Makefile.am
@@ -48,7 +48,7 @@ libring_la_LIBADD = \
 	./config/libconfig.la \
 	./hooks/libhooks.la \
 	./history/libhistory.la \
-	./upnp/libupnp.la \
+	./upnp/libupnpcontrol.la \
 	$(RINGACC_LIBA) \
 	$(IAX_LIBA) \
 	$(IM_LIBA) \
diff --git a/daemon/src/account.cpp b/daemon/src/account.cpp
index 1fc905fd3d86d9614586fda05c2efa930e43fb50..fa8ce0b2a7eef4f3ec0b5d9539842c5beff08eeb 100644
--- a/daemon/src/account.cpp
+++ b/daemon/src/account.cpp
@@ -54,7 +54,7 @@
 
 #include <yaml-cpp/yaml.h>
 
-#include "upnp/upnp.h"
+#include "upnp/upnp_control.h"
 #include "ip_utils.h"
 
 namespace ring {
@@ -107,7 +107,7 @@ Account::Account(const string &accountID)
     , userAgent_(DEFAULT_USER_AGENT)
     , hasCustomUserAgent_(false)
     , mailBox_()
-    , upnpEnabled_(false)
+    , upnp_(new upnp::Controller())
 {
     std::random_device rdev;
     std::seed_seq seed {rdev(), rdev()};
@@ -222,7 +222,9 @@ void Account::unserialize(const YAML::Node &node)
     parseValue(node, RINGTONE_PATH_KEY, ringtonePath_);
     parseValue(node, RINGTONE_ENABLED_KEY, ringtoneEnabled_);
 
-    parseValue(node, UPNP_ENABLED_KEY, upnpEnabled_);
+    bool enabled;
+    parseValue(node, UPNP_ENABLED_KEY, enabled);
+    upnpEnabled_.store(enabled);
 }
 
 void Account::setAccountDetails(const std::map<std::string, std::string> &details)
@@ -242,7 +244,9 @@ void Account::setAccountDetails(const std::map<std::string, std::string> &detail
         parseString(details, Conf::CONFIG_ACCOUNT_USERAGENT, userAgent_);
     else
         userAgent_ = DEFAULT_USER_AGENT;
-    parseBool(details, Conf::CONFIG_UPNP_ENABLED, upnpEnabled_);
+    bool enabled;
+    parseBool(details, Conf::CONFIG_UPNP_ENABLED, enabled);
+    upnpEnabled_.store(enabled);
 }
 
 std::map<std::string, std::string> Account::getAccountDetails() const
@@ -467,40 +471,30 @@ Account::parseBool(const std::map<std::string, std::string> &details, const char
 #undef find_iter
 
 /**
- * Checks whether the upnp settings is enabled in the account.
- * If so, tries to get a controller with a valid IGD.
- * If not, destroys the controller.
- *
- * Returns whether or not there is a controller with a valid IGD to use.
+ * Get the UPnP IP (external router) address.
+ * If use UPnP is set to false, the address will be empty.
  */
-bool
-Account::checkUPnP()
-{
-    std::unique_lock<std::mutex> lk(upnp_mtx);
-
-    if (upnpEnabled_ != (bool)upnp_) {
-        if (upnpEnabled_){
-            upnp_.reset(new upnp::Controller());
-            if (upnp_->hasValidIGD())
-                upnpIp_ = upnp_->getExternalIP();
-        } else {
-            upnp_.reset();
-            upnpIp_ = IpAddr{};
-        }
-    }
+IpAddr
+Account::getUPnPIpAddress() const {
+    std::lock_guard<std::mutex> lk(upnp_mtx);
+    if (not upnpEnabled_)
+        return {};
 
-    return upnp_ and upnp_->hasValidIGD();
+    return upnp_->getExternalIP();
 }
 
 /**
- * Returns whether or not the upnp controller has a valid IGD
- * with which to make port mappings
+ * returns whether or not UPnP is enabled and active_
+ * ie: if it is able to make port mappings
  */
 bool
-Account::getUseUPnP() const
+Account::getUPnPActive() const
 {
-    std::unique_lock<std::mutex> lk(upnp_mtx);
-    return upnp_ and upnp_->hasValidIGD();
+    std::lock_guard<std::mutex> lk(upnp_mtx);
+    if (not upnpEnabled_)
+        return false;
+
+    return upnp_->hasValidIGD();
 }
 
 } // namespace ring
diff --git a/daemon/src/account.h b/daemon/src/account.h
index 9f5075ff203caeba9f003169f0819e572a9ac4a3..9da2304fca4404428eed1fe78c532c6e546c1ede 100644
--- a/daemon/src/account.h
+++ b/daemon/src/account.h
@@ -255,18 +255,16 @@ class Account : public Serializable, public std::enable_shared_from_this<Account
         static const char * const VIDEO_CODEC_BITRATE;
 
         /**
-         * @return bool Flag which determines if UPnP is used or not.
+         * returns whether or not UPnP is enabled and active
+         * ie: if it is able to make port mappings
          */
-        bool getUseUPnP() const;
+        bool getUPnPActive() 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_;
-        }
+        IpAddr getUPnPIpAddress() const;
 
     private:
         NON_COPYABLE(Account);
@@ -403,29 +401,15 @@ class Account : public Serializable, public std::enable_shared_from_this<Account
         std::mt19937_64 rand_ {};
 
         /**
-         * UPnP IP address (external router address),
-         * used only if use UPnP is set to true
+         * UPnP IGD controller and the mutex to access it
          */
-        IpAddr upnpIp_ {};
         std::unique_ptr<ring::upnp::Controller> upnp_;
         mutable std::mutex upnp_mtx {};
 
         /**
-         * Whether or not UPnP is enabled for this account
-         * Note that this is only used to store the account setting,
-         * getUseUPnP() should be used to check if UPnP is being use
-         * ie: if the UPnP controller has been initialized
-         */
-        bool upnpEnabled_;
-
-        /**
-         * Checks whether the upnp settings is enabled in the account.
-         * If so, tries to get a controller with a valid IGD.
-         * If not, destroys the controller.
-         *
-         * Returns whether or not there is a controller with a valid IGD to use.
+         * flag which determines if this account is set to use UPnP.
          */
-        bool checkUPnP();
+        std::atomic_bool upnpEnabled_ {false};
 };
 
 } // namespace ring
diff --git a/daemon/src/call.cpp b/daemon/src/call.cpp
index 2c4d4c838f79d706dacfc431e8924a9cbd722439..7e9bda7e0d8e31e556cf56c53d4f80561def1573 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, account_.getUseUPnP());
+                                                        master, account_.getUPnPActive());
 }
 
 int
diff --git a/daemon/src/ice_transport.cpp b/daemon/src/ice_transport.cpp
index d894daad48c2081209a35c277dc911bbc1a90cb3..6a3671c376065010cf6ad26959731871122ec97c 100644
--- a/daemon/src/ice_transport.cpp
+++ b/daemon/src/ice_transport.cpp
@@ -32,7 +32,7 @@
 #include "logger.h"
 #include "sip/sip_utils.h"
 #include "manager.h"
-#include "upnp/upnp.h"
+#include "upnp/upnp_control.h"
 
 #include <pjlib.h>
 #include <utility>
@@ -477,7 +477,7 @@ IceTransport::selectUPnPIceCandidates()
         IpAddr publicIP = upnp_->getExternalIP();
         if (publicIP) {
             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);
+                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();
@@ -486,11 +486,11 @@ IceTransport::selectUPnPIceCandidates()
                         publicIP.setPort(port_used);
                         addCandidate(comp_id, publicIP);
                     } else
-                        RING_WARN("UPnP : Could not create a port mapping for the ICE candidae.");
+                        RING_WARN("UPnP: Could not create a port mapping for the ICE candidae.");
                 }
             }
         } else {
-            RING_WARN("UPnP : Could not determine public IP for ICE candidates.");
+            RING_WARN("UPnP: Could not determine public IP for ICE candidates.");
         }
     }
 }
diff --git a/daemon/src/ringdht/ringaccount.cpp b/daemon/src/ringdht/ringaccount.cpp
index 783145fc98a922d29b84ba92b2d188646f3aba46..18a733098170f85ff692f791c150a9fdadec460a 100644
--- a/daemon/src/ringdht/ringaccount.cpp
+++ b/daemon/src/ringdht/ringaccount.cpp
@@ -61,7 +61,7 @@
 #include "config/yamlparser.h"
 #include <yaml-cpp/yaml.h>
 
-#include "upnp/upnp.h"
+#include "upnp/upnp_control.h"
 
 #include <algorithm>
 #include <array>
@@ -160,7 +160,7 @@ RingAccount::newOutgoingCall(const std::string& id, const std::string& toUrl)
         ("sip:"+call->getCallId()).c_str(),
         ICE_COMPONENTS,
         true,
-        getUseUPnP()
+        getUPnPActive()
     );
     if (not ice or ice->waitForInitialization(ICE_INIT_TIMEOUT) <= 0) {
         call->setConnectionState(Call::DISCONNECTED);
@@ -241,7 +241,7 @@ RingAccount::createOutgoingCall(const std::shared_ptr<SIPCall>& call, const std:
     call->setCallMediaLocal(call->getIceTransport()->getDefaultLocalAddress());
 
     IpAddr addrSdp;
-    if (getUseUPnP()) {
+    if (getUPnPActive()) {
         /* use UPnP addr, or published addr if its set */
         addrSdp = getPublishedSameasLocal() ?
             getUPnPIpAddress() : getPublishedIpAddress();
@@ -572,7 +572,7 @@ RingAccount::handleEvents()
 
 bool RingAccount::mapPortUPnP()
 {
-    if (getUseUPnP()) {
+    if (getUPnPActive()) {
         /* 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
@@ -581,6 +581,7 @@ bool RingAccount::mapPortUPnP()
          * a different port, if succesfull, then we have to use that port for DHT
          */
         uint16_t port_used;
+        std::lock_guard<std::mutex> lock(upnp_mtx);
         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);
@@ -603,11 +604,23 @@ void RingAccount::doRegister()
         return;
     }
 
-    /* check UPnP to see if the account setting is enabled and try to get a
-     * UPnP controller; if this succeeds then try to map the account port */
-    if (not (checkUPnP() and mapPortUPnP()) )
-        RING_WARN("Could not successfully map DHT port with UPnP, continuing with account registration anyways.");
+    /* if UPnP is enabled, then wait for IGD to complete registration */
+    if ( upnpEnabled_ ) {
+        auto shared = shared_from_this();
+        RING_DBG("UPnP: waiting for IGD to register RING account");
+        setRegistrationState(RegistrationState::TRYING);
+        std::thread{ [shared] {
+            auto this_ = std::static_pointer_cast<RingAccount>(shared).get();
+            if ( not this_->mapPortUPnP())
+                RING_WARN("UPnP: Could not successfully map DHT port with UPnP, continuing with account registration anyways.");
+            this_->doRegister_();
+        }}.detach();
+    } else
+        doRegister_();
 
+}
+void RingAccount::doRegister_()
+{
     try {
         loadTreatedCalls();
         if (dht_.isRunning()) {
@@ -724,7 +737,7 @@ void RingAccount::doRegister()
                             ("sip:"+call->getCallId()).c_str(),
                             ICE_COMPONENTS,
                             false,
-                            this_.getUseUPnP()
+                            this_.getUPnPActive()
                         );
                         if (ice->waitForInitialization(ICE_INIT_TIMEOUT) <= 0)
                             throw std::runtime_error("Can't initialize ICE..");
@@ -787,10 +800,8 @@ void RingAccount::doUnregister(std::function<void(bool)> released_cb)
         pendingSipCalls_.clear();
     }
 
-    if (getUseUPnP()) {
-        RING_DBG("UPnP : removing port mapping for DHT account.");
-        upnp_->removeMappings();
-    }
+    /* RING_DBG("UPnP: removing port mapping for DHT account."); */
+    upnp_->removeMappings();
 
     Manager::instance().unregisterEventHandler((uintptr_t)this);
     saveNodes(dht_.exportNodes());
diff --git a/daemon/src/ringdht/ringaccount.h b/daemon/src/ringdht/ringaccount.h
index 2f3b6b8bc24dbf80685a3723f5f1a15214b468e3..13c5716a4273018ad07af9335c99dcb8ac5b18b5 100644
--- a/daemon/src/ringdht/ringaccount.h
+++ b/daemon/src/ringdht/ringaccount.h
@@ -256,6 +256,8 @@ class RingAccount : public SIPAccountBase {
 
     private:
 
+        void doRegister_();
+
         const dht::ValueType USER_PROFILE_TYPE = {9, "User profile", std::chrono::hours(24 * 7)};
         const dht::ValueType ICE_ANNOUCEMENT_TYPE = {10, "ICE descriptors", std::chrono::minutes(3)};
 
diff --git a/daemon/src/sip/sipaccount.cpp b/daemon/src/sip/sipaccount.cpp
index 94406e7b4901800543d6a05186adcb582a870e27..95d815b8b475341ed2a4f8db9ed44c8e632cf205 100644
--- a/daemon/src/sip/sipaccount.cpp
+++ b/daemon/src/sip/sipaccount.cpp
@@ -70,7 +70,7 @@
 #include <sstream>
 #include <cstdlib>
 
-#include "upnp/upnp.h"
+#include "upnp/upnp_control.h"
 #include "ip_utils.h"
 
 namespace ring {
@@ -231,7 +231,7 @@ SIPAccount::newOutgoingCall(const std::string& id, const std::string& toUrl)
     call->setCallMediaLocal(localAddress);
 
     IpAddr addrSdp;
-    if (getUseUPnP()) {
+    if (getUPnPActive()) {
         /* use UPnP addr, or published addr if its set */
         addrSdp = getPublishedSameasLocal() ?
             getUPnPIpAddress() : getPublishedIpAddress();
@@ -712,7 +712,7 @@ std::map<std::string, std::string> SIPAccount::getVolatileAccountDetails() const
 
 bool SIPAccount::mapPortUPnP()
 {
-    if (getUseUPnP()) {
+    if (getUPnPActive()) {
         /* 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
@@ -745,13 +745,39 @@ void SIPAccount::doRegister()
 
     RING_DBG("doRegister %s", hostname_.c_str());
 
-    /* check UPnP to see if the account setting is enabled and try to get a
-     * UPnP controller; if this succeeds then try to map the account port */
-    if (not (checkUPnP() and mapPortUPnP()) )
-        RING_WARN("Could not successfully map SIP port with UPnP, continuing with account registration anyways.");
+    /* if UPnP is enabled, then wait for IGD to complete registration */
+    if ( upnpEnabled_ ) {
+        RING_DBG("UPnP: waiting for IGD to register SIP account");
+        setRegistrationState(RegistrationState::TRYING);
+        auto shared = shared_from_this();
+        RING_DBG("UPnP: waiting for IGD to register RING account");
+        std::thread{ [shared] {
+            /* We have to register the external thread so it could access the pjsip frameworks */
+            if (!pj_thread_is_registered()) {
+#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8)
+                    static thread_local pj_thread_desc desc;
+                    static thread_local pj_thread_t *this_thread;
+#else
+                    static __thread pj_thread_desc desc;
+                    static __thread pj_thread_t *this_thread;
+#endif
+                    RING_DBG("Registering thread with pjlib");
+                    pj_thread_register(NULL, desc, &this_thread);
+            }
+
+            auto this_ = std::static_pointer_cast<SIPAccount>(shared).get();
+            if ( not this_->mapPortUPnP())
+                RING_WARN("UPnP: Could not successfully map SIP port with UPnP, continuing with account registration anyways.");
+            this_->doRegister1_();
+        }}.detach();
+    } else
+        doRegister1_();
+}
 
+void SIPAccount::doRegister1_()
+{
     if (hostname_.empty() || isIP2IP()) {
-        doRegister_();
+        doRegister2_();
         return;
     }
 
@@ -767,12 +793,12 @@ void SIPAccount::doRegister()
                 return;
             }
             this_->hostIp_ = host_ips[0];
-            this_->doRegister_();
+            this_->doRegister2_();
         }
     );
 }
 
-void SIPAccount::doRegister_()
+void SIPAccount::doRegister2_()
 {
     bool ipv6 = false;
     if (isIP2IP()) {
@@ -888,10 +914,8 @@ void SIPAccount::doUnregister(std::function<void(bool)> released_cb)
     if (released_cb)
         released_cb(true);
 
-    if (getUseUPnP()) {
-        RING_DBG("UPnP : removing port mapping for SIP account.");
-        upnp_->removeMappings();
-    }
+    /* RING_DBG("UPnP: removing port mapping for SIP account."); */
+    upnp_->removeMappings();
 }
 
 void SIPAccount::startKeepAliveTimer()
@@ -969,7 +993,7 @@ SIPAccount::sendRegister()
     const pj_str_t pjContact(getContactHeader());
 
     if (transport_) {
-        if (getUseUPnP() or not getPublishedSameasLocal() or (not received.empty() and received != getPublishedAddress())) {
+        if (getUPnPActive() 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);
 
@@ -1425,7 +1449,7 @@ SIPAccount::getContactHeader(pjsip_transport* t)
         hostname_,
         address, port);
 
-    if (getUseUPnP() and getUPnPIpAddress()) {
+    if (getUPnPActive() and getUPnPIpAddress()) {
         address = getUPnPIpAddress().toString();
         port = publishedPortUsed_;
         useUPnPAddressPortInVIA();
diff --git a/daemon/src/sip/sipaccount.h b/daemon/src/sip/sipaccount.h
index 9cb3a231ca21996ffde431effacbab602150619b..340c62cad328ad07a3f725470dcf4742af63939c 100644
--- a/daemon/src/sip/sipaccount.h
+++ b/daemon/src/sip/sipaccount.h
@@ -491,7 +491,8 @@ class SIPAccount : public SIPAccountBase {
         void onRegister(pjsip_regc_cbparam *param);
 
     private:
-        void doRegister_();
+        void doRegister1_();
+        void doRegister2_();
 
         /**
          * Set the internal state for this account, mainly used to manage account details from the client application.
diff --git a/daemon/src/sip/sipcall.cpp b/daemon/src/sip/sipcall.cpp
index bc7744898768395906933ff26d0eb34186e1a8f0..11027bee8051d37714036283514dd6a2b8d5a04f 100644
--- a/daemon/src/sip/sipcall.cpp
+++ b/daemon/src/sip/sipcall.cpp
@@ -42,7 +42,7 @@
 #include "sdp.h"
 #include "manager.h"
 #include "array_size.h"
-#include "upnp/upnp.h"
+#include "upnp/upnp_control.h"
 
 #include "audio/audiortp/avformat_rtp_session.h"
 #include "client/callmanager.h"
@@ -127,7 +127,7 @@ SIPCall::SIPCall(SIPAccountBase& account, const std::string& id, Call::CallType
 #endif
     , sdp_(new Sdp(id))
 {
-    if (account.getUseUPnP())
+    if (account.getUPnPActive())
         upnp_.reset(new upnp::Controller());
 }
 
@@ -866,7 +866,7 @@ SIPCall::openPortsUPnP()
          *       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.");
+        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;
diff --git a/daemon/src/sip/sipvoiplink.cpp b/daemon/src/sip/sipvoiplink.cpp
index 3f7c8e40bb6527ad623ae008dfffa1ee2ec083f3..b7b78398076e9dad7be0cd6dd6f873e51fbcb230 100644
--- a/daemon/src/sip/sipvoiplink.cpp
+++ b/daemon/src/sip/sipvoiplink.cpp
@@ -320,7 +320,7 @@ transaction_request_cb(pjsip_rx_data *rdata)
     IpAddr addrToUse = ip_utils::getInterfaceAddr(account->getLocalInterface(), family);
 
     IpAddr addrSdp;
-    if (account->getUseUPnP()) {
+    if (account->getUPnPActive()) {
         /* use UPnP addr, or published addr if its set */
         addrSdp = account->getPublishedSameasLocal() ?
             account->getUPnPIpAddress() : account->getPublishedIpAddress();
@@ -899,7 +899,7 @@ sdp_create_offer_cb(pjsip_inv_session *inv, pjmedia_sdp_session **p_offer)
     IpAddr addrToUse = ip_utils::getInterfaceAddr(account.getLocalInterface(), family);
 
     IpAddr address;
-    if (account.getUseUPnP()) {
+    if (account.getUPnPActive()) {
         /* use UPnP addr, or published addr if its set */
         address = account.getPublishedSameasLocal() ?
             account.getUPnPIpAddress() : account.getPublishedIpAddress();
diff --git a/daemon/src/upnp/Makefile.am b/daemon/src/upnp/Makefile.am
index 1560c07e7ce77c70c88b74cfdf88fedec95369aa..e62c6dcfc5619236333eccb6de108da869118526 100644
--- a/daemon/src/upnp/Makefile.am
+++ b/daemon/src/upnp/Makefile.am
@@ -1,11 +1,17 @@
 include $(top_srcdir)/globals.mak
 
-noinst_LTLIBRARIES = libupnp.la
+noinst_LTLIBRARIES = libupnpcontrol.la
 
-libupnp_la_SOURCES = \
-        upnp.cpp \
-        upnp.h
+libupnpcontrol_la_CXXFLAGS = \
+				@CXXFLAGS@ \
+				@LIBUPNP_CFLAGS@
 
-libupnp_la_CXXFLAGS = @CXXFLAGS@
+libupnpcontrol_la_LDFLAGS = @LIBUPNP_LIBS@
 
-libupnp_la_LDFLAGS = @UPNP_LIBS@
+libupnpcontrol_la_SOURCES = \
+        upnp_control.cpp \
+        upnp_control.h \
+        upnp_context.cpp \
+        upnp_context.h \
+        upnp_igd.cpp \
+        upnp_igd.h
diff --git a/daemon/src/upnp/upnp.cpp b/daemon/src/upnp/upnp.cpp
deleted file mode 100644
index c2938242d8c76d9e69817ff7482c668d67457ed2..0000000000000000000000000000000000000000
--- a/daemon/src/upnp/upnp.cpp
+++ /dev/null
@@ -1,631 +0,0 @@
-/*
- *  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
-
-static void
-resetURLs(UPNPUrls& urls)
-{
-    urls.controlURL = nullptr;
-    urls.ipcondescURL = nullptr;
-    urls.controlURL_CIF = nullptr;
-    urls.controlURL_6FC = nullptr;
-#ifdef MINIUPNPC_VERSION /* if not defined, its version 1.6 */
-    urls.rootdescURL = nullptr;
-#endif
-}
-
-/* move constructor */
-IGD::IGD(IGD&& other)
-    : datas_(other.datas_)
-    , urls_(other.urls_)
-{
-    resetURLs(other.urls_);
-}
-
-/* move operator */
-IGD& IGD::operator=(IGD&& other)
-{
-    datas_ = other.datas_;
-    urls_ = other.urls_;
-    resetURLs(other.urls_);
-    return *this;
-}
-
-IGD::~IGD()
-{
-    /* free the URLs */
-    FreeUPNPUrls(&urls_);
-}
-
-#endif /* HAVE_UPNP */
-
-/**
- * removes all mappings with the local IP and the given description
- */
-void
-IGD::removeMappingsByLocalIPAndDescription(const std::string& description)
-{
-#if HAVE_UPNP
-    if (isEmpty())
-        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(getURLs().controlURL,
-                                                      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(getURLs().controlURL, 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--;
-                }
-            }
-        } 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
-}
-
-/**
- * checks if the instance of IGD is empty
- * ie: not actually an IGD
- */
-bool
-IGD::isEmpty() const
-{
-#if HAVE_UPNP
-    if (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()
-    : defaultIGD_(getIGD())
-    , udpGlobalMappings_(getGlobalInstance<UDPMapGlobal>())
-    , tcpGlobalMappings_(getGlobalInstance<TCPMapGlobal>())
-{}
-
-Controller::~Controller()
-{
-    /* remove all mappings */
-    removeMappings();
-}
-
-/**
- * Return whether or not this controller has a valid IGD,
- * if 'flase' then all requests will fail
- */
-bool
-Controller::hasValidIGD()
-{
-#if HAVE_UPNP
-    return not defaultIGD_->isEmpty();
-#endif
-    return false;
-}
-
-/**
- * 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
-    if (defaultIGD_->isEmpty()) {
-        RING_WARN("UPnP : cannot perform command as the IGD has either not been chosen or is not available.");
-        return false;
-    }
-
-    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
-}
-
-/**
- * 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 {};
-    }
-
-    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 {};
-    } else {
-        RING_DBG("UPnP : got external IP = %s", externalIPAddress);
-        return {std::string(externalIPAddress)};
-    }
-#else
-    /* return empty address */
-    return {};
-#endif
-}
-
-/**
- * tries to find a valid IGD on the network
- */
-static IGD
-chooseIGD(void)
-{
-#if HAVE_UPNP
-    std::unique_ptr<UPNPDev, decltype(freeUPNPDevlist)&> devlist(nullptr, freeUPNPDevlist);
-    int upnp_status = 0;
-
-    RING_DBG("UPnP : finding default IGD");
-
-    /* look for UPnP devices on the network */
-    devlist.reset(upnpDiscover(2000, NULL, NULL, 0, 0, &upnp_status));
-
-    if (devlist) {
-        UPNPDev * device = nullptr;
-        std::unique_ptr<UPNPUrls, decltype(FreeUPNPUrls)&> newIGDURLs(new UPNPUrls, FreeUPNPUrls);
-        std::unique_ptr<IGDdatas> newIGDDatas(new IGDdatas);
-        char lanaddr[64];
-
-        RING_DBG("UPnP devices found on the network");
-        for(device = devlist.get(); device; device = device->pNext)
-        {
-            RING_DBG(" desc: %s\n st: %s",
-                     device->descURL, device->st);
-        }
-
-        upnp_status = UPNP_GetValidIGD(devlist.get(), newIGDURLs.get(), newIGDDatas.get(), lanaddr, sizeof(lanaddr));
-
-        switch(upnp_status) {
-            case -1:
-                RING_ERR("UPnP : internal error getting valid IGD");
-                break;
-            case 0:
-                RING_WARN("UPnP : no valid IGD found");
-                break;
-            case 1:
-                RING_DBG("UPnP : found valid IGD : %s", newIGDURLs->controlURL);
-                break;
-            case 2:
-                RING_WARN("UPnP : found an IGD, but it does not seem to be connected : %s", newIGDURLs->controlURL);
-                break;
-            case 3:
-                RING_WARN("UPnP : UPnP device found, but it does not seem to be an IGD : %s", newIGDURLs->controlURL);
-                break;
-            default:
-                RING_WARN("UPnP : device found, but cannot determine if it is a UPnP device : %s", newIGDURLs->controlURL);
-        }
-
-        /* only accept IGD if it was determined to be valid */
-        if (upnp_status == 1) {
-            RING_DBG("UPnP : local IP address reported as: %s", lanaddr);
-            return {*newIGDDatas.release(), *newIGDURLs.release()};
-        } else {
-            return {};
-        }
-    } else {
-        RING_WARN("UPnP : looking for IGD UPnP devices on the network failed with error: %d : %s", upnp_status, strupnperror(upnp_status));
-        return {};
-    }
-
-#else
-    return {};
-#endif
-}
-
-static void
-clean_igd(IGD* igd)
-{
-    if (igd and not igd->isEmpty()) {
-        RING_DBG("UPnP : removing all RING mappings before deleting shared IGD object");
-        igd->removeMappingsByLocalIPAndDescription(Mapping::UPNP_DEFAULT_MAPPING_DESCRIPTION);
-    }
-    delete igd;
-};
-
-std::shared_ptr<IGD>
-getIGD(void)
-{
-    static std::shared_ptr<IGD> igd(nullptr, clean_igd);
-
-    if (not igd) {
-        igd = std::make_shared<IGD>(chooseIGD());
-        if (not igd->isEmpty()) {
-            /* remove any old RING mappings the first time we find an IGD */
-            RING_DBG("UPnP : removing any existing RING mappings for new IGD");
-            igd->removeMappingsByLocalIPAndDescription(Mapping::UPNP_DEFAULT_MAPPING_DESCRIPTION);
-        }
-    }
-
-    return igd;
-}
-
-}} // namespace ring::upnp
diff --git a/daemon/src/upnp/upnp.h b/daemon/src/upnp/upnp.h
deleted file mode 100644
index db2616b88b814d9c83e741219977ee5aebd0a363..0000000000000000000000000000000000000000
--- a/daemon/src/upnp/upnp.h
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- *  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
-
-    /* constructors */
-    IGD() : datas_(), urls_() {};
-    IGD(IGDdatas& d , UPNPUrls& u) : datas_(d), urls_(u) {};
-
-    /* move constructor and operator */
-    IGD(IGD&& other);
-    IGD& operator=(IGD&& other);
-
-    ~IGD();
-
-    const IGDdatas& getDatas() const {return datas_;};
-    const UPNPUrls& getURLs() const {return urls_;};
-#else
-    /* use default constructor and destructor */
-    IGD() = default;
-    ~IGD() = default;;
-    /* use default move constructor and operator */
-    IGD(IGD&&) = default;
-    IGD& operator=(IGD&&) = default;
-#endif
-    bool isEmpty() const;
-
-    /**
-     * removes all mappings with the local IP and the given description
-     */
-    void removeMappingsByLocalIPAndDescription(const std::string& description);
-
-private:
-    NON_COPYABLE(IGD);
-
-#if HAVE_UPNP
-    IGDdatas datas_;
-    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&&) = 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();
-
-    /**
-     * Return whether or not this controller has a valid IGD,
-     * if 'flase' then all requests will fail
-     */
-    bool hasValidIGD();
-
-    /**
-     * 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();
-
-    /**
-     * 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_ */
diff --git a/daemon/src/upnp/upnp_context.cpp b/daemon/src/upnp/upnp_context.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2cdc46d49b50dbba201907236cb02c79c7ffdcf9
--- /dev/null
+++ b/daemon/src/upnp/upnp_context.cpp
@@ -0,0 +1,1061 @@
+/*
+ *  Copyright (C) 2004-2015 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_context.h"
+
+#include <string>
+#include <set>
+#include <mutex>
+#include <memory>
+#include <condition_variable>
+#include <random>
+
+#if HAVE_LIBUPNP
+#include <upnp/upnp.h>
+#include <upnp/upnptools.h>
+#endif
+
+#include "logger.h"
+#include "ip_utils.h"
+#include "upnp_igd.h"
+
+namespace ring { namespace upnp {
+
+/**
+ * This should be used to get a UPnPContext.
+ * It only makes sense to have one unless you have separate
+ * contexts for multiple internet interfaces, which is not currently
+ * supported.
+ */
+std::shared_ptr<UPnPContext>
+getUPnPContext()
+{
+    static auto context = std::make_shared<UPnPContext>();
+    return context;
+}
+
+#if HAVE_LIBUPNP
+
+/* UPnP IGD definitions */
+constexpr static const char * UPNP_ROOT_DEVICE = "upnp:rootdevice";
+constexpr static const char * UPNP_IGD_DEVICE = "urn:schemas-upnp-org:device:InternetGatewayDevice:1";
+constexpr static const char * UPNP_WAN_DEVICE = "urn:schemas-upnp-org:device:WANDevice:1";
+constexpr static const char * UPNP_WANCON_DEVICE = "urn:schemas-upnp-org:device:WANConnectionDevice:1";
+constexpr static const char * UPNP_WANIP_SERVICE = "urn:schemas-upnp-org:service:WANIPConnection:1";
+constexpr static const char * UPNP_WANPPP_SERVICE = "urn:schemas-upnp-org:service:WANPPPConnection:1";
+
+/* UPnP error codes */
+constexpr static int          INVALID_ARGS = 402;
+constexpr static const char * INVALID_ARGS_STR = "402";
+constexpr static int          ARRAY_IDX_INVALID = 713;
+constexpr static const char * ARRAY_IDX_INVALID_STR = "713";
+constexpr static int          CONFLICT_IN_MAPPING = 718;
+constexpr static const char * CONFLICT_IN_MAPPING_STR = "718";
+
+/*
+ * Local prototypes
+ */
+int cp_callback(Upnp_EventType, void*, void*);
+static std::string get_element_text(IXML_Node*);
+static std::string get_first_doc_item(IXML_Document*, const char*);
+static std::string get_first_element_item(IXML_Element*, const char*);
+static void checkResponseError(IXML_Document*);
+
+UPnPContext::UPnPContext()
+{
+    int upnp_err;
+    char* ip_address = nullptr;
+    unsigned short port = 0;
+
+    /* TODO: allow user to specify interface to be used
+     *       by selecting the IP
+     */
+
+#ifdef UPNP_ENABLE_IPV6
+    /* TODO: test if ipv6 support works properly, eg: what if router doesn't support ipv6? */
+    RING_DBG("UPnP: using IPv6");
+    upnp_err = UpnpInit2(0, 0);
+#else
+    RING_DBG("UPnP: using IPv4");
+    upnp_err = UpnpInit(0, 0);
+#endif
+    if ( upnp_err != UPNP_E_SUCCESS ) {
+        RING_ERR("UPnP: error in UpnpInit(): %s", UpnpGetErrorMessage(upnp_err));
+        UpnpFinish();
+    }
+    initialized_ = true;
+
+    ip_address = UpnpGetServerIpAddress(); /* do not free, it is freed by UpnpFinish() */
+    port = UpnpGetServerPort();
+
+    RING_DBG("UPnP: initialiazed on %s:%u", ip_address, port);
+
+    /* relax the parser to allow malformed XML text */
+    ixmlRelaxParser( 1 );
+
+    /* Register a control point to start looking for devices right away */
+    upnp_err = UpnpRegisterClient( cp_callback, nullptr, &ctrlptHandle_ );
+    if ( upnp_err != UPNP_E_SUCCESS ) {
+        RING_ERR("UPnP: error registering control point: %s",
+                 UpnpGetErrorMessage(upnp_err));
+        UpnpFinish();
+        initialized_ = false;
+    }
+    clientRegistered_ = true;
+
+    /* send out async searches;
+     * even if no account is using UPnP currently we might as well start
+     * gathering a list of available devices;
+     * we will probably receive their advertisements either way
+     */
+    searchForIGD();
+}
+
+UPnPContext::~UPnPContext()
+{
+    if (initialized_){
+        /* make sure everything is unregistered, freed, and UpnpFinish() is called */
+
+        {
+            std::lock_guard<std::mutex> lock(validIGDMutex_);
+            for( auto const &it : validIGDs_) {
+                removeMappingsByLocalIPAndDescription(it.second.get(), Mapping::UPNP_DEFAULT_MAPPING_DESCRIPTION);
+            }
+        }
+
+        if (clientRegistered_)
+            UpnpUnRegisterClient( ctrlptHandle_ );
+
+        if (deviceRegistered_)
+            UpnpUnRegisterRootDevice( deviceHandle_ );
+
+        UpnpFinish();
+    }
+}
+
+void
+UPnPContext::searchForIGD()
+{
+    if (clientRegistered_) {
+        std::lock_guard<std::mutex> lock(igdSearchMutex_);
+        /* send out search for both types, as some routers may possibly only reply to one */
+        UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT_SEC, UPNP_ROOT_DEVICE, nullptr);
+        UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT_SEC, UPNP_IGD_DEVICE, nullptr);
+        UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT_SEC, UPNP_WANIP_SERVICE, nullptr);
+        UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT_SEC, UPNP_WANPPP_SERVICE, nullptr);
+
+        pendingIGDSearchRequests_ = 4;
+    } else
+        RING_WARN("UPnP: Control Point not registered.");
+}
+
+/**
+ * returns 'true' if there is at least one valid (connected) IGD
+ * note: this function will until an IGD has been found or SEARCH_TIMEOUT_SEC
+ *       have expired; the timeout starts when the context is created as we start
+ *       searchign for IGDs immediately
+ */
+bool
+UPnPContext::hasValidIGD()
+{
+    if (not clientRegistered_) {
+        RING_WARN("UPnP: Control Point not registered.");
+        return false;
+    }
+
+    {
+        std::unique_lock<std::mutex> lock(igdSearchMutex_);
+        igdSearchCondition_.wait(lock, [this] { return pendingIGDSearchRequests_ <= 0;});
+    }
+
+    std::lock_guard<std::mutex> lock(validIGDMutex_);
+    return not validIGDs_.empty();
+}
+
+/**
+ * chooses the IGD to use,
+ * assumes you already have a lock on validIGDMutex_
+ */
+IGD*
+UPnPContext::chooseIGD_unlocked()
+{
+    if (validIGDs_.empty())
+        return nullptr;
+    return validIGDs_.begin()->second.get();
+}
+
+/**
+ * tries to add mapping
+ */
+Mapping
+UPnPContext::addMapping(IGD* igd,
+                        uint16_t port_external,
+                        uint16_t port_internal,
+                        PortType type,
+                        int *upnp_error)
+{
+    *upnp_error = -1;
+
+    IpAddr local_ip = ip_utils::getLocalAddr(pj_AF_INET());
+    if (!local_ip) {
+        RING_DBG("UPnP: cannot determine local IP");
+        return {};
+    }
+
+    Mapping mapping{local_ip, port_external, port_internal};
+
+    /* check if this mapping already exists
+     * 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 ? &igd->udpMappings : &igd->tcpMappings;
+    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 */
+            *upnp_error = UPNP_E_SUCCESS;
+            ++(mapping_ptr->users);
+            RING_DBG("UPnp : mapping already exists, incrementing number of users: %d",
+                     iter->second.users);
+            return mapping;
+        } 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());
+            *upnp_error = CONFLICT_IN_MAPPING;
+            return {};
+        }
+    }
+
+    /* mapping doesn't exist, so try to add it */
+    RING_DBG("UPnP: adding port mapping : %s", mapping.toString().c_str());
+
+    if(addPortMapping(igd, mapping, upnp_error)) {
+        /* success; add it to global list */
+        globalMappings->emplace(port_external, std::move(GlobalMapping{mapping}));
+        return mapping;
+    }
+    return {};
+}
+
+static uint16_t
+generateRandomPort()
+{
+    /* obtain a random number from hardware */
+    static std::random_device rd;
+    /* seed the generator */
+    static std::mt19937 gen(rd());
+    /* define the range */
+    static std::uniform_int_distribution<uint16_t> dist(Mapping::UPNP_PORT_MIN, Mapping::UPNP_PORT_MAX);
+
+    return dist(gen);;
+}
+
+/**
+ * chooses a random port that is not yet used by the daemon for UPnP
+ */
+uint16_t
+UPnPContext::chooseRandomPort(const IGD* igd, PortType type)
+{
+    auto globalMappings = type == PortType::UDP ?
+                          &igd->udpMappings : &igd->tcpMappings;
+
+    uint16_t port = generateRandomPort();
+
+    /* keep generating random ports until we find one which is not used */
+    while(globalMappings->find(port) != globalMappings->end()) {
+        port = generateRandomPort();
+    }
+
+    RING_DBG("UPnP: chose random port %u", port);
+
+    return port;
+}
+
+/**
+ * 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 external and internal ports are the same
+ *
+ * returns a valid mapping on success and an invalid mapping on failure
+ */
+Mapping
+UPnPContext::addAnyMapping(uint16_t port_desired,
+                           uint16_t port_local,
+                           PortType type,
+                           bool use_same_port,
+                           bool unique)
+{
+    if (not hasValidIGD()) {
+        RING_WARN("UPnP: no valid IGD availabe");
+        return {};
+    }
+
+    /* get a lock on the igd list because we don't want the igd to be modified
+     * or removed from the list while using it */
+    std::lock_guard<std::mutex> lock(validIGDMutex_);
+    IGD* igd = chooseIGD_unlocked();
+    if (not igd) {
+        RING_WARN("UPnP: no valid IGD availabe");
+        return {};
+    }
+
+    auto globalMappings = type == PortType::UDP ?
+                          &igd->udpMappings : &igd->tcpMappings;
+    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(igd, type);
+        }
+    }
+
+    if (use_same_port)
+        port_local = port_desired;
+
+    int upnp_error;
+    Mapping mapping = addMapping(igd, 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 ( not mapping and (upnp_error == CONFLICT_IN_MAPPING or upnp_error == INVALID_ARGS) ) {
+        /* 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(igd, type);
+        if (use_same_port)
+            port_local = port_desired;
+        mapping = addMapping(igd, port_desired, port_local, type, &upnp_error);
+    }
+
+    return mapping;
+}
+
+/**
+ * tries to remove the given mapping
+ */
+void
+UPnPContext::removeMapping(const Mapping& mapping)
+{
+    if (not hasValidIGD()) {
+        RING_WARN("UPnP: no valid IGD availabe");
+        return;
+    }
+
+    /* get a lock on the igd list because we don't want the igd to be modified
+     * or removed from the list while using it */
+    std::lock_guard<std::mutex> lock(validIGDMutex_);
+    IGD* igd = chooseIGD_unlocked();
+    if (not igd) {
+        RING_WARN("UPnP: no valid IGD availabe");
+        return;;
+    }
+
+    /* first make sure the mapping exists in the global list of the igd */
+    auto globalMappings = mapping.getType() == PortType::UDP ?
+                          &igd->udpMappings : &igd->tcpMappings;
+
+    auto iter = globalMappings->find(mapping.getPortExternal());
+    if ( iter != globalMappings->end() ) {
+        /* make sure its the same mapping */
+        GlobalMapping *global_mapping = &iter->second;
+        if (mapping == *global_mapping ) {
+            /* now check the users */
+            if (global_mapping->users > 1) {
+                /* more than one user, simply decrement the number */
+                --(global_mapping->users);
+                RING_DBG("UPnP: decrementing user of mapping: %s, %d users remaining",
+                         mapping.toString().c_str(), global_mapping->users);
+            } else {
+                /* no other users, can delete */
+                RING_DBG("UPnP: removing port mapping : %s",
+                         mapping.toString().c_str());
+                deletePortMapping(igd,
+                                  mapping.getPortExternalStr(),
+                                  mapping.getTypeStr());
+                globalMappings->erase(iter);
+            }
+        } else {
+            RING_WARN("UPnP: cannot remove mapping which doesn't match the existing one in the IGD list");
+        }
+    } else {
+        RING_WARN("UPnP: cannot remove mapping which is not in the list of existing mappings of the IGD");
+    }
+}
+
+IpAddr
+UPnPContext::getExternalIP()
+{
+    if (not hasValidIGD()) {
+        RING_WARN("UPnP: no valid IGD availabe");
+        return {};
+    }
+
+    /* get a lock on the igd list because we don't want the igd to be modified
+     * or removed from the list while using it */
+    std::lock_guard<std::mutex> lock(validIGDMutex_);
+    IGD* igd = chooseIGD_unlocked();
+    if (not igd) {
+        RING_WARN("UPnP: no valid IGD availabe");
+        return {};
+    }
+
+    /* if its a valid igd, we must have already gotten the external ip */
+    return igd->publicIp;
+}
+
+/**
+ * Parses the device description and adds desired devices to
+ * relevant lists
+ */
+void
+UPnPContext::parseDevice(IXML_Document* doc, const Upnp_Discovery* d_event)
+{
+    if (not doc or not d_event)
+        return;
+
+    /* check to see the device type */
+    std::string deviceType = get_first_doc_item(doc, "deviceType");
+    if (deviceType.empty()) {
+        /* RING_DBG("UPnP: could not find deviceType in the description document of the device"); */
+        return;
+    }
+
+    if (deviceType.compare(UPNP_IGD_DEVICE) == 0) {
+        parseIGD(doc, d_event);
+    }
+
+    /* TODO: check if its a ring device */
+}
+
+void
+UPnPContext::parseIGD(IXML_Document* doc, const Upnp_Discovery* d_event)
+{
+    if (not doc or not d_event)
+        return;
+
+    /* check the UDN to see if its already in our device list(s)
+     * if it is, then update the device advertisement timeout (expiration)
+     */
+    std::string UDN = get_first_doc_item(doc, "UDN");
+    if (UDN.empty()) {
+        RING_DBG("UPnP: could not find UDN in description document of device");
+        return;
+    }
+
+    {
+        std::lock_guard<std::mutex> lock(validIGDMutex_);
+        auto it = validIGDs_.find(UDN);
+
+        if (it != validIGDs_.end()) {
+            /* we already have this device in our list */
+            /* TODO: update expiration */
+            return;
+        }
+    }
+
+    std::unique_ptr<IGD> new_igd;
+    int upnp_err;
+
+    std::string friendlyName = get_first_doc_item(doc, "friendlyName");
+    if (not friendlyName.empty() )
+        RING_DBG("UPnP: checking new device of type IGD: '%s'",
+                 friendlyName.c_str());
+
+    /* determine baseURL */
+    std::string baseURL = get_first_doc_item(doc, "URLBase");
+    if (baseURL.empty()) {
+        /* get it from the discovery event location */
+        baseURL = std::string(d_event->Location);
+    }
+
+    /* check if its a valid IGD:
+     *      1. check for IGD device... already done if this function is called
+     *      2. check for WAN device... skip checking for this and check for the services directly
+     *      3. check for WANIPConnection service or WANPPPConnection service
+     *      4. check if connected to Internet (if not, no point in port forwarding)
+     *      5. check that we can get the external IP
+     */
+
+    /* get list of services defined by serviceType */
+    std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&> serviceList(nullptr, ixmlNodeList_free);
+    serviceList.reset(ixmlDocument_getElementsByTagName(doc, "serviceType"));
+
+    /* get list of all 'serviceType' elements */
+    bool found_connected_IGD = false;
+    unsigned long list_length = ixmlNodeList_length(serviceList.get());
+
+    /* go through the 'serviceType' nodes until we find the first service of type
+     * WANIPConnection or WANPPPConnection which is connected to an external network */
+    for (unsigned long node_idx = 0; node_idx < list_length and not found_connected_IGD; node_idx++) {
+        IXML_Node* serviceType_node = ixmlNodeList_item(serviceList.get(), node_idx);
+        std::string serviceType = get_element_text(serviceType_node);
+
+        /* only check serviceType of WANIPConnection or WANPPPConnection */
+        if (serviceType.compare(UPNP_WANIP_SERVICE) == 0
+            or serviceType.compare(UPNP_WANPPP_SERVICE) == 0) {
+
+            /* we found a correct 'serviceType', now get the parent node because
+             * the rest of the service definitions are siblings of 'serviceType' */
+            IXML_Node* service_node = ixmlNode_getParentNode(serviceType_node);
+            if (service_node) {
+                /* perform sanity check; the parent node should be called "service" */
+                if( strcmp(ixmlNode_getNodeName(service_node), "service") == 0) {
+                    /* get the rest of the service definitions */
+
+                    /* serviceId */
+                    IXML_Element* service_element = (IXML_Element*)service_node;
+                    std::string serviceId = get_first_element_item(service_element, "serviceId");
+
+                    /* get the relative controlURL and turn it into absolute address using the URLBase */
+                    std::string controlURL = get_first_element_item(service_element, "controlURL");
+                    if (not controlURL.empty()) {
+                        std::unique_ptr<char> absolute_url(new char[baseURL.size() + controlURL.size() + 1]);
+                        upnp_err = UpnpResolveURL(baseURL.c_str(), controlURL.c_str(), absolute_url.get());
+                        if (upnp_err == UPNP_E_SUCCESS) {
+                            controlURL = std::string(absolute_url.get());
+                        } else {
+                            RING_WARN("UPnP: error resolving absolute controlURL: %s", UpnpGetErrorMessage(upnp_err));
+                        }
+                    }
+
+                    /* get the relative eventSubURL and turn it into absolute address using the URLBase */
+                    std::string eventSubURL = get_first_element_item(service_element, "eventSubURL");
+                    if (not eventSubURL.empty()) {
+                        std::unique_ptr<char> absolute_url(new char[baseURL.size() + controlURL.size() + 1]);
+                        upnp_err = UpnpResolveURL(baseURL.c_str(), eventSubURL.c_str(), absolute_url.get());
+                        if (upnp_err == UPNP_E_SUCCESS) {
+                            eventSubURL = std::string(absolute_url.get());
+                        } else {
+                            RING_WARN("UPnP: error resolving absolute eventSubURL: %s", UpnpGetErrorMessage(upnp_err));
+                        }
+                    }
+
+                    /* make sure all of the services are defined
+                     * and check if the IGD is connected to an external network */
+                    if (not (serviceId.empty() and controlURL.empty() and eventSubURL.empty()) ) {
+                        /* RING_DBG("UPnP: got service info from device:\n\tserviceType: %s\n\tserviceID: %s\n\tcontrolURL: %s\n\teventSubURL: %s",
+                                 serviceType.c_str(), serviceId.c_str(), controlURL.c_str(), eventSubURL.c_str()); */
+                        new_igd.reset(new IGD(UDN, baseURL, friendlyName, serviceType, serviceId, controlURL, eventSubURL));
+                        if (isIGDConnected(new_igd.get())) {
+                            new_igd->publicIp = getExternalIP(new_igd.get());
+                            if (new_igd->publicIp) {
+                                RING_DBG("UPnP: got external IP: %s", new_igd->publicIp.toString().c_str());
+                                found_connected_IGD = true;
+                            }
+                        }
+                    }
+                    /* TODO: subscribe to the service to get events, eg: when IP changes */
+                } else
+                     RING_WARN("UPnP: IGD \"serviceType\" parent node is not called \"service\"!");
+            } else
+                RING_WARN("UPnP: IGD \"serviceType\" has no parent node!");
+        }
+    }
+
+    /* if its a valid IGD, add to list of IGDs (ideally there is only one at a time)
+     * subscribe to the WANIPConnection or WANPPPConnection service to receive
+     * updates about state changes, eg: new external IP
+     */
+    if (found_connected_IGD) {
+        RING_DBG("UPnP: found a valid IGD!");
+
+        {
+            std::lock_guard<std::mutex> lock(validIGDMutex_);
+            /* delete all RING mappings first */
+            removeMappingsByLocalIPAndDescription(new_igd.get(), Mapping::UPNP_DEFAULT_MAPPING_DESCRIPTION);
+            validIGDs_.emplace(UDN, std::move(new_igd));
+        }
+
+        /* check if we have pending search requests and notify anyone waiting */
+        {
+            std::lock_guard<std::mutex> lock(igdSearchMutex_);
+            if (pendingIGDSearchRequests_ > 0) {
+                pendingIGDSearchRequests_ = 0;
+            }
+        }
+        igdSearchCondition_.notify_all();
+    }
+}
+
+static std::string
+get_element_text(IXML_Node* node)
+{
+    std::string ret;
+    if (node) {
+        IXML_Node *textNode = ixmlNode_getFirstChild(node);
+        if (textNode) {
+            const char* value = ixmlNode_getNodeValue(textNode);
+            if (value)
+                ret = std::string(value);
+        }
+    }
+    return ret;
+}
+
+static std::string
+get_first_doc_item(IXML_Document* doc, const char* item)
+{
+    std::string ret;
+
+    std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&>
+        nodeList(ixmlDocument_getElementsByTagName(doc, item), ixmlNodeList_free);
+    if (nodeList) {
+        /* if there are several nodes which match the tag, we only want the first one */
+        ret = get_element_text( ixmlNodeList_item(nodeList.get(), 0) );
+    }
+    return ret;
+}
+
+static std::string
+get_first_element_item(IXML_Element* element, const char* item)
+{
+    std::string ret;
+
+    std::unique_ptr<IXML_NodeList, decltype(ixmlNodeList_free)&>
+        nodeList(ixmlElement_getElementsByTagName(element, item), ixmlNodeList_free);
+    if (nodeList) {
+        /* if there are several nodes which match the tag, we only want the first one */
+        ret = get_element_text( ixmlNodeList_item(nodeList.get(), 0) );
+    }
+    return ret;
+}
+
+
+int
+cp_callback(Upnp_EventType event_type, void* event, void* user_data)
+{
+    auto upnpContext = getUPnPContext();
+
+    switch( event_type )
+    {
+    case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
+        /* RING_DBG("UPnP: CP received a discovery advertisement"); */
+    case UPNP_DISCOVERY_SEARCH_RESULT:
+    {
+        struct Upnp_Discovery* d_event = ( struct Upnp_Discovery* )event;
+        std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> desc_doc(nullptr, ixmlDocument_free);
+        int upnp_err;
+
+        /* if (event_type != UPNP_DISCOVERY_ADVERTISEMENT_ALIVE)
+             RING_DBG("UPnP: CP received a discovery search result"); */
+
+        /* check if we are already in the process of checking this device */
+        std::unique_lock<std::mutex> lock(upnpContext->cpDeviceMutex_);
+        auto it = upnpContext->cpDevices_.find(std::string(d_event->Location));
+
+        if (it == upnpContext->cpDevices_.end()) {
+            upnpContext->cpDevices_.emplace(std::string(d_event->Location));
+            lock.unlock();
+
+            if (d_event->ErrCode != UPNP_E_SUCCESS)
+                RING_WARN("UPnP: Error in discovery event received by the CP: %s",
+                          UpnpGetErrorMessage(d_event->ErrCode));
+
+            /* RING_DBG("UPnP: Control Point received discovery event from device:\n\tid: %s\n\ttype: %s\n\tservice: %s\n\tversion: %s\n\tlocation: %s\n\tOS: %s",
+                     d_event->DeviceId, d_event->DeviceType, d_event->ServiceType, d_event->ServiceVer, d_event->Location, d_event->Os);
+            */
+
+            /* note: this thing will block until success for the system socket timeout
+             *       unless libupnp is compile with '-disable-blocking-tcp-connections'
+             *       in which case it will block for the libupnp specified timeout
+             */
+            IXML_Document* desc_doc_ptr = nullptr;
+            upnp_err = UpnpDownloadXmlDoc( d_event->Location, &desc_doc_ptr);
+            desc_doc.reset(desc_doc_ptr);
+            if ( upnp_err != UPNP_E_SUCCESS ) {
+                RING_WARN("UPnP: Error downloading device description: %s",
+                          UpnpGetErrorMessage(upnp_err));
+            } else {
+                /* TODO: parse device description and add desired devices to relevant lists, etc */
+                upnpContext->parseDevice(desc_doc.get(), d_event);
+            }
+
+            /* finished parsing device; remove it from know devices list,
+             * since next time it could be a new device with same URL
+             * eg: if we switch routers or if a new device with the same IP appears
+             */
+            lock.lock();
+            upnpContext->cpDevices_.erase(d_event->Location);
+            lock.unlock();
+        } else {
+            lock.unlock();
+            /* RING_DBG("UPnP: Control Point is already checking this device"); */
+        }
+    }
+    break;
+
+    case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
+    {
+        struct Upnp_Discovery *d_event = (struct Upnp_Discovery *)event;
+
+        RING_DBG("UPnP: Control Point received ByeBye for device: %s", d_event->DeviceId);
+
+        if (d_event->ErrCode != UPNP_E_SUCCESS)
+            RING_WARN("UPnP: Error in ByeBye received by the CP: %s",
+                      UpnpGetErrorMessage(d_event->ErrCode));
+
+        /* TODO: check if its a device we care about and remove it from the relevant lists */
+    }
+    break;
+
+    case UPNP_EVENT_RECEIVED:
+    {
+        struct Upnp_Event *e_event = (struct Upnp_Event *)event;
+
+        RING_DBG("UPnP: Control Point event received");
+
+        /* TODO: handle event by updating any changed state variables */
+
+    }
+    break;
+
+    case UPNP_EVENT_AUTORENEWAL_FAILED:
+    {
+        RING_WARN("UPnP: Control Point subscription auto-renewal failed");
+    }
+    break;
+
+    case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
+    {
+        RING_DBG("UPnP: Control Point subscription expired");
+    }
+    break;
+
+    case UPNP_EVENT_SUBSCRIBE_COMPLETE:
+        RING_DBG("UPnP: Control Point async subscription complete");
+
+        /* TODO: check if successfull */
+
+        break;
+
+    case UPNP_DISCOVERY_SEARCH_TIMEOUT:
+    {
+        RING_DBG("UPnP: Control Point search timeout");
+        {
+            std::lock_guard<std::mutex> lock(upnpContext->igdSearchMutex_);
+            if (upnpContext->pendingIGDSearchRequests_ > 0)
+                --(upnpContext->pendingIGDSearchRequests_);
+        }
+        upnpContext->igdSearchCondition_.notify_all();
+    }
+    break;
+
+    case UPNP_CONTROL_ACTION_COMPLETE:
+    {
+        struct Upnp_Action_Complete *a_event = (struct Upnp_Action_Complete *)event;
+
+        RING_DBG("UPnP: Control Point async action complete");
+
+        if (a_event->ErrCode != UPNP_E_SUCCESS)
+            RING_WARN("UPnP: Error in action complete event: %s",
+                      UpnpGetErrorMessage(a_event->ErrCode));
+
+        /* TODO: no need for any processing here, just print out results.
+         * Service state table updates are handled by events. */
+    }
+    break;
+
+    case UPNP_CONTROL_GET_VAR_COMPLETE:
+    {
+        struct Upnp_State_Var_Complete *sv_event = (struct Upnp_State_Var_Complete *)event;
+
+        RING_DBG("UPnP: Control Point async get variable complete");
+
+        if (sv_event->ErrCode != UPNP_E_SUCCESS)
+            RING_WARN("UPnP: Error in get variable complete event: %s",
+                      UpnpGetErrorMessage(sv_event->ErrCode));
+
+        /* TODO: update state variables */
+    }
+    break;
+
+    default:
+        RING_WARN("UPnP: unhandled Control Point event");
+        break;
+    }
+
+    return UPNP_E_SUCCESS; /* return value currently ignored by SDK */
+}
+
+static void
+checkResponseError(IXML_Document* doc)
+{
+    if (not doc)
+        return;
+
+    std::string errorCode = get_first_doc_item(doc, "errorCode");
+    if (not errorCode.empty()) {
+        std::string errorDescription = get_first_doc_item(doc, "errorDescription");
+        RING_WARN("UPnP: response contains error: %s : %s",
+                  errorCode.c_str(), errorDescription.c_str());
+    }
+}
+
+bool
+UPnPContext::isIGDConnected(const IGD* igd)
+{
+    bool connected = false;
+    std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free);
+    action.reset(UpnpMakeAction("GetStatusInfo", igd->getServiceType().c_str(), 0, nullptr));
+
+    std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free);
+    IXML_Document* response_ptr = nullptr;
+    int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(),
+                                  igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr);
+    response.reset(response_ptr);
+    checkResponseError(response.get());
+    if( upnp_err != UPNP_E_SUCCESS) {
+        /* TODO: if failed, should we chck if the igd is disconnected? */
+        RING_WARN("UPnP: Failed to get GetStatusInfo from: %s, %s",
+                  igd->getServiceType().c_str(), UpnpGetErrorMessage(upnp_err));
+
+        return false;
+    }
+
+    /* parse response */
+    std::string status = get_first_doc_item(response.get(), "NewConnectionStatus");
+    if (status.compare("Connected") == 0)
+        connected = true;
+
+    /* response should also contain the following elements, but we don't care for now:
+     *  "NewLastConnectionError"
+     *  "NewUptime"
+     */
+    return connected;
+}
+
+IpAddr
+UPnPContext::getExternalIP(const IGD* igd)
+{
+    std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free);
+    action.reset(UpnpMakeAction("GetExternalIPAddress", igd->getServiceType().c_str(), 0, nullptr));
+
+    std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free);
+    IXML_Document* response_ptr = nullptr;
+    int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(),
+                                  igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr);
+    response.reset(response_ptr);
+    checkResponseError(response.get());
+    if( upnp_err != UPNP_E_SUCCESS) {
+        /* TODO: if failed, should we chck if the igd is disconnected? */
+        RING_WARN("UPnP: Failed to get GetExternalIPAddress from: %s, %s",
+                  igd->getServiceType().c_str(), UpnpGetErrorMessage(upnp_err));
+        return {};
+    }
+
+    /* parse response */
+    return {get_first_doc_item(response.get(), "NewExternalIPAddress")};
+}
+
+void
+UPnPContext::removeMappingsByLocalIPAndDescription(const IGD* igd, const std::string& description)
+{
+    /* 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 in function removeMappingsByLocalIPAndDescription()");
+        return;
+    }
+
+    RING_DBG("UPnP: removing all port mappings with description: \"%s\" and local ip: %s",
+             description.c_str(), local_ip.toString().c_str());
+
+    int upnp_err;
+    int entry_idx = 0;
+    bool done = false;
+
+    do {
+        std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free);
+        IXML_Document* action_ptr = nullptr;
+        UpnpAddToAction(&action_ptr, "GetGenericPortMappingEntry", igd->getServiceType().c_str(),
+                        "NewPortMappingIndex", std::to_string(entry_idx).c_str());
+        action.reset(action_ptr);
+
+        std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free);
+        IXML_Document* response_ptr = nullptr;
+        int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(),
+                                      igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr);
+        response.reset(response_ptr);
+        if( not response and upnp_err != UPNP_E_SUCCESS) {
+            /* TODO: if failed, should we chck if the igd is disconnected? */
+            RING_WARN("UPnP: Failed to get GetGenericPortMappingEntry from: %s, %s",
+                      igd->getServiceType().c_str(), UpnpGetErrorMessage(upnp_err));
+            return;
+        }
+
+        /* check if there is an error code */
+        std::string errorCode = get_first_doc_item(response.get(), "errorCode");
+
+        if (errorCode.empty()) {
+            /* no error, prase the rest of the response */
+            std::string desc_actual = get_first_doc_item(response.get(), "NewPortMappingDescription");
+            std::string client_ip = get_first_doc_item(response.get(), "NewInternalClient");
+
+            /* check if same IP and description */
+            if (IpAddr(client_ip) == local_ip and desc_actual.compare(description) == 0) {
+                /* get the rest of the needed parameters */
+                std::string port_internal = get_first_doc_item(response.get(), "NewInternalPort");
+                std::string port_external = get_first_doc_item(response.get(), "NewExternalPort");
+                std::string protocol = get_first_doc_item(response.get(), "NewProtocol");
+
+                RING_DBG("UPnP: deleting entry with matching desciption and ip:\n\t%s %5s->%s:%-5s '%s'",
+                         protocol.c_str(), port_external.c_str(), client_ip.c_str(), port_internal.c_str(), protocol.c_str());
+
+                /* delete entry */
+                if (not deletePortMapping(igd, port_external, protocol)) {
+                    /* failed to delete entry, skip it and try the next one */
+                    ++entry_idx;
+                }
+                /* note: in the case that the entry deletion is successfull, we do not increment the entry
+                 *       idx as the number of entries has decreased by one */
+            } else
+                ++entry_idx;
+
+        } else if (errorCode.compare(ARRAY_IDX_INVALID_STR) == 0
+                   or errorCode.compare(INVALID_ARGS_STR) == 0) {
+            /* 713 means there are no more entires, but some routers will return 402 instead */
+            done = true;
+        } else {
+            std::string errorDescription = get_first_doc_item(response.get(), "errorDescription");
+            RING_WARN("UPnP: GetGenericPortMappingEntry returned with error: %s: %s",
+                      errorCode.c_str(), errorDescription.c_str());
+            done = true;
+        }
+    } while(not done);
+}
+
+bool
+UPnPContext::deletePortMapping(const IGD* igd, const std::string& port_external, const std::string& protocol)
+{
+    std::string action_name{"DeletePortMapping"};
+    std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free);
+    IXML_Document* action_ptr = nullptr;
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+                    "NewRemoteHost", "");
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+                    "NewExternalPort", port_external.c_str());
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+                    "NewProtocol", protocol.c_str());
+    action.reset(action_ptr);
+
+    std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free);
+    IXML_Document* response_ptr = nullptr;
+    int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(),
+                                  igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr);
+    response.reset(response_ptr);
+    if( upnp_err != UPNP_E_SUCCESS) {
+        /* TODO: if failed, should we check if the igd is disconnected? */
+        RING_WARN("UPnP: Failed to get %s from: %s, %s",
+                  action_name.c_str(), igd->getServiceType().c_str(), UpnpGetErrorMessage(upnp_err));
+        return false;
+    }
+    /* check if there is an error code */
+    std::string errorCode = get_first_doc_item(response.get(), "errorCode");
+    if (not errorCode.empty()) {
+        std::string errorDescription = get_first_doc_item(response.get(), "errorDescription");
+        RING_WARN("UPnP: %s returned with error: %s: %s",
+                  action_name.c_str(), errorCode.c_str(), errorDescription.c_str());
+        return false;
+    }
+    return true;
+}
+
+bool
+UPnPContext::addPortMapping(const IGD* igd, const Mapping& mapping, int* error_code)
+{
+    *error_code = UPNP_E_SUCCESS;
+
+    std::string action_name{"AddPortMapping"};
+    std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> action(nullptr, ixmlDocument_free);
+    IXML_Document* action_ptr = nullptr;
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+                    "NewRemoteHost", "");
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+                    "NewExternalPort", mapping.getPortExternalStr().c_str());
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+                    "NewProtocol", mapping.getTypeStr().c_str());
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+                    "NewInternalPort", mapping.getPortInternalStr().c_str());
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+                    "NewInternalClient", mapping.getLocalIp().toString().c_str());
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+                    "NewEnabled", "1");
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+                    "NewPortMappingDescription", mapping.getDescription().c_str());
+    /* for now assume lease duration is always infinite */
+    UpnpAddToAction(&action_ptr, action_name.c_str(), igd->getServiceType().c_str(),
+                    "NewLeaseDuration", "0");
+    action.reset(action_ptr);
+
+    std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&> response(nullptr, ixmlDocument_free);
+    IXML_Document* response_ptr = nullptr;
+    int upnp_err = UpnpSendAction(ctrlptHandle_, igd->getControlURL().c_str(),
+                                  igd->getServiceType().c_str(), nullptr, action.get(), &response_ptr);
+    response.reset(response_ptr);
+    if( not response and upnp_err != UPNP_E_SUCCESS) {
+        /* TODO: if failed, should we chck if the igd is disconnected? */
+        RING_WARN("UPnP: Failed to get GetGenericPortMappingEntry from: %s, %s",
+                  igd->getServiceType().c_str(), UpnpGetErrorMessage(upnp_err));
+        *error_code = upnp_err;
+        return false;
+    }
+
+    /* check if there is an error code */
+    std::string errorCode = get_first_doc_item(response.get(), "errorCode");
+    if (not errorCode.empty()) {
+        std::string errorDescription = get_first_doc_item(response.get(), "errorDescription");
+        RING_WARN("UPnP: %s returned with error: %s: %s",
+                  action_name.c_str(), errorCode.c_str(), errorDescription.c_str());
+        *error_code = std::stoi(errorCode);
+        return false;
+    }
+    return true;
+}
+
+#endif /* HAVE_LIBUPNP */
+
+}} // namespace ring::upnp
diff --git a/daemon/src/upnp/upnp_context.h b/daemon/src/upnp/upnp_context.h
new file mode 100644
index 0000000000000000000000000000000000000000..cff06827cfa24c3431ea745273526a98d6ad76e8
--- /dev/null
+++ b/daemon/src/upnp/upnp_context.h
@@ -0,0 +1,236 @@
+/*
+ *  Copyright (C) 2004-2015 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_CONTEXT_H_
+#define UPNP_CONTEXT_H_
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <set>
+#include <map>
+#include <mutex>
+#include <memory>
+#include <condition_variable>
+
+#if HAVE_LIBUPNP
+#include <upnp/upnp.h>
+#include <upnp/upnptools.h>
+#endif
+
+#include "noncopyable.h"
+#include "upnp_igd.h"
+
+namespace ring {
+class IpAddr;
+}
+
+namespace ring { namespace upnp {
+
+class UPnPContext {
+
+public:
+
+#if HAVE_LIBUPNP
+
+    /* search timeout in seconds */
+    constexpr static int SEARCH_TIMEOUT_SEC = 30;
+
+    UPnPContext();
+    ~UPnPContext();
+
+    /**
+     * returns 'true' if there is at least one valid (connected) IGD
+     * note: this function will wait until an IGD has been found or SEARCH_TIMEOUT_SEC
+     *       have expired; the timeout starts when the context is created as we start
+     *       searching for IGDs immediately
+     */
+    bool hasValidIGD();
+
+    /**
+     * 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 the external and internal ports are the same
+     *
+     * returns a valid mapping on success and an invalid mapping on failure
+     *
+     * note: this function will call hasValidIGD and wait upto SEARCH_TIMEOUT_SEC
+     */
+    Mapping addAnyMapping(uint16_t port_desired,
+                          uint16_t port_local,
+                          PortType type,
+                          bool use_same_port,
+                          bool unique);
+
+    /**
+     * tries to remove the given mapping
+     *
+     * note: this function will call hasValidIGD and wait upto SEARCH_TIMEOUT_SEC
+     */
+    void removeMapping(const Mapping& mapping);
+
+    IpAddr getExternalIP();
+
+#else
+    /* use default constructor and destructor */
+    UPnPContext() = default;
+    ~UPnPContext() = default;
+#endif
+
+    bool isInitialized() const { return initialized_; };
+
+private:
+    NON_COPYABLE(UPnPContext);
+
+    bool initialized_ {false};
+
+#if HAVE_LIBUPNP
+
+    /**
+     * UPnP devices typically send out several discovery
+     * packets at the same time. libupnp creates a separate event
+     * for each discovery packet which is processed in the threadpool,
+     * even if the multiple discovery packets are received from the
+     * same IP at the same time. In order to prevent trying
+     * to download and parse the device description from the
+     * same location in multiple threads at the same time, we
+     * keep track from which URL(s) we are in the process of downloading
+     * and parsing the device description in this set.
+     *
+     * The main purspose of this is to prevent blocking multiple
+     * threads when trying to download the description from an
+     * unresponsive device (the timeout can be several seconds)
+     *
+     * The mutex is to access the set in a thread safe manner
+     */
+    std::set<std::string> cpDevices_;
+    std::mutex cpDeviceMutex_;
+
+    /**
+     * control and device handles;
+     * set by the SDK once each is registered
+     */
+    UpnpClient_Handle ctrlptHandle_ {-1};
+    UpnpDevice_Handle deviceHandle_ {-1};
+
+    /**
+     * keep track if we've successfully registered
+     * a client and/ore device
+     */
+    bool clientRegistered_ {false};
+    bool deviceRegistered_ {false};
+
+    /**
+     * map of valid IGDs - IGDs which have the correct services and are connected
+     * to some external network (have an external IP)
+     *
+     * the UDN string is used to uniquely identify the IGD
+     *
+     * the mutex is used to access these lists and IGDs in a thread-safe manner
+     */
+    std::map<std::string, std::unique_ptr<IGD>> validIGDs_;
+    std::mutex validIGDMutex_;
+
+    /**
+     * chooses the IGD to use (currently selects the first one in the map)
+     * assumes you already have a lock on igd_mutex_
+     */
+    IGD* chooseIGD_unlocked();
+
+    /* sends out async search for IGD */
+    void searchForIGD();
+
+    /* vars to sync search timeout */
+    unsigned pendingIGDSearchRequests_ {0};
+    std::mutex igdSearchMutex_;
+    std::condition_variable igdSearchCondition_;
+
+    /**
+     * callback function for the UPnP client (control point)
+     * all UPnP events received by the client are processed here
+     */
+    friend int cp_callback(Upnp_EventType, void*, void*);
+
+    /**
+     * Parses the device description and adds desired devices to
+     * relevant lists
+     */
+    void parseDevice(IXML_Document* doc, const Upnp_Discovery* d_event);
+
+    void parseIGD(IXML_Document* doc, const Upnp_Discovery* d_event);
+
+    /* tries to add mapping, assumes you alreayd have lock on igd_mutex_ */
+    Mapping addMapping(IGD* igd,
+                       uint16_t port_external,
+                       uint16_t port_internal,
+                       PortType type,
+                       int *upnp_error);
+
+    uint16_t chooseRandomPort(const IGD* igd, PortType type);
+
+    /* these functions directly create UPnP actions
+     * and make synchronous UPnP control point calls
+     * they assume you have a lock on the igd_mutex_ */
+    bool isIGDConnected(const IGD* igd);
+
+    IpAddr getExternalIP(const IGD* igd);
+
+    void removeMappingsByLocalIPAndDescription(const IGD* igd,
+                                               const std::string& description);
+
+    bool deletePortMapping(const IGD* igd,
+                           const std::string& port_external,
+                           const std::string& protocol);
+
+    bool addPortMapping(const IGD* igd,
+                        const Mapping& mapping,
+                        int* error_code);
+
+#endif /* HAVE_LIBUPNP */
+
+};
+
+/**
+ * This should be used to get a UPnPContext.
+ * It only makes sense to have one unless you have separate
+ * contexts for multiple internet interfaces, which is not currently
+ * supported.
+ */
+std::shared_ptr<UPnPContext> getUPnPContext();
+
+}} // namespace ring::upnp
+
+#endif /* UPNP_CONTEXT_H_ */
\ No newline at end of file
diff --git a/daemon/src/upnp/upnp_control.cpp b/daemon/src/upnp/upnp_control.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e21d55c87603148c287fd3a9237b647ddf9b32a7
--- /dev/null
+++ b/daemon/src/upnp/upnp_control.cpp
@@ -0,0 +1,162 @@
+/*
+ *  Copyright (C) 2004-2015 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_control.h"
+
+#include <memory>
+
+#include "logger.h"
+#include "ip_utils.h"
+#include "upnp_context.h"
+#include "upnp_igd.h"
+
+namespace ring { namespace upnp {
+
+Controller::Controller()
+    : upnpContext_(getUPnPContext())
+{
+    if (not upnpContext_->isInitialized())
+        RING_WARN("UPnP: context is not initialized");
+}
+
+Controller::~Controller()
+{
+    /* remove all mappings */
+    removeMappings();
+}
+
+/**
+ * Return whether or not this controller has a valid IGD,
+ * if 'flase' then all requests will fail
+ */
+bool
+Controller::hasValidIGD()
+{
+#if HAVE_LIBUPNP
+    return upnpContext_->hasValidIGD();
+#endif
+    return false;
+}
+
+/**
+ * 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
+Controller::addAnyMapping(uint16_t port_desired,
+                          uint16_t port_local,
+                          PortType type,
+                          bool use_same_port,
+                          bool unique,
+                          uint16_t *port_used)
+{
+#if HAVE_LIBUPNP
+    Mapping mapping = upnpContext_->addAnyMapping(port_desired, port_local, type,
+                                                  use_same_port, unique);
+    if (mapping) {
+        if (port_used)
+            *port_used = mapping.getPortExternal();
+
+        /* add to map */
+        auto& instanceMappings = type == PortType::UDP ? udpMappings_ : tcpMappings_;
+        instanceMappings.emplace(*port_used, std::move(mapping));
+        return true;
+    } else
+        return false;
+#endif
+    return false;
+}
+
+/**
+ * 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);
+}
+
+/**
+ * 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_LIBUPNP
+    auto& instanceMappings = type == PortType::UDP ? udpMappings_ : tcpMappings_;
+    for (auto iter = instanceMappings.begin(); iter != instanceMappings.end(); ){
+        auto& mapping = iter->second;
+        upnpContext_->removeMapping(mapping);
+        iter = instanceMappings.erase(iter);
+    }
+#endif
+}
+
+/**
+ * removes all mappings added by this instance
+ */
+void
+Controller::removeMappings()
+{
+#if HAVE_LIBUPNP
+    removeMappings(PortType::UDP);
+    removeMappings(PortType::TCP);
+#endif
+}
+
+/**
+ * tries to get the external ip of the router
+ */
+IpAddr
+Controller::getExternalIP()
+{
+#if HAVE_LIBUPNP
+    return upnpContext_->getExternalIP();
+#else
+    /* return empty address */
+    return {};
+#endif
+}
+
+}} // namespace ring::upnp
diff --git a/daemon/src/upnp/upnp_control.h b/daemon/src/upnp/upnp_control.h
new file mode 100644
index 0000000000000000000000000000000000000000..e2c1770322d2dc7b560e3be863f2a02a670af81c
--- /dev/null
+++ b/daemon/src/upnp/upnp_control.h
@@ -0,0 +1,130 @@
+/*
+ *  Copyright (C) 2004-2015 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 <memory>
+
+#include "noncopyable.h"
+#include "upnp_igd.h"
+
+namespace ring {
+class IpAddr;
+}
+
+namespace ring { namespace upnp {
+
+class UPnPContext;
+
+class Controller {
+public:
+    typedef std::function<void(bool)> IGDFoundCallback;
+
+    /* constructor */
+    Controller();
+    /* destructor */
+    ~Controller();
+
+    /**
+     * Return whether or not this controller has a valid IGD,
+     * if 'flase' then all requests will fail
+     */
+    bool hasValidIGD();
+
+    /**
+     * like hasValidIGD, but calls the given callback when the IGD is found
+     * or when the search times out without finding one
+     */
+    // void waitForValildIGD(IGDFoundCallback cb);
+
+    /**
+     * 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();
+
+    /**
+     * tries to get the external ip of the IGD (router)
+     */
+    IpAddr getExternalIP();
+
+private:
+
+    /**
+     * All UPnP commands require an initialized upnpContext
+     */
+    std::shared_ptr<UPnPContext> upnpContext_;
+
+    /**
+     * 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
+     */
+    PortMapLocal udpMappings_;
+    PortMapLocal tcpMappings_;
+
+    /**
+     * Try to remove all mappings of the given type
+     */
+    void removeMappings(PortType type);
+};
+
+}} // namespace ring::upnp
+
+#endif /* UPNP_H_ */
diff --git a/daemon/src/upnp/upnp_igd.cpp b/daemon/src/upnp/upnp_igd.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8a9f482f5567da97bdada7d6bcaaa0f26c755087
--- /dev/null
+++ b/daemon/src/upnp/upnp_igd.cpp
@@ -0,0 +1,78 @@
+/*
+ *  Copyright (C) 2004-2015 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.
+ */
+
+#include "upnp_igd.h"
+
+namespace ring { namespace upnp {
+
+/* move constructor and operator */
+Mapping::Mapping(Mapping&& other)
+    : local_ip_(std::move(other.local_ip_))
+    , port_external_(other.port_external_)
+    , port_internal_(other.port_internal_)
+    , type_(other.type_)
+    , description_(std::move(other.description_))
+{
+    other.port_external_ = 0;
+    other.port_internal_ = 0;
+}
+
+Mapping& Mapping::operator=(Mapping&& other)
+{
+    if (this != &other) {
+        local_ip_ = std::move(other.local_ip_);
+        port_external_ = other.port_external_;
+        other.port_external_ = 0;
+        port_internal_ = other.port_internal_;
+        other.port_internal_ = 0;
+        type_ = other.type_;
+        description_ = std::move(other.description_);
+    }
+    return *this;
+}
+
+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);
+}
+
+}} // namespace ring::upnp
\ No newline at end of file
diff --git a/daemon/src/upnp/upnp_igd.h b/daemon/src/upnp/upnp_igd.h
new file mode 100644
index 0000000000000000000000000000000000000000..938b56ae30d7488cdf0212bdf28388871116c4cf
--- /dev/null
+++ b/daemon/src/upnp/upnp_igd.h
@@ -0,0 +1,195 @@
+/*
+ *  Copyright (C) 2004-2015 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_IGD_H_
+#define UPNP_IGD_H_
+
+#include <string>
+#include <map>
+
+#include "noncopyable.h"
+#include "ip_utils.h"
+
+namespace ring { namespace upnp {
+
+enum class PortType {UDP,TCP};
+
+/* defines a UPnP port mapping */
+class Mapping {
+public:
+    constexpr static const char * 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;
+
+    Mapping(
+        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)
+    : local_ip_(local_ip)
+    , port_external_(port_external)
+    , port_internal_(port_internal)
+    , type_(type)
+    , description_(description)
+    {};
+
+    /* move constructor and operator */
+    Mapping(Mapping&&);
+    Mapping& operator=(Mapping&&);
+
+    ~Mapping() = default;
+
+    friend bool operator== (Mapping &cRedir1, Mapping &cRedir2);
+    friend bool operator!= (Mapping &cRedir1, Mapping &cRedir2);
+
+    const IpAddr& getLocalIp()         const { return local_ip_; };
+    uint16_t      getPortExternal()    const { return port_external_; };
+    std::string   getPortExternalStr() const { return std::to_string(port_external_); };
+    uint16_t      getPortInternal()    const { return port_internal_; };
+    std::string   getPortInternalStr() const { return std::to_string(port_internal_); };
+    PortType      getType()            const { return type_; };
+    std::string   getTypeStr()         const { return type_ == PortType::UDP ? "UDP" : "TCP"; }
+    std::string   getDescription()     const { return description_; };
+
+    std::string toString() const {
+        return local_ip_.toString() + ", " + getPortExternalStr() + ":" + getPortInternalStr() + ", " + getTypeStr();
+    };
+
+    bool isValid() const {
+        return port_external_ == 0 or port_internal_ == 0 ? false : true;
+    };
+
+    inline operator bool() const {
+        return isValid();
+    }
+
+private:
+    NON_COPYABLE(Mapping);
+
+protected:
+    IpAddr local_ip_; /* the destination of the mapping */
+    uint16_t port_external_;
+    uint16_t port_internal_;
+    PortType type_; /* UPD or TCP */
+    std::string description_;
+};
+
+/**
+ * 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.getLocalIp()
+        , mapping.getPortExternal()
+        , mapping.getPortInternal()
+        , mapping.getType()
+        , mapping.getDescription())
+        , users(users)
+    {};
+};
+
+/* subclasses to make it easier to differentiate and cast maps of port mappings */
+class PortMapLocal : public std::map<uint16_t, Mapping> {};
+class PortMapGlobal : public std::map<uint16_t, GlobalMapping> {};
+
+/* defines a UPnP capable Internet Gateway Device (a router) */
+class IGD {
+public:
+
+    /* external IP of IGD; can change */
+    IpAddr publicIp;
+
+    /* port mappings associated with this IGD */
+    PortMapGlobal udpMappings;
+    PortMapGlobal tcpMappings;
+
+    /* constructors */
+    IGD() {};
+    IGD(std::string UDN,
+        std::string baseURL,
+        std::string friendlyName,
+        std::string serviceType,
+        std::string serviceId,
+        std::string controlURL,
+        std::string eventSubURL)
+        : UDN_(UDN)
+        , baseURL_(baseURL)
+        , friendlyName_(friendlyName)
+        , serviceType_(serviceType)
+        , serviceId_(serviceId)
+        , controlURL_(controlURL)
+        , eventSubURL_(eventSubURL)
+        {};
+
+    /* move constructor and operator */
+    IGD(IGD&&) = default;
+    IGD& operator=(IGD&&) = default;
+
+    ~IGD() = default;
+
+    const std::string& getUDN() const { return UDN_; };
+    const std::string& getBaseURL() const { return baseURL_; };
+    const std::string& getFriendlyName() const { return friendlyName_; };
+    const std::string& getServiceType() const { return serviceType_; };
+    const std::string& getServiceId() const { return serviceId_; };
+    const std::string& getControlURL() const { return controlURL_; };
+    const std::string& getEventSubURL() const { return eventSubURL_; };
+
+private:
+    NON_COPYABLE(IGD);
+
+    /* root device info */
+    std::string UDN_ {}; /* used to uniquely identify this UPnP device */
+    std::string baseURL_ {};
+    std::string friendlyName_ {};
+
+    /* port forwarding service info */
+    std::string serviceType_ {};
+    std::string serviceId_ {};
+    std::string controlURL_ {};
+    std::string eventSubURL_ {};
+
+};
+
+}} // namespace ring::upnp
+
+#endif /* UPNP_IGD_H_ */
\ No newline at end of file