From fcb98380e49f9e40adf7d750895fbdc20a0007cc Mon Sep 17 00:00:00 2001 From: mattia Date: Wed, 1 Apr 2026 14:46:57 +0200 Subject: [PATCH] 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. --- configs/samples/pjsip.conf.sample | 6 ++ ...e53f_add_rtp_port_range_to_ps_endpoints.py | 26 ++++++ include/asterisk/res_pjsip.h | 4 + include/asterisk/rtp_engine.h | 80 +++++++++++++++++++ main/rtp_engine.c | 35 +++++++- res/res_pjsip/pjsip_config.xml | 29 +++++++ res/res_pjsip/pjsip_configuration.c | 26 ++++++ res/res_pjsip/pjsip_manager.xml | 6 ++ res/res_pjsip_sdp_rtp.c | 22 ++++- res/res_rtp_asterisk.c | 20 ++++- 10 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 contrib/ast-db-manage/config/versions/e89e30cee53f_add_rtp_port_range_to_ps_endpoints.py 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. + + + 20.20.0 + 22.10.0 + 23.4.0 + + Per-endpoint starting RTP port number. + + Defines the starting port number for a dedicated per-endpoint RTP port + range, overriding the global rtp.conf setting. Must be used together + with rtp_port_end. When set to 0, the global + rtp.conf range is used. The value must be between 1024 and 65535. + + + + + 20.20.0 + 22.10.0 + 23.4.0 + + Per-endpoint ending RTP port number. + + Defines the ending port number for a dedicated per-endpoint RTP port + range, overriding the global rtp.conf setting. Must be used together + with rtp_port_start. When set to 0, the global + rtp.conf range is used. The value must be greater than + rtp_port_start and between 1024 and 65535. + + 13.10.0 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 @@ + + + + + + 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 */ -- 2.47.3