diff --git a/include/asterisk/res_pjsip_session.h b/include/asterisk/res_pjsip_session.h index f2e741d76f..3a2be8e895 100644 --- a/include/asterisk/res_pjsip_session.h +++ b/include/asterisk/res_pjsip_session.h @@ -235,6 +235,8 @@ struct ast_sip_session { unsigned int moh_passthrough:1; /*! Whether early media state has been confirmed through PRACK */ unsigned int early_confirmed:1; + /*! Delayed BYE is waiting behind a UAC INVITE with a fallback timeout */ + unsigned int terminate_on_invite_timeout:1; /*! DTMF mode to use with this session, from endpoint but can change */ enum ast_sip_dtmf_mode dtmf; /*! Initial incoming INVITE Request-URI. NULL otherwise. */ diff --git a/res/res_pjsip_session.c b/res/res_pjsip_session.c index 4a1fb56bfa..91fbf15b96 100644 --- a/res/res_pjsip_session.c +++ b/res/res_pjsip_session.c @@ -1394,6 +1394,19 @@ static void delayed_request_free(struct ast_sip_session_delayed_request *delay) ast_free(delay); } +/* + * Delayed requests own pending/active media states. Keep cleanup centralized so + * timeout-driven teardown releases the same state as normal session teardown. + */ +static void flush_delayed_requests(struct ast_sip_session *session) +{ + struct ast_sip_session_delayed_request *delay; + + while ((delay = AST_LIST_REMOVE_HEAD(&session->delayed_requests, next))) { + delayed_request_free(delay); + } +} + /*! * \internal * \brief Send a delayed request @@ -1429,6 +1442,8 @@ static int send_delayed_request(struct ast_sip_session *session, struct ast_sip_ delay->active_media_state = NULL; SCOPE_EXIT_RTN_VALUE(res, "%s\n", ast_sip_session_get_name(session)); case DELAYED_METHOD_BYE: + /* The delayed BYE is being sent now; timeout cleanup is no longer needed. */ + session->terminate_on_invite_timeout = 0; ast_sip_session_terminate(session, 0); SCOPE_EXIT_RTN_VALUE(0, "%s: Terminating session on delayed BYE\n", ast_sip_session_get_name(session)); } @@ -1629,6 +1644,57 @@ static int delay_request(struct ast_sip_session *session, SCOPE_EXIT_RTN_VALUE(0); } +/* + * A UAC re-INVITE that has received a provisional response may no longer have + * Timer B running. If its final response is malformed and rejected before + * transaction processing, invite_tsx can keep the session alive indefinitely. + * Arm PJPROJECT's INVITE timeout so delayed BYE cleanup has a bounded wait. + */ +static int set_outstanding_invite_timeout(struct ast_sip_session *session) +{ + pjsip_transaction *tsx; + pj_status_t status; + + if (!session->inv_session || !session->inv_session->invite_tsx) { + return 0; + } + + tsx = session->inv_session->invite_tsx; + if (tsx->role != PJSIP_ROLE_UAC || tsx->method.id != PJSIP_INVITE_METHOD + || tsx->state >= PJSIP_TSX_STATE_COMPLETED) { + return 0; + } + + status = pjsip_tsx_set_timeout(tsx, pjsip_cfg()->tsx.td); + if (status != PJ_SUCCESS && status != PJ_EEXISTS) { + char errmsg[PJ_ERR_MSG_SIZE]; + + pj_strerror(status, errmsg, sizeof(errmsg)); + ast_log(LOG_WARNING, "%s: Failed to set timeout on outstanding INVITE transaction: %s\n", + ast_sip_session_get_name(session), errmsg); + return 0; + } + + return 1; +} + +/* + * PJPROJECT treats 408/481 on in-dialog UAC requests as dialog terminating. + * When that happens after our timeout, drop Asterisk's queued BYE instead of + * sending a duplicate BYE. + */ +static int uac_invite_tsx_terminates_dialog(pjsip_transaction *tsx) +{ + if (tsx->role != PJSIP_ROLE_UAC || tsx->method.id != PJSIP_INVITE_METHOD + || tsx->state < PJSIP_TSX_STATE_COMPLETED) { + return 0; + } + + return tsx->status_code == PJSIP_SC_CALL_TSX_DOES_NOT_EXIST + || (tsx->status_code == PJSIP_SC_REQUEST_TIMEOUT + && !pjsip_cfg()->endpt.keep_inv_after_tsx_timeout); +} + static pjmedia_sdp_session *generate_session_refresh_sdp(struct ast_sip_session *session) { pjsip_inv_session *inv_session = session->inv_session; @@ -2920,7 +2986,6 @@ static int datastore_cmp(void *obj, void *arg, int flags) static void session_destructor(void *obj) { struct ast_sip_session *session = obj; - struct ast_sip_session_delayed_request *delay; #ifdef TEST_FRAMEWORK /* We dup the endpoint ID in case the endpoint gets freed out from under us */ @@ -2955,9 +3020,7 @@ static void session_destructor(void *obj) ast_sip_session_media_state_free(session->active_media_state); ast_sip_session_media_state_free(session->pending_media_state); - while ((delay = AST_LIST_REMOVE_HEAD(&session->delayed_requests, next))) { - delayed_request_free(delay); - } + flush_delayed_requests(session); ast_party_id_free(&session->id); ao2_cleanup(session->endpoint); ao2_cleanup(session->aor); @@ -3424,22 +3487,26 @@ void ast_sip_session_terminate(struct ast_sip_session *session, int response) if (session->inv_session->invite_tsx) { ast_debug(3, "%s: Delay sending BYE because of outstanding transaction...\n", ast_sip_session_get_name(session)); - /* If this is delayed the only thing that will happen is a BYE request so we don't - * actually need to store the response code for when it happens. + /* + * If this is delayed the only thing that will happen is a BYE request, so + * no response code needs to be stored. Queue the BYE as before, then arm + * a transaction timeout so a malformed/lost final re-INVITE response + * cannot leave the session and RTP state referenced forever. */ - delay_request(session, NULL, NULL, NULL, 0, DELAYED_METHOD_BYE, NULL, NULL, 1); + if (delay_request(session, NULL, NULL, NULL, 0, DELAYED_METHOD_BYE, NULL, NULL, 1)) { + ast_log(LOG_ERROR, "%s: Unable to delay BYE request\n", + ast_sip_session_get_name(session)); + } else if (set_outstanding_invite_timeout(session)) { + session->terminate_on_invite_timeout = 1; + } break; } /* Fall through */ default: status = pjsip_inv_end_session(session->inv_session, response, NULL, &packet); if (status == PJ_SUCCESS && packet) { - struct ast_sip_session_delayed_request *delay; - /* Flush any delayed requests so they cannot overlap this transaction. */ - while ((delay = AST_LIST_REMOVE_HEAD(&session->delayed_requests, next))) { - delayed_request_free(delay); - } + flush_delayed_requests(session); if (packet->msg->type == PJSIP_RESPONSE_MSG) { ast_sip_session_send_response(session, packet); @@ -4924,6 +4991,17 @@ static void session_inv_on_tsx_state_changed(pjsip_inv_session *inv, pjsip_trans break; } + if (session->terminate_on_invite_timeout && uac_invite_tsx_terminates_dialog(tsx)) { + /* + * PJPROJECT already considers this dialog terminated; the delayed BYE is + * obsolete and still owns media state that must be released. + */ + ast_debug(3, "%s: Flushing delayed requests because outstanding INVITE terminated dialog\n", + ast_sip_session_get_name(session)); + flush_delayed_requests(session); + session->terminate_on_invite_timeout = 0; + } + if (AST_LIST_EMPTY(&session->delayed_requests)) { /* No delayed request pending, so just return */ SCOPE_EXIT_RTN("Nothing delayed\n");