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;
}