Merge remote-tracking branch 'origin/CI-ho-attenuator-N2' into integration_2025_w43 (!3708)

Add monolithic N2 handover test pipeline with B210 and Quectel

The goal of this MR is to provide an automated end-to-end N2 HO
validation with real RF (B210) and Quectel UE.

During the testing phase a post-HO assertion failure due to
uninitialized selected_plmn_identity was found: this was also fixed.

CI: Add monolithic N2 handover test pipeline (B210 + Quectel)

- New XML pipeline for N2 HO testing
- Docker Compose for two monolithic gNBs (gNB1/gNB2)
- Handover sequence: gNB1 → gNB2 → gNB1 with 5s controlled attenuation
  (RC4DAT).
- Channel mapping: ch1-2 gNB1, ch3-4 gNB2
- Telnet server enabled for CI commands
- Clean-up/reset of attenuator state included

NGAP

- fix PLMN/cell location for Initial UE and Uplink NAS
- Prevents assertion in ngap_gNB_nas_uplink() due to uninitialized
  selected_plmn_identity
- Initial UE Message: pass UE-selected PLMN and NR cell ID from RRC to
  NGAP
- Uplink NAS Transport: use PLMN, TAC, and cell ID from the message
- Build NR CGI as gNB ID + cell ID
- Store UE-selected PLMN only in RRC UE context (for later NAS); stop
  storing PLMN in NGAP UE context.
- PLMN validation when processing handover requests from the source gNB:
  when PLMN validation fails, the target gNB now properly sends
  NGAP_HANDOVER_FAILURE
- Store serving PLMN for later use in UE context
This commit is contained in:
Robert Schmidt
2025-10-23 19:18:23 +02:00
12 changed files with 619 additions and 52 deletions
+6
View File
@@ -421,6 +421,8 @@ typedef struct ngap_nas_first_req_s {
/* PLMN: Selected PLMN Identity (optional)
* User Location Information (mandatory) */
plmn_id_t plmn;
// NR Cell ID for NR CGI (mandatory)
uint32_t nr_cell_id;
// RRC Establishment Cause (mandatory)
ngap_rrc_establishment_cause_t establishment_cause;
// NAS-PDU (mandatory)
@@ -434,6 +436,10 @@ typedef struct ngap_uplink_nas_s {
uint32_t gNB_ue_ngap_id;
/* NAS pdu */
byte_array_t nas_pdu;
/* UserLocationInformation (mandatory) */
plmn_id_t plmn; // CGI and TAI
uint32_t nr_cell_id; // CGI
uint32_t tac; // TAI
} ngap_uplink_nas_t;
typedef struct target_cell_id_s {
@@ -38,13 +38,11 @@ ue_id_e2sm_t fill_e2sm_gnb_ue_id_data(const gNB_RRC_UE_t *rrc_ue_context, __attr
ue_id.gnb.amf_ue_ngap_id = auni < (1LL << 40) ? auni : 0;
// Mandatory
//GUAMI 6.2.3.17
ue_id.gnb.guami.plmn_id = (e2sm_plmn_t) {
.mcc = rrc_ue_context->ue_guami.mcc,
.mnc = rrc_ue_context->ue_guami.mnc,
.mnc_digit_len = rrc_ue_context->ue_guami.mnc_len
};
//GUAMI 6.2.3.17
ue_id.gnb.guami.plmn_id = (e2sm_plmn_t){.mcc = rrc_ue_context->ue_guami.plmn.mcc,
.mnc = rrc_ue_context->ue_guami.plmn.mnc,
.mnc_digit_len = rrc_ue_context->ue_guami.plmn.mnc_digit_length};
ue_id.gnb.guami.amf_region_id = rrc_ue_context->ue_guami.amf_region_id;
ue_id.gnb.guami.amf_set_id = rrc_ue_context->ue_guami.amf_set_id;
ue_id.gnb.guami.amf_ptr = rrc_ue_context->ue_guami.amf_pointer;
+2
View File
@@ -209,6 +209,8 @@ typedef struct gNB_RRC_UE_s {
uint64_t amf_ue_ngap_id;
// Globally Unique AMF Identifier
nr_guami_t ue_guami;
// Serving PLMN of the UE
plmn_id_t serving_plmn;
ngap_security_capabilities_t security_capabilities;
//NSA block
+52 -10
View File
@@ -110,6 +110,24 @@ static const uint16_t NGAP_INTEGRITY_NIA3_MASK = 0x2000;
static void set_UE_security_algos(const gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE, const ngap_security_capabilities_t *cap);
/** @brief Validates PLMN against allowed PLMN list and returns pointer to matching PLMN
* @param rrc RRC instance
* @param plmn PLMN to validate
* @return pointer to matching PLMN in configuration, or NULL if not found */
static const plmn_id_t *get_serving_plmn(gNB_RRC_INST *rrc, const plmn_id_t *plmn)
{
// Find matching PLMN in configuration
for (int idx = 0; idx < rrc->configuration.num_plmn; idx++) {
const plmn_id_t *allowed = &rrc->configuration.plmn[idx];
if (allowed->mcc == plmn->mcc && allowed->mnc == plmn->mnc && allowed->mnc_digit_length == plmn->mnc_digit_length) {
LOG_D(NR_RRC, "PLMN (MCC:%d, MNC:%*d) matched with allowed PLMN[%d]\n", plmn->mcc, plmn->mnc_digit_length, plmn->mnc, idx);
return allowed;
}
}
LOG_W(NR_RRC, "PLMN (MCC:%d, MNC:%*d) not found in allowed PLMN list\n", plmn->mcc, plmn->mnc_digit_length, plmn->mnc);
return NULL;
}
/*!
*\brief save security key.
*\param UE UE context.
@@ -165,9 +183,7 @@ static nr_guami_t get_guami(const uint32_t amf_Id, const plmn_id_t plmn)
guami.amf_region_id = (amf_Id >> 16) & 0xff;
guami.amf_set_id = (amf_Id >> 6) & 0x3ff;
guami.amf_pointer = amf_Id & 0x3f;
guami.mcc = plmn.mcc;
guami.mnc = plmn.mnc;
guami.mnc_len = plmn.mnc_digit_length;
guami.plmn = plmn;
return guami;
}
@@ -223,17 +239,22 @@ void rrc_gNB_send_NGAP_NAS_FIRST_REQ(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE, NR_RRC
req->nas_pdu = create_byte_array(rrcSetupComplete->dedicatedNAS_Message.size, rrcSetupComplete->dedicatedNAS_Message.buf);
/* Selected PLMN Identity (Optional)
* selectedPLMN-Identity in RRCSetupComplete: Index of the PLMN selected by the UE from the plmn-IdentityInfoList (SIB1)
* Selected PLMN Identity in INITIAL UE MESSAGE: Indicates the selected PLMN id for the non-3GPP access.*/
if (rrcSetupComplete->selectedPLMN_Identity > rrc->configuration.num_plmn) {
* selectedPLMN-Identity in RRCSetupComplete: Index of the PLMN selected by the UE from the plmn-IdentityInfoList (SIB1) */
int idx = rrcSetupComplete->selectedPLMN_Identity - 1; // Convert 1-based PLMN Identity IE to 0-based index
if (idx < 0 || idx >= rrc->configuration.num_plmn) {
LOG_E(NGAP,
"Failed to send Initial UE Message: selected PLMN (%ld) identity is out of bounds (%d)\n",
rrcSetupComplete->selectedPLMN_Identity,
"Failed to send Initial UE Message: selected PLMN index (%d) is out of bounds [0..%d)\n",
idx,
rrc->configuration.num_plmn);
return;
}
int selected_plmn_identity = rrcSetupComplete->selectedPLMN_Identity - 1; // Convert 1-based PLMN Identity IE to 0-based index
req->plmn = rrc->configuration.plmn[selected_plmn_identity]; // Select from the stored list
req->plmn = rrc->configuration.plmn[idx]; // Select from the stored list
/* UE is connected to only one PLMN at a time: store this as the serving PLMN */
UE->serving_plmn = req->plmn;
// Cell ID (NR CGI)
req->nr_cell_id = UE->nr_cellid;
// PLMN (NR CGI and TAI)
plmn_id_t *p = &req->plmn;
LOG_I(NGAP, "Selected PLMN in the NG Initial UE Message: MCC=%03d MNC=%0*d\n", p->mcc, p->mnc_digit_length, p->mnc);
@@ -721,6 +742,10 @@ void rrc_gNB_send_NGAP_UPLINK_NAS(gNB_RRC_INST *rrc, gNB_RRC_UE_t *UE, const NR_
NGAP_UPLINK_NAS(msg_p).gNB_ue_ngap_id = UE->rrc_ue_id;
NGAP_UPLINK_NAS(msg_p).nas_pdu.len = nas->size;
NGAP_UPLINK_NAS(msg_p).nas_pdu.buf = buf;
/* Fill PLMN and location info: use serving PLMN of the UE */
NGAP_UPLINK_NAS(msg_p).plmn = UE->serving_plmn;
NGAP_UPLINK_NAS(msg_p).nr_cell_id = rrc->nr_cellid;
NGAP_UPLINK_NAS(msg_p).tac = rrc->configuration.tac;
itti_send_msg_to_task(TASK_NGAP, rrc->module_id, msg_p);
}
@@ -1154,6 +1179,20 @@ int rrc_gNB_process_Handover_Request(gNB_RRC_INST *rrc, instance_t instance, nga
rrc_gNB_send_NGAP_HANDOVER_FAILURE(rrc, &fail);
return -1;
}
// Validate PLMN from GUAMI against allowed PLMN list
const plmn_id_t *serving_plmn = get_serving_plmn(rrc, &msg->guami.plmn);
if (!serving_plmn) {
LOG_E(NR_RRC, "PLMN from GUAMI not supported - rejecting handover\n");
ngap_handover_failure_t fail = {
.amf_ue_ngap_id = msg->amf_ue_ngap_id,
.cause.type = NGAP_CAUSE_RADIO_NETWORK,
.cause.value = NGAP_CAUSE_RADIO_NETWORK_HO_TARGET_NOT_ALLOWED,
};
rrc_gNB_send_NGAP_HANDOVER_FAILURE(rrc, &fail);
return -1;
}
uint16_t pci = du->setup_req->cell[0].info.nr_pci;
LOG_I(NR_RRC, "Received Handover Request (on NR Cell ID=%lu, PCI=%u) \n", msg->nr_cell_id, pci);
@@ -1169,6 +1208,9 @@ int rrc_gNB_process_Handover_Request(gNB_RRC_INST *rrc, instance_t instance, nga
// Store IDs in UE context
UE->amf_ue_ngap_id = msg->amf_ue_ngap_id;
UE->ue_guami = msg->guami;
// Store the serving PLMN
UE->serving_plmn = *serving_plmn;
UE->ho_context->target->ue_ho_prep_info = copy_byte_array(msg->ue_ho_prep_info);
// store the received UE Security Capabilities in the UE context
FREE_AND_ZERO_BYTE_ARRAY(UE->ue_cap_buffer);