From 85f586c76df8a8750191a86b56e801aee5f83312 Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Fri, 12 Sep 2025 17:18:38 +0200 Subject: [PATCH] Add N2 Handover Cancel (8.4.5 of 3GPP TS 38.413) This commit completes the NGAP Mobility Management procedure for N2 handover. Upon failure on source gNB the Handover Cancel message is sent to the AMF which in return responds with an ack. --- openair2/COMMON/ngap_messages_def.h | 2 + openair2/COMMON/ngap_messages_types.h | 20 +++++++ openair2/RRC/NR/rrc_gNB_NGAP.c | 24 ++++++++ openair2/RRC/NR/rrc_gNB_NGAP.h | 1 + openair2/RRC/NR/rrc_gNB_mobility.c | 11 ++++ openair3/NGAP/ngap_gNB.c | 55 ++++++++++++++++++ openair3/NGAP/ngap_gNB_handlers.c | 42 +++++++++++++- openair3/NGAP/ngap_gNB_mobility_management.c | 59 ++++++++++++++++++++ openair3/NGAP/ngap_gNB_mobility_management.h | 2 + 9 files changed, 215 insertions(+), 1 deletion(-) diff --git a/openair2/COMMON/ngap_messages_def.h b/openair2/COMMON/ngap_messages_def.h index bdeb2455b4..98c1f82260 100644 --- a/openair2/COMMON/ngap_messages_def.h +++ b/openair2/COMMON/ngap_messages_def.h @@ -75,6 +75,7 @@ MESSAGE_DEF(NGAP_HANDOVER_REQUIRED, MESSAGE_PRIORITY_MED, ngap_handover_required MESSAGE_DEF(NGAP_HANDOVER_FAILURE, MESSAGE_PRIORITY_MED, ngap_handover_failure_t, ngap_handover_failure) MESSAGE_DEF(NGAP_HANDOVER_REQUEST_ACKNOWLEDGE, MESSAGE_PRIORITY_MED, ngap_handover_request_ack_t, ngap_handover_request_ack) MESSAGE_DEF(NGAP_HANDOVER_NOTIFY, MESSAGE_PRIORITY_MED, ngap_handover_notify_t, ngap_handover_notify) +MESSAGE_DEF(NGAP_HANDOVER_CANCEL, MESSAGE_PRIORITY_MED, ngap_handover_cancel_t, ngap_handover_cancel) MESSAGE_DEF(NGAP_UL_RAN_STATUS_TRANSFER, MESSAGE_PRIORITY_MED, ngap_ran_status_transfer_t, ngap_ul_ran_status_transfer) MESSAGE_DEF(NGAP_DL_RAN_STATUS_TRANSFER, MESSAGE_PRIORITY_MED, ngap_ran_status_transfer_t, ngap_dl_ran_status_transfer) @@ -89,6 +90,7 @@ MESSAGE_DEF(NGAP_PDUSESSION_RELEASE_COMMAND , MESSAGE_PRIORITY_MED, ngap_pdu MESSAGE_DEF(NGAP_UE_CONTEXT_RELEASE_COMMAND, MESSAGE_PRIORITY_MED, ngap_ue_release_command_t , ngap_ue_release_command) MESSAGE_DEF(NGAP_HANDOVER_REQUEST, MESSAGE_PRIORITY_MED, ngap_handover_request_t, ngap_handover_request) MESSAGE_DEF(NGAP_HANDOVER_COMMAND, MESSAGE_PRIORITY_MED, ngap_handover_command_t, ngap_handover_command) +MESSAGE_DEF(NGAP_HANDOVER_CANCEL_ACK, MESSAGE_PRIORITY_MED, ngap_handover_cancel_ack_t, ngap_handover_cancel_ack) /* NGAP <-> RRC messages (can be initiated either by MME or gNB) */ MESSAGE_DEF(NGAP_UE_CONTEXT_RELEASE_REQ , MESSAGE_PRIORITY_MED, ngap_ue_release_req_t , ngap_ue_release_req) diff --git a/openair2/COMMON/ngap_messages_types.h b/openair2/COMMON/ngap_messages_types.h index a8c2bab58a..ef25f6997d 100644 --- a/openair2/COMMON/ngap_messages_types.h +++ b/openair2/COMMON/ngap_messages_types.h @@ -71,6 +71,8 @@ #define NGAP_HANDOVER_REQUEST_ACKNOWLEDGE(mSGpTR) (mSGpTR)->ittiMsg.ngap_handover_request_ack #define NGAP_HANDOVER_COMMAND(mSGpTR) (mSGpTR)->ittiMsg.ngap_handover_command #define NGAP_HANDOVER_NOTIFY(mSGpTR) (mSGpTR)->ittiMsg.ngap_handover_notify +#define NGAP_HANDOVER_CANCEL(mSGpTR) (mSGpTR)->ittiMsg.ngap_handover_cancel +#define NGAP_HANDOVER_CANCEL_ACK(mSGpTR) (mSGpTR)->ittiMsg.ngap_handover_cancel_ack #define NGAP_UE_CONTEXT_RELEASE_REQ(mSGpTR) (mSGpTR)->ittiMsg.ngap_ue_release_req #define NGAP_PDUSESSION_RELEASE_COMMAND(mSGpTR) (mSGpTR)->ittiMsg.ngap_pdusession_release_command @@ -701,6 +703,24 @@ typedef struct { user_location_information_t user_info; } ngap_handover_notify_t; +/* 9.2.3.11 3GPP TS 38.413 */ +typedef struct { + // RAN UE NGAP ID + uint32_t gNB_ue_ngap_id; + // AMF UE NGAP ID + uint64_t amf_ue_ngap_id; + // Cause + ngap_cause_t cause; +} ngap_handover_cancel_t; + +/* 9.2.3.12 3GPP TS 38.413 */ +typedef struct { + // RAN UE NGAP ID + uint32_t gNB_ue_ngap_id; + // AMF UE NGAP ID + uint64_t amf_ue_ngap_id; +} ngap_handover_cancel_ack_t; + typedef struct ngap_ue_cap_info_ind_s { uint32_t gNB_ue_ngap_id; byte_array_t ue_radio_cap; diff --git a/openair2/RRC/NR/rrc_gNB_NGAP.c b/openair2/RRC/NR/rrc_gNB_NGAP.c index d4068b46a5..be7dce5ca1 100644 --- a/openair2/RRC/NR/rrc_gNB_NGAP.c +++ b/openair2/RRC/NR/rrc_gNB_NGAP.c @@ -1403,6 +1403,30 @@ void rrc_gNB_send_NGAP_HANDOVER_NOTIFY(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE) itti_send_msg_to_task(TASK_NGAP, rrc->module_id, msg_p); } +/** @brief Prepare NG Handover Cancel (source NG-RAN) and inform NGAP (N2-based HO only) + * 3GPP TS 38.413 §8.4.5 (Handover Cancellation) - source NG-RAN -> AMF + * 3GPP TS 38.413 §9.2.3.11 (HANDOVER CANCEL) + * @param rrc CU/RRC instance + * @param UE UE context + * @param cause cancellation cause */ +void rrc_gNB_send_NGAP_HANDOVER_CANCEL(int module_id, gNB_RRC_UE_t *UE, ngap_cause_t cause) +{ + DevAssert(UE != NULL); + + LOG_I(NR_RRC, "Triggering NGAP Handover Cancel (gNB_ue_ngap_id=%d, amf_ue_ngap_id=%ld)\n", UE->rrc_ue_id, UE->amf_ue_ngap_id); + + MessageDef *msg_p = itti_alloc_new_message(TASK_RRC_GNB, 0, NGAP_HANDOVER_CANCEL); + ngap_handover_cancel_t *ho_cancel = &NGAP_HANDOVER_CANCEL(msg_p); + memset(ho_cancel, 0, sizeof(*ho_cancel)); + + /* Mandatory IEs (38.413 §9.2.3.11) */ + ho_cancel->gNB_ue_ngap_id = UE->rrc_ue_id; + ho_cancel->amf_ue_ngap_id = UE->amf_ue_ngap_id; + ho_cancel->cause = cause; + + itti_send_msg_to_task(TASK_NGAP, module_id, msg_p); +} + void rrc_gNB_send_NGAP_PDUSESSION_RELEASE_RESPONSE(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE, uint8_t xid) { int pdu_sessions_released = 0; diff --git a/openair2/RRC/NR/rrc_gNB_NGAP.h b/openair2/RRC/NR/rrc_gNB_NGAP.h index d1867173a2..b90d9a1493 100644 --- a/openair2/RRC/NR/rrc_gNB_NGAP.h +++ b/openair2/RRC/NR/rrc_gNB_NGAP.h @@ -105,6 +105,7 @@ void rrc_gNB_process_HandoverCommand(gNB_RRC_INST *rrc, const ngap_handover_comm void rrc_gNB_free_Handover_Command(ngap_handover_command_t *msg); void rrc_gNB_send_NGAP_HANDOVER_NOTIFY(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE); +void rrc_gNB_send_NGAP_HANDOVER_CANCEL(int module_id, gNB_RRC_UE_t *UE, ngap_cause_t cause); int rrc_gNB_send_NGAP_ul_ran_status_transfer(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE, const int n_to_mod, const e1_pdcp_status_info_t *pdcp_status); diff --git a/openair2/RRC/NR/rrc_gNB_mobility.c b/openair2/RRC/NR/rrc_gNB_mobility.c index 614c27e242..07b742e16c 100644 --- a/openair2/RRC/NR/rrc_gNB_mobility.c +++ b/openair2/RRC/NR/rrc_gNB_mobility.c @@ -489,6 +489,16 @@ static void nr_rrc_n2_ho_complete(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE) rrc_gNB_send_NGAP_HANDOVER_NOTIFY(rrc, UE); } +/** @brief This callback is used by the source gNB to inform the AMF + * about the cancellation of an ongoing handover */ +static void nr_rrc_n2_ho_cancel(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE) +{ + DevAssert(UE->ho_context); + DevAssert(UE->ho_context->source); + ngap_cause_t cause = {.type = NGAP_CAUSE_RADIO_NETWORK, .value = NGAP_CAUSE_RADIO_NETWORK_HANDOVER_CANCELLED}; + rrc_gNB_send_NGAP_HANDOVER_CANCEL(rrc->module_id, UE, cause); +} + /** @brief Callback function to trigger NG Handover Failure on the target gNB, to inform the AMF * that the preparation of resources has failed (e.g. unsatisfied criteria, gNB is already loaded). * This message represents an Unsuccessful Outcome of the Handover Resource Allocation */ @@ -546,6 +556,7 @@ void nr_rrc_trigger_n2_ho(gNB_RRC_INST *rrc, ue->ho_context = alloc_ho_ctx(HO_CTX_SOURCE); ue->ho_context->source->du = get_du_for_ue(rrc, ue->rrc_ue_id); ue->ho_context->source->ho_status_transfer = rrc_gNB_send_NGAP_ul_ran_status_transfer; + ue->ho_context->source->ho_cancel = nr_rrc_n2_ho_cancel; rrc_gNB_send_NGAP_HANDOVER_REQUIRED(rrc, ue, neighbour_config, hoPrepInfo); free_byte_array(hoPrepInfo); diff --git a/openair3/NGAP/ngap_gNB.c b/openair3/NGAP/ngap_gNB.c index afa69c4c9d..8c9ac6a973 100644 --- a/openair3/NGAP/ngap_gNB.c +++ b/openair3/NGAP/ngap_gNB.c @@ -463,6 +463,57 @@ static int ngap_gNB_handover_notify(instance_t instance, const ngap_handover_not return 0; } +/** @brief NGAP: Handover Cancel callback */ +static int ngap_gNB_handover_cancel(instance_t instance, const ngap_handover_cancel_t *msg) +{ + DevAssert(msg != NULL); + byte_array_t ba = {.buf = NULL, .len = 0}; + + // Retrieve NGAP gNB instance + ngap_gNB_instance_t *ngap_gNB_instance_p = ngap_gNB_get_instance(instance); + DevAssert(ngap_gNB_instance_p != NULL); + + // we must have the UE context by RAN UE NGAP ID + ngap_gNB_ue_context_t *ue_context_p = ngap_get_ue_context(msg->gNB_ue_ngap_id); + if (ue_context_p == NULL) { + NGAP_ERROR("Failed to encode Handover Cancel: no UE context for gNB_ue_ngap_id=%d\n", msg->gNB_ue_ngap_id); + return -1; + } + + if (ue_context_p->gNB_ue_ngap_id != msg->gNB_ue_ngap_id) { + NGAP_ERROR("Failed to encode Handover Cancel: mismatched gNB_ue_ngap_id=%d\n", msg->gNB_ue_ngap_id); + return -1; + } + + if (ue_context_p->amf_ue_ngap_id != msg->amf_ue_ngap_id) { + NGAP_ERROR("Failed to encode Handover Cancel: mismatched amf_ue_ngap_id=%ld\n", msg->amf_ue_ngap_id); + return -1; + } + + /* Encode NGAP PDU */ + NGAP_NGAP_PDU_t *pdu = encode_ng_handover_cancel(msg); + if (pdu == NULL) { + NGAP_ERROR("Failed to encode NG Handover Cancel\n"); + return -1; + } + + if (ngap_gNB_encode_pdu(pdu, &ba.buf, (uint32_t *)&ba.len) < 0) { + NGAP_ERROR("Failed to encode NG Handover Cancel message PDU\n"); + ASN_STRUCT_FREE(asn_DEF_NGAP_NGAP_PDU, pdu); + return -1; + } + ASN_STRUCT_FREE(asn_DEF_NGAP_NGAP_PDU, pdu); + + /* UE-associated signalling -> use the UE's allocated SCTP stream */ + ngap_gNB_itti_send_sctp_data_req(ngap_gNB_instance_p->instance, + ue_context_p->amf_ref->assoc_id, + ba.buf, + ba.len, + ue_context_p->tx_stream); + + return 0; +} + int ngap_gNB_handle_ul_ran_status_transfer(instance_t instance, const ngap_ran_status_transfer_t *msg) { NGAP_DEBUG("Triggered UL RAN Status Transfer\n"); @@ -587,6 +638,10 @@ void *ngap_gNB_process_itti_msg(void *notUsed) { ngap_gNB_handover_notify(instance, &NGAP_HANDOVER_NOTIFY(received_msg)); break; + case NGAP_HANDOVER_CANCEL: + ngap_gNB_handover_cancel(instance, &NGAP_HANDOVER_CANCEL(received_msg)); + break; + case NGAP_UL_RAN_STATUS_TRANSFER: ngap_gNB_handle_ul_ran_status_transfer(instance, &NGAP_UL_RAN_STATUS_TRANSFER(received_msg)); break; diff --git a/openair3/NGAP/ngap_gNB_handlers.c b/openair3/NGAP/ngap_gNB_handlers.c index 0257e49698..95c2dd45a7 100644 --- a/openair3/NGAP/ngap_gNB_handlers.c +++ b/openair3/NGAP/ngap_gNB_handlers.c @@ -1051,6 +1051,46 @@ static int ngap_gNB_handle_handover_command(sctp_assoc_t assoc_id, uint32_t stre return 0; } +/** @brief Handler for NG Handover Cancel Acknowledge */ +static int ngap_gNB_handle_handover_cancel_ack(sctp_assoc_t assoc_id, uint32_t stream, NGAP_NGAP_PDU_t *pdu) +{ + NGAP_INFO("Received NG Handover Cancel Acknowledge\n"); + ngap_gNB_amf_data_t *amf_desc_p = NULL; + DevAssert(pdu != NULL); + + amf_desc_p = ngap_gNB_get_AMF(NULL, assoc_id, 0); + if (amf_desc_p == NULL) { + NGAP_ERROR("[SCTP %u] Received Handover Cancel Ack for non-existing AMF context\n", assoc_id); + return -1; + } + + MessageDef *message_p = itti_alloc_new_message(TASK_NGAP, 0, NGAP_HANDOVER_CANCEL_ACK); + ngap_handover_cancel_ack_t *msg = &NGAP_HANDOVER_CANCEL_ACK(message_p); + + if (decode_ng_handover_cancel_ack(msg, pdu) < 0) { + NGAP_ERROR("Failed to decode NG Handover Cancel Acknowledge\n"); + itti_free(TASK_NGAP, message_p); + return -1; + } + + ngap_gNB_ue_context_t *ue_desc_p = ngap_get_ue_context(msg->gNB_ue_ngap_id); + if (!ue_desc_p) { + NGAP_ERROR("[SCTP %u] Handover Cancel Ack for unknown UE context (gNB_ue_ngap_id %d)\n", assoc_id, msg->gNB_ue_ngap_id); + itti_free(TASK_NGAP, message_p); + return -1; + } + + if (ue_desc_p->amf_ue_ngap_id != msg->amf_ue_ngap_id) { + NGAP_WARN("AMF UE NGAP ID mismatch in HO Cancel Ack (ctx %ld != msg %ld)", ue_desc_p->amf_ue_ngap_id, msg->amf_ue_ngap_id); + } + + ue_desc_p->rx_stream = stream; + + // cancel was already handled at RRC level, do not forward + + return 0; +} + static int ngap_gNB_handle_paging(sctp_assoc_t assoc_id, uint32_t stream, NGAP_NGAP_PDU_t *pdu) { ngap_gNB_amf_data_t *amf_desc_p = NULL; @@ -1484,7 +1524,7 @@ const ngap_message_decoded_callback ngap_messages_callback[][3] = { {ngap_gNB_handle_dl_ran_status_transfer, 0, 0}, /* DownlinkRANStatusTransfer */ {0, 0, 0}, /* DownlinkUEAssociatedNRPPaTransport */ {ngap_gNB_handle_error_indication, 0, 0}, /* ErrorIndication */ - {0, 0, 0}, /* HandoverCancel */ + {0, ngap_gNB_handle_handover_cancel_ack, 0}, /* HandoverCancel */ {0, 0, 0}, /* HandoverNotification */ {0, ngap_gNB_handle_handover_command, 0}, /* HandoverPreparation */ {ngap_gNB_handle_handover_request, 0, 0}, /* HandoverResourceAllocation */ diff --git a/openair3/NGAP/ngap_gNB_mobility_management.c b/openair3/NGAP/ngap_gNB_mobility_management.c index 4f1383e5e0..3d281be7c9 100644 --- a/openair3/NGAP/ngap_gNB_mobility_management.c +++ b/openair3/NGAP/ngap_gNB_mobility_management.c @@ -618,6 +618,65 @@ NGAP_NGAP_PDU_t *encode_ng_handover_notify(const ngap_handover_notify_t *msg) return pdu; } +/** @brief NGAP Handover Cancel encoding (9.2.3.11 3GPP TS 38.413 Handover Cancel) + * 8.4.5 Handover Cancellation (source NG-RAN -> AMF) */ +NGAP_NGAP_PDU_t *encode_ng_handover_cancel(const ngap_handover_cancel_t *msg) +{ + DevAssert(msg != NULL); + + NGAP_NGAP_PDU_t *pdu = calloc_or_fail(1, sizeof(*pdu)); + + pdu->present = NGAP_NGAP_PDU_PR_initiatingMessage; + asn1cCalloc(pdu->choice.initiatingMessage, head); + head->procedureCode = NGAP_ProcedureCode_id_HandoverCancel; + head->criticality = NGAP_Criticality_reject; + head->value.present = NGAP_InitiatingMessage__value_PR_HandoverCancel; + NGAP_HandoverCancel_t *out = &head->value.choice.HandoverCancel; + + // AMF UE NGAP ID (M) + asn1cSequenceAdd(out->protocolIEs.list, NGAP_HandoverCancelIEs_t, ie1); + ie1->id = NGAP_ProtocolIE_ID_id_AMF_UE_NGAP_ID; + ie1->criticality = NGAP_Criticality_reject; + ie1->value.present = NGAP_HandoverCancelIEs__value_PR_AMF_UE_NGAP_ID; + asn_uint642INTEGER(&ie1->value.choice.AMF_UE_NGAP_ID, msg->amf_ue_ngap_id); + + // RAN UE NGAP ID (M) + asn1cSequenceAdd(out->protocolIEs.list, NGAP_HandoverCancelIEs_t, ie2); + ie2->id = NGAP_ProtocolIE_ID_id_RAN_UE_NGAP_ID; + ie2->criticality = NGAP_Criticality_reject; + ie2->value.present = NGAP_HandoverCancelIEs__value_PR_RAN_UE_NGAP_ID; + ie2->value.choice.RAN_UE_NGAP_ID = msg->gNB_ue_ngap_id; + + // Cause (M) + asn1cSequenceAdd(out->protocolIEs.list, NGAP_HandoverCancelIEs_t, ie3); + ie3->id = NGAP_ProtocolIE_ID_id_Cause; + ie3->criticality = NGAP_Criticality_reject; + ie3->value.present = NGAP_HandoverCancelIEs__value_PR_Cause; + encode_ngap_cause(&ie3->value.choice.Cause, &msg->cause); + + return pdu; +} + +/** @brief Decode NGAP Handover Cancel Acknowledge (9.2.3.12 3GPP TS 38.413) */ +int decode_ng_handover_cancel_ack(ngap_handover_cancel_ack_t *out, const NGAP_NGAP_PDU_t *pdu) +{ + DevAssert(out != NULL); + DevAssert(pdu != NULL); + + NGAP_HandoverCancelAcknowledge_t *container = &pdu->choice.successfulOutcome->value.choice.HandoverCancelAcknowledge; + NGAP_HandoverCancelAcknowledgeIEs_t *ie; + + /* AMF UE NGAP ID (M) */ + NGAP_FIND_PROTOCOLIE_BY_ID(NGAP_HandoverCancelAcknowledgeIEs_t, ie, container, NGAP_ProtocolIE_ID_id_AMF_UE_NGAP_ID, true); + asn_INTEGER2ulong(&ie->value.choice.AMF_UE_NGAP_ID, &out->amf_ue_ngap_id); + + /* RAN UE NGAP ID (M) */ + NGAP_FIND_PROTOCOLIE_BY_ID(NGAP_HandoverCancelAcknowledgeIEs_t, ie, container, NGAP_ProtocolIE_ID_id_RAN_UE_NGAP_ID, true); + out->gNB_ue_ngap_id = ie->value.choice.RAN_UE_NGAP_ID; + + return 0; +} + /** @brief Encode NGAP UL RAN Status Transfer (9.2.3.14 of 3GPP TS 38.413) */ NGAP_NGAP_PDU_t *encode_ng_ul_ran_status_transfer(const ngap_ran_status_transfer_t *msg) { diff --git a/openair3/NGAP/ngap_gNB_mobility_management.h b/openair3/NGAP/ngap_gNB_mobility_management.h index b7a6f3547c..18aac7c61d 100644 --- a/openair3/NGAP/ngap_gNB_mobility_management.h +++ b/openair3/NGAP/ngap_gNB_mobility_management.h @@ -35,5 +35,7 @@ int decode_ng_handover_command(ngap_handover_command_t *msg, NGAP_NGAP_PDU_t *pd void free_ng_handover_command(ngap_handover_command_t *msg); NGAP_NGAP_PDU_t *encode_ng_handover_notify(const ngap_handover_notify_t *msg); NGAP_NGAP_PDU_t *encode_ng_ul_ran_status_transfer(const ngap_ran_status_transfer_t *msg); +NGAP_NGAP_PDU_t *encode_ng_handover_cancel(const ngap_handover_cancel_t *msg); +int decode_ng_handover_cancel_ack(ngap_handover_cancel_ack_t *out, const NGAP_NGAP_PDU_t *pdu); #endif /* NGAP_GNB_MOBILITY_MANAGEMENT_H_ */