From c3eaef704f37d04eee5fe344d160db1045a2c186 Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Thu, 25 Sep 2025 18:46:25 +0200 Subject: [PATCH 1/4] Add monolithic N2 handover test pipeline with B210 and Quectel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add XML pipeline for N2 handover testing - Add Docker compose for two monolithic gNBs - Features: * Two monolithic gNBs (gNB1, gNB2) with proper CU+DU parameters * Gradual attenuation control using RC4DAT attenuator (5s duration) * N2 handover sequence: gNB1 → gNB2 → gNB1 * Proper channel mapping: ch1-2 for gNB1, ch3-4 for gNB2 * Telnet server configuration for CI commands * Handover-specific neighbor configuration * Clean test state management with attenuator reset --- .../gnb.sa.band77.51prb.usrpb200.n2-ho.conf | 237 ++++++++++++++++++ .../container_sa_n2_ho_b210_quectel.xml | 230 +++++++++++++++++ .../5g_sa_n2_ho_b210/docker-compose.yml | 61 +++++ 3 files changed, 528 insertions(+) create mode 100644 ci-scripts/conf_files/gnb.sa.band77.51prb.usrpb200.n2-ho.conf create mode 100644 ci-scripts/xml_files/container_sa_n2_ho_b210_quectel.xml create mode 100644 ci-scripts/yaml_files/5g_sa_n2_ho_b210/docker-compose.yml diff --git a/ci-scripts/conf_files/gnb.sa.band77.51prb.usrpb200.n2-ho.conf b/ci-scripts/conf_files/gnb.sa.band77.51prb.usrpb200.n2-ho.conf new file mode 100644 index 0000000000..dc24284c18 --- /dev/null +++ b/ci-scripts/conf_files/gnb.sa.band77.51prb.usrpb200.n2-ho.conf @@ -0,0 +1,237 @@ +Active_gNBs = ( "gNB-OAI"); +# Asn1_verbosity, choice in: none, info, annoying +Asn1_verbosity = "none"; + +gNBs = +( + { + ////////// Identification parameters: + gNB_ID = 0xe00; + gNB_name = "gNB-OAI"; + + // Tracking area code, 0x0000 and 0xfffe are reserved values + tracking_area_code = 1; + plmn_list = ({ mcc = 001; mnc = 03; mnc_length = 2; snssaiList = ({ sst = 1, sd = 0xffffff }) }); + + // Include neighbour configuration for N2 handover + @include "neighbour-config-ho.conf" + + nr_cellid = 12345678L; + + ////////// Physical parameters: + + + servingCellConfigCommon = ( + { + #spCellConfigCommon + + physCellId = 0; + +# downlinkConfigCommon + #frequencyInfoDL + # this is 3610.56 MHz + absoluteFrequencySSB = 679104; + dl_frequencyBand = 77; + # this is 3599.94 MHz + dl_absoluteFrequencyPointA = 678388; + #scs-SpecificCarrierList + dl_offstToCarrier = 0; +# subcarrierSpacing +# 0=kHz15, 1=kHz30, 2=kHz60, 3=kHz120 + dl_subcarrierSpacing = 1; + dl_carrierBandwidth = 51; + #initialDownlinkBWP + #genericParameters + # this is RBstart=27,L=48 (275*(L-1))+RBstart + initialDLBWPlocationAndBandwidth = 13750; +# subcarrierSpacing +# 0=kHz15, 1=kHz30, 2=kHz60, 3=kHz120 + initialDLBWPsubcarrierSpacing = 1; + #pdcch-ConfigCommon + initialDLBWPcontrolResourceSetZero = 12; + initialDLBWPsearchSpaceZero = 0; + + #uplinkConfigCommon + #frequencyInfoUL + ul_frequencyBand = 77; + #scs-SpecificCarrierList + ul_offstToCarrier = 0; +# subcarrierSpacing +# 0=kHz15, 1=kHz30, 2=kHz60, 3=kHz120 + ul_subcarrierSpacing = 1; + ul_carrierBandwidth = 51; + pMax = 20; + #initialUplinkBWP + #genericParameters + initialULBWPlocationAndBandwidth = 13750; +# subcarrierSpacing +# 0=kHz15, 1=kHz30, 2=kHz60, 3=kHz120 + initialULBWPsubcarrierSpacing = 1; + #rach-ConfigCommon + #rach-ConfigGeneric + prach_ConfigurationIndex = 98; +#prach_msg1_FDM +#0 = one, 1=two, 2=four, 3=eight + prach_msg1_FDM = 0; + prach_msg1_FrequencyStart = 0; + zeroCorrelationZoneConfig = 13; + preambleReceivedTargetPower = -96; +#preamblTransMax (0...10) = (3,4,5,6,7,8,10,20,50,100,200) + preambleTransMax = 6; +#powerRampingStep +# 0=dB0,1=dB2,2=dB4,3=dB6 + powerRampingStep = 1; +#ssb_perRACH_OccasionAndCB_PreamblesPerSSB_PR +#1=oneeighth,2=onefourth,3=half,4=one,5=two,6=four,7=eight,8=sixteen + ssb_perRACH_OccasionAndCB_PreamblesPerSSB_PR = 4; +#one (0..15) 4,8,12,16,...60,64 + ssb_perRACH_OccasionAndCB_PreamblesPerSSB = 14; +#ra_ContentionResolutionTimer +#(0..7) 8,16,24,32,40,48,56,64 + ra_ContentionResolutionTimer = 7; + rsrp_ThresholdSSB = 19; +#prach-RootSequenceIndex_PR +#1 = 839, 2 = 139 + prach_RootSequenceIndex_PR = 2; + prach_RootSequenceIndex = 1; + # SCS for msg1, can only be 15 for 30 kHz < 6 GHz, takes precendence over the one derived from prach-ConfigIndex + # + msg1_SubcarrierSpacing = 1, +# restrictedSetConfig +# 0=unrestricted, 1=restricted type A, 2=restricted type B + restrictedSetConfig = 0, + + msg3_DeltaPreamble = 1; + p0_NominalWithGrant =-90; + +# pucch-ConfigCommon setup : +# pucchGroupHopping +# 0 = neither, 1= group hopping, 2=sequence hopping + pucchGroupHopping = 0; + hoppingId = 40; + p0_nominal = -90; + + ssb_PositionsInBurst_Bitmap = 1; + +# ssb_periodicityServingCell +# 0 = ms5, 1=ms10, 2=ms20, 3=ms40, 4=ms80, 5=ms160, 6=spare2, 7=spare1 + ssb_periodicityServingCell = 2; + +# dmrs_TypeA_position +# 0 = pos2, 1 = pos3 + dmrs_TypeA_Position = 0; + +# subcarrierSpacing +# 0=kHz15, 1=kHz30, 2=kHz60, 3=kHz120 + subcarrierSpacing = 1; + + + #tdd-UL-DL-ConfigurationCommon +# subcarrierSpacing +# 0=kHz15, 1=kHz30, 2=kHz60, 3=kHz120 + referenceSubcarrierSpacing = 1; + # pattern1 + # dl_UL_TransmissionPeriodicity + # 0=ms0p5, 1=ms0p625, 2=ms1, 3=ms1p25, 4=ms2, 5=ms2p5, 6=ms5, 7=ms10 + dl_UL_TransmissionPeriodicity = 6; + nrofDownlinkSlots = 7; + nrofDownlinkSymbols = 6; + nrofUplinkSlots = 2; + nrofUplinkSymbols = 4; + + ssPBCH_BlockPower = -25; + } + + ); + + # ------- SCTP definitions + SCTP : + { + # Number of streams to use in input/output + SCTP_INSTREAMS = 2; + SCTP_OUTSTREAMS = 2; + }; + + + ////////// AMF parameters: + amf_ip_address = ({ ipv4 = "172.21.6.109"; }); + + + NETWORK_INTERFACES : + { + GNB_IPV4_ADDRESS_FOR_NG_AMF = "172.21.19.98"; + GNB_IPV4_ADDRESS_FOR_NGU = "172.21.19.98"; + GNB_PORT_FOR_S1U = 2152; # Spec 2152 + }; + + } +); + +MACRLCs = ( +{ + num_cc = 1; + tr_s_preference = "local_L1"; + tr_n_preference = "local_RRC"; + pusch_TargetSNRx10 = 200; + pucch_TargetSNRx10 = 200; + ul_prbblack_SNR_threshold = 10; +} +); + +L1s = ( +{ + num_cc = 1; + tr_n_preference = "local_mac"; + prach_dtx_threshold = 120; + pucch0_dtx_threshold = 100; + ofdm_offset_divisor = 8; #set this to UINT_MAX for offset 0 +} +); + +RUs = ( +{ + local_rf = "yes" + nb_tx = 1 + nb_rx = 1 + att_tx = 0; + att_rx = 0; + bands = [77]; + max_pdschReferenceSignalPower = -27; + max_rxgain = 114; + eNB_instances = [0]; + clock_src = "external"; + time_src = "external"; +} +); + + +security = { + # preferred ciphering algorithms + # the first one of the list that an UE supports in chosen + # valid values: nea0, nea1, nea2, nea3 + ciphering_algorithms = ( "nea0" ); + + # preferred integrity algorithms + # the first one of the list that an UE supports in chosen + # valid values: nia0, nia1, nia2, nia3 + integrity_algorithms = ( "nia2", "nia0" ); + + # setting 'drb_ciphering' to "no" disables ciphering for DRBs, no matter + # what 'ciphering_algorithms' configures; same thing for 'drb_integrity' + drb_ciphering = "yes"; + drb_integrity = "no"; +}; + +log_config : +{ + global_log_level ="info"; + hw_log_level ="info"; + phy_log_level ="info"; + mac_log_level ="info"; + rlc_log_level ="info"; + pdcp_log_level ="info"; + rrc_log_level ="info"; + ngap_log_level ="debug"; + f1ap_log_level ="debug"; +}; + diff --git a/ci-scripts/xml_files/container_sa_n2_ho_b210_quectel.xml b/ci-scripts/xml_files/container_sa_n2_ho_b210_quectel.xml new file mode 100644 index 0000000000..b67522b220 --- /dev/null +++ b/ci-scripts/xml_files/container_sa_n2_ho_b210_quectel.xml @@ -0,0 +1,230 @@ + + + TEST-MONO-N2-HO-B210 + Monolithic N2 Handover 20 MHz TDD SA + tasks + + 000001 + 000002 + 000023 + 000003 + 000004 + 000005 + 000006 + 000007 + 000008 + 000009 + 000010 + 000011 + 000012 + 000013 + 000014 + 000015 + 000017 + 000018 + 000019 + 000016 + 000020 + 000021 + 000022 + + + + + DeployCoreNetwork + Initialize 5G Core + oc-cn5g-00103-ho + + + + Pull_Local_Registry + Pull Images from Local Registry + oai-gnb + groot + + + + Pull_Local_Registry + Pull Images from Local Registry + oai-gnb + rocket + + + + Create_Workspace + Create new Workspace for groot + groot + + + + Create_Workspace + Create new Workspace on rocket + rocket + + + + Custom_Command + Set initial attenuation on target gNB + rocket + python3 %%workspace%%/ci-scripts/attenuatorctl.py -s 1,2 0 -s 3,4 15 + + + + Deploy_Object + Deploy Monolithic gNB-PCI0 (TDD/Band77/20MHz/B210) in a container + ci-scripts/yaml_files/5g_sa_n2_ho_b210 + groot + oai-gnb-pci0 + + + + Deploy_Object + Deploy Monolithic gNB-PCI1 (TDD/Band77/20MHz/B210) in a container + ci-scripts/yaml_files/5g_sa_n2_ho_b210 + rocket + oai-gnb-pci1 + + + + Attach_UE + Attach Quectel + raspix + + + + Ping + Ping: 100pings in 20sec + raspix + oc-cn5g-00103-ho + -c 100 -i 0.2 + 5 + + + + IdleSleep + Sleep + 5 + + + + Custom_Command + Gradually attenuate gNB-PCI0 (ch1-2=15dB) and gNB-PCI1 (ch3-4=0dB) over 5s to trigger N2 Handover to gNB-PCI1 + rocket + python3 %%workspace%%/ci-scripts/attenuatorctl.py -r 1,2 15 -r 3,4 0 -d 5 + + + + Ping + Ping: 50pings in 10sec + raspix + oc-cn5g-00103-ho + -c 50 -i 0.2 + 5 + + + + Custom_Command + UE connected to gNB-PCI1? + rocket + echo ci fetch_du_by_ue_id 1 | ncat 127.0.0.1 9090 | grep "3585" + + + + Custom_Command + Gradually attenuate gNB-PCI0 (ch1-2=0dB) and gNB-PCI1 (ch3-4=15dB) over 5s to trigger N2 Handover back to gNB-PCI0 + rocket + python3 %%workspace%%/ci-scripts/attenuatorctl.py -r 1,2 0 -r 3,4 15 -d 5 + + + + Ping + Ping: 50pings in 10sec + raspix + oc-cn5g-00103-ho + -c 50 -i 0.2 + 5 + + + + Custom_Command + UE connected to gNB-PCI0? + groot + echo ci fetch_du_by_ue_id 1 | ncat 127.0.0.1 9090 | grep "3584" + + + + Custom_Command + true + Reset all attenuators to 0dB + rocket + python3 %%workspace%%/ci-scripts/attenuatorctl.py -s 1-4 0 + + + + Detach_UE + true + Detach Quectel + raspix + + + + Undeploy_Object + true + Undeploy Monolithic gNB-PCI0 + groot + ci-scripts/yaml_files/5g_sa_n2_ho_b210 + + + + Undeploy_Object + true + Undeploy Monolithic gNB-PCI1 + rocket + ci-scripts/yaml_files/5g_sa_n2_ho_b210 + + + + Clean_Test_Server_Images + true + Clean Test Images on Test Server + groot + oai-gnb + + + + Clean_Test_Server_Images + true + Clean Test Images on Test Server + rocket + oai-gnb + + + + UndeployCoreNetwork + true + Terminate 5G Core + oc-cn5g-00103-ho + + + diff --git a/ci-scripts/yaml_files/5g_sa_n2_ho_b210/docker-compose.yml b/ci-scripts/yaml_files/5g_sa_n2_ho_b210/docker-compose.yml new file mode 100644 index 0000000000..6704b434db --- /dev/null +++ b/ci-scripts/yaml_files/5g_sa_n2_ho_b210/docker-compose.yml @@ -0,0 +1,61 @@ +services: + oai-gnb-pci0: + image: ${REGISTRY:-oaisoftwarealliance}/${GNB_IMG:-oai-gnb}:${TAG:-develop} + container_name: oai-gnb-pci0 + cap_drop: + - ALL + cap_add: + - SYS_NICE + - IPC_LOCK + ulimits: + core: -1 # for core dumps + environment: + USE_B2XX: 'yes' + USE_ADDITIONAL_OPTIONS: --RUs.[0].sdr_addrs serial=30AD30F + --telnetsrv --telnetsrv.shrmod ci + --log_config.global_log_options level,nocolor,utc_time,line_num,function + ASAN_OPTIONS: detect_leaks=0:detect_odr_violation=0 + devices: + - /dev/bus/usb/:/dev/bus/usb/ + volumes: + - ../../conf_files/neighbour-config-ho.conf:/opt/oai-gnb/etc/neighbour-config-ho.conf + - ../../conf_files/gnb.sa.band77.51prb.usrpb200.n2-ho.conf:/opt/oai-gnb/etc/gnb.conf + network_mode: "host" + healthcheck: + test: /bin/bash -c "ps aux | grep -v grep | grep -c softmodem" + interval: 10s + timeout: 5s + retries: 5 + + oai-gnb-pci1: + image: ${REGISTRY:-oaisoftwarealliance}/${GNB_IMG:-oai-gnb}:${TAG:-develop} + container_name: oai-gnb-pci1 + cap_drop: + - ALL + cap_add: + - SYS_NICE + - IPC_LOCK + ulimits: + core: -1 # for core dumps + environment: + USE_B2XX: 'yes' + USE_ADDITIONAL_OPTIONS: --RUs.[0].sdr_addrs serial=31F8010 + --gNBs.[0].nr_cellid 11111111 + --gNBs.[0].gNB_ID 0xe01 + --gNBs.[0].servingCellConfigCommon.[0].physCellId 1 + --gNBs.[0].servingCellConfigCommon.[0].ssb_PositionsInBurst_Bitmap 2 + --gNBs.[0].NETWORK_INTERFACES.GNB_IPV4_ADDRESS_FOR_NG_AMF 172.21.19.99 + --gNBs.[0].NETWORK_INTERFACES.GNB_IPV4_ADDRESS_FOR_NGU 172.21.19.99 + --telnetsrv --telnetsrv.shrmod ci + --log_config.global_log_options level,nocolor,utc_time,line_num,function + devices: + - /dev/bus/usb/:/dev/bus/usb/ + volumes: + - ../../conf_files/neighbour-config-ho.conf:/opt/oai-gnb/etc/neighbour-config-ho.conf + - ../../conf_files/gnb.sa.band77.51prb.usrpb200.n2-ho.conf:/opt/oai-gnb/etc/gnb.conf + network_mode: "host" + healthcheck: + test: /bin/bash -c "ps aux | grep -v grep | grep -c softmodem" + interval: 10s + timeout: 5s + retries: 5 From 7ec646a8d266eef995d835b85d1d59fa0b9beace Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Mon, 20 Oct 2025 10:19:51 +0200 Subject: [PATCH 2/4] NGAP: fix PLMN/cell location for InitialUE and UplinkNAS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit is removing the storage of selected PLMN in NGAP UE context. We store the Serving PLMN in the RRC UE context instead. The Selected PLMN is the PLMN that the UE chose (or was directed to) for registration and it is sent in the RRCSetupComplete. This is different from the selected PLMN mentioned in the NG INITIAL UE MESSAGE for network sharing. Selected PLMN: determines which core network and AMF the UE is registered with GUAMI = Identifies the serving AMF and includes the PLMN of that AMF Serving PLMN: the PLMN that is currently serving the UE (e.g. contained in GUAMI) The AMF’s PLMN may match the selected PLMN, or not (especially in network sharing or roaming). Regardless, the UE always have a Serving PLMN. Initial UE message does not have a TAI IE per se, but TAI is a mandatory member of the mandatory UserLocationInformation CHOICE. NR CGI is also mandatory and carried inside UserLocationInformation: in network sharing, the PLMN ID in TAI takes on additional significance: RAN is shared but CN are different, so in this case PLMN in TAI is needed for the AMF route the message to the correct operator’s core. If network sharing applies (e.g., shared RAN between PLMN-A and PLMN-B): the UE or network may select PLMN-B instead, and that PLMN identity is included in the Initial UE Message’s TAI IE. Conclusion: 1) UE always sends the selected PLMN during the registration, regardless of whether it is shared or not. 2) Selected PLMN Identity is an actual IE sent in the RRCSetupComplete while in NGAP there's no Selected PLMN Identity IE, only PLMN Identity IEs in both TAI and NR CGI. It is a subtle difference. 3) The serving PLMN of the UE needs to be stored in RRC UE Context for later use in NAS procedures. e.g. at initial registration it matches the Selected PLMN 4) No need to store PLMN in NGAP UE Context, params are passed by RRC 5) TAI and NR CGI, which contain the PLMN identity, are mandatory in UserLocationInformation IE CHOICE, and should reflect current status Changes - Initial UE Message: pass UE-selected PLMN and NR cell id from RRC to NGAP - Uplink NAS Transport: use PLMN, TAC, and cell id provided by the message; - Build NR CGI from gNB ID + cell id - Store UE-selected PLMN only in RRC UE context at NAS_FIRST_REQ for later Uplink NAS, stop storing PLMN in NGAP UE context --- openair2/COMMON/ngap_messages_types.h | 6 ++++++ openair2/RRC/NR/nr_rrc_defs.h | 2 ++ openair2/RRC/NR/rrc_gNB_NGAP.c | 23 +++++++++++++++------- openair3/NGAP/ngap_gNB_nas_procedures.c | 26 ++++++++++++------------- openair3/NGAP/ngap_gNB_ue_context.h | 5 ----- 5 files changed, 37 insertions(+), 25 deletions(-) diff --git a/openair2/COMMON/ngap_messages_types.h b/openair2/COMMON/ngap_messages_types.h index 2c4b0f8a0a..5be5e316ef 100644 --- a/openair2/COMMON/ngap_messages_types.h +++ b/openair2/COMMON/ngap_messages_types.h @@ -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 { diff --git a/openair2/RRC/NR/nr_rrc_defs.h b/openair2/RRC/NR/nr_rrc_defs.h index e76802338c..eaeacae5ab 100644 --- a/openair2/RRC/NR/nr_rrc_defs.h +++ b/openair2/RRC/NR/nr_rrc_defs.h @@ -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 diff --git a/openair2/RRC/NR/rrc_gNB_NGAP.c b/openair2/RRC/NR/rrc_gNB_NGAP.c index 7d75384a9b..90b51ba118 100644 --- a/openair2/RRC/NR/rrc_gNB_NGAP.c +++ b/openair2/RRC/NR/rrc_gNB_NGAP.c @@ -223,17 +223,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 +726,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); } diff --git a/openair3/NGAP/ngap_gNB_nas_procedures.c b/openair3/NGAP/ngap_gNB_nas_procedures.c index cff9cd0b72..55cd7d61f4 100644 --- a/openair3/NGAP/ngap_gNB_nas_procedures.c +++ b/openair3/NGAP/ngap_gNB_nas_procedures.c @@ -181,7 +181,6 @@ int ngap_gNB_handle_nas_first_req(instance_t instance, ngap_nas_first_req_t *UEf .amf_ref = amf, .gNB_ue_ngap_id = UEfirstReq->gNB_ue_ngap_id, .gNB_instance = instance_p, - .selected_plmn_identity = UEfirstReq->plmn, }; // RAN UE NGAP ID (M) @@ -211,16 +210,14 @@ int ngap_gNB_handle_nas_first_req(instance_t instance, ngap_nas_first_req_t *UEf ie->value.choice.UserLocationInformation.present = NGAP_UserLocationInformation_PR_userLocationInformationNR; asn1cCalloc(ie->value.choice.UserLocationInformation.choice.userLocationInformationNR, userinfo_nr_p); - /* Set nRCellIdentity. default userLocationInformationNR */ - MACRO_GNB_ID_TO_CELL_IDENTITY(instance_p->gNB_id, - 0, // Cell ID - &userinfo_nr_p->nR_CGI.nRCellIdentity); + /* NR CGI: use gNB ID and Cell ID passed from RRC */ + MACRO_GNB_ID_TO_CELL_IDENTITY(instance_p->gNB_id, UEfirstReq->nr_cell_id, &userinfo_nr_p->nR_CGI.nRCellIdentity); - plmn_id_t *plmn = &ue_desc_p.selected_plmn_identity; + /* Use UE's selected PLMN for nR-CGI (from Initial UE Message) */ + plmn_id_t *plmn = &UEfirstReq->plmn; MCC_MNC_TO_TBCD(plmn->mcc, plmn->mnc, plmn->mnc_digit_length, &userinfo_nr_p->nR_CGI.pLMNIdentity); - /* In case of network sharing, - the selected PLMN is indicated by the PLMN Identity IE within the TAI IE */ + /* Set TAI - use UE's selected PLMN */ INT24_TO_OCTET_STRING(instance_p->tac, &userinfo_nr_p->tAI.tAC); MCC_MNC_TO_PLMNID(plmn->mcc, plmn->mnc, plmn->mnc_digit_length, &userinfo_nr_p->tAI.pLMNIdentity); } @@ -444,17 +441,20 @@ int ngap_gNB_nas_uplink(instance_t instance, ngap_uplink_nas_t *ngap_uplink_nas_ ie->value.choice.UserLocationInformation.present = NGAP_UserLocationInformation_PR_userLocationInformationNR; asn1cCalloc(ie->value.choice.UserLocationInformation.choice.userLocationInformationNR, userinfo_nr_p); - /* Set nRCellIdentity. default userLocationInformationNR */ + /* Set nRCellIdentity: use gNB ID and Cell ID */ MACRO_GNB_ID_TO_CELL_IDENTITY(ngap_gNB_instance_p->gNB_id, - 0, // Cell ID + ngap_uplink_nas_p->nr_cell_id, &userinfo_nr_p->nR_CGI.nRCellIdentity); - plmn_id_t *plmn = &ue_context_p->selected_plmn_identity; + + /* NR CGI */ + plmn_id_t *plmn = &ngap_uplink_nas_p->plmn; MCC_MNC_TO_TBCD(plmn->mcc, plmn->mnc, plmn->mnc_digit_length, &userinfo_nr_p->nR_CGI.pLMNIdentity); - /* Set TAI */ - INT24_TO_OCTET_STRING(ngap_gNB_instance_p->tac, &userinfo_nr_p->tAI.tAC); + /* TAI */ + INT24_TO_OCTET_STRING(ngap_uplink_nas_p->tac, &userinfo_nr_p->tAI.tAC); MCC_MNC_TO_PLMNID(plmn->mcc, plmn->mnc, plmn->mnc_digit_length, &userinfo_nr_p->tAI.pLMNIdentity); } + if (ngap_gNB_encode_pdu(&pdu, &buffer, &length) < 0) { NGAP_ERROR("Failed to encode uplink NAS transport\n"); /* Encode procedure has failed... */ diff --git a/openair3/NGAP/ngap_gNB_ue_context.h b/openair3/NGAP/ngap_gNB_ue_context.h index 1022f2f4c3..8053e8f8c6 100644 --- a/openair3/NGAP/ngap_gNB_ue_context.h +++ b/openair3/NGAP/ngap_gNB_ue_context.h @@ -69,11 +69,6 @@ typedef struct ngap_gNB_ue_context_s { /* Reference to AMF data this UE is attached to */ struct ngap_gNB_amf_data_s *amf_ref; - /* Signaled by the UE in RRC Connection Setup Complete and used in NAS Uplink - * to route NAS messages correctly. 0-based, not 1-based as in TS 36.331 - * 6.2.2 RRC Connection Setup Complete! */ - plmn_id_t selected_plmn_identity; - /* Reference to gNB data this UE is attached to */ ngap_gNB_instance_t *gNB_instance; } ngap_gNB_ue_context_t; From 32eae0a0a8014e746abce7d24e3acb25f9fd006a Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Tue, 21 Oct 2025 10:37:56 +0200 Subject: [PATCH 3/4] Adopt plmn_id_t in nr_guami_t --- common/5g_platform_types.h | 4 +--- openair2/E2AP/RAN_FUNCTION/O-RAN/ran_e2sm_ue_id.c | 12 +++++------- openair2/RRC/NR/rrc_gNB_NGAP.c | 4 +--- openair3/NGAP/ngap_common.c | 2 +- openair3/NGAP/ngap_gNB_nas_procedures.c | 12 ++++++------ openair3/NGAP/ngap_gNB_nnsf.c | 12 +++++------- 6 files changed, 19 insertions(+), 27 deletions(-) diff --git a/common/5g_platform_types.h b/common/5g_platform_types.h index 4ed7f33e47..7cae0afde7 100644 --- a/common/5g_platform_types.h +++ b/common/5g_platform_types.h @@ -37,9 +37,7 @@ typedef struct nssai_s { // Globally Unique AMF Identifier typedef struct nr_guami_s { - uint16_t mcc; - uint16_t mnc; - uint8_t mnc_len; + plmn_id_t plmn; uint8_t amf_region_id; uint16_t amf_set_id; uint8_t amf_pointer; diff --git a/openair2/E2AP/RAN_FUNCTION/O-RAN/ran_e2sm_ue_id.c b/openair2/E2AP/RAN_FUNCTION/O-RAN/ran_e2sm_ue_id.c index a81a01d394..5d725f1df6 100644 --- a/openair2/E2AP/RAN_FUNCTION/O-RAN/ran_e2sm_ue_id.c +++ b/openair2/E2AP/RAN_FUNCTION/O-RAN/ran_e2sm_ue_id.c @@ -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; diff --git a/openair2/RRC/NR/rrc_gNB_NGAP.c b/openair2/RRC/NR/rrc_gNB_NGAP.c index 90b51ba118..15c5aa19e9 100644 --- a/openair2/RRC/NR/rrc_gNB_NGAP.c +++ b/openair2/RRC/NR/rrc_gNB_NGAP.c @@ -165,9 +165,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; } diff --git a/openair3/NGAP/ngap_common.c b/openair3/NGAP/ngap_common.c index c3865cff10..8be57349ec 100644 --- a/openair3/NGAP/ngap_common.c +++ b/openair3/NGAP/ngap_common.c @@ -106,7 +106,7 @@ ngap_cause_t decode_ngap_cause(const NGAP_Cause_t *in) nr_guami_t decode_ngap_guami(const NGAP_GUAMI_t *in) { nr_guami_t out = {0}; - TBCD_TO_MCC_MNC(&in->pLMNIdentity, out.mcc, out.mnc, out.mnc_len); + TBCD_TO_MCC_MNC(&in->pLMNIdentity, out.plmn.mcc, out.plmn.mnc, out.plmn.mnc_digit_length); OCTET_STRING_TO_INT8(&in->aMFRegionID, out.amf_region_id); OCTET_STRING_TO_INT16(&in->aMFSetID, out.amf_set_id); OCTET_STRING_TO_INT8(&in->aMFPointer, out.amf_pointer); diff --git a/openair3/NGAP/ngap_gNB_nas_procedures.c b/openair3/NGAP/ngap_gNB_nas_procedures.c index 55cd7d61f4..7dd6359b7a 100644 --- a/openair3/NGAP/ngap_gNB_nas_procedures.c +++ b/openair3/NGAP/ngap_gNB_nas_procedures.c @@ -76,9 +76,9 @@ static ngap_gNB_amf_data_t *select_amf(ngap_gNB_instance_t *instance_p, const ng const nr_guami_t *guami = &msg->ue_identity.guami; LOG_D(NGAP, "GUAMI is present: MCC=%03d MNC=%0*d RegionID=%d SetID=%d Pointer=%d\n", - guami->mcc, - guami->mnc_len, - guami->mnc, + guami->plmn.mcc, + guami->plmn.mnc_digit_length, + guami->plmn.mnc, guami->amf_region_id, guami->amf_set_id, guami->amf_pointer); @@ -89,9 +89,9 @@ static ngap_gNB_amf_data_t *select_amf(ngap_gNB_instance_t *instance_p, const ng msg->gNB_ue_ngap_id, amf->amf_name, amf->assoc_id, - guami->mcc, - guami->mnc_len, - guami->mnc, + guami->plmn.mcc, + guami->plmn.mnc_digit_length, + guami->plmn.mnc, guami->amf_region_id, guami->amf_set_id, guami->amf_pointer); diff --git a/openair3/NGAP/ngap_gNB_nnsf.c b/openair3/NGAP/ngap_gNB_nnsf.c index f895737a0d..5fbc3e4d11 100644 --- a/openair3/NGAP/ngap_gNB_nnsf.c +++ b/openair3/NGAP/ngap_gNB_nnsf.c @@ -286,8 +286,7 @@ ngap_gNB_amf_data_t *ngap_gNB_nnsf_select_amf_by_guami(ngap_gNB_instance_t *inst struct plmn_identity_s *served_plmn_p = NULL; STAILQ_FOREACH(served_plmn_p, &guami_p->served_plmns, next) { - if ((served_plmn_p->mcc == guami.mcc) && - (served_plmn_p->mnc == guami.mnc)) { + if ((served_plmn_p->mcc == guami.plmn.mcc) && (served_plmn_p->mnc == guami.plmn.mnc)) { break; } } @@ -322,9 +321,9 @@ ngap_gNB_amf_data_t *ngap_gNB_nnsf_select_amf_by_guami(ngap_gNB_instance_t *inst } NGAP_WARN("No matching AMF found for GUAMI (MCC=%03d MNC=%0*d RegionID=%d SetID=%d Pointer=%d)\n", - guami.mcc, - guami.mnc_len, - guami.mnc, + guami.plmn.mcc, + guami.plmn.mnc_digit_length, + guami.plmn.mnc, guami.amf_region_id, guami.amf_set_id, guami.amf_pointer); @@ -373,8 +372,7 @@ ngap_gNB_amf_data_t *ngap_gNB_nnsf_select_amf_by_guami_no_cause(ngap_gNB_instanc struct plmn_identity_s *served_plmn_p = NULL; STAILQ_FOREACH(served_plmn_p, &guami_p->served_plmns, next) { - if ((served_plmn_p->mcc == guami.mcc) && - (served_plmn_p->mnc == guami.mnc)) { + if ((served_plmn_p->mcc == guami.plmn.mcc) && (served_plmn_p->mnc == guami.plmn.mnc)) { break; } } From 5f261d4f4c74e9cad2d390e42e6fbef11429b7eb Mon Sep 17 00:00:00 2001 From: Guido Casati Date: Tue, 21 Oct 2025 11:12:13 +0200 Subject: [PATCH 4/4] Add PLMN validation for handover requests and store serving PLMN Prevent assertion in ngap_gNB_nas_uplink() due to uninitialized selected_plmn_identity: the selected_plmn_identity field was not being set during N2 handover, causing the assertion ((plmn->mnc_digit_length == 3) || (plmn->mnc_digit_length == 2)) to fail when processing subsequent NAS messages. The issue is that the Serving PLMN is not stored by RRC. During N2 mobility, RRC can get this info from GUAMI. When receiving the Handover Request the target gNB knows from GUAMI which serving PLMN is the AMF belonging to. In current design, allowed PLMNs are stored in rrc.configuration. The incoming PLMN needs to be validate against the PLMNs in the list, then stored in the UE context as Serving PLMN. This change adds PLMN validation when processing handover requests from the source gNB. The validation is needed because: - Target gNB must verify that the PLMN from the GUAMI in the handover request is allowed by its configuration before accepting the handover to prevent handover to unsupported PLMNs. - When PLMN validation fails, the target gNB now properly sends NGAP_HANDOVER_FAILURE - UE Context must store serving PLMN for later use --- openair2/RRC/NR/rrc_gNB_NGAP.c | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/openair2/RRC/NR/rrc_gNB_NGAP.c b/openair2/RRC/NR/rrc_gNB_NGAP.c index 15c5aa19e9..62fb68d92b 100644 --- a/openair2/RRC/NR/rrc_gNB_NGAP.c +++ b/openair2/RRC/NR/rrc_gNB_NGAP.c @@ -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. @@ -1161,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); @@ -1176,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);