Commit 545c459873 for asterisk.org
commit 545c4598733d0437a6ebe1d80c255427cbd6c843
Author: Mike Bradeen <mbradeen@sangoma.com>
Date: Mon Jul 27 12:17:07 2026 -0600
res_pjsip: Qualify all resolved contact targets
Previously, PJSIP endpoint qualification was offloaded to pjproject which
used a single, internally selected contact based on DNS results.
This could result in unused SRV, A, or AAAA targets, leaving a contact
marked as unreachable without probing the remaining targets.
Now, we resolve the complete set of records before sending and creating
OPTIONS transactions for every resolved target. The contact will be marked
as reachable as soon as any target succeeds and unreachable only after every
target fails.
Fixes: #1927
UserNote: DNS-backed PJSIP contacts are now considered reachable when any resolved
SRV, A, or AAAA target responds to qualification.
diff --git a/configure b/configure
index ede97cc9d2..5188ad572b 100755
--- a/configure
+++ b/configure
@@ -3936,8 +3936,8 @@ ac_cxx_conftest_cxx11_main='
}
{
// Unicode literals
- auto const *utf8 = u8"UTF-8 string \u2500";
- char16_t const *utf16 = u"UTF-16 string \u2500";
+ char const *utf8 = u8"UTF-8 string \u2500";
+ char16_t const *utf16 = u"UTF-8 string \u2500";
char32_t const *utf32 = U"UTF-32 string \u2500";
}
'
@@ -19518,9 +19518,6 @@ printf "%s\n" "no" >&6; }
PKG_CONFIG=""
fi
fi
-if test -z "$PKG_CONFIG"; then
- as_fn_error $? "pkg-config not found" "$LINENO" 5
-fi
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for bison that supports parse-param" >&5
printf %s "checking for bison that supports parse-param... " >&6; }
@@ -21213,12 +21210,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- OPENSSL_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "openssl >= 1.1.0" 2>&1`
+ OPENSSL_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "openssl >= 1.1.0" 2>&1`
else
- OPENSSL_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "openssl >= 1.1.0" 2>&1`
+ OPENSSL_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "openssl >= 1.1.0" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$OPENSSL_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$OPENSSL_PKG_ERRORS" >&5
PBX_OPENSSL=0
@@ -21232,8 +21229,8 @@ printf "%s\n" "no" >&6; }
else
- OPENSSL_CFLAGS=$pkg_cv_OPENSSL_CFLAGS
- OPENSSL_LIBS=$pkg_cv_OPENSSL_LIBS
+ OPENSSL_CFLAGS=$pkg_cv_OPENSSL_CFLAGS
+ OPENSSL_LIBS=$pkg_cv_OPENSSL_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
@@ -21304,12 +21301,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- OPENSSL_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "openssl11" 2>&1`
+ OPENSSL_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "openssl11" 2>&1`
else
- OPENSSL_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "openssl11" 2>&1`
+ OPENSSL_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "openssl11" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$OPENSSL_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$OPENSSL_PKG_ERRORS" >&5
PBX_OPENSSL=0
@@ -21323,8 +21320,8 @@ printf "%s\n" "no" >&6; }
else
- OPENSSL_CFLAGS=$pkg_cv_OPENSSL_CFLAGS
- OPENSSL_LIBS=$pkg_cv_OPENSSL_LIBS
+ OPENSSL_CFLAGS=$pkg_cv_OPENSSL_CFLAGS
+ OPENSSL_LIBS=$pkg_cv_OPENSSL_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
@@ -21400,12 +21397,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- OPENSSL_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "openssl" 2>&1`
+ OPENSSL_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "openssl" 2>&1`
else
- OPENSSL_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "openssl" 2>&1`
+ OPENSSL_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "openssl" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$OPENSSL_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$OPENSSL_PKG_ERRORS" >&5
PBX_OPENSSL=0
@@ -21419,8 +21416,8 @@ printf "%s\n" "no" >&6; }
else
- OPENSSL_CFLAGS=$pkg_cv_OPENSSL_CFLAGS
- OPENSSL_LIBS=$pkg_cv_OPENSSL_LIBS
+ OPENSSL_CFLAGS=$pkg_cv_OPENSSL_CFLAGS
+ OPENSSL_LIBS=$pkg_cv_OPENSSL_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
@@ -22066,6 +22063,9 @@ printf "%s\n" "#define HAVE_PJSIP_AUTH_CLT_DEINIT 1" >>confdefs.h
printf "%s\n" "#define HAVE_PJSIP_TSX_LAYER_FIND_TSX2 1" >>confdefs.h
+printf "%s\n" "#define HAVE_PJSIP_ENDPT_SEND_REQUEST2 1" >>confdefs.h
+
+
printf "%s\n" "#define HAVE_PJSIP_INV_ACCEPT_MULTIPLE_SDP_ANSWERS 1" >>confdefs.h
@@ -26040,12 +26040,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- LIBEDIT_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libedit" 2>&1`
+ LIBEDIT_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libedit" 2>&1`
else
- LIBEDIT_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libedit" 2>&1`
+ LIBEDIT_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libedit" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$LIBEDIT_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$LIBEDIT_PKG_ERRORS" >&5
PBX_LIBEDIT=0
@@ -26059,8 +26059,8 @@ printf "%s\n" "no" >&6; }
else
- LIBEDIT_CFLAGS=$pkg_cv_LIBEDIT_CFLAGS
- LIBEDIT_LIBS=$pkg_cv_LIBEDIT_LIBS
+ LIBEDIT_CFLAGS=$pkg_cv_LIBEDIT_CFLAGS
+ LIBEDIT_LIBS=$pkg_cv_LIBEDIT_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
@@ -26533,12 +26533,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- JANSSON_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "jansson >= 2.11" 2>&1`
+ JANSSON_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "jansson >= 2.11" 2>&1`
else
- JANSSON_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "jansson >= 2.11" 2>&1`
+ JANSSON_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "jansson >= 2.11" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$JANSSON_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$JANSSON_PKG_ERRORS" >&5
PBX_JANSSON=0
@@ -26552,8 +26552,8 @@ printf "%s\n" "no" >&6; }
else
- JANSSON_CFLAGS=$pkg_cv_JANSSON_CFLAGS
- JANSSON_LIBS=$pkg_cv_JANSSON_LIBS
+ JANSSON_CFLAGS=$pkg_cv_JANSSON_CFLAGS
+ JANSSON_LIBS=$pkg_cv_JANSSON_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
@@ -26777,12 +26777,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- LIBJWT_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libjwt >= $LIBJWT_VERSION" 2>&1`
+ LIBJWT_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libjwt >= $LIBJWT_VERSION" 2>&1`
else
- LIBJWT_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libjwt >= $LIBJWT_VERSION" 2>&1`
+ LIBJWT_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libjwt >= $LIBJWT_VERSION" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$LIBJWT_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$LIBJWT_PKG_ERRORS" >&5
PBX_LIBJWT=0
@@ -26796,8 +26796,8 @@ printf "%s\n" "no" >&6; }
else
- LIBJWT_CFLAGS=$pkg_cv_LIBJWT_CFLAGS
- LIBJWT_LIBS=$pkg_cv_LIBJWT_LIBS
+ LIBJWT_CFLAGS=$pkg_cv_LIBJWT_CFLAGS
+ LIBJWT_LIBS=$pkg_cv_LIBJWT_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
@@ -26966,12 +26966,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- LIBXML2_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libxml-2.0" 2>&1`
+ LIBXML2_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libxml-2.0" 2>&1`
else
- LIBXML2_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libxml-2.0" 2>&1`
+ LIBXML2_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libxml-2.0" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$LIBXML2_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$LIBXML2_PKG_ERRORS" >&5
PBX_LIBXML2=0
@@ -26985,8 +26985,8 @@ printf "%s\n" "no" >&6; }
else
- LIBXML2_CFLAGS=$pkg_cv_LIBXML2_CFLAGS
- LIBXML2_LIBS=$pkg_cv_LIBXML2_LIBS
+ LIBXML2_CFLAGS=$pkg_cv_LIBXML2_CFLAGS
+ LIBXML2_LIBS=$pkg_cv_LIBXML2_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
@@ -34430,12 +34430,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- ILBC_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libilbc < 3" 2>&1`
+ ILBC_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libilbc < 3" 2>&1`
else
- ILBC_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libilbc < 3" 2>&1`
+ ILBC_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libilbc < 3" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$ILBC_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$ILBC_PKG_ERRORS" >&5
PBX_ILBC=0
@@ -34449,8 +34449,8 @@ printf "%s\n" "no" >&6; }
else
- ILBC_CFLAGS=$pkg_cv_ILBC_CFLAGS
- ILBC_LIBS=$pkg_cv_ILBC_LIBS
+ ILBC_CFLAGS=$pkg_cv_ILBC_CFLAGS
+ ILBC_LIBS=$pkg_cv_ILBC_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
@@ -36806,12 +36806,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- NETSNMP_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "netsnmp-agent" 2>&1`
+ NETSNMP_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "netsnmp-agent" 2>&1`
else
- NETSNMP_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "netsnmp-agent" 2>&1`
+ NETSNMP_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "netsnmp-agent" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$NETSNMP_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$NETSNMP_PKG_ERRORS" >&5
PBX_NETSNMP=0
@@ -36825,8 +36825,8 @@ printf "%s\n" "no" >&6; }
else
- NETSNMP_CFLAGS=$pkg_cv_NETSNMP_CFLAGS
- NETSNMP_LIBS=$pkg_cv_NETSNMP_LIBS
+ NETSNMP_CFLAGS=$pkg_cv_NETSNMP_CFLAGS
+ NETSNMP_LIBS=$pkg_cv_NETSNMP_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
@@ -38329,12 +38329,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- PJPROJECT_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libpjproject" 2>&1`
+ PJPROJECT_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libpjproject" 2>&1`
else
- PJPROJECT_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libpjproject" 2>&1`
+ PJPROJECT_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libpjproject" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$PJPROJECT_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$PJPROJECT_PKG_ERRORS" >&5
PBX_PJPROJECT=0
@@ -38348,8 +38348,8 @@ printf "%s\n" "no" >&6; }
else
- PJPROJECT_CFLAGS=$pkg_cv_PJPROJECT_CFLAGS
- PJPROJECT_LIBS=$pkg_cv_PJPROJECT_LIBS
+ PJPROJECT_CFLAGS=$pkg_cv_PJPROJECT_CFLAGS
+ PJPROJECT_LIBS=$pkg_cv_PJPROJECT_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
@@ -39752,6 +39752,112 @@ fi
+if test "x${PBX_PJSIP_ENDPT_SEND_REQUEST2}" != "x1" -a "${USE_PJSIP_ENDPT_SEND_REQUEST2}" != "no"; then
+ pbxlibdir=""
+ # if --with-PJSIP_ENDPT_SEND_REQUEST2=DIR has been specified, use it.
+ if test "x${PJSIP_ENDPT_SEND_REQUEST2_DIR}" != "x"; then
+ if test -d ${PJSIP_ENDPT_SEND_REQUEST2_DIR}/lib; then
+ pbxlibdir="-L${PJSIP_ENDPT_SEND_REQUEST2_DIR}/lib"
+ else
+ pbxlibdir="-L${PJSIP_ENDPT_SEND_REQUEST2_DIR}"
+ fi
+ fi
+
+ ast_ext_lib_check_save_CFLAGS="${CFLAGS}"
+ CFLAGS="${CFLAGS} $PJPROJECT_CFLAGS"
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for pjsip_endpt_send_request2 in -lpjsip" >&5
+printf %s "checking for pjsip_endpt_send_request2 in -lpjsip... " >&6; }
+if test ${ac_cv_lib_pjsip_pjsip_endpt_send_request2+y}
+then :
+ printf %s "(cached) " >&6
+else case e in #(
+ e) ac_check_lib_save_LIBS=$LIBS
+LIBS="-lpjsip ${pbxlibdir} $PJPROJECT_LIB $LIBS"
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+/* Override any GCC internal prototype to avoid an error.
+ Use char because int might match the return type of a GCC
+ builtin and then its argument prototype would still apply.
+ The 'extern "C"' is for builds by C++ compilers;
+ although this is not generally supported in C code supporting it here
+ has little cost and some practical benefit (sr 110532). */
+#ifdef __cplusplus
+extern "C"
+#endif
+char pjsip_endpt_send_request2 (void);
+int
+main (void)
+{
+return pjsip_endpt_send_request2 ();
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+ ac_cv_lib_pjsip_pjsip_endpt_send_request2=yes
+else case e in #(
+ e) ac_cv_lib_pjsip_pjsip_endpt_send_request2=no ;;
+esac
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+ conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS ;;
+esac
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_pjsip_pjsip_endpt_send_request2" >&5
+printf "%s\n" "$ac_cv_lib_pjsip_pjsip_endpt_send_request2" >&6; }
+if test "x$ac_cv_lib_pjsip_pjsip_endpt_send_request2" = xyes
+then :
+ AST_PJSIP_ENDPT_SEND_REQUEST2_FOUND=yes
+else case e in #(
+ e) AST_PJSIP_ENDPT_SEND_REQUEST2_FOUND=no ;;
+esac
+fi
+
+ CFLAGS="${ast_ext_lib_check_save_CFLAGS}"
+
+
+ # now check for the header.
+ if test "${AST_PJSIP_ENDPT_SEND_REQUEST2_FOUND}" = "yes"; then
+ PJSIP_ENDPT_SEND_REQUEST2_LIB="${pbxlibdir} -lpjsip $PJPROJECT_LIB"
+ # if --with-PJSIP_ENDPT_SEND_REQUEST2=DIR has been specified, use it.
+ if test "x${PJSIP_ENDPT_SEND_REQUEST2_DIR}" != "x"; then
+ PJSIP_ENDPT_SEND_REQUEST2_INCLUDE="-I${PJSIP_ENDPT_SEND_REQUEST2_DIR}/include"
+ fi
+ PJSIP_ENDPT_SEND_REQUEST2_INCLUDE="${PJSIP_ENDPT_SEND_REQUEST2_INCLUDE} $PJPROJECT_CFLAGS"
+
+ # check for the header
+ ast_ext_lib_check_saved_CPPFLAGS="${CPPFLAGS}"
+ CPPFLAGS="${CPPFLAGS} ${PJSIP_ENDPT_SEND_REQUEST2_INCLUDE}"
+ ac_fn_c_check_header_compile "$LINENO" "pjsip.h" "ac_cv_header_pjsip_h" "$ac_includes_default"
+if test "x$ac_cv_header_pjsip_h" = xyes
+then :
+ PJSIP_ENDPT_SEND_REQUEST2_HEADER_FOUND=1
+else case e in #(
+ e) PJSIP_ENDPT_SEND_REQUEST2_HEADER_FOUND=0 ;;
+esac
+fi
+
+ CPPFLAGS="${ast_ext_lib_check_saved_CPPFLAGS}"
+
+ if test "x${PJSIP_ENDPT_SEND_REQUEST2_HEADER_FOUND}" = "x0" ; then
+ PJSIP_ENDPT_SEND_REQUEST2_LIB=""
+ PJSIP_ENDPT_SEND_REQUEST2_INCLUDE=""
+ else
+
+ PBX_PJSIP_ENDPT_SEND_REQUEST2=1
+ cat >>confdefs.h <<_ACEOF
+#define HAVE_PJSIP_ENDPT_SEND_REQUEST2 1
+_ACEOF
+
+ fi
+ fi
+fi
+
+
+
if test "x${PBX_PJSIP_TLS_TRANSPORT_RESTART}" != "x1" -a "${USE_PJSIP_TLS_TRANSPORT_RESTART}" != "no"; then
pbxlibdir=""
# if --with-PJSIP_TLS_TRANSPORT_RESTART=DIR has been specified, use it.
@@ -40128,12 +40234,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- PORTAUDIO_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "portaudio-2.0" 2>&1`
+ PORTAUDIO_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "portaudio-2.0" 2>&1`
else
- PORTAUDIO_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "portaudio-2.0" 2>&1`
+ PORTAUDIO_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "portaudio-2.0" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$PORTAUDIO_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$PORTAUDIO_PKG_ERRORS" >&5
PBX_PORTAUDIO=0
@@ -40147,8 +40253,8 @@ printf "%s\n" "no" >&6; }
else
- PORTAUDIO_CFLAGS=$pkg_cv_PORTAUDIO_CFLAGS
- PORTAUDIO_LIBS=$pkg_cv_PORTAUDIO_LIBS
+ PORTAUDIO_CFLAGS=$pkg_cv_PORTAUDIO_CFLAGS
+ PORTAUDIO_LIBS=$pkg_cv_PORTAUDIO_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
@@ -46481,12 +46587,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- GMIME_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "gmime-$ver" 2>&1`
+ GMIME_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "gmime-$ver" 2>&1`
else
- GMIME_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "gmime-$ver" 2>&1`
+ GMIME_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "gmime-$ver" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$GMIME_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$GMIME_PKG_ERRORS" >&5
PBX_GMIME=0
@@ -46500,8 +46606,8 @@ printf "%s\n" "no" >&6; }
else
- GMIME_CFLAGS=$pkg_cv_GMIME_CFLAGS
- GMIME_LIBS=$pkg_cv_GMIME_LIBS
+ GMIME_CFLAGS=$pkg_cv_GMIME_CFLAGS
+ GMIME_LIBS=$pkg_cv_GMIME_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
@@ -47849,12 +47955,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- GTK2_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "gtk+-2.0" 2>&1`
+ GTK2_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "gtk+-2.0" 2>&1`
else
- GTK2_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "gtk+-2.0" 2>&1`
+ GTK2_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "gtk+-2.0" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$GTK2_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$GTK2_PKG_ERRORS" >&5
PBX_GTK2=0
@@ -47868,8 +47974,8 @@ printf "%s\n" "no" >&6; }
else
- GTK2_CFLAGS=$pkg_cv_GTK2_CFLAGS
- GTK2_LIBS=$pkg_cv_GTK2_LIBS
+ GTK2_CFLAGS=$pkg_cv_GTK2_CFLAGS
+ GTK2_LIBS=$pkg_cv_GTK2_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
@@ -47960,12 +48066,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- SYSTEMD_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libsystemd" 2>&1`
+ SYSTEMD_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libsystemd" 2>&1`
else
- SYSTEMD_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libsystemd" 2>&1`
+ SYSTEMD_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libsystemd" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$SYSTEMD_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$SYSTEMD_PKG_ERRORS" >&5
PBX_SYSTEMD=0
@@ -47979,8 +48085,8 @@ printf "%s\n" "no" >&6; }
else
- SYSTEMD_CFLAGS=$pkg_cv_SYSTEMD_CFLAGS
- SYSTEMD_LIBS=$pkg_cv_SYSTEMD_LIBS
+ SYSTEMD_CFLAGS=$pkg_cv_SYSTEMD_CFLAGS
+ SYSTEMD_LIBS=$pkg_cv_SYSTEMD_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
diff --git a/configure.ac b/configure.ac
index 4bb4a4e7bd..0011dedc1a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2562,6 +2562,7 @@ if test "$USE_PJPROJECT" != "no" ; then
AST_EXT_LIB_CHECK([PJSIP_INV_SESSION_REF], [pjsip], [pjsip_inv_add_ref], [pjsip.h], [$PJPROJECT_LIB], [$PJPROJECT_CFLAGS])
AST_EXT_LIB_CHECK([PJSIP_AUTH_CLT_DEINIT], [pjsip], [pjsip_auth_clt_deinit], [pjsip.h], [$PJPROJECT_LIB], [$PJPROJECT_CFLAGS])
AST_EXT_LIB_CHECK([PJSIP_TSX_LAYER_FIND_TSX2], [pjsip], [pjsip_tsx_layer_find_tsx2], [pjsip.h], [$PJPROJECT_LIB], [$PJPROJECT_CFLAGS])
+ AST_EXT_LIB_CHECK([PJSIP_ENDPT_SEND_REQUEST2], [pjsip], [pjsip_endpt_send_request2], [pjsip.h], [$PJPROJECT_LIB], [$PJPROJECT_CFLAGS])
AST_EXT_LIB_CHECK([PJSIP_TLS_TRANSPORT_RESTART], [pjsip], [pjsip_tls_transport_restart], [pjsip.h], [$PJPROJECT_LIB], [$PJPROJECT_CFLAGS])
AST_EXT_LIB_CHECK([PJSIP_AUTH_NEW_DIGESTS], [pjsip], [pjsip_auth_get_algorithm_by_type], [pjsip.h], [$PJPROJECT_LIB], [$PJPROJECT_CFLAGS])
fi
diff --git a/include/asterisk/autoconfig.h.in b/include/asterisk/autoconfig.h.in
index 27291cda94..1a90f41cdb 100644
--- a/include/asterisk/autoconfig.h.in
+++ b/include/asterisk/autoconfig.h.in
@@ -642,6 +642,9 @@
/* Define if your system has the PJSIP_ENDPOINT_COMPACT_FORM headers. */
#undef HAVE_PJSIP_ENDPOINT_COMPACT_FORM
+/* Define if your system has pjsip_endpt_send_request2 declared. */
+#undef HAVE_PJSIP_ENDPT_SEND_REQUEST2
+
/* Define to 1 if PJPROJECT has the PJSIP EVSUB Group Lock support feature. */
#undef HAVE_PJSIP_EVSUB_GRP_LOCK
diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h
index 12cf847494..0eebc14b12 100644
--- a/include/asterisk/res_pjsip.h
+++ b/include/asterisk/res_pjsip.h
@@ -2695,6 +2695,23 @@ int ast_sip_send_out_of_dialog_request(pjsip_tx_data *tdata,
struct ast_sip_endpoint *endpoint, int timeout, void *token,
void (*callback)(void *token, pjsip_event *e));
+/*!
+ * \brief Send an Out-Of-Dialog SIP request and return its transaction
+ *
+ * This behaves the same as \ref ast_sip_send_out_of_dialog_request, but also
+ * returns the created transaction with its reference count incremented.
+ *
+ * \param[out] tsx The created transaction, or NULL if the send completed with
+ * an error callback before returning. If non-NULL, the caller must release the
+ * reference with pj_grp_lock_dec_ref(tsx->grp_lock) when it is no longer needed.
+ *
+ * \retval 0 Success
+ * \retval -1 Failure (out-of-dialog callback will not be called.)
+ */
+int ast_sip_send_out_of_dialog_request_with_tsx(pjsip_tx_data *tdata,
+ struct ast_sip_endpoint *endpoint, int timeout, void *token,
+ void (*callback)(void *token, pjsip_event *e), pjsip_transaction **tsx);
+
/*!
* \brief General purpose method for creating a SIP response
*
diff --git a/menuselect/configure b/menuselect/configure
index d3810d1ce3..825dd829b5 100755
--- a/menuselect/configure
+++ b/menuselect/configure
@@ -3880,9 +3880,6 @@ printf "%s\n" "no" >&6; }
PKG_CONFIG=""
fi
fi
-if test -z "$PKG_CONFIG"; then
- as_fn_error $? "pkg-config not found" "$LINENO" 5
-fi
ac_ext=c
ac_cpp='$CPP $CPPFLAGS'
@@ -5209,12 +5206,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- LIBXML2_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libxml-2.0" 2>&1`
+ LIBXML2_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libxml-2.0" 2>&1`
else
- LIBXML2_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libxml-2.0" 2>&1`
+ LIBXML2_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libxml-2.0" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$LIBXML2_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$LIBXML2_PKG_ERRORS" >&5
PBX_LIBXML2=0
@@ -5228,8 +5225,8 @@ printf "%s\n" "no" >&6; }
else
- LIBXML2_CFLAGS=$pkg_cv_LIBXML2_CFLAGS
- LIBXML2_LIBS=$pkg_cv_LIBXML2_LIBS
+ LIBXML2_CFLAGS=$pkg_cv_LIBXML2_CFLAGS
+ LIBXML2_LIBS=$pkg_cv_LIBXML2_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
@@ -5457,12 +5454,12 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- GTK2_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "gtk+-2.0" 2>&1`
+ GTK2_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "gtk+-2.0" 2>&1`
else
- GTK2_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "gtk+-2.0" 2>&1`
+ GTK2_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "gtk+-2.0" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$GTK2_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$GTK2_PKG_ERRORS" >&5
PBX_GTK2=0
@@ -5476,8 +5473,8 @@ printf "%s\n" "no" >&6; }
else
- GTK2_CFLAGS=$pkg_cv_GTK2_CFLAGS
- GTK2_LIBS=$pkg_cv_GTK2_LIBS
+ GTK2_CFLAGS=$pkg_cv_GTK2_CFLAGS
+ GTK2_LIBS=$pkg_cv_GTK2_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
diff --git a/res/res_pjsip.c b/res/res_pjsip.c
index 18c68a33c4..369cddb81f 100644
--- a/res/res_pjsip.c
+++ b/res/res_pjsip.c
@@ -1692,12 +1692,17 @@ static void send_request_wrapper_destructor(void *obj)
}
static pj_status_t endpt_send_request(struct ast_sip_endpoint *endpoint,
- pjsip_tx_data *tdata, pj_int32_t timeout, void *token, pjsip_endpt_send_callback cb)
+ pjsip_tx_data *tdata, pj_int32_t timeout, void *token, pjsip_endpt_send_callback cb,
+ pjsip_transaction **tsx)
{
struct send_request_wrapper *req_wrapper;
pj_status_t ret_val;
pjsip_endpoint *endpt = ast_sip_get_pjsip_endpoint();
+ if (tsx) {
+ *tsx = NULL;
+ }
+
if (!cb && token) {
/* Silly. Without a callback we cannot do anything with token. */
pjsip_tx_data_dec_ref(tdata);
@@ -1753,7 +1758,26 @@ static pj_status_t endpt_send_request(struct ast_sip_endpoint *endpoint,
* transaction callback is executed.
*/
ao2_ref(req_wrapper, +1);
- ret_val = pjsip_endpt_send_request(endpt, tdata, -1, req_wrapper, endpt_send_request_cb);
+ if (tsx) {
+#ifdef HAVE_PJSIP_ENDPT_SEND_REQUEST2
+ ret_val = pjsip_endpt_send_request2(endpt, tdata, -1, req_wrapper,
+ endpt_send_request_cb, tsx);
+#else
+ /*
+ * Fallback for PJPROJECT builds that do not provide
+ * pjsip_endpt_send_request2(). This means that any callers to
+ * endpt_send_request will not receive a transaction pointer back,
+ * and therefore will not be able to cancel the in-flight transaction.
+ */
+ ret_val = pjsip_endpt_send_request(endpt, tdata, -1, req_wrapper,
+ endpt_send_request_cb);
+ *tsx = NULL;
+#endif
+ } else {
+ ret_val = pjsip_endpt_send_request(endpt, tdata, -1, req_wrapper,
+ endpt_send_request_cb);
+ }
+
if (ret_val != PJ_SUCCESS) {
char errmsg[PJ_ERR_MSG_SIZE];
@@ -1870,7 +1894,7 @@ static int check_request_status(struct send_request_data *req_data, pjsip_event
if (res) {
res = endpt_send_request(endpoint, tdata, -1,
- req_data, send_request_cb) == PJ_SUCCESS;
+ req_data, send_request_cb, NULL) == PJ_SUCCESS;
}
ao2_ref(endpoint, -1);
@@ -1888,9 +1912,10 @@ static void send_request_cb(void *token, pjsip_event *e)
case PJSIP_EVENT_TRANSPORT_ERROR:
case PJSIP_EVENT_TIMER:
/*
- * Check the request status on transport error or timeout. A transport
- * error can occur when a TCP socket closes and that can be the result
- * of a 503. Also we may need to failover on a timeout (408).
+ * Check the request status on transport error, timeout or user event
+ * (triggered by a user cancel). A transport error can occur when a
+ * TCP socket closes and that can be the result of a 503. Also we may
+ * need to failover on a timeout (408).
*/
if (check_request_status(req_data, e)) {
return;
@@ -1921,6 +1946,14 @@ static void send_request_cb(void *token, pjsip_event *e)
return;
}
break;
+ case PJSIP_EVENT_USER:
+ /*
+ * User event is triggered by a user cancel. We don't need to do
+ * anything here other than invoke the callback.
+ */
+ ast_debug(3, "User event received for request to endpoint %s\n",
+ ast_sorcery_object_get_id(req_data->endpoint));
+ break;
default:
ast_log(LOG_ERROR, "Unexpected PJSIP event %u\n", e->body.tsx_state.type);
break;
@@ -1933,14 +1966,18 @@ static void send_request_cb(void *token, pjsip_event *e)
ao2_ref(req_data, -1);
}
-int ast_sip_send_out_of_dialog_request(pjsip_tx_data *tdata,
+int ast_sip_send_out_of_dialog_request_with_tsx(pjsip_tx_data *tdata,
struct ast_sip_endpoint *endpoint, int timeout, void *token,
- void (*callback)(void *token, pjsip_event *e))
+ void (*callback)(void *token, pjsip_event *e), pjsip_transaction **tsx)
{
struct ast_sip_supplement *supplement;
struct send_request_data *req_data;
struct ast_sip_contact *contact;
+ if (tsx) {
+ *tsx = NULL;
+ }
+
req_data = send_request_data_alloc(endpoint, token, callback);
if (!req_data) {
pjsip_tx_data_dec_ref(tdata);
@@ -1965,7 +2002,7 @@ int ast_sip_send_out_of_dialog_request(pjsip_tx_data *tdata,
ast_sip_mod_data_set(tdata->pool, tdata->mod_data, supplement_module.id, MOD_DATA_CONTACT, NULL);
ao2_cleanup(contact);
- if (endpt_send_request(endpoint, tdata, timeout, req_data, send_request_cb)
+ if (endpt_send_request(endpoint, tdata, timeout, req_data, send_request_cb, tsx)
!= PJ_SUCCESS) {
ao2_cleanup(req_data);
return -1;
@@ -1974,6 +2011,14 @@ int ast_sip_send_out_of_dialog_request(pjsip_tx_data *tdata,
return 0;
}
+int ast_sip_send_out_of_dialog_request(pjsip_tx_data *tdata,
+ struct ast_sip_endpoint *endpoint, int timeout, void *token,
+ void (*callback)(void *token, pjsip_event *e))
+{
+ return ast_sip_send_out_of_dialog_request_with_tsx(tdata, endpoint, timeout,
+ token, callback, NULL);
+}
+
int ast_sip_send_request(pjsip_tx_data *tdata, struct pjsip_dialog *dlg,
struct ast_sip_endpoint *endpoint, void *token,
void (*callback)(void *token, pjsip_event *e))
diff --git a/res/res_pjsip/pjsip_options.c b/res/res_pjsip/pjsip_options.c
index e8e58a99d2..491652bcf4 100644
--- a/res/res_pjsip/pjsip_options.c
+++ b/res/res_pjsip/pjsip_options.c
@@ -33,6 +33,7 @@
#include "asterisk/statsd.h"
#include "include/res_pjsip_private.h"
#include "asterisk/taskprocessor.h"
+#include "asterisk/taskpool.h"
/*
* This implementation for OPTIONS support is based around the idea
@@ -548,8 +549,157 @@ struct sip_options_contact_callback_data {
struct timeval rtt_start;
/*! \brief The new status of the contact */
enum ast_sip_contact_status_type status;
+ /*! \brief Whether the OPTIONS request(s) have been completed */
+ unsigned int completed;
+ /*! \brief Number of resolved targets waiting for a response */
+ unsigned int pending;
+ /*! \brief Transaction data for the resolve request */
+ pjsip_tx_data *resolve_tdata;
+ /*! \brief Endpoint used to create and send each target request */
+ struct ast_sip_endpoint *endpoint;
+ /*! \brief Transactions for all target OPTIONS requests in this batch */
+ AST_VECTOR(, pjsip_transaction *) target_transactions;
};
+struct sip_options_qualify_resolve_task_data {
+ struct sip_options_contact_callback_data *batch;
+ pj_status_t status;
+ unsigned int count;
+ pjsip_server_addresses addresses;
+ char *names[PJSIP_MAX_RESOLVED_ADDRESSES];
+};
+
+static void sip_options_queue_target_result(
+ struct sip_options_contact_callback_data *batch,
+ enum ast_sip_contact_status_type status);
+static void qualify_contact_cb(void *token, pjsip_event *e);
+
+static void sip_options_qualify_resolve_task_dtor(void *obj)
+{
+ struct sip_options_qualify_resolve_task_data *task_data = obj;
+ unsigned int idx;
+
+ for (idx = 0; idx < task_data->count; ++idx) {
+ ast_free(task_data->names[idx]);
+ }
+
+ ao2_ref(task_data->batch, -1);
+}
+
+/*! \brief Process a DNS resolution result on the AOR serializer */
+static int sip_options_qualify_resolve_task(void *obj)
+{
+ struct sip_options_qualify_resolve_task_data *task_data = obj;
+ struct sip_options_contact_callback_data *batch = task_data->batch;
+ unsigned int idx;
+
+ if (task_data->status != PJ_SUCCESS || !task_data->count) {
+ /* This failure is a de facto single pending result. */
+ batch->pending = 1;
+ sip_options_queue_target_result(batch, UNAVAILABLE);
+ ao2_ref(task_data, -1);
+ return 0;
+ }
+
+ /* Each resolved address must contribute exactly one batch result. */
+ batch->pending = task_data->count;
+
+ for (idx = 0; idx < task_data->count; ++idx) {
+ pjsip_tx_data *tdata;
+ pjsip_via_hdr *via;
+ pjsip_transaction *tsx = NULL;
+
+ if (ast_sip_create_request("OPTIONS", NULL, batch->endpoint, NULL,
+ batch->contact, &tdata)) {
+ sip_options_queue_target_result(batch, UNAVAILABLE);
+ continue;
+ }
+
+ if (!ast_strlen_zero(batch->contact->outbound_proxy) &&
+ ast_sip_set_outbound_proxy(tdata, batch->contact->outbound_proxy)) {
+ pjsip_tx_data_dec_ref(tdata);
+ sip_options_queue_target_result(batch, UNAVAILABLE);
+ continue;
+ }
+
+ /*
+ * Build a new request per resolved target so each send gets its own
+ * transaction and Via branch. Setting exactly one pre-resolved
+ * destination here avoids PJSIP re-resolving and choosing only the
+ * first DNS result.
+ */
+ tdata->dest_info.addr.count = 1;
+ tdata->dest_info.cur_addr = 0;
+ tdata->dest_info.addr.entry[0].type = task_data->addresses.entry[idx].type;
+ tdata->dest_info.addr.entry[0].priority = task_data->addresses.entry[idx].priority;
+ tdata->dest_info.addr.entry[0].weight = task_data->addresses.entry[idx].weight;
+ tdata->dest_info.addr.entry[0].addr_len = task_data->addresses.entry[idx].addr_len;
+ pj_sockaddr_cp(&tdata->dest_info.addr.entry[0].addr,
+ &task_data->addresses.entry[idx].addr);
+ pj_strdup(tdata->pool, &tdata->dest_info.addr.entry[0].name,
+ &task_data->addresses.entry[idx].name);
+ via = pjsip_msg_find_hdr(tdata->msg, PJSIP_H_VIA, NULL);
+ if (via) {
+ via->branch_param.slen = 0;
+ }
+
+ /* ast_sip_send_out_of_dialog_request_with_tsx consumes the tdata reference. */
+ if (ast_sip_send_out_of_dialog_request_with_tsx(tdata, batch->endpoint,
+ (int)(batch->aor_options->qualify_timeout * 1000),
+ ao2_bump(batch), qualify_contact_cb, &tsx)) {
+ ao2_ref(batch, -1);
+ sip_options_queue_target_result(batch, UNAVAILABLE);
+ } else if (tsx) {
+ /*
+ * It is possible that we will not get back a transaction under two circumstances:
+ * 1. The send failed.
+ * 2. The build of PJPROJECT used does not contain pjsip_endpt_send_request2() so
+ * there is no way to get an in-flight transaction reference for the OPTIONS.
+ *
+ * With no transaction references, we can not cancel in-flight OPTIONS requests when
+ * an endpoint responds with a 2xx response to one of the requests. This is not ideal
+ * and will result in unnecessary OPTIONS retransmissions being sent.
+ *
+ * The Append should never fail as we init the vector to PJSIP_MAX_RESOLVED_ADDRESSES
+ * and also limit the count of resolved addresses in sip_options_qualify_resolve_cb.
+ */
+ AST_VECTOR_APPEND(&batch->target_transactions, tsx);
+ }
+ }
+
+ ao2_ref(task_data, -1);
+ return 0;
+}
+
+static void sip_options_transaction_free(pjsip_transaction *tsx)
+{
+ if (tsx) {
+ pj_grp_lock_dec_ref(tsx->grp_lock);
+ }
+}
+
+/*! \brief Result of qualifying an individual address belonging to a contact resolved
+ from an A, AAAA or SRV record. */
+struct sip_options_target_result {
+ struct sip_options_contact_callback_data *batch;
+ enum ast_sip_contact_status_type status;
+};
+
+/*! \brief Terminate all in-flight target transactions for a batch */
+static void sip_options_cancel_target_transactions(
+ struct sip_options_contact_callback_data *batch)
+{
+ unsigned int idx;
+
+ for (idx = 0; idx < AST_VECTOR_SIZE(&batch->target_transactions); ++idx) {
+ pjsip_transaction *tsx = AST_VECTOR_GET(&batch->target_transactions, idx);
+
+ if (tsx) {
+ pjsip_tsx_terminate_async(tsx, PJSIP_SC_REQUEST_TERMINATED);
+ }
+ }
+}
+
/*!
* \brief Return the current state of an endpoint state compositor
* \pre The endpoint_state_compositor lock must be held.
@@ -782,7 +932,89 @@ static int sip_options_contact_status_notify_task(void *obj)
return 0;
}
-/*! \brief Callback for when we get a result from a SIP OPTIONS request (a response or a timeout) */
+/*! \brief Destructor for a resolved target's qualification result */
+static void sip_options_target_result_dtor(void *obj)
+{
+ struct sip_options_target_result *result = obj;
+ ao2_cleanup(result->batch);
+}
+
+/*!
+ * \brief Combine the result of one resolved target with the other targets
+ * \note Run by aor_options->serializer
+ */
+static int sip_options_target_result_task(void *obj)
+{
+ struct sip_options_target_result *result = obj;
+ struct sip_options_contact_callback_data *batch = result->batch;
+
+ if (!batch->completed) {
+ /* If the result is available then we can mark the batch as completed
+ and notify the AOR */
+ ast_assert(batch->pending);
+ --batch->pending;
+
+ if (result->status == AVAILABLE) {
+ /* At least one resolved target was available. */
+ batch->completed = 1;
+ batch->status = AVAILABLE;
+ sip_options_cancel_target_transactions(batch);
+ /* The notify task inherits this reference. */
+ sip_options_contact_status_notify_task(ao2_bump(batch));
+ } else if (!batch->pending) {
+ /* Every resolved target failed. */
+ batch->completed = 1;
+ batch->status = UNAVAILABLE;
+ sip_options_contact_status_notify_task(ao2_bump(batch));
+ }
+ }
+
+ ao2_ref(result, -1);
+ return 0;
+}
+
+/*! \brief Queue a resolved target's qualification result to be combined with the other targets */
+static void sip_options_queue_target_result(
+ struct sip_options_contact_callback_data *batch,
+ enum ast_sip_contact_status_type status)
+{
+ struct sip_options_target_result *result;
+
+ result = ao2_alloc_options(sizeof(*result), sip_options_target_result_dtor,
+ AO2_ALLOC_OPT_LOCK_NOLOCK);
+ if (!result) {
+ ast_log(LOG_WARNING, "Unable to allocate qualify target result for '%s'\n",
+ ast_sorcery_object_get_id(batch->contact));
+ return;
+ }
+
+ result->batch = ao2_bump(batch);
+ result->status = status;
+ /*
+ * Callbacks may arrive on non-serializer threads, so they queue one result per
+ * target onto the AOR serializer, the single owner of the batch's processing.
+ *
+ * Sends and callbacks can execute synchronously while we are already
+ * running on that serializer thread. Re-queueing to the same serializer in
+ * that case would defer updates and can break processing.
+ *
+ * Process inline when already on the same taskpool serializer to preserve
+ * immediate, in-order state transitions.
+ */
+ if (batch->aor_options->serializer == ast_taskpool_serializer_get_current()) {
+ sip_options_target_result_task(result);
+ return;
+ }
+
+ if (ast_sip_push_task(batch->aor_options->serializer,
+ sip_options_target_result_task, result)) {
+ ast_log(LOG_WARNING, "Unable to queue qualify target result for '%s'\n",
+ ast_sorcery_object_get_id(batch->contact));
+ ao2_ref(result, -1);
+ }
+}
+
+/*! \brief Callback for an OPTIONS response, timeout, or transport error */
static void qualify_contact_cb(void *token, pjsip_event *e)
{
struct sip_options_contact_callback_data *contact_callback_data = token;
@@ -804,20 +1036,21 @@ static void qualify_contact_cb(void *token, pjsip_event *e)
status = AVAILABLE;
}
break;
- }
-
- /* Update the callback data with the new status, this will get handled in the AOR serializer */
- contact_callback_data->status = status;
-
- if (ast_sip_push_task(contact_callback_data->aor_options->serializer,
- sip_options_contact_status_notify_task, contact_callback_data)) {
- ast_log(LOG_WARNING, "Unable to queue contact status update for '%s' on AOR '%s', state will be incorrect\n",
- ast_sorcery_object_get_id(contact_callback_data->contact),
- contact_callback_data->aor_options->name);
+ case PJSIP_EVENT_USER:
+ /*
+ * When one resolved target responds, we know the endpoint is available and we cancel the other
+ * outstanding transactions. The cancelled transactions will trigger this user event, which we
+ * can ignore as we already have a result for the contact.
+ */
+ ast_debug(3, "Received user event for contact '%s', ignoring\n",
+ ast_sorcery_object_get_id(contact_callback_data->contact));
+ /* Decrement reference count for the contact callback data and return as we have no result */
ao2_ref(contact_callback_data, -1);
+ return;
}
- /* The task inherited our reference so we don't unreference here */
+ sip_options_queue_target_result(contact_callback_data, status);
+ ao2_ref(contact_callback_data, -1);
}
/*! \brief Destructor for contact callback data */
@@ -827,11 +1060,24 @@ static void sip_options_contact_callback_data_dtor(void *obj)
ao2_cleanup(contact_callback_data->contact);
ao2_cleanup(contact_callback_data->aor_options);
+ ao2_cleanup(contact_callback_data->endpoint);
+ /*
+ * sip_options_qualify_resolve_cb should handle cleaning the resolve_tdata, but
+ * keep this fallback just in case.
+ */
+ if (contact_callback_data->resolve_tdata) {
+ pjsip_tx_data_dec_ref(contact_callback_data->resolve_tdata);
+ contact_callback_data->resolve_tdata = NULL;
+ }
+ AST_VECTOR_RESET(&contact_callback_data->target_transactions,
+ sip_options_transaction_free);
+ AST_VECTOR_FREE(&contact_callback_data->target_transactions);
}
/*! \brief Contact callback data allocator */
static struct sip_options_contact_callback_data *sip_options_contact_callback_data_alloc(
- struct ast_sip_contact *contact, struct sip_options_aor *aor_options)
+ struct ast_sip_contact *contact, struct sip_options_aor *aor_options,
+ struct ast_sip_endpoint *endpoint, pjsip_tx_data *resolve_tdata)
{
struct sip_options_contact_callback_data *contact_callback_data;
@@ -841,13 +1087,151 @@ static struct sip_options_contact_callback_data *sip_options_contact_callback_da
return NULL;
}
+ if (AST_VECTOR_INIT(&contact_callback_data->target_transactions, PJSIP_MAX_RESOLVED_ADDRESSES)) {
+ ao2_ref(contact_callback_data, -1);
+ return NULL;
+ }
+
contact_callback_data->contact = ao2_bump(contact);
contact_callback_data->aor_options = ao2_bump(aor_options);
+ contact_callback_data->endpoint = ao2_bump(endpoint);
+ contact_callback_data->resolve_tdata = resolve_tdata;
contact_callback_data->rtt_start = ast_tvnow();
return contact_callback_data;
}
+/*!
+ * \brief Apply a request's transport selector to its DNS destination
+ *
+ * pjsip_get_request_dest builds the destination from the Request-URI and
+ * may miss an outbound transport/listener.
+ * Before manual DNS resolution, apply the selected transport to the
+ * destination and check for mismatches so we use the correct transport
+ * for the request.
+ */
+static pj_status_t sip_options_apply_transport_to_destination(
+ const pjsip_tx_data *tdata, pjsip_host_info *destination)
+{
+ pjsip_transport_type_e transport_type = PJSIP_TRANSPORT_UNSPECIFIED;
+
+ if ((tdata->tp_sel.type != PJSIP_TPSELECTOR_TRANSPORT
+ && tdata->tp_sel.type != PJSIP_TPSELECTOR_LISTENER)
+ || !tdata->tp_sel.u.ptr) {
+ return PJ_SUCCESS;
+ }
+
+ if (tdata->tp_sel.type == PJSIP_TPSELECTOR_TRANSPORT) {
+ transport_type = tdata->tp_sel.u.transport->key.type;
+ } else {
+ transport_type = tdata->tp_sel.u.listener->type;
+ }
+
+ if (destination->type != PJSIP_TRANSPORT_UNSPECIFIED
+ && ((destination->type | PJSIP_TRANSPORT_IPV6)
+ != (transport_type | PJSIP_TRANSPORT_IPV6))) {
+ return PJSIP_ETPNOTSUITABLE;
+ }
+
+ destination->type = transport_type;
+ return PJ_SUCCESS;
+}
+
+/*! \brief DNS callback used to fan out a qualify to every resolved target */
+static void sip_options_qualify_resolve_cb(pj_status_t status, void *token,
+ const pjsip_server_addresses *addresses)
+{
+ struct sip_options_contact_callback_data *batch = token;
+ struct sip_options_qualify_resolve_task_data *task_data;
+ unsigned int idx;
+
+ task_data = ao2_alloc_options(sizeof(*task_data),
+ sip_options_qualify_resolve_task_dtor, AO2_ALLOC_OPT_LOCK_NOLOCK);
+ if (!task_data) {
+ /*
+ * Resolver callbacks are not guaranteed to run on the AOR serializer,
+ * so on allocation failure we must log and drop to avoid potential state
+ * issues.
+ */
+ ast_log(LOG_WARNING,
+ "Unable to allocate qualify resolve task for '%s'\n",
+ ast_sorcery_object_get_id(batch->contact));
+ ao2_ref(batch, -1);
+ return;
+ }
+
+ memset(task_data, 0, sizeof(*task_data));
+ task_data->batch = ao2_bump(batch);
+ task_data->status = status;
+
+ if (status != PJ_SUCCESS || !addresses || !addresses->count) {
+ if (batch->resolve_tdata) {
+ pjsip_tx_data_dec_ref(batch->resolve_tdata);
+ batch->resolve_tdata = NULL;
+ }
+
+ if (ast_sip_push_task(batch->aor_options->serializer,
+ sip_options_qualify_resolve_task, task_data)) {
+ ast_log(LOG_WARNING,
+ "Unable to queue qualify resolve task for '%s'\n",
+ ast_sorcery_object_get_id(batch->contact));
+ ao2_ref(task_data, -1);
+ }
+ ao2_ref(batch, -1);
+ return;
+ }
+
+ /* Each resolved address must contribute exactly one batch result. */
+ task_data->count = addresses->count;
+ if (task_data->count > PJSIP_MAX_RESOLVED_ADDRESSES) {
+ task_data->count = PJSIP_MAX_RESOLVED_ADDRESSES;
+ }
+
+ for (idx = 0; idx < task_data->count; ++idx) {
+ char *name_copy;
+
+ name_copy = ast_malloc(addresses->entry[idx].name.slen + 1);
+ if (!name_copy) {
+ task_data->status = PJ_ENOMEM;
+ task_data->count = idx;
+ break;
+ }
+
+ ast_copy_pj_str(name_copy, &addresses->entry[idx].name,
+ addresses->entry[idx].name.slen + 1);
+ task_data->names[idx] = name_copy;
+ task_data->addresses.entry[idx] = addresses->entry[idx];
+ task_data->addresses.entry[idx].name.ptr = name_copy;
+ task_data->addresses.entry[idx].name.slen = addresses->entry[idx].name.slen;
+ }
+
+ if (task_data->status == PJ_SUCCESS) {
+ task_data->addresses.count = task_data->count;
+ }
+
+ if (batch->resolve_tdata) {
+ pjsip_tx_data_dec_ref(batch->resolve_tdata);
+ batch->resolve_tdata = NULL;
+ }
+
+ /*
+ * Push the qualify resolve task to the AOR serializer to avoid potential race
+ * conditions between the resolve callback and the qualify contact task.
+ */
+ if (ast_sip_push_task(batch->aor_options->serializer,
+ sip_options_qualify_resolve_task, task_data)) {
+ ast_log(LOG_WARNING,
+ "Unable to queue qualify resolve task for '%s'\n",
+ ast_sorcery_object_get_id(batch->contact));
+ ao2_ref(task_data, -1);
+ } else {
+ ao2_ref(batch, -1);
+ return;
+ }
+
+ ao2_ref(batch, -1);
+}
+
/*! \brief Send a SIP OPTIONS request for a contact */
static int sip_options_qualify_contact(void *obj, void *arg, int flags)
{
@@ -855,6 +1239,7 @@ static int sip_options_qualify_contact(void *obj, void *arg, int flags)
struct sip_options_aor *aor_options = arg;
RAII_VAR(struct ast_sip_endpoint *, endpoint, NULL, ao2_cleanup);
pjsip_tx_data *tdata;
+ pjsip_host_info destination;
struct ast_sip_contact_status *contact_status;
struct sip_options_contact_callback_data *contact_callback_data;
@@ -878,7 +1263,13 @@ static int sip_options_qualify_contact(void *obj, void *arg, int flags)
return 0;
}
- if (ast_sip_create_request("OPTIONS", NULL, endpoint, NULL, contact, &tdata)) {
+ /*
+ * Create a request solely for DNS resolution. Its pool keeps the resolver strings alive,
+ * and passing a URI with no contact avoids leaving a contact reference behind in case
+ * resolution fails before it can send.
+ */
+ if (ast_sip_create_request("OPTIONS", NULL, endpoint, contact->uri, NULL,
+ &tdata)) {
ast_log(LOG_ERROR, "Unable to create request to qualify contact %s on AOR %s\n",
contact->uri, aor_options->name);
return 0;
@@ -902,7 +1293,8 @@ static int sip_options_qualify_contact(void *obj, void *arg, int flags)
}
ao2_ref(contact_status, -1);
- contact_callback_data = sip_options_contact_callback_data_alloc(contact, aor_options);
+ contact_callback_data = sip_options_contact_callback_data_alloc(contact,
+ aor_options, endpoint, tdata);
if (!contact_callback_data) {
ast_log(LOG_ERROR, "Unable to create object to contain callback data for contact %s on AOR %s\n",
contact->uri, aor_options->name);
@@ -910,14 +1302,25 @@ static int sip_options_qualify_contact(void *obj, void *arg, int flags)
return 0;
}
- if (ast_sip_send_out_of_dialog_request(tdata, endpoint,
- (int)(aor_options->qualify_timeout * 1000), contact_callback_data,
- qualify_contact_cb)) {
- ast_log(LOG_ERROR, "Unable to send request to qualify contact %s on AOR %s\n",
+
+ /*
+ * Resolve the contact destination explicitly via DNS, then create and send
+ * an independent OPTIONS for each returned transport/address/port tuple.
+ * Keep the resolver-only tdata alive because its pool owns strings used
+ * by the asynchronous resolver.
+ */
+ if (pjsip_get_request_dest(tdata, &destination) != PJ_SUCCESS
+ || sip_options_apply_transport_to_destination(tdata, &destination)
+ != PJ_SUCCESS) {
+ ast_log(LOG_ERROR, "Unable to resolve destination to qualify contact %s on AOR %s\n",
contact->uri, aor_options->name);
ao2_ref(contact_callback_data, -1);
+ return 0;
}
+ pjsip_endpt_resolve(ast_sip_get_pjsip_endpoint(), tdata->pool, &destination,
+ contact_callback_data, sip_options_qualify_resolve_cb);
+
return 0;
}
diff --git a/third-party/pjproject/configure.m4 b/third-party/pjproject/configure.m4
index 8c3c50fcf2..6bd84abca1 100644
--- a/third-party/pjproject/configure.m4
+++ b/third-party/pjproject/configure.m4
@@ -133,6 +133,7 @@ AC_DEFUN([_PJPROJECT_CONFIGURE],
AC_DEFINE([HAVE_PJSIP_INV_SESSION_REF], 1, [Define if your system has PJSIP_INV_SESSION_REF])
AC_DEFINE([HAVE_PJSIP_AUTH_CLT_DEINIT], 1, [Define if your system has pjsip_auth_clt_deinit declared.])
AC_DEFINE([HAVE_PJSIP_TSX_LAYER_FIND_TSX2], 1, [Define if your system has pjsip_tsx_layer_find_tsx2 declared.])
+ AC_DEFINE([HAVE_PJSIP_ENDPT_SEND_REQUEST2], 1, [Define if your system has pjsip_endpt_send_request2 declared.])
AC_DEFINE([HAVE_PJSIP_INV_ACCEPT_MULTIPLE_SDP_ANSWERS], 1, [Define if your system has HAVE_PJSIP_INV_ACCEPT_MULTIPLE_SDP_ANSWERS declared.])
AC_DEFINE([HAVE_PJSIP_ENDPOINT_COMPACT_FORM], 1, [Define if your system has HAVE_PJSIP_ENDPOINT_COMPACT_FORM declared.])
AC_DEFINE([HAVE_PJSIP_TRANSPORT_DISABLE_CONNECTION_REUSE], 1, [Define if your system has HAVE_PJSIP_TRANSPORT_DISABLE_CONNECTION_REUSE declared])
diff --git a/third-party/pjproject/patches/0030-Add_pjsip_endpt_send_request2.patch b/third-party/pjproject/patches/0030-Add_pjsip_endpt_send_request2.patch
new file mode 100644
index 0000000000..c2c2d190b1
--- /dev/null
+++ b/third-party/pjproject/patches/0030-Add_pjsip_endpt_send_request2.patch
@@ -0,0 +1,458 @@
+From f1a1cff26613b5c5ce4087afbf5f3982f09e2765 Mon Sep 17 00:00:00 2001
+From: nanang <nanang@teluu.com>
+Date: Tue, 18 Aug 2026 08:40:44 +0700
+Subject: [PATCH 1/3] pjsip: add pjsip_endpt_send_request2() returning the UAC
+ transaction
+
+pjsip_endpt_send_request() does not return any handle to the transaction
+it creates, so an application has no way to abandon an out-of-dialog
+request, e.g. to stop the retransmissions of an OPTIONS request when the
+response is no longer needed.
+
+Add pjsip_endpt_send_request2(), which optionally returns the created
+transaction with its reference counter incremented, following the same
+pattern as pjsip_endpt_respond(). Application can then terminate the
+request using the existing pjsip_tsx_terminate_async(). The transaction
+is returned before the request is sent, as the completion callback may
+already be invoked from another thread once it is sent.
+
+Co-Authored-By Claude Code
+---
+ pjsip/include/pjsip/sip_util.h | 48 ++++++++++
+ pjsip/src/pjsip/sip_util_statefull.c | 29 ++++++
+ pjsip/src/test/tsx_basic_test.c | 129 +++++++++++++++++++++++++++
+ 3 files changed, 206 insertions(+)
+
+diff --git a/pjsip/include/pjsip/sip_util.h b/pjsip/include/pjsip/sip_util.h
+index bba120fc74..647e40b457 100644
+--- a/pjsip/include/pjsip/sip_util.h
++++ b/pjsip/include/pjsip/sip_util.h
+@@ -793,6 +793,54 @@ PJ_DECL(pj_status_t) pjsip_endpt_send_request( pjsip_endpoint *endpt,
+ void *token,
+ pjsip_endpt_send_callback cb);
+
++/**
++ * Variant of #pjsip_endpt_send_request() which can also return the
++ * transaction created to send the request. Application may use the
++ * transaction, for example, to terminate the request before any response
++ * is received, e.g. to stop the retransmissions of an out-of-dialog request
++ * such as OPTIONS. Note that terminating the transaction only abandons the
++ * request locally, nothing is sent to the network, as CANCEL is not
++ * applicable to non-INVITE requests (see RFC 3261 section 9.1).
++ *
++ * To terminate the transaction, application should use
++ * #pjsip_tsx_terminate_async() or #pjsip_tsx_terminate_async2(), which are
++ * safe to be called from any thread, including from within the callback
++ * \a cb itself. Terminating an already completed transaction is harmless.
++ *
++ * @param endpt The endpoint instance.
++ * @param tdata The transmit data to be sent.
++ * @param timeout Optional timeout for final response to be received, or -1
++ * if the transaction should not have a timeout restriction.
++ * The value is in miliseconds. Note that this is not
++ * implemented yet, so application needs to use its own timer
++ * to handle timeout.
++ * @param token Optional token to be associated with the transaction, and
++ * to be passed to the callback.
++ * @param cb Optional callback to be called when the transaction has
++ * received a final response. The callback will be called with
++ * the previously registered token and the event that triggers
++ * the completion of the transaction.
++ * @param p_tsx Optional pointer to receive the transaction which was
++ * created to send the request. If it is not NULL, on
++ * success it will be set to the transaction with its
++ * reference counter incremented, so application must
++ * release it using pj_grp_lock_dec_ref(tsx->grp_lock) once
++ * it no longer needs the transaction, e.g. after the
++ * callback \a cb is called. On failure, it will be set to
++ * NULL. Note that the callback \a cb may already be called
++ * before this function returns, so application must be
++ * ready for the transaction to be already completed by the
++ * time it inspects this argument.
++ *
++ * @return PJ_SUCCESS, or the appropriate error code.
++ */
++PJ_DECL(pj_status_t) pjsip_endpt_send_request2(pjsip_endpoint *endpt,
++ pjsip_tx_data *tdata,
++ pj_int32_t timeout,
++ void *token,
++ pjsip_endpt_send_callback cb,
++ pjsip_transaction **p_tsx);
++
+ /**
+ * @}
+ */
+diff --git a/pjsip/src/pjsip/sip_util_statefull.c b/pjsip/src/pjsip/sip_util_statefull.c
+index efa2bd8907..9d0ec18160 100644
+--- a/pjsip/src/pjsip/sip_util_statefull.c
++++ b/pjsip/src/pjsip/sip_util_statefull.c
+@@ -88,6 +88,17 @@ PJ_DEF(pj_status_t) pjsip_endpt_send_request( pjsip_endpoint *endpt,
+ pj_int32_t timeout,
+ void *token,
+ pjsip_endpt_send_callback cb)
++{
++ return pjsip_endpt_send_request2(endpt, tdata, timeout, token, cb, NULL);
++}
++
++
++PJ_DEF(pj_status_t) pjsip_endpt_send_request2( pjsip_endpoint *endpt,
++ pjsip_tx_data *tdata,
++ pj_int32_t timeout,
++ void *token,
++ pjsip_endpt_send_callback cb,
++ pjsip_transaction **p_tsx)
+ {
+ pjsip_transaction *tsx;
+ struct tsx_data *tsx_data;
+@@ -100,6 +111,8 @@ PJ_DEF(pj_status_t) pjsip_endpt_send_request( pjsip_endpoint *endpt,
+
+ PJ_UNUSED_ARG(timeout);
+
++ if (p_tsx) *p_tsx = NULL;
++
+ status = pjsip_tsx_create_uac(&mod_stateful_util, tdata, &tsx);
+ if (status != PJ_SUCCESS) {
+ pjsip_tx_data_dec_ref(tdata);
+@@ -119,8 +132,24 @@ PJ_DEF(pj_status_t) pjsip_endpt_send_request( pjsip_endpoint *endpt,
+ */
+ pj_grp_lock_add_ref(tsx->grp_lock);
+
++ /* Return the transaction before sending the request, as the callback may
++ * already be called from another thread once the request is sent.
++ */
++ if (p_tsx) {
++ pj_grp_lock_add_ref(tsx->grp_lock);
++ *p_tsx = tsx;
++ }
++
+ status = pjsip_tsx_send_msg(tsx, NULL);
+ if (status != PJ_SUCCESS) {
++ /* Release the reference and reset the output before terminating the
++ * transaction, as terminating it may invoke the callback which may
++ * destroy the storage of the output argument.
++ */
++ if (p_tsx) {
++ *p_tsx = NULL;
++ pj_grp_lock_dec_ref(tsx->grp_lock);
++ }
+ pjsip_tx_data_dec_ref(tdata);
+ pjsip_tsx_terminate(tsx, tsx->status_code? tsx->status_code:
+ PJSIP_SC_SERVICE_UNAVAILABLE);
+diff --git a/pjsip/src/test/tsx_basic_test.c b/pjsip/src/test/tsx_basic_test.c
+index 01bd2a86bb..caa59ece0d 100644
+--- a/pjsip/src/test/tsx_basic_test.c
++++ b/pjsip/src/test/tsx_basic_test.c
+@@ -23,11 +23,23 @@
+
+ #define THIS_FILE "tsx_basic_test.c"
+
++struct send_request2_data
++{
++ pj_bool_t cb_called;
++ int status_code;
++};
++
+ static struct tsx_basic_test_global_t
+ {
+ char TARGET_URI[PJSIP_MAX_URL_SIZE];
+ char FROM_URI[PJSIP_MAX_URL_SIZE];
+ pjsip_transport *tp;
++
++ /* Must outlive the test function, as the callback may still be called
++ * after the test returns, e.g. when the test fails to terminate the
++ * transaction.
++ */
++ struct send_request2_data sr;
+ } g[MAX_TSX_TESTS];
+
+
+@@ -159,6 +171,116 @@ static int double_terminate(unsigned tid)
+ return PJ_SUCCESS;
+ }
+
++static void send_request2_cb(void *token, pjsip_event *e)
++{
++ struct send_request2_data *sr = (struct send_request2_data*)token;
++
++ sr->cb_called = PJ_TRUE;
++ if (e->type == PJSIP_EVENT_TSX_STATE && e->body.tsx_state.tsx)
++ sr->status_code = e->body.tsx_state.tsx->status_code;
++}
++
++/* Test terminating a request sent using pjsip_endpt_send_request2(), e.g.
++ * to stop the retransmissions before any response is received.
++ */
++static int send_request2_test(unsigned tid)
++{
++ struct send_request2_data *sr = &g[tid].sr;
++ pj_str_t target, from, tsx_key;
++ pjsip_tx_data *tdata;
++ pjsip_transaction *tsx = NULL;
++ pjsip_tpselector tp_sel;
++ pj_bool_t prev_discard;
++ pj_status_t status;
++ int rc = 0;
++
++ PJ_LOG(3,(THIS_FILE, " send request2 and terminate test"));
++
++ target = pj_str(g[tid].TARGET_URI);
++ from = pj_str(g[tid].FROM_URI);
++
++ /* Discard the outgoing request, so it will never be answered. */
++ pjsip_loop_set_discard(g[tid].tp, PJ_TRUE, &prev_discard);
++
++ status = pjsip_endpt_create_request(endpt, &pjsip_options_method, &target,
++ &from, &target, NULL, NULL, -1, NULL,
++ &tdata);
++ if (status != PJ_SUCCESS) {
++ app_perror(" error: unable to create request", status);
++ rc = -200;
++ goto on_return;
++ }
++
++ pj_bzero(&tp_sel, sizeof(tp_sel));
++ tp_sel.type = PJSIP_TPSELECTOR_TRANSPORT;
++ tp_sel.u.transport = g[tid].tp;
++ pjsip_tx_data_set_transport(tdata, &tp_sel);
++
++ pj_bzero(sr, sizeof(*sr));
++
++ status = pjsip_endpt_send_request2(endpt, tdata, -1, sr,
++ &send_request2_cb, &tsx);
++ if (status != PJ_SUCCESS) {
++ app_perror(" error: unable to send request", status);
++ rc = -210;
++ goto on_return;
++ }
++
++ if (tsx == NULL) {
++ PJ_LOG(3,(THIS_FILE, " error: transaction is not returned"));
++ rc = -220;
++ goto on_return;
++ }
++
++ /* Save the key, to verify that the transaction is unregistered later. */
++ pj_strdup_with_null(tsx->pool, &tsx_key, &tsx->transaction_key);
++
++ /* Nothing should complete the transaction by itself. */
++ flush_events(100);
++ if (sr->cb_called) {
++ PJ_LOG(3,(THIS_FILE, " error: transaction completed prematurely"));
++ rc = -230;
++ goto on_dec_ref;
++ }
++
++ status = pjsip_tsx_terminate_async(tsx, PJSIP_SC_REQUEST_TERMINATED);
++ if (status != PJ_SUCCESS) {
++ app_perror(" error: unable to terminate transaction", status);
++ rc = -240;
++ goto on_dec_ref;
++ }
++
++ flush_events(1000);
++
++ if (!sr->cb_called) {
++ PJ_LOG(3,(THIS_FILE, " error: callback is not called"));
++ rc = -250;
++ goto on_dec_ref;
++ }
++
++ if (sr->status_code != PJSIP_SC_REQUEST_TERMINATED) {
++ PJ_LOG(3,(THIS_FILE, " error: unexpected status code %d",
++ sr->status_code));
++ rc = -260;
++ goto on_dec_ref;
++ }
++
++ /* The transaction must have been unregistered from the transaction
++ * layer, while our reference keeps the instance alive.
++ */
++ if (pjsip_tsx_layer_find_tsx2(&tsx_key, PJ_FALSE) != NULL) {
++ PJ_LOG(3,(THIS_FILE, " error: transaction is still registered"));
++ rc = -270;
++ }
++
++on_dec_ref:
++ pj_grp_lock_dec_ref(tsx->grp_lock);
++
++on_return:
++ pjsip_loop_set_discard(g[tid].tp, prev_discard, NULL);
++ return rc;
++}
++
+ int tsx_basic_test(unsigned tid)
+ {
+ struct tsx_test_param *param = &tsx_test[tid];
+@@ -187,6 +309,13 @@ int tsx_basic_test(unsigned tid)
+ if (status != 0)
+ goto on_return;
+
++ /* This test needs the loop transport to blackhole the request. */
++ if (g[tid].tp) {
++ status = send_request2_test(tid);
++ if (status != 0)
++ goto on_return;
++ }
++
+ status = 0;
+
+ on_return:
+
+From 54e3c78c578abcdb3c2986de557673b48c36d725 Mon Sep 17 00:00:00 2001
+From: nanang <nanang@teluu.com>
+Date: Wed, 2 Sep 2026 11:22:59 +0700
+Subject: [PATCH 2/3] pjsip: hand over the send_request2() transaction only on
+ successful send
+
+pjsip_tsx_send_msg() may fail after the transaction has already been
+terminated and the completion callback invoked, e.g. when the stateless
+send completes synchronously with an error, which tsx_send_msg() then
+converts into a failure return (see the "Check if transaction is
+terminated" checks in sip_transaction.c). As the output argument was set
+before the send, the application callback could run while *p_tsx held a
+reference that the failure path then released. An application releasing
+that reference from within the callback, as the documentation suggested,
+would release it twice.
+
+Set *p_tsx and add the reference only after the send succeeds. The
+reference this function already holds keeps the transaction alive there,
+even when it has been completed by the callback, so nothing is lost and
+the failure path has nothing to undo. The callback does not need the
+argument either, as the event already carries the transaction.
+
+Co-Authored-By Claude Code
+---
+ pjsip/include/pjsip/sip_util.h | 17 +++++++++--------
+ pjsip/src/pjsip/sip_util_statefull.c | 22 ++++++++--------------
+ 2 files changed, 17 insertions(+), 22 deletions(-)
+
+diff --git a/pjsip/include/pjsip/sip_util.h b/pjsip/include/pjsip/sip_util.h
+index 647e40b457..b8d6eb0537 100644
+--- a/pjsip/include/pjsip/sip_util.h
++++ b/pjsip/include/pjsip/sip_util.h
+@@ -821,16 +821,17 @@ PJ_DECL(pj_status_t) pjsip_endpt_send_request( pjsip_endpoint *endpt,
+ * the previously registered token and the event that triggers
+ * the completion of the transaction.
+ * @param p_tsx Optional pointer to receive the transaction which was
+- * created to send the request. If it is not NULL, on
+- * success it will be set to the transaction with its
++ * created to send the request. On failure it will be set
++ * to NULL, on success to the transaction with its
+ * reference counter incremented, so application must
+ * release it using pj_grp_lock_dec_ref(tsx->grp_lock) once
+- * it no longer needs the transaction, e.g. after the
+- * callback \a cb is called. On failure, it will be set to
+- * NULL. Note that the callback \a cb may already be called
+- * before this function returns, so application must be
+- * ready for the transaction to be already completed by the
+- * time it inspects this argument.
++ * it no longer needs the transaction. The reference is
++ * handed over only after this function returns PJ_SUCCESS,
++ * so it must not be released from within the callback
++ * \a cb, which may be called before this function returns,
++ * i.e. the transaction may already be completed by the time
++ * application inspects this argument. The transaction is
++ * also available in the event given to the callback.
+ *
+ * @return PJ_SUCCESS, or the appropriate error code.
+ */
+diff --git a/pjsip/src/pjsip/sip_util_statefull.c b/pjsip/src/pjsip/sip_util_statefull.c
+index 9d0ec18160..ee7d62995e 100644
+--- a/pjsip/src/pjsip/sip_util_statefull.c
++++ b/pjsip/src/pjsip/sip_util_statefull.c
+@@ -132,24 +132,18 @@ PJ_DEF(pj_status_t) pjsip_endpt_send_request2( pjsip_endpoint *endpt,
+ */
+ pj_grp_lock_add_ref(tsx->grp_lock);
+
+- /* Return the transaction before sending the request, as the callback may
+- * already be called from another thread once the request is sent.
+- */
+- if (p_tsx) {
+- pj_grp_lock_add_ref(tsx->grp_lock);
+- *p_tsx = tsx;
+- }
+-
+ status = pjsip_tsx_send_msg(tsx, NULL);
+- if (status != PJ_SUCCESS) {
+- /* Release the reference and reset the output before terminating the
+- * transaction, as terminating it may invoke the callback which may
+- * destroy the storage of the output argument.
++ if (status == PJ_SUCCESS) {
++ /* Only hand over the transaction after a successful send, as the
++ * send may fail after the callback has been called. Our reference
++ * above keeps the transaction alive here, even when it has already
++ * been completed by the callback.
+ */
+ if (p_tsx) {
+- *p_tsx = NULL;
+- pj_grp_lock_dec_ref(tsx->grp_lock);
++ pj_grp_lock_add_ref(tsx->grp_lock);
++ *p_tsx = tsx;
+ }
++ } else {
+ pjsip_tx_data_dec_ref(tdata);
+ pjsip_tsx_terminate(tsx, tsx->status_code? tsx->status_code:
+ PJSIP_SC_SERVICE_UNAVAILABLE);
+
+From 4653d3c198155d4ee74e7651192ef4b57a00457d Mon Sep 17 00:00:00 2001
+From: nanang <nanang@teluu.com>
+Date: Wed, 2 Sep 2026 13:51:27 +0700
+Subject: [PATCH 3/3] pjsip: reset send_request2() output before the argument
+ checks
+
+The PJ_ASSERT_RETURN() checks return before the output was reset, so
+*p_tsx was left untouched when an argument is invalid or the transaction
+layer module is not registered, which contradicts the documented "on
+failure it will be set to NULL". Reset it first.
+
+Also document that, unlike the p_tsx output of pjsip_endpt_respond(),
+the transaction is returned with a reference the application must
+release.
+
+Co-Authored-By Claude Code
+---
+ pjsip/include/pjsip/sip_util.h | 5 +++++
+ pjsip/src/pjsip/sip_util_statefull.c | 7 +++++--
+ 2 files changed, 10 insertions(+), 2 deletions(-)
+
+diff --git a/pjsip/include/pjsip/sip_util.h b/pjsip/include/pjsip/sip_util.h
+index b8d6eb0537..5a3b873317 100644
+--- a/pjsip/include/pjsip/sip_util.h
++++ b/pjsip/include/pjsip/sip_util.h
+@@ -807,6 +807,11 @@ PJ_DECL(pj_status_t) pjsip_endpt_send_request( pjsip_endpoint *endpt,
+ * safe to be called from any thread, including from within the callback
+ * \a cb itself. Terminating an already completed transaction is harmless.
+ *
++ * Note that unlike the \a p_tsx output of #pjsip_endpt_respond(), which is
++ * returned without any reference added, the transaction returned by this
++ * function has its reference counter incremented, so application must
++ * release it once it no longer needs the transaction.
++ *
+ * @param endpt The endpoint instance.
+ * @param tdata The transmit data to be sent.
+ * @param timeout Optional timeout for final response to be received, or -1
+diff --git a/pjsip/src/pjsip/sip_util_statefull.c b/pjsip/src/pjsip/sip_util_statefull.c
+index ee7d62995e..806e2f84c1 100644
+--- a/pjsip/src/pjsip/sip_util_statefull.c
++++ b/pjsip/src/pjsip/sip_util_statefull.c
+@@ -104,6 +104,11 @@ PJ_DEF(pj_status_t) pjsip_endpt_send_request2( pjsip_endpoint *endpt,
+ struct tsx_data *tsx_data;
+ pj_status_t status;
+
++ /* Reset the output first, so it is also reset when the checks below
++ * fail.
++ */
++ if (p_tsx) *p_tsx = NULL;
++
+ PJ_ASSERT_RETURN(endpt && tdata && (timeout==-1 || timeout>0), PJ_EINVAL);
+
+ /* Check that transaction layer module is registered to endpoint */
+@@ -111,8 +116,6 @@ PJ_DEF(pj_status_t) pjsip_endpt_send_request2( pjsip_endpoint *endpt,
+
+ PJ_UNUSED_ARG(timeout);
+
+- if (p_tsx) *p_tsx = NULL;
+-
+ status = pjsip_tsx_create_uac(&mod_stateful_util, tdata, &tsx);
+ if (status != PJ_SUCCESS) {
+ pjsip_tx_data_dec_ref(tdata);