From 1c6d19fd1b980fb01c0d1f2c9c9ea2353396dfad Mon Sep 17 00:00:00 2001 From: Greg Anthony <5074839+gaanthony@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:29:33 -0400 Subject: [PATCH] pjnath: add ICE-Lite session support Add an ICE-Lite session option that keeps the agent controlled, responds to incoming connectivity checks, forms valid and nominated pairs, and never originates ordinary or triggered checks. Preserve bounded nomination timeout behavior for silent peers. Add in-memory coverage for peer-reflexive nomination, trickle checklist updates, early checks, role enforcement, and timeout behavior. All implementation and tests were reviewed and validated by the contributor; AI assistance was used for code drafting and review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pjnath/include/pjnath/ice_session.h | 11 +- pjnath/src/pjnath-test/ice_test.c | 252 ++++++++++++++++++++++++++++ pjnath/src/pjnath-test/test.c | 2 +- pjnath/src/pjnath-test/test.h | 2 +- pjnath/src/pjnath/ice_session.c | 102 ++++++++++- 5 files changed, 360 insertions(+), 9 deletions(-) diff --git a/pjnath/include/pjnath/ice_session.h b/pjnath/include/pjnath/ice_session.h index 2bc4508224..4905fe4a25 100644 --- a/pjnath/include/pjnath/ice_session.h +++ b/pjnath/include/pjnath/ice_session.h @@ -721,6 +721,16 @@ typedef struct pj_ice_sess_options */ pj_bool_t check_src_addr; + /** + * Specify whether the session implements ICE-Lite. An ICE-Lite session + * always uses the controlled role, responds to connectivity checks, and + * never originates ordinary or triggered connectivity checks. The + * application is responsible for supplying host candidates only. + * + * Default value is PJ_FALSE. + */ + pj_bool_t lite; + } pj_ice_sess_options; @@ -1170,4 +1180,3 @@ PJ_END_DECL #endif /* __PJNATH_ICE_SESSION_H__ */ - diff --git a/pjnath/src/pjnath-test/ice_test.c b/pjnath/src/pjnath-test/ice_test.c index 1b21b1486b..06343661ce 100644 --- a/pjnath/src/pjnath-test/ice_test.c +++ b/pjnath/src/pjnath-test/ice_test.c @@ -1648,6 +1648,7 @@ struct wvp_sess pj_str_t pass; pj_bool_t completed; pj_status_t status; + unsigned binding_requests_sent; }; static struct wvp_sess *wvp_find_dst(struct wvp_test *t, @@ -1670,9 +1671,16 @@ static pj_status_t wvp_on_tx_pkt(pj_ice_sess *ice, unsigned comp_id, struct wvp_test *t = src->test; struct wvp_sess *dst; struct wvp_pkt *p; + pj_uint16_t msg_type; PJ_UNUSED_ARG(dst_addr_len); + if (size >= sizeof(msg_type)) { + pj_memcpy(&msg_type, pkt, sizeof(msg_type)); + if (pj_ntohs(msg_type) == PJ_STUN_BINDING_REQUEST) + ++src->binding_requests_sent; + } + /* Sends to the bogon address fail synchronously, as if the local route * to it is prohibited (EPERM). */ @@ -2037,3 +2045,247 @@ int ice_wait_valid_pair_test(void) return rc; } + +int ice_lite_test(void) +{ + app_sess_t app_sess; + pj_str_t bogon_ip; + pj_status_t status; + int rc = 0; + + PJ_LOG(3,(THIS_FILE, "ICE-Lite")); + pj_log_push_indent(); + + status = create_stun_config(&app_sess); + if (status != PJ_SUCCESS) { + pj_log_pop_indent(); + return -10; + } + + bogon_ip = pj_str((char*)WVP_BOGON_IP); + + /* A full agent nominates a peer-reflexive pair on the ICE-Lite agent. */ + { + struct wvp_test t; + struct wvp_sess caller, callee; + pj_ice_sess_cand rcand; + pj_ice_sess_options opt; + unsigned i; + + pj_bzero(&t, sizeof(t)); + pj_bzero(&caller, sizeof(caller)); + pj_bzero(&callee, sizeof(callee)); + t.stun_cfg = &app_sess.stun_cfg; + t.pool = app_sess.pool; + pj_list_init(&t.txq); + pj_sockaddr_init(pj_AF_INET(), &t.bogon, &bogon_ip, WVP_BOGON_PORT); + t.caller = &caller; + t.callee = &callee; + + if ((rc=wvp_create_sess(&t, &caller, "liteUA1", + PJ_ICE_SESS_ROLE_CONTROLLING, 34201, -2))) { + rc = -20; goto s1_done; + } + if ((rc=wvp_create_sess(&t, &callee, "liteUA2", + PJ_ICE_SESS_ROLE_CONTROLLED, 34202, -2))) { + rc = -21; goto s1_done; + } + + pj_ice_sess_get_options(callee.ice, &opt); + opt.lite = PJ_TRUE; + opt.trickle = PJ_ICE_SESS_TRICKLE_FULL; + if (pj_ice_sess_set_options(callee.ice, &opt) != PJ_SUCCESS) { + rc = -22; goto s1_done; + } + if (pj_ice_sess_change_role(callee.ice, + PJ_ICE_SESS_ROLE_CONTROLLING) != + PJ_EINVALIDOP) + { + rc = -23; goto s1_done; + } + + wvp_init_rcand(&rcand, &callee.addr); + if (pj_ice_sess_create_check_list(caller.ice, &callee.ufrag, + &callee.pass, 1, &rcand)) { + rc = -24; goto s1_done; + } + wvp_init_rcand(&rcand, &t.bogon); + if (pj_ice_sess_create_check_list(callee.ice, &caller.ufrag, + &caller.pass, 1, &rcand)) { + rc = -25; goto s1_done; + } + + if (pj_ice_sess_start_check(callee.ice) != PJ_SUCCESS) { + rc = -26; goto s1_done; + } + if (pj_ice_sess_update_check_list(callee.ice, &caller.ufrag, + &caller.pass, 1, &rcand, + PJ_FALSE) != PJ_SUCCESS) { + rc = -27; goto s1_done; + } + if (pj_ice_sess_start_check(caller.ice) != PJ_SUCCESS) { + rc = -28; goto s1_done; + } + + for (i=0; i<80 && !(caller.completed && callee.completed); ++i) + wvp_poll(&t, 100); + + if (!caller.completed || caller.status != PJ_SUCCESS) { + rc = -29; goto s1_done; + } + if (!callee.completed || callee.status != PJ_SUCCESS) { + rc = -30; goto s1_done; + } + if (callee.ice->comp[0].nominated_check->rcand->type != + PJ_ICE_CAND_TYPE_PRFLX) + { + rc = -31; goto s1_done; + } + if (caller.binding_requests_sent == 0 || + callee.binding_requests_sent != 0) + { + rc = -32; goto s1_done; + } + if (t.shim_err) { + rc = -33; goto s1_done; + } + + PJ_LOG(3,(THIS_FILE, INDENT "ok: peer-reflexive pair nominated " + "without outbound checks")); + + s1_done: + if (caller.ice) pj_ice_sess_destroy(caller.ice); + if (callee.ice) pj_ice_sess_destroy(callee.ice); + pj_list_init(&t.txq); + if (rc) goto on_return; + } + + /* A silent peer causes bounded failure without an outbound check. */ + { + struct wvp_test t; + struct wvp_sess callee; + pj_ice_sess_cand rcand; + pj_ice_sess_options opt; + + pj_bzero(&t, sizeof(t)); + pj_bzero(&callee, sizeof(callee)); + t.stun_cfg = &app_sess.stun_cfg; + t.pool = app_sess.pool; + pj_list_init(&t.txq); + pj_sockaddr_init(pj_AF_INET(), &t.bogon, &bogon_ip, WVP_BOGON_PORT); + t.callee = &callee; + + if ((rc=wvp_create_sess(&t, &callee, "liteUA3", + PJ_ICE_SESS_ROLE_CONTROLLED, 34203, -2))) { + rc = -40; goto s2_done; + } + + pj_ice_sess_get_options(callee.ice, &opt); + opt.lite = PJ_TRUE; + opt.controlled_agent_want_nom_timeout = 100; + if (pj_ice_sess_set_options(callee.ice, &opt) != PJ_SUCCESS) { + rc = -41; goto s2_done; + } + wvp_init_rcand(&rcand, &t.bogon); + if (pj_ice_sess_create_check_list(callee.ice, &callee.ufrag, + &callee.pass, 1, &rcand)) { + rc = -42; goto s2_done; + } + if (pj_ice_sess_start_check(callee.ice) != PJ_SUCCESS) { + rc = -43; goto s2_done; + } + + wvp_poll(&t, 300); + if (!callee.completed || + callee.status != PJNATH_EICENOMTIMEOUT || + callee.binding_requests_sent != 0) + { + rc = -44; goto s2_done; + } + + PJ_LOG(3,(THIS_FILE, INDENT "ok: silent peer timed out without " + "outbound checks")); + + s2_done: + if (callee.ice) pj_ice_sess_destroy(callee.ice); + pj_list_init(&t.txq); + if (rc) goto on_return; + } + + /* An early nominated check completes after remote data is installed. */ + { + struct wvp_test t; + struct wvp_sess caller, callee; + pj_ice_sess_cand rcand; + pj_ice_sess_options opt; + unsigned i; + + pj_bzero(&t, sizeof(t)); + pj_bzero(&caller, sizeof(caller)); + pj_bzero(&callee, sizeof(callee)); + t.stun_cfg = &app_sess.stun_cfg; + t.pool = app_sess.pool; + pj_list_init(&t.txq); + pj_sockaddr_init(pj_AF_INET(), &t.bogon, &bogon_ip, WVP_BOGON_PORT); + t.caller = &caller; + t.callee = &callee; + + if ((rc=wvp_create_sess(&t, &caller, "liteUA4", + PJ_ICE_SESS_ROLE_CONTROLLING, 34204, -2))) { + rc = -50; goto s3_done; + } + if ((rc=wvp_create_sess(&t, &callee, "liteUA5", + PJ_ICE_SESS_ROLE_CONTROLLED, 34205, -2))) { + rc = -51; goto s3_done; + } + + pj_ice_sess_get_options(callee.ice, &opt); + opt.lite = PJ_TRUE; + if (pj_ice_sess_set_options(callee.ice, &opt) != PJ_SUCCESS) { + rc = -52; goto s3_done; + } + + wvp_init_rcand(&rcand, &callee.addr); + if (pj_ice_sess_create_check_list(caller.ice, &callee.ufrag, + &callee.pass, 1, &rcand)) { + rc = -53; goto s3_done; + } + if (pj_ice_sess_start_check(caller.ice) != PJ_SUCCESS) { + rc = -54; goto s3_done; + } + wvp_poll(&t, 100); + + wvp_init_rcand(&rcand, &caller.addr); + if (pj_ice_sess_create_check_list(callee.ice, &caller.ufrag, + &caller.pass, 1, &rcand)) { + rc = -55; goto s3_done; + } + if (pj_ice_sess_start_check(callee.ice) != PJ_SUCCESS) { + rc = -56; goto s3_done; + } + + for (i=0; i<20 && !callee.completed; ++i) + wvp_poll(&t, 100); + + if (!callee.completed || callee.status != PJ_SUCCESS || + callee.binding_requests_sent != 0 || t.shim_err) + { + rc = -57; goto s3_done; + } + + PJ_LOG(3,(THIS_FILE, INDENT "ok: early check completed without " + "outbound checks")); + + s3_done: + if (caller.ice) pj_ice_sess_destroy(caller.ice); + if (callee.ice) pj_ice_sess_destroy(callee.ice); + pj_list_init(&t.txq); + if (rc) goto on_return; + } + +on_return: + destroy_stun_config(&app_sess); + pj_log_pop_indent(); + + return rc; +} diff --git a/pjnath/src/pjnath-test/test.c b/pjnath/src/pjnath-test/test.c index a46be0c72c..03698cfad3 100644 --- a/pjnath/src/pjnath-test/test.c +++ b/pjnath/src/pjnath-test/test.c @@ -249,6 +249,7 @@ static int test_inner(int argc, char *argv[]) #if INCLUDE_ICE_TEST UT_ADD_TEST(&test_app.ut_app, ice_wait_valid_pair_test, 0); + UT_ADD_TEST(&test_app.ut_app, ice_lite_test, 0); #endif #if INCLUDE_TURN_SOCK_TEST @@ -301,4 +302,3 @@ int test_main(int argc, char *argv[]) return -1; } - diff --git a/pjnath/src/pjnath-test/test.h b/pjnath/src/pjnath-test/test.h index e96ce6b054..0c1ffe00ba 100644 --- a/pjnath/src/pjnath-test/test.h +++ b/pjnath/src/pjnath-test/test.h @@ -70,6 +70,7 @@ int ice_test(void*); int ice_conc_test(void); int trickle_ice_test(void); int ice_wait_valid_pair_test(void); +int ice_lite_test(void); int concur_test(void); int test_main(int argc, char *argv[]); @@ -123,4 +124,3 @@ pj_turn_tp_type get_turn_tp_type(pj_uint32_t flag); #define ERR_MEMORY_LEAK 1 #define ERR_TIMER_LEAK 2 - diff --git a/pjnath/src/pjnath/ice_session.c b/pjnath/src/pjnath/ice_session.c index de4876657c..85eb723f80 100644 --- a/pjnath/src/pjnath/ice_session.c +++ b/pjnath/src/pjnath/ice_session.c @@ -332,6 +332,7 @@ PJ_DEF(void) pj_ice_sess_options_default(pj_ice_sess_options *opt) opt->wait_valid_pair_timeout = PJ_ICE_WAIT_VALID_PAIR_TIMEOUT; opt->trickle = PJ_ICE_SESS_TRICKLE_DISABLED; opt->check_src_addr = PJ_ICE_SESS_CHECK_SRC_ADDR; + opt->lite = PJ_FALSE; } /* @@ -458,7 +459,11 @@ PJ_DEF(pj_status_t) pj_ice_sess_set_options(pj_ice_sess *ice, const pj_ice_sess_options *opt) { PJ_ASSERT_RETURN(ice && opt, PJ_EINVAL); + + pj_grp_lock_acquire(ice->grp_lock); pj_memcpy(&ice->opt, opt, sizeof(*opt)); + if (ice->opt.lite) + ice->role = PJ_ICE_SESS_ROLE_CONTROLLED; ice->is_trickling = (ice->opt.trickle != PJ_ICE_SESS_TRICKLE_DISABLED); if (ice->is_trickling) { LOG5((ice->obj_name, "Trickle ICE is active (%s mode)", @@ -474,6 +479,7 @@ PJ_DEF(pj_status_t) pj_ice_sess_set_options(pj_ice_sess *ice, LOG5((ice->obj_name, "ICE nomination type set to %s", (ice->opt.aggressive ? "aggressive" : "regular"))); + pj_grp_lock_release(ice->grp_lock); return PJ_SUCCESS; } @@ -565,6 +571,9 @@ PJ_DEF(pj_status_t) pj_ice_sess_change_role(pj_ice_sess *ice, { PJ_ASSERT_RETURN(ice, PJ_EINVAL); + if (ice->opt.lite && new_role != PJ_ICE_SESS_ROLE_CONTROLLED) + return PJ_EINVALIDOP; + if (new_role != ice->role) { ice->role = new_role; LOG4((ice->obj_name, "Role changed to %s", role_names[new_role])); @@ -2172,7 +2181,8 @@ static pj_status_t add_rcand_and_update_checklist( /* For trickle ICE: resume the periodic check, it may be halted when * there is no available check pair. */ - if (ice->opt.trickle != PJ_ICE_SESS_TRICKLE_DISABLED && + if (!ice->opt.lite && + ice->opt.trickle != PJ_ICE_SESS_TRICKLE_DISABLED && clist->count > 0 && !ice->is_complete && clist->state == PJ_ICE_SESS_CHECKLIST_ST_RUNNING) { @@ -2675,7 +2685,7 @@ PJ_DEF(pj_status_t) pj_ice_sess_start_check(pj_ice_sess *ice) pj_log_push_indent(); /* If we are using aggressive nomination, set the is_nominating state */ - if (ice->opt.aggressive) + if (ice->opt.aggressive && !ice->opt.lite) ice->is_nominating = PJ_TRUE; /* The agent examines the check list for the first media stream (a @@ -2691,7 +2701,7 @@ PJ_DEF(pj_status_t) pj_ice_sess_start_check(pj_ice_sess *ice) */ clist = &ice->clist; - for (i=0; i < clist->foundation_cnt; ++i) { + for (i=0; !ice->opt.lite && i < clist->foundation_cnt; ++i) { unsigned k; pj_ice_sess_check *chk = NULL; @@ -2742,12 +2752,33 @@ PJ_DEF(pj_status_t) pj_ice_sess_start_check(pj_ice_sess *ice) } pj_list_init(&ice->early_check); + if (ice->opt.lite && !ice->is_complete && + ice->timer.id == TIMER_NONE && + ice->opt.controlled_agent_want_nom_timeout >= 0) + { + pj_time_val delay; + + delay.sec = 0; + delay.msec = ice->opt.controlled_agent_want_nom_timeout; + pj_time_val_normalize(&delay); + status = pj_timer_heap_schedule_w_grp_lock( + ice->stun_cfg.timer_heap, + &ice->timer, &delay, + TIMER_CONTROLLED_WAIT_NOM, + ice->grp_lock); + if (status != PJ_SUCCESS) { + pj_grp_lock_release(ice->grp_lock); + pj_log_pop_indent(); + return status; + } + } + /* Start periodic check */ /* We could start it immediately like below, but lets schedule timer * instead to reduce stack usage: * return start_periodic_check(ice->stun_cfg.timer_heap, &clist->timer); */ - if (!pj_timer_entry_running(&clist->timer)) { + if (!ice->opt.lite && !pj_timer_entry_running(&clist->timer)) { pj_time_val delay = {0, 0}; status = pj_timer_heap_schedule_w_grp_lock(ice->stun_cfg.timer_heap, &clist->timer, &delay, @@ -3325,6 +3356,13 @@ static pj_status_t on_stun_rx_request(pj_stun_session *sess, src_addr, src_addr_len); pj_grp_lock_release(ice->grp_lock); return PJ_SUCCESS; + } else if (ice->opt.lite) { + /* An ICE-Lite agent never changes to the controlling role. */ + pj_stun_session_respond(sess, rdata, PJ_STUN_SC_ROLE_CONFLICT, + NULL, token, PJ_TRUE, + src_addr, src_addr_len); + pj_grp_lock_release(ice->grp_lock); + return PJ_SUCCESS; } else { /* Switch role to controlled */ LOG4((ice->obj_name, @@ -3438,6 +3476,47 @@ static pj_status_t on_stun_rx_request(pj_stun_session *sess, } +/* Complete a candidate pair from an incoming ICE-Lite check. */ +static void complete_incoming_lite_check(pj_ice_sess *ice, + pj_ice_sess_check *check) +{ + pj_ice_sess_check *valid; + unsigned i; + + for (i=0; ivalid_list.count; ++i) { + valid = &ice->valid_list.checks[i]; + if (valid->lcand == check->lcand && valid->rcand == check->rcand) + break; + } + + if (i == ice->valid_list.count) { + if (ice->valid_list.count >= PJ_ICE_MAX_CHECKS) { + LOG4((ice->obj_name, "Unable to add ICE-Lite valid pair: " + "valid list is full")); + check_set_state(ice, check, PJ_ICE_SESS_CHECK_STATE_FAILED, + PJ_ETOOMANY); + on_check_complete(ice, check); + return; + } + + valid = &ice->valid_list.checks[ice->valid_list.count++]; + pj_bzero(valid, sizeof(*valid)); + valid->lcand = check->lcand; + valid->rcand = check->rcand; + valid->prio = check->prio; + valid->foundation_idx = check->foundation_idx; + valid->state = PJ_ICE_SESS_CHECK_STATE_SUCCEEDED; + valid->err_code = PJ_SUCCESS; + } + + check_set_state(ice, check, PJ_ICE_SESS_CHECK_STATE_SUCCEEDED, + PJ_SUCCESS); + valid->nominated = (valid->nominated || check->nominated); + update_comp_check(ice, valid->lcand->comp_id, valid); + sort_checklist(ice, &ice->valid_list); + on_check_complete(ice, check); +} + /* Handle incoming Binding request and perform triggered check. * This function may be called by on_stun_rx_request(), or when * SDP answer is received and we have received early checks. @@ -3569,6 +3648,11 @@ static void handle_incoming_check(pj_ice_sess *ice, rcand->checked = PJ_TRUE; + if (ice->opt.lite) { + complete_incoming_lite_check(ice, c); + return; + } + if (c->state == PJ_ICE_SESS_CHECK_STATE_FROZEN || c->state == PJ_ICE_SESS_CHECK_STATE_WAITING) { @@ -3659,6 +3743,14 @@ static void handle_incoming_check(pj_ice_sess *ice, LOG4((ice->obj_name, "New triggered check added: %d", check_id)); + if (ice->opt.lite) { + c->foundation_idx = get_check_foundation_idx(ice, lcand, rcand, + PJ_TRUE); + complete_incoming_lite_check(ice, c); + sort_checklist(ice, &ice->clist); + return; + } + /* If we are nominating in regular nomination, don't nominate this * newly found pair. */ @@ -3931,5 +4023,3 @@ PJ_DEF(pj_status_t) pj_ice_sess_on_rx_pkt(pj_ice_sess *ice, return status; } - -