From 8b36bf22948ec88a0f45918fdb9b474053fcde3b Mon Sep 17 00:00:00 2001 From: Denis Kollar Date: Mon, 21 Sep 2026 16:08:54 +0200 Subject: [PATCH 1/2] Fix SRTP ROC retry after transport reinit being a no-op After a media reinit following an IP change, the SRTP transport may receive packets with a rolled-over sequence number, so the RX roll over counter (ROC) carried over from the previous session no longer matches. The retry logic introduced in #2846 (fixing #2682) is supposed to try the alternative ROC candidate when unprotect fails with srtp_err_status_auth_fail. However, the retry candidate was selected by comparing against srtp_get_stream_roc(), which reports the replay database value. libsrtp keeps the ROC set via srtp_set_stream_roc() in a separate pending state and only folds it into the replay database once a packet has been unprotected successfully. Since the retry exists precisely for the case where no packet authenticates yet, the comparison always re-selected the ROC that was already pending, and the alternative candidate was never tried. Instead of copying libsrtp's pending ROC state, track which of the two ROC candidates is currently applied in the transport itself, so the candidates alternate on each retry without depending on libsrtp internals. The setting is only updated when a packet authenticates: the winning ROC is written back to setting.rx_roc.roc, so a later SRTP restart re-applies the correct one, and prev_rx_roc.ssrc is cleared to close the retry window. The window also closes when srtp_set_stream_roc() fails, so the failure is logged once and stops. The retry and the window close are bound to the SSRC the candidates belong to, so with the wildcard template in the policy list, a packet of a different SSRC authenticating first neither triggers nor closes the ROC retry. In the remote-change case (rx_roc.roc=0 with a known prev_rx_roc) the RX policy now picks ssrc_specific when the SSRC matches, so the stream exists when the retry calls srtp_set_stream_roc() -- previously ssrc_any_inbound left only a template, which srtp_get_stream() does not search. A wildcard template is kept alongside via the policy list, so a remote SSRC change after probation is still accepted, while changes within probation keep using the restart path (#4490). The policy condition mirrors need_retry, so a leftover prev_rx_roc with a different SSRC cannot select the specific stream. Also take the transport mutex in pjmedia_transport_srtp_get_setting() and pjmedia_transport_srtp_modify_setting(), which copy the whole setting from a thread other than the media thread. Also reject runt packets that cannot hold an RTP header before any header field is read (logged at level 5, like the other per-packet drop log), and log the RTP sequence number when unprotect fails, which tells whether the sender has passed a sequence rollover. The rx_roc/prev_rx_roc docs in transport_srtp.h now mention that the transport updates them itself. Co-authored-by: Jan Seeger --- pjmedia/include/pjmedia/transport_srtp.h | 7 ++- pjmedia/src/pjmedia/transport_srtp.c | 78 +++++++++++++++++++++--- 2 files changed, 74 insertions(+), 11 deletions(-) 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); From c115beeb12fd19e8458efe0fe4cab0f070558ac0 Mon Sep 17 00:00:00 2001 From: Denis Kollar Date: Tue, 6 Oct 2026 10:41:16 +0200 Subject: [PATCH 2/2] Add pjmedia-test coverage for the SRTP ROC retry path Five scenarios over an in-memory loopback transport pair, exercising the full protect/unprotect path without sockets: - a RX stream initialized with a ROC that does not match the sender is recovered by the retry, and the winning ROC is written back to the setting with the retry window closed afterwards (both directions: winner 0 and winner 1), - a remote SSRC change after the probation period is still accepted through the wildcard template 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 test fails against the unpatched transport_srtp.c (no retry recovery), when the wildcard template is dropped from the policy list, and when the retry state is kept in the setting instead of the transport, so a future libsrtp or policy change cannot silently regress the fix. It is registered as exclusive, as it creates and destroys a local pjmedia_endpt, and compiled out when SRTP or the ROC retry is disabled. Co-authored-by: Jan Seeger --- pjmedia/CMakeLists.txt | 1 + pjmedia/build/Makefile | 2 +- pjmedia/build/pjmedia_test.vcproj | 4 + pjmedia/build/pjmedia_test.vcxproj | 1 + pjmedia/build/pjmedia_test.vcxproj.filters | 3 + pjmedia/src/test/test.c | 4 + pjmedia/src/test/test.h | 3 + pjmedia/src/test/transport_srtp_test.c | 663 +++++++++++++++++++++ 8 files changed, 680 insertions(+), 1 deletion(-) create mode 100644 pjmedia/src/test/transport_srtp_test.c 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/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