Commit 16bbc5e0d0 for asterisk.org

commit 16bbc5e0d0b06dff8e0e59196c48967285ab6796
Author: Alexis Chenard <achenard@quesys.com>
Date:   Mon Jun 1 20:38:28 2026 +0000

    res_pjsip: Add external_signaling_hostname transport option

    Adds a new transport option 'external_signaling_hostname' which allows
    a hostname or FQDN to be used in SIP Contact and Via headers instead of
    the automatically determined IP address. This is useful when a remote
    SIP endpoint requires a fully qualified domain name in these headers.

    The option is mutually exclusive with 'external_signaling_address' and
    an error is raised at transport load time if both are set simultaneously.

    Resolves: #1749

    UserNote: A new pjsip.conf transport option 'external_signaling_hostname'
    has been added. When set, this value will be used in SIP Contact and Via
    headers instead of the automatically determined IP address. This option
    is mutually exclusive with 'external_signaling_address'.

diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample
index 3d346789d4..e01f3f756c 100644
--- a/configs/samples/pjsip.conf.sample
+++ b/configs/samples/pjsip.conf.sample
@@ -1203,6 +1203,14 @@
                                 ; "")
 ;external_signaling_port=0      ; External port for SIP signalling (default:
                                 ; "0")
+;external_signaling_hostname=   ; Value to use in SIP Contact and Via headers
+                                ; as hostname instead of the automatically
+                                ; determined IP address. Useful when a remote
+                                ; SIP endpoint requires a fully qualified domain
+                                ; name rather than an IP address. No validation
+                                ; is performed on the provided value. Cannot be
+                                ; set together with external_signaling_address.
+                                ; (default: "")
 ;local_net=     ; Network to consider local used for NAT purposes (default: "")
 ;password=      ; Password required for transport (default: "")
 ;protocol=udp   ; Protocol to use for SIP traffic (default: "udp")
diff --git a/contrib/ast-db-manage/config/versions/2285f2ace275_add_external_signaling_hostname.py b/contrib/ast-db-manage/config/versions/2285f2ace275_add_external_signaling_hostname.py
new file mode 100644
index 0000000000..106beea152
--- /dev/null
+++ b/contrib/ast-db-manage/config/versions/2285f2ace275_add_external_signaling_hostname.py
@@ -0,0 +1,20 @@
+"""Add external_signaling_hostname to ps_transports
+
+Revision ID: 2285f2ace275
+Revises: e89e30cee53f
+Create Date: 2026-06-03
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '2285f2ace275'
+down_revision = 'e89e30cee53f'
+
+from alembic import op
+import sqlalchemy as sa
+
+def upgrade():
+    op.add_column('ps_transports', sa.Column('external_signaling_hostname', sa.String(40)))
+
+def downgrade():
+    op.drop_column('ps_transports', 'external_signaling_hostname')
\ No newline at end of file
diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h
index 34e0bb9d4c..61cb1ce041 100644
--- a/include/asterisk/res_pjsip.h
+++ b/include/asterisk/res_pjsip.h
@@ -236,6 +236,8 @@ struct ast_sip_transport {
 		AST_STRING_FIELD(external_media_address);
 		/*! Optional domain to use for messages if provided could not be found */
 		AST_STRING_FIELD(domain);
+		/*! Optional FQDN to use in SIP Contact and Via headers instead of external_signaling_address */
+		AST_STRING_FIELD(external_signaling_hostname);
 		);
 	/*! Type of transport */
 	enum ast_transport type;
diff --git a/res/res_pjsip/config_transport.c b/res/res_pjsip/config_transport.c
index 850dd21b5e..5139b75662 100644
--- a/res/res_pjsip/config_transport.c
+++ b/res/res_pjsip/config_transport.c
@@ -675,6 +675,15 @@ static int transport_apply(const struct ast_sorcery *sorcery, void *obj)
 	RAII_VAR(struct ast_variable *, changes, NULL, ast_variables_destroy);
 	pj_status_t res = -1;
 	int i;
+
+	/* Ensure external_signaling_address and external_signaling_hostname are mutually exclusive */
+	if (!ast_strlen_zero(transport->external_signaling_address) &&
+		!ast_strlen_zero(transport->external_signaling_hostname)) {
+		ast_log(LOG_ERROR, "Transport '%s' has both 'external_signaling_address' and "
+			"'external_signaling_hostname' set. Only one may be configured at a time.\n",
+			transport_id);
+		return -1;
+	}
 #define BIND_TRIES 3
 #define BIND_DELAY_US 100000

@@ -1856,6 +1865,7 @@ int ast_sip_initialize_sorcery_transport(void)
 	ast_sorcery_object_field_register(sorcery, "transport", "password", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_transport, password));
 	ast_sorcery_object_field_register(sorcery, "transport", "external_signaling_address", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_transport, external_signaling_address));
 	ast_sorcery_object_field_register(sorcery, "transport", "external_signaling_port", "0", OPT_UINT_T, PARSE_IN_RANGE, FLDSET(struct ast_sip_transport, external_signaling_port), 0, 65535);
+	ast_sorcery_object_field_register(sorcery, "transport", "external_signaling_hostname", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_transport, external_signaling_hostname));
 	ast_sorcery_object_field_register(sorcery, "transport", "external_media_address", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_transport, external_media_address));
 	ast_sorcery_object_field_register(sorcery, "transport", "domain", "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct ast_sip_transport, domain));
 	ast_sorcery_object_field_register_custom(sorcery, "transport", "verify_server", "", transport_tls_bool_handler, verify_server_to_str, NULL, 0, 0);
diff --git a/res/res_pjsip/pjsip_config.xml b/res/res_pjsip/pjsip_config.xml
index b2ef0bddfb..a0049be36c 100644
--- a/res/res_pjsip/pjsip_config.xml
+++ b/res/res_pjsip/pjsip_config.xml
@@ -2448,6 +2448,25 @@
 					</since>
 					<synopsis>External port for SIP signalling</synopsis>
 				</configOption>
+				<configOption name="external_signaling_hostname">
+					<since>
+						<version>20.21.0</version>
+						<version>22.11.0</version>
+						<version>23.5.0</version>
+					</since>
+					<synopsis>Value to use in SIP Contact and Via headers as hostname</synopsis>
+					<description><para>
+						When set, this value is used in the SIP Contact and Via
+						headers of outgoing messages instead of the automatically
+						determined IP address. This is useful when a remote SIP
+						endpoint requires a fully qualified domain name in these
+						headers rather than an IP address. No validation is performed
+						on the provided value. This option is mutually exclusive with
+    					<literal>external_signaling_address</literal> and both cannot
+    					be set simultaneously. If not set, existing behavior using
+						<literal>external_signaling_address</literal> is preserved.
+					</para></description>
+				</configOption>
 				<configOption name="method">
 					<since>
 						<version>12.2.0</version>
diff --git a/res/res_pjsip/pjsip_manager.xml b/res/res_pjsip/pjsip_manager.xml
index 548b974332..cdf36b66f3 100644
--- a/res/res_pjsip/pjsip_manager.xml
+++ b/res/res_pjsip/pjsip_manager.xml
@@ -223,6 +223,9 @@
 				<parameter name="ExternalSignalingPort">
 					<para><xi:include xpointer="xpointer(/docs/configInfo[@name='res_pjsip']/configFile[@name='pjsip.conf']/configObject[@name='transport']/configOption[@name='external_signaling_port']/synopsis/node())"/></para>
 				</parameter>
+				<parameter name="ExternalSignalingHostname">
+					<para><xi:include xpointer="xpointer(/docs/configInfo[@name='res_pjsip']/configFile[@name='pjsip.conf']/configObject[@name='transport']/configOption[@name='external_signaling_hostname']/synopsis/node())"/></para>
+				</parameter>
 				<parameter name="ExternalMediaAddress">
 					<para><xi:include xpointer="xpointer(/docs/configInfo[@name='res_pjsip']/configFile[@name='pjsip.conf']/configObject[@name='transport']/configOption[@name='external_media_address']/synopsis/node())"/></para>
 				</parameter>
diff --git a/res/res_pjsip_nat.c b/res/res_pjsip_nat.c
index f4e63a61b4..a7689dee60 100644
--- a/res/res_pjsip_nat.c
+++ b/res/res_pjsip_nat.c
@@ -32,6 +32,7 @@
 #include "asterisk/res_pjsip_session.h"
 #include "asterisk/module.h"
 #include "asterisk/acl.h"
+#include "asterisk/strings.h"

 /*! URI parameter for original host/port */
 #define AST_SIP_X_AST_ORIG_HOST "x-ast-orig-host"
@@ -370,8 +371,12 @@ static pj_status_t process_nat(pjsip_tx_data *tdata)
 		}
 	}

-	if (!ast_sockaddr_isnull(&transport_state->external_signaling_address)) {
+	if (!ast_sockaddr_isnull(&transport_state->external_signaling_address) ||
+		!ast_strlen_zero(transport->external_signaling_hostname)) {
 		pjsip_cseq_hdr *cseq = PJSIP_MSG_CSEQ_HDR(tdata->msg);
+		const char *signaling_host = !ast_strlen_zero(transport->external_signaling_hostname) ?
+			transport->external_signaling_hostname :
+			ast_sockaddr_stringify_host(&transport_state->external_signaling_address);

 		/* Update the Contact header with the external address. We only do this if
 		 * a CSeq is not present (which should not happen - but we are extra safe),
@@ -387,7 +392,7 @@ static pj_status_t process_nat(pjsip_tx_data *tdata)
 			tdata->msg->line.status.code != PJSIP_SC_MOVED_TEMPORARILY )) {
 			/* We can only rewrite the URI when one is present */
 			if (uri || (uri = ast_sip_get_contact_sip_uri(tdata))) {
-				pj_strdup2(tdata->pool, &uri->host, ast_sockaddr_stringify_host(&transport_state->external_signaling_address));
+				pj_strdup2(tdata->pool, &uri->host, signaling_host);
 				if (transport->external_signaling_port) {
 					uri->port = transport->external_signaling_port;
 					ast_debug(4, "Re-wrote Contact URI port to %d\n", uri->port);
@@ -397,7 +402,7 @@ static pj_status_t process_nat(pjsip_tx_data *tdata)

 		/* Update the via header if relevant */
 		if ((tdata->msg->type == PJSIP_REQUEST_MSG) && (via || (via = pjsip_msg_find_hdr(tdata->msg, PJSIP_H_VIA, NULL)))) {
-			pj_strdup2(tdata->pool, &via->sent_by.host, ast_sockaddr_stringify_host(&transport_state->external_signaling_address));
+			pj_strdup2(tdata->pool, &via->sent_by.host, signaling_host);
 			if (transport->external_signaling_port) {
 				via->sent_by.port = transport->external_signaling_port;
 			}