diff --git a/pjmedia/CMakeLists.txt b/pjmedia/CMakeLists.txt
index 10ccef7c87..8a66d929ea 100644
--- a/pjmedia/CMakeLists.txt
+++ b/pjmedia/CMakeLists.txt
@@ -957,6 +957,7 @@ if(BUILD_TESTING)
src/test/tone_detector_test.c
src/test/sdp_neg_test.c
src/test/sdp_attr_test.c
+ src/test/transport_srtp_test.c
)
target_link_libraries(pjmedia-test
diff --git a/pjmedia/build/Makefile b/pjmedia/build/Makefile
index cf72d47b7e..5c7308edab 100644
--- a/pjmedia/build/Makefile
+++ b/pjmedia/build/Makefile
@@ -157,7 +157,7 @@ export PJMEDIA_TEST_SRCDIR = ../src/test
export PJMEDIA_TEST_OBJS += codec_vectors.o conf_test.o jbuf_test.o main.o mips_test.o \
vid_codec_test.o vid_dev_test.o vid_port_test.o \
rtp_test.o test.o tone_detector_test.o
-export PJMEDIA_TEST_OBJS += sdp_neg_test.o sdp_attr_test.o
+export PJMEDIA_TEST_OBJS += sdp_neg_test.o sdp_attr_test.o transport_srtp_test.o
export PJMEDIA_TEST_CFLAGS += $(_CFLAGS)
export PJMEDIA_TEST_CXXFLAGS += $(_CXXFLAGS)
export PJMEDIA_TEST_LDFLAGS += $(PJMEDIA_CODEC_LDLIB) \
diff --git a/pjmedia/build/pjmedia_test.vcproj b/pjmedia/build/pjmedia_test.vcproj
index 6acda435ef..4d4fd6f8e6 100644
--- a/pjmedia/build/pjmedia_test.vcproj
+++ b/pjmedia/build/pjmedia_test.vcproj
@@ -4938,6 +4938,10 @@
/>
+
+
diff --git a/pjmedia/build/pjmedia_test.vcxproj b/pjmedia/build/pjmedia_test.vcxproj
index 49d6359d46..4ab8e64a3a 100644
--- a/pjmedia/build/pjmedia_test.vcxproj
+++ b/pjmedia/build/pjmedia_test.vcxproj
@@ -588,6 +588,7 @@
+
true
true
diff --git a/pjmedia/build/pjmedia_test.vcxproj.filters b/pjmedia/build/pjmedia_test.vcxproj.filters
index 9afffaa09e..2318d8cb0d 100644
--- a/pjmedia/build/pjmedia_test.vcxproj.filters
+++ b/pjmedia/build/pjmedia_test.vcxproj.filters
@@ -36,6 +36,9 @@
Source Files
+
+ Source Files
+
Source Files
diff --git a/pjmedia/include/pjmedia/transport_srtp.h b/pjmedia/include/pjmedia/transport_srtp.h
index 64dab4af62..cbde56e556 100644
--- a/pjmedia/include/pjmedia/transport_srtp.h
+++ b/pjmedia/include/pjmedia/transport_srtp.h
@@ -249,6 +249,9 @@ typedef struct pjmedia_srtp_setting
* RTP sequence rollover counter initialization value for incoming
* direction. This is useful to maintain ROC after media transport
* recreation such as in IP change scenario.
+ * Note that once a packet has been authenticated, the transport
+ * updates this to the ROC of that packet, so callers of
+ * #pjmedia_transport_srtp_get_setting() may see it changed.
*/
pjmedia_srtp_roc rx_roc;
@@ -258,7 +261,9 @@ typedef struct pjmedia_srtp_setting
* ROC but for some reason, they don't. Thus, when we encounter
* SRTP packet unprotect failure during probation, we will retry to
* unprotect with this ROC value as well.
- * Set prev_rx_roc.ssrc to 0 to disable this feature.
+ * Set prev_rx_roc.ssrc to 0 to disable this feature. The transport
+ * clears prev_rx_roc.ssrc once a packet of rx_roc.ssrc has been
+ * authenticated, closing the retry window.
*/
pjmedia_srtp_roc prev_rx_roc;
diff --git a/pjmedia/src/pjmedia/transport_srtp.c b/pjmedia/src/pjmedia/transport_srtp.c
index 8ad5a1bb7d..0fcbb0b0b2 100644
--- a/pjmedia/src/pjmedia/transport_srtp.c
+++ b/pjmedia/src/pjmedia/transport_srtp.c
@@ -349,6 +349,9 @@ typedef struct transport_srtp
pj_uint32_t tx_ssrc;
+ /* ROC retry: prev_rx_roc.roc is the candidate currently applied. */
+ pj_bool_t rx_roc_retry_alt;
+
} transport_srtp;
@@ -917,7 +920,11 @@ PJ_DEF(pj_status_t) pjmedia_transport_srtp_get_setting(
pjmedia_srtp_setting *opt)
{
transport_srtp *srtp = (transport_srtp*) tp;
+
+ pj_lock_acquire(srtp->mutex);
*opt = srtp->setting;
+ pj_lock_release(srtp->mutex);
+
return PJ_SUCCESS;
}
@@ -929,7 +936,11 @@ PJ_DEF(pj_status_t) pjmedia_transport_srtp_modify_setting(
const pjmedia_srtp_setting *opt)
{
transport_srtp *srtp = (transport_srtp*) tp;
+
+ pj_lock_acquire(srtp->mutex);
srtp->setting = *opt;
+ pj_lock_release(srtp->mutex);
+
return PJ_SUCCESS;
}
@@ -943,6 +954,7 @@ static pj_status_t create_srtp_ctx(transport_srtp *srtp,
{
srtp_policy_t tx_;
srtp_policy_t rx_;
+ srtp_policy_t rx_tmpl;
srtp_err_status_t err;
int cr_tx_idx = 0;
int au_tx_idx = 0;
@@ -1054,8 +1066,10 @@ static pj_status_t create_srtp_ctx(transport_srtp *srtp,
else
rx_.rtp.sec_serv = sec_serv_none;
rx_.key = (uint8_t*)ctx->rx_key;
- if (setting->rx_roc.roc != 0 &&
- setting->rx_roc.ssrc != 0)
+ if (setting->rx_roc.ssrc != 0 &&
+ (setting->rx_roc.roc != 0 ||
+ (setting->prev_rx_roc.ssrc == setting->rx_roc.ssrc &&
+ setting->prev_rx_roc.roc != 0)))
{
rx_.ssrc.type = ssrc_specific;
rx_.ssrc.value = setting->rx_roc.ssrc;
@@ -1072,6 +1086,16 @@ static pj_status_t create_srtp_ctx(transport_srtp *srtp,
rx_.rtcp = rx_.rtp;
rx_.rtcp.auth_tag_len = crypto_suites[au_rx_idx].srtcp_auth_tag_len;
rx_.next = NULL;
+ if (rx_.ssrc.type == ssrc_specific) {
+ /* Keep a wildcard template alongside the specific stream, so
+ * a remote SSRC change after probation is still accepted.
+ */
+ rx_tmpl = rx_;
+ rx_tmpl.ssrc.type = ssrc_any_inbound;
+ rx_tmpl.ssrc.value = 0;
+ rx_tmpl.next = NULL;
+ rx_.next = &rx_tmpl;
+ }
err = srtp_create(&ctx->srtp_rx_ctx, &rx_);
if (err != srtp_err_status_ok) {
srtp_dealloc(ctx->srtp_tx_ctx);
@@ -1089,6 +1113,8 @@ static pj_status_t create_srtp_ctx(transport_srtp *srtp,
setting->rx_roc.ssrc, setting->rx_roc.roc,
(err == srtp_err_status_ok)? "succeeded": "failed"));
}
+ if (ctx == &srtp->srtp_ctx)
+ srtp->rx_roc_retry_alt = PJ_FALSE;
ctx->rx_policy = *rx;
pj_strset(&ctx->rx_policy.key, ctx->rx_key, rx->key.slen);
ctx->rx_policy.name=pj_str(crypto_suites[get_crypto_idx(&rx->name)].name);
@@ -1653,6 +1679,13 @@ static void srtp_rtp_cb(pjmedia_tp_cb_param *param)
return;
}
+ /* Runt packets cannot hold an RTP header. */
+ if (size < (pj_ssize_t)sizeof(pjmedia_rtp_hdr)) {
+ PJ_LOG(5, (srtp->pool->obj_name, "Runt RTP packet, size=%ld",
+ (long)size));
+ return;
+ }
+
/* Give the packet to keying first by invoking its send_rtp() op.
* Yes, the usage of send_rtp() is rather hacky, but it is convenient
* as the signature suits the purpose and it is ready to use
@@ -1751,6 +1784,7 @@ static void srtp_rtp_cb(pjmedia_tp_cb_param *param)
#if PJMEDIA_SRTP_CHECK_ROC_ON_RESTART
if (srtp->probation_cnt > 0 && err == srtp_err_status_auth_fail &&
+ rx_ssrc == srtp->setting.rx_roc.ssrc &&
srtp->setting.prev_rx_roc.ssrc != 0 &&
srtp->setting.prev_rx_roc.ssrc == srtp->setting.rx_roc.ssrc &&
srtp->setting.prev_rx_roc.roc != srtp->setting.rx_roc.roc)
@@ -1790,28 +1824,40 @@ static void srtp_rtp_cb(pjmedia_tp_cb_param *param)
}
} else if (need_retry) {
- unsigned roc, new_roc;
+ unsigned old_roc, new_roc;
srtp_err_status_t status;
- srtp_get_stream_roc(srtp->srtp_ctx.srtp_rx_ctx,
- srtp->setting.rx_roc.ssrc, &roc);
- new_roc = (roc == srtp->setting.rx_roc.roc?
- srtp->setting.prev_rx_roc.roc: srtp->setting.rx_roc.roc);
+ /* Try the alternative candidate, the setting is only updated
+ * after a packet authenticates.
+ */
+ old_roc = (srtp->rx_roc_retry_alt? srtp->setting.prev_rx_roc.roc :
+ srtp->setting.rx_roc.roc);
+ new_roc = (srtp->rx_roc_retry_alt? srtp->setting.rx_roc.roc :
+ srtp->setting.prev_rx_roc.roc);
status = srtp_set_stream_roc(srtp->srtp_ctx.srtp_rx_ctx,
srtp->setting.rx_roc.ssrc, new_roc);
if (status == srtp_err_status_ok) {
+ srtp->rx_roc_retry_alt = !srtp->rx_roc_retry_alt;
PJ_LOG(4, (srtp->pool->obj_name,
"Retrying to unprotect SRTP from ROC %d to new ROC %d",
- roc, new_roc));
+ old_roc, new_roc));
err = srtp_unprotect(srtp->srtp_ctx.srtp_rx_ctx, (pj_uint8_t*)pkt,
&len);
+ } else {
+ /* Cannot set the ROC, stop retrying. */
+ srtp->setting.prev_rx_roc.ssrc = 0;
+ PJ_LOG(4, (srtp->pool->obj_name,
+ "Failed to set RX ROC %d for retry, err=%s",
+ new_roc, get_libsrtp_errstr(status)));
}
}
if (err != srtp_err_status_ok) {
PJ_LOG(5,(srtp->pool->obj_name,
- "Failed to unprotect SRTP, pkt size=%ld, SSRC=%u, err=%s",
- size, rx_ssrc, get_libsrtp_errstr(err)));
+ "Failed to unprotect SRTP, pkt size=%ld, SSRC=%u, "
+ "seq=%u, err=%s",
+ size, rx_ssrc, ntohs(((pjmedia_rtp_hdr*)pkt)->seq),
+ get_libsrtp_errstr(err)));
} else {
cb = srtp->rtp_cb;
cb2 = srtp->rtp_cb2;
@@ -1822,6 +1868,18 @@ static void srtp_rtp_cb(pjmedia_tp_cb_param *param)
/* Save SSRC after successful SRTP unprotect */
srtp->rx_ssrc = rx_ssrc;
+
+ /* Settle the ROC retry on the SSRC it belongs to: write back
+ * the winning ROC and close the retry window.
+ */
+ if (srtp->setting.prev_rx_roc.ssrc != 0 &&
+ rx_ssrc == srtp->setting.rx_roc.ssrc)
+ {
+ if (srtp->rx_roc_retry_alt)
+ srtp->setting.rx_roc.roc = srtp->setting.prev_rx_roc.roc;
+ srtp->setting.prev_rx_roc.ssrc = 0;
+ srtp->rx_roc_retry_alt = PJ_FALSE;
+ }
}
pj_lock_release(srtp->mutex);
diff --git a/pjmedia/src/test/test.c b/pjmedia/src/test/test.c
index 795519397e..b14197f33a 100644
--- a/pjmedia/src/test/test.c
+++ b/pjmedia/src/test/test.c
@@ -115,6 +115,10 @@ int test_main(int argc, char *argv[])
#if HAS_TONE_DETECTOR_TEST
UT_ADD_TEST(&test_app.ut_app, tone_detector_test, 0);
#endif
+#if HAS_TRANSPORT_SRTP_TEST
+ /* Creates/destroys a local pjmedia_endpt (see codec_test_vectors). */
+ UT_ADD_TEST(&test_app.ut_app, transport_srtp_test, PJ_TEST_EXCLUSIVE);
+#endif
#if HAS_CODEC_VECTOR_TEST
/* Run in exclusive mode: creates/destroys a local pjmedia_endpt which
* sets/clears the global def_codec_mgr. If sdp_neg_test runs
diff --git a/pjmedia/src/test/test.h b/pjmedia/src/test/test.h
index 765a2b3b8f..7f08987115 100644
--- a/pjmedia/src/test/test.h
+++ b/pjmedia/src/test/test.h
@@ -39,6 +39,8 @@
#define HAS_MIPS_TEST WITH_BENCHMARK
#define HAS_CODEC_VECTOR_TEST 1
#define HAS_TONE_DETECTOR_TEST 1
+#define HAS_TRANSPORT_SRTP_TEST (PJMEDIA_HAS_SRTP && \
+ PJMEDIA_SRTP_CHECK_ROC_ON_RESTART)
int session_test(void);
int rtp_test(void);
@@ -53,6 +55,7 @@ int vid_codec_test(void);
int vid_dev_test(void);
int vid_port_test(void);
int tone_detector_test(void);
+int transport_srtp_test(void);
extern pj_pool_factory *mem;
void app_perror(pj_status_t status, const char *title);
diff --git a/pjmedia/src/test/transport_srtp_test.c b/pjmedia/src/test/transport_srtp_test.c
new file mode 100644
index 0000000000..a42284e63e
--- /dev/null
+++ b/pjmedia/src/test/transport_srtp_test.c
@@ -0,0 +1,663 @@
+/*
+ * Copyright (C) 2026 Teluu Inc. (http://www.teluu.com)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#include
+#include "test.h"
+
+#if HAS_TRANSPORT_SRTP_TEST
+
+#include
+
+#define THIS_FILE "transport_srtp_test.c"
+
+
+/* Tests for the SRTP RX roll over counter (ROC) handling:
+ * - a RX stream initialized with a ROC that does not match the sender is
+ * recovered by retrying with the alternative ROC candidate,
+ * - the ROC that authenticated is written back to the setting, so a later
+ * SRTP restart re-applies the correct one,
+ * - a remote SSRC change is still accepted after the probation period,
+ * which requires the wildcard template to be kept alongside the specific
+ * stream in the RX policy list,
+ * - a packet of a different SSRC authenticating first neither triggers
+ * nor closes the ROC retry,
+ * - a failed retry does not leak a candidate that has not authenticated
+ * into the setting.
+ *
+ * The transports are connected through an in-memory loopback pair, so the
+ * whole protect/unprotect path is exercised without sockets.
+ */
+
+#define TEST_SSRC 0x11111111u
+#define TEST_SSRC_ALT 0x22222222u
+
+/* 30-byte master keys (16-byte AES key + 14-byte salt) for the two
+ * directions. The SRTP transport requires transmit and receive keys to
+ * be different, and the sender's TX key must match the receiver's RX key.
+ */
+static char test_key_ab[31] = "0123456789abcdef0123456789abcd";
+static char test_key_ba[31] = "fedcba9876543210fedcba98765432";
+
+
+/* Minimal in-memory transport, delivering packets to a peer transport. */
+typedef struct loopback_tp
+{
+ pjmedia_transport base;
+ struct loopback_tp *peer;
+
+ /* Callbacks attached via attach2(). */
+ void *user_data;
+ void (*rtp_cb)(void *user_data, void *pkt, pj_ssize_t size);
+ void (*rtp_cb2)(pjmedia_tp_cb_param *param);
+ void (*rtcp_cb)(void *user_data, void *pkt, pj_ssize_t size);
+
+ /* Writable, 32bit aligned buffer for delivering one packet. */
+ pj_uint32_t rx_buf[128];
+} loopback_tp;
+
+
+static pj_status_t lb_get_info(pjmedia_transport *tp,
+ pjmedia_transport_info *info)
+{
+ PJ_UNUSED_ARG(tp);
+ PJ_UNUSED_ARG(info);
+ return PJ_SUCCESS;
+}
+
+static pj_status_t lb_attach2(pjmedia_transport *tp,
+ pjmedia_transport_attach_param *att)
+{
+ loopback_tp *lb = (loopback_tp*)tp;
+
+ PJ_ASSERT_RETURN(tp && att, PJ_EINVAL);
+
+ lb->user_data = att->user_data;
+ lb->rtp_cb = att->rtp_cb;
+ lb->rtp_cb2 = att->rtp_cb2;
+ lb->rtcp_cb = att->rtcp_cb;
+
+ return PJ_SUCCESS;
+}
+
+static void lb_detach(pjmedia_transport *tp, void *strm)
+{
+ loopback_tp *lb = (loopback_tp*)tp;
+
+ PJ_UNUSED_ARG(strm);
+
+ lb->user_data = NULL;
+ lb->rtp_cb = NULL;
+ lb->rtp_cb2 = NULL;
+ lb->rtcp_cb = NULL;
+}
+
+static pj_status_t lb_send_rtp(pjmedia_transport *tp,
+ const void *pkt,
+ pj_size_t size)
+{
+ loopback_tp *lb = (loopback_tp*)tp;
+ loopback_tp *peer = lb->peer;
+ pjmedia_tp_cb_param prm;
+
+ if (!peer)
+ return PJ_SUCCESS;
+
+ if (size > sizeof(peer->rx_buf))
+ return PJ_ETOOBIG;
+
+ /* Deliver a writable, 32bit aligned copy: the receiving SRTP layer
+ * decrypts the packet in place.
+ */
+ pj_memcpy(peer->rx_buf, pkt, size);
+
+ pj_bzero(&prm, sizeof(prm));
+ prm.user_data = peer->user_data;
+ prm.pkt = peer->rx_buf;
+ prm.size = (pj_ssize_t)size;
+
+ if (peer->rtp_cb2) {
+ (*peer->rtp_cb2)(&prm);
+ } else if (peer->rtp_cb) {
+ (*peer->rtp_cb)(peer->user_data, peer->rx_buf, (pj_ssize_t)size);
+ }
+
+ return PJ_SUCCESS;
+}
+
+static pj_status_t lb_send_rtcp(pjmedia_transport *tp,
+ const void *pkt,
+ pj_size_t size)
+{
+ PJ_UNUSED_ARG(tp);
+ PJ_UNUSED_ARG(pkt);
+ PJ_UNUSED_ARG(size);
+ return PJ_SUCCESS;
+}
+
+static pj_status_t lb_send_rtcp2(pjmedia_transport *tp,
+ const pj_sockaddr_t *addr,
+ unsigned addr_len,
+ const void *pkt,
+ pj_size_t size)
+{
+ PJ_UNUSED_ARG(tp);
+ PJ_UNUSED_ARG(addr);
+ PJ_UNUSED_ARG(addr_len);
+ PJ_UNUSED_ARG(pkt);
+ PJ_UNUSED_ARG(size);
+ return PJ_SUCCESS;
+}
+
+static pj_status_t lb_media_create(pjmedia_transport *tp,
+ pj_pool_t *sdp_pool,
+ unsigned options,
+ const pjmedia_sdp_session *sdp_remote,
+ unsigned media_index)
+{
+ PJ_UNUSED_ARG(tp);
+ PJ_UNUSED_ARG(sdp_pool);
+ PJ_UNUSED_ARG(options);
+ PJ_UNUSED_ARG(sdp_remote);
+ PJ_UNUSED_ARG(media_index);
+ return PJ_SUCCESS;
+}
+
+static pj_status_t lb_encode_sdp(pjmedia_transport *tp,
+ pj_pool_t *sdp_pool,
+ pjmedia_sdp_session *sdp_local,
+ const pjmedia_sdp_session *sdp_remote,
+ unsigned media_index)
+{
+ PJ_UNUSED_ARG(tp);
+ PJ_UNUSED_ARG(sdp_pool);
+ PJ_UNUSED_ARG(sdp_local);
+ PJ_UNUSED_ARG(sdp_remote);
+ PJ_UNUSED_ARG(media_index);
+ return PJ_SUCCESS;
+}
+
+static pj_status_t lb_media_start(pjmedia_transport *tp,
+ pj_pool_t *pool,
+ const pjmedia_sdp_session *sdp_local,
+ const pjmedia_sdp_session *sdp_remote,
+ unsigned media_index)
+{
+ PJ_UNUSED_ARG(tp);
+ PJ_UNUSED_ARG(pool);
+ PJ_UNUSED_ARG(sdp_local);
+ PJ_UNUSED_ARG(sdp_remote);
+ PJ_UNUSED_ARG(media_index);
+ return PJ_SUCCESS;
+}
+
+static pj_status_t lb_media_stop(pjmedia_transport *tp)
+{
+ PJ_UNUSED_ARG(tp);
+ return PJ_SUCCESS;
+}
+
+static pj_status_t lb_simulate_lost(pjmedia_transport *tp,
+ pjmedia_dir dir,
+ unsigned pct_lost)
+{
+ PJ_UNUSED_ARG(tp);
+ PJ_UNUSED_ARG(dir);
+ PJ_UNUSED_ARG(pct_lost);
+ return PJ_SUCCESS;
+}
+
+static pj_status_t lb_destroy(pjmedia_transport *tp)
+{
+ PJ_UNUSED_ARG(tp);
+ return PJ_SUCCESS;
+}
+
+
+static pjmedia_transport_op loopback_op =
+{
+ &lb_get_info,
+ NULL, /* &lb_attach, */
+ &lb_detach,
+ &lb_send_rtp,
+ &lb_send_rtcp,
+ &lb_send_rtcp2,
+ &lb_media_create,
+ &lb_encode_sdp,
+ &lb_media_start,
+ &lb_media_stop,
+ &lb_simulate_lost,
+ &lb_destroy,
+ &lb_attach2
+};
+
+
+/* Test fixture: SRTP sender over loopback A, SRTP receiver over loopback B. */
+typedef struct srtp_fixture
+{
+ loopback_tp *a, *b;
+ pjmedia_transport *tx, *rx;
+
+ /* Observed at the receiver's application callback. */
+ unsigned rx_count;
+ pj_uint32_t rx_last_ssrc;
+ pj_uint16_t rx_last_seq;
+} srtp_fixture;
+
+
+static void on_rx_rtp(void *user_data, void *pkt, pj_ssize_t size)
+{
+ srtp_fixture *f = (srtp_fixture*)user_data;
+ const pj_uint8_t *buf = (const pj_uint8_t*)pkt;
+
+ if (size < 12)
+ return;
+
+ f->rx_count++;
+ f->rx_last_seq = (pj_uint16_t)((buf[2] << 8) | buf[3]);
+ f->rx_last_ssrc = ((pj_uint32_t)buf[8] << 24) | ((pj_uint32_t)buf[9] << 16) |
+ ((pj_uint32_t)buf[10] << 8) | (pj_uint32_t)buf[11];
+}
+
+static void loopback_init(loopback_tp *lb, const char *name, loopback_tp *peer)
+{
+ pj_bzero(lb, sizeof(*lb));
+ pj_ansi_strncpy(lb->base.name, name, sizeof(lb->base.name) - 1);
+ lb->base.type = PJMEDIA_TRANSPORT_TYPE_USER;
+ lb->base.op = &loopback_op;
+ lb->peer = peer;
+}
+
+static void make_crypto(pjmedia_srtp_crypto *crypto, char *key)
+{
+ pj_bzero(crypto, sizeof(*crypto));
+ crypto->name = pj_str("AES_CM_128_HMAC_SHA1_80");
+ crypto->key = pj_str(key);
+}
+
+static void fixture_destroy(srtp_fixture *f);
+
+static pj_status_t fixture_init(srtp_fixture *f,
+ pj_pool_t *pool,
+ pjmedia_endpt *endpt,
+ const pjmedia_srtp_setting *tx_setting,
+ const pjmedia_srtp_setting *rx_setting)
+{
+ pjmedia_srtp_crypto crypto_ab, crypto_ba;
+ pjmedia_transport_attach_param att;
+ pj_status_t status;
+
+ pj_bzero(f, sizeof(*f));
+
+ f->a = PJ_POOL_ZALLOC_T(pool, loopback_tp);
+ f->b = PJ_POOL_ZALLOC_T(pool, loopback_tp);
+ loopback_init(f->a, "loopA", f->b);
+ loopback_init(f->b, "loopB", f->a);
+
+ status = pjmedia_transport_srtp_create(endpt, &f->a->base, tx_setting,
+ &f->tx);
+ if (status != PJ_SUCCESS) {
+ fixture_destroy(f);
+ return status;
+ }
+
+ status = pjmedia_transport_srtp_create(endpt, &f->b->base, rx_setting,
+ &f->rx);
+ if (status != PJ_SUCCESS) {
+ fixture_destroy(f);
+ return status;
+ }
+
+ /* Key A protects A->B, key B protects B->A. */
+ make_crypto(&crypto_ab, test_key_ab);
+ make_crypto(&crypto_ba, test_key_ba);
+
+ status = pjmedia_transport_srtp_start(f->tx, &crypto_ab, &crypto_ba);
+ if (status != PJ_SUCCESS) {
+ fixture_destroy(f);
+ return status;
+ }
+
+ status = pjmedia_transport_srtp_start(f->rx, &crypto_ba, &crypto_ab);
+ if (status != PJ_SUCCESS) {
+ fixture_destroy(f);
+ return status;
+ }
+
+ pj_bzero(&att, sizeof(att));
+ att.media_type = PJMEDIA_TYPE_AUDIO;
+ att.user_data = f;
+ att.rtp_cb = &on_rx_rtp;
+
+ status = pjmedia_transport_attach2(f->rx, &att);
+ if (status != PJ_SUCCESS)
+ fixture_destroy(f);
+
+ return status;
+}
+
+static void fixture_destroy(srtp_fixture *f)
+{
+ /* Also closes the member (loopback) transports. */
+ if (f->tx) {
+ pjmedia_transport_close(f->tx);
+ f->tx = NULL;
+ }
+ if (f->rx) {
+ pjmedia_transport_close(f->rx);
+ f->rx = NULL;
+ }
+}
+
+/* Send one RTP packet (20 bytes: 12 byte header + 8 byte payload). */
+static void fixture_send(srtp_fixture *f, pj_uint32_t ssrc, pj_uint16_t seq)
+{
+ pj_uint8_t buf[20];
+ pj_uint32_t v32;
+
+ pj_bzero(buf, sizeof(buf));
+ buf[0] = 0x80; /* V=2 */
+ buf[1] = 96; /* dynamic PT, outside RTCP range */
+ buf[2] = (pj_uint8_t)(seq >> 8);
+ buf[3] = (pj_uint8_t)(seq & 0xFF);
+ v32 = pj_htonl(160u * seq); /* timestamp */
+ pj_memcpy(&buf[4], &v32, 4);
+ v32 = pj_htonl(ssrc);
+ pj_memcpy(&buf[8], &v32, 4);
+ pj_memset(&buf[12], 0x55, 8); /* payload */
+
+ pjmedia_transport_send_rtp(f->tx, buf, sizeof(buf));
+}
+
+
+#define CHECK(cond, msg) \
+ do { \
+ if (!(cond)) { PJ_LOG(3,(THIS_FILE, "FAILED: %s", msg)); rc = 1; } \
+ else { PJ_LOG(4,(THIS_FILE, "ok: %s", msg)); } \
+ } while (0)
+
+
+/* The receiver is initialized with ROC 1 while the sender starts at
+ * ROC 0 (e.g. the remote restarted its stream). The first packet cannot
+ * authenticate, the retry must find ROC 0, and the winning ROC must be
+ * written back to the setting with the retry window closed afterwards.
+ */
+static int scenario_retry_recovery(pj_pool_t *pool, pjmedia_endpt *endpt)
+{
+ srtp_fixture f;
+ pjmedia_srtp_setting tx_setting, rx_setting, after;
+ pj_status_t status;
+ int rc = 0;
+
+ pjmedia_srtp_setting_default(&tx_setting);
+ pjmedia_srtp_setting_default(&rx_setting);
+
+ rx_setting.rx_roc.ssrc = TEST_SSRC;
+ rx_setting.rx_roc.roc = 1;
+ rx_setting.prev_rx_roc.ssrc = TEST_SSRC;
+ rx_setting.prev_rx_roc.roc = 0;
+
+ status = fixture_init(&f, pool, endpt, &tx_setting, &rx_setting);
+ CHECK(status == PJ_SUCCESS, "fixture init (ROC mismatch)");
+ if (status != PJ_SUCCESS)
+ return 1;
+
+ fixture_send(&f, TEST_SSRC, 1000); /* must recover via the retry */
+ fixture_send(&f, TEST_SSRC, 1001);
+ fixture_send(&f, TEST_SSRC, 1002);
+
+ CHECK(f.rx_count == 3, "all packets delivered despite ROC mismatch");
+
+ CHECK(pjmedia_transport_srtp_get_setting(f.rx, &after) == PJ_SUCCESS,
+ "get_setting");
+ CHECK(after.rx_roc.ssrc == TEST_SSRC && after.rx_roc.roc == 0,
+ "winning ROC 0 written back to rx_roc");
+ CHECK(after.prev_rx_roc.ssrc == 0,
+ "retry window closed after the stream authenticated");
+
+ fixture_destroy(&f);
+ return rc;
+}
+
+
+/* Remote SSRC change after the probation period: the receiver has a
+ * specific stream for SSRC 1, but must still accept SSRC 2 through the
+ * wildcard template kept alongside in the RX policy list.
+ */
+static int scenario_new_ssrc(pj_pool_t *pool, pjmedia_endpt *endpt)
+{
+ srtp_fixture f;
+ pjmedia_srtp_setting tx_setting, rx_setting;
+ pj_status_t status;
+ unsigned i;
+ int rc = 0;
+
+ pjmedia_srtp_setting_default(&tx_setting);
+ pjmedia_srtp_setting_default(&rx_setting);
+
+ /* Selects ssrc_specific(TEST_SSRC) + template, and matches the
+ * sender's ROC, so no retry is involved here.
+ */
+ rx_setting.rx_roc.ssrc = TEST_SSRC;
+ rx_setting.rx_roc.roc = 0;
+ rx_setting.prev_rx_roc.ssrc = TEST_SSRC;
+ rx_setting.prev_rx_roc.roc = 1;
+
+ status = fixture_init(&f, pool, endpt, &tx_setting, &rx_setting);
+ CHECK(status == PJ_SUCCESS, "fixture init (SSRC change)");
+ if (status != PJ_SUCCESS)
+ return 1;
+
+ /* Exhaust the SRTP probation (100 packets) on the known SSRC, so the
+ * new SSRC cannot be recovered by the probation restart path.
+ */
+ for (i = 0; i < 105; i++)
+ fixture_send(&f, TEST_SSRC, (pj_uint16_t)(2000 + i));
+
+ CHECK(f.rx_count == 105, "packets of the known SSRC delivered");
+
+ fixture_send(&f, TEST_SSRC_ALT, 5000);
+
+ CHECK(f.rx_count == 106, "packet with new SSRC delivered via template");
+ CHECK(f.rx_last_ssrc == TEST_SSRC_ALT, "delivered packet has the new SSRC");
+
+ fixture_destroy(&f);
+ return rc;
+}
+
+
+/* Remote change shape: the receiver expects ROC 0 while the sender
+ * continues at ROC 1. The retry must recover with the non-zero ROC and
+ * write it back, so a later SRTP restart re-applies the winning ROC.
+ */
+static int scenario_winner_writeback(pj_pool_t *pool, pjmedia_endpt *endpt)
+{
+ srtp_fixture f;
+ pjmedia_srtp_setting tx_setting, rx_setting, after;
+ pj_status_t status;
+ int rc = 0;
+
+ pjmedia_srtp_setting_default(&tx_setting);
+ pjmedia_srtp_setting_default(&rx_setting);
+
+ /* Sender transmits with ROC 1. */
+ tx_setting.tx_roc.ssrc = TEST_SSRC;
+ tx_setting.tx_roc.roc = 1;
+
+ /* Receiver expects ROC 0, with the sender's ROC 1 as alternative. */
+ rx_setting.rx_roc.ssrc = TEST_SSRC;
+ rx_setting.rx_roc.roc = 0;
+ rx_setting.prev_rx_roc.ssrc = TEST_SSRC;
+ rx_setting.prev_rx_roc.roc = 1;
+
+ status = fixture_init(&f, pool, endpt, &tx_setting, &rx_setting);
+ CHECK(status == PJ_SUCCESS, "fixture init (winner write-back)");
+ if (status != PJ_SUCCESS)
+ return 1;
+
+ fixture_send(&f, TEST_SSRC, 1000); /* must recover via the retry */
+ fixture_send(&f, TEST_SSRC, 1001);
+
+ CHECK(f.rx_count == 2, "packets delivered despite ROC mismatch");
+
+ CHECK(pjmedia_transport_srtp_get_setting(f.rx, &after) == PJ_SUCCESS,
+ "get_setting");
+ CHECK(after.rx_roc.ssrc == TEST_SSRC && after.rx_roc.roc == 1,
+ "winning ROC 1 written back to rx_roc");
+ CHECK(after.prev_rx_roc.ssrc == 0,
+ "retry window closed after the stream authenticated");
+
+ fixture_destroy(&f);
+ return rc;
+}
+
+
+/* A packet of a different SSRC authenticates first (e.g. an SBC
+ * switching the media source); it must neither trigger nor close the
+ * ROC retry of the SSRC the candidates belong to.
+ */
+static int scenario_other_ssrc_first(pj_pool_t *pool, pjmedia_endpt *endpt)
+{
+ srtp_fixture f;
+ pjmedia_srtp_setting tx_setting, rx_setting, after;
+ pj_status_t status;
+ int rc = 0;
+
+ pjmedia_srtp_setting_default(&tx_setting);
+ pjmedia_srtp_setting_default(&rx_setting);
+
+ rx_setting.rx_roc.ssrc = TEST_SSRC;
+ rx_setting.rx_roc.roc = 1;
+ rx_setting.prev_rx_roc.ssrc = TEST_SSRC;
+ rx_setting.prev_rx_roc.roc = 0;
+
+ status = fixture_init(&f, pool, endpt, &tx_setting, &rx_setting);
+ CHECK(status == PJ_SUCCESS, "fixture init (other SSRC first)");
+ if (status != PJ_SUCCESS)
+ return 1;
+
+ fixture_send(&f, TEST_SSRC_ALT, 4000);
+ CHECK(f.rx_count == 1, "packet of other SSRC delivered via template");
+
+ fixture_send(&f, TEST_SSRC, 1000); /* must still recover via retry */
+ fixture_send(&f, TEST_SSRC, 1001);
+
+ CHECK(f.rx_count == 3, "known SSRC recovers after the other SSRC");
+
+ CHECK(pjmedia_transport_srtp_get_setting(f.rx, &after) == PJ_SUCCESS,
+ "get_setting");
+ CHECK(after.rx_roc.ssrc == TEST_SSRC && after.rx_roc.roc == 0,
+ "winning ROC 0 written back to rx_roc");
+ CHECK(after.prev_rx_roc.ssrc == 0,
+ "retry window closed after the stream authenticated");
+
+ fixture_destroy(&f);
+ return rc;
+}
+
+
+/* Neither ROC candidate matches the sender: the retries fail, but they
+ * must not leak a candidate that has not authenticated into the
+ * setting, and the retry window must stay open.
+ */
+static int scenario_retry_keeps_setting(pj_pool_t *pool, pjmedia_endpt *endpt)
+{
+ srtp_fixture f;
+ pjmedia_srtp_setting tx_setting, rx_setting, after;
+ pj_status_t status;
+ int rc = 0;
+
+ pjmedia_srtp_setting_default(&tx_setting);
+ pjmedia_srtp_setting_default(&rx_setting);
+
+ /* Sender transmits with ROC 5. */
+ tx_setting.tx_roc.ssrc = TEST_SSRC;
+ tx_setting.tx_roc.roc = 5;
+
+ rx_setting.rx_roc.ssrc = TEST_SSRC;
+ rx_setting.rx_roc.roc = 1;
+ rx_setting.prev_rx_roc.ssrc = TEST_SSRC;
+ rx_setting.prev_rx_roc.roc = 0;
+
+ status = fixture_init(&f, pool, endpt, &tx_setting, &rx_setting);
+ CHECK(status == PJ_SUCCESS, "fixture init (setting untouched)");
+ if (status != PJ_SUCCESS)
+ return 1;
+
+ /* An odd number of failed retries, so an alternating leak would
+ * show up in the setting.
+ */
+ fixture_send(&f, TEST_SSRC, 1000);
+ fixture_send(&f, TEST_SSRC, 1001);
+ fixture_send(&f, TEST_SSRC, 1002);
+
+ CHECK(f.rx_count == 0, "no packet can authenticate");
+
+ CHECK(pjmedia_transport_srtp_get_setting(f.rx, &after) == PJ_SUCCESS,
+ "get_setting");
+ CHECK(after.rx_roc.ssrc == TEST_SSRC && after.rx_roc.roc == 1,
+ "setting untouched by failed retries");
+ CHECK(after.prev_rx_roc.ssrc == TEST_SSRC && after.prev_rx_roc.roc == 0,
+ "retry window still open with the original candidates");
+
+ fixture_destroy(&f);
+ return rc;
+}
+
+
+int transport_srtp_test(void)
+{
+ pjmedia_endpt *endpt = NULL;
+ pj_pool_t *pool;
+ pj_status_t status;
+ int rc = 0;
+
+ status = pjmedia_endpt_create2(mem, NULL, 0, &endpt);
+ if (status != PJ_SUCCESS) {
+ app_perror(status, "Cannot create media endpoint");
+ return 1;
+ }
+
+ pool = pj_pool_create(mem, THIS_FILE, 4000, 4000, NULL);
+ if (!pool) {
+ pjmedia_endpt_destroy2(endpt);
+ return 1;
+ }
+
+ rc |= scenario_retry_recovery(pool, endpt);
+ rc |= scenario_new_ssrc(pool, endpt);
+ rc |= scenario_winner_writeback(pool, endpt);
+ rc |= scenario_other_ssrc_first(pool, endpt);
+ rc |= scenario_retry_keeps_setting(pool, endpt);
+
+ pj_pool_release(pool);
+ pjmedia_endpt_destroy2(endpt);
+
+ return rc;
+}
+
+
+#else
+
+
+int transport_srtp_test(void)
+{
+ return 0;
+}
+
+
+#endif