diff --git a/aconfigure b/aconfigure index 8142df101c..ff16f00395 100755 --- a/aconfigure +++ b/aconfigure @@ -827,6 +827,8 @@ enable_kqueue enable_epoll enable_shared enable_pjsua2 +with_upnp +enable_upnp with_external_speex with_external_gsm with_external_srtp @@ -1526,6 +1528,7 @@ Optional Features: --enable-shared Build shared libraries --disable-pjsua2 Exclude pjsua2 library and application from the build + --disable-upnp Disable UPnP (default: not disabled) --disable-resample Disable resampling implementations --disable-sound Exclude sound (i.e. use null sound) --disable-video Disable video feature @@ -1575,6 +1578,7 @@ Optional Features: Optional Packages: --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] --without-PACKAGE do not use PACKAGE (same as --with-PACKAGE=no) + --with-upnp=DIR Specify alternate libupnp prefix --with-external-speex Use external Speex development files, not the one in "third_party" directory. When this option is set, make sure that Speex is accessible to use (hint: use @@ -6584,6 +6588,93 @@ case $target in esac + + +# Check whether --with-upnp was given. +if test ${with_upnp+y} +then : + withval=$with_upnp; +else $as_nop + with_upnp=no + +fi + + +if test "x$ac_cross_compile" != "x" -a "x$with_upnp" = "xno"; then + enable_upnp=no +fi + +# Check whether --enable-upnp was given. +if test ${enable_upnp+y} +then : + enableval=$enable_upnp; + if test "$enable_upnp" = "no"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Checking if UPnP is disabled... yes" >&5 +printf "%s\n" "Checking if UPnP is disabled... yes" >&6; } + fi + +else $as_nop + + if test "x$with_upnp" != "xno" -a "x$with_upnp" != "x"; then + UPNP_PREFIX=$with_upnp + UPNP_CFLAGS="-I$UPNP_PREFIX/upnp/inc -I$UPNP_PREFIX/ixml/inc" + UPNP_LDFLAGS="-L$UPNP_PREFIX/upnp/.libs -L$UPNP_PREFIX/imxl/.libs" + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Using UPnP prefix... $with_upnp" >&5 +printf "%s\n" "Using UPnP prefix... $with_upnp" >&6; } + else + UPNP_CFLAGS="" + UPNP_LDFLAGS="" + fi + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking UPnP usability" >&5 +printf %s "checking UPnP usability... " >&6; } + + UPNP_LIBS="-lupnp -lixml" + + SAVED_LIBS="$LIBS" + SAVED_LDFLAGS="$LDFLAGS" + SAVED_CFLAGS="$CFLAGS" + + LIBS="$UPNP_LIBS $LIBS" + LDFLAGS="$UPNP_LDFLAGS $LDFLAGS" + CFLAGS="$UPNP_CFLAGS $CFLAGS" + + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include +int +main (void) +{ +UpnpInit2(NULL, 0); + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO" +then : + CFLAGS="$CFLAGS -DPJLIB_UTIL_HAS_UPNP=1 $UPNP_CFLAGS" + LDFLAGS="$LDFLAGS $UPNP_LDFLAGS $UPNP_LIBS" + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } + +else $as_nop + + LIBS="$SAVED_LIBS" + LDFLAGS="$SAVED_LDFLAGS" + CFLAGS="$SAVED_CFLAGS" + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } + +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext + + +fi + + + ac_external_speex=0 diff --git a/aconfigure.ac b/aconfigure.ac index 010966ab6e..e805ec1df2 100644 --- a/aconfigure.ac +++ b/aconfigure.ac @@ -552,6 +552,72 @@ case $target in ;; esac +dnl ########################################## +dnl # PJLIB-UTIL +dnl # + + +dnl # UPnP alt prefix +AC_ARG_WITH(upnp, + AS_HELP_STRING([--with-upnp=DIR], + [Specify alternate libupnp prefix]), + [], + [with_upnp=no] + ) + +dnl # Do not use default libupnp installation if we are cross-compiling +if test "x$ac_cross_compile" != "x" -a "x$with_upnp" = "xno"; then + enable_upnp=no +fi + +dnl # UPnP +AC_ARG_ENABLE(upnp, + AS_HELP_STRING([--disable-upnp], + [Disable UPnP (default: not disabled)]), + [ + if test "$enable_upnp" = "no"; then + AC_MSG_RESULT([Checking if UPnP is disabled... yes]) + fi + ], + [ + if test "x$with_upnp" != "xno" -a "x$with_upnp" != "x"; then + UPNP_PREFIX=$with_upnp + UPNP_CFLAGS="-I$UPNP_PREFIX/upnp/inc -I$UPNP_PREFIX/ixml/inc" + UPNP_LDFLAGS="-L$UPNP_PREFIX/upnp/.libs -L$UPNP_PREFIX/imxl/.libs" + AC_MSG_RESULT([Using UPnP prefix... $with_upnp]) + else + UPNP_CFLAGS="" + UPNP_LDFLAGS="" + fi + + AC_MSG_CHECKING([UPnP usability]) + + UPNP_LIBS="-lupnp -lixml" + + SAVED_LIBS="$LIBS" + SAVED_LDFLAGS="$LDFLAGS" + SAVED_CFLAGS="$CFLAGS" + + LIBS="$UPNP_LIBS $LIBS" + LDFLAGS="$UPNP_LDFLAGS $LDFLAGS" + CFLAGS="$UPNP_CFLAGS $CFLAGS" + + AC_LINK_IFELSE([AC_LANG_PROGRAM([[#include ]], + [UpnpInit2(NULL, 0);] + )], + [ CFLAGS="$CFLAGS -DPJLIB_UTIL_HAS_UPNP=1 $UPNP_CFLAGS" + LDFLAGS="$LDFLAGS $UPNP_LDFLAGS $UPNP_LIBS" + AC_MSG_RESULT(yes) + ], + [ + LIBS="$SAVED_LIBS" + LDFLAGS="$SAVED_LDFLAGS" + CFLAGS="$SAVED_CFLAGS" + AC_MSG_RESULT(no) + ]) + + ]) + dnl ########################################## dnl # dnl # PJMEDIA diff --git a/pjlib-util/build/Makefile b/pjlib-util/build/Makefile index 0fa6457901..5de43f13c0 100644 --- a/pjlib-util/build/Makefile +++ b/pjlib-util/build/Makefile @@ -40,7 +40,7 @@ export PJLIB_UTIL_OBJS += $(OS_OBJS) $(M_OBJS) $(CC_OBJS) $(HOST_OBJS) \ dns_dump.o dns_server.o getopt.o hmac_md5.o hmac_sha1.o \ http_client.o json.o md5.o pcap.o resolver.o scanner.o sha1.o \ srv_resolver.o string.o stun_simple.o \ - stun_simple_client.o xml.o + stun_simple_client.o upnp.o xml.o export PJLIB_UTIL_CFLAGS += $(_CFLAGS) export PJLIB_UTIL_CXXFLAGS += $(_CXXFLAGS) export PJLIB_UTIL_LDFLAGS += $(PJLIB_LDLIB) $(_LDFLAGS) diff --git a/pjlib-util/build/pjlib_util.vcxproj b/pjlib-util/build/pjlib_util.vcxproj index 88bc7cb907..a7fa9e95b4 100644 --- a/pjlib-util/build/pjlib_util.vcxproj +++ b/pjlib-util/build/pjlib_util.vcxproj @@ -750,6 +750,7 @@ + @@ -780,6 +781,7 @@ + diff --git a/pjlib-util/build/pjlib_util.vcxproj.filters b/pjlib-util/build/pjlib_util.vcxproj.filters index 52fd9c12b8..13b6435613 100644 --- a/pjlib-util/build/pjlib_util.vcxproj.filters +++ b/pjlib-util/build/pjlib_util.vcxproj.filters @@ -92,6 +92,9 @@ Source Files + + Source Files + @@ -178,5 +181,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/pjlib-util/include/pjlib-util.h b/pjlib-util/include/pjlib-util.h index 334e73c96b..4001d22fa1 100644 --- a/pjlib-util/include/pjlib-util.h +++ b/pjlib-util/include/pjlib-util.h @@ -61,6 +61,9 @@ /* Old STUN */ #include +/* UPnP */ +#include + /* PCAP */ #include diff --git a/pjlib-util/include/pjlib-util/config.h b/pjlib-util/include/pjlib-util/config.h index 13aa6a3aa8..3fc7bf9dd7 100644 --- a/pjlib-util/include/pjlib-util/config.h +++ b/pjlib-util/include/pjlib-util/config.h @@ -238,6 +238,18 @@ #endif +/* ************************************************************************** + * UPnP + */ + +/* Default duration for searching UPnP Internet Gateway Devices (in seconds). + * Default: 5 seconds + */ +#ifndef PJ_UPNP_DEFAULT_SEARCH_TIME +# define PJ_UPNP_DEFAULT_SEARCH_TIME 5 +#endif + + /* ************************************************************************** * ENCRYPTION */ diff --git a/pjlib-util/include/pjlib-util/upnp.h b/pjlib-util/include/pjlib-util/upnp.h new file mode 100644 index 0000000000..322c4ab1ad --- /dev/null +++ b/pjlib-util/include/pjlib-util/upnp.h @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2022 Teluu Inc. (http://www.teluu.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 2 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +#ifndef __PJ_UPNP_H__ +#define __PJ_UPNP_H__ + +/** + * @file upnp.h + * @brief UPnP client. + */ + +#include + + +PJ_BEGIN_DECL + +/** + * @defgroup PJLIB_UTIL_UPNP Simple UPnP Client + * @ingroup PJ_PROTOCOLS + * @brief A simple UPnP client implementation. + * @{ + * + * This is a simple implementation of UPnP client. Its main function + * is to request a port mapping from an Internet Gateway Device (IGD), + * which will redirect communication received on a specified external + * port to a local socket. + */ + +/* + * This structre describes the parameter to initialize UPnP. + */ +typedef struct pj_upnp_init_param +{ + /** + * The pool factory where memory will be allocated from. + */ + pj_pool_factory *factory; + + /** + * The interface name to use for all UPnP operations. + * + * If NULL, the library will use the first suitable interface found. + */ + const char *if_name; + + /** + * The port number to use for all UPnP operations. + * + * If 0, the library will pick an arbitrary free port. + */ + unsigned port; + + /** + * The time duration to search for IGD devices (in seconds). + * + * If 0, the library will use PJ_UPNP_DEFAULT_SEARCH_TIME. + */ + int search_time; + + /** + * The callback to notify application when the initialization + * has completed. + * + * @param status The initialization status. + */ + void (*upnp_cb)(pj_status_t status); + +} pj_upnp_init_param; + + + +/** + * Initialize UPnP library and initiate the search for valid Internet + * Gateway Devices (IGD) in the network. + * + * @param param The UPnP initialization parameter. + * + * @return PJ_SUCCESS on success, or the appropriate error + * status. + */ +PJ_DECL(pj_status_t) pj_upnp_init(const pj_upnp_init_param *param); + + +/** + * Deinitialize UPnP library. + * + * @return PJ_SUCCESS on success, or the appropriate error + * status. + */ +PJ_DECL(pj_status_t) pj_upnp_deinit(); + + +/** + * This is the main function to request a port mapping. If successful, + * the Internet Gateway Device will redirect communication received on + * the specified external ports to the local sockets. + * + * @param sock_cnt Number of sockets in the socket array. + * @param sock Array of local UDP sockets that will be mapped. + * @param ext_port (Optional) Array of external port numbers. If NULL, + * the external port numbers requested will be identical + * to the sockets' local port numbers. + * @param mapped_addr Array to receive the mapped public addresses and + * ports of the local UDP sockets, when the function + * returns PJ_SUCCESS. + * + * @return PJ_SUCCESS on success, or the appropriate error + * status. + */ +PJ_DECL(pj_status_t)pj_upnp_add_port_mapping(unsigned sock_cnt, + const pj_sock_t sock[], + unsigned ext_port[], + pj_sockaddr mapped_addr[]); + + +/** + * Send request to delete a port mapping. + * + * @param mapped_addr The public address and external port mapping to + * be deleted. + * + * @return PJ_SUCCESS on success, or the appropriate error + * status. + */ +PJ_DECL(pj_status_t)pj_upnp_del_port_mapping(const pj_sockaddr *mapped_addr); + + +PJ_END_DECL + +/** + * @} + */ + +#endif /* __PJ_UPNP_H__ */ diff --git a/pjlib-util/src/pjlib-util/upnp.c b/pjlib-util/src/pjlib-util/upnp.c new file mode 100644 index 0000000000..2c61e4f8e0 --- /dev/null +++ b/pjlib-util/src/pjlib-util/upnp.c @@ -0,0 +1,917 @@ +/* + * Copyright (C) 2022 Teluu Inc. (http://www.teluu.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 2 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(PJLIB_UTIL_HAS_UPNP) && (PJLIB_UTIL_HAS_UPNP != 0) + +#include +#include +#include + +#define THIS_FILE "upnp.c" + +#define TRACE_(...) // PJ_LOG(6, (THIS_FILE, ##__VA_ARGS__)) + +/* Set to 1 to enable UPnP native logging */ +#define ENABLE_LOG 0 + +/* Maximum number of devices. */ +#define MAX_DEVS 16 + + +/* UPnP device descriptions. */ +static const char* UPNP_ROOT_DEVICE = "upnp:rootdevice"; +static const char* UPNP_IGD_DEVICE = + "urn:schemas-upnp-org:device:InternetGatewayDevice:1"; +static const char* UPNP_WANIP_SERVICE = + "urn:schemas-upnp-org:service:WANIPConnection:1"; +static const char* UPNP_WANPPP_SERVICE = + "urn:schemas-upnp-org:service:WANPPPConnection:1"; + + +/* Structure for IGD device. */ +struct igd +{ + pj_str_t dev_id; + pj_str_t url; + pj_str_t service_type; + pj_str_t control_url; + pj_str_t public_ip; + pj_sockaddr public_ip_addr; + + pj_bool_t valid; + pj_bool_t alive; +}; + +/* UPnP manager. */ +static struct upnp +{ + unsigned initialized; + pj_pool_t *pool; + pj_thread_desc thread_desc; + pj_thread_t *thread; + pj_mutex_t *mutex; + int search_cnt; + pj_status_t status; + + unsigned igd_cnt; + struct igd igd_devs[20]; + int primary_igd_idx; + + UpnpClient_Handle client_hnd; + void (*upnp_cb)(pj_status_t status); +} upnp_mgr; + + +/* Get the value of the node. */ +static const char * get_node_value(IXML_Node *node) +{ + const char *ret = NULL; + if (node) { + IXML_Node* child = ixmlNode_getFirstChild(node); + if (child) + ret = ixmlNode_getNodeValue(child); + } + return ret; +} + +/* Get the value of the first element in the doc with the specified name. */ +static const char * doc_get_elmt_value(IXML_Document *doc, const char *name) +{ + const char *ret = NULL; + IXML_NodeList *node_list = ixmlDocument_getElementsByTagName(doc, name); + if (node_list) { + ret = get_node_value(ixmlNodeList_item(node_list, 0)); + ixmlNodeList_free(node_list); + } + return ret; +} + +/* Get the value of the first element with the specified name. */ +static const char * elmt_get_elmt_value(IXML_Element *elmt, const char *name) +{ + const char *ret = NULL; + IXML_NodeList *node_list = ixmlElement_getElementsByTagName(elmt, name); + if (node_list) { + ret = get_node_value(ixmlNodeList_item(node_list, 0)); + ixmlNodeList_free(node_list); + } + return ret; +} + +/* Check if response contains errorCode. */ +static const char * check_error_response(IXML_Document *doc) +{ + const char *error_code = doc_get_elmt_value(doc, "errorCode"); + + if (error_code) { + const char *error_desc = doc_get_elmt_value(doc, "errorDescription"); + + PJ_LOG(3, (THIS_FILE, "Response error code: %s (%s)", + error_code, error_desc)); + } + + return error_code; +} + +/* Query the external IP of the IGD. */ +static const char *action_get_external_ip(struct igd *igd) +{ + static const char* action_name = "GetExternalIPAddress"; + IXML_Document *action = NULL; + IXML_Document *response = NULL; + const char *public_ip = NULL; + int upnp_err; + + /* Create action XML. */ + action = UpnpMakeAction(action_name, igd->service_type.ptr, 0, NULL); + if (!action) { + PJ_LOG(3, (THIS_FILE, "Failed to make GetExternalIPAddress action")); + return NULL; + } + + /* Send the action XML. */ + upnp_err = UpnpSendAction(upnp_mgr.client_hnd, igd->control_url.ptr, + igd->service_type.ptr, NULL, action, &response); + if (upnp_err != UPNP_E_SUCCESS || !response) { + PJ_LOG(3, (THIS_FILE, "Failed to send GetExternalIPAddress action: %s", + UpnpGetErrorMessage(upnp_err))); + goto on_error; + } + + if (check_error_response(response)) + goto on_error; + + /* Get the external IP address from the response. */ + public_ip = doc_get_elmt_value(response, "NewExternalIPAddress"); + if (!public_ip) { + PJ_LOG(3, (THIS_FILE, "IGD %s has no external IP", igd->dev_id.ptr)); + goto on_error; + } + pj_strdup2_with_null(upnp_mgr.pool, &igd->public_ip, public_ip); + pj_sockaddr_parse(pj_AF_UNSPEC(), 0, &igd->public_ip, + &igd->public_ip_addr); + public_ip = igd->public_ip.ptr; + +on_error: + ixmlDocument_free(action); + if (response) ixmlDocument_free(response); + + return public_ip; +} + +/* Download the XML document of the IGD. */ +static void download_igd_xml(unsigned dev_idx) +{ + struct igd *igd_dev = &upnp_mgr.igd_devs[dev_idx]; + const char *url = igd_dev->url.ptr; + IXML_Document *doc = NULL; + int upnp_err; + const char *dev_type; + const char *friendly_name; + const char *base_url; + const char *control_url; + const char *public_ip; + char *abs_control_url = NULL; + IXML_NodeList *service_list = NULL; + unsigned i, n; + + upnp_err = UpnpDownloadXmlDoc(url, &doc); + if (upnp_err != UPNP_E_SUCCESS || !doc) { + PJ_LOG(3, (THIS_FILE, "Error downloading device XML doc from %s: %s", + url, UpnpGetErrorMessage(upnp_err))); + goto on_error; + } + + /* Check device type. */ + dev_type = doc_get_elmt_value(doc, "deviceType"); + if (!dev_type) return; + if (pj_ansi_strcmp(dev_type, UPNP_IGD_DEVICE) != 0) { + /* Device type is not IGD. */ + goto on_error; + } + + /* Get friendly name. */ + friendly_name = doc_get_elmt_value(doc, "friendlyName"); + if (!friendly_name) + friendly_name = ""; + + /* Get base URL. */ + base_url = doc_get_elmt_value(doc, "URLBase"); + if (!base_url) + base_url = url; + + /* Get list of services defined by serviceType. */ + service_list = ixmlDocument_getElementsByTagName(doc, "serviceType"); + n = ixmlNodeList_length(service_list); + + for (i = 0; i < n; i++) { + IXML_Node *service_type_node = ixmlNodeList_item(service_list, i); + IXML_Node *service_node = ixmlNode_getParentNode(service_type_node); + IXML_Element* service_element = (IXML_Element*) service_node; + const char *service_type; + pj_bool_t call_cb = PJ_FALSE; + + /* Check if parent node is "service". */ + if (!service_node || + (pj_ansi_strcmp(ixmlNode_getNodeName(service_node), "service"))) + { + continue; + } + + /* We only want serviceType of WANIPConnection or WANPPPConnection. */ + service_type = get_node_value(service_type_node); + if (pj_ansi_strcmp(service_type, UPNP_WANIP_SERVICE) && + pj_ansi_strcmp(service_type, UPNP_WANPPP_SERVICE)) + { + continue; + } + + /* Get the controlURL. */ + control_url = elmt_get_elmt_value(service_element, "controlURL"); + if (!control_url) + continue; + + /* Resolve the absolute address of controlURL. */ + upnp_err = UpnpResolveURL2(base_url, control_url, &abs_control_url); + if (upnp_err == UPNP_E_SUCCESS) { + pj_strdup2_with_null(upnp_mgr.pool, &igd_dev->control_url, + abs_control_url); + free(abs_control_url); + } else { + PJ_LOG(4, (THIS_FILE, "Error resolving absolute controlURL: %s", + UpnpGetErrorMessage(upnp_err))); + pj_strdup2_with_null(upnp_mgr.pool, &igd_dev->control_url, + control_url); + } + + pj_strdup2_with_null(upnp_mgr.pool, &igd_dev->service_type, service_type); + + /* Get the public IP of the IGD. */ + public_ip = action_get_external_ip(igd_dev); + if (!public_ip) + break; + + /* We find a valid IGD. */ + igd_dev->valid = PJ_TRUE; + igd_dev->alive = PJ_TRUE; + + PJ_LOG(4, (THIS_FILE, "Valid IGD:\n" + "\tUDN : %s\n" + "\tName : %s\n" + "\tService Type : %s\n" + "\tControl URL : %s\n" + "\tPublic IP : %s", + igd_dev->dev_id.ptr, + friendly_name, + igd_dev->service_type.ptr, + igd_dev->control_url.ptr, + public_ip)); + + /* Use this as primary IGD if we haven't had one. */ + pj_mutex_lock(upnp_mgr.mutex); + if (upnp_mgr.primary_igd_idx < 0) { + upnp_mgr.primary_igd_idx = dev_idx; + call_cb = PJ_TRUE; + upnp_mgr.status = PJ_SUCCESS; + } + pj_mutex_unlock(upnp_mgr.mutex); + + if (call_cb && upnp_mgr.upnp_cb) { + (*upnp_mgr.upnp_cb)(upnp_mgr.status); + } + + break; + } + +on_error: + if (service_list) + ixmlNodeList_free(service_list); + if (doc) + ixmlDocument_free(doc); +} + +/* Add a newly discovered IGD. */ +static void add_device(const char *dev_id, const char *url) +{ + unsigned i; + + if (upnp_mgr.igd_cnt >= MAX_DEVS) { + PJ_LOG(3, (THIS_FILE, "Warning: Too many UPnP devices discovered")); + return; + } + + pj_mutex_lock(upnp_mgr.mutex); + for (i = 0; i < upnp_mgr.igd_cnt; i++) { + if (!pj_strcmp2(&upnp_mgr.igd_devs[i].dev_id, dev_id) && + !pj_strcmp2(&upnp_mgr.igd_devs[i].url, url)) + { + /* Device exists. */ + pj_mutex_unlock(upnp_mgr.mutex); + return; + } + } + + pj_strdup2_with_null(upnp_mgr.pool, + &upnp_mgr.igd_devs[upnp_mgr.igd_cnt].dev_id, dev_id); + pj_strdup2_with_null(upnp_mgr.pool, + &upnp_mgr.igd_devs[upnp_mgr.igd_cnt++].url, url); + pj_mutex_unlock(upnp_mgr.mutex); + + PJ_LOG(4, (THIS_FILE, "Discovered a new IGD %s, url: %s", dev_id, url)); + + /* Download the IGD's XML doc. */ + download_igd_xml(upnp_mgr.igd_cnt-1); +} + +/* Update online status of an IGD. */ +static void set_device_online(const char *dev_id) +{ + unsigned i; + + for (i = 0; i < upnp_mgr.igd_cnt; i++) { + struct igd *igd = &upnp_mgr.igd_devs[i]; + + /* We are only interested in valid IGDs that we can use. */ + if (!pj_strcmp2(&igd->dev_id, dev_id) && igd->valid) { + igd->alive = PJ_TRUE; + + if (upnp_mgr.primary_igd_idx < 0) { + /* If we don't have a primary IGD, use this. */ + pj_mutex_lock(upnp_mgr.mutex); + upnp_mgr.primary_igd_idx = i; + pj_mutex_unlock(upnp_mgr.mutex); + + PJ_LOG(4, (THIS_FILE, "Using primary IGD %s", + upnp_mgr.igd_devs[i].dev_id.ptr)); + } + } + } +} + +/* Update IGD status to offline. */ +static void set_device_offline(const char *dev_id) +{ + int i; + + for (i = 0; i < (int)upnp_mgr.igd_cnt; i++) { + struct igd *igd = &upnp_mgr.igd_devs[i]; + + /* We are only interested in valid IGDs that we can use. */ + if (!pj_strcmp2(&igd->dev_id, dev_id) && igd->valid) { + igd->alive = PJ_FALSE; + + pj_mutex_lock(upnp_mgr.mutex); + if (i == upnp_mgr.primary_igd_idx) { + unsigned j; + + /* The primary IGD is offline, try to find another one. */ + upnp_mgr.primary_igd_idx = -1; + for (j = 0; j < upnp_mgr.igd_cnt; j++) { + igd = &upnp_mgr.igd_devs[j]; + if (igd->valid && igd->alive) { + upnp_mgr.primary_igd_idx = j; + break; + } + } + + PJ_LOG(4, (THIS_FILE, "Device %s offline, now using IGD %s", + upnp_mgr.igd_devs[i].dev_id.ptr, + (upnp_mgr.primary_igd_idx < 0? "(none)": + igd->dev_id.ptr))); + } + pj_mutex_unlock(upnp_mgr.mutex); + } + } +} + +/* UPnP client callback. */ +static int client_cb(Upnp_EventType event_type, const void *event, + void * user_data) +{ + /* Ignore if already uninitialized or incorrect user data. */ + if (!upnp_mgr.initialized || user_data != &upnp_mgr) + return UPNP_E_SUCCESS; + + if (!pj_thread_is_registered()) { + pj_bzero(upnp_mgr.thread_desc, sizeof(pj_thread_desc)); + pj_thread_register("upnp_cb", upnp_mgr.thread_desc, + &upnp_mgr.thread); + } + + switch (event_type) { + case UPNP_DISCOVERY_SEARCH_RESULT: + { + const UpnpDiscovery *d_event = (const UpnpDiscovery *) event; + int upnp_status = UpnpDiscovery_get_ErrCode(d_event); + const char *dev_id, *location; + + if (upnp_status != UPNP_E_SUCCESS) { + PJ_LOG(4, (THIS_FILE, "UPnP discovery error: %s", + UpnpGetErrorMessage(upnp_status))); + break; + } + + dev_id = UpnpDiscovery_get_DeviceID_cstr(d_event); + location = UpnpDiscovery_get_Location_cstr(d_event); + + add_device(dev_id, location); + break; + } + case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE: + { + const UpnpDiscovery* d_event = (const UpnpDiscovery*) event; + set_device_online(UpnpDiscovery_get_DeviceID_cstr(d_event)); + break; + } + + case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE: + { + const UpnpDiscovery* d_event = (const UpnpDiscovery*) event; + set_device_offline(UpnpDiscovery_get_DeviceID_cstr(d_event)); + break; + } + + case UPNP_DISCOVERY_SEARCH_TIMEOUT: + { + pj_bool_t call_cb = PJ_FALSE; + + pj_mutex_lock(upnp_mgr.mutex); + if (upnp_mgr.search_cnt > 0) { + --upnp_mgr.search_cnt; + if (upnp_mgr.search_cnt == 0 && upnp_mgr.primary_igd_idx < 0) { + PJ_LOG(4,(THIS_FILE, "Search timed out, no valid IGD found")); + call_cb = PJ_TRUE; + upnp_mgr.status = PJ_ENOTFOUND; + } + } + pj_mutex_unlock(upnp_mgr.mutex); + + if (call_cb && upnp_mgr.upnp_cb) { + (*upnp_mgr.upnp_cb)(upnp_mgr.status); + } + + break; + } + case UPNP_CONTROL_ACTION_COMPLETE: + { + int err_code; + IXML_Document *response = NULL; + const UpnpActionComplete* a_event = (const UpnpActionComplete *) event; + if (!a_event) + break; + + /* The only action complete event we're supposed to receive is + * from port mapping deletion action. + */ + err_code = UpnpActionComplete_get_ErrCode(a_event); + if (err_code != UPNP_E_SUCCESS) { + PJ_LOG(4, (THIS_FILE, "Port mapping deletion action complete " + "error: %d (%s)", err_code, + UpnpGetErrorMessage(err_code))); + break; + } + + response = UpnpActionComplete_get_ActionResult(a_event); + if (!response) { + PJ_LOG(4, (THIS_FILE, "Failed to get response to delete port " + "mapping")); + } else { + if (!check_error_response(response)) { + PJ_LOG(4, (THIS_FILE, "Successfully deleted port mapping")); + } + ixmlDocument_free(response); + } + + break; + } + default: + TRACE_("Unhandled UPnP client callback %d", event_type); + break; + } + + return UPNP_E_SUCCESS; +} + +/* Initiate search for Internet Gateway Devices. */ +static void search_igd(int search_time) +{ + int err; + + upnp_mgr.search_cnt = 4; + + err = UpnpSearchAsync(upnp_mgr.client_hnd, search_time, + UPNP_ROOT_DEVICE, &upnp_mgr); + if (err != UPNP_E_SUCCESS) { + PJ_LOG(4, (THIS_FILE, "Searching for UPNP_ROOT_DEVICE failed: %s", + UpnpGetErrorMessage(err))); + } + + err = UpnpSearchAsync(upnp_mgr.client_hnd, search_time, + UPNP_IGD_DEVICE, &upnp_mgr); + if (err != UPNP_E_SUCCESS) { + PJ_LOG(4, (THIS_FILE, "Searching for UPNP_IGD_DEVICE failed: %s", + UpnpGetErrorMessage(err))); + } + + err = UpnpSearchAsync(upnp_mgr.client_hnd, search_time, + UPNP_WANIP_SERVICE, &upnp_mgr); + if (err != UPNP_E_SUCCESS) { + PJ_LOG(4, (THIS_FILE, "Searching for UPNP_WANIP_SERVICE failed: %s", + UpnpGetErrorMessage(err))); + } + + err = UpnpSearchAsync(upnp_mgr.client_hnd, search_time, + UPNP_WANPPP_SERVICE, &upnp_mgr); + if (err != UPNP_E_SUCCESS) { + PJ_LOG(4, (THIS_FILE, "Searching for UPNP_WANPPP_SERVICE failed: %s", + UpnpGetErrorMessage(err))); + } +} + +/* Initialize UPnP. */ +PJ_DEF(pj_status_t) pj_upnp_init(const pj_upnp_init_param *param) +{ + int upnp_err; + const char *ip_address; + unsigned short port; + const char *ip_address6 = NULL; + unsigned short port6 = 0; + pj_status_t status; + + if (upnp_mgr.initialized) + return PJ_SUCCESS; + +#if ENABLE_LOG + UpnpSetLogLevel(UPNP_ALL); + UpnpSetLogFileNames("upnp.log", NULL); + upnp_err = UpnpInitLog(); + if (upnp_err != UPNP_E_SUCCESS) { + PJ_LOG(4, (THIS_FILE, "Failed to initialize UPnP log: %s", + UpnpGetErrorMessage(upnp_err))); + } +#endif + + pj_bzero(&upnp_mgr, sizeof(upnp_mgr)); + upnp_err = UpnpInit2(param->if_name, (unsigned short)param->port); + if (upnp_err != UPNP_E_SUCCESS) { + PJ_LOG(1, (THIS_FILE, "Failed to initialize libupnp with " + "interface %s: %s", + (param->if_name? param->if_name: "NULL"), + UpnpGetErrorMessage(upnp_err))); + return PJ_EUNKNOWN; + } + + /* Register client. */ + upnp_err = UpnpRegisterClient(client_cb, &upnp_mgr, &upnp_mgr.client_hnd); + if (upnp_err != UPNP_E_SUCCESS) { + PJ_LOG(1, (THIS_FILE, "Failed to register client: %s", + UpnpGetErrorMessage(upnp_err))); + UpnpFinish(); + return PJ_EINVALIDOP; + } + + /* Try to disable web server. */ + if (UpnpIsWebserverEnabled()) { + UpnpEnableWebserver(0); + if (UpnpIsWebserverEnabled()) { + PJ_LOG(4, (THIS_FILE, "Failed to disable web server")); + } + } + + /* Makes the XML parser more tolerant to malformed text. */ + ixmlRelaxParser(1); + + upnp_mgr.initialized = 1; + upnp_mgr.primary_igd_idx = -1; + upnp_mgr.upnp_cb = param->upnp_cb; + upnp_mgr.pool = pj_pool_create(param->factory, "upnp", 512, 512, NULL); + if (!upnp_mgr.pool) { + pj_upnp_deinit(); + return PJ_ENOMEM; + } + pj_mutex_create_recursive(upnp_mgr.pool, "upnp", &upnp_mgr.mutex); + + ip_address = UpnpGetServerIpAddress(); + port = UpnpGetServerPort(); +#if PJ_HAS_IPV6 + ip_address6 = UpnpGetServerIp6Address(); + port6 = UpnpGetServerPort6(); +#endif + if (param->if_name) { + PJ_LOG(4, (THIS_FILE, "UPnP initialized with interface %s", + param->if_name)); + } + if (ip_address6 && port6) { + PJ_LOG(4, (THIS_FILE, "UPnP initialized on %s:%u (IPv4) and " + "%s:%u (IPv6)", ip_address, port, + ip_address6, port6)); + } else { + PJ_LOG(4, (THIS_FILE, "UPnP initialized on %s:%u", ip_address, port)); + } + + /* Search for Internet Gateway Devices. */ + upnp_mgr.status = PJ_EPENDING; + search_igd(param->search_time > 0? param->search_time: + PJ_UPNP_DEFAULT_SEARCH_TIME); + + return PJ_SUCCESS; +} + +/* Deinitialize UPnP. */ +PJ_DEF(pj_status_t) pj_upnp_deinit() +{ + PJ_LOG(4, (THIS_FILE, "UPnP deinitializing...")); + + /* Note that this function will wait until all its worker threads + * complete. + */ + UpnpFinish(); + + if (upnp_mgr.mutex) + pj_mutex_destroy(upnp_mgr.mutex); + + if (upnp_mgr.pool) + pj_pool_release(upnp_mgr.pool); + + pj_bzero(&upnp_mgr, sizeof(upnp_mgr)); + upnp_mgr.primary_igd_idx = -1; + + PJ_LOG(4, (THIS_FILE, "UPnP deinitialized")); + + return PJ_SUCCESS; +} + + +/* Send request to add port mapping. */ +PJ_DECL(pj_status_t)pj_upnp_add_port_mapping(unsigned sock_cnt, + const pj_sock_t sock[], + unsigned ext_port[], + pj_sockaddr mapped_addr[]) +{ + unsigned max_wait = 20; + unsigned i; + struct igd *igd = NULL; + pj_status_t status = PJ_SUCCESS; + + if (!upnp_mgr.initialized) { + PJ_LOG(3, (THIS_FILE, "UPnP not initialized yet")); + return PJ_EINVALIDOP; + } + + /* If IGD search hasn't completed, wait momentarily. */ + while (upnp_mgr.status == PJ_EPENDING && max_wait > 0) { + pj_thread_sleep(100); + max_wait--; + } + + /* Need to lock in case the device becomes offline at the same time. */ + pj_mutex_lock(upnp_mgr.mutex); + if (upnp_mgr.primary_igd_idx < 0) { + PJ_LOG(3, (THIS_FILE, "No valid IGD")); + pj_mutex_unlock(upnp_mgr.mutex); + return PJ_ENOTFOUND; + } + + igd = &upnp_mgr.igd_devs[upnp_mgr.primary_igd_idx]; + pj_mutex_unlock(upnp_mgr.mutex); + + for (i = 0; i < sock_cnt; i++) { + static const char *ACTION_ADD_PORT_MAPPING = "AddPortMapping"; + static const char *PORT_MAPPING_DESCRIPTION = "pjsip-upnp"; + int upnp_err; + IXML_Document *action = NULL; + IXML_Document *response = NULL; + char int_port_buf[10], ext_port_buf[10]; + char addr_buf[PJ_INET6_ADDRSTRLEN]; + unsigned int_port; + pj_sockaddr bound_addr; + int namelen = sizeof(pj_sockaddr); + const char *pext_port = (ext_port? ext_port_buf: int_port_buf); + + /* Get socket's bound address. */ + status = pj_sock_getsockname(sock[i], &bound_addr, &namelen); + if (status != PJ_SUCCESS) { + PJ_LOG(3, (THIS_FILE, "getsockname() error")); + goto on_error; + } + + if (!pj_sockaddr_has_addr(&bound_addr)) { + pj_sockaddr addr; + + /* Get local IP address. */ + status = pj_gethostip(bound_addr.addr.sa_family, &addr); + if (status != PJ_SUCCESS) + goto on_error; + + pj_sockaddr_copy_addr(&bound_addr, &addr); + } + + pj_sockaddr_print(&bound_addr, addr_buf, sizeof(addr_buf), 0); + int_port = pj_sockaddr_get_port(&bound_addr); + pj_utoa(int_port, int_port_buf); + if (ext_port) + pj_utoa(ext_port[i], ext_port_buf); + + /* Create action XML. */ + UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING, + igd->service_type.ptr, "NewRemoteHost", ""); + UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING, + igd->service_type.ptr, "NewExternalPort", pext_port); + UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING, + igd->service_type.ptr, "NewProtocol", "UDP"); + UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING, + igd->service_type.ptr, "NewInternalPort",int_port_buf); + UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING, + igd->service_type.ptr, "NewInternalClient", addr_buf); + UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING, + igd->service_type.ptr, "NewEnabled", "1"); + UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING, + igd->service_type.ptr, + "NewPortMappingDescription", PORT_MAPPING_DESCRIPTION); + UpnpAddToAction(&action, ACTION_ADD_PORT_MAPPING, + igd->service_type.ptr, "NewLeaseDuration","0"); + + /* Send the action XML. */ + upnp_err = UpnpSendAction(upnp_mgr.client_hnd, igd->control_url.ptr, + igd->service_type.ptr, NULL, action, + &response); + if (upnp_err != UPNP_E_SUCCESS || !response) { + PJ_LOG(3, (THIS_FILE, "Failed to %s IGD %s to add port mapping " + "for %s:%s -> %s:%s: %d (%s)", + response? "send action to": + "get response from", + igd->dev_id.ptr, + igd->public_ip.ptr, pext_port, + addr_buf, int_port_buf, upnp_err, + UpnpGetErrorMessage(upnp_err))); + status = PJ_ETIMEDOUT; + } + + TRACE_("Add port mapping XML action:\n%s", + ixmlPrintDocument(action)); + TRACE_("Add port mapping XML response:\n%s", + (response? ixmlPrintDocument(response): "empty")); + + if (response && check_error_response(response)) { + /* The error detail will be printed by check_error_response(). */ + status = PJ_EINVALIDOP; + } + + ixmlDocument_free(action); + if (response) ixmlDocument_free(response); + + pj_sockaddr_cp(&mapped_addr[i], &bound_addr); + pj_sockaddr_set_str_addr(bound_addr.addr.sa_family, + &mapped_addr[i], &igd->public_ip); + pj_sockaddr_set_port(&mapped_addr[i], + (pj_uint16_t)(ext_port? ext_port[i]: int_port)); + + if (status != PJ_SUCCESS) + goto on_error; + + PJ_LOG(4, (THIS_FILE, "Successfully add port mapping to IGD %s: " + "%s:%s -> %s:%s", igd->dev_id.ptr, + igd->public_ip.ptr, pext_port, + addr_buf, int_port_buf)); + } + + return PJ_SUCCESS; + +on_error: + /* Port mapping was unsuccessful, so we need to delete all + * the previous port mappings. + */ + while (i > 0) { + pj_upnp_del_port_mapping(&mapped_addr[--i]); + } + + return status; +} + + +/* Send request to delete port mapping. */ +PJ_DEF(pj_status_t)pj_upnp_del_port_mapping(const pj_sockaddr *mapped_addr) +{ + static const char* ACTION_DELETE_PORT_MAPPING = "DeletePortMapping"; + int upnp_err; + struct igd *igd = NULL; + IXML_Document *action = NULL; + pj_status_t status = PJ_SUCCESS; + pj_sockaddr host_addr; + unsigned ext_port; + char ext_port_buf[10]; + + if (!upnp_mgr.initialized) + return PJ_EINVALIDOP; + + /* Need to lock in case the device becomes offline at the same time. */ + pj_mutex_lock(upnp_mgr.mutex); + if (upnp_mgr.primary_igd_idx < 0) { + PJ_LOG(3, (THIS_FILE, "No valid IGD")); + pj_mutex_unlock(upnp_mgr.mutex); + return PJ_ENOTFOUND; + } + + igd = &upnp_mgr.igd_devs[upnp_mgr.primary_igd_idx]; + pj_mutex_unlock(upnp_mgr.mutex); + + /* Compare IGD's public IP to the mapped public address. */ + pj_sockaddr_cp(&host_addr, mapped_addr); + pj_sockaddr_set_port(&host_addr, 0); + if (pj_sockaddr_cmp(&igd->public_ip_addr, &host_addr)) { + unsigned i; + + /* The primary IGD's public IP is different. Find the IGD + * that matches the mapped address. + */ + igd = NULL; + for (i = 0; i < upnp_mgr.igd_cnt; i++, igd = NULL) { + igd = &upnp_mgr.igd_devs[i]; + if (igd->valid && igd->alive && + !pj_sockaddr_cmp(&igd->public_ip_addr, &host_addr)) + { + break; + } + } + } + + if (!igd) { + /* Either the IGD we previously requested to add port mapping has become + * offline, or the address is actually not a valid. + */ + PJ_LOG(3, (THIS_FILE, "The IGD is offline or invalid mapped address")); + return PJ_EGONE; + } + + ext_port = pj_sockaddr_get_port(mapped_addr); + if (ext_port == 0) { + /* Deleting port zero should be harmless, but it's a waste of time. */ + PJ_LOG(3, (THIS_FILE, "Invalid port number to be deleted")); + return PJ_EINVALIDOP; + } + pj_utoa(ext_port, ext_port_buf); + + /* Create action XML. */ + UpnpAddToAction(&action, ACTION_DELETE_PORT_MAPPING, igd->service_type.ptr, + "NewRemoteHost", ""); + UpnpAddToAction(&action, ACTION_DELETE_PORT_MAPPING, igd->service_type.ptr, + "NewExternalPort", ext_port_buf); + UpnpAddToAction(&action, ACTION_DELETE_PORT_MAPPING, igd->service_type.ptr, + "NewProtocol", "UDP"); + + /* For mapping deletion, send the action XML async, to avoid long + * wait in network disconnection scenario. + */ + upnp_err = UpnpSendActionAsync(upnp_mgr.client_hnd, igd->control_url.ptr, + igd->service_type.ptr, NULL, action, + client_cb, &upnp_mgr); + if (upnp_err == UPNP_E_SUCCESS) { + PJ_LOG(4, (THIS_FILE, "Successfully sending async action to " + "delete port mapping to IGD %s for " + "%s:%s", igd->dev_id.ptr, + igd->public_ip.ptr, ext_port_buf)); + } else { + PJ_LOG(3, (THIS_FILE, "Failed to send action to IGD %s to delete " + "port mapping for %s:%s: %d (%s)", + igd->dev_id.ptr, igd->public_ip.ptr, + ext_port_buf, upnp_err, + UpnpGetErrorMessage(upnp_err))); + status = PJ_EINVALIDOP; + } + + ixmlDocument_free(action); + + return status; +} + +#if defined(_MSC_VER) +# pragma comment(lib, "libupnp") +# pragma comment(lib, "libixml") +# pragma comment(lib, "libpthread") +#endif + +#endif /* PJLIB_UTIL_HAS_UPNP */ diff --git a/pjsip-apps/src/pjsua/pjsua_app_config.c b/pjsip-apps/src/pjsua/pjsua_app_config.c index d427d1fe5b..5b1e2e5c31 100644 --- a/pjsip-apps/src/pjsua/pjsua_app_config.c +++ b/pjsip-apps/src/pjsua/pjsua_app_config.c @@ -109,6 +109,8 @@ static void usage(void) puts (" May be specified multiple times"); puts (" --stun-srv=FORMAT Set STUN server host or domain. This option may be"); puts (" specified more than once. FORMAT is hostdom[:PORT]"); + puts (" --upnp=if_name Enable UPnP using the interface name. If interface is"); + puts (" not specified, the first interface found will be used"); #if defined(PJSIP_HAS_TLS_TRANSPORT) && (PJSIP_HAS_TLS_TRANSPORT != 0) puts (""); @@ -367,7 +369,7 @@ static pj_status_t parse_args(int argc, char *argv[], OPT_BOUND_ADDR, OPT_CONTACT_PARAMS, OPT_CONTACT_URI_PARAMS, OPT_100REL, OPT_USE_IMS, OPT_REALM, OPT_USERNAME, OPT_PASSWORD, OPT_REG_RETRY_INTERVAL, OPT_REG_USE_PROXY, - OPT_MWI, OPT_NAMESERVER, OPT_STUN_SRV, OPT_OUTB_RID, + OPT_MWI, OPT_NAMESERVER, OPT_STUN_SRV, OPT_UPNP, OPT_OUTB_RID, OPT_ADD_BUDDY, OPT_OFFER_X_MS_MSG, OPT_NO_PRESENCE, OPT_AUTO_ANSWER, OPT_AUTO_PLAY, OPT_AUTO_PLAY_HANGUP, OPT_AUTO_LOOP, OPT_AUTO_CONF, OPT_CLOCK_RATE, OPT_SND_CLOCK_RATE, OPT_STEREO, @@ -448,6 +450,7 @@ static pj_status_t parse_args(int argc, char *argv[], { "reg-use-proxy", 1, 0, OPT_REG_USE_PROXY}, { "nameserver", 1, 0, OPT_NAMESERVER}, { "stun-srv", 1, 0, OPT_STUN_SRV}, + { "upnp", 2, 0, OPT_UPNP}, { "add-buddy", 1, 0, OPT_ADD_BUDDY}, { "offer-x-ms-msg",0,0,OPT_OFFER_X_MS_MSG}, { "no-presence", 0, 0, OPT_NO_PRESENCE}, @@ -941,6 +944,11 @@ static pj_status_t parse_args(int argc, char *argv[], cfg->cfg.stun_srv[cfg->cfg.stun_srv_cnt++] = pj_str(pj_optarg); break; + case OPT_UPNP: /* UPnP */ + cfg->cfg.enable_upnp = PJ_TRUE; + cfg->cfg.upnp_if_name = pj_str(pj_optarg); + break; + case OPT_ADD_BUDDY: /* Add to buddy list. */ if (pjsua_verify_url(pj_optarg) != 0) { PJ_LOG(1,(THIS_FILE, diff --git a/pjsip-apps/src/swig/symbols.i b/pjsip-apps/src/swig/symbols.i index d8cd75d5ab..2ac83a77dc 100644 --- a/pjsip-apps/src/swig/symbols.i +++ b/pjsip-apps/src/swig/symbols.i @@ -769,6 +769,12 @@ typedef enum pjsua_stun_use PJSUA_STUN_RETRY_ON_FAILURE } pjsua_stun_use; +typedef enum pjsua_upnp_use +{ + PJSUA_UPNP_USE_DEFAULT, + PJSUA_UPNP_USE_DISABLED +} pjsua_upnp_use; + typedef enum pjsua_call_hold_type { PJSUA_CALL_HOLD_TYPE_RFC3264, diff --git a/pjsip-apps/src/swig/symbols.lst b/pjsip-apps/src/swig/symbols.lst index 2f1187988d..9052cf6720 100644 --- a/pjsip-apps/src/swig/symbols.lst +++ b/pjsip-apps/src/swig/symbols.lst @@ -36,4 +36,4 @@ pjsip-simple/evsub.h pjsip_evsub_state pjsip-ua/sip_inv.h pjsip_inv_state -pjsua-lib/pjsua.h pjsua_invalid_id_const_ pjsua_state pjsua_stun_use pjsua_call_hold_type pjsua_acc_id pjsua_destroy_flag pjsua_100rel_use pjsua_sip_timer_use pjsua_ipv6_use pjsua_nat64_opt pjsua_buddy_status pjsua_call_media_status pjsua_vid_win_id pjsua_call_id pjsua_med_tp_st pjsua_call_vid_strm_op pjsua_vid_req_keyframe_method pjsua_call_flag pjsua_create_media_transport_flag pjsua_snd_dev_id pjsua_snd_dev_mode pjsua_ip_change_op pjsua_dtmf_method +pjsua-lib/pjsua.h pjsua_invalid_id_const_ pjsua_state pjsua_stun_use pjsua_upnp_use pjsua_call_hold_type pjsua_acc_id pjsua_destroy_flag pjsua_100rel_use pjsua_sip_timer_use pjsua_ipv6_use pjsua_nat64_opt pjsua_buddy_status pjsua_call_media_status pjsua_vid_win_id pjsua_call_id pjsua_med_tp_st pjsua_call_vid_strm_op pjsua_vid_req_keyframe_method pjsua_call_flag pjsua_create_media_transport_flag pjsua_snd_dev_id pjsua_snd_dev_mode pjsua_ip_change_op pjsua_dtmf_method diff --git a/pjsip/include/pjsua-lib/pjsua.h b/pjsip/include/pjsua-lib/pjsua.h index 1aae8e0ea3..783a00717d 100644 --- a/pjsip/include/pjsua-lib/pjsua.h +++ b/pjsip/include/pjsua-lib/pjsua.h @@ -2307,6 +2307,27 @@ typedef struct pjsua_config */ pj_bool_t hangup_forked_call; + /** + * Specify whether to enable UPnP. + * + * Note that this setting can be further customized in account + * configuration (#pjsua_acc_config). + * + * Default: PJ_FALSE + */ + pj_bool_t enable_upnp; + + /** + * Specify which interface to use for UPnP. If empty, UPnP will use + * the first suitable interface found. + * + * Note that this setting is only applicable if UPnP is enabled and + * the string must be NULL terminated. + * + * Default: empty string + */ + pj_str_t upnp_if_name; + } pjsua_config; @@ -3505,6 +3526,23 @@ typedef enum pjsua_stun_use } pjsua_stun_use; +/** + * This enumeration controls the use of UPnP in the account. + */ +typedef enum pjsua_upnp_use +{ + /** + * Follow the default setting in the global \a pjsua_config. + */ + PJSUA_UPNP_USE_DEFAULT, + + /** + * Disable UPnP. + */ + PJSUA_UPNP_USE_DISABLED + +} pjsua_upnp_use; + /** * This enumeration controls the use of ICE settings in the account. */ @@ -4132,6 +4170,20 @@ typedef struct pjsua_acc_config */ pjsua_stun_use media_stun_use; + /** + * Control the use of UPnP for the SIP signaling. + * + * Default: PJSUA_UPNP_USE_DEFAULT + */ + pjsua_upnp_use sip_upnp_use; + + /** + * Control the use of UPnP for the media transports. + * + * Default: PJSUA_UPNP_USE_DEFAULT + */ + pjsua_upnp_use media_upnp_use; + /** * Use loopback media transport. This may be useful if application * doesn't want PJSIP to create real media transports/sockets, such as diff --git a/pjsip/include/pjsua-lib/pjsua_internal.h b/pjsip/include/pjsua-lib/pjsua_internal.h index 89b1ffee95..8d35121d04 100644 --- a/pjsip/include/pjsua-lib/pjsua_internal.h +++ b/pjsip/include/pjsua-lib/pjsua_internal.h @@ -102,6 +102,9 @@ struct pjsua_call_media pjmedia_srtp_use rem_srtp_use; /**< Remote's SRTP usage policy. */ pj_timestamp last_req_keyframe;/**< Last TX keyframe request. */ + pj_bool_t use_upnp; /**< Use UPnP? */ + pj_sockaddr mapped_addr[2]; /* UPnP mapped address. */ + pjsua_med_tp_state_cb med_init_cb;/**< Media transport initialization callback. */ @@ -342,6 +345,7 @@ typedef struct pjsua_transport_data int index; pjsip_transport_type_e type; pjsip_host_port local_name; + pj_sockaddr pub_addr; union { pjsip_transport *tp; @@ -507,6 +511,9 @@ struct pjsua_data unsigned stun_opt; /**< STUN resolution option. */ pj_dns_resolver *resolver; /**< DNS resolver. */ + /* UPnP */ + pj_status_t upnp_status; /**< UPnP status. */ + /* Detected NAT type */ pj_stun_nat_type nat_type; /**< NAT type. */ pj_status_t nat_status; /**< Detection status. */ @@ -710,6 +717,10 @@ pj_status_t normalize_route_uri(pj_pool_t *pool, pj_str_t *uri); pj_bool_t pjsua_sip_acc_is_using_stun(pjsua_acc_id acc_id); pj_bool_t pjsua_media_acc_is_using_stun(pjsua_acc_id acc_id); +/* acc use UPnP? */ +pj_bool_t pjsua_sip_acc_is_using_upnp(pjsua_acc_id acc_id); +pj_bool_t pjsua_media_acc_is_using_upnp(pjsua_acc_id acc_id); + /* acc use IPv6? */ pj_bool_t pjsua_sip_acc_is_using_ipv6(pjsua_acc_id acc_id); diff --git a/pjsip/include/pjsua2/account.hpp b/pjsip/include/pjsua2/account.hpp index 46bddbe7e5..22961cd5f8 100644 --- a/pjsip/include/pjsua2/account.hpp +++ b/pjsip/include/pjsua2/account.hpp @@ -487,6 +487,20 @@ struct AccountNatConfig : public PersistentObject */ pjsua_stun_use mediaStunUse; + /** + * Control the use of UPnP for the SIP signaling. + * + * Default: PJSUA_UPNP_USE_DEFAULT + */ + pjsua_upnp_use sipUpnpUse; + + /** + * Control the use of UPnP for the media transports. + * + * Default: PJSUA_UPNP_USE_DEFAULT + */ + pjsua_upnp_use mediaUpnpUse; + /** * Specify NAT64 options. * @@ -718,6 +732,8 @@ struct AccountNatConfig : public PersistentObject */ AccountNatConfig() : sipStunUse(PJSUA_STUN_USE_DEFAULT), mediaStunUse(PJSUA_STUN_USE_DEFAULT), + sipUpnpUse(PJSUA_UPNP_USE_DEFAULT), + mediaUpnpUse(PJSUA_UPNP_USE_DEFAULT), nat64Opt(PJSUA_NAT64_DISABLED), iceEnabled(false), iceTrickle(PJ_ICE_SESS_TRICKLE_DISABLED), diff --git a/pjsip/include/pjsua2/endpoint.hpp b/pjsip/include/pjsua2/endpoint.hpp index e12a2f8565..308a492d0f 100644 --- a/pjsip/include/pjsua2/endpoint.hpp +++ b/pjsip/include/pjsua2/endpoint.hpp @@ -704,6 +704,26 @@ struct UaConfig : public PersistentObject */ bool mwiUnsolicitedEnabled; + /** + * Specify whether to enable UPnP. + * + * Note that this setting can be further customized in account + * configuration (#pjsua_acc_config). + * + * Default: FALSE + */ + bool enableUpnp; + + /** + * Specify which interface to use for UPnP. If empty, UPnP will use + * the first suitable interface found. + * + * Note that this setting is only applicable if UPnP is enabled. + * + * Default: empty string + */ + string upnpIfName; + public: /** * Default constructor to initialize with default values. diff --git a/pjsip/src/pjsua-lib/pjsua_acc.c b/pjsip/src/pjsua-lib/pjsua_acc.c index e6d09a4933..1ba4a53486 100644 --- a/pjsip/src/pjsua-lib/pjsua_acc.c +++ b/pjsip/src/pjsua-lib/pjsua_acc.c @@ -2797,6 +2797,24 @@ pj_bool_t pjsua_media_acc_is_using_stun(pjsua_acc_id acc_id) pjsua_var.ua_cfg.stun_srv_cnt != 0; } +pj_bool_t pjsua_sip_acc_is_using_upnp(pjsua_acc_id acc_id) +{ + pjsua_acc *acc = &pjsua_var.acc[acc_id]; + + return acc->cfg.sip_upnp_use != PJSUA_UPNP_USE_DISABLED && + pjsua_var.ua_cfg.enable_upnp && + pjsua_var.upnp_status == PJ_SUCCESS; +} + +pj_bool_t pjsua_media_acc_is_using_upnp(pjsua_acc_id acc_id) +{ + pjsua_acc *acc = &pjsua_var.acc[acc_id]; + + return acc->cfg.media_upnp_use != PJSUA_UPNP_USE_DISABLED && + pjsua_var.ua_cfg.enable_upnp && + pjsua_var.upnp_status == PJ_SUCCESS; +} + /* * Update registration or perform unregistration. */ @@ -2892,9 +2910,11 @@ PJ_DEF(pj_status_t) pjsua_acc_set_registration( pjsua_acc_id acc_id, pjsip_regc_set_via_sent_by(pjsua_var.acc[acc_id].regc, &pjsua_var.acc[acc_id].via_addr, pjsua_var.acc[acc_id].via_tp); - } else if (!pjsua_sip_acc_is_using_stun(acc_id)) { + } else if (!pjsua_sip_acc_is_using_stun(acc_id) && + !pjsua_sip_acc_is_using_upnp(acc_id)) + { /* Choose local interface to use in Via if acc is not using - * STUN + * STUN nor UPnP. */ pjsua_acc_get_uac_addr(acc_id, tdata->pool, &acc->cfg.reg_uri, @@ -3315,9 +3335,11 @@ PJ_DEF(pj_status_t) pjsua_acc_create_request(pjsua_acc_id acc_id, { tdata->via_addr = pjsua_var.acc[acc_id].via_addr; tdata->via_tp = pjsua_var.acc[acc_id].via_tp; - } else if (!pjsua_sip_acc_is_using_stun(acc_id)) { + } else if (!pjsua_sip_acc_is_using_stun(acc_id) && + !pjsua_sip_acc_is_using_upnp(acc_id)) + { /* Choose local interface to use in Via if acc is not using - * STUN + * STUN nor UPnP. */ pjsua_acc_get_uac_addr(acc_id, tdata->pool, target, @@ -3428,7 +3450,8 @@ pj_status_t pjsua_acc_get_uac_addr(pjsua_acc_id acc_id, tfla2_prm.tp_type = tp_type; tfla2_prm.tp_sel = &tp_sel; tfla2_prm.dst_host = sip_uri->host; - tfla2_prm.local_if = (!pjsua_sip_acc_is_using_stun(acc_id) || + tfla2_prm.local_if = ((!pjsua_sip_acc_is_using_stun(acc_id) && + !pjsua_sip_acc_is_using_upnp(acc_id)) || (flag & PJSIP_TRANSPORT_RELIABLE)); tpmgr = pjsip_endpt_get_tpmgr(pjsua_var.endpt); @@ -3448,7 +3471,8 @@ pj_status_t pjsua_acc_get_uac_addr(pjsua_acc_id acc_id, tfla2_prm2.tp_type = PJSIP_TRANSPORT_UDP6; tfla2_prm2.tp_sel = NULL; - tfla2_prm2.local_if = (!pjsua_sip_acc_is_using_stun(acc_id)); + tfla2_prm2.local_if = (!pjsua_sip_acc_is_using_stun(acc_id) && + !pjsua_sip_acc_is_using_upnp(acc_id)); status = pjsip_tpmgr_find_local_addr2(tpmgr, pool, &tfla2_prm2); if (status == PJ_SUCCESS) { update_addr = PJ_FALSE; @@ -3806,7 +3830,8 @@ PJ_DEF(pj_status_t) pjsua_acc_create_uas_contact( pj_pool_t *pool, tfla2_prm.tp_type = tp_type; tfla2_prm.tp_sel = &tp_sel; tfla2_prm.dst_host = sip_uri->host; - tfla2_prm.local_if = (!pjsua_sip_acc_is_using_stun(acc_id) || + tfla2_prm.local_if = ((!pjsua_sip_acc_is_using_stun(acc_id) && + !pjsua_sip_acc_is_using_upnp(acc_id)) || (flag & PJSIP_TRANSPORT_RELIABLE)); tpmgr = pjsip_endpt_get_tpmgr(pjsua_var.endpt); diff --git a/pjsip/src/pjsua-lib/pjsua_call.c b/pjsip/src/pjsua-lib/pjsua_call.c index f544c44cb0..b46652a8cc 100644 --- a/pjsip/src/pjsua-lib/pjsua_call.c +++ b/pjsip/src/pjsua-lib/pjsua_call.c @@ -725,9 +725,11 @@ static void dlg_set_via(pjsip_dialog *dlg, pjsua_acc *acc) { if (acc->cfg.allow_via_rewrite && acc->via_addr.host.slen > 0) { pjsip_dlg_set_via_sent_by(dlg, &acc->via_addr, acc->via_tp); - } else if (!pjsua_sip_acc_is_using_stun(acc->index)) { + } else if (!pjsua_sip_acc_is_using_stun(acc->index) && + !pjsua_sip_acc_is_using_upnp(acc->index)) + { /* Choose local interface to use in Via if acc is not using - * STUN. See https://trac.pjsip.org/repos/ticket/1804 + * STUN nor UPnP. See https://trac.pjsip.org/repos/ticket/1804 */ pjsip_host_port via_addr; const void *via_tp; @@ -1789,9 +1791,11 @@ pj_bool_t pjsua_call_on_incoming(pjsip_rx_data *rdata) { pjsip_dlg_set_via_sent_by(dlg, &pjsua_var.acc[acc_id].via_addr, pjsua_var.acc[acc_id].via_tp); - } else if (!pjsua_sip_acc_is_using_stun(acc_id)) { + } else if (!pjsua_sip_acc_is_using_stun(acc_id) && + !pjsua_sip_acc_is_using_upnp(acc_id)) + { /* Choose local interface to use in Via if acc is not using - * STUN. See https://trac.pjsip.org/repos/ticket/1804 + * STUN nor UPnP. See https://trac.pjsip.org/repos/ticket/1804 */ char target_buf[PJSIP_MAX_URL_SIZE]; pj_str_t target; diff --git a/pjsip/src/pjsua-lib/pjsua_core.c b/pjsip/src/pjsua-lib/pjsua_core.c index fa25d36a2f..8bdf6c4f79 100644 --- a/pjsip/src/pjsua-lib/pjsua_core.c +++ b/pjsip/src/pjsua-lib/pjsua_core.c @@ -994,6 +994,14 @@ PJ_DEF(pj_status_t) pjsua_create(void) } +#if defined(PJLIB_UTIL_HAS_UPNP) && (PJLIB_UTIL_HAS_UPNP != 0) +/* UPnP callback. */ +static void upnp_cb(pj_status_t status) +{ + pjsua_var.upnp_status = status; +} +#endif + /* * Initialize pjsua with the specified settings. All the settings are * optional, and the default values will be used when the config is not @@ -1194,6 +1202,25 @@ PJ_DEF(pj_status_t) pjsua_init( const pjsua_config *ua_cfg, goto on_error; } +#if defined(PJLIB_UTIL_HAS_UPNP) && (PJLIB_UTIL_HAS_UPNP != 0) + /* Initialize UPnP if enabled */ + if (pjsua_var.ua_cfg.enable_upnp) { + pj_upnp_init_param param; + + pj_bzero(¶m, sizeof(param)); + param.factory = pjsua_var.pool->factory; + if (pjsua_var.ua_cfg.upnp_if_name.slen > 0) + param.if_name = pjsua_var.ua_cfg.upnp_if_name.ptr; + param.upnp_cb = &upnp_cb; + + pjsua_var.upnp_status = pj_upnp_init(¶m); + if (pjsua_var.upnp_status != PJ_SUCCESS) { + pjsua_perror(THIS_FILE, "Error initializing UPnP", + pjsua_var.upnp_status); + } + } +#endif + /* Initialize PJSUA media subsystem */ status = pjsua_media_subsys_init(media_cfg); if (status != PJ_SUCCESS) @@ -2008,6 +2035,28 @@ PJ_DEF(pj_status_t) pjsua_destroy2(unsigned flags) } } + /* Close pjsua transports */ + for (i = 0; i < (int)PJ_ARRAY_SIZE(pjsua_var.tpdata); i++) { + if (pjsua_var.tpdata[i].data.ptr) { + pjsip_transport_type_e tp_type; + + tp_type = pjsua_var.tpdata[i].type & ~PJSIP_TRANSPORT_IPV6; + if ((flags & PJSUA_DESTROY_NO_TX_MSG) && + tp_type == PJSIP_TRANSPORT_UDP && + pjsua_var.ua_cfg.enable_upnp && + pjsua_var.upnp_status == PJ_SUCCESS) + { + /* If we are not supposed to send any outgoing message + * at all, avoid sending UPnP message as it may take + * a while to time out. + */ + continue; + } + + pjsua_transport_close(i, PJ_FALSE); + } + } + /* Destroy media (to shutdown media endpoint, etc) */ pjsua_media_subsys_destroy(flags); @@ -2036,6 +2085,13 @@ PJ_DEF(pj_status_t) pjsua_destroy2(unsigned flags) } } +#if defined(PJLIB_UTIL_HAS_UPNP) && (PJLIB_UTIL_HAS_UPNP != 0) + /* Deinitialize UPnP */ + if (pjsua_var.ua_cfg.enable_upnp) { + pj_upnp_deinit(); + } +#endif + /* Destroy mutex */ if (pjsua_var.mutex) { pj_mutex_destroy(pjsua_var.mutex); @@ -2349,6 +2405,7 @@ static pj_status_t create_sip_udp_sock(int af, /* Get the published address, either by STUN or by resolving * the name of local host. */ + status = PJ_SUCCESS; if (pj_sockaddr_has_addr(p_pub_addr)) { /* * Public address is already specified, no need to resolve the @@ -2384,19 +2441,21 @@ static pj_status_t create_sip_udp_sock(int af, pj_sock_close(sock); return status; } - - /* Otherwise, just use host IP */ - pj_sockaddr_init(af, p_pub_addr, NULL, (pj_uint16_t)port); - status = pj_gethostip(af, p_pub_addr); - if (status != PJ_SUCCESS) { - pjsua_perror(THIS_FILE, "Unable to get local host IP", status); - pj_sock_close(sock); - return status; - } } + } - } else { +#if defined(PJLIB_UTIL_HAS_UPNP) && (PJLIB_UTIL_HAS_UPNP != 0) + else if (pjsua_var.ua_cfg.enable_upnp && + pjsua_var.upnp_status == PJ_SUCCESS) + { + status = pj_upnp_add_port_mapping(1, &sock, NULL, p_pub_addr); + if (status != PJ_SUCCESS) { + pjsua_perror(THIS_FILE, "Error adding UPnP port mapping", status); + } + } +#endif + if (!pj_sockaddr_has_addr(p_pub_addr)) { pj_bzero(p_pub_addr, sizeof(pj_sockaddr)); if (pj_sockaddr_has_addr(&bind_addr)) { @@ -2516,6 +2575,7 @@ PJ_DEF(pj_status_t) pjsua_transport_create( pjsip_transport_type_e type, pjsua_var.tpdata[id].type = type; pjsua_var.tpdata[id].local_name = tp->local_name; pjsua_var.tpdata[id].data.tp = tp; + pj_sockaddr_cp(&pjsua_var.tpdata[id].pub_addr, &pub_addr); if (cfg->bound_addr.slen) pjsua_var.tpdata[id].has_bound_addr = PJ_TRUE; @@ -2901,6 +2961,19 @@ PJ_DEF(pj_status_t) pjsua_transport_close( pjsua_transport_id id, */ switch (tp_type) { case PJSIP_TRANSPORT_UDP: +#if defined(PJLIB_UTIL_HAS_UPNP) && (PJLIB_UTIL_HAS_UPNP != 0) + if (pjsua_var.ua_cfg.enable_upnp && + pjsua_var.upnp_status == PJ_SUCCESS) + { + status = pj_upnp_del_port_mapping( + &pjsua_var.tpdata[id].pub_addr); + if (status != PJ_SUCCESS) { + pjsua_perror(THIS_FILE, "Error deleting pjsua_transport " + "UPnP port mapping", status); + } + } +#endif + status = pjsip_transport_shutdown(pjsua_var.tpdata[id].data.tp); break; case PJSIP_TRANSPORT_TLS: diff --git a/pjsip/src/pjsua-lib/pjsua_media.c b/pjsip/src/pjsua-lib/pjsua_media.c index 5de3edf893..c716d37043 100644 --- a/pjsip/src/pjsua-lib/pjsua_media.c +++ b/pjsip/src/pjsua-lib/pjsua_media.c @@ -249,7 +249,7 @@ pj_status_t pjsua_media_subsys_destroy(unsigned flags) /* * Create RTP and RTCP socket pair, and possibly resolve their public - * address via STUN. + * address via STUN/UPnP. */ static pj_status_t create_rtp_rtcp_sock(pjsua_call_media *call_med, const pjsua_transport_config *cfg, @@ -542,6 +542,43 @@ static pj_status_t create_rtp_rtcp_sock(pjsua_call_media *call_med, break; #endif +#if defined(PJLIB_UTIL_HAS_UPNP) && (PJLIB_UTIL_HAS_UPNP != 0) + } else if ((!use_ipv6 || use_nat64) && + pjsua_media_acc_is_using_upnp(call_med->call->acc_id) && + pjsua_var.upnp_status == PJ_SUCCESS) + { + status = pj_upnp_add_port_mapping(2, sock, NULL, mapped_addr); + if (status == PJ_SUCCESS) { + call_med->use_upnp = PJ_TRUE; + pj_sockaddr_cp(&call_med->mapped_addr[0], &mapped_addr[0]); + pj_sockaddr_cp(&call_med->mapped_addr[1], &mapped_addr[1]); + } else { + pjsua_perror(THIS_FILE, "Error adding UPnP " + "port mapping", status); + + if (!pj_sockaddr_has_addr(&bound_addr)) { + pj_sockaddr addr; + + /* Get local IP address. */ + status = pj_gethostip(af, &addr); + if (status != PJ_SUCCESS) + goto on_error; + + pj_sockaddr_copy_addr(&bound_addr, &addr); + } + + for (i = 0; i < 2; ++i) { + pj_sockaddr_init(af, &mapped_addr[i], NULL, 0); + pj_sockaddr_copy_addr(&mapped_addr[i], &bound_addr); + pj_sockaddr_set_port(&mapped_addr[i], + (pj_uint16_t)(acc->next_rtp_port+i)); + } + break; + } + + break; +#endif + } else if (cfg->public_addr.slen) { status = pj_sockaddr_init(af, &mapped_addr[0], &cfg->public_addr, @@ -3226,6 +3263,13 @@ pj_status_t pjsua_media_channel_deinit(pjsua_call_id call_id) for (mi=0; mimed_cnt; ++mi) { pjsua_call_media *call_med = &call->media[mi]; + if (call_med->use_upnp) { +#if defined(PJLIB_UTIL_HAS_UPNP) && (PJLIB_UTIL_HAS_UPNP != 0) + pj_upnp_del_port_mapping(&call_med->mapped_addr[0]); + pj_upnp_del_port_mapping(&call_med->mapped_addr[1]); +#endif + } + if (call_med->tp_st > PJSUA_MED_TP_IDLE) { pjmedia_transport_info tpinfo; pjmedia_srtp_info *srtp_info; diff --git a/pjsip/src/pjsua-lib/pjsua_pres.c b/pjsip/src/pjsua-lib/pjsua_pres.c index defe176416..b42665bfd3 100644 --- a/pjsip/src/pjsua-lib/pjsua_pres.c +++ b/pjsip/src/pjsua-lib/pjsua_pres.c @@ -882,9 +882,11 @@ static pj_bool_t pres_on_rx_request(pjsip_rx_data *rdata) if (acc->cfg.allow_via_rewrite && acc->via_addr.host.slen > 0) { pjsip_dlg_set_via_sent_by(dlg, &acc->via_addr, acc->via_tp); - } else if (!pjsua_sip_acc_is_using_stun(acc_id)) { + } else if (!pjsua_sip_acc_is_using_stun(acc_id) && + !pjsua_sip_acc_is_using_upnp(acc_id)) + { /* Choose local interface to use in Via if acc is not using - * STUN. See https://trac.pjsip.org/repos/ticket/1412 + * STUN nor UPnP. See https://trac.pjsip.org/repos/ticket/1412 */ char target_buf[PJSIP_MAX_URL_SIZE]; pj_str_t target; @@ -1278,9 +1280,11 @@ static pj_status_t send_publish(int acc_id, pj_bool_t active) if (acc->cfg.allow_via_rewrite && acc->via_addr.host.slen > 0) { pjsip_publishc_set_via_sent_by(acc->publish_sess, &acc->via_addr, acc->via_tp); - } else if (!pjsua_sip_acc_is_using_stun(acc_id)) { + } else if (!pjsua_sip_acc_is_using_stun(acc_id) && + !pjsua_sip_acc_is_using_upnp(acc_id)) + { /* Choose local interface to use in Via if acc is not using - * STUN. See https://trac.pjsip.org/repos/ticket/1412 + * STUN nor UPnP. See https://trac.pjsip.org/repos/ticket/1412 */ pjsip_host_port via_addr; const void *via_tp; @@ -1848,9 +1852,11 @@ static void subscribe_buddy_presence(pjsua_buddy_id buddy_id) if (acc->cfg.allow_via_rewrite && acc->via_addr.host.slen > 0) { pjsip_dlg_set_via_sent_by(buddy->dlg, &acc->via_addr, acc->via_tp); - } else if (!pjsua_sip_acc_is_using_stun(acc_id)) { + } else if (!pjsua_sip_acc_is_using_stun(acc_id) && + !pjsua_sip_acc_is_using_upnp(acc_id)) + { /* Choose local interface to use in Via if acc is not using - * STUN. See https://trac.pjsip.org/repos/ticket/1412 + * STUN nor UPnP. See https://trac.pjsip.org/repos/ticket/1412 */ pjsip_host_port via_addr; const void *via_tp; @@ -2191,9 +2197,11 @@ pj_status_t pjsua_start_mwi(pjsua_acc_id acc_id, pj_bool_t force_renew) if (acc->cfg.allow_via_rewrite && acc->via_addr.host.slen > 0) { pjsip_dlg_set_via_sent_by(acc->mwi_dlg, &acc->via_addr, acc->via_tp); - } else if (!pjsua_sip_acc_is_using_stun(acc_id)) { + } else if (!pjsua_sip_acc_is_using_stun(acc_id) && + !pjsua_sip_acc_is_using_upnp(acc_id)) + { /* Choose local interface to use in Via if acc is not using - * STUN. See https://trac.pjsip.org/repos/ticket/1412 + * STUN nor UPnP. See https://trac.pjsip.org/repos/ticket/1412 */ pjsip_host_port via_addr; const void *via_tp; diff --git a/pjsip/src/pjsua2/account.cpp b/pjsip/src/pjsua2/account.cpp index 186e9d0905..cf0c7ea414 100644 --- a/pjsip/src/pjsua2/account.cpp +++ b/pjsip/src/pjsua2/account.cpp @@ -387,6 +387,8 @@ void AccountNatConfig::readObject(const ContainerNode &node) NODE_READ_NUM_T ( this_node, pjsua_stun_use, sipStunUse); NODE_READ_NUM_T ( this_node, pjsua_stun_use, mediaStunUse); + NODE_READ_NUM_T ( this_node, pjsua_upnp_use, sipUpnpUse); + NODE_READ_NUM_T ( this_node, pjsua_upnp_use, mediaUpnpUse); NODE_READ_NUM_T ( this_node, pjsua_nat64_opt, nat64Opt); NODE_READ_BOOL ( this_node, iceEnabled); NODE_READ_NUM_T ( this_node, pj_ice_sess_trickle, iceTrickle); @@ -421,6 +423,8 @@ void AccountNatConfig::writeObject(ContainerNode &node) const NODE_WRITE_NUM_T ( this_node, pjsua_stun_use, sipStunUse); NODE_WRITE_NUM_T ( this_node, pjsua_stun_use, mediaStunUse); + NODE_WRITE_NUM_T ( this_node, pjsua_upnp_use, sipUpnpUse); + NODE_WRITE_NUM_T ( this_node, pjsua_upnp_use, mediaUpnpUse); NODE_WRITE_NUM_T ( this_node, pjsua_nat64_opt, nat64Opt); NODE_WRITE_BOOL ( this_node, iceEnabled); NODE_WRITE_NUM_T ( this_node, pj_ice_sess_trickle, iceTrickle); @@ -641,6 +645,8 @@ void AccountConfig::toPj(pjsua_acc_config &ret) const // AccountNatConfig ret.sip_stun_use = natConfig.sipStunUse; ret.media_stun_use = natConfig.mediaStunUse; + ret.sip_upnp_use = natConfig.sipUpnpUse; + ret.media_upnp_use = natConfig.mediaUpnpUse; ret.nat64_opt = natConfig.nat64Opt; ret.ice_cfg_use = PJSUA_ICE_CONFIG_USE_CUSTOM; ret.ice_cfg.enable_ice = natConfig.iceEnabled; @@ -803,6 +809,8 @@ void AccountConfig::fromPj(const pjsua_acc_config &prm, // AccountNatConfig natConfig.sipStunUse = prm.sip_stun_use; natConfig.mediaStunUse = prm.media_stun_use; + natConfig.sipUpnpUse = prm.sip_upnp_use; + natConfig.mediaUpnpUse = prm.media_upnp_use; natConfig.nat64Opt = prm.nat64_opt; if (prm.ice_cfg_use == PJSUA_ICE_CONFIG_USE_CUSTOM) { natConfig.iceEnabled = PJ2BOOL(prm.ice_cfg.enable_ice); diff --git a/pjsip/src/pjsua2/endpoint.cpp b/pjsip/src/pjsua2/endpoint.cpp index f35467d0a1..8dc77d9ba6 100644 --- a/pjsip/src/pjsua2/endpoint.cpp +++ b/pjsip/src/pjsua2/endpoint.cpp @@ -269,6 +269,8 @@ void UaConfig::fromPj(const pjsua_config &ua_cfg) this->stunIgnoreFailure = PJ2BOOL(ua_cfg.stun_ignore_failure); this->natTypeInSdp = ua_cfg.nat_type_in_sdp; this->mwiUnsolicitedEnabled = PJ2BOOL(ua_cfg.enable_unsolicited_mwi); + this->enableUpnp = PJ2BOOL(ua_cfg.enable_upnp); + this->upnpIfName = pj2Str(ua_cfg.upnp_if_name); } pjsua_config UaConfig::toPj() const @@ -307,6 +309,8 @@ pjsua_config UaConfig::toPj() const pua_cfg.enable_unsolicited_mwi = this->mwiUnsolicitedEnabled; pua_cfg.stun_try_ipv6 = this->stunTryIpv6; pua_cfg.stun_ignore_failure = this->stunIgnoreFailure; + pua_cfg.enable_upnp = this->enableUpnp; + pua_cfg.upnp_if_name = str2Pj(this->upnpIfName); return pua_cfg; } @@ -325,6 +329,8 @@ void UaConfig::readObject(const ContainerNode &node) PJSUA2_THROW(Error) NODE_READ_BOOL ( this_node, stunIgnoreFailure); NODE_READ_INT ( this_node, natTypeInSdp); NODE_READ_BOOL ( this_node, mwiUnsolicitedEnabled); + NODE_READ_BOOL ( this_node, enableUpnp); + NODE_READ_STRING ( this_node, upnpIfName); } void UaConfig::writeObject(ContainerNode &node) const PJSUA2_THROW(Error) @@ -341,6 +347,8 @@ void UaConfig::writeObject(ContainerNode &node) const PJSUA2_THROW(Error) NODE_WRITE_BOOL ( this_node, stunIgnoreFailure); NODE_WRITE_INT ( this_node, natTypeInSdp); NODE_WRITE_BOOL ( this_node, mwiUnsolicitedEnabled); + NODE_WRITE_BOOL ( this_node, enableUpnp); + NODE_WRITE_STRING ( this_node, upnpIfName); } ///////////////////////////////////////////////////////////////////////////////