Commit fcb98380e4 for asterisk.org

commit fcb98380e49f9e40adf7d750895fbdc20a0007cc
Author: mattia <me@mattiacampagna.com>
Date:   Wed Apr 1 14:46:57 2026 +0200

    res_pjsip: Add per-endpoint RTP port range configuration

    Add rtp_port_start and rtp_port_end options to PJSIP endpoint
    configuration, allowing each endpoint to use a dedicated RTP port
    range instead of the global rtp.conf setting.

    This is useful for scenarios where different endpoints need isolated
    port ranges, such as firewall rules per trunk, multi-tenant systems,
    or network QoS policies tied to port ranges.

    The implementation adds ast_rtp_instance_new_with_port_range() to the
    RTP engine API, which sets the port range on the instance before the
    engine allocates the transport. The default RTP engine
    (res_rtp_asterisk) checks for per-instance overrides in
    rtp_allocate_transport() and falls back to the global range when
    none is set.

    Both options must be set together, with values >= 1024 and
    rtp_port_end > rtp_port_start. Setting both to 0 (the default)
    preserves existing behavior.

    Resolves: https://github.com/asterisk/asterisk-feature-requests/issues/71

    UserNote: PJSIP endpoints now support rtp_port_start and
    rtp_port_end options to configure a dedicated RTP port range per
    endpoint, overriding the global rtp.conf setting.

    UpgradeNote: An alembic database migration has been added to add
    the rtp_port_start and rtp_port_end columns to the ps_endpoints
    table. Run "alembic upgrade head" to apply the schema change.

    DeveloperNote: New public API: ast_rtp_instance_new_with_port_range()
    creates an RTP instance with a per-instance port range.
    ast_rtp_instance_get_port_start() and ast_rtp_instance_get_port_end()
    allow RTP engines to query the override. Third-party RTP engines can
    use these getters to support per-instance port ranges.

diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample
index 319a7d5806..623a73a830 100644
--- a/configs/samples/pjsip.conf.sample
+++ b/configs/samples/pjsip.conf.sample
@@ -846,6 +846,12 @@
 ;rtp_timeout_hold= ; Hang up channel if RTP is not received for the specified
                    ; number of seconds when the channel is on hold (default:
                    ; "0" or not enabled)
+;rtp_port_start=   ; Per-endpoint starting RTP port number, overriding the
+                   ; global rtp.conf setting. Must be used together with
+                   ; rtp_port_end. (default: "0" or use global range)
+;rtp_port_end=     ; Per-endpoint ending RTP port number, overriding the
+                   ; global rtp.conf setting. Must be used together with
+                   ; rtp_port_start. (default: "0" or use global range)
 ;contact_user= ; On outgoing requests, force the user portion of the Contact
                ; header to this value (default: "")
 ;incoming_call_offer_pref= ; Based on this setting, a joint list of
diff --git a/contrib/ast-db-manage/config/versions/e89e30cee53f_add_rtp_port_range_to_ps_endpoints.py b/contrib/ast-db-manage/config/versions/e89e30cee53f_add_rtp_port_range_to_ps_endpoints.py
new file mode 100644
index 0000000000..819e4e7c49
--- /dev/null
+++ b/contrib/ast-db-manage/config/versions/e89e30cee53f_add_rtp_port_range_to_ps_endpoints.py
@@ -0,0 +1,26 @@
+"""add rtp_port_start and rtp_port_end to ps_endpoints
+
+Revision ID: e89e30cee53f
+Revises: bb6d54e22913
+Create Date: 2026-04-02 10:00:00.000000
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = 'e89e30cee53f'
+down_revision = 'bb6d54e22913'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+    op.add_column('ps_endpoints',
+                  sa.Column('rtp_port_start', sa.Integer))
+    op.add_column('ps_endpoints',
+                  sa.Column('rtp_port_end', sa.Integer))
+
+
+def downgrade():
+    op.drop_column('ps_endpoints', 'rtp_port_end')
+    op.drop_column('ps_endpoints', 'rtp_port_start')
diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h
index e122c6e5a4..34e0bb9d4c 100644
--- a/include/asterisk/res_pjsip.h
+++ b/include/asterisk/res_pjsip.h
@@ -966,6 +966,10 @@ struct ast_sip_media_rtp_configuration {
 	unsigned int follow_early_media_fork;
 	/*! Accept updated SDPs on non-100rel 18X and 2XX responses with the same To tag */
 	unsigned int accept_multiple_sdp_answers;
+	/*! Per-endpoint RTP port range start (0 means use global rtp.conf setting) */
+	unsigned int port_start;
+	/*! Per-endpoint RTP port range end (0 means use global rtp.conf setting) */
+	unsigned int port_end;
 };

 /*!
diff --git a/include/asterisk/rtp_engine.h b/include/asterisk/rtp_engine.h
index 0a38e43cd5..0caca86120 100644
--- a/include/asterisk/rtp_engine.h
+++ b/include/asterisk/rtp_engine.h
@@ -980,6 +980,60 @@ struct ast_rtp_instance *ast_rtp_instance_new(const char *engine_name,
                 struct ast_sched_context *sched, const struct ast_sockaddr *sa,
                 void *data);

+/*!
+ * \brief Options for creating a new RTP instance
+ *
+ * This structure allows passing additional options when creating an
+ * RTP instance via \ref ast_rtp_instance_new_with_options. New fields
+ * can be added in the future without changing the function signature.
+ *
+ * \since 20.20.0
+ * \since 22.10.0
+ * \since 23.4.0
+ */
+struct ast_rtp_instance_options {
+	/*! Starting port number for this instance (0 to use global) */
+	unsigned int port_start;
+	/*! Ending port number for this instance (0 to use global) */
+	unsigned int port_end;
+};
+
+/*!
+ * \brief Create a new RTP instance with additional options
+ *
+ * \param engine_name Name of the engine to use for the RTP instance
+ * \param sched Scheduler context that the RTP engine may want to use
+ * \param sa Address we want to bind to
+ * \param data Unique data for the engine
+ * \param options Pointer to options struct, or NULL to use defaults
+ *
+ * \retval non-NULL success
+ * \retval NULL failure
+ *
+ * Example usage:
+ *
+ * \code
+ * struct ast_rtp_instance_options options = { .port_start = 15000, .port_end = 15010 };
+ * struct ast_rtp_instance *instance = NULL;
+ * instance = ast_rtp_instance_new_with_options(NULL, sched, &sin, NULL, &options);
+ * \endcode
+ *
+ * This creates a new RTP instance using the specified options. When a
+ * per-instance port range is provided the global rtp.conf range is ignored.
+ * If options is NULL or port values are both 0 the global range is used.
+ *
+ * \note The per-instance port range overrides the global rtp.conf settings
+ *       for this specific RTP instance only. It does not need to be a
+ *       subset of the global range.
+ *
+ * \since 20.20.0
+ * \since 22.10.0
+ * \since 23.4.0
+ */
+struct ast_rtp_instance *ast_rtp_instance_new_with_options(const char *engine_name,
+                struct ast_sched_context *sched, const struct ast_sockaddr *sa,
+                void *data, const struct ast_rtp_instance_options *options);
+
 /*!
  * \brief Destroy an RTP instance
  *
@@ -2668,6 +2722,32 @@ int ast_rtp_instance_get_hold_timeout(struct ast_rtp_instance *instance);
  */
 int ast_rtp_instance_get_keepalive(struct ast_rtp_instance *instance);

+/*!
+ * \brief Get the per-instance RTP port range start
+ *
+ * \param instance The RTP instance
+ *
+ * \return port start value (0 means use global setting)
+ *
+ * \since 20.20.0
+ * \since 22.10.0
+ * \since 23.4.0
+ */
+unsigned int ast_rtp_instance_get_port_start(struct ast_rtp_instance *instance);
+
+/*!
+ * \brief Get the per-instance RTP port range end
+ *
+ * \param instance The RTP instance
+ *
+ * \return port end value (0 means use global setting)
+ *
+ * \since 20.20.0
+ * \since 22.10.0
+ * \since 23.4.0
+ */
+unsigned int ast_rtp_instance_get_port_end(struct ast_rtp_instance *instance);
+
 /*!
  * \brief Get the RTP engine in use on an RTP instance
  *
diff --git a/main/rtp_engine.c b/main/rtp_engine.c
index b0c1bc2c42..184f39b604 100644
--- a/main/rtp_engine.c
+++ b/main/rtp_engine.c
@@ -232,6 +232,10 @@ struct ast_rtp_instance {
 	AST_VECTOR(, int) extmap_negotiated;
 	/*! Negotiated RTP extensions (using index based on unique id) */
 	AST_VECTOR(, struct rtp_extmap) extmap_unique_ids;
+	/*! Per-instance RTP port range start (0 means use global) */
+	unsigned int rtp_port_start;
+	/*! Per-instance RTP port range end (0 means use global) */
+	unsigned int rtp_port_end;
 };

 /*!
@@ -493,11 +497,21 @@ int ast_rtp_instance_destroy(struct ast_rtp_instance *instance)
 struct ast_rtp_instance *ast_rtp_instance_new(const char *engine_name,
 		struct ast_sched_context *sched, const struct ast_sockaddr *sa,
 		void *data)
+{
+	return ast_rtp_instance_new_with_options(
+		engine_name, sched, sa, data, NULL);
+}
+
+struct ast_rtp_instance *ast_rtp_instance_new_with_options(const char *engine_name,
+		struct ast_sched_context *sched, const struct ast_sockaddr *sa,
+		void *data, const struct ast_rtp_instance_options *options)
 {
 	struct ast_sockaddr address = {{0,}};
 	struct ast_rtp_instance *instance = NULL;
 	struct ast_rtp_engine *engine = NULL;
 	struct ast_module *mod_ref;
+	unsigned int port_start = options ? options->port_start : 0;
+	unsigned int port_end = options ? options->port_end : 0;

 	AST_RWLIST_RDLOCK(&engines);

@@ -538,6 +552,10 @@ struct ast_rtp_instance *ast_rtp_instance_new(const char *engine_name,
 	ast_sockaddr_copy(&instance->local_address, sa);
 	ast_sockaddr_copy(&address, sa);

+	/* Set the per-instance port range before the engine allocates the transport */
+	instance->rtp_port_start = port_start;
+	instance->rtp_port_end = port_end;
+
 	if (ast_rtp_codecs_payloads_initialize(&instance->codecs)) {
 		ao2_ref(instance, -1);
 		return NULL;
@@ -551,7 +569,12 @@ struct ast_rtp_instance *ast_rtp_instance_new(const char *engine_name,
 		return NULL;
 	}

-	ast_debug(1, "Using engine '%s' for RTP instance '%p'\n", engine->name, instance);
+	if (port_start && port_end) {
+		ast_debug(1, "Using engine '%s' for RTP instance '%p' with port range %d-%d\n",
+			engine->name, instance, port_start, port_end);
+	} else {
+		ast_debug(1, "Using engine '%s' for RTP instance '%p'\n", engine->name, instance);
+	}

 	/*
 	 * And pass it off to the engine to setup
@@ -2908,6 +2931,16 @@ int ast_rtp_instance_get_keepalive(struct ast_rtp_instance *instance)
 	return instance->keepalive;
 }

+unsigned int ast_rtp_instance_get_port_start(struct ast_rtp_instance *instance)
+{
+	return instance->rtp_port_start;
+}
+
+unsigned int ast_rtp_instance_get_port_end(struct ast_rtp_instance *instance)
+{
+	return instance->rtp_port_end;
+}
+
 struct ast_rtp_engine *ast_rtp_instance_get_engine(struct ast_rtp_instance *instance)
 {
 	return instance->engine;
diff --git a/res/res_pjsip/pjsip_config.xml b/res/res_pjsip/pjsip_config.xml
index eb78ac32b1..b533643851 100644
--- a/res/res_pjsip/pjsip_config.xml
+++ b/res/res_pjsip/pjsip_config.xml
@@ -1659,6 +1659,35 @@
 						channel is hung up. By default this option is set to 0, which means do not check.
 					</para></description>
 				</configOption>
+				<configOption name="rtp_port_start" default="0">
+					<since>
+						<version>20.20.0</version>
+						<version>22.10.0</version>
+						<version>23.4.0</version>
+					</since>
+					<synopsis>Per-endpoint starting RTP port number.</synopsis>
+					<description><para>
+						Defines the starting port number for a dedicated per-endpoint RTP port
+						range, overriding the global rtp.conf setting. Must be used together
+						with <literal>rtp_port_end</literal>. When set to 0, the global
+						rtp.conf range is used. The value must be between 1024 and 65535.
+					</para></description>
+				</configOption>
+				<configOption name="rtp_port_end" default="0">
+					<since>
+						<version>20.20.0</version>
+						<version>22.10.0</version>
+						<version>23.4.0</version>
+					</since>
+					<synopsis>Per-endpoint ending RTP port number.</synopsis>
+					<description><para>
+						Defines the ending port number for a dedicated per-endpoint RTP port
+						range, overriding the global rtp.conf setting. Must be used together
+						with <literal>rtp_port_start</literal>. When set to 0, the global
+						rtp.conf range is used. The value must be greater than
+						<literal>rtp_port_start</literal> and between 1024 and 65535.
+					</para></description>
+				</configOption>
 				<configOption name="acl">
 					<since>
 						<version>13.10.0</version>
diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c
index cfc1456004..12b8ee6f88 100644
--- a/res/res_pjsip/pjsip_configuration.c
+++ b/res/res_pjsip/pjsip_configuration.c
@@ -1666,6 +1666,30 @@ static int sip_endpoint_apply_handler(const struct ast_sorcery *sorcery, void *o
 		return -1;
 	}

+	if (endpoint->media.rtp.port_start || endpoint->media.rtp.port_end) {
+		if (!endpoint->media.rtp.port_start || !endpoint->media.rtp.port_end) {
+			ast_log(LOG_ERROR, "Endpoint '%s': Both rtp_port_start and rtp_port_end must be set together\n",
+				ast_sorcery_object_get_id(endpoint));
+			return -1;
+		}
+		if (endpoint->media.rtp.port_start < 1024 || endpoint->media.rtp.port_end < 1024) {
+			ast_log(LOG_ERROR, "Endpoint '%s': rtp_port_start and rtp_port_end must be at least 1024\n",
+				ast_sorcery_object_get_id(endpoint));
+			return -1;
+		}
+		if (endpoint->media.rtp.port_end <= endpoint->media.rtp.port_start) {
+			ast_log(LOG_ERROR, "Endpoint '%s': rtp_port_end (%u) must be greater than rtp_port_start (%u)\n",
+				ast_sorcery_object_get_id(endpoint),
+				endpoint->media.rtp.port_end,
+				endpoint->media.rtp.port_start);
+			return -1;
+		}
+		ast_debug(1, "Endpoint '%s': Using per-endpoint RTP port range %u-%u\n",
+			ast_sorcery_object_get_id(endpoint),
+			endpoint->media.rtp.port_start,
+			endpoint->media.rtp.port_end);
+	}
+
 	if (endpoint->preferred_codec_only) {
 		if (endpoint->media.incoming_call_offer_pref.flags != (AST_SIP_CALL_CODEC_PREF_LOCAL | AST_SIP_CALL_CODEC_PREF_INTERSECT | AST_SIP_CALL_CODEC_PREF_ALL)) {
 			ast_log(LOG_ERROR, "Setting both preferred_codec_only and incoming_call_offer_pref is not supported on endpoint '%s'\n",
@@ -2291,6 +2315,8 @@ int ast_res_pjsip_initialize_configuration(void)
 	ast_sorcery_object_field_register(sip_sorcery, "endpoint", "rtp_keepalive", "0", OPT_UINT_T, 0, FLDSET(struct ast_sip_endpoint, media.rtp.keepalive));
 	ast_sorcery_object_field_register(sip_sorcery, "endpoint", "rtp_timeout", "0", OPT_UINT_T, 0, FLDSET(struct ast_sip_endpoint, media.rtp.timeout));
 	ast_sorcery_object_field_register(sip_sorcery, "endpoint", "rtp_timeout_hold", "0", OPT_UINT_T, 0, FLDSET(struct ast_sip_endpoint, media.rtp.timeout_hold));
+	ast_sorcery_object_field_register(sip_sorcery, "endpoint", "rtp_port_start", "0", OPT_UINT_T, PARSE_IN_RANGE, FLDSET(struct ast_sip_endpoint, media.rtp.port_start), 0, 65535);
+	ast_sorcery_object_field_register(sip_sorcery, "endpoint", "rtp_port_end", "0", OPT_UINT_T, PARSE_IN_RANGE, FLDSET(struct ast_sip_endpoint, media.rtp.port_end), 0, 65535);
 	ast_sorcery_object_field_register(sip_sorcery, "endpoint", "one_touch_recording", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, info.recording.enabled));
 	ast_sorcery_object_field_register(sip_sorcery, "endpoint", "inband_progress", "no", OPT_BOOL_T, 1, FLDSET(struct ast_sip_endpoint, inband_progress));
 	ast_sorcery_object_field_register_custom(sip_sorcery, "endpoint", "call_group", "", group_handler, callgroup_to_str, NULL, 0, 0);
diff --git a/res/res_pjsip/pjsip_manager.xml b/res/res_pjsip/pjsip_manager.xml
index 9f2cf634c5..548b974332 100644
--- a/res/res_pjsip/pjsip_manager.xml
+++ b/res/res_pjsip/pjsip_manager.xml
@@ -677,6 +677,12 @@
 				<parameter name="RtpTimeoutHold">
 					<para><xi:include xpointer="xpointer(/docs/configInfo[@name='res_pjsip']/configFile[@name='pjsip.conf']/configObject[@name='endpoint']/configOption[@name='rtp_timeout_hold']/synopsis/node())"/></para>
 				</parameter>
+				<parameter name="RtpPortStart">
+					<para><xi:include xpointer="xpointer(/docs/configInfo[@name='res_pjsip']/configFile[@name='pjsip.conf']/configObject[@name='endpoint']/configOption[@name='rtp_port_start']/synopsis/node())"/></para>
+				</parameter>
+				<parameter name="RtpPortEnd">
+					<para><xi:include xpointer="xpointer(/docs/configInfo[@name='res_pjsip']/configFile[@name='pjsip.conf']/configObject[@name='endpoint']/configOption[@name='rtp_port_end']/synopsis/node())"/></para>
+				</parameter>
 				<parameter name="SecurityNegotiation">
 					<para><xi:include xpointer="xpointer(/docs/configInfo[@name='res_pjsip']/configFile[@name='pjsip.conf']/configObject[@name='endpoint']/configOption[@name='security_negotiation']/synopsis/node())"/></para>
 				</parameter>
diff --git a/res/res_pjsip_sdp_rtp.c b/res/res_pjsip_sdp_rtp.c
index 7d2a5e7c4a..635b0827d2 100644
--- a/res/res_pjsip_sdp_rtp.c
+++ b/res/res_pjsip_sdp_rtp.c
@@ -267,9 +267,25 @@ static int create_rtp(struct ast_sip_session *session, struct ast_sip_session_me
 		}
 	}

-	if (!(session_media->rtp = ast_rtp_instance_new(session->endpoint->media.rtp.engine, sched, media_address, NULL))) {
-		ast_log(LOG_ERROR, "Unable to create RTP instance using RTP engine '%s'\n", session->endpoint->media.rtp.engine);
-		return -1;
+	if (session->endpoint->media.rtp.port_start && session->endpoint->media.rtp.port_end) {
+		struct ast_rtp_instance_options options = {
+			.port_start = session->endpoint->media.rtp.port_start,
+			.port_end = session->endpoint->media.rtp.port_end,
+		};
+		if (!(session_media->rtp = ast_rtp_instance_new_with_options(
+				session->endpoint->media.rtp.engine, sched, media_address, NULL,
+				&options))) {
+			ast_log(LOG_ERROR, "Unable to create RTP instance using RTP engine '%s' with port range %u-%u\n",
+				session->endpoint->media.rtp.engine,
+				session->endpoint->media.rtp.port_start,
+				session->endpoint->media.rtp.port_end);
+			return -1;
+		}
+	} else {
+		if (!(session_media->rtp = ast_rtp_instance_new(session->endpoint->media.rtp.engine, sched, media_address, NULL))) {
+			ast_log(LOG_ERROR, "Unable to create RTP instance using RTP engine '%s'\n", session->endpoint->media.rtp.engine);
+			return -1;
+		}
 	}

 	ast_rtp_instance_set_prop(session_media->rtp, AST_RTP_PROPERTY_NAT, session->endpoint->media.rtp.symmetric);
diff --git a/res/res_rtp_asterisk.c b/res/res_rtp_asterisk.c
index dc66f84770..5df1d70c8b 100644
--- a/res/res_rtp_asterisk.c
+++ b/res/res_rtp_asterisk.c
@@ -4063,9 +4063,21 @@ static int ice_create(struct ast_rtp_instance *instance, struct ast_sockaddr *ad
 static int rtp_allocate_transport(struct ast_rtp_instance *instance, struct ast_rtp *rtp)
 {
 	int x, startplace, i, maxloops;
+	unsigned int port_start, port_end;

 	rtp->strict_rtp_state = (strictrtp ? STRICT_RTP_CLOSED : STRICT_RTP_OPEN);

+	/* Determine the port range to use: per-instance override or global */
+	port_start = ast_rtp_instance_get_port_start(instance);
+	port_end = ast_rtp_instance_get_port_end(instance);
+	if (port_start > 0 && port_end > 0 && port_end > port_start) {
+		ast_debug_rtp(1, "(%p) RTP using per-instance port range %d-%d\n",
+			instance, port_start, port_end);
+	} else {
+		port_start = rtpstart;
+		port_end = rtpend;
+	}
+
 	/* Create a new socket for us to listen on and use */
 	if ((rtp->s = create_new_socket("RTP", &rtp->bind_address)) < 0) {
 		ast_log(LOG_WARNING, "Failed to create a new socket for RTP instance '%p'\n", instance);
@@ -4073,13 +4085,13 @@ static int rtp_allocate_transport(struct ast_rtp_instance *instance, struct ast_
 	}

 	/* Now actually find a free RTP port to use */
-	x = (ast_random() % (rtpend - rtpstart)) + rtpstart;
+	x = (ast_random() % (port_end - port_start)) + port_start;
 	x = x & ~1;
 	startplace = x;

 	/* Protection against infinite loops in the case there is a potential case where the loop is not broken such as an odd
 	   start port sneaking in (even though this condition is checked at load.) */
-	maxloops = rtpend - rtpstart;
+	maxloops = port_end - port_start;
 	for (i = 0; i <= maxloops; i++) {
 		ast_sockaddr_set_port(&rtp->bind_address, x);
 		/* Try to bind, this will tell us whether the port is available or not */
@@ -4091,8 +4103,8 @@ static int rtp_allocate_transport(struct ast_rtp_instance *instance, struct ast_
 		}

 		x += 2;
-		if (x > rtpend) {
-			x = (rtpstart + 1) & ~1;
+		if (x > port_end) {
+			x = (port_start + 1) & ~1;
 		}

 		/* See if we ran out of ports or if the bind actually failed because of something other than the address being in use */