]> git.ipfire.org Git - thirdparty/asterisk.git/commitdiff
res_pjsip: Add per-endpoint RTP port range configuration master
authormattia <me@mattiacampagna.com>
Wed, 1 Apr 2026 12:46:57 +0000 (14:46 +0200)
committergithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Tue, 28 Apr 2026 17:46:01 +0000 (17:46 +0000)
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.

configs/samples/pjsip.conf.sample
contrib/ast-db-manage/config/versions/e89e30cee53f_add_rtp_port_range_to_ps_endpoints.py [new file with mode: 0644]
include/asterisk/res_pjsip.h
include/asterisk/rtp_engine.h
main/rtp_engine.c
res/res_pjsip/pjsip_config.xml
res/res_pjsip/pjsip_configuration.c
res/res_pjsip/pjsip_manager.xml
res/res_pjsip_sdp_rtp.c
res/res_rtp_asterisk.c

index 319a7d5806a6b21814519a3297ed13747016c02a..623a73a830bce5573f7aa9c6be5ffe32b28ef6fc 100644 (file)
 ;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 (file)
index 0000000..819e4e7
--- /dev/null
@@ -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')
index e122c6e5a498a5d91af7e744283992a80b154363..34e0bb9d4cef97adb45b9ee299baff02090d6dd6 100644 (file)
@@ -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;
 };
 
 /*!
index 0a38e43cd579f5109ddcd1d15c42288d6e2b3554..0caca86120e93c93fd6439adf749025eea50460f 100644 (file)
@@ -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
  *
index b0c1bc2c42abd19deb87cff6ad0c6623d6c2f2fd..184f39b604198ffe8aa65542deeddb313fd5104e 100644 (file)
@@ -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;
index eb78ac32b1019d5db75de275c08e53be6e4047f9..b5336438516eaf301286e59b1f8639b977e2fc67 100644 (file)
                                                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>
index cfc14560045750cd9db98e598d65d7733e1a2044..12b8ee6f88676da60941338d5c95d443e0e216be 100644 (file)
@@ -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);
index 9f2cf634c57d4b363e92dd40014ea3a52c15cae1..548b974332366c4964bedbf6a6fc5734ef5b7b2f 100644 (file)
                                <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>
index 7d2a5e7c4acb85ce2617a98df37f80a4931d0493..635b0827d288823536ae4b156d6129fe5c4788ac 100644 (file)
@@ -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);
index dc66f84770ff6a28da5337984f52f399dc5fee4b..5df1d70c8be598fb9a3ac6dfa0df0a3794fe00d6 100644 (file)
@@ -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 */