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