Commit 85de5156d5 for asterisk.org
commit 85de5156d5111a962ae3585ca76b6e77b9e5a006
Author: Mehrdad Seifzadeh <mehrdad.seifzadeh@gmail.com>
Date: Mon Jul 6 15:29:12 2026 +0330
res_pjsip: Accept zero-port BUNDLE-only media offers
diff --git a/res/res_pjsip_session.c b/res/res_pjsip_session.c
index 53dcbbfe0e..473b498c36 100644
--- a/res/res_pjsip_session.c
+++ b/res/res_pjsip_session.c
@@ -770,6 +770,21 @@ static void remove_stream_from_bundle(struct ast_sip_session_media *session_medi
session_media->bundled = 0;
}
+/*!
+ * \brief Determine whether a zero-port stream is offered for BUNDLE-only use.
+ *
+ * RFC 9143 permits an offerer to use a zero port together with the
+ * bundle-only attribute when the stream is to be accepted only as part of
+ * the associated BUNDLE group. A zero port without these conditions still
+ * means that the stream is disabled.
+ */
+static int is_bundle_only_stream(const struct ast_sip_session_media *session_media,
+ const struct pjmedia_sdp_media *stream)
+{
+ return !stream->desc.port && session_media->bundled
+ && pjmedia_sdp_media_find_attr2(stream, "bundle-only", NULL);
+}
+
static int handle_incoming_sdp(struct ast_sip_session *session, const pjmedia_sdp_session *sdp)
{
int i;
@@ -798,6 +813,7 @@ static int handle_incoming_sdp(struct ast_sip_session *session, const pjmedia_sd
RAII_VAR(struct sdp_handler_list *, handler_list, NULL, ao2_cleanup);
struct ast_sip_session_media *session_media = NULL;
int res;
+ int bundle_only;
enum ast_media_type type;
struct ast_stream *stream = NULL;
pjmedia_sdp_media *remote_stream = sdp->media[i];
@@ -892,14 +908,25 @@ static int handle_incoming_sdp(struct ast_sip_session *session, const pjmedia_sd
ast_sip_session_get_name(session));
}
- /* If this stream is already declined mark it as such, or mark it as such if we've reached the limit */
- if (!remote_stream->desc.port || is_stream_limitation_reached(type, session->endpoint, type_streams)) {
+ /* MID and BUNDLE membership must be known before interpreting a zero port. */
+ set_mid_and_bundle_group(session, session_media, sdp, remote_stream);
+ bundle_only = is_bundle_only_stream(session_media, remote_stream);
+
+ /* A zero port normally declines a stream. RFC 9143 defines an exception
+ * for a bundled stream carrying the bundle-only attribute.
+ */
+ if ((!remote_stream->desc.port && !bundle_only)
+ || is_stream_limitation_reached(type, session->endpoint, type_streams)) {
remove_stream_from_bundle(session_media, stream);
SCOPE_EXIT_EXPR(continue, "%s: Declining incoming SDP media stream %s'\n",
ast_sip_session_get_name(session), ast_str_tmp(128, ast_stream_to_str(stream, &STR_TMP)));
}
- set_mid_and_bundle_group(session, session_media, sdp, remote_stream);
+ if (bundle_only) {
+ ast_trace(-1, "%s: Accepting zero-port bundle-only SDP media stream %s\n",
+ ast_sip_session_get_name(session), ast_str_tmp(128, ast_stream_to_str(stream, &STR_TMP)));
+ }
+
set_remote_mslabel_and_stream_group(session, session_media, sdp, remote_stream, stream);
if (session_media->handler) {
@@ -1132,7 +1159,15 @@ static int handle_negotiated_sdp(struct ast_sip_session *session, const pjmedia_
* the state to REMOVED, then our work here is done, so go ahead and move on
* to the next stream.
*/
- if (!remote->media[i]->desc.port) {
+ /*
+ * A zero port in a remote answer declines the stream. In an offer,
+ * however, RFC 9143 permits a bundled stream to use port zero together
+ * with a=bundle-only. In that case, keep the stream if our local answer
+ * accepted it with a non-zero port.
+ */
+ if (!local->media[i]->desc.port
+ || (!remote->media[i]->desc.port
+ && !is_bundle_only_stream(session_media, remote->media[i]))) {
ast_stream_set_state(stream, AST_STREAM_STATE_REMOVED);
continue;
}
diff --git a/third-party/pjproject/patches/0030-pjmedia-handle-bundle-only-offers.patch b/third-party/pjproject/patches/0030-pjmedia-handle-bundle-only-offers.patch
new file mode 100644
index 0000000000..dc29b272da
--- /dev/null
+++ b/third-party/pjproject/patches/0030-pjmedia-handle-bundle-only-offers.patch
@@ -0,0 +1,253 @@
+From 74ae132492b3a7ce1c5ece28096173e878c7ca48 Mon Sep 17 00:00:00 2001
+From: Mehrdad Seifzadeh <mehrdad.seifzadeh@gmail.com>
+Date: Mon, 13 Jul 2026 12:26:45 +0330
+Subject: [PATCH] pjmedia: Handle zero-port bundle-only offers (#5067)
+
+(cherry picked from commit 58666750d6bfe19dc213d010be63b1ab6b33ef65)
+---
+ pjmedia/src/pjmedia/sdp_neg.c | 90 ++++++++++++++++++++++++++++++--
+ pjmedia/src/test/sdp_neg_test.c | 91 +++++++++++++++++++++++++++++++++
+ 2 files changed, 178 insertions(+), 3 deletions(-)
+
+diff --git a/pjmedia/src/pjmedia/sdp_neg.c b/pjmedia/src/pjmedia/sdp_neg.c
+index 66682cc..713ea2f 100644
+--- a/pjmedia/src/pjmedia/sdp_neg.c
++++ b/pjmedia/src/pjmedia/sdp_neg.c
+@@ -1242,10 +1242,89 @@ static void apply_answer_symmetric_pt(pj_pool_t *pool,
+ }
+
+
++/* Return PJ_TRUE if a session-level group attribute contains the supplied MID
++ * in a BUNDLE group.
++ */
++static pj_bool_t bundle_group_contains_mid(const pjmedia_sdp_session *sdp,
++ const pj_str_t *mid)
++{
++ unsigned i;
++
++ for (i = 0; i < sdp->attr_count; ++i) {
++ const pjmedia_sdp_attr *attr = sdp->attr[i];
++ const char *pos, *end;
++ unsigned token_index = 0;
++ pj_bool_t is_bundle = PJ_FALSE;
++
++ if (pj_stricmp2(&attr->name, "group") != 0)
++ continue;
++
++ if (attr->value.ptr == NULL || attr->value.slen == 0)
++ continue;
++
++ pos = attr->value.ptr;
++ end = attr->value.ptr + attr->value.slen;
++ while (pos < end) {
++ const char *token_start;
++ pj_str_t token;
++
++ while (pos < end && (*pos == ' ' || *pos == '\t'))
++ ++pos;
++ if (pos == end)
++ break;
++
++ token_start = pos;
++ while (pos < end && *pos != ' ' && *pos != '\t')
++ ++pos;
++
++ token.ptr = (char *)token_start;
++ token.slen = pos - token_start;
++
++ if (token_index++ == 0) {
++ is_bundle = (pj_stricmp2(&token, "BUNDLE") == 0);
++ if (!is_bundle)
++ break;
++ continue;
++ }
++
++ if (is_bundle && pj_strcmp(&token, mid) == 0)
++ return PJ_TRUE;
++ }
++ }
++
++ return PJ_FALSE;
++}
++
++
++/* RFC 9143 allows a bundled m= section in an offer to use port zero when
++ * a=bundle-only is present. The exception only applies when the section has
++ * a MID and that MID is actually listed in a session-level group:BUNDLE
++ * attribute.
++ */
++static pj_bool_t is_bundle_only_offer(const pjmedia_sdp_session *sdp,
++ const pjmedia_sdp_media *media)
++{
++ const pjmedia_sdp_attr *mid;
++
++ if (media->desc.port != 0 ||
++ !pjmedia_sdp_media_find_attr2(media, "bundle-only", NULL))
++ {
++ return PJ_FALSE;
++ }
++
++ mid = pjmedia_sdp_media_find_attr2(media, "mid", NULL);
++ if (!mid)
++ return PJ_FALSE;
++
++ return bundle_group_contains_mid(sdp, &mid->value);
++}
++
++
+ /* Try to match offer with answer. */
+ static pj_status_t match_offer(pj_pool_t *pool,
+ pj_bool_t prefer_remote_codec_order,
+ pj_bool_t answer_with_multiple_codecs,
++ pj_bool_t bundle_only,
+ const pjmedia_sdp_media *offer,
+ const pjmedia_sdp_media *preanswer,
+ const pjmedia_sdp_session *preanswer_sdp,
+@@ -1267,8 +1346,11 @@ static pj_status_t match_offer(pj_pool_t *pool,
+ unsigned nclockrate = 0, clockrate[PJMEDIA_MAX_SDP_FMT];
+ unsigned ntel_clockrate = 0, tel_clockrate[PJMEDIA_MAX_SDP_FMT];
+
+- /* If offer has zero port, just clone the offer */
+- if (offer->desc.port == 0) {
++ /* A zero port normally disables an offered media section. RFC 9143
++ * defines an exception for a valid bundle-only section, which must be
++ * answered using the BUNDLE transport rather than being rejected.
++ */
++ if (offer->desc.port == 0 && !bundle_only) {
+ answer = sdp_media_clone_deactivate(pool, offer, preanswer,
+ preanswer_sdp);
+ *p_answer = answer;
+@@ -1709,10 +1791,12 @@ static pj_status_t create_answer( pj_pool_t *pool,
+ const pjmedia_sdp_media *om; /* offer */
+ const pjmedia_sdp_media *im; /* initial media */
+ pjmedia_sdp_media *am = NULL; /* answer/result */
++ pj_bool_t bundle_only;
+ pj_uint32_t om_tp;
+ unsigned j;
+
+ om = offer->media[i];
++ bundle_only = is_bundle_only_offer(offer, om);
+
+ om_tp = pjmedia_sdp_transport_get_proto(&om->desc.transport);
+ PJMEDIA_TP_PROTO_TRIM_FLAG(om_tp, PJMEDIA_TP_PROFILE_RTCP_FB);
+@@ -1737,7 +1821,7 @@ static pj_status_t create_answer( pj_pool_t *pool,
+
+ /* See if it has matching codec. */
+ status2 = match_offer(pool, prefer_remote_codec_order,
+- answer_with_multiple_codecs,
++ answer_with_multiple_codecs, bundle_only,
+ om, im, initial, &am);
+ if (status2 == PJ_SUCCESS) {
+ /* Mark media as used. */
+diff --git a/pjmedia/src/test/sdp_neg_test.c b/pjmedia/src/test/sdp_neg_test.c
+index 3cf8d52..e5326b5 100644
+--- a/pjmedia/src/test/sdp_neg_test.c
++++ b/pjmedia/src/test/sdp_neg_test.c
+@@ -1652,6 +1652,82 @@ static int perform_test(pj_pool_t *pool, int test_index)
+ return 0;
+ }
+
++/* RFC 9143: an offered media section using port zero is not rejected when
++ * it has a=bundle-only and its MID belongs to the BUNDLE group.
++ */
++static int sdp_neg_bundle_only_test(pj_pool_t *pool)
++{
++ static char offer_str[] =
++ "v=0\r\n"
++ "o=- 0 0 IN IP4 127.0.0.1\r\n"
++ "s=-\r\n"
++ "c=IN IP4 127.0.0.1\r\n"
++ "t=0 0\r\n"
++ "a=group:BUNDLE 0 1\r\n"
++ "m=audio 4000 RTP/AVP 0\r\n"
++ "a=mid:0\r\n"
++ "m=video 0 RTP/AVP 96\r\n"
++ "a=mid:1\r\n"
++ "a=bundle-only\r\n"
++ "a=rtpmap:96 VP8/90000\r\n";
++
++ static char local_str[] =
++ "v=0\r\n"
++ "o=- 1 1 IN IP4 127.0.0.1\r\n"
++ "s=-\r\n"
++ "c=IN IP4 127.0.0.1\r\n"
++ "t=0 0\r\n"
++ "a=group:BUNDLE 0 1\r\n"
++ "m=audio 5000 RTP/AVP 0\r\n"
++ "a=mid:0\r\n"
++ "m=video 5002 RTP/AVP 96\r\n"
++ "a=mid:1\r\n"
++ "a=rtpmap:96 VP8/90000\r\n";
++
++ pjmedia_sdp_session *offer, *local;
++ pjmedia_sdp_neg *neg;
++ const pjmedia_sdp_session *active_local;
++ pj_status_t status;
++ char b1[sizeof(offer_str)], b2[sizeof(local_str)];
++
++ pj_memcpy(b1, offer_str, sizeof(offer_str));
++ pj_memcpy(b2, local_str, sizeof(local_str));
++
++ if (pjmedia_sdp_parse(pool, b1, pj_ansi_strlen(b1), &offer) !=
++ PJ_SUCCESS)
++ {
++ return -3000;
++ }
++
++ if (pjmedia_sdp_parse(pool, b2, pj_ansi_strlen(b2), &local) !=
++ PJ_SUCCESS)
++ {
++ return -3010;
++ }
++
++ if (pjmedia_sdp_neg_create_w_remote_offer(pool, local, offer, &neg) !=
++ PJ_SUCCESS)
++ {
++ return -3020;
++ }
++
++ status = pjmedia_sdp_neg_negotiate(pool, neg, 0);
++ if (status != PJ_SUCCESS) {
++ app_perror(status, " sdp_neg_bundle_only_test: negotiate failed");
++ return -3030;
++ }
++
++ pjmedia_sdp_neg_get_active_local(neg, &active_local);
++
++ if (active_local->media_count != 2)
++ return -3040;
++
++ if (active_local->media[1]->desc.port == 0)
++ return -3050;
++
++ return 0;
++}
++
+ int sdp_neg_test()
+ {
+ unsigned i;
+@@ -1674,6 +1750,21 @@ int sdp_neg_test()
+ }
+ }
+
++ {
++ pj_pool_t *pool;
++
++ pool = pj_pool_create(mem, "sdp_neg_bundle", 4000, 4000, NULL);
++ if (!pool)
++ return PJ_ENOMEM;
++
++ PJ_LOG(3,(THIS_FILE, " sdp_neg_bundle_only_test"));
++ status = sdp_neg_bundle_only_test(pool);
++ pj_pool_release(pool);
++
++ if (status != 0)
++ return status;
++ }
++
+ return 0;
+ }
+
+--
+2.47.3
+