/src/ocudu/lib/xnap/xnap_impl.cpp
Line | Count | Source |
1 | | // SPDX-FileCopyrightText: Copyright (C) 2021-2026 Software Radio Systems Limited |
2 | | // SPDX-License-Identifier: BSD-3-Clause-Open-MPI |
3 | | // Portions of this file may implement 3GPP specifications, which may be subject to additional licensing requirements. |
4 | | |
5 | | #include "xnap_impl.h" |
6 | | #include "log_helpers.h" |
7 | | #include "procedures/ngran_node_cfg_update_procedure.h" |
8 | | #include "procedures/retrieve_ue_context_asn1_helpers.h" |
9 | | #include "procedures/sn_status_transfer_asn1_helpers.h" |
10 | | #include "procedures/xn_handover_asn1_helpers.h" |
11 | | #include "procedures/xn_setup_asn1_helpers.h" |
12 | | #include "procedures/xn_setup_procedure.h" |
13 | | #include "procedures/xn_setup_procedure_asn1_helpers.h" |
14 | | #include "procedures/xnap_new_node_retrieve_ue_context_procedure.h" |
15 | | #include "procedures/xnap_old_node_retrieve_ue_context_procedure.h" |
16 | | #include "procedures/xnap_sn_status_transfer_procedure.h" |
17 | | #include "procedures/xnap_source_handover_preparation_procedure.h" |
18 | | #include "procedures/xnap_target_handover_preparation_procedure.h" |
19 | | #include "xnap_asn1_converters.h" |
20 | | #include "xnap_asn1_utils.h" |
21 | | #include "ocudu/adt/scope_exit.h" |
22 | | #include "ocudu/asn1/xnap/common.h" |
23 | | #include "ocudu/asn1/xnap/xnap.h" |
24 | | #include "ocudu/asn1/xnap/xnap_ies.h" |
25 | | #include "ocudu/asn1/xnap/xnap_pdu_contents.h" |
26 | | #include "ocudu/support/async/async_no_op_task.h" |
27 | | #include "ocudu/xnap/xnap_message.h" |
28 | | #include "ocudu/xnap/xnap_types.h" |
29 | | #include <algorithm> |
30 | | #include <vector> |
31 | | |
32 | | using namespace ocudu; |
33 | | using namespace asn1::xnap; |
34 | | using namespace ocucp; |
35 | | |
36 | | xnap_impl::xnap_impl(xnc_peer_index_t xnc_index_, |
37 | | const xnap_configuration& xnap_cfg_, |
38 | | xnap_cu_cp_notifier& cu_cp_notifier_, |
39 | | timer_manager& timers_, |
40 | | task_executor& ctrl_exec_) : |
41 | 0 | logger(ocudulog::fetch_basic_logger("XNAP")), |
42 | 0 | ue_ctxt_list(timers_, ctrl_exec_, logger), |
43 | 0 | xnc_index(xnc_index_), |
44 | 0 | xnap_cfg(xnap_cfg_), |
45 | 0 | cu_cp_notifier(cu_cp_notifier_), |
46 | 0 | timers(timers_), |
47 | 0 | ctrl_exec(ctrl_exec_), |
48 | 0 | xn_setup_outcome(timer_factory{timers, ctrl_exec}), |
49 | 0 | cfg_update_outcome(timer_factory{timers, ctrl_exec}) |
50 | 0 | { |
51 | 0 | } |
52 | | |
53 | | async_task<void> xnap_impl::stop() |
54 | 0 | { |
55 | | // Stop XN setup procedure if in progress. |
56 | 0 | xn_setup_outcome.stop(); |
57 | | |
58 | | // Stop NG-RAN node configuration update procedure if in progress. |
59 | 0 | cfg_update_outcome.stop(); |
60 | | |
61 | | // Cancel pending per-UE transactions (e.g. Handover Preparation, SN Status Transfer). |
62 | 0 | ue_ctxt_list.cancel_all_transactions(); |
63 | |
|
64 | 0 | if (nof_ue_procedures == 0) { |
65 | 0 | return launch_no_op_task(); |
66 | 0 | } |
67 | | |
68 | | // Await the UE-associated procedures that are suspended on a CU-CP notifier. Cancelling the XNAP transactions |
69 | | // does not resume those, and they access this XNAP instance once they do resume. The caller removes this |
70 | | // instance as soon as this task completes. |
71 | 0 | logger.debug("Awaiting the completion of {} UE procedures before stopping XNAP", nof_ue_procedures); |
72 | 0 | return launch_async([this](coro_context<async_task<void>>& ctx) { |
73 | 0 | CORO_BEGIN(ctx); |
74 | |
|
75 | 0 | while (nof_ue_procedures != 0) { |
76 | 0 | ue_procedures_done.reset(); |
77 | 0 | CORO_AWAIT(ue_procedures_done); |
78 | 0 | } |
79 | | |
80 | 0 | CORO_RETURN(); |
81 | 0 | }); |
82 | 0 | } |
83 | | |
84 | | void xnap_impl::handle_message(const xnap_message& msg) |
85 | 0 | { |
86 | | // Run XNAP protocols in Control executor. |
87 | 0 | if (not ctrl_exec.execute([this, msg]() { |
88 | 0 | log_xnap_pdu(logger, logger.debug.enabled(), true, msg.pdu); |
89 | 0 | switch (msg.pdu.type().value) { |
90 | 0 | case xn_ap_pdu_c::types_opts::init_msg: |
91 | 0 | handle_initiating_message(msg.pdu.init_msg()); |
92 | 0 | break; |
93 | 0 | case xn_ap_pdu_c::types_opts::successful_outcome: |
94 | 0 | handle_successful_outcome(msg.pdu.successful_outcome()); |
95 | 0 | break; |
96 | 0 | case xn_ap_pdu_c::types_opts::unsuccessful_outcome: |
97 | 0 | handle_unsuccessful_outcome(msg.pdu.unsuccessful_outcome()); |
98 | 0 | break; |
99 | 0 | default: |
100 | 0 | logger.error("Invalid PDU type"); |
101 | 0 | break; |
102 | 0 | } |
103 | 0 | })) { |
104 | 0 | logger.error("Discarding Rx XNAP PDU. Cause: task queue is full"); |
105 | 0 | } |
106 | 0 | } |
107 | | |
108 | | void xnap_impl::remove_ue_context(cu_cp_ue_index_t ue_index) |
109 | 0 | { |
110 | 0 | if (!ue_ctxt_list.contains(ue_index)) { |
111 | 0 | logger.debug("ue={}: UE context not found", ue_index); |
112 | 0 | return; |
113 | 0 | } |
114 | | |
115 | 0 | ue_ctxt_list.remove_ue_context(ue_index); |
116 | 0 | } |
117 | | |
118 | | void xnap_impl::handle_initiating_message(const init_msg_s& msg) |
119 | 0 | { |
120 | 0 | switch (msg.value.type().value) { |
121 | 0 | case xnap_elem_procs_o::init_msg_c::types_opts::xn_setup_request: |
122 | 0 | handle_xn_setup_request(msg.value.xn_setup_request()); |
123 | 0 | break; |
124 | 0 | case xnap_elem_procs_o::init_msg_c::types_opts::ngran_node_cfg_upd: |
125 | 0 | handle_ngran_node_cfg_update(msg.value.ngran_node_cfg_upd()); |
126 | 0 | break; |
127 | 0 | case xnap_elem_procs_o::init_msg_c::types_opts::ho_request: |
128 | 0 | handle_handover_request(msg.value.ho_request()); |
129 | 0 | break; |
130 | 0 | case xnap_elem_procs_o::init_msg_c::types_opts::ho_cancel: |
131 | 0 | handle_handover_cancel(msg.value.ho_cancel()); |
132 | 0 | break; |
133 | 0 | case xnap_elem_procs_o::init_msg_c::types_opts::sn_status_transfer: |
134 | 0 | handle_sn_status_transfer(msg.value.sn_status_transfer()); |
135 | 0 | break; |
136 | 0 | case xnap_elem_procs_o::init_msg_c::types_opts::ue_context_release: |
137 | 0 | handle_ue_context_release(msg.value.ue_context_release()); |
138 | 0 | break; |
139 | 0 | case xnap_elem_procs_o::init_msg_c::types_opts::ho_success: |
140 | 0 | handle_handover_success(msg.value.ho_success()); |
141 | 0 | break; |
142 | 0 | case xnap_elem_procs_o::init_msg_c::types_opts::conditional_ho_cancel: |
143 | 0 | handle_conditional_ho_cancel(msg.value.conditional_ho_cancel()); |
144 | 0 | break; |
145 | 0 | case xnap_elem_procs_o::init_msg_c::types_opts::retrieve_ue_context_request: |
146 | 0 | handle_retrieve_ue_context_request(msg.value.retrieve_ue_context_request()); |
147 | 0 | break; |
148 | 0 | default: |
149 | 0 | logger.error("Initiating message of type {} is not supported", msg.value.type().to_string()); |
150 | 0 | } |
151 | 0 | } |
152 | | |
153 | | void xnap_impl::handle_successful_outcome(const successful_outcome_s& outcome) |
154 | 0 | { |
155 | 0 | switch (outcome.value.type().value) { |
156 | 0 | case xnap_elem_procs_o::successful_outcome_c::types_opts::xn_setup_resp: { |
157 | 0 | xn_setup_outcome.set(outcome.value.xn_setup_resp()); |
158 | 0 | } break; |
159 | 0 | case xnap_elem_procs_o::successful_outcome_c::types_opts::ngran_node_cfg_upd_ack: { |
160 | 0 | cfg_update_outcome.set(outcome.value.ngran_node_cfg_upd_ack()); |
161 | 0 | } break; |
162 | 0 | case xnap_elem_procs_o::successful_outcome_c::types_opts::ho_request_ack: { |
163 | 0 | auto* ue_ctxt = asn1_utils::get_ue_ctxt_in_ue_assoc_msg(outcome, ue_ctxt_list, logger); |
164 | 0 | if (ue_ctxt == nullptr) { |
165 | 0 | break; |
166 | 0 | } |
167 | 0 | const ho_request_ack_s& ack = outcome.value.ho_request_ack(); |
168 | | // TS 38.423 Section 8.2.1.2: for a conditional handover the target shall echo the requested target cell, which |
169 | | // tells parallel preparations sharing a Source NG-RAN node UE XnAP ID apart. |
170 | 0 | std::optional<nr_cell_global_id_t> cell; |
171 | 0 | if (ack->ch_oinfo_ack_present) { |
172 | 0 | if (ack->ch_oinfo_ack.requested_target_cell_global_id.type() != target_cgi_c::types_opts::nr) { |
173 | 0 | ue_ctxt->logger.log_warning( |
174 | 0 | "Discarding HandoverRequestAcknowledge: Requested Target Cell Global ID is not an NR cell"); |
175 | 0 | break; |
176 | 0 | } |
177 | 0 | cell = asn1_to_cgi(ack->ch_oinfo_ack.requested_target_cell_global_id.nr()); |
178 | 0 | } |
179 | 0 | if (!ue_ctxt->deliver_ho_prep_outcome(cell, ack)) { |
180 | 0 | ue_ctxt->logger.log_warning("Discarding HandoverRequestAcknowledge: no matching handover preparation"); |
181 | 0 | } |
182 | 0 | } break; |
183 | 0 | case xnap_elem_procs_o::successful_outcome_c::types_opts::retrieve_ue_context_resp: { |
184 | 0 | if (auto* ue_ctxt = asn1_utils::get_ue_ctxt_in_ue_assoc_msg(outcome, ue_ctxt_list, logger)) { |
185 | 0 | ue_ctxt->retrieve_ue_context_outcome.set(outcome.value.retrieve_ue_context_resp()); |
186 | 0 | } |
187 | 0 | } break; |
188 | 0 | default: |
189 | 0 | logger.error("Successful outcome of type {} is not supported", outcome.value.type().to_string()); |
190 | 0 | } |
191 | 0 | } |
192 | | |
193 | | void xnap_impl::handle_unsuccessful_outcome(const unsuccessful_outcome_s& outcome) |
194 | 0 | { |
195 | 0 | switch (outcome.value.type().value) { |
196 | 0 | case xnap_elem_procs_o::unsuccessful_outcome_c::types_opts::xn_setup_fail: { |
197 | 0 | xn_setup_outcome.set(outcome.value.xn_setup_fail()); |
198 | 0 | } break; |
199 | 0 | case xnap_elem_procs_o::unsuccessful_outcome_c::types_opts::ngran_node_cfg_upd_fail: { |
200 | 0 | cfg_update_outcome.set(outcome.value.ngran_node_cfg_upd_fail()); |
201 | 0 | } break; |
202 | 0 | case xnap_elem_procs_o::unsuccessful_outcome_c::types_opts::ho_prep_fail: { |
203 | 0 | auto* ue_ctxt = asn1_utils::get_ue_ctxt_in_ue_assoc_msg(outcome, ue_ctxt_list, logger); |
204 | 0 | if (ue_ctxt == nullptr) { |
205 | 0 | break; |
206 | 0 | } |
207 | 0 | const ho_prep_fail_s& fail = outcome.value.ho_prep_fail(); |
208 | | // TS 38.423 Section 8.2.1.3: the failure carries the requested target cell when it rejects one of several |
209 | | // parallel CHO preparations. The IE is optional, so it may have to be routed without one. |
210 | 0 | std::optional<nr_cell_global_id_t> cell; |
211 | 0 | if (fail->requested_target_cell_global_id_present) { |
212 | 0 | if (fail->requested_target_cell_global_id.type() != target_cgi_c::types_opts::nr) { |
213 | | // The named cell cannot be one we prepared, so do not let it resolve an unrelated transaction. |
214 | 0 | ue_ctxt->logger.log_warning( |
215 | 0 | "Discarding HandoverPreparationFailure: Requested Target Cell Global ID is not an NR cell"); |
216 | 0 | break; |
217 | 0 | } |
218 | 0 | cell = asn1_to_cgi(fail->requested_target_cell_global_id.nr()); |
219 | 0 | } |
220 | 0 | if (!ue_ctxt->deliver_ho_prep_outcome(cell, fail)) { |
221 | 0 | ue_ctxt->logger.log_warning("Discarding HandoverPreparationFailure: no matching handover preparation"); |
222 | 0 | } |
223 | 0 | } break; |
224 | 0 | case xnap_elem_procs_o::unsuccessful_outcome_c::types_opts::retrieve_ue_context_fail: { |
225 | 0 | if (auto* ue_ctxt = asn1_utils::get_ue_ctxt_in_ue_assoc_msg(outcome, ue_ctxt_list, logger)) { |
226 | 0 | ue_ctxt->retrieve_ue_context_outcome.set(outcome.value.retrieve_ue_context_fail()); |
227 | 0 | } |
228 | 0 | } break; |
229 | 0 | default: |
230 | 0 | logger.error("Unsuccessful outcome of type {} is not supported", outcome.value.type().to_string()); |
231 | 0 | } |
232 | 0 | } |
233 | | |
234 | | async_task<bool> xnap_impl::handle_xn_setup_request_required() |
235 | 0 | { |
236 | 0 | advertised_cells = cu_cp_notifier.on_served_cells_required(); |
237 | |
|
238 | 0 | return launch_async<xn_setup_procedure>( |
239 | 0 | xnap_cfg, advertised_cells, peer_ctxt, tx_notifier, xn_setup_outcome, timer_factory{timers, ctrl_exec}, logger); |
240 | 0 | } |
241 | | |
242 | | async_task<bool> xnap_impl::handle_served_cells_update_required() |
243 | 0 | { |
244 | 0 | return launch_async<ngran_node_cfg_update_procedure>(xnap_cfg, |
245 | 0 | cu_cp_notifier.on_served_cells_required(), |
246 | 0 | advertised_cells, |
247 | 0 | peer_ctxt, |
248 | 0 | tx_notifier, |
249 | 0 | cfg_update_outcome, |
250 | 0 | logger); |
251 | 0 | } |
252 | | |
253 | | void xnap_impl::handle_ngran_node_cfg_update(const ngran_node_cfg_upd_s& msg) |
254 | 0 | { |
255 | 0 | if (not peer_ctxt.has_value()) { |
256 | 0 | logger.warning("Rejecting NGRANNodeConfigurationUpdate. Cause: XN setup has not been completed"); |
257 | 0 | if (not tx_notifier.on_new_message( |
258 | 0 | generate_asn1_ngran_node_cfg_update_failure(cause_protocol_t::msg_not_compatible_with_receiver_state))) { |
259 | 0 | logger.error("Failed to send NGRANNodeConfigurationUpdateFailure. Cause: no SCTP association available"); |
260 | 0 | } |
261 | 0 | return; |
262 | 0 | } |
263 | | |
264 | 0 | const auto& asn1_init_node_choice = msg->cfg_upd_init_node_choice; |
265 | 0 | if (asn1_init_node_choice.type() == cfg_upd_init_node_choice_c::types_opts::gnb and |
266 | 0 | asn1_init_node_choice.gnb().served_cells_to_upd_nr_present) { |
267 | 0 | update_peer_served_cells(peer_ctxt->list_of_served_cells_nr, asn1_init_node_choice.gnb().served_cells_to_upd_nr); |
268 | 0 | logger.info("Xn-C peer serves {} cell(s)", peer_ctxt->list_of_served_cells_nr.size()); |
269 | 0 | } |
270 | |
|
271 | 0 | if (not tx_notifier.on_new_message(generate_asn1_ngran_node_cfg_update_ack())) { |
272 | 0 | logger.error("Failed to send NGRANNodeConfigurationUpdateAcknowledge. Cause: no SCTP association available"); |
273 | 0 | } |
274 | 0 | } |
275 | | |
276 | | void xnap_impl::handle_xn_setup_request(const xn_setup_request_s& request) |
277 | 0 | { |
278 | 0 | xnap_message xn_setup_result; |
279 | | |
280 | | // Message content validation. |
281 | 0 | auto msgerr = validate_xn_setup_request_response(request); |
282 | 0 | if (not msgerr.has_value()) { |
283 | 0 | logger.warning("Rejecting XN Setup Request. Cause: {}", msgerr.error().second); |
284 | 0 | xn_setup_result = generate_asn1_xn_setup_failure(asn1_to_cause(msgerr.error().first)); |
285 | 0 | } else { |
286 | | // Store peer context information. |
287 | 0 | peer_ctxt = create_peer_xnap_context(request); |
288 | | // Generate XN Setup Response. |
289 | 0 | advertised_cells = cu_cp_notifier.on_served_cells_required(); |
290 | 0 | xn_setup_result = generate_asn1_xn_setup_response(xnap_cfg, advertised_cells); |
291 | 0 | } |
292 | | |
293 | | // Transmit XN Setup Response/Failure. |
294 | 0 | if (not tx_notifier.on_new_message(xn_setup_result)) { |
295 | 0 | logger.error("Failed to send XN Setup Response. Cause: no SCTP association available"); |
296 | 0 | } |
297 | 0 | } |
298 | | |
299 | | void xnap_impl::handle_handover_request(const asn1::xnap::ho_request_s& msg) |
300 | 0 | { |
301 | | // Add lambda that generates and transmits Handover Preparation Failure message. |
302 | 0 | auto send_handover_failure = [this](uint64_t peer_xnap_ue_id, xnap_cause_t cause) { |
303 | 0 | xnap_message xnap_msg; |
304 | 0 | xnap_msg.pdu.set_unsuccessful_outcome(); |
305 | 0 | xnap_msg.pdu.unsuccessful_outcome().load_info_obj(ASN1_XNAP_ID_HO_PREP); |
306 | 0 | auto& ho_fail = xnap_msg.pdu.unsuccessful_outcome().value.ho_prep_fail(); |
307 | | // This is sent from the target to the source, so the peer XNAP UE ID is the source UE ID. |
308 | 0 | ho_fail->source_ng_ra_nnode_ue_xn_ap_id = peer_xnap_ue_id; |
309 | 0 | ho_fail->cause = cause_to_asn1(cause); |
310 | |
|
311 | 0 | if (!tx_notifier.on_new_message(xnap_msg)) { |
312 | 0 | logger.warning("Xn-C association is not set. Cannot send HandoverFailure"); |
313 | 0 | return; |
314 | 0 | } |
315 | 0 | logger.warning("Sending HandoverFailure"); |
316 | 0 | }; |
317 | | |
318 | | // Convert Handover Request to common type. |
319 | 0 | xnap_handover_request ho_request; |
320 | 0 | if (!asn1_to_handover_request(ho_request, msg)) { |
321 | 0 | logger.info("Received invalid HandoverRequest"); |
322 | 0 | send_handover_failure(msg->source_ng_ra_nnode_ue_xn_ap_id, |
323 | 0 | cause_protocol_t::abstract_syntax_error_falsely_constructed_msg); |
324 | |
|
325 | 0 | return; |
326 | 0 | } |
327 | | |
328 | 0 | logger.info("HandoverRequest - extracted target cell. plmn={}, target cell_id=0x{:x}", |
329 | 0 | ho_request.nr_cgi.plmn_id, |
330 | 0 | ho_request.nr_cgi.nci); |
331 | | |
332 | | // Create UE in target cell. |
333 | 0 | ho_request.ue_index = cu_cp_notifier.request_new_ue_index_allocation(ho_request.nr_cgi, ho_request.guami.plmn); |
334 | 0 | if (ho_request.ue_index == cu_cp_ue_index_t::invalid) { |
335 | 0 | logger.debug("Couldn't allocate UE index for handover target cell"); |
336 | 0 | send_handover_failure(msg->source_ng_ra_nnode_ue_xn_ap_id, xnap_cause_misc_t::not_enough_user_plane_processing_res); |
337 | 0 | return; |
338 | 0 | } |
339 | | |
340 | | // Inititialize security context of target UE. |
341 | 0 | if (!cu_cp_notifier.on_handover_request_received( |
342 | 0 | ho_request.ue_index, ho_request.guami.plmn, ho_request.ue_context_info_ho_request.security_context)) { |
343 | 0 | logger.debug("Failed to initialize security context for UE index {}. Rejecting handover request", |
344 | 0 | ho_request.ue_index); |
345 | 0 | send_handover_failure(msg->source_ng_ra_nnode_ue_xn_ap_id, xnap_cause_misc_t::not_enough_user_plane_processing_res); |
346 | 0 | return; |
347 | 0 | } |
348 | | |
349 | 0 | if (!cu_cp_notifier.schedule_async_task(ho_request.ue_index, |
350 | 0 | track_ue_procedure(launch_async<xnap_target_handover_preparation_procedure>( |
351 | 0 | ho_request, |
352 | 0 | xnc_index, |
353 | 0 | uint_to_peer_xnap_ue_id(msg->source_ng_ra_nnode_ue_xn_ap_id), |
354 | 0 | ue_ctxt_list, |
355 | 0 | cu_cp_notifier, |
356 | 0 | tx_notifier, |
357 | 0 | logger)))) { |
358 | 0 | logger.debug("Couldn't schedule targer handover preparation procedure"); |
359 | 0 | send_handover_failure(msg->source_ng_ra_nnode_ue_xn_ap_id, xnap_cause_misc_t::not_enough_user_plane_processing_res); |
360 | 0 | return; |
361 | 0 | } |
362 | 0 | } |
363 | | |
364 | | void xnap_impl::handle_handover_cancel(const asn1::xnap::ho_cancel_s& msg) |
365 | 0 | { |
366 | | // TS 38.423 Section 8.2.3.2: the cancelled handover is the one on the signalling connection identified by the Source |
367 | | // NG-RAN node UE XnAP ID and, if included, also by the Target NG-RAN node UE XnAP ID, restricted to the candidate |
368 | | // cells of the Candidate Cells To Be Cancelled List when that IE is present. Parallel CHO preparations from one |
369 | | // source share the source ID, so it alone does not identify a context here. |
370 | | // This is sent from the source to the target, so the source XNAP UE ID is the peer UE ID. |
371 | 0 | const peer_xnap_ue_id_t peer_xnap_ue_id = uint_to_peer_xnap_ue_id(msg->source_ng_ra_nnode_ue_xn_ap_id); |
372 | | |
373 | | // Releases one prepared candidate: the CU-CP drops its UE, then the local XNAP UE context goes. |
374 | 0 | auto release = [this](xnap_ue_context& ue_ctxt) { |
375 | 0 | const cu_cp_ue_index_t ue_index = ue_ctxt.ue_ids.ue_index; |
376 | 0 | cu_cp_notifier.on_handover_cancel_received(ue_index); |
377 | 0 | ue_ctxt_list.remove_ue_context(ue_index); |
378 | 0 | }; |
379 | |
|
380 | 0 | if (msg->target_ng_ra_nnode_ue_xn_ap_id_present) { |
381 | 0 | xnap_ue_context* ue_ctxt = ue_ctxt_list.find(uint_to_local_xnap_ue_id(msg->target_ng_ra_nnode_ue_xn_ap_id)); |
382 | | // Both IDs name the same signalling connection, so a context whose source ID differs is not the addressed one. |
383 | 0 | if (ue_ctxt != nullptr and ue_ctxt->ue_ids.peer_xnap_ue_id != peer_xnap_ue_id) { |
384 | 0 | logger.info("Discarding HandoverCancel: peer_xnap_ue_id={} does not match local_xnap_ue_id={}", |
385 | 0 | msg->source_ng_ra_nnode_ue_xn_ap_id, |
386 | 0 | msg->target_ng_ra_nnode_ue_xn_ap_id); |
387 | 0 | return; |
388 | 0 | } |
389 | 0 | if (ue_ctxt == nullptr) { |
390 | 0 | logger.info("Received HandoverCancel for unknown UE. peer_xnap_ue_id={}", msg->source_ng_ra_nnode_ue_xn_ap_id); |
391 | 0 | return; |
392 | 0 | } |
393 | | // Section 8.2.3.2 scopes the cancellation to the listed cells as well as to the connection the IDs name, so a |
394 | | // list that does not mention this context's cell is not cancelling it. |
395 | 0 | if (msg->target_cells_to_cancel.size() != 0 and ue_ctxt->ho_target_cell.has_value()) { |
396 | 0 | const bool names_this_cell = std::any_of(msg->target_cells_to_cancel.begin(), |
397 | 0 | msg->target_cells_to_cancel.end(), |
398 | 0 | [&ue_ctxt](const asn1::xnap::target_cell_list_item_s& item) { |
399 | 0 | return item.target_cell.type() == target_cgi_c::types_opts::nr and |
400 | 0 | asn1_to_cgi(item.target_cell.nr()) == *ue_ctxt->ho_target_cell; |
401 | 0 | }); |
402 | 0 | if (not names_this_cell) { |
403 | 0 | logger.info("Discarding HandoverCancel: the candidate cell list does not name local_xnap_ue_id={}'s cell", |
404 | 0 | msg->target_ng_ra_nnode_ue_xn_ap_id); |
405 | 0 | return; |
406 | 0 | } |
407 | 0 | } |
408 | 0 | release(*ue_ctxt); |
409 | 0 | return; |
410 | 0 | } |
411 | | |
412 | | // Without the target ID the named candidate cells are what tell parallel CHO preparations apart. Every named cell |
413 | | // is cancelled, and only those: Section 8.2.3.2 scopes the procedure to the cells the list identifies. |
414 | 0 | bool named_a_cell = false; |
415 | 0 | for (const auto& item : msg->target_cells_to_cancel) { |
416 | 0 | if (item.target_cell.type() != target_cgi_c::types_opts::nr) { |
417 | 0 | continue; |
418 | 0 | } |
419 | 0 | named_a_cell = true; |
420 | 0 | xnap_ue_context* cell_ctxt = |
421 | 0 | ue_ctxt_list.find_by_peer_and_cell(peer_xnap_ue_id, asn1_to_cgi(item.target_cell.nr())); |
422 | 0 | if (cell_ctxt == nullptr) { |
423 | 0 | logger.info("HandoverCancel names a cell with no prepared UE. peer_xnap_ue_id={}", |
424 | 0 | msg->source_ng_ra_nnode_ue_xn_ap_id); |
425 | 0 | continue; |
426 | 0 | } |
427 | 0 | release(*cell_ctxt); |
428 | 0 | } |
429 | 0 | if (named_a_cell) { |
430 | 0 | return; |
431 | 0 | } |
432 | | |
433 | | // Neither the target ID nor a candidate cell: the whole UE-associated signalling connection is cancelled. |
434 | 0 | xnap_ue_context* ue_ctxt = ue_ctxt_list.find(peer_xnap_ue_id); |
435 | 0 | if (ue_ctxt == nullptr) { |
436 | 0 | logger.info("Received HandoverCancel for unknown UE. peer_xnap_ue_id={}", msg->source_ng_ra_nnode_ue_xn_ap_id); |
437 | 0 | return; |
438 | 0 | } |
439 | 0 | release(*ue_ctxt); |
440 | 0 | } |
441 | | |
442 | | void xnap_impl::handle_sn_status_transfer(const asn1::xnap::sn_status_transfer_s& msg) |
443 | 0 | { |
444 | | // This is sent from the source to the target, so the target XNAP UE ID is the local UE ID. Address the context by it: |
445 | | // parallel CHO preparations from one source share the Source NG-RAN node UE XnAP ID, so it does not identify a single |
446 | | // context here (TS 38.423 Section 9.1.1.4, the Target NG-RAN node UE XnAP ID is mandatory and allocated by us). |
447 | 0 | const local_xnap_ue_id_t local_xnap_ue_id = uint_to_local_xnap_ue_id(msg->target_ng_ra_nnode_ue_xn_ap_id); |
448 | |
|
449 | 0 | if (!ue_ctxt_list.contains(local_xnap_ue_id)) { |
450 | 0 | logger.warning("peer_xnap_ue={} local_xnap_ue={}: Dropping SNStatusTransfer. UE context does not exist", |
451 | 0 | msg->source_ng_ra_nnode_ue_xn_ap_id, |
452 | 0 | msg->target_ng_ra_nnode_ue_xn_ap_id); |
453 | 0 | return; |
454 | 0 | } |
455 | | |
456 | 0 | xnap_ue_context& ue_ctxt = ue_ctxt_list[local_xnap_ue_id]; |
457 | | |
458 | | // Both IDs name the same signalling connection, so a context whose source ID differs is not the addressed one. |
459 | 0 | if (ue_ctxt.ue_ids.peer_xnap_ue_id != uint_to_peer_xnap_ue_id(msg->source_ng_ra_nnode_ue_xn_ap_id)) { |
460 | 0 | logger.warning("peer_xnap_ue={} local_xnap_ue={}: Dropping SNStatusTransfer. The IDs name different contexts", |
461 | 0 | msg->source_ng_ra_nnode_ue_xn_ap_id, |
462 | 0 | msg->target_ng_ra_nnode_ue_xn_ap_id); |
463 | 0 | return; |
464 | 0 | } |
465 | | |
466 | 0 | ue_ctxt.sn_status_transfer_outcome.set(msg); |
467 | 0 | } |
468 | | |
469 | | void xnap_impl::handle_ue_context_release(const asn1::xnap::ue_context_release_s& msg) |
470 | 0 | { |
471 | | // This is sent from the target to the source, so the source XNAP UE ID is the local UE ID and the target XNAP UE ID |
472 | | // is the peer UE ID. |
473 | 0 | local_xnap_ue_id_t local_xnap_ue_id = uint_to_local_xnap_ue_id(msg->source_ng_ra_nnode_ue_xn_ap_id); |
474 | |
|
475 | 0 | if (!ue_ctxt_list.contains(local_xnap_ue_id)) { |
476 | 0 | logger.warning("local_xnap_ue={} peer_xnap_ue={}: Dropping UEContextRelease. UE context does not exist", |
477 | 0 | msg->source_ng_ra_nnode_ue_xn_ap_id, |
478 | 0 | msg->target_ng_ra_nnode_ue_xn_ap_id); |
479 | 0 | return; |
480 | 0 | } |
481 | | |
482 | 0 | xnap_ue_context& ue_ctxt = ue_ctxt_list[local_xnap_ue_id]; |
483 | | |
484 | | // Request CU-CP to release the UE context. |
485 | 0 | cu_cp_notifier.on_ue_context_release_received(ue_ctxt.ue_ids.ue_index); |
486 | 0 | } |
487 | | |
488 | | async_task<xnap_handover_preparation_response> |
489 | | xnap_impl::handle_handover_request_required(const xnap_handover_request& request) |
490 | 0 | { |
491 | | // TS 38.423 Section 8.2.1.1: all CHO candidate cells served by this peer share one Source NG-RAN node UE XnAP ID, |
492 | | // hence one UE context, just like the successive handovers of an immediate HO. |
493 | 0 | const local_xnap_ue_id_t local_xnap_ue_id = ue_ctxt_list.find_or_create_ue_context(request.ue_index); |
494 | 0 | if (local_xnap_ue_id == local_xnap_ue_id_t::invalid) { |
495 | 0 | logger.error("Failed to allocate XNAP UE ID for ue={}. Cannot transmit HandoverPreparationRequest", |
496 | 0 | request.ue_index); |
497 | 0 | return launch_no_op_task(xnap_handover_preparation_response{false}); |
498 | 0 | } |
499 | | |
500 | 0 | xnap_ue_context& ue_ctxt = ue_ctxt_list[local_xnap_ue_id]; |
501 | | |
502 | | // The event source this preparation awaits. A conditional handover gets its own, keyed by target cell, so that |
503 | | // parallel candidates each await their own outcome; an immediate handover uses the one of the UE context. |
504 | 0 | ho_prep_outcome_t* ho_outcome = &ue_ctxt.xn_handover_outcome; |
505 | 0 | if (request.is_conditional_handover) { |
506 | 0 | ho_outcome = ue_ctxt.add_cho_cell_prep(request.nr_cgi, timer_factory{timers, ctrl_exec}); |
507 | 0 | if (ho_outcome == nullptr) { |
508 | 0 | ue_ctxt.logger.log_warning("CHO preparation already in flight for nci={:#x}", request.nr_cgi.nci.value()); |
509 | 0 | return launch_no_op_task(xnap_handover_preparation_response{false}); |
510 | 0 | } |
511 | 0 | ue_ctxt.logger.log_debug("Starting CHO source preparation for nci={:#x}", request.nr_cgi.nci.value()); |
512 | 0 | } else { |
513 | 0 | ue_ctxt.logger.log_debug("Starting HO source preparation"); |
514 | 0 | } |
515 | | |
516 | 0 | return track_ue_procedure(launch_async<xnap_source_handover_preparation_procedure>( |
517 | 0 | request, ue_ctxt, *ho_outcome, ue_ctxt_list, tx_notifier, cu_cp_notifier, timer_factory{timers, ctrl_exec})); |
518 | 0 | } |
519 | | |
520 | | void xnap_impl::handle_cho_cancel_required(cu_cp_ue_index_t ue_index, const nr_cell_global_id_t& target_cgi) |
521 | 0 | { |
522 | 0 | if (!ue_ctxt_list.contains(ue_index)) { |
523 | 0 | logger.warning("ue={}: Cannot send HandoverCancel (CHO non-winner): UE context not found", ue_index); |
524 | 0 | return; |
525 | 0 | } |
526 | | |
527 | 0 | xnap_ue_context& ue_ctxt = ue_ctxt_list[ue_index]; |
528 | 0 | const local_xnap_ue_id_t local_xnap_ue_id = ue_ctxt.ue_ids.local_xnap_ue_id; |
529 | 0 | const peer_xnap_ue_id_t peer_ue_id = ue_ctxt_list.remove_cho_prepared(local_xnap_ue_id, target_cgi); |
530 | |
|
531 | 0 | if (peer_ue_id == peer_xnap_ue_id_t::invalid) { |
532 | 0 | ue_ctxt.logger.log_warning("HandoverCancel (CHO non-winner) skipped: no candidate prepared for nci={:#x}", |
533 | 0 | target_cgi.nci.value()); |
534 | 0 | } else { |
535 | | // TS 38.423 Section 8.2.3: source sends HANDOVER CANCEL to release a non-winning prepared context at the target. |
536 | | // Include TargetCellsToCancel IE to identify the specific candidate cell being cancelled. |
537 | 0 | xnap_message msg = {}; |
538 | 0 | msg.pdu.set_init_msg(); |
539 | 0 | msg.pdu.init_msg().load_info_obj(ASN1_XNAP_ID_HO_CANCEL); |
540 | 0 | ho_cancel_s& ho_cancel = msg.pdu.init_msg().value.ho_cancel(); |
541 | 0 | ho_cancel->source_ng_ra_nnode_ue_xn_ap_id = to_underlying(local_xnap_ue_id); |
542 | 0 | ho_cancel->cause.set_radio_network() = cause_radio_network_layer_opts::proc_cancelled; |
543 | 0 | ho_cancel->target_ng_ra_nnode_ue_xn_ap_id_present = true; |
544 | 0 | ho_cancel->target_ng_ra_nnode_ue_xn_ap_id = to_underlying(peer_ue_id); |
545 | 0 | ho_cancel->target_cells_to_cancel_present = true; |
546 | 0 | asn1::xnap::target_cell_list_item_s cell_item; |
547 | 0 | cell_item.target_cell.set_nr() = cgi_to_asn1(target_cgi); |
548 | 0 | ho_cancel->target_cells_to_cancel.push_back(cell_item); |
549 | |
|
550 | 0 | if (!tx_notifier.on_new_message(msg)) { |
551 | 0 | ue_ctxt.logger.log_warning("Cannot send HandoverCancel to release non-winning CHO target"); |
552 | 0 | } else { |
553 | 0 | ue_ctxt.logger.log_debug("HandoverCancel sent to release non-winning CHO target nci={:#x}", |
554 | 0 | target_cgi.nci.value()); |
555 | 0 | } |
556 | 0 | } |
557 | | |
558 | | // Release the local XNAP UE context once this peer holds no candidate for this UE any more. Other candidates at |
559 | | // the same peer keep it alive. |
560 | 0 | if (ue_ctxt.cho_idle()) { |
561 | 0 | ue_ctxt_list.remove_ue_context(ue_index); |
562 | 0 | } |
563 | 0 | } |
564 | | |
565 | | void xnap_impl::handle_handover_success_required(cu_cp_ue_index_t ue_index, const nr_cell_global_id_t& cgi) |
566 | 0 | { |
567 | 0 | if (!ue_ctxt_list.contains(ue_index)) { |
568 | 0 | logger.warning("ue={}: Cannot send HandoverSuccess: UE context not found", ue_index); |
569 | 0 | return; |
570 | 0 | } |
571 | | |
572 | 0 | xnap_ue_context& ue_ctxt = ue_ctxt_list[ue_index]; |
573 | |
|
574 | 0 | xnap_message xnap_msg = {}; |
575 | 0 | xnap_msg.pdu.set_init_msg(); |
576 | 0 | xnap_msg.pdu.init_msg().load_info_obj(ASN1_XNAP_ID_HO_SUCCESS); |
577 | |
|
578 | 0 | ho_success_s& ho_success = xnap_msg.pdu.init_msg().value.ho_success(); |
579 | | // HandoverSuccess: target -> source. source_id is the peer (source) XNAP UE ID; target_id is the local XNAP UE ID. |
580 | 0 | ho_success->source_ng_ra_nnode_ue_xn_ap_id = to_underlying(ue_ctxt.ue_ids.peer_xnap_ue_id); |
581 | 0 | ho_success->target_ng_ra_nnode_ue_xn_ap_id = to_underlying(ue_ctxt.ue_ids.local_xnap_ue_id); |
582 | 0 | ho_success->requested_target_cell_global_id.set_nr() = cgi_to_asn1(cgi); |
583 | |
|
584 | 0 | if (!tx_notifier.on_new_message(xnap_msg)) { |
585 | 0 | ue_ctxt.logger.log_warning("Xn-C association is not set. Cannot send HandoverSuccess"); |
586 | 0 | return; |
587 | 0 | } |
588 | 0 | } |
589 | | |
590 | | void xnap_impl::handle_sn_status_transfer_required(const cu_cp_status_transfer& sn_status_transfer) |
591 | 0 | { |
592 | 0 | const cu_cp_ue_index_t ue_index = sn_status_transfer.ue_index; |
593 | 0 | if (!ue_ctxt_list.contains(ue_index)) { |
594 | 0 | logger.warning("ue={}: Cannot transmit SNStatusTransfer. UE context does not exist", ue_index); |
595 | 0 | return; |
596 | 0 | } |
597 | | |
598 | 0 | xnap_ue_context& ue_ctxt = ue_ctxt_list[ue_index]; |
599 | |
|
600 | 0 | xnap_message xnap_msg = {}; |
601 | 0 | xnap_msg.pdu.set_init_msg(); |
602 | 0 | xnap_msg.pdu.init_msg().load_info_obj(ASN1_XNAP_ID_S_N_STATUS_TRANSFER); |
603 | |
|
604 | 0 | sn_status_transfer_s& asn1_sn_status = xnap_msg.pdu.init_msg().value.sn_status_transfer(); |
605 | | // This is sent from the source to the target, so the source XNAP UE ID is the local UE ID and the target XNAP UE ID |
606 | | // is the peer UE ID. |
607 | 0 | asn1_sn_status->source_ng_ra_nnode_ue_xn_ap_id = to_underlying(ue_ctxt.ue_ids.local_xnap_ue_id); |
608 | 0 | asn1_sn_status->target_ng_ra_nnode_ue_xn_ap_id = to_underlying(ue_ctxt.ue_ids.peer_xnap_ue_id); |
609 | |
|
610 | 0 | sn_status_transfer_to_asn1(asn1_sn_status, sn_status_transfer.drbs_subject_to_status_transfer_list); |
611 | | |
612 | | // Forward message to Xn-C peer CU-CP. |
613 | 0 | if (!tx_notifier.on_new_message(xnap_msg)) { |
614 | 0 | ue_ctxt.logger.log_warning("Xn-C association is not set. Cannot send SNStatusTransfer"); |
615 | 0 | return; |
616 | 0 | } |
617 | 0 | } |
618 | | |
619 | | async_task<expected<cu_cp_status_transfer>> xnap_impl::handle_sn_status_transfer_expected(cu_cp_ue_index_t ue_index) |
620 | 0 | { |
621 | 0 | if (!ue_ctxt_list.contains(ue_index)) { |
622 | 0 | logger.warning("ue={}: Cannot await SNStatusTransfer. UE context does not exist", ue_index); |
623 | 0 | expected<cu_cp_status_transfer> ret = make_unexpected(default_error_t{}); |
624 | 0 | return launch_no_op_task(ret); |
625 | 0 | } |
626 | | |
627 | 0 | xnap_ue_context& ue_ctxt = ue_ctxt_list[ue_index]; |
628 | 0 | return track_ue_procedure(launch_async<xnap_sn_status_transfer_procedure>( |
629 | 0 | xnap_cfg.procedure_timeout, ue_ctxt.sn_status_transfer_outcome, ue_ctxt.logger)); |
630 | 0 | } |
631 | | |
632 | | void xnap_impl::handle_handover_success(const asn1::xnap::ho_success_s& msg) |
633 | 0 | { |
634 | | // HandoverSuccess is sent from the target to the source. From the source's perspective, |
635 | | // Source NG-RAN node UE XnAP ID is our own (local) XNAP UE ID set in the HandoverRequest. |
636 | 0 | local_xnap_ue_id_t local_xnap_ue_id = uint_to_local_xnap_ue_id(msg->source_ng_ra_nnode_ue_xn_ap_id); |
637 | 0 | if (!ue_ctxt_list.contains(local_xnap_ue_id)) { |
638 | 0 | logger.warning("Received HandoverSuccess for unknown local_xnap_ue_id={}", local_xnap_ue_id); |
639 | 0 | return; |
640 | 0 | } |
641 | | |
642 | 0 | xnap_ue_context& ue_ctxt_ref = ue_ctxt_list[local_xnap_ue_id]; |
643 | | |
644 | | // TS 38.423 Section 8.2.8: the Requested Target Cell Global ID names the cell the UE accessed. It is what |
645 | | // identifies the winning candidate, since XNAP UE IDs are only unique per Xn interface. |
646 | 0 | if (msg->requested_target_cell_global_id.type() != target_cgi_c::types_opts::nr) { |
647 | 0 | ue_ctxt_ref.logger.log_warning("Discarding HandoverSuccess: Requested Target Cell Global ID is not an NR cell"); |
648 | 0 | return; |
649 | 0 | } |
650 | 0 | const nr_cell_global_id_t winner_cgi = asn1_to_cgi(msg->requested_target_cell_global_id.nr()); |
651 | | |
652 | | // Only a cell we prepared can have won. Adopting the ID first would leave the context addressed by an ID that |
653 | | // belongs to no candidate if the peer names a stale or unknown cell. |
654 | 0 | if (ue_ctxt_ref.find_cho_prepared(winner_cgi) == peer_xnap_ue_id_t::invalid) { |
655 | 0 | ue_ctxt_ref.logger.log_warning("Discarding HandoverSuccess: no CHO candidate prepared for {}", winner_cgi); |
656 | 0 | return; |
657 | 0 | } |
658 | | |
659 | | // The Target NG-RAN node UE XnAP ID is the winning target's own local ID, so adopt it: the SN Status Transfer that |
660 | | // follows addresses the UE by it, and the other candidates' IDs are of no further use. |
661 | 0 | ue_ctxt_list.update_peer_xnap_ue_id(local_xnap_ue_id, uint_to_peer_xnap_ue_id(msg->target_ng_ra_nnode_ue_xn_ap_id)); |
662 | |
|
663 | 0 | ue_ctxt_ref.logger.log_debug("CHO winner is {}", winner_cgi); |
664 | | |
665 | | // Notify CU-CP: source must now send SN Status Transfer and release UE context. |
666 | 0 | cu_cp_notifier.on_handover_success_received(ue_ctxt_ref.ue_ids.ue_index, winner_cgi); |
667 | 0 | } |
668 | | |
669 | | void xnap_impl::handle_conditional_ho_cancel(const asn1::xnap::conditional_ho_cancel_s& msg) |
670 | 0 | { |
671 | | // ConditionalHandoverCancel is sent from TARGET to SOURCE (TS 38.423 Section 8.2.9). |
672 | | // The target self-cancels prepared CHO contexts it no longer wants to hold. |
673 | | // Source NG-RAN node UE XnAP ID is our local XNAP UE ID (we allocated it when initiating HO). |
674 | | // Target NG-RAN node UE XnAP ID is the cancelling target's XNAP UE ID. |
675 | 0 | local_xnap_ue_id_t local_id = uint_to_local_xnap_ue_id(msg->source_ng_ra_nnode_ue_xn_ap_id); |
676 | 0 | if (!ue_ctxt_list.contains(local_id)) { |
677 | 0 | logger.info("Received ConditionalHandoverCancel for unknown local_xnap_ue_id={}", |
678 | 0 | msg->source_ng_ra_nnode_ue_xn_ap_id); |
679 | 0 | return; |
680 | 0 | } |
681 | | |
682 | 0 | xnap_ue_context& ue_ctxt = ue_ctxt_list[local_id]; |
683 | 0 | const peer_xnap_ue_id_t cancelling_peer = uint_to_peer_xnap_ue_id(msg->target_ng_ra_nnode_ue_xn_ap_id); |
684 | | |
685 | | // TS 38.423 Section 8.2.9.2: when the target names cells, only those candidates are released; the remaining ones |
686 | | // stay prepared under the same UE context. Both lists identify candidates by NG-RAN CGI. |
687 | 0 | std::vector<nr_cell_global_id_t> cancelled_cells; |
688 | 0 | unsigned nof_non_nr_cells = 0; |
689 | 0 | for (const auto& item : msg->target_cells_to_cancel) { |
690 | 0 | if (item.target_cell.type() == target_cgi_c::types_opts::nr) { |
691 | 0 | cancelled_cells.push_back(asn1_to_cgi(item.target_cell.nr())); |
692 | 0 | } else { |
693 | 0 | ++nof_non_nr_cells; |
694 | 0 | } |
695 | 0 | } |
696 | 0 | for (const auto& item : msg->conditional_recfg_to_cancel_list) { |
697 | 0 | if (item.pcell_id.type() == target_cgi_c::types_opts::nr) { |
698 | 0 | cancelled_cells.push_back(asn1_to_cgi(item.pcell_id.nr())); |
699 | 0 | } else { |
700 | 0 | ++nof_non_nr_cells; |
701 | 0 | } |
702 | 0 | } |
703 | 0 | if (nof_non_nr_cells != 0) { |
704 | 0 | ue_ctxt.logger.log_warning("ConditionalHandoverCancel names {} cell(s) that are not NR cells; ignoring them", |
705 | 0 | nof_non_nr_cells); |
706 | 0 | } |
707 | |
|
708 | 0 | if (!msg->target_cells_to_cancel_present and !msg->conditional_recfg_to_cancel_list_present) { |
709 | | // Neither list is present: the target withdraws every candidate it prepared for this UE. |
710 | 0 | for (const auto& [cell, peer_xnap_ue_id] : ue_ctxt.cho_prepared) { |
711 | 0 | if (peer_xnap_ue_id == cancelling_peer) { |
712 | 0 | cancelled_cells.push_back(cell); |
713 | 0 | } |
714 | 0 | } |
715 | 0 | } |
716 | |
|
717 | 0 | for (const nr_cell_global_id_t& cell : cancelled_cells) { |
718 | | // TS 38.423 Section 8.2.9.4: candidate cells that were not prepared using this UE-associated signalling |
719 | | // connection are ignored. The connection is named by both XNAP UE IDs (Section 8.2.9.2), so a cell prepared |
720 | | // under another Target NG-RAN node UE XnAP ID is not this sender's to cancel. |
721 | 0 | const peer_xnap_ue_id_t prepared_peer = ue_ctxt.find_cho_prepared(cell); |
722 | 0 | if (prepared_peer == peer_xnap_ue_id_t::invalid) { |
723 | 0 | ue_ctxt.logger.log_info("ConditionalHandoverCancel names nci={:#x}, which is not prepared", cell.nci.value()); |
724 | 0 | continue; |
725 | 0 | } |
726 | 0 | if (prepared_peer != cancelling_peer) { |
727 | 0 | ue_ctxt.logger.log_info("Ignoring nci={:#x} in ConditionalHandoverCancel: prepared for peer_xnap_ue={}, not {}", |
728 | 0 | cell.nci.value(), |
729 | 0 | fmt::underlying(prepared_peer), |
730 | 0 | fmt::underlying(cancelling_peer)); |
731 | 0 | continue; |
732 | 0 | } |
733 | | // The peer XNAP UE ID lookup is released from the candidate's own record, never from the ID carried by this |
734 | | // message, so a stale or misaddressed one cannot unregister an unrelated UE. |
735 | 0 | ue_ctxt_list.remove_cho_prepared(local_id, cell); |
736 | 0 | } |
737 | | |
738 | | // Release the XNAP UE context once this peer holds no candidate for this UE any more. |
739 | 0 | if (ue_ctxt.cho_idle()) { |
740 | 0 | ue_ctxt_list.remove_ue_context(ue_ctxt.ue_ids.ue_index); |
741 | 0 | } |
742 | 0 | } |
743 | | |
744 | | bool xnap_impl::handle_ue_context_release_required(cu_cp_ue_index_t ue_index) |
745 | 0 | { |
746 | 0 | if (!ue_ctxt_list.contains(ue_index)) { |
747 | 0 | logger.warning("ue={}: Cannot transmit UEContextReleaseRequest. UE context does not exist", ue_index); |
748 | 0 | return false; |
749 | 0 | } |
750 | | |
751 | 0 | xnap_ue_context& ue_ctxt = ue_ctxt_list[ue_index]; |
752 | |
|
753 | 0 | xnap_message xnap_msg = {}; |
754 | 0 | xnap_msg.pdu.set_init_msg(); |
755 | 0 | xnap_msg.pdu.init_msg().load_info_obj(ASN1_XNAP_ID_U_E_CONTEXT_RELEASE); |
756 | |
|
757 | 0 | ue_context_release_s& ue_ctxt_release = xnap_msg.pdu.init_msg().value.ue_context_release(); |
758 | | // This is sent from the target to the source, so the local XNAP UE ID is the target and the peer XNAP UE ID is the |
759 | | // source. |
760 | 0 | ue_ctxt_release->source_ng_ra_nnode_ue_xn_ap_id = to_underlying(ue_ctxt.ue_ids.peer_xnap_ue_id); |
761 | 0 | ue_ctxt_release->target_ng_ra_nnode_ue_xn_ap_id = to_underlying(ue_ctxt.ue_ids.local_xnap_ue_id); |
762 | | |
763 | | // Forward message to Xn-C peer CU-CP. |
764 | 0 | if (!tx_notifier.on_new_message(xnap_msg)) { |
765 | 0 | ue_ctxt.logger.log_warning("Xn-C association is not set. Cannot send UEContextReleaseRequest"); |
766 | 0 | return false; |
767 | 0 | } |
768 | | |
769 | | // Remove UE context locally as well, as no further messages for this UE are expected. |
770 | 0 | ue_ctxt_list.remove_ue_context(ue_index); |
771 | |
|
772 | 0 | return true; |
773 | 0 | } |
774 | | |
775 | | std::optional<cu_cp_served_cell_info> xnap_impl::find_peer_served_cell(nr_cell_identity nci) const |
776 | 0 | { |
777 | 0 | if (!peer_ctxt.has_value()) { |
778 | 0 | return std::nullopt; |
779 | 0 | } |
780 | | |
781 | 0 | auto cell_it = std::find_if(peer_ctxt->list_of_served_cells_nr.begin(), |
782 | 0 | peer_ctxt->list_of_served_cells_nr.end(), |
783 | 0 | [nci](const cu_cp_served_cell_info& cell) { return cell.nr_cgi.nci == nci; }); |
784 | 0 | if (cell_it == peer_ctxt->list_of_served_cells_nr.end()) { |
785 | 0 | return std::nullopt; |
786 | 0 | } |
787 | | |
788 | 0 | return *cell_it; |
789 | 0 | } |
790 | | |
791 | | async_task<xnap_retrieve_ue_context_response> |
792 | | xnap_impl::handle_retrieve_ue_context_required(const xnap_retrieve_ue_context_request& request) |
793 | 0 | { |
794 | 0 | if (!ue_ctxt_list.contains(request.ue_index)) { |
795 | 0 | local_xnap_ue_id_t local_xnap_ue_id = ue_ctxt_list.allocate_local_xnap_ue_id(); |
796 | 0 | if (local_xnap_ue_id == local_xnap_ue_id_t::invalid) { |
797 | 0 | logger.warning("ue={}: No local XNAP UE ID available", request.ue_index); |
798 | 0 | return launch_no_op_task(xnap_retrieve_ue_context_response{}); |
799 | 0 | } |
800 | | |
801 | 0 | ue_ctxt_list.add_ue(request.ue_index, local_xnap_ue_id); |
802 | 0 | } |
803 | | |
804 | 0 | return track_ue_procedure( |
805 | 0 | launch_async<xnap_new_node_retrieve_ue_context_procedure>(request, ue_ctxt_list, tx_notifier)); |
806 | 0 | } |
807 | | |
808 | | void xnap_impl::handle_retrieve_ue_context_request(const asn1::xnap::retrieve_ue_context_request_s& msg) |
809 | 0 | { |
810 | | // This is sent from the new to the old NG-RAN node, so the new NG-RAN node UE XnAP ID is the peer XNAP UE ID. |
811 | 0 | const peer_xnap_ue_id_t peer_xnap_ue_id = uint_to_peer_xnap_ue_id(msg->new_ng_ra_nnode_ue_xn_ap_id); |
812 | | |
813 | | // Add lambda that generates and transmits a Retrieve UE Context Failure message. It is used before the procedure is |
814 | | // launched, so it addresses the peer by the UE XnAP ID the request carried. |
815 | 0 | auto send_retrieve_ue_context_failure = [this, peer_xnap_ue_id](const xnap_cause_t& cause) { |
816 | 0 | xnap_message xnap_msg; |
817 | 0 | xnap_msg.pdu.set_unsuccessful_outcome(); |
818 | 0 | xnap_msg.pdu.unsuccessful_outcome().load_info_obj(ASN1_XNAP_ID_RETRIEVE_UE_CONTEXT); |
819 | 0 | auto& asn1_failure = xnap_msg.pdu.unsuccessful_outcome().value.retrieve_ue_context_fail(); |
820 | 0 | asn1_failure->new_ng_ra_nnode_ue_xn_ap_id = to_underlying(peer_xnap_ue_id); |
821 | 0 | asn1_failure->cause = cause_to_asn1(cause); |
822 | |
|
823 | 0 | if (!tx_notifier.on_new_message(xnap_msg)) { |
824 | 0 | logger.warning("Xn-C association is not set. Cannot send RetrieveUEContextFailure"); |
825 | 0 | return; |
826 | 0 | } |
827 | 0 | logger.info("Sending RetrieveUEContextFailure"); |
828 | 0 | }; |
829 | |
|
830 | 0 | xnap_retrieve_ue_context_request request; |
831 | 0 | if (!asn1_to_retrieve_ue_context_request(request, msg)) { |
832 | 0 | logger.info("Received invalid RetrieveUEContextRequest"); |
833 | 0 | send_retrieve_ue_context_failure(cause_protocol_t::abstract_syntax_error_falsely_constructed_msg); |
834 | 0 | return; |
835 | 0 | } |
836 | | |
837 | | // Resolve the UE the peer is asking for. Only this node can do so, as the UE Context ID refers to identities this |
838 | | // node allocated. |
839 | 0 | request.ue_index = cu_cp_notifier.on_xnap_ue_context_id_lookup(request.ue_context_id); |
840 | 0 | if (request.ue_index == cu_cp_ue_index_t::invalid) { |
841 | 0 | logger.info("Received RetrieveUEContextRequest for unknown UE Context ID"); |
842 | 0 | send_retrieve_ue_context_failure(xnap_cause_radio_network_t::ue_context_id_not_known); |
843 | 0 | return; |
844 | 0 | } |
845 | | |
846 | | // Resolve the target cell, which the CU-CP needs to derive KgNB* (TS 33.501 section 6.11). |
847 | 0 | request.target_cell = find_peer_served_cell(request.target_nci); |
848 | 0 | if (!request.target_cell.has_value()) { |
849 | 0 | logger.info("ue={}: Received RetrieveUEContextRequest for a target cell the peer did not advertise. nci={}", |
850 | 0 | request.ue_index, |
851 | 0 | request.target_nci); |
852 | 0 | send_retrieve_ue_context_failure(xnap_cause_radio_network_t::cell_not_available); |
853 | 0 | return; |
854 | 0 | } |
855 | | |
856 | 0 | if (!cu_cp_notifier.schedule_async_task( |
857 | 0 | request.ue_index, |
858 | 0 | track_ue_procedure(launch_async<xnap_old_node_retrieve_ue_context_procedure>( |
859 | 0 | request, peer_xnap_ue_id, ue_ctxt_list, cu_cp_notifier, tx_notifier, logger)))) { |
860 | 0 | logger.debug("ue={}: Couldn't schedule the retrieve UE context procedure", request.ue_index); |
861 | 0 | send_retrieve_ue_context_failure(xnap_cause_radio_network_t::unspecified); |
862 | 0 | return; |
863 | 0 | } |
864 | 0 | } |
865 | | |
866 | | template <typename Result> |
867 | | async_task<Result> xnap_impl::track_ue_procedure(async_task<Result> proc) |
868 | 0 | { |
869 | 0 | ++nof_ue_procedures; |
870 | | |
871 | | // Runs when the procedure completes, or when it is discarded without ever running. |
872 | 0 | auto on_completion = make_scope_exit([this]() { |
873 | 0 | if (--nof_ue_procedures == 0) { |
874 | 0 | ue_procedures_done.set(); |
875 | 0 | } |
876 | 0 | }); Unexecuted instantiation: ocudu::ocucp::xnap_impl::track_ue_procedure<ocudu::ocucp::xnap_handover_preparation_response>(ocudu::async_task<ocudu::ocucp::xnap_handover_preparation_response>)::{lambda()#1}::operator()() constUnexecuted instantiation: ocudu::ocucp::xnap_impl::track_ue_procedure<tl::expected<ocudu::ocucp::cu_cp_status_transfer, ocudu::default_error_t> >(ocudu::async_task<tl::expected<ocudu::ocucp::cu_cp_status_transfer, ocudu::default_error_t> >)::{lambda()#1}::operator()() constUnexecuted instantiation: ocudu::ocucp::xnap_impl::track_ue_procedure<ocudu::ocucp::xnap_retrieve_ue_context_response>(ocudu::async_task<ocudu::ocucp::xnap_retrieve_ue_context_response>)::{lambda()#1}::operator()() const |
877 | |
|
878 | 0 | return launch_async([proc = std::move(proc), on_completion = std::move(on_completion), res = Result{}]( |
879 | 0 | coro_context<async_task<Result>>& ctx) mutable { |
880 | 0 | CORO_BEGIN(ctx); |
881 | 0 | CORO_AWAIT_VALUE(res, std::move(proc)); |
882 | 0 | CORO_RETURN(std::move(res)); |
883 | 0 | }); Unexecuted instantiation: ocudu::ocucp::xnap_impl::track_ue_procedure<ocudu::ocucp::xnap_handover_preparation_response>(ocudu::async_task<ocudu::ocucp::xnap_handover_preparation_response>)::{lambda(ocudu::detail::base_coro_frame<ocudu::async_task<ocudu::ocucp::xnap_handover_preparation_response>::promise_type>&)#1}::operator()(ocudu::detail::base_coro_frame<ocudu::async_task<ocudu::ocucp::xnap_handover_preparation_response>::promise_type>&)Unexecuted instantiation: ocudu::ocucp::xnap_impl::track_ue_procedure<tl::expected<ocudu::ocucp::cu_cp_status_transfer, ocudu::default_error_t> >(ocudu::async_task<tl::expected<ocudu::ocucp::cu_cp_status_transfer, ocudu::default_error_t> >)::{lambda(ocudu::detail::base_coro_frame<ocudu::async_task<tl::expected<ocudu::ocucp::cu_cp_status_transfer, ocudu::default_error_t> >::promise_type>&)#1}::operator()(ocudu::detail::base_coro_frame<ocudu::async_task<tl::expected<ocudu::ocucp::cu_cp_status_transfer, ocudu::default_error_t> >::promise_type>&)Unexecuted instantiation: ocudu::ocucp::xnap_impl::track_ue_procedure<ocudu::ocucp::xnap_retrieve_ue_context_response>(ocudu::async_task<ocudu::ocucp::xnap_retrieve_ue_context_response>)::{lambda(ocudu::detail::base_coro_frame<ocudu::async_task<ocudu::ocucp::xnap_retrieve_ue_context_response>::promise_type>&)#1}::operator()(ocudu::detail::base_coro_frame<ocudu::async_task<ocudu::ocucp::xnap_retrieve_ue_context_response>::promise_type>&) |
884 | 0 | } Unexecuted instantiation: ocudu::async_task<ocudu::ocucp::xnap_handover_preparation_response> ocudu::ocucp::xnap_impl::track_ue_procedure<ocudu::ocucp::xnap_handover_preparation_response>(ocudu::async_task<ocudu::ocucp::xnap_handover_preparation_response>) Unexecuted instantiation: ocudu::async_task<tl::expected<ocudu::ocucp::cu_cp_status_transfer, ocudu::default_error_t> > ocudu::ocucp::xnap_impl::track_ue_procedure<tl::expected<ocudu::ocucp::cu_cp_status_transfer, ocudu::default_error_t> >(ocudu::async_task<tl::expected<ocudu::ocucp::cu_cp_status_transfer, ocudu::default_error_t> >) Unexecuted instantiation: ocudu::async_task<ocudu::ocucp::xnap_retrieve_ue_context_response> ocudu::ocucp::xnap_impl::track_ue_procedure<ocudu::ocucp::xnap_retrieve_ue_context_response>(ocudu::async_task<ocudu::ocucp::xnap_retrieve_ue_context_response>) |
885 | | |
886 | | async_task<void> xnap_impl::track_ue_procedure(async_task<void> proc) |
887 | 0 | { |
888 | 0 | ++nof_ue_procedures; |
889 | | |
890 | | // Runs when the procedure completes, or when it is discarded without ever running. |
891 | 0 | auto on_completion = make_scope_exit([this]() { |
892 | 0 | if (--nof_ue_procedures == 0) { |
893 | 0 | ue_procedures_done.set(); |
894 | 0 | } |
895 | 0 | }); |
896 | |
|
897 | 0 | return launch_async( |
898 | 0 | [proc = std::move(proc), on_completion = std::move(on_completion)](coro_context<async_task<void>>& ctx) mutable { |
899 | 0 | CORO_BEGIN(ctx); |
900 | 0 | CORO_AWAIT(std::move(proc)); |
901 | 0 | CORO_RETURN(); |
902 | 0 | }); |
903 | 0 | } |